Added blocks_by_name, first_block_by_name methods to StreamValue

- A shortcut for accessing StreamField blocks by name
pull/8878/head
tijani 2021-06-21 21:16:44 +02:00 zatwierdzone przez LB (Ben Johnston)
rodzic 0c9d72669c
commit 8cdb3f3a60
5 zmienionych plików z 168 dodań i 2 usunięć

Wyświetl plik

@ -76,6 +76,8 @@ Changelog
* Add an extra confirmation prompt when deleting pages with a large number of child pages (Jaspreet Singh) * Add an extra confirmation prompt when deleting pages with a large number of child pages (Jaspreet Singh)
* Adopt the slim header in page listing views, with buttons moved under the "Actions" dropdown (Paarth Agarwal) * Adopt the slim header in page listing views, with buttons moved under the "Actions" dropdown (Paarth Agarwal)
* Improve help block styles in Windows High Contrast Mode with less reliance on communication via colour alone (Anuja Verma) * Improve help block styles in Windows High Contrast Mode with less reliance on communication via colour alone (Anuja Verma)
* Replace latin abbreviations (i.e. / e.g.) with common English phrases so that documentation is easier to understand (Dominik Lech)
* Add shortcut for accessing StreamField blocks by block name with new `blocks_by_name` and `first_block_by_name` methods on `StreamValue` (Tidiane Dia, Matt Westcott)
* Fix: Typo in `ResumeWorkflowActionFormatter` message (Stefan Hammer) * Fix: Typo in `ResumeWorkflowActionFormatter` message (Stefan Hammer)
* Fix: Throw a meaningful error when saving an image to an unrecognised image format (Christian Franke) * Fix: Throw a meaningful error when saving an image to an unrecognised image format (Christian Franke)
* Fix: Remove extra padding for headers with breadcrumbs on mobile viewport (Steven Steinwand) * Fix: Remove extra padding for headers with breadcrumbs on mobile viewport (Steven Steinwand)
@ -2204,7 +2206,6 @@ Changelog
* Added a system check to warn if Pillow is compiled without JPEG / PNG support * Added a system check to warn if Pillow is compiled without JPEG / PNG support
* Page chooser now prevents users from selecting the root node where this would be invalid * Page chooser now prevents users from selecting the root node where this would be invalid
* New translations for Dutch (Netherlands), Georgian, Swedish and Turkish (Turkey) * New translations for Dutch (Netherlands), Georgian, Swedish and Turkish (Turkey)
* Replace latin abbreviations (i.e. / e.g.) with common English phrases so that documentation is easier to understand (Dominik Lech)
* Fix: Page slugs are no longer auto-updated from the page title if the page is already published * Fix: Page slugs are no longer auto-updated from the page title if the page is already published
* Fix: Deleting a page permission from the groups admin UI does not immediately submit the form * Fix: Deleting a page permission from the groups admin UI does not immediately submit the form
* Fix: Wagtail userbar is shown on pages that do not pass a `page` variable to the template (e.g. because they override the `serve` method) * Fix: Wagtail userbar is shown on pages that do not pass a `page` variable to the template (e.g. because they override the `serve` method)

Wyświetl plik

@ -97,6 +97,7 @@ Wagtails page preview is now available in a side panel within the page editor
* Adopt the slim header in page listing views, with buttons moved under the "Actions" dropdown (Paarth Agarwal) * Adopt the slim header in page listing views, with buttons moved under the "Actions" dropdown (Paarth Agarwal)
* Improve help block styles in Windows High Contrast Mode with less reliance on communication via colour alone (Anuja Verma) * Improve help block styles in Windows High Contrast Mode with less reliance on communication via colour alone (Anuja Verma)
* Replace latin abbreviations (i.e. / e.g.) with common English phrases so that documentation is easier to understand (Dominik Lech) * Replace latin abbreviations (i.e. / e.g.) with common English phrases so that documentation is easier to understand (Dominik Lech)
* Add shortcut for accessing StreamField blocks by block name with new [`blocks_by_name` and `first_block_by_name` methods on `StreamValue`](streamfield_retrieving_blocks_by_name) (Tidiane Dia, Matt Westcott)
### Bug fixes ### Bug fixes

Wyświetl plik

@ -519,6 +519,43 @@ my_page.body.append(('paragraph', RichText("<p>And they all lived happily ever a
my_page.save() my_page.save()
``` ```
(streamfield_retrieving_blocks_by_name)=
## Retrieving blocks by name
```{versionadded} 4.0
The `blocks_by_name` and `first_block_by_name` methods were added.
```
StreamField values provide a `blocks_by_name` method for retrieving all blocks of a given name:
```python
my_page.body.blocks_by_name('heading') # returns a list of 'heading' blocks
```
Calling `blocks_by_name` with no arguments returns a `dict`-like object, mapping block names to the list of blocks of that name. This is particularly useful in template code, where passing arguments isn't possible:
```html+django
<h2>Table of contents</h2>
<ol>
{% for heading_block in page.body.blocks_by_name.heading %}
<li>{{ heading_block.value }}</li>
{% endfor %}
</ol>
```
The `first_block_by_name` method returns the first block of the given name in the stream, or `None` if no matching block is found:
```
hero_image = my_page.body.first_block_by_name('image')
```
`first_block_by_name` can also be called without arguments to return a `dict`-like mapping:
```html+django
<div class="hero-image">{{ page.body.first_block_by_name.image }}</div>
```
(streamfield_migrating_richtext)= (streamfield_migrating_richtext)=
## Migrating RichTextFields to StreamField ## Migrating RichTextFields to StreamField

Wyświetl plik

@ -1,7 +1,7 @@
import itertools import itertools
import uuid import uuid
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict
from collections.abc import MutableSequence from collections.abc import Mapping, MutableSequence
from django import forms from django import forms
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
@ -460,6 +460,52 @@ class StreamValue(MutableSequence):
def __repr__(self): def __repr__(self):
return repr(list(self)) return repr(list(self))
class BlockNameLookup(Mapping):
"""
Dict-like object returned from `blocks_by_name`, for looking up a stream's blocks by name.
Uses lazy evaluation on access, so that we're not redundantly constructing StreamChild
instances for blocks of different names.
"""
def __init__(self, stream_value, find_all=True):
self.stream_value = stream_value
self.block_names = stream_value.stream_block.child_blocks.keys()
self.find_all = (
find_all # whether to return all results rather than just the first
)
def __getitem__(self, block_name):
result = [] if self.find_all else None
if block_name not in self.block_names:
# skip the search and return an empty result
return result
for i in range(len(self.stream_value)):
# Skip over blocks that have not yet been instantiated from _raw_data and are of
# different names to the one we're looking for
if (
self.stream_value._bound_blocks[i] is None
and self.stream_value._raw_data[i]["type"] != block_name
):
continue
block = self.stream_value[i]
if block.block_type == block_name:
if self.find_all:
result.append(block)
else:
return block
return result
def __iter__(self):
for block_name in self.block_names:
yield block_name
def __len__(self):
return len(self.block_names)
def __init__(self, stream_block, stream_data, is_lazy=False, raw_text=None): def __init__(self, stream_block, stream_data, is_lazy=False, raw_text=None):
""" """
Construct a StreamValue linked to the given StreamBlock, Construct a StreamValue linked to the given StreamBlock,
@ -590,6 +636,20 @@ class StreamValue(MutableSequence):
return prep_value return prep_value
def blocks_by_name(self, block_name=None):
lookup = StreamValue.BlockNameLookup(self, find_all=True)
if block_name:
return lookup[block_name]
else:
return lookup
def first_block_by_name(self, block_name=None):
lookup = StreamValue.BlockNameLookup(self, find_all=False)
if block_name:
return lookup[block_name]
else:
return lookup
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, StreamValue) or len(other) != len(self): if not isinstance(other, StreamValue) or len(other) != len(self):
return False return False

Wyświetl plik

@ -3969,6 +3969,73 @@ class TestStreamBlock(WagtailTestUtils, SimpleTestCase):
}, },
) )
def test_block_names(self):
class ArticleBlock(blocks.StreamBlock):
heading = blocks.CharBlock()
paragraph = blocks.TextBlock()
date = blocks.DateBlock()
block = ArticleBlock()
value = block.to_python(
[
{
"type": "heading",
"value": "My title",
},
{
"type": "paragraph",
"value": "My first paragraph",
},
{
"type": "paragraph",
"value": "My second paragraph",
},
]
)
blocks_by_name = value.blocks_by_name()
assert isinstance(blocks_by_name, blocks.StreamValue.BlockNameLookup)
# unpack results to a dict of {block name: list of block values} for easier comparison
result = {
block_name: [block.value for block in blocks]
for block_name, blocks in blocks_by_name.items()
}
self.assertEqual(
result,
{
"heading": ["My title"],
"paragraph": ["My first paragraph", "My second paragraph"],
"date": [],
},
)
paragraph_blocks = value.blocks_by_name(block_name="paragraph")
# We can also access by indexing on the stream
self.assertEqual(paragraph_blocks, value.blocks_by_name()["paragraph"])
self.assertEqual(len(paragraph_blocks), 2)
for block in paragraph_blocks:
self.assertEqual(block.block_type, "paragraph")
self.assertEqual(value.blocks_by_name(block_name="date"), [])
self.assertEqual(value.blocks_by_name(block_name="invalid_type"), [])
first_heading_block = value.first_block_by_name(block_name="heading")
self.assertEqual(first_heading_block.block_type, "heading")
self.assertEqual(first_heading_block.value, "My title")
self.assertIs(value.first_block_by_name(block_name="date"), None)
self.assertIs(value.first_block_by_name(block_name="invalid_type"), None)
# first_block_by_name with no argument returns a dict-like lookup of first blocks per name
first_blocks_by_name = value.first_block_by_name()
first_heading_block = first_blocks_by_name["heading"]
self.assertEqual(first_heading_block.block_type, "heading")
self.assertEqual(first_heading_block.value, "My title")
self.assertIs(first_blocks_by_name["date"], None)
self.assertIs(first_blocks_by_name["invalid_type"], None)
def test_adapt_with_classname_via_class_meta(self): def test_adapt_with_classname_via_class_meta(self):
"""form_classname from meta to be used as an additional class when rendering stream block""" """form_classname from meta to be used as an additional class when rendering stream block"""