Welcome, guest | Sign In | My Account | Store | Cart

Notice! PyPM is being replaced with the ActiveState Platform, which enhances PyPM’s build and deploy capabilities. Create your free Platform account to download ActivePython or customize Python with the packages you require and get automatic updates.

Download
ActivePython
INSTALL>
pypm install zc.ajaxform

How to install zc.ajaxform

  1. Download and install ActivePython
  2. Open Command Prompt
  3. Type pypm install zc.ajaxform
 Python 2.7Python 3.2Python 3.3
Windows (32-bit)
0.7.0 Available View build log
Windows (64-bit)
0.7.0 Available View build log
Mac OS X (10.5+)
0.7.0 Available View build log
Linux (32-bit)
0.7.0 Available View build log
Linux (64-bit)
0.7.0 Available View build log
 
Author
License
ZPL 2.1
Depended by
Imports
Lastest release
version 0.7.0 on Jan 5th, 2011

The zc.ajaxform package provides framework to support:

  • A single-class application model
  • Nested-application support
  • Integration with zope.formlib

Detailed Documentation

Application support

The zc.ajaxform.application module provides support for writing ajax [1] 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. :)

System Message: WARNING/2 (<string>, line 39)

Literal block expected; none found.

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

System Message: WARNING/2 (<string>, line 117)

Bullet list ends without a blank line; unexpected unindent.

corresponding attributes,

  • traversal to attributes that provide IBrowserPublisher with

System Message: WARNING/2 (<string>, line 120)

Bullet list ends without a blank line; unexpected unindent.

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

System Message: WARNING/2 (<string>, line 127)

Bullet list ends without a blank line; unexpected unindent.

template,

  • an interface declaration that it provides IBrowserPublisher, and
  • an adapter declaration that adapts

System Message: WARNING/2 (<string>, line 132)

Bullet list ends without a blank line; unexpected unindent.

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 [2]. 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 [3]. 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 [4], 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"

System Message: ERROR/3 (<string>, line 169)

Inconsistent literal block quoting.

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

System Message: WARNING/2 (<string>, line 217)

Bullet list ends without a blank line; unexpected unindent.

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.

>>> 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 [5] 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 [6] .

(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:

System Message: WARNING/2 (<string>, line 424)

Literal block expected; none found.

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"

System Message: ERROR/3 (<string>, line 483)

Inconsistent literal block quoting.

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!'}
[1]Technically, these aren't really AJAX applications, since

System Message: WARNING/2 (<string>, line 607)

Explicit markup ends without a blank line; unexpected unindent.

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.

[2]In the near future, there will also be support for

System Message: WARNING/2 (<string>, line 613)

Explicit markup ends without a blank line; unexpected unindent.

JSON method input. This will provide a number of benefits:

  • It will provide more automatic marshaling of non-string

System Message: WARNING/2 (<string>, line 616)

Bullet list ends without a blank line; unexpected unindent.

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

System Message: WARNING/2 (<string>, line 625)

Bullet list ends without a blank line; unexpected unindent.

in simplejson.

  • We will assume that data passed are valid method arguments and

System Message: WARNING/2 (<string>, line 628)

Bullet list ends without a blank line; unexpected unindent.

avoid method introspection.

[3]A custom attribute error message

System Message: WARNING/2 (<string>, line 631)

Explicit markup ends without a blank line; unexpected unindent.

is used if this attribute is missing that tries to be more informative than the default attribute error.

[4]For search-engine optimization, one generally wants a

System Message: WARNING/2 (<string>, line 635)

Explicit markup ends without a blank line; unexpected unindent.

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.

[5]See form.txt.
[6]Traversing into a subapplication adds IAjaxRequest to the

System Message: WARNING/2 (<string>, line 643)

Explicit markup ends without a blank line; unexpected unindent.

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

System Message: WARNING/2 (<string>, line 726)

Bullet list ends without a blank line; unexpected unindent.

zc.ajaxform.application.Application [7] provides traversal to json methods,

  • a definitions method that can be used by ajax methods to get a form

System Message: WARNING/2 (<string>, line 730)

Bullet list ends without a blank line; unexpected unindent.

definition as Python data, and

  • a getObjectData method for getting initial form data from an

System Message: WARNING/2 (<string>, line 733)

Bullet list ends without a blank line; unexpected unindent.

existing object.

Here's a simple example:

System Message: WARNING/2 (<string>, line 739)

Literal block expected; none found.

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 tallnRazor 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__ )

System Message: WARNING/2 (<string>, line 905); backlink

Inline strong start-string without end-string.

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 [8].

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 [9].

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,

System Message: WARNING/2 (<string>, line 1149)

Bullet list ends without a blank line; unexpected unindent.

number of siblings, temperment, or weight, which are all required

  • In the form we did not specify deleting either of our two current

System Message: WARNING/2 (<string>, line 1152)

Bullet list ends without a blank line; unexpected unindent.

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

System Message: WARNING/2 (<string>, line 1201)

Bullet list ends without a blank line; unexpected unindent.

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
[7]See application.txt
[8]Form classes are also

System Message: WARNING/2 (<string>, line 1819)

Explicit markup ends without a blank line; unexpected unindent.

descriptors. They get called with the instance they're accessed through.

[9]The Javascript code that sets up action buttons uses

System Message: WARNING/2 (<string>, line 1823)

Explicit markup ends without a blank line; unexpected unindent.

action name as the button's ID.

Subscribe to package updates

Last updated Jan 5th, 2011

Download Stats

Last month:1

What does the lock icon mean?

Builds marked with a lock icon are only available via PyPM to users with a current ActivePython Business Edition subscription.

Need custom builds or support?

ActivePython Enterprise Edition guarantees priority access to technical support, indemnification, expert consulting and quality-assured language builds.

Plan on re-distributing ActivePython?

Get re-distribution rights and eliminate legal risks with ActivePython OEM Edition.