Updated shared forms
authorThierry Florac <thierry.florac@onf.fr>
Tue, 19 Sep 2017 11:11:30 +0200
changeset 170 26aefef3d0aa
parent 169 483b0f16e9a6
child 171 13db231ca918
Updated shared forms
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/__init__.py
src/pyams_content/shared/form/zmi/__init__.py
src/pyams_content/shared/form/zmi/field.py
src/pyams_content/shared/form/zmi/properties.py
src/pyams_content/shared/form/zmi/summary.py
--- a/src/pyams_content/shared/form/__init__.py	Tue Sep 19 11:11:12 2017 +0200
+++ b/src/pyams_content/shared/form/__init__.py	Tue Sep 19 11:11:30 2017 +0200
@@ -16,22 +16,56 @@
 # import standard library
 
 # import interfaces
-from pyams_content.component.links.interfaces import ILinkContainerTarget
-from pyams_content.shared.form.interfaces import IWfForm, IForm, FORM_CONTENT_TYPE, FORM_CONTENT_NAME
+from pyams_content.shared.form.interfaces import IWfForm, IForm, FORM_CONTENT_TYPE, FORM_CONTENT_NAME, \
+    IFormFieldContainerTarget, IFormHandler
 
 # import packages
 from pyams_content.shared.common import WfSharedContent, register_content_type, SharedContent
-from zope.interface import implementer
+from zope.component.globalregistry import getGlobalSiteManager
+from zope.interface import implementer, alsoProvides, noLongerProvides
 from zope.schema.fieldproperty import FieldProperty
 
 
-@implementer(IWfForm, ILinkContainerTarget)
+@implementer(IWfForm, IFormFieldContainerTarget)
 class WfForm(WfSharedContent):
     """Base form"""
 
     content_type = FORM_CONTENT_TYPE
     content_name = FORM_CONTENT_NAME
 
+    user_title = FieldProperty(IWfForm['user_title'])
+    header = FieldProperty(IWfForm['header'])
+    _handler = FieldProperty(IWfForm['handler'])
+    auth_only = FieldProperty(IWfForm['auth_only'])
+    use_captcha = FieldProperty(IWfForm['use_captcha'])
+    submit_label = FieldProperty(IWfForm['submit_label'])
+
+    @property
+    def handler(self):
+        return self._handler
+
+    @handler.setter
+    def handler(self, value):
+        old_handler = self._handler
+        if value == old_handler:
+            return
+        if old_handler is not None:
+            handler = self.query_handler(old_handler)
+            if (handler is not None) and handler.target_interface:
+                noLongerProvides(self, handler.target_interface)
+        if value is not None:
+            handler = self.query_handler(value)
+            if (handler is not None) and handler.target_interface:
+                alsoProvides(self, handler.target_interface)
+        self._handler = value
+
+    def query_handler(self, handler=None):
+        if handler is None:
+            handler = self._handler
+        if handler:
+            registry = getGlobalSiteManager()
+            return registry.queryUtility(IFormHandler, name=handler)
+
 register_content_type(WfForm)
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/form/field.py	Tue Sep 19 11:11:30 2017 +0200
@@ -0,0 +1,265 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+from collections import OrderedDict
+
+# import interfaces
+from pyams_content.shared.form.interfaces import IFormFieldFactory, IFormField, IFormFieldContainer, \
+    IFormFieldContainerTarget, FORM_FIELD_CONTAINER_KEY
+from pyams_i18n.interfaces import II18n
+from zope.annotation.interfaces import IAnnotations
+from zope.location.interfaces import ISublocations
+from zope.traversing.interfaces import ITraversable
+
+# import packages
+from persistent import Persistent
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.registry import utility_config
+from pyams_utils.request import check_request
+from pyams_utils.schema import MailAddressField
+from pyams_utils.vocabulary import vocabulary_config
+from pyramid.threadlocal import get_current_registry
+from zope.component.globalregistry import getGlobalSiteManager
+from zope.container.contained import Contained
+from zope.container.ordered import OrderedContainer
+from zope.interface import implementer
+from zope.lifecycleevent import ObjectCreatedEvent
+from zope.location import locate
+from zope.schema import TextLine, Text, Bool, Int, Decimal, URI, Date, Choice, List
+from zope.schema.fieldproperty import FieldProperty
+from zope.componentvocabulary.vocabulary import UtilityVocabulary, UtilityTerm
+
+from pyams_content import _
+
+
+@implementer(IFormField)
+class FormField(Persistent, Contained):
+    """Form field definition persistent class"""
+
+    name = FieldProperty(IFormField['name'])
+    field_type = FieldProperty(IFormField['field_type'])
+    label = FieldProperty(IFormField['label'])
+    description = FieldProperty(IFormField['description'])
+    placeholder = FieldProperty(IFormField['placeholder'])
+    values = FieldProperty(IFormField['values'])
+    default = FieldProperty(IFormField['default'])
+    required = FieldProperty(IFormField['required'])
+    visible = FieldProperty(IFormField['visible'])
+
+
+@implementer(IFormFieldContainer)
+class FormFieldContainer(OrderedContainer):
+    """Form fields container persistent class"""
+
+    def get_fields(self):
+        registry = getGlobalSiteManager()
+        fields = []
+        for field in self.values():
+            if field.visible:
+                factory = registry.queryUtility(IFormFieldFactory, name=field.field_type)
+                if factory is not None:
+                    fields.append(factory.get_schema_field(field))
+        return fields
+
+
+@adapter_config(context=IFormFieldContainerTarget, provides=IFormFieldContainer)
+def FormFieldContainerFactory(context):
+    """Form fields container factory"""
+    annotations = IAnnotations(context)
+    container = annotations.get(FORM_FIELD_CONTAINER_KEY)
+    if container is None:
+        container = annotations[FORM_FIELD_CONTAINER_KEY] = FormFieldContainer()
+        get_current_registry().notify(ObjectCreatedEvent(container))
+        locate(container, context, '++fields++')
+    return container
+
+
+@adapter_config(name='fields', context=IFormFieldContainerTarget, provides=ITraversable)
+class FormFieldContainerNamespace(ContextAdapter):
+    """Form fields container ++fields++ namespace"""
+
+    def traverse(self, name, firtherpath=None):
+        return IFormFieldContainer(self.context)
+
+
+@adapter_config(name='fields', context=IFormFieldContainerTarget, provides=ISublocations)
+class FormFieldsContainerSublocations(ContextAdapter):
+    """Form fields container sub-locations adapter"""
+
+    def sublocations(self):
+        return IFormFieldContainer(self.context).values()
+
+
+#
+# Form fields factories
+#
+
+@vocabulary_config(name='PyAMS form field types')
+class FormFieldTypesVocabulary(UtilityVocabulary):
+    """Form field types vocabulary"""
+
+    interface = IFormFieldFactory
+
+    def __init__(self, context, **kw):
+        request = check_request()
+        registry = request.registry
+        translate = request.localizer.translate
+        utils = [(name, translate(util.label))
+                 for (name, util) in sorted(registry.getUtilitiesFor(self.interface),
+                                            key=lambda x: x[1].weight)]
+        self._terms = OrderedDict((title, UtilityTerm(name, title)) for name, title in utils)
+
+    def __iter__(self):
+        return iter(self._terms.values())
+
+
+class BaseFormFieldFactory(object):
+    """Base form field factory"""
+
+    field_factory = None
+
+    def get_schema_field(self, field):
+        i18n = II18n(field)
+        result = self.field_factory(title=i18n.query_attribute('label'),
+                                    description=i18n.query_attribute('description'),
+                                    required=field.required,
+                                    default=i18n.query_attribute('default'))
+        result.__name__ = field.name
+        return result
+
+
+@utility_config(name='textline', provides=IFormFieldFactory)
+class TextLineFieldFactory(BaseFormFieldFactory):
+    """Textline field factory"""
+
+    label = _("Text")
+    weight = 1
+
+    field_factory = TextLine
+
+
+@utility_config(name='text', provides=IFormFieldFactory)
+class TextFieldFactory(BaseFormFieldFactory):
+    """Text field factory"""
+
+    label = _("Multi-lines text")
+    weight = 2
+
+    field_factory = Text
+
+
+@utility_config(name='bool', provides=IFormFieldFactory)
+class BooleanFieldFactory(BaseFormFieldFactory):
+    """Boolean field factory"""
+
+    label = _("Boolean")
+    weight = 3
+
+    field_factory = Bool
+
+
+@utility_config(name='integer', provides=IFormFieldFactory)
+class IntegerFieldFactory(BaseFormFieldFactory):
+    """Integer field factory"""
+
+    label = _("Integer")
+    weight = 4
+
+    field_factory = Int
+
+
+@utility_config(name='decimal', provides=IFormFieldFactory)
+class DecimalFieldFactory(BaseFormFieldFactory):
+    """Decimal field factory"""
+
+    label = _("Decimal")
+    weight = 5
+
+    field_factory = Decimal
+
+
+@utility_config(name='mail', provides=IFormFieldFactory)
+class MailFieldFactory(BaseFormFieldFactory):
+    """Mail field factory"""
+
+    label = _("E-mail address")
+    weight = 10
+
+    field_factory = MailAddressField
+
+
+@utility_config(name='uri', provides=IFormFieldFactory)
+class URIFieldFactory(BaseFormFieldFactory):
+    """URI field factory"""
+
+    label = _("URI")
+    weight = 11
+
+    field_factory = URI
+
+
+@utility_config(name='date', provides=IFormFieldFactory)
+class DateFieldFactory(BaseFormFieldFactory):
+    """Date field factory"""
+
+    label = _("Date")
+    weight = 15
+
+    field_factory = Date
+
+
+class ValuesFieldFactory(BaseFormFieldFactory):
+    """Values-based field factory"""
+
+
+@utility_config(name='choice', provides=IFormFieldFactory)
+class ChoiceFieldFactory(ValuesFieldFactory):
+    """Choice field factory"""
+
+    label = _("Choice")
+    weight = 20
+
+    field_factory = Choice
+
+    def get_schema_field(self, field):
+        i18n = II18n(field)
+        result = self.field_factory(title=i18n.query_attribute('label'),
+                                    description=i18n.query_attribute('description'),
+                                    required=field.required,
+                                    default=i18n.query_attribute('default'),
+                                    values=field.values)
+        result.__name__ = field.name
+        return result
+
+
+@utility_config(name='list', provides=IFormFieldFactory)
+class ListFieldFactory(ValuesFieldFactory):
+    """List field factory"""
+
+    label = _("List")
+    weight = 51
+
+    field_factory = List
+
+    def get_schema_field(self, field):
+        i18n = II18n(field)
+        result = self.field_factory(title=i18n.query_attribute('label'),
+                                    description=i18n.query_attribute('description'),
+                                    required=field.required,
+                                    default=[i18n.query_attribute('default')],
+                                    value_type=Choice(values=field.values))
+        result.__name__ = field.name
+        return result
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/form/handler.py	Tue Sep 19 11:11:30 2017 +0200
@@ -0,0 +1,91 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.shared.form.interfaces import IFormHandler, IMailtoHandlerTarget, IMailtoHandlerInfo
+from zope.annotation.interfaces import IAnnotations
+
+# import packages
+from persistent import Persistent
+from pyams_utils.adapter import adapter_config
+from pyams_utils.registry import utility_config
+from pyams_utils.request import check_request
+from pyams_utils.vocabulary import vocabulary_config
+from zope.componentvocabulary.vocabulary import UtilityTerm, UtilityVocabulary
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+from pyams_content import _
+
+
+@vocabulary_config(name='PyAMS form handlers')
+class FormHandlersVocabulary(UtilityVocabulary):
+    """Form handlers vocabulary"""
+
+    interface = IFormHandler
+
+    def __init__(self, context, **kw):
+        request = check_request()
+        registry = request.registry
+        translate = request.localizer.translate
+        utils = [(None, translate(_("No selected handler...")))] + \
+                [(name, translate(util.label))
+                 for (name, util) in registry.getUtilitiesFor(self.interface)]
+        self._terms = dict((title, UtilityTerm(name, title)) for name, title in utils)
+
+    def __iter__(self):
+        return iter(self._terms.values())
+
+
+#
+# Mailto form handler
+#
+
+MAILTO_HANDLER_ANNOTATIONS_KEY = 'pyams_content.form.handler.mailto'
+
+
+@implementer(IMailtoHandlerInfo)
+class MailtoFormHandlerInfo(Persistent):
+    """Mailto form handler persistent info"""
+
+    source_address = FieldProperty(IMailtoHandlerInfo['source_address'])
+    source_name = FieldProperty(IMailtoHandlerInfo['source_name'])
+    target_address = FieldProperty(IMailtoHandlerInfo['target_address'])
+    target_name = FieldProperty(IMailtoHandlerInfo['target_name'])
+
+
+@adapter_config(context=IMailtoHandlerTarget, provides=IMailtoHandlerInfo)
+def mailto_form_handler_factory(context):
+    """Mailto form handler factory"""
+    annotations = IAnnotations(context)
+    info = annotations.get(MAILTO_HANDLER_ANNOTATIONS_KEY)
+    if info is None:
+        info = annotations[MAILTO_HANDLER_ANNOTATIONS_KEY] = MailtoFormHandlerInfo()
+    return info
+
+
+@utility_config(name='mailto', provides=IFormHandler)
+class MailtoFormHandler(object):
+    """Mailto form handler"""
+
+    label = _("Mailto form handler")
+    target_interface = IMailtoHandlerTarget
+    handler_info = IMailtoHandlerInfo
+
+    def handle(self, data):
+        # TODO: handle form data
+        pass
--- a/src/pyams_content/shared/form/interfaces/__init__.py	Tue Sep 19 11:11:12 2017 +0200
+++ b/src/pyams_content/shared/form/interfaces/__init__.py	Tue Sep 19 11:11:30 2017 +0200
@@ -17,8 +17,15 @@
 
 # import interfaces
 from pyams_content.shared.common.interfaces import ISharedTool, IWfSharedContent, ISharedContent
+from zope.annotation.interfaces import IAttributeAnnotatable
+from zope.container.interfaces import IContainer, IContained
 
 # import packages
+from pyams_i18n.schema import I18nTextLineField, I18nTextField
+from pyams_utils.schema import MailAddressField, TextLineListField
+from zope.container.constraints import containers, contains
+from zope.interface import Interface, Attribute
+from zope.schema import TextLine, Choice, Bool
 
 from pyams_content import _
 
@@ -26,14 +33,157 @@
 FORM_CONTENT_TYPE = 'form'
 FORM_CONTENT_NAME = _('Form')
 
+FORM_FIELD_CONTAINER_KEY = 'pyams_content.shared.form_fields'
+
 
 class IFormsManager(ISharedTool):
     """Formq manager interface"""
 
 
+class IFormField(IContained):
+    """Form field interface"""
+
+    containers('.IFormFieldContainer')
+
+    name = TextLine(title=_("Field name"),
+                    description=_("Field internal name; must be unique for a given form"),
+                    required=True)
+
+    field_type = Choice(title=_("Field type"),
+                        description=_("Selected field type"),
+                        vocabulary='PyAMS form field types',
+                        required=True)
+
+    label = I18nTextLineField(title=_("Label"),
+                              description=_("User field label"),
+                              required=True)
+
+    description = I18nTextField(title=_("Description"),
+                                description=_("Field description can be displayed as hint"),
+                                required=False)
+
+    placeholder = TextLine(title=_("Placeholder"),
+                           description=_("Some field types like textline can display a placeholder"),
+                           required=False)
+
+    values = TextLineListField(title=_("Optional values"),
+                               description=_("List of available values (for 'choice' and 'list' field types)"),
+                               required=False)
+
+    default = I18nTextLineField(title=_("Default value"),
+                                description=_("Give default value if field type can use it"),
+                                required=False)
+
+    required = Bool(title=_("Required?"),
+                    description=_("Select 'yes' to set field as mandatory"),
+                    required=True,
+                    default=False)
+
+    visible = Bool(title=_("Visible?"),
+                   description=_("Select 'no' to hide given field..."),
+                   required=True,
+                   default=True)
+
+
+class IFormFieldFactory(Interface):
+    """Form field factory interface"""
+
+    label = Attribute("Factory label")
+    weight = Attribute("Factory weight")
+
+    def get_schema_field(self, field):
+        """Get schema field matching given form field"""
+
+
+class IFormFieldContainer(IContainer):
+    """Form fields container interface"""
+
+    contains(IFormField)
+
+    def append(self, field):
+        """Append given field to container"""
+
+    def get_fields(self):
+        """Get schema fields matching current fields"""
+
+
+class IFormFieldContainerTarget(Interface):
+    """Form fields container target marker interface"""
+
+
 class IWfForm(IWfSharedContent):
     """Form interface"""
 
+    user_title = I18nTextLineField(title=_("Form title"),
+                                   required=True)
+
+    header = I18nTextField(title=_("Form header"),
+                           required=False)
+
+    handler = Choice(title=_("Form handler"),
+                     description=_("Select how form data is transmitted"),
+                     vocabulary='PyAMS form handlers')
+
+    auth_only = Bool(title=_("Authenticated only?"),
+                     description=_("If 'yes', only authenticated users will be able to see and submit form"),
+                     required=True,
+                     default=False)
+
+    use_captcha = Bool(title=_("Use captcha?"),
+                       description=_("If 'yes', a captcha will be added automatically to the form"),
+                       required=True,
+                       default=True)
+
+    submit_label = I18nTextLineField(title=_("Submit label"),
+                                     description=_("Label of form submit button"),
+                                     required=True)
+
+    def query_handler(self, handler=None):
+        """Get form handler utility"""
+
 
 class IForm(ISharedContent):
     """Workflow managed form interface"""
+
+
+#
+# Form handler
+#
+
+class IFormHandler(Interface):
+    """Form handler interface"""
+
+    label = Attribute("Handler label")
+    target_interface = Attribute("Handler target marker interface")
+    handler_info = Attribute("Handler info interface")
+
+    def handle(self, data):
+        """Handle entered data"""
+
+
+class IFormHandlerInfo(Interface):
+    """Base handler info interface"""
+
+
+class IMailtoHandlerInfo(IFormHandlerInfo):
+    """Mailto form handler info interface"""
+
+    source_address = MailAddressField(title=_("Source address"),
+                                      description=_("Mail address from which form data is sent"),
+                                      required=True)
+
+    source_name = TextLine(title=_("Source name"),
+                           description=_("Name of mail data sender"),
+                           required=False)
+
+    target_address = MailAddressField(title=_("Recipient address"),
+                                      description=_("Mail address to which form data is sent"),
+                                      required=True)
+
+    target_name = TextLine(title=_("Recipient name"),
+                           description=_("Name of data recipient"),
+                           required=False)
+
+
+class IMailtoHandlerTarget(IAttributeAnnotatable):
+    """Mailto handler target marker interface"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/form/zmi/__init__.py	Tue Sep 19 11:11:30 2017 +0200
@@ -0,0 +1,80 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.interfaces import CREATE_CONTENT_PERMISSION
+from pyams_content.shared.form.interfaces import IWfForm, IFormsManager
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces import IContentTitle
+from pyams_skin.interfaces.viewlet import IMenuHeader, IWidgetTitleViewletManager
+from pyams_skin.layer import IPyAMSLayer
+from pyams_zmi.interfaces.menu import IContentManagementMenu
+from pyams_zmi.layer import IAdminLayer
+
+# import packages
+from pyams_content.shared.common.zmi import SharedContentAddForm, SharedContentAJAXAddForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.toolbar import ToolbarAction
+from pyams_utils.adapter import adapter_config, ContextRequestAdapter, ContextRequestViewAdapter
+from pyams_viewlet.viewlet import viewlet_config
+from pyramid.view import view_config
+from zope.interface import Interface
+
+from pyams_content import _
+
+
+@adapter_config(context=(IWfForm, IContentManagementMenu), provides=IMenuHeader)
+class FormContentMenuHeader(ContextRequestAdapter):
+    """Form content menu header adapter"""
+
+    header = _("This form")
+
+
+@adapter_config(context=(IWfForm, IPyAMSLayer, Interface), provides=IContentTitle)
+class FormTitleAdapter(ContextRequestViewAdapter):
+    """Form title adapter"""
+
+    @property
+    def title(self):
+        translate = self.request.localizer.translate
+        return translate(_("Form « {title} »")).format(
+            title=II18n(self.context).query_attribute('title', request=self.request))
+
+
+@viewlet_config(name='add-shared-content.action', context=IFormsManager, layer=IAdminLayer, view=Interface,
+                manager=IWidgetTitleViewletManager, permission=CREATE_CONTENT_PERMISSION, weight=1)
+class FormAddAction(ToolbarAction):
+    """Form adding action"""
+
+    label = _("Add form")
+    label_css_class = 'fa fa-fw fa-plus'
+    url = 'add-shared-content.html'
+    modal_target = True
+
+
+@pagelet_config(name='add-shared-content.html', context=IFormsManager, layer=IPyAMSLayer,
+                permission=CREATE_CONTENT_PERMISSION)
+class FormAddForm(SharedContentAddForm):
+    """Form add form"""
+
+    legend = _("Add form")
+
+
+@view_config(name='add-shared-content.json', context=IFormsManager, request_type=IPyAMSLayer,
+             permission=CREATE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class FormAJAXAddForm(SharedContentAJAXAddForm, FormAddForm):
+    """Form add form, JSON renderer"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/form/zmi/field.py	Tue Sep 19 11:11:30 2017 +0200
@@ -0,0 +1,336 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import json
+
+# import interfaces
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_content.shared.common.interfaces import IWfSharedContent
+from pyams_content.shared.form.interfaces import IFormFieldContainerTarget, IFormFieldContainer, IFormField, \
+    IFormFieldFactory
+from pyams_form.interfaces.form import IFormSecurityContext
+from pyams_i18n.interfaces import II18n
+from pyams_skin.interfaces.viewlet import IWidgetTitleViewletManager
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_zmi.interfaces.menu import IPropertiesMenu
+from pyams_zmi.layer import IAdminLayer
+from z3c.form.interfaces import DISPLAY_MODE, IDataExtractedEvent
+from z3c.table.interfaces import IColumn, IValues
+
+# import packages
+from pyams_content.shared.common.zmi import WfModifiedContentColumnMixin
+from pyams_content.shared.form.field import FormField
+from pyams_form.form import AJAXAddForm, AJAXEditForm
+from pyams_form.security import ProtectedFormObjectMixin
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.table import BaseTable, SorterColumn, JsActionColumn, I18nColumn, TrashColumn, I18nValueColumn
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_skin.viewlet.toolbar import ToolbarAction
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from pyams_utils.traversing import get_parent
+from pyams_utils.url import absolute_url
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm
+from pyams_zmi.view import ContainerAdminView
+from pyramid.decorator import reify
+from pyramid.events import subscriber
+from pyramid.exceptions import NotFound
+from pyramid.view import view_config
+from z3c.form import field
+from z3c.table.column import GetAttrColumn
+from zope.interface import Invalid
+
+from pyams_content import _
+
+
+@viewlet_config(name='form-fields.menu', context=IFormFieldContainerTarget, layer=IAdminLayer,
+                manager=IPropertiesMenu, permission=VIEW_SYSTEM_PERMISSION, weight=110)
+class FormFieldsMenu(MenuItem):
+    """Form fields menu"""
+
+    label = _("Form fields...")
+    icon_class = 'fa-pencil-square-o'
+    url = '#form-fields.html'
+
+
+#
+# Form fields container view
+#
+
+class FormFieldsContainerTable(ProtectedFormObjectMixin, BaseTable):
+    """Form fields table"""
+
+    id = 'form_fields_list'
+    hide_header = True
+    sortOn = None
+
+    @property
+    def cssClasses(self):
+        classes = ['table', 'table-bordered', 'table-striped', 'table-hover', 'table-tight']
+        permission = self.permission
+        if (not permission) or self.request.has_permission(permission, self.context):
+            classes.append('table-dnd')
+        return {'table': ' '.join(classes)}
+
+    @property
+    def data_attributes(self):
+        attributes = super(FormFieldsContainerTable, self).data_attributes
+        attributes['table'] = {'id': self.id,
+                               'data-ams-plugins': 'pyams_content',
+                               'data-ams-plugin-pyams_content-src':
+                                   '/--static--/pyams_content/js/pyams_content{MyAMS.devext}.js',
+                               'data-ams-location': absolute_url(IFormFieldContainer(self.context), self.request),
+                               'data-ams-tablednd-drag-handle': 'td.sorter',
+                               'data-ams-tablednd-drop-target': 'set-form-fields-order.json'}
+        return attributes
+
+    @reify
+    def values(self):
+        return list(super(FormFieldsContainerTable, self).values)
+
+    def render(self):
+        if not self.values:
+            translate = self.request.localizer.translate
+            return translate(_("No currently defined form field."))
+        return super(FormFieldsContainerTable, self).render()
+
+
+@adapter_config(name='sorter', context=(IFormFieldContainerTarget, IPyAMSLayer, FormFieldsContainerTable),
+                provides=IColumn)
+class FormFieldsContainerSorterColumn(ProtectedFormObjectMixin, SorterColumn):
+    """Form fields container sorter column"""
+
+
+@adapter_config(name='show-hide', context=(IFormFieldContainerTarget, IPyAMSLayer, FormFieldsContainerTable),
+                provides=IColumn)
+class FormFieldsContainerShowHideColumn(ProtectedFormObjectMixin, JsActionColumn):
+    """Form fields container visibility switcher column"""
+
+    cssClasses = {'th': 'action',
+                  'td': 'action switcher'}
+
+    icon_class = 'fa fa-fw fa-eye'
+    icon_hint = _("Switch field visibility")
+
+    url = 'PyAMS_content.fields.switchVisibility'
+
+    weight = 5
+
+    def get_icon(self, item):
+        if item.visible:
+            icon_class = 'fa fa-fw fa-eye'
+        else:
+            icon_class = 'fa fa-fw fa-eye-slash text-danger'
+        return '<i class="{icon_class}"></i>'.format(icon_class=icon_class)
+
+    def renderCell(self, item):
+        if self.permission and not self.request.has_permission(self.permission, context=item):
+            return self.get_icon(item)
+        else:
+            return super(FormFieldsContainerShowHideColumn, self).renderCell(item)
+
+
+@adapter_config(context=FormFieldsContainerShowHideColumn, provides=IFormSecurityContext)
+def ShowHideColumnSecurityContextFactory(column):
+    """Show/hide column security context factory"""
+    return column.table.context
+
+
+@adapter_config(name='name', context=(IFormFieldContainerTarget, IPyAMSLayer, FormFieldsContainerTable),
+                provides=IColumn)
+class FormFieldsContainerNameColumn(I18nColumn, WfModifiedContentColumnMixin, GetAttrColumn):
+    """Form fields container name column"""
+
+    _header = _("Name")
+
+    attrName = 'name'
+    weight = 50
+
+
+@adapter_config(name='label', context=(IFormFieldContainerTarget, IPyAMSLayer, FormFieldsContainerTable),
+                provides=IColumn)
+class FormFieldsContainerLabelColumn(I18nColumn, WfModifiedContentColumnMixin, I18nValueColumn):
+    """Form fields container label column"""
+
+    _header = _("Label")
+
+    attrName = 'label'
+    weight = 55
+
+
+@adapter_config(name='type', context=(IFormFieldContainerTarget, IPyAMSLayer, FormFieldsContainerTable),
+                provides=IColumn)
+class FormFieldsContainerTypeColumn(I18nColumn, WfModifiedContentColumnMixin, GetAttrColumn):
+    """Form fields container label column"""
+
+    _header = _("Field type")
+
+    weight = 60
+
+    def getValue(self, obj):
+        adapter = self.request.registry.queryUtility(IFormFieldFactory, name=obj.field_type)
+        if adapter is not None:
+            label = adapter.label
+        else:
+            label = _("-- unknown field type --")
+        return self.request.localizer.translate(label)
+
+
+@adapter_config(name='trash', context=(IFormFieldContainerTarget, IPyAMSLayer, FormFieldsContainerTable),
+                provides=IColumn)
+class FormFieldsContainerTrashColumn(ProtectedFormObjectMixin, TrashColumn):
+    """Form fields container trash column"""
+
+
+@adapter_config(context=(IFormFieldContainerTarget, IPyAMSLayer, FormFieldsContainerTable), provides=IValues)
+class FormFieldsContainerValues(ContextRequestViewAdapter):
+    """Form fields container values"""
+
+    @property
+    def values(self):
+        return IFormFieldContainer(self.context).values()
+
+
+@pagelet_config(name='form-fields.html', context=IFormFieldContainerTarget, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+class FormFieldsContainerView(ContainerAdminView):
+    """Form fields container view"""
+
+    title = _("Form fields list")
+    table_class = FormFieldsContainerTable
+
+
+@view_config(name='set-form-fields-order.json', context=IFormFieldContainer, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+def set_form_fields_order(request):
+    """Update form fields order"""
+    order = list(map(str, json.loads(request.params.get('names'))))
+    request.context.updateOrder(order)
+    return {'status': 'success'}
+
+
+@view_config(name='set-form-field-visibility.json', context=IFormFieldContainer, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+def set_form_field_visibility(request):
+    """Set form field visibility"""
+    container = IFormFieldContainer(request.context)
+    paragraph = container.get(str(request.params.get('object_name')))
+    if paragraph is None:
+        raise NotFound()
+    field = IFormField(paragraph)
+    field.visible = not field.visible
+    return {'visible': field.visible}
+
+
+#
+# Form field views
+#
+
+@viewlet_config(name='add-form-field.action', context=IFormFieldContainerTarget, layer=IAdminLayer,
+                view=FormFieldsContainerView, manager=IWidgetTitleViewletManager,
+                permission=MANAGE_CONTENT_PERMISSION, weight=1)
+class FormFieldAddAction(ProtectedFormObjectMixin, ToolbarAction):
+    """Form field add action"""
+
+    label = _("Add form field")
+    label_css_class = 'fa fa-fw fa-plus'
+    url = 'add-form-field.html'
+    modal_target = True
+
+
+@pagelet_config(name='add-form-field.html', context=IFormFieldContainerTarget, layer=IPyAMSLayer,
+                permission=MANAGE_CONTENT_PERMISSION)
+class FormFieldAddForm(AdminDialogAddForm):
+    """Form field add form"""
+
+    legend = _("Add form field")
+    icon_css_class = 'fa fa-fw fa-pencil-square-o'
+
+    fields = field.Fields(IFormField).omit('__parent__', '__name__', 'visible')
+    ajax_handler = 'add-form-field.json'
+    edit_permission = MANAGE_CONTENT_PERMISSION
+
+    def updateWidgets(self, prefix=None):
+        super(FormFieldAddForm, self).updateWidgets(prefix)
+        if 'description' in self.widgets:
+            self.widgets['description'].widget_css_class = 'textarea'
+
+    def create(self, data):
+        return FormField()
+
+    def add(self, object):
+        IFormFieldContainer(self.context)[object.name] = object
+
+
+@subscriber(IDataExtractedEvent, form_selector=FormFieldAddForm)
+def handle_new_form_field_data_extraction(event):
+    """Handle new form field form data extraction"""
+    container = IFormFieldContainer(event.form.context)
+    name = event.data.get('name')
+    if name in container:
+        event.form.widgets.errors += (Invalid(_("Specified name is already used!")),)
+
+
+@view_config(name='add-form-field.json', context=IFormFieldContainerTarget, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class FormFieldAJAXAddForm(AJAXAddForm, FormFieldAddForm):
+    """Form field add form, JSON renderer"""
+
+    def nextURL(self):
+        return '#form-fields.html'
+
+
+@pagelet_config(name='properties.html', context=IFormField, layer=IPyAMSLayer, permission=MANAGE_CONTENT_PERMISSION)
+class FormFieldPropertiesEditForm(AdminDialogEditForm):
+    """Form field properties edit form"""
+
+    @property
+    def title(self):
+        content = get_parent(self.context, IWfSharedContent)
+        return II18n(content).query_attribute('title', request=self.request)
+
+    legend = _("Edit form field properties")
+    icon_class = 'fa fa-fw fa-pencil-square-o'
+
+    fields = field.Fields(IFormField).omit('__parent__', '__name__', 'visible')
+    ajax_handler = 'properties.json'
+    edit_permission = MANAGE_CONTENT_PERMISSION
+
+    def updateWidgets(self, prefix=None):
+        super(FormFieldPropertiesEditForm, self).updateWidgets(prefix)
+        if 'name' in self.widgets:
+            self.widgets['name'].mode = DISPLAY_MODE
+        if 'description' in self.widgets:
+            self.widgets['description'].widget_css_class = 'textarea'
+
+
+@view_config(name='properties.json', context=IFormField, request_type=IPyAMSLayer,
+             permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class FormFieldPropertiesAJAXEditForm(AJAXEditForm, FormFieldPropertiesEditForm):
+    """Form field properties edit form, JSON renderer"""
+
+    def get_ajax_output(self, changes):
+        output = super(FormFieldPropertiesAJAXEditForm, self).get_ajax_output(changes)
+        if 'label' in changes.get(IFormField, ()):
+            output.setdefault('events', []).append({
+                'event': 'PyAMS_content.changed_item',
+                'options': {'object_type': 'form_field',
+                            'object_name': self.context.__name__,
+                            'title': II18n(self.context).query_attribute('label', request=self.request),
+                            'visible': self.context.visible}
+            })
+        return output
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/form/zmi/properties.py	Tue Sep 19 11:11:30 2017 +0200
@@ -0,0 +1,82 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.shared.form.interfaces import IWfForm
+from pyams_form.interfaces.form import IInnerSubForm
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_content.shared.common.zmi.properties import SharedContentPropertiesEditForm
+from pyams_utils.adapter import adapter_config
+from pyams_zmi.form import InnerAdminEditForm
+from z3c.form import field
+from zope.interface import Interface
+
+from pyams_content import _
+
+
+@adapter_config(name='form-settings',
+                context=(IWfForm, IPyAMSLayer, SharedContentPropertiesEditForm),
+                provides=IInnerSubForm)
+class FormPropertiesEditForm(InnerAdminEditForm):
+    """Form properties edit form extension"""
+
+    legend = _("Main form settings")
+    fieldset_class = 'bordered no-x-margin margin-y-10'
+
+    fields = field.Fields(IWfForm).select('user_title', 'header', 'handler', 'auth_only', 'use_captcha', 'submit_label')
+    weight = 1
+
+    def updateWidgets(self, prefix=None):
+        super(FormPropertiesEditForm, self).updateWidgets(prefix)
+        if 'header' in self.widgets:
+            self.widgets['header'].widget_css_class = 'textarea'
+
+    def get_ajax_output(self, changes):
+        if 'handler' in changes.get(IWfForm, ()):
+            return {'status': 'reload',
+                    'message': self.request.localizer.translate(self.successMessage)}
+        else:
+            return super(FormPropertiesEditForm, self).get_ajax_output(changes)
+
+
+@adapter_config(name='handler-settings',
+                context=(IWfForm, IPyAMSLayer, SharedContentPropertiesEditForm),
+                provides=IInnerSubForm)
+class FormHandlerPropertiesEditForm(InnerAdminEditForm):
+    """Form handler properties edit form extension"""
+
+    legend = _("Form handler settings")
+    fieldset_class = 'bordered no-x-margin margin-y-10'
+
+    def __new__(cls, context, request, view):
+        handler = context.query_handler()
+        if handler is None:
+            return None
+        return InnerAdminEditForm.__new__(cls)
+
+    @property
+    def fields(self):
+        handler = self.context.query_handler()
+        if handler is None:
+            interface = Interface
+        else:
+            interface = handler.handler_info
+        return field.Fields(interface)
+
+    weight = 2
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/shared/form/zmi/summary.py	Tue Sep 19 11:11:30 2017 +0200
@@ -0,0 +1,93 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.shared.common.interfaces.zmi import IInnerSummaryView
+from pyams_content.shared.form.interfaces import IFormFieldContainerTarget, IFormFieldContainer
+from pyams_form.interfaces.form import IInnerTabForm, IFormHelp
+from pyams_i18n.interfaces import II18n
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+
+# import packages
+from pyams_content.shared.common.zmi.summary import SharedContentSummaryForm
+from pyams_form.help import FormHelp
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_utils.adapter import adapter_config
+from pyams_zmi.form import InnerAdminDisplayForm, InnerAdminAddForm
+from z3c.form import field, button
+from zope.interface import implementer, Interface
+
+from pyams_content import _
+
+
+@adapter_config(name='form-fields-summary',
+                context=(IFormFieldContainerTarget, IPyAMSLayer, SharedContentSummaryForm),
+                provides=IInnerTabForm)
+class FormFieldContainerSummary(InnerAdminDisplayForm):
+    """Form fields container summary"""
+
+    weight = 20
+    tab_label = _("Quick preview")
+    tab_target = 'form-fields-summary.html'
+
+    fields = field.Fields(Interface)
+
+
+@pagelet_config(name='form-fields-summary.html', context=IFormFieldContainerTarget, layer=IPyAMSLayer,
+                permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IInnerSummaryView)
+class FormFieldContainerSummaryView(InnerAdminAddForm):
+    """Form fields container summary view"""
+
+    @property
+    def legend(self):
+        return II18n(self.context).query_attribute('user_title', request=self.request)
+
+    @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 updateWidgets(self, prefix=None):
+        super(FormFieldContainerSummaryView, 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
+
+
+@adapter_config(context=(IFormFieldContainerTarget, IPyAMSLayer, FormFieldContainerSummaryView), provides=IFormHelp)
+class FormFieldContainerSummaryHelp(FormHelp):
+    """Form field container summary help adapter"""
+
+    def __new__(cls, context, request, view):
+        if not context.header:
+            return None
+        return FormHelp.__new__(cls)
+
+    @property
+    def message(self):
+        return II18n(self.context).query_attribute('header', request=self.request)
+
+    message_format = 'text'