[Checkins] SVN: z3c.form/trunk/ - Feature: An event handler for ``ActionErrorOccurred`` events is registered

Stephan Richter srichter at cosmos.phy.tufts.edu
Tue Aug 14 11:44:04 EDT 2007


Log message for revision 78810:
  - Feature: An event handler for ``ActionErrorOccurred`` events is registered
    to merge the action error into the form's error collectors, such as
    ``form.widgets.errors`` and ``form.widgets['name'].error`` (if
    applicable). It also sets the status of the form. (Thanks to Herman
    Himmelbauer, who requested the feature, for providing use cases.)
  
  - Feature: Action can now raise ``ActionExecutionError`` exceptions that will
    be handled by the framework. These errors wrap the original error. If an
    error is specific to a widget, then the widget name is passed to a special
    ``WidgetActionExecutionError`` error. (Thanks to Herman Himmelbauer, who
    requested the feature, for providing use cases.)
  
  - Feature: After an action handler has been executed, an action executed event
    is sent to the system. If the execution was successful, the event is
    ``ActionSuccessfull`` event is sent. If an action execution error was
    raised, the ``ActionErrorOccurred`` event is raised. (Thanks to Herman
    Himmelbauer, who requested the feature, for providing use cases.)
  
  

Changed:
  U   z3c.form/trunk/CHANGES.txt
  U   z3c.form/trunk/src/z3c/form/action.py
  U   z3c.form/trunk/src/z3c/form/action.txt
  U   z3c.form/trunk/src/z3c/form/configure.zcml
  U   z3c.form/trunk/src/z3c/form/form.py
  U   z3c.form/trunk/src/z3c/form/form.txt
  U   z3c.form/trunk/src/z3c/form/interfaces.py
  U   z3c.form/trunk/src/z3c/form/testing.py

-=-
Modified: z3c.form/trunk/CHANGES.txt
===================================================================
--- z3c.form/trunk/CHANGES.txt	2007-08-14 11:50:24 UTC (rev 78809)
+++ z3c.form/trunk/CHANGES.txt	2007-08-14 15:44:04 UTC (rev 78810)
@@ -5,6 +5,24 @@
 Version 1.6.0 (?/??/2007)
 -------------------------
 
+- Feature: An event handler for ``ActionErrorOccurred`` events is registered
+  to merge the action error into the form's error collectors, such as
+  ``form.widgets.errors`` and ``form.widgets['name'].error`` (if
+  applicable). It also sets the status of the form. (Thanks to Herman
+  Himmelbauer, who requested the feature, for providing use cases.)
+
+- Feature: Action can now raise ``ActionExecutionError`` exceptions that will
+  be handled by the framework. These errors wrap the original error. If an
+  error is specific to a widget, then the widget name is passed to a special
+  ``WidgetActionExecutionError`` error. (Thanks to Herman Himmelbauer, who
+  requested the feature, for providing use cases.)
+
+- Feature: After an action handler has been executed, an action executed event
+  is sent to the system. If the execution was successful, the event is
+  ``ActionSuccessfull`` event is sent. If an action execution error was
+  raised, the ``ActionErrorOccurred`` event is raised. (Thanks to Herman
+  Himmelbauer, who requested the feature, for providing use cases.)
+
 - Feature: The ``applyChanges()`` function now returns a dictionary of changes
   (grouped by interface) instead of a boolean. This allows us to generate a
   more detailed object-modified event. If no changes are applied, an empty

Modified: z3c.form/trunk/src/z3c/form/action.py
===================================================================
--- z3c.form/trunk/src/z3c/form/action.py	2007-08-14 11:50:24 UTC (rev 78809)
+++ z3c.form/trunk/src/z3c/form/action.py	2007-08-14 15:44:04 UTC (rev 78810)
@@ -22,6 +22,30 @@
 from z3c.form import interfaces, util
 
 
+class ActionEvent(object):
+    zope.interface.implements(interfaces.IActionEvent)
+
+    def __init__(self, action):
+        self.action = action
+
+    def __repr__(self):
+        return '<%s for %r>' %(self.__class__.__name__, self.action)
+
+
+class ActionErrorOccurred(ActionEvent):
+    """An event telling the system that an error occurred during action
+    execution."""
+    zope.interface.implements(interfaces.IActionErrorEvent)
+
+    def __init__(self, action, error):
+        super(ActionErrorOccurred, self).__init__(action)
+        self.error = error
+
+
+class ActionSuccessful(ActionEvent):
+    """An event signalizing that an action has been successfully executed."""
+
+
 class Action(object):
     """Action class."""
 
@@ -66,13 +90,18 @@
 
     def execute(self):
         """See z3c.form.interfaces.IActions."""
-        adapter = None
         for action in self.executedActions:
-            adapter = zope.component.queryMultiAdapter(
+            handler = zope.component.queryMultiAdapter(
                 (self.form, self.request, self.content, action),
                 interface=interfaces.IActionHandler)
-            if adapter is not None:
-                return adapter()
+            if handler is not None:
+                try:
+                    result = handler()
+                except interfaces.ActionExecutionError, error:
+                    zope.event.notify(ActionErrorOccurred(action, error))
+                else:
+                    zope.event.notify(ActionSuccessful(action))
+                    return result
 
     def __repr__(self):
         return '<%s %r>' % (self.__class__.__name__, self.__name__)

Modified: z3c.form/trunk/src/z3c/form/action.txt
===================================================================
--- z3c.form/trunk/src/z3c/form/action.txt	2007-08-14 11:50:24 UTC (rev 78809)
+++ z3c.form/trunk/src/z3c/form/action.txt	2007-08-14 15:44:04 UTC (rev 78810)
@@ -185,3 +185,55 @@
   >>> request = TestRequest(form={'cancel': 'Cancel'})
   >>> manager.request = apply.request = cancel.request = request
   >>> manager.execute()
+
+Further, when a handler is successfully executed, an event is sent out, so
+let's register an event handler:
+
+  >>> eventlog = []
+  >>> @zope.component.adapter(interfaces.IActionEvent)
+  ... def handleEvent(event):
+  ...     eventlog.append(event)
+
+  >>> zope.component.provideHandler(handleEvent)
+
+Let's now execute the "Apply" action again:
+
+  >>> request = TestRequest(form={'apply': 'Apply'})
+  >>> manager.request = apply.request = cancel.request = request
+  >>> manager.execute()
+  successfully applied
+
+  >>> eventlog[-1]
+  <ActionSuccessful for <Action 'apply' u'Apply'>>
+
+Action handlers, however, can also raise action errors. These action errors
+are caught and an event is created notifying the system of the problem. The
+error is not further propagated. Other errors are not handled by the system to
+avoid hiding real failures of the code.
+
+Let's see how action errors can be used by implementing a handler for the
+cancel action:
+
+  >>> class ErrorActionHandler(action.ActionHandlerBase):
+  ...     zope.component.adapts(
+  ...         None, TestRequest, None, util.getSpecification(cancel))
+  ...     def __call__(self):
+  ...         raise interfaces.ActionExecutionError(
+  ...             zope.interface.Invalid('Something went wrong'))
+
+  >>> zope.component.provideAdapter(ErrorActionHandler)
+
+As you can see, the action execution error wraps some other execption, in this
+case a simple invalid error.
+
+Executing the "Cancel" action now produces the action error event:
+
+  >>> request = TestRequest(form={'cancel': 'Cancel'})
+  >>> manager.request = apply.request = cancel.request = request
+  >>> manager.execute()
+
+  >>> eventlog[-1]
+  <ActionErrorOccurred for <Action 'cancel' u'Cancel'>>
+
+  >>> eventlog[-1].error
+  <ActionExecutionError wrapping <zope.interface.exceptions.Invalid ...>>

Modified: z3c.form/trunk/src/z3c/form/configure.zcml
===================================================================
--- z3c.form/trunk/src/z3c/form/configure.zcml	2007-08-14 11:50:24 UTC (rev 78809)
+++ z3c.form/trunk/src/z3c/form/configure.zcml	2007-08-14 15:44:04 UTC (rev 78810)
@@ -89,6 +89,9 @@
   <adapter
       factory=".button.ButtonActionHandler"
       />
+  <subscriber
+      handler=".form.handleActionError"
+      />
 
   <!-- Error Views -->
   <adapter

Modified: z3c.form/trunk/src/z3c/form/form.py
===================================================================
--- z3c.form/trunk/src/z3c/form/form.py	2007-08-14 11:50:24 UTC (rev 78809)
+++ z3c.form/trunk/src/z3c/form/form.py	2007-08-14 15:44:04 UTC (rev 78810)
@@ -66,6 +66,34 @@
             f_locals['handlers'] += getattr(arg, 'handlers', button.Handlers())
 
 
+ at zope.component.adapter(interfaces.IActionErrorEvent)
+def handleActionError(event):
+    # Only react to the event, if the form is a standard form.
+    if not (interfaces.IFormAware.providedBy(event.action) and
+            interfaces.IForm.providedBy(event.action.form)):
+        return
+    # If the error was widget-specific, look up the widget.
+    widget = None
+    if isinstance(event.error, interfaces.WidgetActionExecutionError):
+        widget = event.action.form.widgets[event.error.widgetName]
+    # Create an error view for the error.
+    action = event.action
+    form = action.form
+    errorView = zope.component.getMultiAdapter(
+        (event.error.error, action.request, widget,
+         getattr(widget, 'field', None), form, form.getContent()),
+        interfaces.IErrorViewSnippet)
+    errorView.update()
+    # Assign the error view to all necessary places.
+    if widget:
+        widget.error = errorView
+    form.widgets.errors += (errorView,)
+    # If the form supports the ``formErrorsMessage`` attribute, then set the
+    # status to it.
+    if hasattr(form, 'formErrorsMessage'):
+        form.status = form.formErrorsMessage
+
+
 class BaseForm(browser.BrowserPage):
     """A base form."""
     zope.interface.implements(interfaces.IForm,

Modified: z3c.form/trunk/src/z3c/form/form.txt
===================================================================
--- z3c.form/trunk/src/z3c/form/form.txt	2007-08-14 11:50:24 UTC (rev 78809)
+++ z3c.form/trunk/src/z3c/form/form.txt	2007-08-14 15:44:04 UTC (rev 78810)
@@ -1158,7 +1158,7 @@
   ...     fields = field.Fields(IPerson).select('name')
   ...
   ...     @button.buttonAndHandler(u'Apply')
-  ...     def handleApply(self, form):
+  ...     def handleApply(self, action):
   ...         print 'success'
 
   >>> BaseForm.fields.keys()
@@ -1174,7 +1174,7 @@
   ...     fields = field.Fields(IPerson).select('gender')
   ...
   ...     @button.buttonAndHandler(u'Cancel')
-  ...     def handleCancel(self, form):
+  ...     def handleCancel(self, action):
   ...         print 'cancel'
 
   >>> DerivedForm.fields.keys()
@@ -1195,7 +1195,7 @@
   ...     fields += field.Fields(IPerson).select('gender')
   ...
   ...     @button.buttonAndHandler(u'Cancel')
-  ...     def handleCancel(self, form):
+  ...     def handleCancel(self, action):
   ...         print 'cancel'
 
   >>> DerivedForm.fields.keys()
@@ -1216,7 +1216,7 @@
   ...     fields += field.Fields(IPerson).select('gender')
   ...
   ...     @button.buttonAndHandler(u'Cancel')
-  ...     def handleCancel(self, form):
+  ...     def handleCancel(self, action):
   ...         print 'cancel'
 
   >>> DerivedForm.fields.keys()
@@ -1237,7 +1237,7 @@
   ...     fields += field.Fields(IPerson).select('gender')
   ...
   ...     @button.buttonAndHandler(u'Cancel')
-  ...     def handleCancel(self, form):
+  ...     def handleCancel(self, action):
   ...         print 'cancel'
 
   >>> DerivedForm.fields.keys()
@@ -1320,3 +1320,77 @@
          name="form.widgets.age" class="hidden-widget"
          value="29" />
   ...
+
+
+Actions with Errors
+-------------------
+
+Even though the data might be validated correctly, it sometimes happens that
+data turns out to be invalid while the action is executed. In those cases a
+special action execution error can be raised that wraps the original error.
+
+  >>> class PersonAddForm(form.AddForm):
+  ...
+  ...     fields = field.Fields(IPerson).select('id')
+  ...
+  ...     @button.buttonAndHandler(u'Check')
+  ...     def handleCheck(self, action):
+  ...         data, errors = self.extractData()
+  ...         if data['id'] in self.getContent():
+  ...             raise interfaces.WidgetActionExecutionError(
+  ...                 'id', zope.interface.Invalid('Id already exists'))
+
+In this case the action execution error is specific to a widget. The framework
+will attach a proper error view to the widget and the widget manager:
+
+  >>> request = TestRequest(form={
+  ...     'form.widgets.id': u'srichter',
+  ...     'form.buttons.check': u'Check'}
+  ...     )
+
+  >>> addForm = PersonAddForm(root, request)
+  >>> addForm.update()
+
+  >>> addForm.widgets.errors
+  (<InvalidErrorViewSnippet for Invalid>,)
+  >>> addForm.widgets['id'].error
+  <InvalidErrorViewSnippet for Invalid>
+  >>> addForm.status
+  u'There were some errors.'
+
+If the error is non-widget specific, then we can simply use the generic action
+execution error:
+
+  >>> class PersonAddForm(form.AddForm):
+  ...
+  ...     fields = field.Fields(IPerson).select('id')
+  ...
+  ...     @button.buttonAndHandler(u'Check')
+  ...     def handleCheck(self, action):
+  ...         raise interfaces.ActionExecutionError(
+  ...             zope.interface.Invalid('Some problem occurred.'))
+
+Let's have a look at the result:
+
+  >>> addForm = PersonAddForm(root, request)
+  >>> addForm.update()
+
+  >>> addForm.widgets.errors
+  (<InvalidErrorViewSnippet for Invalid>,)
+  >>> addForm.status
+  u'There were some errors.'
+
+__Note__:
+
+  The action execution errors are connected to the form via an event
+  listener called ``handlerActionError``. This event listener listens for
+  ``IActionErrorEvent`` events. If the event is called for an action associated
+  with a form, the listener does its work as seen above. If the action is not
+  coupled to a form, then event listener does nothing:
+
+    >>> from z3c.form import action
+
+    >>> cancel = action.Action(request, u'Cancel')
+    >>> event = action.ActionErrorOccurred(cancel, ValueError(3))
+
+    >>> form.handleActionError(event)

Modified: z3c.form/trunk/src/z3c/form/interfaces.py
===================================================================
--- z3c.form/trunk/src/z3c/form/interfaces.py	2007-08-14 11:50:24 UTC (rev 78809)
+++ z3c.form/trunk/src/z3c/form/interfaces.py	2007-08-14 15:44:04 UTC (rev 78810)
@@ -522,6 +522,25 @@
 
 # ----[ Actions ]------------------------------------------------------------
 
+class ActionExecutionError(Exception):
+    """An error that occurs during the execution of an action handler."""
+
+    def __init__(self, error):
+        self.error = error
+
+    def __repr__(self):
+        return '<%s wrapping %r>' %(self.__class__.__name__, self.error)
+
+
+class WidgetActionExecutionError(ActionExecutionError):
+    """An action execution error that occurred due to a widget value being
+    incorrect."""
+
+    def __init__(self, widgetName, error):
+        ActionExecutionError.__init__(self, error)
+        self.widgetName = widgetName
+
+
 class IAction(zope.interface.Interface):
     """Action"""
 
@@ -544,6 +563,25 @@
     """Action handler."""
 
 
+class IActionEvent(zope.interface.Interface):
+    """An event specific for an action."""
+
+    action = zope.schema.Object(
+        title=_('Action'),
+        description=_('The action for which the event is created.'),
+        schema=IAction,
+        required=True)
+
+
+class IActionErrorEvent(IActionEvent):
+    """An action event that is created when an error occurred."""
+
+    error = zope.schema.Field(
+        title=_('Error'),
+        description=_('The error that occurred during the action.'),
+        required=True)
+
+
 class IActions(IManager):
     """A action manager"""
 
@@ -554,9 +592,14 @@
         """Setup actions."""
 
     def execute():
-        """Exceute actions."""
+        """Exceute actions.
 
+        If an action execution error is raised, the system is notified using
+        the action occurred error; on the other hand, if successful, the
+        action successfull event is sent to the system.
+        """
 
+
 class IButton(zope.schema.interfaces.IField):
     """A button in a form."""
 

Modified: z3c.form/trunk/src/z3c/form/testing.py
===================================================================
--- z3c.form/trunk/src/z3c/form/testing.py	2007-08-14 11:50:24 UTC (rev 78809)
+++ z3c.form/trunk/src/z3c/form/testing.py	2007-08-14 15:44:04 UTC (rev 78810)
@@ -28,7 +28,7 @@
 from zope.app.testing import setup
 
 from z3c.form import browser, button, converter, datamanager, error, field
-from z3c.form import interfaces, term, validator, widget
+from z3c.form import form, interfaces, term, validator, widget
 from z3c.form.browser import radio, select, text
 
 
@@ -143,6 +143,8 @@
     zope.component.provideAdapter(button.ButtonActions)
     # Adapter to use form.handlers to generate handle actions
     zope.component.provideAdapter(button.ButtonActionHandler)
+    # Subscriber handling action execution error events
+    zope.component.provideHandler(form.handleActionError)
     # Error View(s)
     zope.component.provideAdapter(error.ErrorViewSnippet)
     zope.component.provideAdapter(error.InvalidErrorViewSnippet)



More information about the Checkins mailing list