diff --git a/wagtail/core/blocks/base.py b/wagtail/core/blocks/base.py index 1beb5463dd..8adb22f731 100644 --- a/wagtail/core/blocks/base.py +++ b/wagtail/core/blocks/base.py @@ -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: diff --git a/wagtail/core/blocks/stream_block.py b/wagtail/core/blocks/stream_block.py index 3fd933033d..ed6b060408 100644 --- a/wagtail/core/blocks/stream_block.py +++ b/wagtail/core/blocks/stream_block.py @@ -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 diff --git a/wagtail/core/fields.py b/wagtail/core/fields.py index dfcc17e94e..cc9477d55c 100644 --- a/wagtail/core/fields.py +++ b/wagtail/core/fields.py @@ -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'