Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Field validation #40

Draft
wants to merge 60 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
c458ee2
Add class for 'Fields' to simplify custom logic and use it for filter…
JeffersonBledsoe Sep 22, 2023
d6aa386
Fix non-required fields
JeffersonBledsoe Sep 24, 2023
17697d8
Fix reading incorrect internal value
JeffersonBledsoe Sep 24, 2023
b9bb05f
Fix values not displayign
JeffersonBledsoe Sep 24, 2023
3c45a3a
Use @property for 'value'
JeffersonBledsoe Sep 24, 2023
70a17f1
Fix some existing behaviours
JeffersonBledsoe Sep 25, 2023
9bf7879
Add test for custom_field_id
JeffersonBledsoe Sep 25, 2023
624170d
Fix custom field ID
JeffersonBledsoe Sep 25, 2023
3ea20ae
Fix inconsistent use of field_id in requess
JeffersonBledsoe Sep 25, 2023
b3ca7da
Restore support for falling back to field_id if label doesn't exist
JeffersonBledsoe Sep 25, 2023
e821e3f
Fix data storing
JeffersonBledsoe Sep 25, 2023
cd8e9bb
Fix custom_field_id in data export
JeffersonBledsoe Sep 25, 2023
bd40e5c
Merge branch 'main' into custom-label-mapping
JeffersonBledsoe Sep 25, 2023
e1994e0
Add tests for display values
JeffersonBledsoe Sep 26, 2023
b5c75da
Fix catalog using display value for storage
JeffersonBledsoe Sep 26, 2023
f2ff588
Fix XML using external value
JeffersonBledsoe Sep 27, 2023
2cf29d3
Update frontend field name
JeffersonBledsoe Sep 27, 2023
810d675
Test fix
JeffersonBledsoe Sep 27, 2023
9a76a5c
Initial validation support
JeffersonBledsoe Dec 12, 2023
c184b55
Fix HTTP response code
JeffersonBledsoe Dec 12, 2023
bd1e2d0
Fix crash when no validations are set
JeffersonBledsoe Dec 12, 2023
5524684
Readme
JeffersonBledsoe Dec 15, 2023
ac749fb
Typo fix
JeffersonBledsoe Dec 15, 2023
8df42f5
Initial serializer for validation settings
JeffersonBledsoe Dec 19, 2023
94cd023
Add "ignore" to list of internal ignored fields
JeffersonBledsoe Dec 20, 2023
97b1815
Fix serializer deleting the actual validation object's settings
JeffersonBledsoe Dec 20, 2023
ff79791
Fix IPloneSiteRoot import
JeffersonBledsoe Dec 21, 2023
2dcd6fb
Fix validations not occurign
JeffersonBledsoe Dec 22, 2023
afc6b7d
Able to return nice error messages
JeffersonBledsoe Dec 22, 2023
83ad017
WIP: Custom validation
JeffersonBledsoe Dec 22, 2023
262fce7
Merge branch 'main' into custom-label-mapping
JeffersonBledsoe Jan 8, 2024
30adf28
Add test for custom field ID and ensuring it isn't used in emails
JeffersonBledsoe Jan 9, 2024
47ede0b
Fix using field_id in XML
JeffersonBledsoe Jan 9, 2024
3785922
Remove upgrades and test files from coverage
JeffersonBledsoe Jan 9, 2024
adb24a4
Improve tests for sending and storing custom fields
JeffersonBledsoe Jan 11, 2024
c778d90
Fix for storing CSVs
JeffersonBledsoe Jan 11, 2024
1f301f7
Merge branch 'custom-label-mapping' into field-validation
JeffersonBledsoe Jan 12, 2024
a109261
Merge branch 'main' into field-validation
JeffersonBledsoe Jan 12, 2024
b4ac3e1
Quick messy test for validation
JeffersonBledsoe Jan 15, 2024
6606bc1
Fix settings saving (still need to get the correct type information f…
JeffersonBledsoe Jan 15, 2024
731d331
Overhaul how we're passing settings back and forth
JeffersonBledsoe Jan 17, 2024
70d9aaf
Fix setting deserialization after overhall
JeffersonBledsoe Jan 17, 2024
bec2107
Fix settings not being passed to validators
JeffersonBledsoe Jan 17, 2024
61b020a
Fix bad settings appearing in UI
JeffersonBledsoe Jan 17, 2024
2899a93
Fix crash on serializing existing fields
JeffersonBledsoe Jan 18, 2024
e98dd67
Fix type of validations
JeffersonBledsoe Jan 18, 2024
ac67bdd
Fix characters validation behaviour
JeffersonBledsoe Jan 18, 2024
38e9f9b
Fix bad attribute
JeffersonBledsoe Jan 18, 2024
0251d4b
Fix passing type information to the frontend and reduce some complexi…
JeffersonBledsoe Jan 26, 2024
41a7cda
Remove validations and settings if the field type doesn't allow for v…
JeffersonBledsoe Jan 26, 2024
d54879d
Don't include `inNumericRange` as the code isn't great currently
JeffersonBledsoe Jan 29, 2024
7178369
Fix exceptions for existing forms
JeffersonBledsoe Feb 5, 2024
b35cb89
Temp test fix
JeffersonBledsoe Feb 5, 2024
caebd31
Don't validate if the field shouldn't be shown
JeffersonBledsoe Mar 12, 2024
77bd00b
Fix for validations without settings save
JeffersonBledsoe Mar 13, 2024
30a619d
Skip validating non-required fields
JeffersonBledsoe Mar 19, 2024
976e0f0
Fix bad breakpoint... whoops!
JeffersonBledsoe Mar 27, 2024
d5e052f
Server-validate required field
JeffersonBledsoe Apr 2, 2024
3251e4f
Fix hidden fields sometimes validating when they are hidden
JeffersonBledsoe Apr 8, 2024
7df5dda
Number of words validator
JeffersonBledsoe Apr 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
[run]
relative_files = True

omit =
src/collective/volto/formsupport/tests/*
src/collective/volto/formsupport/upgrades.py

[report]
include =
*/src/collective/*
Expand Down
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,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
============
Expand Down
3 changes: 2 additions & 1 deletion base.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ extensions =
parts =
instance
test
code-analysis
# code-analysis
coverage
test-coverage
createcoverage
Expand All @@ -33,6 +33,7 @@ eggs =
Plone
Pillow
collective.volto.formsupport [test]
collective.volto.formsupport [validation]

zcml-additional +=
<configure xmlns="http://namespaces.zope.org/zope"
Expand Down
4 changes: 4 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/collective/volto/formsupport/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

<include file="permissions.zcml" />
<include file="upgrades.zcml" />
<include file="validation/validation.zcml" />

<genericsetup:registerProfile
name="default"
Expand Down
14 changes: 8 additions & 6 deletions src/collective/volto/formsupport/datamanager/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,19 @@ 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 = []
for field_data in data:
field_id = field_data.get("field_id", "")
value = field_data.get("value", "")
field_id = field_data.field_id
# 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]
Expand Down
1 change: 1 addition & 0 deletions src/collective/volto/formsupport/restapi/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

<include package="plone.restapi" />

<include package=".deserializer" />
<include package=".services" />
<include package=".serializer" />

Expand Down
44 changes: 44 additions & 0 deletions src/collective/volto/formsupport/restapi/deserializer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from plone.base.interfaces import IPloneSiteRoot
from plone.restapi.behaviors import IBlocks
from plone.restapi.interfaces import IBlockFieldDeserializationTransformer
from zope.component import adapter
from zope.interface import implementer
from zope.publisher.interfaces.browser import IBrowserRequest


@adapter(IBlocks, IBrowserRequest)
class FormBlockDeserializerBase:
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,
):
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)
class FormBlockDeserializer(FormBlockDeserializerBase):
"""Serializer for content-types with IBlocks behavior"""


@implementer(IBlockFieldDeserializationTransformer)
@adapter(IPloneSiteRoot, IBrowserRequest)
class FormBlockDeserializerRoot(FormBlockDeserializerBase):
"""Serializer for site root"""
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:zcml="http://namespaces.zope.org/zcml"
i18n_domain="plone.restapi"
>

<subscriber
factory="collective.volto.formsupport.restapi.deserializer.FormBlockDeserializer"
provides="plone.restapi.interfaces.IBlockFieldDeserializationTransformer"
/>
<subscriber
factory="collective.volto.formsupport.restapi.deserializer.FormBlockDeserializerRoot"
provides="plone.restapi.interfaces.IBlockFieldDeserializationTransformer"
/>

</configure>
18 changes: 13 additions & 5 deletions src/collective/volto/formsupport/restapi/serializer/blocks.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
# -*- 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 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 get_validation_information


class FormSerializer(object):
Expand Down Expand Up @@ -37,6 +40,11 @@ def __call__(self, value):
attachments_limit = os.environ.get("FORM_ATTACHMENTS_LIMIT", "")
if attachments_limit:
value["attachments_limit"] = attachments_limit

# 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_")}
Expand Down
158 changes: 158 additions & 0 deletions src/collective/volto/formsupport/restapi/services/submit_form/field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import re
from typing import Any

from collective.volto.formsupport.validation import getValidations

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: dict[str, Any]):
def _attribute(attribute_name: str):
setattr(self, attribute_name, field_data.get(attribute_name))

_attribute("field_type")
_attribute("id")
_attribute("show_when_when")
_attribute("show_when_is")
_attribute("show_when_to")
_attribute("input_values")
_attribute("widget")
_attribute("use_as_reply_to")
_attribute("use_as_reply_bcc")
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")
self._label = field_data.get("label")
self._field_id = field_data.get("field_id", "")

@property
def value(self):
if self._display_value_mapping:
return self._display_value_mapping.get(self._value, self._value)
return self._value

@value.setter
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

@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

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()
]
for validation in available_validations:
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
# `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
) # Return None to match how errors normally return in z3c.form


class YesNoField(Field):
@property
def value(self):
if self._display_value_mapping:
if self._value is True:
return self._display_value_mapping.get("yes")
elif self._value is False:
return self._display_value_mapping.get("no")
return self._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]
Loading