Skip to content
Open
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
9 changes: 9 additions & 0 deletions springfield/cms/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,14 @@ def get_thumbnail_url(self, icon_name):
return thumbnails.get(icon_name, "")


class ConditionalDisplayValue(blocks.StructValue):
@property
def has_conditions(self):
"""True when at least one display condition is set, meaning the block is
shown to a subset of users rather than to everyone."""
return any(self.values())


class ConditionalDisplayBlock(blocks.StructBlock):
platforms = blocks.MultipleChoiceBlock(
choices=PLATFORM_CHOICES,
Expand Down Expand Up @@ -430,6 +438,7 @@ class Meta:
icon = "view"
collapsed = True
form_classname = "compact-form struct-block"
value_class = ConditionalDisplayValue


# Element blocks
Expand Down
28 changes: 20 additions & 8 deletions springfield/cms/templates/cms/free_form_page2026.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<div class="fl-page fl-freeform-page">
{% if page.upper_content %}
<div class="fl-main has-gradient-bottom">
{% set ns = namespace(headings=0) %}
{% set ns = namespace(headings=0, leading_conditional_run=True) %}
{% set block_level = 1 %}

<div class="fl-split-page-upper has-gradient-bottom no-gap">
Expand All @@ -35,9 +35,13 @@
{% set block_type = block.block_type %}
{% set block_position = "upper-block-" ~ block_index ~ "-" ~ block_type %}

{% if (block.value.heading and block.value.heading.heading_text) or block.value.headline %}
{% set block_level = 1 if ns.headings == 0 else 2 %}
{% if block_type != "notification" and ((block.value.heading and block.value.heading.heading_text) or block.value.headline) %}
{% set block_has_display_conditions = block.value.settings and block.value.settings.show_to and block.value.settings.show_to.has_conditions %}
{% set block_level = 1 if ns.headings == 0 or (ns.leading_conditional_run and block_has_display_conditions) else 2 %}
{% set ns.headings = ns.headings + 1 %}
{% if not block_has_display_conditions %}
{% set ns.leading_conditional_run = False %}
{% endif %}
{% endif %}

{% include_block block %}
Expand All @@ -51,9 +55,13 @@
{% set block_type = block.block_type %}
{% set block_position = "lower-block-" ~ block_index ~ "-" ~ block_type %}

{% if (block.value.heading and block.value.heading.heading_text) or block.value.headline %}
{% set block_level = 1 if ns.headings == 0 else 2 %}
{% if block_type != "notification" and ((block.value.heading and block.value.heading.heading_text) or block.value.headline) %}
{% set block_has_display_conditions = block.value.settings and block.value.settings.show_to and block.value.settings.show_to.has_conditions %}
{% set block_level = 1 if ns.headings == 0 or (ns.leading_conditional_run and block_has_display_conditions) else 2 %}
{% set ns.headings = ns.headings + 1 %}
{% if not block_has_display_conditions %}
{% set ns.leading_conditional_run = False %}
{% endif %}
{% endif %}

{% include_block block %}
Expand All @@ -63,15 +71,19 @@
</div>
{% else %}
<div class="fl-main has-gradient-bottom">
{% set ns = namespace(headings=0) %}
{% set ns = namespace(headings=0, leading_conditional_run=True) %}
{% for block in page.content %}
{% set block_index = loop.index %}
{% set block_type = block.block_type %}
{% set block_position = "block-" ~ block_index ~ "-" ~ block_type %}

{% if (block.value.heading and block.value.heading.heading_text) or block.value.headline %}
{% set block_level = 1 if ns.headings == 0 else 2 %}
{% if block_type != "notification" and ((block.value.heading and block.value.heading.heading_text) or block.value.headline) %}
{% set block_has_display_conditions = block.value.settings and block.value.settings.show_to and block.value.settings.show_to.has_conditions %}
{% set block_level = 1 if ns.headings == 0 or (ns.leading_conditional_run and block_has_display_conditions) else 2 %}
{% set ns.headings = ns.headings + 1 %}
{% if not block_has_display_conditions %}
{% set ns.leading_conditional_run = False %}
{% endif %}
{% endif %}

{% include_block block %}
Expand Down
18 changes: 13 additions & 5 deletions springfield/cms/templates/cms/smart_window_explainer_page.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
{% block content %}
<div class="fl-page fl-freeform-page">
<main class="fl-main has-gradient-bottom">
{% set ns = namespace(headings=0) %}
{% set ns = namespace(headings=0, leading_conditional_run=True) %}
{% set block_level = 1 %}

<div class="fl-smart-window-explainer-hero">
Expand All @@ -26,9 +26,13 @@
{% set block_type = block.block_type %}
{% set block_position = "upper-block-" ~ block_index ~ "-" ~ block_type %}

{% if (block.value.heading and block.value.heading.heading_text) or block.value.headline %}
{% set block_level = 1 if ns.headings == 0 else 2 %}
{% if block_type != "notification" and ((block.value.heading and block.value.heading.heading_text) or block.value.headline) %}
{% set block_has_display_conditions = block.value.settings and block.value.settings.show_to and block.value.settings.show_to.has_conditions %}
{% set block_level = 1 if ns.headings == 0 or (ns.leading_conditional_run and block_has_display_conditions) else 2 %}
{% set ns.headings = ns.headings + 1 %}
{% if not block_has_display_conditions %}
{% set ns.leading_conditional_run = False %}
{% endif %}
{% endif %}

{% include_block block %}
Expand All @@ -42,9 +46,13 @@
{% set block_type = block.block_type %}
{% set block_position = "lower-block-" ~ block_index ~ "-" ~ block_type %}

{% if (block.value.heading and block.value.heading.heading_text) or block.value.headline %}
{% set block_level = 1 if ns.headings == 0 else 2 %}
{% if block_type != "notification" and ((block.value.heading and block.value.heading.heading_text) or block.value.headline) %}
{% set block_has_display_conditions = block.value.settings and block.value.settings.show_to and block.value.settings.show_to.has_conditions %}
{% set block_level = 1 if ns.headings == 0 or (ns.leading_conditional_run and block_has_display_conditions) else 2 %}
{% set ns.headings = ns.headings + 1 %}
{% if not block_has_display_conditions %}
{% set ns.leading_conditional_run = False %}
{% endif %}
{% endif %}

{% include_block block %}
Expand Down
170 changes: 168 additions & 2 deletions springfield/cms/tests/test_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
get_theme_page_pictogram_row_section,
)
from springfield.cms.fixtures.banner_fixtures import get_banner_test_page, get_banner_variants
from springfield.cms.fixtures.base_fixtures import get_placeholder_images
from springfield.cms.fixtures.base_fixtures import get_or_create_page, get_placeholder_images
from springfield.cms.fixtures.blog_fixtures import (
FEATURED_DESCRIPTIONS,
FEATURED_TITLES,
Expand Down Expand Up @@ -170,8 +170,16 @@
)
from springfield.cms.fixtures.topic_list_fixtures import get_topic_list_lower_variants, get_topic_list_test_page, get_topic_list_upper_variants
from springfield.cms.fixtures.two_column_cards_fixtures import get_two_column_cards_test_page, get_two_column_cards_variants
from springfield.cms.fixtures.whats_new_page_fixtures import get_whatsnew_index_page
from springfield.cms.icon_utils import icon_value_fn
from springfield.cms.models import ArticleDetailPage, PretranslatedPhrase, SpringfieldImage
from springfield.cms.models import (
ArticleDetailPage,
FreeFormPage2026,
PretranslatedPhrase,
SmartWindowExplainerPage,
SpringfieldImage,
WhatsNewPage2026,
)
from springfield.cms.models.locale import SpringfieldLocale
from springfield.cms.models.snippets import BlogTag, BlogTopic
from springfield.cms.templatetags.cms_tags import add_utm_parameters
Expand Down Expand Up @@ -5888,3 +5896,161 @@ def test_topic_section_exempts_its_own_topic():

assert topic_keys == {topic.translation_key}
assert tag_keys == set()


# Page heading levels

# The page types whose free form content stream supplies the page's h1. SmartWindowPage
# is absent on purpose: it renders the same blocks below its own h1, starting at h2.
FREEFORM_PAGE_TYPES = [
pytest.param(FreeFormPage2026, id="free_form_page"),
pytest.param(WhatsNewPage2026, id="whats_new_page"),
pytest.param(SmartWindowExplainerPage, id="smart_window_explainer_page"),
]

# The platform intros used by the heading-level tests: mutually exclusive, so a
# visitor only ever sees one of them.
PLATFORM_INTROS = [
("condition-windows", "Firefox for Windows", ["windows"]),
("condition-osx", "Firefox for macOS", ["osx"]),
("condition-other-os", "Firefox for every other platform", ["linux", "android", "ios", "other-os", "unsupported"]),
]


def intro_block(heading_text, platforms=None):
"""Intro block carrying only a heading, shown to `platforms` if given and to everyone otherwise."""
return {
"type": "intro",
"value": {
"settings": {"show_to": {"platforms": platforms or []}},
"heading": {"heading_text": f"<p>{heading_text}</p>"},
},
}


def conditional_intro_blocks():
return [intro_block(heading_text, platforms) for _, heading_text, platforms in PLATFORM_INTROS]


def section_and_banner_blocks():
"""The two blocks that follow the intros in the heading-level tests: a section
holding a media + content block, and a banner."""
return [
{
"type": "section",
"value": {
"heading": {"heading_text": "<p>Ways to browse</p>"},
"content": [{"type": "media_content", "value": {"heading": {"heading_text": "<p>Private by default</p>"}}}],
},
},
{"type": "banner", "value": {"heading": {"heading_text": "<p>Get Firefox now</p>"}}},
]


def publish_freeform_content_page(page_model, slug, parent, content):
"""Publish a page of `page_model` whose free form content stream holds `content`.

What's New pages carry a version and only live under a What's New index page; the
other types sit happily under any parent.
"""
defaults = {"title": slug.replace("-", " ").capitalize()}
if page_model is WhatsNewPage2026:
parent = get_whatsnew_index_page()
defaults["version"] = "150"

page = get_or_create_page(page_model, slug=f"{slug}-{page_model._meta.model_name}", parent=parent, defaults=defaults)
page.content = content
page.save_revision().publish()
return page


def render_main_element(page, rf):
"""Serve `page` and return the soup for its main content wrapper."""
response = page.serve(rf.get(page.get_full_url()))
assert response.status_code == 200
return BeautifulSoup(response.content, "html.parser").find(class_="fl-main")


def assert_intro_heading(main, condition_class, heading_text, heading_tag):
conditional_wrapper = main.find("div", class_=condition_class)
heading = conditional_wrapper.find("div", class_="fl-intro").find(class_="fl-heading")
assert heading.name == heading_tag
assert heading.get_text(strip=True) == heading_text


def assert_section_and_banner_heading_levels(main):
"""The section, the media + content block nested inside it, and the banner sit
below the intros in the hierarchy, however many intros lead the page."""
section_element = main.find("section", class_="fl-section")
section_heading = section_element.find(class_="fl-heading")
assert section_heading.name == "h2"
assert section_heading.get_text(strip=True) == "Ways to browse"

media_content_heading = section_element.find("div", class_="fl-mediacontent").find(class_="fl-heading")
assert media_content_heading.name == "h3"
assert media_content_heading.get_text(strip=True) == "Private by default"

banner_heading = main.find("div", class_="fl-banner-container").find(class_="fl-heading")
assert banner_heading.name == "h2"
assert banner_heading.get_text(strip=True) == "Get Firefox now"


@pytest.mark.parametrize("page_model", FREEFORM_PAGE_TYPES)
def test_heading_levels_respect_page_hierarchy(page_model, index_page, rf):
page = publish_freeform_content_page(
page_model,
slug="heading-hierarchy",
parent=index_page,
content=[intro_block("Firefox for everyone")] + section_and_banner_blocks(),
)

main = render_main_element(page, rf)

intro_heading = main.find("div", class_="fl-intro").find(class_="fl-heading")
assert intro_heading.name == "h1"
assert intro_heading.get_text(strip=True) == "Firefox for everyone"

assert_section_and_banner_heading_levels(main)


@pytest.mark.parametrize("page_model", FREEFORM_PAGE_TYPES)
def test_heading_levels_consider_conditional_display(page_model, index_page, rf):
page = publish_freeform_content_page(
page_model,
slug="platform-intros",
parent=index_page,
content=conditional_intro_blocks() + section_and_banner_blocks(),
)

main = render_main_element(page, rf)

for condition_class, heading_text, _ in PLATFORM_INTROS:
assert_intro_heading(main, condition_class, heading_text, "h1")

assert_section_and_banner_heading_levels(main)


@pytest.mark.parametrize("notification_headline", ["", "<p>Your Firefox is up to date.</p>"], ids=["message_only", "with_headline"])
@pytest.mark.parametrize("page_model", FREEFORM_PAGE_TYPES)
def test_heading_levels_skip_a_leading_block_without_a_heading(page_model, notification_headline, index_page, rf):
notification = {
"type": "notification",
"value": {"headline": notification_headline, "message": "<p>Firefox has been updated.</p>"},
}
page = publish_freeform_content_page(
page_model,
slug="notification-and-platform-intros",
parent=index_page,
content=[notification] + conditional_intro_blocks() + section_and_banner_blocks(),
)

main = render_main_element(page, rf)

notification_element = main.find("div", class_="fl-notification")
assert "Firefox has been updated." in notification_element.get_text(strip=True)
assert notification_element.find(["h1", "h2", "h3", "h4", "h5", "h6"]) is None

for condition_class, heading_text, _ in PLATFORM_INTROS:
assert_intro_heading(main, condition_class, heading_text, "h1")

assert_section_and_banner_heading_levels(main)
Loading
Loading