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
33 changes: 33 additions & 0 deletions 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 @@ -10,16 +11,48 @@ class HomepageHeroCity(Enum):
BOLOGNA = "bologna"


@strawberry.enum
class HomepageHeroVariant(Enum):
ILLUSTRATION_ONLY = "illustration_only"
OVERLAY = "overlay"


@register_page_block
@strawberry.type
class HomepageHero:
id: strawberry.ID
city: HomepageHeroCity | None
variant: HomepageHeroVariant
title: str
subtitle: str
body: str
highlight: str
primary_cta: CTA | None
secondary_cta: CTA | None

@classmethod
def from_block(cls, block) -> Self:
city = block.value.get("city")
variant = block.value.get("variant")
primary_cta = block.value.get("primary_cta")
secondary_cta = block.value.get("secondary_cta")

return cls(
id=block.id,
city=HomepageHeroCity(city) if city else None,
variant=HomepageHeroVariant(variant or "illustration_only"),
title=block.value.get("title") or "",
subtitle=block.value.get("subtitle") or "",
body=block.value.get("body") or "",
highlight=block.value.get("highlight") or "",
primary_cta=(
CTA.from_block(primary_cta)
if primary_cta and primary_cta["label"]
else None
),
secondary_cta=(
CTA.from_block(secondary_cta)
if secondary_cta and 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
Expand Up @@ -6,6 +6,7 @@
SliderCardsSection,
)
from cms.components.page.models import BodyBlock
from cms.components.base.blocks.cta import CTA
from cms.components.base.blocks.map import Map
from wagtail_factories import (
CharBlockFactory,
Expand Down Expand Up @@ -56,7 +57,18 @@ class Params:
p = factory.Faker("text", max_nb_chars=300)


class CTAFactory(StructBlockFactory):
label = factory.SubFactory(CharBlockFactory)
link = factory.SubFactory(CharBlockFactory)

class Meta:
model = CTA


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

class Meta:
model = HomepageHero

Expand Down
78 changes: 78 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 @@ -70,6 +70,84 @@ def test_page(graphql_client, locale):
}


def test_page_with_homepage_hero_overlay(graphql_client, locale):
parent = GenericPageFactory()
page = GenericPageFactory(
slug="bubble-tea",
locale=locale("en"),
parent=parent,
title="Bubble",
body__0__homepage_hero__city="bologna",
body__0__homepage_hero__variant="overlay",
body__0__homepage_hero__title="PyCon Italia 2027",
body__0__homepage_hero__subtitle="Bologna, May 27 - 30, 2027",
body__0__homepage_hero__body="Four days of talks, tutorials and community",
body__0__homepage_hero__highlight="1,000+ attendees",
body__0__homepage_hero__primary_cta__label__value="Get your ticket",
body__0__homepage_hero__primary_cta__link__value="/tickets",
body__0__homepage_hero__secondary_cta__label__value="See the schedule",
body__0__homepage_hero__secondary_cta__link__value="/schedule",
body__1__homepage_hero__city="bologna",
body__1__homepage_hero__primary_cta__label__value="",
body__1__homepage_hero__secondary_cta__label__value="",
)
page.save_revision().publish()
SiteFactory(hostname="pycon", port=80, root_page=parent)
query = """
query Page ($hostname: String!, $language: String!, $slug: String!) {
cmsPage(hostname: $hostname, language: $language, slug: $slug){
...on GenericPage {
body {
... on HomepageHero {
city
variant
title
subtitle
body
highlight
primaryCta {
label
link
}
secondaryCta {
label
link
}
}
}
}
}
}
"""

response = graphql_client.query(
query, variables={"hostname": "pycon", "slug": "bubble-tea", "language": "en"}
)

assert response["data"]["cmsPage"]["body"] == [
{
"city": "BOLOGNA",
"variant": "OVERLAY",
"title": "PyCon Italia 2027",
"subtitle": "Bologna, May 27 - 30, 2027",
"body": "Four days of talks, tutorials and community",
"highlight": "1,000+ attendees",
"primaryCta": {"label": "Get your ticket", "link": "/tickets"},
"secondaryCta": {"label": "See the schedule", "link": "/schedule"},
},
{
"city": "BOLOGNA",
"variant": "ILLUSTRATION_ONLY",
"title": "",
"subtitle": "",
"body": "",
"highlight": "",
"primaryCta": None,
"secondaryCta": None,
},
]


def test_page_with_ticket_restriction_and_ticket_returns_content(
graphql_client, locale, user, mock_has_ticket
):
Expand Down
38 changes: 38 additions & 0 deletions backend/cms/components/page/blocks/homepage_hero.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from cms.components.base.blocks.cta import CTA
from wagtail import blocks


Expand All @@ -8,6 +9,43 @@ class HomepageHero(blocks.StructBlock):
("bologna", "Bologna"),
]
)
variant = blocks.ChoiceBlock(
required=False,
default="illustration_only",
choices=[
("illustration_only", "Illustration only"),
("overlay", "Overlay (copy on top of the illustration)"),
],
help_text=(
"Illustration only keeps the full-screen illustration with no text. "
"Overlay puts the copy and the CTAs below on top of it."
),
)
title = blocks.CharBlock(
required=False,
label="Title",
help_text="The conference name as text, e.g. PyCon Italia 2027.",
)
subtitle = blocks.CharBlock(
required=False,
label="Dates and city",
help_text="e.g. Bologna, May 27 - 30, 2027.",
)
body = blocks.CharBlock(
required=False,
label="One line description",
help_text=(
"What kind of event this is, in one line, e.g. Four days of talks, "
"tutorials and community, in Bologna."
),
)
highlight = blocks.CharBlock(
required=False,
label="Scale",
help_text="Optional credibility marker, e.g. 1,000+ attendees.",
)
primary_cta = CTA(label="Primary CTA")
secondary_cta = CTA(label="Secondary CTA")

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 23:16

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.ChoiceBlock', [], {'choices': [('illustration_only', 'Illustration only'), ('overlay', 'Overlay (copy on top of the illustration)')], 'help_text': 'Illustration only keeps the full-screen illustration with no text. Overlay puts the copy and the CTAs below on top of it.', 'required': False}), 38: ('wagtail.blocks.CharBlock', (), {'help_text': 'The conference name as text, e.g. PyCon Italia 2027.', 'label': 'Title', 'required': False}), 39: ('wagtail.blocks.CharBlock', (), {'help_text': 'e.g. Bologna, May 27 - 30, 2027.', 'label': 'Dates and city', 'required': False}), 40: ('wagtail.blocks.CharBlock', (), {'help_text': 'What kind of event this is, in one line, e.g. Four days of talks, tutorials and community, in Bologna.', 'label': 'One line description', 'required': False}), 41: ('wagtail.blocks.CharBlock', (), {'help_text': 'Optional credibility marker, e.g. 1,000+ attendees.', 'label': 'Scale', 'required': False}), 42: ('wagtail.blocks.StructBlock', [[('city', 36), ('variant', 37), ('title', 38), ('subtitle', 39), ('body', 40), ('highlight', 41), ('primary_cta', 24), ('secondary_cta', 25)]], {}), 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)]], {})}),
),
]
12 changes: 12 additions & 0 deletions backend/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -542,13 +542,25 @@ type HomeIntroSection {
type HomepageHero {
id: ID!
city: HomepageHeroCity
variant: HomepageHeroVariant!
title: String!
subtitle: String!
body: String!
highlight: String!
primaryCta: CTA
secondaryCta: CTA
}

enum HomepageHeroCity {
FLORENCE
BOLOGNA
}

enum HomepageHeroVariant {
ILLUSTRATION_ONLY
OVERLAY
}

type InformationSection {
id: ID!
title: String!
Expand Down
Loading
Loading