A shortcut for accessing StreamField blocks by name
See original GitHub issueIs your proposal related to a problem?
It’s common for people to want to retrieve a block of a particular type from a StreamField value - for example, if ‘hero image’ is one of the block types defined in a page’s content, a developer may want to pull out the first such hero image for use in listings or as a social share image. (An example of this in the wild…)
This can be done in user code (either in Python or Django template code) by looping over the StreamField value until the appropriate block is found - however, I think this is a common enough scenario that Wagtail should provide a standard method / idiom for it.
Describe the solution you’d like
The StreamValue
class could implement a by_type
method that returns a list of blocks matching the given type:
page.body.by_type('hero_image') # returns a list of all `hero_image` blocks in the `body` StreamField
Passing a parameter to by_type
in this way would not be possible within a template, so as an additional feature, by_type
could return a (lazily-evaluated?) dict consisting of all blocks organised by type when invoked with no parameters. This would make it possible to write something like:
<h1>{{ page.body.by_type.heading.0 }}</h1>
The by_type
method will need tests and documentation.
Describe alternatives you’ve considered
StreamValue could be updated to behave as something like a dict as well as a list, e.g.page.body['hero_image']
- however, this feels a bit ambiguous about whether it should return a single item or a list. (Also, in template code where foo.bar
is equivalent to foo['bar']
, it means the block names end up in the same namespace as methods on StreamValue, which feels icky.)
Given that people will probably be using this for retrieving a single item most of the time, we could borrow BeautifulSoup’s pattern of page.body.find('hero_image')
(or page.body.find(type='hero_image')
?) which returns the first item or None, and page.body.find_all('hero_image')
which returns a list. It’s not so obvious how to make this syntax work nicely within templates, though.
Additional context
Perhaps something for a later phase, but it’s worth bearing in mind that for this kind of StreamField access, optimisations like bulk_to_python
and #7239 are actually counter-productive, since they’re pre-fetching all database objects in the stream but we only want one of them. Maybe this calls for us to treat StreamValues more like querysets, where the actual data retrieval happens as late as possible and these kinds of ‘filter’ operations act as modifiers to that final lookup.
Issue Analytics
- State:
- Created 2 years ago
- Reactions:5
- Comments:9 (4 by maintainers)
Top GitHub Comments
This seems like a great idea! What do you think about calling it
blocks_by_type
? Justby_type
feels a little ambiguous to me.@chosak That sounds fair. The distinction between ‘types’ and ‘names’ is a bit blurred, partly due to the detail that when you write something like
blocks.CharBlock()
in a block definition, you’re instantiating CharBlock even though you’re not notionally creating any objects, just writing a definition. And that’s not something we’re going to solve here…I’m happy to go with
blocks_by_name
as the less ambiguous choice. The one snag is that - following the names from @Tijani-Dia’s PR -blocks_of_name
andfirst_block_of_name
don’t sound right, because blocks don’t “belong to” a name. So maybe the answer is to overload the methods, depending on whether you pass a block name or not:blocks_by_name('heading')
- returns a list of allheading
blocksblocks_by_name()
- returns a dict mapping block names to the list of blocks for that name, so that you can write things like{% for block in page.body.blocks_by_name.heading %}
in templatesfirst_block_by_name('heading')
- returns the firstheading
block, or None if no blocks of that type existand maybe (for symmetry)…
first_block_by_name()
- returns a dict mapping block names to the first block for that name (or None if no such blocks exist), so that you can write{{ page.body.first_block_by_name.heading }}