Ver Fonte

Convert demo StreamField pages to RecipePage data model

Thibaud Colas há 2 anos atrás
pai
commit
f3754842d6

+ 52 - 32
bakerydemo/recipes/blocks.py

@@ -1,54 +1,74 @@
+from django import forms
+from wagtail.contrib.table_block.blocks import TableBlock
 from wagtail.contrib.typed_table_block.blocks import TypedTableBlock
 from wagtail.core.blocks import (
     CharBlock,
     ChoiceBlock,
     FloatBlock,
     ListBlock,
-    PageChooserBlock,
     RichTextBlock,
     StreamBlock,
     StructBlock,
-    TextBlock,
-    URLBlock,
 )
-from wagtail.documents.blocks import DocumentChooserBlock
 from wagtail.embeds.blocks import EmbedBlock
 from wagtail.images.blocks import ImageChooserBlock
 
+from bakerydemo.base.blocks import BlockQuote, HeadingBlock, ImageBlock
 
-class RecipeTableBlock(StructBlock):
-    title = CharBlock()
-    description = TextBlock()
-    table = TypedTableBlock(
+
+class RecipeStepBlock(StructBlock):
+    text = RichTextBlock(features=["bold", "italic", "link"])
+    difficulty = ChoiceBlock(
+        widget=forms.RadioSelect,
+        choices=[("S", "Small"), ("M", "Medium"), ("L", "Large")],
+        default="S",
+    )
+
+    class Meta:
+        template = 'blocks/recipe_step_block.html'
+        icon = 'tick'
+
+
+class RecipeStreamBlock(StreamBlock):
+    """
+    Define the custom blocks that `StreamField` will utilize
+    """
+
+    heading_block = HeadingBlock(group="Content")
+    paragraph_block = RichTextBlock(
+        icon="pilcrow", template="blocks/paragraph_block.html", group="Content"
+    )
+    block_quote = BlockQuote(group="Content")
+    table_block = TableBlock(group="Content")
+    typed_table_block = TypedTableBlock(
         [
             ("text", CharBlock()),
             ("numeric", FloatBlock()),
             ("rich_text", RichTextBlock()),
             ("image", ImageChooserBlock()),
-        ]
+        ],
+        group="Content",
     )
 
+    image_block = ImageBlock(group="Media")
+    embed_block = EmbedBlock(
+        help_text="Insert an embed URL e.g https://www.youtube.com/watch?v=SGJFWirQ3ks",
+        icon="media",
+        template="blocks/embed_block.html",
+        group="Media",
+    )
 
-class RecipeStreamBlock(StreamBlock):
-    page = PageChooserBlock()
-    embed = EmbedBlock()
-    image = ImageChooserBlock()
-
-
-BLOCKS = [
-    ("char", CharBlock()),
-    (
-        "choice",
-        ChoiceBlock(choices=[("M", "Medium"), ("L", "Large"), ("XL", "Extra large")]),
-    ),
-    ("list", ListBlock(child_block=CharBlock())),
-    ("page", PageChooserBlock()),
-    ("text", TextBlock()),
-    ("rich_text", RichTextBlock()),
-    ("url", URLBlock()),
-    ("document", DocumentChooserBlock()),
-    ("embed", EmbedBlock()),
-    ("image", ImageChooserBlock()),
-    ("table", RecipeTableBlock()),
-    ("stream", RecipeStreamBlock()),
-]
+    ingredients_list = ListBlock(
+        RichTextBlock(features=["bold", "italic", "link"]),
+        min_num=2,
+        max_num=10,
+        icon="list-ol",
+        group="Cooking",
+    )
+    steps_list = ListBlock(
+        RecipeStepBlock(),
+        min_num=2,
+        max_num=10,
+        icon="tasks",
+        group="Cooking",
+    )

+ 232 - 274
bakerydemo/recipes/migrations/0001_initial.py

@@ -1,11 +1,11 @@
-# Generated by Django 4.0.8 on 2022-11-17 15:14
+# Generated by Django 4.1.4 on 2022-12-20 14:17
 
 from django.db import migrations, models
 import django.db.models.deletion
 import modelcluster.fields
 import wagtail.blocks
+import wagtail.contrib.table_block.blocks
 import wagtail.contrib.typed_table_block.blocks
-import wagtail.documents.blocks
 import wagtail.embeds.blocks
 import wagtail.fields
 import wagtail.images.blocks
@@ -16,10 +16,35 @@ class Migration(migrations.Migration):
     initial = True
 
     dependencies = [
+        ("base", "0012_person_expire_at_person_expired_and_more"),
         ("wagtailcore", "0078_referenceindex"),
     ]
 
     operations = [
+        migrations.CreateModel(
+            name="RecipeIndexPage",
+            fields=[
+                (
+                    "page_ptr",
+                    models.OneToOneField(
+                        auto_created=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        parent_link=True,
+                        primary_key=True,
+                        serialize=False,
+                        to="wagtailcore.page",
+                    ),
+                ),
+                (
+                    "introduction",
+                    models.TextField(blank=True, help_text="Text to describe the page"),
+                ),
+            ],
+            options={
+                "abstract": False,
+            },
+            bases=("wagtailcore.page",),
+        ),
         migrations.CreateModel(
             name="RecipePage",
             fields=[
@@ -34,368 +59,301 @@ class Migration(migrations.Migration):
                         to="wagtailcore.page",
                     ),
                 ),
-                ("title_1", models.CharField(default="Title 1", max_length=255)),
                 (
-                    "section_1",
+                    "date_published",
+                    models.DateField(
+                        blank=True, null=True, verbose_name="Date article published"
+                    ),
+                ),
+                ("subtitle", models.CharField(blank=True, max_length=255)),
+                ("introduction", models.TextField(blank=True, max_length=500)),
+                (
+                    "backstory",
                     wagtail.fields.StreamField(
                         [
-                            ("char", wagtail.blocks.CharBlock()),
-                            (
-                                "choice",
-                                wagtail.blocks.ChoiceBlock(
-                                    choices=[
-                                        ("M", "Medium"),
-                                        ("L", "Large"),
-                                        ("XL", "Extra large"),
-                                    ]
-                                ),
-                            ),
-                            (
-                                "list",
-                                wagtail.blocks.ListBlock(
-                                    child_block=wagtail.blocks.CharBlock()
-                                ),
-                            ),
-                            ("page", wagtail.blocks.PageChooserBlock()),
-                            ("text", wagtail.blocks.TextBlock()),
-                            ("rich_text", wagtail.blocks.RichTextBlock()),
-                            ("url", wagtail.blocks.URLBlock()),
                             (
-                                "document",
-                                wagtail.documents.blocks.DocumentChooserBlock(),
-                            ),
-                            ("embed", wagtail.embeds.blocks.EmbedBlock()),
-                            ("image", wagtail.images.blocks.ImageChooserBlock()),
-                            (
-                                "table",
+                                "heading_block",
                                 wagtail.blocks.StructBlock(
                                     [
-                                        ("title", wagtail.blocks.CharBlock()),
-                                        ("description", wagtail.blocks.TextBlock()),
                                         (
-                                            "table",
-                                            wagtail.contrib.typed_table_block.blocks.TypedTableBlock(
-                                                [
-                                                    (
-                                                        "text",
-                                                        wagtail.blocks.CharBlock(),
-                                                    ),
-                                                    (
-                                                        "numeric",
-                                                        wagtail.blocks.FloatBlock(),
-                                                    ),
-                                                    (
-                                                        "rich_text",
-                                                        wagtail.blocks.RichTextBlock(),
-                                                    ),
-                                                    (
-                                                        "image",
-                                                        wagtail.images.blocks.ImageChooserBlock(),
-                                                    ),
-                                                ]
+                                            "heading_text",
+                                            wagtail.blocks.CharBlock(
+                                                form_classname="title", required=True
                                             ),
                                         ),
-                                    ]
-                                ),
-                            ),
-                            (
-                                "stream",
-                                wagtail.blocks.StreamBlock(
-                                    [
-                                        ("page", wagtail.blocks.PageChooserBlock()),
-                                        ("embed", wagtail.embeds.blocks.EmbedBlock()),
                                         (
-                                            "image",
-                                            wagtail.images.blocks.ImageChooserBlock(),
+                                            "size",
+                                            wagtail.blocks.ChoiceBlock(
+                                                blank=True,
+                                                choices=[
+                                                    ("", "Select a header size"),
+                                                    ("h2", "H2"),
+                                                    ("h3", "H3"),
+                                                    ("h4", "H4"),
+                                                ],
+                                                required=False,
+                                            ),
                                         ),
                                     ]
                                 ),
                             ),
-                        ],
-                        blank=True,
-                        help_text="Section 1 is a StreamField in a regular FieldPanel",
-                        use_json_field=True,
-                    ),
-                ),
-                ("title_2", models.CharField(default="Title 2", max_length=255)),
-                (
-                    "section_2",
-                    wagtail.fields.StreamField(
-                        [
-                            ("char", wagtail.blocks.CharBlock()),
                             (
-                                "choice",
-                                wagtail.blocks.ChoiceBlock(
-                                    choices=[
-                                        ("M", "Medium"),
-                                        ("L", "Large"),
-                                        ("XL", "Extra large"),
-                                    ]
+                                "paragraph_block",
+                                wagtail.blocks.RichTextBlock(
+                                    icon="pilcrow",
+                                    template="blocks/paragraph_block.html",
                                 ),
                             ),
                             (
-                                "list",
-                                wagtail.blocks.ListBlock(
-                                    child_block=wagtail.blocks.CharBlock()
-                                ),
-                            ),
-                            ("page", wagtail.blocks.PageChooserBlock()),
-                            ("text", wagtail.blocks.TextBlock()),
-                            ("rich_text", wagtail.blocks.RichTextBlock()),
-                            ("url", wagtail.blocks.URLBlock()),
-                            (
-                                "document",
-                                wagtail.documents.blocks.DocumentChooserBlock(),
-                            ),
-                            ("embed", wagtail.embeds.blocks.EmbedBlock()),
-                            ("image", wagtail.images.blocks.ImageChooserBlock()),
-                            (
-                                "table",
+                                "image_block",
                                 wagtail.blocks.StructBlock(
                                     [
-                                        ("title", wagtail.blocks.CharBlock()),
-                                        ("description", wagtail.blocks.TextBlock()),
                                         (
-                                            "table",
-                                            wagtail.contrib.typed_table_block.blocks.TypedTableBlock(
-                                                [
-                                                    (
-                                                        "text",
-                                                        wagtail.blocks.CharBlock(),
-                                                    ),
-                                                    (
-                                                        "numeric",
-                                                        wagtail.blocks.FloatBlock(),
-                                                    ),
-                                                    (
-                                                        "rich_text",
-                                                        wagtail.blocks.RichTextBlock(),
-                                                    ),
-                                                    (
-                                                        "image",
-                                                        wagtail.images.blocks.ImageChooserBlock(),
-                                                    ),
-                                                ]
+                                            "image",
+                                            wagtail.images.blocks.ImageChooserBlock(
+                                                required=True
                                             ),
                                         ),
+                                        (
+                                            "caption",
+                                            wagtail.blocks.CharBlock(required=False),
+                                        ),
+                                        (
+                                            "attribution",
+                                            wagtail.blocks.CharBlock(required=False),
+                                        ),
                                     ]
                                 ),
                             ),
                             (
-                                "stream",
-                                wagtail.blocks.StreamBlock(
+                                "block_quote",
+                                wagtail.blocks.StructBlock(
                                     [
-                                        ("page", wagtail.blocks.PageChooserBlock()),
-                                        ("embed", wagtail.embeds.blocks.EmbedBlock()),
+                                        ("text", wagtail.blocks.TextBlock()),
                                         (
-                                            "image",
-                                            wagtail.images.blocks.ImageChooserBlock(),
+                                            "attribute_name",
+                                            wagtail.blocks.CharBlock(
+                                                blank=True,
+                                                label="e.g. Mary Berry",
+                                                required=False,
+                                            ),
                                         ),
                                     ]
                                 ),
                             ),
+                            (
+                                "embed_block",
+                                wagtail.embeds.blocks.EmbedBlock(
+                                    help_text="Insert an embed URL e.g https://www.youtube.com/watch?v=SGJFWirQ3ks",
+                                    icon="media",
+                                    template="blocks/embed_block.html",
+                                ),
+                            ),
                         ],
                         blank=True,
-                        help_text="Section 2 is a StreamField in a MultiFieldPanel",
+                        help_text="Use only a minimum number of headings and large blocks.",
                         use_json_field=True,
                     ),
                 ),
-            ],
-            options={
-                "abstract": False,
-            },
-            bases=("wagtailcore.page",),
-        ),
-        migrations.CreateModel(
-            name="Item",
-            fields=[
                 (
-                    "id",
-                    models.AutoField(
-                        auto_created=True,
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name="ID",
+                    "recipe_headline",
+                    wagtail.fields.RichTextField(
+                        blank=True, help_text="Keep to a single line", max_length=120
                     ),
                 ),
                 (
-                    "sort_order",
-                    models.IntegerField(blank=True, editable=False, null=True),
-                ),
-                ("title_1", models.CharField(default="Title 1", max_length=255)),
-                (
-                    "section_1",
+                    "body",
                     wagtail.fields.StreamField(
                         [
-                            ("char", wagtail.blocks.CharBlock()),
                             (
-                                "choice",
-                                wagtail.blocks.ChoiceBlock(
-                                    choices=[
-                                        ("M", "Medium"),
-                                        ("L", "Large"),
-                                        ("XL", "Extra large"),
-                                    ]
+                                "heading_block",
+                                wagtail.blocks.StructBlock(
+                                    [
+                                        (
+                                            "heading_text",
+                                            wagtail.blocks.CharBlock(
+                                                form_classname="title", required=True
+                                            ),
+                                        ),
+                                        (
+                                            "size",
+                                            wagtail.blocks.ChoiceBlock(
+                                                blank=True,
+                                                choices=[
+                                                    ("", "Select a header size"),
+                                                    ("h2", "H2"),
+                                                    ("h3", "H3"),
+                                                    ("h4", "H4"),
+                                                ],
+                                                required=False,
+                                            ),
+                                        ),
+                                    ],
+                                    group="Content",
                                 ),
                             ),
                             (
-                                "list",
-                                wagtail.blocks.ListBlock(
-                                    child_block=wagtail.blocks.CharBlock()
+                                "paragraph_block",
+                                wagtail.blocks.RichTextBlock(
+                                    group="Content",
+                                    icon="pilcrow",
+                                    template="blocks/paragraph_block.html",
                                 ),
                             ),
-                            ("page", wagtail.blocks.PageChooserBlock()),
-                            ("text", wagtail.blocks.TextBlock()),
-                            ("rich_text", wagtail.blocks.RichTextBlock()),
-                            ("url", wagtail.blocks.URLBlock()),
                             (
-                                "document",
-                                wagtail.documents.blocks.DocumentChooserBlock(),
-                            ),
-                            ("embed", wagtail.embeds.blocks.EmbedBlock()),
-                            ("image", wagtail.images.blocks.ImageChooserBlock()),
-                            (
-                                "table",
+                                "block_quote",
                                 wagtail.blocks.StructBlock(
                                     [
-                                        ("title", wagtail.blocks.CharBlock()),
-                                        ("description", wagtail.blocks.TextBlock()),
+                                        ("text", wagtail.blocks.TextBlock()),
                                         (
-                                            "table",
-                                            wagtail.contrib.typed_table_block.blocks.TypedTableBlock(
-                                                [
-                                                    (
-                                                        "text",
-                                                        wagtail.blocks.CharBlock(),
-                                                    ),
-                                                    (
-                                                        "numeric",
-                                                        wagtail.blocks.FloatBlock(),
-                                                    ),
-                                                    (
-                                                        "rich_text",
-                                                        wagtail.blocks.RichTextBlock(),
-                                                    ),
-                                                    (
-                                                        "image",
-                                                        wagtail.images.blocks.ImageChooserBlock(),
-                                                    ),
-                                                ]
+                                            "attribute_name",
+                                            wagtail.blocks.CharBlock(
+                                                blank=True,
+                                                label="e.g. Mary Berry",
+                                                required=False,
                                             ),
                                         ),
-                                    ]
+                                    ],
+                                    group="Content",
                                 ),
                             ),
                             (
-                                "stream",
-                                wagtail.blocks.StreamBlock(
+                                "table_block",
+                                wagtail.contrib.table_block.blocks.TableBlock(
+                                    group="Content"
+                                ),
+                            ),
+                            (
+                                "typed_table_block",
+                                wagtail.contrib.typed_table_block.blocks.TypedTableBlock(
                                     [
-                                        ("page", wagtail.blocks.PageChooserBlock()),
-                                        ("embed", wagtail.embeds.blocks.EmbedBlock()),
+                                        ("text", wagtail.blocks.CharBlock()),
+                                        ("numeric", wagtail.blocks.FloatBlock()),
+                                        ("rich_text", wagtail.blocks.RichTextBlock()),
                                         (
                                             "image",
                                             wagtail.images.blocks.ImageChooserBlock(),
                                         ),
-                                    ]
+                                    ],
+                                    group="Content",
                                 ),
                             ),
-                        ],
-                        blank=True,
-                        help_text="Section 1 is a StreamField in a regular FieldPanel",
-                        use_json_field=True,
-                    ),
-                ),
-                ("title_2", models.CharField(default="Title 2", max_length=255)),
-                (
-                    "section_2",
-                    wagtail.fields.StreamField(
-                        [
-                            ("char", wagtail.blocks.CharBlock()),
                             (
-                                "choice",
-                                wagtail.blocks.ChoiceBlock(
-                                    choices=[
-                                        ("M", "Medium"),
-                                        ("L", "Large"),
-                                        ("XL", "Extra large"),
-                                    ]
+                                "image_block",
+                                wagtail.blocks.StructBlock(
+                                    [
+                                        (
+                                            "image",
+                                            wagtail.images.blocks.ImageChooserBlock(
+                                                required=True
+                                            ),
+                                        ),
+                                        (
+                                            "caption",
+                                            wagtail.blocks.CharBlock(required=False),
+                                        ),
+                                        (
+                                            "attribution",
+                                            wagtail.blocks.CharBlock(required=False),
+                                        ),
+                                    ],
+                                    group="Media",
                                 ),
                             ),
                             (
-                                "list",
-                                wagtail.blocks.ListBlock(
-                                    child_block=wagtail.blocks.CharBlock()
+                                "embed_block",
+                                wagtail.embeds.blocks.EmbedBlock(
+                                    group="Media",
+                                    help_text="Insert an embed URL e.g https://www.youtube.com/watch?v=SGJFWirQ3ks",
+                                    icon="media",
+                                    template="blocks/embed_block.html",
                                 ),
                             ),
-                            ("page", wagtail.blocks.PageChooserBlock()),
-                            ("text", wagtail.blocks.TextBlock()),
-                            ("rich_text", wagtail.blocks.RichTextBlock()),
-                            ("url", wagtail.blocks.URLBlock()),
-                            (
-                                "document",
-                                wagtail.documents.blocks.DocumentChooserBlock(),
-                            ),
-                            ("embed", wagtail.embeds.blocks.EmbedBlock()),
-                            ("image", wagtail.images.blocks.ImageChooserBlock()),
                             (
-                                "table",
-                                wagtail.blocks.StructBlock(
-                                    [
-                                        ("title", wagtail.blocks.CharBlock()),
-                                        ("description", wagtail.blocks.TextBlock()),
-                                        (
-                                            "table",
-                                            wagtail.contrib.typed_table_block.blocks.TypedTableBlock(
-                                                [
-                                                    (
-                                                        "text",
-                                                        wagtail.blocks.CharBlock(),
-                                                    ),
-                                                    (
-                                                        "numeric",
-                                                        wagtail.blocks.FloatBlock(),
-                                                    ),
-                                                    (
-                                                        "rich_text",
-                                                        wagtail.blocks.RichTextBlock(),
-                                                    ),
-                                                    (
-                                                        "image",
-                                                        wagtail.images.blocks.ImageChooserBlock(),
-                                                    ),
-                                                ]
-                                            ),
-                                        ),
-                                    ]
+                                "ingredients_list",
+                                wagtail.blocks.ListBlock(
+                                    wagtail.blocks.RichTextBlock(
+                                        features=["bold", "italic", "link"]
+                                    ),
+                                    group="Cooking",
+                                    icon="list-ol",
+                                    max_num=10,
+                                    min_num=2,
                                 ),
                             ),
                             (
-                                "stream",
-                                wagtail.blocks.StreamBlock(
-                                    [
-                                        ("page", wagtail.blocks.PageChooserBlock()),
-                                        ("embed", wagtail.embeds.blocks.EmbedBlock()),
-                                        (
-                                            "image",
-                                            wagtail.images.blocks.ImageChooserBlock(),
-                                        ),
-                                    ]
+                                "steps_list",
+                                wagtail.blocks.ListBlock(
+                                    wagtail.blocks.StructBlock(
+                                        [
+                                            (
+                                                "text",
+                                                wagtail.blocks.RichTextBlock(
+                                                    features=["bold", "italic", "link"]
+                                                ),
+                                            ),
+                                            (
+                                                "difficulty",
+                                                wagtail.blocks.ChoiceBlock(
+                                                    choices=[
+                                                        ("S", "Small"),
+                                                        ("M", "Medium"),
+                                                        ("L", "Large"),
+                                                    ]
+                                                ),
+                                            ),
+                                        ]
+                                    ),
+                                    group="Cooking",
+                                    icon="tasks",
+                                    max_num=10,
+                                    min_num=2,
                                 ),
                             ),
                         ],
                         blank=True,
-                        help_text="Section 2 is a StreamField in a MultiFieldPanel",
+                        help_text="The recipe’s step-by-step instructions and any other relevant information.",
                         use_json_field=True,
                     ),
                 ),
+            ],
+            options={
+                "abstract": False,
+            },
+            bases=("wagtailcore.page",),
+        ),
+        migrations.CreateModel(
+            name="RecipePersonRelationship",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        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="items",
+                        related_name="recipe_person_relationship",
                         to="recipes.recipepage",
                     ),
                 ),
+                (
+                    "person",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="person_recipe_relationship",
+                        to="base.person",
+                    ),
+                ),
             ],
             options={
                 "ordering": ["sort_order"],

+ 123 - 44
bakerydemo/recipes/models.py

@@ -1,73 +1,152 @@
 from django.db import models
 from modelcluster.fields import ParentalKey
-from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel
-from wagtail.fields import StreamField
+from wagtail.admin.panels import FieldPanel, HelpPanel, InlinePanel, MultiFieldPanel
+from wagtail.fields import RichTextField, StreamField
 from wagtail.models import Orderable, Page
+from wagtail.search import index
 
-from .blocks import BLOCKS
+from bakerydemo.base.blocks import BaseStreamBlock
+from bakerydemo.base.models import Person
+
+from .blocks import RecipeStreamBlock
+
+
+class RecipePersonRelationship(Orderable, models.Model):
+    """
+    This defines the relationship between the `Person` within the `base`
+    app and the RecipePage below. This allows people to be added to a RecipePage.
+
+    We have created a two way relationship between RecipePage and Person using
+    the ParentalKey and ForeignKey
+    """
+
+    page = ParentalKey(
+        "RecipePage",
+        related_name="recipe_person_relationship",
+        on_delete=models.CASCADE,
+    )
+    person = models.ForeignKey(
+        "base.Person",
+        related_name="person_recipe_relationship",
+        on_delete=models.CASCADE,
+    )
+    panels = [FieldPanel("person")]
 
 
 class RecipePage(Page):
     """
-    RecipePage, to illustrate some Recipe Wagtail scenario's
+    Recipe pages are more complex than blog pages, demonstrating more advanced StreamField patterns.
     """
 
-    title_1 = models.CharField(max_length=255, default="Title 1")
-    section_1 = StreamField(
-        BLOCKS,
+    date_published = models.DateField("Date article published", blank=True, null=True)
+    subtitle = models.CharField(blank=True, max_length=255)
+    introduction = models.TextField(blank=True, max_length=500)
+    backstory = StreamField(
+        BaseStreamBlock(),
+        # Demonstrate block_counts to keep the backstory concise.
+        block_counts={
+            "heading_block": {"max_num": 1},
+            "image_block": {"max_num": 1},
+            "embed_block": {"max_num": 1},
+        },
         blank=True,
         use_json_field=True,
-        help_text="Section 1 is a StreamField in a regular FieldPanel",
+        help_text="Use only a minimum number of headings and large blocks.",
     )
-    title_2 = models.CharField(max_length=255, default="Title 2")
-    section_2 = StreamField(
-        BLOCKS,
+
+    # An example of using rich text for single-line content.
+    recipe_headline = RichTextField(
+        blank=True,
+        max_length=120,
+        features=["bold", "italic", "link"],
+        help_text="Keep to a single line",
+    )
+    body = StreamField(
+        RecipeStreamBlock(),
         blank=True,
         use_json_field=True,
-        help_text="Section 2 is a StreamField in a MultiFieldPanel",
+        help_text="The recipe’s step-by-step instructions and any other relevant information.",
     )
 
     content_panels = Page.content_panels + [
-        FieldPanel("title_1"),
-        FieldPanel("section_1"),
+        FieldPanel("date_published"),
+        # Using `title` to make a field larger.
+        FieldPanel("subtitle", classname="title"),
         MultiFieldPanel(
             [
-                FieldPanel("title_2"),
-                FieldPanel("section_2"),
+                # Example use case for HelpPanel.
+                HelpPanel(
+                    "Refer to keywords analysis and correct international ingredients names to craft the best introduction backstory, and headline."
+                ),
+                FieldPanel("introduction"),
+                # StreamField inside a MultiFieldPanel.
+                FieldPanel("backstory"),
+                FieldPanel("recipe_headline"),
             ],
-            heading="MultiFieldPanel for section 2",
+            heading="Preface",
         ),
+        FieldPanel("body"),
         InlinePanel(
-            "items", label="Items", help_text="Related items via an inline panel"
+            "recipe_person_relationship",
+            heading="Authors",
+            label="Author",
+            help_text="Select between one and three authors",
+            panels=None,
+            min_num=1,
+            max_num=3,
         ),
     ]
 
+    search_fields = Page.search_fields + [
+        index.SearchField("backstory"),
+        index.SearchField("body"),
+    ]
 
-class Item(Orderable):
-    page = ParentalKey(RecipePage, on_delete=models.CASCADE, related_name="items")
-    title_1 = models.CharField(max_length=255, default="Title 1")
-    section_1 = StreamField(
-        BLOCKS,
-        blank=True,
-        use_json_field=True,
-        help_text="Section 1 is a StreamField in a regular FieldPanel",
-    )
-    title_2 = models.CharField(max_length=255, default="Title 2")
-    section_2 = StreamField(
-        BLOCKS,
-        blank=True,
-        use_json_field=True,
-        help_text="Section 2 is a StreamField in a MultiFieldPanel",
-    )
+    def authors(self):
+        """
+        Returns the BlogPage's related people. Again note that we are using
+        the ParentalKey's related_name from the BlogPersonRelationship model
+        to access these objects. This allows us to access the Person objects
+        with a loop on the template. If we tried to access the blog_person_
+        relationship directly we'd print `blog.BlogPersonRelationship.None`
+        """
+        return Person.objects.filter(live=True, person_recipe_relationship__page=self)
 
-    panels = [
-        FieldPanel("title_1"),
-        FieldPanel("section_1"),
-        MultiFieldPanel(
-            [
-                FieldPanel("title_2"),
-                FieldPanel("section_2"),
-            ],
-            heading="MultiFieldPanel for section 2",
-        ),
+    # Specifies parent to Recipe as being RecipeIndexPages
+    parent_page_types = ["RecipeIndexPage"]
+
+    # Specifies what content types can exist as children of RecipePage.
+    # Empty list means that no child content types are allowed.
+    subpage_types = []
+
+
+class RecipeIndexPage(Page):
+    """
+    Index page for recipe.
+    We need to alter the page model's context to return the child page objects,
+    the RecipePage objects, so that it works as an index page
+    """
+
+    introduction = models.TextField(help_text="Text to describe the page", blank=True)
+
+    content_panels = Page.content_panels + [
+        FieldPanel("introduction"),
     ]
+
+    # Specifies that only RecipePage objects can live under this index page
+    subpage_types = ["RecipePage"]
+
+    # Defines a method to access the children of the page (e.g. RecipePage
+    # objects).
+    def children(self):
+        return self.get_children().specific().live()
+
+    # Overrides the context to list all child items, that are live, by the
+    # date that they were published
+    # https://docs.wagtail.org/en/stable/getting_started/tutorial.html#overriding-context
+    def get_context(self, request):
+        context = super(RecipeIndexPage, self).get_context(request)
+        context["recipes"] = (
+            RecipePage.objects.descendant_of(self).live().order_by("-date_published")
+        )
+        return context

+ 1 - 0
bakerydemo/settings/base.py

@@ -54,6 +54,7 @@ INSTALLED_APPS = [
     "wagtail.admin",
     "wagtail.api.v2",
     "wagtail.locales",
+    "wagtail.contrib.table_block",
     "wagtail.contrib.typed_table_block",
     "wagtail.contrib.modeladmin",
     "wagtail.contrib.routable_page",

+ 11 - 0
bakerydemo/static/css/main.css

@@ -404,6 +404,17 @@ blockquote .attribute-name {
   height: 100%;
 }
 
+/* stylelint-disable-next-line selector-class-pattern */
+:is(.block-table_block, .block-typed_table_block) :is(table, tr, td, th) {
+  border: 1px solid currentColor;
+  padding: 0.25em;
+}
+
+/* stylelint-disable-next-line selector-class-pattern */
+:is(.block-table_block, .block-typed_table_block) table {
+  margin-bottom: 30px;
+}
+
 @media screen and (min-width: 768px) {
   .header,
   .footer {

+ 1 - 0
bakerydemo/templates/blocks/recipe_step_block.html

@@ -0,0 +1 @@
+{{ self.text }}

+ 18 - 0
bakerydemo/templates/recipes/recipe_index_page.html

@@ -0,0 +1,18 @@
+{% extends "base.html" %}
+{% load wagtailcore_tags navigation_tags wagtailimages_tags %}
+
+{% block content %}
+    <div class="container">
+        <div class="blog-list">
+            {% if recipes %}
+                {% for recipe in recipes %}
+                    {% include "includes/card/blog-listing-card.html" with blog=recipe %}
+                {% endfor %}
+            {% else %}
+                <div class="col-md-12">
+                    <p>Oh, snap. Looks like we were too busy baking to write any recipes. Sorry.</p>
+                </div>
+            {% endif %}
+        </div>
+    </div>
+{% endblock content %}

+ 40 - 0
bakerydemo/templates/recipes/recipe_page.html

@@ -0,0 +1,40 @@
+{% extends "base.html" %}
+{% load navigation_tags wagtailimages_tags wagtailcore_tags %}
+
+{% block content %}
+
+    {% image self.image fill-1920x600 as hero_img %}
+    {% include "base/include/header-blog.html" %}
+
+    <div class="container">
+        <div class="row">
+            <div class="col-md-8">
+                <div class="blog__meta">
+                    {% if page.authors %}
+                        <div class="blog__avatars">
+                            {% for author in page.authors %}
+                                <div class="blog__author">{% image author.image fill-50x50-c100 class="blog__avatar" %}
+                                    {{ author.first_name }} {{ author.last_name }}</div>
+                            {% endfor %}
+                        </div>
+                    {% endif %}
+                </div>
+
+                {% if page.backstory %}
+                    {{ page.backstory }}
+
+                    <hr>
+                {% endif %}
+
+                {# Give a label to the recipe section for screen reader users. #}
+                <div id="recipe-headline" class="sr-only">
+                    Recipe{% if page.recipe_headline %}: {{ page.recipe_headline|richtext }}{% endif %}
+                </div>
+
+                <section aria-labelledby="recipe-headline">
+                    {{ page.body }}
+                </section>
+            </div>
+        </div>
+    </div>
+{% endblock content %}

+ 0 - 14
bakerydemo/templates/specials/recipe_page.html

@@ -1,14 +0,0 @@
-{% extends "base.html" %}
-
-{% block content %}
-    {% include "base/include/header-hero.html" %}
-
-    <div class="container bread-detail">
-        <div class="row">
-            <div class="col-md-12">
-                <p>Recipe page is about admin capabilities and does not render content.</p>
-            </div>
-        </div>
-    </div>
-
-{% endblock content %}