Added RGPD consent management properties
authorThierry Florac <tflorac@ulthar.net>
Tue, 17 Sep 2019 12:03:03 +0200
changeset 1346 88b5ce31afdc
parent 1345 9b406fb98cfa
child 1347 0230c7fd71c3
Added RGPD consent management properties
src/pyams_content/shared/form/__init__.py
src/pyams_content/shared/form/field.py
src/pyams_content/shared/form/handler.py
src/pyams_content/shared/form/interfaces.py
src/pyams_content/shared/form/zmi/properties.py
--- a/src/pyams_content/shared/form/__init__.py	Mon Sep 16 16:57:01 2019 +0200
+++ b/src/pyams_content/shared/form/__init__.py	Tue Sep 17 12:03:03 2019 +0200
@@ -49,6 +49,9 @@
     client_captcha_key = FieldProperty(IWfForm['client_captcha_key'])
     server_captcha_key = FieldProperty(IWfForm['server_captcha_key'])
     captcha_proxy = FieldProperty(IWfForm['captcha_proxy'])
+    rgpd_consent = FieldProperty(IWfForm['rgpd_consent'])
+    rgpd_warning = FieldProperty(IWfForm['rgpd_warning'])
+    rgpd_user_rights = FieldProperty(IWfForm['rgpd_user_rights'])
 
     @property
     def handler(self):
--- a/src/pyams_content/shared/form/field.py	Mon Sep 16 16:57:01 2019 +0200
+++ b/src/pyams_content/shared/form/field.py	Tue Sep 17 12:03:03 2019 +0200
@@ -9,11 +9,6 @@
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
 # FOR A PARTICULAR PURPOSE.
 #
-from pyams_content.component.paragraph import BaseParagraph, IParagraphFactory, BaseParagraphFactory
-from pyams_content.features.renderer import RenderersVocabulary
-
-
-__docformat__ = 'restructuredtext'
 
 from collections import OrderedDict
 
@@ -27,10 +22,12 @@
 from zope.schema.fieldproperty import FieldProperty
 from zope.traversing.interfaces import ITraversable
 
-from pyams_content.shared.form.interfaces import FORM_FIELD_CONTAINER_KEY, IFormField, \
-    IFormFieldContainer, \
-    IFormFieldContainerTarget, IFormFieldFactory, IWfForm, IFormFieldsParagraph, \
-    FORM_FIELDS_PARAGRAPH_NAME, FORM_FIELDS_PARAGRAPH_TYPE, FORM_FIELDS_PARAGRAPH_RENDERERS
+from pyams_content.component.paragraph import BaseParagraph, BaseParagraphFactory, IParagraphFactory
+from pyams_content.features.renderer import RenderersVocabulary
+from pyams_content.shared.form.interfaces import FORM_FIELDS_PARAGRAPH_NAME, \
+    FORM_FIELDS_PARAGRAPH_RENDERERS, FORM_FIELDS_PARAGRAPH_TYPE, FORM_FIELD_CONTAINER_KEY, \
+    IFormField, IFormFieldContainer, IFormFieldContainerTarget, IFormFieldFactory, \
+    IFormFieldsParagraph, IWfForm
 from pyams_form.interfaces.form import IFormContextPermissionChecker
 from pyams_i18n.interfaces import II18n
 from pyams_utils.adapter import ContextAdapter, adapter_config, get_annotation_adapter
@@ -41,6 +38,9 @@
 from pyams_utils.traversing import get_parent
 from pyams_utils.vocabulary import vocabulary_config
 
+
+__docformat__ = 'restructuredtext'
+
 from pyams_content import _
 
 
@@ -80,7 +80,7 @@
 
     def find_fields(self, factory):
         for field in self.values():
-            if field.field_type == factory:
+            if field.visible and (field.field_type == factory):
                 yield field
 
 
--- a/src/pyams_content/shared/form/handler.py	Mon Sep 16 16:57:01 2019 +0200
+++ b/src/pyams_content/shared/form/handler.py	Tue Sep 17 12:03:03 2019 +0200
@@ -76,6 +76,6 @@
     target_interface = IMailtoHandlerTarget
     handler_info = IMailtoHandlerInfo
 
-    def handle(self, form, data):
+    def handle(self, form, data, user_data):
         # TODO: handle form data
         pass
--- a/src/pyams_content/shared/form/interfaces.py	Mon Sep 16 16:57:01 2019 +0200
+++ b/src/pyams_content/shared/form/interfaces.py	Tue Sep 17 12:03:03 2019 +0200
@@ -19,7 +19,7 @@
 from pyams_content.component.paragraph import IBaseParagraph
 from pyams_content.shared.common.interfaces import ISharedContent, ISharedToolPortalContext, \
     IWfSharedContentPortalContext
-from pyams_i18n.schema import I18nTextField, I18nTextLineField, I18nHTMLField
+from pyams_i18n.schema import I18nHTMLField, I18nTextField, I18nTextLineField
 from pyams_utils.schema import MailAddressField, TextLineListField
 
 
@@ -99,6 +99,13 @@
         """Get schema field matching given form field"""
 
 
+class IFormFieldDataConverter(Interface):
+    """Interface of a converter adapter which can be used to convert form data"""
+
+    def convert(self, value):
+        """Convert given input value"""
+
+
 FORM_FIELDS_PARAGRAPH_TYPE = 'form-fields'
 FORM_FIELDS_PARAGRAPH_NAME = _("Form fields")
 FORM_FIELDS_PARAGRAPH_RENDERERS = 'PyAMS.paragraph.formfields.renderers'
@@ -145,6 +152,8 @@
                                 required=False)
 
     user_title = I18nTextLineField(title=_("Form title"),
+                                   description=_("If set, this title will be displayed above input "
+                                                 "fields"),
                                    required=False)
 
     auth_only = Bool(title=_("Authenticated only?"),
@@ -187,6 +196,27 @@
                                            "support..."),
                              required=False)
 
+    rgpd_consent = Bool(title=_("Required RGPD consent?"),
+                        description=_("If 'yes', an RGPD compliance warning will be displayed "
+                                      "above form's submit button; form can't be submitted as long "
+                                      "as the associated checkbox will not be checked explicitly "
+                                      "by the user"),
+                        required=True,
+                        default=True)
+
+    rgpd_warning = I18nTextField(title=_("RGPD consent text"),
+                                 description=_("User consent must be explicit, and user must be "
+                                               "warned about usage which will be made of submitted "
+                                               "data; text samples are given below"),
+                                 required=False)
+
+    rgpd_user_rights = I18nHTMLField(title=_("RGPD user rights"),
+                                     description=_("The internet user must be able to easily "
+                                                   "revoke his consent later on, so it is "
+                                                   "important to inform him how to proceed; below "
+                                                   "are examples of possible formulations"),
+                                     required=False)
+
     def query_handler(self, handler=None):
         """Get form handler utility"""
 
@@ -210,8 +240,13 @@
     target_interface = Attribute("Handler target marker interface")
     handler_info = Attribute("Handler info interface")
 
-    def handle(self, form, data):
-        """Handle entered data"""
+    def handle(self, form, data, user_data):
+        """Handle entered data
+
+        :param form: input form
+        :param data: raw form data
+        :param user_data: user friendly form input data
+        """
 
 
 class IFormHandlerInfo(Interface):
--- a/src/pyams_content/shared/form/zmi/properties.py	Mon Sep 16 16:57:01 2019 +0200
+++ b/src/pyams_content/shared/form/zmi/properties.py	Tue Sep 17 12:03:03 2019 +0200
@@ -9,10 +9,11 @@
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
 # FOR A PARTICULAR PURPOSE.
 #
-
+from pyramid.events import subscriber
 from z3c.form import field
 from z3c.form.browser.checkbox import SingleCheckBoxFieldWidget
-from zope.interface import Interface
+from z3c.form.interfaces import IDataExtractedEvent, INPUT_MODE
+from zope.interface import Interface, Invalid
 
 from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
 from pyams_content.shared.common.zmi.properties import SharedContentPropertiesEditForm
@@ -58,11 +59,32 @@
     fields = field.Fields(IWfForm).select('form_header', 'user_title', 'auth_only',
                                           'submit_label', 'submit_message', 'handler',
                                           'use_captcha', 'client_captcha_key',
-                                          'server_captcha_key', 'captcha_proxy')
+                                          'server_captcha_key', 'captcha_proxy',
+                                          'rgpd_consent', 'rgpd_warning', 'rgpd_user_rights')
     fields['use_captcha'].widgetFactory = SingleCheckBoxFieldWidget
+    fields['rgpd_consent'].widgetFactory = SingleCheckBoxFieldWidget
 
     weight = 1
 
+    def updateWidgets(self, prefix=None):
+        super(FormPropertiesInnerEditForm, self).updateWidgets(prefix)
+        if self.mode == INPUT_MODE:
+            translate = self.request.localizer.translate
+            if 'rgpd_warning' in self.widgets:
+                self.widgets['rgpd_warning'].after_widget_notice = \
+                    '<div class="alert-info padding-5">{0}</div>'.format(
+                        translate(_("Text samples:<br />"
+                                    "- By submitting this form, I agree that the information "
+                                    "entered may be used for the purpose of my request and the "
+                                    "business relationship that may result from it.")))
+            if 'rgpd_user_rights' in self.widgets:
+                self.widgets['rgpd_user_rights'].after_widget_notice = \
+                    '<div class="alert-info padding-5">{0}</div>'.format(
+                        translate(_("Text samples:<br />"
+                                    "- To know and enforce your rights, including the right to "
+                                    "withdraw your consent to the use of the data collected by "
+                                    "this form, please consult our privacy policy.")))
+
     def updateGroups(self):
         self.add_group(NamedWidgetsGroup(self, 'head', self.widgets,
                                          ('form_header', 'user_title', 'auth_only',
@@ -76,6 +98,14 @@
                                          switch=True,
                                          checkbox_switch=True,
                                          checkbox_field=IWfForm['use_captcha']))
+        self.add_group(NamedWidgetsGroup(self, 'rgpd', self.widgets,
+                                         ('rgpd_consent', 'rgpd_warning', 'rgpd_user_rights'),
+                                         fieldset_class='inner bordered',
+                                         legend=_("Add RGPD warning"),
+                                         css_class='inner',
+                                         switch=True,
+                                         checkbox_switch=True,
+                                         checkbox_field=IWfForm['rgpd_consent']))
         super(FormPropertiesInnerEditForm, self).updateGroups()
 
     def get_ajax_output(self, changes):
@@ -88,6 +118,23 @@
             return super(FormPropertiesInnerEditForm, self).get_ajax_output(changes)
 
 
+@subscriber(IDataExtractedEvent, form_selector=FormPropertiesInnerEditForm)
+def check_form_properties_data(event):
+    """Check form properties input data"""
+    data = event.data
+    if data.get('rgpd_consent'):
+        for attr in ('rgpd_warning', 'rgpd_user_rights'):
+            attr_ok = False
+            for lang, value in data.get(attr, {}).items():
+                if value:
+                    attr_ok = True
+                    break
+            if not attr_ok:
+                event.form.widgets.errors += (Invalid(_("You MUST set an RGPD consent text and "
+                                                        "RGPD user rights to enable RGPD!")),)
+                return
+
+
 @adapter_config(name='handler-settings',
                 context=(IWfForm, IPyAMSLayer, SharedContentPropertiesEditForm),
                 provides=IInnerSubForm)