From 0251d4b95a692bc8023c199968de6ccda6b9b30b Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Fri, 26 Jan 2024 00:55:08 +0000 Subject: [PATCH] Fix passing type information to the frontend and reduce some complexity in the submission flow --- .../restapi/deserializer/__init__.py | 82 ------------------- .../formsupport/restapi/serializer/blocks.py | 37 +-------- .../restapi/services/submit_form/field.py | 11 +-- .../restapi/services/submit_form/post.py | 14 ++++ .../volto/formsupport/validation/__init__.py | 52 ++++++++++++ .../custom_validators/CharactersValidator.py | 12 +-- .../formsupport/validation/definition.py | 6 +- 7 files changed, 83 insertions(+), 131 deletions(-) diff --git a/src/collective/volto/formsupport/restapi/deserializer/__init__.py b/src/collective/volto/formsupport/restapi/deserializer/__init__.py index 57b7379f..ce8c4fed 100644 --- a/src/collective/volto/formsupport/restapi/deserializer/__init__.py +++ b/src/collective/volto/formsupport/restapi/deserializer/__init__.py @@ -1,31 +1,10 @@ from plone.base.interfaces import IPloneSiteRoot from plone.restapi.behaviors import IBlocks from plone.restapi.interfaces import IBlockFieldDeserializationTransformer - -# from plone.restapi.interfaces import IBlockFieldSerializationTransformer from zope.component import adapter from zope.interface import implementer from zope.publisher.interfaces.browser import IBrowserRequest -from collective.volto.formsupport.validation import getValidations - -IGNORED_VALIDATION_DEFINITION_ARGUMENTS = [ - "title", - "description", - "name", - "errmsg", - "regex", - "regex_strings", - "ignore", - "_internal_type", -] - -python_type_to_volto_type_mapping = { - "int": "integer", - "float": "number", - "bool": "boolean", -} - @adapter(IBlocks, IBrowserRequest) class FormBlockDeserializerBase: @@ -43,69 +22,8 @@ def _process_data( self, data, ): - # Field is the full field definition - for index, field in enumerate(data.get("subblocks", [])): - if len(field.get("validations", [])) > 0: - data["subblocks"][index] = self._update_validations(field) - return data - def _update_validations(self, field): - validation_ids_on_field = field.get("validations") - all_validation_settings = field.get("validationSettings") - - if not validation_ids_on_field: - field["validationSettings"] = {} - return field - - # The settings were collapsed to a single control on the frontend, we need to find the validation it was for and tidy things up before continuing - all_setting_ids = all_validation_settings.keys() - top_level_setting_ids = [] - for validation_id in validation_ids_on_field: - id_to_check = f"{validation_id}-" - for setting_id in all_setting_ids: - if setting_id.startswith(id_to_check): - top_level_setting_ids.append(setting_id) - for top_level_setting_id in top_level_setting_ids: - validation_id, setting_id = top_level_setting_id.split("-") - all_validation_settings[validation_id][ - setting_id - ] = all_validation_settings[top_level_setting_id] - - # update the internal definitions for the field settings - for validation_id in validation_ids_on_field: - validation_to_update = [ - validation - for validation in getValidations() - if validation[0] == validation_id - ][0][1] - - validation_settings = all_validation_settings.get(validation_id) - - if validation_settings: - for setting_name, setting_value in all_validation_settings[ - validation_id - ].items(): - if setting_name in IGNORED_VALIDATION_DEFINITION_ARGUMENTS: - continue - validation_to_update._settings[setting_name] = setting_value - - field["validationSettings"][validation_id] = { - k: v - for k, v in validation_to_update.settings.items() - if k not in IGNORED_VALIDATION_DEFINITION_ARGUMENTS - } - - # Remove any old settings - keys_to_delete = [] - for key in all_validation_settings.keys(): - if key not in validation_ids_on_field: - keys_to_delete.append(key) - for key in keys_to_delete: - del all_validation_settings[key] - - return field - @implementer(IBlockFieldDeserializationTransformer) @adapter(IBlocks, IBrowserRequest) diff --git a/src/collective/volto/formsupport/restapi/serializer/blocks.py b/src/collective/volto/formsupport/restapi/serializer/blocks.py index e6c53b5c..895b1f81 100644 --- a/src/collective/volto/formsupport/restapi/serializer/blocks.py +++ b/src/collective/volto/formsupport/restapi/serializer/blocks.py @@ -12,17 +12,7 @@ ICaptchaSupport, ICollectiveVoltoFormsupportLayer, ) - -IGNORED_VALIDATION_DEFINITION_ARGUMENTS = [ - "title", - "description", - "name", - "errmsg", - "regex", - "regex_strings", - "ignore", - "_internal_type", -] +from collective.volto.formsupport.validation import get_validation_information class FormSerializer(object): @@ -51,33 +41,14 @@ def __call__(self, value): if attachments_limit: value["attachments_limit"] = attachments_limit - for index, field in enumerate(value.get("subblocks", [])): - if field.get("validationSettings"): - value["subblocks"][index] = self._expand_validation_field(field) + # Add information on the settings for validations to the response + validation_settings = get_validation_information() + value["validationSettings"] = validation_settings if api.user.has_permission("Modify portal content", obj=self.context): return value return {k: v for k, v in value.items() if not k.startswith("default_")} - def _expand_validation_field(self, field): - """Adds the individual validation settings to the `validationSettings` key in the format `{validation_id}-{setting_name}`""" - validation_settings = field.get("validationSettings") - settings_to_add = {} - for validation_id, settings in validation_settings.items(): - if not isinstance(settings, dict): - continue - cleaned_settings = { - f"{validation_id}-{setting_name}": val - for setting_name, val in settings.items() - if setting_name not in IGNORED_VALIDATION_DEFINITION_ARGUMENTS - } - - if cleaned_settings: - settings_to_add = {**settings_to_add, **cleaned_settings} - field["validationSettings"] = {**validation_settings, **settings_to_add} - - return field - @implementer(IBlockFieldSerializationTransformer) @adapter(IBlocks, ICollectiveVoltoFormsupportLayer) diff --git a/src/collective/volto/formsupport/restapi/services/submit_form/field.py b/src/collective/volto/formsupport/restapi/services/submit_form/field.py index 0cb2176b..a07c4069 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/field.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/field.py @@ -7,7 +7,7 @@ class Field: def __init__(self, field_data): - def _attribute(attribute_name): + def _attribute(attribute_name: str): setattr(self, attribute_name, field_data.get(attribute_name)) _attribute("field_type") @@ -26,9 +26,6 @@ def _attribute(attribute_name): self._custom_field_id = field_data.get("custom_field_id") self._label = field_data.get("label") self._field_id = field_data.get("field_id", "") - self._validations = field_data.get( - "validations", [] - ) # No need to expose the available validations @property def value(self): @@ -63,16 +60,16 @@ def send_in_email(self): return True def validate(self): - # Products.validation isn't included by default + # Making sure we've got a validation that actually exists. available_validations = [ validation for validationId, validation in getValidations() - if validationId in self._validations + if validationId in self.validations.keys() ] errors = {} for validation in available_validations: - error = validation(self._value) + error = validation(self._value, **self.validations.get(validation._name)) if error: match_result = validation_message_matcher.match(error) # We should be able to clean up messages that follow the diff --git a/src/collective/volto/formsupport/restapi/services/submit_form/post.py b/src/collective/volto/formsupport/restapi/services/submit_form/post.py index f5473d9d..20c8ea7e 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/post.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/post.py @@ -79,12 +79,26 @@ def reply(self): if field.get("id", field.get("field_id")) == submitted_field.get( "field_id" ): + validation_ids_to_apply = field.get("validations") + validations_for_field = {} + for validation_and_setting_id, setting_value in field.get( + "validationSettings" + ).items(): + validation_id, setting_id = validation_and_setting_id.split("-") + if validation_id not in validation_ids_to_apply: + continue + if validation_id not in validations_for_field: + validations_for_field[validation_id] = {} + validations_for_field[validation_id][setting_id] = setting_value fields_data.append( { **field, **submitted_field, "display_value_mapping": field.get("display_values"), "custom_field_id": self.block.get(field["field_id"]), + # We're straying from how validations are serialized and deserialized here to make our lives easier. + # Let's use a dictionary of {'validation_id': {'setting_id': 'setting_value'}} when working inside fields for simplicity. + "validations": validations_for_field, } ) self.fields = construct_fields(fields_data) diff --git a/src/collective/volto/formsupport/validation/__init__.py b/src/collective/volto/formsupport/validation/__init__.py index 0f3140bb..84c9fdf0 100644 --- a/src/collective/volto/formsupport/validation/__init__.py +++ b/src/collective/volto/formsupport/validation/__init__.py @@ -15,6 +15,18 @@ baseValidators = None +IGNORED_VALIDATION_DEFINITION_ARGUMENTS = [ + "title", + "description", + "name", + "errmsg", + "regex", + "regex_strings", + "ignore", + "_internal_type", +] + + class IFieldValidator(Interface): """Base marker for collective.volto.formsupport field validators.""" @@ -48,6 +60,46 @@ def getValidations(): return utils +PYTHON_TYPE_SCHEMA_TYPE_MAPPING = { + "bool": "boolean", + "date": "date", + "dict": "obj", + "float": "number", + "int": "integer", + "list": "array", + "str": "string", + "time": "datetime", +} + + +def get_validation_information(): + """Adds the individual validation settings to the `validationSettings` key in the format `{validation_id}-{setting_name}`""" + settings_to_add = {} + + for validation_id, validation in getValidations(): + settings = validation.settings + if not isinstance(settings, dict) or not settings: + # We don't have any settings, skip including it + continue + cleaned_settings = { + setting_name: val + for setting_name, val in settings.items() + if setting_name not in IGNORED_VALIDATION_DEFINITION_ARGUMENTS + } + + for setting_id, setting_value in cleaned_settings.items(): + settings_to_add[f"{validation_id}-{setting_id}"] = { + "validation_title": getattr(settings, "title", validation_id), + "title": setting_id, + "type": PYTHON_TYPE_SCHEMA_TYPE_MAPPING.get( + type(setting_value).__name__, "string" + ), + "default": setting_value, + } + + return settings_to_add + + @provider(IVocabularyFactory) def ValidatorsVocabularyFactory(context, **rest): """Field validators vocabulary""" diff --git a/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py b/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py index 6ce0a08a..4a2b4bb6 100644 --- a/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py +++ b/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py @@ -5,18 +5,18 @@ @implementer(IValidator) class CharactersValidator: def __init__(self, name, title="", description="", characters=0, _internal_type=""): + """ "Unused properties are for default values and type information""" self.name = name self.title = title or name self.description = description - self.characters = characters self._internal_type = _internal_type + # Default values + self.characters = characters def __call__(self, value="", *args, **kwargs): - characters = ( - int(self.characters) - if isinstance(self.characters, str) - else self.characters - ) + characters = kwargs.get("characters", self.characters) + characters = int(characters) if isinstance(characters, str) else characters + if self._internal_type == "max": if not value: return diff --git a/src/collective/volto/formsupport/validation/definition.py b/src/collective/volto/formsupport/validation/definition.py index 415ba17c..1ab07b64 100644 --- a/src/collective/volto/formsupport/validation/definition.py +++ b/src/collective/volto/formsupport/validation/definition.py @@ -11,7 +11,7 @@ def __init__(self, validator): def __call__(self, value, **kwargs): """Allow using the class directly as a validator""" - return self.validate(value, **kwargs) + return self.validate(value=value, **kwargs) @property def settings(self): @@ -19,12 +19,12 @@ def settings(self): @settings.setter def settings(self, value): - self._value = value + self._settings = value def validate(self, value, **kwargs): if value is None: # Let the system for required take care of None values return - res = validation(self._name, value, **self.settings, **kwargs) + res = validation(self._name, value, **kwargs) if res != 1: return res