Allow StreamBlock to pick up blank, min_num, max_num and block_counts kwargs from StreamField

pull/6730/head
Matt Westcott 2021-01-21 18:34:46 +00:00
rodzic f03c11f1d1
commit 643bbfc600
3 zmienionych plików z 54 dodań i 6 usunięć

Wyświetl plik

@ -47,6 +47,12 @@ class Block(metaclass=BaseBlock):
classname = None
group = ''
# Attributes of Meta which can legally be modified after the block has been instantiated.
# Used to implement __eq__. label is not included here, despite it technically being mutable via
# set_name, since its value must originate from either the constructor arguments or set_name,
# both of which are captured by the equality test, so checking label as well would be redundant.
MUTABLE_META_ATTRIBUTES = []
"""
Setting a 'dependencies' list serves as a shortcut for the common case where a complex block type
(such as struct, list or stream) relies on one or more inner block objects, and needs to ensure that
@ -121,6 +127,16 @@ class Block(metaclass=BaseBlock):
if not self.meta.label:
self.label = capfirst(force_str(name).replace('_', ' '))
def set_meta_options(self, options):
"""
Called when this block is used as the top-level block of a StreamField, to pass on any options
from the StreamField constructor that ought to be handled by the block, e.g.
body = StreamField(SomeStreamBlock(), max_num=5)
"""
# Ignore all options here; block types that are allowed at the top level (i.e. currently just
# StreamBlock) and recognise these options will override this method
pass
@property
def media(self):
return forms.Media()
@ -395,9 +411,9 @@ class Block(metaclass=BaseBlock):
def __eq__(self, other):
"""
Implement equality on block objects so that two blocks with matching definitions are considered
equal. (Block objects are intended to be immutable with the exception of set_name(), so here
'matching definitions' means that both the 'name' property and the constructor args/kwargs - as
captured in _constructor_args - are equal on both blocks.)
equal. Block objects are intended to be immutable with the exception of set_name() and any meta
attributes identified in MUTABLE_META_ATTRIBUTES, so checking these along with the result of
deconstruct (which captures the constructor arguments) is sufficient to identify (valid) differences.
This was originally necessary as a workaround for https://code.djangoproject.com/ticket/24340
in Django <1.9; the deep_deconstruct function used to detect changes for migrations did not
@ -439,7 +455,14 @@ class Block(metaclass=BaseBlock):
# the migration, rather than leaving the migration vulnerable to future changes to FooBlock / BarBlock
# in models.py.
return (self.name == other.name) and (self.deconstruct() == other.deconstruct())
return (
self.name == other.name
and self.deconstruct() == other.deconstruct()
and all(
getattr(self.meta, attr, None) == getattr(other.meta, attr, None)
for attr in self.MUTABLE_META_ATTRIBUTES
)
)
class BoundBlock:

Wyświetl plik

@ -51,6 +51,11 @@ class BaseStreamBlock(Block):
self.dependencies = self.child_blocks.values()
def set_meta_options(self, opts):
for attr in ['required', 'min_num', 'max_num', 'block_counts']:
if attr in opts:
setattr(self.meta, attr, opts[attr])
def get_default(self):
"""
Default values set on a StreamBlock should be a list of (type_name, value) tuples -
@ -381,6 +386,8 @@ class BaseStreamBlock(Block):
max_num = None
block_counts = {}
MUTABLE_META_ATTRIBUTES = ['required', 'min_num', 'max_num', 'block_counts']
class StreamBlock(BaseStreamBlock, metaclass=DeclarativeSubBlocksMetaclass):
pass

Wyświetl plik

@ -51,13 +51,31 @@ class Creator:
class StreamField(models.Field):
def __init__(self, block_types, **kwargs):
# extract kwargs that are to be passed on to the block, not handled by super
block_opts = {}
for arg in ['min_num', 'max_num', 'block_counts']:
if arg in kwargs:
block_opts[arg] = kwargs.pop(arg)
# for a top-level block, the 'blank' kwarg (defaulting to False) always overrides the
# block's own 'required' meta attribute, even if not passed explicitly; this ensures
# that the field and block have consistent definitions
block_opts['required'] = not kwargs.get('blank', False)
super().__init__(**kwargs)
if isinstance(block_types, Block):
# use the passed block as the top-level block
self.stream_block = block_types
elif isinstance(block_types, type):
self.stream_block = block_types(required=not self.blank)
# block passed as a class - instantiate it
self.stream_block = block_types()
else:
self.stream_block = StreamBlock(block_types, required=not self.blank)
# construct a top-level StreamBlock from the list of block types
self.stream_block = StreamBlock(block_types)
self.stream_block.set_meta_options(block_opts)
def get_internal_type(self):
return 'TextField'