Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TWE-11 - BE - Service area page #335

Merged
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e4abc32
Move PartersBlock to core.blocks
SharmaineLim Dec 18, 2024
84a7048
Add divisions app
SharmaineLim Dec 19, 2024
7144e99
Prepare division page blocks
SharmaineLim Dec 19, 2024
643ff22
Add DivisionPage
SharmaineLim Dec 19, 2024
a38b9d1
Make schema migration
SharmaineLim Dec 19, 2024
aa3dfd6
Add faker & factory-boy as explicit dependencies since we use them
SharmaineLim Dec 19, 2024
6663d23
Create DivisionPage factory
SharmaineLim Dec 19, 2024
bc24e99
Do something different if there's only 1 dynamic text
SharmaineLim Dec 19, 2024
685f6cc
Add 'caption' to four-photo collage
SharmaineLim Dec 20, 2024
7f5d684
Merge branch 'integration/2024-evolution' into feature/twe-7-division…
SharmaineLim Jan 6, 2025
aa12999
Add small_caption to FourPhotoCollageBlock
SharmaineLim Jan 9, 2025
6bb1c7d
Create ServiceAreaPage model
SharmaineLim Jan 10, 2025
911c8d0
Create DivisionMixin
SharmaineLim Jan 10, 2025
7cc3fca
Create story components for ServiceAreaPage
SharmaineLim Jan 10, 2025
4460bb5
Add page factories for the services module
SharmaineLim Jan 10, 2025
c7116a0
Make schema migration
SharmaineLim Jan 10, 2025
7154f5d
Add documentation about divisions
SharmaineLim Jan 10, 2025
bde48ca
Update comment on FourPhotoCollageBlock
SharmaineLim Jan 10, 2025
8a46015
Add division to ServiceAreaPage promote tab
SharmaineLim Jan 13, 2025
1cd3c0c
Add tests for the DivisionMixin
SharmaineLim Jan 13, 2025
14c7941
Use setUpTestData instead of setUp
SharmaineLim Jan 14, 2025
b5f3204
From get_division method to final_division cached property
SharmaineLim Jan 14, 2025
b5152a8
Filter out Root & Homepage, defer streamfields
SharmaineLim Jan 14, 2025
e0a3b4d
Add a page_description
SharmaineLim Jan 14, 2025
aca20db
Only allow ServiceAreaPages under a DivisionPage
SharmaineLim Jan 14, 2025
6ce7997
Merge branch 'integration/2024-evolution' into feature/twe-11-service…
SharmaineLim Jan 15, 2025
7a025d1
Update division story container to match block YAMLs
SharmaineLim Jan 15, 2025
4e291c6
Mention in docs: only 1 division allowed
SharmaineLim Jan 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/custom-features/divisions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Divisions
zerolab marked this conversation as resolved.
Show resolved Hide resolved

The site's target audience can be grouped into divisions; e.g. the charity division, the public sector division, and the Wagtail division. All content going forward can be associated to one of these divisions.

The idea is that if you're a charity organisation, you can find content that's specific and relevant for you because the relevant content will all be in one place.

This feature allows content to be associated to a specific `DivisionPage`, which allows us to display the same theme, logo and navigation for any content related to a division.

## Options

The available options are dependent on the `DivisionPage`s that have been created.

## Division configuration

The `tbx.core.utils.models.DivisionMixin` provides a mechanism for associating a specific division with a page. It offers the following functionality:

- `division` field: Adds a ForeignKey field to associate a specific division with a page.
- `final_division`: A cached property that determines the appropriate division to associate to a page. It first checks if the page has a `division` specified. If not, it traverses the page's ancestors to find the first page that either has a `division` specified or is a `DivisionPage`, defaulting to `None`.

---

???+ note

Please ensure that the Editors' guide is updated accordingly whenever any changes are made to this feature. A private link, for Torchbox employees only, can be found at https://intranet.torchbox.com/torchbox-com-project-docs.
5 changes: 3 additions & 2 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,11 @@ nav:
- front-end/incident-form.md
- 'Navigation': 'navigation.md'
- 'Custom features':
- 'Contact': 'custom-features/contact.md'
- 'Division': 'custom-features/divisions.md'
- 'Migration-friendly StreamFields': 'custom-features/migration-friendly-streamfields.md'
- 'Theme': 'custom-features/theme.md'
- 'Modes': 'custom-features/modes.md'
- 'Contact': 'custom-features/contact.md'
- 'Theme': 'custom-features/theme.md'
- 'Continuous integration': 'continuous-integration.md'
- 'Anonymised data': 'anonymised-data.md'
- 'Data import': 'data-import.md'
Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ mkdocs-material = "^9.5.41"
pymdown-extensions = "^10.11.2"

# Testing
factory-boy = "^3.3.0"
faker = "^24.11.0"
wagtail-factories = "^4.2.1"


Expand Down
83 changes: 83 additions & 0 deletions tbx/core/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _

from tbx.images.models import CustomImage
from wagtail import blocks
Expand Down Expand Up @@ -262,6 +263,88 @@ class Meta:
template = "patterns/molecules/streamfield/blocks/contact_call_to_action.html"


class DynamicHeroBlock(blocks.StructBlock):
"""
This block displays text that will be cycled through.
"""

static_text = blocks.CharBlock(required=False)
dynamic_text = blocks.ListBlock(
blocks.CharBlock(),
help_text=_(
"The hero will cycle through these texts on larger screen sizes "
"and only show the first text on smaller screen sizes."
),
required=False,
)

class Meta:
icon = "title"
template = "patterns/molecules/streamfield/blocks/dynamic_hero_block.html"


class FourPhotoCollageBlock(blocks.StructBlock):
"""
Accepts 4 photos shown as a collage + text below.
Used on the division page and the service area page.
"""

images = blocks.ListBlock(
ImageWithAltTextBlock(label="Photo"),
min_num=4,
max_num=4,
label="Photos",
help_text=_("Exactly four required."),
default=[{"image": None, "alt_text": ""}] * 4,
)
caption = blocks.RichTextBlock(
features=settings.PARAGRAPH_RICH_TEXT_FEATURES, required=False
)
small_caption = blocks.RichTextBlock(
features=settings.PARAGRAPH_RICH_TEXT_FEATURES, required=False
)

class Meta:
group = "Custom"
icon = "image"
template = "patterns/molecules/streamfield/blocks/four_photo_collage_block.html"


class IntroductionWithImagesBlock(blocks.StructBlock):
"""Used on the division page."""

introduction = blocks.RichTextBlock(features=settings.PARAGRAPH_RICH_TEXT_FEATURES)
description = blocks.RichTextBlock(
blank=True, features=settings.NO_HEADING_RICH_TEXT_FEATURES
)
images = blocks.ListBlock(
ImageWithAltTextBlock(label="Photo"),
min_num=2,
max_num=2,
label="Photos",
help_text=_("Exactly two required."),
default=[{"image": None, "alt_text": ""}] * 2,
)

class Meta:
group = "Custom"
icon = "pilcrow"
template = (
"patterns/molecules/streamfield/blocks/introduction_with_images_block.html"
)


class PartnersBlock(blocks.StructBlock):
title = blocks.CharBlock(max_length=255, required=False)
partner_logos = blocks.ListBlock(CustomImageChooserBlock(), label="Logos")

class Meta:
icon = "openquote"
label = "Partner logos"
template = "patterns/molecules/streamfield/blocks/partners_block.html"
group = "Custom"


class ShowcaseBlock(blocks.StructBlock):
"""
This block is a standard ShowcaseBlock, available on the home page and
Expand Down
23 changes: 20 additions & 3 deletions tbx/core/factories.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
import factory
import wagtail_factories
from tbx.core.blocks import StoryBlock
from faker import Faker
from tbx.core.blocks import DynamicHeroBlock, StoryBlock
from tbx.core.models import HomePage, StandardPage
from wagtail.blocks import RichTextBlock
from wagtail import blocks

fake = Faker()


class DynamicHeroBlockFactory(wagtail_factories.StructBlockFactory):
class Meta:
model = DynamicHeroBlock

static_text = fake.sentence()

@factory.post_generation
def dynamic_text(obj, create, extracted, **kwargs):
values = extracted or fake.sentences(nb=5)
obj["dynamic_text"] = blocks.list_block.ListValue(
blocks.ListBlock(blocks.CharBlock()), values
)


class RichTextBlockFactory(wagtail_factories.blocks.BlockFactory):
class Meta:
model = RichTextBlock
model = blocks.RichTextBlock


class StoryBlockFactory(wagtail_factories.StreamBlockFactory):
Expand Down
97 changes: 97 additions & 0 deletions tbx/core/tests/test_division_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from tbx.core.factories import HomePageFactory
from tbx.divisions.factories import DivisionPageFactory
from tbx.services.factories import ServiceAreaPageFactory
from wagtail.models import Site
from wagtail.test.utils import WagtailPageTestCase


class TestDivisionMixin(WagtailPageTestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()

# Set up the site & homepage.
site = Site.objects.get(is_default_site=True)
root = site.root_page.specific
cls.home = HomePageFactory(parent=root)

site.root_page = cls.home
site.save()

# Set up a services "index" page.
cls.services = ServiceAreaPageFactory(title="Services", parent=cls.home)
cls.service_1 = ServiceAreaPageFactory(title="Service 1", parent=cls.services)
cls.service_2 = ServiceAreaPageFactory(title="Service 2", parent=cls.service_1)

# Set up a division page.
cls.division_1 = DivisionPageFactory(parent=cls.home)
cls.division_2 = DivisionPageFactory(title="Public sector", parent=cls.home)

def test_division_selected(self):
"""
For a page that has division selected,
final_division should return the selected page.
"""
self.service_1.division = self.division_1
self.service_1.save()

service_3 = ServiceAreaPageFactory(
division=self.division_2,
parent=self.service_2,
title="Service 3",
)

self.assertEqual(self.service_1.final_division, self.division_1)
self.assertEqual(service_3.final_division, self.division_2)

def test_division_selected_on_ancestor(self):
"""
For a page that does not have a division selected
but an ancestor page has a division selected,
final_division should return the ancestor's selected page.
"""
self.service_1.division = self.division_1
self.service_1.save()

service_3 = ServiceAreaPageFactory(
parent=self.service_2,
title="Service 3",
)

self.assertEqual(self.service_1.final_division, self.division_1)
self.assertEqual(self.service_2.final_division, self.division_1)
self.assertEqual(service_3.final_division, self.division_1)

def test_division_as_ancestor(self):
"""
For a page that does not have a division selected
but an ancestor page is a DivisionPage,
final_division should return the ancestor DivisionPage.
"""
service_a = ServiceAreaPageFactory(
title="Service A",
parent=self.division_1,
)
service_b = ServiceAreaPageFactory(
title="Service A",
parent=service_a,
)

self.assertEqual(service_a.final_division, self.division_1)
self.assertEqual(service_b.final_division, self.division_1)

def test_no_division(self):
"""
For a page that does not have a division selected
and is not a descendant of a DivisionPage
nor has an ancestor with a selected division,
final_division should return None.
"""
service_3 = ServiceAreaPageFactory(
parent=self.service_2,
title="Service 3",
)

self.assertIsNone(self.service_1.final_division)
self.assertIsNone(self.service_2.final_division)
self.assertIsNone(service_3.final_division)
41 changes: 34 additions & 7 deletions tbx/core/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from django.apps import apps
from django.test import TestCase
from django.utils.module_loading import import_string, module_has_submodule

from tbx.core.factories import HomePageFactory, StandardPageFactory
from tbx.core.models import HomePage, StandardPage
from wagtail.models import Site
from wagtail.models import Page, Site
from wagtail.test.utils import WagtailPageTestCase
from wagtail.test.utils.form_data import (
nested_form_data,
Expand All @@ -9,14 +13,37 @@
)


class TestHomePageFactory(WagtailPageTestCase):
def test_create(self):
HomePageFactory()
class TestPageFactory(TestCase):
"""Sanity tests to make sure all pages have a factory."""

# Exclude these modules from the check.
# (They currently don't have factories. Un-exclude once they have factories.)
EXCLUDE = ["tbx.events", "tbx.impact_reports"]

def test_pages(self):
app_configs = apps.get_app_configs()
home_page = HomePageFactory()

# Create one of every page type using their factory.
for app in app_configs:
for model in app.models.values():
if issubclass(model, Page) and model not in [Page, HomePage]:
if app.name in self.EXCLUDE:
continue

with self.subTest(model=model.__name__):
# Get the model's factory
assert module_has_submodule(
app.module, "factories"
), f"App '{app.name}' does not have a factories module."

page_factory = import_string(
f"{app.module.__name__}.factories.{model.__name__}Factory"
)

page = page_factory(parent=home_page)

class TestStandardPageFactory(WagtailPageTestCase):
def test_create(self):
StandardPageFactory()
self.assertIsInstance(page, model)


class TestStandardPage(WagtailPageTestCase):
Expand Down
Loading
Loading