Josh Munn's Website

Working with wagtail-factories block factories

Wagtail’s killer feature is the stream field system for flexible content. In this tutorial we will learn how to create and use factory classes that enable us to generate content for stream field blocks, just like we would with factories for Django models.

We assume a working knowledge of Wagtail and a passing knowledge of factory boy. This tutorial also assumes you’ve read the getting started tutorial, and have a Wagtail project with the structures, models, and factories as defined there.


Note: this tutorial was written as documentation for the wagtail-factories Python library, so lacks some context on its own. I’m hosting it here as there has been some difficulty publishing the documentation at its canonical location, and I believe it is of value to the Wagtail developer community.

Defining stream field blocks

Before creating any factories, we will create a Django model with a stream field and a set of blocks that define its content model. Create the following model for a fictional animal charity in home/models.py.

from wagtail.fields import StreamField
from wagtail.models import Page

from home.blocks import PetsBlock


class PetPage(Page):
    pets = StreamField(PetsBlock())

We need to define PetsBlock, so create it and its sub-blocks in home/blocks.py.

from wagtail import blocks
from wagtail.images.blocks import ImageBlock


def get_colour_choices():
    return [
        ("calico", "Calico"),
        ("tabby", "Tabby"),
        ("orange", "Orange"),
    ]


class ScheduledFeedingBlock(blocks.StructBlock):
    time = blocks.TimeBlock()
    portions = blocks.IntegerBlock()
    food = blocks.CharBlock()


class PetStoryBlock(blocks.StreamBlock):
    text = blocks.TextBlock()
    link = blocks.URLBlock()
    image = ImageBlock()


class PetBlock(blocks.StructBlock):
    story = PetStoryBlock()
    name = blocks.CharBlock()
    date_of_birth = blocks.DateBlock()
    feeding_schedule = blocks.ListBlock(ScheduledFeedingBlock())
    colour = blocks.ChoiceBlock(choices=get_colour_choices)
    picture = ImageBlock()


class CatBlock(PetBlock):
    pass


class DogBlock(PetBlock):
    pass


class PetsBlock(blocks.StreamBlock):
    cat = CatBlock()
    dog = DogBlock()

The block definition contains a variety of structures:

Create and run the migrations.

uv run python manage.py makemigrations --noinput --no-color
uv run python manage.py migrate --noinput --no-color

Block factories

With our model and block definitions in place, it’s time to create our block factories. wagtail-factories provides us with the following tools:

Creating factories for our block types, like we would for Page classes or other Django models, will help us to easily create meaningful values for tests and placeholder content.

We’ll start with the bottom of the tree, a factory for ScheduledFeedingBlock.

Factories for struct blocks

Add the following code to home/factories.py.

import factory
from wagtail_factories import StructBlockFactory

from home.blocks import ScheduledFeedingBlock


class ScheduledFeedingBlockFactory(StructBlockFactory):
    time = factory.Faker("time_object")
    portions = factory.Faker("random_int", min=1, max=100)
    food = factory.Faker(
        "random_element", elements=["kibble", "tuna", "salmon", "carrots"]
    )

    class Meta:
        model = ScheduledFeedingBlock

We have:

The Meta.model declaration is essential: wagtail-factories needs this to create values of the correct type. It should be the relevant block class.

In this example, we’re using the API exposed by factory.Faker. This helps us to generate reasonable-looking defaults for fields we don’t specify explicit values for when creating block instances.

import home.factories as f


f.ScheduledFeedingBlockFactory()
StructValue([('time', datetime.time(12, 25, 24, 100924)),
             ('portions', 77),
             ('food', 'carrots')])

We can also specify values for some or all of the fields.

f.ScheduledFeedingBlockFactory(
    portions=3,
    food="kibble",
)
StructValue([('time', datetime.time(10, 1, 21, 873829)),
             ('portions', 3),
             ('food', 'kibble')])

In the next section, we’ll learn how to create and use factories for another of Wagtail’s compound block types: StreamBlock.

Stream block factories

Looking back at the definition of PetBlock, we can see that it contains a stream block definition.

class PetStoryBlock(blocks.StreamBlock):
    text = blocks.TextBlock()
    link = blocks.URLBlock()
    image = ImageBlock()


class PetBlock(blocks.StructBlock):
    ...
    story = PetStoryBlock()
    ...

Create a factory for PetStoryBlock in home/factories.py. We’ll use faker instances for the atomic fields, and a SubFactory for the nested ImageBlock.

import factory
from wagtail_factories import ImageBlockFactory, StreamBlockFactory

from home.blocks import PetStoryBlock


class PetStoryBlockFactory(StreamBlockFactory):
    image = factory.SubFactory(ImageBlockFactory)
    text = factory.Faker("sentence")
    link = factory.Faker("uri")

    class Meta:
        model = PetStoryBlock

Again, note the inner Meta class with model definition - this is required.

Using a stream block factory

Let’s try using our new stream block value to generate a value.

f.PetStoryBlockFactory()
<StreamValue []>

With no parameters, an empty StreamValue is generated.

Given that a StreamValue is an ordered sequence type, how do we specify values for its elements? wagtail-factories supports a syntax for declaring parameters that includes indices for list block and stream block factories. For stream block factories, that syntax comes in two flavours:

  1. a “default value” flavour; and
  2. a “specified value” flavour.

The default value flavour looks like this:

<index>=<block name string>

So, to create an instance of PetStoryBlock where the first element is a text block, we would do the following:

f.PetStoryBlockFactory(**{"0": "text"})
<StreamValue [<block text: 'College yet herself order.'>]>

This creates a block instance at index 0 using a default value as provided by the text declaration on PetStoryBlockFactory.

Ideally, we wouldn’t need the dict-unpacking to insert the keyword-argument parameters, but Python identifiers cannot begin with a numeric character. This will not be an issue when used in the context of a page (or other containing model), as you’ll see in later examples.

The syntax for the “specified value” flavour looks like this:

<index>__<block name>=<value>

For example:

f.PetStoryBlockFactory(**{"0__text": "hello"})
<StreamValue [<block text: 'hello'>]>

This lets us specify the position of the block in the stream, the type of block, and its value. We can combine these two syntaxes arbitrarily, and create streams with multiple elements:

f.PetStoryBlockFactory(**{"0__text": "hello", "1": "link", "2": "text"})
<StreamValue [<block text: 'hello'>, <block link: 'http://www.haynes.biz/tagsterms.html'>, <block text: 'Fast could reveal.'>]>

However, indices must start at zero, and must be sequential.

f.PetStoryBlockFactory(**{"0": "link", "7": "link"})

wagtail_factories.builder.InvalidDeclaration: Parameters for <PetStoryBlockFactory for <class ‘home.blocks.PetStoryBlock’>> missing required index 1

We can also use double-underscores to traverse the block definition tree, and specify values for nested compound blocks, such as the image block option in PetStoryBlock.

with_image = f.PetStoryBlockFactory(**{"0__image__decorative": True})
with_image[0].value.decorative
True

This declaration can be read as:

<index>__<block name>__<block field>=<value>

To specify multiple values for a particular nested block, we can add declarations with the same <index>__<block_name> prefix.

with_image = f.PetStoryBlockFactory(
    **{
        "0__image__decorative": False,
        "0__image__alt_text": "An orange cat lying in the sun",
        "0__image__image__image__file__color": "orange",
    }
)

with_image[0].value.decorative, with_image[0].value.contextual_alt_text
(False, 'An orange cat lying in the sun')

Factories for list blocks

With the nested factory definitions taken care of, we can now create a factory for our PetBlock.

from wagtail_factories import (
    CharBlockFactory,
    ListBlockFactory,
    PageFactory,
    StreamFieldFactory,
)
from home.blocks import PetBlock, get_colour_choices


class PetBlockFactory(StructBlockFactory):
    story = StreamFieldFactory(PetStoryBlockFactory)
    name = factory.Faker("name")
    date_of_birth = factory.Faker("date_object")
    feeding_schedule = ListBlockFactory(ScheduledFeedingBlockFactory)
    colour = factory.Faker(
        "random_element", elements=[x[0] for x in get_colour_choices()]
    )
    picture = factory.SubFactory(ImageBlockFactory)

    class Meta:
        model = PetBlock

This example illustrates an important point:

If the corresponding sub-block is a ListBlock, we use ListBlockFactory, as seen in the declaration for feeding_schedule, above.

The syntax for declaring values for list block elements is similar to that of stream block factories, except:

The syntax is:

<index>=<value>

Let’s create some PetBlock instances, providing values for the feeding schedule.

f.PetBlockFactory()
StructValue([('story', <StreamValue []>),
             ('name', 'Amanda Watts'),
             ('date_of_birth', datetime.date(2024, 3, 25)),
             ('feeding_schedule', <ListValue: []>),
             ('colour', 'calico'),
             ('picture', <Image: An image>)])

Without parameters, an empty ListValue is generated for feeding_schedule. Let’s add some data for a pet that loves tuna.

from datetime import time

f.PetBlockFactory(
    feeding_schedule__0__food="tuna",
    feeding_schedule__0__time=time(6, 0),
    feeding_schedule__1__food="tuna",
    feeding_schedule__1__time=time(12, 0),
    feeding_schedule__2__food="tuna",
    feeding_schedule__2__time=time(18, 0),
)["feeding_schedule"]
<ListValue: [StructValue([('time', datetime.time(6, 0)), ('portions', 99), ('food', 'tuna')]), StructValue([('time', datetime.time(12, 0)), ('portions', 78), ('food', 'tuna')]), StructValue([('time', datetime.time(18, 0)), ('portions', 68), ('food', 'tuna')])]>

If we only care when the pet is fed, we can declare the times only, and the factory mechanisms will take care of the rest.

f.PetBlockFactory(
    feeding_schedule__0__time=time(6, 0),
    feeding_schedule__1__time=time(12, 0),
    feeding_schedule__2__time=time(18, 0),
    feeding_schedule__3__time=time(23, 0),
)["feeding_schedule"]
<ListValue: [StructValue([('time', datetime.time(6, 0)), ('portions', 88), ('food', 'kibble')]), StructValue([('time', datetime.time(12, 0)), ('portions', 59), ('food', 'carrots')]), StructValue([('time', datetime.time(18, 0)), ('portions', 88), ('food', 'salmon')]), StructValue([('time', datetime.time(23, 0)), ('portions', 95), ('food', 'tuna')])]>

As with stream block factories, the aggregated block indices must result in an uninterrupted sequence of integers starting from 0.

Tying it all together

Let’s create our final block factories, and bundle them into the PetPageFactory.

StreamBlockFactory supports sub-classing, just like StreamBlock, so create the following factories in home/factories.py.

from home.blocks import CatBlock, DogBlock


class CatBlockFactory(PetBlockFactory):
    class Meta:
        model = CatBlock


class DogBlockFactory(PetBlockFactory):
    class Meta:
        model = DogBlock

Then add them to our top-level PetsBlockFactory.

from home.blocks import PetsBlock


class PetsBlockFactory(StreamBlockFactory):
    cat = factory.SubFactory(CatBlockFactory)
    dog = factory.SubFactory(DogBlockFactory)

    class Meta:
        model = PetsBlock

And finally, create PetPageFactory.

from wagtail_factories import (
    PageFactory,
    StreamFieldFactory,
)
from home.models import PetPage


class PetPageFactory(PageFactory):
    pets = StreamFieldFactory(PetsBlockFactory)

    class Meta:
        model = PetPage

We’ve now built a family of factories from the bottom up, that mirrors our data-type definition. The following diagram illustrates the factory hierarchy we’ve created:

PetPageFactory
└── pets (StreamFieldFactory)
    └── PetsBlockFactory (StreamBlockFactory)
        ├── cat (SubFactory)
        │   └── CatBlockFactory (PetBlockFactory)
        │       ├── story (StreamFieldFactory)
        │       │   └── PetStoryBlockFactory (StreamBlockFactory)
        │       │       ├── image (SubFactory → ImageBlockFactory)
        │       │       ├── text (Faker)
        │       │       └── link (Faker)
        │       ├── name (Faker)
        │       ├── date_of_birth (Faker)
        │       ├── feeding_schedule (ListBlockFactory)
        │       │   └── ScheduledFeedingBlockFactory (StructBlockFactory)
        │       │       ├── time (Faker)
        │       │       ├── portions (Faker)
        │       │       └── food (Faker)
        │       ├── colour (Faker)
        │       └── picture (SubFactory → ImageBlockFactory)
        └── dog (SubFactory)
            └── DogBlockFactory (PetBlockFactory)
                [same structure as CatBlockFactory]

This hierarchy shows how each factory builds upon its sub-factories, creating a complete system for generating test data for complex Wagtail stream field structures.

Taking it for a spin

We can now test our factories, and get familiar with the syntax for declaring stream field structures. The simplest use is to call the PetPageFactory with no parameters.

page = f.PetPageFactory()
page
<PetPage: Test page>

We can see that the stream field is empty.

page.pets
<StreamValue []>

Let’s create a CatBlock and a DogBlock at the top level, using the factory defaults.

page = f.PetPageFactory(
    pets__0="cat",
    pets__1="dog",
)
page.pets
<StreamValue [<block cat: StructValue([('story', <StreamValue []>), ('name', 'Rodney Henderson'), ('date_of_birth', datetime.date(2020, 11, 8)), ('feeding_schedule', <ListValue: []>), ('colour', 'tabby'), ('picture', <Image: An image>)])>, <block dog: StructValue([('story', <StreamValue []>), ('name', 'Dr. Catherine Pope PhD'), ('date_of_birth', datetime.date(1985, 1, 13)), ('feeding_schedule', <ListValue: []>), ('colour', 'tabby'), ('picture', <Image: An image>)])>]>

The syntax used here mirrors the “default value” syntax described earlier, with the added prefix for the stream field name:

pets__0=“cat”

<model field name>__<stream field index>=<block name>

Let’s create an instance with some specific values for the CatBlock struct block.

page = f.PetPageFactory(
    pets__0__cat__name="Praxidike",
    pets__0__cat__colour="tabby",
)
page.pets[0]
<block cat: StructValue([('story', <StreamValue []>), ('name', 'Praxidike'), ('date_of_birth', datetime.date(2019, 1, 22)), ('feeding_schedule', <ListValue: []>), ('colour', 'tabby'), ('picture', <Image: An image>)])>

The declaration syntax here is:

<field>__<index>__<block name>__<field name>=<value>

What about nested stream blocks? CatBlock.story is such a block. To declare values, we follow the syntactic patterns we’ve already encountered for stream values:

<index>=<block name> for a default; or <index>__<block name>=<value>

page = f.PetPageFactory(
    pets__0__cat__name="Praxidike",
    pets__0__cat__colour="tabby",
    pets__0__cat__story__0="text",
    pets__0__cat__story__1__link="https://http.cat/",
)
page.pets[0]
<block cat: StructValue([('story', <StreamValue [<block text: 'Direction something hotel once.'>, <block link: 'https://http.cat/'>]>), ('name', 'Praxidike'), ('date_of_birth', datetime.date(1985, 4, 19)), ('feeding_schedule', <ListValue: []>), ('colour', 'tabby'), ('picture', <Image: An image>)])>

Prax needs to eat, so we should add some entries to the feeding schedule. Recall that the basic syntax for declaring list block elements is:

<index>=<value>

This composes across field and factory boundaries as in our other examples. So, to specify values for the fields of a struct block:

<index>__<field name>=<value>

page = f.PetPageFactory(
    pets__0__cat__feeding_schedule__0__time="06:00:00",
    pets__0__cat__feeding_schedule__1__food="tuna",
)
page.refresh_from_db()          # Normalizes the time value.
page.pets[0].value["feeding_schedule"]
<ListValue: [StructValue([('time', datetime.time(6, 0)), ('portions', 27), ('food', 'salmon')]), StructValue([('time', datetime.time(0, 32, 26, 937000)), ('portions', 54), ('food', 'tuna')])]>

Finally, here’s an example of specifying multiple fields on multiple stream elements.

page = f.PetPageFactory(
    pets__0__cat__name="Frog",
    pets__0__cat__story__0="text",
    pets__0__cat__story__1__link="https://http.cat/",
    pets__1="cat",
    pets__2__dog__name="Werner",
    pets__2__dog__colour="orange",
    pets__2__dog__feeding_schedule__0__time="08:30:00",
    pets__2__dog__feeding_schedule__1__time="12:30:00",
    pets__2__dog__feeding_schedule__2__time="18:30:00",
    pets__2__dog__story__0="text",
    pets__2__dog__picture__image__image__file__width=200,
)

page
<PetPage: Test page>

Footnotes

1 Technically we can use factory.SubFactory instead of StreamFieldFactory for nested stream block factory declarations, and it is common to see this in the wild. However, this will result in errors if the containing block factory is used directly - i.e. not in the context of a containing model factory with a top level StreamFieldFactory. This discrepancy should be resolved in a future release of wagtail-factories.

Tags: