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

Stuart Bishop zen@shangri-la.dropbear.id.au
Sat, 12 Jul 2003 02:18:47 -0400


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

Modified Files:
	meta.zcml submit.py 
Added Files:
	editwizard.pt editwizard.py 
Log Message:
Wizards generated from schemas, take I


=== Added File Zope3/src/zope/app/browser/form/editwizard.pt ===
<tal:tag condition="view/update"
/><html metal:use-macro="views/standard_macros/page">
  <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:define="status view/update"
           tal:condition="status"
           tal:content="status" />

        <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="field"><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 tal:replace="structure view/renderHidden">
          <!-- type=hidden input controls for passing state without session -->
          <input type="hidden" name="example" value="foo" />
        </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>

    </form>

  </div>

  </div>
  </body>

</html>


=== Added File Zope3/src/zope/app/browser/form/editwizard.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: editwizard.py,v 1.1 2003/07/12 06:18:40 Zen Exp $
"""

import logging
from UserDict import UserDict
from zope.interface import implements, classProvides
from zope.publisher.interfaces.browser import IBrowserPresentation
from zope.component import getAdapter
from zope.app.publisher.browser.globalbrowsermenuservice \
     import menuItemDirective, globalBrowserMenuService
from zope.configuration.action import Action
from zope.configuration.interfaces import INonEmptyDirective
from zope.configuration.interfaces import ISubdirectiveHandler
from zope.app.pagetemplate.simpleviewclass import SimpleViewClass
from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
from editview import normalize, EditViewFactory, EditView
from zope.security.checker import defineChecker, NamesChecker
from zope.app.context import ContextWrapper
from zope.component.view import provideView
from zope.app.form.utility import setUpEditWidgets, getWidgetsData
from submit import Next, Previous, Update
from zope.app.interfaces.form import WidgetsError

PaneNumber = 'CURRENT_PANE_IDX'

# TODO: Needs to be persistent aware for session (?)
class WizardStorage(dict):
    def __init__(self, fields, content):
        super(WizardStorage, self).__init__(self)
        for k in fields:
            self[k] = getattr(content,k)

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError, key

    def __setattr__(self, key, value):
        self[key] = value

class WizardEditView(EditView):

    def _setUpWidgets(self):
        adapted = getAdapter(self.context, self.schema)
        if adapted is not self.context:
            adapted = ContextWrapper(adapted, self.context, name='(adapted)')
        self.adapted = adapted

        if self.use_session:
            raise NotImplementedError, 'use_storage'
        else:
            self.storage = WizardStorage(self.fieldNames, adapted)

        # Add all our widgets as attributes on this view
        setUpEditWidgets(
            self, self.schema, names=self.fieldNames, content=self.storage
            )

    def widgets(self):
        return [getattr(self, name+'_widget') 
            for name in self.currentPane().names
            ]

    _current_pane_idx = 0

    def currentPane(self):
        return self.panes[self._current_pane_idx]

    _update_called = 0

    def update(self):
        '''
        if not pane submitted, 
            current_pane = 0
        else
            if current_pane is valid:
                if submit == 'Next':
                    current_pane += 1
                elif submit == 'Previous':
                    current_pane -= 1
                elif submit == 'Submit':
                    apply_changes()
                    return redirect(next_pane())
        assert current_pane > 0
        assert current_pane < len(self.panes())
        display current_pane
        '''
        if self._update_called:
            return
        self._update_called = 1

        # Determine the current pane
        if PaneNumber in self.request:
            self._current_pane_idx = int(self.request[PaneNumber])
            assert self._current_pane_idx >= 0
            assert self._current_pane_idx < len(self.panes)
        else:
            # First page
            self._current_pane_idx = 0
            self.errors = []
            self.label = self.currentPane().label
            self._choose_buttons()
            return

        # Validate the current pane, and set self.errors
        try:
            if self.use_session:
                names = self.currentPane().names
            else:
                names = self.fieldNames
            #data = getWidgetsData(
            #    self, self.schema, strict=False, set_missing=True, 
            #    names=names, exclude_readonly=True
            #    )
            data = getWidgetsData(
                self, self.schema, strict=True, set_missing=True, 
                names=names, exclude_readonly=True
                )
            valid = 1
            self.errors = []
        except WidgetsError, errors:
            self.errors = errors
            valid = 0
            data = getWidgetsData(
                self, self.schema, strict=False, set_missing=True,
                names=names, exclude_readonly=True, do_not_raise=True
                )
            logging.fatal('data is %r' % (data,))
            self.storage.update(data)
            for k,v in self.storage.items():
                getattr(self,k).setData(v)

        else:
            self.storage.update(data)

            if Next in self.request:
                self._current_pane_idx += 1
                assert self._current_pane_idx < len(self.panes)
            elif Previous in self.request:
                self._current_pane_idx -= 1
                assert self._current_pane_idx >= 0
            elif Update in self.request:
                self.apply_update(self.storage)

        # Set last_pane flag - last_pane always gets a submit button
        if self._current_pane_idx == len(self.panes) - 1:
            self.last_pane = True
        else:
            self.last_pane = False

        # Set the current label
        self.label = self.currentPane().label

        self._choose_buttons()

    def _choose_buttons(self):
        # TODO: show_submit should be true if all fields on every pane 
        # except the current one are valid
        self.show_submit = 1 

        self.show_next = (self._current_pane_idx < len(self.panes) - 1)

        self.show_previous = self._current_pane_idx > 0


    def renderHidden(self):
        ''' Render state as hidden fields. Also render hidden fields to 
            propagate self.storage if we are not using the session to do this.
        '''
        olist = []
        out = olist.append
        out('<input class="hiddenType" type="hidden" name="%s" value="%d" />'%(
            PaneNumber, self._current_pane_idx
            ))

        if self.use_session:
            ''.join(olist)

        current_fields = self.currentPane().names
        for k in self.fieldNames:
            if k not in current_fields:
                widget = getattr(self, k)
                out(widget.hidden())
        return ''.join(olist)


class Pane:
    def __init__(self, field_names, label):
        self.names = field_names
        self.label = label


class EditWizardDirective:

    classProvides(INonEmptyDirective)
    implements(ISubdirectiveHandler)

    def __init__(self, _context, name, schema, permission, 
                 for_=None, class_=None, template=None, layer='default',
                 menu=None, title='Edit', use_session='yes'):
        self.name = name
        self.permission = permission
        self.title = title
        self.layer = layer
        self.menu = menu

        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,))

        if menu:
            actions = menuItemDirective(
                _context, menu, for_ or schema, '@@' + name, title,
                permission=permission)
        else:
            actions = []

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

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

        self.panes = []
        self.actions = actions

    def pane(self, _context, fields, label=''):
        # TODO: Maybe accept a default of None for fields, meaning 'all 
        # remaining fields
        fields = [str(f) for f in fields.split(' ')]
        # TODO: unittest validation
        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):
        self.actions.append(
            Action(
                discriminator=(
                    'view', self.for_, self.name, IBrowserPresentation, 
                    self.layer
                    ),
                callable=EditWizardViewFactory,
                args=(
                    self.name, self.schema, self.permission, self.layer, 
                    self.panes, self.all_fields, self.template, 'editwizard.pt',
                    self.bases, self.for_, self.menu, u'', self.use_session
                    )
                )
            )
        return self.actions

def EditWizardViewFactory(name, schema, permission, layer,
                    panes, fields, template, default_template, bases, for_, 
                    menu=u'', usage=u'', use_session=True):
    # XXX What about the __implements__ of the bases?
    class_ = SimpleViewClass(template, used_for=schema, bases=bases)
    class_.schema = schema
    class_.panes = panes
    class_.fieldNames = fields
    class_.use_session = use_session

    class_.generated_form = ViewPageTemplateFile(default_template)

    class_.usage = usage or (
        menu and globalBrowserMenuService.getMenuUsage(menu))

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

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




=== Zope3/src/zope/app/browser/form/meta.zcml 1.11 => 1.12 ===
--- Zope3/src/zope/app/browser/form/meta.zcml:1.11	Mon Jun  2 17:18:05 2003
+++ Zope3/src/zope/app/browser/form/meta.zcml	Sat Jul 12 02:18:40 2003
@@ -2,6 +2,121 @@
 
   <directives namespace="http://namespaces.zope.org/browser">
 
+    <directive name="editwizard" handler=".editwizard.EditWizardDirective">
+      <description>
+        Define an automatically generated edit wizard.
+
+        The editwizard directive creates and register's a view for
+        editing an object based on a schema.
+      </description>
+
+      <attribute name="name" required="yes">
+        <description>
+        The name of the generated View
+        </description>
+      </attribute>
+
+      <attribute name="schema" required="yes">
+        <description>
+        The schema from which the edit 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.
+
+        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="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 edit form.
+
+        XXX Need to document how to extend the default.
+        </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="class" required="no">
+        <description>
+        A class to provide custom widget definitions or methods to be
+        used by a custom template.
+
+        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="menu" required="no">
+        <description>
+          The browser menu to include the edit 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="title" required="no">
+        <description>
+          The browser menu label for the edit form
+
+          This attribute defaults to "Edit".
+          </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="editform" handler=".editview.edit">
 
       <description>


=== Zope3/src/zope/app/browser/form/submit.py 1.2 => 1.3 ===
--- Zope3/src/zope/app/browser/form/submit.py:1.2	Wed Dec 25 09:12:32 2002
+++ Zope3/src/zope/app/browser/form/submit.py	Sat Jul 12 02:18:40 2003
@@ -19,4 +19,7 @@
 $Id$
 """
 
+Next = "NEXT_SUBMIT"
+Previous = "PREVIOUS_SUBMIT"
 Update = "UPDATE_SUBMIT"
+