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
54 changes: 54 additions & 0 deletions bakerydemo/base/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@
from wagtail.blocks import (
CharBlock,
ChoiceBlock,
ListBlock,
PageChooserBlock,
RichTextBlock,
StreamBlock,
StructBlock,
TextBlock,
URLBlock,
)
from wagtail.documents.blocks import DocumentChooserBlock
from wagtail.embeds.blocks import EmbedBlock
from wagtail.images import get_image_model
from wagtail.images.blocks import ImageChooserBlock
Expand Down Expand Up @@ -158,3 +162,53 @@ class BaseStreamBlock(StreamBlock):
preview_value="https://www.youtube.com/watch?v=mwrGSfiB1Mg",
description="An embedded video or other media",
)


# Navigation Blocks
class MenuItemLinkBlock(StreamBlock):
"""
A link that can point to a page, an external URL, or a document.
Uses StreamBlock with max_num=1 so editors pick exactly one link type.
"""

page = PageChooserBlock(label="Page", required=False)
external_url = URLBlock(label="External URL", required=False)
document = DocumentChooserBlock(label="Document", required=False)

class Meta:
icon = "link"
max_num = 1
label = "Link"


class MenuItemBlock(StructBlock):
"""
A single menu item with an optional custom title and a link.
If the title is left blank, the linked page's title will be used.
"""

title = CharBlock(
max_length=255,
required=False,
help_text="Optional. If left blank, the linked page's title will be used.",
)
link = MenuItemLinkBlock(required=True)

class Meta:
icon = "link"
label = "Menu item"


class MenuSectionBlock(StructBlock):
"""
A footer menu section with a heading and a list of menu items.
This creates a column in the footer navigation.
"""

heading = CharBlock(max_length=255)
items = ListBlock(MenuItemBlock())

class Meta:
icon = "list-ul"
label = "Menu section"

39 changes: 39 additions & 0 deletions bakerydemo/base/migrations/0028_footermenu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Generated by Django 6.0.3 on 2026-03-13 09:41

import django.db.models.deletion
import wagtail.fields
import wagtail.models.preview
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('base', '0027_rename_promo_image_homepage_lead_image_and_more'),
('wagtailcore', '0096_referenceindex_referenceindex_source_object_and_more'),
]

operations = [
migrations.CreateModel(
name='FooterMenu',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('live', models.BooleanField(default=True, editable=False, verbose_name='live')),
('has_unpublished_changes', models.BooleanField(default=False, editable=False, verbose_name='has unpublished changes')),
('first_published_at', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='first published at')),
('last_published_at', models.DateTimeField(editable=False, null=True, verbose_name='last published at')),
('go_live_at', models.DateTimeField(blank=True, null=True, verbose_name='go live date/time')),
('expire_at', models.DateTimeField(blank=True, null=True, verbose_name='expiry date/time')),
('expired', models.BooleanField(default=False, editable=False, verbose_name='expired')),
('name', models.CharField(max_length=255)),
('sections', wagtail.fields.StreamField([('section', 8)], blank=True, block_lookup={0: ('wagtail.blocks.CharBlock', (), {'max_length': 255}), 1: ('wagtail.blocks.CharBlock', (), {'help_text': "Optional. If left blank, the linked page's title will be used.", 'max_length': 255, 'required': False}), 2: ('wagtail.blocks.PageChooserBlock', (), {'label': 'Page', 'required': False}), 3: ('wagtail.blocks.URLBlock', (), {'label': 'External URL', 'required': False}), 4: ('wagtail.documents.blocks.DocumentChooserBlock', (), {'label': 'Document', 'required': False}), 5: ('wagtail.blocks.StreamBlock', [[('page', 2), ('external_url', 3), ('document', 4)]], {'required': True}), 6: ('wagtail.blocks.StructBlock', [[('title', 1), ('link', 5)]], {}), 7: ('wagtail.blocks.ListBlock', (6,), {}), 8: ('wagtail.blocks.StructBlock', [[('heading', 0), ('items', 7)]], {})})),
('latest_revision', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailcore.revision', verbose_name='latest revision')),
('live_revision', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailcore.revision', verbose_name='live revision')),
],
options={
'verbose_name': 'footer menu',
'verbose_name_plural': 'footer menus',
},
bases=(wagtail.models.preview.PreviewableMixin, models.Model),
),
]
39 changes: 39 additions & 0 deletions bakerydemo/base/migrations/0029_mainmenu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Generated by Django 6.0.3 on 2026-03-13 13:57

import django.db.models.deletion
import wagtail.fields
import wagtail.models.preview
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('base', '0028_footermenu'),
('wagtailcore', '0096_referenceindex_referenceindex_source_object_and_more'),
]

operations = [
migrations.CreateModel(
name='MainMenu',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('live', models.BooleanField(default=True, editable=False, verbose_name='live')),
('has_unpublished_changes', models.BooleanField(default=False, editable=False, verbose_name='has unpublished changes')),
('first_published_at', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='first published at')),
('last_published_at', models.DateTimeField(editable=False, null=True, verbose_name='last published at')),
('go_live_at', models.DateTimeField(blank=True, null=True, verbose_name='go live date/time')),
('expire_at', models.DateTimeField(blank=True, null=True, verbose_name='expiry date/time')),
('expired', models.BooleanField(default=False, editable=False, verbose_name='expired')),
('name', models.CharField(max_length=255)),
('items', wagtail.fields.StreamField([('item', 5)], blank=True, block_lookup={0: ('wagtail.blocks.CharBlock', (), {'help_text': "Optional. If left blank, the linked page's title will be used.", 'max_length': 255, 'required': False}), 1: ('wagtail.blocks.PageChooserBlock', (), {'label': 'Page', 'required': False}), 2: ('wagtail.blocks.URLBlock', (), {'label': 'External URL', 'required': False}), 3: ('wagtail.documents.blocks.DocumentChooserBlock', (), {'label': 'Document', 'required': False}), 4: ('wagtail.blocks.StreamBlock', [[('page', 1), ('external_url', 2), ('document', 3)]], {'required': True}), 5: ('wagtail.blocks.StructBlock', [[('title', 0), ('link', 4)]], {})})),
('latest_revision', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailcore.revision', verbose_name='latest revision')),
('live_revision', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailcore.revision', verbose_name='live revision')),
],
options={
'verbose_name': 'main menu',
'verbose_name_plural': 'main menus',
},
bases=(wagtail.models.preview.PreviewableMixin, models.Model),
),
]
90 changes: 89 additions & 1 deletion bakerydemo/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
)
from wagtail.search import index

from .blocks import BaseStreamBlock
from .blocks import BaseStreamBlock, MenuItemBlock, MenuSectionBlock

# Allow filtering by collection
Image.api_fields = [APIField("collection")]
Expand Down Expand Up @@ -221,6 +221,94 @@ class Meta(TranslatableMixin.Meta):
verbose_name_plural = "footer text"


class FooterMenu(
DraftStateMixin,
RevisionMixin,
PreviewableMixin,
models.Model,
):
"""
A block-based footer menu managed via StreamField.
Editors can create menu sections, each containing links to pages,
external URLs, or documents. Registered as a snippet in wagtail_hooks.py.
"""

name = models.CharField(max_length=255)
sections = StreamField(
[("section", MenuSectionBlock())],
blank=True,
use_json_field=True,
)

revisions = GenericRelation(
"wagtailcore.Revision",
content_type_field="base_content_type",
object_id_field="object_id",
related_query_name="footer_menu",
for_concrete_model=False,
)

panels = [
FieldPanel("name"),
FieldPanel("sections"),
PublishingPanel(),
]

def __str__(self):
return self.name

def get_preview_template(self, request, mode_name):
return "base/include/footer_menu.html"

def get_preview_context(self, request, mode_name):
return {"footer_menu": self}

class Meta:
verbose_name = "footer menu"
verbose_name_plural = "footer menus"


class MainMenu(
DraftStateMixin,
RevisionMixin,
PreviewableMixin,
models.Model,
):
name = models.CharField(max_length=255)
items = StreamField(
[("item", MenuItemBlock())],
blank=True,
use_json_field=True,
)

revisions = GenericRelation(
"wagtailcore.Revision",
content_type_field="base_content_type",
object_id_field="object_id",
related_query_name="main_menu",
for_concrete_model=False,
)

panels = [
FieldPanel("name"),
FieldPanel("items"),
PublishingPanel(),
]

def __str__(self):
return self.name

def get_preview_template(self, request, mode_name):
return "base/include/main_menu.html"

def get_preview_context(self, request, mode_name):
return {"main_menu": self}

class Meta:
verbose_name = "main menu"
verbose_name_plural = "main menus"


class StandardPage(Page):
"""
A generic content page. On this demo site we use it for an about page but
Expand Down
48 changes: 22 additions & 26 deletions bakerydemo/base/templatetags/navigation_tags.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,30 @@
from django import template
from wagtail.models import Page, Site

from bakerydemo.base.models import FooterText
from bakerydemo.base.models import FooterMenu, FooterText, MainMenu

register = template.Library()
# https://docs.djangoproject.com/en/stable/howto/custom-template-tags/


@register.simple_tag(takes_context=True)
def get_site_root(context):
# This returns a core.Page. The main menu needs to have the site.root_page
# defined else will return an object attribute error ('str' object has no
# attribute 'get_children')
return Site.find_for_request(context["request"]).root_page


def has_children(page):
# Generically allow index pages to list their children
return page.get_children().live().exists()
@register.inclusion_tag("base/include/main_menu.html", takes_context=True)
def get_main_menu(context):
main_menu = context.get("main_menu", None)

if not main_menu:
instance = MainMenu.objects.filter(live=True).first()
main_menu = instance if instance else None

def is_active(page, current_page):
# To give us active state on main navigation
return current_page.url_path.startswith(page.url_path) if current_page else False
calling_page = context.get("self", None)


# Retrieves the top menu items - the immediate children of the parent page
@register.inclusion_tag("tags/top_menu.html", takes_context=True)
def top_menu(context, parent, calling_page=None):
menuitems = parent.get_children().live().in_menu()
for menuitem in menuitems:
# We don't directly check if calling_page is None since the template
# engine can pass an empty string to calling_page
# if the variable passed as calling_page does not exist.
menuitem.active = (
calling_page.url_path.startswith(menuitem.url_path)
if calling_page
else False
)
return {
"main_menu": main_menu,
"calling_page": calling_page,
"menuitems": menuitems,
# required by the pageurl tag that we want to use within this template
"request": context["request"],
}

Expand Down Expand Up @@ -75,3 +58,16 @@ def get_footer_text(context):
return {
"footer_text": footer_text,
}


@register.inclusion_tag("base/include/footer_menu.html", takes_context=True)
def get_footer_menu(context):
footer_menu = context.get("footer_menu", None)

if not footer_menu:
instance = FooterMenu.objects.filter(live=True).first()
footer_menu = instance if instance else None

return {
"footer_menu": footer_menu,
}
Loading