Skip to content

"normalize" mechanism for StreamField blocks #9738

@gasman

Description

@gasman

Is your proposal related to a problem?

When writing to a StreamField block, e.g. page.body = [('heading', "Hello world!")], or setting a default value, we expect the block value to be passed in the block's native data type. This data type is often undocumented and non-obvious:

  • for RichTextBlock, it's a wagtail.rich_text.RichText object, not a string
  • for EmbedBlock, it's an EmbedValue object
  • for StreamBlock, it's a StreamValue instance. (The list-of-tuples representation is only recognised by StreamField.to_python, not StreamBlock, which means it's only valid for the top level of the stream, not nested StreamBlocks.)
  • For ListBlock, it used to be a Python list, but is now a ListValue instance as of Wagtail 4.1.

StreamValue (and possibly ListValue too, now) is especially troublesome, as the constructor needs to be passed a reference to the StreamBlock, which is hard to get hold of, and impossible in the case of setting a default (because it would have to be a reference to the StreamBlock that you're still in the middle of defining).

In these situations, we should follow Postel's Law and accept "close enough" data types whenever a block value is being supplied from user code: a string for RichTextBlock, a list-of-tuples for StreamBlock and so on.

Describe the solution you'd like

The Block class should implement a normalize(value) method that accepts an input value in a range of allowable types, and returns that value in the block's native type. This differs from existing methods such as to_python in that it's not aiming to convert data between representations - just standardising data that "should" be in the native type. We would then run any user-supplied data through normalize before using it in the block's internal logic.

The default implementation in Block would simply return value unchanged, while blocks that act as containers for child blocks will recursively call normalize on the child values along with doing whatever conversion they require for themselves.

Additional context

If we were to move the "sniffing various types to decide what to do" logic from StreamField.to_python to StreamBlock.normalize, I believe this would remove the remaining blocker to supporting other kinds of block besides StreamBlock as the top-level block of a StreamField, providing a solution to #2048.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions