# HG changeset patch # User Thierry Florac # Date 1505812290 -7200 # Node ID 26aefef3d0aae3276d33effdf12a05fcab6de5e1 # Parent 483b0f16e9a6631eaa1fe6b4369a6b45a8b505ba Updated shared forms diff -r 483b0f16e9a6 -r 26aefef3d0aa src/pyams_content/shared/form/__init__.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) diff -r 483b0f16e9a6 -r 26aefef3d0aa src/pyams_content/shared/form/field.py --- /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 +# 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 diff -r 483b0f16e9a6 -r 26aefef3d0aa src/pyams_content/shared/form/handler.py --- /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 +# 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 diff -r 483b0f16e9a6 -r 26aefef3d0aa src/pyams_content/shared/form/interfaces/__init__.py --- 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""" diff -r 483b0f16e9a6 -r 26aefef3d0aa src/pyams_content/shared/form/zmi/__init__.py --- /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 +# 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""" diff -r 483b0f16e9a6 -r 26aefef3d0aa src/pyams_content/shared/form/zmi/field.py --- /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 +# 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 ''.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 diff -r 483b0f16e9a6 -r 26aefef3d0aa src/pyams_content/shared/form/zmi/properties.py --- /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 +# 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 diff -r 483b0f16e9a6 -r 26aefef3d0aa src/pyams_content/shared/form/zmi/summary.py --- /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 +# 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'