src/ztfy/myams/form.py
changeset 0 8a19e25e39e4
child 53 bc3aa1f34e2d
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ztfy/myams/form.py	Wed May 07 10:36:24 2014 +0200
@@ -0,0 +1,407 @@
+#
+# Copyright (c) 2012 Thierry Florac <tflorac AT onf.fr>
+# All Rights Reserved.
+#
+
+
+# import standard packages
+
+# import Zope3 interfaces
+from z3c.json.interfaces import IJSONWriter
+
+# import local interfaces
+from ztfy.baseskin.interfaces import IDialog
+from ztfy.baseskin.interfaces.form import IGroupsBasedForm, IWidgetsGroup, IAJAXForm, IForm, IInnerSubForm, \
+    IInnerTabForm, ICustomUpdateSubForm
+from ztfy.myams.interfaces import IModalEditFormButtons, IModalAddFormButtons, IEditFormButtons, IAddFormButtons, \
+    IModalDisplayFormButtons, IObjectData
+
+# import Zope3 packages
+from z3c.form import button
+from z3c.formjs import ajax
+from z3c.formui import form
+from zope.component import getAdapters, getUtility
+from zope.event import notify
+from zope.i18n import translate
+from zope.interface import implements
+from zope.lifecycleevent import ObjectCreatedEvent, Attributes
+from zope.schema.fieldproperty import FieldProperty
+from zope.security.proxy import removeSecurityProxy
+
+# import local packages
+from ztfy.baseskin.form import FormObjectCreatedEvent, FormObjectModifiedEvent
+from ztfy.utils.property import cached_property
+
+from ztfy.myams import _
+
+
+#
+# Form widgets group
+#
+
+class WidgetsGroup(object):
+    """Widgets group"""
+
+    implements(IWidgetsGroup)
+
+    def __init__(self, id, widgets=(), legend=None, help=None, css_class='', switch=False,
+                 checkbox_switch=False, checkbox_field=None, hide_if_empty=False):
+        assert (not checkbox_switch) or checkbox_field, "You must define checkbox field when using checkbox switch"
+        self.id = id
+        self.widgets = widgets
+        self.legend = (legend is None) and id or legend
+        self.help = help
+        self._css_class = css_class
+        self.switch = switch
+        self.checkbox_switch = checkbox_switch
+        self.checkbox_field = checkbox_field
+        self.hide_if_empty = hide_if_empty
+
+    @property
+    def switchable(self):
+        return self.switch or self.checkbox_switch
+
+    @property
+    def checkbox_widget(self):
+        if self.checkbox_field is None:
+            return None
+        for widget in self.widgets:
+            if widget.field is self.checkbox_field.field:
+                return widget
+
+    @cached_property
+    def visible(self):
+        if self.checkbox_switch:
+            widget = self.checkbox_widget
+            context = widget.context
+            name = widget.field.getName()
+            value = getattr(context, name, None)
+            return bool(value)
+        else:
+            if not (self.switch and self.hide_if_empty):
+                return True
+            for widget in self.widgets:
+                if not widget.ignoreContext:
+                    field = widget.field
+                    context = widget.context
+                    name = field.getName()
+                    value = getattr(context, name, None)
+                    if value and (value != field.default):
+                        return True
+            return False
+
+    @property
+    def visible_widgets(self):
+        for widget in self.widgets:
+            if (self.checkbox_field is None) or (widget.field is not self.checkbox_field.field):
+                yield widget
+
+    @property
+    def css_class(self):
+        css_class = self._css_class
+        if self.switch:
+            if self.checkbox_switch:
+                css_class += ' checker'
+            else:
+                css_class += ' switcher'
+        return css_class
+
+    @property
+    def switcher_state(self):
+        return 'on' if self.visible else 'off'
+
+    @property
+    def checker_state(self):
+        return 'on' if self.visible else 'off'
+
+
+def NamedWidgetsGroup(id, widgets, names=(), legend=None, help=None, css_class='', switch=False,
+                      checkbox_switch=False, checkbox_field=None, hide_if_empty=False):
+    """Create a widgets group based on widgets names"""
+    return WidgetsGroup(id, [widgets.get(name) for name in names], legend, help, css_class, switch,
+                        checkbox_switch, checkbox_field, hide_if_empty)
+
+
+class GroupsBasedForm(object):
+    """Groups based form"""
+
+    implements(IGroupsBasedForm)
+
+    def __init__(self):
+        self._groups = []
+
+    def addGroup(self, group):
+        self._groups.append(group)
+
+    @property
+    def groups(self):
+        result = self._groups[:]
+        others = []
+        for widget in self.widgets.values():
+            found = False
+            for group in result:
+                if widget in group.widgets:
+                    found = True
+                    break
+            if not found:
+                others.append(widget)
+        if others:
+            result.insert(0, WidgetsGroup(None, others))
+        return result
+
+
+#
+# AJAX form
+#
+
+class AjaxForm(ajax.AJAXRequestHandler):
+    """Custom base class used to handle AJAX errors"""
+
+    implements(IAJAXForm, IObjectData)
+
+    object_data = None
+    form_options = None
+
+    def getFormOptions(self):
+        if self.form_options:
+            writer = getUtility(IJSONWriter)
+            return writer.writer(self.form_options)
+
+    def getAjaxErrors(self):
+        errors = {'status': u'error',
+                  'error_message': translate(self.status, context=self.request)}
+        for error in self.errors:
+            error.update()
+            error = removeSecurityProxy(error)
+            if hasattr(error, 'widget'):
+                widget = removeSecurityProxy(error.widget)
+                if widget is not None:
+                    errors.setdefault('widgets', []).append({'name': widget.name,
+                                                             'label': translate(widget.label, context=self.request),
+                                                             'message': translate(error.message, context=self.request)})
+                else:
+                    errors.setdefault('messages', []).append({'message': translate(error.message, context=self.request)})
+            else:
+                errors.setdefault('messages', []).append(translate(error.message, context=self.request))
+        return errors
+
+
+#
+# Base forms
+#
+
+def getFormWeight(form):
+    try:
+        return form.weight
+    except AttributeError:
+        return 0
+
+
+class BaseForm(GroupsBasedForm, AjaxForm, form.Form):
+    """Base add form"""
+
+    implements(IForm)
+
+    autocomplete = 'on'
+    display_hints_on_widgets = True
+    handle_upload = False
+
+    subforms_legend = None
+
+    css_class = 'ams-form form-horizontal'
+    label_css_class = FieldProperty(IForm['label_css_class'])
+    input_css_class = FieldProperty(IForm['input_css_class'])
+
+    callbacks = {}
+
+    def __init__(self, context, request):
+        GroupsBasedForm.__init__(self)
+        form.Form.__init__(self, context, request)
+
+    def update(self):
+        form.Form.update(self)
+        self.getForms()
+        [subform.update() for subform in self.subforms]
+        [tabform.update() for tabform in self.tabforms]
+
+    def isDialog(self):
+        return IDialog.providedBy(self)
+
+    def getForms(self, with_self=True):
+        if not hasattr(self, 'subforms'):
+            self.subforms = self.createSubForms()
+        if not hasattr(self, 'tabforms'):
+            self.tabforms = self.createTabForms()
+        if with_self:
+            return [self, ] + self.subforms + self.tabforms
+        else:
+            return self.subforms + self.tabforms
+
+    def createSubForms(self):
+        return sorted((adapter[1] for adapter in getAdapters((self, ), IInnerSubForm)), key=getFormWeight)
+
+    def createTabForms(self):
+        return sorted((adapter[1] for adapter in getAdapters((self, ), IInnerTabForm)), key=getFormWeight)
+
+    def getWidgetCallback(self, widget):
+        return self.callbacks.get(widget)
+
+    def updateWidgets(self, prefix=None):
+        form.Form.updateWidgets(self, prefix)
+        self.getForms()
+        [subform.updateWidgets(prefix) for subform in self.subforms]
+        [tabform.updateWidgets(prefix) for tabform in self.tabforms]
+
+    @property
+    def errors(self):
+        result = []
+        for subform in self.getForms():
+            result.extend(subform.widgets.errors)
+        return result
+
+    def updateContent(self, content, data):
+        changes = form.applyChanges(self, content, data)
+        self.getForms()
+        for subform in self.subforms:
+            if ICustomUpdateSubForm.providedBy(subform):
+                updates = ICustomUpdateSubForm(subform).updateContent(content, data)
+                if isinstance(updates, dict):
+                    changes.update(updates)
+            else:
+                changes.update(form.applyChanges(subform, content, data))
+        for tabform in self.tabforms:
+            if ICustomUpdateSubForm.providedBy(tabform):
+                updates = ICustomUpdateSubForm(tabform).updateContent(content, data)
+                if isinstance(updates, dict):
+                    changes.update(updates)
+            else:
+                changes.update(form.applyChanges(subform, content, data))
+        return changes
+
+
+#
+# Add forms
+#
+
+class AddForm(BaseForm, form.AddForm):
+    """Base add form class"""
+
+    buttons = button.Buttons(IAddFormButtons)
+
+    legend = _("Add form")
+    handler = '/@@ajax/ajaxCreate'
+    formErrorsMessage = _("There were some errors.")
+
+    def updateActions(self):
+        form.AddForm.updateActions(self)
+        if 'add' in self.actions:
+            self.actions['add'].addClass('btn-primary')
+
+    @ajax.handler
+    def ajaxCreate(self, action):
+        writer = getUtility(IJSONWriter)
+        self.updateWidgets()
+        data, errors = self.extractData()
+        if errors:
+            return writer.write(self.getAjaxErrors())
+        result = self.createAndAdd(data)
+        return self.getSubmitOutput(writer, result)
+
+    def createAndAdd(self, data):
+        object = self.create(data)
+        notify(ObjectCreatedEvent(object))
+        self.add(object)
+        self.updateContent(object, data)
+        notify(FormObjectCreatedEvent(object, self))
+        return object
+
+    def getSubmitOutput(self, writer, changes):
+        return writer.write({'status': 'reload',
+                             'location': self.nextURL()})
+
+
+class DialogAddForm(AddForm):
+    """Base dialog add form"""
+
+    implements(IDialog)
+
+    buttons = button.Buttons(IModalAddFormButtons)
+    dialog_class = 'modal-medium'
+
+
+#
+# Edit forms
+#
+
+class EditForm(BaseForm, form.EditForm):
+    """Base edit form class"""
+
+    buttons = button.Buttons(IEditFormButtons)
+
+    legend = _("Edit form")
+    handler = '/@@ajax/ajaxEdit'
+    formErrorsMessage = _('There were some errors.')
+    successMessage = _('Data successfully updated.')
+    noChangesMessage = _('No changes were applied.')
+
+    def updateActions(self):
+        form.EditForm.updateActions(self)
+        if 'submit' in self.actions:
+            self.actions['submit'].addClass('btn-primary')
+
+    @ajax.handler
+    def ajaxEdit(self):
+        return self.handleApply()
+
+    def handleApply(self):
+        writer = getUtility(IJSONWriter)
+        self.updateWidgets()
+        data, errors = self.extractData()
+        if errors:
+            return writer.write(self.getAjaxErrors())
+        changes = self.applyChanges(data)
+        return self.getSubmitOutput(writer, changes)
+
+    def applyChanges(self, data):
+        content = self.getContent()
+        changes = self.updateContent(content, data)
+        if changes:
+            descriptions = []
+            for interface, names in changes.items():
+                descriptions.append(Attributes(interface, *names))
+            notify(FormObjectModifiedEvent(content, self, *descriptions))
+        return changes
+
+    def getSubmitOutput(self, writer, changes):
+        if changes:
+            message = self.successMessage
+        else:
+            message = self.noChangesMessage
+        return writer.write({'status': 'success',
+                             'message': translate(message, context=self.request)})
+
+
+class DialogEditForm(EditForm):
+    """Base dialog edit form"""
+
+    implements(IDialog)
+
+    buttons = button.Buttons(IModalEditFormButtons)
+    dialog_class = 'modal-medium'
+
+
+#
+# Display forms
+#
+
+class DisplayForm(BaseForm, form.DisplayForm):
+    """Base display form class"""
+
+
+class DialogDisplayForm(DisplayForm):
+    """Base dialog display form"""
+
+    implements(IDialog)
+
+    buttons = button.Buttons(IModalDisplayFormButtons)
+    dialog_class = 'modal-medium'