Using a Wagtail "ChoiceBlock" with dynamic choices rather than a hardcoded list
Asked Answered
F

1

5

We have a setup with a Blog model that has a manytomany relation for BlogPageCategory, and we have a "recent blog posts" streamfield block that lets you specify whether to show cards for X latest blog posts, or X latest blog posts from a specific category.

As such, we started with the following code:

from wagtail.core import blocks


class RecentBlogEntries(blocks.StructBlock):
    title = blocks.CharBlock(
        required=True,
    )

    category_filter = blocks.ChoiceBlock(
        label='Filter by Category',
        required=False,
        choices=[
            ('all', 'All'),
            ('First Category', 'First Category'),
            ('...',. '...'),
        ],
    )

    ...

But hardcoding the categories is kind of silly, and being able to pick them from "what the list is, right now, based on the CMS data for BlogPageCategory" would be far more convenient. However, the following code (of course) turns into an equally hardcoded migration:

from wagtail.core import blocks
from ... import BlogPageCategory


class RecentBlogEntries(blocks.StructBlock):
    title = blocks.CharBlock(
        required=True,
    )

    choices = [ (cat.name, cat.name) for cat in BlogPageCategory.objects.all()]
    choices.sort()
    choices.insert(0, ('all', 'All'))

    category_filter = blocks.ChoiceBlock(
        label='Filter by Category',
        required=False,
        choices=choices,
    )

    ...

Is there any way to make this a dynamic value instead of a list that is fixed by makemigrations?

Fruity answered 1/4, 2020 at 17:34 Comment(0)
P
11

ChoiceBlock accepts a callable function as the choices argument:

def get_categories():
    return [(cat.name, cat.name) for cat in BlogPageCategory.objects.all()]


class RecentBlogEntries(blocks.StructBlock):
    title = blocks.CharBlock(
        required=True,
    )

    category_filter = blocks.ChoiceBlock(
        label='Filter by Category',
        required=False,
        choices=get_categories,
    )

The callable needs to be defined at the top level of a module so that the migration can make a reference to it (i.e. it can't be a method on a class), and if it gets subsequently moved or renamed, you'll need to edit the migration accordingly.

Peek answered 1/4, 2020 at 19:43 Comment(2)
Does this mean that whenever a new BlogPageCategory object is created in the admin, migrations need to be run?Potable
@PhilGyford No - as long as you use choices=get_categories, the migration will save a reference to the get_categories function, which will remain unchanged across migrations. (If you were to do choices=get_categories() instead, you'd end up with the evaluated list from that moment in time embedded into the migration, so you want to avoid that.)Peek

© 2022 - 2025 — McMap. All rights reserved.