[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