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 classname = None
group = '' 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 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 (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: if not self.meta.label:
self.label = capfirst(force_str(name).replace('_', ' ')) 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 @property
def media(self): def media(self):
return forms.Media() return forms.Media()
@ -395,9 +411,9 @@ class Block(metaclass=BaseBlock):
def __eq__(self, other): def __eq__(self, other):
""" """
Implement equality on block objects so that two blocks with matching definitions are considered 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 equal. Block objects are intended to be immutable with the exception of set_name() and any meta
'matching definitions' means that both the 'name' property and the constructor args/kwargs - as attributes identified in MUTABLE_META_ATTRIBUTES, so checking these along with the result of
captured in _constructor_args - are equal on both blocks.) 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 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 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 # the migration, rather than leaving the migration vulnerable to future changes to FooBlock / BarBlock
# in models.py. # 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: class BoundBlock:

Wyświetl plik

@ -51,6 +51,11 @@ class BaseStreamBlock(Block):
self.dependencies = self.child_blocks.values() 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): def get_default(self):
""" """
Default values set on a StreamBlock should be a list of (type_name, value) tuples - 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 max_num = None
block_counts = {} block_counts = {}
MUTABLE_META_ATTRIBUTES = ['required', 'min_num', 'max_num', 'block_counts']
class StreamBlock(BaseStreamBlock, metaclass=DeclarativeSubBlocksMetaclass): class StreamBlock(BaseStreamBlock, metaclass=DeclarativeSubBlocksMetaclass):
pass pass

Wyświetl plik

@ -51,13 +51,31 @@ class Creator:
class StreamField(models.Field): class StreamField(models.Field):
def __init__(self, block_types, **kwargs): 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) super().__init__(**kwargs)
if isinstance(block_types, Block): if isinstance(block_types, Block):
# use the passed block as the top-level block
self.stream_block = block_types self.stream_block = block_types
elif isinstance(block_types, type): 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: 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): def get_internal_type(self):
return 'TextField' return 'TextField'