[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