Implement bulk_to_python on StreamBlock

pull/6469/head
Matt Westcott 2020-04-21 22:41:11 +01:00
rodzic 47faf79308
commit 56b2d86e9a
2 zmienionych plików z 107 dodań i 0 usunięć

Wyświetl plik

@ -252,6 +252,57 @@ class BaseStreamBlock(Block):
if child_data['type'] in self.child_blocks
], is_lazy=True)
def bulk_to_python(self, values):
# 'values' is a list of streams, each stream being a list of dicts with 'type', 'value' and
# optionally 'id'.
# We will iterate over these streams, constructing:
# 1) a set of per-child-block lists ('child_inputs'), to be sent to each child block's
# bulk_to_python method in turn (giving us 'child_outputs')
# 2) a 'block map' of each stream, telling us the type and id of each block and the index we
# need to look up in the corresponding child_outputs list to obtain its final value
child_inputs = defaultdict(list)
block_maps = []
for stream in values:
block_map = []
for block_dict in stream:
block_type = block_dict['type']
if block_type not in self.child_blocks:
# skip any blocks with an unrecognised type
continue
child_input_list = child_inputs[block_type]
child_index = len(child_input_list)
child_input_list.append(block_dict['value'])
block_map.append(
(block_type, block_dict.get('id'), child_index)
)
block_maps.append(block_map)
# run each list in child_inputs through the relevant block's bulk_to_python
# to obtain child_outputs
child_outputs = {
block_type: self.child_blocks[block_type].bulk_to_python(child_input_list)
for block_type, child_input_list in child_inputs.items()
}
# for each stream, go through the block map, picking out the appropriately-indexed
# value from the relevant list in child_outputs
return [
StreamValue(
self,
[
(block_type, child_outputs[block_type][child_index], id)
for block_type, id, child_index in block_map
],
is_lazy=False
)
for block_map in block_maps
]
def get_prep_value(self, value):
if not value:
# Falsy values (including None, empty string, empty list, and

Wyświetl plik

@ -3327,6 +3327,62 @@ class TestStructBlockWithFixtures(TestCase):
])
class TestStreamBlockWithFixtures(TestCase):
fixtures = ['test.json']
def test_bulk_to_python(self):
stream_block = blocks.StreamBlock([
('page', blocks.PageChooserBlock()),
('heading', blocks.CharBlock()),
])
# The naive implementation of bulk_to_python (calling to_python on each item) would perform
# NO queries, as StreamBlock.to_python returns a lazy StreamValue that only starts calling
# to_python on its children (and thus triggering DB queries) when its items are accessed.
# This is a good thing for a standalone to_python call, because loading a model instance
# with a StreamField in it will immediately call StreamField.to_python which in turn calls
# to_python on the top-level StreamBlock, and we really don't want
# SomeModelWithAStreamField.objects.get(id=1) to immediately trigger a cascading fetch of
# all objects referenced in the StreamField.
#
# However, for bulk_to_python that's bad, as it means each stream in the list would end up
# doing its own object lookups in isolation, missing the opportunity to group them together
# into a single call to the child block's bulk_to_python. Therefore, the ideal outcome is
# that we perform one query now (covering all PageChooserBlocks across all streams),
# returning a list of non-lazy StreamValues.
with self.assertNumQueries(1):
results = stream_block.bulk_to_python([
[{'type': 'heading', 'value': 'interesting pages'}, {'type': 'page', 'value': 2}, {'type': 'page', 'value': 3}],
[{'type': 'heading', 'value': 'pages written by dogs'}, {'type': 'woof', 'value': 'woof woof'}],
[{'type': 'heading', 'value': 'boring pages'}, {'type': 'page', 'value': 4}],
])
# If bulk_to_python has indeed given us non-lazy StreamValues, then no further queries
# should be performed when iterating over its child blocks.
with self.assertNumQueries(0):
block_types = [
[block.block_type for block in stream]
for stream in results
]
self.assertEqual(block_types, [
['heading', 'page', 'page'],
['heading'],
['heading', 'page'],
])
with self.assertNumQueries(0):
block_values = [
[block.value for block in stream]
for stream in results
]
self.assertEqual(block_values, [
['interesting pages', Page.objects.get(id=2), Page.objects.get(id=3)],
['pages written by dogs'],
['boring pages', Page.objects.get(id=4)],
])
class TestPageChooserBlock(TestCase):
fixtures = ['test.json']