[Checkins] SVN: zc.extjs/branches/dev/src/zc/extjs/ Moved to public repository.

Jim Fulton jim at zope.com
Thu Mar 27 17:31:58 EDT 2008


Log message for revision 84981:
  Moved to public repository.
  
  More documentation and tests on the way.
  

Changed:
  A   zc.extjs/branches/dev/src/zc/extjs/
  A   zc.extjs/branches/dev/src/zc/extjs/CHANGES.txt
  A   zc.extjs/branches/dev/src/zc/extjs/README.txt
  A   zc.extjs/branches/dev/src/zc/extjs/__init__.py
  A   zc.extjs/branches/dev/src/zc/extjs/application.py
  A   zc.extjs/branches/dev/src/zc/extjs/application.txt
  A   zc.extjs/branches/dev/src/zc/extjs/calculator_example.py
  A   zc.extjs/branches/dev/src/zc/extjs/calculator_example.zcml
  A   zc.extjs/branches/dev/src/zc/extjs/calculator_subapplication_example.py
  A   zc.extjs/branches/dev/src/zc/extjs/calculator_subapplication_example.zcml
  A   zc.extjs/branches/dev/src/zc/extjs/configure.zcml
  A   zc.extjs/branches/dev/src/zc/extjs/form.py
  A   zc.extjs/branches/dev/src/zc/extjs/form.txt
  A   zc.extjs/branches/dev/src/zc/extjs/form_example.py
  A   zc.extjs/branches/dev/src/zc/extjs/form_example.zcml
  A   zc.extjs/branches/dev/src/zc/extjs/interfaces.py
  A   zc.extjs/branches/dev/src/zc/extjs/resources/
  A   zc.extjs/branches/dev/src/zc/extjs/resources/calculator_example.js
  A   zc.extjs/branches/dev/src/zc/extjs/resources/util.css
  A   zc.extjs/branches/dev/src/zc/extjs/resources/util.js
  A   zc.extjs/branches/dev/src/zc/extjs/resources/util.txt
  A   zc.extjs/branches/dev/src/zc/extjs/resources/widgets.js
  A   zc.extjs/branches/dev/src/zc/extjs/selenium.py
  A   zc.extjs/branches/dev/src/zc/extjs/selenium.zcml
  A   zc.extjs/branches/dev/src/zc/extjs/session.py
  A   zc.extjs/branches/dev/src/zc/extjs/session.txt
  A   zc.extjs/branches/dev/src/zc/extjs/session.zcml
  A   zc.extjs/branches/dev/src/zc/extjs/testing.py
  A   zc.extjs/branches/dev/src/zc/extjs/tests.py
  A   zc.extjs/branches/dev/src/zc/extjs/tests.zcml
  A   zc.extjs/branches/dev/src/zc/extjs/widgets.py
  A   zc.extjs/branches/dev/src/zc/extjs/widgets.txt
  A   zc.extjs/branches/dev/src/zc/extjs/widgets.zcml

-=-
Added: zc.extjs/branches/dev/src/zc/extjs/CHANGES.txt
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/CHANGES.txt	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/CHANGES.txt	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,21 @@
+Release history
+***************
+
+This package is in a somewhat experimental state.  I *think* it's stable
+enough that future changes are unlikely to break things.  
+
+Future work
+===========
+
+- Better documentation and tests for the javascript part of the
+  implementation.
+
+- More widgets.
+
+- Support for "live search" combo boxes that make ajax calls to update
+  results as people refine searches.
+
+0.1 (2008-03-??)
+================
+
+Initial public release.


Property changes on: zc.extjs/branches/dev/src/zc/extjs/CHANGES.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/README.txt
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/README.txt	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/README.txt	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,13 @@
+********************
+Zope 3 + Ext Support
+********************
+
+The zc.extjs package provides framework to support:
+
+- A single-class application model
+
+- Nested-application support
+
+- Integration with zope.formlib
+
+.. contents::


Property changes on: zc.extjs/branches/dev/src/zc/extjs/README.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/__init__.py
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/__init__.py	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/__init__.py	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,21 @@
+##############################################################################
+#
+# 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.deferredimport
+
+zope.deferredimport.define(
+    Application = 'zc.extjs.extjs:Application',
+    jsonpage = 'zc.extjs.extjs:jsonpage',
+    page = 'zc.extjs.extjs:page',
+    )


Property changes on: zc.extjs/branches/dev/src/zc/extjs/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/application.py
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/application.py	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/application.py	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,192 @@
+##############################################################################
+#
+# 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 simplejson
+import zc.extjs.interfaces
+import zc.resourcelibrary
+import zope.app.exception.browser.unauthorized
+import zope.app.pagetemplate
+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.extjs.interfaces.IAjaxRequest,
+                zope.interface.directlyProvidedBy(object),
+                )
+            return result
+        raise zope.publisher.interfaces.NotFound(self, name, request)    
+
+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.extjs.interfaces.IAjaxRequest)
+
+    def __init__(self, context, request):
+        self.context = context
+
+    def __call__(self):
+        return simplejson.dumps(dict(
+            success = False,
+            error = str(self.context),
+            ))


Property changes on: zc.extjs/branches/dev/src/zc/extjs/application.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/application.txt
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/application.txt	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/application.txt	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,454 @@
+Application support
+===================
+
+The zc.extjs.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 method 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.extjs.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.extjs.application.Application.
+
+We also subclass zc.extjs.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.extjs.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.extjs.application.page decorator that makes methods accessable
+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, which ExtJS
+makes easy to do, 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 specificy 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_seek_wrapper: 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" />
+    <BLANKLINE>
+        <script
+            src="http://localhost/@@/Ext-ext-adapter/adapter/ext/ext-base.js" 
+            type="text/javascript">
+        </script>
+        <script
+            src="http://localhost/@@/Ext-support/set_blank_image.js" 
+            type="text/javascript">
+        </script>
+        <style type="text/css" media="all">
+          <!--
+       @import url("http://localhost/@@/Ext-support/resources/css/ext-all.css");
+          -->
+        </style>
+        <script src="http://localhost/@@/Ext-library/ext-all.js" 
+            type="text/javascript">
+        </script>
+        <script src="http://localhost/@@/zc.extjs/util.js" 
+            type="text/javascript">
+        </script>
+        <script src="http://localhost/@@/zc.extjs/widgets.js" 
+            type="text/javascript">
+        </script>
+        <style type="text/css" media="all">
+          <!--
+            @import url("http://localhost/@@/zc.extjs/util.css");
+          -->
+        </style>
+        <script
+            src="http://localhost/@@/zc.extjs.calculator_example/calculator_example.js" 
+            type="text/javascript">
+        </script>
+    </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.extjs.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. ExtJS generally expects objects to be returned from ajax
+calls.  The ExtJS form framework expects results to have a success
+property indicating whether a form was successfully processed. If
+success is false, it also expects an errors property that is an object
+mapping field names to errors to be displayed with error fields.  The
+zc.extjs packge provides an ajax helper, named call_server that follows a
+similar convention.  If a result has a false success propery and an
+error property containing an error message, it displays an alert with
+the error message.  There's also a form helper that displays an alert
+if there is an error message.  The marshaller used by the
+zc.extjs.application.jsonpage decorator adds a success property if a
+dictionary is returned that doesn't have one.  The property is true
+unless there are error or errors properties.  
+
+    >>> browser.open('http://localhost/@@calculator.html/value')
+    >>> print browser.contents
+    {"success": true, "value": 0}
+
+    >>> browser.open('http://localhost/@@calculator.html/add?value=hi')
+    >>> print browser.contents
+    {"success": false, "error": "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')
+    >>> print browser.contents
+    {"success": true}
+
+If something other than a dictionary is returened from a Python
+method, no success attribute is added:
+
+    >>> browser.open('http://localhost/@@calculator.html/about')
+    >>> print browser.contents
+    "Calculator 1.0"
+
+    >>> browser.open('http://localhost/@@calculator.html/operations')
+    >>> print browser.contents
+    ["add", "subtract"]
+
+If you want to marshal JSON yourself, you can use the
+zc.extjs.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')
+    >>> print browser.contents
+    {"success": false, "error": "The value must be an integer!"}
+
+This works because there is a view registered for
+zope.exceptions.interfaces.IUserError, and
+zc.extjs.interfaces.IAjaxRequest.
+
+Testing support
+===============
+
+zc.extjs.testing has some helper functions to make it easier to test
+ajax calls.  
+
+The call_form function makes an ajax call, marshaling input data and
+de-marshaling output data. It's called call_form because it marshals
+input data as form variables.  Data can be given as a dictionary
+positional argument or as keyword parameters:
+
+    >>> from zc.extjs.testing import call_form, print_form
+    >>> from pprint import pprint
+    >>> pprint(call_form(browser, '/calculator.html/add',
+    ...        {'value': 1}), width=1)
+    {u'success': True,
+     u'value': 1}
+
+    >>> pprint(call_form(browser, '/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 first two arguments are a test browser and a URL.
+
+Keyword arguments are convenient, but sometimes, we need to pass form
+data whose keys are not valid Python identifiers.
+
+Data can be unicode, strings, integers, floats, boolean, or lists. 
+
+    >>> pprint(call_form(browser, '/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}
+
+The function print_form combines pprint and call_form:
+
+    >>> print_form(browser, '/calculator.html/add', {'value': 1})
+    {u'success': True,
+     u'value': 3}
+
+    >>> print_form(browser, '/calculator.html/add', value=1)
+    {u'success': True,
+     u'value': 4}
+
+    >>> print_form(browser, '/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.
+
+.. Edge case: we can't traverse to undecorated methods:
+
+    >>> print_form(browser, '/calculator.html/do_add', value=1)
+    Traceback (most recent call last):
+    ...
+    httperror_seek_wrapper: HTTP Error 404: Not Found
+
+
+"Library" applications
+======================
+
+The "application" model described above is pretty appealing in it's
+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.extjs.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.
+
+    (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:
+
+    >>> print_form(browser, '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:
+
+    >>> print_form(browser, '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:
+
+    >>> browser = zope.testbrowser.testing.Browser()
+    >>> print_form(browser, '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.extjs.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.extjs.application   
+    >>> class Foo:
+    ...     def __str__(self):
+    ...         return 'a '+self.__class__.__name__
+    ...
+    ...     @zc.extjs.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
+    {}
+
+
+.. [#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 actuially contain it's 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.


Property changes on: zc.extjs/branches/dev/src/zc/extjs/application.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/calculator_example.py
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/calculator_example.py	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/calculator_example.py	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,50 @@
+import zc.extjs.application
+import zope.exceptions
+
+class Calculator(zc.extjs.application.Trusted,
+                 zc.extjs.application.Application):
+
+    resource_library_name = 'zc.extjs.calculator_example'
+
+    @zc.extjs.application.jsonpage
+    def about(self):
+        return 'Calculator 1.0'
+
+    @zc.extjs.application.jsonpage
+    def operations(self):
+        return ['add', "subtract"]
+
+    @zc.extjs.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.extjs.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.extjs.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.extjs.application.jsonpage
+    def noop(self):
+        pass
+
+    @zc.extjs.application.page
+    def none(self):
+        return "null"
+
+    @zc.extjs.application.jsonpage
+    def echo_form(self):
+        return dict(self.request.form)
+


Property changes on: zc.extjs/branches/dev/src/zc/extjs/calculator_example.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/calculator_example.zcml
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/calculator_example.zcml	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/calculator_example.zcml	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,16 @@
+<configure xmlns="http://namespaces.zope.org/zope">
+  <adapter name="calculator.html"
+           factory="zc.extjs.calculator_example.Calculator"
+           permission="zope.View"
+           />
+  <resourceLibrary
+     name="zc.extjs.calculator_example"
+     require="Ext zc.extjs"
+     >
+    <directory
+       source="resources"
+       include="calculator_example.js"
+       />
+  </resourceLibrary>
+
+</configure>


Property changes on: zc.extjs/branches/dev/src/zc/extjs/calculator_example.zcml
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/calculator_subapplication_example.py
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/calculator_subapplication_example.py	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/calculator_subapplication_example.py	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,45 @@
+import zc.extjs.application
+import zope.exceptions
+
+class Container(zc.extjs.application.Application):
+
+    resource_library_name = 'zc.extjs'
+
+    @property
+    def calc(self):
+        return Calculator(self.context, self.request, base_href='calc')
+
+
+class Calculator(zc.extjs.application.Trusted,
+                 zc.extjs.application.SubApplication,
+                 zc.extjs.application.PublicTraversable,
+                 ):
+
+    @zc.extjs.application.jsonpage
+    def operations(self):
+        return [['add', self.base_href+'/add'],
+                ['add', self.base_href+'/subtract'],
+                ]
+                
+    @zc.extjs.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.extjs.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.extjs.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.extjs/branches/dev/src/zc/extjs/calculator_subapplication_example.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/calculator_subapplication_example.zcml
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/calculator_subapplication_example.zcml	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/calculator_subapplication_example.zcml	2008-03-27 21:31:56 UTC (rev 84981)
@@ -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.extjs.calculator_subapplication_example.Container"
+           permission="zope.View"
+           />
+</configure>


Property changes on: zc.extjs/branches/dev/src/zc/extjs/calculator_subapplication_example.zcml
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/configure.zcml
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/configure.zcml	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/configure.zcml	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,22 @@
+<configure 
+   xmlns="http://namespaces.zope.org/zope"
+   xmlns:browser="http://namespaces.zope.org/browser"
+   >
+
+  <include package="zc.extjsresource" />
+  <include file="widgets.zcml" />
+
+  <adapter factory=".application.UserError" name="index.html" />
+
+  <resourceLibrary name="zc.extjs">
+    <directory
+       source="resources"
+       include="util.js widgets.js util.css"
+       />
+  </resourceLibrary>
+
+  <class class=".form.Action">
+    <allow attributes="__call__ browserDefault" />
+  </class>
+
+</configure>


Property changes on: zc.extjs/branches/dev/src/zc/extjs/configure.zcml
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/form.py
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/form.py	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/form.py	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,164 @@
+##############################################################################
+#
+# 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.extjs.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, context, request=None):
+        self.context = context
+        if request is None:
+            request = context.request
+        self.request = request
+
+    @zope.cachedescriptors.property.Lazy
+    def prefix(self):
+        return self.base_href.replace('/', '.')
+
+    @zope.cachedescriptors.property.Lazy
+    def base_href(self):
+        base_href = getattr(self.context, '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.context, self.request,
+            ignore_request=True)
+
+        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.extjs.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.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.extjs.application.result(dict(errors=field_errors))
+
+
+        # XXX invariants and action conditions
+        # XXX action validator and failure handlers
+
+        return zc.extjs.application.result(self.action.success(data))
+
+    def browserDefault(self, request):
+        return self, ()


Property changes on: zc.extjs/branches/dev/src/zc/extjs/form.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/form.txt
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/form.txt	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/form.txt	2008-03-27 21:31:56 UTC (rev 84981)
@@ -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.extjs.form.Form. This base class provides:
+
+- an ajax __call__ method that retuirns a form definition,
+
+- traversal to form actions, in much the same way that
+  zc.extjs.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 becomes the form's page attribute.
+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.extjs.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.extjs.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.extjs resource library that provides additional information,
+like Javascript validators, that can't be expressed in JSON.
+
+The're an action definition for each action defined in the form.  The
+action information includes the url to post the result to, reletive 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_class_name': u'ExampleForm',
+     u'self_context_class_name': u'FormExample',
+     u'self_context_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 profix removed, and that
+
+- the value of the age key is an integer, since the field was an
+  integer field.
+
+The action also prints out the classes of it's self argument, and it's
+contexts.  Actions are methods of forms so their self argument is the
+form. The form context is the page it's accessed through.
+
+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 from
+calling get_definition.  
+
+    >>> import zc.extjs.form_example
+    >>> import zope.publisher.browser
+    >>> request = zope.publisher.browser.TestRequest()
+    >>> import zc.extjs.interfaces
+    >>> import zope.interface
+    >>> zope.interface.alsoProvides(request, 
+    ...                                 zc.extjs.interfaces.IAjaxRequest)
+    >>> ex = zc.extjs.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.extjs.widgets.InputInt'}]}
+
+Note that we had to stamp the request with IAjaxRequest.  This is done
+during application traversal.  We need it som 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 it's class name.
+The form's base_href also includes the base_href of it's page, if it's page has
+a base_href. This is useful for sub-application. Let's give our sample
+application a base_href attribute as if it was a sub-application:
+
+    >>> ex = zc.extjs.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.extjs.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 it's 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 adta suitable for etting form field
+values.  Let's create a person and use out form to get data for them:
+
+    >>> bob = zc.extjs.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.extjs/branches/dev/src/zc/extjs/form.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/form_example.py
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/form_example.py	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/form_example.py	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,56 @@
+import zc.extjs.application
+import zc.extjs.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.extjs.application.Application):
+
+    resource_library_name = None
+
+    class ExampleForm(zc.extjs.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_context_class_name = self.context.__class__.__name__,
+                self_context_context_class_name =
+                self.context.context.__class__.__name__,
+                )


Property changes on: zc.extjs/branches/dev/src/zc/extjs/form_example.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/form_example.zcml
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/form_example.zcml	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/form_example.zcml	2008-03-27 21:31:56 UTC (rev 84981)
@@ -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.extjs.form_example.FormExample"
+           permission="zope.View"
+           />
+</configure>


Property changes on: zc.extjs/branches/dev/src/zc/extjs/form_example.zcml
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/interfaces.py
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/interfaces.py	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/interfaces.py	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,52 @@
+##############################################################################
+#
+# 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.
+        """
+


Property changes on: zc.extjs/branches/dev/src/zc/extjs/interfaces.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/resources/calculator_example.js
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/resources/calculator_example.js	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/resources/calculator_example.js	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,64 @@
+Ext.namespace('zc.extjs');
+
+zc.extjs.calculator_example = function () {
+
+    function init () {
+        var input = new Ext.form.TextField({
+            id: 'input',
+            maskRe: /[0-9]/,
+            value: 1
+        });
+
+        function setValue(result)
+        {
+            Ext.get('value').dom.innerHTML = result.value;
+        }
+
+        zc.extjs.util.call_server({
+            url: 'value',
+            task: 'Getting value',
+            success: setValue
+        });
+
+        new Ext.Viewport({
+            items: [
+                {xtype: 'box', autoEl: 
+                 {
+                     tag: 'div', 
+                     html: 'Current value: <span id="value">?</span><br>'}
+                },
+                {xtype: 'box', autoEl: {tag: 'span', html: 'Input:'}},
+                input,
+                new Ext.Button({
+                    text: '+', id: 'add-button',
+                    handler: function () {
+                        zc.extjs.util.call_server({
+                            url: 'add',
+                            params: {'value:int': input.getValue()},
+                            task: 'Adding',
+                            success: setValue
+                        });
+                    }
+                }),
+                new Ext.Button({
+                    text: '-', id: 'subtract-button',
+                    handler: function () {
+                        zc.extjs.util.call_server({
+                            url: 'subtract',
+                            params: {'value:int': input.getValue()},
+                            task: 'Subtracting',
+                            success: setValue
+                        });
+                    }
+                })
+            ]
+        })
+    }
+
+    return {
+        init: init
+    };
+
+} ();
+
+Ext.onReady(zc.extjs.calculator_example.init);


Property changes on: zc.extjs/branches/dev/src/zc/extjs/resources/calculator_example.js
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/resources/util.css
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/resources/util.css	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/resources/util.css	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,4 @@
+
+.zc-required-field .x-form-item-label {
+  font-weight: bold;
+}


Property changes on: zc.extjs/branches/dev/src/zc/extjs/resources/util.css
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/resources/util.js
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/resources/util.js	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/resources/util.js	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,229 @@
+Ext.namespace('zc.extjs');
+
+zc.extjs.util = function() {
+
+    // Maybe connections are expensive. Who knows. They appear to want
+    // to be reused.
+    var server_connection = new Ext.data.Connection();
+
+    function call_server (args)
+    {
+        var callback = function (options, success, response) {
+            if (! success)
+            {
+                system_error(args.task);
+                if (args.failure)
+                    args.failure({});
+            }
+            else
+            {
+                result = eval("("+response.responseText+")");
+                if (result.session_expired)
+                    return session_expired();
+
+                if (result.error)
+                {
+                    Ext.MessageBox.alert(args.task+' failed',
+                                         result.error);
+                    if (args.failure)
+                        args.failure(result);
+                }
+                else
+                {
+                    if (args.success)
+                        args.success(result);
+                }
+            }
+        };
+
+        if (args.jsonData)
+        {
+//             YAHOO.util.Connect.setDefaultPostHeader(false);
+            server_connection.request({
+                url: args.url, jsonData: args.jsonData, method: 'POST',
+                callback: callback});
+//             YAHOO.util.Connect.setDefaultPostHeader(true);
+        }
+        else
+            server_connection.request({
+                url: args.url, params: args.params,
+                callback: callback});
+    }
+
+    function new_form(args)
+    {
+        var config = Ext.apply({}, args.config);
+
+        if (config.buttons === undefined)
+            config.buttons = [];
+
+        if (config.items === undefined)
+            config.items = [];
+
+        for (var i=0; i < args.definition.widgets.length; i++)
+            config.items.push(zc.extjs.widgets.Field(
+                args.definition.widgets[i]));
+
+        for (var i=0; i < args.definition.actions.length; i++)
+        {
+            var url = args.definition.actions[i].url;
+            config.buttons.push({
+                text: args.definition.actions[i].label,
+                id: args.definition.actions[i].name,
+                handler: function () 
+                {
+                    if (! form_valid(form))
+                        return;
+                    form.getForm().submit({
+                        url: url,
+                        waitMsg: '...',
+                        failure: zc.extjs.util.form_failure,
+                        success: args.after
+                    });
+                }
+            });
+        }
+        var form = new Ext.form.FormPanel(config);
+        return form;
+    }
+
+    function form_dialog (args)
+    {
+        var dialog;
+        return function (data) {
+            if (dialog)
+            {
+                form_reset(dialog.initialConfig.items[0], data);
+                return dialog.show();
+            }        
+            call_server({
+                url: args.url,
+                params: args.params,
+                task: "Loading form definition",
+                success: function (result) {
+
+                    var form_config = {
+                        autoHeight: true,
+                        buttons: [{
+                            text: 'Cancel',
+                            handler: function ()
+                            {
+                                dialog.hide();
+                            }
+                        }]
+                    };
+                    if (args.form_config)
+                    {
+                        if (args.form_config.buttons)
+                            args.form_config.buttons = (
+                                args.form_config.buttons.concat(
+                                    form_config.buttons));
+                        form_config = ext.apply(form_config, args.form_config);
+                    }
+
+                    var config = {
+                        layout: 'fit',
+                        modal: true,
+                        autoHeight: true,
+                        items: [
+                            new_form({
+                                definition: result.definition,
+                                config: form_config,
+                                after: function (form, action)
+                                {
+                                    dialog.hide();
+                                    if (args.after)
+                                        args.after(form, action);
+                                }
+                            })
+                        ]
+                    };
+                    if (args.window_config)
+                        config = Ext.apply(config, args.window_config);
+                    if (result.definition.title)
+                        config.title = result.definition.title;
+                    dialog = new Ext.Window(config);
+                    dialog.show();
+                    
+                    if (data)
+                    {
+                        if (result.data)
+                            result.data = Ext.apply(result.data, data);
+                        else
+                            result.data = data;
+                    }
+                    form_reset(dialog.initialConfig.items[0], result.data);
+                }
+            });
+        };
+    }
+
+    function form_failure (form, action)
+    {
+        if (action.result && action.result.session_expired)
+            session_expired();
+        else if (action.result && action.result.error)
+            Ext.MessageBox.alert("Couldn't submit form", action.result.error);
+        else if (! (action.result && action.result.errors))
+            system_error("Submitting this form");
+    }
+
+    function form_reset (form_panel, data)
+    {
+        form_panel.getForm().reset();
+        if (data)
+            for (var field_name in data)
+                form_panel.find('name', field_name)[0].setValue(
+                    data[field_name]);
+     }
+   
+    function form_valid(form) 
+    {
+        if (form.getForm().isValid())
+            return true;
+        Ext.MessageBox.alert('Errors', 'Please fix the errors noted.');
+        return false;
+    }
+
+    function init ()
+    {
+        Ext.QuickTips.init();
+    }
+
+    function map (func, sequence)
+    {
+        var result = [];
+        for (var i=0; i < sequence.length; i++)
+            result.push(func(sequence[i]));
+        return result;
+    }
+
+    function session_expired () 
+    {
+        Ext.MessageBox.alert(
+            'Session Expired',
+            "You will need to log-in again.",
+            window.location.reload.createDelegate(window.location)
+        );
+    }
+        
+    function system_error (task)
+    {
+        Ext.MessageBox.alert("Failed",task+" failed for an unknown reason");
+    }
+
+    return {
+        call_server: call_server,
+        form: new_form,
+        form_dialog: form_dialog,
+        form_failure: form_failure,
+        form_reset: form_reset,
+        form_valid: form_valid,
+        init: init,
+        map: map,
+        system_error: system_error
+    };
+}();
+
+Ext.form.Field.prototype.msgTarget = 'under';
+Ext.onReady(zc.extjs.util.init);


Property changes on: zc.extjs/branches/dev/src/zc/extjs/resources/util.js
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/resources/util.txt
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/resources/util.txt	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/resources/util.txt	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,173 @@
+=======================
+ExtJS Utility Functions
+=======================
+
+The zc.extjs.util Java Script module contains a collection of utility
+functions found useful.  This is a work in progress and perhaps
+someday they will further organized into separate modules.
+
+call_server(config)
+===================
+
+Make an ajax call to a server.  This is a small wrapper around
+Ext.data.Connection that has allowed me (Jim) to avoid some
+repetition.
+
+The arguments are given as a configuration argument with items:
+
+url
+    The URL to call
+
+params (optional)
+    An object giving request parameters.
+
+task
+    A string to be used in failure messages.
+
+    The task string should describe what you're doing, such as
+    "Saving".
+
+success (optional)
+    A function to call with a result if the call suceeds
+
+faulure (optional)
+    A function to call with a result if the call fails
+
+Exammple::
+
+   zc.extjs.util.call_server({
+       url: 'Updating',
+       params: {login: login, email: email},
+       task: "Updating user information"
+       })
+
+The function doesn't return anything.
+
+form(config)
+============
+
+Create a form panel using a form definition loaded from a formlib-based
+server form.
+
+The arguments are given as a configuration argument with items:
+
+definition
+    the form definition object as loaded from a server
+
+config (optional)
+    configuration parameters to be passed when creating a form
+    panel
+
+    Note that this can contain buttons or other items.  New buttons
+    and items computed from the form definition will be appended.
+
+after (optional)
+    A Callback function to call when a form has been successfully
+    submitted without errors.
+
+A new form panel is returned.
+
+form_dialog(config)
+===================
+
+Create a modal dialog by loading formlib-based form definitions from a server.
+
+The arguments are given as a configuration argument with items:
+
+url
+  The URL to load the definitions from.
+
+window_config
+  A configuration object giving extra window configuration data.  This
+  should generally include:
+
+  title
+     The window title
+
+  width
+     The window width
+
+     Ext seems to be able to calculate the window height OK, but not
+     the window width.
+
+form_config
+    configuration parameters to be passed when creating a form
+    panel within the dialog
+
+
+after (optional)
+    A Callback function to call when a form has been successfully
+    submitted without errors.
+
+A function is returned that can be used to display the dialog. This
+function can be called without arguments or with a data object to be
+used to initialize the form.
+
+Example::
+
+   add_button = new Ext.Button({
+       text: '+',
+       handler: zc.extjs.form_dialog({
+          url: 'add_slot_form',
+          window_config: {title: 'Add slot', width: 500},
+          after: function () {
+              slot_tree.getRootNode().reload();
+          }
+       })
+   });
+
+
+form_failure(form, action)
+==========================
+
+This is a generic failure handler that can be used to failed form submissions.
+
+If the action.result object has a true session_expired attribute, it
+will display a notification to the user that they need to log in again
+and then redirect then cause the page to be reloaded.
+
+If the action.result object has an error attribute, it will display
+the error as an alert.  Otherwise, it will display a "system
+error"-ish message.
+
+form_reset(form_panel, data)
+============================
+
+Reset a form.
+
+Arguments:
+
+form_panel
+    The form panel to be reset
+
+data (optional)
+    Data to be used to initialize form fields.
+
+
+form_valid(form_panel)
+======================
+
+Do client-side validation.  If there is a problem, pop up an alert
+with a generic message.
+
+A boolean value is returned indicating whether the form is valid.
+
+This would typically be used in a button handler before submitting a
+form::
+
+    if (! form_valid(my_form_panel))
+        return;
+    my_form_panel.getForm().submit({
+    ...
+
+
+map(func, sequence)
+===================
+
+Create a sequence by applying a function to an existing sequence.
+
+system_error(task)
+==================
+
+Pop up an alert with a generic error that includes the task being
+performed.


Property changes on: zc.extjs/branches/dev/src/zc/extjs/resources/util.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/resources/widgets.js
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/resources/widgets.js	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/resources/widgets.js	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,77 @@
+Ext.namespace('zc.extjs');
+
+zc.extjs.widgets = function() {
+    return {
+        Field: function (widget)
+        {
+            var config;
+
+            if (widget.widget_constructor)
+                config = eval(widget.widget_constructor)(widget);
+            else
+                config = Ext.apply(widget, {})
+
+            if (widget.fieldHint)
+            {
+                if (! config.listeners)
+                    config.listeners = {};
+                config.listeners.render = function (field) {
+                    Ext.QuickTips.register({
+                        target: field.getEl(),
+                        title: widget.fieldLabel,
+                        text: widget.fieldHint
+                    });
+                };
+            }
+            return config;
+        },
+
+        InputInt: function (widget)
+        {
+            var config = Ext.apply({xtype: 'textfield'}, widget);
+
+            config.validator = function (value) {
+                value = Number(value);
+                if (config.field_min !== undefined && value < config.field_min)
+                    return "The value must be at least "+config.field_min;
+                if (config.field_max !== undefined && value > config.field_max)
+                    return ("The value must be less than or equal to "
+                            +config.field_max);
+                return true;
+            };
+            if (config.field_min !== undefined && config.field_min >= 0)
+                config.maskRe = /[0-9]/;
+            else
+            {
+                config.maskRe = /[-0-9]/;
+            }
+            config.regex = /^-?[0-9]+$/;
+            config.regexText = 'The input must be an integer.';
+            return config;
+        },
+
+        InputChoice: function (widget)
+        {
+            var config = Ext.apply({xtype: 'combo'}, widget);
+
+            if (config.values)
+                config.store = new Ext.data.SimpleStore({
+                    fields: [{name: 'value', mapping: 0},
+                             {name: 'display', mapping: 1}
+                            ],
+                    id: 0,
+                    data: config.values
+                });
+            config = Ext.apply(config, {
+                valueField: 'value',
+                displayField: 'display',
+                triggerAction: 'all',
+                selectOnFocus:true,
+                editable: false,
+                forceSelection: true,
+                mode: 'local'
+            });
+            return config;
+        }
+    };    
+}();


Property changes on: zc.extjs/branches/dev/src/zc/extjs/resources/widgets.js
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/selenium.py
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/selenium.py	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/selenium.py	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,31 @@
+##############################################################################
+#
+# 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 zc.selenium.pytest
+class SeleniumTests(zc.selenium.pytest.Test):
+
+    def testCalculator(self):
+        s = self.selenium
+        s.open('/calculator.html?login')
+        s.waitForText('value', '0')
+        s.type('input', '2')
+        s.click('add-button')
+        s.waitForText('value', '2')
+        s.click('add-button')
+        s.waitForText('value', '4')
+        s.type('input', '3')
+        s.click('subtract-button')
+        s.waitForText('value', '1')
+      


Property changes on: zc.extjs/branches/dev/src/zc/extjs/selenium.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/selenium.zcml
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/selenium.zcml	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/selenium.zcml	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,14 @@
+<configure xmlns="http://namespaces.zope.org/zope" package="zc.extjs">
+
+  <include file="tests.zcml" />
+
+  <include package="zope.app.server" />
+  <include package="zc.selenium" />
+
+  <adapter 
+     factory="zc.extjs.selenium.SeleniumTests" 
+     name="zc.extjs.tests.SeleniumTests.html"
+     permission="zope.Public"
+     />
+
+</configure>


Property changes on: zc.extjs/branches/dev/src/zc/extjs/selenium.zcml
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/session.py
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/session.py	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/session.py	2008-03-27 21:31:56 UTC (rev 84981)
@@ -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.extjs/branches/dev/src/zc/extjs/session.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/session.txt
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/session.txt	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/session.txt	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,43 @@
+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.extjs 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.extjs" 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.extjs" 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_seek_wrapper: 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')
+    >>> browser.contents
+    '{"session_expired": true, "success": false}'
+
+
+.. [#application] See application.txt


Property changes on: zc.extjs/branches/dev/src/zc/extjs/session.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/session.zcml
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/session.zcml	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/session.zcml	2008-03-27 21:31:56 UTC (rev 84981)
@@ -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.extjs/branches/dev/src/zc/extjs/session.zcml
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/testing.py
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/testing.py	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/testing.py	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,146 @@
+##############################################################################
+#
+# 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 pprint
+import simplejson
+import urllib
+import zc.extjs.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
+
+def _marshal_scalar(n, v):
+    if not isinstance(v, str):
+        if isinstance(v, bool):
+            return "%s:boolean=%s" % (n, v and '1' or '')
+        if isinstance(v, int):
+            return "%s:int=%s" % (n, v)
+        if isinstance(v, float):
+            return "%s:float=%s" % (n, v)
+        if isinstance(v, unicode):
+            return "%s=%s" % (n, urllib.quote(v.encode('utf-8')))
+        raise ValueError("can't marshal %r" % v)
+    return "%s=%s" % (n, urllib.quote(v.encode('ascii')))
+    
+def call_form(browser, url, __params=(), **params):
+
+    browser.addHeader('X-Requested-With', 'XMLHTTPRequest')
+    if params or __params:
+        params = params.copy()
+        params.update(__params)
+        query = []
+        for n, v in params.items():
+            if isinstance(v, list):
+                n += ':list'
+                for vv in v:
+                    query.append(_marshal_scalar(n, vv))
+            else:
+                query.append(_marshal_scalar(n, v))
+
+        browser.open(url, '&'.join(query))
+    else:
+        browser.open(url)
+
+    # 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 print_form(*a, **kw):
+    pprint.pprint(call_form(*a, **kw), 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.extjs.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.extjs/branches/dev/src/zc/extjs/testing.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/tests.py
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/tests.py	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/tests.py	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,46 @@
+##############################################################################
+#
+# 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
+
+    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.extjs/branches/dev/src/zc/extjs/tests.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/tests.zcml
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/tests.zcml	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/tests.zcml	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,17 @@
+<configure xmlns="http://namespaces.zope.org/zope" package="zc.extjs">
+
+  <include package="zope.app.zcmlfiles" file="meta.zcml" /> <!-- Gaaaah -->
+  <include file="session.zcml" />
+  <include package="zc.extjs" />
+  <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.extjs/branches/dev/src/zc/extjs/tests.zcml
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/widgets.py
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/widgets.py	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/widgets.py	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,280 @@
+##############################################################################
+#
+# 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.extjs.interfaces
+import zope.app.form
+import zope.app.form.interfaces
+import zope.app.form.browser.interfaces
+import zope.interface
+import zope.cachedescriptors.property
+import zope.component
+import zope.schema.interfaces
+
+class Base(zope.app.form.InputWidget):
+
+    zope.interface.implements(zc.extjs.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 _toValue(self, v):
+        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._toValue(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.extjs.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.extjs.interfaces.IAjaxRequest,
+        )
+
+    widget_constructor = 'zc.extjs.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 _toValue(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.extjs.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 _toValue(self, v):
+        return self.source.getTermByToken(v).value
+            
+class InputInt(Base):
+
+    zope.component.adapts(
+        zope.schema.interfaces.IInt,
+        zc.extjs.interfaces.IAjaxRequest,
+        )
+
+    widget_constructor = 'zc.extjs.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 _toValue(self, v):
+        try:
+            return int(v)
+        except:
+            raise zope.app.form.interfaces.ConversionError(
+                u"Invalid integer: %r" % v
+                )
+
+class InputTextLine(Base):
+
+    zope.component.adapts(
+        zope.schema.interfaces.ITextLine,
+        zc.extjs.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.extjs.interfaces.IAjaxRequest,
+        )
+
+    xtype = 'textarea'
+
+class Hidden(Base):
+
+    xtype = 'hidden'


Property changes on: zc.extjs/branches/dev/src/zc/extjs/widgets.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/widgets.txt
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/widgets.txt	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/widgets.txt	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,628 @@
+Ajax widgets
+============
+
+The zc.extjs.form module provides s ajax form support based on the
+ExtJS form framework.
+
+Widgets are looked up for fields and requests providing
+zc.extjs.interfaces.IAjaxRequest.
+
+Ajax widgets must implement zc.extjs.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.extjs.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.extjs.widgets.Base
+---------------------
+
+The zc.extjs.widgets.Base provides a basic widget implementation and
+hooks that can be overridden for specific widget types.  These hooks
+are:
+
+  xtype
+      This is an string attribute that, if set, should contain the ExtJS type
+      name for the ExtJS widget to be used.
+
+  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.
+
+  _toValue(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.extjs.interfaces.IInputWidget may also be
+overridden as needed. For example, because ExtJS doesn't send input
+for unset check boxes [#noinput]_, zc.extjs.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.extjs.widgets
+    >>> import zope.schema
+    >>> f = zope.schema.TextLine(
+    ...    __name__='f', title=u'label', description=u'hint')
+    >>> w = zc.extjs.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._toValue = 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.extjs.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.extjs.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.extjs.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.extjs.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.extjs.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.extjs.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.extjs.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.extjs.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.extjs.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.extjs.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.extjs.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.extjs.widgets.InputChoiceTokenized(f, f.source, request)
+
+Note that we passed the field, it's 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.extjs.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.extjs.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.extjs.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'
+
+
+Hidden fields
+-------------
+
+    >>> request = zope.publisher.browser.TestRequest()
+    >>> f = zope.schema.TextLine(
+    ...    __name__='f', title=u'label', description=u'hint')
+    >>> w = zc.extjs.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.extjs/branches/dev/src/zc/extjs/widgets.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.extjs/branches/dev/src/zc/extjs/widgets.zcml
===================================================================
--- zc.extjs/branches/dev/src/zc/extjs/widgets.zcml	                        (rev 0)
+++ zc.extjs/branches/dev/src/zc/extjs/widgets.zcml	2008-03-27 21:31:56 UTC (rev 84981)
@@ -0,0 +1,33 @@
+<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.InputInt"
+     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.extjs/branches/dev/src/zc/extjs/widgets.zcml
___________________________________________________________________
Name: svn:eol-style
   + native



More information about the Checkins mailing list