Skip to content
Draft
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
24 changes: 23 additions & 1 deletion backend/api/cms/page/blocks/homepage_hero.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from enum import Enum
from typing import Self
from api.cms.base.blocks.cta import CTA
from api.cms.page.registry import register_page_block
import strawberry

Expand All @@ -15,11 +16,32 @@ class HomepageHeroCity(Enum):
class HomepageHero:
id: strawberry.ID
city: HomepageHeroCity | None
pretitle: str
title: str
subtitle: str
highlight: str
illustration: str
primary_cta: CTA | None
secondary_cta: CTA | None

@classmethod
def from_block(cls, block) -> Self:
city = block.value.get("city")
# Blocks saved before a field was added don't have a value for it,
# Wagtail fills those with the block default, which is None.
city = block.value["city"]
primary_cta = block.value["primary_cta"]
secondary_cta = block.value["secondary_cta"]

return cls(
id=block.id,
city=HomepageHeroCity(city) if city else None,
pretitle=block.value["pretitle"] or "",
title=block.value["title"] or "",
subtitle=block.value["subtitle"] or "",
highlight=block.value["highlight"] or "",
illustration=block.value["illustration"] or "",
primary_cta=(CTA.from_block(primary_cta) if primary_cta["label"] else None),
secondary_cta=(
CTA.from_block(secondary_cta) if secondary_cta["label"] else None
),
)
12 changes: 12 additions & 0 deletions backend/api/cms/tests/factories.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from cms.components.base.blocks.cta import CTA
from cms.components.page.blocks.homepage_hero import HomepageHero
from cms.components.page.models import GenericPage
from cms.components.page.blocks.text_section import TextSection
Expand Down Expand Up @@ -56,7 +57,18 @@ class Params:
p = factory.Faker("text", max_nb_chars=300)


class CTAFactory(StructBlockFactory):
label = ""
link = ""

class Meta:
model = CTA


class HomepageHeroFactory(StructBlockFactory):
primary_cta = factory.SubFactory(CTAFactory)
secondary_cta = factory.SubFactory(CTAFactory)

class Meta:
model = HomepageHero

Expand Down
114 changes: 114 additions & 0 deletions backend/api/cms/tests/page/queries/test_cms_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,3 +467,117 @@ def test_page_filter_by_site_and_language(graphql_client, locale):
assert response["data"] == {
"cmsPage": {"body": [{"title": "There they are, all standing in a row"}]}
}


HOMEPAGE_HERO_QUERY = """
query Page ($hostname: String!, $language: String!, $slug: String!) {
cmsPage(hostname: $hostname, language: $language, slug: $slug){
...on GenericPage {
body {
... on HomepageHero {
city
pretitle
title
subtitle
highlight
illustration
primaryCta {
label
link
}
secondaryCta {
label
link
}
}
}
}
}
}
"""


def test_homepage_hero_with_copy_and_ctas(graphql_client, locale):
parent = GenericPageFactory()
page = GenericPageFactory(
slug="home",
locale=locale("en"),
parent=parent,
title="Home",
body__0__homepage_hero__city="bologna",
body__0__homepage_hero__pretitle="Bologna, May 27 - 30, 2027",
body__0__homepage_hero__title="PyCon Italia 2027",
body__0__homepage_hero__subtitle=(
"Four days of talks, tutorials and community, in Bologna"
),
body__0__homepage_hero__highlight="1,000+ attendees",
body__0__homepage_hero__illustration="snakeWithBalloon",
body__0__homepage_hero__primary_cta__label="Buy tickets",
body__0__homepage_hero__primary_cta__link="/tickets",
body__0__homepage_hero__secondary_cta__label="See the programme",
body__0__homepage_hero__secondary_cta__link="/schedule",
)
page.save_revision().publish()
SiteFactory(hostname="pycon", port=80, root_page=parent)

response = graphql_client.query(
HOMEPAGE_HERO_QUERY,
variables={"hostname": "pycon", "slug": "home", "language": "en"},
)

assert response["data"] == {
"cmsPage": {
"body": [
{
"city": "BOLOGNA",
"pretitle": "Bologna, May 27 - 30, 2027",
"title": "PyCon Italia 2027",
"subtitle": (
"Four days of talks, tutorials and community, in Bologna"
),
"highlight": "1,000+ attendees",
"illustration": "snakeWithBalloon",
"primaryCta": {"label": "Buy tickets", "link": "/tickets"},
"secondaryCta": {
"label": "See the programme",
"link": "/schedule",
},
}
],
}
}


def test_homepage_hero_saved_before_the_copy_fields_existed(graphql_client, locale):
parent = GenericPageFactory()
page = GenericPageFactory(
slug="home",
locale=locale("en"),
parent=parent,
title="Home",
body__0__homepage_hero__city="bologna",
)
page.save_revision().publish()
SiteFactory(hostname="pycon", port=80, root_page=parent)

response = graphql_client.query(
HOMEPAGE_HERO_QUERY,
variables={"hostname": "pycon", "slug": "home", "language": "en"},
)

assert response["data"] == {
"cmsPage": {
"body": [
{
"city": "BOLOGNA",
"pretitle": "",
"title": "",
"subtitle": "",
"highlight": "",
"illustration": "",
"primaryCta": None,
"secondaryCta": None,
}
],
}
}
30 changes: 30 additions & 0 deletions backend/cms/components/page/blocks/homepage_hero.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from cms.components.base.blocks.cta import CTA
from cms.components.page.fields import IllustrationChoiceBlock
from wagtail import blocks


Expand All @@ -8,6 +10,34 @@ class HomepageHero(blocks.StructBlock):
("bologna", "Bologna"),
]
)
pretitle = blocks.CharBlock(
required=False,
help_text="When and where, e.g. 'Bologna, May 27 – 30, 2027'",
)
title = blocks.CharBlock(
required=False,
help_text="The conference name as text, e.g. 'PyCon Italia 2027'",
)
subtitle = blocks.CharBlock(
required=False,
help_text=(
"One line describing the event, e.g. 'Four days of talks, "
"tutorials and community, in Bologna'"
),
)
highlight = blocks.CharBlock(
required=False,
help_text="Optional credibility marker, e.g. '1,000+ attendees'",
)
primary_cta = CTA(label="Primary CTA")
secondary_cta = CTA(label="Secondary CTA")
illustration = IllustrationChoiceBlock(
required=False,
help_text=(
"Shown next to the copy instead of the animated city illustration. "
"Renders immediately, so prefer it when the fold has to be fast."
),
)

class Meta:
label = "Homepage Hero"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 5.2.8 on 2026-08-15 22:38

import cms.components.base.blocks.accordion
import cms.components.page.blocks.communities_section
import wagtail.fields
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('page', '0008_alter_genericpage_body'),
]

operations = [
migrations.AlterField(
model_name='genericpage',
name='body',
field=wagtail.fields.StreamField([('text_section', 7), ('map', 11), ('slider_cards_section', 18), ('sponsors_section', 21), ('home_intro_section', 22), ('keynoters_section', 23), ('schedule_preview_section', 26), ('socials_section', 27), ('special_guest_section', 30), ('information_section', 33), ('news_grid_section', 34), ('checkout_section', 35), ('live_streaming_section', 34), ('homepage_hero', 42), ('dynamic_content_display_section', 44), ('communities_section', 46)], block_lookup={0: ('wagtail.blocks.CharBlock', (), {'required': False}), 1: ('wagtail.blocks.BooleanBlock', (), {'default': False, 'required': False}), 2: ('wagtail.blocks.RichTextBlock', (), {'required': False}), 3: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('text-1', 'Text-1'), ('text-2', 'Text-2')], 'required': False}), 4: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('cathedral', 'Cathedral'), ('florence', 'Florence'), ('florence2', 'Florence2'), ('handWithSnakeInside', 'Hand With Snake Inside'), ('snake1', 'Snake1'), ('snake2', 'Snake2'), ('snake4', 'Snake4'), ('snake5', 'Snake5'), ('snakeBody', 'Snake Body'), ('snakeCouple', 'Snake Couple'), ('snakeDNA', 'Snake D N A'), ('snakeHead', 'Snake Head'), ('snakeInDragon', 'Snake In Dragon'), ('snakeInDragonInverted', 'Snake In Dragon Inverted'), ('snakeLetter', 'Snake Letter'), ('snakeLongNeck', 'Snake Long Neck'), ('snakePencil', 'Snake Pencil'), ('snakeTail', 'Snake Tail'), ('snakeWithBalloon', 'Snake With Balloon'), ('snakeWithContacts', 'Snake With Contacts'), ('snakesWithBanner', 'Snakes With Banner'), ('snakesWithCocktail', 'Snakes With Cocktail'), ('snakesWithDirections', 'Snakes With Directions'), ('snakesWithOutlines', 'Snakes With Outlines'), ('tripleSnakes', 'Triple Snakes')], 'required': False}), 5: ('wagtail.blocks.ListBlock', (cms.components.base.blocks.accordion.Accordion,), {}), 6: ('wagtail.blocks.StructBlock', [[('label', 0), ('link', 0)]], {}), 7: ('wagtail.blocks.StructBlock', [[('title', 0), ('is_main_title', 1), ('subtitle', 0), ('body', 2), ('body_text_size', 3), ('illustration', 4), ('accordions', 5), ('cta', 6)]], {}), 8: ('wagtail.blocks.DecimalBlock', (), {'decimal_places': 8, 'max_digits': 11}), 9: ('wagtail.blocks.IntegerBlock', (), {'default': 15}), 10: ('wagtail.blocks.URLBlock', (), {}), 11: ('wagtail.blocks.StructBlock', [[('longitude', 8), ('latitude', 8), ('zoom', 9), ('link', 10)]], {}), 12: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('xl', 'Extra Large'), ('3xl', '3 Extra Large')]}), 13: ('wagtail.blocks.CharBlock', (), {}), 14: ('wagtail.blocks.RichTextBlock', (), {}), 15: ('wagtail.blocks.StructBlock', [[('title', 13), ('body', 14), ('cta', 6)]], {}), 16: ('wagtail.blocks.StructBlock', [[('title', 13), ('body', 14), ('price', 13), ('price_tier', 13), ('cta', 6)]], {}), 17: ('wagtail.blocks.StreamBlock', [[('simple_text_card', 15), ('price_card', 16)]], {}), 18: ('wagtail.blocks.StructBlock', [[('title', 0), ('spacing', 12), ('snake_background', 1), ('cards', 17)]], {}), 19: ('wagtail.blocks.CharBlock', (), {'required': True}), 20: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('side-by-side', 'Side-by-side'), ('vertical', 'Vertical')], 'required': False}), 21: ('wagtail.blocks.StructBlock', [[('title', 19), ('body', 19), ('cta', 6), ('layout', 20)]], {}), 22: ('wagtail.blocks.StructBlock', [[('pretitle', 0), ('title', 0)]], {}), 23: ('wagtail.blocks.StructBlock', [[('title', 19), ('cta', 6)]], {}), 24: ('wagtail.blocks.StructBlock', [[('label', 0), ('link', 0)]], {'label': 'Primary CTA'}), 25: ('wagtail.blocks.StructBlock', [[('label', 0), ('link', 0)]], {'label': 'Secondary CTA'}), 26: ('wagtail.blocks.StructBlock', [[('title', 19), ('primary_cta', 24), ('secondary_cta', 25)]], {}), 27: ('wagtail.blocks.StructBlock', [[('label', 19), ('hashtag', 19)]], {}), 28: ('wagtail.images.blocks.ImageChooserBlock', (), {'required': True}), 29: ('wagtail.blocks.DateBlock', (), {'required': True}), 30: ('wagtail.blocks.StructBlock', [[('title', 19), ('guest_name', 19), ('guest_photo', 28), ('guest_job_title', 19), ('event_date', 29), ('cta', 6)]], {}), 31: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('coral', 'coral'), ('caramel', 'caramel'), ('cream', 'cream'), ('yellow', 'yellow'), ('green', 'green'), ('purple', 'purple'), ('pink', 'pink'), ('blue', 'blue'), ('red', 'red'), ('success', 'success'), ('warning', 'warning'), ('neutral', 'neutral'), ('error', 'error'), ('black', 'black'), ('grey', 'grey'), ('white', 'white'), ('milk', 'milk')]}), 32: ('wagtail.blocks.DateTimeBlock', (), {'required': False}), 33: ('wagtail.blocks.StructBlock', [[('title', 19), ('body', 2), ('illustration', 4), ('background_color', 31), ('countdown_to_datetime', 32), ('countdown_to_deadline', 0), ('cta', 6)]], {}), 34: ('wagtail.blocks.StructBlock', [[]], {}), 35: ('wagtail.blocks.StructBlock', [[('show_conference_tickets_products', 1), ('show_social_events_products', 1), ('show_tours_products', 1), ('show_gadgets_products', 1), ('show_membership_products', 1)]], {}), 36: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('florence', 'Florence'), ('bologna', 'Bologna')]}), 37: ('wagtail.blocks.CharBlock', (), {'help_text': "When and where, e.g. 'Bologna, May 27 – 30, 2027'", 'required': False}), 38: ('wagtail.blocks.CharBlock', (), {'help_text': "The conference name as text, e.g. 'PyCon Italia 2027'", 'required': False}), 39: ('wagtail.blocks.CharBlock', (), {'help_text': "One line describing the event, e.g. 'Four days of talks, tutorials and community, in Bologna'", 'required': False}), 40: ('wagtail.blocks.CharBlock', (), {'help_text': "Optional credibility marker, e.g. '1,000+ attendees'", 'required': False}), 41: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('cathedral', 'Cathedral'), ('florence', 'Florence'), ('florence2', 'Florence2'), ('handWithSnakeInside', 'Hand With Snake Inside'), ('snake1', 'Snake1'), ('snake2', 'Snake2'), ('snake4', 'Snake4'), ('snake5', 'Snake5'), ('snakeBody', 'Snake Body'), ('snakeCouple', 'Snake Couple'), ('snakeDNA', 'Snake D N A'), ('snakeHead', 'Snake Head'), ('snakeInDragon', 'Snake In Dragon'), ('snakeInDragonInverted', 'Snake In Dragon Inverted'), ('snakeLetter', 'Snake Letter'), ('snakeLongNeck', 'Snake Long Neck'), ('snakePencil', 'Snake Pencil'), ('snakeTail', 'Snake Tail'), ('snakeWithBalloon', 'Snake With Balloon'), ('snakeWithContacts', 'Snake With Contacts'), ('snakesWithBanner', 'Snakes With Banner'), ('snakesWithCocktail', 'Snakes With Cocktail'), ('snakesWithDirections', 'Snakes With Directions'), ('snakesWithOutlines', 'Snakes With Outlines'), ('tripleSnakes', 'Triple Snakes')], 'help_text': 'Shown next to the copy instead of the animated city illustration. Renders immediately, so prefer it when the fold has to be fast.', 'required': False}), 42: ('wagtail.blocks.StructBlock', [[('city', 36), ('pretitle', 37), ('title', 38), ('subtitle', 39), ('highlight', 40), ('primary_cta', 24), ('secondary_cta', 25), ('illustration', 41)]], {}), 43: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('speakers', 'Speakers'), ('keynoters', 'Keynoters'), ('proposals', 'Proposals')]}), 44: ('wagtail.blocks.StructBlock', [[('source', 43)]], {}), 45: ('wagtail.blocks.ListBlock', (cms.components.page.blocks.communities_section.Community,), {}), 46: ('wagtail.blocks.StructBlock', [[('title', 19), ('communities', 45)]], {})}),
),
]
7 changes: 7 additions & 0 deletions backend/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,13 @@ type HomeIntroSection {
type HomepageHero {
id: ID!
city: HomepageHeroCity
pretitle: String!
title: String!
subtitle: String!
highlight: String!
illustration: String!
primaryCta: CTA
secondaryCta: CTA
}

enum HomepageHeroCity {
Expand Down
Loading
Loading