UNKNOWN
Project description
********************
Ajax Support
********************
The zc.ajaxform package provides framework to support:
- A single-class application model
- Nested-application support
- Integration with zope.formlib
.. contents::
Detailed Documentation
**********************
Application support
===================
The zc.ajaxform.application module provides support for writing ajax
[#ajax]_ applications. This framework started out as an experiment in
simplifying writing applications with Zope 3. I was frustrated with
ZCML situps and generally too much indirection. I ended up with a
model that I'm pretty happy with. It might not be for everybody. :)
The basic idea is that an application can be provided using a single
Zope 3 view plus necessary resource-library definitions. This view
has a URL. It typically provides many ajax methods whose URLs have the
view URL as a base.
Many applications can be implemented using a simple class that can be
registered as a view.
Let's look at a simple stupid application. :)
::
import zc.ajaxform.application
import zope.exceptions
class Calculator(zc.ajaxform.application.Trusted,
zc.ajaxform.application.Application):
resource_library_name = None
@zc.ajaxform.application.jsonpage
def about(self):
return 'Calculator 1.0'
@zc.ajaxform.application.jsonpage
def operations(self):
return ['add', "subtract"]
@zc.ajaxform.application.jsonpage
def value(self):
return dict(value=getattr(self.context, 'calculator_value', 0))
def do_add(self, value):
value += getattr(self.context, 'calculator_value', 0)
self.context.calculator_value = value
return dict(value=value)
@zc.ajaxform.application.jsonpage
def add(self, value):
if not isinstance(value, int):
return dict(error="The value must be an integer!")
return self.do_add(value)
@zc.ajaxform.application.jsonpage
def subtract(self, value):
if not isinstance(value, int):
raise zope.exceptions.UserError(
"The value must be an integer!")
return self.do_add(-value)
@zc.ajaxform.application.jsonpage
def noop(self):
pass
@zc.ajaxform.application.page
def none(self):
return "null"
@zc.ajaxform.application.jsonpage
def echo_form(self):
def maybe_file(v):
if hasattr(v, 'read'):
return ("<File upload name=%r content-type=%r size=%r>"
% (v.filename, v.headers['content-type'], len(v.read()))
)
else:
return v
return dict(
(name, maybe_file(v))
for (name, v) in self.request.form.items()
)
@zc.ajaxform.application.jsonpage
def doh(self):
raise TypeError("Doh!")
We subclass zc.ajaxform.application.Trusted. This is a minimal
base class that provides a constructor that takes a context and a
request and removes the security proxy from the context. It
overrides the constructor from zc.ajaxform.application.Application.
We also subclass zc.ajaxform.application.Application. This is a base
class that provides:
- a basic constructor that takes context and request arguments and sets
corresponding attributes,
- traversal to attributes that provide IBrowserPublisher with
conversion of dots to underscores,
- a default "page" named index.html,
- a template method that returns an HTML page with an empty head.
- an index_html method that loads a resource library and calls the
template,
- an interface declaration that it provides IBrowserPublisher, and
- an adapter declaration that adapts
zope.traversing.interfaces.IContainmentRoot and
zope.publisher.interfaces.browser.IBrowserRequest.
The main goals of this base class are to make it easy to load
Javascript and to make it easy to define ajax methods to support the
Javascript. For that reason, we provide a traverser that traverses to
object attributes that provide IBrowserPublisher. The
zc.ajaxform.application.jsonpage decorator is also an important part of
this. It makes methods accessible and automatically marshals their
result to JSON [#jsoninput]_. There's also a
zc.ajaxform.application.page decorator that makes methods accessible
without the automatic marshalling. The use of a default page, rather
than just a __call__ method is to cause the URL base to be the view,
rather than the view's context. This allows the Javascript code to
use relative URLs to refer to the ajax methods.
The class expects subclasses to define a resource_library_name
attribute [#missing_resource_library_name]_. For these applications,
you pretty much always want to use an associated Javascript file and
other resources (supporting JS, CSS, etc.). You can suppress the use
of the resource library by setting the value of this attribute to
None.
For applications that build pages totally in Javascript, the default
template is adequate. For applications that need to support
non-Javascript-enabled browsers, that want to support search-engine
optimization [#sso]_, or that want to provide some Javascript data
during the initial page load, a custom template can be provided by
simply overriding the template method with a page template or a method
that calls one.
The view can be registered with a simple adapter registration:
::
<configure xmlns="http://namespaces.zope.org/zope">
<adapter name="calculator.html"
factory="zc.ajaxform.calculator_example.Calculator"
permission="zope.View"
/>
</configure>
If we wanted to register it for an object other than the an
IContainmentRoot, we could just provide specifically adapted interfaces
or classes in the registration.
Let's access the calculator with a test browser
>>> import zope.testbrowser.testing
>>> browser = zope.testbrowser.testing.Browser()
>>> browser.open('http://localhost/')
Traceback (most recent call last):
...
HTTPError: HTTP Error 401: Unauthorized
Because our view was registered to require zope.View, the request was
unauthorized. Let's login. In the demo setup, we login by just
providing a login form variable.
>>> browser.open('http://localhost/calculator.html?login')
>>> print browser.contents # doctest: +NORMALIZE_WHITESPACE
<html><head>
<base href="http://localhost/calculator.html/index.html" />
</head></html>
We registered our view as calculator.html. Because of the way it sets the
browser default page for itself, it becomes the base href for the
page. This allows us to access ajax methods using relative URLs.
Our calculator view provides a value method. It uses the
zc.ajaxform.application.jsonpage decorator. This does 2 things:
- Arranges that the method can be traversed to,
- marshals the result to JSON.
The way results are marshalled to JSON deserves some
explanation. To support automation of ajax calls, we:
- Always return objects
- If there is an error, we include:
- an error property providing an error messahe, and/or
- when handling form submissions, an errors property with am object value
mapping field names to field-specific error messages.
::
>>> import simplejson
>>> browser.open('http://localhost/@@calculator.html/value')
>>> simplejson.loads(browser.contents)
{u'value': 0}
>>> browser.open('http://localhost/@@calculator.html/add?value=hi')
>>> simplejson.loads(browser.contents)
{u'error': u'The value must be an integer!'}
Things other than a dictionary can be returned:
>>> browser.open('http://localhost/@@calculator.html/about')
>>> simplejson.loads(browser.contents)
u'Calculator 1.0'
>>> browser.open('http://localhost/@@calculator.html/operations')
>>> simplejson.loads(browser.contents)
[u'add', u'subtract']
If you want to marshal JSON yourself, you can use the
zc.ajaxform.application.jsonpage decorator:
>>> browser.open('http://localhost/@@calculator.html/none')
An alternative way to return errors is to raise user errors, as is
done by the subtract method in our example:
>>> browser.open('http://localhost/@@calculator.html/subtract?value=hi')
>>> simplejson.loads(browser.contents)
{u'error': u'The value must be an integer!'}
This works because there is a view registered for
zope.exceptions.interfaces.IUserError, and
zc.ajaxform.interfaces.IAjaxRequest.
Testing support
===============
zc.ajaxform.testing has some helper functions to make it easier to test
ajax calls.
The zc.ajaxform.testing.FormServer class provides some convenience for
making ajax calls in which data are sent as form data and returned as
JSON. The class takes a browser and returns an object that can be
called to make server calls:
>>> import zc.ajaxform.testing, pprint
>>> server = zc.ajaxform.testing.FormServer(browser)
>>> pprint.pprint(server('/calculator.html/echo_form',
... {'a': 1.0}, b=[1, 2, 3], c=True, d='d', e=u'e\u1234'
... ), width=1)
{u'a': u'1.0',
u'b': [u'1',
u'2',
u'3'],
u'c': u'1',
u'd': u'd',
u'e': u'e\u1234'}
When we call the server, we pass a URL to invoke, which may be
relative, a optional dictionary of parameter values, and optional
keyword arguments.
Note that the application will recieve data as strings, which is what
we see echoed back in the example above.
If the application is written using Zope, then we can enable Zope form
marshalling, by passing a True value when we create the server:
>>> server = zc.ajaxform.testing.FormServer(browser, True)
>>> pprint.pprint(server('/calculator.html/echo_form',
... {'a': 1.0}, b=[1, 2, 3], c=True, d='d', e=u'e\u1234'
... ), width=1)
{u'a': 1.0,
u'b': [1,
2,
3],
u'c': True,
u'd': u'd',
u'e': u'e\u1234'}
>>> pprint.pprint(server('/calculator.html/add', {'value': 1}), width=1)
{u'value': 1}
>>> pprint.pprint(server('/calculator.html/add', value=1), width=1)
{u'value': 2}
The methods called are assumed to return JSON and the resulting data
is converted back into Python.
The function pprint method combines pprint and calling:
>>> server.pprint('/calculator.html/add', {'value': 1})
{u'value': 3}
>>> server.pprint('/calculator.html/add', value=1)
{u'value': 4}
>>> server.pprint('/calculator.html/echo_form',
... {'a': 1.0}, b=[1, 2, 3], c=True, d='d', e=u'e\u1234'
... )
{u'a': 1.0,
u'b': [1,
2,
3],
u'c': True,
u'd': u'd',
u'e': u'e\u1234'}
In the future, there will be versions of these functions that send
data as JSON.
We can include file-upload data by including a 3-tuple with a file
name, a content type, and a data string:
>>> server.pprint('/calculator.html/echo_form',
... b=[1, 2, 3], c=True, d='d',
... file=('foo.xml', 'test/xml', '<foo></foo>'),
... )
{u'b': [1,
2,
3],
u'c': True,
u'd': u'd',
u'file': u"<File upload name=u'foo.xml' content-type='test/xml' size=11>"}
as a convenience, you can pass a URL string to the server constructor,
which will create a browser for you that has opened that URL. You can
also omit the brower and an unopened browser will be created.
>>> server = zc.ajaxform.testing.FormServer(
... 'http://localhost/calculator.html?login')
>>> server.browser.url
'http://localhost/calculator.html?login'
>>> server.pprint('/calculator.html/echo_form', x=1)
{u'x': u'1'}
>>> server = zc.ajaxform.testing.FormServer(zope_form_marshalling=True)
>>> server.browser.open('http://localhost/calculator.html?login')
>>> server.pprint('/calculator.html/echo_form', x=1)
{u'x': 1}
In the example above, we didn't provide a browser, but we provided the
zope_form_marshalling flag as a keyword option.
.. Edge case: we can't traverse to undecorated methods:
>>> server.pprint('/calculator.html/do_add', value=1)
Traceback (most recent call last):
...
HTTPError: HTTP Error 404: Not Found
"Library" applications
======================
The "application" model described above is pretty appealing in its
simplicity -- at least to me. :) Usually, we'd like to make out
applications a bit more flexible in their use. In particular, we
often want to assemble applications together. At the Javascript level,
this often means having an application return a panel that can be used
in some higher-level layout. At the server level, we need to provide
a way to access application logic within some larger context. There
are two parts to this:
1. The containing application needs to support traversal to the
sub-application.
2. The subapplication needs to know how it was traversed to, at least
if it generates URLs. For example, the form machinery [#forms]_
generates URLs for action handlers.
Sub-application should expose the URL needed to access then as a
base_href attribute. This is usually a relative URL relative to the base
application.
There are a number of classes defined in zc.ajaxform.application that
help with creating sub-applications:
SubApplication
This class, which Application subclasses, provides traversal to
attributes that provide IBrowserPublisher. It also stamps
IAjaxRequest on the request object when an object is traversed
[#iajaxrequest]_ .
(Maybe this request stamping should be done further down the
traversal chain or perhaps only done if X-Requested-With is
xmlhttprequest.)
PublicTraversable
This class provides security declarations that allow objects to be
traversable publically. This is appropriate for sub-applications
that want the same protections as the object being traversed.
Let's look at our calculator example as a subapplication:
::
import zc.ajaxform.application
import zope.exceptions
class Container(zc.ajaxform.application.Application):
resource_library_name = None
@property
def calc(self):
return Calculator(self.context, self.request, base_href='calc')
class Calculator(zc.ajaxform.application.Trusted,
zc.ajaxform.application.SubApplication,
zc.ajaxform.application.PublicTraversable,
):
@zc.ajaxform.application.jsonpage
def operations(self):
return [['add', self.base_href+'/add'],
['add', self.base_href+'/subtract'],
]
@zc.ajaxform.application.jsonpage
def value(self):
return dict(value=getattr(self.context, 'calculator_value', 0))
def do_add(self, value):
value += getattr(self.context, 'calculator_value', 0)
self.context.calculator_value = value
return dict(value=value)
@zc.ajaxform.application.jsonpage
def add(self, value):
if not isinstance(value, int):
return dict(error="The value must be an integer!")
return self.do_add(value)
@zc.ajaxform.application.jsonpage
def subtract(self, value):
if not isinstance(value, int):
raise zope.exceptions.UserError(
"The value must be an integer!")
return self.do_add(-value)
Here, we've defined a container application that simply provides
traversal to a calculator subapplication as a static property. It
creates the calculator with the application's context and request. It
passes a base_href as a keyword argument, which SubApplication's
constructor accepts. Our ZCML configuration is pretty simple:
::
<configure xmlns="http://namespaces.zope.org/zope">
<include package="zope.app.component" file="meta.zcml" />
<adapter name="container.html"
factory="zc.ajaxform.calculator_subapplication_example.Container"
permission="zope.View"
/>
</configure>
Using the container application, we access the calculator via the
container:
>>> server.pprint('http://localhost/@@container.html/calc/add', value=1)
{u'value': 5}
We've updated the operations method to include the URL for each
operation, which is computed based on the base_href:
>>> server.pprint('http://localhost/@@container.html/calc/operations')
[[u'add',
u'calc/add'],
[u'add',
u'calc/subtract']]
Note that we didn't make any security declarations for the Calculator
class. We're relying on the protection for the container. If we
restart the browser, we see, indeed, that we can't access the
calculator:
>>> server = zc.ajaxform.testing.FormServer()
>>> server.pprint('http://localhost/@@container.html/calc/operations')
{u'session_expired': True}
Dynamic Traversal
=================
In the previous example, we traversed to a sub-application using a
static property. Sometimes, we need to traverse dynamically. We
might have a container application with a variable number of
subapplications. Examples include a portlet container and a system for
managing user-defined data types. In the later case, as users define
new data types, one or more applications get defined for each type.
zc.ajaxform.application provides a helper descriptor that allows custom
traversers to be implemented with simple Python methods. Let's look
at a simple example.
>>> import zc.ajaxform.application
>>> class Foo:
... def __str__(self):
... return 'a '+self.__class__.__name__
...
... @zc.ajaxform.application.traverser
... def demo_traverse(self, request, name):
... return "traverse: %s %s %s" % (self, request, name)
This is a rather silly traverser for demonstration purposes that just
returnes a transformed name.
>>> foo = Foo()
>>> foo.demo_traverse.publishTraverse("a request", "xxx")
'traverse: a Foo a request xxx'
We can still call the method:
>>> foo.demo_traverse("a request", "xxx")
'traverse: a Foo a request xxx'
The method provides IBrowserPublisher:
>>> import zope.publisher.interfaces.browser
>>> zope.publisher.interfaces.browser.IBrowserPublisher.providedBy(
... foo.demo_traverse)
True
The descriptor has a security declaration that allows it to be
traversed but not called from untrusted code:
>>> import zope.security.checker
>>> checker = zope.security.checker.getChecker(
... zope.security.checker.ProxyFactory(foo.demo_traverse))
>>> checker.get_permissions
{'publishTraverse': Global(CheckerPublic,zope.security.checker)}
>>> checker.set_permissions
{}
Acquisition
===========
Applications and sub-applications have __parent__ properties that
return their context. This is to support frameworks that ise
__parent__ to perform acquisition.
>>> class MyApp(zc.ajaxform.application.Application):
... pass
>>> myapp = MyApp(foo, None)
>>> myapp.__parent__ is foo
True
>>> class MySubApp(zc.ajaxform.application.SubApplication):
... pass
>>> mysubapp = MySubApp(foo, None)
>>> mysubapp.__parent__ is foo
True
System Errors
=============
System errors will be rendered as json.
>>> server = zc.ajaxform.testing.FormServer(
... 'http://localhost/calculator.html?login')
>>> server('/calculator.html/doh')
Traceback (most recent call last):
...
HTTPError: HTTP Error 500: Internal Server Error
>>> pprint.pprint(simplejson.loads(server.browser.contents), width=1)
{u'error': u'TypeError: Doh!'}
.. [#ajax] Technically, these aren't really AJAX applications, since
we rarely. if ever, use XML as a serialization format. To
emphasize this I'll use lower-case "ajax" to refer to the generic
approach of making low-level calls from Javascript rather than
doing page loads.
.. [#jsoninput] In the near future, there will also be support for
JSON method input. This will provide a number of benefits:
- It will provide more automatic marshaling of non-string
data. Now, we either have to de-marshal in the server application
code or embed marshaling data into parameter names in client
code.
- It will allow richer data structures than is practical with form data.
- It will probably allow faster ajax requests because:
- Server-side de-marshalling is done with highly optimized code
in simplejson.
- We will assume that data passed are valid method arguments and
avoid method introspection.
.. [#missing_resource_library_name] A custom attribute error message
is used if this attribute is missing that tries to be more
informative than the default attribute error.
.. [#sso] For search-engine optimization, one generally wants a
content page to actually contain its content. If one depends on
Javascript-enabled browsers, one can improve performance and
search-engine optimization by adding ancilary data in Javascript,
so as not to dilute the content.
.. [#forms] See form.txt.
.. [#iajaxrequest] Traversing into a subapplication adds IAjaxRequest to the
list of interfaces provided by the request.
>>> import zc.ajaxform.application
>>> import zc.ajaxform.interfaces
>>> import zope.publisher.browser
>>> request = zope.publisher.browser.TestRequest()
>>> class SubApp(zc.ajaxform.application.SubApplication):
... @zc.ajaxform.application.jsonpage
... def foo(self):
... return 'bar'
>>> subapp = SubApp(object(), request)
Now let's try traversing into the subapplication:
>>> zc.ajaxform.interfaces.IAjaxRequest.providedBy(request)
False
>>> subapp.publishTraverse(request, 'foo')()
'"bar"'
>>> zc.ajaxform.interfaces.IAjaxRequest.providedBy(request)
True
Note that the request keeps any provided interfaces:
>>> request = zope.publisher.browser.TestRequest()
>>> class IMyInterface(zope.interface.Interface):
... pass
>>> zope.interface.directlyProvides(request, IMyInterface)
>>> subapp.publishTraverse(request, 'foo')()
'"bar"'
>>> IMyInterface.providedBy(request)
True
System Error Logging
====================
When an error is generated, zope.app.publishing checks to see if the
error handler provides the System Error interface. If so, zope.app.publishing
logs the error.
Rather than use this indirect method of logging, there is an explicit
statement in ExceptionView that imports the logging module and logs and error
itself. We can test this with the zope.testing.loggingsupport functions.
First we set up loggingsupport. This keeps track of all error messages
written via the module passed as the parameter. In this case, zc.ajaxform.
>>> import logging
>>> import zope.testing.loggingsupport
>>> log_handler = zope.testing.loggingsupport.InstalledHandler('zc.ajaxform')
Then we create an error.
>>> server = zc.ajaxform.testing.FormServer(
... 'http://localhost/calculator.html?login')
>>> server('/calculator.html/doh')
Traceback (most recent call last):
...
HTTPError: HTTP Error 500: Internal Server Error
...And check to see that it was logged.
>>> print log_handler
zc.ajaxform.application ERROR
SysError created by zc.ajaxform
Form Processing
===============
zc.ajaxform.form provides support for server-generated forms based on
the zope.formlib library.
Forms are meant to be used as parts of larger applications. A form
provides output of JSON data for building javascript forms. Forms also
provide validation and call actions with validated data to perform
actions on form submit.
To create a form, just create a form class as a subclass of
zc.ajaxform.form.Form. This base class provides:
- an ajax __call__ method that returns a form definition,
- traversal to form actions, in much the same way that
zc.ajaxform.application.Application [#application]_ provides traversal
to json methods,
- a definitions method that can be used by ajax methods to get a form
definition as Python data, and
- a getObjectData method for getting initial form data from an
existing object.
Here's a simple example:
::
import zc.ajaxform.application
import zc.ajaxform.interfaces
import zc.ajaxform.widgets
import zc.ajaxform.form
import zope.component
import zope.interface
import zope.formlib
import zope.schema
class IAddress(zope.interface.Interface):
street = zope.schema.TextLine(
title = u"Street",
description = u"The street",
)
city = zope.schema.TextLine(
title = u"City",
description = u"The city",
)
awesomeness = zope.schema.Int(
title = u"Awesomeness",
description = u"The awesomeness on a scale of 1 to 10",
min = 1,
max = 10,
)
class Pets(zc.sourcefactory.basic.BasicSourceFactory):
def getValues(self):
return (u'Dog', u'Cat', u'Fish')
class Pet(zope.schema.TextLine):
"""A textline representing a pet.
This is just a textline, but we also have a source of common pets that
the user can choose from.
"""
class IPerson(zope.interface.Interface):
first_name = zope.schema.TextLine(
title = u"First name",
description = u"Given name.",
default= u'Happy'
)
last_name = zope.schema.TextLine(
title = u"Last name",
description = u"Family name.",
default= u'Camper'
)
favorite_color = zope.schema.TextLine(
title = u"Favorite color",
required = False,
default= u'Blue'
)
age = zope.schema.Int(
title = u"Age",
description = u"Age in years",
min = 0,
max = 200,
default= 23
)
happy = zope.schema.Bool(
title = u"Happy",
description = u"Are they happy?",
default= True
)
pet = Pet(
title=u'Pet',
description=u'This person\'s best friend.',
required=False,
)
temperment = zope.schema.Choice(
title = u"Temperment",
description = u"What is the person like?",
values = ['Nice', 'Mean', 'Ornery', 'Right Neighborly'],
default = u'Right Neighborly'
)
weight = zope.schema.Decimal(
title = u"Weight",
description = u"Weight in lbs?"
)
description = zope.schema.Text(
title = u"Description",
description = u"What do they look like?",
default = u'10ft tall\nRazor sharp scales.'
)
secret = zope.schema.TextLine(
title = u"Secret Key",
description = u"Don't tell anybody",
default = u'5ecret sauce'
)
siblings = zope.schema.Int(
title = u"Siblings",
description = u"Number of siblings",
min = 0,
max = 8,
default = 1
)
addresses = zope.schema.List(
title = u'Addresses',
description = u"All my wonderful homes",
value_type = zope.schema.Object(schema=IAddress),
default= [{'street':'123 fake street',
'city': 'fakeville',
'awesomeness': '9'},
{'street':'345 false street',
'city': 'falsetown',
'awesomeness': '9001'}
]
)
other = zope.schema.Text(
title = u"Other",
description = u"Any other notes",
default = u"I've got a magic toenail"
)
class Person:
zope.interface.implements(IPerson)
def __init__(self, first_name, last_name, favorite_color, age, happy,
pet, temperment, weight, description, secret, siblings,
addresses, other):
self.first_name = first_name
self.last_name = last_name
self.favorite_color = favorite_color
self.age = age
self.happy = happy
self.pet = pet
self.temperment = temperment
self.weight = weight
self.description = description
self.secret = secret
self.siblings = siblings
self.addresses = addresses
self.other = other
class FormExample(zc.ajaxform.application.Application):
resource_library_name = None
class ExampleForm(zc.ajaxform.form.Form):
leftFields = ('first_name', 'last_name', 'age', 'other')
form_fields = zope.formlib.form.Fields(IPerson)
form_fields['secret'].custom_widget = zc.ajaxform.widgets.Hidden
form_fields['siblings'].custom_widget = zc.ajaxform.widgets.NumberSpinner
@zope.formlib.form.action("Register")
def register(self, action, data):
person = Person(**data)
return dict(
data = data,
self_class_name = self.__class__.__name__,
self_app_class_name = self.app.__class__.__name__,
self_context_class_name = self.context.__class__.__name__
)
class PetWidget(zc.ajaxform.widgets.ComboBox):
zope.component.adapts(
Pet,
zc.ajaxform.interfaces.IAjaxRequest)
zope.interface.implements(
zc.ajaxform.interfaces.IInputWidget)
def __init__(self, context, request):
super(PetWidget, self).__init__(context, Pets(), request)
Note that we've nested our form definition in an application. We can
define the form class elsewhere and use it, but if a form is only used
in an application, then it's often easiest to define it within an
application class. Forms are instantiated by calling them with a
single argument. This argument, the application, becomes the form's `app`
attribute. The application's context becomes the form's context. Form
classes are automatically instantiated when a form class is assigned to
an attribute in a class and accessed through an instance
[#form_classes_are_descriptors]_.
Let's try accessing our form, which can be found in its python form
in form_example.py:
>>> import zope.testbrowser.testing
>>> from zc.ajaxform.testing import call_form, print_form
>>> browser = zope.testbrowser.testing.Browser()
>>> browser.open('http://localhost/form.html?login')
>>> print_form(browser, 'http://localhost/form.html/ExampleForm')
{u'definition': {u'actions': [{u'label': u'Register',
u'name': u'ExampleForm.actions.register',
u'url': u'ExampleForm/register'}],
u'left_fields': {u'addresses': False,
u'age': True,
u'description': False,
u'favorite_color': False,
u'first_name': True,
u'happy': False,
u'last_name': True,
u'other': True,
u'pet': False,
u'secret': False,
u'siblings': False,
u'temperment': False,
u'weight': False},
u'prefix': u'ExampleForm',
u'widgets': [{u'fieldHint': u'Given name.',
u'fieldLabel': u'First name',
u'id': u'first_name',
u'minLength': 0,
u'name': u'first_name',
u'required': True,
u'value': u'Happy',
u'widget_constructor': u'zope.schema.TextLine'},
{u'fieldHint': u'Family name.',
u'fieldLabel': u'Last name',
u'id': u'last_name',
u'minLength': 0,
u'name': u'last_name',
u'required': True,
u'value': u'Camper',
u'widget_constructor': u'zope.schema.TextLine'},
{u'fieldHint': u'',
u'fieldLabel': u'Favorite color',
u'id': u'favorite_color',
u'minLength': 0,
u'name': u'favorite_color',
u'required': False,
u'value': u'Blue',
u'widget_constructor': u'zope.schema.TextLine'},
{u'allowBlank': False,
u'fieldHint': u'Age in years',
u'fieldLabel': u'Age',
u'field_max': 200,
u'field_min': 0,
u'id': u'age',
u'name': u'age',
u'required': True,
u'value': u'23',
u'widget_constructor': u'zope.schema.Int'},
{u'fieldHint': u'Are they happy?',
u'fieldLabel': u'Happy',
u'id': u'happy',
u'name': u'happy',
u'required': True,
u'value': True,
u'widget_constructor': u'zope.schema.Bool'},
{u'fieldHint': u"This person's best friend.",
u'fieldLabel': u'Pet',
u'id': u'pet',
u'name': u'pet',
u'required': False,
u'values': [[u'c935d187f0b998ef720390f85014ed1e',
u'Dog'],
[u'fa3ebd6742c360b2d9652b7f78d9bd7d',
u'Cat'],
[u'071642fa72ba780ee90ed36350d82745',
u'Fish']],
u'widget_constructor': u'zc.ajaxform.widgets.ComboBox'},
{u'allowBlank': False,
u'fieldHint': u'What is the person like?',
u'fieldLabel': u'Temperment',
u'hiddenName': u'temperment.value',
u'id': u'temperment',
u'name': u'temperment',
u'required': True,
u'value': u'Right Neighborly',
u'values': [[u'Nice',
u'Nice'],
[u'Mean',
u'Mean'],
[u'Ornery',
u'Ornery'],
[u'Right Neighborly',
u'Right Neighborly']],
u'widget_constructor': u'zope.schema.Choice'},
{u'allowBlank': False,
u'fieldHint': u'Weight in lbs?',
u'fieldLabel': u'Weight',
u'id': u'weight',
u'name': u'weight',
u'required': True,
u'widget_constructor': u'zope.schema.Decimal'},
{u'fieldHint': u'What do they look like?',
u'fieldLabel': u'Description',
u'id': u'description',
u'minLength': 0,
u'name': u'description',
u'required': True,
u'value': u'10ft tall\nRazor sharp scales.',
u'widget_constructor': u'zope.schema.Text'},
{u'fieldHint': u"Don't tell anybody",
u'fieldLabel': u'Secret Key',
u'id': u'secret',
u'name': u'secret',
u'required': True,
u'value': u'5ecret sauce',
u'widget_constructor': u'zc.ajaxform.widgets.Hidden'},
{u'allowBlank': False,
u'fieldHint': u'Number of siblings',
u'fieldLabel': u'Siblings',
u'field_max': 8,
u'field_min': 0,
u'id': u'siblings',
u'name': u'siblings',
u'required': True,
u'value': u'1',
u'widget_constructor': u'zc.ajaxform.widgets.NumberSpinner'},
{u'fieldHint': u'All my wonderful homes',
u'fieldLabel': u'Addresses',
u'id': u'addresses',
u'name': u'addresses',
u'record_schema': {u'readonly': False,
u'widgets': [{u'fieldHint': u'The street',
u'fieldLabel': u'Street',
u'id': u'street',
u'minLength': 0,
u'name': u'street',
u'required': True,
u'widget_constructor': u'zope.schema.TextLine'},
{u'fieldHint': u'The city',
u'fieldLabel': u'City',
u'id': u'city',
u'minLength': 0,
u'name': u'city',
u'required': True,
u'widget_constructor': u'zope.schema.TextLine'},
{u'allowBlank': False,
u'fieldHint': u'The awesomeness on a scale of 1 to 10',
u'fieldLabel': u'Awesomeness',
u'field_max': 10,
u'field_min': 1,
u'id': u'awesomeness',
u'name': u'awesomeness',
u'required': True,
u'widget_constructor': u'zope.schema.Int'}]},
u'required': True,
u'value': [{u'awesomeness': u'9',
u'city': u'fakeville',
u'street': u'123 fake street'},
{u'awesomeness': u'9001',
u'city': u'falsetown',
u'street': u'345 false street'}],
u'widget_constructor': u'zope.schema.List'},
{u'fieldHint': u'Any other notes',
u'fieldLabel': u'Other',
u'id': u'other',
u'minLength': 0,
u'name': u'other',
u'required': True,
u'value': u"I've got a magic toenail",
u'widget_constructor': u'zope.schema.Text'}]}}
Our application is at: "http://localhost/form.html". The form is
exposed as an ajax method named "ExampleForm", which comes from the attribute
name in the class definition.
The form definition contains both action definitions and widget
definitions. The widget definitions may be full js field definitions
or name a widget_constructor, which is a Javascript helper provided by
the zc.ajaxform resource library that provides additional information,
like Javascript validators, that can't be expressed in JSON.
There is an action definition for each action defined in the form. The
action information includes the url to post the result to, relative to
the application.
Note that the name of the form class is used as the form prefix and
that the form prefix is used as the prefix for widget and action names
and ids [#actionids]_.
Let's post a result back:
>>> print_form(browser, 'http://localhost/form.html/ExampleForm/register',
... {'first_name': 'Bob',
... 'last_name': '',
... 'favorite_color': '',
... 'age': '-1',
... })
{u'errors': {u'addresses': u'Addresses: Missing Input',
u'age': u'Value is too small',
u'description': u'Description: Missing Input',
u'last_name': u'Last name: Missing Input',
u'other': u'Other: Missing Input',
u'secret': u'Secret Key: Missing Input',
u'siblings': u'Siblings: Missing Input',
u'temperment': u'Temperment: Missing Input',
u'weight': u'Weight: Missing Input'}}
The result had 9 problems:
- We didn't provide a last name, description, secret key,
number of siblings, temperment, or weight, which are all required
- In the form we did not specify deleting either of our two current
address records, but also ommitted their data, and the first of them
reported a missing field. Following this we will delete them both and
add a new record.
- We specified an invalid age.
Let's pass valid data:
>>> print_form(browser, 'http://localhost/form.html/ExampleForm/register',
... {'first_name': 'Bob',
... 'last_name': 'Zope',
... 'favorite_color': '',
... 'age': '11',
... 'addresses.street.0': '123 Fake Ln.',
... 'addresses.city.0': 'Fakeville',
... 'addresses.awesomeness.0': 7,
... 'description': 'Hello',
... 'other': 'So nice to meet you',
... 'secret': 'Oh nothing',
... 'siblings': 1,
... 'temperment': 'Nice',
... 'weight': '170.5',
... 'pet': 'Carrier Pigeon'
... })
{u'data': {u'addresses': [{u'awesomeness': 7,
u'city': u'Fakeville',
u'street': u'123 Fake Ln.'}],
u'age': 11,
u'description': u'Hello',
u'favorite_color': u'',
u'first_name': u'Bob',
u'happy': False,
u'last_name': u'Zope',
u'other': u'So nice to meet you',
u'pet': u'Carrier Pigeon',
u'secret': u'Oh nothing',
u'siblings': 1,
u'temperment': u'Nice',
u'weight': u'170.5'},
u'self_app_class_name': u'FormExample',
u'self_class_name': u'ExampleForm',
u'self_context_class_name': u'Folder'}
Here we get a successful result. Our contrived action in the example
simply echoed back the data it was passed, Note, in particular that:
- the data keys have the form prefix removed, and
- the value of the age key is an integer, since the field was an
integer field.
Note that for the list field, the original request added a prefix of the list
field name to prevent collisions with a field with the same name in another
list field (if it existed).
The action also prints out the classes of its self argument, its app
and its context. Actions are methods of forms so their `self` argument is the
form. The form's `app` is the app through which it is accessed and
`context` is the app's context.
For list widgets if a field in its record is missing and it is required, the
error reported lists the field name:
>>> print_form(browser, 'http://localhost/form.html/ExampleForm/register',
... {'first_name': 'Bob',
... 'last_name': 'Zope',
... 'favorite_color': '',
... 'age': '11',
... 'addresses.street.0': '123 Fake Ln.',
... 'addresses.city.0': 'Fakeville',
... 'addresses.awesomeness.0': 7,
... 'addresses.street.1': 'The 2nd Missing field St.',
... 'addresses.awesomeness.1': 3,
... 'description': 'Hello',
... 'other': 'So nice to meet you',
... 'secret': 'Oh nothing',
... 'siblings': 1,
... 'temperment': 'Nice',
... 'weight': '170.5',
... 'pet': 'Carrier Pigeon'
... })
{u'errors': {u'addresses': u'City: Missing Input'}}
Let's provide this value now:
>>> print_form(browser, 'http://localhost/form.html/ExampleForm/register',
... {'first_name': 'Bob',
... 'last_name': 'Zope',
... 'favorite_color': '',
... 'age': '11',
... 'addresses.street.0': '123 Fake Ln.',
... 'addresses.city.0': 'Fakeville',
... 'addresses.awesomeness.0': 7,
... 'addresses.street.1': 'The 2nd Missing field St.',
... 'addresses.city.1': 'A Void',
... 'addresses.awesomeness.1': '3',
... 'description': 'Hello',
... 'other': 'So nice to meet you',
... 'secret': 'Oh nothing',
... 'siblings': 1,
... 'temperment': 'Nice',
... 'weight': '170.5',
... 'pet': 'Carrier Pigeon'
... })
{u'data': {u'addresses': [{u'awesomeness': 7,
u'city': u'Fakeville',
u'street': u'123 Fake Ln.'},
{u'awesomeness': 3,
u'city': u'A Void',
u'street': u'The 2nd Missing field St.'}],
u'age': 11,
u'description': u'Hello',
u'favorite_color': u'',
u'first_name': u'Bob',
u'happy': False,
u'last_name': u'Zope',
u'other': u'So nice to meet you',
u'pet': u'Carrier Pigeon',
u'secret': u'Oh nothing',
u'siblings': 1,
u'temperment': u'Nice',
u'weight': u'170.5'},
u'self_app_class_name': u'FormExample',
u'self_class_name': u'ExampleForm',
u'self_context_class_name': u'Folder'}
Reordering items in a list is accomplished by reordering the suffix for the
record fields:
>>> print_form(browser, 'http://localhost/form.html/ExampleForm/register',
... {'first_name': 'Bob',
... 'last_name': 'Zope',
... 'favorite_color': '',
... 'age': '11',
... 'addresses.street.1': '123 Fake Ln.',
... 'addresses.city.1': 'Fakeville',
... 'addresses.awesomeness.1': 7,
... 'addresses.street.0': 'The 2nd Missing field St.',
... 'addresses.city.0': 'A Void',
... 'addresses.awesomeness.0': 3,
... 'description': 'Hello',
... 'other': 'So nice to meet you',
... 'secret': 'Oh nothing',
... 'siblings': 1,
... 'temperment': 'Nice',
... 'weight': '170.5',
... 'pet': 'Carrier Pigeon'
... })
{u'data': {u'addresses': [{u'awesomeness': 3,
u'city': u'A Void',
u'street': u'The 2nd Missing field St.'},
{u'awesomeness': 7,
u'city': u'Fakeville',
u'street': u'123 Fake Ln.'}],
u'age': 11,
u'description': u'Hello',
u'favorite_color': u'',
u'first_name': u'Bob',
u'happy': False,
u'last_name': u'Zope',
u'other': u'So nice to meet you',
u'pet': u'Carrier Pigeon',
u'secret': u'Oh nothing',
u'siblings': 1,
u'temperment': u'Nice',
u'weight': u'170.5'},
u'self_app_class_name': u'FormExample',
u'self_class_name': u'ExampleForm',
u'self_context_class_name': u'Folder'}
Getting definitions from Python
-------------------------------
Sometimes we want to get form definitions from Python. The form
__call__ method returns a JSON string. We can get Python data by
calling get_definition.
>>> import zc.ajaxform.form_example
>>> import zope.publisher.browser
>>> request = zope.publisher.browser.TestRequest()
>>> import zc.ajaxform.interfaces
>>> import zope.interface
>>> zope.interface.alsoProvides(
... request, zc.ajaxform.interfaces.IAjaxRequest)
>>> ex = zc.ajaxform.form_example.FormExample(None, request)
>>> from pprint import pprint
>>> pprint(ex.ExampleForm.get_definition(), width=1)
{'actions': [{'label': 'Register',
'name': u'ExampleForm.actions.register',
'url': u'ExampleForm/register'}],
'left_fields': {'addresses': False,
'age': True,
'description': False,
'favorite_color': False,
'first_name': True,
'happy': False,
'last_name': True,
'other': True,
'pet': False,
'secret': False,
'siblings': False,
'temperment': False,
'weight': False},
'prefix': 'ExampleForm',
'widgets': [{'fieldHint': u'Given name.',
'fieldLabel': u'First name',
'id': 'first_name',
'minLength': 0,
'name': 'first_name',
'required': True,
'value': u'Happy',
'widget_constructor': 'zope.schema.TextLine'},
{'fieldHint': u'Family name.',
'fieldLabel': u'Last name',
'id': 'last_name',
'minLength': 0,
'name': 'last_name',
'required': True,
'value': u'Camper',
'widget_constructor': 'zope.schema.TextLine'},
{'fieldHint': u'',
'fieldLabel': u'Favorite color',
'id': 'favorite_color',
'minLength': 0,
'name': 'favorite_color',
'required': False,
'value': u'Blue',
'widget_constructor': 'zope.schema.TextLine'},
{'allowBlank': False,
'fieldHint': u'Age in years',
'fieldLabel': u'Age',
'field_max': 200,
'field_min': 0,
'id': 'age',
'name': 'age',
'required': True,
'value': u'23',
'widget_constructor': 'zope.schema.Int'},
{'fieldHint': u'Are they happy?',
'fieldLabel': u'Happy',
'id': 'happy',
'name': 'happy',
'required': True,
'value': True,
'widget_constructor': 'zope.schema.Bool'},
{'fieldHint': u"This person's best friend.",
'fieldLabel': u'Pet',
'id': 'pet',
'name': 'pet',
'required': False,
'values': [['c935d187f0b998ef720390f85014ed1e',
u'Dog'],
['fa3ebd6742c360b2d9652b7f78d9bd7d',
u'Cat'],
['071642fa72ba780ee90ed36350d82745',
u'Fish']],
'widget_constructor': 'zc.ajaxform.widgets.ComboBox'},
{'allowBlank': False,
'fieldHint': u'What is the person like?',
'fieldLabel': u'Temperment',
'hiddenName': 'temperment.value',
'id': 'temperment',
'name': 'temperment',
'required': True,
'value': 'Right Neighborly',
'values': [['Nice',
u'Nice'],
['Mean',
u'Mean'],
['Ornery',
u'Ornery'],
['Right Neighborly',
u'Right Neighborly']],
'widget_constructor': 'zope.schema.Choice'},
{'allowBlank': False,
'fieldHint': u'Weight in lbs?',
'fieldLabel': u'Weight',
'id': 'weight',
'name': 'weight',
'required': True,
'widget_constructor': 'zope.schema.Decimal'},
{'fieldHint': u'What do they look like?',
'fieldLabel': u'Description',
'id': 'description',
'minLength': 0,
'name': 'description',
'required': True,
'value': u'10ft tall\nRazor sharp scales.',
'widget_constructor': 'zope.schema.Text'},
{'fieldHint': u"Don't tell anybody",
'fieldLabel': u'Secret Key',
'id': 'secret',
'name': 'secret',
'required': True,
'value': u'5ecret sauce',
'widget_constructor': 'zc.ajaxform.widgets.Hidden'},
{'allowBlank': False,
'fieldHint': u'Number of siblings',
'fieldLabel': u'Siblings',
'field_max': 8,
'field_min': 0,
'id': 'siblings',
'name': 'siblings',
'required': True,
'value': u'1',
'widget_constructor': 'zc.ajaxform.widgets.NumberSpinner'},
{'fieldHint': u'All my wonderful homes',
'fieldLabel': u'Addresses',
'id': 'addresses',
'name': 'addresses',
'record_schema': {'readonly': False,
'widgets': [{'fieldHint': u'The street',
'fieldLabel': u'Street',
'id': 'street',
'minLength': 0,
'name': 'street',
'required': True,
'widget_constructor': 'zope.schema.TextLine'},
{'fieldHint': u'The city',
'fieldLabel': u'City',
'id': 'city',
'minLength': 0,
'name': 'city',
'required': True,
'widget_constructor': 'zope.schema.TextLine'},
{'allowBlank': False,
'fieldHint': u'The awesomeness on a scale of 1 to 10',
'fieldLabel': u'Awesomeness',
'field_max': 10,
'field_min': 1,
'id': 'awesomeness',
'name': 'awesomeness',
'required': True,
'widget_constructor': 'zope.schema.Int'}]},
'required': True,
'value': [{'awesomeness': u'9',
'city': u'fakeville',
'street': u'123 fake street'},
{'awesomeness': u'9001',
'city': u'falsetown',
'street': u'345 false street'}],
'widget_constructor': 'zope.schema.List'},
{'fieldHint': u'Any other notes',
'fieldLabel': u'Other',
'id': 'other',
'minLength': 0,
'name': 'other',
'required': True,
'value': u"I've got a magic toenail",
'widget_constructor': 'zope.schema.Text'}]}
Note that we had to stamp the request with IAjaxRequest. This is done
during application traversal. We need it so widgets can get looked
up.
Base and prefix
---------------
Forms have base_href and prefix variables. The base_href is used to compute
URLs for form actions. A form's base_href defaults to its class name.
The form's base_href also includes the base_href of its app, if its app has
a base_href. This is useful for sub-applications. Let's give our sample
application a base_href attribute as if it were a sub-application:
>>> ex = zc.ajaxform.form_example.FormExample(None, request)
>>> ex.base_href = 'sample'
>>> ex.ExampleForm.base_href
'sample/ExampleForm'
>>> pprint(ex.ExampleForm.get_definition(), width=1)
{'actions': [{'label': 'Register',
'name': u'sample.ExampleForm.actions.register',
'url': u'sample/ExampleForm/register'}],
'left_fields': {'addresses': False,
'age': True,
'description': False,
'favorite_color': False,
'first_name': True,
'happy': False,
'last_name': True,
'other': True,
'pet': False,
'secret': False,
'siblings': False,
'temperment': False,
'weight': False},
'prefix': 'sample.ExampleForm',
'widgets': [{'fieldHint': u'Given name.',
'fieldLabel': u'First name',
'id': 'first_name',
'minLength': 0,
'name': 'first_name',
'required': True,
'value': u'Happy',
'widget_constructor': 'zope.schema.TextLine'},
{'fieldHint': u'Family name.',
'fieldLabel': u'Last name',
'id': 'last_name',
'minLength': 0,
'name': 'last_name',
'required': True,
'value': u'Camper',
'widget_constructor': 'zope.schema.TextLine'},
{'fieldHint': u'',
'fieldLabel': u'Favorite color',
'id': 'favorite_color',
'minLength': 0,
'name': 'favorite_color',
'required': False,
'value': u'Blue',
'widget_constructor': 'zope.schema.TextLine'},
{'allowBlank': False,
'fieldHint': u'Age in years',
'fieldLabel': u'Age',
'field_max': 200,
'field_min': 0,
'id': 'age',
'name': 'age',
'required': True,
'value': u'23',
'widget_constructor': 'zope.schema.Int'},
{'fieldHint': u'Are they happy?',
'fieldLabel': u'Happy',
'id': 'happy',
'name': 'happy',
'required': True,
'value': True,
'widget_constructor': 'zope.schema.Bool'},
{'fieldHint': u"This person's best friend.",
'fieldLabel': u'Pet',
'id': 'pet',
'name': 'pet',
'required': False,
'values': [['c935d187f0b998ef720390f85014ed1e',
u'Dog'],
['fa3ebd6742c360b2d9652b7f78d9bd7d',
u'Cat'],
['071642fa72ba780ee90ed36350d82745',
u'Fish']],
'widget_constructor': 'zc.ajaxform.widgets.ComboBox'},
{'allowBlank': False,
'fieldHint': u'What is the person like?',
'fieldLabel': u'Temperment',
'hiddenName': 'temperment.value',
'id': 'temperment',
'name': 'temperment',
'required': True,
'value': 'Right Neighborly',
'values': [['Nice',
u'Nice'],
['Mean',
u'Mean'],
['Ornery',
u'Ornery'],
['Right Neighborly',
u'Right Neighborly']],
'widget_constructor': 'zope.schema.Choice'},
{'allowBlank': False,
'fieldHint': u'Weight in lbs?',
'fieldLabel': u'Weight',
'id': 'weight',
'name': 'weight',
'required': True,
'widget_constructor': 'zope.schema.Decimal'},
{'fieldHint': u'What do they look like?',
'fieldLabel': u'Description',
'id': 'description',
'minLength': 0,
'name': 'description',
'required': True,
'value': u'10ft tall\nRazor sharp scales.',
'widget_constructor': 'zope.schema.Text'},
{'fieldHint': u"Don't tell anybody",
'fieldLabel': u'Secret Key',
'id': 'secret',
'name': 'secret',
'required': True,
'value': u'5ecret sauce',
'widget_constructor': 'zc.ajaxform.widgets.Hidden'},
{'allowBlank': False,
'fieldHint': u'Number of siblings',
'fieldLabel': u'Siblings',
'field_max': 8,
'field_min': 0,
'id': 'siblings',
'name': 'siblings',
'required': True,
'value': u'1',
'widget_constructor': 'zc.ajaxform.widgets.NumberSpinner'},
{'fieldHint': u'All my wonderful homes',
'fieldLabel': u'Addresses',
'id': 'addresses',
'name': 'addresses',
'record_schema': {'readonly': False,
'widgets': [{'fieldHint': u'The street',
'fieldLabel': u'Street',
'id': 'street',
'minLength': 0,
'name': 'street',
'required': True,
'widget_constructor': 'zope.schema.TextLine'},
{'fieldHint': u'The city',
'fieldLabel': u'City',
'id': 'city',
'minLength': 0,
'name': 'city',
'required': True,
'widget_constructor': 'zope.schema.TextLine'},
{'allowBlank': False,
'fieldHint': u'The awesomeness on a scale of 1 to 10',
'fieldLabel': u'Awesomeness',
'field_max': 10,
'field_min': 1,
'id': 'awesomeness',
'name': 'awesomeness',
'required': True,
'widget_constructor': 'zope.schema.Int'}]},
'required': True,
'value': [{'awesomeness': u'9',
'city': u'fakeville',
'street': u'123 fake street'},
{'awesomeness': u'9001',
'city': u'falsetown',
'street': u'345 false street'}],
'widget_constructor': 'zope.schema.List'},
{'fieldHint': u'Any other notes',
'fieldLabel': u'Other',
'id': 'other',
'minLength': 0,
'name': 'other',
'required': True,
'value': u"I've got a magic toenail",
'widget_constructor': 'zope.schema.Text'}]}
Note that the action URL now includes "sample/" as a prefix. Also
note that the widget and action names have "" as a prefix. The
form prefix is simply its base with "/"s converted to "."s.
>>> ex.ExampleForm.prefix
'sample.ExampleForm'
Form data
---------
Ajax forms are a bit different from normal web forms because the data
and the form definition can be fetched separately. For example, we
may use the same form to edit multiple objects. Form objects have a
getObjectData method that returns data suitable for editing form field
values. Let's create a person and use out form to get data for them:
>>> bob = zc.ajaxform.form_example.Person(
... first_name='bob',
... last_name='smith',
... favorite_color=None,
... age=11,
... happy=True,
... pet=u'Cockatiel',
... temperment='Nice',
... weight = 175.5,
... description = 'A real cool guy',
... secret = 'Noone knows!',
... siblings = 1,
... addresses = [],
... other = 'stuff')
>>> pprint(ex.ExampleForm.getObjectData(bob), width=1)
{'addresses': [],
'age': u'11',
'description': u'A real cool guy',
'first_name': u'bob',
'happy': True,
'last_name': u'smith',
'other': u'stuff',
'pet': u'Cockatiel',
'secret': u'Noone knows!',
'siblings': u'1',
'temperment': 'Nice',
'weight': u'175.5'}
We didn't set the favorite_color for the person, so it is ommitted
from the data.
We can pass in a dictionary of values that take precedence over object data:
>>> pprint(ex.ExampleForm.getObjectData(
... bob, {'age': u'1'}),
... width=1)
{'addresses': [],
'age': u'1',
'description': u'A real cool guy',
'first_name': u'bob',
'happy': True,
'last_name': u'smith',
'other': u'stuff',
'pet': u'Cockatiel',
'secret': u'Noone knows!',
'siblings': u'1',
'temperment': 'Nice',
'weight': u'175.5'}
Display Options
---------------
Additional display options may be sent in the widget definition if the widget
can be adapted to `IDisplayOptions`. The result of the adaptation only need
be JSON serializable.
>>> import zope.app.form.interfaces
>>> def example_options(widget):
... field, name = widget.context, widget.context.__name__
... if name == 'favorite_color':
... return {'picker': 'crayons'}
... elif name == 'secret':
... return 'super-secret'
... else:
... return None
>>> site_manager = zope.component.getSiteManager()
>>> site_manager.registerAdapter(
... example_options,
... required=(zope.app.form.interfaces.IWidget,),
... provided=zc.ajaxform.interfaces.IDisplayOptions)
>>> result = call_form(
... browser, 'http://localhost/form.html/ExampleForm')
>>> widgets = result['definition']['widgets']
>>> for widget in widgets:
... if widget.get('display_options'):
... print widget['name'] + ':', widget['display_options']
favorite_color: {u'picker': u'crayons'}
secret: super-secret
Finally, clean up.
>>> site_manager.unregisterAdapter(
... example_options,
... required=(zope.app.form.interfaces.IWidget,),
... provided=zc.ajaxform.interfaces.IDisplayOptions)
True
To-do (maybe)
-------------
More widgets!
Interface invariants
Actions:
- conditions
- validators
- failure handlers
.. [#application] See application.txt
.. [#form_classes_are_descriptors] Form classes are also
descriptors. They get called with the instance they're accessed
through.
.. [#actionids] The Javascript code that sets up action buttons uses
action name as the button's ID.
Download
********
Ajax Support
********************
The zc.ajaxform package provides framework to support:
- A single-class application model
- Nested-application support
- Integration with zope.formlib
.. contents::
Detailed Documentation
**********************
Application support
===================
The zc.ajaxform.application module provides support for writing ajax
[#ajax]_ applications. This framework started out as an experiment in
simplifying writing applications with Zope 3. I was frustrated with
ZCML situps and generally too much indirection. I ended up with a
model that I'm pretty happy with. It might not be for everybody. :)
The basic idea is that an application can be provided using a single
Zope 3 view plus necessary resource-library definitions. This view
has a URL. It typically provides many ajax methods whose URLs have the
view URL as a base.
Many applications can be implemented using a simple class that can be
registered as a view.
Let's look at a simple stupid application. :)
::
import zc.ajaxform.application
import zope.exceptions
class Calculator(zc.ajaxform.application.Trusted,
zc.ajaxform.application.Application):
resource_library_name = None
@zc.ajaxform.application.jsonpage
def about(self):
return 'Calculator 1.0'
@zc.ajaxform.application.jsonpage
def operations(self):
return ['add', "subtract"]
@zc.ajaxform.application.jsonpage
def value(self):
return dict(value=getattr(self.context, 'calculator_value', 0))
def do_add(self, value):
value += getattr(self.context, 'calculator_value', 0)
self.context.calculator_value = value
return dict(value=value)
@zc.ajaxform.application.jsonpage
def add(self, value):
if not isinstance(value, int):
return dict(error="The value must be an integer!")
return self.do_add(value)
@zc.ajaxform.application.jsonpage
def subtract(self, value):
if not isinstance(value, int):
raise zope.exceptions.UserError(
"The value must be an integer!")
return self.do_add(-value)
@zc.ajaxform.application.jsonpage
def noop(self):
pass
@zc.ajaxform.application.page
def none(self):
return "null"
@zc.ajaxform.application.jsonpage
def echo_form(self):
def maybe_file(v):
if hasattr(v, 'read'):
return ("<File upload name=%r content-type=%r size=%r>"
% (v.filename, v.headers['content-type'], len(v.read()))
)
else:
return v
return dict(
(name, maybe_file(v))
for (name, v) in self.request.form.items()
)
@zc.ajaxform.application.jsonpage
def doh(self):
raise TypeError("Doh!")
We subclass zc.ajaxform.application.Trusted. This is a minimal
base class that provides a constructor that takes a context and a
request and removes the security proxy from the context. It
overrides the constructor from zc.ajaxform.application.Application.
We also subclass zc.ajaxform.application.Application. This is a base
class that provides:
- a basic constructor that takes context and request arguments and sets
corresponding attributes,
- traversal to attributes that provide IBrowserPublisher with
conversion of dots to underscores,
- a default "page" named index.html,
- a template method that returns an HTML page with an empty head.
- an index_html method that loads a resource library and calls the
template,
- an interface declaration that it provides IBrowserPublisher, and
- an adapter declaration that adapts
zope.traversing.interfaces.IContainmentRoot and
zope.publisher.interfaces.browser.IBrowserRequest.
The main goals of this base class are to make it easy to load
Javascript and to make it easy to define ajax methods to support the
Javascript. For that reason, we provide a traverser that traverses to
object attributes that provide IBrowserPublisher. The
zc.ajaxform.application.jsonpage decorator is also an important part of
this. It makes methods accessible and automatically marshals their
result to JSON [#jsoninput]_. There's also a
zc.ajaxform.application.page decorator that makes methods accessible
without the automatic marshalling. The use of a default page, rather
than just a __call__ method is to cause the URL base to be the view,
rather than the view's context. This allows the Javascript code to
use relative URLs to refer to the ajax methods.
The class expects subclasses to define a resource_library_name
attribute [#missing_resource_library_name]_. For these applications,
you pretty much always want to use an associated Javascript file and
other resources (supporting JS, CSS, etc.). You can suppress the use
of the resource library by setting the value of this attribute to
None.
For applications that build pages totally in Javascript, the default
template is adequate. For applications that need to support
non-Javascript-enabled browsers, that want to support search-engine
optimization [#sso]_, or that want to provide some Javascript data
during the initial page load, a custom template can be provided by
simply overriding the template method with a page template or a method
that calls one.
The view can be registered with a simple adapter registration:
::
<configure xmlns="http://namespaces.zope.org/zope">
<adapter name="calculator.html"
factory="zc.ajaxform.calculator_example.Calculator"
permission="zope.View"
/>
</configure>
If we wanted to register it for an object other than the an
IContainmentRoot, we could just provide specifically adapted interfaces
or classes in the registration.
Let's access the calculator with a test browser
>>> import zope.testbrowser.testing
>>> browser = zope.testbrowser.testing.Browser()
>>> browser.open('http://localhost/')
Traceback (most recent call last):
...
HTTPError: HTTP Error 401: Unauthorized
Because our view was registered to require zope.View, the request was
unauthorized. Let's login. In the demo setup, we login by just
providing a login form variable.
>>> browser.open('http://localhost/calculator.html?login')
>>> print browser.contents # doctest: +NORMALIZE_WHITESPACE
<html><head>
<base href="http://localhost/calculator.html/index.html" />
</head></html>
We registered our view as calculator.html. Because of the way it sets the
browser default page for itself, it becomes the base href for the
page. This allows us to access ajax methods using relative URLs.
Our calculator view provides a value method. It uses the
zc.ajaxform.application.jsonpage decorator. This does 2 things:
- Arranges that the method can be traversed to,
- marshals the result to JSON.
The way results are marshalled to JSON deserves some
explanation. To support automation of ajax calls, we:
- Always return objects
- If there is an error, we include:
- an error property providing an error messahe, and/or
- when handling form submissions, an errors property with am object value
mapping field names to field-specific error messages.
::
>>> import simplejson
>>> browser.open('http://localhost/@@calculator.html/value')
>>> simplejson.loads(browser.contents)
{u'value': 0}
>>> browser.open('http://localhost/@@calculator.html/add?value=hi')
>>> simplejson.loads(browser.contents)
{u'error': u'The value must be an integer!'}
Things other than a dictionary can be returned:
>>> browser.open('http://localhost/@@calculator.html/about')
>>> simplejson.loads(browser.contents)
u'Calculator 1.0'
>>> browser.open('http://localhost/@@calculator.html/operations')
>>> simplejson.loads(browser.contents)
[u'add', u'subtract']
If you want to marshal JSON yourself, you can use the
zc.ajaxform.application.jsonpage decorator:
>>> browser.open('http://localhost/@@calculator.html/none')
An alternative way to return errors is to raise user errors, as is
done by the subtract method in our example:
>>> browser.open('http://localhost/@@calculator.html/subtract?value=hi')
>>> simplejson.loads(browser.contents)
{u'error': u'The value must be an integer!'}
This works because there is a view registered for
zope.exceptions.interfaces.IUserError, and
zc.ajaxform.interfaces.IAjaxRequest.
Testing support
===============
zc.ajaxform.testing has some helper functions to make it easier to test
ajax calls.
The zc.ajaxform.testing.FormServer class provides some convenience for
making ajax calls in which data are sent as form data and returned as
JSON. The class takes a browser and returns an object that can be
called to make server calls:
>>> import zc.ajaxform.testing, pprint
>>> server = zc.ajaxform.testing.FormServer(browser)
>>> pprint.pprint(server('/calculator.html/echo_form',
... {'a': 1.0}, b=[1, 2, 3], c=True, d='d', e=u'e\u1234'
... ), width=1)
{u'a': u'1.0',
u'b': [u'1',
u'2',
u'3'],
u'c': u'1',
u'd': u'd',
u'e': u'e\u1234'}
When we call the server, we pass a URL to invoke, which may be
relative, a optional dictionary of parameter values, and optional
keyword arguments.
Note that the application will recieve data as strings, which is what
we see echoed back in the example above.
If the application is written using Zope, then we can enable Zope form
marshalling, by passing a True value when we create the server:
>>> server = zc.ajaxform.testing.FormServer(browser, True)
>>> pprint.pprint(server('/calculator.html/echo_form',
... {'a': 1.0}, b=[1, 2, 3], c=True, d='d', e=u'e\u1234'
... ), width=1)
{u'a': 1.0,
u'b': [1,
2,
3],
u'c': True,
u'd': u'd',
u'e': u'e\u1234'}
>>> pprint.pprint(server('/calculator.html/add', {'value': 1}), width=1)
{u'value': 1}
>>> pprint.pprint(server('/calculator.html/add', value=1), width=1)
{u'value': 2}
The methods called are assumed to return JSON and the resulting data
is converted back into Python.
The function pprint method combines pprint and calling:
>>> server.pprint('/calculator.html/add', {'value': 1})
{u'value': 3}
>>> server.pprint('/calculator.html/add', value=1)
{u'value': 4}
>>> server.pprint('/calculator.html/echo_form',
... {'a': 1.0}, b=[1, 2, 3], c=True, d='d', e=u'e\u1234'
... )
{u'a': 1.0,
u'b': [1,
2,
3],
u'c': True,
u'd': u'd',
u'e': u'e\u1234'}
In the future, there will be versions of these functions that send
data as JSON.
We can include file-upload data by including a 3-tuple with a file
name, a content type, and a data string:
>>> server.pprint('/calculator.html/echo_form',
... b=[1, 2, 3], c=True, d='d',
... file=('foo.xml', 'test/xml', '<foo></foo>'),
... )
{u'b': [1,
2,
3],
u'c': True,
u'd': u'd',
u'file': u"<File upload name=u'foo.xml' content-type='test/xml' size=11>"}
as a convenience, you can pass a URL string to the server constructor,
which will create a browser for you that has opened that URL. You can
also omit the brower and an unopened browser will be created.
>>> server = zc.ajaxform.testing.FormServer(
... 'http://localhost/calculator.html?login')
>>> server.browser.url
'http://localhost/calculator.html?login'
>>> server.pprint('/calculator.html/echo_form', x=1)
{u'x': u'1'}
>>> server = zc.ajaxform.testing.FormServer(zope_form_marshalling=True)
>>> server.browser.open('http://localhost/calculator.html?login')
>>> server.pprint('/calculator.html/echo_form', x=1)
{u'x': 1}
In the example above, we didn't provide a browser, but we provided the
zope_form_marshalling flag as a keyword option.
.. Edge case: we can't traverse to undecorated methods:
>>> server.pprint('/calculator.html/do_add', value=1)
Traceback (most recent call last):
...
HTTPError: HTTP Error 404: Not Found
"Library" applications
======================
The "application" model described above is pretty appealing in its
simplicity -- at least to me. :) Usually, we'd like to make out
applications a bit more flexible in their use. In particular, we
often want to assemble applications together. At the Javascript level,
this often means having an application return a panel that can be used
in some higher-level layout. At the server level, we need to provide
a way to access application logic within some larger context. There
are two parts to this:
1. The containing application needs to support traversal to the
sub-application.
2. The subapplication needs to know how it was traversed to, at least
if it generates URLs. For example, the form machinery [#forms]_
generates URLs for action handlers.
Sub-application should expose the URL needed to access then as a
base_href attribute. This is usually a relative URL relative to the base
application.
There are a number of classes defined in zc.ajaxform.application that
help with creating sub-applications:
SubApplication
This class, which Application subclasses, provides traversal to
attributes that provide IBrowserPublisher. It also stamps
IAjaxRequest on the request object when an object is traversed
[#iajaxrequest]_ .
(Maybe this request stamping should be done further down the
traversal chain or perhaps only done if X-Requested-With is
xmlhttprequest.)
PublicTraversable
This class provides security declarations that allow objects to be
traversable publically. This is appropriate for sub-applications
that want the same protections as the object being traversed.
Let's look at our calculator example as a subapplication:
::
import zc.ajaxform.application
import zope.exceptions
class Container(zc.ajaxform.application.Application):
resource_library_name = None
@property
def calc(self):
return Calculator(self.context, self.request, base_href='calc')
class Calculator(zc.ajaxform.application.Trusted,
zc.ajaxform.application.SubApplication,
zc.ajaxform.application.PublicTraversable,
):
@zc.ajaxform.application.jsonpage
def operations(self):
return [['add', self.base_href+'/add'],
['add', self.base_href+'/subtract'],
]
@zc.ajaxform.application.jsonpage
def value(self):
return dict(value=getattr(self.context, 'calculator_value', 0))
def do_add(self, value):
value += getattr(self.context, 'calculator_value', 0)
self.context.calculator_value = value
return dict(value=value)
@zc.ajaxform.application.jsonpage
def add(self, value):
if not isinstance(value, int):
return dict(error="The value must be an integer!")
return self.do_add(value)
@zc.ajaxform.application.jsonpage
def subtract(self, value):
if not isinstance(value, int):
raise zope.exceptions.UserError(
"The value must be an integer!")
return self.do_add(-value)
Here, we've defined a container application that simply provides
traversal to a calculator subapplication as a static property. It
creates the calculator with the application's context and request. It
passes a base_href as a keyword argument, which SubApplication's
constructor accepts. Our ZCML configuration is pretty simple:
::
<configure xmlns="http://namespaces.zope.org/zope">
<include package="zope.app.component" file="meta.zcml" />
<adapter name="container.html"
factory="zc.ajaxform.calculator_subapplication_example.Container"
permission="zope.View"
/>
</configure>
Using the container application, we access the calculator via the
container:
>>> server.pprint('http://localhost/@@container.html/calc/add', value=1)
{u'value': 5}
We've updated the operations method to include the URL for each
operation, which is computed based on the base_href:
>>> server.pprint('http://localhost/@@container.html/calc/operations')
[[u'add',
u'calc/add'],
[u'add',
u'calc/subtract']]
Note that we didn't make any security declarations for the Calculator
class. We're relying on the protection for the container. If we
restart the browser, we see, indeed, that we can't access the
calculator:
>>> server = zc.ajaxform.testing.FormServer()
>>> server.pprint('http://localhost/@@container.html/calc/operations')
{u'session_expired': True}
Dynamic Traversal
=================
In the previous example, we traversed to a sub-application using a
static property. Sometimes, we need to traverse dynamically. We
might have a container application with a variable number of
subapplications. Examples include a portlet container and a system for
managing user-defined data types. In the later case, as users define
new data types, one or more applications get defined for each type.
zc.ajaxform.application provides a helper descriptor that allows custom
traversers to be implemented with simple Python methods. Let's look
at a simple example.
>>> import zc.ajaxform.application
>>> class Foo:
... def __str__(self):
... return 'a '+self.__class__.__name__
...
... @zc.ajaxform.application.traverser
... def demo_traverse(self, request, name):
... return "traverse: %s %s %s" % (self, request, name)
This is a rather silly traverser for demonstration purposes that just
returnes a transformed name.
>>> foo = Foo()
>>> foo.demo_traverse.publishTraverse("a request", "xxx")
'traverse: a Foo a request xxx'
We can still call the method:
>>> foo.demo_traverse("a request", "xxx")
'traverse: a Foo a request xxx'
The method provides IBrowserPublisher:
>>> import zope.publisher.interfaces.browser
>>> zope.publisher.interfaces.browser.IBrowserPublisher.providedBy(
... foo.demo_traverse)
True
The descriptor has a security declaration that allows it to be
traversed but not called from untrusted code:
>>> import zope.security.checker
>>> checker = zope.security.checker.getChecker(
... zope.security.checker.ProxyFactory(foo.demo_traverse))
>>> checker.get_permissions
{'publishTraverse': Global(CheckerPublic,zope.security.checker)}
>>> checker.set_permissions
{}
Acquisition
===========
Applications and sub-applications have __parent__ properties that
return their context. This is to support frameworks that ise
__parent__ to perform acquisition.
>>> class MyApp(zc.ajaxform.application.Application):
... pass
>>> myapp = MyApp(foo, None)
>>> myapp.__parent__ is foo
True
>>> class MySubApp(zc.ajaxform.application.SubApplication):
... pass
>>> mysubapp = MySubApp(foo, None)
>>> mysubapp.__parent__ is foo
True
System Errors
=============
System errors will be rendered as json.
>>> server = zc.ajaxform.testing.FormServer(
... 'http://localhost/calculator.html?login')
>>> server('/calculator.html/doh')
Traceback (most recent call last):
...
HTTPError: HTTP Error 500: Internal Server Error
>>> pprint.pprint(simplejson.loads(server.browser.contents), width=1)
{u'error': u'TypeError: Doh!'}
.. [#ajax] Technically, these aren't really AJAX applications, since
we rarely. if ever, use XML as a serialization format. To
emphasize this I'll use lower-case "ajax" to refer to the generic
approach of making low-level calls from Javascript rather than
doing page loads.
.. [#jsoninput] In the near future, there will also be support for
JSON method input. This will provide a number of benefits:
- It will provide more automatic marshaling of non-string
data. Now, we either have to de-marshal in the server application
code or embed marshaling data into parameter names in client
code.
- It will allow richer data structures than is practical with form data.
- It will probably allow faster ajax requests because:
- Server-side de-marshalling is done with highly optimized code
in simplejson.
- We will assume that data passed are valid method arguments and
avoid method introspection.
.. [#missing_resource_library_name] A custom attribute error message
is used if this attribute is missing that tries to be more
informative than the default attribute error.
.. [#sso] For search-engine optimization, one generally wants a
content page to actually contain its content. If one depends on
Javascript-enabled browsers, one can improve performance and
search-engine optimization by adding ancilary data in Javascript,
so as not to dilute the content.
.. [#forms] See form.txt.
.. [#iajaxrequest] Traversing into a subapplication adds IAjaxRequest to the
list of interfaces provided by the request.
>>> import zc.ajaxform.application
>>> import zc.ajaxform.interfaces
>>> import zope.publisher.browser
>>> request = zope.publisher.browser.TestRequest()
>>> class SubApp(zc.ajaxform.application.SubApplication):
... @zc.ajaxform.application.jsonpage
... def foo(self):
... return 'bar'
>>> subapp = SubApp(object(), request)
Now let's try traversing into the subapplication:
>>> zc.ajaxform.interfaces.IAjaxRequest.providedBy(request)
False
>>> subapp.publishTraverse(request, 'foo')()
'"bar"'
>>> zc.ajaxform.interfaces.IAjaxRequest.providedBy(request)
True
Note that the request keeps any provided interfaces:
>>> request = zope.publisher.browser.TestRequest()
>>> class IMyInterface(zope.interface.Interface):
... pass
>>> zope.interface.directlyProvides(request, IMyInterface)
>>> subapp.publishTraverse(request, 'foo')()
'"bar"'
>>> IMyInterface.providedBy(request)
True
System Error Logging
====================
When an error is generated, zope.app.publishing checks to see if the
error handler provides the System Error interface. If so, zope.app.publishing
logs the error.
Rather than use this indirect method of logging, there is an explicit
statement in ExceptionView that imports the logging module and logs and error
itself. We can test this with the zope.testing.loggingsupport functions.
First we set up loggingsupport. This keeps track of all error messages
written via the module passed as the parameter. In this case, zc.ajaxform.
>>> import logging
>>> import zope.testing.loggingsupport
>>> log_handler = zope.testing.loggingsupport.InstalledHandler('zc.ajaxform')
Then we create an error.
>>> server = zc.ajaxform.testing.FormServer(
... 'http://localhost/calculator.html?login')
>>> server('/calculator.html/doh')
Traceback (most recent call last):
...
HTTPError: HTTP Error 500: Internal Server Error
...And check to see that it was logged.
>>> print log_handler
zc.ajaxform.application ERROR
SysError created by zc.ajaxform
Form Processing
===============
zc.ajaxform.form provides support for server-generated forms based on
the zope.formlib library.
Forms are meant to be used as parts of larger applications. A form
provides output of JSON data for building javascript forms. Forms also
provide validation and call actions with validated data to perform
actions on form submit.
To create a form, just create a form class as a subclass of
zc.ajaxform.form.Form. This base class provides:
- an ajax __call__ method that returns a form definition,
- traversal to form actions, in much the same way that
zc.ajaxform.application.Application [#application]_ provides traversal
to json methods,
- a definitions method that can be used by ajax methods to get a form
definition as Python data, and
- a getObjectData method for getting initial form data from an
existing object.
Here's a simple example:
::
import zc.ajaxform.application
import zc.ajaxform.interfaces
import zc.ajaxform.widgets
import zc.ajaxform.form
import zope.component
import zope.interface
import zope.formlib
import zope.schema
class IAddress(zope.interface.Interface):
street = zope.schema.TextLine(
title = u"Street",
description = u"The street",
)
city = zope.schema.TextLine(
title = u"City",
description = u"The city",
)
awesomeness = zope.schema.Int(
title = u"Awesomeness",
description = u"The awesomeness on a scale of 1 to 10",
min = 1,
max = 10,
)
class Pets(zc.sourcefactory.basic.BasicSourceFactory):
def getValues(self):
return (u'Dog', u'Cat', u'Fish')
class Pet(zope.schema.TextLine):
"""A textline representing a pet.
This is just a textline, but we also have a source of common pets that
the user can choose from.
"""
class IPerson(zope.interface.Interface):
first_name = zope.schema.TextLine(
title = u"First name",
description = u"Given name.",
default= u'Happy'
)
last_name = zope.schema.TextLine(
title = u"Last name",
description = u"Family name.",
default= u'Camper'
)
favorite_color = zope.schema.TextLine(
title = u"Favorite color",
required = False,
default= u'Blue'
)
age = zope.schema.Int(
title = u"Age",
description = u"Age in years",
min = 0,
max = 200,
default= 23
)
happy = zope.schema.Bool(
title = u"Happy",
description = u"Are they happy?",
default= True
)
pet = Pet(
title=u'Pet',
description=u'This person\'s best friend.',
required=False,
)
temperment = zope.schema.Choice(
title = u"Temperment",
description = u"What is the person like?",
values = ['Nice', 'Mean', 'Ornery', 'Right Neighborly'],
default = u'Right Neighborly'
)
weight = zope.schema.Decimal(
title = u"Weight",
description = u"Weight in lbs?"
)
description = zope.schema.Text(
title = u"Description",
description = u"What do they look like?",
default = u'10ft tall\nRazor sharp scales.'
)
secret = zope.schema.TextLine(
title = u"Secret Key",
description = u"Don't tell anybody",
default = u'5ecret sauce'
)
siblings = zope.schema.Int(
title = u"Siblings",
description = u"Number of siblings",
min = 0,
max = 8,
default = 1
)
addresses = zope.schema.List(
title = u'Addresses',
description = u"All my wonderful homes",
value_type = zope.schema.Object(schema=IAddress),
default= [{'street':'123 fake street',
'city': 'fakeville',
'awesomeness': '9'},
{'street':'345 false street',
'city': 'falsetown',
'awesomeness': '9001'}
]
)
other = zope.schema.Text(
title = u"Other",
description = u"Any other notes",
default = u"I've got a magic toenail"
)
class Person:
zope.interface.implements(IPerson)
def __init__(self, first_name, last_name, favorite_color, age, happy,
pet, temperment, weight, description, secret, siblings,
addresses, other):
self.first_name = first_name
self.last_name = last_name
self.favorite_color = favorite_color
self.age = age
self.happy = happy
self.pet = pet
self.temperment = temperment
self.weight = weight
self.description = description
self.secret = secret
self.siblings = siblings
self.addresses = addresses
self.other = other
class FormExample(zc.ajaxform.application.Application):
resource_library_name = None
class ExampleForm(zc.ajaxform.form.Form):
leftFields = ('first_name', 'last_name', 'age', 'other')
form_fields = zope.formlib.form.Fields(IPerson)
form_fields['secret'].custom_widget = zc.ajaxform.widgets.Hidden
form_fields['siblings'].custom_widget = zc.ajaxform.widgets.NumberSpinner
@zope.formlib.form.action("Register")
def register(self, action, data):
person = Person(**data)
return dict(
data = data,
self_class_name = self.__class__.__name__,
self_app_class_name = self.app.__class__.__name__,
self_context_class_name = self.context.__class__.__name__
)
class PetWidget(zc.ajaxform.widgets.ComboBox):
zope.component.adapts(
Pet,
zc.ajaxform.interfaces.IAjaxRequest)
zope.interface.implements(
zc.ajaxform.interfaces.IInputWidget)
def __init__(self, context, request):
super(PetWidget, self).__init__(context, Pets(), request)
Note that we've nested our form definition in an application. We can
define the form class elsewhere and use it, but if a form is only used
in an application, then it's often easiest to define it within an
application class. Forms are instantiated by calling them with a
single argument. This argument, the application, becomes the form's `app`
attribute. The application's context becomes the form's context. Form
classes are automatically instantiated when a form class is assigned to
an attribute in a class and accessed through an instance
[#form_classes_are_descriptors]_.
Let's try accessing our form, which can be found in its python form
in form_example.py:
>>> import zope.testbrowser.testing
>>> from zc.ajaxform.testing import call_form, print_form
>>> browser = zope.testbrowser.testing.Browser()
>>> browser.open('http://localhost/form.html?login')
>>> print_form(browser, 'http://localhost/form.html/ExampleForm')
{u'definition': {u'actions': [{u'label': u'Register',
u'name': u'ExampleForm.actions.register',
u'url': u'ExampleForm/register'}],
u'left_fields': {u'addresses': False,
u'age': True,
u'description': False,
u'favorite_color': False,
u'first_name': True,
u'happy': False,
u'last_name': True,
u'other': True,
u'pet': False,
u'secret': False,
u'siblings': False,
u'temperment': False,
u'weight': False},
u'prefix': u'ExampleForm',
u'widgets': [{u'fieldHint': u'Given name.',
u'fieldLabel': u'First name',
u'id': u'first_name',
u'minLength': 0,
u'name': u'first_name',
u'required': True,
u'value': u'Happy',
u'widget_constructor': u'zope.schema.TextLine'},
{u'fieldHint': u'Family name.',
u'fieldLabel': u'Last name',
u'id': u'last_name',
u'minLength': 0,
u'name': u'last_name',
u'required': True,
u'value': u'Camper',
u'widget_constructor': u'zope.schema.TextLine'},
{u'fieldHint': u'',
u'fieldLabel': u'Favorite color',
u'id': u'favorite_color',
u'minLength': 0,
u'name': u'favorite_color',
u'required': False,
u'value': u'Blue',
u'widget_constructor': u'zope.schema.TextLine'},
{u'allowBlank': False,
u'fieldHint': u'Age in years',
u'fieldLabel': u'Age',
u'field_max': 200,
u'field_min': 0,
u'id': u'age',
u'name': u'age',
u'required': True,
u'value': u'23',
u'widget_constructor': u'zope.schema.Int'},
{u'fieldHint': u'Are they happy?',
u'fieldLabel': u'Happy',
u'id': u'happy',
u'name': u'happy',
u'required': True,
u'value': True,
u'widget_constructor': u'zope.schema.Bool'},
{u'fieldHint': u"This person's best friend.",
u'fieldLabel': u'Pet',
u'id': u'pet',
u'name': u'pet',
u'required': False,
u'values': [[u'c935d187f0b998ef720390f85014ed1e',
u'Dog'],
[u'fa3ebd6742c360b2d9652b7f78d9bd7d',
u'Cat'],
[u'071642fa72ba780ee90ed36350d82745',
u'Fish']],
u'widget_constructor': u'zc.ajaxform.widgets.ComboBox'},
{u'allowBlank': False,
u'fieldHint': u'What is the person like?',
u'fieldLabel': u'Temperment',
u'hiddenName': u'temperment.value',
u'id': u'temperment',
u'name': u'temperment',
u'required': True,
u'value': u'Right Neighborly',
u'values': [[u'Nice',
u'Nice'],
[u'Mean',
u'Mean'],
[u'Ornery',
u'Ornery'],
[u'Right Neighborly',
u'Right Neighborly']],
u'widget_constructor': u'zope.schema.Choice'},
{u'allowBlank': False,
u'fieldHint': u'Weight in lbs?',
u'fieldLabel': u'Weight',
u'id': u'weight',
u'name': u'weight',
u'required': True,
u'widget_constructor': u'zope.schema.Decimal'},
{u'fieldHint': u'What do they look like?',
u'fieldLabel': u'Description',
u'id': u'description',
u'minLength': 0,
u'name': u'description',
u'required': True,
u'value': u'10ft tall\nRazor sharp scales.',
u'widget_constructor': u'zope.schema.Text'},
{u'fieldHint': u"Don't tell anybody",
u'fieldLabel': u'Secret Key',
u'id': u'secret',
u'name': u'secret',
u'required': True,
u'value': u'5ecret sauce',
u'widget_constructor': u'zc.ajaxform.widgets.Hidden'},
{u'allowBlank': False,
u'fieldHint': u'Number of siblings',
u'fieldLabel': u'Siblings',
u'field_max': 8,
u'field_min': 0,
u'id': u'siblings',
u'name': u'siblings',
u'required': True,
u'value': u'1',
u'widget_constructor': u'zc.ajaxform.widgets.NumberSpinner'},
{u'fieldHint': u'All my wonderful homes',
u'fieldLabel': u'Addresses',
u'id': u'addresses',
u'name': u'addresses',
u'record_schema': {u'readonly': False,
u'widgets': [{u'fieldHint': u'The street',
u'fieldLabel': u'Street',
u'id': u'street',
u'minLength': 0,
u'name': u'street',
u'required': True,
u'widget_constructor': u'zope.schema.TextLine'},
{u'fieldHint': u'The city',
u'fieldLabel': u'City',
u'id': u'city',
u'minLength': 0,
u'name': u'city',
u'required': True,
u'widget_constructor': u'zope.schema.TextLine'},
{u'allowBlank': False,
u'fieldHint': u'The awesomeness on a scale of 1 to 10',
u'fieldLabel': u'Awesomeness',
u'field_max': 10,
u'field_min': 1,
u'id': u'awesomeness',
u'name': u'awesomeness',
u'required': True,
u'widget_constructor': u'zope.schema.Int'}]},
u'required': True,
u'value': [{u'awesomeness': u'9',
u'city': u'fakeville',
u'street': u'123 fake street'},
{u'awesomeness': u'9001',
u'city': u'falsetown',
u'street': u'345 false street'}],
u'widget_constructor': u'zope.schema.List'},
{u'fieldHint': u'Any other notes',
u'fieldLabel': u'Other',
u'id': u'other',
u'minLength': 0,
u'name': u'other',
u'required': True,
u'value': u"I've got a magic toenail",
u'widget_constructor': u'zope.schema.Text'}]}}
Our application is at: "http://localhost/form.html". The form is
exposed as an ajax method named "ExampleForm", which comes from the attribute
name in the class definition.
The form definition contains both action definitions and widget
definitions. The widget definitions may be full js field definitions
or name a widget_constructor, which is a Javascript helper provided by
the zc.ajaxform resource library that provides additional information,
like Javascript validators, that can't be expressed in JSON.
There is an action definition for each action defined in the form. The
action information includes the url to post the result to, relative to
the application.
Note that the name of the form class is used as the form prefix and
that the form prefix is used as the prefix for widget and action names
and ids [#actionids]_.
Let's post a result back:
>>> print_form(browser, 'http://localhost/form.html/ExampleForm/register',
... {'first_name': 'Bob',
... 'last_name': '',
... 'favorite_color': '',
... 'age': '-1',
... })
{u'errors': {u'addresses': u'Addresses: Missing Input',
u'age': u'Value is too small',
u'description': u'Description: Missing Input',
u'last_name': u'Last name: Missing Input',
u'other': u'Other: Missing Input',
u'secret': u'Secret Key: Missing Input',
u'siblings': u'Siblings: Missing Input',
u'temperment': u'Temperment: Missing Input',
u'weight': u'Weight: Missing Input'}}
The result had 9 problems:
- We didn't provide a last name, description, secret key,
number of siblings, temperment, or weight, which are all required
- In the form we did not specify deleting either of our two current
address records, but also ommitted their data, and the first of them
reported a missing field. Following this we will delete them both and
add a new record.
- We specified an invalid age.
Let's pass valid data:
>>> print_form(browser, 'http://localhost/form.html/ExampleForm/register',
... {'first_name': 'Bob',
... 'last_name': 'Zope',
... 'favorite_color': '',
... 'age': '11',
... 'addresses.street.0': '123 Fake Ln.',
... 'addresses.city.0': 'Fakeville',
... 'addresses.awesomeness.0': 7,
... 'description': 'Hello',
... 'other': 'So nice to meet you',
... 'secret': 'Oh nothing',
... 'siblings': 1,
... 'temperment': 'Nice',
... 'weight': '170.5',
... 'pet': 'Carrier Pigeon'
... })
{u'data': {u'addresses': [{u'awesomeness': 7,
u'city': u'Fakeville',
u'street': u'123 Fake Ln.'}],
u'age': 11,
u'description': u'Hello',
u'favorite_color': u'',
u'first_name': u'Bob',
u'happy': False,
u'last_name': u'Zope',
u'other': u'So nice to meet you',
u'pet': u'Carrier Pigeon',
u'secret': u'Oh nothing',
u'siblings': 1,
u'temperment': u'Nice',
u'weight': u'170.5'},
u'self_app_class_name': u'FormExample',
u'self_class_name': u'ExampleForm',
u'self_context_class_name': u'Folder'}
Here we get a successful result. Our contrived action in the example
simply echoed back the data it was passed, Note, in particular that:
- the data keys have the form prefix removed, and
- the value of the age key is an integer, since the field was an
integer field.
Note that for the list field, the original request added a prefix of the list
field name to prevent collisions with a field with the same name in another
list field (if it existed).
The action also prints out the classes of its self argument, its app
and its context. Actions are methods of forms so their `self` argument is the
form. The form's `app` is the app through which it is accessed and
`context` is the app's context.
For list widgets if a field in its record is missing and it is required, the
error reported lists the field name:
>>> print_form(browser, 'http://localhost/form.html/ExampleForm/register',
... {'first_name': 'Bob',
... 'last_name': 'Zope',
... 'favorite_color': '',
... 'age': '11',
... 'addresses.street.0': '123 Fake Ln.',
... 'addresses.city.0': 'Fakeville',
... 'addresses.awesomeness.0': 7,
... 'addresses.street.1': 'The 2nd Missing field St.',
... 'addresses.awesomeness.1': 3,
... 'description': 'Hello',
... 'other': 'So nice to meet you',
... 'secret': 'Oh nothing',
... 'siblings': 1,
... 'temperment': 'Nice',
... 'weight': '170.5',
... 'pet': 'Carrier Pigeon'
... })
{u'errors': {u'addresses': u'City: Missing Input'}}
Let's provide this value now:
>>> print_form(browser, 'http://localhost/form.html/ExampleForm/register',
... {'first_name': 'Bob',
... 'last_name': 'Zope',
... 'favorite_color': '',
... 'age': '11',
... 'addresses.street.0': '123 Fake Ln.',
... 'addresses.city.0': 'Fakeville',
... 'addresses.awesomeness.0': 7,
... 'addresses.street.1': 'The 2nd Missing field St.',
... 'addresses.city.1': 'A Void',
... 'addresses.awesomeness.1': '3',
... 'description': 'Hello',
... 'other': 'So nice to meet you',
... 'secret': 'Oh nothing',
... 'siblings': 1,
... 'temperment': 'Nice',
... 'weight': '170.5',
... 'pet': 'Carrier Pigeon'
... })
{u'data': {u'addresses': [{u'awesomeness': 7,
u'city': u'Fakeville',
u'street': u'123 Fake Ln.'},
{u'awesomeness': 3,
u'city': u'A Void',
u'street': u'The 2nd Missing field St.'}],
u'age': 11,
u'description': u'Hello',
u'favorite_color': u'',
u'first_name': u'Bob',
u'happy': False,
u'last_name': u'Zope',
u'other': u'So nice to meet you',
u'pet': u'Carrier Pigeon',
u'secret': u'Oh nothing',
u'siblings': 1,
u'temperment': u'Nice',
u'weight': u'170.5'},
u'self_app_class_name': u'FormExample',
u'self_class_name': u'ExampleForm',
u'self_context_class_name': u'Folder'}
Reordering items in a list is accomplished by reordering the suffix for the
record fields:
>>> print_form(browser, 'http://localhost/form.html/ExampleForm/register',
... {'first_name': 'Bob',
... 'last_name': 'Zope',
... 'favorite_color': '',
... 'age': '11',
... 'addresses.street.1': '123 Fake Ln.',
... 'addresses.city.1': 'Fakeville',
... 'addresses.awesomeness.1': 7,
... 'addresses.street.0': 'The 2nd Missing field St.',
... 'addresses.city.0': 'A Void',
... 'addresses.awesomeness.0': 3,
... 'description': 'Hello',
... 'other': 'So nice to meet you',
... 'secret': 'Oh nothing',
... 'siblings': 1,
... 'temperment': 'Nice',
... 'weight': '170.5',
... 'pet': 'Carrier Pigeon'
... })
{u'data': {u'addresses': [{u'awesomeness': 3,
u'city': u'A Void',
u'street': u'The 2nd Missing field St.'},
{u'awesomeness': 7,
u'city': u'Fakeville',
u'street': u'123 Fake Ln.'}],
u'age': 11,
u'description': u'Hello',
u'favorite_color': u'',
u'first_name': u'Bob',
u'happy': False,
u'last_name': u'Zope',
u'other': u'So nice to meet you',
u'pet': u'Carrier Pigeon',
u'secret': u'Oh nothing',
u'siblings': 1,
u'temperment': u'Nice',
u'weight': u'170.5'},
u'self_app_class_name': u'FormExample',
u'self_class_name': u'ExampleForm',
u'self_context_class_name': u'Folder'}
Getting definitions from Python
-------------------------------
Sometimes we want to get form definitions from Python. The form
__call__ method returns a JSON string. We can get Python data by
calling get_definition.
>>> import zc.ajaxform.form_example
>>> import zope.publisher.browser
>>> request = zope.publisher.browser.TestRequest()
>>> import zc.ajaxform.interfaces
>>> import zope.interface
>>> zope.interface.alsoProvides(
... request, zc.ajaxform.interfaces.IAjaxRequest)
>>> ex = zc.ajaxform.form_example.FormExample(None, request)
>>> from pprint import pprint
>>> pprint(ex.ExampleForm.get_definition(), width=1)
{'actions': [{'label': 'Register',
'name': u'ExampleForm.actions.register',
'url': u'ExampleForm/register'}],
'left_fields': {'addresses': False,
'age': True,
'description': False,
'favorite_color': False,
'first_name': True,
'happy': False,
'last_name': True,
'other': True,
'pet': False,
'secret': False,
'siblings': False,
'temperment': False,
'weight': False},
'prefix': 'ExampleForm',
'widgets': [{'fieldHint': u'Given name.',
'fieldLabel': u'First name',
'id': 'first_name',
'minLength': 0,
'name': 'first_name',
'required': True,
'value': u'Happy',
'widget_constructor': 'zope.schema.TextLine'},
{'fieldHint': u'Family name.',
'fieldLabel': u'Last name',
'id': 'last_name',
'minLength': 0,
'name': 'last_name',
'required': True,
'value': u'Camper',
'widget_constructor': 'zope.schema.TextLine'},
{'fieldHint': u'',
'fieldLabel': u'Favorite color',
'id': 'favorite_color',
'minLength': 0,
'name': 'favorite_color',
'required': False,
'value': u'Blue',
'widget_constructor': 'zope.schema.TextLine'},
{'allowBlank': False,
'fieldHint': u'Age in years',
'fieldLabel': u'Age',
'field_max': 200,
'field_min': 0,
'id': 'age',
'name': 'age',
'required': True,
'value': u'23',
'widget_constructor': 'zope.schema.Int'},
{'fieldHint': u'Are they happy?',
'fieldLabel': u'Happy',
'id': 'happy',
'name': 'happy',
'required': True,
'value': True,
'widget_constructor': 'zope.schema.Bool'},
{'fieldHint': u"This person's best friend.",
'fieldLabel': u'Pet',
'id': 'pet',
'name': 'pet',
'required': False,
'values': [['c935d187f0b998ef720390f85014ed1e',
u'Dog'],
['fa3ebd6742c360b2d9652b7f78d9bd7d',
u'Cat'],
['071642fa72ba780ee90ed36350d82745',
u'Fish']],
'widget_constructor': 'zc.ajaxform.widgets.ComboBox'},
{'allowBlank': False,
'fieldHint': u'What is the person like?',
'fieldLabel': u'Temperment',
'hiddenName': 'temperment.value',
'id': 'temperment',
'name': 'temperment',
'required': True,
'value': 'Right Neighborly',
'values': [['Nice',
u'Nice'],
['Mean',
u'Mean'],
['Ornery',
u'Ornery'],
['Right Neighborly',
u'Right Neighborly']],
'widget_constructor': 'zope.schema.Choice'},
{'allowBlank': False,
'fieldHint': u'Weight in lbs?',
'fieldLabel': u'Weight',
'id': 'weight',
'name': 'weight',
'required': True,
'widget_constructor': 'zope.schema.Decimal'},
{'fieldHint': u'What do they look like?',
'fieldLabel': u'Description',
'id': 'description',
'minLength': 0,
'name': 'description',
'required': True,
'value': u'10ft tall\nRazor sharp scales.',
'widget_constructor': 'zope.schema.Text'},
{'fieldHint': u"Don't tell anybody",
'fieldLabel': u'Secret Key',
'id': 'secret',
'name': 'secret',
'required': True,
'value': u'5ecret sauce',
'widget_constructor': 'zc.ajaxform.widgets.Hidden'},
{'allowBlank': False,
'fieldHint': u'Number of siblings',
'fieldLabel': u'Siblings',
'field_max': 8,
'field_min': 0,
'id': 'siblings',
'name': 'siblings',
'required': True,
'value': u'1',
'widget_constructor': 'zc.ajaxform.widgets.NumberSpinner'},
{'fieldHint': u'All my wonderful homes',
'fieldLabel': u'Addresses',
'id': 'addresses',
'name': 'addresses',
'record_schema': {'readonly': False,
'widgets': [{'fieldHint': u'The street',
'fieldLabel': u'Street',
'id': 'street',
'minLength': 0,
'name': 'street',
'required': True,
'widget_constructor': 'zope.schema.TextLine'},
{'fieldHint': u'The city',
'fieldLabel': u'City',
'id': 'city',
'minLength': 0,
'name': 'city',
'required': True,
'widget_constructor': 'zope.schema.TextLine'},
{'allowBlank': False,
'fieldHint': u'The awesomeness on a scale of 1 to 10',
'fieldLabel': u'Awesomeness',
'field_max': 10,
'field_min': 1,
'id': 'awesomeness',
'name': 'awesomeness',
'required': True,
'widget_constructor': 'zope.schema.Int'}]},
'required': True,
'value': [{'awesomeness': u'9',
'city': u'fakeville',
'street': u'123 fake street'},
{'awesomeness': u'9001',
'city': u'falsetown',
'street': u'345 false street'}],
'widget_constructor': 'zope.schema.List'},
{'fieldHint': u'Any other notes',
'fieldLabel': u'Other',
'id': 'other',
'minLength': 0,
'name': 'other',
'required': True,
'value': u"I've got a magic toenail",
'widget_constructor': 'zope.schema.Text'}]}
Note that we had to stamp the request with IAjaxRequest. This is done
during application traversal. We need it so widgets can get looked
up.
Base and prefix
---------------
Forms have base_href and prefix variables. The base_href is used to compute
URLs for form actions. A form's base_href defaults to its class name.
The form's base_href also includes the base_href of its app, if its app has
a base_href. This is useful for sub-applications. Let's give our sample
application a base_href attribute as if it were a sub-application:
>>> ex = zc.ajaxform.form_example.FormExample(None, request)
>>> ex.base_href = 'sample'
>>> ex.ExampleForm.base_href
'sample/ExampleForm'
>>> pprint(ex.ExampleForm.get_definition(), width=1)
{'actions': [{'label': 'Register',
'name': u'sample.ExampleForm.actions.register',
'url': u'sample/ExampleForm/register'}],
'left_fields': {'addresses': False,
'age': True,
'description': False,
'favorite_color': False,
'first_name': True,
'happy': False,
'last_name': True,
'other': True,
'pet': False,
'secret': False,
'siblings': False,
'temperment': False,
'weight': False},
'prefix': 'sample.ExampleForm',
'widgets': [{'fieldHint': u'Given name.',
'fieldLabel': u'First name',
'id': 'first_name',
'minLength': 0,
'name': 'first_name',
'required': True,
'value': u'Happy',
'widget_constructor': 'zope.schema.TextLine'},
{'fieldHint': u'Family name.',
'fieldLabel': u'Last name',
'id': 'last_name',
'minLength': 0,
'name': 'last_name',
'required': True,
'value': u'Camper',
'widget_constructor': 'zope.schema.TextLine'},
{'fieldHint': u'',
'fieldLabel': u'Favorite color',
'id': 'favorite_color',
'minLength': 0,
'name': 'favorite_color',
'required': False,
'value': u'Blue',
'widget_constructor': 'zope.schema.TextLine'},
{'allowBlank': False,
'fieldHint': u'Age in years',
'fieldLabel': u'Age',
'field_max': 200,
'field_min': 0,
'id': 'age',
'name': 'age',
'required': True,
'value': u'23',
'widget_constructor': 'zope.schema.Int'},
{'fieldHint': u'Are they happy?',
'fieldLabel': u'Happy',
'id': 'happy',
'name': 'happy',
'required': True,
'value': True,
'widget_constructor': 'zope.schema.Bool'},
{'fieldHint': u"This person's best friend.",
'fieldLabel': u'Pet',
'id': 'pet',
'name': 'pet',
'required': False,
'values': [['c935d187f0b998ef720390f85014ed1e',
u'Dog'],
['fa3ebd6742c360b2d9652b7f78d9bd7d',
u'Cat'],
['071642fa72ba780ee90ed36350d82745',
u'Fish']],
'widget_constructor': 'zc.ajaxform.widgets.ComboBox'},
{'allowBlank': False,
'fieldHint': u'What is the person like?',
'fieldLabel': u'Temperment',
'hiddenName': 'temperment.value',
'id': 'temperment',
'name': 'temperment',
'required': True,
'value': 'Right Neighborly',
'values': [['Nice',
u'Nice'],
['Mean',
u'Mean'],
['Ornery',
u'Ornery'],
['Right Neighborly',
u'Right Neighborly']],
'widget_constructor': 'zope.schema.Choice'},
{'allowBlank': False,
'fieldHint': u'Weight in lbs?',
'fieldLabel': u'Weight',
'id': 'weight',
'name': 'weight',
'required': True,
'widget_constructor': 'zope.schema.Decimal'},
{'fieldHint': u'What do they look like?',
'fieldLabel': u'Description',
'id': 'description',
'minLength': 0,
'name': 'description',
'required': True,
'value': u'10ft tall\nRazor sharp scales.',
'widget_constructor': 'zope.schema.Text'},
{'fieldHint': u"Don't tell anybody",
'fieldLabel': u'Secret Key',
'id': 'secret',
'name': 'secret',
'required': True,
'value': u'5ecret sauce',
'widget_constructor': 'zc.ajaxform.widgets.Hidden'},
{'allowBlank': False,
'fieldHint': u'Number of siblings',
'fieldLabel': u'Siblings',
'field_max': 8,
'field_min': 0,
'id': 'siblings',
'name': 'siblings',
'required': True,
'value': u'1',
'widget_constructor': 'zc.ajaxform.widgets.NumberSpinner'},
{'fieldHint': u'All my wonderful homes',
'fieldLabel': u'Addresses',
'id': 'addresses',
'name': 'addresses',
'record_schema': {'readonly': False,
'widgets': [{'fieldHint': u'The street',
'fieldLabel': u'Street',
'id': 'street',
'minLength': 0,
'name': 'street',
'required': True,
'widget_constructor': 'zope.schema.TextLine'},
{'fieldHint': u'The city',
'fieldLabel': u'City',
'id': 'city',
'minLength': 0,
'name': 'city',
'required': True,
'widget_constructor': 'zope.schema.TextLine'},
{'allowBlank': False,
'fieldHint': u'The awesomeness on a scale of 1 to 10',
'fieldLabel': u'Awesomeness',
'field_max': 10,
'field_min': 1,
'id': 'awesomeness',
'name': 'awesomeness',
'required': True,
'widget_constructor': 'zope.schema.Int'}]},
'required': True,
'value': [{'awesomeness': u'9',
'city': u'fakeville',
'street': u'123 fake street'},
{'awesomeness': u'9001',
'city': u'falsetown',
'street': u'345 false street'}],
'widget_constructor': 'zope.schema.List'},
{'fieldHint': u'Any other notes',
'fieldLabel': u'Other',
'id': 'other',
'minLength': 0,
'name': 'other',
'required': True,
'value': u"I've got a magic toenail",
'widget_constructor': 'zope.schema.Text'}]}
Note that the action URL now includes "sample/" as a prefix. Also
note that the widget and action names have "" as a prefix. The
form prefix is simply its base with "/"s converted to "."s.
>>> ex.ExampleForm.prefix
'sample.ExampleForm'
Form data
---------
Ajax forms are a bit different from normal web forms because the data
and the form definition can be fetched separately. For example, we
may use the same form to edit multiple objects. Form objects have a
getObjectData method that returns data suitable for editing form field
values. Let's create a person and use out form to get data for them:
>>> bob = zc.ajaxform.form_example.Person(
... first_name='bob',
... last_name='smith',
... favorite_color=None,
... age=11,
... happy=True,
... pet=u'Cockatiel',
... temperment='Nice',
... weight = 175.5,
... description = 'A real cool guy',
... secret = 'Noone knows!',
... siblings = 1,
... addresses = [],
... other = 'stuff')
>>> pprint(ex.ExampleForm.getObjectData(bob), width=1)
{'addresses': [],
'age': u'11',
'description': u'A real cool guy',
'first_name': u'bob',
'happy': True,
'last_name': u'smith',
'other': u'stuff',
'pet': u'Cockatiel',
'secret': u'Noone knows!',
'siblings': u'1',
'temperment': 'Nice',
'weight': u'175.5'}
We didn't set the favorite_color for the person, so it is ommitted
from the data.
We can pass in a dictionary of values that take precedence over object data:
>>> pprint(ex.ExampleForm.getObjectData(
... bob, {'age': u'1'}),
... width=1)
{'addresses': [],
'age': u'1',
'description': u'A real cool guy',
'first_name': u'bob',
'happy': True,
'last_name': u'smith',
'other': u'stuff',
'pet': u'Cockatiel',
'secret': u'Noone knows!',
'siblings': u'1',
'temperment': 'Nice',
'weight': u'175.5'}
Display Options
---------------
Additional display options may be sent in the widget definition if the widget
can be adapted to `IDisplayOptions`. The result of the adaptation only need
be JSON serializable.
>>> import zope.app.form.interfaces
>>> def example_options(widget):
... field, name = widget.context, widget.context.__name__
... if name == 'favorite_color':
... return {'picker': 'crayons'}
... elif name == 'secret':
... return 'super-secret'
... else:
... return None
>>> site_manager = zope.component.getSiteManager()
>>> site_manager.registerAdapter(
... example_options,
... required=(zope.app.form.interfaces.IWidget,),
... provided=zc.ajaxform.interfaces.IDisplayOptions)
>>> result = call_form(
... browser, 'http://localhost/form.html/ExampleForm')
>>> widgets = result['definition']['widgets']
>>> for widget in widgets:
... if widget.get('display_options'):
... print widget['name'] + ':', widget['display_options']
favorite_color: {u'picker': u'crayons'}
secret: super-secret
Finally, clean up.
>>> site_manager.unregisterAdapter(
... example_options,
... required=(zope.app.form.interfaces.IWidget,),
... provided=zc.ajaxform.interfaces.IDisplayOptions)
True
To-do (maybe)
-------------
More widgets!
Interface invariants
Actions:
- conditions
- validators
- failure handlers
.. [#application] See application.txt
.. [#form_classes_are_descriptors] Form classes are also
descriptors. They get called with the instance they're accessed
through.
.. [#actionids] The Javascript code that sets up action buttons uses
action name as the button's ID.
Download
********
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
zc.ajaxform-0.7.0.tar.gz
(57.1 kB
view hashes)