src/pyams_form/form.py
changeset 204 c435de184bda
parent 195 e4b68da720e2
child 216 33bbf7a09d29
equal deleted inserted replaced
203:ba372884335f 204:c435de184bda
     8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
     9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
    10 # FOR A PARTICULAR PURPOSE.
    10 # FOR A PARTICULAR PURPOSE.
    11 #
    11 #
    12 
    12 
    13 __docformat__ = 'restructuredtext'
    13 """PyAMS_form.form module
    14 
    14 
       
    15 This module is the core of PyAMS_form package; it provides additions to form management as
       
    16 provided by z3c.form package.
       
    17 """
       
    18 
       
    19 import json
    15 import logging
    20 import logging
    16 logger = logging.getLogger('PyAMS (form)')
    21 
    17 
       
    18 import json
       
    19 import transaction
    22 import transaction
    20 import venusian
    23 import venusian
    21 from persistent import IPersistent
    24 from persistent import IPersistent
    22 from pyramid.decorator import reify
    25 from pyramid.decorator import reify
    23 from pyramid.events import subscriber
    26 from pyramid.events import subscriber
    24 from pyramid.response import Response
    27 from pyramid.response import Response
    25 from pyramid_chameleon.interfaces import IChameleonTranslate
    28 from pyramid_chameleon.interfaces import IChameleonTranslate
    26 from pyramid_zope_request import PyramidPublisherRequest, PyramidToPublisher
    29 from pyramid_zope_request import PyramidPublisherRequest, PyramidToPublisher
    27 from z3c.form.button import Buttons
    30 from z3c.form.button import Buttons
    28 from z3c.form.form import AddForm as BaseAddForm, DisplayForm as BaseDisplayForm, EditForm as BaseEditForm, Form, \
    31 from z3c.form.form import AddForm as BaseAddForm, DisplayForm as BaseDisplayForm, \
    29     applyChanges
    32     EditForm as BaseEditForm, Form, applyChanges
    30 from z3c.form.interfaces import DISPLAY_MODE, IErrorViewSnippet, IMultipleErrors
    33 from z3c.form.interfaces import DISPLAY_MODE, IErrorViewSnippet, IMultipleErrors
    31 from zope.component import queryUtility
    34 from zope.component import queryUtility
    32 from zope.interface import Interface, alsoProvides, classImplements, implementer, Invalid
    35 from zope.interface import Interface, Invalid, alsoProvides, classImplements, implementer
    33 from zope.lifecycleevent import Attributes, ObjectCreatedEvent
    36 from zope.lifecycleevent import Attributes, ObjectCreatedEvent
    34 from zope.location import locate
    37 from zope.location import locate
    35 from zope.publisher.interfaces.browser import IBrowserRequest
    38 from zope.publisher.interfaces.browser import IBrowserRequest
    36 from zope.schema.fieldproperty import FieldProperty
    39 from zope.schema.fieldproperty import FieldProperty
    37 from zope.schema.interfaces import ValidationError
    40 from zope.schema.interfaces import ValidationError
    38 
    41 
    39 from pyams_form.group import GroupsBasedForm
    42 from pyams_form.group import GroupsBasedForm
    40 from pyams_form.interfaces import get_form_weight
    43 from pyams_form.interfaces import get_form_weight
    41 from pyams_form.interfaces.form import FormCreatedEvent, IAJAXForm, ICustomUpdateSubForm, IForm, \
    44 from pyams_form.interfaces.form import FormCreatedEvent, IAJAXForm, ICustomUpdateSubForm, IForm, \
    42     IFormContextPermissionChecker, IFormCreatedEvent, IFormLayer, IInnerForm, IInnerSubForm, IInnerTabForm
    45     IFormContextPermissionChecker, IFormCreatedEvent, IFormLayer, IInnerForm, IInnerSubForm, \
       
    46     IInnerTabForm
    43 from pyams_form.interfaces.form import FormObjectCreatedEvent, FormObjectModifiedEvent
    47 from pyams_form.interfaces.form import FormObjectCreatedEvent, FormObjectModifiedEvent
    44 from pyams_form.interfaces.form import IAddFormButtons, IEditFormButtons, IModalAddFormButtons, \
    48 from pyams_form.interfaces.form import IAddFormButtons, IEditFormButtons, IModalAddFormButtons, \
    45     IModalDisplayFormButtons, IModalEditFormButtons
    49     IModalDisplayFormButtons, IModalEditFormButtons
    46 from pyams_i18n.interfaces import II18n
    50 from pyams_i18n.interfaces import II18n
    47 from pyams_pagelet.interfaces import PageletCreatedEvent
    51 from pyams_pagelet.interfaces import PageletCreatedEvent
    50 from pyams_template.interfaces import IContentTemplate, ILayoutTemplate
    54 from pyams_template.interfaces import IContentTemplate, ILayoutTemplate
    51 from pyams_utils.adapter import ContextRequestViewAdapter, adapter_config
    55 from pyams_utils.adapter import ContextRequestViewAdapter, adapter_config
    52 from pyams_utils.interfaces import FORBIDDEN_PERMISSION, ICacheKeyValue
    56 from pyams_utils.interfaces import FORBIDDEN_PERMISSION, ICacheKeyValue
    53 from pyams_utils.url import absolute_url
    57 from pyams_utils.url import absolute_url
    54 
    58 
    55 from pyams_form import _
    59 
    56 
    60 __docformat__ = 'restructuredtext'
       
    61 
       
    62 from pyams_form import _    # pylint: disable=ungrouped-imports
       
    63 
       
    64 
       
    65 LOGGER = logging.getLogger('PyAMS (form)')
    57 
    66 
    58 REDIRECT_STATUS_CODES = (300, 301, 302, 303, 304, 305, 307)
    67 REDIRECT_STATUS_CODES = (300, 301, 302, 303, 304, 305, 307)
    59 
    68 
    60 
    69 
    61 @PyramidToPublisher(IBrowserRequest)
    70 @PyramidToPublisher(IBrowserRequest)
    94             alsoProvides(req, self.layer)
   103             alsoProvides(req, self.layer)
    95         request.registry.notify(FormCreatedEvent(self))
   104         request.registry.notify(FormCreatedEvent(self))
    96 
   105 
    97     @property
   106     @property
    98     def title(self):
   107     def title(self):
       
   108         """Get form's title"""
    99         registry = self.request.registry
   109         registry = self.request.registry
   100         adapter = registry.queryMultiAdapter((self.context, self.request, self), IContentTitle)
   110         adapter = registry.queryMultiAdapter((self.context, self.request, self), IContentTitle)
   101         if adapter is None:
   111         if adapter is None:
   102             adapter = registry.queryAdapter(self.context, IContentTitle)
   112             adapter = registry.queryAdapter(self.context, IContentTitle)
   103         if adapter is not None:
   113         if adapter is not None:
   104             return adapter.title
   114             return adapter.title
   105         else:
   115         return II18n(self.context).query_attribute('title', request=self.request)
   106             return II18n(self.context).query_attribute('title', request=self.request)
       
   107 
   116 
   108     def check_mode(self):
   117     def check_mode(self):
       
   118         """Check form's mode according to context's permissions"""
   109         content = self.getContent()
   119         content = self.getContent()
   110         # check form permission to get form mode
   120         # check form permission to get form mode
   111         if self.edit_permission and not self.request.has_permission(self.edit_permission, content):
   121         if self.edit_permission and not self.request.has_permission(self.edit_permission, content):
   112             self.mode = DISPLAY_MODE
   122             self.mode = DISPLAY_MODE
   113             return
   123             return
   114         # check form mode based on context checker
   124         # check form mode based on context checker
   115         registry = self.request.registry
   125         registry = self.request.registry
   116         permission = None
   126         permission = None
   117         checker = registry.queryMultiAdapter((content, self.request, self), IFormContextPermissionChecker)
   127         checker = registry.queryMultiAdapter((content, self.request, self),
       
   128                                              IFormContextPermissionChecker)
   118         if checker is None:
   129         if checker is None:
   119             checker = registry.queryAdapter(content, IFormContextPermissionChecker)
   130             checker = registry.queryAdapter(content, IFormContextPermissionChecker)
   120         if checker is not None:
   131         if checker is not None:
   121             permission = checker.edit_permission
   132             permission = checker.edit_permission
   122         if permission and (permission != self.edit_permission):
   133         if permission and (permission != self.edit_permission):
   123             if (permission == FORBIDDEN_PERMISSION) or not self.request.has_permission(permission, content):
   134             if (permission == FORBIDDEN_PERMISSION) or not self.request.has_permission(permission,
       
   135                                                                                        content):
   124                 self.mode = DISPLAY_MODE
   136                 self.mode = DISPLAY_MODE
   125 
   137 
   126     def update(self):
   138     def update(self):
       
   139         """Update form and all it's subforms"""
   127         # check form mode
   140         # check form mode
   128         self.check_mode()
   141         self.check_mode()
   129         # update form and sub-forms
   142         # update form and sub-forms
   130         [subform.update() for subform in self.subforms]
   143         [subform.update() for subform in self.subforms]  # pylint: disable=expression-not-assigned
   131         [tabform.update() for tabform in self.tabforms]
   144         [tabform.update() for tabform in self.tabforms]  # pylint: disable=expression-not-assigned
   132         Form.update(self)
   145         Form.update(self)
   133         # savepoint is required for each inner component to be persisted!!
   146         # savepoint is required for each inner component to be persisted!!
   134         transaction.savepoint()
   147         transaction.savepoint()
   135 
   148 
   136     def updateWidgets(self, prefix=None):
   149     def updateWidgets(self, prefix=None):
       
   150         """Update form's widgets"""
   137         super(BaseForm, self).updateWidgets(prefix)
   151         super(BaseForm, self).updateWidgets(prefix)
   138         if not self._groups:
   152         if not self._groups:
   139             self.updateGroups()
   153             self.updateGroups()
   140 
   154 
   141     def get_form_action(self):
   155     def get_form_action(self):
       
   156         """Get action associated with form"""
   142         return self.action
   157         return self.action
   143 
   158 
   144     @reify
   159     @reify
   145     def subforms(self):
   160     def subforms(self):
       
   161         """Get subforms adapters associated with this form"""
   146         registry = self.request.registry
   162         registry = self.request.registry
   147         return sorted((adapter[1]
   163         return sorted((adapter[1]
   148                        for adapter in registry.getAdapters((self.context, self.request, self), IInnerSubForm)),
   164                        for adapter in
       
   165                        registry.getAdapters((self.context, self.request, self), IInnerSubForm)),
   149                       key=get_form_weight)
   166                       key=get_form_weight)
   150 
   167 
   151     @reify
   168     @reify
   152     def tabforms(self):
   169     def tabforms(self):
       
   170         """Get tabforms adapters associated with this form"""
   153         registry = self.request.registry
   171         registry = self.request.registry
   154         return sorted((adapter[1]
   172         return sorted((adapter[1]
   155                        for adapter in registry.getAdapters((self.context, self.request, self), IInnerTabForm)),
   173                        for adapter in
       
   174                        registry.getAdapters((self.context, self.request, self), IInnerTabForm)),
   156                       key=get_form_weight)
   175                       key=get_form_weight)
   157 
   176 
   158     @property
   177     @property
   159     def forms(self):
   178     def forms(self):
       
   179         """Get all forms associated with this form, including the form itself"""
   160         return [self, ] + self.subforms + self.tabforms
   180         return [self, ] + self.subforms + self.tabforms
   161 
   181 
   162     def get_forms(self, include_self=True):
   182     def get_forms(self, include_self=True):
       
   183         """Get forms associated with this form; if *include_self* argument is True, self is also
       
   184         included into results list
       
   185         """
   163         if include_self:
   186         if include_self:
   164             yield self
   187             yield self
   165         for group in self.groups:
   188         for group in self.groups:
   166             for subform in group.get_forms():
   189             for subform in group.get_forms():
   167                 yield subform
   190                 yield subform
   171         for form in self.tabforms:
   194         for form in self.tabforms:
   172             yield form
   195             yield form
   173 
   196 
   174     @reify
   197     @reify
   175     def warn_on_change(self):
   198     def warn_on_change(self):
       
   199         """JSON boolean value specifying "warn on change" flag"""
   176         if self._warn_on_change is True:
   200         if self._warn_on_change is True:
   177             return 'true'
   201             return 'true'
   178         elif self._warn_on_change is False:
   202         if self._warn_on_change is False:
   179             return 'false'
   203             return 'false'
   180         else:
   204         return None
   181             return None
       
   182 
   205 
   183     @property
   206     @property
   184     def is_dialog(self):
   207     def is_dialog(self):
       
   208         """Boolean flag specifying if current form should be displayed in a modal dialog box"""
   185         return IDialog.providedBy(self)
   209         return IDialog.providedBy(self)
   186 
   210 
   187     def get_widget_callback(self, widget):
   211     def get_widget_callback(self, widget):
       
   212         """Get callback associated with a given widget"""
   188         return (self.callbacks or {}).get(widget)
   213         return (self.callbacks or {}).get(widget)
   189 
   214 
   190     # Default z3c.form methods
   215     # Default z3c.form methods
   191 
   216 
   192     @property
   217     @property
   193     def errors(self):
   218     def errors(self):
       
   219         """Get form's errors"""
   194         result = []
   220         result = []
   195         for form in self.forms:
   221         for form in self.forms:
   196             result.extend(form.widgets.errors)
   222             result.extend(form.widgets.errors)
   197         return result
   223         return result
   198 
   224 
   199     def add_error(self, error, widget, status=None):
   225     def add_error(self, error, widget, status=None):
       
   226         """Add error to current list of form's errors"""
   200         if isinstance(error, str):
   227         if isinstance(error, str):
   201             error = Invalid(error)
   228             error = Invalid(error)
   202         if isinstance(widget, str):
   229         if isinstance(widget, str):
   203             widget = self.widgets[widget]
   230             widget = self.widgets[widget]
   204         snippet = self.request.registry.getMultiAdapter((error, self.request, widget,
   231         snippet = self.request.registry.getMultiAdapter((error, self.request, widget,
   211         if not self.status:
   238         if not self.status:
   212             self.status = translate(status or self.formErrorsMessage)
   239             self.status = translate(status or self.formErrorsMessage)
   213         self.status += '\n{0}'.format(translate(error.args[0]))
   240         self.status += '\n{0}'.format(translate(error.args[0]))
   214 
   241 
   215     def update_content(self, content, data):
   242     def update_content(self, content, data):
       
   243         """Update content properties with given data"""
   216         changes = applyChanges(self, content, data.get(self, data))
   244         changes = applyChanges(self, content, data.get(self, data))
   217         for subform in self.get_forms(include_self=False):
   245         for subform in self.get_forms(include_self=False):
   218             if subform.mode == DISPLAY_MODE:
   246             if subform.mode == DISPLAY_MODE:
   219                 continue
   247                 continue
   220             subform_update = ICustomUpdateSubForm(subform, None)
   248             subform_update = ICustomUpdateSubForm(subform, None)
   221             if subform_update is not None:
   249             if subform_update is not None:
   222                 updates = subform_update.update_content(subform.getContent(), data.get(subform, data))
   250                 # pylint: disable=assignment-from-no-return
       
   251                 updates = subform_update.update_content(subform.getContent(),
       
   252                                                         data.get(subform, data))
   223                 if isinstance(updates, dict):
   253                 if isinstance(updates, dict):
   224                     changes.update(updates)
   254                     changes.update(updates)
   225             else:
   255             else:
   226                 changes.update(applyChanges(subform, subform.getContent(), data.get(subform, data)))
   256                 changes.update(applyChanges(subform, subform.getContent(), data.get(subform, data)))
   227         return changes
   257         return changes
   228 
   258 
   229     def render(self):
   259     def render(self):
       
   260         """Render form using content template
       
   261 
       
   262         Several default templates are defined for all base forms interfaces.
       
   263         """
   230         request = self.request
   264         request = self.request
   231         if isinstance(request, PyramidPublisherRequest):
   265         if isinstance(request, PyramidPublisherRequest):
   232             request = request._request
   266             request = request._request  # pylint: disable=protected-access
   233         cdict = {
   267         cdict = {
   234             'context': self.context,
   268             'context': self.context,
   235             'request': request,
   269             'request': request,
   236             'view': self,
   270             'view': self,
   237             'translate': queryUtility(IChameleonTranslate)
   271             'translate': queryUtility(IChameleonTranslate)
   240             registry = request.registry
   274             registry = request.registry
   241             template = registry.queryMultiAdapter((self, request, self.context), IContentTemplate)
   275             template = registry.queryMultiAdapter((self, request, self.context), IContentTemplate)
   242             if template is None:
   276             if template is None:
   243                 template = registry.getMultiAdapter((self, request), IContentTemplate)
   277                 template = registry.getMultiAdapter((self, request), IContentTemplate)
   244             return template(**cdict)
   278             return template(**cdict)
   245         return self.template(**cdict)
   279         return self.template(**cdict)  # pylint: disable=not-callable
   246 
   280 
   247     def __call__(self, **kwargs):
   281     def __call__(self, **kwargs):  # pylint: disable=arguments-differ
   248         self.update()
   282         self.update()
   249         if self.request.response.status_code in REDIRECT_STATUS_CODES:
   283         if self.request.response.status_code in REDIRECT_STATUS_CODES:
   250             return Response('')
   284             return Response('')
   251 
   285 
   252         request = self.request
   286         request = self.request
   264             layout = registry.queryMultiAdapter((self, request, self.context),
   298             layout = registry.queryMultiAdapter((self, request, self.context),
   265                                                 ILayoutTemplate)
   299                                                 ILayoutTemplate)
   266             if layout is None:
   300             if layout is None:
   267                 layout = registry.getMultiAdapter((self, request), ILayoutTemplate)
   301                 layout = registry.getMultiAdapter((self, request), ILayoutTemplate)
   268             return Response(layout(**cdict))
   302             return Response(layout(**cdict))
   269         return Response(self.layout(**cdict))
   303         return Response(self.layout(**cdict))  # pylint: disable=not-callable
   270 
   304 
   271     def get_skin(self, request=None):
   305     def get_skin(self, request=None):
       
   306         """Get current skin applied to request"""
       
   307         if request is None:
       
   308             request = self.request
   272         return request.annotations.get('__skin__')
   309         return request.annotations.get('__skin__')
   273 
   310 
   274 
   311 
   275 @implementer(IAJAXForm)
   312 @implementer(IAJAXForm)
   276 class AJAXForm(BaseForm):
   313 class AJAXForm(BaseForm):
   277     """AJAX form base class"""
   314     # pylint: disable=abstract-method
       
   315     """AJAX form base class
       
   316 
       
   317     An AJAX form is a form which is submitted through an AJAX call; submit response is
       
   318     returned as a JSON object which is handled by MyAMS.
       
   319     """
   278 
   320 
   279     ajax_handler = FieldProperty(IAJAXForm['ajax_handler'])
   321     ajax_handler = FieldProperty(IAJAXForm['ajax_handler'])
   280     form_options = FieldProperty(IAJAXForm['form_options'])
   322     form_options = FieldProperty(IAJAXForm['form_options'])
   281     form_target = FieldProperty(IAJAXForm['form_target'])
   323     form_target = FieldProperty(IAJAXForm['form_target'])
   282     ajax_callback = FieldProperty(IAJAXForm['ajax_callback'])
   324     ajax_callback = FieldProperty(IAJAXForm['ajax_callback'])
   283 
   325 
   284     def get_form_action(self):
   326     def get_form_action(self):
       
   327         """Get form's target action URL"""
   285         return absolute_url(self.context, self.request, self.request.view_name)
   328         return absolute_url(self.context, self.request, self.request.view_name)
   286 
   329 
   287     def get_form_options(self):
   330     def get_form_options(self):
       
   331         """Get form's options"""
   288         return json.dumps(self.form_options) if self.form_options else None
   332         return json.dumps(self.form_options) if self.form_options else None
   289 
   333 
   290     def get_ajax_handler(self):
   334     def get_ajax_handler(self):
       
   335         """Get form's AJAX handler"""
   291         return absolute_url(self.context, self.request, self.ajax_handler)
   336         return absolute_url(self.context, self.request, self.ajax_handler)
   292 
   337 
   293     def get_ajax_errors(self, ajax_errors=None):
   338     def get_ajax_errors(self, ajax_errors=None):
   294         """Extract form errors in AJAX format"""
   339         """Extract form errors in JSON format"""
   295         translate = self.request.localizer.translate
   340         translate = self.request.localizer.translate
   296         errors = {
   341         errors = {
   297             'status': u'error',
   342             'status': u'error',
   298             'error_message': translate(self.status)
   343             'error_message': translate(self.status)
   299         }
   344         }
   300         registry = self.request.registry
   345         registry = self.request.registry
   301         for error in (ajax_errors or self.errors):
   346         for error in (ajax_errors or self.errors):
   302             if isinstance(error, Exception):
   347             if isinstance(error, Exception):
   303                 error = registry.getMultiAdapter((error, self.request, None, None, self, self.request),
   348                 error = registry.getMultiAdapter(
   304                                                  IErrorViewSnippet)
   349                     (error, self.request, None, None, self, self.request),
       
   350                     IErrorViewSnippet)
   305             error.update()
   351             error.update()
   306             if IMultipleErrors.providedBy(error.error):
   352             if IMultipleErrors.providedBy(error.error):
   307                 for inner_error in error.error.errors:
   353                 for inner_error in error.error.errors:
   308                     if hasattr(inner_error, 'widget'):
   354                     if hasattr(inner_error, 'widget'):
   309                         widget = inner_error.widget
   355                         widget = inner_error.widget
   354 #
   400 #
   355 # Add forms
   401 # Add forms
   356 #
   402 #
   357 
   403 
   358 class AddForm(AJAXForm, BaseAddForm):
   404 class AddForm(AJAXForm, BaseAddForm):
       
   405     # pylint: disable=abstract-method
   359     """Add form base class"""
   406     """Add form base class"""
   360 
   407 
   361     prefix = 'add_form.'
   408     prefix = 'add_form.'
   362 
   409 
   363     buttons = Buttons(IAddFormButtons)
   410     buttons = Buttons(IAddFormButtons)
   371             self.actions['add'].addClass('btn-primary')
   418             self.actions['add'].addClass('btn-primary')
   372 
   419 
   373     def createAndAdd(self, data):
   420     def createAndAdd(self, data):
   374         registry = self.request.registry
   421         registry = self.request.registry
   375         # create object
   422         # create object
   376         object = self.create(data.get(self, data))
   423         obj = self.create(data.get(self, data))
   377         if IPersistent.providedBy(object):
   424         if IPersistent.providedBy(obj):
   378             registry.notify(ObjectCreatedEvent(object))
   425             registry.notify(ObjectCreatedEvent(obj))
   379             # set parent temporarily to avoid NotYet exceptions
   426             # set parent temporarily to avoid NotYet exceptions
   380             locate(object, self.context)
   427             locate(obj, self.context)
   381             # update object properties before adding it
   428             # update object properties before adding it
   382             self.update_content(object, data)
   429             self.update_content(obj, data)
   383             self.add(object)
   430             self.add(obj)
   384             registry.notify(FormObjectCreatedEvent(object, self))
   431             registry.notify(FormObjectCreatedEvent(obj, self))
   385         return object
   432         return obj
   386 
   433 
   387     def update_content(self, content, data):
   434     def update_content(self, content, data):
   388         changes = applyChanges(self, content, data.get(self, data))
   435         changes = applyChanges(self, content, data.get(self, data))
   389         for subform in self.get_forms(include_self=False):
   436         for subform in self.get_forms(include_self=False):
   390             if subform.mode == DISPLAY_MODE:
   437             if subform.mode == DISPLAY_MODE:
   391                 continue
   438                 continue
   392             subform_update = ICustomUpdateSubForm(subform, None)
   439             subform_update = ICustomUpdateSubForm(subform, None)
   393             if subform_update is not None:
   440             if subform_update is not None:
       
   441                 # pylint: disable=assignment-from-no-return
   394                 updates = subform_update.update_content(content, data.get(subform, data))
   442                 updates = subform_update.update_content(content, data.get(subform, data))
   395                 if isinstance(updates, dict):
   443                 if isinstance(updates, dict):
   396                     changes.update(updates)
   444                     changes.update(updates)
   397             else:
   445             else:
   398                 changes.update(applyChanges(subform, content, data.get(subform, data)))
   446                 changes.update(applyChanges(subform, content, data.get(subform, data)))
   403 class AddFormContextPermissionChecker(ContextRequestViewAdapter):
   451 class AddFormContextPermissionChecker(ContextRequestViewAdapter):
   404     """Add form context permission checker"""
   452     """Add form context permission checker"""
   405 
   453 
   406     @property
   454     @property
   407     def edit_permission(self):
   455     def edit_permission(self):
       
   456         """Get permission associated with this form's context"""
   408         return self.view.edit_permission
   457         return self.view.edit_permission
   409 
   458 
   410 
   459 
   411 class AJAXAddForm(AddForm):
   460 class AJAXAddForm(AddForm):
       
   461     # pylint: disable=abstract-method
   412     """AJAX add form"""
   462     """AJAX add form"""
   413 
   463 
   414     def __call__(self):
   464     def __call__(self):  # pylint: disable=arguments-differ
   415         self.request.registry.notify(PageletCreatedEvent(self))
   465         self.request.registry.notify(PageletCreatedEvent(self))
   416         data, errors = {}, ()
   466         data, errors = {}, ()
   417         for form in self.get_forms():
   467         for form in self.get_forms():
   418             form.check_mode()
   468             form.check_mode()
   419             form.updateWidgets()
   469             form.updateWidgets()
   433                 try:
   483                 try:
   434                     widget = form.widgets[error.args[-1]]
   484                     widget = form.widgets[error.args[-1]]
   435                 except KeyError:
   485                 except KeyError:
   436                     continue
   486                     continue
   437                 else:
   487                 else:
   438                     view = registry.getMultiAdapter((error, self.request, widget, widget.field, self, self.context),
   488                     view = registry.getMultiAdapter(
   439                                                     IErrorViewSnippet)
   489                         (error, self.request, widget, widget.field, self, self.context),
       
   490                         IErrorViewSnippet)
   440                     view.update()
   491                     view.update()
   441                     widget.error = view
   492                     widget.error = view
   442                     errors = (view,)
   493                     errors = (view,)
   443                     return self.get_ajax_errors(errors)
   494                     return self.get_ajax_errors(errors)
   444             # Fallback when you can't find widget
   495             # Fallback when you can't find widget
   455         for form in self.get_forms(include_self=False):
   506         for form in self.get_forms(include_self=False):
   456             try:
   507             try:
   457                 form_output = form.get_ajax_output(changes)
   508                 form_output = form.get_ajax_output(changes)
   458                 if form_output:
   509                 if form_output:
   459                     for key, value in form_output.items():
   510                     for key, value in form_output.items():
   460                         if isinstance(value, (list, tuple)) and (key in output):  # concatenate lists
   511                         if isinstance(value, (list, tuple)) and (
       
   512                                 key in output):  # concatenate lists
   461                             form_output[key] += output[key]
   513                             form_output[key] += output[key]
   462                     output.update(form_output)
   514                     output.update(form_output)
   463             except NotImplementedError:
   515             except NotImplementedError:
   464                 pass
   516                 pass
   465         if output:
   517         if output:
   466             return output
   518             return output
   467         else:
   519         return {
   468             return {
   520             'status': 'reload',
   469                 'status': 'reload',
   521             'location': self.nextURL()
   470                 'location': self.nextURL()
   522         }
   471             }
       
   472 
   523 
   473 
   524 
   474 @implementer(IDialog)
   525 @implementer(IDialog)
   475 class DialogAddForm(AddForm):
   526 class DialogAddForm(AddForm):
       
   527     # pylint: disable=abstract-method
   476     """Modal dialog add form"""
   528     """Modal dialog add form"""
   477 
   529 
   478     buttons = Buttons(IModalAddFormButtons)
   530     buttons = Buttons(IModalAddFormButtons)
   479     dialog_class = 'modal-medium'
   531     dialog_class = 'modal-medium'
   480 
   532 
   481 
   533 
   482 @implementer(IInnerForm)
   534 @implementer(IInnerForm)
   483 class InnerAddForm(AddForm):
   535 class InnerAddForm(AddForm):
       
   536     # pylint: disable=abstract-method
   484     """Inner add form"""
   537     """Inner add form"""
   485 
   538 
   486     css_class = 'inner'
   539     css_class = 'inner'
   487 
   540 
   488     buttons = Buttons(Interface)
   541     buttons = Buttons(Interface)
   526 
   579 
   527 
   580 
   528 class AJAXEditForm(EditForm):
   581 class AJAXEditForm(EditForm):
   529     """AJAX edit form"""
   582     """AJAX edit form"""
   530 
   583 
   531     def __call__(self):
   584     def __call__(self):  # pylint: disable=arguments-differ,too-many-branches
   532         # call form elements
   585         # call form elements
   533         self.request.registry.notify(PageletCreatedEvent(self))
   586         self.request.registry.notify(PageletCreatedEvent(self))
   534         data, errors = {}, ()
   587         data, errors = {}, ()
   535         for form in self.get_forms():
   588         for form in self.get_forms():
   536             form.check_mode()
   589             form.check_mode()
   553                 try:
   606                 try:
   554                     widget = form.widgets[error.args[-1]]
   607                     widget = form.widgets[error.args[-1]]
   555                 except KeyError:
   608                 except KeyError:
   556                     continue
   609                     continue
   557                 else:
   610                 else:
   558                     view = registry.getMultiAdapter((error, self.request, widget, widget.field, self, self.context),
   611                     view = registry.getMultiAdapter(
   559                                                     IErrorViewSnippet)
   612                         (error, self.request, widget, widget.field, self, self.context),
       
   613                         IErrorViewSnippet)
   560                     view.update()
   614                     view.update()
   561                     widget.error = view
   615                     widget.error = view
   562                     errors = (view,)
   616                     errors = (view,)
   563                     return self.get_ajax_errors(errors)
   617                     return self.get_ajax_errors(errors)
   564             # Fallback when you can't find widget
   618             # Fallback when you can't find widget
   591                 continue
   645                 continue
   592             try:
   646             try:
   593                 form_output = form.get_ajax_output(changes)
   647                 form_output = form.get_ajax_output(changes)
   594                 if form_output:
   648                 if form_output:
   595                     for key, value in form_output.items():
   649                     for key, value in form_output.items():
   596                         if isinstance(value, (list, tuple)) and (key in output):  # concatenate lists
   650                         if isinstance(value, (list, tuple)) and (
       
   651                                 key in output):  # concatenate lists
   597                             form_output[key] += output[key]
   652                             form_output[key] += output[key]
   598                     output.update(form_output)
   653                     output.update(form_output)
   599             except NotImplementedError:
   654             except NotImplementedError:
   600                 pass
   655                 pass
   601         return output
   656         return output
   665 # Form events subscribers
   720 # Form events subscribers
   666 #
   721 #
   667 
   722 
   668 @subscriber(IFormCreatedEvent, context_selector=ISkinnable)
   723 @subscriber(IFormCreatedEvent, context_selector=ISkinnable)
   669 def handle_form_skin(event):
   724 def handle_form_skin(event):
       
   725     """Apply skin on form's creation event"""
   670     request = _request = event.object.request
   726     request = _request = event.object.request
   671     if isinstance(request, PyramidPublisherRequest):
   727     if isinstance(request, PyramidPublisherRequest):
   672         _request = request._request
   728         _request = request._request  # pylint: disable=protected-access
   673     skin = ISkinnable(event.object).get_skin(_request)
   729     skin = ISkinnable(event.object).get_skin(_request)  # pylint: disable=assignment-from-no-return
   674     if skin is not None:
   730     if skin is not None:
   675         apply_skin(request, skin)
   731         apply_skin(request, skin)
   676 
   732 
   677 
   733 
   678 class FormSelector(object):
   734 class FormSelector:
   679     """Form event selector
   735     """Form event selector
   680 
   736 
   681     This selector can be used by subscriber to filter form events
   737     This selector can be used by subscriber to filter form events
   682     """
   738     """
   683 
   739 
   684     def __init__(self, ifaces, config):
   740     def __init__(self, ifaces, config):  # pylint: disable=unused-argument
   685         if not isinstance(ifaces, (list, tuple)):
   741         if not isinstance(ifaces, (list, tuple)):
   686             ifaces = (ifaces,)
   742             ifaces = (ifaces,)
   687         self.interfaces = ifaces
   743         self.interfaces = ifaces
   688 
   744 
   689     def text(self):
   745     def text(self):
       
   746         """Form's selector text"""
   690         return 'form_selector = %s' % str(self.interfaces)
   747         return 'form_selector = %s' % str(self.interfaces)
   691 
   748 
   692     phash = text
   749     phash = text
   693 
   750 
   694     def __call__(self, event):
   751     def __call__(self, event):
   700                 if isinstance(event.form, intf):
   757                 if isinstance(event.form, intf):
   701                     return True
   758                     return True
   702         return False
   759         return False
   703 
   760 
   704 
   761 
   705 class WidgetSelector(object):
   762 class WidgetSelector:
   706     """Widget event selector
   763     """Widget event selector
   707 
   764 
   708     This selector can be used by subscribers to filter widgets events
   765     This selector can be used by subscribers to filter widgets events
   709     """
   766     """
   710 
   767 
   711     def __init__(self, ifaces, config):
   768     def __init__(self, ifaces, config):  # pylint: disable=unused-argument
   712         if not isinstance(ifaces, (list, tuple)):
   769         if not isinstance(ifaces, (list, tuple)):
   713             ifaces = (ifaces,)
   770             ifaces = (ifaces,)
   714         self.interfaces = ifaces
   771         self.interfaces = ifaces
   715 
   772 
   716     def text(self):
   773     def text(self):
       
   774         """Widget's selector text"""
   717         return 'widget_selector = %s' % str(self.interfaces)
   775         return 'widget_selector = %s' % str(self.interfaces)
   718 
   776 
   719     phash = text
   777     phash = text
   720 
   778 
   721     def __call__(self, event):
   779     def __call__(self, event):
   727                 if isinstance(event.widget, intf):
   785                 if isinstance(event.widget, intf):
   728                     return True
   786                     return True
   729         return False
   787         return False
   730 
   788 
   731 
   789 
   732 class ajax_config(object):
   790 class ajax_config:  # pylint: disable=invalid-name
   733     """Class decorator used to declare AJAX settings for a form.
   791     """Class decorator used to declare AJAX settings for a form.
   734 
   792 
   735     When decorating a form class, this decorator create a new subclass which will handle AJAX queries
   793     When decorating a form class, this decorator create a new subclass which will handle AJAX
   736     executed when submitting the form, and register this class as Pyramid's view.
   794     queries executed when submitting the form, and register this class as Pyramid's view.
   737 
   795 
   738     Decorator arguments (all optional) are:
   796     Decorator arguments (all optional) are:
   739 
   797 
   740     - **name**: AJAX view name
   798     - :param name: AJAX view name
   741     - **context** (or **for_**): view context type
   799     - :param context: (or **for_**): view context type
   742     - **layer** (or **request_type**): request type for which view is registered
   800     - :param layer: (or **request_type**): request type for which view is registered
   743     - **permission**: permission required to call the view; if not set, permission is extracted from form's
   801     - :param permission: permission required to call the view; if not set, permission is
   744       "edit_permission" attribute
   802           extracted from form's "edit_permission" attribute
   745     - **base**: base class for newly created AJAX form; if not set, inherits from :py:class:`AJAXEditForm`
   803     - :param base: base class for newly created AJAX form; if not set, inherits from
   746     - **implementer**: list of interfaces implemented by the new class
   804           :py:class:`AJAXEditForm`
   747     - **method** (or **request_method**): HTTP method name; if not defined, view is restricted to "POST" requests
   805     - :param implementer: list of interfaces implemented by the new class
   748     - **renderer**: name of Pyramid renderer used to return view output; defaults to 'json'
   806     - :param method: (or **request_method**): HTTP method name; if not defined, view is
   749     - **xhr**: Pyramid's view's "xhr" predicate; **True** by default
   807           restricted to "POST" requests
       
   808     - :param renderer: name of Pyramid renderer used to return view output; defaults to 'json'
       
   809     - :param xhr: Pyramid's view's "xhr" predicate; **True** by default
   750     """
   810     """
   751 
   811 
   752     venusian = venusian  # for testing injection
   812     venusian = venusian  # for testing injection
   753 
   813 
   754     def __init__(self, **settings):
   814     def __init__(self, **settings):
   771 
   831 
   772     def __call__(self, wrapped):
   832     def __call__(self, wrapped):
   773         settings = self.__dict__.copy()
   833         settings = self.__dict__.copy()
   774         depth = settings.pop('_depth', 0)
   834         depth = settings.pop('_depth', 0)
   775 
   835 
   776         def callback(context, name, ob):
   836         def callback(context, name, obj):  # pylint: disable=unused-argument
   777             cdict = {
   837             cdict = {
   778                 '__name__': settings.get('name'),
   838                 '__name__': settings.get('name'),
   779                 '__module__': ob.__module__,
   839                 '__module__': obj.__module__,
   780                 'permission': settings.get('permission') or ob.edit_permission
   840                 'permission': settings.get('permission') or obj.edit_permission
   781             }
   841             }
   782 
   842 
   783             # Set form's AJAX handler
   843             # Set form's AJAX handler
   784             ob.ajax_handler = settings.get('name')
   844             obj.ajax_handler = settings.get('name')
   785 
   845 
   786             # Create new AJAX form and register view
   846             # Create new AJAX form and register view
   787             base = settings.pop('base')
   847             base = settings.pop('base')
   788             new_class = type('AJAX' + ob.__name__, (base, ob), cdict)
   848             new_class = type('AJAX' + obj.__name__, (base, obj), cdict)
   789             try:
   849             try:
   790                 # check if current form is overriding "get_ajax_output" method
   850                 # check if current form is overriding "get_ajax_output" method
   791                 if base not in (AJAXAddForm, AJAXEditForm):  # custom base
   851                 if base not in (AJAXAddForm, AJAXEditForm):  # custom base
   792                     ob_ajax_parent, _ = ob.get_ajax_output.__qualname__.split('.')
   852                     ob_ajax_parent, _ = obj.get_ajax_output.__qualname__.split('.')
   793                     base_ajax_parent, _ = base.get_ajax_output.__qualname__.split('.')
   853                     base_ajax_parent, _ = base.get_ajax_output.__qualname__.split('.')
   794                     if (ob_ajax_parent != base_ajax_parent) and \
   854                     if (ob_ajax_parent != base_ajax_parent) and \
   795                        (ob_ajax_parent in ('AJAXForm', 'AJAXAddForm', 'AJAXEditForm')):
   855                             (ob_ajax_parent in ('AJAXForm', 'AJAXAddForm', 'AJAXEditForm')):
   796                         new_class.get_ajax_output = base.get_ajax_output
   856                         new_class.get_ajax_output = base.get_ajax_output
   797                     else:
   857                     else:
   798                         new_class.get_ajax_output = ob.get_ajax_output
   858                         new_class.get_ajax_output = obj.get_ajax_output
   799                 else:
   859                 else:
   800                     base_ajax_parent, _ = ob.get_ajax_output.__qualname__.split('.')
   860                     base_ajax_parent, _ = obj.get_ajax_output.__qualname__.split('.')
   801                     if base_ajax_parent not in ('AJAXForm', 'AJAXAddForm', 'AJAXEditForm'):  # overriden method
   861                     if base_ajax_parent not in ('AJAXForm', 'AJAXAddForm', 'AJAXEditForm'):
   802                         new_class.get_ajax_output = ob.get_ajax_output
   862                         # overriden method
       
   863                         new_class.get_ajax_output = obj.get_ajax_output
   803             except AttributeError:
   864             except AttributeError:
   804                 pass
   865                 pass
   805 
   866 
   806             if 'implementer' in settings:
   867             if 'implementer' in settings:
   807                 implementer = settings.pop('implementer')
   868                 impl = settings.pop('implementer')
   808                 if not isinstance(implementer, (list, tuple, set)):
   869                 if not isinstance(impl, (list, tuple, set)):
   809                     implementer = (implementer,)
   870                     impl = (impl,)
   810                 classImplements(new_class, *implementer)
   871                 classImplements(new_class, *impl)
   811 
   872 
   812             logger.debug('Registering AJAX view "{0}" for {1} ({2})'.format(settings.get('name'),
   873             LOGGER.debug('Registering AJAX view "{0}" for {1} ({2})'.format(
   813                                                                             str(settings.get('context', Interface)),
   874                 settings.get('name'), str(settings.get('context', Interface)), str(new_class)))
   814                                                                             str(new_class)))
   875 
   815 
   876             config = context.config.with_package(info.module)  # pylint: disable=no-member
   816             config = context.config.with_package(info.module)
       
   817             config.add_view(view=new_class, **settings)
   877             config.add_view(view=new_class, **settings)
   818 
   878 
   819         info = self.venusian.attach(wrapped, callback, category='pyams_form',
   879         info = self.venusian.attach(wrapped, callback, category='pyams_form', depth=depth + 1)
   820                                     depth=depth + 1)
   880 
   821 
   881         if info.scope == 'class':  # pylint: disable=no-member
   822         if info.scope == 'class':
       
   823             # if the decorator was attached to a method in a class, or
   882             # if the decorator was attached to a method in a class, or
   824             # otherwise executed at class scope, we need to set an
   883             # otherwise executed at class scope, we need to set an
   825             # 'attr' into the settings if one isn't already in there
   884             # 'attr' into the settings if one isn't already in there
   826             if settings.get('attr') is None:
   885             if settings.get('attr') is None:
   827                 settings['attr'] = wrapped.__name__
   886                 settings['attr'] = wrapped.__name__
   828 
   887 
   829         settings['_info'] = info.codeinfo  # fbo "action_method"
   888         settings['_info'] = info.codeinfo  # pylint: disable=no-member
   830         return wrapped
   889         return wrapped