wagtail: Create fields for structural block types

I want to make a field that’s a simple list downloads. The following code accomplishes what I want using StreamField, but the API is a little weird.

class ResourcesPage(Page):
    files = StreamField([
        ('files', DocumentChooserBlock())
    ])

Ideally it would look something like this:

class ResourcesPage(Page):
    files = ListField(DocumentChooserBlock())

Here I’ve created a new imaginary type, ListField. There are a few possible solutions (such changing the way StreamField parameters work) but I think it makes sense to have a wrapper field around each of the structural block types.

Here’s an example of how an implementation could look:

from wagtail.wagtailcore.fields import StructField, ListField, StreamField
from wagtail.wagtaildocs.blocks import DocumentChooserBlock
from wagtail.wagtailimages.blocks import ImageChooserBlock
from wagtail.wagtailcore import blocks


class RecipePage(Page):
    # StructBlock wrapper
    chef = StructField([
        ('first_name', blocks.CharBlock(required=True)),
        ('surname', blocks.CharBlock(required=True)),
        ('photo', ImageChooserBlock()),
        ('biography', blocks.RichTextBlock())
    ])

    # ListBlock wrapper
    ingredients_list = ListField(blocks.CharBlock(label="Ingredient"))

    # StreamBlock (just a regular StreamField)
    instructions = StreamField([
        ('paragraph', blocks.RichTextBlock(icon="pilcrow")),
        ('h1', blocks.CharBlock(classname="title", icon="title"))
    ])

This could also help us determine how the fields should appear in the admin (example: #1549)

About this issue

  • Original URL
  • State: open
  • Created 9 years ago
  • Reactions: 20
  • Comments: 20 (4 by maintainers)

Most upvoted comments

The core team has discussed this today, and we’re happy with the principle of adding ListField / StructField types in parallel with StreamField (or, perhaps more simply, allowing StreamField to accept ListBlock / StructBlock as the top-level block).

In my opinion, the best approach is to have an specific field for each structural block type:

  • StructField for StructBlock
  • ListField for ListBlock
  • StreamField for StreamBlock

Regarding to the wagtail admin views:

  • StructField wouldn’t need to include a “new” button, just render the block form.
  • ListField wouldn’t need to show the stream menu to choose the model to add.
  • StreamField would work as usual

Of course ListField could be considered just a particular case of StreamField and be omitted, but if ListBlock exists, I think ListField should exists too.

Any updates on this?

I agree with this:

In my opinion, the best approach is to have an specific field for each structural block type:

* `StructField` for `StructBlock`

* `ListField` for `ListBlock`

* `StreamField` for `StreamBlock`

Any updates?

Any updates on this ?

I just asked about this on Slack and was pointed here. This would be a nice to have feature that would make the models a little cleaner. My use case is that I have a collection of pre-defined block types that go into a technical content page. I want to be able to add blocks as fixed blocks (hence the need for a StructBlock), or stream fields, which already works beautifully.

The options available are:

  • StreamField – which allows N blocks, when I want just 1,
  • Individual Fields for each “block”, grouped using a MultiFieldPannel – this isn’t a re-usable block
  • External Model, with ForeignKey… not even sure if there is a 1-1 key here?

A “StructField” could take a block, similar to a StreamField, but accept just a single block, hence it would work just like a stream field from a DB perspective, but have a single block item, rather than an array of items as a StreamField does.

Just thinking out loud

Bringing this back up, because I do need a ListField for the project I’m working on.

  1. My page is divided into sections
  2. Users must be able to add, rearrange, and delete sections
  3. Sections contain StreamField content

In this case, it makes sense to have something like:

body = ListField(blocks.StructBlock([
    ('title', blocks.CharBlock()),
    ('content', MyStreamBlock())
]))

I was hoping this feature was out already while scrolling down the page. 😦

+1

This would be so very very useful. I’m willing to bet this would get a lot of use of it existed.

I already have a streamfield block which already does what I want, but I need a given set of pages to have a single optional instance of said block separate from the main page stream field. Trying to achieve the same thing using Django models and an inline field just isn’t possible, or is very painful.

A BlockField() would solve my problem for me. The GUI would make sense, and it would allow reusing the existing block template.

If someone points me where to start I’d gladly help with this, or am more than happy to help review or debug any work towards this.

I would use this feature for sure. I have in multiple places StreamField with a single Block in it and limiting the StreamField to one block. But the UI is not great as you can add another block and when you save it says you can only have one.

2 options:

  • StructField that was discussed above.
  • Remove the + button in the UI to add another block if the limit is 1

Following - this kind of comes down to defining reusable fieldsets more than anything else and it’s an issue I face frequently in wagtail dev!

For a site we are developing now, I created a “workaround” StructField class that can be used to add StructBlocks directly to your page. It feels a bit hacky, but makes my page models so much cleaner. Note: not tested in production, and we have a headless setup so I’m only interested in the API output (I did not check how this behaves in a template).

from wagtail.admin.edit_handlers import StreamFieldPanel
from wagtail.core import blocks
from wagtail.core.fields import StreamField


class StructField(StreamField):
    """
    A `StructField` allows you to place a single `StructBlock` as a field in your page model, where
    normally `StructBlock`s can only be contained inside a `StreamField`.

    Use like you would add any other field, e.g. `example = StructField(ExampleBlock, blank=True)`.

    Note: this is actually just a `StreamField` under the hood (with a `max_num` of 1 and a
    modified `get_api_representation` to output the value directly instead of an array).  Your
    `StructBlock`s should have a label, otherwise the page editor will display "_structvalue".
    """

    def __init__(self, structblock, blank=False, **kwargs):
        def get_api_representation(self, value, context=None):
            if value is None or len(value) is 0:
                return None

            return value[0].block.get_api_representation(
                value[0].value, context=context
            )

        # Dynamically create a class based on StreamBlock, that contains just one field called
        # `_structvalue` (this name is unused in the StructField API output; the value itself is
        # outputted directly).
        StructValueStreamBlock = type(
            "StructValueStreamBlock",
            (blocks.StreamBlock,),
            {
                "_structvalue": structblock,
                "get_api_representation": get_api_representation,
            },
        )

        super().__init__(
            StructValueStreamBlock(max_num=1, required=not blank), blank=blank, **kwargs
        )


class StructFieldPanel(StreamFieldPanel):
    pass

This would be an awesome feature to have for the upcoming LTS 😃

I’ll work on that.