Introduction

I’ve recently been building some content systems within Wagtail that utilise the ChoiceBlock and MultipleChoiceBlock. The project required these to be modifiable without generating migrations for a few reasons, so I’d like to share my solution for facilitating this.

The Code

When copying from the following examples, be sure to update the class’ path within the deconstruct method. In my first example, it’s myapp.blocks.DynamicChoiceBlock, meaning that the code should be in myapp/blocks.py.

wagtail.blocks.ChoiceBlock

from wagtail import blocks
from django.utils.translation import gettext_lazy as _

class DynamicChoiceBlock(blocks.ChoiceBlock):
    """A ChoiceBlock which doesn't trigger new migrations with dynamic choices."""

    def deconstruct(self):
        """Override the deconstruct method to prevent migrations from being created when the choices are changed."""
        return ("myapp.blocks.DynamicChoiceBlock", [], {})

# Example of using the DynamicChoiceBlock:
class ExampleStructBlock(blocks.StructBlock):
    example = DynamicChoiceBlock(
        choices=[
            ("foo", _("Foo"),
            ("bar", _("Bar"),
        ],
        required=True,
        label=_("Example"),
    )

wagtail.blocks.MultipleChoiceBlock

from wagtail import blocks
from django.utils.translation import gettext_lazy as _

class DynamicMultipleChoiceBlock(blocks.MultipleChoiceBlock):
    """A MultipleChoiceBlock which doesn't trigger new migrations with dynamic choices."""

    def deconstruct(self):
        """Override the deconstruct method to prevent migrations from being created when the choices are changed."""
        return ("myapp.blocks.DynamicMultipleChoiceBlock", [], {})

# Example of using the DynamicMultipleChoiceBlock:
class ExampleStructBlock(blocks.StructBlock):
    example = DynamicMultipleChoiceBlock(
        choices=[
            ("foo", _("Foo"),
            ("bar", _("Bar"),
        ],
        required=True,
        label=_("Example"),
    )

How It Works

This approach overrides the deconstruct method from the parent class. Here is the default deconstruct method from the wagtail.blocks.ChoiceBlock class:

def deconstruct(self):
    """
    Always deconstruct ChoiceBlock instances as if they were plain ChoiceBlocks with their
    choice list passed in the constructor, even if they are actually subclasses. This allows
    users to define subclasses of ChoiceBlock in their models.py, with specific choice lists
    passed in, without references to those classes ending up frozen into migrations.
    """
    return ("wagtail.blocks.ChoiceBlock", [], self._constructor_kwargs)

Wagtail uses this method when evaluating the block to determine if any properties have changed. Comparing this to our new one, you’ll see that it no longer returns the self._constructor_kwargs attribute which contains the choices for the block. By not including them in the evaluation process, changes to the choices don’t trigger migrations within Wagtail, as it isn’t aware that any changes have happened at all.

Gotchas

While this approach is desirable for some situations, the choices are included within migrations for a reason. By removing the validation that migrations provide, there’s the potential for unexpected behaviour.

I used this approach on a large project where the same code was used on several deployments, and each required different choices. The app I was writing needed to avoid migrations being generated for each deployment, so this was a great solution.

Leave a Reply

Your email address will not be published. Required fields are marked *