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-7 - BE - Division page #324

Open
wants to merge 11 commits into
base: integration/2024-evolution
Choose a base branch
from
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"
Comment on lines +69 to +70
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added as explicit dependencies since we use them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

factory-boy is a wagtail-factories dependency, and faker is a factory-boy dependency

Normally we don't make them explicit like this, but happy with this

wagtail-factories = "^4.2.1"


Expand Down
80 changes: 80 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,85 @@ 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=_(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: are we ever going to translate the site? Since the other blocks don't use gettext(_lazy) we should keep it to plain strings for consistency

"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."""

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note to self: see example with caption / small caption.

question: would these not be better as description and short_note? (naming is hard!)

)

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: nice use of factory boy features

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
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", "tbx.services"]

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
Empty file added tbx/divisions/__init__.py
Empty file.
12 changes: 12 additions & 0 deletions tbx/divisions/blocks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from tbx.core.blocks import (
FourPhotoCollageBlock,
IntroductionWithImagesBlock,
PartnersBlock,
StoryBlock,
)


class DivisionStoryBlock(StoryBlock):
four_photo_collage = FourPhotoCollageBlock()
introduction_with_images = IntroductionWithImagesBlock()
partners_block = PartnersBlock()
36 changes: 36 additions & 0 deletions tbx/divisions/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import factory
import wagtail_factories
from tbx.core.blocks import DynamicHeroBlock
from tbx.core.factories import DynamicHeroBlockFactory, StoryBlockFactory
from wagtail import blocks

from .models import DivisionPage


class DynamicHeroStreamBlock(blocks.StreamBlock):
hero = DynamicHeroBlock()


class DynamicHeroStreamBlockFactory(wagtail_factories.StreamBlockFactory):
class Meta:
model = DynamicHeroStreamBlock

hero = factory.SubFactory(DynamicHeroBlockFactory)


class DivisionPageFactory(wagtail_factories.PageFactory):
class Meta:
model = DivisionPage

title = "Charity"
label = "Charity"

@factory.post_generation
def hero(obj, create, extracted, **kwargs):
blocks = kwargs or {"0": "hero"}
obj.hero = DynamicHeroStreamBlockFactory(**blocks)

@factory.post_generation
def body(obj, create, extracted, **kwargs):
blocks = kwargs or {"0": "paragraph"}
obj.body = StoryBlockFactory(**blocks)
89 changes: 89 additions & 0 deletions tbx/divisions/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Generated by Django 4.2.16 on 2024-12-18 08:18

from django.db import migrations, models
import django.db.models.deletion
import tbx.core.utils.fields


class Migration(migrations.Migration):

initial = True

dependencies = [
("wagtailcore", "0094_alter_page_locale"),
("people", "0011_update_theme_colour_choices"),
("images", "0001_initial"),
]

operations = [
migrations.CreateModel(
name="DivisionPage",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
(
"navigation_text",
models.CharField(
blank=True,
help_text="\n Text entered here will appear instead of the page title in the navigation menu.\n For top-level menu items do this in the navigaiton settings instead.\n ",
max_length=255,
),
),
("social_text", models.CharField(blank=True, max_length=255)),
(
"theme",
models.CharField(
blank=True,
choices=[
("", "None"),
("theme-coral", "Coral"),
("theme-nebuline", "Nebuline"),
("theme-lagoon", "Lagoon"),
("theme-green", "Green"),
],
max_length=25,
),
),
("label", models.CharField(blank=True, max_length=50)),
("hero", tbx.core.utils.fields.StreamField(block_lookup={})),
(
"body",
tbx.core.utils.fields.StreamField(blank=True, block_lookup={}),
),
(
"contact",
models.ForeignKey(
blank=True,
help_text="The contact will be applied to this page's footer and all of its descendants.\nIf no contact is selected, it will be derived from this page's ancestors, eventually falling back to the default contact.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="people.contact",
),
),
(
"social_image",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="images.customimage",
),
),
],
options={
"abstract": False,
},
bases=("wagtailcore.page", models.Model),
),
]
Empty file.
50 changes: 50 additions & 0 deletions tbx/divisions/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from django.db import models

from tbx.core.blocks import DynamicHeroBlock
from tbx.core.utils.fields import StreamField
from tbx.core.utils.models import (
ColourThemeMixin,
NavigationFields,
SocialFields,
)
from tbx.people.models import ContactMixin
from wagtail.admin.panels import FieldPanel, MultiFieldPanel
from wagtail.models import Page

from .blocks import DivisionStoryBlock


class DivisionPage(
ColourThemeMixin, ContactMixin, SocialFields, NavigationFields, Page
):
template = "patterns/pages/divisions/division_page.html"

label = models.CharField(blank=True, max_length=50)

hero = StreamField([("hero", DynamicHeroBlock())], max_num=1, min_num=1)
body = StreamField(DivisionStoryBlock(), blank=True)

content_panels = Page.content_panels + [
FieldPanel(
"label",
heading="Division label",
help_text=(
"Label displayed beside the logo for this page and any other pages"
" under this division. (e.g. Charity)"
),
),
FieldPanel("hero"),
FieldPanel("body"),
]

promote_panels = (
[
MultiFieldPanel(Page.promote_panels, "Common page configuration"),
]
+ NavigationFields.promote_panels
+ ColourThemeMixin.promote_panels
+ ContactMixin.promote_panels
+ [
MultiFieldPanel(SocialFields.promote_panels, "Social fields"),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

{% include "patterns/molecules/streamfield/blocks/introduction_with_images_block.html" %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
tags:
srcset_image:
item.image format-webp loading="lazy" fill-{100x100} alt=item.image.alt_text:
raw: |
<img alt="" width="100" height="100" loading="lazy" sizes="100px" src="https://picsum.photos/100/100.webp" srcset="https://picsum.photos/100/100.webp 100w">
Loading
Loading