From 687eab87d297cbddfb8d99e9340bd920d772a82d Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Sun, 10 Mar 2024 14:37:03 +0000 Subject: [PATCH 01/11] Add initial Union Block motivation --- text/000-union-block.md | 85 ++++++++++++++++++++++++++ text/assets/000/stream-style-link.png | Bin 0 -> 9441 bytes text/assets/000/union-style-link.png | Bin 0 -> 15830 bytes 3 files changed, 85 insertions(+) create mode 100644 text/000-union-block.md create mode 100644 text/assets/000/stream-style-link.png create mode 100644 text/assets/000/union-style-link.png diff --git a/text/000-union-block.md b/text/000-union-block.md new file mode 100644 index 00000000..52d2dfee --- /dev/null +++ b/text/000-union-block.md @@ -0,0 +1,85 @@ +# RFC : Union Block + +* RFC: +* Author: Joshua Munn +* Created: 2024-03-09 +* Last Modified: 2024-03-09 + +## Abstract + +This RFC proposes the implementation of a new stream field block type in Wagtail, the `UnionBlock`. The `UnionBlock` is a block type that presents editors with a choice of block types, allowing them to select one and insert a single corresponding "block" of content. The available choices for a given `UnionBlock` instance are defined by the developer, as with `StreamBlock`. + +A proof of concept implementation can be reviewed at [https://github.com/jams2/wagtail/tree/feature/union-block](https://github.com/jams2/wagtail/tree/feature/union-block). + +## Motivation + +Wagtail's `StreamField` allows editors to author content with a great degree of flexibility. The facilities that enable this are the various `Block` types, which can be broadly summarised (some omissions for brevity) as follows: + +- `StreamBlock`, a non-homogeneous sequence of other blocks; +- `ListBlock`, a homogeneous sequence of some block; +- `StructBlock`, a type that allows a set of blocks to be combined into a single compound block; and +- `FieldBlock` and its subclasses - atomic block types that capture a single value. + +One notable omission from this family of types is a block that captures a single value from a union of types. + +Unions are incredibly useful - they represent _choice_. Choice is currently represented in the block type family by `StreamBlock`. `StreamBlock` allows editors to create content comprised of any number of any type of block, in any order (subject to developer-defined constraints). However, Wagtail's block type family does not provide any facility for choice at the atomic block level, as `StreamBlock` is a sequence type. + +`StreamBlock` is poorly suited to modelling a _single_ value, chosen from a union of types. The attempt to shoehorn `StreamBlock` into this use case is common, results in a suboptimal user experience for editors, and requires developers to write unwieldy code to work around the mismatch between use case and implementation. + +### Example: Using `StreamBlock` to represent a single choice + +### Impact on UX + +A common request from users of Wagtail CMS instances is the functionality to include a link in some fragment of structured content (e.g. as part of a call to action), where that link might point to: + +- a web resource not provided by the Wagtail instance; +- a `Page` in the Wagtail instance; or +- an email address, etc. + +In an attempt to provide the required UI (while working within the tools provided by Wagtail's core) developers will often implement a `StreamBlock` with a choice of link types, as illustrated below. + +```python +class LinkChooserBlock(blocks.StreamBlock): + page = blocks.PageChooserBlock() + url = blocks.URLBlock() + + class Meta: + max_num = 1 + + +class LinkBlock(blocks.StructBlock): + title = blocks.CharBlock() + link = LinkChooserBlock() +``` + +![Link block implemented stream block style](../assets/000/stream-style-link.png) + +The UI generated for inserting a link requires editors to first select the "+" button to insert a block, and then choose the block type. In the typical case that the link value is _required_ this creates dissonance between what is required by data validation and what is communicated to users by visual language - requiring users to insert a block when that block is required is a sub-par experience. Compare this to a link block implemented as a `UnionBlock`: + +![Link block implemented union block style](../assets/000/union-style-link.png) + +In this example all fields required to make a valid submission are immediately present in the UI, which clearly communicates the requirements of the system to users and prevents a class of validation errors from occurring. + +### Impact on code quality + +Data inserted in stream fields is typically destined to be rendered as HTML and served as part of a web page. To facilitate this, blocks which implement a choice of types for a single value may be required to go through a process of narrowing to facilitate each possible sub-block's particular rendering requirements. In the case of our `LinkChooserBlock` example, we will need to invoke Wagtail's page URL resolution machinery if the page option is selected. A typical solution is to implement a custom `StructValue` class for the `LinkBlock`. + +``` python +class LinkStructValue(blocks.StructValue): + def get_url(self): + block = self.get("link")[0] + if (block_type := block.block_type) == "page": + if block.value and block.value.live: + return block.value.url + elif block_type == "url": + return block.value +``` + +Developers must deal with the fact the `link` field on `LinkBlock` is a sequence, which: + +- is a poor mapping of the solution domain onto the problem domain; and +- requires error handling for the case that the sequence might be empty, regardless of the current validation constraints (the author suspects that developers that have worked with Wagtail regularly will empathise with the need for this). + +## Specification + +## Open Questions diff --git a/text/assets/000/stream-style-link.png b/text/assets/000/stream-style-link.png new file mode 100644 index 0000000000000000000000000000000000000000..52ac6e248092a5e762968ea96432b44aa2ec2af2 GIT binary patch literal 9441 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 YHKzD<{~(zpV09uDMGb{=@XNpc50C{c%K!iX literal 0 HcmV?d00001 From 1ed9530dc3efc284f42c3ef6d9b184b359edb5cf Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Sun, 10 Mar 2024 14:38:52 +0000 Subject: [PATCH 02/11] Update image links --- text/000-union-block.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/000-union-block.md b/text/000-union-block.md index 52d2dfee..61ad0e2c 100644 --- a/text/000-union-block.md +++ b/text/000-union-block.md @@ -52,11 +52,11 @@ class LinkBlock(blocks.StructBlock): link = LinkChooserBlock() ``` -![Link block implemented stream block style](../assets/000/stream-style-link.png) +![Link block implemented stream block style](./assets/000/stream-style-link.png) The UI generated for inserting a link requires editors to first select the "+" button to insert a block, and then choose the block type. In the typical case that the link value is _required_ this creates dissonance between what is required by data validation and what is communicated to users by visual language - requiring users to insert a block when that block is required is a sub-par experience. Compare this to a link block implemented as a `UnionBlock`: -![Link block implemented union block style](../assets/000/union-style-link.png) +![Link block implemented union block style](./assets/000/union-style-link.png) In this example all fields required to make a valid submission are immediately present in the UI, which clearly communicates the requirements of the system to users and prevents a class of validation errors from occurring. From 35b4a348ad5d44b2bdd6623ba9b3705ad0a82e2a Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Sun, 10 Mar 2024 15:22:05 +0000 Subject: [PATCH 03/11] Add struct block example --- text/000-union-block.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/text/000-union-block.md b/text/000-union-block.md index 61ad0e2c..ac8785df 100644 --- a/text/000-union-block.md +++ b/text/000-union-block.md @@ -28,7 +28,7 @@ Unions are incredibly useful - they represent _choice_. Choice is currently repr ### Example: Using `StreamBlock` to represent a single choice -### Impact on UX +#### Impact on UX A common request from users of Wagtail CMS instances is the functionality to include a link in some fragment of structured content (e.g. as part of a call to action), where that link might point to: @@ -60,7 +60,7 @@ The UI generated for inserting a link requires editors to first select the "+" b In this example all fields required to make a valid submission are immediately present in the UI, which clearly communicates the requirements of the system to users and prevents a class of validation errors from occurring. -### Impact on code quality +#### Impact on code quality Data inserted in stream fields is typically destined to be rendered as HTML and served as part of a web page. To facilitate this, blocks which implement a choice of types for a single value may be required to go through a process of narrowing to facilitate each possible sub-block's particular rendering requirements. In the case of our `LinkChooserBlock` example, we will need to invoke Wagtail's page URL resolution machinery if the page option is selected. A typical solution is to implement a custom `StructValue` class for the `LinkBlock`. @@ -80,6 +80,20 @@ Developers must deal with the fact the `link` field on `LinkBlock` is a sequence - is a poor mapping of the solution domain onto the problem domain; and - requires error handling for the case that the sequence might be empty, regardless of the current validation constraints (the author suspects that developers that have worked with Wagtail regularly will empathise with the need for this). +### Example: Using `StructBlock` to represent a single choice + +Another approach that developers might take to provide a block that allows a single choice from a set of sub-blocks is to implement a `StructBlock` with a field for each sub-block, with custom JavaScript for the interface and custom validation. This is the approach taken by the [wagtail-link-block](https://github.com/developersociety/wagtail-link-block) package. + +`wagtail-link-block` provides a `StructBlock` subclass with a field for each handled link type[^1]. Each sub-block is marked as optional, and a custom `clean` method enforces that a single value is provided[^2]. A `ChoiceBlock` is included to allow users to select their desired link type[^3]. Custom JavaScript is provided that hides the form fields for all except the one that corresponds to the chosen link type[^4]. + +This approach is reasonable, however the author feels that the underlying concept (a single value chosen from a union of types) has enough utility that it should be provided by Wagtail. The existence of the `wagtail-link-block` package illustrates that the use case is often required. A solution provided as part of Wagtail's core set of blocks would be more extendable, and present a consistent user experience. `wagtail-link-block` appears to be a well built package, but a criticism of it is that it is not simple to extend. A developer may wish to exclude the use of `mailto` links, for example, which would require interaction beyond the API presented by `wagtail-link-block`. If Wagtail provided a `UnionBlock`, developers would be empowered to implement their own union block types with arbitrary combinations of blocks. + ## Specification ## Open Questions + +--- +[^1]: https://github.com/developersociety/wagtail-link-block/blob/219ad4cb543e2ed6da900d7c5c7a4be59ef58d27/wagtail_link_block/blocks.py#L70 +[^2]: https://github.com/developersociety/wagtail-link-block/blob/219ad4cb543e2ed6da900d7c5c7a4be59ef58d27/wagtail_link_block/blocks.py#L133 +[^3]: https://github.com/developersociety/wagtail-link-block/blob/219ad4cb543e2ed6da900d7c5c7a4be59ef58d27/wagtail_link_block/blocks.py#L77 +[^4]: https://github.com/developersociety/wagtail-link-block/blob/219ad4cb543e2ed6da900d7c5c7a4be59ef58d27/wagtail_link_block/static/link_block/link_block.js#L8 From 17b9de380d8936d8d7a6aa7e63d96200f4dd9eac Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Sun, 10 Mar 2024 16:08:53 +0000 Subject: [PATCH 04/11] Add initial implementation details --- text/000-union-block.md | 68 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/text/000-union-block.md b/text/000-union-block.md index ac8785df..dede27cf 100644 --- a/text/000-union-block.md +++ b/text/000-union-block.md @@ -90,9 +90,75 @@ This approach is reasonable, however the author feels that the underlying concep ## Specification +### `UnionBlock` implementation + +`UnionBlock` is a new block type that allows editors to select a block type from a set of types defined by the developer, and then insert a single value for that chosen type. + +For each instance of `UnionBlock`, Editors should be presented with a `ChoiceField`, with one option for each sub-block that is a member of the union. When they make a selection, the UI should be updated so that the native form widget for the selected block type is presented. Only a single form field for the block's value should ever be presented. A default value must always be provided, as an empty choice requires editors to make an interaction to reveal a form field for the value, when they have already made an interaction indicating that they wish to enter a value when they selected the `UnionBlock` (or the block containing it) in a `StreamBlock`. + +#### Creation of subclasses + +`UnionBlock` must support definition by subclassing. As with `StreamBlock` and `StructBlock`, a developer must be able to create a custom block type inheriting from `UnionBlock`, where the sub-blocks defined as class level attributes are the options available to editors. + +`UnionBlock` must also support "anonymous" subclasses, like `StreamBlock` and `StructBlock`. For example, the following two definitions should be equivalent: + +``` python +class MyUnion(UnionBlock): + text = TextBlock() + char = CharBlock() + +my_union = UnionBlock([("text", TextBlock()), ("char", CharBlock())]) +``` + +Any existing block type should be a valid member of a `UnionBlock`. + +#### Implementation of the type selector + +The base `UnionBlock` class must insert a `ChoiceField` into the UI, the choices of which have values that are the names of the declared sub-blocks, with the labels being the labels of those sub-blocks. + +#### Parameters and meta-options + +`UnionBlock.__init__` must support a `local_blocks` keyword argument, in the first position, as with `StreamBlock` and `StructBlock`. If provided, this parameter must be a list of 2-tuples, where the first element is the name of the block option, and the second element is the block instance to use if that option is selected. + +In addition to `local_blocks` (and the existing base block options), `UnionBlock` must support the following options, either as keyword arguments to its constructor, or attributes defined on its nested `Meta` class: + +- `default_type` (`Optional[str]`, default value: `None`) - the name of the block to be presented as the default option to editors. If no parameter is passed, the first option should be automatically selected as the default. If the value is not in the set of names declared for that block, an error must be raised. +- `type_selector_label` (`Optional[str]`, default value: `"Type"`) - the label to be associated with the field presented to editors for selecting the type for a given block instance. +- `type_selector_widget` (`Optional[django.forms.Widget]`, default value: `None`) - the widget to use for the type selector field. If no widget is provided, Wagtail should default to Django's `RadioSelect` widget (as opposed to the `Select` widget, which is less usable/accessible). +- `value_label` (`Optional[str]`, default value: `"Value"`) - the label to associate with the value field presented to editors. This is provided for visual consistency and to reduce redundancy in the UI. As the sub-block labels will be used for the choices in the type selector field, they need not be repeated with the value field, and we prefer for less dynamic content in the UI. +- `value_class` (`Optional[UnionValue]`, default value: `None`) - the value class to use to represent the value of a `UnionBlock` instance in Python. If no value is provided, a base `UnionValue` class must be used. + +#### Help text + +The help text declared for the `UnionBlock` must be presented at the top level of the block's UI. + +The help text declared on any sub-block must be presented alongside the form field for that sub-block, whenever it is present in the UI. + +#### Validation + +The implementation must validate that the selected type is a member of the declared sub-blocks. + +The implementation must validate the provided value, using the selected block type's validation methods. + +If a `UnionBlock` is marked as required, a valid value must be provided. + +#### Value classes + +Similar to how `StructValue` is required for `StructBlock`, an extendable `UnionValue` class should be provided. This will allow developers to provide additional properties and methods, which will be required for the use of `UnionBlock` in the presentation layer. + +The base `UnionValue` class must provide the following attributes: + +- `block_type` - the name of the block type that was selected for the given instance. +- `value` - the value for the given instance, in the native format of the selected sub-block type (e.g. if the sub-block is a `CharBlock` this will be a `str`, if it is a `ListBlock` it will be a `ListValue`, if it is a `StructBlock` it will be a `StructValue`, etc.). + +#### Serialisation + +#### Deserialisation + +#### Impact on external libraries + ## Open Questions ---- [^1]: https://github.com/developersociety/wagtail-link-block/blob/219ad4cb543e2ed6da900d7c5c7a4be59ef58d27/wagtail_link_block/blocks.py#L70 [^2]: https://github.com/developersociety/wagtail-link-block/blob/219ad4cb543e2ed6da900d7c5c7a4be59ef58d27/wagtail_link_block/blocks.py#L133 [^3]: https://github.com/developersociety/wagtail-link-block/blob/219ad4cb543e2ed6da900d7c5c7a4be59ef58d27/wagtail_link_block/blocks.py#L77 From 8cb9fa693918c0782b6b556ecc91abea65ce3612 Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Mon, 11 Mar 2024 09:57:02 +0000 Subject: [PATCH 05/11] Add sections on templates and deserialisation --- text/000-union-block.md | 88 ++++++++++++++++++++++++++++++++++------- 1 file changed, 74 insertions(+), 14 deletions(-) diff --git a/text/000-union-block.md b/text/000-union-block.md index dede27cf..acce58cd 100644 --- a/text/000-union-block.md +++ b/text/000-union-block.md @@ -80,6 +80,8 @@ Developers must deal with the fact the `link` field on `LinkBlock` is a sequence - is a poor mapping of the solution domain onto the problem domain; and - requires error handling for the case that the sequence might be empty, regardless of the current validation constraints (the author suspects that developers that have worked with Wagtail regularly will empathise with the need for this). +Note: there may be more elegant/Pythonic/Wagtailish approaches than the one illustrated above, but it was taken from a real project, and the author believes it is representative of the kinds of solutions widely in use. + ### Example: Using `StructBlock` to represent a single choice Another approach that developers might take to provide a block that allows a single choice from a set of sub-blocks is to implement a `StructBlock` with a field for each sub-block, with custom JavaScript for the interface and custom validation. This is the approach taken by the [wagtail-link-block](https://github.com/developersociety/wagtail-link-block) package. @@ -88,15 +90,27 @@ Another approach that developers might take to provide a block that allows a sin This approach is reasonable, however the author feels that the underlying concept (a single value chosen from a union of types) has enough utility that it should be provided by Wagtail. The existence of the `wagtail-link-block` package illustrates that the use case is often required. A solution provided as part of Wagtail's core set of blocks would be more extendable, and present a consistent user experience. `wagtail-link-block` appears to be a well built package, but a criticism of it is that it is not simple to extend. A developer may wish to exclude the use of `mailto` links, for example, which would require interaction beyond the API presented by `wagtail-link-block`. If Wagtail provided a `UnionBlock`, developers would be empowered to implement their own union block types with arbitrary combinations of blocks. -## Specification +### Example: Using `UnionBlock` to implement a link block + +The following example shows how a `LinkBlock`, equivalent to the other examples in this section, would be implemented using `UnionBlock`. + +``` python +class LinkBlock(UnionBlock): + page = PageChooserBlock(template="blocks/link_as_page.html") + url = URLBlock(template="blocks/link_as_url.html") +``` -### `UnionBlock` implementation +This approach to the `LinkBlock` problem requires developers to write less code - significantly less when taking into account the custom JavaScript required when taking the approach illustrated by `wagtail-link-block`. + +In summary, the author believes that the UX and code quality improvements illustrated here present a compelling case for the inclusion of a `UnionBlock` in Wagtail. + +## Specification `UnionBlock` is a new block type that allows editors to select a block type from a set of types defined by the developer, and then insert a single value for that chosen type. For each instance of `UnionBlock`, Editors should be presented with a `ChoiceField`, with one option for each sub-block that is a member of the union. When they make a selection, the UI should be updated so that the native form widget for the selected block type is presented. Only a single form field for the block's value should ever be presented. A default value must always be provided, as an empty choice requires editors to make an interaction to reveal a form field for the value, when they have already made an interaction indicating that they wish to enter a value when they selected the `UnionBlock` (or the block containing it) in a `StreamBlock`. -#### Creation of subclasses +### Creation of subclasses `UnionBlock` must support definition by subclassing. As with `StreamBlock` and `StructBlock`, a developer must be able to create a custom block type inheriting from `UnionBlock`, where the sub-blocks defined as class level attributes are the options available to editors. @@ -112,11 +126,11 @@ my_union = UnionBlock([("text", TextBlock()), ("char", CharBlock())]) Any existing block type should be a valid member of a `UnionBlock`. -#### Implementation of the type selector +### Implementation of the type selector The base `UnionBlock` class must insert a `ChoiceField` into the UI, the choices of which have values that are the names of the declared sub-blocks, with the labels being the labels of those sub-blocks. -#### Parameters and meta-options +### Parameters and meta-options `UnionBlock.__init__` must support a `local_blocks` keyword argument, in the first position, as with `StreamBlock` and `StructBlock`. If provided, this parameter must be a list of 2-tuples, where the first element is the name of the block option, and the second element is the block instance to use if that option is selected. @@ -128,13 +142,13 @@ In addition to `local_blocks` (and the existing base block options), `UnionBlock - `value_label` (`Optional[str]`, default value: `"Value"`) - the label to associate with the value field presented to editors. This is provided for visual consistency and to reduce redundancy in the UI. As the sub-block labels will be used for the choices in the type selector field, they need not be repeated with the value field, and we prefer for less dynamic content in the UI. - `value_class` (`Optional[UnionValue]`, default value: `None`) - the value class to use to represent the value of a `UnionBlock` instance in Python. If no value is provided, a base `UnionValue` class must be used. -#### Help text +### Help text The help text declared for the `UnionBlock` must be presented at the top level of the block's UI. The help text declared on any sub-block must be presented alongside the form field for that sub-block, whenever it is present in the UI. -#### Validation +### Validation The implementation must validate that the selected type is a member of the declared sub-blocks. @@ -142,22 +156,68 @@ The implementation must validate the provided value, using the selected block ty If a `UnionBlock` is marked as required, a valid value must be provided. -#### Value classes +### Value classes -Similar to how `StructValue` is required for `StructBlock`, an extendable `UnionValue` class should be provided. This will allow developers to provide additional properties and methods, which will be required for the use of `UnionBlock` in the presentation layer. +Similar to how `StructValue` is required for `StructBlock`, an extendable `UnionValue` class must be provided. This will allow developers to provide additional properties and methods, which may be required for the use of `UnionBlock` in the presentation layer. The base `UnionValue` class must provide the following attributes: -- `block_type` - the name of the block type that was selected for the given instance. +- `block` - a reference to the relevant `UnionBlock` class; +- `block_type` - the name of the sub-block type that was selected for the given instance; and - `value` - the value for the given instance, in the native format of the selected sub-block type (e.g. if the sub-block is a `CharBlock` this will be a `str`, if it is a `ListBlock` it will be a `ListValue`, if it is a `StructBlock` it will be a `StructValue`, etc.). -#### Serialisation +### Behaviour in templates + +If Wagtail's `include_block` template tag is called with a `UnionValue` instance as its argument, it should defer to the `render_as_block` method of the associated `UnionBlock`. + +If a `template` parameter/meta-option was supplied for the `UnionBlock` instance, `render_as_block` should render that template, with context supplied by the `get_context` method. + +If no `template` parameter/meta-option was supplied for the `UnionBlock` instance, `render_as_block` should defer to the `render_as_block` method of the selected sub-block for that data instance. + +This cascading approach will allow developers to either: + +1. implement a single template that is used to render all union members; or +2. implement individual templates for each sub-block. + +### Serialisation + +The serialised value of a `UnionBlock` should be a JSON object, with the following required keys: + +- `type` - the name of the `UnionBlock`; +- `value` - the serialised value of the selected sub-block; and +- `__union_type__` - a special key which serves two purposes: + 1. identifying which sub-block was chosen (its value should be a `str`, the name of the chosen sub-block); and + 2. disambiguating the `UnionBlock` from other block types. + +### Deserialisation + +When deserialising the JSON representation of a `UnionBlock`, a `UnionValue` instance should be created. + +To deserialise the value of the chosen sub-block, Wagtail must: + +1. consult the `__union_type__` field of the serialised object; +2. retrieve the corresponding block definition from the `UnionBlock`'s `child_blocks`; and +3. defer deserialisation of the `value` field to the sub-block. + +If the sub-block indicated by `__union_type__` is not found on the `UnionBlock`'s definition (e.g. if the given sub-block is removed from the union after data has been committed to the database), a `UnionValue` with an empty `value` attribute should be created. + +### Impact on external libraries + +As `UnionBlock` is a new feature, it is not expected to impact existing external libraries. However, the following libraries may benefit from updates to support `UnionBlock`. + +### `wagtail-factories` + +A `UnionBlockFactory` should be created to aid the creation of test data in projects. + +### `wagtail-streamfield-migration-toolkit` + +A `StreamBlockToUnionBlockOperation` operation would be a useful tool for projects where `StreamBlock` has been used as a surrogate `UnionBlock`. + +### `wagtail-grapple` -#### Deserialisation +`wagtail-grapple` will likely need changes to handle `UnionBlock` in its GraphQL representation of stream fields. -#### Impact on external libraries -## Open Questions [^1]: https://github.com/developersociety/wagtail-link-block/blob/219ad4cb543e2ed6da900d7c5c7a4be59ef58d27/wagtail_link_block/blocks.py#L70 [^2]: https://github.com/developersociety/wagtail-link-block/blob/219ad4cb543e2ed6da900d7c5c7a4be59ef58d27/wagtail_link_block/blocks.py#L133 From d71d66f24a8471991f8d2da94331699d80c66622 Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Mon, 11 Mar 2024 10:00:19 +0000 Subject: [PATCH 06/11] Fix indentation --- text/000-union-block.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/000-union-block.md b/text/000-union-block.md index acce58cd..7c7a7e2a 100644 --- a/text/000-union-block.md +++ b/text/000-union-block.md @@ -97,7 +97,7 @@ The following example shows how a `LinkBlock`, equivalent to the other examples ``` python class LinkBlock(UnionBlock): page = PageChooserBlock(template="blocks/link_as_page.html") - url = URLBlock(template="blocks/link_as_url.html") + url = URLBlock(template="blocks/link_as_url.html") ``` This approach to the `LinkBlock` problem requires developers to write less code - significantly less when taking into account the custom JavaScript required when taking the approach illustrated by `wagtail-link-block`. @@ -119,7 +119,7 @@ For each instance of `UnionBlock`, Editors should be presented with a `ChoiceFie ``` python class MyUnion(UnionBlock): text = TextBlock() - char = CharBlock() + char = CharBlock() my_union = UnionBlock([("text", TextBlock()), ("char", CharBlock())]) ``` From 58c269aa8fb46be7e85acde61c89657bff8c6ec1 Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Mon, 11 Mar 2024 10:02:12 +0000 Subject: [PATCH 07/11] Clarify block inclusion statement --- text/000-union-block.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/000-union-block.md b/text/000-union-block.md index 7c7a7e2a..d584aa76 100644 --- a/text/000-union-block.md +++ b/text/000-union-block.md @@ -124,7 +124,7 @@ class MyUnion(UnionBlock): my_union = UnionBlock([("text", TextBlock()), ("char", CharBlock())]) ``` -Any existing block type should be a valid member of a `UnionBlock`. +Any existing Wagtail block type should be valid for inclusion as a member of a `UnionBlock`. ### Implementation of the type selector From 70d0062e0ce1cfb34c49c99828b0b98a88637e05 Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Mon, 11 Mar 2024 10:07:11 +0000 Subject: [PATCH 08/11] Fix header levels --- text/000-union-block.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/text/000-union-block.md b/text/000-union-block.md index d584aa76..589dee5f 100644 --- a/text/000-union-block.md +++ b/text/000-union-block.md @@ -205,15 +205,15 @@ If the sub-block indicated by `__union_type__` is not found on the `UnionBlock`' As `UnionBlock` is a new feature, it is not expected to impact existing external libraries. However, the following libraries may benefit from updates to support `UnionBlock`. -### `wagtail-factories` +#### `wagtail-factories` A `UnionBlockFactory` should be created to aid the creation of test data in projects. -### `wagtail-streamfield-migration-toolkit` +#### `wagtail-streamfield-migration-toolkit` A `StreamBlockToUnionBlockOperation` operation would be a useful tool for projects where `StreamBlock` has been used as a surrogate `UnionBlock`. -### `wagtail-grapple` +#### `wagtail-grapple` `wagtail-grapple` will likely need changes to handle `UnionBlock` in its GraphQL representation of stream fields. From b514b63b5c273d107df6f16856a0693accf35e66 Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Mon, 11 Mar 2024 12:59:11 +0000 Subject: [PATCH 09/11] Incorporate feedback from Ben D --- text/000-union-block.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/000-union-block.md b/text/000-union-block.md index 589dee5f..62d6ea72 100644 --- a/text/000-union-block.md +++ b/text/000-union-block.md @@ -211,7 +211,7 @@ A `UnionBlockFactory` should be created to aid the creation of test data in proj #### `wagtail-streamfield-migration-toolkit` -A `StreamBlockToUnionBlockOperation` operation would be a useful tool for projects where `StreamBlock` has been used as a surrogate `UnionBlock`. +A new `StreamBlockToUnionBlockOperation` operation would be a useful tool for projects where `StreamBlock` has been used as a surrogate `UnionBlock`. As part of the motivation for implementing `UnionBlock` is the historical (ab)use of single-value stream blocks, implementing this and documenting the migration pathway should be considered a necessary part of a complete solution. #### `wagtail-grapple` From e8f49138fcb611ceeeb562e0e4980ad772b8a3ab Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Sat, 16 Mar 2024 16:10:24 +0000 Subject: [PATCH 10/11] =?UTF-8?q?Rename=20from=20000=20=E2=86=92=20094?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- text/{000-union-block.md => 094-union-block.md} | 6 +++--- text/assets/{000 => 094}/stream-style-link.png | Bin text/assets/{000 => 094}/union-style-link.png | Bin 3 files changed, 3 insertions(+), 3 deletions(-) rename text/{000-union-block.md => 094-union-block.md} (99%) rename text/assets/{000 => 094}/stream-style-link.png (100%) rename text/assets/{000 => 094}/union-style-link.png (100%) diff --git a/text/000-union-block.md b/text/094-union-block.md similarity index 99% rename from text/000-union-block.md rename to text/094-union-block.md index 62d6ea72..2a2f5f43 100644 --- a/text/000-union-block.md +++ b/text/094-union-block.md @@ -1,4 +1,4 @@ -# RFC : Union Block +# RFC 94: Union Block * RFC: * Author: Joshua Munn @@ -52,11 +52,11 @@ class LinkBlock(blocks.StructBlock): link = LinkChooserBlock() ``` -![Link block implemented stream block style](./assets/000/stream-style-link.png) +![Link block implemented stream block style](./assets/094/stream-style-link.png) The UI generated for inserting a link requires editors to first select the "+" button to insert a block, and then choose the block type. In the typical case that the link value is _required_ this creates dissonance between what is required by data validation and what is communicated to users by visual language - requiring users to insert a block when that block is required is a sub-par experience. Compare this to a link block implemented as a `UnionBlock`: -![Link block implemented union block style](./assets/000/union-style-link.png) +![Link block implemented union block style](./assets/094/union-style-link.png) In this example all fields required to make a valid submission are immediately present in the UI, which clearly communicates the requirements of the system to users and prevents a class of validation errors from occurring. diff --git a/text/assets/000/stream-style-link.png b/text/assets/094/stream-style-link.png similarity index 100% rename from text/assets/000/stream-style-link.png rename to text/assets/094/stream-style-link.png diff --git a/text/assets/000/union-style-link.png b/text/assets/094/union-style-link.png similarity index 100% rename from text/assets/000/union-style-link.png rename to text/assets/094/union-style-link.png From 0d57283241342cfdfe205188e83186b8a681095a Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Sat, 16 Mar 2024 16:10:59 +0000 Subject: [PATCH 11/11] Fix LinkBlock as UnionBlock example --- text/094-union-block.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/text/094-union-block.md b/text/094-union-block.md index 2a2f5f43..7a723660 100644 --- a/text/094-union-block.md +++ b/text/094-union-block.md @@ -95,9 +95,13 @@ This approach is reasonable, however the author feels that the underlying concep The following example shows how a `LinkBlock`, equivalent to the other examples in this section, would be implemented using `UnionBlock`. ``` python -class LinkBlock(UnionBlock): +class LinkChooserBlock(UnionBlock): page = PageChooserBlock(template="blocks/link_as_page.html") url = URLBlock(template="blocks/link_as_url.html") + +class LinkBlock(blocks.StructBlock): + title = blocks.CharBlock() + link = LinkChooserBlock() ``` This approach to the `LinkBlock` problem requires developers to write less code - significantly less when taking into account the custom JavaScript required when taking the approach illustrated by `wagtail-link-block`.