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
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# 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.14 on 2026-06-12 23:47

import django.db.models.deletion
from django.db import migrations, models

import modelcluster.fields


class Migration(migrations.Migration):
dependencies = [
("cms", "0103_contactpage"),
]

operations = [
migrations.CreateModel(
name="ArticleDetailPagePencilBannerPlacement",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("sort_order", models.IntegerField(blank=True, editable=False, null=True)),
(
"page",
modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE, related_name="pencil_banner_placements", to="cms.articledetailpage"
),
),
("snippet", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="+", to="cms.pencilbannersnippet")),
],
options={
"verbose_name": "Article Detail Page Pencil Banner Placement",
"verbose_name_plural": "Article Detail Page Pencil Banner Placements",
"ordering": ["sort_order"],
"abstract": False,
},
),
migrations.CreateModel(
name="ArticleThemePagePencilBannerPlacement",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("sort_order", models.IntegerField(blank=True, editable=False, null=True)),
(
"page",
modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE, related_name="pencil_banner_placements", to="cms.articlethemepage"
),
),
("snippet", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="+", to="cms.pencilbannersnippet")),
],
options={
"verbose_name": "Article Theme Page Pencil Banner Placement",
"verbose_name_plural": "Article Theme Page Pencil Banner Placements",
"ordering": ["sort_order"],
"abstract": False,
},
),
migrations.CreateModel(
name="HomePagePencilBannerPlacement",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("sort_order", models.IntegerField(blank=True, editable=False, null=True)),
(
"page",
modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE, related_name="pencil_banner_placements", to="cms.homepage"
),
),
("snippet", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="+", to="cms.pencilbannersnippet")),
],
options={
"verbose_name": "Home Page Pencil Banner Placement",
"verbose_name_plural": "Home Page Pencil Banner Placements",
"ordering": ["sort_order"],
"abstract": False,
},
),
]
51 changes: 51 additions & 0 deletions springfield/cms/models/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ class HomePage(UTMParamsMixin, AbstractSpringfieldCMSPage):
content_panels = AbstractSpringfieldCMSPage.content_panels + [
FieldPanel("upper_content"),
FieldPanel("lower_content"),
InlinePanel("pencil_banner_placements", label="Pencil Banners"),
]

class Meta:
Expand Down Expand Up @@ -809,6 +810,7 @@ class ArticleDetailPage(UTMParamsMixin, AbstractSpringfieldCMSPage):
),
FieldPanel("content"),
FieldPanel("related_articles"),
InlinePanel("pencil_banner_placements", label="Pencil Banners"),
]

if TYPE_CHECKING:
Expand Down Expand Up @@ -847,6 +849,7 @@ class ArticleThemePage(UTMParamsMixin, AbstractSpringfieldCMSPage):
content_panels = AbstractSpringfieldCMSPage.content_panels + [
FieldPanel("upper_content"),
FieldPanel("content"),
InlinePanel("pencil_banner_placements", label="Pencil Banners"),
]

def __str__(self):
Expand Down Expand Up @@ -934,6 +937,54 @@ def __str__(self):
return self.page.title + " -> " + self.snippet.title


class HomePagePencilBannerPlacement(Orderable):
page = ParentalKey("cms.HomePage", on_delete=models.CASCADE, related_name="pencil_banner_placements")
snippet = models.ForeignKey("cms.PencilBannerSnippet", on_delete=models.CASCADE, related_name="+")

class Meta(Orderable.Meta):
verbose_name = "Home Page Pencil Banner Placement"
verbose_name_plural = "Home Page Pencil Banner Placements"

panels = [
FieldPanel("snippet"),
]

def __str__(self):
return self.page.title + " -> " + self.snippet.title


class ArticleThemePagePencilBannerPlacement(Orderable):
page = ParentalKey("cms.ArticleThemePage", on_delete=models.CASCADE, related_name="pencil_banner_placements")
snippet = models.ForeignKey("cms.PencilBannerSnippet", on_delete=models.CASCADE, related_name="+")

class Meta(Orderable.Meta):
verbose_name = "Article Theme Page Pencil Banner Placement"
verbose_name_plural = "Article Theme Page Pencil Banner Placements"

panels = [
FieldPanel("snippet"),
]

def __str__(self):
return self.page.title + " -> " + self.snippet.title


class ArticleDetailPagePencilBannerPlacement(Orderable):
page = ParentalKey("cms.ArticleDetailPage", on_delete=models.CASCADE, related_name="pencil_banner_placements")
snippet = models.ForeignKey("cms.PencilBannerSnippet", on_delete=models.CASCADE, related_name="+")

class Meta(Orderable.Meta):
verbose_name = "Article Detail Page Pencil Banner Placement"
verbose_name_plural = "Article Detail Page Pencil Banner Placements"

panels = [
FieldPanel("snippet"),
]

def __str__(self):
return self.page.title + " -> " + self.snippet.title


class FreeFormPage2026(PromotedPageMixin, UTMParamsMixin, QRCodeFloatingSnippetMixin, AbstractSpringfieldCMSPage):
"""A flexible 2026 page type with optional upper/lower split layout."""

Expand Down
11 changes: 11 additions & 0 deletions springfield/cms/templates/cms/base-flare26.html
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,17 @@
>
{{ wagtailuserbar() }}

{% block pencil_banners %}
{% if page is defined and page.pencil_banner_placements is defined %}
{% for placement in page.pencil_banner_placements.all() %}
{% set value = placement.snippet.get_localized() %}
{% if value %}
{% include "cms/snippets/pencil-banner.html" %}
{% endif %}
{% endfor %}
{% endif %}
{% endblock %}

{% block flare_header %}
<include:flare26-header />
{% endblock %}
Expand Down
7 changes: 0 additions & 7 deletions springfield/cms/templates/cms/free_form_page2026.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,6 @@


{% block flare_header %}
{% for placement in page.pencil_banner_placements.all() %}
{% set value = placement.snippet.get_localized() %}
{% if value %}
{% include "cms/snippets/pencil-banner.html" %}
{% endif %}
{% endfor %}

{% set theme = "dark" if page.upper_content else None %}
{% set hide_nav_cta = not page.show_nav_cta %}
{% set no_menu = not page.show_navigation %}
Expand Down
51 changes: 51 additions & 0 deletions springfield/cms/tests/test_pencil_banner_snippet.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@
from bs4 import BeautifulSoup
from wagtail.models import Locale

from springfield.cms.fixtures.feature_page_fixtures import get_features_theme_page
from springfield.cms.fixtures.freeformpage import get_freeform_page_test_page
from springfield.cms.fixtures.homepage_fixtures import get_home_test_page
from springfield.cms.fixtures.snippet_fixtures import get_pencil_banner_snippet
from springfield.cms.models import ArticleDetailPage
from springfield.cms.models.pages import (
ArticleDetailPagePencilBannerPlacement,
ArticleThemePagePencilBannerPlacement,
HomePagePencilBannerPlacement,
)

pytestmark = [pytest.mark.django_db]

Expand Down Expand Up @@ -94,3 +102,46 @@ def test_page_without_pencil_banner_placement_does_not_render_banner(minimal_sit

soup = BeautifulSoup(response.content, "html.parser")
assert not soup.find("div", class_="fl-pencil-banner"), "Pencil banner should not render when no placement exists"


def _make_home_page():
return get_home_test_page(), HomePagePencilBannerPlacement


def _make_article_theme_page():
return get_features_theme_page(), ArticleThemePagePencilBannerPlacement


def _make_article_detail_page():
theme_page = get_features_theme_page()
page = ArticleDetailPage.objects.child_of(theme_page).filter(slug="test-pencil-detail").first()
if not page:
page = ArticleDetailPage(
slug="test-pencil-detail",
title="Test Pencil Detail",
content=[],
)
theme_page.add_child(instance=page)
return page, ArticleDetailPagePencilBannerPlacement


PAGES_WITH_PENCIL_BANNER = [
pytest.param(_make_home_page, id="home"),
pytest.param(_make_article_theme_page, id="article_theme"),
pytest.param(_make_article_detail_page, id="article_detail"),
]


@pytest.mark.parametrize("page_factory", PAGES_WITH_PENCIL_BANNER)
def test_page_renders_pencil_banner(page_factory, minimal_site, rf):
page, placement_cls = page_factory()
snippet = get_pencil_banner_snippet()
placement_cls.objects.get_or_create(page=page, snippet=snippet)
page.save_revision().publish()

request = rf.get(page.get_full_url())
response = page.serve(request)
assert response.status_code == 200

soup = BeautifulSoup(response.content, "html.parser")
assert soup.find("div", class_="fl-pencil-banner"), f"Pencil banner should render on {page.__class__.__name__}"
Loading