Updated shared form rendering
authorThierry Florac <tflorac@ulthar.net>
Fri, 26 Jul 2019 13:10:04 +0200
changeset 451 0c41d132ccb7
parent 450 3dd8901d34cf
child 452 df3f13ea95cc
Updated shared form rendering
src/pyams_default_theme/shared/form/__init__.py
src/pyams_default_theme/shared/form/templates/form-submit.pt
src/pyams_default_theme/shared/form/templates/recaptcha.pt
--- a/src/pyams_default_theme/shared/form/__init__.py	Fri Jul 26 13:08:32 2019 +0200
+++ b/src/pyams_default_theme/shared/form/__init__.py	Fri Jul 26 13:10:04 2019 +0200
@@ -10,23 +10,42 @@
 # FOR A PARTICULAR PURPOSE.
 #
 
-__docformat__ = 'restructuredtext'
-
+import requests
+from pyramid.csrf import get_csrf_token
 from z3c.form import button, field
+from z3c.form.interfaces import HIDDEN_MODE
 from zope.interface import Interface
+from zope.schema import TextLine
 
 from pyams_content.features.renderer.interfaces import ISharedContentRenderer
-from pyams_content.shared.form import IFormFieldContainer, IFormFieldContainerTarget
+from pyams_content.shared.form import IFormFieldContainer, IFormFieldContainerTarget, IWfForm
+from pyams_default_theme.component.paragraph.interfaces import IParagraphContainerPortletRenderer
 from pyams_default_theme.features.renderer import BaseContentRenderer
-from pyams_form.form import InnerAddForm
+from pyams_default_theme.layer import IPyAMSDefaultLayer
+from pyams_form.form import AddForm
 from pyams_form.help import FormHelp
-from pyams_form.interfaces.form import IFormHelp
+from pyams_form.interfaces.form import IFormHelp, IFormSuffixViewletsManager
 from pyams_i18n.interfaces import II18n
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_portal.views import PortalContextIndexPage
 from pyams_skin.layer import IPyAMSLayer
+from pyams_template.template import template_config
 from pyams_utils.adapter import adapter_config
+from pyams_utils.interfaces import PUBLIC_PERMISSION, VIEW_PERMISSION
+from pyams_utils.url import relative_url
+from pyams_viewlet.viewlet import ViewContentProvider, Viewlet, viewlet_config
 
 
-class FormFieldContainerDisplayForm(InnerAddForm):
+__docformat__ = 'restructuredtext'
+
+from pyams_default_theme import _
+
+
+CSRF_FIELD_NAME = 'csrf_token'
+RECAPTCHA_FIELD_NAME = 'g-recaptcha-response'
+
+
+class FormFieldContainerDisplayForm(AddForm):
     """Form fields container display form"""
 
     @property
@@ -34,22 +53,80 @@
         return II18n(self.context).query_attribute('user_title', request=self.request)
 
     @property
+    def edit_permission(self):
+        if self.context.auth_only:
+            return VIEW_PERMISSION
+        else:
+            return PUBLIC_PERMISSION
+
+    def get_form_action(self):
+        return relative_url(self.context, self.request, view_name='submit.html')
+
+    @property
     def fields(self):
-        fields = field.Fields(*IFormFieldContainer(self.context).get_fields())
-        if self.context.use_captcha:
-            # TODO: add captcha
-            # fields += field.Fields(Captcha(title='', description='', required=True))
-            pass
-        return fields
 
-    buttons = button.Buttons(Interface)
+        def get_fields():
+            token = TextLine(title=_("CSRF token"), required=True)
+            token.__name__ = CSRF_FIELD_NAME
+            yield token
+            if self.context.use_captcha:
+                captcha = TextLine(title=_("Captcha"), required=True)
+                captcha.__name__ = RECAPTCHA_FIELD_NAME
+                yield captcha
+            yield from IFormFieldContainer(self.context).get_fields()
+
+        return field.Fields(*tuple(get_fields()))
+
+    def updateActions(self):
+        super(FormFieldContainerDisplayForm, self).updateActions()
+        if 'submit' in self.actions:
+            self.actions['submit'].title = II18n(self.context).query_attribute('submit_label',
+                                                                               request=self.request)
 
     def updateWidgets(self, prefix=None):
         super(FormFieldContainerDisplayForm, self).updateWidgets(prefix)
         for widget in self.widgets.values():
-            field = IFormFieldContainer(self.context).get(widget.field.__name__)
-            if field is not None:
-                widget.placeholder = field.placeholder
+            if widget.field.__name__ == CSRF_FIELD_NAME:
+                widget.name = CSRF_FIELD_NAME
+                widget.mode = HIDDEN_MODE
+                widget.value = get_csrf_token(self.request)
+            elif widget.field.__name__ == RECAPTCHA_FIELD_NAME:
+                widget.name = RECAPTCHA_FIELD_NAME
+                widget.mode = HIDDEN_MODE
+            else:
+                field = IFormFieldContainer(self.context).get(widget.field.__name__)
+                if field is not None:
+                    widget.placeholder = field.placeholder
+
+    @button.buttonAndHandler('title', name='submit')
+    def update_content(self, action):
+        form = IWfForm(self.context)
+        handler = form.query_handler()
+        if handler is not None:
+            data, errors = self.extractData()
+            if errors:
+                self.status = self.formErrorsMessage
+            else:
+                if CSRF_FIELD_NAME in data:
+                    del data[CSRF_FIELD_NAME]
+                if self.context.use_captcha:
+                    if RECAPTCHA_FIELD_NAME not in data:
+                        self.status = self.request.localizer.translate(_("Missing recaptcha token!"))
+                        return
+                    proxies = {'https': self.context.captcha_proxy} if self.context.captcha_proxy else {}
+                    recaptcha_verify_api = self.request.registry.settings.get('pyams.recaptcha.verify')
+                    if not recaptcha_verify_api:
+                        recaptcha_verify_api = 'https://www.google.com/recaptcha/api/siteverify'
+                    verify = requests.post(recaptcha_verify_api, {
+                        'secret': self.context.server_captcha_key,
+                        'response': data[RECAPTCHA_FIELD_NAME]
+                    }, proxies=proxies).json()
+                    if not verify['success']:
+                        self.status = self.request.localizer.translate(
+                            _("Can't verify recaptcha token! Are you a robot?"))
+                        return
+                    del data[RECAPTCHA_FIELD_NAME]
+                handler.handle(form, data)
 
 
 @adapter_config(context=(IFormFieldContainerTarget, IPyAMSLayer, FormFieldContainerDisplayForm),
@@ -64,26 +141,69 @@
 
     @property
     def message(self):
-        return II18n(self.context).query_attribute('header', request=self.request)
+        return II18n(self.context).query_attribute('form_header', request=self.request) or ''
 
     message_format = 'text'
 
 
+@viewlet_config(name='recaptcha', context=IFormFieldContainerTarget, layer=IPyAMSDefaultLayer,
+                view=FormFieldContainerDisplayForm, manager=IFormSuffixViewletsManager)
+@template_config(template='templates/recaptcha.pt', layer=IPyAMSDefaultLayer)
+class FormCaptchaViewlet(Viewlet):
+    """Form captcha viewlet"""
+
+    def __new__(cls, context, request, view, manager):
+        if not context.use_captcha:
+            return None
+        return Viewlet.__new__(cls)
+
+
 @adapter_config(name='form-render', context=(IFormFieldContainerTarget, IPyAMSLayer),
                 provides=ISharedContentRenderer)
 class FormFieldContainerRenderer(BaseContentRenderer):
     """Form field container renderer"""
 
     weight = 20
-    display_form = None
+    input_form = None
 
     def __init__(self, context, request):
         super(FormFieldContainerRenderer, self).__init__(context, request)
-        self.display_form = FormFieldContainerDisplayForm(context, self.request)
+        self.input_form = FormFieldContainerDisplayForm(context, self.request)
 
     def update(self):
         super(FormFieldContainerRenderer, self).update()
-        self.display_form.update()
+        self.input_form.update()
 
     def render(self):
-        return self.display_form.render()
+        return self.input_form.render()
+
+
+@pagelet_config(name='submit.html', context=IWfForm, layer=IPyAMSDefaultLayer)
+class FormSubmitPage(PortalContextIndexPage):
+    """Form submit page"""
+
+    input_form = None
+
+    def __init__(self, context, request):
+        super(FormSubmitPage, self).__init__(context, request)
+        self.input_form = FormFieldContainerDisplayForm(context, self.request)
+
+    def update(self):
+        super(FormSubmitPage, self).update()
+        self.input_form.update()
+
+
+@adapter_config(name='submit.html', context=(IWfForm, IPyAMSDefaultLayer, Interface),
+                provides=IParagraphContainerPortletRenderer)
+@template_config(template='templates/form-submit.pt', layer=IPyAMSDefaultLayer)
+class FormSubmitPortletRenderer(ViewContentProvider):
+    """Form submit message portlet renderer"""
+
+    use_portlets_cache = True
+
+    def render(self):
+        form = self.view.input_form
+        if form.widgets.errors:
+            return form.render()
+        else:
+            return super(FormSubmitPortletRenderer, self).render()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_default_theme/shared/form/templates/form-submit.pt	Fri Jul 26 13:10:04 2019 +0200
@@ -0,0 +1,1 @@
+<div>${structure:i18n:context.submit_message}</div>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_default_theme/shared/form/templates/recaptcha.pt	Fri Jul 26 13:10:04 2019 +0200
@@ -0,0 +1,8 @@
+<script src="https://www.google.com/recaptcha/api.js?render=${context.client_captcha_key}"></script>
+<script>
+	grecaptcha.ready(function() {
+		grecaptcha.execute('${context.client_captcha_key}', {action: 'form_submit'}).then(function(token) {
+			$('input[name="g-recaptcha-response"]').val(token);
+		})
+	});
+</script>
\ No newline at end of file