[Checkins] SVN: zc.ajax/ Experimental ajax library based on earlier work with Ext.
Jim Fulton
jim at zope.com
Wed Apr 1 14:21:34 EDT 2009
Log message for revision 98767:
Experimental ajax library based on earlier work with Ext.
Among other things, this includes a very basic framework for JS form
generation.
Changed:
A zc.ajax/
A zc.ajax/branches/
A zc.ajax/branches/dev/
A zc.ajax/branches/dev/README.txt
A zc.ajax/branches/dev/buildout.cfg
A zc.ajax/branches/dev/setup.py
A zc.ajax/branches/dev/src/
A zc.ajax/branches/dev/src/zc/
A zc.ajax/branches/dev/src/zc/__init__.py
A zc.ajax/branches/dev/src/zc/ajax/
A zc.ajax/branches/dev/src/zc/ajax/CHANGES.txt
A zc.ajax/branches/dev/src/zc/ajax/README.txt
A zc.ajax/branches/dev/src/zc/ajax/__init__.py
A zc.ajax/branches/dev/src/zc/ajax/application.py
A zc.ajax/branches/dev/src/zc/ajax/application.txt
A zc.ajax/branches/dev/src/zc/ajax/calculator_example.py
A zc.ajax/branches/dev/src/zc/ajax/calculator_example.zcml
A zc.ajax/branches/dev/src/zc/ajax/calculator_subapplication_example.py
A zc.ajax/branches/dev/src/zc/ajax/calculator_subapplication_example.zcml
A zc.ajax/branches/dev/src/zc/ajax/configure.zcml
A zc.ajax/branches/dev/src/zc/ajax/form.py
A zc.ajax/branches/dev/src/zc/ajax/form.txt
A zc.ajax/branches/dev/src/zc/ajax/form_example.py
A zc.ajax/branches/dev/src/zc/ajax/form_example.zcml
A zc.ajax/branches/dev/src/zc/ajax/interfaces.py
A zc.ajax/branches/dev/src/zc/ajax/session.py
A zc.ajax/branches/dev/src/zc/ajax/session.txt
A zc.ajax/branches/dev/src/zc/ajax/session.zcml
A zc.ajax/branches/dev/src/zc/ajax/testing.py
A zc.ajax/branches/dev/src/zc/ajax/tests.py
A zc.ajax/branches/dev/src/zc/ajax/tests.zcml
A zc.ajax/branches/dev/src/zc/ajax/widgets.py
A zc.ajax/branches/dev/src/zc/ajax/widgets.txt
A zc.ajax/branches/dev/src/zc/ajax/widgets.zcml
A zc.ajax/branches/dev/versions.cfg
A zc.ajax/tags/
-=-
Added: zc.ajax/branches/dev/README.txt
===================================================================
--- zc.ajax/branches/dev/README.txt (rev 0)
+++ zc.ajax/branches/dev/README.txt 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1 @@
+See src/zc/extjs/README.txt.
Property changes on: zc.ajax/branches/dev/README.txt
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/buildout.cfg
===================================================================
--- zc.ajax/branches/dev/buildout.cfg (rev 0)
+++ zc.ajax/branches/dev/buildout.cfg 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,16 @@
+[buildout]
+develop = .
+parts = test py
+allow-picked-versions = false
+use-dependency-links = false
+versions = versions
+extends = versions.cfg
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = zc.ajax [test]
+
+[py]
+recipe = zc.recipe.egg
+eggs = ${test:eggs}
+interpreter = py
Property changes on: zc.ajax/branches/dev/buildout.cfg
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/setup.py
===================================================================
--- zc.ajax/branches/dev/setup.py (rev 0)
+++ zc.ajax/branches/dev/setup.py 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,72 @@
+##############################################################################
+#
+# Copyright (c) Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+name = 'zc.ajax'
+version = '0'
+
+import os, re
+from setuptools import setup, find_packages
+
+
+entry_points = """
+"""
+
+def read(rname):
+ return open(rname).read()
+
+here = os.getcwd()
+os.chdir(os.path.join(os.path.dirname(__file__), 'src', *name.split('.')))
+long_description = re.sub(
+ r'..\s*include::\s*(\S+)\s*\n\s+:literal:',
+ (lambda m: '::\n\n %s\n' % ' '.join(open(m.group(1)).readlines())),
+ (read('README.txt')
+ + '\n'
+ 'Detailed Documentation\n'
+ '**********************\n'
+ '\n'
+ + read('application.txt')
+ + '\n'
+ + read('form.txt')
+ + '\n'
+ 'Download\n'
+ '********\n'
+ )
+ )
+
+os.chdir(here)
+
+setup(
+ name = name,
+ version = version,
+ author = 'Jim Fulton',
+ author_email = 'jim at zope.com',
+ description = '',
+ long_description=long_description,
+ license = 'ZPL 2.1',
+
+ packages = find_packages('src'),
+ namespace_packages = ['zc'],
+ package_dir = {'': 'src'},
+ include_package_data = True,
+ install_requires = [
+ 'setuptools',
+ 'simplejson',
+ 'zc.form',
+ ],
+ extras_require = dict(
+ test=['zope.app.zcmlfiles',
+ 'zope.testbrowser']),
+ zip_safe = False,
+ entry_points=entry_points,
+ )
Property changes on: zc.ajax/branches/dev/setup.py
___________________________________________________________________
Added: svn:keywords
+ Id
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/__init__.py
===================================================================
--- zc.ajax/branches/dev/src/zc/__init__.py (rev 0)
+++ zc.ajax/branches/dev/src/zc/__init__.py 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,5 @@
+try:
+ __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+ from pkgutil import extend_path
+ __path__ = extend_path(__path__, __name__)
Property changes on: zc.ajax/branches/dev/src/zc/__init__.py
___________________________________________________________________
Added: svn:keywords
+ Id
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/CHANGES.txt
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/CHANGES.txt (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/CHANGES.txt 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,8 @@
+Release history
+***************
+
+0.1.0 (unreleased)
+==================
+
+Forked from the zc.extjs and narrowed scope to just ajax APIs.
+Removed Javascript frameworks.
Property changes on: zc.ajax/branches/dev/src/zc/ajax/CHANGES.txt
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/README.txt
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/README.txt (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/README.txt 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,13 @@
+********************
+Ajax Support
+********************
+
+The zc.ajax package provides framework to support:
+
+- A single-class application model
+
+- Nested-application support
+
+- Integration with zope.formlib
+
+.. contents::
Property changes on: zc.ajax/branches/dev/src/zc/ajax/README.txt
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/__init__.py
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/__init__.py (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/__init__.py 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,13 @@
+##############################################################################
+#
+# Copyright (c) Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
Property changes on: zc.ajax/branches/dev/src/zc/ajax/__init__.py
___________________________________________________________________
Added: svn:keywords
+ Id
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/application.py
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/application.py (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/application.py 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,216 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL). A copy of the ZVSL should accompany this
+# distribution.
+#
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+
+"""Experimental support for browser "applications"
+"""
+
+import cgi
+import logging
+import simplejson
+import zc.ajax.interfaces
+import zc.resourcelibrary
+import zope.app.exception.browser.unauthorized
+import zope.app.pagetemplate
+import zope.cachedescriptors.property
+import zope.component
+import zope.exceptions.interfaces
+import zope.interface
+import zope.publisher.browser
+import zope.publisher.interfaces.browser
+import zope.security.proxy
+import zope.traversing.interfaces
+
+def result(data):
+ if not data:
+ data = dict(success=True)
+ elif isinstance(data, dict) and not 'success' in data:
+ data['success'] = not (('error' in data)
+ or ('errors' in data))
+
+ return simplejson.dumps(data)
+
+class _method(object):
+
+ zope.interface.implements(
+ zope.publisher.interfaces.browser.IBrowserPublisher)
+
+ __Security_checker__ = zope.security.checker.NamesChecker(
+ ('__call__', 'browserDefault')
+ )
+
+ def __init__(self, inst, func):
+ self.im_self = inst
+ self.im_func = func
+
+ def __call__(self, *a, **k):
+ return self.im_func(self.im_self, *a, **k)
+
+ def browserDefault(self, request):
+ return self, ()
+
+class _jsonmethod(_method):
+
+ def __call__(self, *a, **k):
+ return result(self.im_func(self.im_self, *a, **k))
+
+class page(object):
+
+ _method_class = _method
+
+ def __init__(self, func):
+ self.func = func
+
+ def __get__(self, inst, cls):
+ if inst is None:
+ return self
+ return self._method_class(inst, self.func)
+
+class jsonpage(page):
+ _method_class = _jsonmethod
+
+class AttributeTraversable(object):
+
+ zope.interface.implements(
+ zope.publisher.interfaces.browser.IBrowserPublisher)
+
+ def publishTraverse(self, request, name):
+ name = name.replace('.', '_')
+ result = getattr(self, name, None)
+ if zope.publisher.interfaces.browser.IBrowserPublisher.providedBy(
+ result):
+ zope.interface.directlyProvides(
+ request,
+ zc.ajax.interfaces.IAjaxRequest,
+ zope.interface.directlyProvidedBy(request),
+ )
+ return result
+ raise zope.publisher.interfaces.NotFound(self, name, request)
+
+ @zope.cachedescriptors.property.Lazy
+ def __parent__(self):
+ return self.context
+
+class PublicTraversable(object):
+
+ __Security_checker__ = zope.security.checker.NamesChecker((
+ 'browserDefault', 'publishTraverse'))
+
+class Trusted(object):
+
+ def __init__(self, context, *a, **kw):
+ context = zope.security.proxy.removeSecurityProxy(context)
+ super(Trusted, self).__init__(context, *a, **kw)
+
+class Application(AttributeTraversable):
+
+ zope.component.adapts(
+ zope.traversing.interfaces.IContainmentRoot,
+ zope.publisher.interfaces.browser.IBrowserRequest,
+ )
+
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+
+ def browserDefault(self, request):
+ return self, ('index.html', )
+
+ def template(self):
+ return '<html><head></head></html>'
+
+ @page
+ def index_html(self):
+ try:
+ library = self.resource_library_name
+ except AttributeError:
+ raise AttributeError(
+ "No resource_library_name attribute is defined.\n"
+ "This attribute is required to specify the name of a\n"
+ "library to use (need). It may be set to None to avoid\n"
+ "requiring a resource library."
+ )
+ if library is not None:
+ zc.resourcelibrary.need(library)
+ return self.template()
+
+class SubApplication(AttributeTraversable):
+
+ def __init__(self, context, request, base_href=None):
+ self.context = context
+ self.request = request
+ if base_href is not None:
+ self.base_href = base_href
+
+
+class traverser(object):
+
+ def __init__(self, func, inst=None):
+ self.func = func
+ self.inst = inst
+
+ def __get__(self, inst, cls):
+ if inst is None:
+ return self
+ return traverser(self.func, inst)
+
+ zope.interface.implements(
+ zope.publisher.interfaces.browser.IBrowserPublisher)
+
+ __Security_checker__ = zope.security.checker.NamesChecker((
+ 'publishTraverse', ))
+
+ def publishTraverse(self, request, name):
+ return self.func(self.inst, request, name)
+
+ def __call__(self, *args, **kw):
+ if self.inst is None:
+ return self.func(*args, **kw)
+ else:
+ return self.func(self.inst, *args, **kw)
+
+
+class UserError:
+
+ zope.interface.implements(
+ zope.publisher.interfaces.browser.IBrowserPublisher)
+ zope.component.adapts(zope.exceptions.interfaces.IUserError,
+ zc.ajax.interfaces.IAjaxRequest)
+
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+
+ def __call__(self):
+ return simplejson.dumps(dict(
+ success = False,
+ error = str(self.context),
+ ))
+
+class ExceptionView(UserError):
+
+ zope.component.adapts(Exception,
+ zc.ajax.interfaces.IAjaxRequest)
+
+ def __call__(self):
+ self.request.response.setStatus(500)
+
+ logger = logging.getLogger(__name__)
+ logger.exception(
+ 'SysError created by zc.ajax'
+ )
+ return simplejson.dumps(dict(
+ success = False,
+ error = "%s: %s" % (self.context.__class__.__name__, self.context),
+ ))
Property changes on: zc.ajax/branches/dev/src/zc/ajax/application.py
___________________________________________________________________
Added: svn:keywords
+ Id
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/application.txt
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/application.txt (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/application.txt 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,580 @@
+Application support
+===================
+
+The zc.ajax.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. :)
+
+.. include:: calculator_example.py
+ :literal:
+
+We subclass zc.ajax.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.ajax.application.Application.
+
+We also subclass zc.ajax.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.ajax.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.ajax.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:
+
+.. include:: calculator_example.zcml
+ :literal:
+
+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.ajax.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
+- Always include a "success" property, and
+- 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, u'success': True}
+
+ >>> browser.open('http://localhost/@@calculator.html/add?value=hi')
+ >>> simplejson.loads(browser.contents)
+ {u'success': False, u'error': u'The value must be an integer!'}
+
+Also, if a method returns None, an object with a true success property
+is returned:
+
+ >>> browser.open('http://localhost/@@calculator.html/noop')
+ >>> simplejson.loads(browser.contents)
+ {u'success': True}
+
+If something other than a dictionary is returned from a Python
+method, no success attribute is added:
+
+ >>> 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.ajax.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'success': False, u'error': u'The value must be an integer!'}
+
+This works because there is a view registered for
+zope.exceptions.interfaces.IUserError, and
+zc.ajax.interfaces.IAjaxRequest.
+
+Testing support
+===============
+
+zc.ajax.testing has some helper functions to make it easier to test
+ajax calls.
+
+The zc.ajax.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.ajax.testing, pprint
+ >>> server = zc.ajax.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',
+ u'success': True}
+
+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.ajax.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',
+ u'success': True}
+
+ >>> pprint.pprint(server('/calculator.html/add', {'value': 1}), width=1)
+ {u'success': True,
+ u'value': 1}
+
+ >>> pprint.pprint(server('/calculator.html/add', value=1), width=1)
+ {u'success': True,
+ 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'success': True,
+ u'value': 3}
+
+ >>> server.pprint('/calculator.html/add', value=1)
+ {u'success': True,
+ 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',
+ u'success': True}
+
+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>",
+ u'success': True}
+
+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.ajax.testing.FormServer(
+ ... 'http://localhost/calculator.html?login')
+ >>> server.browser.url
+ 'http://localhost/calculator.html?login'
+
+ >>> server.pprint('/calculator.html/echo_form', x=1)
+ {u'success': True,
+ u'x': u'1'}
+
+ >>> server = zc.ajax.testing.FormServer(zope_form_marshalling=True)
+ >>> server.browser.open('http://localhost/calculator.html?login')
+ >>> server.pprint('/calculator.html/echo_form', x=1)
+ {u'success': True,
+ 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.ajax.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:
+
+.. include:: calculator_subapplication_example.py
+ :literal:
+
+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:
+
+.. include:: calculator_subapplication_example.zcml
+ :literal:
+
+Using the container application, we access the calculator via the
+container:
+
+ >>> server.pprint('http://localhost/@@container.html/calc/add', value=1)
+ {u'success': True,
+ 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.ajax.testing.FormServer()
+ >>> server.pprint('http://localhost/@@container.html/calc/operations')
+ {u'session_expired': True,
+ u'success': False}
+
+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.ajax.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.ajax.application
+ >>> class Foo:
+ ... def __str__(self):
+ ... return 'a '+self.__class__.__name__
+ ...
+ ... @zc.ajax.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.ajax.application.Application):
+ ... pass
+
+ >>> myapp = MyApp(foo, None)
+ >>> myapp.__parent__ is foo
+ True
+
+ >>> class MySubApp(zc.ajax.application.SubApplication):
+ ... pass
+
+ >>> mysubapp = MySubApp(foo, None)
+ >>> mysubapp.__parent__ is foo
+ True
+
+
+System Errors
+=============
+
+ System errors will be rendered as json.
+
+ >>> server = zc.ajax.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!',
+ u'success': False}
+
+.. [#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.ajax.application
+ >>> import zc.ajax.interfaces
+ >>> import zope.publisher.browser
+ >>> request = zope.publisher.browser.TestRequest()
+ >>> class SubApp(zc.ajax.application.SubApplication):
+ ... @zc.ajax.application.jsonpage
+ ... def foo(self):
+ ... return 'bar'
+ >>> subapp = SubApp(object(), request)
+
+ Now let's try traversing into the subapplication:
+
+ >>> zc.ajax.interfaces.IAjaxRequest.providedBy(request)
+ False
+ >>> subapp.publishTraverse(request, 'foo')()
+ '"bar"'
+ >>> zc.ajax.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.ajax.
+
+ >>> import logging
+ >>> import zope.testing.loggingsupport
+ >>> log_handler = zope.testing.loggingsupport.InstalledHandler('zc.ajax')
+
+ Then we create an error.
+
+ >>> server = zc.ajax.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.ajax.application ERROR
+ SysError created by zc.ajax
Property changes on: zc.ajax/branches/dev/src/zc/ajax/application.txt
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/calculator_example.py
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/calculator_example.py (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/calculator_example.py 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,66 @@
+import zc.ajax.application
+import zope.exceptions
+
+class Calculator(zc.ajax.application.Trusted,
+ zc.ajax.application.Application):
+
+ resource_library_name = None
+
+ @zc.ajax.application.jsonpage
+ def about(self):
+ return 'Calculator 1.0'
+
+ @zc.ajax.application.jsonpage
+ def operations(self):
+ return ['add', "subtract"]
+
+ @zc.ajax.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.ajax.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.ajax.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.ajax.application.jsonpage
+ def noop(self):
+ pass
+
+ @zc.ajax.application.page
+ def none(self):
+ return "null"
+
+ @zc.ajax.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.ajax.application.jsonpage
+ def doh(self):
+ raise TypeError("Doh!")
+
Property changes on: zc.ajax/branches/dev/src/zc/ajax/calculator_example.py
___________________________________________________________________
Added: svn:keywords
+ Id
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/calculator_example.zcml
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/calculator_example.zcml (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/calculator_example.zcml 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,6 @@
+<configure xmlns="http://namespaces.zope.org/zope">
+ <adapter name="calculator.html"
+ factory="zc.ajax.calculator_example.Calculator"
+ permission="zope.View"
+ />
+</configure>
Property changes on: zc.ajax/branches/dev/src/zc/ajax/calculator_example.zcml
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/calculator_subapplication_example.py
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/calculator_subapplication_example.py (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/calculator_subapplication_example.py 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,45 @@
+import zc.ajax.application
+import zope.exceptions
+
+class Container(zc.ajax.application.Application):
+
+ resource_library_name = 'zc.ajax'
+
+ @property
+ def calc(self):
+ return Calculator(self.context, self.request, base_href='calc')
+
+
+class Calculator(zc.ajax.application.Trusted,
+ zc.ajax.application.SubApplication,
+ zc.ajax.application.PublicTraversable,
+ ):
+
+ @zc.ajax.application.jsonpage
+ def operations(self):
+ return [['add', self.base_href+'/add'],
+ ['add', self.base_href+'/subtract'],
+ ]
+
+ @zc.ajax.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.ajax.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.ajax.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)
+
Property changes on: zc.ajax/branches/dev/src/zc/ajax/calculator_subapplication_example.py
___________________________________________________________________
Added: svn:keywords
+ Id
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/calculator_subapplication_example.zcml
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/calculator_subapplication_example.zcml (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/calculator_subapplication_example.zcml 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,7 @@
+<configure xmlns="http://namespaces.zope.org/zope">
+ <include package="zope.app.component" file="meta.zcml" />
+ <adapter name="container.html"
+ factory="zc.ajax.calculator_subapplication_example.Container"
+ permission="zope.View"
+ />
+</configure>
Property changes on: zc.ajax/branches/dev/src/zc/ajax/calculator_subapplication_example.zcml
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/configure.zcml
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/configure.zcml (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/configure.zcml 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,15 @@
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:browser="http://namespaces.zope.org/browser"
+ >
+
+ <include file="widgets.zcml" />
+
+ <adapter factory=".application.UserError" name="index.html" />
+ <adapter factory=".application.ExceptionView" name="index.html" />
+
+ <class class=".form.Action">
+ <allow attributes="__call__ browserDefault" />
+ </class>
+
+</configure>
Property changes on: zc.ajax/branches/dev/src/zc/ajax/configure.zcml
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/form.py
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/form.py (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/form.py 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,167 @@
+##############################################################################
+#
+# Copyright (c) Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+import zc.ajax.application
+import zope.app.form.browser.interfaces
+import zope.app.form.interfaces
+import zope.cachedescriptors.property
+import zope.formlib.form
+import zope.publisher.interfaces.browser
+import zope.security.checker
+
+class FormType(type):
+
+ def __get__(self, inst, class_):
+ if inst is None:
+ return self
+ return self(inst)
+
+_FormBase = FormType('_FormBase', (object, ), {})
+
+class Form(_FormBase):
+
+ zope.interface.implements(
+ zope.publisher.interfaces.browser.IBrowserPublisher)
+
+ __Security_checker__ = zope.security.checker.NamesChecker((
+ '__call__', 'browserDefault', 'publishTraverse'))
+
+ def __init__(self, app, request=None):
+ self.app = app
+ if request is None:
+ request = app.request
+ self.request = request
+ self.context = app.context
+
+ @zope.cachedescriptors.property.Lazy
+ def prefix(self):
+ return self.base_href.replace('/', '.')
+
+ @zope.cachedescriptors.property.Lazy
+ def base_href(self):
+ base_href = getattr(self.app, 'base_href', None)
+ if base_href is not None:
+ base_href += '/'
+ else:
+ base_href = ''
+ return base_href+self.__class__.__name__
+
+ def get_definition(self):
+ widgets = zope.formlib.form.setUpWidgets(
+ self.form_fields, self.prefix, self.context, self.request,
+ ignore_request=True)
+
+ for widget in widgets:
+ # Make sure that we have the right type of widget.
+ assert hasattr(widget, 'js_config'), (
+ 'Could not find an Ext widget for %r' % widget.name)
+
+ return dict(
+ widgets=[widget.js_config() for widget in widgets],
+ widget_names = dict((widget.name, i)
+ for (i, widget) in enumerate(widgets)),
+ actions=[dict(label=action.label,
+ url="%s/%s" % (self.base_href,
+ action.__name__.split('.')[-1]),
+ name=action.__name__)
+ for action in self.actions]
+ )
+
+ def __call__(self):
+ """Return rendered js widget configs
+ """
+ return zc.ajax.application.result(
+ dict(definition=self.get_definition()))
+
+ def publishTraverse(self, request, name):
+ result = getattr(self, name, None)
+ if isinstance(result, zope.formlib.form.Action):
+ return Action(self, result)
+
+ raise zope.publisher.interfaces.NotFound(self, name, request)
+
+ def browserDefault(self, request):
+ return self, ()
+
+ def getObjectData(self, ob, extra=()):
+ widgets = zope.formlib.form.setUpWidgets(
+ self.form_fields, self.prefix, self.context, self.request,
+ ignore_request=True)
+
+ result = {}
+ for widget in widgets:
+ if widget.name in extra:
+ result[widget.name] = extra[widget.name]
+ else:
+ v = widget.formValue(widget.context.get(ob))
+ if v is not None:
+ result[widget.name] = v
+
+ return result
+
+
+class Action(object):
+
+ zope.interface.implementsOnly(
+ zope.publisher.interfaces.browser.IBrowserPublisher)
+
+ def __init__(self, form, action):
+ self.form = form
+ self.action = action
+
+ def __call__(self):
+ widgets = zope.formlib.form.setUpWidgets(
+ self.form.form_fields,
+ self.form.prefix,
+ self.form.context,
+ self.form.request,
+ ignore_request=True)
+ data = {}
+ field_errors = {}
+
+ for input, widget in widgets.__iter_input_and_widget__():
+ if (input and
+ zope.app.form.interfaces.IInputWidget.providedBy(widget)
+ ):
+ if (not widget.hasInput()) and not widget.required:
+ continue
+
+ name = widget.name
+ if name.startswith(self.form.prefix+'.'):
+ name = name[len(self.form.prefix)+1:]
+
+ try:
+ data[name] = widget.getInputValue()
+ except zope.app.form.interfaces.InputErrors, error:
+
+ if not isinstance(error, basestring):
+ view = zope.component.getMultiAdapter(
+ (error, self.form.request),
+ zope.app.form.browser.interfaces.
+ IWidgetInputErrorView,
+ )
+ error = view.snippet()
+
+ field_errors[widget.name] = error
+
+ if field_errors:
+ return zc.ajax.application.result(dict(errors=field_errors))
+
+ # XXX invariants and action conditions
+ # XXX action validator and failure handlers
+
+ return zc.ajax.application.result(self.action.success(data))
+
+ def browserDefault(self, request):
+ return self, ()
Property changes on: zc.ajax/branches/dev/src/zc/ajax/form.py
___________________________________________________________________
Added: svn:keywords
+ Id
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/form.txt
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/form.txt (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/form.txt 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,334 @@
+Form Processing
+===============
+
+zc.ext.form provides support for server-generated EXT 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 Ext 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.ajax.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.ajax.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:
+
+.. include:: form_example.py
+ :literal:
+
+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:
+
+ >>> import zope.testbrowser.testing
+ >>> from zc.ajax.testing import print_form
+ >>> browser = zope.testbrowser.testing.Browser()
+ >>> browser.handleErrors = False
+ >>> browser.open('http://localhost/form.html?login')
+ >>> print_form(browser, 'http://localhost/form.html/ExampleForm')
+ ... # doctest: +NORMALIZE_WHITESPACE
+ {u'definition': {u'actions': [{u'label': u'Register',
+ u'name': u'ExampleForm.actions.register',
+ u'url': u'ExampleForm/register'}],
+ u'widget_names': {u'ExampleForm.age': 3,
+ u'ExampleForm.favorite_color': 2,
+ u'ExampleForm.first_name': 0,
+ u'ExampleForm.last_name': 1},
+ u'widgets': [{u'fieldHint': u'Given name.',
+ u'fieldLabel': u'First name',
+ u'id': u'ExampleForm.first_name',
+ u'itemCls': u'zc-required-field',
+ u'minLength': 0,
+ u'name': u'ExampleForm.first_name',
+ u'xtype': u'textfield'},
+ {u'fieldHint': u'Family name.',
+ u'fieldLabel': u'Last name',
+ u'id': u'ExampleForm.last_name',
+ u'itemCls': u'zc-required-field',
+ u'minLength': 0,
+ u'name': u'ExampleForm.last_name',
+ u'xtype': u'textfield'},
+ {u'fieldHint': u'',
+ u'fieldLabel': u'Favorite color',
+ u'id': u'ExampleForm.favorite_color',
+ u'minLength': 0,
+ u'name': u'ExampleForm.favorite_color',
+ u'xtype': u'textfield'},
+ {u'allowBlank': False,
+ u'fieldHint': u'Age in years',
+ u'fieldLabel': u'Age',
+ u'field_min': 0,
+ u'id': u'ExampleForm.age',
+ u'itemCls': u'zc-required-field',
+ u'name': u'ExampleForm.age',
+ u'widget_constructor':
+ u'zc.ajax.widgets.InputInt'}]},
+ u'success': True}
+
+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 ext field definitions
+or name a widget_constructor, which is a Javascript helper provided by
+the zc.ajax 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]_.
+
+The widget_names property helps the Javascript code to access the
+widgets by name, which is useful when doing custon form layouts.
+
+Let's post a result back:
+
+ >>> browser.handleErrors = False
+ >>> print_form(browser, 'http://localhost/form.html/ExampleForm/register',
+ ... {'ExampleForm.first_name': 'Bob',
+ ... 'ExampleForm.last_name': '',
+ ... 'ExampleForm.favorite_color': '',
+ ... 'ExampleForm.age': '-1',
+ ... })
+ ... # doctest: +NORMALIZE_WHITESPACE
+ {u'errors': {u'ExampleForm.age':
+ u'<span class="error">Value is too small</span>',
+ u'ExampleForm.last_name': u'<span class="error"></span>'},
+ u'success': False}
+
+The result had 2 problems:
+
+- We didn't provide a last name, which was required, and
+
+- We specified an invalid age.
+
+(Note that both of these errors would have been caught on the client,
+but we also validate on the server.)
+
+Let's pass valid data:
+
+ >>> print_form(browser, 'http://localhost/form.html/ExampleForm/register',
+ ... {'ExampleForm.first_name': 'Bob',
+ ... 'ExampleForm.last_name': 'Zope',
+ ... 'ExampleForm.favorite_color': '',
+ ... 'ExampleForm.age': '11',
+ ... })
+ {u'data': {u'age': 11,
+ u'favorite_color': u'',
+ u'first_name': u'Bob',
+ u'last_name': u'Zope'},
+ u'self_app_class_name': u'FormExample',
+ u'self_class_name': u'ExampleForm',
+ u'self_context_class_name': u'Folder',
+ u'success': True}
+
+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.
+
+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.
+
+
+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.ajax.form_example
+ >>> import zope.publisher.browser
+ >>> request = zope.publisher.browser.TestRequest()
+ >>> import zc.ajax.interfaces
+ >>> import zope.interface
+ >>> zope.interface.alsoProvides(request,
+ ... zc.ajax.interfaces.IAjaxRequest)
+ >>> ex = zc.ajax.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'}],
+ 'widget_names': {'ExampleForm.age': 3,
+ 'ExampleForm.favorite_color': 2,
+ 'ExampleForm.first_name': 0,
+ 'ExampleForm.last_name': 1},
+ 'widgets': [{'fieldHint': u'Given name.',
+ 'fieldLabel': u'First name',
+ 'id': 'ExampleForm.first_name',
+ 'itemCls': 'zc-required-field',
+ 'minLength': 0,
+ 'name': 'ExampleForm.first_name',
+ 'xtype': 'textfield'},
+ {'fieldHint': u'Family name.',
+ 'fieldLabel': u'Last name',
+ 'id': 'ExampleForm.last_name',
+ 'itemCls': 'zc-required-field',
+ 'minLength': 0,
+ 'name': 'ExampleForm.last_name',
+ 'xtype': 'textfield'},
+ {'fieldHint': u'',
+ 'fieldLabel': u'Favorite color',
+ 'id': 'ExampleForm.favorite_color',
+ 'minLength': 0,
+ 'name': 'ExampleForm.favorite_color',
+ 'xtype': 'textfield'},
+ {'allowBlank': False,
+ 'fieldHint': u'Age in years',
+ 'fieldLabel': u'Age',
+ 'field_min': 0,
+ 'id': 'ExampleForm.age',
+ 'itemCls': 'zc-required-field',
+ 'name': 'ExampleForm.age',
+ 'widget_constructor': 'zc.ajax.widgets.InputInt'}]}
+
+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.ajax.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'}],
+ 'widget_names': {'sample.ExampleForm.age': 3,
+ 'sample.ExampleForm.favorite_color': 2,
+ 'sample.ExampleForm.first_name': 0,
+ 'sample.ExampleForm.last_name': 1},
+ 'widgets': [{'fieldHint': u'Given name.',
+ 'fieldLabel': u'First name',
+ 'id': 'sample.ExampleForm.first_name',
+ 'itemCls': 'zc-required-field',
+ 'minLength': 0,
+ 'name': 'sample.ExampleForm.first_name',
+ 'xtype': 'textfield'},
+ {'fieldHint': u'Family name.',
+ 'fieldLabel': u'Last name',
+ 'id': 'sample.ExampleForm.last_name',
+ 'itemCls': 'zc-required-field',
+ 'minLength': 0,
+ 'name': 'sample.ExampleForm.last_name',
+ 'xtype': 'textfield'},
+ {'fieldHint': u'',
+ 'fieldLabel': u'Favorite color',
+ 'id': 'sample.ExampleForm.favorite_color',
+ 'minLength': 0,
+ 'name': 'sample.ExampleForm.favorite_color',
+ 'xtype': 'textfield'},
+ {'allowBlank': False,
+ 'fieldHint': u'Age in years',
+ 'fieldLabel': u'Age',
+ 'field_min': 0,
+ 'id': 'sample.ExampleForm.age',
+ 'itemCls': 'zc-required-field',
+ 'name': 'sample.ExampleForm.age',
+ 'widget_constructor': 'zc.ajax.widgets.InputInt'}]}
+
+Note that the action URL now includes "sample/" as a prefix. Also
+note that the widget and action names have "sample." 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.ajax.form_example.Person('bob', 'smith', None, 11)
+ >>> pprint(ex.ExampleForm.getObjectData(bob), width=1)
+ {'sample.ExampleForm.age': u'11',
+ 'sample.ExampleForm.first_name': u'bob',
+ 'sample.ExampleForm.last_name': u'smith'}
+
+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, {'sample.ExampleForm.age': u'1'}),
+ ... width=1)
+ {'sample.ExampleForm.age': u'1',
+ 'sample.ExampleForm.first_name': u'bob',
+ 'sample.ExampleForm.last_name': u'smith'}
+
+
+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.
Property changes on: zc.ajax/branches/dev/src/zc/ajax/form.txt
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/form_example.py
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/form_example.py (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/form_example.py 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,55 @@
+import zc.ajax.application
+import zc.ajax.form
+import zope.formlib
+import zope.interface
+import zope.schema
+
+class IPerson(zope.interface.Interface):
+
+ first_name = zope.schema.TextLine(
+ title = u"First name",
+ description = u"Given name.",
+ )
+
+ last_name = zope.schema.TextLine(
+ title = u"Last name",
+ description = u"Family name.",
+ )
+
+ favorite_color = zope.schema.TextLine(
+ title = u"Favorite color",
+ required = False,
+ )
+
+ age = zope.schema.Int(
+ title = u"Age",
+ description = u"Age in years",
+ min = 0,
+ )
+
+class Person:
+
+ zope.interface.implements(IPerson)
+
+ def __init__(self, first_name, last_name, favorite_color, age):
+ self.first_name = first_name
+ self.last_name = last_name
+ self.favorite_color = favorite_color
+ self.age = age
+
+class FormExample(zc.ajax.application.Application):
+
+ resource_library_name = None
+
+ class ExampleForm(zc.ajax.form.Form):
+
+ form_fields = zope.formlib.form.Fields(IPerson)
+
+ @zope.formlib.form.action("Register")
+ def register(self, action, 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__
+ )
Property changes on: zc.ajax/branches/dev/src/zc/ajax/form_example.py
___________________________________________________________________
Added: svn:keywords
+ Id
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/form_example.zcml
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/form_example.zcml (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/form_example.zcml 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,7 @@
+<configure xmlns="http://namespaces.zope.org/zope">
+ <include package="zope.app.component" file="meta.zcml" />
+ <adapter name="form.html"
+ factory="zc.ajax.form_example.FormExample"
+ permission="zope.View"
+ />
+</configure>
Property changes on: zc.ajax/branches/dev/src/zc/ajax/form_example.zcml
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/interfaces.py
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/interfaces.py (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/interfaces.py 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,56 @@
+##############################################################################
+#
+# Copyright (c) Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+import zope.app.form.interfaces
+import zope.interface
+import zope.publisher.interfaces.browser
+
+class IAjaxRequest(zope.publisher.interfaces.browser.IBrowserRequest):
+ """Ajax requests
+ """
+
+class IInputWidget(zope.app.form.interfaces.IInputWidget):
+ """Ajax widgets
+
+ Ajax widgets work much like browser widgets except that rather
+ than rendering HTML, they render ExtJS widget configurations.
+ """
+
+ def js_config(self):
+ """Return an ExtJS widget configuration
+
+ The return value is a dictionary containing data needed to
+ create an ExtJS field.
+
+ The resule may contain a widget_constructor property
+ containing the name of a Javascript to be used to build the
+ widget, in which case the data is passed to the Javascript function.
+
+ If rendered data have been set, the output should contain a
+ value property.
+
+ The output must contain name and id properties.
+ """
+
+ def formValue(v):
+ """Return a value suitable to passing to a Ext field setValue method
+
+ This will typically be a string. None may be returned if the
+ value passed in is a missing value.
+ """
+
+ def value(raw):
+ """Convert a raw value, from a form, to an application value
+ """
+
Property changes on: zc.ajax/branches/dev/src/zc/ajax/interfaces.py
___________________________________________________________________
Added: svn:keywords
+ Id
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/session.py
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/session.py (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/session.py 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,28 @@
+##############################################################################
+#
+# Copyright (c) Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL). A copy of the ZVSL should accompany this
+# distribution.
+#
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+
+import simplejson
+import zope.app.exception.browser.unauthorized
+
+class Unauthorized(zope.app.exception.browser.unauthorized.Unauthorized):
+
+ def __call__(self):
+ if (self.request.getHeader('X-Requested-With', '').lower()
+ == 'xmlhttprequest'):
+ return simplejson.dumps(
+ dict(success=False, session_expired = True)
+ )
+ return zope.app.exception.browser.unauthorized.Unauthorized.__call__(
+ self)
Property changes on: zc.ajax/branches/dev/src/zc/ajax/session.py
___________________________________________________________________
Added: svn:keywords
+ Id
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/session.txt
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/session.txt (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/session.txt 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,44 @@
+Supporting session authentication
+=================================
+
+Session authentication provides a challenge for AJAX applications.
+For normal page loads, the user is redirected to a login page. This
+isn't useful for AJAX requests. The zc.ajax package provides an
+Unauthorized error view that return a JSON page with a session_expired
+flag set if the request has an X-Requested-With header set to
+xmlhttprequest.
+
+To get this view, you need to include session.zcml::
+
+ <include package="zc.ajax" file="session.zcml" />
+
+Tou need to do this before zope.app.exception.browser is included::
+
+ <include package="zope.app.zcmlfiles" file="meta.zcml" />
+ <include package="zc.ajax" file="session.zcml" />
+
+
+We have a calculator application set up [#application]_. If we access
+it using an ordinary unauthenticated browser, we get an unauthorized error:
+
+ >>> import zope.testbrowser.testing
+ >>> browser = zope.testbrowser.testing.Browser()
+ >>> browser.open('http://localhost/@@index.html/about')
+ Traceback (most recent call last):
+ ...
+ HTTPError: HTTP Error 401: Unauthorized
+
+We get an unauthorized error, rather than getting redirected because
+we're using basic authentication in the test setup. Had we been using
+session authentication, we'd have been redirected.
+
+If we set the X-Requested-With header:
+
+ >>> browser.addHeader('X-Requested-With', 'XMLHTTPRequest')
+ >>> browser.open('http://localhost/@@index.html/about')
+ >>> import simplejson
+ >>> simplejson.loads(browser.contents)
+ {u'session_expired': True, u'success': False}
+
+
+.. [#application] See application.txt
Property changes on: zc.ajax/branches/dev/src/zc/ajax/session.txt
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/session.zcml
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/session.zcml (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/session.zcml 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,20 @@
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:browser="http://namespaces.zope.org/browser"
+ >
+
+ <include package="zope.app.exception.browser" />
+
+ <browser:page
+ for="zope.security.interfaces.IUnauthorized"
+ name="index.html"
+ permission="zope.Public"
+ class=".session.Unauthorized"
+ />
+ <adapter
+ for=".session.Unauthorized"
+ factory="zope.app.exception.browser.unauthorized.default_template"
+ name="default"
+ />
+
+</configure>
Property changes on: zc.ajax/branches/dev/src/zc/ajax/session.zcml
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/testing.py
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/testing.py (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/testing.py 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,221 @@
+##############################################################################
+#
+# Copyright (c) Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+import base64
+import ClientForm
+import pprint
+import simplejson
+import StringIO
+import urllib
+import zc.ajax.session
+import zope.app.exception.browser.unauthorized
+import zope.app.security.interfaces
+import zope.formlib.namedtemplate
+import zope.interface
+import zope.security.checker
+import zope.security.interfaces
+import zope.security.simplepolicies
+import zope.testbrowser.testing
+
+def _result(browser):
+ # XXX TestBrowser needs a removeHeader
+ browser.mech_browser.addheaders[:] = [
+ header for header in browser.mech_browser.addheaders
+ if header[0] != 'X-Requested-With']
+
+ return simplejson.loads(browser.contents)
+
+def call_form(browser, url, __params=(), __use_zope_type_decorators=True,
+ **params):
+
+ browser.addHeader('X-Requested-With', 'XMLHTTPRequest')
+ if not (params or __params):
+ browser.open(url)
+ return _result(browser)
+
+
+ params = params.copy()
+ params.update(__params)
+
+ pairs = []
+ for n, v in params.items():
+ if isinstance(v, list):
+ if __use_zope_type_decorators:
+ n += ':list'
+ for vv in v:
+ pairs.append((n, vv))
+ else:
+ pairs.append((n, v))
+
+ multipart = False
+ marshalled_pairs = []
+ for n, v in pairs:
+ if isinstance(v, str):
+ v = v.encode('ascii')
+ elif isinstance(v, bool):
+ if __use_zope_type_decorators:
+ n += ':boolean'
+ v = v and '1' or ''
+ elif isinstance(v, int):
+ if __use_zope_type_decorators:
+ n += ':int'
+ v = str(v)
+ elif isinstance(v, float):
+ if __use_zope_type_decorators:
+ n += ':float'
+ v = str(v)
+ elif isinstance(v, unicode):
+ v = v.encode('utf-8')
+ elif isinstance(v, tuple):
+ multipart = True
+ else:
+ raise ValueError("can't marshal %r" % v)
+
+ marshalled_pairs.append((n, v))
+
+ if not multipart:
+ browser.post(url,
+ '&'.join((n+'='+urllib.quote(v))
+ for (n, v)
+ in marshalled_pairs
+ )
+ )
+ return _result(browser)
+
+ body = StringIO.StringIO()
+ headers = []
+ mw = ClientForm.MimeWriter(body, headers)
+ mw.startmultipartbody("form-data", add_to_http_hdrs=True, prefix=0)
+ for n, v in marshalled_pairs:
+ mw2 = mw.nextpart()
+ if isinstance(v, tuple):
+ filename, contenttype, data = v
+ mw2.addheader("Content-disposition",
+ 'form-data; name="%s"; filename="%s"'
+ % (n, filename))
+ f = mw2.startbody(contenttype, prefix=0)
+ f.write(data)
+ else:
+ mw2.addheader("Content-disposition", 'form-data; name="%s"' % n)
+ f = mw2.startbody(prefix=0)
+ f.write(v)
+
+ mw.lastpart()
+ body = body.getvalue()
+ [[n, content_type]] = headers
+ assert n.lower() == 'content-type'
+ browser.post(url, body, content_type)
+ return _result(browser)
+
+
+def print_form(*a, **kw):
+ pprint.pprint(call_form(*a, **kw), width=1)
+
+class FormServer:
+
+ def __init__(self, browser=None, zope_form_marshalling=False):
+ if browser is None:
+ browser = zope.testbrowser.testing.Browser()
+ elif isinstance(browser, str):
+ url = browser
+ browser = zope.testbrowser.testing.Browser()
+ browser.open(url)
+
+ self.browser = browser
+ self._zope_marshalling = zope_form_marshalling
+
+ def __call__(self, method, __params=(), **params):
+ return call_form(self.browser, method, __params,
+ __use_zope_type_decorators = self._zope_marshalling,
+ **params)
+
+ def pprint(self, *a, **k):
+ return pprint.pprint(self(*a, **k), width=1)
+
+class Principal:
+
+ description = ''
+
+ def __init__(self, id):
+ self.id = id
+ self.title = 'principal: '+id
+
+class UnauthenticatedPrincipal:
+
+ id = 'unauthenticatedprincipal'
+ title = u'UnauthenticatedPrincipal'
+ description = u''
+
+unauthenticatedPrincipal = UnauthenticatedPrincipal()
+
+class Login:
+
+ def __init__(self, context, request):
+ pass
+
+ def logout(self):
+ """<html><body>
+ Unauthorized! Add ?logged-in=name to log in. :)
+ </body></html>
+ """
+
+class DumbAuth:
+
+ zope.interface.implements(zope.app.security.interfaces.IAuthentication)
+
+ def authenticate(self, request):
+ if request.get('logout') is not None:
+ return None
+
+ if not 'login' in request:
+ return None
+
+ if 'login' in request.form:
+ request.response.setCookie('login', '')
+
+ return Principal('user')
+
+ def unauthenticatedPrincipal(self):
+ return unauthenticatedPrincipal
+
+ def unauthorized(self, id, request):
+ request.response.expireCookie('login')
+ request.response.setStatus(401)
+ return
+
+ def getPrincipal(self, id):
+ return Principal(id)
+
+unauth_template = zope.formlib.namedtemplate.NamedTemplateImplementation(
+ lambda inst: """<html><body>
+ Unauthorized! Add ?login to log in. :)
+ </body></html>
+ """,
+ zc.ajax.session.Unauthorized)
+
+
+class AuthenticatedAllowed(zope.security.simplepolicies.ParanoidSecurityPolicy):
+
+ def checkPermission(self, permission, object):
+ if permission is zope.security.checker.CheckerPublic:
+ return True
+
+ users = [p.principal
+ for p in self.participations
+ if p.principal is unauthenticatedPrincipal]
+
+ return not users
+
+
+
Property changes on: zc.ajax/branches/dev/src/zc/ajax/testing.py
___________________________________________________________________
Added: svn:keywords
+ Id
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/tests.py
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/tests.py (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/tests.py 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,47 @@
+##############################################################################
+#
+# Copyright (c) Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL). A copy of the ZVSL should accompany this
+# distribution.
+#
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+
+from zope.testing import doctest
+import os
+import unittest
+import zope.app.testing.functional
+
+class Test(zope.app.testing.functional.ZCMLLayer):
+
+ def __init__(self):
+ pass # Circumvent inherited constructior :)
+
+ allow_teardown = False
+ config_file = os.path.join(os.path.dirname(__file__), 'tests.zcml')
+ __name__ = config_file
+ product_config = None
+
+ def __call__(self, *args, **kw):
+ test = doctest.DocFileSuite(*args, **kw)
+ test.layer = self
+ return test
+
+def test_suite():
+ return unittest.TestSuite((
+ doctest.DocFileSuite('widgets.txt',
+ setUp=zope.app.testing.placelesssetup.setUp,
+ tearDown=zope.app.testing.placelesssetup.tearDown,
+ ),
+ Test()('application.txt', 'session.txt', 'form.txt'),
+ ))
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
+
Property changes on: zc.ajax/branches/dev/src/zc/ajax/tests.py
___________________________________________________________________
Added: svn:keywords
+ Id
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/tests.zcml
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/tests.zcml (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/tests.zcml 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,17 @@
+<configure xmlns="http://namespaces.zope.org/zope" package="zc.ajax">
+
+ <include package="zope.app.zcmlfiles" file="meta.zcml" /> <!-- Gaaaah -->
+ <include file="session.zcml" />
+ <include package="zc.ajax" />
+ <include package="zope.app.zcmlfiles" /> <!-- Gaaaah -->
+ <include package="zope.formlib" />
+
+ <utility factory=".testing.DumbAuth" />
+ <securityPolicy component=".testing.AuthenticatedAllowed" />
+ <adapter factory=".testing.unauth_template" name="default" />
+
+ <include file="calculator_example.zcml" />
+ <include file="form_example.zcml" />
+ <include file="calculator_subapplication_example.zcml" />
+
+</configure>
Property changes on: zc.ajax/branches/dev/src/zc/ajax/tests.zcml
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/widgets.py
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/widgets.py (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/widgets.py 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,334 @@
+##############################################################################
+#
+# Copyright (c) Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+import decimal
+import pytz
+import zc.ajax.interfaces
+import zc.form.interfaces
+import zc.sourcefactory.basic
+import zope.app.form
+import zope.app.form.browser.interfaces
+import zope.app.form.interfaces
+import zope.cachedescriptors.property
+import zope.component
+import zope.interface
+import zope.schema.interfaces
+
+
+class Base(zope.app.form.InputWidget):
+
+ zope.interface.implements(zc.ajax.interfaces.IInputWidget)
+
+ xtype = None
+ widget_constructor = None
+
+ def js_config(self, **kw):
+ config = dict(
+ fieldLabel = self.label,
+ fieldHint = self.hint,
+ name = self.name,
+ id = self.name,
+ **kw)
+
+ if self.xtype:
+ config['xtype'] = self.xtype
+ elif not self.widget_constructor:
+ raise ValueError(
+ 'Neither xtype nor widget_constructor are defined.')
+
+ if self.widget_constructor:
+ config['widget_constructor'] = self.widget_constructor
+
+ if self.required:
+ config['itemCls'] = 'zc-required-field'
+
+ if self._renderedValueSet():
+ value = self.formValue(self._data)
+ if value is not None:
+ config['value'] = value
+
+ return config
+
+ def formValue(self, v):
+ if v == self.context.missing_value:
+ return None
+ return unicode(v)
+
+ def value(self, raw):
+ return self._toValue(raw)
+
+ def _toValue(self, v): # for backward compat for a while
+ return v
+
+ def hasInput(self):
+ return self.name in self.request.form
+
+ def _is_missing(self, raw):
+ return False
+
+ def _get_raw(self):
+ return self.request.form[self.name]
+
+ def getInputValue(self):
+ if not self.hasInput():
+ raise zope.app.form.interfaces.MissingInputError(
+ self.name, self.label, None)
+
+ raw = self._get_raw()
+ if self._is_missing(raw):
+ if self.required:
+ raise zope.app.form.interfaces.MissingInputError(
+ self.name, self.label, None)
+ else:
+ return self.context.missing_value
+
+ value = self.value(raw)
+
+ # value must be valid per the field constraints
+ try:
+ self.context.validate(value)
+ except zope.schema.interfaces.ValidationError, v:
+ raise zope.app.form.interfaces.WidgetInputError(
+ self.context.__name__, self.label, v)
+
+ return value
+
+ @zope.cachedescriptors.property.Lazy
+ def required(self):
+ return self.context.required
+
+class InputBool(Base):
+
+ zope.component.adapts(
+ zope.schema.interfaces.IBool,
+ zc.ajax.interfaces.IAjaxRequest,
+ )
+
+ xtype = 'checkbox'
+
+ def hasInput(self):
+ return True
+
+ def getInputValue(self):
+ return self.request.form.get(self.name, '') == 'on'
+
+ def formValue(self, v):
+ if v == self.context.missing_value:
+ return None
+ return bool(v)
+
+
+class InputChoiceIterable(Base):
+
+ zope.component.adapts(
+ zope.schema.interfaces.IChoice,
+ zope.schema.interfaces.IIterableSource,
+ zc.ajax.interfaces.IAjaxRequest,
+ )
+
+ widget_constructor = 'zc.ajax.widgets.InputChoice'
+
+ def __init__(self, context, source, request):
+ Base.__init__(self, context, request)
+ self.source = source
+
+ def _is_missing(self, raw):
+ return not raw
+
+ def _get_raw(self):
+ return self.request.form[self.name+'.value']
+
+ def hasInput(self):
+ return self.name+'.value' in self.request.form
+
+ def js_config(self):
+ result = Base.js_config(self)
+ result['hiddenName'] = result['name']+'.value'
+
+ terms = zope.component.getMultiAdapter(
+ (self.source, self.request),
+ zope.app.form.browser.interfaces.ITerms,
+ )
+
+ result['values'] = [
+ [term.token, term.title]
+ for term in (terms.getTerm(v) for v in self.source)
+ ]
+
+ if self.required:
+ result['allowBlank'] = False
+
+ return result
+
+ def formValue(self, v):
+ if v == self.context.missing_value:
+ return None
+ terms = zope.component.getMultiAdapter(
+ (self.source, self.request),
+ zope.app.form.browser.interfaces.ITerms,
+ )
+ return terms.getTerm(v).token
+
+ def value(self, v):
+ terms = zope.component.getMultiAdapter(
+ (self.source, self.request),
+ zope.app.form.browser.interfaces.ITerms,
+ )
+ return terms.getValue(v)
+
+class InputChoiceTokenized(InputChoiceIterable):
+
+ zope.component.adapts(
+ zope.schema.interfaces.IChoice,
+ zope.schema.interfaces.IVocabularyTokenized,
+ zc.ajax.interfaces.IAjaxRequest,
+ )
+
+ def js_config(self):
+ result = Base.js_config(self)
+ result['hiddenName'] = result['name']+'.value'
+
+ result['values'] = [
+ [term.token, term.title or unicode(term.value)]
+ for term in self.source
+ ]
+
+ if self.required:
+ result['allowBlank'] = False
+
+ return result
+
+ def formValue(self, v):
+ if v == self.context.missing_value:
+ return None
+ return self.source.getTerm(v).token
+
+ def value(self, v):
+ return self.source.getTermByToken(v).value
+
+
+class InputTimeZone(InputChoiceTokenized):
+
+ zope.component.adapts(
+ zope.schema.interfaces.IChoice,
+ zc.form.interfaces.AvailableTimeZones,
+ zc.ajax.interfaces.IAjaxRequest
+ )
+
+ _timezones = sorted([(tzname, pytz.timezone(tzname))
+ for tzname in pytz.all_timezones])
+
+ def __init__(self, context, source, request):
+ source = zope.schema.vocabulary.SimpleVocabulary.fromItems(
+ self._timezones)
+ InputChoiceIterable.__init__(self, context, source, request)
+
+
+class InputInt(Base):
+
+ zope.component.adapts(
+ zope.schema.interfaces.IInt,
+ zc.ajax.interfaces.IAjaxRequest,
+ )
+
+ widget_constructor = 'zc.ajax.widgets.InputInt'
+
+ def js_config(self):
+ config = Base.js_config(self)
+
+ if self.required:
+ config['allowBlank'] = False
+
+ if self.context.min is not None:
+ config['field_min'] = self.context.min
+ if self.context.max is not None:
+ config['field_max'] = self.context.max
+ return config
+
+ def _is_missing(self, raw):
+ return not raw
+
+ def value(self, v):
+ try:
+ return int(v)
+ except:
+ raise zope.app.form.interfaces.ConversionError(
+ u"Invalid integer: %r" % v
+ )
+
+
+class InputDecimal(Base):
+
+ zope.component.adapts(
+ zope.schema.interfaces.IDecimal,
+ zc.ajax.interfaces.IAjaxRequest)
+
+ widget_constructor = 'zc.ajax.widgets.InputDecimal'
+
+ def js_config(self):
+ result = Base.js_config(self)
+ if self.required:
+ result['allowBlank'] = False
+ return result
+
+ def _is_missing(self, raw):
+ return not raw
+
+ def _toForm(self, v):
+ return str(v)
+
+ def value(self, v):
+ try:
+ return decimal.Decimal(v)
+ except decimal.InvalidOperation:
+ raise zope.app.form.interfaces.ConversionError(
+ u"Invalid decimal: %r" % v)
+
+
+class InputTextLine(Base):
+
+ zope.component.adapts(
+ zope.schema.interfaces.ITextLine,
+ zc.ajax.interfaces.IAjaxRequest,
+ )
+
+ xtype = 'textfield'
+
+ def _is_missing(self, raw):
+ return (not raw) and self.required
+
+ def js_config(self):
+ config = Base.js_config(self)
+ if self.context.min_length is not None:
+ config['minLength'] = self.context.min_length
+ if self.context.min_length > 0 and self.required:
+ config['allowBlank'] = False
+
+ if self.context.max_length is not None:
+ config['maxLength'] = self.context.max_length
+
+ return config
+
+class InputText(InputTextLine):
+
+ zope.component.adapts(
+ zope.schema.interfaces.IText,
+ zc.ajax.interfaces.IAjaxRequest,
+ )
+
+ xtype = 'textarea'
+
+class Hidden(Base):
+
+ xtype = 'hidden'
Property changes on: zc.ajax/branches/dev/src/zc/ajax/widgets.py
___________________________________________________________________
Added: svn:keywords
+ Id
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/widgets.txt
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/widgets.txt (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/widgets.txt 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,711 @@
+Ajax widgets
+============
+
+The zc.ajax.form module provides ajax form support.
+
+Widgets are looked up for fields and requests providing
+zc.ajax.interfaces.IAjaxRequest.
+
+Ajax widgets must implement zc.ajax.interfaces.IInputWidget. In
+particular, they must provide a js_config method in addition to the
+usual input-widget behavior defined by
+zope.app.form.interfaces.IInputWidget. A number of widgets are defined
+by the zc.ajax.widgets module. We'll test/demonstrate those later.
+There is also a base class that provides some help with implementing
+IInputWidget. It subclasses zope.app.form.InputWidget, which also
+automates some of the more mundate aspects of implementing input
+widgets.
+
+zc.ajax.widgets.Base
+---------------------
+
+The zc.ajax.widgets.Base provides a basic widget implementation and
+hooks that can be overridden for specific widget types. These hooks
+are:
+
+ widget_type
+ This is an string attribute that contains the
+ name of a Javascript widget factory.
+
+ widget_constructor
+ This is a string attribute giving the name of a Javascript
+ constructor to call.
+
+ formValue(v)
+ Convert an application value to a value that can be marshaled to
+ JSON and used to initialize the Ext widget.
+
+ value(v)
+ Convert a raw value sent from the client to an application value.
+
+ _is_missing(self, v)
+ Return a boolean value indicating whether the given raw value is
+ equivalent to the user not providing a value.
+
+Other methods from zc.ajax.interfaces.IInputWidget may also be
+overridden as needed. For example, zc.ajax.widgets.InputBool
+overrides hasInput, and getInputValue.
+
+To see how this works, we'll try some examples with the base widget:
+
+ >>> import zope.publisher.browser
+ >>> request = zope.publisher.browser.TestRequest()
+ >>> import zc.ajax.widgets
+ >>> import zope.schema
+ >>> f = zope.schema.TextLine(
+ ... __name__='f', title=u'label', description=u'hint')
+ >>> w = zc.ajax.widgets.Base(f, request)
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, False, False)
+
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ MissingInputError: ('field.f', u'label', None)
+
+ >>> from pprint import pprint
+ >>> pprint(w.js_config(), width=1)
+ Traceback (most recent call last):
+ ...
+ ValueError: Neither xtype nor widget_constructor are defined.
+
+Oops, let's make this a text widget. Normally we'd do this in a widget
+class. We'll just hack the widget instance. :)
+
+ >>> w.xtype = 'textfield'
+ >>> pprint(w.js_config(), width=1)
+ {'fieldHint': u'hint',
+ 'fieldLabel': u'label',
+ 'id': 'field.f',
+ 'itemCls': 'zc-required-field',
+ 'name': 'field.f',
+ 'xtype': 'textfield'}
+
+We can also (or instead) provide a widget constructor:
+
+ >>> w.widget_constructor = 'my.widget.constructor'
+ >>> pprint(w.js_config(), width=1)
+ {'fieldHint': u'hint',
+ 'fieldLabel': u'label',
+ 'id': 'field.f',
+ 'itemCls': 'zc-required-field',
+ 'name': 'field.f',
+ 'widget_constructor': 'my.widget.constructor',
+ 'xtype': 'textfield'}
+
+Let's add some data to the request:
+
+ >>> request.form['field.f'] = 'xxx'
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, False)
+
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ WidgetInputError: ('f', u'label', xxx <type 'unicode'>)
+
+ >>> request.form['field.f'] = u'xxx'
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, True)
+
+ >>> w.getInputValue()
+ u'xxx'
+
+Let's try overriding some methods:
+
+ >>> w.value = lambda raw: raw+' cooked'
+ >>> w.formValue = lambda v: v[:-7]
+ >>> w._is_missing = lambda raw: raw == u'xxx'
+
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, False)
+
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ MissingInputError: ('field.f', u'label', None)
+
+ >>> request.form['field.f'] = u'foo'
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, True)
+
+ >>> w.getInputValue()
+ u'foo cooked'
+
+ >>> pprint(w.js_config(), width=1)
+ {'fieldHint': u'hint',
+ 'fieldLabel': u'label',
+ 'id': 'field.f',
+ 'itemCls': 'zc-required-field',
+ 'name': 'field.f',
+ 'widget_constructor': 'my.widget.constructor',
+ 'xtype': 'textfield'}
+
+ >>> w.setRenderedValue(w.getInputValue())
+ >>> pprint(w.js_config(), width=1)
+ {'fieldHint': u'hint',
+ 'fieldLabel': u'label',
+ 'id': 'field.f',
+ 'itemCls': 'zc-required-field',
+ 'name': 'field.f',
+ 'value': u'foo',
+ 'widget_constructor': 'my.widget.constructor',
+ 'xtype': 'textfield'}
+
+The field constraint doesn't get checked on the client, but is checked
+on the server:
+
+ >>> import re, zope.schema.interfaces
+ >>> def digits(v):
+ ... if not re.match('\d+$', v):
+ ... raise zope.schema.interfaces.ValidationError(
+ ... "Must be a number")
+ ... return True
+
+ >>> f = zope.schema.TextLine(
+ ... __name__='f', title=u'label', description=u'hint',
+ ... constraint=digits)
+ >>> w = zc.ajax.widgets.Base(f, request)
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, False)
+
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ WidgetInputError: ('f', u'label', Must be a number)
+
+
+ >>> request.form['field.f'] = u'123'
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, True)
+
+ >>> w.getInputValue()
+ u'123'
+
+Boolean Data
+------------
+
+The boolean widget uses a Ext checkbox field:
+
+ >>> request = zope.publisher.browser.TestRequest()
+ >>> f = zope.schema.Bool(
+ ... __name__='f', title=u'label', description=u'hint')
+ >>> w = zc.ajax.widgets.InputBool(f, request)
+ >>> pprint(w.js_config(), width=1)
+ {'fieldHint': u'hint',
+ 'fieldLabel': u'label',
+ 'id': 'field.f',
+ 'itemCls': 'zc-required-field',
+ 'name': 'field.f',
+ 'xtype': 'checkbox'}
+
+ >>> w.formValue(None), w.formValue(True), w.formValue(False)
+ (None, True, False)
+
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, True)
+
+ >>> w.getInputValue()
+ False
+
+Note that unchecked Ext check boxes, like unchecked HTML checkboxes
+aren't included in form data, so this widget treats lack of form data
+as a "false" value [#noinput]_.
+
+ >>> request.form['field.f'] = 'on'
+ >>> w.getInputValue()
+ True
+
+.. Edge case. Required matches field:
+
+ >>> request = zope.publisher.browser.TestRequest()
+ >>> f = zope.schema.Bool(
+ ... __name__='f', title=u'label', description=u'hint', required=False)
+ >>> w = zc.ajax.widgets.InputBool(f, request)
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (False, True, True)
+
+
+Text data
+---------
+
+There are widgets for both single- and multi-line text.
+
+ >>> request = zope.publisher.browser.TestRequest()
+ >>> f = zope.schema.TextLine(
+ ... __name__='f', title=u'label', description=u'hint')
+ >>> w = zc.ajax.widgets.InputTextLine(f, request)
+ >>> pprint(w.js_config(), width=1)
+ {'fieldHint': u'hint',
+ 'fieldLabel': u'label',
+ 'id': 'field.f',
+ 'itemCls': 'zc-required-field',
+ 'minLength': 0,
+ 'name': 'field.f',
+ 'xtype': 'textfield'}
+
+ >>> w.formValue(None), w.formValue(u'xxx')
+ (None, u'xxx')
+
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, False, False)
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ MissingInputError: ('field.f', u'label', None)
+
+ >>> request.form['field.f'] = u'xxx'
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, True)
+ >>> w.getInputValue()
+ u'xxx'
+
+Constraints are included in the widget data and are checked on the
+server:
+
+ >>> f = zope.schema.TextLine(
+ ... __name__='f', title=u'label', description=u'hint',
+ ... min_length=5, max_length=30)
+ >>> w = zc.ajax.widgets.InputTextLine(f, request)
+ >>> pprint(w.js_config(), width=1)
+ {'allowBlank': False,
+ 'fieldHint': u'hint',
+ 'fieldLabel': u'label',
+ 'id': 'field.f',
+ 'itemCls': 'zc-required-field',
+ 'maxLength': 30,
+ 'minLength': 5,
+ 'name': 'field.f',
+ 'xtype': 'textfield'}
+
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, False)
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ WidgetInputError: ('f', u'label', xxx 5)
+
+ >>> request.form['field.f'] = u'x'*9
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, True)
+ >>> w.getInputValue()
+ u'xxxxxxxxx'
+
+ >>> request.form['field.f'] = u'x'*33
+
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, False)
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ WidgetInputError: ('f', u'label', xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 30)
+
+newlines aren't allowed in text lines:
+
+ >>> request.form['field.f'] = u'foo\nbar'
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, False)
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ WidgetInputError: ('f', u'label', foo
+ bar)
+
+Text fields become text areas:
+
+ >>> request = zope.publisher.browser.TestRequest()
+ >>> f = zope.schema.Text(
+ ... __name__='f', title=u'label', description=u'hint')
+ >>> w = zc.ajax.widgets.InputText(f, request)
+ >>> pprint(w.js_config(), width=1)
+ {'fieldHint': u'hint',
+ 'fieldLabel': u'label',
+ 'id': 'field.f',
+ 'itemCls': 'zc-required-field',
+ 'minLength': 0,
+ 'name': 'field.f',
+ 'xtype': 'textarea'}
+
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, False, False)
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ MissingInputError: ('field.f', u'label', None)
+
+ >>> request.form['field.f'] = u'xxx'
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, True)
+ >>> w.getInputValue()
+ u'xxx'
+
+Constraints are included in the widget data and are checked on the
+server:
+
+ >>> f = zope.schema.Text(
+ ... __name__='f', title=u'label', description=u'hint',
+ ... min_length=5, max_length=30)
+ >>> w = zc.ajax.widgets.InputText(f, request)
+ >>> pprint(w.js_config(), width=1)
+ {'allowBlank': False,
+ 'fieldHint': u'hint',
+ 'fieldLabel': u'label',
+ 'id': 'field.f',
+ 'itemCls': 'zc-required-field',
+ 'maxLength': 30,
+ 'minLength': 5,
+ 'name': 'field.f',
+ 'xtype': 'textarea'}
+
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, False)
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ WidgetInputError: ('f', u'label', xxx 5)
+
+ >>> request.form['field.f'] = u'x'*9
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, True)
+ >>> w.getInputValue()
+ u'xxxxxxxxx'
+
+ >>> request.form['field.f'] = u'x'*33
+
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, False)
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ WidgetInputError: ('f', u'label', xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 30)
+
+newlines are allowed in text lines:
+
+ >>> request.form['field.f'] = u'foo\nbar'
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, True)
+ >>> w.getInputValue()
+ u'foo\nbar'
+
+Integers
+--------
+
+ >>> request = zope.publisher.browser.TestRequest()
+ >>> f = zope.schema.Int(
+ ... __name__='f', title=u'label', description=u'hint')
+ >>> w = zc.ajax.widgets.InputInt(f, request)
+ >>> pprint(w.js_config(), width=1)
+ {'allowBlank': False,
+ 'fieldHint': u'hint',
+ 'fieldLabel': u'label',
+ 'id': 'field.f',
+ 'itemCls': 'zc-required-field',
+ 'name': 'field.f',
+ 'widget_constructor': 'zc.ajax.widgets.InputInt'}
+
+Note that we use a custom widget constructor, which provides
+integer-specific validation on the client.
+
+ >>> w.formValue(None), w.formValue(42)
+ (None, u'42')
+
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, False, False)
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ MissingInputError: ('field.f', u'label', None)
+
+ >>> request.form['field.f'] = u'xxx'
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, False)
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ ConversionError: (u"Invalid integer: u'xxx'", None)
+
+ >>> request.form['field.f'] = u'33'
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, True)
+ >>> w.getInputValue()
+ 33
+
+
+Constraints are included in the widget data and are checked on the
+server:
+
+ >>> f = zope.schema.Int(
+ ... __name__='f', title=u'label', description=u'hint', min=1, max=9)
+ >>> w = zc.ajax.widgets.InputInt(f, request)
+ >>> pprint(w.js_config(), width=1)
+ {'allowBlank': False,
+ 'fieldHint': u'hint',
+ 'fieldLabel': u'label',
+ 'field_max': 9,
+ 'field_min': 1,
+ 'id': 'field.f',
+ 'itemCls': 'zc-required-field',
+ 'name': 'field.f',
+ 'widget_constructor': 'zc.ajax.widgets.InputInt'}
+
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, False)
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ WidgetInputError: ('f', u'label', 33 9)
+
+ >>> request.form['field.f'] = u'3'
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, True)
+ >>> w.getInputValue()
+ 3
+
+Simple Choices
+--------------
+
+Choices from simple vocabularies are supported:
+
+ >>> request = zope.publisher.browser.TestRequest()
+ >>> f = zope.schema.Choice(
+ ... __name__='f', title=u'label', description=u'hint',
+ ... values=['red', 'green', 'blue', 1, 2, 3])
+ >>> w = zc.ajax.widgets.InputChoiceTokenized(f, f.source, request)
+
+Note that we passed the field, its source, and the request. This is
+because InputChoiceTokenized adapts a choice field, a simple
+vocabulary, and a request.
+
+ >>> pprint(w.js_config(), width=1)
+ {'allowBlank': False,
+ 'fieldHint': u'hint',
+ 'fieldLabel': u'label',
+ 'hiddenName': 'field.f.value',
+ 'id': 'field.f',
+ 'itemCls': 'zc-required-field',
+ 'name': 'field.f',
+ 'values': [['red',
+ u'red'],
+ ['green',
+ u'green'],
+ ['blue',
+ u'blue'],
+ ['1',
+ u'1'],
+ ['2',
+ u'2'],
+ ['3',
+ u'3']],
+ 'widget_constructor': 'zc.ajax.widgets.InputChoice'}
+
+ >>> w.formValue(None), w.formValue(2)
+ (None, '2')
+
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, False, False)
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ MissingInputError: ('field.f', u'label', None)
+
+ >>> request.form['field.f.value'] = u'2'
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, True)
+ >>> w.getInputValue()
+ 2
+
+Iterable-source Choices
+-----------------------
+
+Iterable sources are handled similarly to simple vocabularies. A list
+of values is included in the Javascript config:
+
+ >>> import zope.interface
+ >>> class Chars(list):
+ ... zope.interface.implements(zope.schema.interfaces.IIterableSource)
+
+ >>> class Term:
+ ... def __init__(self, c):
+ ... self.value = self.title = c
+ ... self.token = hex(ord(c))
+
+ >>> class Terms:
+ ... def __init__(self, source, request):
+ ... pass
+ ... def getTerm(self, v):
+ ... return Term(v)
+ ... def getValue(self, token):
+ ... return unichr(int(token, 16))
+
+ >>> zope.component.provideAdapter(
+ ... Terms, (Chars, zope.publisher.interfaces.browser.IBrowserRequest),
+ ... zope.app.form.browser.interfaces.ITerms)
+
+
+ >>> request = zope.publisher.browser.TestRequest()
+ >>> f = zope.schema.Choice(
+ ... __name__='f', title=u'label', description=u'hint',
+ ... source=Chars('ABCDEFG'))
+ >>> w = zc.ajax.widgets.InputChoiceIterable(f, f.source, request)
+ >>> pprint(w.js_config(), width=1)
+ {'allowBlank': False,
+ 'fieldHint': u'hint',
+ 'fieldLabel': u'label',
+ 'hiddenName': 'field.f.value',
+ 'id': 'field.f',
+ 'itemCls': 'zc-required-field',
+ 'name': 'field.f',
+ 'values': [['0x41',
+ 'A'],
+ ['0x42',
+ 'B'],
+ ['0x43',
+ 'C'],
+ ['0x44',
+ 'D'],
+ ['0x45',
+ 'E'],
+ ['0x46',
+ 'F'],
+ ['0x47',
+ 'G']],
+ 'widget_constructor': 'zc.ajax.widgets.InputChoice'}
+
+ >>> w.formValue(None), w.formValue('D')
+ (None, '0x44')
+
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, False, False)
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ MissingInputError: ('field.f', u'label', None)
+
+ >>> request.form['field.f.value'] = u'0x43'
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, True)
+ >>> w.getInputValue()
+ u'C'
+
+Timezones
+---------
+
+ >>> f = zope.schema.Choice(
+ ... __name__='tz',
+ ... title=u'Timezone',
+ ... description=u'Some timezone',
+ ... source=zc.form.interfaces.AvailableTimeZones())
+ >>> w = zc.ajax.widgets.InputTimeZone(f, 'ignored source', request)
+
+ >>> pprint(w.js_config(), width=1) # doctest: +ELLIPSIS
+ {'allowBlank': False,
+ 'fieldHint': u'Some timezone',
+ 'fieldLabel': u'Timezone',
+ 'hiddenName': 'field.tz.value',
+ 'id': 'field.tz',
+ 'itemCls': 'zc-required-field',
+ 'name': 'field.tz',
+ 'values': [['Africa/Abidjan',
+ u'Africa/Abidjan'],
+ ...],
+ 'widget_constructor': 'zc.ajax.widgets.InputChoice'}
+
+ >>> print w.formValue(None)
+ None
+ >>> import pytz
+ >>> w.formValue(pytz.utc)
+ 'UTC'
+
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, False, False)
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ MissingInputError: ('field.tz', u'Timezone', None)
+
+ >>> request.form['field.tz.value'] = u'UTC'
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, True)
+ >>> w.getInputValue()
+ <UTC>
+
+Decimals
+--------
+
+ >>> request = zope.publisher.browser.TestRequest()
+ >>> f = zope.schema.Decimal(
+ ... __name__='f', title=u'label', description=u'hint')
+ >>> w = zc.ajax.widgets.InputDecimal(f, request)
+ >>> pprint(w.js_config(), width=1)
+ {'allowBlank': False,
+ 'fieldHint': u'hint',
+ 'fieldLabel': u'label',
+ 'id': 'field.f',
+ 'itemCls': 'zc-required-field',
+ 'name': 'field.f',
+ 'widget_constructor': 'zc.ajax.widgets.InputDecimal'}
+
+Note that we use a custom widget constructor, which provides
+decimal-specific validation on the client.
+
+ >>> import decimal
+ >>> w.formValue(None), w.formValue(decimal.Decimal("42.1"))
+ (None, u'42.1')
+
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, False, False)
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ MissingInputError: ('field.f', u'label', None)
+
+ >>> request.form['field.f'] = u'xxx'
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, False)
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ ConversionError: (u"Invalid decimal: u'xxx'", None)
+
+ >>> request.form['field.f'] = u'33.1'
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, True)
+ >>> w.getInputValue()
+ Decimal("33.1")
+
+Hidden fields
+-------------
+
+ >>> request = zope.publisher.browser.TestRequest()
+ >>> f = zope.schema.TextLine(
+ ... __name__='f', title=u'label', description=u'hint')
+ >>> w = zc.ajax.widgets.Hidden(f, request)
+ >>> pprint(w.js_config(), width=1)
+ {'fieldHint': u'hint',
+ 'fieldLabel': u'label',
+ 'id': 'field.f',
+ 'itemCls': 'zc-required-field',
+ 'name': 'field.f',
+ 'xtype': 'hidden'}
+
+ >>> w.formValue(None), w.formValue(u'xxx')
+ (None, u'xxx')
+
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, False, False)
+ >>> w.getInputValue()
+ Traceback (most recent call last):
+ ...
+ MissingInputError: ('field.f', u'label', None)
+
+ >>> request.form['field.f'] = u'xxx'
+ >>> w.required, w.hasInput(), w.hasValidInput()
+ (True, True, True)
+ >>> w.getInputValue()
+ u'xxx'
+
+
+.. [#noinput] Another way we might handle widgets that don't send data
+ on certain inputs would be to add extra hidden fields. This is
+ might be be something we do or allow in the future.
+
Property changes on: zc.ajax/branches/dev/src/zc/ajax/widgets.txt
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/src/zc/ajax/widgets.zcml
===================================================================
--- zc.ajax/branches/dev/src/zc/ajax/widgets.zcml (rev 0)
+++ zc.ajax/branches/dev/src/zc/ajax/widgets.zcml 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,43 @@
+<configure xmlns="http://namespaces.zope.org/zope">
+
+ <adapter
+ factory=".widgets.InputBool"
+ provides="zope.app.form.interfaces.IInputWidget"
+ />
+
+ <adapter
+ factory=".widgets.InputChoiceIterable"
+ provides="zope.app.form.interfaces.IInputWidget"
+ />
+
+ <adapter
+ factory=".widgets.InputChoiceTokenized"
+ provides="zope.app.form.interfaces.IInputWidget"
+ />
+
+ <adapter
+ factory=".widgets.InputTimeZone"
+ provides="zope.app.form.interfaces.IInputWidget"
+ />
+
+ <adapter
+ factory=".widgets.InputInt"
+ provides="zope.app.form.interfaces.IInputWidget"
+ />
+
+ <adapter
+ factory=".widgets.InputDecimal"
+ provides="zope.app.form.interfaces.IInputWidget"
+ />
+
+ <adapter
+ factory=".widgets.InputTextLine"
+ provides="zope.app.form.interfaces.IInputWidget"
+ />
+
+ <adapter
+ factory=".widgets.InputText"
+ provides="zope.app.form.interfaces.IInputWidget"
+ />
+
+</configure>
Property changes on: zc.ajax/branches/dev/src/zc/ajax/widgets.zcml
___________________________________________________________________
Added: svn:eol-style
+ native
Added: zc.ajax/branches/dev/versions.cfg
===================================================================
--- zc.ajax/branches/dev/versions.cfg (rev 0)
+++ zc.ajax/branches/dev/versions.cfg 2009-04-01 18:21:34 UTC (rev 98767)
@@ -0,0 +1,99 @@
+[versions]
+ClientForm = 0.2.9
+RestrictedPython = 3.4.3
+ZConfig = 2.6.0
+ZODB3 = 3.9.0-dev-r77011
+docutils = 0.5
+mechanize = 0.1.9
+pytz = 2008i
+setuptools = 0.6c9
+simplejson = 1.7.3
+zc.buildout = 1.2.1
+zc.form = 0.1.2
+zc.recipe.egg = 1.2.1
+zc.recipe.testrunner = 1.2.0
+zc.resourcelibrary = 1.0.1
+zc.selenium = 0.1dev-r72479
+zc.sourcefactory = 0.3.4
+zc.zope3recipes = 0.7.0
+zdaemon = 2.0.2
+zodbcode = 3.4.0
+zope.annotation = 3.4.1
+zope.app.applicationcontrol = 3.4.3
+zope.app.appsetup = 3.8.0
+zope.app.authentication = 3.4.3
+zope.app.basicskin = 3.4.0
+zope.app.broken = 3.4.0
+zope.app.catalog = 3.5.1
+zope.app.component = 3.5.0
+zope.app.container = 3.6.2
+zope.app.content = 3.4.0
+zope.app.debug = 3.4.1
+zope.app.dependable = 3.4.0
+zope.app.error = 3.5.1
+zope.app.exception = 3.4.1
+zope.app.folder = 3.4.0
+zope.app.form = 3.6.3
+zope.app.generations = 3.4.1
+zope.app.http = 3.4.1
+zope.app.i18n = 3.4.4
+zope.app.interface = 3.4.0
+zope.app.intid = 3.5.0
+zope.app.keyreference = 3.5.0b2
+zope.app.locales = 3.4.5
+zope.app.pagetemplate = 3.4.1
+zope.app.principalannotation = 3.4.0
+zope.app.publication = 3.5.0
+zope.app.publisher = 3.5.1
+zope.app.renderer = 3.4.0
+zope.app.rotterdam = 3.4.1
+zope.app.schema = 3.4.0
+zope.app.security = 3.5.2
+zope.app.securitypolicy = 3.4.6
+zope.app.server = 3.4.2
+zope.app.session = 3.5.1
+zope.app.testing = 3.5.6
+zope.app.wsgi = 3.4.1
+zope.app.zapi = 3.4.0
+zope.app.zcmlfiles = 3.4.3
+zope.app.zopeappgenerations = 3.4.0
+zope.cachedescriptors = 3.4.1
+zope.component = 3.5.1
+zope.configuration = 3.4.0
+zope.contenttype = 3.4.0
+zope.copypastemove = 3.4.0
+zope.datetime = 3.4.0
+zope.deferredimport = 3.4.0
+zope.deprecation = 3.4.0
+zope.dottedname = 3.4.2
+zope.dublincore = 3.4.0
+zope.error = 3.5.1
+zope.event = 3.4.0
+zope.exceptions = 3.5.2
+zope.filerepresentation = 3.4.0
+zope.formlib = 3.4.0
+zope.hookable = 3.4.0
+zope.i18n = 3.6.0
+zope.i18nmessageid = 3.4.3
+zope.index = 3.4.1
+zope.interface = 3.5.0
+zope.lifecycleevent = 3.4.0
+zope.location = 3.4.0
+zope.minmax = 1.1.0
+zope.modulealias = 3.4.0
+zope.pagetemplate = 3.4.0
+zope.proxy = 3.4.2
+zope.publisher = 3.5.4
+zope.schema = 3.5.0a1
+zope.security = 3.5.2
+zope.securitypolicy = 3.4.1
+zope.server = 3.5.0
+zope.session = 3.7.0
+zope.size = 3.4.0
+zope.structuredtext = 3.4.0
+zope.tal = 3.5.0
+zope.tales = 3.4.0
+zope.testbrowser = 3.5.1
+zope.testing = 3.7.1
+zope.thread = 3.4
+zope.traversing = 3.5.0a4
Property changes on: zc.ajax/branches/dev/versions.cfg
___________________________________________________________________
Added: svn:eol-style
+ native
More information about the Checkins
mailing list