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
7 changes: 4 additions & 3 deletions bedrock/mozorg/blocks/advertising.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ class SectionHeaderBlock(blocks.StructBlock):
)
superheading_text = blocks.CharBlock(char_max_length=255, required=False)
heading_text = blocks.CharBlock(char_max_length=255, required=False)
subheading_text = blocks.CharBlock(char_max_length=255, required=False)
subheading_text = blocks.RichTextBlock(required=False)
image = ImageChooserBlock(required=False)

def clean(self, value):
errors = {}

superheading = value.get("superheading_text", "").strip()
heading = value.get("heading_text", "").strip()
subheading = value.get("subheading_text", "").strip()
subheading = value.get("subheading_text") # RichText (or None); falsy when it has no source

if not superheading and not heading and not subheading:
errors["heading_text"] = ValidationError("You must provide either superheading, or heading, or subheading.")
Expand Down Expand Up @@ -317,7 +317,8 @@ class SectionBlock(blocks.StructBlock):
("statistic_callout_block", StatisticCalloutBlock()),
("features_with_modals", FeatureListWithModalsBlock()),
("feature_list_block", FeatureListBlock()),
]
],
required=False,
)
call_to_action = blocks.ListBlock(RowTextAndLinkBlock(), min_num=0, max_num=1, default=[], label="Call to Action")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

# Generated by Django 5.2.18 on 2026-10-27 20:28

import wagtail.admin.forms.choosers
import wagtail.fields
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('mozorg', '0055_prose_rich_text_and_heading_size'),
]

operations = [
migrations.AlterField(
model_name='advertisingindexpage',
name='sections',
field=wagtail.fields.StreamField([('section', 40)], blank=True, block_lookup={0: ('wagtail.blocks.CharBlock', (), {'help_text': "Optional: Add an ID to make this section linkable from navigation (e.g., 'solutions', 'why-mozilla')", 'max_length': 100, 'required': False}), 1: ('wagtail.blocks.BooleanBlock', (), {'default': False, 'inline_form': True, 'label': 'Should the section have a divider line on top?', 'required': False}), 2: ('wagtail.blocks.BooleanBlock', (), {'default': False, 'inline_form': True, 'label': 'Should the section have a dark background?', 'required': False}), 3: ('wagtail.blocks.StructBlock', [[('anchor_id', 0), ('has_top_divider', 1), ('display_on_dark_background', 2)]], {}), 4: ('wagtail.blocks.CharBlock', (), {'char_max_length': 255, 'required': False}), 5: ('wagtail.blocks.RichTextBlock', (), {'required': False}), 6: ('wagtail.images.blocks.ImageChooserBlock', (), {'required': False}), 7: ('wagtail.blocks.StructBlock', [[('anchor_id', 0), ('superheading_text', 4), ('heading_text', 4), ('subheading_text', 5), ('image', 6)]], {}), 8: ('wagtail.blocks.CharBlock', (), {'help_text': 'Optional: Add an ID to make this section linkable from navigation', 'max_length': 100, 'required': False}), 9: ('wagtail.blocks.RichTextBlock', (), {'char_max_length': 255, 'required': False}), 10: ('wagtail.blocks.CharBlock', (), {'char_max_length': 255}), 11: ('wagtail.blocks.BooleanBlock', (), {'default': False, 'inline_form': True, 'label': 'Position the image to the right of the statistic', 'required': False}), 12: ('wagtail.blocks.StructBlock', [[('anchor_id', 8), ('image', 6), ('image_caption', 9), ('statistic_value', 10), ('statistic_label', 10), ('align_image_on_right', 11)]], {}), 13: ('wagtail.blocks.StructBlock', [[('statistic_superlabel', 4), ('statistic_value', 10), ('statistic_label', 10)]], {}), 14: ('wagtail.blocks.ListBlock', (13,), {'min_num': 1}), 15: ('wagtail.blocks.StructBlock', [[('anchor_id', 8), ('heading_text', 10), ('statistics', 14)]], {}), 16: ('wagtail.blocks.TextBlock', (), {}), 17: ('wagtail.blocks.TextBlock', (), {'char_max_length': 255, 'required': False}), 18: ('wagtail.blocks.StructBlock', [[('image', 6), ('image_caption_heading', 17), ('image_caption_text', 17)]], {}), 19: ('wagtail.blocks.ListBlock', (18,), {'min_num': 1}), 20: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('page', 'Page'), ('file', 'File'), ('custom_url', 'Custom URL'), ('email', 'Email'), ('anchor', 'Anchor'), ('phone', 'Phone')], 'classname': 'link_choice_type_selector', 'label': 'Link to', 'required': False}), 21: ('wagtail.blocks.PageChooserBlock', (), {'form_classname': 'page_link', 'label': 'Page', 'required': False}), 22: ('wagtail.documents.blocks.DocumentChooserBlock', (), {'form_classname': 'file_link', 'label': 'File', 'required': False}), 23: ('wagtail.blocks.CharBlock', (), {'form_classname': 'custom_url_link url_field', 'label': 'Custom URL', 'max_length': 300, 'required': False, 'validators': [wagtail.admin.forms.choosers.URLOrAbsolutePathValidator()]}), 24: ('wagtail.blocks.CharBlock', (), {'form_classname': 'anchor_link', 'label': '#', 'max_length': 300, 'required': False}), 25: ('wagtail.blocks.EmailBlock', (), {'required': False}), 26: ('wagtail.blocks.CharBlock', (), {'form_classname': 'phone_link', 'label': 'Phone', 'max_length': 30, 'required': False}), 27: ('wagtail.blocks.BooleanBlock', (), {'form_classname': 'new_window_toggle', 'label': 'Open in new window', 'required': False}), 28: ('wagtail.blocks.StructBlock', [[('link_to', 20), ('page', 21), ('file', 22), ('custom_url', 23), ('anchor', 24), ('email', 25), ('phone', 26), ('new_window', 27)]], {'label': 'CTA Link', 'required': False}), 29: ('wagtail.blocks.StructBlock', [[('heading_text', 10), ('figures', 19), ('cta_text', 4), ('cta_link', 28)]], {}), 30: ('wagtail.blocks.ListBlock', (29,), {'min_num': 1}), 31: ('wagtail.blocks.StructBlock', [[('anchor_id', 8), ('heading_text', 10), ('supporting_text', 16), ('feature_list_items', 30)]], {}), 32: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link']}), 33: ('wagtail.blocks.StructBlock', [[('heading_text', 16), ('supporting_text', 32)]], {}), 34: ('wagtail.blocks.ListBlock', (33,), {'min_num': 1}), 35: ('wagtail.blocks.StructBlock', [[('anchor_id', 8), ('feature_list_items', 34)]], {}), 36: ('wagtail.blocks.StreamBlock', [[('section_header_block', 7), ('figure_with_statistic_block', 12), ('statistic_callout_block', 15), ('features_with_modals', 31), ('feature_list_block', 35)]], {'required': False}), 37: ('wagtail.blocks.StructBlock', [[('link_to', 20), ('page', 21), ('file', 22), ('custom_url', 23), ('anchor', 24), ('email', 25), ('phone', 26), ('new_window', 27)]], {'label': 'Link'}), 38: ('wagtail.blocks.StructBlock', [[('text', 4), ('link_text', 10), ('link', 37)]], {}), 39: ('wagtail.blocks.ListBlock', (38,), {'default': [], 'label': 'Call to Action', 'max_num': 1, 'min_num': 0}), 40: ('wagtail.blocks.StructBlock', [[('settings', 3), ('header', 7), ('content', 36), ('call_to_action', 39)]], {})}, null=True),
),
migrations.AlterField(
model_name='contentsubpage',
name='sections',
field=wagtail.fields.StreamField([('section', 40)], blank=True, block_lookup={0: ('wagtail.blocks.CharBlock', (), {'help_text': "Optional: Add an ID to make this section linkable from navigation (e.g., 'solutions', 'why-mozilla')", 'max_length': 100, 'required': False}), 1: ('wagtail.blocks.BooleanBlock', (), {'default': False, 'inline_form': True, 'label': 'Should the section have a divider line on top?', 'required': False}), 2: ('wagtail.blocks.BooleanBlock', (), {'default': False, 'inline_form': True, 'label': 'Should the section have a dark background?', 'required': False}), 3: ('wagtail.blocks.StructBlock', [[('anchor_id', 0), ('has_top_divider', 1), ('display_on_dark_background', 2)]], {}), 4: ('wagtail.blocks.CharBlock', (), {'char_max_length': 255, 'required': False}), 5: ('wagtail.blocks.RichTextBlock', (), {'required': False}), 6: ('wagtail.images.blocks.ImageChooserBlock', (), {'required': False}), 7: ('wagtail.blocks.StructBlock', [[('anchor_id', 0), ('superheading_text', 4), ('heading_text', 4), ('subheading_text', 5), ('image', 6)]], {}), 8: ('wagtail.blocks.CharBlock', (), {'help_text': 'Optional: Add an ID to make this section linkable from navigation', 'max_length': 100, 'required': False}), 9: ('wagtail.blocks.RichTextBlock', (), {'char_max_length': 255, 'required': False}), 10: ('wagtail.blocks.CharBlock', (), {'char_max_length': 255}), 11: ('wagtail.blocks.BooleanBlock', (), {'default': False, 'inline_form': True, 'label': 'Position the image to the right of the statistic', 'required': False}), 12: ('wagtail.blocks.StructBlock', [[('anchor_id', 8), ('image', 6), ('image_caption', 9), ('statistic_value', 10), ('statistic_label', 10), ('align_image_on_right', 11)]], {}), 13: ('wagtail.blocks.StructBlock', [[('statistic_superlabel', 4), ('statistic_value', 10), ('statistic_label', 10)]], {}), 14: ('wagtail.blocks.ListBlock', (13,), {'min_num': 1}), 15: ('wagtail.blocks.StructBlock', [[('anchor_id', 8), ('heading_text', 10), ('statistics', 14)]], {}), 16: ('wagtail.blocks.TextBlock', (), {}), 17: ('wagtail.blocks.TextBlock', (), {'char_max_length': 255, 'required': False}), 18: ('wagtail.blocks.StructBlock', [[('image', 6), ('image_caption_heading', 17), ('image_caption_text', 17)]], {}), 19: ('wagtail.blocks.ListBlock', (18,), {'min_num': 1}), 20: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('page', 'Page'), ('file', 'File'), ('custom_url', 'Custom URL'), ('email', 'Email'), ('anchor', 'Anchor'), ('phone', 'Phone')], 'classname': 'link_choice_type_selector', 'label': 'Link to', 'required': False}), 21: ('wagtail.blocks.PageChooserBlock', (), {'form_classname': 'page_link', 'label': 'Page', 'required': False}), 22: ('wagtail.documents.blocks.DocumentChooserBlock', (), {'form_classname': 'file_link', 'label': 'File', 'required': False}), 23: ('wagtail.blocks.CharBlock', (), {'form_classname': 'custom_url_link url_field', 'label': 'Custom URL', 'max_length': 300, 'required': False, 'validators': [wagtail.admin.forms.choosers.URLOrAbsolutePathValidator()]}), 24: ('wagtail.blocks.CharBlock', (), {'form_classname': 'anchor_link', 'label': '#', 'max_length': 300, 'required': False}), 25: ('wagtail.blocks.EmailBlock', (), {'required': False}), 26: ('wagtail.blocks.CharBlock', (), {'form_classname': 'phone_link', 'label': 'Phone', 'max_length': 30, 'required': False}), 27: ('wagtail.blocks.BooleanBlock', (), {'form_classname': 'new_window_toggle', 'label': 'Open in new window', 'required': False}), 28: ('wagtail.blocks.StructBlock', [[('link_to', 20), ('page', 21), ('file', 22), ('custom_url', 23), ('anchor', 24), ('email', 25), ('phone', 26), ('new_window', 27)]], {'label': 'CTA Link', 'required': False}), 29: ('wagtail.blocks.StructBlock', [[('heading_text', 10), ('figures', 19), ('cta_text', 4), ('cta_link', 28)]], {}), 30: ('wagtail.blocks.ListBlock', (29,), {'min_num': 1}), 31: ('wagtail.blocks.StructBlock', [[('anchor_id', 8), ('heading_text', 10), ('supporting_text', 16), ('feature_list_items', 30)]], {}), 32: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link']}), 33: ('wagtail.blocks.StructBlock', [[('heading_text', 16), ('supporting_text', 32)]], {}), 34: ('wagtail.blocks.ListBlock', (33,), {'min_num': 1}), 35: ('wagtail.blocks.StructBlock', [[('anchor_id', 8), ('feature_list_items', 34)]], {}), 36: ('wagtail.blocks.StreamBlock', [[('section_header_block', 7), ('figure_with_statistic_block', 12), ('statistic_callout_block', 15), ('features_with_modals', 31), ('feature_list_block', 35)]], {'required': False}), 37: ('wagtail.blocks.StructBlock', [[('link_to', 20), ('page', 21), ('file', 22), ('custom_url', 23), ('anchor', 24), ('email', 25), ('phone', 26), ('new_window', 27)]], {'label': 'Link'}), 38: ('wagtail.blocks.StructBlock', [[('text', 4), ('link_text', 10), ('link', 37)]], {}), 39: ('wagtail.blocks.ListBlock', (38,), {'default': [], 'label': 'Call to Action', 'max_num': 1, 'min_num': 0}), 40: ('wagtail.blocks.StructBlock', [[('settings', 3), ('header', 7), ('content', 36), ('call_to_action', 39)]], {})}, null=True),
),
]
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<hgroup class="mza-hgroup">
{% if value.superheading_text %}<p class="mza-eyebrow">{{ value.superheading_text }}</p>{% endif %}
{% if value.heading_text %}<h2 class="mza-section-heading">{{ value.heading_text }}</h2>{% endif %}
{% if value.subheading_text %}<p class="mza-highlight-text">{{ value.subheading_text }}</p>{% endif %}
{% if value.subheading_text %}<div class="mza-highlight-text">{{ value.subheading_text }}</div>{% endif %}
</hgroup>

{% if value.image %}
Expand Down
17 changes: 16 additions & 1 deletion bedrock/mozorg/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@

import pytest
from bs4 import BeautifulSoup
from wagtail import blocks
from wagtail.rich_text import RichText

from bedrock.cms.tests.conftest import minimal_site # noqa: F401, F811
from bedrock.mozorg import models
from bedrock.mozorg.blocks import common
from bedrock.mozorg.blocks.advertising import SectionHeaderBlock
from bedrock.mozorg.tests import factories

pytestmark = [
Expand Down Expand Up @@ -63,7 +65,7 @@ def test_advertising_index_page(minimal_site, rf, serving_method): # noqa
settings=factories.SectionSettingsFactory(anchor_id=""),
header=factories.SectionHeaderBlockFactory(
heading_text="Test Section Header",
subheading_text="Test subheading",
subheading_text=RichText("<p>Test subheading</p>"),
),
),
)
Expand Down Expand Up @@ -96,6 +98,19 @@ def test_advertising_index_page(minimal_site, rf, serving_method): # noqa
assert "Follow Mozilla Ads to get the latest secure advertising trends." in page_content


def test_section_header_block_requires_a_heading():
block = SectionHeaderBlock()

# All heading fields empty -> validation error (covers the RichText .source path)
empty = block.to_python({"superheading_text": "", "heading_text": "", "subheading_text": ""})
with pytest.raises(blocks.StructBlockValidationError):
block.clean(empty)

# A rich-text subheading alone is enough to pass
with_subheading = block.to_python({"superheading_text": "", "heading_text": "", "subheading_text": "<p>Hi</p>"})
block.clean(with_subheading) # should not raise


@pytest.mark.parametrize("serving_method", ("serve", "serve_preview"))
def test_two_column_subpage(minimal_site, rf, serving_method): # noqa
root_page = minimal_site.root_page
Expand Down
Loading