diff --git a/example/wagtailstreamforms_fields.py b/example/wagtailstreamforms_fields.py index b23c849..ff22649 100644 --- a/example/wagtailstreamforms_fields.py +++ b/example/wagtailstreamforms_fields.py @@ -17,14 +17,25 @@ def get_options(self, block_value): options.update({"required": True}) return options + def get_form_block_class(self): + return blocks.StructBlock + + def get_local_blocks(self): + return [ + ("label", blocks.CharBlock()), + ("help_text", blocks.CharBlock(required=False)), + ] + + def get_form_block_kwargs(self): + return { + "icon": self.icon, + "label": self.label, + } + def get_form_block(self): - return blocks.StructBlock( - [ - ("label", blocks.CharBlock()), - ("help_text", blocks.CharBlock(required=False)), - ], - icon=self.icon, - label=self.label, + return self.get_form_block_class()( + self.get_local_blocks(), + **self.get_form_block_kwargs(), ) @@ -49,18 +60,20 @@ def get_regex_choices(self): ("^[a-zA-Z0-9]+$", "Letters and numbers only"), ) + def get_local_blocks(self): + return [ + ("label", blocks.CharBlock()), + ("help_text", blocks.CharBlock(required=False)), + ("required", blocks.BooleanBlock(required=False)), + ("regex", blocks.ChoiceBlock(choices=self.get_regex_choices())), + ("error_message", blocks.CharBlock()), + ("default_value", blocks.CharBlock(required=False)), + ] + def get_form_block(self): - return blocks.StructBlock( - [ - ("label", blocks.CharBlock()), - ("help_text", blocks.CharBlock(required=False)), - ("required", blocks.BooleanBlock(required=False)), - ("regex", blocks.ChoiceBlock(choices=self.get_regex_choices())), - ("error_message", blocks.CharBlock()), - ("default_value", blocks.CharBlock(required=False)), - ], - icon=self.icon, - label=self.label, + return self.get_form_block_class()( + self.get_local_blocks(), + **self.get_form_block_kwargs() ) @@ -79,13 +92,15 @@ def get_options(self, block_value): options.update({"queryset": self.get_queryset()}) return options + def get_local_blocks(self): + return [ + ("label", blocks.CharBlock()), + ("help_text", blocks.CharBlock(required=False)), + ("required", blocks.BooleanBlock(required=False)), + ] + def get_form_block(self): - return blocks.StructBlock( - [ - ("label", blocks.CharBlock()), - ("help_text", blocks.CharBlock(required=False)), - ("required", blocks.BooleanBlock(required=False)), - ], - icon=self.icon, - label=self.label, + return self.get_form_block_class()( + self.get_local_blocks(), + **self.get_form_block_kwargs() ) diff --git a/wagtailstreamforms/fields.py b/wagtailstreamforms/fields.py index 2d68201..5e8bcb4 100644 --- a/wagtailstreamforms/fields.py +++ b/wagtailstreamforms/fields.py @@ -25,7 +25,6 @@ class SingleLineTextField(BaseField): """ if cls is None: - def decorator(cls): register(field_name, cls) return cls @@ -147,24 +146,60 @@ def get_options(self, block_value): "initial": self.get_formfield_initial(block_value), } + def get_form_block_class(self): + """ + The StreamField block class to be created for this field. This is + almost always a StructBlock, but conceptually it could be any structural block. + + Override this method and return a subclass of a structural block for further + control over the block class, such as overriding the clean() method to provide + custom validation. + :return: The ``wagtail.blocks.StructBlock`` to be used in the StreamField + """ + return blocks.StructBlock + + def get_form_block_kwargs(self): + """The kwargs to be passed into the StreamField block class. + + Override this to provide additional kwargs to the StreamField block class. + + :return: The kwargs to be passed into the StreamField block class + """ + return { + "icon": self.icon, + "label": self.label, + } + def get_form_block(self): - """The StreamField StructBlock. + """The StreamField block class. Override this to provide additional fields in the StreamField. - :return: The ``wagtail.core.blocks.StructBlock`` to be used in the StreamField - """ - return blocks.StructBlock( - [ - ("label", blocks.CharBlock()), - ("help_text", blocks.CharBlock(required=False)), - ("required", blocks.BooleanBlock(required=False)), - ("default_value", blocks.CharBlock(required=False)), - ], - icon=self.icon, - label=self.label, + :return: The resuld of calling get_form_block_class() is to be used in the + StreamField + """ + return self.get_form_block_class()( + self.get_local_blocks(), + **self.get_form_block_kwargs(), ) + def get_local_blocks(self): + """The blocks that should be added to the StructBlock for this field. + + Override this to add blocks to, or remove blocks from, the StructBlock + before it is instantiated. This is useful because adding blocks to the + StructBlock after instantiation requires mucking with the StructBlock's + internal, undocumented API. + + :return: A list of tuples containing the block name and block instance. + """ + return [ + ("label", blocks.CharBlock()), + ("help_text", blocks.CharBlock(required=False)), + ("required", blocks.BooleanBlock(required=False)), + ("default_value", blocks.CharBlock(required=False)), + ] + class HookMultiSelectFormField(forms.MultipleChoiceField): widget = forms.CheckboxSelectMultiple diff --git a/wagtailstreamforms/forms.py b/wagtailstreamforms/forms.py index 0880531..754d183 100644 --- a/wagtailstreamforms/forms.py +++ b/wagtailstreamforms/forms.py @@ -26,20 +26,8 @@ def formfields(self): formfields = OrderedDict() - registered_fields = get_fields() - for field in self.fields: - field_type = field.get("type") - field_value = field.get("value") - - # check we have the field - if field_type not in registered_fields: - raise AttributeError("Could not find a registered field of type %s" % field_type) - - # get the field - registered_cls = registered_fields[field_type]() - field_name = registered_cls.get_formfield_name(field_value) - field_cls = registered_cls.get_formfield(field_value) + field_name, field_cls = self.create_field_class(field) formfields[field_name] = field_cls # add fields to uniquely identify the form @@ -48,6 +36,45 @@ def formfields(self): return formfields + def create_field_class(self, field): + """ + Encapsulates the field_cls creation such that there is a method to override + when the field_cls needs to be modified. + + :param field: StreamBlock representing a form field; an item in + fields.stream_data + :return: a tuple of field_name - the name to use in the html form for this + field, and field_cls - in instantiated field class that may be added to a form + """ + registered_fields = get_fields() + + field_type = field.get("type") + field_value = field.get("value") + # check we have the field + if field_type not in registered_fields: + raise AttributeError( + "Could not find a registered field of type %s" % field_type + ) + + # get the field + registered_cls = registered_fields[field_type]() + field_cls = registered_cls.get_formfield(field_value) + field_name = self.create_field_name(registered_cls, field) + return field_name, field_cls + + def create_field_name(self, registered_cls, field): + """ + Encapsulates the field_name creation such that there is a method to override + when the field_name needs to be modified. + + :param field: StreamBlock representing a form field; an item in + fields.stream_data + :param registered_cls: The subclass of wagtailstreamforms.fields.BaseField + that defined this form field + :return: a name to use in the html form for this field + """ + return registered_cls.get_formfield_name(field.get("value")) + def get_form_class(self): return type(str("StreamformsForm"), (BaseForm,), self.formfields) diff --git a/wagtailstreamforms/streamfield.py b/wagtailstreamforms/streamfield.py index d07d344..1f6a7d5 100644 --- a/wagtailstreamforms/streamfield.py +++ b/wagtailstreamforms/streamfield.py @@ -26,13 +26,21 @@ def __init__(self, local_blocks=None, **kwargs) -> None: "'%s' must be a subclass of '%s'" % (field_class, BaseField) ) - # assign the block - block = field_class().get_form_block() - block.set_name(name) - self._child_blocks[name] = block + # assign the block if instantiation returns non None + if block := self.instantiate_block(field_class, name): + self._child_blocks[name] = block self._dependencies = self._child_blocks.values() + def instantiate_block(self, field_class, name): + """ + Provides an extension point for changing attributes of blocks, like the + meta.group + """ + block = field_class().get_form_block() + block.set_name(name) + return block + @property def child_blocks(self): return self._child_blocks @@ -43,6 +51,6 @@ def dependencies(self): class FormFieldsStreamField(StreamField): - def __init__(self, block_types, **kwargs) -> None: - super().__init__(block_types, **kwargs) + def __init__(self, block_types, use_json_field=None, **kwargs): + super().__init__(block_types, use_json_field=use_json_field, **kwargs) self.stream_block = FormFieldStreamBlock(block_types, required=not self.blank) diff --git a/wagtailstreamforms/wagtailstreamforms_fields.py b/wagtailstreamforms/wagtailstreamforms_fields.py index ba3aa7f..e3719eb 100644 --- a/wagtailstreamforms/wagtailstreamforms_fields.py +++ b/wagtailstreamforms/wagtailstreamforms_fields.py @@ -3,7 +3,6 @@ from django import forms from django.utils.translation import gettext_lazy as _ from wagtail import blocks - from wagtailstreamforms.conf import get_setting from wagtailstreamforms.fields import BaseField, register @@ -67,17 +66,19 @@ def get_options(self, block_value): options.update({"choices": choices}) return options + def get_local_blocks(self): + return [ + ("label", blocks.CharBlock()), + ("help_text", blocks.CharBlock(required=False)), + ("required", blocks.BooleanBlock(required=False)), + ("empty_label", blocks.CharBlock(required=False)), + ("choices", blocks.ListBlock(blocks.CharBlock(label="Option"))), + ] + def get_form_block(self): - return blocks.StructBlock( - [ - ("label", blocks.CharBlock()), - ("help_text", blocks.CharBlock(required=False)), - ("required", blocks.BooleanBlock(required=False)), - ("empty_label", blocks.CharBlock(required=False)), - ("choices", blocks.ListBlock(blocks.CharBlock(label="Option"))), - ], - icon=self.icon, - label=self.label, + return self.get_form_block_class()( + self.get_local_blocks(), + **self.get_form_block_kwargs(), ) @@ -97,16 +98,18 @@ def get_options(self, block_value): options.update({"choices": choices}) return options + def get_local_blocks(self): + return [ + ("label", blocks.CharBlock()), + ("help_text", blocks.CharBlock(required=False)), + ("required", blocks.BooleanBlock(required=False)), + ("choices", blocks.ListBlock(blocks.CharBlock(label="Option"))), + ] + def get_form_block(self): - return blocks.StructBlock( - [ - ("label", blocks.CharBlock()), - ("help_text", blocks.CharBlock(required=False)), - ("required", blocks.BooleanBlock(required=False)), - ("choices", blocks.ListBlock(blocks.CharBlock(label="Option"))), - ], - icon=self.icon, - label=self.label, + return self.get_form_block_class()( + self.get_local_blocks(), + **self.get_form_block_kwargs(), ) @@ -127,16 +130,18 @@ def get_options(self, block_value): options.update({"choices": choices}) return options + def get_local_blocks(self): + return [ + ("label", blocks.CharBlock()), + ("help_text", blocks.CharBlock(required=False)), + ("required", blocks.BooleanBlock(required=False)), + ("choices", blocks.ListBlock(blocks.CharBlock(label="Option"))), + ] + def get_form_block(self): - return blocks.StructBlock( - [ - ("label", blocks.CharBlock()), - ("help_text", blocks.CharBlock(required=False)), - ("required", blocks.BooleanBlock(required=False)), - ("choices", blocks.ListBlock(blocks.CharBlock(label="Option"))), - ], - icon=self.icon, - label=self.label, + return self.get_form_block_class()( + self.get_local_blocks(), + **self.get_form_block_kwargs(), ) @@ -157,16 +162,18 @@ def get_options(self, block_value): options.update({"choices": choices}) return options + def get_local_blocks(self): + return [ + ("label", blocks.CharBlock()), + ("help_text", blocks.CharBlock(required=False)), + ("required", blocks.BooleanBlock(required=False)), + ("choices", blocks.ListBlock(blocks.CharBlock(label="Option"))), + ] + def get_form_block(self): - return blocks.StructBlock( - [ - ("label", blocks.CharBlock()), - ("help_text", blocks.CharBlock(required=False)), - ("required", blocks.BooleanBlock(required=False)), - ("choices", blocks.ListBlock(blocks.CharBlock(label="Option"))), - ], - icon=self.icon, - label=self.label, + return self.get_form_block_class()( + self.get_local_blocks(), + **self.get_form_block_kwargs(), ) @@ -175,15 +182,17 @@ class CheckboxField(BaseField): icon = "tick-inverse" label = _("Checkbox field") + def get_local_blocks(self): + return [ + ("label", blocks.CharBlock()), + ("help_text", blocks.CharBlock(required=False)), + ("required", blocks.BooleanBlock(required=False)), + ] + def get_form_block(self): - return blocks.StructBlock( - [ - ("label", blocks.CharBlock()), - ("help_text", blocks.CharBlock(required=False)), - ("required", blocks.BooleanBlock(required=False)), - ], - icon=self.icon, - label=self.label, + return self.get_form_block_class()( + self.get_local_blocks(), + **self.get_form_block_kwargs(), ) @@ -200,15 +209,17 @@ class SingleFileField(BaseField): icon = "doc-full-inverse" label = _("File field") + def get_local_blocks(self): + return [ + ("label", blocks.CharBlock()), + ("help_text", blocks.CharBlock(required=False)), + ("required", blocks.BooleanBlock(required=False)), + ] + def get_form_block(self): - return blocks.StructBlock( - [ - ("label", blocks.CharBlock()), - ("help_text", blocks.CharBlock(required=False)), - ("required", blocks.BooleanBlock(required=False)), - ], - icon=self.icon, - label=self.label, + return self.get_form_block_class()( + self.get_local_blocks(), + **self.get_form_block_kwargs(), ) @@ -236,15 +247,17 @@ class MultiFileField(BaseField): icon = "doc-full-inverse" label = _("Files field") + def get_local_blocks(self): + return [ + ("label", blocks.CharBlock()), + ("help_text", blocks.CharBlock(required=False)), + ("required", blocks.BooleanBlock(required=False)), + ] + def get_form_block(self): - return blocks.StructBlock( - [ - ("label", blocks.CharBlock()), - ("help_text", blocks.CharBlock(required=False)), - ("required", blocks.BooleanBlock(required=False)), - ], - icon=self.icon, - label=self.label, + return self.get_form_block_class()( + self.get_local_blocks(), + **self.get_form_block_kwargs(), )