[Zope3-checkins] CVS: Zope3/src/zope/app/browser/form - addwizard.pt:1.1 addwizard.py:1.1 editwizard.pt:1.2 editwizard.py:1.3 meta.zcml:1.13

Stuart Bishop zen@shangri-la.dropbear.id.au
Sun, 13 Jul 2003 00:06:10 -0400


Update of /cvs-repository/Zope3/src/zope/app/browser/form
In directory cvs.zope.org:/tmp/cvs-serv7897/src/zope/app/browser/form

Modified Files:
	editwizard.pt editwizard.py meta.zcml 
Added Files:
	addwizard.pt addwizard.py 
Log Message:
Wizards take-III - now with added addwizard and 100% more unit tests

=== Added File Zope3/src/zope/app/browser/form/addwizard.pt ===
<tal:b condition="view/update"/><html metal:use-macro="context/@@standard_macros/dialog">
  <body>
  <div metal:fill-slot="body">

  <div metal:define-macro="body">

    <form action="." tal:attributes="action request/URL" method="post"
          enctype="multipart/form-data">

      <div metal:define-macro="formbody">

        <h3 tal:condition="view/label"
            tal:content="view/label"
            metal:define-slot="heading"
            >Edit something</h3>

        <p tal:condition="view/feedback" tal:content="view/feedback">
          A feedback message to the user
        </p>

        <div tal:condition="view/errors">
           <ul>
              <li tal:repeat="error view/errors">
                 <strong tal:content="error/__class__" i18n:translate="">
                    Error Type</strong>:
                 <span tal:content="error">Error text</span>
              </li>
           </ul>
        </div>

        <div metal:define-slot="extra_info" tal:replace="nothing">
        </div>

        <div class="row" metal:define-slot="extra_top" tal:replace="nothing">
            <div class="label">Extra top</div>
            <div class="label"><input type="text" style="width:100%" /></div>
        </div>
        <div class="row"
             metal:define-macro="widget_rows" tal:repeat="widget view/widgets"
             tal:content="structure widget/row">
            <div class="label">Name</div>
            <div class="field"><input type="text" style="width:100%" /></div>
        </div>
        <div class="row"
             metal:define-slot="extra_bottom" tal:replace="nothing">
            <div class="label">Extra bottom</div>
            <div class="field"><input type="text" style="width:100%" /></div>
        </div>

      </div>

      <div class="row">
        <div class="controls">
          <!-- <input type="submit" value="Refresh" 
              i18n:attributes="value refresh-button" /> -->
          <input tal:condition="view/show_previous"
            type="submit" name="PREVIOUS_SUBMIT" value="Previous"
            i18n:attributes="value previous-button" /> 
          <input tal:condition="view/show_submit"
            type="submit" name="UPDATE_SUBMIT" value="Submit" 
            i18n:attributes="value submit-button"/>
          <input tal:condition="view/show_next"
            type="submit" name="NEXT_SUBMIT" value="Next"
            i18n:attributes="value next-button" /> 
        </div>
      </div>

      <div tal:replace="structure view/renderHidden">
        <!-- type=hidden input controls for passing state without session -->
        <input type="hidden" name="example" value="foo" />
      </div>

    </form>

  </div>

  </div>
  </body>

</html>


=== Added File Zope3/src/zope/app/browser/form/addwizard.py ===
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (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.
#
##############################################################################
"""
$Id: addwizard.py,v 1.1 2003/07/13 04:05:58 Zen Exp $
"""

import sys
import logging

from zope.interface import implements, classProvides
from zope.schema.interfaces import ValidationError
from zope.app.event import publish
from zope.app.event.objectevent import ObjectCreatedEvent
from zope.app.interfaces.form import WidgetsError
from zope.app.form.utility import setUpWidgets, getWidgetsData
from zope.configuration.action import Action
from zope.configuration.interfaces import INonEmptyDirective
from zope.configuration.interfaces import ISubdirectiveHandler
from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
from zope.security.checker import defineChecker, NamesChecker
from zope.component import getAdapter
from zope.component.view import provideView
from zope.publisher.interfaces.browser import IBrowserPresentation
from zope.app.pagetemplate.simpleviewclass import SimpleViewClass
from zope.app.browser.form.submit import Update
from zope.app.browser.form.editview import EditView, normalize
from zope.app.publisher.browser.globalbrowsermenuservice \
     import menuItemDirective
from editwizard import EditWizardView, Pane, WizardStorage

class AddWizardView(EditWizardView):
    """Multi-page add-view base class.

    Subclasses should provide a schema attribute defining the schema
    to be edited.
    """

    def _setUpWidgets(self):
        if self.use_session:
            # Need session for File upload fields
            raise NotImplementedError, 'Need a working ISessionDataManager'
        else:
            self.storage = WizardStorage(self.fieldNames, None)

        setUpWidgets(self, self.schema, names=self.fieldNames)

    def create(self, *args, **kw):
        """Do the actual instantiation.
        """
        return self._factory(*args, **kw)

    #def createAndAdd(self, data):
    def apply_update(self, data):
        """Add the desired object using the data in the data argument.

        The data argument is a dictionary with the data entered in the form.

        Issues a redirect to context.nextURL()

        Returns False, as per editview.apply_update
        """

        # Cut & Paste from add.py
        
        args = []
        for name in self._arguments:
            args.append(data[name])

        kw = {}
        for name in self._keyword_arguments:
            if name in data:
                kw[str(name)] = data[name]

        content = self.create(*args, **kw)
        adapted = getAdapter(content, self.schema, context=self.context)

        errors = []

        for name in self._set_before_add:
            if name in data:
                field = self.schema[name]
                try:
                    field.set(adapted, data[name])
                except ValidationError:
                    errors.append(sys.exc_info()[1])

        if errors:
            raise WidgetsError(*errors)

        publish(self.context, ObjectCreatedEvent(content))

        content = self.add(content)

        adapted = getAdapter(content, self.schema)

        for name in self._set_after_add:
            if name in data:
                field = self.schema[name]
                try:
                    field.set(adapted, data[name])
                except ValidationError:
                    errors.append(sys.exc_info()[1])

        if errors:
            raise WidgetsError(*errors)

        #return content
        self.request.response.redirect(self.context.nextURL())
        return False

    def add(self, content):
        # Cut & Paste from add.py
        return self.context.add(content)

    def nextURL(self):
        # Cut & Paste from add.py
        return self.context.nextURL()


def AddWizardViewFactory(
    name, schema, permission, layer, panes, fields,
    template, default_template, bases, for_, content_factory, arguments,
    keyword_arguments, set_before_add, set_after_add, use_session=True):

    class_  = SimpleViewClass(template, used_for = schema, bases = bases)

    class_.schema = schema
    class_.panes = panes
    class_.fieldNames = fields
    class_._factory = content_factory
    class_._arguments = arguments
    class_._keyword_arguments = keyword_arguments
    class_._set_before_add = set_before_add
    class_._set_after_add = set_after_add
    class_.use_session = use_session

    class_.generated_form = ViewPageTemplateFile(default_template)

    defineChecker(class_,
                  NamesChecker(
                    ("__call__", "__getitem__", "browserDefault"),
                    permission,
                    )
                  )

    provideView(for_, name, IBrowserPresentation, class_, layer)


class AddWizardDirective:

    classProvides(INonEmptyDirective)
    implements(ISubdirectiveHandler)

    def __init__(
        self, _context, name, schema, permission, content_factory='',
        layer='default', template=None, 
        for_='zope.app.interfaces.container.IAdding', class_=None, 
        arguments='',keyword_arguments='', set_before_add='', set_after_add='',
        menu=None, title=None, use_session='yes'
        ):

        self.name = name
        self.permission = permission
        self.title = title
        self.layer = layer
        self.menu = menu
        self.set_before_add = set_before_add
        self.set_after_add = set_after_add

        if use_session == 'yes':
            self.use_session = True
        elif use_session == 'no':
            self.use_session = False
        else:
            raise ValueError('Invalid value %r for use_session'%(use_session,))

        # Handle menu attrs. We do this now to rather than later becaise
        # menuItemDirective expects a dotted name for for_. 
        if menu or title:
            if (not menu) or (not title):
                raise ValueError("If either menu or title are specified, "
                                "they must both be specified")
            actions = menuItemDirective(
                _context, menu, for_, '@@' + name, title,
                permission=permission)
        else:
            actions = []

        self.content_factory = (
            content_factory and _context.resolve(content_factory) or None
            )

        schema, for_, bases, template, fields = normalize(
            _context, schema, for_, class_, template, 'addwizard.pt', 
            fields=None, omit=None, view=AddWizardView
            )

        self.schema = schema
        self.for_ = for_
        self.bases = bases
        self.template = template
        self.all_fields = fields

        self.arguments = arguments
        self.keyword_arguments = keyword_arguments
 
        self.panes = []
        self.actions = actions

    def pane(self, _context, fields, label=''):
        fields = [str(f) for f in fields.split(' ')]
        for f in fields:
            if f not in self.all_fields:
                raise ValueError(
                    'Field name is not in schema', 
                    name, self.schema
                    )
        self.panes.append(Pane(fields, label))
        return []

    def __call__(self):

        # Argument code Cut & Paste from add.py
        leftover = []
        for pane in self.panes:
            leftover.extend(pane.names)
        fields = leftover[:]

        arguments = self.arguments
        if arguments:
            arguments = arguments.split()
            missing = [n for n in arguments if n not in fields]
            if missing:
                raise ValueError("Some arguments are not included in the form",
                                missing)
            optional = [n for n in arguments if not self.schema[n].required]
            if optional:
                raise ValueError("Some arguments are optional, use"
                                " keyword_arguments for them",
                                optional)
            leftover = [n for n in leftover if n not in arguments]

        keyword_arguments = self.keyword_arguments
        if keyword_arguments:
            keyword_arguments = keyword_arguments.split()
            missing = [n for n in keyword_arguments if n not in fields]
            if missing:
                raise ValueError(
                    "Some keyword_arguments are not included in the form",
                    missing)
            leftover = [n for n in leftover if n not in keyword_arguments]

        set_before_add = self.set_before_add
        if set_before_add:
            set_before_add = set_before_add.split()
            missing = [n for n in set_before_add if n not in fields]
            if missing:
                raise ValueError(
                    "Some set_before_add are not included in the form",
                    missing)
            leftover = [n for n in leftover if n not in set_before_add]

        set_after_add = self.set_after_add
        if set_after_add:
            set_after_add = set_after_add.split()
            missing = [n for n in set_after_add if n not in fields]
            if missing:
                raise ValueError(
                    "Some set_after_add are not included in the form",
                    missing)
            leftover = [n for n in leftover if n not in set_after_add]

            set_after_add += leftover

        else:

            set_after_add = leftover

        self.actions.append(
            Action(
                discriminator=(
                    'view', self.for_, self.name, IBrowserPresentation, 
                    self.layer
                    ),
                callable=AddWizardViewFactory,
                args=(
                    self.name, self.schema, self.permission, self.layer, 
                    self.panes, self.all_fields, self.template, 'editwizard.pt',
                    self.bases, self.for_, self.content_factory, arguments,
                    keyword_arguments, self.set_before_add, self.set_after_add,
                    self.use_session,
                    )
                )
            )
        return self.actions




=== Zope3/src/zope/app/browser/form/editwizard.pt 1.1 => 1.2 ===
--- Zope3/src/zope/app/browser/form/editwizard.pt:1.1	Sat Jul 12 02:18:40 2003
+++ Zope3/src/zope/app/browser/form/editwizard.pt	Sun Jul 13 00:05:58 2003
@@ -6,8 +6,7 @@
   <div metal:define-macro="body">
 
     <form action="." tal:attributes="action request/URL" method="POST"
-          enctype="multipart/form-data"
-          >
+          enctype="multipart/form-data" >
 
       <div metal:define-macro="formbody">
 
@@ -16,9 +15,7 @@
             metal:define-slot="heading"
             >Edit something</h3>
 
-        <p tal:define="status view/update"
-           tal:condition="status"
-           tal:content="status" />
+        <p tal:condition="view/feedback" tal:content="view/feedback" />
 
         <div tal:condition="view/errors">
            <ul>


=== Zope3/src/zope/app/browser/form/editwizard.py 1.2 => 1.3 ===
--- Zope3/src/zope/app/browser/form/editwizard.py:1.2	Sat Jul 12 20:36:15 2003
+++ Zope3/src/zope/app/browser/form/editwizard.py	Sun Jul 13 00:05:58 2003
@@ -15,6 +15,7 @@
 $Id$
 """
 
+import logging
 from UserDict import UserDict
 from zope.interface import implements, classProvides
 from zope.publisher.interfaces.browser import IBrowserPresentation
@@ -34,6 +35,7 @@
 from zope.app.interfaces.form import WidgetInputError
 from submit import Next, Previous, Update
 from zope.app.interfaces.form import WidgetsError
+from zope.i18n import MessageIDFactory
 
 PaneNumber = 'CURRENT_PANE_IDX'
 
@@ -41,8 +43,9 @@
 class WizardStorage(dict):
     def __init__(self, fields, content):
         super(WizardStorage, self).__init__(self)
-        for k in fields:
-            self[k] = getattr(content,k)
+        if content:
+            for k in fields:
+                self[k] = getattr(content,k)
 
     def __getattr__(self, key):
         try:
@@ -54,7 +57,7 @@
         self[key] = value
 
 
-class WizardEditView(EditView):
+class EditWizardView(EditView):
 
     def _setUpWidgets(self):
         adapted = getAdapter(self.context, self.schema)
@@ -86,6 +89,9 @@
 
     _update_called = 0
 
+    # Message rendered at the top of the form, probably set by update()
+    feedback = u'' 
+
     def update(self):
         '''
         Called before rendering each pane. It is responsible
@@ -136,7 +142,10 @@
                 self._current_pane_idx -= 1
                 assert self._current_pane_idx >= 0
             elif Update in self.request:
-                self.apply_update(self.storage)
+                if self.apply_update(self.storage):
+                    self.feedback = _(u'No changes to save')
+                else:
+                    self.feedback = _(u'Changes saved')
 
         # Set last_pane flag - last_pane always gets a submit button
         if self._current_pane_idx == len(self.panes) - 1:
@@ -158,7 +167,7 @@
         try:
             for k in self.fieldNames:
                 if k not in self.currentPane().names:
-                    getattr(self, k).getData(1)
+                    debug = getattr(self, k).getData(1)
             self.show_submit = 1 
         except WidgetInputError,x:
             self.show_submit = 0
@@ -195,6 +204,9 @@
                     out(widget.hidden())
             return ''.join(olist)
 
+    def done(self):
+        ''' Called after changes have been saved '''
+
 
 class Pane:
     # TODO: Add more funky stuff to each pane, such as a validator
@@ -233,7 +245,7 @@
 
         schema, for_, bases, template, fields = normalize(
             _context, schema, for_, class_, template, 'editwizard.pt', 
-            fields=None, omit=None, view=WizardEditView
+            fields=None, omit=None, view=EditWizardView
             )
 
         self.schema = schema


=== Zope3/src/zope/app/browser/form/meta.zcml 1.12 => 1.13 ===
--- Zope3/src/zope/app/browser/form/meta.zcml:1.12	Sat Jul 12 02:18:40 2003
+++ Zope3/src/zope/app/browser/form/meta.zcml	Sun Jul 13 00:05:58 2003
@@ -2,9 +2,171 @@
 
   <directives namespace="http://namespaces.zope.org/browser">
 
+    <directive name="addwizard"
+       handler="zope.app.browser.form.addwizard.AddWizardDirective">
+
+      <description>
+        Define an automatically generated add wizard (multi-page form)
+
+        The addwizard directive creates and register's a view for
+        adding an object based on a schema.
+
+        Adding an object is a bit trickier than editing an object,
+        because the object the schema applies to isn't available when
+        forms are being rendered.  The addform directive provides an
+        customization interface to overcome this difficulty.
+
+        See zope.app.interfaces.browser.form.IAddFormCustomization.
+      </description>
+
+      <attribute name="name" required="yes">
+        <description>
+          The name of the generated add view.
+        </description>
+      </attribute>
+
+      <attribute name="schema" required="yes">
+        <description>
+        The schema from which the add form is generated.
+
+        A schema is an interface that includes fields.
+        </description>
+      </attribute>
+
+      <attribute name="for" required="no">
+        <description>
+        The interface this page (view) applies to.
+
+        The view will be for all objects that implement this interface.
+
+        zope.app.interfaces.container.IAdding is used if this
+        attribute isn't specified.  If this attribute is specified,
+        then either the named interface must extend IAdding or a class
+        attribute must be used to supply a class implements certain
+        methods described in
+        zope.app.interfaces.browser.form.IAddFormCustomization.
+
+        The schema is used if the for attribute is not specified.
+
+        If the for attribute is specified, then the objects views must
+        implement or be adaptable to the schema.
+        </description>
+      </attribute>
+
+      <attribute name="layer" required="no">
+        <description>
+          The layer the view is in.
+
+          A skin is composed of layers. It is common to put skin specific
+          views in a layer named after the skin. If the 'layer' attribute
+          is not supplied, it defaults to 'default'.
+        </description>
+      </attribute>
+
+      <attribute name="permission" required="yes">
+        <description>
+          The permission needed to use the view. 
+        </description>
+      </attribute>
+
+      <attribute name="template" required="no">
+        <description>
+          An alternate template to use for the add form.
+
+          XXX Need to document how to extend the default.
+        </description>
+      </attribute>
+
+      <attribute name="class" required="no">
+        <description>
+          A class to provide custom widget definitions or methods to be
+          used by a custom template.
+
+          This class can override methods defined in IAddFormCustomization.
+
+          This class is used as a mix-in class. As a result, it needn't
+          subclass any special classes, such as BrowserView.
+        </description>
+      </attribute>
+
+      <attribute name="content_factory" required="no">
+        <description>
+          The dotted name of an object to call to create new content objects.
+
+          This attribute isn't used if a class is specified that
+          implements createAndAdd.
+          </description>
+        </attribute>
+
+      <attribute name="arguments" required="no">
+        <description>
+          A list of field names to supply as positional arguments to
+          the factory.
+        </description>
+      </attribute>
+
+      <attribute name="keyword_arguments" required="no">
+        <description>
+          A list of field names to supply as keyword arguments to
+          the factory.
+        </description>
+      </attribute>
+
+      <attribute name="set_before_add" required="no">
+        <description>
+          A list of fields to be assigned to the newly created object
+          before it is added.
+        </description>
+      </attribute>
+
+      <attribute name="set_after_add" required="no">
+        <description>
+          A list of fields to be assigned to the newly created object
+          after it is added.
+        </description>
+      </attribute>
+
+      <attribute name="menu" required="no">
+        <description>
+          The browser menu to include the add form in.
+
+          Many views are included in menus. It's convenient to name
+          the menu in the page directive, rather than having to give a
+          separate menuItem directive.
+        </description>
+      </attribute>
+
+      <attribute name="use_session" required="no">
+        <description>
+          If 'no', hidden input controls are used to maintain state
+          between panes in the wizard. Only simple data types can
+          be propogated with this method.
+
+          Defaults to 'yes'.
+        </description>
+      </attribute>
+
+      <subdirective name="pane" 
+        description="Define a Pane (page) of the wizard">
+        <attribute name="fields" required="yes">
+          <description>
+            The fields and the order in which to display them.  If this
+            is not specified, all schema fields will be displayed in the
+            order specified in the schema itself.
+          </description>
+        </attribute>
+        <attribute name="label" required="no">
+          <description>
+            The label used as the heading on this pane
+          </description>
+        </attribute>
+      </subdirective>
+
+    </directive>
+
     <directive name="editwizard" handler=".editwizard.EditWizardDirective">
       <description>
-        Define an automatically generated edit wizard.
+        Define an automatically generated edit wizard (multi-page form).
 
         The editwizard directive creates and register's a view for
         editing an object based on a schema.