Skip to content

Wagtail - How to add a List of Related Fields to a Page


header
The Requirement


I recently had a client who wanted the ability to display multiple videos about their products and have them in a scrollable carousel.  As I've learned over the years, a client's initial request is not actually what they want.  So I dug a bit deeper and discoverened that they actually want the ability to show multiple videos and some written content telling the user what the video is about.  After learning this I felt I had the fully story.  I had previously created a product page in Wagtail that allowed for a single video element and other fields that described various attributes of the product. 

The Problem

Now I needed to modify this page to allow for multiple videos and descriptions for each product.  This should be simple right?  I (incorrectly) assumed I could add a list of objects to the page.  Just create a list of fields?  Hmm.  I searched the Wagtail documents and the Django documents and was unable to find the elusive ListField.  In fact, I don't believe one exists.  (Feel free to comment if that is incorrect)  I truly believe that is something can be imagined then it can be built, so I knew there must be a solution out there and I had to figure it out.

The Solution

After spending a bit of time trying to understand how this was going to be possible I came across StreamFields.  Everything I was reading was pointing me back to StreamFields as the solution but I was having a really hard time finding any real information about how to implement StreamFields with a RichTextField and a URLField (or any set of multiple fields).  Finally I came across the StructBlock.


Structural block types
In addition to the basic block types above, it is possible to define new 
block types made up of sub-blocks: for example, a ‘person’ block consisting 
of sub-blocks for first name, surname and image, or a ‘carousel’ block 
consisting of an unlimited number of image blocks. These structures can 
be nested to any depth, making it possible to have a structure containing 
a list, or a list of structures.

https://docs.wagtail.io/en/stable/topics/streamfield.html?highlight=StructBlock#structural-block-types

The example (PersonBlock) that is provided made this clear that I had found my way.  This shows a class that contains multiple related fields, which is eactly what I was looking for to solve my client's problem.


class PersonBlock(blocks.StructBlock):
    first_name = blocks.CharBlock()
    surname = blocks.CharBlock()
    photo = ImageChooserBlock(required=False)
    biography = blocks.RichTextBlock()

    class Meta:
        icon = 'user'

And then to use this on a Wagtail page the below example is shown.  OK, that's not exactly what I was going for, but I believe it will get me there.  The StreamField below shows many blocks within the StreamField, but I am looking to only add a single StructBlock.


body = StreamField([
    ('heading', blocks.CharBlock(form_classname="full title")),
    ('paragraph', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
    ('person', PersonBlock()),
])

The Code

So my actual solution was to create a StructBlock class that conatins a URLBlock and a RichTextBlock and then embed that into a StreamField on my page. 

For the StructBlock class I created a new class called DescAndUrlBlock.  First, in this class I add a RichTextBlock called description, which I set required equal to True and added help text of 'Add your description'.  Then I added the URLBlock called link, this is also set to requried equal True and has a help text of 'Add video URL'.  Below this I add a meta class which contains the icon and a label.  If you want to see all of the available icons in Wagtail, add 'wagtail.contrib.styleguide' to your settings.  Then in your admin page you can navigate to Settings > Styleguide and click on Icons to view all of the available choices.

Below is the code - 


from wagtail.core import blocks

class DescAndUrlBlock(blocks.StructBlock):
    """Description and URL"""

    description = blocks.RichTextBlock(required=True, help_text="Add your description")
    link = blocks.URLBlock(required=True, help_text="Add video URL")

    class Meta:
        icon = "snippet"
        label = "Add a Video link and Description"

Next I update my page, I imported StreamFieldPanel and StreamField from Wagtail, and imported the new DescAndUrlBlock that I had just created.  In the page class I add the StreamField which takes in a list of tuples.  Each tuple requires a desciption and a block (remember, I added a StrucBlock, which allows for more than one block).  I also set null equal to False and blank equal to False.  I created a meta class to give my page a name and plural name.  Then I add my content element to the content_panels, here I use the StreamFieldPanel and pass in the content field.  With that I have everything in place.


from wagtail.core.models import Page
from wagtail.admin.edit_handlers import StreamFieldPanel
from wagtail.core.fields import StreamField

from .blocks import DescAndUrlBlock

class VideoPage(Page):
    content = StreamField([
        ('desc_and_url', DescAndUrlBlock())
    ], null=False, blank=False)

    class Meta:
        verbose_name = "Video Page"
        verbose_name_plural = "Videos Pages"

    content_panels = Page.content_panels + [
        StreamFieldPanel('content')
    ]

The Result

youtube

Header Photo by Jake Hills on Unsplash