kopia lustrzana https://github.com/wagtail/wagtail
Added blocks_by_name, first_block_by_name methods to StreamValue
- A shortcut for accessing StreamField blocks by namepull/8878/head
rodzic
0c9d72669c
commit
8cdb3f3a60
|
@ -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)
|
||||||
|
|
|
@ -97,6 +97,7 @@ Wagtail’s 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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue