Skip to main content
The breads app showcases bread products with support for ingredients, countries of origin, bread types, and pagination.

BreadPage

Detail page for a specific bread product.

Model Definition

bakerydemo/breads/models.py
class BreadPage(Page):
    """
    Detail view for a specific bread
    """
    introduction = models.TextField(help_text="Text to describe the page", blank=True)
    image = models.ForeignKey(
        "wagtailimages.Image",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
        help_text="Landscape mode only; horizontal width between 1000px and 3000px.",
    )
    body = StreamField(
        BaseStreamBlock(), verbose_name="Page body", blank=True, use_json_field=True
    )
    origin = models.ForeignKey(
        Country,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )
    bread_type = models.ForeignKey(
        "breads.BreadType",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    ingredients = ParentalManyToManyField("BreadIngredient", blank=True)

Fields

introduction
TextField
Introductory text describing the bread. Optional.
image
ForeignKey
Hero image for the bread. Should be landscape mode with horizontal width between 1000px and 3000px.
body
StreamField
Main content using BaseStreamBlock. Supports rich content blocks.
origin
ForeignKey
Country of origin for the bread. Links to the Country snippet.
bread_type
ForeignKey
Type classification (e.g., sourdough, whole wheat). Links to BreadType snippet.
ingredients
ParentalManyToManyField
Multiple ingredients can be selected. Uses BreadIngredient snippet with checkbox selection.

Helper Methods

The ordered_ingredients property returns ingredients sorted by their sort order:
@property
def ordered_ingredients(self):
    """Return ingredients ordered by sort_order, then name."""
    return self.ingredients.order_by("sort_order", "name")

Admin Configuration

content_panels = Page.content_panels + [
    FieldPanel("introduction"),
    FieldPanel("image"),
    FieldPanel("body"),
    FieldPanel("origin"),
    FieldPanel("bread_type"),
    MultiFieldPanel(
        [
            FieldPanel(
                "ingredients",
                widget=forms.CheckboxSelectMultiple,
            ),
        ],
        heading="Additional Metadata",
        classname="collapsed",
    ),
]
Ingredients are displayed as checkboxes within a collapsible panel.

Page Hierarchy

Parent: BreadPage can only be created under BreadsIndexPage.

BreadsIndexPage

Index page listing all breads with pagination support.

Model Definition

bakerydemo/breads/models.py
class BreadsIndexPage(Page):
    """
    Index page for breads.
    
    This is more complex than other index pages on the bakery demo site as we've
    included pagination. We've separated the different aspects of the index page
    to be discrete functions to make it easier to follow
    """
    introduction = models.TextField(help_text="Text to describe the page", blank=True)
    image = models.ForeignKey(
        "wagtailimages.Image",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
        help_text="Landscape mode only; horizontal width between 1000px and 3000px.",
    )

Pagination

The index page includes built-in pagination displaying 12 breads per page:
def paginate(self, request, *args):
    page = request.GET.get("page")
    paginator = Paginator(self.get_breads(), 12)
    try:
        pages = paginator.page(page)
    except PageNotAnInteger:
        pages = paginator.page(1)
    except EmptyPage:
        pages = paginator.page(paginator.num_pages)
    return pages

Helper Methods

get_breads()

Returns all live bread pages ordered by publication date.
def get_breads(self):
    return (
        BreadPage.objects
        .live()
        .descendant_of(self)
        .order_by("-first_published_at")
    )

children()

Returns live child pages with their specific types.
def children(self):
    return self.get_children().specific().live()

get_context()

Provides paginated breads to the template.
def get_context(self, request):
    context = super().get_context(request)
    breads = self.paginate(request, self.get_breads())
    context["breads"] = breads
    return context

Page Hierarchy

Children: BreadsIndexPage can only have BreadPage children.

Country Snippet

Stores countries of origin for bread products.

Model Definition

bakerydemo/breads/models.py
class Country(models.Model):
    """
    A Django model to store set of countries of origin.
    It is made accessible in the Wagtail admin interface through the 
    CountrySnippetViewSet class in wagtail_hooks.py.
    """
    title = models.CharField(max_length=100)
    sort_order = models.IntegerField(null=True, blank=True, db_index=True)
    
    class Meta:
        verbose_name = "country of origin"
        verbose_name_plural = "countries of origin"
Country uses a simple model (not ClusterableModel) and is registered as a snippet through CountrySnippetViewSet in wagtail_hooks.py.

BreadIngredient Snippet

Represents individual ingredients with revision and draft support.

Model Definition

bakerydemo/breads/models.py
class BreadIngredient(Orderable, DraftStateMixin, RevisionMixin, models.Model):
    """
    A Django model to store a single ingredient.
    It uses DraftStateMixin and RevisionMixin to support drafts and revisions.
    """
    name = models.CharField(max_length=255)
    
    revisions = GenericRelation(
        "wagtailcore.Revision",
        content_type_field="base_content_type",
        object_id_field="object_id",
        related_query_name="bread_ingredient",
        for_concrete_model=False,
    )
    
    panels = [
        FieldPanel("name"),
    ]
    
    class Meta:
        verbose_name = "bread ingredient"
        verbose_name_plural = "bread ingredients"
        ordering = ["sort_order", "name"]

Features

Draft Support

DraftStateMixin enables draft/live states for ingredients.

Revision History

RevisionMixin tracks changes over time with full revision history.

Ordering

Orderable allows manual drag-and-drop ordering in the admin.
The GenericRelation for revisions is required when using RevisionMixin on a non-Page model.

BreadType Snippet

Defines types of bread (e.g., sourdough, rye, whole wheat).

Model Definition

bakerydemo/breads/models.py
class BreadType(RevisionMixin, models.Model):
    """
    A Django model to define the bread type
    It is made accessible in the Wagtail admin interface through the 
    BreadTypeSnippetViewSet class in wagtail_hooks.py.
    """
    title = models.CharField(max_length=255)
    
    revisions = GenericRelation(
        "wagtailcore.Revision",
        content_type_field="base_content_type",
        object_id_field="object_id",
        related_query_name="bread_type",
        for_concrete_model=False,
    )
    
    panels = [
        FieldPanel("title"),
    ]
    
    class Meta:
        verbose_name = "bread type"
        verbose_name_plural = "bread types"
BreadType includes RevisionMixin for tracking changes but does not have draft/live states.

Snippet Registration

All bread-related snippets are registered in wagtail_hooks.py using SnippetViewSet:
  • CountrySnippetViewSet - Manages countries of origin
  • BreadIngredientSnippetViewSet - Manages ingredients with revision support
  • BreadTypeSnippetViewSet - Manages bread type classifications
This provides a customized admin interface for each snippet type.

Usage Example

  1. Create ingredient snippets (e.g., “Flour”, “Water”, “Yeast”)
  2. Create bread type snippets (e.g., “Sourdough”, “Rye”)
  3. Create country snippets (e.g., “France”, “Germany”)
  4. Create a BreadsIndexPage
  5. Add BreadPage instances with selected ingredients, types, and origins
  6. Pages automatically appear in the paginated index

Build docs developers (and LLMs) love