Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions actions/tests/test_graphql_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,10 @@ def test_plan_root_page_exists(graphql_client_query_data, plan_with_pages):
assert any(page['__typename'] == 'PlanRootPage' and page['id'] == str(plan.root_page.id) for page in pages)


def test_plan_root_page_contains_block(graphql_client_query_data, plan_with_pages):
@pytest.mark.parametrize('layout', ['big_image', 'side_by_side'])
def test_plan_root_page_contains_block(graphql_client_query_data, plan_with_pages, layout):
plan = plan_with_pages
hero_data = {'layout': 'big_image', 'heading': 'foo', 'lead': 'bar'}
hero_data = {'layout': layout, 'heading': 'foo', 'lead': 'bar'}
plan.root_page.body = json.dumps([
{'type': 'front_page_hero', 'value': hero_data},
])
Expand Down
68 changes: 61 additions & 7 deletions actions/tests/test_graphql_planpage.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
from indicators.blocks import IndicatorBlock
from indicators.tests.factories import IndicatorGroupBlockFactory
from pages.blocks import CardBlock, QuestionBlock
from pages.tests.factories import CardListBlockFactory, QuestionAnswerBlockFactory
from pages.tests.factories import (
CardListBlockFactory,
FrontPageHeroAdditionalSettingsBlockFactory,
FrontPageHeroBlockFactory,
QuestionAnswerBlockFactory,
)

pytestmark = pytest.mark.django_db

Expand Down Expand Up @@ -95,11 +100,13 @@ def expected_result_multi_use_image_fragment(image_block):
}


def test_front_page_hero_block(graphql_client_query_data, front_page_hero_block, plan_with_pages):
@pytest.mark.parametrize('layout', ['big_image', 'side_by_side'])
def test_front_page_hero_block(graphql_client_query_data, plan_with_pages, layout):
plan = plan_with_pages
page = plan.root_page
block = FrontPageHeroBlockFactory.create(layout=layout)
page.body = [
('front_page_hero', front_page_hero_block),
('front_page_hero', block),
]
page.save()
assert_body_block(
Expand All @@ -117,14 +124,61 @@ def test_front_page_hero_block(graphql_client_query_data, front_page_hero_block,
""",
extra_fragments=[MULTI_USE_IMAGE_FRAGMENT],
expected={
'heading': front_page_hero_block['heading'],
'image': expected_result_multi_use_image_fragment(front_page_hero_block['image']),
'layout': 'big_image',
'lead': str(front_page_hero_block['lead']),
'heading': block['heading'],
'image': expected_result_multi_use_image_fragment(block['image']),
'layout': layout,
'lead': str(block['lead']),
},
)


@pytest.mark.parametrize('layout', ['small_image', 'side_by_side'])
@pytest.mark.parametrize('fit_image', [False, True])
def test_front_page_hero_block_fit_image(graphql_client_query_data, plan_with_pages, layout, fit_image):
plan = plan_with_pages
page = plan.root_page
additional_settings = FrontPageHeroAdditionalSettingsBlockFactory.create(fit_image=fit_image)
block = FrontPageHeroBlockFactory.create(layout=layout, additional_settings=additional_settings)
page.body = [
('front_page_hero', block),
]
page.save()
assert_body_block(
graphql_client_query_data,
plan=plan,
page=page,
block_fields="""
additionalSettings {
fitImage
}
""",
expected={
'additionalSettings': {'fitImage': fit_image},
},
)


def test_front_page_hero_block_additional_settings_hidden_for_big_image(graphql_client_query_data, plan_with_pages):
plan = plan_with_pages
page = plan.root_page
block = FrontPageHeroBlockFactory.create(layout='big_image')
page.body = [
('front_page_hero', block),
]
page.save()
assert_body_block(
graphql_client_query_data,
plan=plan,
page=page,
block_fields="""
additionalSettings {
fitImage
}
""",
expected={'additionalSettings': None},
)


def test_category_list_block(graphql_client_query_data, category_list_block, plan_with_pages):
plan = plan_with_pages
page = plan.root_page
Expand Down
1 change: 1 addition & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def request(self, **kwargs):
register(pages_factories.CategoryPageFactory)
register(pages_factories.CategoryTypePageFactory, parent=LazyFixture(lambda plan: plan.root_page))
register(pages_factories.CategoryTypePageLevelLayoutFactory)
register(pages_factories.FrontPageHeroAdditionalSettingsBlockFactory)
register(pages_factories.FrontPageHeroBlockFactory)
register(pages_factories.PageChooserBlockFactory, parent=LazyFixture(lambda plan: plan.root_page))
register(pages_factories.PageLinkBlockFactory)
Expand Down
101 changes: 97 additions & 4 deletions pages/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
from grapple.registry import registry
from grapple.types.streamfield import ListBlock as GrappleListBlock, StructBlockItem

from kausal_common.graphene.grapple import make_grapple_field
from kausal_common.blocks.conditional_struct_block import ConditionalFieldVisibility, ConditionalStructBlock, Match
from kausal_common.graphene.grapple import grapple_field, make_grapple_field

from actions.blocks.choosers import CategoryChooserBlock
from actions.models.category import Category
Expand Down Expand Up @@ -182,26 +183,118 @@ class Meta:
]


class FrontPageHeroAdditionalSettingsValue(blocks.StructValue):
background_colour: str
fit_image: bool
show_image_accent: bool
background_covers_full_section: bool


@register_streamfield_block
class FrontPageHeroBlock(blocks.StructBlock):
class FrontPageHeroAdditionalSettingsBlock(blocks.StructBlock):
background_colour = blocks.CharBlock(
required=False,
label=_('Background colour'),
help_text=_('CSS colour value, e.g. #FFFFFF or rgb(255, 255, 255)'),
)
fit_image = blocks.BooleanBlock(
required=False,
default=True,
label=_('Fit image without clipping'),
help_text=_('Show the image at its exact scaled dimensions; it will never be cropped'),
)
show_image_accent = blocks.BooleanBlock(
required=False,
default=True,
label=_('Show image accent'),
help_text=_('Display a brand color accent bar below the image'),
)
background_covers_full_section = blocks.BooleanBlock(
required=False,
default=False,
label=_('Background covers full block'),
help_text=_('Extend the background colour to cover the content below the image, not just the image area'),
)

class Meta:
label = _('Additional settings')
collapsed = True
label_format = ''
value_class = FrontPageHeroAdditionalSettingsValue

graphql_fields = [
GraphQLString('background_colour'),
GraphQLBoolean('fit_image'),
GraphQLBoolean('show_image_accent'),
GraphQLBoolean('background_covers_full_section'),
]


_LAYOUTS_WITH_ADDITIONAL_SETTINGS = {'small_image', 'side_by_side'}


def _resolve_hero_additional_settings(root, _info) -> FrontPageHeroAdditionalSettingsValue | None:
# Standalone graphene.Field resolvers receive the raw BoundBlock (StreamChild) as root,
# not the StructValue — unlike grapple's MethodType-bound resolvers which receive the
# StructValue as `instance`. Use getattr to handle both cases.
data = getattr(root, 'value', root)
if data.get('layout') not in _LAYOUTS_WITH_ADDITIONAL_SETTINGS:
return None
return data.get('additional_settings')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve defaults for existing small-image heroes

When a page already contains a front_page_hero saved before this migration with layout='small_image', the stored StreamField JSON has no additional_settings key because the migration only alters the block schema. This resolver therefore returns null for a layout that now exposes additional settings, so existing heroes do not receive the new defaults such as fit_image=True unless every page is manually resaved or the resolver/data migration synthesizes the default settings.

Useful? React with 👍 / 👎.



@register_streamfield_block
class FrontPageHeroBlock(ConditionalStructBlock):
conditional_rules = [
ConditionalFieldVisibility(
show='additional_settings',
when=Match(layout=('small_image', 'side_by_side')),
),
ConditionalFieldVisibility(
show=['additional_settings', 'show_image_accent'],
when=Match(layout=('small_image',)),
),
ConditionalFieldVisibility(
show=['additional_settings', 'background_covers_full_section'],
when=Match(layout=('small_image',)),
),
]

layout = blocks.ChoiceBlock(
choices=[
('big_image', _('Big image')),
('small_image', _('Small image')),
('big_image', _('Full-width: content over image')),
('small_image', _('Large image: image above, content below')),
('side_by_side', _('Side-by-side: image left, content right')),
]
)
image = ImageChooserBlock(label=_('Image'))
heading = blocks.CharBlock(classname='full title', label=_('Heading'), required=False)
lead = blocks.RichTextBlock(label=_('Lead'), required=False)
additional_settings = FrontPageHeroAdditionalSettingsBlock(required=False)

class Meta:
label = _('Front page hero block')

def clean(self, value):
result = super().clean(value)
if result.get('layout') not in _LAYOUTS_WITH_ADDITIONAL_SETTINGS:
additional = result.get('additional_settings')
if additional is not None:
additional['fit_image'] = False
additional['background_colour'] = ''
return result

graphql_fields = [
GraphQLString('layout', required=True),
GraphQLImage('image'),
GraphQLString('heading'),
GraphQLString('lead'),
grapple_field(
'additional_settings',
lambda: registry.streamfield_blocks[FrontPageHeroAdditionalSettingsBlock],
resolver=_resolve_hero_additional_settings,
required=False,
),
]


Expand Down
Loading
Loading