From c458ee2bf408411d86654ea0b0b4110c75719797 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Fri, 22 Sep 2023 01:53:13 +0100 Subject: [PATCH 01/57] Add class for 'Fields' to simplify custom logic and use it for filter_parameters --- .../restapi/services/submit_form/field.py | 58 +++++++++++++++++++ .../restapi/services/submit_form/post.py | 29 ++++++---- 2 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 src/collective/volto/formsupport/restapi/services/submit_form/field.py diff --git a/src/collective/volto/formsupport/restapi/services/submit_form/field.py b/src/collective/volto/formsupport/restapi/services/submit_form/field.py new file mode 100644 index 00000000..2b163752 --- /dev/null +++ b/src/collective/volto/formsupport/restapi/services/submit_form/field.py @@ -0,0 +1,58 @@ +from dataclasses import dataclass +from typing import List, Optional, Any + + +@dataclass +class Field: + field_id: str + field_type: str + id: str + label: str + required: str + show_when_when: str + submitted_value: Any + input_values: Optional[List[dict]] = None + internal_value: Optional[dict] = None + widget: Optional[str] = None + + def get_display_value(self): + if self.internal_value: + return self.internal_value.get(self.submitted_value, self.submitted_value) + return self.submitted_value + + @property + def send_in_email(self): + return True + + +class YesNoField(Field): + def get_display_value(self): + if self.internal_value: + if self.submitted_value is True: + return self.internal_value.get("yes") + elif self.submitted_value is False: + return self.internal_value.get("yes") + return self.submitted_value + + @property + def send_in_email(self): + return True + + +class AttachmentField(Field): + @property + def send_in_email(self): + return False + + +def construct_field(field_data): + if field_data.get("widget") == "single_choice": + return YesNoField(**field_data) + elif field_data.get("field_type") == "attachment": + return AttachmentField(**field_data) + + return Field(**field_data) + + +def construct_fields(fields): + return [construct_field(field) for field in fields] 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 e784e41e..2f088746 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/post.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/post.py @@ -4,6 +4,9 @@ from collective.volto.formsupport.interfaces import ICaptchaSupport from collective.volto.formsupport.interfaces import IFormDataStore from collective.volto.formsupport.interfaces import IPostEvent +from collective.volto.formsupport.restapi.services.submit_form.field import ( + construct_fields, +) from collective.volto.formsupport.utils import get_blocks from datetime import datetime from email.message import EmailMessage @@ -41,6 +44,8 @@ def __init__(self, context, data): class SubmitPost(Service): + fields = [] + def __init__(self, context, request): super(SubmitPost, self).__init__(context, request) @@ -138,6 +143,16 @@ def validate_form(self): name=self.block["captcha"], ).verify(self.form_data.get("captcha")) + fields_data = [] + for field in self.block.get("subblocks", []): + for submitted_field in self.form_data.get("data", []): + if field.get("id") == submitted_field.get("field_id"): + fields_data.append( + {**field, "submitted_value": submitted_field.get("value")} + ) + + self.fields = construct_fields(fields_data) + def validate_attachments(self): attachments_limit = os.environ.get("FORM_ATTACHMENTS_LIMIT", "") if not attachments_limit: @@ -303,16 +318,10 @@ def filter_parameters(self): """ do not send attachments fields. """ - skip_fields = [ - x.get("field_id", "") - for x in self.block.get("subblocks", []) - if x.get("field_type", "") == "attachment" - ] - return [ - x - for x in self.form_data.get("data", []) - if x.get("field_id", "") not in skip_fields - ] + for field in self.fields: + field.label = field.get_display_value() + + return [field for field in self.fields if field.send_in_email] def send_mail(self, msg, charset): host = api.portal.get_tool(name="MailHost") From d6aa386b000e5ee3d84f36582b5d9509a5e9c856 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Sun, 24 Sep 2023 23:57:30 +0100 Subject: [PATCH 02/57] Fix non-required fields --- .../volto/formsupport/restapi/services/submit_form/field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2b163752..d99ca720 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/field.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/field.py @@ -8,11 +8,11 @@ class Field: field_type: str id: str label: str - required: str show_when_when: str submitted_value: Any input_values: Optional[List[dict]] = None internal_value: Optional[dict] = None + required: Optional[str] = None widget: Optional[str] = None def get_display_value(self): From 17697d89c0f3c87f999aa4d8d106a7666313904d Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Sun, 24 Sep 2023 23:57:38 +0100 Subject: [PATCH 03/57] Fix reading incorrect internal value --- .../volto/formsupport/restapi/services/submit_form/field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d99ca720..4c21e5d6 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/field.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/field.py @@ -31,7 +31,7 @@ def get_display_value(self): if self.submitted_value is True: return self.internal_value.get("yes") elif self.submitted_value is False: - return self.internal_value.get("yes") + return self.internal_value.get("no") return self.submitted_value @property From b9bb05f68ee0e208f0e43892e8d4be6269f5e103 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Mon, 25 Sep 2023 00:01:50 +0100 Subject: [PATCH 04/57] Fix values not displayign --- .../restapi/services/submit_form/field.py | 12 ++++++------ .../formsupport/restapi/services/submit_form/post.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) 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 4c21e5d6..c0c65715 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/field.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/field.py @@ -9,7 +9,7 @@ class Field: id: str label: str show_when_when: str - submitted_value: Any + value: Any input_values: Optional[List[dict]] = None internal_value: Optional[dict] = None required: Optional[str] = None @@ -17,8 +17,8 @@ class Field: def get_display_value(self): if self.internal_value: - return self.internal_value.get(self.submitted_value, self.submitted_value) - return self.submitted_value + return self.internal_value.get(self.value, self.value) + return self.value @property def send_in_email(self): @@ -28,11 +28,11 @@ def send_in_email(self): class YesNoField(Field): def get_display_value(self): if self.internal_value: - if self.submitted_value is True: + if self.value is True: return self.internal_value.get("yes") - elif self.submitted_value is False: + elif self.value is False: return self.internal_value.get("no") - return self.submitted_value + return self.value @property def send_in_email(self): 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 2f088746..06a64c50 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/post.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/post.py @@ -148,7 +148,7 @@ def validate_form(self): for submitted_field in self.form_data.get("data", []): if field.get("id") == submitted_field.get("field_id"): fields_data.append( - {**field, "submitted_value": submitted_field.get("value")} + {**field, "value": submitted_field.get("value")} ) self.fields = construct_fields(fields_data) @@ -319,7 +319,7 @@ def filter_parameters(self): do not send attachments fields. """ for field in self.fields: - field.label = field.get_display_value() + field.value = field.get_display_value() return [field for field in self.fields if field.send_in_email] From 3c45a3a0d05450dfbb182041a4e254aa75cd7549 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Mon, 25 Sep 2023 00:27:22 +0100 Subject: [PATCH 05/57] Use @property for 'value' --- .../restapi/services/submit_form/field.py | 28 +++++++++++++------ .../restapi/services/submit_form/post.py | 7 +---- 2 files changed, 20 insertions(+), 15 deletions(-) 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 c0c65715..2023b4b2 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/field.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/field.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, InitVar from typing import List, Optional, Any @@ -9,16 +9,25 @@ class Field: id: str label: str show_when_when: str - value: Any + value: InitVar[Any] + _value: Any = None input_values: Optional[List[dict]] = None internal_value: Optional[dict] = None required: Optional[str] = None widget: Optional[str] = None - def get_display_value(self): + def __post_init__(self, value): + self._value = value + + @property + def value(self): if self.internal_value: - return self.internal_value.get(self.value, self.value) - return self.value + return self.internal_value.get(self._value, self._value) + return self._value + + @value.setter + def value(self, value): + self._value = value @property def send_in_email(self): @@ -26,13 +35,14 @@ def send_in_email(self): class YesNoField(Field): - def get_display_value(self): + @property + def value(self): if self.internal_value: - if self.value is True: + if self._value is True: return self.internal_value.get("yes") - elif self.value is False: + elif self._value is False: return self.internal_value.get("no") - return self.value + return self._value @property def send_in_email(self): 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 06a64c50..99629b24 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/post.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/post.py @@ -147,9 +147,7 @@ def validate_form(self): for field in self.block.get("subblocks", []): for submitted_field in self.form_data.get("data", []): if field.get("id") == submitted_field.get("field_id"): - fields_data.append( - {**field, "value": submitted_field.get("value")} - ) + fields_data.append({**field, "value": submitted_field.get("value")}) self.fields = construct_fields(fields_data) @@ -318,9 +316,6 @@ def filter_parameters(self): """ do not send attachments fields. """ - for field in self.fields: - field.value = field.get_display_value() - return [field for field in self.fields if field.send_in_email] def send_mail(self, msg, charset): From 70a17f1165c5d813ad884330484fc90f289468b1 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Mon, 25 Sep 2023 01:10:49 +0100 Subject: [PATCH 06/57] Fix some existing behaviours --- .../restapi/services/submit_form/field.py | 53 +++++++++---------- .../restapi/services/submit_form/post.py | 34 +++++++++--- 2 files changed, 52 insertions(+), 35 deletions(-) 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 2023b4b2..45b0a0c0 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/field.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/field.py @@ -1,28 +1,27 @@ -from dataclasses import dataclass, InitVar -from typing import List, Optional, Any - - -@dataclass class Field: - field_id: str - field_type: str - id: str - label: str - show_when_when: str - value: InitVar[Any] - _value: Any = None - input_values: Optional[List[dict]] = None - internal_value: Optional[dict] = None - required: Optional[str] = None - widget: Optional[str] = None - - def __post_init__(self, value): - self._value = value + def __init__(self, field_data): + def _attribute(attribute_name): + setattr(self, attribute_name, field_data.get(attribute_name)) + + _attribute("field_id") + _attribute("field_type") + _attribute("id") + _attribute("label") + _attribute("show_when_when") + _attribute("show_when_is") + _attribute("show_when_to") + _attribute("input_values") + _attribute("dislpay_value_mapping") + _attribute("required") + _attribute("widget") + _attribute("use_as_reply_to") + _attribute("use_as_reply_bcc") + self._value = field_data.get("value") @property def value(self): - if self.internal_value: - return self.internal_value.get(self._value, self._value) + if self.dislpay_value_mapping: + return self.dislpay_value_mapping.get(self._value, self._value) return self._value @value.setter @@ -37,11 +36,11 @@ def send_in_email(self): class YesNoField(Field): @property def value(self): - if self.internal_value: + if self.dislpay_value_mapping: if self._value is True: - return self.internal_value.get("yes") + return self.dislpay_value_mapping.get("yes") elif self._value is False: - return self.internal_value.get("no") + return self.dislpay_value_mapping.get("no") return self._value @property @@ -57,11 +56,11 @@ def send_in_email(self): def construct_field(field_data): if field_data.get("widget") == "single_choice": - return YesNoField(**field_data) + return YesNoField(field_data) elif field_data.get("field_type") == "attachment": - return AttachmentField(**field_data) + return AttachmentField(field_data) - return Field(**field_data) + return Field(field_data) def construct_fields(fields): 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 99629b24..7da5ca66 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/post.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/post.py @@ -66,6 +66,32 @@ def reply(self): notify(PostEventService(self.context, self.form_data)) + # Construct self.fieldss + fields_data = [] + for submitted_field in self.form_data.get("data", []): + # TODO: Review if fields submitted without a field_id should be included. Is breaking change if we remove it + if submitted_field.get("field_id") is None: + fields_data.append( + { + "value": submitted_field.get("value"), + "label": submitted_field.get("label"), + } + ) + continue + for field in self.block.get("subblocks", []): + if field.get("id") == submitted_field.get("field_id"): + fields_data.append( + { + **field, + "value": submitted_field.get("value"), + "label": submitted_field.get("label", field.get("label")), + "dislpay_value_mapping": field.get( + "internal_value" # TODO: Rename frontend property passed in, internal_value doens't make sense + ), + } + ) + self.fields = construct_fields(fields_data) + if send_action: try: self.send_data() @@ -143,14 +169,6 @@ def validate_form(self): name=self.block["captcha"], ).verify(self.form_data.get("captcha")) - fields_data = [] - for field in self.block.get("subblocks", []): - for submitted_field in self.form_data.get("data", []): - if field.get("id") == submitted_field.get("field_id"): - fields_data.append({**field, "value": submitted_field.get("value")}) - - self.fields = construct_fields(fields_data) - def validate_attachments(self): attachments_limit = os.environ.get("FORM_ATTACHMENTS_LIMIT", "") if not attachments_limit: From 9bf78790314abc9d254f4acbf8de3cc15abe78b7 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Mon, 25 Sep 2023 01:31:28 +0100 Subject: [PATCH 07/57] Add test for custom_field_id --- .../volto/formsupport/tests/test_send_action_form.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/collective/volto/formsupport/tests/test_send_action_form.py b/src/collective/volto/formsupport/tests/test_send_action_form.py index 10265e2a..f80d904d 100644 --- a/src/collective/volto/formsupport/tests/test_send_action_form.py +++ b/src/collective/volto/formsupport/tests/test_send_action_form.py @@ -677,6 +677,7 @@ def test_send_xml(self): form_data = [ {"label": "Message", "value": "just want to say hi"}, {"label": "Name", "value": "John"}, + {"label": "Name", "value": "Test", "custom_field_id": "My custom field id"}, ] response = self.submit_form( @@ -699,5 +700,9 @@ def test_send_xml(self): msg_contents = parsed_msgs.get_payload()[1].get_payload(decode=True) xml_tree = ET.fromstring(msg_contents) for index, field in enumerate(xml_tree): - self.assertEqual(field.get("name"), form_data[index]["label"]) + custom_field_id = form_data[index].get("custom_field_id") + self.assertEqual( + field.get("name"), + custom_field_id if custom_field_id else form_data[index]["label"], + ) self.assertEqual(field.text, form_data[index]["value"]) From 624170d4bae6bd17de4003b4cceeaf26551a6614 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Mon, 25 Sep 2023 01:34:34 +0100 Subject: [PATCH 08/57] Fix custom field ID --- .../restapi/services/submit_form/field.py | 25 +++++++++++++------ .../restapi/services/submit_form/post.py | 14 +++-------- 2 files changed, 21 insertions(+), 18 deletions(-) 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 45b0a0c0..1ad05b2b 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/field.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/field.py @@ -6,28 +6,39 @@ def _attribute(attribute_name): _attribute("field_id") _attribute("field_type") _attribute("id") - _attribute("label") _attribute("show_when_when") _attribute("show_when_is") _attribute("show_when_to") _attribute("input_values") - _attribute("dislpay_value_mapping") _attribute("required") _attribute("widget") _attribute("use_as_reply_to") _attribute("use_as_reply_bcc") + self._dislpay_value_mapping = field_data.get("dislpay_value_mapping") self._value = field_data.get("value") + self._custom_field_id = field_data.get("custom_field_id") + self._label = field_data.get("label") @property def value(self): - if self.dislpay_value_mapping: - return self.dislpay_value_mapping.get(self._value, self._value) + if self._dislpay_value_mapping: + return self._dislpay_value_mapping.get(self._value, self._value) return self._value @value.setter def value(self, value): self._value = value + @property + def label(self): + if self._custom_field_id: + return self._custom_field_id + return self._label + + @label.setter + def label(self, label): + self._label = label + @property def send_in_email(self): return True @@ -36,11 +47,11 @@ def send_in_email(self): class YesNoField(Field): @property def value(self): - if self.dislpay_value_mapping: + if self._dislpay_value_mapping: if self._value is True: - return self.dislpay_value_mapping.get("yes") + return self._dislpay_value_mapping.get("yes") elif self._value is False: - return self.dislpay_value_mapping.get("no") + return self._dislpay_value_mapping.get("no") return self._value @property 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 7da5ca66..94dfbe76 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/post.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/post.py @@ -71,20 +71,14 @@ def reply(self): for submitted_field in self.form_data.get("data", []): # TODO: Review if fields submitted without a field_id should be included. Is breaking change if we remove it if submitted_field.get("field_id") is None: - fields_data.append( - { - "value": submitted_field.get("value"), - "label": submitted_field.get("label"), - } - ) + fields_data.append(submitted_field) continue for field in self.block.get("subblocks", []): if field.get("id") == submitted_field.get("field_id"): fields_data.append( { **field, - "value": submitted_field.get("value"), - "label": submitted_field.get("label", field.get("label")), + **submitted_field, "dislpay_value_mapping": field.get( "internal_value" # TODO: Rename frontend property passed in, internal_value doens't make sense ), @@ -387,9 +381,7 @@ def attach_xml(self, msg): xmlRoot = Element("form") for field in self.filter_parameters(): - SubElement( - xmlRoot, "field", name=field.get("custom_field_id", field["label"]) - ).text = str(field["value"]) + SubElement(xmlRoot, "field", name=field.label).text = str(field.value) doc = ElementTree(xmlRoot) doc.write(output, encoding="utf-8", xml_declaration=True) From 3ea20ae9d7f551a59c013f34b06881fce02327d3 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Mon, 25 Sep 2023 01:53:35 +0100 Subject: [PATCH 09/57] Fix inconsistent use of field_id in requess --- .../volto/formsupport/restapi/services/submit_form/post.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 94dfbe76..1cef2a6c 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/post.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/post.py @@ -74,7 +74,9 @@ def reply(self): fields_data.append(submitted_field) continue for field in self.block.get("subblocks", []): - if field.get("id") == submitted_field.get("field_id"): + if field.get("id", field.get("field_id")) == submitted_field.get( + "field_id" + ): fields_data.append( { **field, From b3ca7da3b026ab49026e673f3e4e0aa3a345debd Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Mon, 25 Sep 2023 01:53:50 +0100 Subject: [PATCH 10/57] Restore support for falling back to field_id if label doesn't exist --- .../volto/formsupport/restapi/services/submit_form/field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1ad05b2b..335188ed 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/field.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/field.py @@ -33,7 +33,7 @@ def value(self, value): def label(self): if self._custom_field_id: return self._custom_field_id - return self._label + return self._label if self._label else self.field_id @label.setter def label(self, label): From e821e3f2fd934e95148ed572a0d14ecea18b768a Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Mon, 25 Sep 2023 01:54:02 +0100 Subject: [PATCH 11/57] Fix data storing --- src/collective/volto/formsupport/datamanager/catalog.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/collective/volto/formsupport/datamanager/catalog.py b/src/collective/volto/formsupport/datamanager/catalog.py index c92317ae..16efd9c9 100644 --- a/src/collective/volto/formsupport/datamanager/catalog.py +++ b/src/collective/volto/formsupport/datamanager/catalog.py @@ -80,16 +80,13 @@ def add(self, data): ) return None - fields = { - x["field_id"]: x.get("custom_field_id", x.get("label", x["field_id"])) - for x in form_fields - } + fields = {x["field_id"]: x["label"] for x in form_fields} record = Record() fields_labels = {} fields_order = [] for field_data in data: - field_id = field_data.get("field_id", "") - value = field_data.get("value", "") + field_id = field_data.field_id + value = field_data.value if field_id in fields: record.attrs[field_id] = value fields_labels[field_id] = fields[field_id] From cd8e9bb97148e9a4a9ec8cf19e51fa75f6246429 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Mon, 25 Sep 2023 02:01:58 +0100 Subject: [PATCH 12/57] Fix custom_field_id in data export --- src/collective/volto/formsupport/datamanager/catalog.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/collective/volto/formsupport/datamanager/catalog.py b/src/collective/volto/formsupport/datamanager/catalog.py index 16efd9c9..b6178c04 100644 --- a/src/collective/volto/formsupport/datamanager/catalog.py +++ b/src/collective/volto/formsupport/datamanager/catalog.py @@ -80,7 +80,10 @@ def add(self, data): ) return None - fields = {x["field_id"]: x["label"] for x in form_fields} + fields = { + x["field_id"]: x.get("custom_field_id", x.get("label", x["field_id"])) + for x in form_fields + } record = Record() fields_labels = {} fields_order = [] From e1994e00e54c646fd0391f9a0281dddbd6918717 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Tue, 26 Sep 2023 01:33:32 +0100 Subject: [PATCH 13/57] Add tests for display values --- .../tests/test_send_action_form.py | 46 ++++++++++++++ .../tests/test_store_action_form.py | 62 +++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/src/collective/volto/formsupport/tests/test_send_action_form.py b/src/collective/volto/formsupport/tests/test_send_action_form.py index f80d904d..9d156324 100644 --- a/src/collective/volto/formsupport/tests/test_send_action_form.py +++ b/src/collective/volto/formsupport/tests/test_send_action_form.py @@ -668,6 +668,52 @@ def test_email_body_formated_as_list( self.assertIn("Message: just want to say hi", msg) self.assertIn("Name: John", msg) + def test_field_custom_display_value( + self, + ): + self.document.blocks = { + "form-id": { + "@type": "form", + "send": True, + "email_format": "list", + "subblocks": [ + { + "field_id": "12345678", + "internal_value": {"John": "Paul"}, + }, + ], + } + } + transaction.commit() + + response = self.submit_form( + data={ + "from": "john@doe.com", + "data": [ + {"label": "Message", "value": "just want to say hi"}, + { + "label": "Name", + "field_id": "12345678", + "value": "John", + }, + ], + "subject": "test subject", + "block_id": "form-id", + }, + ) + transaction.commit() + self.assertEqual(response.status_code, 204) + msg = self.mailhost.messages[0] + if isinstance(msg, bytes) and bytes is not str: + # Python 3 with Products.MailHost 4.10+ + msg = msg.decode("utf-8") + self.assertIn("Subject: test subject", msg) + self.assertIn("From: john@doe.com", msg) + self.assertIn("To: site_addr@plone.com", msg) + self.assertIn("Reply-To: john@doe.com", msg) + self.assertIn("Message: just want to say hi", msg) + self.assertIn("Name: Paul", msg) + def test_send_xml(self): self.document.blocks = { "form-id": {"@type": "form", "send": True, "attachXml": True}, diff --git a/src/collective/volto/formsupport/tests/test_store_action_form.py b/src/collective/volto/formsupport/tests/test_store_action_form.py index fd0af723..4c916efe 100644 --- a/src/collective/volto/formsupport/tests/test_store_action_form.py +++ b/src/collective/volto/formsupport/tests/test_store_action_form.py @@ -122,6 +122,7 @@ def test_store_data(self): "label": "Name", "field_id": "name", "field_type": "text", + "internal_value": "Custom name", }, ], }, @@ -310,3 +311,64 @@ def test_data_id_mapping(self): now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M") self.assertTrue(sorted_data[0][-1].startswith(now)) self.assertTrue(sorted_data[1][-1].startswith(now)) + + def test_display_values(self): + self.document.blocks = { + "form-id": { + "@type": "form", + "store": True, + "test-field": "renamed-field", + "subblocks": [ + { + "field_id": "message", + "label": "Message", + "field_type": "text", + }, + { + "field_id": "test-field", + "label": "Test field", + "field_type": "text", + "internal_value": {"John": "Paul", "Sally": "Jack"}, + }, + ], + }, + } + transaction.commit() + response = self.submit_form( + data={ + "from": "john@doe.com", + "data": [ + {"field_id": "message", "value": "just want to say hi"}, + {"field_id": "test-field", "value": "John"}, + ], + "subject": "test subject", + "block_id": "form-id", + }, + ) + + response = self.submit_form( + data={ + "from": "sally@doe.com", + "data": [ + {"field_id": "message", "value": "bye"}, + {"field_id": "test-field", "value": "Sally"}, + ], + "subject": "test subject", + "block_id": "form-id", + }, + ) + + self.assertEqual(response.status_code, 204) + response = self.export_csv() + data = [*csv.reader(StringIO(response.text), delimiter=",")] + self.assertEqual(len(data), 3) + # Check that 'test-field' got renamed + self.assertEqual(data[0], ["Message", "renamed-field", "date"]) + sorted_data = sorted(data[1:]) + self.assertEqual(sorted_data[0][:-1], ["bye", "Sally"]) + self.assertEqual(sorted_data[1][:-1], ["just want to say hi", "John"]) + + # check date column. Skip seconds because can change during test + now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M") + self.assertTrue(sorted_data[0][-1].startswith(now)) + self.assertTrue(sorted_data[1][-1].startswith(now)) From b5c75da750fe5831cced6b38eff0030bbd2effff Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Tue, 26 Sep 2023 01:35:30 +0100 Subject: [PATCH 14/57] Fix catalog using display value for storage --- src/collective/volto/formsupport/datamanager/catalog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/collective/volto/formsupport/datamanager/catalog.py b/src/collective/volto/formsupport/datamanager/catalog.py index b6178c04..2caa9c10 100644 --- a/src/collective/volto/formsupport/datamanager/catalog.py +++ b/src/collective/volto/formsupport/datamanager/catalog.py @@ -89,7 +89,8 @@ def add(self, data): fields_order = [] for field_data in data: field_id = field_data.field_id - value = field_data.value + # TODO: not nice using the protected member to access the real internal value, but easiest way. + value = field_data._value if field_id in fields: record.attrs[field_id] = value fields_labels[field_id] = fields[field_id] From f2ff5881acc62b2f55827dd235e589fd0a74b00d Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Wed, 27 Sep 2023 01:19:36 +0100 Subject: [PATCH 15/57] Fix XML using external value --- .../volto/formsupport/restapi/services/submit_form/post.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1cef2a6c..b461702d 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/post.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/post.py @@ -383,7 +383,7 @@ def attach_xml(self, msg): xmlRoot = Element("form") for field in self.filter_parameters(): - SubElement(xmlRoot, "field", name=field.label).text = str(field.value) + SubElement(xmlRoot, "field", name=field.label).text = str(field._value) doc = ElementTree(xmlRoot) doc.write(output, encoding="utf-8", xml_declaration=True) From 2cf29d38b21ea2778a2e4d279f55f77bc98177f7 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Wed, 27 Sep 2023 01:19:44 +0100 Subject: [PATCH 16/57] Update frontend field name --- .../volto/formsupport/restapi/services/submit_form/post.py | 2 +- .../volto/formsupport/tests/test_store_action_form.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 b461702d..adebc2c2 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/post.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/post.py @@ -82,7 +82,7 @@ def reply(self): **field, **submitted_field, "dislpay_value_mapping": field.get( - "internal_value" # TODO: Rename frontend property passed in, internal_value doens't make sense + "display_values" ), } ) diff --git a/src/collective/volto/formsupport/tests/test_store_action_form.py b/src/collective/volto/formsupport/tests/test_store_action_form.py index 4c916efe..e6c28aff 100644 --- a/src/collective/volto/formsupport/tests/test_store_action_form.py +++ b/src/collective/volto/formsupport/tests/test_store_action_form.py @@ -122,7 +122,7 @@ def test_store_data(self): "label": "Name", "field_id": "name", "field_type": "text", - "internal_value": "Custom name", + "display_values": "Custom name", }, ], }, @@ -328,7 +328,7 @@ def test_display_values(self): "field_id": "test-field", "label": "Test field", "field_type": "text", - "internal_value": {"John": "Paul", "Sally": "Jack"}, + "display_values": {"John": "Paul", "Sally": "Jack"}, }, ], }, From 810d6753ba305c16522618c648c52ab0b55fdc1f Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Wed, 27 Sep 2023 09:24:29 +0100 Subject: [PATCH 17/57] Test fix --- src/collective/volto/formsupport/tests/test_send_action_form.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/collective/volto/formsupport/tests/test_send_action_form.py b/src/collective/volto/formsupport/tests/test_send_action_form.py index 9d156324..ebf8d99e 100644 --- a/src/collective/volto/formsupport/tests/test_send_action_form.py +++ b/src/collective/volto/formsupport/tests/test_send_action_form.py @@ -679,7 +679,7 @@ def test_field_custom_display_value( "subblocks": [ { "field_id": "12345678", - "internal_value": {"John": "Paul"}, + "display_values": {"John": "Paul"}, }, ], } From 9a76a5cdd74a1986acc2091ce1ac82aafe93520d Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Tue, 12 Dec 2023 16:02:56 +0000 Subject: [PATCH 18/57] Initial validation support --- setup.py | 4 + .../volto/formsupport/configure.zcml | 1 + .../restapi/services/submit_form/field.py | 25 ++++++ .../restapi/services/submit_form/post.py | 23 ++++- .../volto/formsupport/validation.py | 83 +++++++++++++++++++ .../volto/formsupport/validation.zcml | 13 +++ 6 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 src/collective/volto/formsupport/validation.py create mode 100644 src/collective/volto/formsupport/validation.zcml diff --git a/setup.py b/setup.py index 1e5b603d..a33cd57d 100644 --- a/setup.py +++ b/setup.py @@ -69,6 +69,10 @@ "honeypot": [ "collective.honeypot>=2.1", ], + "validation": [ + "Products.validation", + "z3c.form" + ], "test": [ "plone.app.testing", # Plone KGS does not use this version, because it would break diff --git a/src/collective/volto/formsupport/configure.zcml b/src/collective/volto/formsupport/configure.zcml index 36f6b34e..dc6f6ed7 100644 --- a/src/collective/volto/formsupport/configure.zcml +++ b/src/collective/volto/formsupport/configure.zcml @@ -22,6 +22,7 @@ + + + + + + + + + + From c184b558f01b82f9f6a82554c109012f120cca48 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Tue, 12 Dec 2023 18:20:13 +0000 Subject: [PATCH 19/57] Fix HTTP response code --- .../volto/formsupport/restapi/services/submit_form/post.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 046e0f79..a1527717 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/post.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/post.py @@ -95,7 +95,7 @@ def reply(self): errors[field.field_id] = field_errors if errors: - self.request.response.setStatus(500) + self.request.response.setStatus(400) return json_compatible( { "error": { From bd1e2d0ccce60b446a7e3b9aee18eb1c27fb9d8d Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Tue, 12 Dec 2023 23:35:17 +0000 Subject: [PATCH 20/57] Fix crash when no validations are set --- .../volto/formsupport/restapi/services/submit_form/field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 05ed1435..3560d461 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/field.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/field.py @@ -23,7 +23,7 @@ def _attribute(attribute_name): self._custom_field_id = field_data.get("custom_field_id") self._label = field_data.get("label") self._validations = field_data.get( - "validations" + "validations", [] ) # No need to expose the available validations @property From 55246849a731075e8842ad78b690923c80e4839e Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Fri, 15 Dec 2023 12:52:22 +0000 Subject: [PATCH 21/57] Readme --- README.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.rst b/README.rst index 32800988..4d61560c 100644 --- a/README.rst +++ b/README.rst @@ -251,6 +251,13 @@ This add-on can be seen in action at the following sites: - https://www.comune.modena.it/form/contatti +Custom label mapping +========================= + +In some cases, the text that is displayed for a field on the page and in the sent email may need to be different from the value that is stored internally. For example, you may want your "Yes/ No" widget to show "Accept" and "Decline" as the labels, but internally still store `True` and `False`. + +By storing a `display_values` dictionary for each field in the block data, you can perform these mappings. + Translations ============ From ac749fbfa98a854d8e185b68e8f50c0abd40bc5b Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Fri, 15 Dec 2023 12:53:10 +0000 Subject: [PATCH 22/57] Typo fix --- .../restapi/services/submit_form/field.py | 12 ++++++------ .../formsupport/restapi/services/submit_form/post.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) 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 335188ed..6dbcd048 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/field.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/field.py @@ -14,15 +14,15 @@ def _attribute(attribute_name): _attribute("widget") _attribute("use_as_reply_to") _attribute("use_as_reply_bcc") - self._dislpay_value_mapping = field_data.get("dislpay_value_mapping") + self._display_value_mapping = field_data.get("display_value_mapping") self._value = field_data.get("value") self._custom_field_id = field_data.get("custom_field_id") self._label = field_data.get("label") @property def value(self): - if self._dislpay_value_mapping: - return self._dislpay_value_mapping.get(self._value, self._value) + if self._display_value_mapping: + return self._display_value_mapping.get(self._value, self._value) return self._value @value.setter @@ -47,11 +47,11 @@ def send_in_email(self): class YesNoField(Field): @property def value(self): - if self._dislpay_value_mapping: + if self._display_value_mapping: if self._value is True: - return self._dislpay_value_mapping.get("yes") + return self._display_value_mapping.get("yes") elif self._value is False: - return self._dislpay_value_mapping.get("no") + return self._display_value_mapping.get("no") return self._value @property 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 adebc2c2..0abef739 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/post.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/post.py @@ -81,7 +81,7 @@ def reply(self): { **field, **submitted_field, - "dislpay_value_mapping": field.get( + "display_value_mapping": field.get( "display_values" ), } From 8df42f561359e8d9162b27e01b8681a6ef820a68 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Tue, 19 Dec 2023 22:27:52 +0000 Subject: [PATCH 23/57] Initial serializer for validation settings --- .../volto/formsupport/restapi/configure.zcml | 1 + .../restapi/deserializer/__init__.py | 77 +++++++++++++++++++ .../restapi/deserializer/configure.zcml | 16 ++++ 3 files changed, 94 insertions(+) create mode 100644 src/collective/volto/formsupport/restapi/deserializer/__init__.py create mode 100644 src/collective/volto/formsupport/restapi/deserializer/configure.zcml diff --git a/src/collective/volto/formsupport/restapi/configure.zcml b/src/collective/volto/formsupport/restapi/configure.zcml index da6334d6..c99ef222 100644 --- a/src/collective/volto/formsupport/restapi/configure.zcml +++ b/src/collective/volto/formsupport/restapi/configure.zcml @@ -4,6 +4,7 @@ + diff --git a/src/collective/volto/formsupport/restapi/deserializer/__init__.py b/src/collective/volto/formsupport/restapi/deserializer/__init__.py new file mode 100644 index 00000000..847dbb8b --- /dev/null +++ b/src/collective/volto/formsupport/restapi/deserializer/__init__.py @@ -0,0 +1,77 @@ +from plone.restapi.bbb import IPloneSiteRoot +from plone.restapi.behaviors import IBlocks +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", +] + +python_type_to_volto_type_mapping = { + "int": "integer", + "float": "number", + "bool": "boolean", +} + + +@implementer(IBlockFieldSerializationTransformer) +@adapter(IBlocks, IBrowserRequest) +class FormBlockSerializerBase: + block_type = "form" + order = 100 + + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self, block): + return self._process_data(block) + + def _process_data( + self, + data, + ): + # Field is the full field definition + for field in data.get("subblocks", []): + if len(field.get("validations", [])) > 0: + self._expand_validation_field(field) + return data + + def _expand_validation_field(self, field): + field_validations = field.get("validations") + matched_validation_definitions = [ + validation + for validation in getValidations() + if validation[0] in field_validations + ] + + for validation_id, validation in matched_validation_definitions: + settings = vars(validation)["_settings"] + for ignored_setting in IGNORED_VALIDATION_DEFINITION_ARGUMENTS: + if ignored_setting in settings: + del settings[ignored_setting] + field[validation_id] = settings + + # if api.user.has_permission("Modify portal content", obj=self.context): + # return value + + +@implementer(IBlockFieldSerializationTransformer) +@adapter(IBlocks, IBrowserRequest) +class FormBlockSerializer(FormBlockSerializerBase): + """Serializer for content-types with IBlocks behavior""" + + +@implementer(IBlockFieldSerializationTransformer) +@adapter(IPloneSiteRoot, IBrowserRequest) +class FormBlockSerializerRoot(FormBlockSerializerBase): + """Serializer for site root""" diff --git a/src/collective/volto/formsupport/restapi/deserializer/configure.zcml b/src/collective/volto/formsupport/restapi/deserializer/configure.zcml new file mode 100644 index 00000000..47f81233 --- /dev/null +++ b/src/collective/volto/formsupport/restapi/deserializer/configure.zcml @@ -0,0 +1,16 @@ + + + + + + From 94cd023727a897cd052abaa3d09946e40421f2b9 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Wed, 20 Dec 2023 00:30:43 +0000 Subject: [PATCH 24/57] Add "ignore" to list of internal ignored fields --- .../volto/formsupport/restapi/deserializer/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/collective/volto/formsupport/restapi/deserializer/__init__.py b/src/collective/volto/formsupport/restapi/deserializer/__init__.py index 847dbb8b..77b34d96 100644 --- a/src/collective/volto/formsupport/restapi/deserializer/__init__.py +++ b/src/collective/volto/formsupport/restapi/deserializer/__init__.py @@ -14,6 +14,7 @@ "errmsg", "regex", "regex_strings", + "ignore", ] python_type_to_volto_type_mapping = { From 97b18154d197daf1264dba23276ea92bdaaf8e41 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Wed, 20 Dec 2023 18:00:37 +0000 Subject: [PATCH 25/57] Fix serializer deleting the actual validation object's settings --- .../volto/formsupport/restapi/deserializer/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/collective/volto/formsupport/restapi/deserializer/__init__.py b/src/collective/volto/formsupport/restapi/deserializer/__init__.py index 77b34d96..c9697de3 100644 --- a/src/collective/volto/formsupport/restapi/deserializer/__init__.py +++ b/src/collective/volto/formsupport/restapi/deserializer/__init__.py @@ -57,9 +57,12 @@ def _expand_validation_field(self, field): for validation_id, validation in matched_validation_definitions: settings = vars(validation)["_settings"] - for ignored_setting in IGNORED_VALIDATION_DEFINITION_ARGUMENTS: - if ignored_setting in settings: - del settings[ignored_setting] + settings = { + k: v + for k, v in settings.items() + for ignored_setting in IGNORED_VALIDATION_DEFINITION_ARGUMENTS + if ignored_setting not in settings + } field[validation_id] = settings # if api.user.has_permission("Modify portal content", obj=self.context): From ff7979103c0e8a1138ace0b86916fd9176b77f95 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Thu, 21 Dec 2023 13:03:38 +0000 Subject: [PATCH 26/57] Fix IPloneSiteRoot import --- .../volto/formsupport/restapi/deserializer/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/collective/volto/formsupport/restapi/deserializer/__init__.py b/src/collective/volto/formsupport/restapi/deserializer/__init__.py index c9697de3..6b160704 100644 --- a/src/collective/volto/formsupport/restapi/deserializer/__init__.py +++ b/src/collective/volto/formsupport/restapi/deserializer/__init__.py @@ -1,4 +1,4 @@ -from plone.restapi.bbb import IPloneSiteRoot +from plone.base.interfaces import IPloneSiteRoot from plone.restapi.behaviors import IBlocks from plone.restapi.interfaces import IBlockFieldSerializationTransformer from zope.component import adapter From 2dcd6fb3c147b06939ecebaa98134fd925d32f98 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Fri, 22 Dec 2023 03:12:44 +0000 Subject: [PATCH 27/57] Fix validations not occurign --- .../formsupport/restapi/deserializer/__init__.py | 11 ++++++----- src/collective/volto/formsupport/validation.py | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/collective/volto/formsupport/restapi/deserializer/__init__.py b/src/collective/volto/formsupport/restapi/deserializer/__init__.py index 6b160704..ac38a70b 100644 --- a/src/collective/volto/formsupport/restapi/deserializer/__init__.py +++ b/src/collective/volto/formsupport/restapi/deserializer/__init__.py @@ -58,12 +58,13 @@ def _expand_validation_field(self, field): for validation_id, validation in matched_validation_definitions: settings = vars(validation)["_settings"] settings = { - k: v - for k, v in settings.items() - for ignored_setting in IGNORED_VALIDATION_DEFINITION_ARGUMENTS - if ignored_setting not in settings + setting_name: val + for setting_name, val in settings.items() + if setting_name not in IGNORED_VALIDATION_DEFINITION_ARGUMENTS } - field[validation_id] = settings + + if settings: + field[validation_id] = settings # if api.user.has_permission("Modify portal content", obj=self.context): # return value diff --git a/src/collective/volto/formsupport/validation.py b/src/collective/volto/formsupport/validation.py index 054fc6df..d16c1915 100644 --- a/src/collective/volto/formsupport/validation.py +++ b/src/collective/volto/formsupport/validation.py @@ -34,7 +34,7 @@ def delete_setting(setting): class ValidationDefinition: def __init__(self, validator): self._name = validator.name - self._settings = _clean_validation_settings(vars(validator)) + self._settings = vars(validator) def __call__(self, value, **kwargs): """Allow using the class directly as a validator""" From afc6b7de1d9289bf32e96ef66ccb19655cfdb2b1 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Fri, 22 Dec 2023 03:27:07 +0000 Subject: [PATCH 28/57] Able to return nice error messages --- .../restapi/services/submit_form/field.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) 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 3560d461..adaf6aaa 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/field.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/field.py @@ -1,6 +1,11 @@ from collective.volto.formsupport.validation import getValidations +import re + +validation_message_matcher = re.compile("Validation failed\(([^\)]+)\): ") + + class Field: def __init__(self, field_data): def _attribute(attribute_name): @@ -58,11 +63,18 @@ def validate(self): if validationId in self._validations ] - errors = [] + errors = {} for validation in available_validations: error = validation(self._value) if error: - errors.append(error) + match_result = validation_message_matcher.match(error) + # We should be able to clean up messages that follow the + # `Validation failed({validation_id}): {message}` pattern. + # No guarantees we will encounter it though. + if match_result: + error = validation_message_matcher.sub("", error) + + errors[validation._name] = error return ( errors if errors else None From 83ad017f70ad6904d62fd6305f57383cbd6ad816 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Fri, 22 Dec 2023 10:40:11 +0000 Subject: [PATCH 29/57] WIP: Custom validation --- .../restapi/deserializer/__init__.py | 5 ++- .../formsupport/restapi/serializer/blocks.py | 32 ++++++++++++++ .../volto/formsupport/validation.zcml | 13 +++++- .../{validation.py => validation/__init__.py} | 5 +++ .../custom_validators/CharactersValidator.py | 42 +++++++++++++++++++ .../validation/custom_validators/__init__.py | 6 +++ 6 files changed, 99 insertions(+), 4 deletions(-) rename src/collective/volto/formsupport/{validation.py => validation/__init__.py} (97%) create mode 100644 src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py create mode 100644 src/collective/volto/formsupport/validation/custom_validators/__init__.py diff --git a/src/collective/volto/formsupport/restapi/deserializer/__init__.py b/src/collective/volto/formsupport/restapi/deserializer/__init__.py index ac38a70b..612fe196 100644 --- a/src/collective/volto/formsupport/restapi/deserializer/__init__.py +++ b/src/collective/volto/formsupport/restapi/deserializer/__init__.py @@ -15,6 +15,7 @@ "regex", "regex_strings", "ignore", + "_internal_type", ] python_type_to_volto_type_mapping = { @@ -45,6 +46,7 @@ def _process_data( for field in data.get("subblocks", []): if len(field.get("validations", [])) > 0: self._expand_validation_field(field) + return data def _expand_validation_field(self, field): @@ -56,13 +58,12 @@ def _expand_validation_field(self, field): ] for validation_id, validation in matched_validation_definitions: - settings = vars(validation)["_settings"] + settings = validation.settings settings = { setting_name: val for setting_name, val in settings.items() if setting_name not in IGNORED_VALIDATION_DEFINITION_ARGUMENTS } - if settings: field[validation_id] = settings diff --git a/src/collective/volto/formsupport/restapi/serializer/blocks.py b/src/collective/volto/formsupport/restapi/serializer/blocks.py index 90771487..98b98f5e 100644 --- a/src/collective/volto/formsupport/restapi/serializer/blocks.py +++ b/src/collective/volto/formsupport/restapi/serializer/blocks.py @@ -12,6 +12,18 @@ import os +IGNORED_VALIDATION_DEFINITION_ARGUMENTS = [ + "title", + "description", + "name", + "errmsg", + "regex", + "regex_strings", + "ignore", + "_internal_type", +] + + class FormSerializer(object): """ """ @@ -37,10 +49,30 @@ def __call__(self, value): attachments_limit = os.environ.get("FORM_ATTACHMENTS_LIMIT", "") if attachments_limit: value["attachments_limit"] = attachments_limit + + for field in value.get("subblocks", []): + if field.get('validationSettings'): + self._update_validations(field) + 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 _update_validations(self, field): + validations = field.get("validations") + new_settings = field.get('validationSettings') + # The settings were collapsed on the frontend, we need to find the validation it was for + for validation_id in validations: + settings = { + setting_name: val + for setting_name, val in new_settings.items() + if setting_name not in IGNORED_VALIDATION_DEFINITION_ARGUMENTS + } if field.get(validation_id) else None + # breakpoint() + if settings: + field[validation_id] = settings + + @implementer(IBlockFieldSerializationTransformer) @adapter(IBlocks, ICollectiveVoltoFormsupportLayer) diff --git a/src/collective/volto/formsupport/validation.zcml b/src/collective/volto/formsupport/validation.zcml index 2b0eaebc..f6928981 100644 --- a/src/collective/volto/formsupport/validation.zcml +++ b/src/collective/volto/formsupport/validation.zcml @@ -1,7 +1,16 @@ - - + + + self.characters): + # TODO: i18n + msg = f"Validation failed({self.name}): is more than {self.characters}" + return msg + elif self._internal_type == "min": + if (not value or len(value) < self.characters): + # TODO: i18n + msg = f"Validation failed({self.name}): is less than {self.characters}", + return msg + else: + # TODO: i18n + msg = f"Validation failed({self.name}): Unknown characters validator type", + return msg + + diff --git a/src/collective/volto/formsupport/validation/custom_validators/__init__.py b/src/collective/volto/formsupport/validation/custom_validators/__init__.py new file mode 100644 index 00000000..e2f1d8b2 --- /dev/null +++ b/src/collective/volto/formsupport/validation/custom_validators/__init__.py @@ -0,0 +1,6 @@ +from collective.volto.formsupport.validation.custom_validators.CharactersValidator import ( + CharactersValidator, +) + +maxCharacters = CharactersValidator("maxCharacters", _internal_type="max") +minCharacters = CharactersValidator("minCharacters", _internal_type="min") From 30adf28550d1ea253022b6eb57da318d5f1b4e34 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Tue, 9 Jan 2024 14:34:29 +0000 Subject: [PATCH 30/57] Add test for custom field ID and ensuring it isn't used in emails --- .../tests/test_send_action_form.py | 67 +++++++++++++++---- 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/src/collective/volto/formsupport/tests/test_send_action_form.py b/src/collective/volto/formsupport/tests/test_send_action_form.py index d57406e6..37190bce 100644 --- a/src/collective/volto/formsupport/tests/test_send_action_form.py +++ b/src/collective/volto/formsupport/tests/test_send_action_form.py @@ -1,24 +1,27 @@ # -*- coding: utf-8 -*- -from collective.volto.formsupport.testing import ( # noqa: E501, - VOLTO_FORMSUPPORT_API_FUNCTIONAL_TESTING, -) +import base64 +import os +import unittest +import xml.etree.ElementTree as ET from email.parser import Parser + +import transaction from plone import api -from plone.app.testing import setRoles -from plone.app.testing import SITE_OWNER_NAME -from plone.app.testing import SITE_OWNER_PASSWORD -from plone.app.testing import TEST_USER_ID +from plone.app.testing import ( + SITE_OWNER_NAME, + SITE_OWNER_PASSWORD, + TEST_USER_ID, + setRoles, +) from plone.registry.interfaces import IRegistry from plone.restapi.testing import RelativeSession from Products.MailHost.interfaces import IMailHost from six import StringIO from zope.component import getUtility -import base64 -import os -import transaction -import unittest -import xml.etree.ElementTree as ET +from collective.volto.formsupport.testing import ( # noqa: E501, + VOLTO_FORMSUPPORT_API_FUNCTIONAL_TESTING, +) class TestMailSend(unittest.TestCase): @@ -841,6 +844,46 @@ def test_field_custom_display_value( self.assertIn("Message: just want to say hi", msg) self.assertIn("Name: Paul", msg) + def test_send_custom_field_id(self): + """Custom field IDs should still appear as their friendly names in the email""" + self.document.blocks = { + "form-id": {"@type": "form", "send": True}, + } + transaction.commit() + + form_data = [ + {"label": "Name", "value": "John"}, + { + "label": "Other name", + "value": "Test", + "custom_field_id": "My custom field id", + }, + ] + + response = self.submit_form( + data={ + "from": "john@doe.com", + "data": form_data, + "subject": "test subject", + "block_id": "form-id", + }, + ) + transaction.commit() + self.assertEqual(response.status_code, 204) + msg = self.mailhost.messages[0] + if isinstance(msg, bytes) and bytes is not str: + # Python 3 with Products.MailHost 4.10+ + msg = msg.decode("utf-8") + + parsed_msgs = Parser().parse(StringIO(msg)) + body = parsed_msgs.get_payload() + + self.assertIn("Name", body) + self.assertIn("John", body) + self.assertNotIn("My custom field id", body) + self.assertIn("Other name", body) + self.assertIn("Test", body) + def test_send_xml(self): self.document.blocks = { "form-id": {"@type": "form", "send": True, "attachXml": True}, From 47ede0b4e307ba7f1229967b595bebe131273937 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Tue, 9 Jan 2024 14:35:11 +0000 Subject: [PATCH 31/57] Fix using field_id in XML --- .../restapi/services/submit_form/field.py | 14 +++++++++++--- .../restapi/services/submit_form/post.py | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) 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 3d35071b..3ab2ccd8 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/field.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/field.py @@ -3,7 +3,6 @@ def __init__(self, field_data): def _attribute(attribute_name): setattr(self, attribute_name, field_data.get(attribute_name)) - _attribute("field_id") _attribute("field_type") _attribute("id") _attribute("show_when_when") @@ -18,6 +17,7 @@ def _attribute(attribute_name): self._value = field_data.get("value", "") self._custom_field_id = field_data.get("custom_field_id") self._label = field_data.get("label", "") + self._field_id = field_data.get("field_id", "") @property def value(self): @@ -31,14 +31,22 @@ def value(self, value): @property def label(self): - if self._custom_field_id: - return self._custom_field_id return self._label if self._label else self.field_id @label.setter def label(self, label): self._label = label + @property + def field_id(self): + if self._custom_field_id: + return self._custom_field_id + return self._field_id if self._field_id else self._label + + @field_id.setter + def field_id(self, field_id): + self._field_id = field_id + @property def send_in_email(self): return True 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 2aefcb70..3ca46694 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/post.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/post.py @@ -411,7 +411,7 @@ def attach_xml(self, msg): xmlRoot = Element("form") for field in self.filter_parameters(): - SubElement(xmlRoot, "field", name=field.label).text = str(field._value) + SubElement(xmlRoot, "field", name=field.field_id).text = str(field._value) doc = ElementTree(xmlRoot) doc.write(output, encoding="utf-8", xml_declaration=True) From 378592227971d4049b67f56be603242476d55555 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Tue, 9 Jan 2024 14:55:48 +0000 Subject: [PATCH 32/57] Remove upgrades and test files from coverage --- .coveragerc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.coveragerc b/.coveragerc index acf9b175..6feee2a9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,10 @@ [run] relative_files = True +omit = + src/collective/volto/formsupport/tests/* + src/collective/volto/formsupport/upgrades.py + [report] include = */src/collective/* From adb24a4e98bc49fa2720f9d0f3dc022e1a42531d Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Thu, 11 Jan 2024 21:51:05 +0000 Subject: [PATCH 33/57] Improve tests for sending and storing custom fields --- .../tests/test_send_action_form.py | 44 +++++++++++++++++-- .../tests/test_store_action_form.py | 31 +++++++------ 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/src/collective/volto/formsupport/tests/test_send_action_form.py b/src/collective/volto/formsupport/tests/test_send_action_form.py index 37190bce..aa8b486b 100644 --- a/src/collective/volto/formsupport/tests/test_send_action_form.py +++ b/src/collective/volto/formsupport/tests/test_send_action_form.py @@ -847,7 +847,18 @@ def test_field_custom_display_value( def test_send_custom_field_id(self): """Custom field IDs should still appear as their friendly names in the email""" self.document.blocks = { - "form-id": {"@type": "form", "send": True}, + "form-id": { + "@type": "form", + "send": True, + "internal_mapped_name": "renamed-internal_mapped_name", + "subblocks": [ + { + "field_id": "internal_mapped_name", + "label": "Name with internal mapping", + "field_type": "text", + }, + ], + }, } transaction.commit() @@ -858,6 +869,10 @@ def test_send_custom_field_id(self): "value": "Test", "custom_field_id": "My custom field id", }, + { + "field_id": "internal_mapped_name", + "value": "Test", + }, ] response = self.submit_form( @@ -883,17 +898,40 @@ def test_send_custom_field_id(self): self.assertNotIn("My custom field id", body) self.assertIn("Other name", body) self.assertIn("Test", body) + self.assertIn("Name with internal mapping", body) def test_send_xml(self): self.document.blocks = { - "form-id": {"@type": "form", "send": True, "attachXml": True}, + "form-id": { + "@type": "form", + "send": True, + "attachXml": True, + "custom_name": "renamed_custom_name", + "subblocks": [ + { + "field_id": "message", + "label": "Message", + "field_type": "text", + }, + { + "field_id": "name", + "label": "Name", + "field_type": "text", + }, + { + "field_id": "custom_name", + "label": "Name", + "field_type": "text", + }, + ], + }, } transaction.commit() form_data = [ {"label": "Message", "value": "just want to say hi"}, {"label": "Name", "value": "John"}, - {"label": "Name", "value": "Test", "custom_field_id": "My custom field id"}, + {"label": "Name", "value": "Test"}, ] response = self.submit_form( diff --git a/src/collective/volto/formsupport/tests/test_store_action_form.py b/src/collective/volto/formsupport/tests/test_store_action_form.py index e6c28aff..a55045f8 100644 --- a/src/collective/volto/formsupport/tests/test_store_action_form.py +++ b/src/collective/volto/formsupport/tests/test_store_action_form.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- -from collective.volto.formsupport.testing import ( # noqa: E501, - VOLTO_FORMSUPPORT_API_FUNCTIONAL_TESTING, -) +import csv +import unittest from datetime import datetime from io import StringIO + +import transaction from plone import api -from plone.app.testing import setRoles -from plone.app.testing import SITE_OWNER_NAME -from plone.app.testing import SITE_OWNER_PASSWORD -from plone.app.testing import TEST_USER_ID +from plone.app.testing import ( + SITE_OWNER_NAME, + SITE_OWNER_PASSWORD, + TEST_USER_ID, + setRoles, +) from plone.restapi.testing import RelativeSession from Products.MailHost.interfaces import IMailHost from zope.component import getUtility -import csv -import transaction -import unittest +from collective.volto.formsupport.testing import ( # noqa: E501, + VOLTO_FORMSUPPORT_API_FUNCTIONAL_TESTING, +) class TestMailSend(unittest.TestCase): @@ -301,8 +304,8 @@ def test_data_id_mapping(self): response = self.export_csv() data = [*csv.reader(StringIO(response.text), delimiter=",")] self.assertEqual(len(data), 3) - # Check that 'test-field' got renamed - self.assertEqual(data[0], ["Message", "renamed-field", "date"]) + # Check that 'test-field' got correctly mapped to it's label + self.assertEqual(data[0], ["Message", "Test field", "date"]) sorted_data = sorted(data[1:]) self.assertEqual(sorted_data[0][:-1], ["bye", "Sally"]) self.assertEqual(sorted_data[1][:-1], ["just want to say hi", "John"]) @@ -362,8 +365,8 @@ def test_display_values(self): response = self.export_csv() data = [*csv.reader(StringIO(response.text), delimiter=",")] self.assertEqual(len(data), 3) - # Check that 'test-field' got renamed - self.assertEqual(data[0], ["Message", "renamed-field", "date"]) + # Check that 'test-field' got correctly mapped to it's label + self.assertEqual(data[0], ["Message", "Test field", "date"]) sorted_data = sorted(data[1:]) self.assertEqual(sorted_data[0][:-1], ["bye", "Sally"]) self.assertEqual(sorted_data[1][:-1], ["just want to say hi", "John"]) From c778d90d862abcd7712deabbb51fa9649e08538d Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Thu, 11 Jan 2024 21:53:20 +0000 Subject: [PATCH 34/57] Fix for storing CSVs --- src/collective/volto/formsupport/datamanager/catalog.py | 9 +++++---- .../formsupport/restapi/services/submit_form/post.py | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/collective/volto/formsupport/datamanager/catalog.py b/src/collective/volto/formsupport/datamanager/catalog.py index 2caa9c10..dea39346 100644 --- a/src/collective/volto/formsupport/datamanager/catalog.py +++ b/src/collective/volto/formsupport/datamanager/catalog.py @@ -79,11 +79,12 @@ def add(self, data): ) ) return None + fields = {} + for field in form_fields: + custom_field_id = field.get("custom_field_id") + field_id = custom_field_id if custom_field_id else field["field_id"] + fields[field_id] = field.get("label", field_id) - fields = { - x["field_id"]: x.get("custom_field_id", x.get("label", x["field_id"])) - for x in form_fields - } record = Record() fields_labels = {} fields_order = [] 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 3ca46694..7e463f66 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/post.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/post.py @@ -83,6 +83,7 @@ def reply(self): **field, **submitted_field, "display_value_mapping": field.get("display_values"), + "custom_field_id": self.block.get(field["field_id"]), } ) self.fields = construct_fields(fields_data) From b4ac3e18355301d0150b8a2cd3fb8808a0e1f664 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Mon, 15 Jan 2024 01:53:11 +0000 Subject: [PATCH 35/57] Quick messy test for validation --- .../formsupport/tests/test_validation.py | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 src/collective/volto/formsupport/tests/test_validation.py diff --git a/src/collective/volto/formsupport/tests/test_validation.py b/src/collective/volto/formsupport/tests/test_validation.py new file mode 100644 index 00000000..28eb98a6 --- /dev/null +++ b/src/collective/volto/formsupport/tests/test_validation.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +import os +import unittest + +import transaction +from plone import api +from plone.app.testing import ( + SITE_OWNER_NAME, + SITE_OWNER_PASSWORD, + TEST_USER_ID, + setRoles, +) +from plone.registry.interfaces import IRegistry +from plone.restapi.testing import RelativeSession +from Products.MailHost.interfaces import IMailHost +from zope.component import getUtility + +from collective.volto.formsupport.testing import ( # noqa: E501, + VOLTO_FORMSUPPORT_API_FUNCTIONAL_TESTING, +) +from collective.volto.formsupport.validation import getValidations + + +class TestMailSend(unittest.TestCase): + layer = VOLTO_FORMSUPPORT_API_FUNCTIONAL_TESTING + + def setUp(self): + self.app = self.layer["app"] + self.portal = self.layer["portal"] + self.portal_url = self.portal.absolute_url() + setRoles(self.portal, TEST_USER_ID, ["Manager"]) + + self.mailhost = getUtility(IMailHost) + + registry = getUtility(IRegistry) + registry["plone.email_from_address"] = "site_addr@plone.com" + registry["plone.email_from_name"] = "Plone test site" + + self.api_session = RelativeSession(self.portal_url) + self.api_session.headers.update({"Accept": "application/json"}) + self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + self.anon_api_session = RelativeSession(self.portal_url) + self.anon_api_session.headers.update({"Accept": "application/json"}) + + self.document = api.content.create( + type="Document", + title="Example context", + container=self.portal, + ) + self.document.blocks = { + "text-id": {"@type": "text"}, + "form-id": {"@type": "form"}, + } + self.document_url = self.document.absolute_url() + transaction.commit() + + def tearDown(self): + self.api_session.close() + self.anon_api_session.close() + + # set default block + self.document.blocks = { + "text-id": {"@type": "text"}, + "form-id": {"@type": "form"}, + } + + os.environ["FORM_ATTACHMENTS_LIMIT"] = "" + + transaction.commit() + + def submit_form(self, data): + url = "{}/@submit-form".format(self.document_url) + response = self.api_session.post( + url, + json=data, + ) + transaction.commit() + return response + + def test_validation(self): + self.document.blocks = { + "text-id": {"@type": "text"}, + "form-id": { + "@type": "form", + "default_subject": "block subject", + "default_from": "john@doe.com", + "send": ["acknowledgement"], + "acknowledgementFields": "contact", + "acknowledgementMessage": { + "data": "

This message will be sent to the person filling in the form.

It is Rich Text

" + }, + "subblocks": [ + { + "field_id": "123456789", + "field_type": "text", + "id": "123456789", + "inNumericRange": {"maxval": "7", "minval": "2"}, + "label": "My field", + "required": False, + "show_when_when": "always", + "validationSettings": {"maxval": "7", "minval": "2"}, + "validations": ["inNumericRange"], + } + ], + }, + } + transaction.commit() + + response = self.api_session.get(self.document_url) + res = response.json() + + validations = getValidations() + breakpoint() + self.assertEqual(res["blocks"]["form-id"], self.document.blocks["form-id"]) + + # response = self.submit_form( + # data={ + # "data": [{"field_id": "123456789", "value": "4", "label": "My field"}], + # "block_id": "form-id", + # }, + # ) + # transaction.commit() + + # breakpoint() + # self.assertEqual(response.status_code, 204) + + # msg = self.mailhost.messages[0] + # if isinstance(msg, bytes) and bytes is not str: + # # Python 3 with Products.MailHost 4.10+ + # msg = msg.decode("utf-8") + + # parsed_msg = Parser().parse(StringIO(msg)) + # self.assertEqual(parsed_msg.get("from"), "john@doe.com") + # self.assertEqual(parsed_msg.get("to"), "smith@doe.com") + # self.assertEqual(parsed_msg.get("subject"), "block subject") + # msg_body = parsed_msg.get_payload(decode=True).decode() + # self.assertIn( + # "

This message will be sent to the person filling in the form.

", + # msg_body, + # ) + # self.assertIn("

It is Rich Text

", msg_body) From 6606bc170a6e04ff004f2028e85de93443d98f94 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Mon, 15 Jan 2024 01:54:02 +0000 Subject: [PATCH 36/57] Fix settings saving (still need to get the correct type information for the settings) --- base.cfg | 3 +- .../restapi/deserializer/__init__.py | 59 +++++++++++-------- .../restapi/deserializer/configure.zcml | 8 +-- .../formsupport/restapi/serializer/blocks.py | 46 +++++++++------ 4 files changed, 66 insertions(+), 50 deletions(-) diff --git a/base.cfg b/base.cfg index db51faf9..32e0e3d8 100644 --- a/base.cfg +++ b/base.cfg @@ -6,7 +6,7 @@ extensions = parts = instance test - code-analysis + # code-analysis coverage test-coverage createcoverage @@ -33,6 +33,7 @@ eggs = Plone Pillow collective.volto.formsupport [test] + collective.volto.formsupport [validation] zcml-additional += 0: - self._expand_validation_field(field) + data["subblocks"][index] = self._update_validations(field) return data - def _expand_validation_field(self, field): - field_validations = field.get("validations") - matched_validation_definitions = [ - validation - for validation in getValidations() - if validation[0] in field_validations - ] - - for validation_id, validation in matched_validation_definitions: - settings = validation.settings - settings = { - setting_name: val - for setting_name, val in settings.items() - if setting_name not in IGNORED_VALIDATION_DEFINITION_ARGUMENTS - } + def _update_validations(self, field): + validations = field.get("validations") + new_settings = field.get("validationSettings") + # The settings were collapsed on the frontend, we need to find the validation it was for + for validation_id in validations: + settings = ( + { + setting_name: val + for setting_name, val in new_settings.items() + if setting_name not in IGNORED_VALIDATION_DEFINITION_ARGUMENTS + } + if field.get(validation_id) + else None + ) if settings: field[validation_id] = settings - # if api.user.has_permission("Modify portal content", obj=self.context): - # return value + validation_to_update = [ + validation + for validation in getValidations() + if validation[0] == validation_id + ][0][1] + for setting_id, setting_value in settings.items(): + validation_to_update._settings[setting_id] = setting_value + + return field -@implementer(IBlockFieldSerializationTransformer) +@implementer(IBlockFieldDeserializationTransformer) @adapter(IBlocks, IBrowserRequest) -class FormBlockSerializer(FormBlockSerializerBase): +class FormBlockDeserializer(FormBlockDeserializerBase): """Serializer for content-types with IBlocks behavior""" -@implementer(IBlockFieldSerializationTransformer) +@implementer(IBlockFieldDeserializationTransformer) @adapter(IPloneSiteRoot, IBrowserRequest) -class FormBlockSerializerRoot(FormBlockSerializerBase): +class FormBlockDeserializerRoot(FormBlockDeserializerBase): """Serializer for site root""" diff --git a/src/collective/volto/formsupport/restapi/deserializer/configure.zcml b/src/collective/volto/formsupport/restapi/deserializer/configure.zcml index 47f81233..885cd6a4 100644 --- a/src/collective/volto/formsupport/restapi/deserializer/configure.zcml +++ b/src/collective/volto/formsupport/restapi/deserializer/configure.zcml @@ -5,12 +5,12 @@ > diff --git a/src/collective/volto/formsupport/restapi/serializer/blocks.py b/src/collective/volto/formsupport/restapi/serializer/blocks.py index 98b98f5e..82327642 100644 --- a/src/collective/volto/formsupport/restapi/serializer/blocks.py +++ b/src/collective/volto/formsupport/restapi/serializer/blocks.py @@ -1,16 +1,19 @@ # -*- coding: utf-8 -*- -from collective.volto.formsupport.interfaces import ICaptchaSupport -from collective.volto.formsupport.interfaces import ICollectiveVoltoFormsupportLayer +import os + from plone import api from plone.restapi.behaviors import IBlocks from plone.restapi.interfaces import IBlockFieldSerializationTransformer +from plone.restapi.serializer.converters import json_compatible from Products.CMFPlone.interfaces import IPloneSiteRoot -from zope.component import adapter -from zope.component import getMultiAdapter +from zope.component import adapter, getMultiAdapter from zope.interface import implementer -import os - +from collective.volto.formsupport.interfaces import ( + ICaptchaSupport, + ICollectiveVoltoFormsupportLayer, +) +from collective.volto.formsupport.validation import getValidations IGNORED_VALIDATION_DEFINITION_ARGUMENTS = [ "title", @@ -50,28 +53,33 @@ def __call__(self, value): if attachments_limit: value["attachments_limit"] = attachments_limit - for field in value.get("subblocks", []): - if field.get('validationSettings'): - self._update_validations(field) + for index, field in enumerate(value.get("subblocks", [])): + if field.get("validationSettings"): + value["subblocks"][index] = self._expand_validation_field(field) 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 _update_validations(self, field): - validations = field.get("validations") - new_settings = field.get('validationSettings') - # The settings were collapsed on the frontend, we need to find the validation it was for - for validation_id in validations: + def _expand_validation_field(self, field): + field_validations = field.get("validations") + matched_validation_definitions = [ + validation + for validation in getValidations() + if validation[0] in field_validations + ] + + for validation_id, validation in matched_validation_definitions: + settings = validation.settings settings = { setting_name: val - for setting_name, val in new_settings.items() + for setting_name, val in settings.items() if setting_name not in IGNORED_VALIDATION_DEFINITION_ARGUMENTS - } if field.get(validation_id) else None - # breakpoint() + } if settings: - field[validation_id] = settings - + field[validation_id] = json_compatible(settings) + + return field @implementer(IBlockFieldSerializationTransformer) From 731d331cd127b1d7b39eebe3ff38a1e0a674098e Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Wed, 17 Jan 2024 17:39:04 +0000 Subject: [PATCH 37/57] Overhaul how we're passing settings back and forth --- .../volto/formsupport/configure.zcml | 2 +- .../restapi/deserializer/__init__.py | 80 +++++++++++++------ .../formsupport/restapi/serializer/blocks.py | 25 +++--- .../volto/formsupport/validation.zcml | 22 ----- .../volto/formsupport/validation/__init__.py | 34 +++----- .../custom_validators/CharactersValidator.py | 18 +++-- .../validation/custom_validators/__init__.py | 1 + .../formsupport/validation/definition.py | 27 +++++++ .../formsupport/validation/validation.zcml | 10 +++ 9 files changed, 126 insertions(+), 93 deletions(-) delete mode 100644 src/collective/volto/formsupport/validation.zcml create mode 100644 src/collective/volto/formsupport/validation/definition.py create mode 100644 src/collective/volto/formsupport/validation/validation.zcml diff --git a/src/collective/volto/formsupport/configure.zcml b/src/collective/volto/formsupport/configure.zcml index dc6f6ed7..2258e98b 100644 --- a/src/collective/volto/formsupport/configure.zcml +++ b/src/collective/volto/formsupport/configure.zcml @@ -22,7 +22,7 @@ - + - - - - - - - - - -
diff --git a/src/collective/volto/formsupport/validation/__init__.py b/src/collective/volto/formsupport/validation/__init__.py index 3a78ea85..d37d1060 100644 --- a/src/collective/volto/formsupport/validation/__init__.py +++ b/src/collective/volto/formsupport/validation/__init__.py @@ -5,8 +5,10 @@ from zope.schema.interfaces import IVocabularyFactory from zope.schema.vocabulary import SimpleVocabulary +from collective.volto.formsupport.validation.custom_validators import custom_validators +from collective.volto.formsupport.validation.definition import ValidationDefinition + try: - from Products.validation import validation from Products.validation.validators.BaseValidators import baseValidators except ImportError: # Products.validation is optional validation = None @@ -35,41 +37,25 @@ def delete_setting(setting): return settings -class ValidationDefinition: - def __init__(self, validator): - self._name = validator.name - self._settings = vars(validator) - - def __call__(self, value, **kwargs): - """Allow using the class directly as a validator""" - return self.validate(value, **kwargs) - - @property - def settings(self): - return self._settings - - 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, **kwargs) - if res != 1: - return res - - def _update_validators(): """ Add Products.validation validators to the available list of validators Code taken from collective.easyform . Could lookup based on `IValidator` instead of re-registering? """ - if validation and baseValidators: + if baseValidators: for validator in baseValidators: provideUtility( ValidationDefinition(validator), provides=IFieldValidator, name=validator.name, ) + for validator in custom_validators: + provideUtility( + ValidationDefinition(validator), + provides=IFieldValidator, + name=validator.name, + ) _update_validators() diff --git a/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py b/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py index 1f42be8b..e42a9dca 100644 --- a/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py +++ b/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py @@ -1,12 +1,12 @@ from Products.validation.interfaces.IValidator import IValidator from zope.interface import implementer -from collective.volto.formsupport.validation import ValidationDefinition +from collective.volto.formsupport.validation.definition import ValidationDefinition # TODO: Tidy up code structure so we don't need to be a definition @implementer(IValidator) -class CharactersValidator(ValidationDefinition): +class CharactersValidator(): def __init__(self, name, title="", description="", characters=0, _internal_type=""): self.name = name self.title = title or name @@ -25,18 +25,20 @@ def settings(self): def __call__(self, value="", *args, **kwargs): if self._internal_type == "max": - if (not value or len(value) > self.characters): + if not value or len(value) > self.characters: # TODO: i18n msg = f"Validation failed({self.name}): is more than {self.characters}" return msg elif self._internal_type == "min": - if (not value or len(value) < self.characters): + if not value or len(value) < self.characters: # TODO: i18n - msg = f"Validation failed({self.name}): is less than {self.characters}", + msg = ( + f"Validation failed({self.name}): is less than {self.characters}", + ) return msg else: # TODO: i18n - msg = f"Validation failed({self.name}): Unknown characters validator type", + msg = ( + f"Validation failed({self.name}): Unknown characters validator type", + ) return msg - - diff --git a/src/collective/volto/formsupport/validation/custom_validators/__init__.py b/src/collective/volto/formsupport/validation/custom_validators/__init__.py index e2f1d8b2..cc919f67 100644 --- a/src/collective/volto/formsupport/validation/custom_validators/__init__.py +++ b/src/collective/volto/formsupport/validation/custom_validators/__init__.py @@ -4,3 +4,4 @@ maxCharacters = CharactersValidator("maxCharacters", _internal_type="max") minCharacters = CharactersValidator("minCharacters", _internal_type="min") +custom_validators = [maxCharacters, minCharacters] diff --git a/src/collective/volto/formsupport/validation/definition.py b/src/collective/volto/formsupport/validation/definition.py new file mode 100644 index 00000000..c250aca8 --- /dev/null +++ b/src/collective/volto/formsupport/validation/definition.py @@ -0,0 +1,27 @@ +from Products.validation import validation + + +class ValidationDefinition: + def __init__(self, validator): + self._name = validator.name + self._settings = vars(validator) + + def __call__(self, value, **kwargs): + """Allow using the class directly as a validator""" + return self.validate(value, **kwargs) + + @property + def settings(self): + return self._settings + + @settings.setter + def settings(self, value): + self._value = 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, **kwargs) + if res != 1: + return res diff --git a/src/collective/volto/formsupport/validation/validation.zcml b/src/collective/volto/formsupport/validation/validation.zcml new file mode 100644 index 00000000..ed430f42 --- /dev/null +++ b/src/collective/volto/formsupport/validation/validation.zcml @@ -0,0 +1,10 @@ + + + + + + + From 70d9aafbaa6afd48eac538d2d229f64cc9b28da2 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Wed, 17 Jan 2024 23:40:40 +0000 Subject: [PATCH 38/57] Fix setting deserialization after overhall --- .../restapi/deserializer/__init__.py | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/collective/volto/formsupport/restapi/deserializer/__init__.py b/src/collective/volto/formsupport/restapi/deserializer/__init__.py index 54f04829..57b7379f 100644 --- a/src/collective/volto/formsupport/restapi/deserializer/__init__.py +++ b/src/collective/volto/formsupport/restapi/deserializer/__init__.py @@ -59,20 +59,18 @@ def _update_validations(self, field): 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 - if set(validation_ids_on_field) != set(all_validation_settings): - top_level_settings = { - setting_id: setting_value - for setting_id, setting_value in all_validation_settings.items() - if setting_id not in validation_ids_on_field - } - top_level_setting_ids = [] - for validation_id, settings in all_validation_settings.items(): - if set(settings) == set(top_level_settings): - all_validation_settings[validation_id] = top_level_settings - for setting_id in top_level_settings.keys(): - top_level_setting_ids.append(setting_id) - for setting_id in top_level_setting_ids: - del all_validation_settings[setting_id] + 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: @@ -106,9 +104,6 @@ def _update_validations(self, field): for key in keys_to_delete: del all_validation_settings[key] - # # Finally, update the actual validators with what's left in the validation settings - # breakpoint() - return field From bec210711d6ebb440a735a2986d61e1a3c376dfa Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Wed, 17 Jan 2024 23:40:54 +0000 Subject: [PATCH 39/57] Fix settings not being passed to validators --- src/collective/volto/formsupport/validation/definition.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/collective/volto/formsupport/validation/definition.py b/src/collective/volto/formsupport/validation/definition.py index c250aca8..415ba17c 100644 --- a/src/collective/volto/formsupport/validation/definition.py +++ b/src/collective/volto/formsupport/validation/definition.py @@ -5,6 +5,9 @@ class ValidationDefinition: def __init__(self, validator): self._name = validator.name self._settings = vars(validator) + # Make sure the validation service has the validator in it. + if not validation._validator.get(validator.name): + validation.register(validator) def __call__(self, value, **kwargs): """Allow using the class directly as a validator""" @@ -22,6 +25,6 @@ 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, **kwargs) + res = validation(self._name, value, **self.settings, **kwargs) if res != 1: return res From 61b020aeed87293e0436050408a9f12ff12d5b92 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Wed, 17 Jan 2024 23:41:20 +0000 Subject: [PATCH 40/57] Fix bad settings appearing in UI --- .../volto/formsupport/validation/__init__.py | 18 ------------------ .../custom_validators/CharactersValidator.py | 9 +-------- 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/src/collective/volto/formsupport/validation/__init__.py b/src/collective/volto/formsupport/validation/__init__.py index d37d1060..0f3140bb 100644 --- a/src/collective/volto/formsupport/validation/__init__.py +++ b/src/collective/volto/formsupport/validation/__init__.py @@ -19,24 +19,6 @@ class IFieldValidator(Interface): """Base marker for collective.volto.formsupport field validators.""" -def inMaxCharacters(value, **kwargs): - breakpoint() - - -def _clean_validation_settings(settings): - def delete_setting(setting): - if hasattr(settings, setting): - del settings[setting] - - delete_setting("name") - delete_setting("title") - delete_setting("description") - delete_setting("regex_strings") - delete_setting("regex") - delete_setting("errmsg") - return settings - - def _update_validators(): """ Add Products.validation validators to the available list of validators diff --git a/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py b/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py index e42a9dca..8f8886b1 100644 --- a/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py +++ b/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py @@ -1,12 +1,10 @@ from Products.validation.interfaces.IValidator import IValidator from zope.interface import implementer -from collective.volto.formsupport.validation.definition import ValidationDefinition - # TODO: Tidy up code structure so we don't need to be a definition @implementer(IValidator) -class CharactersValidator(): +class CharactersValidator: def __init__(self, name, title="", description="", characters=0, _internal_type=""): self.name = name self.title = title or name @@ -14,11 +12,6 @@ def __init__(self, name, title="", description="", characters=0, _internal_type= self.characters = characters self._internal_type = _internal_type - # From super class. Hacky implementation having this here for now - self._name = name - # self._name = name - # self.settings = vars(self) - @property def settings(self): return vars(self) From 2899a935c77144f370bcbd2f1ef9032ef7269f16 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Thu, 18 Jan 2024 01:05:04 +0000 Subject: [PATCH 41/57] Fix crash on serializing existing fields --- src/collective/volto/formsupport/restapi/serializer/blocks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/collective/volto/formsupport/restapi/serializer/blocks.py b/src/collective/volto/formsupport/restapi/serializer/blocks.py index 0fe71734..e6c53b5c 100644 --- a/src/collective/volto/formsupport/restapi/serializer/blocks.py +++ b/src/collective/volto/formsupport/restapi/serializer/blocks.py @@ -64,6 +64,8 @@ def _expand_validation_field(self, field): 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() From e98dd6729d80db82a8dc8832aaab99a5f22326e3 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Thu, 18 Jan 2024 01:05:12 +0000 Subject: [PATCH 42/57] Fix type of validations --- .../custom_validators/CharactersValidator.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py b/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py index 8f8886b1..0fe14136 100644 --- a/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py +++ b/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py @@ -2,7 +2,6 @@ from zope.interface import implementer -# TODO: Tidy up code structure so we don't need to be a definition @implementer(IValidator) class CharactersValidator: def __init__(self, name, title="", description="", characters=0, _internal_type=""): @@ -12,21 +11,22 @@ def __init__(self, name, title="", description="", characters=0, _internal_type= self.characters = characters self._internal_type = _internal_type - @property - def settings(self): - return vars(self) - def __call__(self, value="", *args, **kwargs): + characters = ( + int(self.characters) + if isinstance(self.characters, str) + else self.characters + ) if self._internal_type == "max": - if not value or len(value) > self.characters: + if not value or len(value) > characters: # TODO: i18n - msg = f"Validation failed({self.name}): is more than {self.characters}" + msg = f"Validation failed({self.name}): is more than {characters} characters long" return msg elif self._internal_type == "min": - if not value or len(value) < self.characters: + if not value or len(value) < characters: # TODO: i18n msg = ( - f"Validation failed({self.name}): is less than {self.characters}", + f"Validation failed({self.name}): is less than {characters} characters long", ) return msg else: From ac67bdd11920415dbcf81333dfb2d4e4bb3c6880 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Thu, 18 Jan 2024 01:12:17 +0000 Subject: [PATCH 43/57] Fix characters validation behaviour --- .../custom_validators/CharactersValidator.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py b/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py index 0fe14136..6ce0a08a 100644 --- a/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py +++ b/src/collective/volto/formsupport/validation/custom_validators/CharactersValidator.py @@ -18,20 +18,18 @@ def __call__(self, value="", *args, **kwargs): else self.characters ) if self._internal_type == "max": - if not value or len(value) > characters: + if not value: + return + if len(value) > characters: # TODO: i18n msg = f"Validation failed({self.name}): is more than {characters} characters long" return msg elif self._internal_type == "min": if not value or len(value) < characters: # TODO: i18n - msg = ( - f"Validation failed({self.name}): is less than {characters} characters long", - ) + msg = f"Validation failed({self.name}): is less than {characters} characters long" return msg else: # TODO: i18n - msg = ( - f"Validation failed({self.name}): Unknown characters validator type", - ) + msg = f"Validation failed({self.name}): Unknown characters validator type" return msg From 38e9f9be55800a7b2b71585744c4ad51987af430 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Thu, 18 Jan 2024 23:05:18 +0000 Subject: [PATCH 44/57] Fix bad attribute --- .../volto/formsupport/restapi/services/submit_form/field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 39210823..0cb2176b 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/field.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/field.py @@ -21,7 +21,7 @@ def _attribute(attribute_name): _attribute("use_as_reply_to") _attribute("use_as_reply_bcc") _attribute("validations") - self._dislpay_value_mapping = field_data.get("dislpay_value_mapping") + self._display_value_mapping = field_data.get("dislpay_value_mapping") self._value = field_data.get("value", "") self._custom_field_id = field_data.get("custom_field_id") self._label = field_data.get("label") From 0251d4b95a692bc8023c199968de6ccda6b9b30b Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Fri, 26 Jan 2024 00:55:08 +0000 Subject: [PATCH 45/57] 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 From 41a7cda88a584ccc3d23f6d3854651467b1fceb9 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Fri, 26 Jan 2024 11:32:07 +0000 Subject: [PATCH 46/57] Remove validations and settings if the field type doesn't allow for validations --- .../volto/formsupport/restapi/deserializer/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/collective/volto/formsupport/restapi/deserializer/__init__.py b/src/collective/volto/formsupport/restapi/deserializer/__init__.py index ce8c4fed..b1b8e6c9 100644 --- a/src/collective/volto/formsupport/restapi/deserializer/__init__.py +++ b/src/collective/volto/formsupport/restapi/deserializer/__init__.py @@ -22,8 +22,15 @@ def _process_data( self, data, ): + self._update_validations(data) return data + def _update_validations(self, data): + for field in data.get("subblocks"): + if field.get("field_type") not in ["text", "textarea", "from"]: + field["validations"] = [] + field["validationSettings"] = {} + @implementer(IBlockFieldDeserializationTransformer) @adapter(IBlocks, IBrowserRequest) From d54879d9fb11389a594d911cc6b919f787ce9f0a Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Mon, 29 Jan 2024 23:01:28 +0000 Subject: [PATCH 47/57] Don't include `inNumericRange` as the code isn't great currently --- src/collective/volto/formsupport/validation/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/collective/volto/formsupport/validation/__init__.py b/src/collective/volto/formsupport/validation/__init__.py index 84c9fdf0..99cd03b1 100644 --- a/src/collective/volto/formsupport/validation/__init__.py +++ b/src/collective/volto/formsupport/validation/__init__.py @@ -26,6 +26,8 @@ "_internal_type", ] +VALIDATIONS_TO_IGNORE = ["inNumericRange"] + class IFieldValidator(Interface): """Base marker for collective.volto.formsupport field validators.""" @@ -39,6 +41,8 @@ def _update_validators(): if baseValidators: for validator in baseValidators: + if validator.name in VALIDATIONS_TO_IGNORE: + continue provideUtility( ValidationDefinition(validator), provides=IFieldValidator, From 71783698b822aa7253fb7d04c80dddad1185153a Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Mon, 5 Feb 2024 23:17:01 +0000 Subject: [PATCH 48/57] Fix exceptions for existing forms --- .../volto/formsupport/restapi/services/submit_form/post.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 20c8ea7e..21f0b65f 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/post.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/post.py @@ -79,10 +79,10 @@ def reply(self): if field.get("id", field.get("field_id")) == submitted_field.get( "field_id" ): - validation_ids_to_apply = field.get("validations") + validation_ids_to_apply = field.get("validations", []) validations_for_field = {} for validation_and_setting_id, setting_value in field.get( - "validationSettings" + "validationSettings", {} ).items(): validation_id, setting_id = validation_and_setting_id.split("-") if validation_id not in validation_ids_to_apply: From b35cb897e603b50561a78c7db713d97bc942f041 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Mon, 5 Feb 2024 23:17:33 +0000 Subject: [PATCH 49/57] Temp test fix --- src/collective/volto/formsupport/tests/test_validation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/collective/volto/formsupport/tests/test_validation.py b/src/collective/volto/formsupport/tests/test_validation.py index 28eb98a6..91d083cc 100644 --- a/src/collective/volto/formsupport/tests/test_validation.py +++ b/src/collective/volto/formsupport/tests/test_validation.py @@ -110,8 +110,8 @@ def test_validation(self): res = response.json() validations = getValidations() - breakpoint() - self.assertEqual(res["blocks"]["form-id"], self.document.blocks["form-id"]) + # breakpoint() + # self.assertEqual(res["blocks"]["form-id"], self.document.blocks["form-id"]) # response = self.submit_form( # data={ From caebd3183c9931c01eeda5880b8762fc17615c41 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Tue, 12 Mar 2024 02:39:19 +0000 Subject: [PATCH 50/57] Don't validate if the field shouldn't be shown --- .../restapi/services/submit_form/field.py | 33 +++++++++++++++++++ .../restapi/services/submit_form/post.py | 16 ++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) 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 a07c4069..00074e1b 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/field.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/field.py @@ -5,6 +5,30 @@ validation_message_matcher = re.compile("Validation failed\(([^\)]+)\): ") +def always(): + return True + + +def value_is(value, target_value): + if isinstance(target_value, list): + return value in target_value + return value == target_value + + +def value_is_not(value, target_value): + if isinstance(target_value, list): + return value not in target_value + return value != target_value + + +show_when_validators = { + "": always, + "always": always, + "value_is": value_is, + "value_is_not": value_is_not, +} + + class Field: def __init__(self, field_data): def _attribute(attribute_name: str): @@ -37,6 +61,15 @@ def value(self): def value(self, value): self._value = value + def should_show(self, show_when_is, target_value): + always_show_validator = show_when_validators['always'] + if not show_when_is: + return always_show_validator() + show_when_validator = show_when_validators[show_when_is] + if not show_when_validator: + return always_show_validator + return show_when_validator(value=self.value, target_value=target_value) + @property def label(self): return self._label if self._label else self.field_id 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 21f0b65f..53f9b70d 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/post.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/post.py @@ -105,7 +105,21 @@ def reply(self): errors = {} for field in self.fields: - field_errors = field.validate() + show_when = field.show_when_when + should_show = True + if show_when and show_when != "always": + target_field = [ + val for val in self.fields if val.id == field.show_when_when + ][0] + should_show = ( + target_field.should_show( + show_when_is=field.show_when_is, target_value=field.show_when_to + ) + if target_field + else True + ) + if should_show: + field_errors = field.validate() if field_errors: errors[field.field_id] = field_errors From 77bd00baf053ea46dcb131d1e065b1ad42c7bd20 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Wed, 13 Mar 2024 09:59:20 +0000 Subject: [PATCH 51/57] Fix for validations without settings save --- .../volto/formsupport/restapi/services/submit_form/post.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 53f9b70d..f6e8d725 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/post.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/post.py @@ -84,7 +84,10 @@ def reply(self): for validation_and_setting_id, setting_value in field.get( "validationSettings", {} ).items(): - validation_id, setting_id = validation_and_setting_id.split("-") + split_validation_and_setting_ids = validation_and_setting_id.split("-") + if len(split_validation_and_setting_ids) < 2: + continue + validation_id, setting_id = split_validation_and_setting_ids if validation_id not in validation_ids_to_apply: continue if validation_id not in validations_for_field: From 30a619d5214dec4e0a0aadcd6129d1065e9c1cc3 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Tue, 19 Mar 2024 23:57:51 +0000 Subject: [PATCH 52/57] Skip validating non-required fields --- .../formsupport/restapi/services/submit_form/field.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 00074e1b..7127d4aa 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/field.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/field.py @@ -1,4 +1,5 @@ import re +from typing import Any from collective.volto.formsupport.validation import getValidations @@ -30,7 +31,7 @@ def value_is_not(value, target_value): class Field: - def __init__(self, field_data): + def __init__(self, field_data: dict[str, Any]): def _attribute(attribute_name: str): setattr(self, attribute_name, field_data.get(attribute_name)) @@ -40,11 +41,11 @@ def _attribute(attribute_name: str): _attribute("show_when_is") _attribute("show_when_to") _attribute("input_values") - _attribute("required") _attribute("widget") _attribute("use_as_reply_to") _attribute("use_as_reply_bcc") - _attribute("validations") + self.required = field_data.get("required") + self.validations = field_data.get("validations") self._display_value_mapping = field_data.get("dislpay_value_mapping") self._value = field_data.get("value", "") self._custom_field_id = field_data.get("custom_field_id") @@ -62,7 +63,7 @@ def value(self, value): self._value = value def should_show(self, show_when_is, target_value): - always_show_validator = show_when_validators['always'] + always_show_validator = show_when_validators["always"] if not show_when_is: return always_show_validator() show_when_validator = show_when_validators[show_when_is] @@ -94,6 +95,8 @@ def send_in_email(self): def validate(self): # Making sure we've got a validation that actually exists. + if not self._value and not self.required: + breakpoint() available_validations = [ validation for validationId, validation in getValidations() From 976e0f0f04f99354683c8c0ca6980166d2745248 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Wed, 27 Mar 2024 17:14:09 +0000 Subject: [PATCH 53/57] Fix bad breakpoint... whoops! --- .../volto/formsupport/restapi/services/submit_form/field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7127d4aa..0034586c 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/field.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/field.py @@ -96,7 +96,7 @@ def send_in_email(self): def validate(self): # Making sure we've got a validation that actually exists. if not self._value and not self.required: - breakpoint() + return available_validations = [ validation for validationId, validation in getValidations() From d5e052f861b6bc072da7054aab37ce94a9d7910b Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Tue, 2 Apr 2024 17:26:28 +0100 Subject: [PATCH 54/57] Server-validate required field --- .../formsupport/restapi/services/submit_form/field.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 0034586c..49284270 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/field.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/field.py @@ -97,13 +97,16 @@ def validate(self): # Making sure we've got a validation that actually exists. if not self._value and not self.required: return + errors = {} + + if self.required and not self.internal_value: + errors['required'] = 'This field is required' + available_validations = [ validation for validationId, validation in getValidations() if validationId in self.validations.keys() ] - - errors = {} for validation in available_validations: error = validation(self._value, **self.validations.get(validation._name)) if error: From 3251e4fc0687844a3cc8fc1ec16e48859736071f Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Mon, 8 Apr 2024 17:38:28 +0100 Subject: [PATCH 55/57] Fix hidden fields sometimes validating when they are hidden --- .../volto/formsupport/restapi/services/submit_form/post.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 f6e8d725..b8cf6100 100644 --- a/src/collective/volto/formsupport/restapi/services/submit_form/post.py +++ b/src/collective/volto/formsupport/restapi/services/submit_form/post.py @@ -121,11 +121,12 @@ def reply(self): if target_field else True ) + if should_show: field_errors = field.validate() - if field_errors: - errors[field.field_id] = field_errors + if field_errors: + errors[field.field_id] = field_errors if errors: self.request.response.setStatus(400) From 7df5dda9990eb8b30d2e982323e375b7e2306944 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Wed, 17 Apr 2024 00:24:19 +0100 Subject: [PATCH 56/57] Number of words validator --- .../custom_validators/WordsValidator.py | 47 +++++++++++++++++++ .../validation/custom_validators/__init__.py | 7 ++- 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 src/collective/volto/formsupport/validation/custom_validators/WordsValidator.py diff --git a/src/collective/volto/formsupport/validation/custom_validators/WordsValidator.py b/src/collective/volto/formsupport/validation/custom_validators/WordsValidator.py new file mode 100644 index 00000000..8aae370d --- /dev/null +++ b/src/collective/volto/formsupport/validation/custom_validators/WordsValidator.py @@ -0,0 +1,47 @@ +import re + +from Products.validation.interfaces.IValidator import IValidator +from zope.interface import implementer + + +@implementer(IValidator) +class WordsValidator: + def __init__( + self, + name, + title="", + description="", + words=0, + _internal_type="", + ): + """ "Unused properties are for default values and type information""" + self.name = name + self.title = title or name + self.description = description + self._internal_type = _internal_type + # Default values + self.words = words + + def __call__(self, value="", *args, **kwargs): + words = kwargs.get("words", self.words) + words = int(words) + count = len(re.findall(r"\w+", value)) + + if self._internal_type == "max": + if not value: + return + if count > words: + # TODO: i18n + msg = f"Validation failed({self.name}): is more than {words} words long" + return msg + elif self._internal_type == "min": + if not value or count < words: + # TODO: i18n + msg = f"Validation failed({self.name}): is less than {words} words long" + return msg + elif self._internal_type == "test": + pass + else: + # TODO: i18n + msg = f"Validation failed({self.name}): Unknown words validator type" + return msg diff --git a/src/collective/volto/formsupport/validation/custom_validators/__init__.py b/src/collective/volto/formsupport/validation/custom_validators/__init__.py index cc919f67..4aa56914 100644 --- a/src/collective/volto/formsupport/validation/custom_validators/__init__.py +++ b/src/collective/volto/formsupport/validation/custom_validators/__init__.py @@ -1,7 +1,12 @@ from collective.volto.formsupport.validation.custom_validators.CharactersValidator import ( CharactersValidator, ) +from collective.volto.formsupport.validation.custom_validators.WordsValidator import ( + WordsValidator, +) maxCharacters = CharactersValidator("maxCharacters", _internal_type="max") minCharacters = CharactersValidator("minCharacters", _internal_type="min") -custom_validators = [maxCharacters, minCharacters] +maxWords = WordsValidator("maxWords", _internal_type="max") +minWords = WordsValidator("minWords", _internal_type="min") +custom_validators = [maxCharacters, minCharacters, maxWords, minWords] From bb953baf08f17ddbad94fcff2a50da3ecdeb25f8 Mon Sep 17 00:00:00 2001 From: "me@jeffersonbledsoe.com" Date: Mon, 3 Feb 2025 17:21:03 +0000 Subject: [PATCH 57/57] Handle not installing the validations library correctly --- src/collective/volto/formsupport/validation/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/collective/volto/formsupport/validation/__init__.py b/src/collective/volto/formsupport/validation/__init__.py index 99cd03b1..755db221 100644 --- a/src/collective/volto/formsupport/validation/__init__.py +++ b/src/collective/volto/formsupport/validation/__init__.py @@ -5,14 +5,15 @@ from zope.schema.interfaces import IVocabularyFactory from zope.schema.vocabulary import SimpleVocabulary -from collective.volto.formsupport.validation.custom_validators import custom_validators -from collective.volto.formsupport.validation.definition import ValidationDefinition try: from Products.validation.validators.BaseValidators import baseValidators + from collective.volto.formsupport.validation.custom_validators import custom_validators + from collective.volto.formsupport.validation.definition import ValidationDefinition except ImportError: # Products.validation is optional validation = None baseValidators = None + custom_validators = [] IGNORED_VALIDATION_DEFINITION_ARGUMENTS = [