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)
* 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)
* 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: 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)
@ -2204,7 +2206,6 @@ Changelog
* 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
* 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: 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)

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)
* 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`](streamfield_retrieving_blocks_by_name) (Tidiane Dia, Matt Westcott)
### 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()
```
(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)=
## Migrating RichTextFields to StreamField

Wyświetl plik

@ -1,7 +1,7 @@
import itertools
import uuid
from collections import OrderedDict, defaultdict
from collections.abc import MutableSequence
from collections.abc import Mapping, MutableSequence
from django import forms
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
@ -460,6 +460,52 @@ class StreamValue(MutableSequence):
def __repr__(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):
"""
Construct a StreamValue linked to the given StreamBlock,
@ -590,6 +636,20 @@ class StreamValue(MutableSequence):
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):
if not isinstance(other, StreamValue) or len(other) != len(self):
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):
"""form_classname from meta to be used as an additional class when rendering stream block"""