From eb20843aff6c45e7ffd7db47da3890a1cdd352f6 Mon Sep 17 00:00:00 2001 From: David Glick Date: Mon, 23 Sep 2024 15:24:19 -0700 Subject: [PATCH] progress on new email processor --- .github/workflows/frontend.yml | 6 +- backend/README.md | 49 +- .../volto/formsupport/captcha/honeypot.py | 2 +- .../volto/formsupport/configure.zcml | 1 + .../volto/formsupport/datamanager/catalog.py | 10 +- .../volto/formsupport/interfaces.py | 1 + .../volto/formsupport/processors/__init__.py | 13 +- .../formsupport/processors/configure.zcml | 8 +- .../volto/formsupport/processors/email.py | 121 ++--- .../volto/formsupport/processors/store.py | 2 +- .../restapi/services/submit_form/post.py | 145 ++--- .../formsupport/testing/event_handler.py | 4 +- backend/tests/functional/conftest.py | 2 +- ...action_form.py => test_email_processor.py} | 505 +++++++----------- backend/tests/functional/test_event.py | 39 +- ...action_form.py => test_store_processor.py} | 166 ++---- .../src/schemaFormBlock/schema.js | 2 +- 17 files changed, 351 insertions(+), 725 deletions(-) rename backend/tests/functional/{test_send_action_form.py => test_email_processor.py} (72%) rename backend/tests/functional/{test_store_action_form.py => test_store_processor.py} (50%) diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 62ced8d..f9d5476 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -26,7 +26,7 @@ jobs: id: vars run: | echo 'BASE_TAG=sha-$(git rev-parse --short HEAD)' >> $GITHUB_OUTPUT - python3 -c 'import json; data = json.load(open("./mrs.developer.json")); print("VOLTO_VERSION=" + data["core"].get("tag") or "latest")' >> $GITHUB_OUTPUT + python3 -c 'import json; data = json.load(open("./mrs.developer.json")); print("VOLTO_VERSION=" + data["core"].get("tag") or data["core"].get("branch") or "latest")' >> $GITHUB_OUTPUT - name: Test vars run: | echo 'BASE_TAG=${{ steps.vars.outputs.BASE_TAG }}' @@ -97,7 +97,6 @@ jobs: packages: write steps: - - name: Checkout uses: actions/checkout@v4 @@ -109,8 +108,7 @@ jobs: ${{ env.IMAGE_NAME_PREFIX }}-${{ env.IMAGE_NAME_SUFFIX }} labels: | org.label-schema.docker.cmd=docker run -d -p 3000:3000 ${{ env.IMAGE_NAME_PREFIX }}-${{ env.IMAGE_NAME_SUFFIX }}:latest - flavor: - latest=false + flavor: latest=false tags: | type=ref,event=branch type=sha diff --git a/backend/README.md b/backend/README.md index 3508593..dec4372 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,6 +1,6 @@ # collective.volto.formsupport -Add some helper routes and functionalities for Volto sites with ``form`` blocks provided by `volto-form-block `_ Volto plugin. +Add some helper routes and functionalities for Volto sites with `form` blocks provided by `volto-form-block `\_ Volto plugin. ## volto-form-block version @@ -15,7 +15,7 @@ Endpoint that the frontend should call as a submit action. You can call it with a POST on the context where the block form is stored like this: ```shell -> curl -i -X POST http://localhost:8080/Plone/my-form/@submit-form -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"block_id": "123456789", "data": [{"field_id": "foo", "value":"foo", "label": "Foo"},{"field_id": "from", "value": "support@foo.com"}, {"field_id":"name", "value": "John Doe", "label": "Name"}]}' +> curl -i -X POST http://localhost:8080/Plone/my-form/@schemaform-data -H 'Accept: application/json' -H 'Content-Type: application/json' --data-raw '{"block_id": "123456789", "data": [{"field_id": "foo", "value":"foo", "label": "Foo"},{"field_id": "from", "value": "support@foo.com"}, {"field_id":"name", "value": "John Doe", "label": "Name"}]}' ``` where: @@ -26,7 +26,6 @@ where: Calling this endpoint, it will do some actions (based on block settings) and returns a `200` response with the submitted data. - ### `@form-data` This is an expansion component. @@ -86,8 +85,8 @@ Reset the store (only for users that have **Modify portal content** permission): Optional parameters could be passed in the payload: -* `block_id` to delete only data related to a specific block on the page, otherwise data from all form blocks on the page will be deleted -* `expired` a boolean that, if `true`, removes only records older than the value of days specified in the block configuration (the above `block_id` parameter is required) +- `block_id` to delete only data related to a specific block on the page, otherwise data from all form blocks on the page will be deleted +- `expired` a boolean that, if `true`, removes only records older than the value of days specified in the block configuration (the above `block_id` parameter is required) ### `@validate-email-address` @@ -100,8 +99,8 @@ Returns a HTTP 204 in case of success or HTTP 400 in case the email is badly com parameters: -* `email` email address. -* `uid` uid related to email field +- `email` email address. +- `uid` uid related to email field ### `@validate-email-token` @@ -114,13 +113,13 @@ Returns HTTP 204 in case of success or HTTP 400 in case of failure :: parameters: -* `email` email address -* `uid` uid used to generate the OTP -* `otp` OTP code +- `email` email address +- `uid` uid used to generate the OTP +- `otp` OTP code ## Form actions -Using `volto-form-block `_ you can set if the form submit should send data to an email address +Using `volto-form-block `\_ you can set if the form submit should send data to an email address or store it into an internal catalog (or both). ### Send @@ -149,13 +148,13 @@ Set the 'Send to' value to include `acknowledgement` to enable this behaviour. T ### Store -If block is set to store data, we store it into the content that has that block (with a `souper.plone `_ catalog). +If block is set to store data, we store it into the content that has that block (with a `souper.plone `\_ catalog). -The store is an adapter registered for *IFormDataStore* interface, so you can override it easily. +The store is an adapter registered for _IFormDataStore_ interface, so you can override it easily. Only fields that are also in block settings are stored. Missing ones will be skipped. -Each Record stores also two *service* attributes: +Each Record stores also two _service_ attributes: - **fields_labels**: a mapping of field ids to field labels. This is useful when we export csv files, so we can labels for the columns. - **fields_order**: sorted list of field ids. This can be used in csv export to keep the order of fields. @@ -168,14 +167,14 @@ The exported CSV file may need to be used by further processes which require spe ## Block serializer -There is a custom block serializer for type ``form``. +There is a custom block serializer for type `form`. -This serializer removes all fields that start with "\**default_**\" if the user can't edit the current context. +This serializer removes all fields that start with "\*\*default\_\*\*\" if the user can't edit the current context. This is useful because we don't want to expose some internals configurations (for example the recipient email address) to external users that should only fill the form. -If the block has a field ``captcha``, an additional property ``captcha_props`` is serialized by the ``serialize`` +If the block has a field `captcha`, an additional property `captcha_props` is serialized by the `serialize` method provided by the ICaptchaSupport named adapter, the result contains useful metadata for the client, as the captcha public_key, ie: @@ -202,7 +201,6 @@ This product contains implementations for: - Custom questions and answers (collective.z3cform.norobots) - Honeypot (collective.honeypot) - Each implementation must be included, installed and configured separately. To include one implementation, you need to install the egg with the needed extras_require: @@ -220,7 +218,7 @@ For captcha support `volto-form-block` version >= 2.4.0 is required. If honeypot dependency is available in the buildout, the honeypot validation is enabled and selectable in forms. -Default field name is `protected_1` and you can change it with an environment variable. See `collective.honeypot `_ for details. +Default field name is `protected_1` and you can change it with an environment variable. See `collective.honeypot `\_ for details. ## Attachments upload limits @@ -257,12 +255,10 @@ This is useful for some SMTP servers that have problems with `quoted-printable` By default the content-transfer-encoding is `quoted-printable` as overridden in https://github.com/zopefoundation/Products.MailHost/blob/master/src/Products/MailHost/MailHost.py#L65 - ## Email subject templating You can also interpolate the form values to the email subject using the field id, in this way: ${123321123} - ## Header forwarding It is possible to configure some headers from the form POST request to be included in the email's headers by configuring the `httpHeaders` field in your volto block. @@ -302,14 +298,12 @@ This add-on can be seen in action at the following sites: - https://www.comune.modena.it/form/contatti - ## Translations This product has been translated into - Italian - ## Installation Install collective.volto.formsupport by adding it to your buildout:: @@ -323,15 +317,13 @@ Install collective.volto.formsupport by adding it to your buildout:: collective.volto.formsupport ``` -and then running ``bin/buildout`` - +and then running `bin/buildout` ## Contribute - Issue Tracker: https://github.com/collective/volto-form-block/issues - Source Code: https://github.com/collective/volto-form-block - ## License The project is licensed under the GPLv2. @@ -341,9 +333,8 @@ The project is licensed under the GPLv2. This product was developed by **RedTurtle Technology** team. .. image:: https://avatars1.githubusercontent.com/u/1087171?s=100&v=4 - :alt: RedTurtle Technology Site - :target: https://www.redturtle.it/ - +:alt: RedTurtle Technology Site +:target: https://www.redturtle.it/ ## Credits and Acknowledgements 🙏 diff --git a/backend/src/collective/volto/formsupport/captcha/honeypot.py b/backend/src/collective/volto/formsupport/captcha/honeypot.py index 8f77dcf..34327c5 100644 --- a/backend/src/collective/volto/formsupport/captcha/honeypot.py +++ b/backend/src/collective/volto/formsupport/captcha/honeypot.py @@ -36,7 +36,7 @@ def verify(self, data): if isinstance(data, str): data = json.loads(data) if not data: - # @submit-form has been called not from volto-form-block so do the standard + # @schemaform-data has been called not from volto-form-block so do the standard # validation. form_data = json_body(self.request).get("data", []) form = {x["label"]: x["value"] for x in form_data} diff --git a/backend/src/collective/volto/formsupport/configure.zcml b/backend/src/collective/volto/formsupport/configure.zcml index 73f3fbc..8302f89 100644 --- a/backend/src/collective/volto/formsupport/configure.zcml +++ b/backend/src/collective/volto/formsupport/configure.zcml @@ -17,5 +17,6 @@ + diff --git a/backend/src/collective/volto/formsupport/datamanager/catalog.py b/backend/src/collective/volto/formsupport/datamanager/catalog.py index 032bce9..c5ab132 100644 --- a/backend/src/collective/volto/formsupport/datamanager/catalog.py +++ b/backend/src/collective/volto/formsupport/datamanager/catalog.py @@ -54,21 +54,13 @@ def get_form_fields(self): {"field_id": name, "label": field.get("title", name)} for name, field in block["schema"]["properties"].items() ] - elif block_type == "form": - subblocks = block.get("subblocks", []) - # Add the 'custom_field_id' field back in as this isn't stored with each - # subblock - for index, field in enumerate(subblocks): - if block.get(field["field_id"]): - subblocks[index]["custom_field_id"] = block.get(field["field_id"]) - return subblocks return {} def add(self, data): form_fields = self.get_form_fields() if not form_fields: logger.error( - f'Block with id {self.block_id} and type "form" not found in context: {self.context.absolute_url()}.' # noqa: E501 + f'Block with id {self.block_id} and type "schemaForm" not found in context: {self.context.absolute_url()}.' # noqa: E501 ) return None diff --git a/backend/src/collective/volto/formsupport/interfaces.py b/backend/src/collective/volto/formsupport/interfaces.py index 1c97f81..52d72ba 100644 --- a/backend/src/collective/volto/formsupport/interfaces.py +++ b/backend/src/collective/volto/formsupport/interfaces.py @@ -3,6 +3,7 @@ from zope.interface import Interface from zope.publisher.interfaces.browser import IDefaultBrowserLayer from ZPublisher.BaseRequest import BaseRequest + import dataclasses diff --git a/backend/src/collective/volto/formsupport/processors/__init__.py b/backend/src/collective/volto/formsupport/processors/__init__.py index e9537f9..4151039 100644 --- a/backend/src/collective/volto/formsupport/processors/__init__.py +++ b/backend/src/collective/volto/formsupport/processors/__init__.py @@ -2,8 +2,11 @@ def filter_parameters(data, block): """ TODO do not send attachments fields. """ - return [{ - "field_id": k, - "value": v, - "label": block["schema"]["properties"].get(k, {}).get("title", k), - } for k, v in data.items()] + return [ + { + "field_id": k, + "value": v, + "label": block["schema"]["properties"].get(k, {}).get("title", k), + } + for k, v in data.items() + ] diff --git a/backend/src/collective/volto/formsupport/processors/configure.zcml b/backend/src/collective/volto/formsupport/processors/configure.zcml index f96d5a9..82ce341 100644 --- a/backend/src/collective/volto/formsupport/processors/configure.zcml +++ b/backend/src/collective/volto/formsupport/processors/configure.zcml @@ -1,8 +1,6 @@ - + - - + + diff --git a/backend/src/collective/volto/formsupport/processors/email.py b/backend/src/collective/volto/formsupport/processors/email.py index 1e67ae6..16883b2 100644 --- a/backend/src/collective/volto/formsupport/processors/email.py +++ b/backend/src/collective/volto/formsupport/processors/email.py @@ -1,8 +1,8 @@ from bs4 import BeautifulSoup from collective.volto.formsupport import _ -from collective.volto.formsupport.processors import filter_parameters from collective.volto.formsupport.interfaces import FormSubmissionContext from collective.volto.formsupport.interfaces import IFormSubmissionProcessor +from collective.volto.formsupport.processors import filter_parameters from email import policy from email.message import EmailMessage from plone import api @@ -13,10 +13,12 @@ from zope.component import getUtility from zope.i18n import translate from zope.interface import implementer + import codecs import os import re + try: from plone.base.interfaces.controlpanel import IMailSchema except ImportError: @@ -55,9 +57,8 @@ def __call__(self): subject = self.get_subject() - # TODO - mfrom = self.form_data.get("from", "") or self.block.get("default_from", "") or mail_settings.email_from_address - mreply_to = self.get_reply_to() + mfrom = mail_settings.email_from_address + mreply_to = self.get_reply_to() or mfrom if not subject or not mfrom: raise BadRequest( @@ -70,11 +71,6 @@ def __call__(self): ) ) - # TODO sort out admin email vs acknowledgment - send_to = self.block.get("send", ["recipient"]) - if not isinstance(send_to, list): - send_to = ["recipient"] if send_to else [] - portal_transforms = api.portal.get_tool(name="portal_transforms") mto = self.block.get("recipients", mail_settings.email_from_address) message = self.prepare_message() @@ -99,57 +95,37 @@ def __call__(self): self.manage_attachments(msg=msg) - if "recipient" in send_to: - self.send_mail(msg=msg, charset=charset) + self.send_mail(msg=msg, charset=charset) # send a copy also to the fields with bcc flag for bcc in self.get_bcc(): msg.replace_header("To", bcc) self.send_mail(msg=msg, charset=charset) - acknowledgement_message = self.block.get("acknowledgementMessage") - if acknowledgement_message and "acknowledgement" in send_to: - acknowledgement_address = self.get_acknowledgement_field_value() - if acknowledgement_address: - acknowledgement_mail = EmailMessage(policy=policy.SMTP) - acknowledgement_mail["Subject"] = subject - acknowledgement_mail["From"] = mfrom - acknowledgement_mail["To"] = acknowledgement_address - ack_msg = acknowledgement_message.get("data") - ack_msg_text = ( - portal_transforms.convertTo( - "text/plain", ack_msg, mimetype="text/html" - ) - .getData() - .strip() - ) - acknowledgement_mail.set_content(ack_msg_text, cte=CTE) - acknowledgement_mail.add_alternative(ack_msg, subtype="html", cte=CTE) - self.send_mail(msg=acknowledgement_mail, charset=charset) + # acknowledgement_message = self.block.get("acknowledgementMessage") + # if acknowledgement_message: + # acknowledgement_address = self.get_acknowledgement_field_value() + # if acknowledgement_address: + # acknowledgement_mail = EmailMessage(policy=policy.SMTP) + # acknowledgement_mail["Subject"] = subject + # acknowledgement_mail["From"] = mfrom + # acknowledgement_mail["To"] = acknowledgement_address + # ack_msg = acknowledgement_message.get("data") + # ack_msg_text = ( + # portal_transforms.convertTo( + # "text/plain", ack_msg, mimetype="text/html" + # ) + # .getData() + # .strip() + # ) + # acknowledgement_mail.set_content(ack_msg_text, cte=CTE) + # acknowledgement_mail.add_alternative(ack_msg, subtype="html", cte=CTE) + # self.send_mail(msg=acknowledgement_mail, charset=charset) def get_reply_to(self): - """This method retrieves the 'reply to' email address. - - Three "levels" of logic: - 1. If there is a field marked with 'use_as_reply_to' set to True, that - field wins and we use that. - If not: - 2. We search for the "from" field. - If not present: - 3. We use the fallback field: "default_from" - """ - - subblocks = self.block.get("subblocks", "") - if subblocks: - for field in subblocks: - if field.get("use_as_reply_to", False): - field_id = field.get("field_id", "") - if field_id: - for data in data.get("data", ""): - if data.get("field_id", "") == field_id: - return data.get("value", "") - - return self.form_data.get("from", "") or self.block.get("default_from", "") + sender = self.block.get("sender", "") + sender = self.substitute_variables(sender) + return sender def get_subject(self): subject = self.block.get("subject") or "${subject}" @@ -161,35 +137,12 @@ def substitute_variables(self, value): return re.sub(pattern, lambda match: self.get_value(match.group(1), ""), value) def get_value(self, field_id, default=None): - if self.block.get("@type") == "schemaForm": - return self.form_data.get(field_id, default) + return self.form_data.get(field_id, default) - for field in self.form_data: - if field.get("field_id") == field_id: - return field.get("value", default) - return default - - def get_bcc(self): - # todo: handle bcc for schemaForm - subblocks = self.block.get("subblocks", []) - if not subblocks: - return [] - - bcc = [] - bcc_fields = [] - for field in self.block.get("subblocks", []): - if field.get("use_as_bcc", False): - field_id = field.get("field_id", "") - if field_id not in bcc_fields: - bcc_fields.append(field_id) - bcc = [] - for field in self.form_data: - value = field.get("value", "") - if not value: - continue - if field.get("field_id", "") in bcc_fields: - bcc.append(field["value"]) - return bcc + def get_bcc(self) -> list: + bcc = self.block.get("bcc", "") + bcc = self.substitute_variables(bcc) + return bcc.split(";") if bcc else [] def prepare_message(self): mail_header = self.block.get("mail_header", {}).get("data", "") @@ -258,11 +211,3 @@ def send_mail(self, msg, charset): # we set immediate=True because we need to catch exceptions. # by default (False) exceptions are handled by MailHost and we can't catch them. host.send(msg, charset=charset, immediate=True) - - def get_acknowledgement_field_value(self): - acknowledgementField = self.block["acknowledgementFields"] - for field in self.block.get("subblocks", []): - if field.get("field_id") == acknowledgementField: - for submitted in self.form_data: - if submitted.get("field_id", "") == field.get("field_id"): - return submitted.get("value") diff --git a/backend/src/collective/volto/formsupport/processors/store.py b/backend/src/collective/volto/formsupport/processors/store.py index 55493db..37a4f07 100644 --- a/backend/src/collective/volto/formsupport/processors/store.py +++ b/backend/src/collective/volto/formsupport/processors/store.py @@ -1,6 +1,6 @@ +from collective.volto.formsupport.interfaces import FormSubmissionContext from collective.volto.formsupport.interfaces import IFormDataStore from collective.volto.formsupport.interfaces import IFormSubmissionProcessor -from collective.volto.formsupport.interfaces import FormSubmissionContext from collective.volto.formsupport.processors import filter_parameters from zExceptions import BadRequest from zope.component import adapter diff --git a/backend/src/collective/volto/formsupport/restapi/services/submit_form/post.py b/backend/src/collective/volto/formsupport/restapi/services/submit_form/post.py index 877346f..b9156d0 100644 --- a/backend/src/collective/volto/formsupport/restapi/services/submit_form/post.py +++ b/backend/src/collective/volto/formsupport/restapi/services/submit_form/post.py @@ -4,15 +4,9 @@ from collective.volto.formsupport.interfaces import IFormSubmissionProcessor from collective.volto.formsupport.interfaces import IPostEvent from collective.volto.formsupport.utils import get_blocks -from collective.volto.formsupport.utils import validate_email_token -from copy import deepcopy -from plone import api - - from plone.protect.interfaces import IDisableCSRFProtection from plone.restapi.deserializer import json_body from plone.restapi.services import Service -from plone.schema.email import _isemail from zExceptions import BadRequest from zope.component import getMultiAdapter from zope.component import subscribers @@ -29,7 +23,6 @@ logger = logging.getLogger(__name__) -CTE = os.environ.get("MAIL_CONTENT_TRANSFER_ENCODING", None) @implementer(IPostEvent) @@ -41,27 +34,31 @@ def __init__(self, context, data): class SubmitPost(Service): def reply(self): + self.body = json_body(self.request) self.block = {} - self.form_data = self.cleanup_data() - self.block_id = self.form_data.get("block_id", "") + self.block_id = self.body.get("block_id", "") if self.block_id: self.block = self.get_block_data(block_id=self.block_id) + self.form_data = self.cleanup_data() self.validate_form() # Disable CSRF protection alsoProvides(self.request, IDisableCSRFProtection) - notify(PostEventService(self.context, self.form_data)) + notify(PostEventService(self.context, self.body)) form_submission_context = FormSubmissionContext( context=self.context, request=self.request, block=self.block, - form_data=self.form_data.get("data", {}), - attachments=self.form_data.get("attachments", {}), + form_data=self.form_data, + attachments=self.body.get("attachments", {}), ) - for handler in sorted(subscribers((form_submission_context,), IFormSubmissionProcessor), key=lambda h: h.order): + for handler in sorted( + subscribers((form_submission_context,), IFormSubmissionProcessor), + key=lambda h: h.order, + ): try: handler() except BadRequest: @@ -71,46 +68,32 @@ def reply(self): message = translate( _( "form_action_exception", - default="Unable to process form. Please retry later or contact site administrator.", # noqa: E501 + default="Unable to process form. " + "Please retry later or contact site administrator.", ), context=self.request, ) self.request.response.setStatus(500) return {"type": "InternalServerError", "message": message} - return {"data": self.form_data.get("data", [])} + return {"data": self.form_data} def cleanup_data(self): - """ - Avoid XSS injections and other attacks. - - - cleanup HTML with plone transform - - remove from data, fields not defined in form schema - """ - form_data = json_body(self.request) - fixed_fields = [] - transforms = api.portal.get_tool(name="portal_transforms") - - block = self.get_block_data(block_id=form_data.get("block_id", "")) - - if block.get("@type") == "form": - block_fields = [x.get("field_id", "") for x in block.get("subblocks", [])] - # cleanup form data if it's a form block - for form_field in form_data.get("data", []): - if form_field.get("field_id", "") not in block_fields: - # unknown field, skip it - continue - new_field = deepcopy(form_field) - value = new_field.get("value", "") - if isinstance(value, str): - stream = transforms.convertTo( - "text/plain", value, mimetype="text/html" - ) - new_field["value"] = stream.getData().strip() - fixed_fields.append(new_field) - form_data["data"] = fixed_fields - - # TODO: cleanup form data if it's a schemaForm block + """Ignore fields not defined in form schema""" + schema = self.block.get("schema", {}) + form_data = self.body.get("data", {}) + if not isinstance(form_data, dict): + raise BadRequest(translate( + _( + "invalid_form_data", + default="Invalid form data.", + ), + context=self.request, + ) + ) + form_data = { + k: v for k, v in form_data.items() if k in schema.get("properties", {}) + } return form_data def validate_form(self): @@ -129,7 +112,7 @@ def validate_form(self): translate( _( "block_form_not_found_label", - default='Block with @type "form" and id "$block" not found in this context: $context', # noqa: E501 + default='Block with @type "schemaForm" and id "$block" not found in this context: $context', # noqa: E501 mapping={ "block": self.block_id, "context": self.context.absolute_url(), @@ -138,8 +121,7 @@ def validate_form(self): context=self.request, ), ) - - if not self.form_data.get("data", []): + if not self.form_data: raise BadRequest( translate( _( @@ -157,17 +139,12 @@ def validate_form(self): (self.context, self.request), ICaptchaSupport, name=self.block["captcha"], - ).verify(self.form_data.get("captcha")) - - self.validate_email_fields() - self.validate_bcc() + ).verify(self.body.get("captcha")) def validate_schema(self): - if self.block.get("@type") != "schemaForm": - return - validator = jsonschema.Draft202012Validator(self.block["schema"]) + validator = jsonschema.Draft202012Validator(self.block.get("schema", {})) errors = [] - for err in validator.iter_errors(self.form_data["data"]): + for err in validator.iter_errors(self.form_data): error = {"message": err.message} if err.path: error["field"] = ".".join(err.path) @@ -175,37 +152,12 @@ def validate_schema(self): if errors: raise BadRequest(json.dumps(errors)) - def validate_email_fields(self): - # TODO: validate email fields for schemaForm block - if self.block["@type"] == "schemaForm": - return - email_fields = [ - x.get("field_id", "") - for x in self.block.get("subblocks", []) - if x.get("field_type", "") == "from" - ] - for form_field in self.form_data.get("data", []): - if form_field.get("field_id", "") not in email_fields: - continue - if _isemail(form_field.get("value", "")) is None: - raise BadRequest( - translate( - _( - "wrong_email", - default='Email not valid in "${field}" field.', - mapping={ - "field": form_field.get("label", ""), - }, - ), - context=self.request, - ) - ) - def validate_attachments(self): + # TODO handle schemaForm attachments attachments_limit = os.environ.get("FORM_ATTACHMENTS_LIMIT", "") if not attachments_limit: return - attachments = self.form_data.get("attachments", {}) + attachments = self.body.get("attachments", {}) attachments_len = 0 for attachment in attachments.values(): data = attachment.get("data", "") @@ -231,31 +183,6 @@ def validate_attachments(self): ) ) - def validate_bcc(self): - # TODO: validate email fields for schemaForm block - if self.block["@type"] == "schemaForm": - return - - bcc_fields = [] - for field in self.block.get("subblocks", []): - if field.get("use_as_bcc", False): - field_id = field.get("field_id", "") - if field_id not in bcc_fields: - bcc_fields.append(field_id) - - for data in self.form_data.get("data", []): - value = data.get("value", "") - if not value: - continue - - if data.get("field_id", "") in bcc_fields: - if not validate_email_token( - self.form_data.get("block_id", ""), data["value"], data["otp"] - ): - raise BadRequest( - _("{email}'s OTP is wrong").format(email=data["value"]) - ) - def get_block_data(self, block_id): blocks = get_blocks(self.context) if not blocks: @@ -264,7 +191,7 @@ def get_block_data(self, block_id): if id_ != block_id: continue block_type = block.get("@type", "") - if not (block_type == "form" or block_type == "schemaForm"): + if block_type != "schemaForm": continue return block return {} diff --git a/backend/src/collective/volto/formsupport/testing/event_handler.py b/backend/src/collective/volto/formsupport/testing/event_handler.py index 2607cc0..aee6ae6 100644 --- a/backend/src/collective/volto/formsupport/testing/event_handler.py +++ b/backend/src/collective/volto/formsupport/testing/event_handler.py @@ -3,6 +3,4 @@ def event_handler(event): if os.environ.get("__TEST_EVENT_HANDLER"): - event.data["data"].append( - {"label": "Reply", "value": "hello"}, - ) + event.data["data"]["reply"] = "hello" diff --git a/backend/tests/functional/conftest.py b/backend/tests/functional/conftest.py index 78c2ea3..46a652d 100644 --- a/backend/tests/functional/conftest.py +++ b/backend/tests/functional/conftest.py @@ -36,7 +36,7 @@ def mailhost(portal): @pytest.fixture def submit_form(manager_request): def func(url, data): - url = f"{url}/@submit-form" + url = f"{url}/@schemaform-data" response = manager_request.post( url, json=data, diff --git a/backend/tests/functional/test_send_action_form.py b/backend/tests/functional/test_email_processor.py similarity index 72% rename from backend/tests/functional/test_send_action_form.py rename to backend/tests/functional/test_email_processor.py index 5def6be..bc0f8c8 100644 --- a/backend/tests/functional/test_send_action_form.py +++ b/backend/tests/functional/test_email_processor.py @@ -24,10 +24,10 @@ def _set_up(self, portal, document, mailhost): self.document_url = self.document.absolute_url() transaction.commit() - def test_email_not_send_if_block_id_is_not_given(self, submit_form): + def test_email_not_sent_if_block_id_is_not_given(self, submit_form): response = submit_form( url=self.document_url, - data={"from": "john@doe.com", "message": "Just want to say hi."}, + data={}, ) transaction.commit() @@ -35,87 +35,73 @@ def test_email_not_send_if_block_id_is_not_given(self, submit_form): assert response.status_code == 400 assert res["message"] == "Missing block_id" - def test_email_not_send_if_block_id_is_incorrect_or_not_present(self, submit_form): + def test_email_not_sent_if_block_id_is_incorrect_or_not_present(self, submit_form): response = submit_form( url=self.document_url, data={ - "from": "john@doe.com", - "message": "Just want to say hi.", "block_id": "unknown", }, ) transaction.commit() - res = response.json() assert response.status_code == 400 assert res["message"] == ( - f'Block with @type "form" and id "unknown" not found in this context: {self.document_url}' # noqa: E501 + f'Block with @type "schemaForm" and id "unknown" not found in this context: {self.document_url}' # noqa: E501 ) + response = submit_form( url=self.document_url, data={ - "from": "john@doe.com", - "message": "Just want to say hi.", "block_id": "text-id", }, ) transaction.commit() - res = response.json() assert response.status_code == 400 assert res["message"] == ( - f'Block with @type "form" and id "text-id" not found in this context: {self.document_url}' # noqa: E501 + f'Block with @type "schemaForm" and id "text-id" not found in this context: {self.document_url}' # noqa: E501 ) - def test_email_not_send_if_no_action_set(self, submit_form): - response = submit_form( - url=self.document_url, - data={"from": "john@doe.com", "block_id": "form-id"}, - ) - transaction.commit() - res = response.json() - assert response.status_code == 400 - assert res["message"] == ( - 'You need to set at least one form action between "send" and "store".' - ) - - def test_email_not_send_if_block_id_is_correct_but_form_data_missing( + def test_email_not_sent_if_block_id_is_correct_but_form_data_missing( self, submit_form ): self.document.blocks = { "form-id": { - "@type": "form", - "send": ["recipient"], + "@type": "schemaForm", + "send": True, }, } transaction.commit() - response = submit_form( url=self.document_url, data={ - "from": "john@doe.com", - "subject": "test subject", "block_id": "form-id", }, ) - transaction.commit() res = response.json() assert response.status_code == 400 assert res["message"] == "Empty form data." - def test_email_not_send_if_block_id_is_correct_but_required_fields_missing( + def test_email_not_sent_if_block_id_is_correct_but_required_fields_missing( self, submit_form ): self.document.blocks = { "form-id": { - "@type": "form", - "send": ["recipient"], - "subblocks": [ - { - "field_id": "xxx", - "field_type": "text", - }, - ], + "@type": "schemaForm", + "send": True, + "schema": { + "fieldsets": [ + { + "id": "default", + "title": "Default", + "fields": ["xxx"], + }, + ], + "properties": { + "xxx": {}, + }, + "required": [], + }, }, } transaction.commit() @@ -123,9 +109,8 @@ def test_email_not_send_if_block_id_is_correct_but_required_fields_missing( response = submit_form( url=self.document_url, data={ - "from": "john@doe.com", "block_id": "form-id", - "data": [{"field_id": "xxx", "label": "foo", "value": "bar"}], + "data": {"xxx": "bar"}, }, ) transaction.commit() @@ -133,11 +118,11 @@ def test_email_not_send_if_block_id_is_correct_but_required_fields_missing( assert response.status_code == 400 assert res["message"] == "Missing required field: subject or from." - def test_email_not_send_if_all_fields_are_not_in_form_schema(self, submit_form): + def test_email_not_sent_if_all_fields_are_not_in_form_schema(self, submit_form): self.document.blocks = { "form-id": { - "@type": "form", - "send": ["recipient"], + "@type": "schemaForm", + "send": True, }, } transaction.commit() @@ -145,9 +130,8 @@ def test_email_not_send_if_all_fields_are_not_in_form_schema(self, submit_form): response = submit_form( url=self.document_url, data={ - "from": "john@doe.com", "block_id": "form-id", - "data": [{"label": "foo", "value": "bar"}], + "data": {"xxx": "bar"}, }, ) transaction.commit() @@ -158,14 +142,23 @@ def test_email_not_send_if_all_fields_are_not_in_form_schema(self, submit_form): def test_email_sent_with_only_fields_from_schema(self, submit_form): self.document.blocks = { "form-id": { - "@type": "form", - "send": ["recipient"], - "subblocks": [ - { - "field_id": "xxx", - "field_type": "text", - }, - ], + "@type": "schemaForm", + "send": True, + "sender": "john@doe.com", + "subject": "test subject", + "schema": { + "fieldsets": [ + { + "id": "default", + "title": "Default", + "fields": ["xxx"], + }, + ], + "properties": { + "xxx": {"title": "foo"}, + }, + "required": [], + }, }, } transaction.commit() @@ -173,24 +166,19 @@ def test_email_sent_with_only_fields_from_schema(self, submit_form): response = submit_form( url=self.document_url, data={ - "from": "john@doe.com", "block_id": "form-id", - "subject": "test subject", - "data": [ - {"label": "foo", "value": "foo", "field_id": "xxx"}, - {"label": "bar", "value": "bar", "field_id": "yyy"}, - ], + "data": {"xxx": "foo", "yyy": "bar"}, }, ) transaction.commit() res = response.json() assert response.status_code == 200 - assert res["data"][0] == {"field_id": "xxx", "label": "foo", "value": "foo"} + assert res["data"] == {"xxx": "foo"} msg = self.mailhost.messages[0].decode("utf-8") msg = re.sub(r"\s+", " ", msg) assert "Subject: test subject" in msg - assert "From: john@doe.com" in msg + assert "From: site_addr@plone.com" in msg assert "To: site_addr@plone.com" in msg assert "Reply-To: john@doe.com" in msg assert "foo: foo" in msg @@ -199,18 +187,24 @@ def test_email_sent_with_only_fields_from_schema(self, submit_form): def test_email_sent_with_site_recipient(self, submit_form): self.document.blocks = { "form-id": { - "@type": "form", - "send": ["recipient"], - "subblocks": [ - { - "field_id": "message", - "field_type": "text", - }, - { - "field_id": "name", - "field_type": "text", - }, - ], + "@type": "schemaForm", + "send": True, + "sender": "john@doe.com", + "subject": "test subject", + "schema": { + "fieldsets": [ + { + "id": "default", + "title": "Default", + "fields": ["message", "name"], + }, + ], + "properties": { + "message": {"title": "Message"}, + "name": {"title": "Name"}, + }, + "required": [], + }, }, } transaction.commit() @@ -218,16 +212,7 @@ def test_email_sent_with_site_recipient(self, submit_form): response = submit_form( url=self.document_url, data={ - "from": "john@doe.com", - "data": [ - { - "field_id": "message", - "label": "Message", - "value": "just want to say hi", - }, - {"field_id": "name", "label": "Name", "value": "John"}, - ], - "subject": "test subject", + "data": {"message": "just want to say hi", "name": "John"}, "block_id": "form-id", }, ) @@ -236,7 +221,7 @@ def test_email_sent_with_site_recipient(self, submit_form): msg = self.mailhost.messages[0].decode("utf-8") msg = re.sub(r"\s+", " ", msg) assert "Subject: test subject" in msg - assert "From: john@doe.com" in msg + assert "From: site_addr@plone.com" in msg assert "To: site_addr@plone.com" in msg assert "Reply-To: john@doe.com" in msg assert "Message: just want to say hi" in msg @@ -245,69 +230,28 @@ def test_email_sent_with_site_recipient(self, submit_form): def test_email_sent_with_forwarded_headers(self, submit_form): self.document.blocks = { "form-id": { - "@type": "form", + "@type": "schemaForm", "send": True, - "httpHeaders": [], - "subblocks": [ - { - "field_id": "message", - "field_type": "text", - }, - { - "field_id": "name", - "field_type": "text", - }, - ], - }, - } - transaction.commit() - - response = submit_form( - url=self.document_url, - data={ - "from": "john@doe.com", - "data": [ - { - "field_id": "message", - "label": "Message", - "value": "just want to say hi", - }, - {"field_id": "name", "label": "Name", "value": "John"}, - ], + "sender": "john@doe.com", "subject": "test subject", - "block_id": "form-id", - }, - ) - transaction.commit() - assert response.status_code == 200 - msg = self.mailhost.messages[0].decode("utf-8") - msg = re.sub(r"\s+", " ", msg) - assert "Subject: test subject" in msg - assert "From: john@doe.com" in msg - assert "To: site_addr@plone.com" in msg - assert "Reply-To: john@doe.com" in msg - assert "Message: just want to say hi" in msg - assert "Name: John" in msg - assert "REMOTE_ADDR" not in msg - - self.document.blocks = { - "form-id": { - "@type": "form", - "send": True, "httpHeaders": [ "REMOTE_ADDR", "PATH_INFO", ], - "subblocks": [ - { - "field_id": "message", - "field_type": "text", - }, - { - "field_id": "name", - "field_type": "text", - }, - ], + "schema": { + "fieldsets": [ + { + "id": "default", + "title": "Default", + "fields": ["message", "name"], + }, + ], + "properties": { + "message": {"title": "Message"}, + "name": {"title": "Name"}, + }, + "required": [], + }, }, } transaction.commit() @@ -315,26 +259,16 @@ def test_email_sent_with_forwarded_headers(self, submit_form): response = submit_form( url=self.document_url, data={ - "from": "john@doe.com", - "data": [ - { - "field_id": "message", - "label": "Message", - "value": "just want to say hi", - }, - {"field_id": "name", "label": "Name", "value": "John"}, - ], - "subject": "test subject", + "data": {"message": "just want to say hi", "name": "John"}, "block_id": "form-id", }, ) transaction.commit() assert response.status_code == 200 - - msg = self.mailhost.messages[1].decode("utf-8") + msg = self.mailhost.messages[0].decode("utf-8") msg = re.sub(r"\s+", " ", msg) assert "Subject: test subject" in msg - assert "From: john@doe.com" in msg + assert "From: site_addr@plone.com" in msg assert "To: site_addr@plone.com" in msg assert "Reply-To: john@doe.com" in msg assert "Message: just want to say hi" in msg @@ -342,70 +276,29 @@ def test_email_sent_with_forwarded_headers(self, submit_form): assert "REMOTE_ADDR" in msg assert "PATH_INFO" in msg - def test_email_sent_ignore_passed_recipient(self, submit_form): - self.document.blocks = { - "form-id": { - "@type": "form", - "send": ["recipient"], - "subblocks": [ - { - "field_id": "message", - "field_type": "text", - }, - { - "field_id": "name", - "field_type": "text", - }, - ], - }, - } - transaction.commit() - - response = submit_form( - url=self.document_url, - data={ - "from": "john@doe.com", - "to": "to@spam.com", - "data": [ - { - "field_id": "message", - "label": "Message", - "value": "just want to say hi", - }, - {"field_id": "name", "label": "Name", "value": "John"}, - ], - "subject": "test subject", - "block_id": "form-id", - }, - ) - transaction.commit() - assert response.status_code == 200 - msg = self.mailhost.messages[0].decode("utf-8") - msg = re.sub(r"\s+", " ", msg) - assert "Subject: test subject" in msg - assert "From: john@doe.com" in msg - assert "To: site_addr@plone.com" in msg - assert "Reply-To: john@doe.com" in msg - assert "Message: just want to say hi" in msg - assert "Name: John" in msg - def test_email_sent_with_block_recipient_if_set(self, submit_form): self.document.blocks = { "text-id": {"@type": "text"}, "form-id": { - "@type": "form", - "default_to": "to@block.com", - "send": ["recipient"], - "subblocks": [ - { - "field_id": "message", - "field_type": "text", - }, - { - "field_id": "name", - "field_type": "text", - }, - ], + "@type": "schemaForm", + "recipients": "to@block.com", + "send": True, + "sender": "john@doe.com", + "subject": "test subject", + "schema": { + "fieldsets": [ + { + "id": "default", + "title": "Default", + "fields": ["message", "name"], + }, + ], + "properties": { + "message": {"title": "Message"}, + "name": {"title": "Name"}, + }, + "required": [], + }, }, } transaction.commit() @@ -413,16 +306,7 @@ def test_email_sent_with_block_recipient_if_set(self, submit_form): response = submit_form( url=self.document_url, data={ - "from": "john@doe.com", - "data": [ - { - "field_id": "message", - "label": "Message", - "value": "just want to say hi", - }, - {"field_id": "name", "label": "Name", "value": "John"}, - ], - "subject": "test subject", + "data": {"message": "just want to say hi", "name": "John"}, "block_id": "form-id", }, ) @@ -431,29 +315,32 @@ def test_email_sent_with_block_recipient_if_set(self, submit_form): msg = self.mailhost.messages[0].decode("utf-8") msg = re.sub(r"\s+", " ", msg) assert "Subject: test subject" in msg - assert "From: john@doe.com" in msg + assert "From: site_addr@plone.com" in msg assert "To: to@block.com" in msg assert "Reply-To: john@doe.com" in msg assert "Message: just want to say hi" in msg assert "Name: John" in msg - def test_email_sent_with_block_subject_if_set_and_not_passed(self, submit_form): + def test_email_sent_with_subject_from_form_data(self, submit_form): self.document.blocks = { - "text-id": {"@type": "text"}, "form-id": { - "@type": "form", - "default_subject": "block subject", - "send": ["recipient"], - "subblocks": [ - { - "field_id": "message", - "field_type": "text", - }, - { - "field_id": "name", - "field_type": "text", - }, - ], + "@type": "schemaForm", + "subject": "${message}", + "send": True, + "schema": { + "fieldsets": [ + { + "id": "default", + "title": "Default", + "fields": ["message", "name"], + }, + ], + "properties": { + "message": {"title": "Message"}, + "name": {"title": "Name"}, + }, + "required": [], + }, }, } transaction.commit() @@ -461,15 +348,7 @@ def test_email_sent_with_block_subject_if_set_and_not_passed(self, submit_form): response = submit_form( url=self.document_url, data={ - "from": "john@doe.com", - "data": [ - { - "field_id": "message", - "label": "Message", - "value": "just want to say hi", - }, - {"field_id": "name", "label": "Name", "value": "John"}, - ], + "data": {"message": "just want to say hi", "name": "John"}, "block_id": "form-id", }, ) @@ -478,36 +357,35 @@ def test_email_sent_with_block_subject_if_set_and_not_passed(self, submit_form): assert response.status_code == 200 msg = self.mailhost.messages[0].decode("utf-8") msg = re.sub(r"\s+", " ", msg) - assert "Subject: block subject" in msg - assert "From: john@doe.com" in msg + assert "Subject: just want to say hi" in msg + assert "From: site_addr@plone.com" in msg assert "To: site_addr@plone.com" in msg - assert "Reply-To: john@doe.com" in msg + assert "Reply-To: site_addr@plone.com" in msg assert "Message: just want to say hi" in msg assert "Name: John" in msg - def test_email_with_use_as_reply_to(self, submit_form): + def test_email_with_sender_from_form_data(self, submit_form): self.document.blocks = { - "text-id": {"@type": "text"}, "form-id": { - "@type": "form", - "default_subject": "block subject", - "default_from": "john@doe.com", - "send": ["recipient"], - "subblocks": [ - { - "field_id": "contact", - "field_type": "from", - "use_as_reply_to": True, - }, - { - "field_id": "message", - "field_type": "text", - }, - { - "field_id": "name", - "field_type": "text", - }, - ], + "@type": "schemaForm", + "send": True, + "sender": "${email}", + "subject": "test subject", + "schema": { + "fieldsets": [ + { + "id": "default", + "title": "Default", + "fields": ["message", "name", "email"], + }, + ], + "properties": { + "message": {"title": "Message"}, + "name": {"title": "Name"}, + "email": {} + }, + "required": [], + }, }, } transaction.commit() @@ -515,15 +393,7 @@ def test_email_with_use_as_reply_to(self, submit_form): response = submit_form( url=self.document_url, data={ - "data": [ - { - "field_id": "message", - "label": "Message", - "value": "just want to say hi", - }, - {"field_id": "name", "label": "Name", "value": "Smith"}, - {"field_id": "contact", "label": "Email", "value": "smith@doe.com"}, - ], + "data": {"message": "just want to say hi", "name": "Smith", "email": "smith@doe.com"}, "block_id": "form-id", }, ) @@ -532,36 +402,34 @@ def test_email_with_use_as_reply_to(self, submit_form): assert response.status_code == 200 msg = self.mailhost.messages[0].decode("utf-8") msg = re.sub(r"\s+", " ", msg) - assert "Subject: block subject" in msg - assert "From: john@doe.com" in msg + assert "From: site_addr@plone.com" in msg assert "To: site_addr@plone.com" in msg assert "Reply-To: smith@doe.com" in msg assert "Message: just want to say hi" in msg assert "Name: Smith" in msg - def test_email_field_used_as_bcc(self, submit_form): + def test_email_with_bcc_from_form_data(self, submit_form): self.document.blocks = { - "text-id": {"@type": "text"}, "form-id": { - "@type": "form", - "default_subject": "block subject", - "default_from": "john@doe.com", - "send": ["recipient"], - "subblocks": [ - { - "field_id": "contact", - "field_type": "from", - "use_as_bcc": True, - }, - { - "field_id": "message", - "field_type": "text", - }, - { - "field_id": "name", - "field_type": "text", - }, - ], + "@type": "schemaForm", + "send": True, + "subject": "test subject", + "bcc": "${email}", + "schema": { + "fieldsets": [ + { + "id": "default", + "title": "Default", + "fields": ["message", "name", "email"], + }, + ], + "properties": { + "message": {"title": "Message"}, + "name": {"title": "Name"}, + "email": {} + }, + "required": [], + }, }, } transaction.commit() @@ -569,18 +437,7 @@ def test_email_field_used_as_bcc(self, submit_form): response = submit_form( url=self.document_url, data={ - "data": [ - {"label": "Message", "value": "just want to say hi"}, - {"label": "Name", "value": "Smith"}, - { - "field_id": "contact", - "label": "Email", - "value": "smith@doe.com", - "otp": generate_email_token( - uid="form-id", email="smith@doe.com" - ), - }, - ], + "data": {"message": "just want to say hi", "name": "Smith", "email": "smith@doe.com"}, "block_id": "form-id", }, ) @@ -589,11 +446,11 @@ def test_email_field_used_as_bcc(self, submit_form): assert response.status_code == 200 assert len(self.mailhost.messages) == 2 msg = self.mailhost.messages[0].decode("utf-8") - assert "To: site_addr@plone.com" in msg - assert "To: smith@doe.com" not in msg + assert "\nTo: site_addr@plone.com" in msg + assert "\nTo: smith@doe.com" not in msg bcc_msg = self.mailhost.messages[1].decode("utf-8") - assert "To: site_addr@plone.com" not in bcc_msg - assert "To: smith@doe.com" in bcc_msg + assert "\nTo: site_addr@plone.com" not in bcc_msg + assert "\nTo: smith@doe.com" in bcc_msg def test_send_attachment(self, submit_form, file_str): self.document.blocks = { @@ -834,7 +691,7 @@ def test_send_recipient_and_acknowledgement(self, submit_form): ) assert "

It is Rich Text

" in ack_msg_body - def test_email_body_formated_as_table(self, submit_form): + def test_email_body_formatted_as_table(self, submit_form): self.document.blocks = { "form-id": { "@type": "form", @@ -894,7 +751,7 @@ def test_email_body_formated_as_table(self, submit_form): assert """""" in msg assert f'{message}' in msg - def test_email_body_formated_as_list(self, submit_form): + def test_email_body_formatted_as_list(self, submit_form): self.document.blocks = { "form-id": { "@type": "form", diff --git a/backend/tests/functional/test_event.py b/backend/tests/functional/test_event.py index 00b3aed..9dc4f4a 100644 --- a/backend/tests/functional/test_event.py +++ b/backend/tests/functional/test_event.py @@ -14,21 +14,24 @@ def _set_up(self, portal, mailhost, document, monkeypatch): self.document.blocks = { "text-id": {"@type": "text"}, "form-id": { - "@type": "form", - "default_subject": "block subject", - "default_from": "john@doe.com", - "send": ["recipient"], - "subblocks": [ - { - "field_id": "contact", - "field_type": "from", - "use_as_bcc": True, + "@type": "schemaForm", + "subject": "block subject", + "sender": "john@doe.com", + "send": True, + "schema": { + "fieldsets": [ + { + "id": "default", + "title": "Default", + "fields": ["message"], + }, + ], + "properties": { + "message": {"title": "Message"}, + "reply": {"title": "Reply"}, }, - { - "field_id": "message", - "field_type": "text", - }, - ], + "required": [], + }, }, } transaction.commit() @@ -37,13 +40,7 @@ def test_trigger_event(self, submit_form): response = submit_form( url=self.document_url, data={ - "data": [ - { - "field_id": "message", - "label": "Message", - "value": "just want to say hi", - }, - ], + "data": {"message": "just want to say hi"}, "block_id": "form-id", }, ) diff --git a/backend/tests/functional/test_store_action_form.py b/backend/tests/functional/test_store_processor.py similarity index 50% rename from backend/tests/functional/test_store_action_form.py rename to backend/tests/functional/test_store_processor.py index ce590d0..12a241a 100644 --- a/backend/tests/functional/test_store_action_form.py +++ b/backend/tests/functional/test_store_processor.py @@ -45,10 +45,10 @@ def _set_up(self, portal, document, registry): transaction.commit() def test_unable_to_store_data(self, submit_form): - """form schema not defined, unable to store data""" + """empty form data, unable to store data""" self.document.blocks = { "form-id": { - "@type": "form", + "@type": "schemaForm", "store": True, }, } @@ -57,16 +57,6 @@ def test_unable_to_store_data(self, submit_form): response = submit_form( url=self.document_url, data={ - "from": "john@doe.com", - "data": [ - { - "field_id": "message", - "label": "Message", - "value": "just want to say hi", - }, - {"field_id": "name", "label": "Name", "value": "John"}, - ], - "subject": "test subject", "block_id": "form-id", }, ) @@ -77,20 +67,22 @@ def test_unable_to_store_data(self, submit_form): def test_store_data(self, submit_form, export_data, export_csv, clear_data): self.document.blocks = { "form-id": { - "@type": "form", + "@type": "schemaForm", "store": True, - "subblocks": [ - { - "label": "Message", - "field_id": "message", - "field_type": "text", - }, - { - "label": "Name", - "field_id": "name", - "field_type": "text", + "schema": { + "fieldsets": [ + { + "id": "default", + "title": "Default", + "fields": ["message", "name"], + }, + ], + "properties": { + "message": {"title": "Message"}, + "name": {"title": "Name"}, }, - ], + "required": [], + }, }, } transaction.commit() @@ -98,13 +90,11 @@ def test_store_data(self, submit_form, export_data, export_csv, clear_data): response = submit_form( url=self.document_url, 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", + "data": { + "message": "just want to say hi", + "name": "John", + "foo": "skip this", + }, "block_id": "form-id", }, ) @@ -127,12 +117,7 @@ def test_store_data(self, submit_form, export_data, export_csv, clear_data): response = submit_form( url=self.document_url, data={ - "from": "sally@doe.com", - "data": [ - {"field_id": "message", "value": "bye"}, - {"field_id": "name", "value": "Sally"}, - ], - "subject": "test subject", + "data": {"message": "bye", "name": "Sally"}, "block_id": "form-id", }, ) @@ -167,33 +152,33 @@ def test_store_data(self, submit_form, export_data, export_csv, clear_data): def test_export_csv(self, submit_form, export_csv): self.document.blocks = { "form-id": { - "@type": "form", + "@type": "schemaForm", "store": True, - "subblocks": [ - { - "label": "Message", - "field_id": "message", - "field_type": "text", - }, - { - "label": "Name", - "field_id": "name", - "field_type": "text", + "schema": { + "fieldsets": [ + { + "id": "default", + "title": "Default", + "fields": ["message", "name"], + }, + ], + "properties": { + "message": {"title": "Message"}, + "name": {"title": "Name"}, }, - ], + "required": [], + }, }, } transaction.commit() response = submit_form( url=self.document_url, 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", + "data": { + "message": "just want to say hi", + "name": "John", + "foo": "skip this", + }, "block_id": "form-id", }, ) @@ -201,12 +186,7 @@ def test_export_csv(self, submit_form, export_csv): response = submit_form( url=self.document_url, data={ - "from": "sally@doe.com", - "data": [ - {"field_id": "message", "value": "bye"}, - {"field_id": "name", "value": "Sally"}, - ], - "subject": "test subject", + "data": {"message": "bye", "name": "Sally"}, "block_id": "form-id", }, ) @@ -224,65 +204,3 @@ def test_export_csv(self, submit_form, export_csv): now = datetime.now().strftime("%Y-%m-%dT%H:%M") assert sorted_data[0][-1].startswith(now) assert sorted_data[1][-1].startswith(now) - - def test_data_id_mapping(self, submit_form, export_csv): - 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", - }, - ], - }, - } - transaction.commit() - response = submit_form( - url=self.document_url, - 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 = submit_form( - url=self.document_url, - data={ - "from": "sally@doe.com", - "data": [ - {"field_id": "message", "value": "bye"}, - {"field_id": "test-field", "value": "Sally"}, - ], - "subject": "test subject", - "block_id": "form-id", - }, - ) - - assert response.status_code == 200 - response = export_csv(self.document_url) - data = [*csv.reader(StringIO(response.text), delimiter=",")] - assert len(data) == 3 - # Check that 'test-field' got renamed - assert data[0] == ["Message", "renamed-field", "date"] - sorted_data = sorted(data[1:]) - assert sorted_data[0][:-1] == ["bye", "Sally"] - assert sorted_data[1][:-1] == ["just want to say hi", "John"] - - # check date column. Skip seconds because can change during test - now = datetime.now().strftime("%Y-%m-%dT%H:%M") - assert sorted_data[0][-1].startswith(now) - assert sorted_data[1][-1].startswith(now) diff --git a/frontend/packages/volto-form-block/src/schemaFormBlock/schema.js b/frontend/packages/volto-form-block/src/schemaFormBlock/schema.js index 2e759af..5a5ef7f 100644 --- a/frontend/packages/volto-form-block/src/schemaFormBlock/schema.js +++ b/frontend/packages/volto-form-block/src/schemaFormBlock/schema.js @@ -279,6 +279,6 @@ export const schemaFormBlockSchema = ({ data, intl }) => { default: -1, }, }, - required: ['default_from', ...conditional_required], + required: ['subject', ...conditional_required], }; };