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)
|
||||
* 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)
|
||||
|
|
|
@ -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)
|
||||
* 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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue