Commit 31d4a7c2 authored by Hanno Schlichting's avatar Hanno Schlichting

Backported c107134 from trunk

parent 140ff24e
...@@ -43,6 +43,7 @@ eggs = ...@@ -43,6 +43,7 @@ eggs =
ExtensionClass ExtensionClass
Persistence Persistence
RestrictedPython RestrictedPython
five.formlib
tempstorage tempstorage
zLOG zLOG
zope.annotation zope.annotation
...@@ -66,7 +67,6 @@ eggs = ...@@ -66,7 +67,6 @@ eggs =
zope.event zope.event
zope.exceptions zope.exceptions
zope.filerepresentation zope.filerepresentation
zope.formlib
zope.hookable zope.hookable
zope.i18n zope.i18n
zope.i18nmessageid zope.i18nmessageid
......
...@@ -13,6 +13,12 @@ Bugs Fixed ...@@ -13,6 +13,12 @@ Bugs Fixed
- Fixed a SyntaxError in utilities/load_site.py script. - Fixed a SyntaxError in utilities/load_site.py script.
Features Added
++++++++++++++
- Moved zope.formlib / zope.app.form integration into a separate package
called five.formlib.
Zope 2.12.2 (2009-12-22) Zope 2.12.2 (2009-12-22)
------------------------ ------------------------
......
...@@ -94,6 +94,7 @@ params = dict(name='Zope2', ...@@ -94,6 +94,7 @@ params = dict(name='Zope2',
'ZConfig', 'ZConfig',
'ZODB3', 'ZODB3',
'docutils', 'docutils',
'five.formlib',
'pytz', 'pytz',
'setuptools', 'setuptools',
'tempstorage', 'tempstorage',
...@@ -108,7 +109,6 @@ params = dict(name='Zope2', ...@@ -108,7 +109,6 @@ params = dict(name='Zope2',
'zope.deferredimport', 'zope.deferredimport',
'zope.event', 'zope.event',
'zope.exceptions', 'zope.exceptions',
'zope.formlib',
'zope.i18n [zcml]', 'zope.i18n [zcml]',
'zope.i18nmessageid', 'zope.i18nmessageid',
'zope.interface', 'zope.interface',
...@@ -131,7 +131,6 @@ params = dict(name='Zope2', ...@@ -131,7 +131,6 @@ params = dict(name='Zope2',
'zope.testing', 'zope.testing',
'zope.traversing', 'zope.traversing',
'zope.viewlet', 'zope.viewlet',
'zope.app.form',
'zope.app.publication', 'zope.app.publication',
'zope.app.publisher', 'zope.app.publisher',
'zope.app.schema', 'zope.app.schema',
......
...@@ -32,8 +32,6 @@ v1.3 ...@@ -32,8 +32,6 @@ v1.3
v1.4 v1.4
---- ----
- namedtemplate in Five.formlib?
- l10n (philikon) - l10n (philikon)
- Figure out where add-view redirects should go. - Figure out where add-view redirects should go.
......
...@@ -258,7 +258,7 @@ High-level security ...@@ -258,7 +258,7 @@ High-level security
>>> protected_view_names = [ >>> protected_view_names = [
... 'eagle.txt', 'falcon.html', 'owl.html', 'flamingo.html', ... 'eagle.txt', 'falcon.html', 'owl.html', 'flamingo.html',
... 'condor.html', 'protectededitform.html'] ... 'condor.html']
>>> >>>
>>> public_view_names = [ >>> public_view_names = [
... 'public_attribute_page', ... 'public_attribute_page',
......
...@@ -232,15 +232,6 @@ ...@@ -232,15 +232,6 @@
class=".pages.SimpleView" class=".pages.SimpleView"
permission="zope2.Public" permission="zope2.Public"
/> />
<!-- XXX this should really be in Five.form.tests -->
<!-- protected edit form for permission check -->
<browser:editform
schema="Products.Five.tests.testing.simplecontent.ISimpleContent"
name="protectededitform.html"
permission="zope2.ViewManagementScreens"
/>
<!-- stuff that we'll override in overrides.zcml --> <!-- stuff that we'll override in overrides.zcml -->
<browser:page <browser:page
......
...@@ -60,7 +60,7 @@ those; we start by adding two users: ...@@ -60,7 +60,7 @@ those; we start by adding two users:
>>> protected_view_names = [ >>> protected_view_names = [
... 'eagle.txt', 'falcon.html', 'owl.html', 'flamingo.html', ... 'eagle.txt', 'falcon.html', 'owl.html', 'flamingo.html',
... 'condor.html', 'protectededitform.html'] ... 'condor.html']
>>> >>>
>>> public_view_names = [ >>> public_view_names = [
... 'public_attribute_page', ... 'public_attribute_page',
......
============================
zope.formlib support in Five
============================
Five supports zope.formlib, an alternative for constructing add, edit, and
other forms based on schema. See zope/formlib/form.txt for a thorough
description of the functionality provided by formlib.
Formlib forms are normal view classes, registered as browser pages. Where
in a pure Zope-3 context you would derive from one of the zope.formlib.form
baseclasses, you now need to derive from one of the baseclasses provided by
Products.Five.formlib.formbase.
In almost all cases you need to import from zope.formlib.from as well -
e.g. Fields and Actions in order to define and select fields and to add
actions to your form.
See Products.Five.formlib.tests for a minimal example.
############################################################################## # BBB
# from five.formlib import AddView
# Copyright (c) 2004, 2005 Zope Corporation and Contributors. from five.formlib import EditView
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (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.
#
##############################################################################
"""Add and edit views
$Id$
"""
import sys
from datetime import datetime
import transaction
from zope.event import notify
from zope.lifecycleevent import ObjectCreatedEvent, ObjectModifiedEvent
from zope.lifecycleevent import Attributes
from zope.location.interfaces import ILocation
from zope.location import LocationProxy
from zope.schema.interfaces import ValidationError
from zope.i18nmessageid import MessageFactory
_ = MessageFactory('zope')
from zope.app.form.browser.submit import Update
from zope.app.form.interfaces import IInputWidget
from zope.app.form.interfaces import WidgetsError
from zope.app.form.utility import setUpEditWidgets, applyWidgetsChanges
from zope.app.form.utility import setUpWidgets, getWidgetsData
from Products.Five.browser import BrowserView
from Products.Five.browser.decode import processInputs, setPageEncoding
from Products.Five.browser.pagetemplatefile import ZopeTwoPageTemplateFile
class EditView(BrowserView):
"""Simple edit-view base class
Subclasses should provide a schema attribute defining the schema
to be edited.
"""
errors = ()
update_status = None
label = ''
charsets = None
# Fall-back field names computes from schema
fieldNames = property(lambda self: getFieldNamesInOrder(self.schema))
# Fall-back template
generated_form = ZopeTwoPageTemplateFile('edit.pt')
def __init__(self, context, request):
BrowserView.__init__(self, context, request)
processInputs(self.request, self.charsets)
setPageEncoding(self.request)
self._setUpWidgets()
def _setUpWidgets(self):
adapted = self.schema(self.context)
if adapted is not self.context:
if not ILocation.providedBy(adapted):
adapted = LocationProxy(adapted)
adapted.__parent__ = self.context
self.adapted = adapted
setUpEditWidgets(self, self.schema, source=self.adapted,
names=self.fieldNames)
def setPrefix(self, prefix):
for widget in self.widgets():
widget.setPrefix(prefix)
def widgets(self):
return [getattr(self, name+'_widget')
for name in self.fieldNames]
def changed(self):
# This method is overridden to execute logic *after* changes
# have been made.
pass
def update(self):
if self.update_status is not None:
# We've been called before. Just return the status we previously
# computed.
return self.update_status
status = ''
content = self.adapted
if Update in self.request.form.keys():
changed = False
try:
changed = applyWidgetsChanges(self, self.schema,
target=content, names=self.fieldNames)
# We should not generate events when an adapter is used.
# That's the adapter's job. We need to unwrap the objects to
# compare them, as they are wrapped differently.
# Additionally, we can't use Acquisition.aq_base() because
# it strangely returns different objects for these two even
# when they are identical. In particular
# aq_base(self.adapted) != self.adapted.aq_base :-(
if changed and getattr(self.context, 'aq_base', self.context)\
is getattr(self.adapted, 'aq_base', self.adapted):
description = Attributes(self.schema, *self.fieldNames)
notify(ObjectModifiedEvent(content, description))
except WidgetsError, errors:
self.errors = errors
status = _("An error occurred.")
transaction.abort()
else:
setUpEditWidgets(self, self.schema, source=self.adapted,
ignoreStickyValues=True,
names=self.fieldNames)
if changed:
self.changed()
formatter = self.request.locale.dates.getFormatter(
'dateTime', 'medium')
status = _("Updated on ${date_time}",
mapping={'date_time':
formatter.format(datetime.utcnow())})
self.update_status = status
return status
class AddView(EditView):
"""Simple edit-view base class.
Subclasses should provide a schema attribute defining the schema
to be edited.
"""
def _setUpWidgets(self):
setUpWidgets(self, self.schema, IInputWidget, names=self.fieldNames)
def update(self):
if self.update_status is not None:
# We've been called before. Just return the previous result.
return self.update_status
if self.request.form.has_key(Update):
self.update_status = ''
try:
data = getWidgetsData(self, self.schema, names=self.fieldNames)
self.createAndAdd(data)
except WidgetsError, errors:
self.errors = errors
self.update_status = _("An error occurred.")
return self.update_status
self.request.response.redirect(self.nextURL())
return self.update_status
def create(self, *args, **kw):
"""Do the actual instantiation."""
# hack to please typical Zope 2 factories, which expect id and title
# Any sane schema will use a unicode title, and may fail on a
# non-unicode one.
args = ('tmp_id', u'Temporary title') + args
return self._factory(*args, **kw)
def createAndAdd(self, data):
"""Add the desired object using the data in the data argument.
The data argument is a dictionary with the data entered in the form.
"""
args = []
if self._arguments:
for name in self._arguments:
args.append(data[name])
kw = {}
if self._keyword_arguments:
for name in self._keyword_arguments:
if name in data:
kw[str(name)] = data[name]
content = self.create(*args, **kw)
adapted = self.schema(content)
errors = []
if self._set_before_add:
for name in self._set_before_add:
if name in data:
field = self.schema[name]
try:
field.set(adapted, data[name])
except ValidationError:
errors.append(sys.exc_info()[1])
if errors:
raise WidgetsError(*errors)
notify(ObjectCreatedEvent(content))
content = self.add(content)
adapted = self.schema(content)
if self._set_after_add:
for name in self._set_after_add:
if name in data:
field = self.schema[name]
try:
field.set(adapted, data[name])
except ValidationError:
errors.append(sys.exc_info()[1])
# We have modified the object, so we need to publish an
# object-modified event:
description = Attributes(self.schema, *self._set_after_add)
notify(ObjectModifiedEvent(content, description))
if errors:
raise WidgetsError(*errors)
return content
def add(self, content):
return self.context.add(content)
def nextURL(self):
return self.context.nextURL()
<html metal:use-macro="context/@@standard_macros/page"
i18n:domain="zope">
<body>
<div metal:fill-slot="body">
<div metal:define-macro="addform">
<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"
>Add something</h3>
<p tal:define="status view/update"
tal:condition="status"
tal:content="status" />
<p tal:condition="view/errors" i18n:translate="">
There are <strong tal:content="python:len(view.errors)"
i18n:name="num_errors">6</strong> input errors.
</p>
<div metal:define-slot="extra_info" tal:replace="nothing">
</div>
<div class="row" metal:define-slot="extra_top" tal:replace="nothing">
<div class="label">Extra top</div>
<div class="label"><input type="text" style="width:100%" /></div>
</div>
<div metal:use-macro="context/@@form_macros/widget_rows" />
<div class="separator"></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 class="separator"></div>
</div>
<br/><br/>
<div class="row">
<div class="controls"><hr />
<input type='submit' value='Refresh'
i18n:attributes='value refresh-button' />
<input type='submit' value='Add' name='UPDATE_SUBMIT'
i18n:attributes='value add-button' />
<span tal:condition="context/nameAllowed|nothing" tal:omit-tag="">
&nbsp;&nbsp;<b i18n:translate="">Object Name</b>&nbsp;&nbsp;
<input type='text' name='add_input_name'
tal:attributes="value context/contentName" />
</span>
</div>
</div>
<div class="row" metal:define-slot="extra_buttons" tal:replace="nothing">
</div>
<div class="separator"></div>
</form>
</div>
</div>
</body>
</html>
<configure xmlns="http://namespaces.zope.org/zope" <configure xmlns="http://namespaces.zope.org/zope">
xmlns:browser="http://namespaces.zope.org/browser">
<include package="zope.app.form.browser" /> <include package="five.formlib" />
<browser:page
for="*"
name="form_macros"
permission="zope2.View"
class=".macros.FormMacros"
allowed_interface="zope.interface.common.mapping.IItemMapping"
/>
</configure> </configure>
\ No newline at end of file
<tal:tag condition="view/update"/>
<html metal:use-macro="context/@@standard_macros/view"
i18n:domain="zope">
<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" />
<p tal:condition="view/errors" i18n:translate="">
There are <strong tal:content="python:len(view.errors)"
i18n:name="num_errors">6</strong> input errors.
</p>
<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 metal:use-macro="context/@@form_macros/widget_rows" />
<div class="separator"></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 class="separator"></div>
</div>
<div class="row">
<div class="controls">
<input type="submit" value="Refresh"
i18n:attributes="value refresh-button" />
<input type="submit" name="UPDATE_SUBMIT" value="Change"
i18n:attributes="value submit-button"/>
</div>
</div>
<div class="row" metal:define-slot="extra_buttons" tal:replace="nothing">
</div>
<div class="separator"></div>
</form>
</div>
</div>
</body>
</html>
############################################################################## # BBB
# from five.formlib.macros import FormMacros
# Copyright (c) 2004, 2005 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (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.
#
##############################################################################
"""Form macros
$Id$
"""
from Products.Five.skin.standardmacros import StandardMacros
# copy of zope.app.form.browser.macros.FormMacros
class FormMacros(StandardMacros):
macro_pages = ('widget_macros', 'addform_macros')
<configure <configure xmlns="http://namespaces.zope.org/zope">
xmlns="http://namespaces.zope.org/zope"
xmlns:meta="http://namespaces.zope.org/meta">
<meta:directives namespace="http://namespaces.zope.org/browser"> <include package="five.formlib" file="meta.zcml" />
<meta:complexDirective
name="editform"
schema="zope.app.form.browser.metadirectives.IEditFormDirective"
handler=".metaconfigure.EditFormDirective"
>
<meta:subdirective
name="widget"
schema="zope.app.form.browser.metadirectives.IWidgetSubdirective"
/>
</meta:complexDirective>
<meta:complexDirective
name="addform"
schema="zope.app.form.browser.metadirectives.IAddFormDirective"
handler=".metaconfigure.AddFormDirective"
>
<meta:subdirective
name="widget"
schema="zope.app.form.browser.metadirectives.IWidgetSubdirective"
/>
</meta:complexDirective>
</meta:directives>
</configure> </configure>
############################################################################## # BBB
# from five.formlib.metaconfigure import EditViewFactory
# Copyright (c) 2004, 2005 Zope Corporation and Contributors. from five.formlib.metaconfigure import FiveFormDirective
# All Rights Reserved. from five.formlib.metaconfigure import EditFormDirective
# from five.formlib.metaconfigure import AddViewFactory
# This software is subject to the provisions of the Zope Public License, from five.formlib.metaconfigure import AddFormDirective
# Version 2.1 (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.
#
##############################################################################
"""Edit form directives
$Id$
"""
from App.class_init import InitializeClass
from ExtensionClass import Base
import zope.component
from zope.interface import Interface
from zope.i18nmessageid import MessageFactory
_ = MessageFactory('zope')
from zope.app.publisher.browser.menumeta import menuItemDirective
from zope.app.form.browser.metaconfigure import BaseFormDirective
from zope.browser.interfaces import IAdding
from Products.Five.form import EditView, AddView
from Products.Five.metaclass import makeClass
from Products.Five.security import protectClass
from Products.Five.browser.pagetemplatefile import ZopeTwoPageTemplateFile
from Products.Five.browser.metaconfigure import makeClassForTemplate
def EditViewFactory(name, schema, label, permission, layer,
template, default_template, bases, for_, fields,
fulledit_path=None, fulledit_label=None, menu=u''):
class_ = makeClassForTemplate(template, globals(), used_for=schema,
bases=bases)
class_.schema = schema
class_.label = label
class_.fieldNames = fields
class_.fulledit_path = fulledit_path
if fulledit_path and (fulledit_label is None):
fulledit_label = "Full edit"
class_.fulledit_label = fulledit_label
class_.generated_form = ZopeTwoPageTemplateFile(default_template)
if layer is None:
layer = IDefaultBrowserLayer
s = zope.component.getGlobalSiteManager()
s.registerAdapter(class_, (for_, layer), Interface, name)
# Reminder: the permission we got has already been processed by
# BaseFormDirective, that means that zope.Public has been
# translated to the CheckerPublic object
protectClass(class_, permission)
InitializeClass(class_)
class FiveFormDirective(BaseFormDirective):
def _processWidgets(self):
if self._widgets:
customWidgetsObject = makeClass(
'CustomWidgetsMixin', (Base,), self._widgets)
self.bases = self.bases + (customWidgetsObject,)
class EditFormDirective(FiveFormDirective):
view = EditView
default_template = 'edit.pt'
title = _('Edit')
def _handle_menu(self):
if self.menu:
menuItemDirective(
self._context, self.menu, self.for_ or self.schema,
'@@' + self.name, self.title, permission=self.permission,
layer=self.layer)
def __call__(self):
self._processWidgets()
self._handle_menu()
self._context.action(
discriminator=self._discriminator(),
callable=EditViewFactory,
args=self._args(),
kw={'menu': self.menu},
)
def AddViewFactory(name, schema, label, permission, layer,
template, default_template, bases, for_,
fields, content_factory, arguments,
keyword_arguments, set_before_add, set_after_add,
menu=u''):
class_ = makeClassForTemplate(template, globals(), used_for=schema,
bases=bases)
class_.schema = schema
class_.label = label
class_.fieldNames = fields
class_._factory = content_factory
class_._arguments = arguments
class_._keyword_arguments = keyword_arguments
class_._set_before_add = set_before_add
class_._set_after_add = set_after_add
class_.generated_form = ZopeTwoPageTemplateFile(default_template)
if layer is None:
layer = IDefaultBrowserLayer
s = zope.component.getGlobalSiteManager()
s.registerAdapter(class_, (for_, layer), Interface, name)
# Reminder: the permission we got has already been processed by
# BaseFormDirective, that means that zope.Public has been
# translated to the CheckerPublic object
protectClass(class_, permission)
InitializeClass(class_)
class AddFormDirective(FiveFormDirective):
view = AddView
default_template = 'add.pt'
for_ = IAdding
# default add form information
description = None
content_factory = None
arguments = None
keyword_arguments = None
set_before_add = None
set_after_add = None
def _handle_menu(self):
if self.menu or self.title:
if (not self.menu) or (not self.title):
raise ValueError("If either menu or title are specified, "
"they must both be specified")
# Add forms are really for IAdding components, so do not use
# for=self.schema.
menuItemDirective(
self._context, self.menu, self.for_, '@@' + self.name,
self.title, permission=self.permission, layer=self.layer,
description=self.description)
def _handle_arguments(self, leftover=None):
schema = self.schema
fields = self.fields
arguments = self.arguments
keyword_arguments = self.keyword_arguments
set_before_add = self.set_before_add
set_after_add = self.set_after_add
if leftover is None:
leftover = fields
if arguments:
missing = [n for n in arguments if n not in fields]
if missing:
raise ValueError("Some arguments are not included in the form",
missing)
optional = [n for n in arguments if not schema[n].required]
if optional:
raise ValueError("Some arguments are optional, use"
" keyword_arguments for them",
optional)
leftover = [n for n in leftover if n not in arguments]
if keyword_arguments:
missing = [n for n in keyword_arguments if n not in fields]
if missing:
raise ValueError(
"Some keyword_arguments are not included in the form",
missing)
leftover = [n for n in leftover if n not in keyword_arguments]
if set_before_add:
missing = [n for n in set_before_add if n not in fields]
if missing:
raise ValueError(
"Some set_before_add are not included in the form",
missing)
leftover = [n for n in leftover if n not in set_before_add]
if set_after_add:
missing = [n for n in set_after_add if n not in fields]
if missing:
raise ValueError(
"Some set_after_add are not included in the form",
missing)
leftover = [n for n in leftover if n not in set_after_add]
self.set_after_add += leftover
else:
self.set_after_add = leftover
def __call__(self):
self._processWidgets()
self._handle_menu()
self._handle_arguments()
self._context.action(
discriminator=self._discriminator(),
callable=AddViewFactory,
args=self._args()+(self.content_factory, self.arguments,
self.keyword_arguments,
self.set_before_add, self.set_after_add),
kw={'menu': self.menu},
)
<fieldset>
<legend tal:content="context/legendTitle"
i18n:translate="">The Legend</legend>
<div class="row" tal:repeat="widget context/subwidgets">
<tal:comment condition="nothing">
This is why we have to duplicate this template: we want to look
up the @@form_macros browser page from something that's
definitely five:traversable (it doesn't really matter where we
look it up, just *that* we look it up); we know the object we're
editing is five:traversable, so we just use that. Yes, three
times context. Weird, eh?
</tal:comment>
<metal:block use-macro="context/context/context/@@form_macros/widget_row" />
</div>
</fieldset>
############################################################################## # BBB
# from five.formlib.objectwidget import ObjectWidgetView
# Copyright (c) 2005 Zope Corporation and Contributors. from five.formlib.objectwidget import ObjectWidget
# All Rights Reserved. from five.formlib.objectwidget import ObjectWidgetClass
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (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.
#
##############################################################################
"""Five-compatible version of ObjectWidget
This is needed because ObjectWidget uses ViewPageTemplateFile whose
macro definition is unfortunately incompatible with ZopeTwoPageTemplateFile.
So this subclass uses ZopeTwoPageTemplateFile for the template that renders
the widget's sub-editform.
$Id$
"""
from AccessControl.SecurityInfo import ClassSecurityInfo
from App.class_init import InitializeClass
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from zope.app.form.browser.objectwidget import ObjectWidget as OWBase
from zope.app.form.browser.objectwidget import ObjectWidgetView as OWVBase
class ObjectWidgetView(OWVBase):
security = ClassSecurityInfo()
security.declareObjectPublic()
template = ViewPageTemplateFile('objectwidget.pt')
InitializeClass(ObjectWidgetView)
class ObjectWidgetClass(OWBase):
def setRenderedValue(self, value):
"""Slightly more robust re-implementation this method."""
# re-call setupwidgets with the content
self._setUpEditWidgets()
for name in self.names:
val = getattr(value, name, None)
if val is None:
# this is where we are more robust than Zope 3.2's
# object widget: we supply subwidgets with the default
# from the schema, not None (Zope 3.2's list widget
# breaks when the rendered value is None)
val = self.context.schema[name].default
self.getSubWidget(name).setRenderedValue(val)
ObjectWidget = ObjectWidgetClass
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:meta="http://namespaces.zope.org/meta"
xmlns:browser="http://namespaces.zope.org/browser"
xmlns:five="http://namespaces.zope.org/five"
xmlns:i18n="http://namespaces.zope.org/i18n"
i18n_domain="formtest">
<!-- make the zope2.Public permission work -->
<meta:redefinePermission from="zope2.Public" to="zope.Public" />
<!-- browser forms -->
<browser:editform
schema=".schemacontent.IFieldContent"
for=".schemacontent.IFieldContent"
name="edit.html"
label="Edit Field Content"
permission="zope2.Public"
/>
<browser:editform
schema=".schemacontent.IComplexSchemaContent"
for=".schemacontent.IComplexSchemaContent"
name="edit.html"
permission="zope2.Public"
class=".schemacontent.ComplexSchemaView"
/>
<view
type="zope.publisher.interfaces.browser.IBrowserRequest"
for="zope.schema.interfaces.IObject"
provides="zope.app.form.interfaces.IInputWidget"
factory="Products.Five.form.objectwidget.ObjectWidget"
permission="zope.Public"
/>
<!-- With a widget override -->
<browser:editform
schema=".schemacontent.IFieldContent"
for=".schemacontent.IFieldContent"
name="widgetoverride.html"
permission="zope2.Public"
>
<widget
field="description"
class="zope.app.form.browser.TextAreaWidget"
/>
</browser:editform>
<browser:addform
schema=".schemacontent.IFieldContent"
content_factory=".schemacontent.FieldContent"
name="addfieldcontent.html"
label="Add Field Content"
permission="zope2.Public"
/>
<browser:addform
schema=".schemacontent.IFieldContent"
content_factory=".schemacontent.FieldContent"
name="addwidgetoverride.html"
permission="zope2.Public"
set_before_add="title description somenumber somelist">
<widget
field="description"
class="zope.app.form.browser.TextAreaWidget"
/>
</browser:addform>
<browser:addform
schema=".schemacontent.IFieldContent"
content_factory=".schemacontent.FieldContent"
name="protectedaddform.html"
permission="zope2.ViewManagementScreens"
/>
<subscriber
for=".schemacontent.IFieldContent
zope.lifecycleevent.interfaces.IObjectModifiedEvent"
handler=".schemacontent.modifiedSubscriber"
/>
<subscriber
for=".schemacontent.IFieldContent
zope.lifecycleevent.interfaces.IObjectCreatedEvent"
handler=".schemacontent.createdSubscriber"
/>
<i18n:registerTranslations directory="locales"/>
</configure>
This diff is collapsed.
##############################################################################
#
# Copyright (c) 2005 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (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.
#
##############################################################################
msgid ""
msgstr ""
"Project-Id-Version: Five form tests\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: 2005-07-29 11:38+0100\n"
"Last-Translator: \n"
"Language-Team: Five Developers <z3-five@zope.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Title"
msgstr "Titel"
msgid "A short description of the event."
msgstr "Eine kurze Beschreibung des Ereignisses."
msgid "Description"
msgstr "Beschreibung"
msgid "A long description of the event."
msgstr "Eine ausführliche Beschreibung des Ereignisses."
msgid "Some number"
msgstr "Irgendeine Zahl"
msgid "Some List"
msgstr "Irgendeine Liste"
msgid "Some item"
msgstr "Irgendeine Element"
msgid "Edit Field Content"
msgstr "Felderinhalt bearbeiten"
msgid "Add Field Content"
msgstr "Felderinhalt hinzufügen"
##############################################################################
#
# Copyright (c) 2005 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (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.
#
##############################################################################
msgid ""
msgstr ""
"Project-Id-Version: Five form tests\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: Five Developers <z3-five@zope.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Title"
msgstr ""
msgid "A short description of the event."
msgstr ""
msgid "Description"
msgstr ""
msgid "A long description of the event."
msgstr ""
msgid "Some number"
msgstr ""
msgid "Some List"
msgstr ""
msgid "Some item"
msgstr ""
msgid "Edit Field Content"
msgstr ""
msgid "Add Field Content"
msgstr ""
##############################################################################
#
# Copyright (c) 2004, 2005 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (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.
#
##############################################################################
"""Demo schema content
$Id$
"""
from App.class_init import InitializeClass
from OFS.SimpleItem import SimpleItem
from Products.Five.form.objectwidget import ObjectWidget
from zope.i18nmessageid import MessageFactory
from zope.interface import implements, Interface
from zope.schema import TextLine, Text, Object, Int, List
from zope.app.form import CustomWidgetFactory
_ = MessageFactory('formtest')
class IFieldContent(Interface):
title = TextLine(
title=_(u"Title"),
description=_(u"A short description of the event."),
default=u"",
required=True
)
description = Text(
title=_(u"Description"),
description=_(u"A long description of the event."),
default=u"",
required=False
)
somenumber = Int(
title=_(u"Some number"),
default=0,
required=False
)
somelist = List(
title=_(u"Some List"),
value_type=TextLine(title=_(u"Some item")),
default=[],
required=False
)
class FieldContent(SimpleItem):
"""A Viewable piece of content with fields"""
implements(IFieldContent)
meta_type = 'Five FieldContent'
def __init__(self, id, title):
self.id = id
self.title = title
InitializeClass(FieldContent)
def manage_addFieldContent(self, id, title, REQUEST=None):
"""Add the field content"""
id = self._setObject(id, FieldContent(id, title))
return ''
class IComplexSchemaContent(Interface):
fishtype = TextLine(
title=u"Fish type",
description=u"The type of fish",
default=u"It was a lovely little fish. And it went wherever I did go.",
required=False)
fish = Object(
title=u"Fish",
schema=IFieldContent,
description=u"The fishy object",
required=True)
class ComplexSchemaContent(SimpleItem):
implements(IComplexSchemaContent)
meta_type ="Five ComplexSchemaContent"
def __init__(self, id):
self.id = id
self.fish = FieldContent('fish', 'title')
self.fish.description = ""
self.fishtype = 'Lost fishy'
class ComplexSchemaView:
"""Needs a docstring"""
fish_widget = CustomWidgetFactory(ObjectWidget, FieldContent)
InitializeClass(ComplexSchemaContent)
def manage_addComplexSchemaContent(self, id, REQUEST=None):
"""Add the complex schema content"""
id = self._setObject(id, ComplexSchemaContent(id))
return ''
def modifiedSubscriber(content, ev):
"""A simple event handler, which sets a flag on the object"""
content._modified_flag = True
def createdSubscriber(content,ev):
"""A simple event handler, which sets a flag on the object"""
content._created_flag = True
##############################################################################
#
# Copyright (c) 2004, 2005 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (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.
#
##############################################################################
"""Test forms
$Id$
"""
def test_get_widgets_for_schema_fields():
"""
Test widget lookup for schema fields
First, load the configuration files:
>>> import Products.Five
>>> from Products.Five import zcml
>>> zcml.load_config('configure.zcml', Products.Five)
Now for some actual testing...
>>> from zope.schema import Choice, TextLine
>>> salutation = Choice(title=u'Salutation',
... values=("Mr.", "Mrs.", "Captain", "Don"))
>>> contactname = TextLine(title=u'Name')
>>> from zope.publisher.browser import TestRequest
>>> request = TestRequest()
>>> salutation = salutation.bind(request)
>>> contactname = contactname.bind(request)
>>> from zope.component import getMultiAdapter
>>> from zope.app.form.interfaces import IInputWidget
>>> from zope.app.form.browser.textwidgets import TextWidget
>>> from zope.app.form.browser.itemswidgets import DropdownWidget
>>> view1 = getMultiAdapter((contactname, request), IInputWidget)
>>> view1.__class__ == TextWidget
True
>>> view2 = getMultiAdapter((salutation, request), IInputWidget)
>>> view2.__class__ == DropdownWidget
True
Clean up:
>>> from zope.component.testing import tearDown
>>> tearDown()
"""
def test_suite():
import unittest
from zope.testing.doctest import DocTestSuite
from Testing.ZopeTestCase import FunctionalDocFileSuite
return unittest.TestSuite((
DocTestSuite(),
FunctionalDocFileSuite('forms.txt',
package="Products.Five.form.tests",),
))
# package # BBB
\ No newline at end of file \ No newline at end of file
<configure <configure xmlns="http://namespaces.zope.org/zope">
package="zope.formlib"
xmlns="http://namespaces.zope.org/zope"
i18n_domain="zope">
<adapter <include package="five.formlib" />
factory=".form.render_submit_button"
name="render"
/>
<!-- Error view for 'Invalid' -->
<adapter
for="zope.interface.Invalid
zope.publisher.interfaces.browser.IBrowserRequest"
factory=".errors.InvalidErrorView"
permission="zope.Public"
/>
</configure> </configure>
############################################################################## # BBB
# from five.formlib.formbase import FiveFormlibMixin
# Copyright (c) 2006 Zope Corporation and Contributors. from five.formlib.formbase import FormBase
# All Rights Reserved. from five.formlib.formbase import EditFormBase
# from five.formlib.formbase import DisplayFormBase
# This software is subject to the provisions of the Zope Public License, from five.formlib.formbase import AddFormBase
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. from five.formlib.formbase import PageForm
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED from five.formlib.formbase import Form
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED from five.formlib.formbase import PageEditForm
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS from five.formlib.formbase import EditForm
# FOR A PARTICULAR PURPOSE. from five.formlib.formbase import PageDisplayForm
# from five.formlib.formbase import DisplayForm
############################################################################## from five.formlib.formbase import PageAddForm
"""Five baseclasses for zope.formlib.form from five.formlib.formbase import AddForm
from five.formlib.formbase import SubPageForm
$Id$ from five.formlib.formbase import SubPageEditForm
""" from five.formlib.formbase import SubPageDisplayForm
import os.path
import zope.event
import zope.formlib
import zope.lifecycleevent
from zope import interface
from zope.formlib import interfaces, form
from zope.i18nmessageid import MessageFactory
_ = MessageFactory("zope")
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from Products.Five.browser.decode import processInputs, setPageEncoding
_FORMLIB_DIR = os.path.dirname(zope.formlib.__file__)
_PAGEFORM_PATH = os.path.join(_FORMLIB_DIR, 'pageform.pt')
_SUBPAGEFORM_PATH = os.path.join(_FORMLIB_DIR, 'subpageform.pt')
class FiveFormlibMixin(object):
# Overrides the formlib.form.FormBase.template attributes implemented
# using NamedTemplates. NamedTemplates using ViewPageTemplateFile (like
# formlib does by default) cannot work in Zope2.
# XXX Maybe we need to have Five-compatible NamedTemplates?
template = ViewPageTemplateFile(_PAGEFORM_PATH)
# Overrides formlib.form.FormBase.update. Make sure user input is
# decoded first and the page encoding is set before proceeding.
def update(self):
processInputs(self.request)
setPageEncoding(self.request)
super(FiveFormlibMixin, self).update()
class FormBase(FiveFormlibMixin, form.FormBase):
pass
class EditFormBase(FiveFormlibMixin, form.EditFormBase):
pass
class DisplayFormBase(FiveFormlibMixin, form.DisplayFormBase):
pass
class AddFormBase(FiveFormlibMixin, form.AddFormBase):
pass
class PageForm(FormBase):
interface.implements(interfaces.IPageForm)
Form = PageForm
class PageEditForm(EditFormBase):
interface.implements(interfaces.IPageForm)
EditForm = PageEditForm
class PageDisplayForm(DisplayFormBase):
interface.implements(interfaces.IPageForm)
DisplayForm = PageDisplayForm
class PageAddForm(AddFormBase):
interface.implements(interfaces.IPageForm)
AddForm = PageAddForm
class SubPageForm(FormBase):
template = ViewPageTemplateFile(_SUBPAGEFORM_PATH)
interface.implements(interfaces.ISubPageForm)
class SubPageEditForm(EditFormBase):
template = ViewPageTemplateFile(_SUBPAGEFORM_PATH)
interface.implements(interfaces.ISubPageForm)
class SubPageDisplayForm(DisplayFormBase):
template = ViewPageTemplateFile(_SUBPAGEFORM_PATH)
interface.implements(interfaces.ISubPageForm)
# package
\ No newline at end of file
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:meta="http://namespaces.zope.org/meta"
xmlns:browser="http://namespaces.zope.org/browser"
xmlns:five="http://namespaces.zope.org/five"
xmlns:i18n="http://namespaces.zope.org/i18n"
i18n_domain="formtest"
>
<browser:page
name="add_content"
for="*"
class=".view.AddContentForm"
permission="zope2.Public"
/>
<browser:page
name="edit_content"
for=".content.IContent"
class=".view.EditContentForm"
permission="zope2.Public"
/>
</configure>
##############################################################################
#
# Copyright (c) 2004, 2005 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (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.
#
##############################################################################
"""Test content.
$Id$
"""
from App.class_init import InitializeClass
from OFS.SimpleItem import SimpleItem
from zope.i18nmessageid import MessageFactory
from zope.interface import implements
from zope.interface import Interface
from zope.schema import ASCIILine
from zope.schema import List
from zope.schema import TextLine
_ = MessageFactory('formtest')
class IContent(Interface):
id = ASCIILine(
title=_(u"Id"),
description=_(u"The object id."),
default='',
required=True
)
title = TextLine(
title=_(u"Title"),
description=_(u"A short description of the event."),
default=u"",
required=True
)
somelist = List(
title=_(u"Some List"),
value_type=TextLine(title=_(u"Some item")),
default=[],
required=False
)
class Content(SimpleItem):
"""A Viewable piece of content with fields
"""
implements(IContent)
meta_type = 'Five Formlib Test Content'
def __init__(self, id, title, somelist=None):
self.id = id
self.title = title
self.somelist = somelist
InitializeClass(Content)
Testing formlib integration
===========================
This doctest is will test the Five formlib support and to provide some
examples.
It will not test the actual formlib functionality. See
zope/formlib/form.txt for tests and more explanations of zope.formlib
Before we can begin, we need to set up a few things. We need a manager
account:
>>> uf = self.folder.acl_users
>>> uf._doAddUser('manager', 'r00t', ['Manager'], [])
We need to configure all of Five and the necessary formlib components for
this test:
>>> from Products.Five import zcml
>>> import Products.Five
>>> zcml.load_config('meta.zcml', Products.Five)
>>> import Products.Five.form.tests
>>> zcml.load_config('configure.zcml', package=Products.Five)
>>> zcml.load_config('configure.zcml', package=Products.Five.formlib.tests)
Finally, we need to setup a traversable folder. Otherwise, Five won't get
to to do its view lookup:
>>> from Products.Five.tests.testing import manage_addFiveTraversableFolder
>>> manage_addFiveTraversableFolder(self.folder, 'ftf')
Let's set up a testbrowser:
>>> from Products.Five.testbrowser import Browser
>>> browser = Browser()
>>> browser.addHeader('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7')
>>> browser.addHeader('Accept-Language', 'en-US')
>>> browser.addHeader('Authorization', 'Basic manager:r00t')
Let's 'manually' create a Content instance and add it to the folder:
>>> from Products.Five.formlib.tests import content
>>> folder = self.folder.ftf
>>> obj = content.Content('content_1', 'Title', [])
>>> folder._setObject('content_1', obj)
'content_1'
>>> print folder.content_1.title
Title
Now we can edit this content object, e.g. changing the title. Formlib,
like the traditional AddView and EditView, supports unicode:
>>> browser.open("http://localhost/test_folder_1_/ftf/content_1/@@edit_content")
>>> ni_hao = '\xe4\xbd\xa0\xe5\xa5\xbd'
>>> ctl = browser.getControl(name="form.title")
>>> ctl.value = ni_hao
>>> browser.getControl(name="form.actions.apply").click()
>>> isinstance(folder.content_1.title, unicode)
True
>>> folder.content_1.title == ni_hao.decode('utf-8')
True
Adding a new content object, with two list items:
>>> browser.open("http://localhost/test_folder_1_/ftf/@@add_content")
>>> ctl = browser.getControl(name="form.id")
>>> ctl.value = 'test123'
>>> ctl = browser.getControl(name="form.title")
>>> ctl.value = ni_hao
>>> browser.getControl(name="form.somelist.add").click()
>>> ctl = browser.getControl(name="form.somelist.0.")
>>> ctl.value = 'a nice list item'
>>> browser.getControl(name="form.somelist.add").click()
>>> ctl = browser.getControl(name="form.somelist.1.")
>>> ctl.value = 'a nice list item'
>>> browser.getControl(name="form.actions.add").click()
>>> print folder.test123.somelist
[u'a nice list item', u'a nice list item']
Clean up
--------
Finally, we need to clean up:
>>> from zope.component.testing import tearDown
>>> tearDown()
##############################################################################
#
# Copyright (c) 2006 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (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.
#
##############################################################################
"""Test forms
$Id$
"""
def test_suite():
import unittest
from Testing.ZopeTestCase import FunctionalDocFileSuite
return unittest.TestSuite((
FunctionalDocFileSuite(
'formlib.txt', package='Products.Five.formlib.tests'),
))
##############################################################################
#
# Copyright (c) 2006 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (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.
#
##############################################################################
"""Five baseclasses for zope.formlib.form
$Id$
"""
from zope.formlib import form
from Products.Five.formlib.formbase import AddForm, EditForm
from Products.Five.formlib.tests.content import IContent, Content
class AddContentForm(AddForm):
"""AddForm for creating and adding IContent objects
"""
form_fields = form.Fields(IContent)
def createAndAdd(self, data):
id = data.get('id')
ctnt = Content(
id, data.get('title'), somelist=data.get('somelist'))
self.context._setObject(id, ctnt)
class EditContentForm(EditForm):
"""EditForm for editing IContent objects
"""
form_fields = form.Fields(IContent)
...@@ -7,6 +7,7 @@ ClientForm = 0.2.10 ...@@ -7,6 +7,7 @@ ClientForm = 0.2.10
DateTime = 2.12.0 DateTime = 2.12.0
docutils = 0.6 docutils = 0.6
ExtensionClass = 2.11.3 ExtensionClass = 2.11.3
five.formlib = 1.0.2
lxml = 2.2 lxml = 2.2
mechanize = 0.1.11 mechanize = 0.1.11
Persistence = 2.11.1 Persistence = 2.11.1
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment