Wagtail - How to add a List of Related Fields to a Page
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.
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
Header Photo by Jake Hills on Unsplash