[Checkins] SVN: z3c.multiform/Sandbox/src/z3c/multiform/ Started a common place for the upcoming z3c package multiform.

Stefan Martin s.martin at iwm-kmrc.de
Sat Apr 15 16:53:52 EDT 2006


Log message for revision 67065:
  Started a common place for the upcoming z3c package multiform.

Changed:
  A   z3c.multiform/Sandbox/src/z3c/multiform/README.txt
  A   z3c.multiform/Sandbox/src/z3c/multiform/TODO.txt
  A   z3c.multiform/Sandbox/src/z3c/multiform/__init__.py
  A   z3c.multiform/Sandbox/src/z3c/multiform/actions.txt
  A   z3c.multiform/Sandbox/src/z3c/multiform/configure.zcml
  A   z3c.multiform/Sandbox/src/z3c/multiform/container/
  A   z3c.multiform/Sandbox/src/z3c/multiform/container/__init__.py
  A   z3c.multiform/Sandbox/src/z3c/multiform/container/configure.zcml
  A   z3c.multiform/Sandbox/src/z3c/multiform/container/container.txt
  A   z3c.multiform/Sandbox/src/z3c/multiform/container/grid.pt
  A   z3c.multiform/Sandbox/src/z3c/multiform/container/griditem.pt
  A   z3c.multiform/Sandbox/src/z3c/multiform/container/interfaces.py
  A   z3c.multiform/Sandbox/src/z3c/multiform/container/location.py
  A   z3c.multiform/Sandbox/src/z3c/multiform/container/tests.py
  A   z3c.multiform/Sandbox/src/z3c/multiform/container/views.py
  A   z3c.multiform/Sandbox/src/z3c/multiform/grid.pt
  A   z3c.multiform/Sandbox/src/z3c/multiform/gridform.py
  A   z3c.multiform/Sandbox/src/z3c/multiform/gridform.txt
  A   z3c.multiform/Sandbox/src/z3c/multiform/griditem.pt
  A   z3c.multiform/Sandbox/src/z3c/multiform/interfaces.py
  A   z3c.multiform/Sandbox/src/z3c/multiform/multiform.py
  A   z3c.multiform/Sandbox/src/z3c/multiform/resources/
  A   z3c.multiform/Sandbox/src/z3c/multiform/resources/sort_down.gif
  A   z3c.multiform/Sandbox/src/z3c/multiform/resources/sort_up.gif
  A   z3c.multiform/Sandbox/src/z3c/multiform/selection.py
  A   z3c.multiform/Sandbox/src/z3c/multiform/selections.txt
  A   z3c.multiform/Sandbox/src/z3c/multiform/sort.py
  A   z3c.multiform/Sandbox/src/z3c/multiform/tests.py
  A   z3c.multiform/Sandbox/src/z3c/multiform/z3c.multiform-configure.zcml

-=-
Added: z3c.multiform/Sandbox/src/z3c/multiform/README.txt
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/README.txt	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/README.txt	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,88 @@
+===================
+ Multiform Package
+===================
+
+This Package provides an API to handle multiple forms with matching
+form fields on multiple items. The creation of multiforms is derived
+from the Form class of the formlib package.
+
+    >>> from zope import interface, schema
+    >>> from zope.app.location.interfaces import ILocation
+
+    >>> class IOrder(interface.Interface):
+    ...     identifier = schema.Int(title=u"Identifier", readonly=True)
+    ...     name = schema.TextLine(title=u"Name")
+    >>> class Order:
+    ...     interface.implements(IOrder,ILocation)
+    ...
+    ...     def __init__(self, identifier, name=''):
+    ...         self.identifier = identifier
+    ...         self.name = name
+    ...         self.__name__ = name
+
+    >>> orderMapping = dict([(str(k),Order(k,name='n%s'%k)) for k in range(5)])
+
+
+Let us create a new multiform class which should display IOrder objects.
+
+    >>> from zope.formlib import form
+    >>> from z3c.multiform.multiform import MultiFormBase,ItemFormBase
+
+    >>> class OrderForm(ItemFormBase):
+    ...     form_fields = form.Fields(IOrder,omit_readonly=False,
+    ...     render_context=True)
+    ...     def __call__(self, ignore_request=False):
+    ...         widgets = form.setUpWidgets(
+    ...             self.form_fields, self.prefix, self.context, self.request,
+    ...             ignore_request=ignore_request)
+    ...         return '\n<div>%s</div>\n' % '</div><div>'.join([w() for w in
+    ...     widgets])
+
+
+    >>> class OrdersForm(MultiFormBase):
+    ...     
+    ...     itemFormFactory = OrderForm
+    ...     def __call__(self, ignore_request=False):
+    ...         self.setUpWidgets()
+    ...         res = u''
+    ...         names = sorted(self.subForms.keys())
+    ...         for name in names:
+    ...             res += '<div>%s</div>\n' % self.subForms[name](
+    ...             ignore_request=ignore_request)
+    ...         return res
+    ...     
+
+    >>> from zope.publisher.browser import TestRequest
+    >>> request = TestRequest()
+    >>> view = OrdersForm(orderMapping, request)
+    >>> print view()
+    <div>
+    <div>0</div><div><input ... name="form.0.name" ... value="n0" ...</div>
+    </div>
+    <div>
+    ...
+    </div>
+
+If the request contains any form data, that will be reflected in the
+output:
+
+    >>> request.form['form.1.name'] = u'bob'
+    >>> print OrdersForm(orderMapping,request)()
+    <div>
+    ...
+    <div>1</div><div><input ... name="form.1.name" ... value="bob" ...</div>
+    ...
+    </div>
+
+Sometimes we don't want this behavior: we want to ignore the request values,
+particularly after a form has been processed and before it is drawn again.
+This can be accomplished with the 'ignore_request' argument in
+setUpWidgets.
+
+    >>> print OrdersForm(orderMapping, request)(ignore_request=True)
+    <div>
+    ...
+    <div>1</div><div><input ... name="form.1.name" ... value="n1" ...</div>
+    ...
+    </div>
+

Added: z3c.multiform/Sandbox/src/z3c/multiform/TODO.txt
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/TODO.txt	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/TODO.txt	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,40 @@
+TODO
+====
+
+We are at the very beginning. The first concrete multiform is
+container.views.ContainerGridForm. The actions should work fine.
+
+Missing functionalities:
+------------------------
+
+	- customwidget for container.interfaces.IMovableLocation['__name__']
+
+		Returns an inputwidget if an IInputWidget is needed and returns
+		an displaywidget if an IDisplayWidget is needed.
+	
+	- Size columns
+
+	    Enable to register a size information field (ISized has only IMethods).
+	    Pay attention to register a customwidget to display the size.
+
+	- Sort and Batch
+
+		Write sort and batch handling in the template. Show the current sort
+		column and sort direction. Show the current batch.
+	
+	- Catch DuplicateError in handle_save_action
+	
+	- View Errors and Status of each ItemForm
+	
+	- Use namedtemplate for ContainerGridForm
+	
+	- write more test in gridform.txt
+	
+		Test singleedit and singlesave
+
+	- write functional tests for ContainerGridForm
+
+	- sort_down and sort_up gifs from the skin
+	
+		It would be a nice feature of a skin to provides these to icons
+

Added: z3c.multiform/Sandbox/src/z3c/multiform/__init__.py
===================================================================

Added: z3c.multiform/Sandbox/src/z3c/multiform/actions.txt
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/actions.txt	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/actions.txt	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,197 @@
+===================
+ Multiform Actions
+===================
+
+The multiform package defines a new type of action called
+ParentAction. A parent action is rendered only one time in the parent
+multiform, but applied to all subforms.
+
+In this example we create a specialized item form class, which defines
+a new parent action called ``Save``. Additionally two standard actions
+are defined in the multiform (parent) class called ``Edit`` and
+``Cancel```.
+
+    >>> from zope import interface, schema
+    >>> from zope.formlib import form
+    >>> from zope.publisher.browser import TestRequest
+    >>> from zope.app.form.interfaces import IInputWidget
+    >>> from zope.app.location.interfaces import ILocation
+    >>> from z3c.multiform import multiform
+    >>> from z3c.multiform.multiform import ItemFormBase,MultiFormBase
+
+    >>> class IOrder(interface.Interface):
+    ...     identifier = schema.Int(title=u"Identifier", readonly=True)
+    ...     name = schema.TextLine(title=u"Name")
+    >>> class Order:
+    ...     interface.implements(IOrder,ILocation)
+    ...
+    ...     def __init__(self, identifier, name=''):
+    ...         self.identifier = identifier
+    ...         self.name = name
+    ...         self.__name__= name
+
+    >>> orderMapping = dict([('n%s'%k,Order(k,name='n%s'%k)) for k in range(2)])
+
+    >>> class OrderForm(ItemFormBase):
+    ...     inputMode=False
+    ...     form_fields = form.Fields(IOrder,omit_readonly=False,
+    ...         render_context=True)
+    ...     
+    ...     @multiform.parentAction(u"Save",
+    ...     condition=multiform.anySubFormInputMode,inputMode=True)
+    ...     def handle_save_action(self, action, data):
+    ...         form.applyChanges(self.context, self.form_fields,
+    ...         data, self.adapters)
+    ...         self.newInputMode = False
+    ...         
+    ...     @form.action(u"Upper", condition=multiform.allSubFormsDisplayMode)
+    ...     def handle_uppercase_action(self, action, data):
+    ...			self.context.name = self.context.name.upper()
+    ...			
+    ...     def template(self):
+    ...         return '\n<div>%s</div>\n' % '</div><div>'.join([w() for w in
+    ...     self.widgets] + [action.render() for action in 
+    ...     self.availableActions()])
+
+
+    >>> class OrdersForm(MultiFormBase):
+    ...     itemFormFactory=OrderForm
+    ...     def template(self):
+    ...         res = u''
+    ...         names = sorted(self.subForms.keys())
+    ...         for name in names:
+    ...             res += '<div>%s</div>\n' % self.subForms[name].render()
+    ...         for action in self.availableActions():
+    ...             res += '<div>%s</div>\n' % action.render()
+    ...         for action in self.availableSubActions():
+    ...             res += '<div>%s</div>\n' % action.render()
+    ...         return res
+    ...     
+    ...     @form.action('Edit',condition=multiform.allSubFormsDisplayMode)
+    ...     def handle_edit_action(self, action, data):
+    ...         for form in self.subForms.values():
+    ...             form.newInputMode = True
+    ...         
+    ...     @form.action('Cancel',condition=multiform.anySubFormInputMode)
+    ...     def handle_cancel_action(self, action, data):
+    ...         for form in self.subForms.values():
+    ...             form.newInputMode = False
+
+
+So in our new form all widgets are display widgets per default
+
+    >>> request = TestRequest()
+    >>> pf = OrdersForm(orderMapping,request)
+    >>> print pf()
+    <div>
+    <div>0</div><div>n0</div>...<input...name="form.n0.actions.upper"...
+    </div>
+    <div>
+    <div>1</div><div>n1</div>...<input...name="form.n1.actions.upper"...
+    </div>
+    <div><input...name="form.actions.edit"...</div>
+
+And the save action should not be available, due to the reason that there
+are no input widgets in the sub forms.
+
+    >>> pf.subActionNames
+    []
+    >>> [action.__name__ for action in pf.availableActions()]
+    [u'form.actions.edit']
+
+Now let's call the edit action to set the widgets to input widgets.
+
+    >>> request.form['form.actions.edit']=u''
+    >>> pf =  OrdersForm(orderMapping,request)
+    >>> print pf()
+    <div>
+    <div...<input class="textType" ... value="n0"...
+    </div>
+    <div>
+    <div...<input class="textType" ... value="n1"...
+    <div><input...name="form.actions.cancel"...</div>
+    <div><input...name="form.actions.save"...</div>
+    
+Now the save action should be available in the subActionNames and
+the cancel action in the multiform actions.
+
+    >>> pf.subActionNames
+    [u'form.actions.save']
+    >>> [a.__name__ for a in pf.availableActions()]
+    [u'form.actions.cancel']
+
+
+Now Let us save some data.
+
+    >>> request = TestRequest()
+    >>> request.form['form.actions.save']=u''
+    >>> for i in range(2):
+    ...     request.form['form.n%s.name' % i]='newer name %s' % i
+    ...     request.form['form.n%s.identifier' % i]= i
+    >>> pf =  OrdersForm(orderMapping,request)
+    >>> result = pf()
+
+After the form is called, the changes are applied to the objects. 
+    >>> sorted([obj.name for obj in orderMapping.values()])
+    [u'newer name 0', u'newer name 1']
+
+Due to the reason the save handler sets the inputMode to False,
+only display widgets are rendered in the results
+
+    >>> print result
+    <div>
+    <div>0</div><div>newer name 0</div>...
+    </div>
+    <div>
+    <div>1</div><div>newer name 1</div>...
+    </div>
+    ...
+
+Now we should only have the edit action be available, which is a
+multiform action, therefore not contained in the subActionNames
+
+    >>> pf.subActionNames
+    []
+    >>> [a.__name__ for a in pf.availableActions()]
+    [u'form.actions.edit']
+
+Now Let us cancel the edit mode.
+
+    >>> request = TestRequest()
+    >>> request.form['form.actions.cancel']=u''
+    >>> for i in range(2):
+    ...     request.form['form.n%s.name' % i]='next name %s' % i
+    ...     request.form['form.n%s.identifier' % i]= i
+    >>> pf =  OrdersForm(orderMapping,request)
+    >>> result = pf()
+
+After the form is called, the objects are left unchanged. And the form
+should be in display mode again.
+
+    >>> sorted([obj.name for obj in orderMapping.values()])
+    [u'newer name 0', u'newer name 1']
+
+    >>> print result
+    <div>
+    <div>0</div><div>newer name 0</div>...
+    </div>
+    <div>
+    <div>1</div><div>newer name 1</div>...
+    </div>...
+
+Now let us call the upper action, which should uppercase the name
+attributes of the items. This action is an action on the items itself,
+so we have a unique prefix with the key of the item in the parent
+mapping.
+
+    >>> request = TestRequest()
+    >>> request.form['form.n1.actions.upper']=u''
+    >>> pf =  OrdersForm(orderMapping,request)
+    >>> result = pf()
+    >>> print result
+    <div>
+    <div>0</div><div>newer name 0</div>...
+    </div>
+    <div>
+    <div>1</div><div>NEWER NAME 1</div>...
+    </div>...

Added: z3c.multiform/Sandbox/src/z3c/multiform/configure.zcml
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/configure.zcml	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/configure.zcml	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<configure xmlns="http://namespaces.zope.org/zope"
+           xmlns:i18n="http://namespaces.zope.org/i18n"
+           i18n_domain="zope">
+
+  <adapter for="zope.app.location.interfaces.ILocation
+                zope.formlib.interfaces.IForm"
+           provides=".interfaces.IFormLocation"
+           factory=".selection.FormLocationProxy"
+           trusted="true"
+           />
+
+  <adapter for=".interfaces.IFormLocation"
+           provides=".interfaces.ISelection"
+           factory=".selection.FormLocationSelection"
+           />
+
+  <adapter for="zope.interface.Interface
+                zope.schema.interfaces.IField"
+           factory=".sort.SchemaSorter"
+           provides=".interfaces.ISorter"
+           />
+
+  <!-- namedtemplate for IParentAction -->
+  <adapter factory=".multiform.render_submit_button" name="render" />
+ 
+  <include package=".container"/>
+ 
+</configure>
\ No newline at end of file

Added: z3c.multiform/Sandbox/src/z3c/multiform/container/__init__.py
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/container/__init__.py	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/container/__init__.py	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,3 @@
+# container
+
+

Added: z3c.multiform/Sandbox/src/z3c/multiform/container/configure.zcml
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/container/configure.zcml	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/container/configure.zcml	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<configure xmlns:zope="http://namespaces.zope.org/zope"
+           xmlns="http://namespaces.zope.org/browser"
+           xmlns:i18n="http://namespaces.zope.org/i18n"
+           i18n_domain="zope">
+
+ <page for="zope.app.container.interfaces.IContainer"
+       name="grid.html"
+       permission="zope.View"
+       class=".views.ContainerGridForm"/>
+
+  <zope:adapter for="zope.app.location.interfaces.ILocation"
+                factory=".location.MovableLocation"
+                provides=".interfaces.IMovableLocation"/>
+ 
+</configure>
+

Added: z3c.multiform/Sandbox/src/z3c/multiform/container/container.txt
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/container/container.txt	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/container/container.txt	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,46 @@
+=====================
+ Container Grid Form
+=====================
+
+A container contents view implementation using multiform. This view
+provides cut, copy, paste, edit and save actions. Additionally the
+__name__ attribute of the items can be transparently changed.
+
+In order to test the behaviour of the ``ContainerGridForm`` we
+subclass it and override the template method.
+
+    >>> from zope import interface
+    >>> from zope.publisher.browser import TestRequest
+    >>> from zope.app.container.sample import SampleContainer
+    >>> from zope.app.annotation.interfaces import IAttributeAnnotatable
+    >>> from zope.app.dublincore.interfaces import IZopeDublinCore
+    >>> from zope.app.dublincore.interfaces import IDCDescriptiveProperties
+    >>> from z3c.multiform.container.views import ContainerGridForm
+
+    >>> class ContainerTest(ContainerGridForm):
+    ...     actions = ContainerGridForm.actions.copy()
+    ...     def template(self):
+    ...         res = u''
+    ...         forms = list(self.getForms())
+    ...         forms.sort(lambda x,y: cmp(x.prefix,y.prefix))
+    ...         for form in forms:
+    ...             res += '<div>%s</div>\n' % form.render()
+    ...         return res
+
+    >>> c = SampleContainer()
+    >>> for i in range(2):
+    ...     o = SampleContainer()
+    ...     interface.directlyProvides(o,IAttributeAnnotatable)
+    ...     c[u'name of %s' % i]=o
+
+    >>> request = TestRequest()
+    >>> view = ContainerTest(c,request)
+    >>> print view()
+    <div...<td>name of 0</td>...<td>name of 1</td>...             
+
+
+
+
+
+    
+

Added: z3c.multiform/Sandbox/src/z3c/multiform/container/grid.pt
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/container/grid.pt	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/container/grid.pt	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,195 @@
+<html metal:extend-macro="context/@@standard_macros/view"
+      metal:define-macro="main">
+<head>
+</head>
+
+<body>
+<div metal:fill-slot="body">
+
+<div metal:define-macro="form">
+
+<form action="." metal:define-macro="master"
+      tal:attributes="action request/URL" method="post"
+      class="edit-form" enctype="multipart/form-data"
+      id="zc.page.browser_form">
+
+<script type="text/javascript"><!--
+
+function toggleFormFieldHelp(ob,state) {
+  // ob is the label element
+  var field = ob.form[ob.htmlFor];
+  if (field) {
+    var viz = state && 'hidden' || 'visible';
+    if (field.length == null) {
+      field.style.visibility = viz;
+    }
+    else {
+      for (var i = 0; i < field.length; ++i) {
+        var e = field.item(i);
+        e.style.visibility = viz;
+      }
+    }
+    var help = document.getElementById("field-help-for-" + ob.htmlFor);
+    if (help) {
+      help.style.visibility = state && 'visible' || 'hidden';
+    }
+  }
+}
+
+//-->
+</script>
+
+<div id="viewspace" metal:define-slot="viewspace">
+
+  <h1  i18n:translate=""
+       tal:condition="view/label"
+       tal:content="view/label"
+       metal:define-slot="heading"
+       >Do something</h1>
+
+  <metal:block define-macro="header">
+
+    <div class="form-status"
+       tal:define="status view/status"
+       tal:condition="status">
+
+      <div class="summary"
+           i18n:translate=""
+           tal:content="view/status">
+        Form status summary
+      </div>
+
+      <ul class="errors" tal:condition="view/errors">
+         <li tal:repeat="error view/error_views">
+            <span tal:replace="structure error">Error Type</span>
+         </li>
+      </ul>
+    </div>
+
+  </metal:block>
+
+  <div metal:define-slot="extra_info" tal:replace="nothing">
+  </div>
+
+  <div metal:define-slot="main_form">
+    <table class="form-fields" metal:define-macro="formtable">
+      <tr class="row" metal:define-slot="extra_top" tal:replace="nothing">
+          <td class="label">Extra top</td>
+          <td class="field"><input type="text" /></td>
+      </tr>
+      <tbody metal:define-slot="formbody" tal:omit-tag="">
+        <tal:block omit-tag="" repeat="widget view/widgets">
+          <tr metal:define-macro="formrow">
+            <td class="label" tal:define="hint widget/hint"
+              metal:define-macro="labelcell">
+              <label tal:condition="python:hint"
+                     tal:attributes="for widget/name"
+                     onmousedown="toggleFormFieldHelp(this,1)"
+                     onmouseup="toggleFormFieldHelp(this,0)"
+                     onmouseout="toggleFormFieldHelp(this,0)"
+                     style="cursor: help">
+                <span class="required" tal:condition="widget/required"
+                >*</span><span i18n:translate=""
+                               tal:content="widget/label">label</span>
+              </label>
+              <label tal:condition="python:not hint"
+                     tal:attributes="for widget/name">
+                <span class="required" tal:condition="widget/required"
+                >*</span><span i18n:translate=""
+                               tal:content="widget/label">label</span>
+              </label>
+            </td>
+            <td class="field" tal:define="hint widget/hint"
+                metal:define-macro="widgetcell">
+              <div class="form-fields-help"
+                   i18n:translate=""
+                   tal:content="hint"
+                   tal:condition="hint"
+                   tal:attributes="id string:field-help-for-${widget/name}"
+                   onclick="this.style.visibility='hidden';"
+                   style="visibility: hidden; position: absolute;"
+                   >Title of this content object.</div>
+              <div class="widget" tal:content="structure widget">
+              <input type="text" /></div>
+              <div class="error"
+                   tal:condition="widget/error"
+                   >
+                <!-- TODO Put this back, the Zope3 way.
+                <img src="alert.gif" alt="Error"
+                tal:replace="structure context/alert.gif" />
+                -->
+                <span tal:replace="structure widget/error">error</span>
+              </div>
+            </td>
+          </tr>
+        </tal:block>
+      </tbody>
+      <tr class="row" metal:define-slot="extra_bottom" tal:replace="nothing">
+        <td class="label">Extra bottom</td>
+        <td class="label"><input type="text" /></td>
+      </tr>
+    </table>
+  </div>
+  <!-- start: subforms -->
+  <table id="sortable" class="listing"
+         summary="Content listing">
+   <thead>
+    <tr>
+     <th tal:repeat="field view/itemFormFactory/form_fields"
+         tal:content="field/field/title" i18n:translate="">
+     </th>
+    </tr>
+   </thead>
+   <tbody>
+    <tr tal:repeat="subForm view/getForms"
+        tal:attributes="class python:repeat['subForm'].even and 'even' or 'odd'"
+        tal:content="structure subForm"/>
+   </tbody>
+  </table>
+  
+  <!-- end: subforms -->
+
+  <!-- start: parentActions -->
+
+  <div id="parentActionsView"
+       metal:define-macro="form_parent_actions">
+   <span class="actionButtons"
+        tal:condition="view/availableSubActions"
+        metal:define-slot="bottom_buttons">
+    <input tal:repeat="action view/availableSubActions"
+           tal:replace="structure action/render"
+           />
+  </span>
+</div>
+
+  <!-- end: parentActions -->
+
+  
+  
+  <metal:block define-slot="above_buttons" />
+</div>
+<div id="actionsView"
+     metal:define-macro="form_actions">
+  <span class="actionButtons"
+        tal:condition="view/availableActions"
+        metal:define-slot="bottom_buttons">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </span>
+</div>
+
+</form>
+
+<div tal:content="view/request/form"/>
+
+<script type="text/javascript" metal:define-slot="trackChanges">
+  zc_trackChanges(document.getElementById('zc.page.browser_form'));
+</script>
+
+<script type="text/javascript"
+    tal:define="extra_script view/extra_script | nothing"
+    tal:condition="extra_script"
+    tal:content="structure extra_script" />
+
+</div></div></body></html>

Added: z3c.multiform/Sandbox/src/z3c/multiform/container/griditem.pt
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/container/griditem.pt	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/container/griditem.pt	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,2 @@
+<td tal:repeat="widget view/widgets"
+     tal:content="structure widget"/>

Added: z3c.multiform/Sandbox/src/z3c/multiform/container/interfaces.py
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/container/interfaces.py	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/container/interfaces.py	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,16 @@
+from zope import schema
+from zope.interface import Interface, Attribute
+from zope.app.location.interfaces import ILocation
+
+
+class IMovableLocation(ILocation):
+
+    """a located object that can change its __name__ attribute by
+    itself"""
+
+    __name__ = schema.TextLine(
+        title=u"The name within the parent",
+        description=u"Traverse the parent with this name to get the object.",
+        required=False,
+        default=None)
+

Added: z3c.multiform/Sandbox/src/z3c/multiform/container/location.py
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/container/location.py	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/container/location.py	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,37 @@
+from zope.interface import implements
+from zope.security.proxy import removeSecurityProxy
+from zope.app.copypastemove.interfaces import IContainerItemRenamer
+
+from z3c.multiform.interfaces import IFormLocation
+from interfaces import IMovableLocation
+
+
+class MovableLocation(object):
+
+    implements(IMovableLocation)
+
+    def __init__(self,context):
+        self.context = context
+        self.__parent__ = self.context.__parent__
+
+    def _setName(self,v):
+        old = self.context.__name__
+        if v != old:
+            renamer = IContainerItemRenamer(self.__parent__)
+            renamer.renameItem(old, v)
+            if IFormLocation.providedBy(self.context):
+                # if we are in a multiform, we have to add the request
+                # data to our prefix
+                form = removeSecurityProxy(self.context.__form__)
+                oldPrefix = form.prefix
+                newPrefix = form.prefix[:-len(old)] + v
+                for oldKey in list(form.request.form.keys()):
+                    if oldKey.startswith(oldPrefix):
+                        newKey = newPrefix + oldKey[len(oldPrefix):]
+                        form.request.form[newKey]=form.request.form[oldKey]
+                
+    def _getName(self):
+        return self.context.__name__
+
+    __name__ = property(_getName,_setName)
+    

Added: z3c.multiform/Sandbox/src/z3c/multiform/container/tests.py
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/container/tests.py	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/container/tests.py	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,115 @@
+import doctest
+import unittest
+import location
+
+from zope import component
+from zope.testing.doctestunit import DocFileSuite, DocTestSuite
+from zope.app.testing import setup
+import zope.schema.interfaces
+import zope.app.form.browser
+import zope.publisher.interfaces.browser
+import zope.app.form.interfaces
+from zope.app.dublincore.annotatableadapter import ZDCAnnotatableAdapter
+from zope.app.dublincore.interfaces import IWriteZopeDublinCore
+from zope.app.annotation.interfaces import IAnnotatable
+from zope.app.location.interfaces import ILocation
+from zope.formlib import form
+
+from z3c.multiform import gridform,multiform,selection
+from z3c.multiform.interfaces import IFormLocation,ISelection
+from interfaces import IMovableLocation
+
+
+def setUp(test):
+    setup.placefulSetUp()
+
+    component.provideAdapter(
+        zope.app.form.browser.TextWidget,
+        [zope.schema.interfaces.ITextLine,
+         zope.publisher.interfaces.browser.IBrowserRequest,
+         ],
+        zope.app.form.interfaces.IInputWidget,
+        )
+    component.provideAdapter(
+        zope.app.form.browser.UnicodeDisplayWidget,
+        [zope.schema.interfaces.ITextLine,
+         zope.publisher.interfaces.browser.IBrowserRequest,
+         ],
+        zope.app.form.interfaces.IDisplayWidget,
+        )
+    component.provideAdapter(
+        zope.app.form.browser.boolwidgets.BooleanDisplayWidget,
+        [zope.schema.interfaces.IBool,
+         zope.publisher.interfaces.browser.IBrowserRequest,
+         ],
+        zope.app.form.interfaces.IDisplayWidget,
+        )
+    component.provideAdapter(
+        zope.app.form.browser.CheckBoxWidget,
+        [zope.schema.interfaces.IBool,
+         zope.publisher.interfaces.browser.IBrowserRequest,
+         ],
+        zope.app.form.interfaces.IInputWidget,
+        )
+    component.provideAdapter(
+        zope.app.form.browser.UnicodeDisplayWidget,
+        [zope.schema.interfaces.IInt,
+         zope.publisher.interfaces.browser.IBrowserRequest,
+         ],
+        zope.app.form.interfaces.IDisplayWidget,
+        )
+    component.provideAdapter(
+        zope.app.form.browser.IntWidget,
+        [zope.schema.interfaces.IInt,
+         zope.publisher.interfaces.browser.IBrowserRequest,
+         ],
+        zope.app.form.interfaces.IInputWidget,
+        )
+    component.provideAdapter(
+        selection.FormLocationProxy,
+        [zope.app.location.interfaces.ILocation,
+         zope.formlib.interfaces.IForm
+         ],
+        IFormLocation
+        )
+    component.provideAdapter(
+        selection.FormLocationSelection,
+        [IFormLocation],
+        ISelection
+        )
+    component.provideAdapter(
+        location.MovableLocation,
+        [ILocation],
+        IMovableLocation
+        )
+    component.provideAdapter(
+        ZDCAnnotatableAdapter,
+        [IAnnotatable],
+        IWriteZopeDublinCore
+        )
+
+    
+    component.provideAdapter(gridform.default_grid_template,
+                             name="default")
+    component.provideAdapter(gridform.default_griditem_template,
+                             name="default")
+    component.provideAdapter(form.render_submit_button, name='render')
+    component.provideAdapter(multiform.render_submit_button, name='render')
+    
+def tearDown(test):
+    setup.placefulTearDown()
+
+
+def test_suite():
+    
+    return unittest.TestSuite(
+        (
+        DocFileSuite('container.txt',
+                     setUp=setUp, tearDown=tearDown,
+                     optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+                     ),
+        ))
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')

Added: z3c.multiform/Sandbox/src/z3c/multiform/container/views.py
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/container/views.py	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/container/views.py	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,302 @@
+import datetime
+import pytz
+from zope.interface.common import idatetime
+from zope.formlib.i18n import _
+from zope.formlib import form
+from zope.app.dublincore.interfaces import IWriteZopeDublinCore
+from zope.app.pagetemplate import ViewPageTemplateFile
+from zope.event import notify
+from zope.app.event.objectevent import ObjectModifiedEvent
+from zope.app import zapi
+from zope.app.copypastemove.interfaces import IPrincipalClipboard
+from zope.app.copypastemove.interfaces import IObjectCopier
+from zope.app.copypastemove.interfaces import IObjectMover
+from zope.app.principalannotation.interfaces import IPrincipalAnnotationUtility
+from zope.app.container.interfaces import DuplicateIDError
+from zope.security.interfaces import Unauthorized
+from zope.app.traversing.interfaces import TraversalError
+
+from z3c.multiform import multiform, gridform
+from z3c.multiform.interfaces import ISelection
+from interfaces import IMovableLocation
+
+
+def isSelected(form,action):
+    return ISelection(form.context).selected
+
+    
+def isSelectedInput(form,action):
+    print "isSelectedInput",form,form.inputMode,action.__name__,isSelected(form,action)
+    if not form.inputMode:
+        return False
+    return isSelected(form,action)
+
+
+def isSelectedOrDisplay(form,action):
+    print "isSelectedOrDisplay",form.inputMode,isSelected(form,action)
+    return not form.inputMode
+
+
+def pasteable(containerForm):
+    """Decide if there is anything to paste."""
+    target = containerForm.context
+    clipboard = getPrincipalClipboard(containerForm.request)
+    items = clipboard.getContents()
+    for item in items:
+        try:
+            obj = zapi.traverse(target, item['target'])
+        except TraversalError:
+            pass
+        else:
+            if item['action'] == 'cut':
+                mover = IObjectMover(obj)
+                moveableTo = safe_getattr(mover, 'moveableTo', None)
+                if moveableTo is None or not moveableTo(target):
+                    return False
+            elif item['action'] == 'copy':
+                copier = IObjectCopier(obj)
+                copyableTo = safe_getattr(copier, 'copyableTo', None)
+                if copyableTo is None or not copyableTo(target):
+                    return False
+            else:
+                raise
+
+    return True
+
+
+def safe_getattr(obj, attr, default):
+    """Attempts to read the attr, returning default if Unauthorized."""
+    try:
+        return getattr(obj, attr, default)
+    except Unauthorized:
+        return default
+
+
+def hasClipboardContents(form, action):
+    """ interogates the `PrinicipalAnnotation` to see if
+       clipboard contents exist """
+
+    if multiform.anySubFormInputMode(form,action):
+        return False
+
+    if not pasteable(form):
+        return False
+
+    # touch at least one item to in clipboard confirm contents
+    clipboard = getPrincipalClipboard(form.request)
+    items = clipboard.getContents()
+    for item in items:
+        try:
+            zapi.traverse(form.context, item['target'])
+        except TraversalError:
+            pass
+        else:
+            return True
+
+    return False
+
+
+class ContainerItemForm(multiform.ItemFormBase):
+
+    inputMode=False
+    forceInput=['selected']
+    template = ViewPageTemplateFile('griditem.pt')
+    form_fields = form.Fields(ISelection['selected'],
+                              IMovableLocation['__name__'],
+                              IWriteZopeDublinCore['title'],
+                        omit_readonly=False,render_context=True)
+
+    @multiform.parentAction('Edit',
+                            condition=multiform.allSubFormsDisplayMode)
+    def handle_edit_action(self, action, data):
+        #print "handle_edit_action",action,data,isSelected(self,action)
+        if isSelected(self,action):
+            self.newInputMode = True
+
+    @multiform.parentAction("Save", inputMode=True,
+                            condition=multiform.anySubFormInputMode)
+    def handle_save_action(self, action, data):
+
+        if not isSelected(self,action):
+            return
+        if form.applyChanges(self.context, self.form_fields,
+                             data, self.adapters):
+            notify(ObjectModifiedEvent(self.context))
+            formatter = self.request.locale.dates.getFormatter(
+                'dateTime', 'medium')
+            try:
+                time_zone = idatetime.ITZInfo(self.request)
+            except TypeError:
+                time_zone = pytz.UTC
+            m = {'date_time':formatter.format(datetime.datetime.now(time_zone))}
+            self.status = (_("Updated on ${date_time}", mapping=m),)
+        else:
+            self.status = (_('No changes'),)
+        ISelection(self.context).selected=False
+        self.newInputMode = False
+       
+
+class ContainerGridForm(multiform.MultiFormBase):
+
+    itemFormFactory=ContainerItemForm
+
+    template = ViewPageTemplateFile('grid.pt')
+
+    @form.action('Cancel',condition=multiform.anySubFormInputMode)
+    def handle_cancel_action(self, action, data):
+        for form in self.subForms.values():
+            form.newInputMode = False
+
+    @form.action("Paste", condition=hasClipboardContents)
+    def handle_paste_action(self, action, data):
+        """Paste ojects in the user clipboard to the container"""
+        self.form_reset = True
+        target = self.context
+        clipboard = getPrincipalClipboard(self.request)
+        items = clipboard.getContents()
+        moved = False
+        not_pasteable_ids = []
+        for item in items:
+            duplicated_id = False
+            try:
+                obj = zapi.traverse(target, item['target'])
+            except TraversalError:
+                pass
+            else:
+                if item['action'] == 'cut':
+                    mover = IObjectMover(obj)
+                    try:
+                        mover.moveTo(target)
+                        moved = True
+                    except DuplicateIDError:
+                        duplicated_id = True
+                elif item['action'] == 'copy':
+                    copier = IObjectCopier(obj)
+                    try:
+                        copier.copyTo(target)
+                    except DuplicateIDError:
+                        duplicated_id = True
+                else:
+                    raise
+
+            if duplicated_id:
+                not_pasteable_ids.append(zapi.getName(obj))                
+
+        if moved:
+            # Clear the clipboard if we do a move, but not if we only do a copy
+            clipboard.clearContents()
+
+        if not_pasteable_ids != []:
+            # Show the ids of objects that can't be pasted because
+            # their ids are already taken.
+            # TODO Can't we add a 'copy_of' or something as a prefix
+            # instead of raising an exception ?
+            self.errors = (
+                _("The given name(s) %s is / are already being used" %(
+                str(not_pasteable_ids))),)
+
+    @form.action("Cut", condition=multiform.allSubFormsDisplayMode)
+    def handle_cut_action(self, action, data):
+        """move objects specified in a list of object ids"""
+
+        container_path = zapi.getPath(self.context)
+
+        # For each item, check that it can be moved; if so, save the
+        # path of the object for later moving when a destination has
+        # been selected; if not movable, provide an error message
+        # explaining that the object can't be moved.
+        items = []
+        for form in self.getForms():
+            ob = form.context
+            name = ob.__name__
+            selection = ISelection(ob)
+            if not selection.selected:
+                continue
+            selection.selected=False
+            mover = IObjectMover(ob)
+            if not mover.moveable():
+                m = {"name": name}
+                title = getDCTitle(ob)
+                if title:
+                    m["title"] = title
+                    self.errors = (_(
+                        "Object '${name}' (${title}) cannot be moved",
+                        mapping=m),)
+                else:
+                    self.errors = (_("Object '${name}' cannot be moved",
+                                     mapping=m),)
+                return
+            items.append(zapi.joinPath(container_path, name))
+        if len(items) == 0:
+            self.errors = (_("You didn't specify any ids to cut."),)
+        else:
+            # store the requested operation in the principal annotations:
+            clipboard = getPrincipalClipboard(self.request)
+            clipboard.clearContents()
+            clipboard.addItems('cut', items)
+
+    @form.action("Copy", condition=multiform.allSubFormsDisplayMode)
+    def handle_copy_action(self, action, data):
+        """Copy objects specified in a list of object ids"""
+
+        container_path = zapi.getPath(self.context)
+
+        # For each item, check that it can be copied; if so, save the
+        # path of the object for later copying when a destination has
+        # been selected; if not copyable, provide an error message
+        # explaining that the object can't be copied.
+
+        items = []
+        for form in self.getForms():
+            ob = form.context
+            name = ob.__name__
+            selection = ISelection(ob)
+            if not selection.selected:
+                continue
+            selection.selected=False
+            copier = IObjectCopier(ob)
+            if not copier.copyable():
+                m = {"name": name}
+                title = getDCTitle(ob)
+                if title:
+                    m["title"] = title
+                    self.errors = (_(
+                        "Object '${name}' (${title}) cannot be copied",
+                        mapping=m),)
+                else:
+                    self.errors = (_("Object '${name}' cannot be copied",
+                                     mapping=m),)
+                return
+            items.append(zapi.joinPath(container_path, name))
+        if len(items) == 0:
+            self.errors = (_("You didn't specify any ids to copy."),)
+        else:
+            # store the requested operation in the principal annotations:
+            clipboard = getPrincipalClipboard(self.request)
+            clipboard.clearContents()
+            clipboard.addItems('copy', items)
+
+    @form.action("Delete", condition=multiform.allSubFormsDisplayMode)
+    def handle_delete_action(self, action, data):
+        """Delete objects specified in a list of object ids"""
+        container = self.context
+        toDelete = []
+        for form in self.getForms():
+            if not ISelection(form.context).selected:
+                continue
+            toDelete.append(form.context.__name__)
+        if toDelete:
+            for name in toDelete:
+                del(container[name])
+            self.form_reset = True
+        else:
+            self.errors = (_("You didn't specify any ids to delete."),)            
+
+
+def getPrincipalClipboard(request):
+    """Return the clipboard based on the request."""
+    user = request.principal
+    annotationutil = zapi.getUtility(IPrincipalAnnotationUtility)
+    annotations = annotationutil.getAnnotations(user)
+    return IPrincipalClipboard(annotations)
+

Added: z3c.multiform/Sandbox/src/z3c/multiform/grid.pt
===================================================================

Added: z3c.multiform/Sandbox/src/z3c/multiform/gridform.py
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/gridform.py	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/gridform.py	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,149 @@
+from zope.component import getMultiAdapter
+from zope.interface import implements
+from zope.formlib import namedtemplate
+from zope.app.pagetemplate import ViewPageTemplateFile
+from zope.app.location.interfaces import ILocation
+from interfaces import IGridForm, IGridItemForm, ISorter
+import multiform
+
+
+default_grid_template = namedtemplate.NamedTemplateImplementation(
+    ViewPageTemplateFile('grid.pt'), IGridForm)
+
+default_griditem_template = namedtemplate.NamedTemplateImplementation(
+    ViewPageTemplateFile('griditem.pt'), IGridItemForm)
+
+
+class GridItemFormBase(multiform.ItemFormBase):
+    implements(IGridItemForm)
+    template = namedtemplate.NamedTemplate('default')
+
+
+class GridFormBase(multiform.MultiFormBase):
+
+    implements(IGridForm)
+
+    default_batch_size = None
+    default_sort_on = None
+    default_sort_reverse = None
+
+    template = namedtemplate.NamedTemplate('default')
+
+    def __init__(self, context, request):
+        context = FilterMapping(context, request, self)
+        super(GridFormBase,self).__init__(context, request)
+        
+
+class FilterMapping(object):
+    
+    implements(ILocation)
+
+    def __init__(self, context, request, form):
+        self.context = context
+        self.request = request
+        if ILocation.providedBy(context):
+            self.__parent__ = context.__parent__
+            self.__name__ = context.__name__
+            
+        else:
+            self.__parent__ = None
+            self.__name__ = u""
+        self.form = form
+        self.batch_start = request.form.get(
+                           '%s.handle.batch_start' % form.prefix,0)
+        self.batch_size = request.form.get(
+                           '%s.handle.batch_size' % form.prefix,
+                           form.default_batch_size)
+        self.sort_on = request.form.get(
+                           '%s.handle.sort_on' % form.prefix,
+                           form.default_sort_on)
+        self.sort_reverse = request.form.get(
+                           '%s.handle.sort_reverse' % form.prefix,
+                           form.default_sort_reverse)
+
+    def sortAllKeys(self):
+        sorter = None
+        if self.sort_on:
+            sortName = self.sort_on
+            sortField = None
+            for field in self.form.itemFormFactory.form_fields:
+                if field.__name__ == sortName:
+                    sortField = field
+                    break
+            if sortField:
+                sorter = getMultiAdapter((sortField.field.interface,
+                                          sortField.field),ISorter)
+        if sorter:
+            items = sorter.sort(self.context.items())
+            if self.sort_reverse:
+                items.reverse()
+            keys = []
+            for key, value in items:
+                yield key
+        else:
+            for key in self.context.keys():
+                yield key
+
+    def keys(self):
+        sortKeys = self.sortAllKeys()
+        batch_start = self.batch_start or 0
+        batch_size = self.batch_size or 0
+        if not self.batch_size:
+            if not batch_start:
+                for k in sortKeys:
+                    yield k
+                raise StopIteration
+            batch_end = None
+        else:
+            batch_end = batch_start + batch_size
+        for i, key in enumerate(sortKeys):
+            if batch_end is not None and i >= batch_end:
+                return
+            if i >= batch_start:
+                yield key
+
+    def values(self):
+        for k in self.keys():
+            yield self.context[k]
+
+    def items(self):
+        for k in self.keys():
+            yield k, self.context[k]
+
+    def __iter__(self):
+        return iter(self.keys())
+
+    def __getitem__(self, key):
+        '''See interface `IReadContainer`'''
+        if key in self.keys():
+            return self.context[key]
+        else:
+            raise KeyError, key
+
+    def get(self, key, default=None):
+        '''See interface `IReadContainer`'''
+        try:
+            return self.__getitem__(key)
+        except KeyError:
+            return default
+
+    def __len__(self):
+        '''See interface `IReadContainer`'''
+        return len(self.keys())
+
+    def __contains__(self, key):
+        '''See interface `IReadContainer`'''
+        return key in self.keys()
+
+    has_key = __contains__
+
+    def __setitem__(self, key, object):
+        '''See interface `IWriteContainer`'''
+        self.context.__setitem__(key, object)
+
+    def __delitem__(self, key):
+        '''See interface `IWriteContainer`'''
+        if key in self.keys():
+            self.context.__delitem__(key)
+        else:
+            raise KeyError, key

Added: z3c.multiform/Sandbox/src/z3c/multiform/gridform.txt
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/gridform.txt	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/gridform.txt	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,312 @@
+===========
+ Gridforms
+===========
+
+Gridforms are specialized multiforms which are supposed to be
+displayed as grids.
+
+Let's define a simple content object
+
+    >>> from zope import interface, schema
+    >>> from zope.formlib import form
+    >>> from zope.publisher.browser import TestRequest
+    >>> from zope.app.location.interfaces import ILocation
+    >>> from z3c.multiform.interfaces import ISelection
+    >>> from z3c.multiform import multiform, gridform
+
+    >>> class IOrder(interface.Interface):
+    ...     identifier = schema.Int(title=u"Identifier", readonly=True)
+    ...     name = schema.TextLine(title=u"Name")
+
+    >>> class Order:
+    ...     interface.implements(IOrder,ILocation)
+    ...     
+    ...     def __init__(self, identifier, name=''):
+    ...         self.identifier = identifier
+    ...         self.name = name
+    ...         self.__name__= name
+
+    >>> orderMapping = dict([('n%s'%k,Order(k,name='n%s'%k)) for k in range(4)])
+
+Now we use the ``GridForm`` as a base class to display orders in
+tabular form. Additionally to the IOrder schema the ISelection schema
+is added to the ``form_fields`` of the ``OrderForm`` in order to get
+selectable items in our order grid.
+
+
+    >>> def isSelected(form,action):
+    ...     return ISelection(form.context).selected
+    
+    >>> def isSelectedInput(form,action):
+    ...     if not form.inputMode:
+    ...         return False
+    ...     return ISelection(form.context).selected
+
+    >>> def isSelectedDisplay(form,action):
+    ...     if not form.inputMode:
+    ...         return False
+    ...     return ISelection(form.context).selected
+    
+
+    >>> class OrderForm(gridform.GridItemFormBase):
+    ...     inputMode=False
+    ...     forceInput=['selected'] 
+    ...     form_fields = form.Fields(ISelection,IOrder,
+    ...         omit_readonly=False,render_context=True)
+    ...         
+    ...     def template(self):
+    ...         return '\n<div>%s</div>\n' % '</div>\n<div>'.join([w() for w in
+    ...     self.widgets])
+    ...         
+    ...     @multiform.parentAction(u"Save",
+    ...         condition=isSelectedInput,inputMode=True)
+    ...     def handle_save_action(self, action, data):
+    ...         form.applyChanges(self.context, self.form_fields,
+    ...         data, self.adapters)
+    ...         self.newInputMode = False
+    ...         
+    ...     @multiform.parentAction('Edit',
+    ...         condition=multiform.allSubFormsDisplayMode)
+    ...     def handle_edit_action(self, action, data):
+    ...         if isSelected(self,action):
+    ...             self.newInputMode = True
+    ...
+    ...     @multiform.itemAction(u"SingleSave",
+    ...         condition=multiform.isFormInputMode,inputMode=True)
+    ...     def handle_singlesave_action(self, action, data):
+    ...         form.applyChanges(self.context, self.form_fields,
+    ...         data, self.adapters)
+    ...         self.newInputMode = False
+    ...         
+    ...     @multiform.itemAction('SingleEdit',
+    ...         condition=multiform.isFormDisplayMode)
+    ...     def handle_singleedit_action(self, action, data):
+    ...         self.newInputMode = True
+
+
+    >>> class OrdersForm(gridform.GridFormBase):
+    ...     itemFormFactory=OrderForm
+    ...     def template(self):
+    ...         res = u''
+    ...         names = sorted(self.subForms.keys())
+    ...         for name in names:
+    ...             res += '<div>%s</div>\n' % self.subForms[name].render()
+    ...         return res
+    ...         
+    ...     @form.action('Cancel',condition=multiform.isFormInputMode)
+    ...     def handle_cancel_action(self, action, data):
+    ...         for form in self.subForms.values():
+    ...             form.newInputMode = False
+
+Due to the reason that the ``inputMode`` attribute is ``False`` we now
+get DisplayWidgets for all fields, except the ``selected`` field
+which's name is defined in the ``forceInput`` attribute of the class,
+because we always want checkboxes to select some items.
+
+    >>> request = TestRequest()
+    >>> gf = OrdersForm(orderMapping,request)
+    >>> gf.update()
+    >>> print gf.render()
+    <div>
+    <div><input ... name="form.n0.selected" type="checkbox" ...  /></div>
+    <div>0</div>
+    <div>n0</div>
+    </div>
+    <div>
+    <div><input ... name="form.n1.selected" type="checkbox" ...  /></div>
+    <div>1</div>
+    <div>n1</div>
+    ...
+    </div>
+
+Also the edit action should be available.
+
+    >>> print gf.subActionNames
+    [u'form.actions.edit']
+    >>> [a.__name__ for a in gf.availableSubActions()]
+    [u'form.actions.edit']
+    
+Now we are going to select some items. And the checkbox of the ``selected``
+
+    >>> request = TestRequest()
+    >>> request.form['form.n1.selected']=u'on'
+    >>> request.form['form.n0.selected.used']=u''
+    >>> gf = OrdersForm(orderMapping,request)
+    >>> gf.update()
+    >>> print gf.render()
+    <div>
+    <div><input... name="form.n0.selected" type="checkbox" value="on"  /></div>
+    <div>0</div>
+    <div>n0</div>
+    </div>
+    <div>
+    <div><input... checked="checked" ... name="form.n1.selected" ... /></div>
+    <div>1</div>
+    <div>n1</div>
+    ...
+    </div>
+
+By using the Edit action we can now switch all selected item forms to
+input mode.
+
+    >>> request = TestRequest()
+    >>> request.form['form.actions.edit']=u''
+    >>> request.form['form.n0.selected.used']=u''
+    >>> request.form['form.n1.selected']=u'on'
+    >>> request.form['form.n2.selected.used']=u''
+    >>> request.form['form.n3.selected.used']=u''
+    >>> gf = OrdersForm(orderMapping,request)
+    >>> res = gf()
+    >>> ISelection(gf.subForms['n1'].context).selected
+    True
+    >>> print res
+    <div>
+    <div><input... name="form.n0.selected" type="checkbox" value="on"  /></div>
+    <div>0</div>
+    <div>n0</div>
+    </div>
+    <div>
+    <div><input... checked="checked" ... name="form.n1.selected" ... /></div>
+    <div>1</div>
+    <div><input... name="form.n1.name" ...</div>
+    ...
+    </div>
+
+Also the edit action should be disabled now and the save action should
+be enabled.
+
+    >>> print gf.subActionNames
+    [u'form.actions.save']
+
+Now we test the actions which are displayed in each row.
+
+    >>> request = TestRequest()
+    >>> #request.form['form.n1.actions.singleedit']=u''
+    >>> gf = OrdersForm(orderMapping,request)
+    >>> res = gf()
+    >>> sorted([action.__name__ for action in
+    ...     gf.subForms['n1'].availableActions()])
+    [u'form.n1.actions.singleedit']
+
+We call the singleedit to edit a single row.
+    
+    >>> request = TestRequest()
+    >>> request.form['form.n0.selected.used']=u''
+    >>> request.form['form.n1.selected.used']=u''
+    >>> request.form['form.n1.actions.singleedit']=u''
+    >>> gf = OrdersForm(orderMapping,request)
+    >>> res = gf()
+
+Batching
+--------
+The gridform class is able to handle batch_start and batch_size. Let us reduce
+the output.
+
+    >>> request = TestRequest()
+    >>> request.form['form.handle.batch_start']=1
+    >>> request.form['form.handle.batch_size']=2
+    >>> gf = OrdersForm(orderMapping,request)
+    >>> gf.update()
+    >>> print gf.render()
+    <div>
+    <div><input ... name="form.n1.selected" type="checkbox" ...  /></div>
+    <div>1</div>
+    <div>n1</div>
+    </div>
+    <div>
+    <div><input ... name="form.n2.selected" type="checkbox" ...  /></div>
+    <div>2</div>
+    <div>n2</div>
+    </div>    
+
+Check the results of fraktal informations about batch_size and batch_start:
+
+    >>> request = TestRequest()
+    >>> request.form['form.handle.batch_start']=1
+    >>> gf = OrdersForm(orderMapping,request)
+    >>> gf.update()
+    >>> sorted([name for name in gf.subForms])
+    ['n1', 'n2', 'n3']
+    >>> request = TestRequest()
+    >>> request.form['form.handle.batch_size']=2
+    >>> gf = OrdersForm(orderMapping,request)
+    >>> gf.update()
+    >>> sorted([name for name in gf.subForms])
+    ['n0', 'n1']
+
+We can have a default value for batch size. Here we just set the attribute
+default_batch_size by hand.
+
+    >>> request = TestRequest()
+    >>> gf = OrdersForm(orderMapping,request)
+    >>> gf.default_batch_size = 3
+    >>> gf.update()
+    >>> print gf.render()
+    <div>
+    <div><input ... name="form.n0.selected" ...
+    <div>1</div>
+    <div>n1</div>
+    ...
+    <div>2</div>
+    <div>n2</div>
+    ...
+    <div>3</div>
+    <div>n3</div>
+    </div>
+
+Sorting
+-------
+We can sort the grid with the informations field name (string) and
+reverse (bool).
+
+    >>> class Orders2Form(gridform.GridFormBase):
+    ...     itemFormFactory=OrderForm
+    ...     def template(self):
+    ...         res = u''
+    ...         for form in self.getForms():
+    ...             res += '<div>%s</div>\n' % form.render()
+    ...         return res
+    ...         
+    ...     @form.action('Cancel',condition=multiform.isFormInputMode)
+    ...     def handle_cancel_action(self, action, data):
+    ...         for form in self.subForms.values():
+    ...             form.newInputMode = False
+
+    >>> orderMapping2 = dict([('n%s'%k,Order(4-k,name='n%s'%k)) for k
+    ... in range(4)])
+
+We sort the forms by the value of the column name
+    
+    >>> request = TestRequest()
+    >>> request.form['form.handle.sort_on']=u'name'
+    >>> gf = Orders2Form(orderMapping2,request)
+    >>> gf.update()
+    >>> [(form.context.identifier,
+    ...   form.context.name) for form in gf.getForms()]
+    [(4, 'n0'), (3, 'n1'), (2, 'n2'), (1, 'n3')]
+
+and reserve.
+
+    >>> request = TestRequest()
+    >>> request.form['form.handle.sort_on']=u'name'
+    >>> request.form['form.handle.sort_reverse']=u'on'
+    >>> gf = Orders2Form(orderMapping2,request)
+    >>> gf.update()
+    >>> [(form.context.identifier,
+    ...   form.context.name) for form in gf.getForms()]
+    [(1, 'n3'), (2, 'n2'), (3, 'n1'), (4, 'n0')]
+
+We can have default values for sort field and sort direction. Here we just set
+the attributes default_sort_on or default_sort_reverse by hand.
+
+    >>> request = TestRequest()
+    >>> gf = Orders2Form(orderMapping2,request)
+    >>> gf.default_sort_on = u'name'
+    >>> gf.update()
+    >>> [(form.context.identifier,
+    ...   form.context.name) for form in gf.getForms()]
+    [(4, 'n0'), (3, 'n1'), (2, 'n2'), (1, 'n3')]
+
+TODO:  
+-----
+- test singleedit, singlesave

Added: z3c.multiform/Sandbox/src/z3c/multiform/griditem.pt
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/griditem.pt	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/griditem.pt	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,3 @@
+<div>
+ griditem
+</div>
\ No newline at end of file

Added: z3c.multiform/Sandbox/src/z3c/multiform/interfaces.py
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/interfaces.py	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/interfaces.py	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,49 @@
+from zope import schema
+from zope.interface import Interface,Attribute
+from zope.formlib.interfaces import IAction
+from zope.formlib.i18n import _
+
+
+class IMultiForm(Interface):
+
+    """multiform"""
+
+class IItemForm(Interface):
+
+    """a sub form for an item of a multiform"""
+
+
+class IGridItemForm(IItemForm):
+
+    """an form for an item of a grid form"""
+
+
+class IGridForm(IMultiForm):
+
+    """a special grid multiform"""
+
+
+class IItemAction(IAction):
+    """a item action"""
+
+    
+class IParentAction(IAction):
+    """a parent action"""
+
+
+class ISelection(Interface):
+
+    """Provides information about the selection state of an object"""
+
+    selected = schema.Bool(title=_(u'Selected'),default=False)
+
+
+class IFormLocation(Interface):
+
+    __form_name__ = Attribute('The unique name of the item in a multiform')
+
+
+class ISorter(Interface):
+
+    def sort(items):
+        """return the items sorted. items are (key,value) tuples"""

Added: z3c.multiform/Sandbox/src/z3c/multiform/multiform.py
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/multiform.py	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/multiform.py	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,322 @@
+import copy
+from zope import interface
+from zope.component import getMultiAdapter
+from zope.interface import implements
+import zope.i18n
+from zope.app.publisher.browser import BrowserView
+from zope.app import zapi
+from zope.app.form.browser.interfaces import IWidgetInputErrorView
+from zope.formlib import form
+from zope.formlib import namedtemplate
+from zope.formlib.interfaces import IBoundAction
+from zope.formlib.i18n import _
+
+from interfaces import IMultiForm, IParentAction, IItemAction, ISelection
+from interfaces import IFormLocation,IItemForm
+
+
+def isFormDisplayMode(f,action):
+    return not f.inputMode
+    
+def isFormInputMode(f,action):
+    return f.inputMode
+
+def anySubFormInputMode(form,action):
+    if not IMultiForm.providedBy(form):
+        form = form.parentForm
+    for f in form.subForms.values():
+        if f.inputMode:
+            return True
+
+def allSubFormsDisplayMode(form,action):
+    if not IMultiForm.providedBy(form):
+        form = form.parentForm
+    for f in form.subForms.values():
+        if f.inputMode:
+            return False
+    return True
+
+
+class ItemAction(form.Action):
+
+    """an action that is rendered in the itemform object and can
+    handle the toggle between input and display."""
+    
+    implements(IItemAction)
+    def __init__(self, label, **options):
+        self.inputMode = options.pop('inputMode',None)
+        super(ItemAction,self).__init__(label,**options)
+    
+class ParentAction(form.Action):
+
+    """an action that is rendered in the parent multiform object and
+    is applied to all subForms"""
+    
+    implements(IParentAction)
+    def __init__(self, label, **options):
+        self.inputMode = options.pop('inputMode',None)
+        super(ParentAction,self).__init__(label,**options)
+          
+    def __get__(self, form, class_=None):
+        if form is None:
+            return self
+        result = self.__class__.__new__(self.__class__)
+        result.__dict__.update(self.__dict__)
+        result.form = form
+        result.__name__ = form.parentForm.prefix + '.' + result.__name__
+        interface.alsoProvides(result, IBoundAction)
+        return result
+
+    def submitted(self):
+        # override to find the matching prefix
+        if not self.available():
+            res = False
+        else:
+            res =  self.__name__ in self.form.request
+        return res
+
+ at namedtemplate.implementation(IParentAction)
+def render_submit_button(self):
+    if IBoundAction.providedBy(self) and not self.available():
+        return ''
+    label = self.label
+    if isinstance(label, (zope.i18n.Message, zope.i18n.MessageID)):
+        label = zope.i18n.translate(self.label, context=self.form.request)
+    return ('<input type="submit" id="%s" name="%s" value="%s"'
+            ' class="button" />' %
+            (self.__name__, self.__name__, label)
+            )
+
+
+class itemAction(form.action):
+
+    def __call__(self, success):
+        action = ItemAction(self.label, success=success, **self.options)
+        self.actions.append(action)
+        return action
+
+
+class parentAction(form.action):
+
+    def __call__(self, success):
+        action = ParentAction(self.label, success=success, **self.options)
+        self.actions.append(action)
+        return action
+
+
+class ItemFormBase(form.FormBase):
+
+    implements(IItemForm)
+    forceInput = []
+    parentForm = None
+    inputMode = None
+    newInputMode = None
+    form_fields=[]
+    actions = []
+    
+    def __init__(self,context,request,parentForm):
+        # we have to copy the default fields, so that we can mutate
+        # them in our instance
+        self.form_fields = copy.deepcopy(self.__class__.form_fields)
+        self.request = request
+        self.context = getMultiAdapter([context,self],IFormLocation)
+        self.parentForm = parentForm
+
+    def update(self):
+        self.form_reset = False
+
+        data = {}
+        errors, action = form.handleSubmit(self.actions, data, self.validate)
+        self.errors = errors
+
+        if errors:
+            self.status = _('There were errors')
+            result = action.failure(data, errors)
+        elif errors is not None:
+            self.form_reset = True
+            result = action.success(data)
+        else:
+            result = None
+
+        self.form_result = result
+
+    def setUpWidgets(self,ignore_request=False):
+        super(ItemFormBase,self).setUpWidgets(ignore_request)
+        # XXX how to check for the field
+        if self.widgets.get('selected'):
+            widget = self.widgets['selected']
+            widget.setRenderedValue(ISelection(self.context).selected)
+
+    def availableActions(self):
+        # we need to override this, because we should not return the
+        # parentActions
+        if not hasattr(self,'actions'):
+            return []
+        actions = [action for action in self.actions
+                   if not IParentAction.providedBy(action)]
+        return form.availableActions(self, actions)
+        
+    def availableParentActions(self):
+        if not hasattr(self,'actions'):
+            return []
+        actions = [action for action in self.actions
+                   if IParentAction.providedBy(action)]
+        return form.availableActions(self, actions)
+
+
+class MultiFormBase(form.FormBase):
+
+    implements(IMultiForm)
+    itemFormFactory = ItemFormBase
+    subForms={}
+    form_fields = []
+    actions = []
+    subActionNames = []
+    subFormInputMode = {}
+    selection = []
+    actions = []
+
+    def update(self):
+        self.checkInputMode()
+        self.updateSelection()
+        super(MultiFormBase,self).update()
+        preForms = list(self.getForms())
+        for form in preForms:
+            form.update()
+        postForms = list(self.getForms())
+        refresh = postForms != preForms
+        for form in postForms:
+            if form.newInputMode is not None:
+                newInputMode = form.newInputMode
+                context = self.context[form.context.__name__]
+                name = context.__name__
+                self.setUpForm(name, context, newInputMode,
+                               ignore_request=True)
+                self.subFormInputMode[name] = newInputMode
+                refresh = True
+            if refresh:
+                self.refreshSubActionNames()
+
+    def setUpWidgets(self, *args, **kw):
+        super(MultiFormBase,self).setUpWidgets(*args,**kw)
+        self.setUpForms(*args, **kw)
+
+    def setUpForm(self, name, item, inputMode, *args, **kw):
+        prefix = (self.prefix and self.prefix+'.' or '') + name
+        subForm = self.newSubForm(item)
+        if inputMode is not None and not inputMode:
+            forceInput = self.itemFormFactory.forceInput
+            for field in subForm.form_fields:
+                if field.__name__ not in forceInput:
+                    field.for_display=True
+        subForm.inputMode = inputMode
+        subForm.setPrefix(prefix)
+        subForm.setUpWidgets(*args, **kw)
+        self.subForms[name] = subForm
+
+    def setUpForms(self, *args, **kw):
+        self.subForms = {}
+        for name, item in self.context.items():
+            inputMode = self.subFormInputMode.get(name,self.itemFormFactory.inputMode)
+            self.setUpForm(name, item, inputMode)
+        self.refreshSubActionNames()
+
+    def getForms(self):
+        # we have to use the keys here to support all actions that
+        # modifies the keys of our mapping, e.g. rename, delete
+        keys = list(self.context.keys())
+        deleted = filter(lambda x: x not in keys,self.subForms.keys())
+        for k in deleted:
+            del(self.subForms[k])
+            try:
+                del(self.subFormInputMode[k])
+            except KeyError:
+                pass
+        for name in self.context.keys():
+            if not self.subForms.has_key(name):
+                self.setUpForm(name,self.context[name],
+                               self.itemFormFactory.inputMode,
+                               ignore_request=True)
+            yield self.subForms[name]
+
+
+    def refreshSubActionNames(self):
+        availableActions = set()
+        for subForm in self.getForms():
+            availableActions.update([action.__name__ for action in \
+                                     subForm.availableParentActions()])
+        self.subActionNames = []
+        if hasattr(self.itemFormFactory,'actions'):
+            for action in self.itemFormFactory.actions:
+                name = '%s.%s' % (self.prefix,action.__name__)
+                if name in availableActions:
+                    self.subActionNames.append(name)
+                    
+            
+    def checkInputMode(self):
+        self.subFormInputMode = {}
+        inputMode = None
+        for action in self.itemFormFactory.actions:
+            name = '%s.%s' % (self.prefix,action.__name__)
+            if name in self.request.form and getattr(action,
+                                'inputMode', None) is not None:
+                inputMode = action.inputMode
+                break
+        if inputMode is None:
+            return
+        inputField = None
+        if len(self.context) > 0:
+            for name, item in self.context.items():
+                break
+            tmpForm = self.newSubForm(item)
+            for field in tmpForm.form_fields:
+                if not field.for_display and field.__name__ not in tmpForm.forceInput:
+                    inputField = field
+                    break
+            for name in self.context.keys():
+                prefix = self.prefix + '.' + name + '.' + field.__name__
+                self.subFormInputMode[name] = (prefix in self.request.form)
+                
+    def updateSelection(self):
+        for field in self.itemFormFactory.form_fields:
+            if issubclass(field.field.interface,ISelection):
+                form_fields = form.Fields(field)
+                for name,item in self.context.items():
+                    sForm = SelectionForm(item, self.request, form_fields)
+                    prefix = (self.prefix and self.prefix+'.' or '') + name
+                    sForm.setPrefix(prefix)
+                    sForm.form_fields = form_fields
+                    sForm.setUpWidgets()
+                    data = {}
+                    try:
+                        form.getWidgetsData(sForm.widgets, sForm.prefix, data)
+                    except:
+                       pass
+                    form.applyChanges(sForm.context, sForm.form_fields, data)
+                return
+
+    def newSubForm(self,item):
+
+        """creates a new instance from the itemFormFactory for
+        temporary usage"""
+        
+        return self.itemFormFactory(item,self.request,self)
+
+    def availableSubActions(self):
+        if self.subActionNames:
+            for name in self.subActionNames:
+                # remove the prefix of our form because, the actions in
+                # the class variable have no prefix in their name
+                actionName = name[len(self.prefix)+1:]
+                action = self.itemFormFactory.actions.byname[actionName]
+                action = copy.copy(action)
+                action.__name__ = name
+                yield action
+
+
+class SelectionForm(form.FormBase):
+    
+    def __init__(self, context, request, form_fields):
+        self.form_fields = form_fields
+        self.request = request
+        self.context = getMultiAdapter([context,self],IFormLocation)

Added: z3c.multiform/Sandbox/src/z3c/multiform/resources/sort_down.gif
===================================================================
(Binary files differ)


Property changes on: z3c.multiform/Sandbox/src/z3c/multiform/resources/sort_down.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: z3c.multiform/Sandbox/src/z3c/multiform/resources/sort_up.gif
===================================================================
(Binary files differ)


Property changes on: z3c.multiform/Sandbox/src/z3c/multiform/resources/sort_up.gif
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: z3c.multiform/Sandbox/src/z3c/multiform/selection.py
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/selection.py	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/selection.py	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,138 @@
+from zope.interface import implements
+from zope.proxy import ProxyBase, getProxiedObject
+from zope.security.checker import NamesChecker,defineChecker
+from zope.security.proxy import removeSecurityProxy
+from zope.app.decorator import DecoratorSpecificationDescriptor
+from zope.app.decorator import DecoratedSecurityCheckerDescriptor
+from zope.app.location import location
+
+from interfaces import ISelection,IFormLocation
+
+
+class FormLocationSelection(object):
+
+    __doc__ = """Form Location-object proxy
+
+    This is a non-picklable proxy that can be put around objects that
+    implement `ILocation`.
+    >>> from zope.publisher.browser import TestRequest
+    >>> from zope import interface
+    >>> class L(object):
+    ...     __name__ = u'name'
+    >>> l= L()
+    >>> class F(object):
+    ...     request = TestRequest()
+    ...     context = 1
+    ...     prefix = u"form.n1"
+    >>> f = F()
+    >>> p = FormLocationProxy(l, f)
+    >>> p = FormLocationSelection(p)
+    >>> p.selected
+    False
+    >>> p.selected = True
+    >>> p.selected
+    True
+    >>> p.selected = False
+    >>> p.selected
+    False
+    """
+
+    implements(ISelection)
+
+    def __init__(self,context):
+        self.context = context
+
+    def _setSelected(self,v):
+        key = '_mf_selection.' + removeSecurityProxy(
+            self.context.__form__).prefix
+        form = removeSecurityProxy(self.context.__form__)
+        form.request.form[key] = v
+
+    def _getSelected(self):
+        key = '_mf_selection.' + removeSecurityProxy(
+            self.context.__form__).prefix
+        res =  removeSecurityProxy(
+            self.context.__form__).request.form.get(key,False)
+        return res
+
+    selected = property(_getSelected ,_setSelected)
+        
+
+class FormLocationProxy(ProxyBase):
+
+    __doc__ = """Form Location-object proxy
+
+    XXX the attributes of the form are not available because of a
+    security proxy issue
+
+    This is a non-picklable proxy that can be put around objects that
+    implement `ILocation`.
+
+    >>> from zope import interface
+    >>> class IMarker(interface.Interface): pass
+    >>> class L(object):
+    ...     x = 1
+    >>> l = L()
+    >>> interface.directlyProvides(l,IMarker)
+    >>> p = FormLocationProxy(l, "Form")
+    >>> p.x
+    1
+    >>> p.x=2
+    >>> p.x
+    2
+    >>> l.x
+    2
+    >>> p.__form__
+    'Form'
+
+    >>> IFormLocation.providedBy(p)
+    True
+
+    >>> IMarker.providedBy(p)
+    True
+
+    >>> import pickle
+    >>> p2 = pickle.dumps(p)
+    Traceback (most recent call last):
+    ...
+    TypeError: Not picklable
+
+    Proxies should get their doc strings from the object they proxy:
+
+    >>> p.__doc__ == l.__doc__
+    True
+
+    """
+
+    implements(IFormLocation)
+
+    __slots__ = '__form__'
+    __safe_for_unpickling__ = True
+
+    def __new__(self, ob, form):
+        return ProxyBase.__new__(self, ob)
+
+    def __init__(self, ob, form):
+        ProxyBase.__init__(self, ob)
+        self.__form__ = form
+
+    def __reduce__(self, proto=None):
+        raise TypeError("Not picklable")
+
+
+    __doc__ = location.ClassAndInstanceDescr(
+        lambda inst: getProxiedObject(inst).__doc__,
+        lambda cls, __doc__ = __doc__: __doc__,
+        )
+    
+    __reduce_ex__ = __reduce__
+
+    __providedBy__ = DecoratorSpecificationDescriptor()
+
+    __Security_checker__ = DecoratedSecurityCheckerDescriptor()
+
+formLocationChecker = NamesChecker(['__form__'])
+defineChecker(FormLocationProxy, formLocationChecker)
+
+
+

Added: z3c.multiform/Sandbox/src/z3c/multiform/selections.txt
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/selections.txt	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/selections.txt	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,14 @@
+======================
+ Multiform Selections
+======================
+
+Selecting subforms of a multiform is a common task which is supported
+by the multiform package. Selections are implemented by the ISelection
+interface. This interface can be adapted to from objects which
+implement IFormLocation which in turn can be adapted to from ILocation
+and IForm. The IFormLocation adpater is a proxy which provides the
+``__form_name__`` attribute.
+
+The ISelection implementation in this package uses the request to
+store the selection state, by using the ILocationForm interface.
+

Added: z3c.multiform/Sandbox/src/z3c/multiform/sort.py
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/sort.py	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/sort.py	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,35 @@
+from zope.interface import implements
+
+from interfaces import ISorter
+
+
+class SchemaSorter(object):
+
+    implements(ISorter)
+
+    def __init__(self,schema,field):
+        self.schema = schema
+        self.name = field.__name__
+        self.field = field
+
+    def sort(self,seq):
+
+        l = []
+        def _v(o):
+            o = self.schema(o)
+            field=self.field.bind(o)
+            v = field.get(o)
+            if hasattr(field,'vocabulary'):
+                # XXX maybe this is an adapter issue
+                v = field.vocabulary.getTerm(v).token
+            return v
+        def _cmp(a,b):
+            # none comparison
+            a,b = a[0],b[0]
+            if a==None and b: return -1
+            if b==None and a: return 1
+            return cmp(a,b)
+        for item in seq:
+            l.append((_v(item[1]),item))
+        l.sort(_cmp)
+        return map(lambda i:i[1],l)        

Added: z3c.multiform/Sandbox/src/z3c/multiform/tests.py
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/tests.py	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/tests.py	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1,117 @@
+import doctest
+import unittest
+
+from zope import component
+from zope.testing.doctestunit import DocFileSuite, DocTestSuite
+from zope.app.testing import setup
+import zope.schema.interfaces
+import zope.app.form.browser
+import zope.publisher.interfaces.browser
+import zope.app.form.interfaces
+from zope.formlib import form
+
+import interfaces
+import gridform
+import multiform
+import selection
+import sort
+
+
+def setUp(test):
+    setup.placefulSetUp()
+
+    component.provideAdapter(
+        zope.app.form.browser.TextWidget,
+        [zope.schema.interfaces.ITextLine,
+         zope.publisher.interfaces.browser.IBrowserRequest,
+         ],
+        zope.app.form.interfaces.IInputWidget,
+        )
+    component.provideAdapter(
+        zope.app.form.browser.UnicodeDisplayWidget,
+        [zope.schema.interfaces.ITextLine,
+         zope.publisher.interfaces.browser.IBrowserRequest,
+         ],
+        zope.app.form.interfaces.IDisplayWidget,
+        )
+    component.provideAdapter(
+        zope.app.form.browser.boolwidgets.BooleanDisplayWidget,
+        [zope.schema.interfaces.IBool,
+         zope.publisher.interfaces.browser.IBrowserRequest,
+         ],
+        zope.app.form.interfaces.IDisplayWidget,
+        )
+    component.provideAdapter(
+        zope.app.form.browser.CheckBoxWidget,
+        [zope.schema.interfaces.IBool,
+         zope.publisher.interfaces.browser.IBrowserRequest,
+         ],
+        zope.app.form.interfaces.IInputWidget,
+        )
+    component.provideAdapter(
+        zope.app.form.browser.UnicodeDisplayWidget,
+        [zope.schema.interfaces.IInt,
+         zope.publisher.interfaces.browser.IBrowserRequest,
+         ],
+        zope.app.form.interfaces.IDisplayWidget,
+        )
+    component.provideAdapter(
+        zope.app.form.browser.IntWidget,
+        [zope.schema.interfaces.IInt,
+         zope.publisher.interfaces.browser.IBrowserRequest,
+         ],
+        zope.app.form.interfaces.IInputWidget,
+        )
+    component.provideAdapter(
+        selection.FormLocationProxy,
+        [zope.app.location.interfaces.ILocation,
+         zope.formlib.interfaces.IForm
+         ],
+        interfaces.IFormLocation
+        )
+    component.provideAdapter(
+        selection.FormLocationSelection,
+        [interfaces.IFormLocation],
+        interfaces.ISelection
+        )
+    component.provideAdapter(
+        sort.SchemaSorter,
+        [zope.interface.Interface,
+         zope.schema.interfaces.IField],
+        interfaces.ISorter
+        )    
+    component.provideAdapter(gridform.default_grid_template,
+                             name="default")
+    component.provideAdapter(gridform.default_griditem_template,
+                             name="default")
+    component.provideAdapter(form.render_submit_button, name='render')
+    component.provideAdapter(multiform.render_submit_button, name='render')
+    
+def tearDown(test):
+    setup.placefulTearDown()
+
+
+def test_suite():
+    
+    return unittest.TestSuite(
+        (
+        DocTestSuite('multiform.selection',
+                     optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+                     ),
+        DocFileSuite('README.txt',
+                     setUp=setUp, tearDown=tearDown,
+                     optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+                     ),
+        DocFileSuite('actions.txt',
+                     setUp=setUp, tearDown=tearDown,
+                     optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+                     ),
+        DocFileSuite('gridform.txt',
+                     setUp=setUp, tearDown=tearDown,
+                     optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+                     ),
+        ))
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')

Added: z3c.multiform/Sandbox/src/z3c/multiform/z3c.multiform-configure.zcml
===================================================================
--- z3c.multiform/Sandbox/src/z3c/multiform/z3c.multiform-configure.zcml	2006-04-15 20:45:33 UTC (rev 67064)
+++ z3c.multiform/Sandbox/src/z3c/multiform/z3c.multiform-configure.zcml	2006-04-15 20:53:48 UTC (rev 67065)
@@ -0,0 +1 @@
+<include package="z3c.multiform"/>
\ No newline at end of file



More information about the Checkins mailing list