From a05c03d7ca17b9d0800589981e2fec46e6f6fc56 Mon Sep 17 00:00:00 2001 From: Filippo Campi Date: Wed, 24 Jul 2024 14:57:25 +0200 Subject: [PATCH 1/8] Add supporto for limit number of submit in collective.volto.formsupport + added possibility to add a unique constraint for form block --- src/design/plone/policy/__init__.py | 2 + src/design/plone/policy/patches/__init__.py | 15 ++ .../patches/collective_volto_formsupport.py | 244 ++++++++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 src/design/plone/policy/patches/__init__.py create mode 100644 src/design/plone/policy/patches/collective_volto_formsupport.py diff --git a/src/design/plone/policy/__init__.py b/src/design/plone/policy/__init__.py index c52d626..34e8a1a 100644 --- a/src/design/plone/policy/__init__.py +++ b/src/design/plone/policy/__init__.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- """Init and utils.""" +from .patches import apply as apply_patches from .sensitive import apply from zope.i18nmessageid import MessageFactory _ = MessageFactory("design.plone.policy") apply() +apply_patches() diff --git a/src/design/plone/policy/patches/__init__.py b/src/design/plone/policy/patches/__init__.py new file mode 100644 index 0000000..090c78f --- /dev/null +++ b/src/design/plone/policy/patches/__init__.py @@ -0,0 +1,15 @@ +from design.plone.policy.patches.collective_volto_formsupport import ( + patch_FormDataExportGet_get_data, +) +from design.plone.policy.patches.collective_volto_formsupport import ( + patch_FormDataStore_methods, +) +from design.plone.policy.patches.collective_volto_formsupport import ( + patch_SubmitPost_reply, +) + + +def apply(): + patch_FormDataExportGet_get_data() + patch_SubmitPost_reply() + patch_FormDataStore_methods() diff --git a/src/design/plone/policy/patches/collective_volto_formsupport.py b/src/design/plone/policy/patches/collective_volto_formsupport.py new file mode 100644 index 0000000..7fe972c --- /dev/null +++ b/src/design/plone/policy/patches/collective_volto_formsupport.py @@ -0,0 +1,244 @@ +# -*- coding: utf-8 -*- +""" +We use this file to change the base behavior of collective.volto.formsupport +to support some new feature: + - limit on form submit + - unique field in one form + +Why do we use monkeypatch instead of overriding the classes? +Because it's temporary, until collective.volto.formsupport can support backend +validation for data +""" +from collective.volto.formsupport import _ +from collective.volto.formsupport.datamanager.catalog import FormDataStore +from collective.volto.formsupport.interfaces import IFormDataStore +from collective.volto.formsupport.restapi.services.form_data.csv import ( + FormDataExportGet, +) +from collective.volto.formsupport.restapi.services.submit_form.post import logger +from collective.volto.formsupport.restapi.services.submit_form.post import ( + PostEventService, +) +from collective.volto.formsupport.restapi.services.submit_form.post import SubmitPost +from datetime import datetime +from io import StringIO +from plone.protect.interfaces import IDisableCSRFProtection +from plone.restapi.serializer.converters import json_compatible +from souper.soup import Record +from zExceptions import BadRequest +from zope.component import getMultiAdapter +from zope.event import notify +from zope.i18n import translate +from zope.interface import alsoProvides + +import csv + + +SKIP_ATTRS = ["block_id", "fields_labels", "fields_order"] + + +def get_data(self): + store = getMultiAdapter((self.context, self.request), IFormDataStore) + sbuf = StringIO() + fixed_columns = ["date"] + columns = [] + # # # start patch + custom_colums = [] + if self.form_block.get("limit", None) is not None: + limit = int(self.form_block["limit"]) + if limit > -1: + custom_colums.append("waiting_list") + # # # + + rows = [] + # # # start patch + for index, item in enumerate(store.search()): + # # # + data = {} + fields_labels = item.attrs.get("fields_labels", {}) + for k in self.get_ordered_keys(item): + if k in SKIP_ATTRS: + continue + value = item.attrs.get(k, None) + label = fields_labels.get(k, k) + if label not in columns and label not in fixed_columns: + columns.append(label) + data[label] = json_compatible(value) + for k in fixed_columns: + # add fixed columns values + value = item.attrs.get(k, None) + data[k] = json_compatible(value) + # # # start patch + if "waiting_list" in custom_colums: + data.update( + { + "waiting_list": ( + translate(_("yes_label", default="Yes")) + if not (index < limit) + else translate(_("no_label", default="No")) + ) + } + ) + # # # + + rows.append(data) + columns.extend(fixed_columns) + columns.extend(custom_colums) + writer = csv.DictWriter(sbuf, fieldnames=columns, quoting=csv.QUOTE_ALL) + writer.writeheader() + for row in rows: + writer.writerow(row) + res = sbuf.getvalue() + sbuf.close() + return res + + +def patch_FormDataExportGet_get_data(): + logger.info( + "Patch get_data methos of class FormDataExporterGet from collective.volto.formsupport" # noqa + ) + FormDataExportGet.get_data = get_data + + +def reply(self): + self.validate_form() + + self.store_action = self.block.get("store", False) + self.send_action = self.block.get("send", []) + self.submit_limit = int(self.block.get("limit", "-1")) + + # Disable CSRF protection + alsoProvides(self.request, IDisableCSRFProtection) + + notify(PostEventService(self.context, self.form_data)) + data = self.form_data.get("data", []) + + if self.send_action: + try: + self.send_data() + except BadRequest as e: + raise e + except Exception as e: + logger.exception(e) + message = translate( + _( + "mail_send_exception", + default="Unable to send confirm email. Please retry later or contact site administrator.", # noqa + ), + context=self.request, + ) + self.request.response.setStatus(500) + return dict(type="InternalServerError", message=message) + if self.store_action: + try: + data = self.store_data() + except ValueError as e: + logger.exception(e) + message = translate( + _( + "save_data_exception", + default="Unable to save data. Value not unique: '${fields}'", + mapping={"fields": e.args[0]}, + ), + context=self.request, + ) + self.request.response.setStatus(500) + return dict(type="InternalServerError", message=message) + + return {"data": data} + + +def store_data(self): + store = getMultiAdapter((self.context, self.request), IFormDataStore) + data = {"form_data": self.filter_parameters()} + + res = store.add(data=data) + if not res: + raise BadRequest("Unable to store data") + + data.update( + { + "waiting_list": self.submit_limit is not None + and -1 < self.submit_limit < self.count_data() + } + ) + + return data + + +def count_data(self): + store = getMultiAdapter((self.context, self.request), IFormDataStore) + return store.count() + + +def patch_SubmitPost_reply(): + logger.info( + "Patch reply method of class SubmitPost from collective.volto.formsupport" + ) + SubmitPost.reply = reply + SubmitPost.store_data = store_data + SubmitPost.count_data = count_data + + +def add(self, data): + form_fields = self.get_form_fields() + if not form_fields: + logger.error( + 'Block with id {} and type "form" not found in context: {}.'.format( + self.block_id, self.context.absolute_url() + ) + ) + return None + + 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 = [] + for field_data in data["form_data"]: + field_id = field_data.get("field_id", "") + value = field_data.get("value", "") + if field_id in fields: + record.attrs[field_id] = value + fields_labels[field_id] = fields[field_id] + fields_order.append(field_id) + record.attrs["fields_labels"] = fields_labels + record.attrs["fields_order"] = fields_order + record.attrs["date"] = datetime.now() + record.attrs["block_id"] = self.block_id + + keys = [(x["field_id"], x["label"]) for x in form_fields if x.get("unique", False)] + if keys: + saved_data = self.soup.data.values() + for saved_record in saved_data: + unique = False + for key in keys: + if record.attrs.storage[key[0]] != saved_record.attrs.storage[key[0]]: + unique = True + break + + if not unique: + raise ValueError(f" {', '.join([x[1] for x in keys])}") + + return self.soup.add(record) + + +def count(self, query=None): + records = [] + if not query: + records = self.soup.data.values() + + return len(records) + + +def patch_FormDataStore_methods(): + logger.info( + "Patch method add of FormDataStore class for collective.volto.formsupport" + ) + FormDataStore.add = add + logger.info( + "Add method count of FormDataStore class for collective.volto.formsupport" + ) + FormDataStore.count = count From 09591a21c1c6a6745bffb10d9e66f6db6e473386 Mon Sep 17 00:00:00 2001 From: Filippo Campi Date: Fri, 9 Aug 2024 13:02:43 +0200 Subject: [PATCH 2/8] add some comment --- .../patches/collective_volto_formsupport.py | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/design/plone/policy/patches/collective_volto_formsupport.py b/src/design/plone/policy/patches/collective_volto_formsupport.py index 7fe972c..376a8b9 100644 --- a/src/design/plone/policy/patches/collective_volto_formsupport.py +++ b/src/design/plone/policy/patches/collective_volto_formsupport.py @@ -42,18 +42,18 @@ def get_data(self): sbuf = StringIO() fixed_columns = ["date"] columns = [] - # # # start patch + # start patch custom_colums = [] if self.form_block.get("limit", None) is not None: limit = int(self.form_block["limit"]) if limit > -1: custom_colums.append("waiting_list") - # # # + # end patch rows = [] - # # # start patch + # start patch for index, item in enumerate(store.search()): - # # # + # end patch data = {} fields_labels = item.attrs.get("fields_labels", {}) for k in self.get_ordered_keys(item): @@ -68,7 +68,7 @@ def get_data(self): # add fixed columns values value = item.attrs.get(k, None) data[k] = json_compatible(value) - # # # start patch + # start patch if "waiting_list" in custom_colums: data.update( { @@ -79,7 +79,7 @@ def get_data(self): ) } ) - # # # + # end patch rows.append(data) columns.extend(fixed_columns) @@ -103,6 +103,7 @@ def patch_FormDataExportGet_get_data(): def reply(self): self.validate_form() + # start patch self.store_action = self.block.get("store", False) self.send_action = self.block.get("send", []) self.submit_limit = int(self.block.get("limit", "-1")) @@ -112,6 +113,7 @@ def reply(self): notify(PostEventService(self.context, self.form_data)) data = self.form_data.get("data", []) + # end patch if self.send_action: try: @@ -129,6 +131,7 @@ def reply(self): ) self.request.response.setStatus(500) return dict(type="InternalServerError", message=message) + # start patch if self.store_action: try: data = self.store_data() @@ -146,10 +149,12 @@ def reply(self): return dict(type="InternalServerError", message=message) return {"data": data} + # end patch def store_data(self): store = getMultiAdapter((self.context, self.request), IFormDataStore) + # start patch data = {"form_data": self.filter_parameters()} res = store.add(data=data) @@ -171,6 +176,9 @@ def count_data(self): return store.count() +# end patch + + def patch_SubmitPost_reply(): logger.info( "Patch reply method of class SubmitPost from collective.volto.formsupport" @@ -197,7 +205,9 @@ def add(self, data): record = Record() fields_labels = {} fields_order = [] + # start patch for field_data in data["form_data"]: + # end patch field_id = field_data.get("field_id", "") value = field_data.get("value", "") if field_id in fields: @@ -209,6 +219,7 @@ def add(self, data): record.attrs["date"] = datetime.now() record.attrs["block_id"] = self.block_id + # start patch keys = [(x["field_id"], x["label"]) for x in form_fields if x.get("unique", False)] if keys: saved_data = self.soup.data.values() @@ -221,10 +232,12 @@ def add(self, data): if not unique: raise ValueError(f" {', '.join([x[1] for x in keys])}") + # end patch return self.soup.add(record) +# start patch def count(self, query=None): records = [] if not query: @@ -233,6 +246,9 @@ def count(self, query=None): return len(records) +# end patch + + def patch_FormDataStore_methods(): logger.info( "Patch method add of FormDataStore class for collective.volto.formsupport" From 7add554954700573a2624e684d3caded8da1304e Mon Sep 17 00:00:00 2001 From: Filippo Campi Date: Fri, 9 Aug 2024 13:11:27 +0200 Subject: [PATCH 3/8] black, flake8, zpretty --- .pre-commit-config.yaml | 2 +- src/design/plone/policy/profiles/default/registry.xml | 4 ++-- src/design/plone/policy/profiles/default/rolemap.xml | 4 +++- src/design/plone/policy/restapi/search_filters/get.py | 2 +- src/design/plone/policy/testing.py | 2 +- src/design/plone/policy/tests/test_initial_structure.py | 2 +- src/design/plone/policy/tests/test_registry_entries.py | 9 ++++----- src/design/plone/policy/upgrades.py | 9 +++++---- 8 files changed, 18 insertions(+), 16 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 38f35c0..b372374 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: types: [python] args: ["--max-complexity=30", "--max-line-length=88", "--ignore=E203,DJ01,DJ08,W503,ANN101", "--exclude=docs/*", "src/", "setup.py"] - repo: https://github.com/pycqa/isort - rev: 5.10.1 + rev: 5.13.2 hooks: - id: isort name: isort (python) diff --git a/src/design/plone/policy/profiles/default/registry.xml b/src/design/plone/policy/profiles/default/registry.xml index 347ab87..627d9f8 100644 --- a/src/design/plone/policy/profiles/default/registry.xml +++ b/src/design/plone/policy/profiles/default/registry.xml @@ -46,9 +46,9 @@ data-element - + diff --git a/src/design/plone/policy/profiles/default/rolemap.xml b/src/design/plone/policy/profiles/default/rolemap.xml index c261573..957db82 100644 --- a/src/design/plone/policy/profiles/default/rolemap.xml +++ b/src/design/plone/policy/profiles/default/rolemap.xml @@ -1,7 +1,9 @@ - + diff --git a/src/design/plone/policy/restapi/search_filters/get.py b/src/design/plone/policy/restapi/search_filters/get.py index 6b8f0d8..eda10cd 100644 --- a/src/design/plone/policy/restapi/search_filters/get.py +++ b/src/design/plone/policy/restapi/search_filters/get.py @@ -5,8 +5,8 @@ from plone.registry.interfaces import IRegistry from plone.restapi.interfaces import ISerializeToJsonSummary from plone.restapi.services import Service -from Products.CMFPlone.interfaces import ISearchSchema from Products.CMFCore.utils import getToolByName +from Products.CMFPlone.interfaces import ISearchSchema from zope.component import getMultiAdapter from zope.component import getUtility from zope.i18n import translate diff --git a/src/design/plone/policy/testing.py b/src/design/plone/policy/testing.py index 340ead9..ca18b2a 100644 --- a/src/design/plone/policy/testing.py +++ b/src/design/plone/policy/testing.py @@ -16,10 +16,10 @@ import collective.volto.dropdownmenu import collective.volto.formsupport import collective.volto.secondarymenu +import collective.volto.slimheader import collective.volto.socialsettings import collective.volto.subfooter import collective.volto.subsites -import collective.volto.slimheader import collective.z3cform.datagridfield import design.plone.contenttypes import design.plone.policy diff --git a/src/design/plone/policy/tests/test_initial_structure.py b/src/design/plone/policy/tests/test_initial_structure.py index 11f6ffa..9bfb1ab 100644 --- a/src/design/plone/policy/tests/test_initial_structure.py +++ b/src/design/plone/policy/tests/test_initial_structure.py @@ -7,8 +7,8 @@ from design.plone.policy.utils import TASSONOMIA_SERVIZI from plone.app.testing import setRoles from plone.app.testing import TEST_USER_ID -from plone.restapi.behaviors import IBlocks from plone.i18n.normalizer.interfaces import IURLNormalizer +from plone.restapi.behaviors import IBlocks from zope.component import getUtility import unittest diff --git a/src/design/plone/policy/tests/test_registry_entries.py b/src/design/plone/policy/tests/test_registry_entries.py index c60c41a..b1574fc 100644 --- a/src/design/plone/policy/tests/test_registry_entries.py +++ b/src/design/plone/policy/tests/test_registry_entries.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- +from design.plone.policy.testing import DESIGN_PLONE_POLICY_INTEGRATION_TESTING +from plone import api from plone.app.testing import setRoles from plone.app.testing import TEST_USER_ID -from plone import api - -from design.plone.policy.testing import DESIGN_PLONE_POLICY_INTEGRATION_TESTING import unittest @@ -17,11 +16,11 @@ def setUp(self): setRoles(self.portal, TEST_USER_ID, ["Manager"]) - def test_plone_base_interfaces_syndication_ISiteSyndicationSettings_show_author_info( + def test_plone_base_interfaces_syndication_ISiteSyndicationSettings_show_author_info( # noqa self, ): self.assertFalse( api.portal.get_registry_record( - "plone.base.interfaces.syndication.ISiteSyndicationSettings.show_author_info" + "plone.base.interfaces.syndication.ISiteSyndicationSettings.show_author_info" # noqa ) ) diff --git a/src/design/plone/policy/upgrades.py b/src/design/plone/policy/upgrades.py index 7c7ce07..b9d9b2e 100644 --- a/src/design/plone/policy/upgrades.py +++ b/src/design/plone/policy/upgrades.py @@ -2,10 +2,10 @@ from Acquisition import aq_base from collective.volto.blocksfield.field import BlocksField from copy import deepcopy +from design.plone.policy.interfaces import IDesignPlonePolicySettings from design.plone.policy.setuphandlers import disable_searchable_types from design.plone.policy.setuphandlers import set_default_subsite_colors from design.plone.policy.utils import create_default_blocks -from design.plone.policy.interfaces import IDesignPlonePolicySettings from plone import api from plone.app.upgrade.utils import installOrReinstallProduct from plone.dexterity.utils import iterSchemata @@ -16,7 +16,6 @@ from zope.component import getUtility from zope.schema import getFields - import json import logging @@ -375,7 +374,8 @@ def to_3101(context): for name, field in getFields(schema).items(): if name == "blocks": logger.info( - f"[3100 - 3101] Deleting twitter blocks if exist from {'/'.join(item.getPhysicalPath())}" + f"[3100 - 3101] Deleting twitter blocks if " + f"exist from {'/'.join(item.getPhysicalPath())}" ) twitter_block_uids = [] @@ -410,7 +410,8 @@ def to_3101(context): if blocks: logger.info( - f"[3100 - 3101] Deleting twitter blocks if exist from {'/'.join(item.getPhysicalPath())}" + f"[3100 - 3101] Deleting twitter blocks if " + f"exist from {'/'.join(item.getPhysicalPath())}" ) twitter_block_uids = [] From 9e095e552c5628fdfece95ad48934ce9fe5c2a9d Mon Sep 17 00:00:00 2001 From: Filippo Campi Date: Fri, 9 Aug 2024 15:06:00 +0200 Subject: [PATCH 4/8] fixed code for waiting_list --- .../plone/policy/patches/collective_volto_formsupport.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/design/plone/policy/patches/collective_volto_formsupport.py b/src/design/plone/policy/patches/collective_volto_formsupport.py index 376a8b9..b2df078 100644 --- a/src/design/plone/policy/patches/collective_volto_formsupport.py +++ b/src/design/plone/policy/patches/collective_volto_formsupport.py @@ -161,12 +161,10 @@ def store_data(self): if not res: raise BadRequest("Unable to store data") - data.update( - { - "waiting_list": self.submit_limit is not None - and -1 < self.submit_limit < self.count_data() - } + waiting_list = ( + self.submit_limit is not None and -1 < self.submit_limit < self.count_data() ) + data.update({"waiting_list": waiting_list}) return data From 41000e2c0733e43e3ec619860c579b4b6911eac6 Mon Sep 17 00:00:00 2001 From: Filippo Campi Date: Fri, 9 Aug 2024 17:29:41 +0200 Subject: [PATCH 5/8] [dev] test --- .../policy/tests/test_limit_submit_form.py | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 src/design/plone/policy/tests/test_limit_submit_form.py diff --git a/src/design/plone/policy/tests/test_limit_submit_form.py b/src/design/plone/policy/tests/test_limit_submit_form.py new file mode 100644 index 0000000..f2e600a --- /dev/null +++ b/src/design/plone/policy/tests/test_limit_submit_form.py @@ -0,0 +1,105 @@ +from collective.volto.formsupport.testing import ( # noqa: E501, + VOLTO_FORMSUPPORT_API_FUNCTIONAL_TESTING, +) +from datetime import datetime +from io import StringIO +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.restapi.testing import RelativeSession +from Products.MailHost.interfaces import IMailHost +from zope.component import getUtility + +import csv +import transaction +import unittest + + +class TestLimitMailStore(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) + + 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"}, + } + transaction.commit() + + def submit_form(self, data): + url = f"{self.document_url}/@submit-form" + response = self.api_session.post( + url, + json=data, + ) + transaction.commit() + return response + + def test_limit_submit(self): + self.document.blocks = { + "form-id": { + "@type": "form", + "store": True, + "limit": 1, + "subblocks": [ + { + "label": "Message", + "field_id": "message", + "field_type": "text", + }, + { + "label": "Name", + "field_id": "name", + "field_type": "text", + }, + ], + }, + } + transaction.commit() + + response = self.submit_form( + data={ + "from": "john@doe.com", + "data": [ + {"field_id": "message", "value": "just want to say hi"}, + {"field_id": "name", "value": "John"}, + {"field_id": "foo", "value": "skip this"}, + ], + "subject": "test subject", + "block_id": "form-id", + }, + ) + transaction.commit() + self.assertEqual(response.status_code, 200) From ec018da8970277ceb24ca0d134ea584efdb5a1a9 Mon Sep 17 00:00:00 2001 From: Filippo Campi Date: Mon, 12 Aug 2024 12:14:20 +0200 Subject: [PATCH 6/8] added test --- .../patches/collective_volto_formsupport.py | 2 +- .../policy/tests/test_limit_submit_form.py | 69 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/design/plone/policy/patches/collective_volto_formsupport.py b/src/design/plone/policy/patches/collective_volto_formsupport.py index b2df078..2b378c6 100644 --- a/src/design/plone/policy/patches/collective_volto_formsupport.py +++ b/src/design/plone/policy/patches/collective_volto_formsupport.py @@ -232,7 +232,7 @@ def add(self, data): raise ValueError(f" {', '.join([x[1] for x in keys])}") # end patch - return self.soup.add(record) + return self.soup.add(record) # start patch diff --git a/src/design/plone/policy/tests/test_limit_submit_form.py b/src/design/plone/policy/tests/test_limit_submit_form.py index f2e600a..b12ad0e 100644 --- a/src/design/plone/policy/tests/test_limit_submit_form.py +++ b/src/design/plone/policy/tests/test_limit_submit_form.py @@ -103,3 +103,72 @@ def test_limit_submit(self): ) transaction.commit() self.assertEqual(response.status_code, 200) + + response = self.submit_form( + data={ + "from": "john@doe.com", + "data": [ + {"field_id": "message", "value": "just want to say hi"}, + {"field_id": "name", "value": "John"}, + {"field_id": "foo", "value": "skip this"}, + ], + "subject": "test subject", + "block_id": "form-id", + }, + ) + transaction.commit() + self.assertEqual(response.status_code, 200) + self.assertTrue(response.json()["data"]["waiting_list"]) + + def test_unique_field(self): + self.document.blocks = { + "form-id": { + "@type": "form", + "store": True, + "subblocks": [ + { + "label": "Message", + "field_id": "message", + "field_type": "text", + }, + { + "label": "Name", + "field_id": "name", + "field_type": "text", + "unique": True, + }, + ], + }, + } + transaction.commit() + + response = self.submit_form( + data={ + "from": "john@doe.com", + "data": [ + {"field_id": "message", "value": "just want to say hi"}, + {"field_id": "name", "value": "John"}, + {"field_id": "foo", "value": "skip this"}, + ], + "subject": "test subject", + "block_id": "form-id", + }, + ) + transaction.commit() + self.assertEqual(response.status_code, 200) + + response = self.submit_form( + data={ + "from": "john@doe.com", + "data": [ + {"field_id": "message", "value": "just want to say hi"}, + {"field_id": "name", "value": "John"}, + {"field_id": "foo", "value": "skip this"}, + ], + "subject": "test subject", + "block_id": "form-id", + }, + ) + transaction.commit() + self.assertEqual(response.status_code, 500) + self.assertTrue("Value not unique" in response.json()["message"]) From daf722adc5880d21e041d63956efdac8092a5d56 Mon Sep 17 00:00:00 2001 From: Filippo Campi Date: Mon, 12 Aug 2024 12:14:47 +0200 Subject: [PATCH 7/8] added test + flake --- src/design/plone/policy/tests/test_limit_submit_form.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/design/plone/policy/tests/test_limit_submit_form.py b/src/design/plone/policy/tests/test_limit_submit_form.py index b12ad0e..2c41ae4 100644 --- a/src/design/plone/policy/tests/test_limit_submit_form.py +++ b/src/design/plone/policy/tests/test_limit_submit_form.py @@ -1,8 +1,6 @@ from collective.volto.formsupport.testing import ( # noqa: E501, VOLTO_FORMSUPPORT_API_FUNCTIONAL_TESTING, ) -from datetime import datetime -from io import StringIO from plone import api from plone.app.testing import setRoles from plone.app.testing import SITE_OWNER_NAME @@ -12,7 +10,6 @@ from Products.MailHost.interfaces import IMailHost from zope.component import getUtility -import csv import transaction import unittest From 84509d76eb4756b5492d0076fc404f6fbc6fd6dd Mon Sep 17 00:00:00 2001 From: Filippo Campi Date: Wed, 21 Aug 2024 11:52:46 +0200 Subject: [PATCH 8/8] chaged order of submit in export --- .../plone/policy/patches/collective_volto_formsupport.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/design/plone/policy/patches/collective_volto_formsupport.py b/src/design/plone/policy/patches/collective_volto_formsupport.py index 2b378c6..0558c55 100644 --- a/src/design/plone/policy/patches/collective_volto_formsupport.py +++ b/src/design/plone/policy/patches/collective_volto_formsupport.py @@ -52,7 +52,7 @@ def get_data(self): rows = [] # start patch - for index, item in enumerate(store.search()): + for index, item in enumerate(reversed(store.search())): # end patch data = {} fields_labels = item.attrs.get("fields_labels", {}) @@ -68,6 +68,7 @@ def get_data(self): # add fixed columns values value = item.attrs.get(k, None) data[k] = json_compatible(value) + # start patch if "waiting_list" in custom_colums: data.update(