From jwashin at codespeak.net Sat Oct 7 23:20:50 2006
From: jwashin at codespeak.net (jwashin at codespeak.net)
Date: Sat, 7 Oct 2006 23:20:50 +0200 (CEST)
Subject: [z3-checkins] r32995 - z3/jsonserver/trunk
Message-ID: <20061007212050.8F07B1006E@code0.codespeak.net>
Author: jwashin
Date: Sat Oct 7 23:20:46 2006
New Revision: 32995
Added:
z3/jsonserver/trunk/JSONViews.txt
z3/jsonserver/trunk/ftests.py
Modified:
z3/jsonserver/trunk/__init__.py
z3/jsonserver/trunk/interfaces.py
z3/jsonserver/trunk/jsonrpc.py
Log:
Added JSONViews, and JSONViews.txt as ftest
Added: z3/jsonserver/trunk/JSONViews.txt
==============================================================================
--- (empty file)
+++ z3/jsonserver/trunk/JSONViews.txt Sat Oct 7 23:20:46 2006
@@ -0,0 +1,162 @@
+JSON Views are used when an HTTP GET is a reasonable way to get some
+JSON-formatted data from the server, for example, if you have a javascript
+library that employs JSON GETs, e.g., Dojo.
+
+A JSON View is simply a page that, instead of HTML, is a JSON.representation of
+some data.
+
+To do this you need to create a view class and register it in ZCML.
+
+We'll follow the xmlrpc README to demonstrate.
+
+First, write a view class, descended from JSONView. Whatever is returned in the
+doResponse method is what will be sent as a response. The usual view instance
+variables context and request are available.
+
+ >>> from jsonserver import JSONView
+ >>> class FolderListing(JSONView):
+ ... def doResponse(self):
+ ... return list(self.context.keys())
+
+Register it as a view. Usually, this will be a browser:page or browser:view
+directive (I'm not sure which is really preferred, either should work.).
+browser2:page should also work, though I have not tried it.
+
+ >>> from zope.configuration import xmlconfig
+ >>> ignored = xmlconfig.string("""
+ ...
+ ...
+ ...
+ ...
+ ...
+ ... """)
+
+Exactly like the xmlrpc example, we add some items to the root folder.
+
+ >>> print http(r"""
+ ... POST /@@contents.html HTTP/1.1
+ ... Authorization: Basic bWdyOm1ncnB3
+ ... Content-Length: 73
+ ... Content-Type: application/x-www-form-urlencoded
+ ...
+ ... type_name=BrowserAdd__zope.app.folder.folder.Folder&new_value=f1""")
+ HTTP/1.1 303 See Other
+ ...
+
+ >>> print http(r"""
+ ... POST /@@contents.html HTTP/1.1
+ ... Authorization: Basic bWdyOm1ncnB3
+ ... Content-Length: 73
+ ... Content-Type: application/x-www-form-urlencoded
+ ...
+ ... type_name=BrowserAdd__zope.app.folder.folder.Folder&new_value=f2""")
+ HTTP/1.1 303 See Other
+ ...
+
+Before we can JSONView, there needs to be an IJSONWriter utility. Here's one.
+ >>> import zope.component
+ >>> import jsoncomponent
+ >>> from jsonserver.interfaces import IJSONWriter
+ >>> zope.component.provideUtility(jsoncomponent.JSONWriter(),IJSONWriter)
+
+Let's set up a browser.
+ >>> from zope.testbrowser.testing import Browser
+ >>> browser = Browser('http://localhost/@@/testbrowser/simple.html')
+ >>> #this was how I figured out the need for the IJSONWriter utility...
+ >>> #browser.handleErrors = False
+
+Now, we can call our new JSONView and get a response:
+ >>> browser.open('/folderlist')
+ Traceback (most recent call last):
+ ...
+ HTTPError: HTTP Error 401: Unauthorized
+
+That was expected Let's view again, and provide authentication this time.
+Content-type is set appropriately.
+ >>> browser.addHeader('Authorization','Basic mgr:mgrpw')
+ >>> browser.open('/folderlist')
+ >>> browser.headers['content-type']
+ 'application/json;charset=utf-8'
+ >>> browser.contents
+ '["f1","f2"]'
+
+Pretty cool, yes? This is much smaller than a similar xmlrpc response.
+
+But what about parameters? Let's create another class with some error handling.
+There's no real standard on this, so you may need to commune with the cliemt
+implementation to see how to handle errors.
+ >>> import decimal
+ >>> class FolderStupidSum(JSONView):
+ ... """return two values and their sum"""
+ ... def doResponse(self, a=0, b=0):
+ ... try:
+ ... a = decimal.Decimal(a)
+ ... b = decimal.Decimal(b)
+ ... except decimal.InvalidOperation:
+ ... self.request.response.setStatus(500)
+ ... return {'error':'bad params','a':a, 'b':b}
+ ... return {'a':a,'b':b,'sum':a+b}
+
+and register it.
+ >>> from zope.configuration import xmlconfig
+ >>> ignored = xmlconfig.string("""
+ ...
+ ...
+ ...
+ ...
+ ...
+ ... """)
+
+Start a new browser.
+ >>> browser = Browser('http://localhost/@@/testbrowser/simple.html')
+ >>> #browser.handleErrors = False
+ >>> browser.addHeader('Authorization','Basic mgr:mgrpw')
+
+Let's do a couple of views. browser is already authenticated. Asssure the
+parameters in the GET match the names in the doResponse method. Default values
+are OK, and probably a good idea.
+ >>> browser.open('/sum?a=5')
+
+We are expecting something that looks like '{"a":5,"sum":5,"b":0}'
+ >>> '"a"' in browser.contents
+ True
+ >>> '"b"' in browser.contents
+ True
+ >>> '"sum"' in browser.contents
+ True
+ >>> browser.contents.count('5') == 2
+ True
+
+This request should get something that looks like '{"a":5,"sum":15,"b":10}'
+ >>> browser.open('/sum?a=5&b=10')
+ >>> browser.contents.count('15') == 1
+ True
+
+Let's see if the error handling works. We should get an HTTP error and something
+like '{"a":"zzz5","b":"10","error":"bad params"}
+ >>> browser.handleErrors = True
+ >>> browser.open('/sum?a=zzz5&b=10')
+ Traceback (most recent call last):
+ ...
+ HTTPError: HTTP Error 500: Internal Server Error
+ >>> 'zzz5' in browser.contents
+ True
+ >>> 'bad params' in browser.contents
+ True
+
Modified: z3/jsonserver/trunk/__init__.py
==============================================================================
--- z3/jsonserver/trunk/__init__.py (original)
+++ z3/jsonserver/trunk/__init__.py Sat Oct 7 23:20:46 2006
@@ -1 +1,2 @@
#python package
+from jsonrpc import MethodPublisher, JSONView
Added: z3/jsonserver/trunk/ftests.py
==============================================================================
--- (empty file)
+++ z3/jsonserver/trunk/ftests.py Sat Oct 7 23:20:46 2006
@@ -0,0 +1,57 @@
+##############################################################################
+#
+# Copyright (c) 2004 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.
+#
+##############################################################################
+"""Functional tests for JSON Views
+Original file z.a.publisher.xmlrpc/ftests.py
+$Id: ftests.py 29787 2005-04-01 16:41:05Z srichter $
+Mod by jmw 7 Oct 06 for JSON Views
+"""
+import zope.interface
+import zope.app.folder.folder
+import zope.publisher.interfaces.xmlrpc
+from zope.app.testing import ztapi, functional, setup
+
+def setUp(test):
+ setup.setUpTestAsModule(test, 'jsonserver.JSONViews')
+
+def tearDown(test):
+ # clean up the views we registered:
+
+ # we use the fact that registering None unregisters whatever is
+ # registered. We can't use an unregistration call because that
+ # requires the object that was registered and we don't have that handy.
+ # (OK, we could get it if we want. Maybe later.)
+
+ ztapi.provideView(zope.app.folder.folder.IFolder,
+ zope.publisher.interfaces.IRequest,
+ zope.interface,
+ 'folderlist',
+ None,
+ )
+
+ ztapi.provideView(zope.app.folder.folder.IFolder,
+ zope.publisher.interfaces.xmlrpc.IXMLRPCRequest,
+ zope.interface,
+ 'sum',
+ None,
+ )
+
+ setup.tearDownTestAsModule(test)
+
+def test_suite():
+ return functional.FunctionalDocFileSuite(
+ 'JSONViews.txt', setUp=setUp, tearDown=tearDown)
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main(defaultTest='test_suite')
Modified: z3/jsonserver/trunk/interfaces.py
==============================================================================
--- z3/jsonserver/trunk/interfaces.py (original)
+++ z3/jsonserver/trunk/interfaces.py Sat Oct 7 23:20:46 2006
@@ -23,12 +23,15 @@
from zope.publisher.interfaces.http import IHTTPApplicationRequest,\
IHTTPCredentials
from zope.interface import Interface
-from zope.component.interfaces import IView, IPresentation
+from zope.component.interfaces import IView
from zope.interface import Attribute
from zope.app.publisher.xmlrpc import IMethodPublisher
from zope.publisher.interfaces.xmlrpc import IXMLRPCPublication
from zope.app.publication.interfaces import IRequestFactory
-from zope.publisher.interfaces.browser import IDefaultBrowserLayer
+from zope.publisher.interfaces.browser import IDefaultBrowserLayer, \
+ IBrowserPage
+from zope.schema.interfaces import TextLine
+
class IJSONRPCRequestFactory(IRequestFactory):
"""Browser request factory"""
@@ -78,3 +81,12 @@
"""Premarshaller to remove security proxies"""
def __call__():
"""return the object without proxies"""
+
+class IJSONView(IBrowserPage):
+ """A view that is a JSON representation of an object"""
+ contentType = TextLine(title=u"content-type", default=u"application/json")
+ def doResponse():
+ """return the list or dict that is response for this view"""
+ def doCacheControl():
+ """set any cache headers that may be needed. Default sends 'no-cache'
+ to KHTML browsers. May be extended/overridden in subclasses"""
Modified: z3/jsonserver/trunk/jsonrpc.py
==============================================================================
--- z3/jsonserver/trunk/jsonrpc.py (original)
+++ z3/jsonserver/trunk/jsonrpc.py Sat Oct 7 23:20:46 2006
@@ -22,12 +22,14 @@
#2006-03-09 enabled gzip compression for large responses
#2006-05-10 removed gzip compression and (prematurely) enabled json-rpc 1.1 jmw
#2006-06-19 updated with ctheune's xmlrpc solution for removing proxies jmw
+#2006-09-27 added JSONView class
__docformat__ = 'restructuredtext'
from zope.app.publication.http import BaseHTTPPublication
from interfaces import IMethodPublisher, IJSONRPCView, IJSONRPCPublisher,\
- IJSONRPCRequest, IJSONReader, IJSONWriter, IJSONRPCPremarshaller
+ IJSONRPCRequest, IJSONReader, IJSONWriter, IJSONRPCPremarshaller, \
+ IJSONView
from zope.interface import implements
#from zope.publisher.http import IResult
from zope.location.location import Location
@@ -36,8 +38,11 @@
from zope.publisher.browser import BrowserRequest
from zope.security.proxy import isinstance
from zope.publisher.interfaces.browser import IBrowserRequest
+
from zope.publisher.interfaces.browser import IBrowserApplicationRequest
from zope.component import getUtility
+from zope.publisher.browser import BrowserPage
+
try:
from cStringIO import StringIO
except ImportError:
@@ -50,6 +55,8 @@
keyword_key = "pythonKwMaRkEr"
+json_charsets = ('utf-8','utf-16', 'utf-32')
+
#writeProfileData transcribes reads and writes files in the zope3
# instance directory for profiling use.
# profiledata.py has response dicts that can be read as python
@@ -208,6 +215,9 @@
def _prepareResult(self,result):
#we've asked json to return unicode; result should be unicode
encoding = getCharsetUsingRequest(self._request) or 'utf-8'
+ enc = encoding.lower()
+ if not enc in json_charsets:
+ encoding = 'utf-8'
#at outgoing boundary; encode it.
if isinstance(result,unicode):
body = result.encode(encoding)
@@ -268,7 +278,7 @@
return map(premarshal, self.data)
def premarshal(data):
- """Premarshal data before handing it to xmlrpclib for marhalling
+ """Premarshal data before handing it to JSON writer for marshalling
The initial purpose of this function is to remove security proxies
without resorting to removeSecurityProxy. This way, we can avoid
@@ -303,6 +313,69 @@
#return premarshaller(data)
#return data
+class JSONView(BrowserPage):
+ """This is a base view class for 'ordinary' JSON methods.
+ JSONViews are accessible by ordinary URLs and HTTP GETs.
+ """
+ implements(IJSONView)
+ contentType = 'application/json'
+
+ def doResponse(self, *args, **kw):
+ """return the python list or dict that will be the body of the response.
+ This needs to be overridden in subclasses"""
+ raise NotImplementedError("Subclasses should override doResponse to "
+ "provide a response body")
+ def doCacheControl(self):
+ """ at the moment, KHTML-based browsers do not handle cached JSON data
+ properly. This may be Dojo-specific, and may be only necessary for
+ a short time until Konq and Safari behave like other browsers in this
+ respect.
+ Default here is to send 'no-cache' header to KHTML browsers.
+ For other user agents, a 1-hour public cache is specified.
+
+ May be overridden/extended in subclasses.
+ """
+ agent = self.request.get('HTTP_USER_AGENT','')
+ response = self.request.response
+ if 'KHTML' in agent:
+ response.setHeader('cache-control','no-cache')
+ else:
+ response.setHeader('cache-control',
+ 'public, must-revalidate, max-age=3600')
+
+ def __call__(self, *args, **kw):
+ """the doResponse method is called.
+
+ First, anything that matches the method signature in request.form is
+ put in the method's **kw.
+
+ After call, the response is JSONized and sent out with appropriate
+ encoding.
+
+ """
+ request = self.request
+ meth = self.doResponse
+ #introspect the method and set kw params if the arg is in request.form
+ params = meth.im_func.func_code.co_varnames[1:]
+ for key in request.form.keys():
+ if key in params:
+ kw[str(key)] = request.form.get(key)
+ resp = premarshal(self.doResponse(*args,**kw))
+
+ if not isinstance(resp,dict) and not isinstance(resp,list):
+ raise ValueError("JSON responses must be dicts or lists")
+
+ self.doCacheControl()
+
+ encoding = getCharsetUsingRequest(self.request)
+ enc = encoding.lower()
+ if not enc in json_charsets:
+ #we'll allow utf-32, utf-16 or utf-8; if not specified, use utf-8
+ enc = 'utf-8'
+ request.response.setHeader('content-type','%s;charset=%s' % (self.contentType,enc))
+ json = getUtility(IJSONWriter)
+ s = json.write(resp).encode(enc)
+ return s
class JSONRPCView(object):
"""A base JSON-RPC view that can be used as mix-in for JSON-RPC views.
From jwashin at codespeak.net Sun Oct 8 16:09:02 2006
From: jwashin at codespeak.net (jwashin at codespeak.net)
Date: Sun, 8 Oct 2006 16:09:02 +0200 (CEST)
Subject: [z3-checkins] r33001 - in z3/jsonserver/trunk: . tests
Message-ID: <20061008140902.B660710072@code0.codespeak.net>
Author: jwashin
Date: Sun Oct 8 16:08:59 2006
New Revision: 33001
Added:
z3/jsonserver/trunk/tests/test_sum_form.pt
Modified:
z3/jsonserver/trunk/JSONViews.txt
z3/jsonserver/trunk/ftests.py
Log:
added a POST test to JSONViews.txt
Modified: z3/jsonserver/trunk/JSONViews.txt
==============================================================================
--- z3/jsonserver/trunk/JSONViews.txt (original)
+++ z3/jsonserver/trunk/JSONViews.txt Sun Oct 8 16:08:59 2006
@@ -10,8 +10,8 @@
We'll follow the xmlrpc README to demonstrate.
First, write a view class, descended from JSONView. Whatever is returned in the
-doResponse method is what will be sent as a response. The usual view instance
-variables context and request are available.
+doResponse method is what will be sent as a response. The usual view instance
+variables, context and request, are available.
>>> from jsonserver import JSONView
>>> class FolderListing(JSONView):
@@ -39,27 +39,21 @@
...
... """)
-Exactly like the xmlrpc example, we add some items to the root folder.
-
- >>> print http(r"""
- ... POST /@@contents.html HTTP/1.1
- ... Authorization: Basic bWdyOm1ncnB3
- ... Content-Length: 73
- ... Content-Type: application/x-www-form-urlencoded
- ...
- ... type_name=BrowserAdd__zope.app.folder.folder.Folder&new_value=f1""")
- HTTP/1.1 303 See Other
- ...
+Let's set up a browser.
+ >>> from zope.testbrowser.testing import Browser
+ >>> browser = Browser('http://localhost/@@/testbrowser/simple.html')
+ >>> #N.B.,this was how I figured out the need for the IJSONWriter utility...
+ >>> #browser.handleErrors = False
+ >>> browser.addHeader('Authorization','Basic mgr:mgrpw')
- >>> print http(r"""
- ... POST /@@contents.html HTTP/1.1
- ... Authorization: Basic bWdyOm1ncnB3
- ... Content-Length: 73
- ... Content-Type: application/x-www-form-urlencoded
- ...
- ... type_name=BrowserAdd__zope.app.folder.folder.Folder&new_value=f2""")
- HTTP/1.1 303 See Other
- ...
+Almost exactly like the xmlrpc example, we add some items to the root folder.
+ >>> typeName = "BrowserAdd__zope.app.folder.folder.Folder"
+ >>> newValue = 'f1'
+ >>> browser.open('/@@contents.html?type_name=%s&new_value=%s' % (typeName,
+ ... newValue))
+ >>> newValue = 'f2'
+ >>> browser.open('/@@contents.html?type_name=%s&new_value=%s' % (typeName,
+ ... newValue))
Before we can JSONView, there needs to be an IJSONWriter utility. Here's one.
>>> import zope.component
@@ -67,11 +61,8 @@
>>> from jsonserver.interfaces import IJSONWriter
>>> zope.component.provideUtility(jsoncomponent.JSONWriter(),IJSONWriter)
-Let's set up a browser.
- >>> from zope.testbrowser.testing import Browser
+We reset the browser just for fun...
>>> browser = Browser('http://localhost/@@/testbrowser/simple.html')
- >>> #this was how I figured out the need for the IJSONWriter utility...
- >>> #browser.handleErrors = False
Now, we can call our new JSONView and get a response:
>>> browser.open('/folderlist')
@@ -91,8 +82,8 @@
Pretty cool, yes? This is much smaller than a similar xmlrpc response.
But what about parameters? Let's create another class with some error handling.
-There's no real standard on this, so you may need to commune with the cliemt
-implementation to see how to handle errors.
+There's no real standard on error handling in JSON, so you may need to commune
+with the client implementation to see how to handle errors.
>>> import decimal
>>> class FolderStupidSum(JSONView):
... """return two values and their sum"""
@@ -105,7 +96,11 @@
... return {'error':'bad params','a':a, 'b':b}
... return {'a':a,'b':b,'sum':a+b}
-and register it.
+First, a hack to make the page template file findable, then register the sum
+page and a sum_form page.
+ >>> import os
+ >>> loc = os.path.dirname(__file__)
+ >>> pt = os.path.join(loc,'tests','test_sum_form.pt')
>>> from zope.configuration import xmlconfig
>>> ignored = xmlconfig.string("""
...
+ ...
...
- ... """)
+ ... """ % pt)
Start a new browser.
>>> browser = Browser('http://localhost/@@/testbrowser/simple.html')
>>> #browser.handleErrors = False
>>> browser.addHeader('Authorization','Basic mgr:mgrpw')
-Let's do a couple of views. browser is already authenticated. Asssure the
+Let's do a couple of views. Browser is already authenticated. Asssure the
parameters in the GET match the names in the doResponse method. Default values
are OK, and probably a good idea.
>>> browser.open('/sum?a=5')
@@ -160,3 +161,15 @@
>>> 'bad params' in browser.contents
True
+Now, let's open the test form in the browser so that we can see if POST works.
+Ordinarily, a POST for a JSONView would be done in an XHR, but we are just
+testing functionality here.
+ >>> browser.open('/sum_form.html')
+ >>> a = browser.getControl(name='a')
+ >>> b = browser.getControl(name='b')
+ >>> a.value="34"
+ >>> b.value="66"
+ >>> submit = browser.getControl(name="submit")
+ >>> submit.click()
+ >>> '"sum":100' in browser.contents
+ True
Modified: z3/jsonserver/trunk/ftests.py
==============================================================================
--- z3/jsonserver/trunk/ftests.py (original)
+++ z3/jsonserver/trunk/ftests.py Sun Oct 8 16:08:59 2006
@@ -18,7 +18,7 @@
"""
import zope.interface
import zope.app.folder.folder
-import zope.publisher.interfaces.xmlrpc
+import zope.publisher.interfaces.browser
from zope.app.testing import ztapi, functional, setup
def setUp(test):
@@ -33,19 +33,23 @@
# (OK, we could get it if we want. Maybe later.)
ztapi.provideView(zope.app.folder.folder.IFolder,
- zope.publisher.interfaces.IRequest,
+ zope.publisher.interfaces.browser.IBrowserRequest,
zope.interface,
'folderlist',
None,
)
-
ztapi.provideView(zope.app.folder.folder.IFolder,
- zope.publisher.interfaces.xmlrpc.IXMLRPCRequest,
+ zope.publisher.interfaces.browser.IBrowserRequest,
zope.interface,
'sum',
None,
)
-
+ ztapi.provideView(zope.app.folder.folder.IFolder,
+ zope.publisher.interfaces.browser.IBrowserRequest,
+ zope.interface,
+ 'sum_form.html',
+ None,
+ )
setup.tearDownTestAsModule(test)
def test_suite():
Added: z3/jsonserver/trunk/tests/test_sum_form.pt
==============================================================================
--- (empty file)
+++ z3/jsonserver/trunk/tests/test_sum_form.pt Sun Oct 8 16:08:59 2006
@@ -0,0 +1,14 @@
+
+
+ This is a form for testing stupid sums
+
+
+
Stupid form test
+
Enter two values, and submit
+
+
+
From jwashin at codespeak.net Sun Oct 8 20:18:07 2006
From: jwashin at codespeak.net (jwashin at codespeak.net)
Date: Sun, 8 Oct 2006 20:18:07 +0200 (CEST)
Subject: [z3-checkins] r33005 - z3/jsonserver/trunk
Message-ID: <20061008181807.ED81110068@code0.codespeak.net>
Author: jwashin
Date: Sun Oct 8 20:18:05 2006
New Revision: 33005
Modified:
z3/jsonserver/trunk/JSONViews.txt
z3/jsonserver/trunk/jsonrpc.py
Log:
a bit more error handling for JSONView
Modified: z3/jsonserver/trunk/JSONViews.txt
==============================================================================
--- z3/jsonserver/trunk/JSONViews.txt (original)
+++ z3/jsonserver/trunk/JSONViews.txt Sun Oct 8 20:18:05 2006
@@ -81,13 +81,14 @@
Pretty cool, yes? This is much smaller than a similar xmlrpc response.
-But what about parameters? Let's create another class with some error handling.
-There's no real standard on error handling in JSON, so you may need to commune
-with the client implementation to see how to handle errors.
+But what about parameters? Let's create another class with a parameter and an
+optional parameter. We'll also do some local error handling. There's no real
+standard on error handling in JSON, so you may need to commune with the client
+implementation to see how to handle errors.
>>> import decimal
>>> class FolderStupidSum(JSONView):
... """return two values and their sum"""
- ... def doResponse(self, a=0, b=0):
+ ... def doResponse(self, a, b=0):
... try:
... a = decimal.Decimal(a)
... b = decimal.Decimal(b)
@@ -131,7 +132,7 @@
Let's do a couple of views. Browser is already authenticated. Asssure the
parameters in the GET match the names in the doResponse method. Default values
-are OK, and probably a good idea.
+in the method signature are OK, and probably a good idea.
>>> browser.open('/sum?a=5')
We are expecting something that looks like '{"a":5,"sum":5,"b":0}'
@@ -149,8 +150,25 @@
>>> browser.contents.count('15') == 1
True
-Let's see if the error handling works. We should get an HTTP error and something
-like '{"a":"zzz5","b":"10","error":"bad params"}
+This request does not send enough parameters.
+ >>> browser.open('/sum')
+ Traceback (most recent call last):
+ ...
+ HTTPError: HTTP Error 500: Internal Server Error
+ >>> browser.contents
+ '{"error":"doResponse() takes at least 2 arguments (1 given)"}'
+
+This request also does not send enough parameters, because the parameter
+provided does not match the method signature.
+ >>> browser.open('/sum?d=20')
+ Traceback (most recent call last):
+ ...
+ HTTPError: HTTP Error 500: Internal Server Error
+ >>> browser.contents
+ '{"error":"doResponse() takes at least 2 arguments (1 given)"}'
+
+Let's see if the local error handling works. We should get an HTTP error and
+something like '{"a":"zzz5","b":"10","error":"bad params"}
>>> browser.handleErrors = True
>>> browser.open('/sum?a=zzz5&b=10')
Traceback (most recent call last):
Modified: z3/jsonserver/trunk/jsonrpc.py
==============================================================================
--- z3/jsonserver/trunk/jsonrpc.py (original)
+++ z3/jsonserver/trunk/jsonrpc.py Sun Oct 8 20:18:05 2006
@@ -360,7 +360,11 @@
for key in request.form.keys():
if key in params:
kw[str(key)] = request.form.get(key)
- resp = premarshal(self.doResponse(*args,**kw))
+ try:
+ resp = premarshal(self.doResponse(*args,**kw))
+ except TypeError, err:
+ request.response.setStatus('500')
+ resp = {'error':'%s' % err}
if not isinstance(resp,dict) and not isinstance(resp,list):
raise ValueError("JSON responses must be dicts or lists")
From jwashin at codespeak.net Sun Oct 8 20:24:15 2006
From: jwashin at codespeak.net (jwashin at codespeak.net)
Date: Sun, 8 Oct 2006 20:24:15 +0200 (CEST)
Subject: [z3-checkins] r33007 - z3/jsonserver/trunk
Message-ID: <20061008182415.2283210068@code0.codespeak.net>
Author: jwashin
Date: Sun Oct 8 20:24:13 2006
New Revision: 33007
Modified:
z3/jsonserver/trunk/JSONViews.txt
Log:
Oops python error msgs are not always in English.
Modified: z3/jsonserver/trunk/JSONViews.txt
==============================================================================
--- z3/jsonserver/trunk/JSONViews.txt (original)
+++ z3/jsonserver/trunk/JSONViews.txt Sun Oct 8 20:24:13 2006
@@ -150,22 +150,23 @@
>>> browser.contents.count('15') == 1
True
-This request does not send enough parameters.
+This request does not send enough parameters. The error I get is
+'{"error":"doResponse() takes at least 2 arguments (1 given)"}'
>>> browser.open('/sum')
Traceback (most recent call last):
...
HTTPError: HTTP Error 500: Internal Server Error
- >>> browser.contents
- '{"error":"doResponse() takes at least 2 arguments (1 given)"}'
+ >>> 'error' in browser.contents
+ True
This request also does not send enough parameters, because the parameter
-provided does not match the method signature.
+provided does not match the method signature. Same error as above.
>>> browser.open('/sum?d=20')
Traceback (most recent call last):
...
HTTPError: HTTP Error 500: Internal Server Error
- >>> browser.contents
- '{"error":"doResponse() takes at least 2 arguments (1 given)"}'
+ >>> 'error' in browser.contents
+ True
Let's see if the local error handling works. We should get an HTTP error and
something like '{"a":"zzz5","b":"10","error":"bad params"}
From jwashin at codespeak.net Sun Oct 8 20:58:05 2006
From: jwashin at codespeak.net (jwashin at codespeak.net)
Date: Sun, 8 Oct 2006 20:58:05 +0200 (CEST)
Subject: [z3-checkins] r33011 - z3/jsonserver/trunk
Message-ID: <20061008185805.4708810068@code0.codespeak.net>
Author: jwashin
Date: Sun Oct 8 20:58:03 2006
New Revision: 33011
Modified:
z3/jsonserver/trunk/JSONViews.txt
Log:
small typo fixed in JSONViews.txt
Modified: z3/jsonserver/trunk/JSONViews.txt
==============================================================================
--- z3/jsonserver/trunk/JSONViews.txt (original)
+++ z3/jsonserver/trunk/JSONViews.txt Sun Oct 8 20:58:03 2006
@@ -2,7 +2,7 @@
JSON-formatted data from the server, for example, if you have a javascript
library that employs JSON GETs, e.g., Dojo.
-A JSON View is simply a page that, instead of HTML, is a JSON.representation of
+A JSON View is simply a page that, instead of HTML, is a JSON representation of
some data.
To do this you need to create a view class and register it in ZCML.
From cabraham at codespeak.net Mon Oct 9 22:09:20 2006
From: cabraham at codespeak.net (cabraham at codespeak.net)
Date: Mon, 9 Oct 2006 22:09:20 +0200 (CEST)
Subject: [z3-checkins] r33076 -
z3/deliverance/branches/packaged/deliverance/tests
Message-ID: <20061009200920.E439B100C8@code0.codespeak.net>
Author: cabraham
Date: Mon Oct 9 22:08:59 2006
New Revision: 33076
Removed:
z3/deliverance/branches/packaged/deliverance/tests/
Log:
deletes old tests
From ltucker at codespeak.net Mon Oct 9 22:04:02 2006
From: ltucker at codespeak.net (ltucker at codespeak.net)
Date: Mon, 9 Oct 2006 22:04:02 +0200 (CEST)
Subject: [z3-checkins] r33075 - in
z3/deliverance/branches/packaged/deliverance: . test-data
test-data/nabuur test-data/nycsr test-data/static
Message-ID: <20061009200402.ED901100CC@code0.codespeak.net>
Author: ltucker
Date: Mon Oct 9 22:03:44 2006
New Revision: 33075
Added:
z3/deliverance/branches/packaged/deliverance/handtransform.py
z3/deliverance/branches/packaged/deliverance/htmlserialize.py
z3/deliverance/branches/packaged/deliverance/interpreter.py
z3/deliverance/branches/packaged/deliverance/test-data/
z3/deliverance/branches/packaged/deliverance/test-data/nabuur/
z3/deliverance/branches/packaged/deliverance/test-data/nabuur/nabuur.html
z3/deliverance/branches/packaged/deliverance/test-data/nabuur/nabuur.theme
z3/deliverance/branches/packaged/deliverance/test-data/nabuur/rules.xml
z3/deliverance/branches/packaged/deliverance/test-data/nabuur/standardrules.xml
z3/deliverance/branches/packaged/deliverance/test-data/nycsr/
z3/deliverance/branches/packaged/deliverance/test-data/nycsr/nycsr.theme
z3/deliverance/branches/packaged/deliverance/test-data/nycsr/nycsr.xml
z3/deliverance/branches/packaged/deliverance/test-data/nycsr/nycsr_expected.html
z3/deliverance/branches/packaged/deliverance/test-data/nycsr/openplans.html
z3/deliverance/branches/packaged/deliverance/test-data/nycsr/standardrules.xml
z3/deliverance/branches/packaged/deliverance/test-data/static/
z3/deliverance/branches/packaged/deliverance/test-data/static/example.html
z3/deliverance/branches/packaged/deliverance/test-data/static/example_expected.html
z3/deliverance/branches/packaged/deliverance/test-data/static/rules.xml
z3/deliverance/branches/packaged/deliverance/test-data/static/standardrules.xml
z3/deliverance/branches/packaged/deliverance/test-data/static/standardrules2.xml
z3/deliverance/branches/packaged/deliverance/test-data/static/text-rules.xml
z3/deliverance/branches/packaged/deliverance/test-data/static/texttest_expected.html
z3/deliverance/branches/packaged/deliverance/test-data/static/theme.html
z3/deliverance/branches/packaged/deliverance/test-data/static/xinclude_expected.html
z3/deliverance/branches/packaged/deliverance/test-data/static/xinclude_rules.xml
z3/deliverance/branches/packaged/deliverance/test-data/static/xinclude_theme.html
z3/deliverance/branches/packaged/deliverance/test-data/test_append.xml
z3/deliverance/branches/packaged/deliverance/test-data/test_appendorreplace.xml
z3/deliverance/branches/packaged/deliverance/test-data/test_baserules.xml
z3/deliverance/branches/packaged/deliverance/test-data/test_comments.xml
z3/deliverance/branches/packaged/deliverance/test-data/test_copy.xml
z3/deliverance/branches/packaged/deliverance/test-data/test_prepend.xml
z3/deliverance/branches/packaged/deliverance/test-data/test_replace.xml
z3/deliverance/branches/packaged/deliverance/test-data/test_submatch.xml
z3/deliverance/branches/packaged/deliverance/test-data/test_url.xml
z3/deliverance/branches/packaged/deliverance/test_wsgi.py
z3/deliverance/branches/packaged/deliverance/tests.py
z3/deliverance/branches/packaged/deliverance/utils.py
z3/deliverance/branches/packaged/deliverance/xinclude.py
z3/deliverance/branches/packaged/deliverance/xslt.py
Removed:
z3/deliverance/branches/packaged/deliverance/ThemedHTTPServer.py
z3/deliverance/branches/packaged/deliverance/main.py
z3/deliverance/branches/packaged/deliverance/mpfilter.py
z3/deliverance/branches/packaged/deliverance/renderer.xsl
z3/deliverance/branches/packaged/deliverance/themecompiler.xsl
Modified:
z3/deliverance/branches/packaged/deliverance/wsgifilter.py
Log:
parallel python and xslt renderers, tests, wsgi filter
Deleted: /z3/deliverance/branches/packaged/deliverance/ThemedHTTPServer.py
==============================================================================
--- /z3/deliverance/branches/packaged/deliverance/ThemedHTTPServer.py Mon Oct 9 22:03:44 2006
+++ (empty file)
@@ -1,92 +0,0 @@
-"""Simple HTTP Server.
-
-This module builds on BaseHTTPServer by implementing the standard GET
-and HEAD requests in a fairly straightforward manner.
-
-"""
-
-
-__version__ = "0.6"
-
-__all__ = ["ThemedHTTPRequestHandler"]
-
-import os
-import mimetypes
-import BaseHTTPServer
-import SimpleHTTPServer
-from StringIO import StringIO
-
-from deliverance import AppMap
-appmap = AppMap()
-
-
-class ThemedHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
-
- def send_head(self):
- """Common code for GET and HEAD commands.
-
- This sends the response code and MIME headers.
-
- Return value is either a file object (which has to be copied
- to the outputfile by the caller unless the command was HEAD,
- and must be closed by the caller under all circumstances), or
- None, in which case the caller has nothing further to do.
-
- """
- path = self.translate_path(self.path)
- f = None
- if os.path.isdir(path):
- for index in "index.html", "index.htm":
- index = os.path.join(path, index)
- if os.path.exists(index):
- path = index
- break
- else:
- return self.list_directory(path)
- ctype = self.guess_type(path)
- try:
- # Always read in binary mode. Opening files in text mode may cause
- # newline translations, making the actual size of the content
- # transmitted *less* than the content-length!
-
- # This is where we apply theming
-
- # First check to see if there is a query string. If so, presume that
- # to mean they want the source version.
- qs = len(self.path.split("?"))
- if ctype == "text/html" and qs == 99:
- print "Applying theme to", path
- xmlstring = open(path, "r").read()
- response = appmap.publish(xmlstring)
- f = StringIO(response)
- responsesize = str(len(xmlstring))
- else:
- f = open(path, 'rb')
- responsesize = str(os.fstat(f.fileno())[6])
- except IOError:
- self.send_error(404, "File not found")
- return None
- self.send_response(200)
- self.send_header("Content-type", ctype)
- self.send_header("Content-Length", responsesize)
- self.end_headers()
- return f
-
- extensions_map = mimetypes.types_map.copy()
- extensions_map.update({
- '': 'application/octet-stream', # Default
- '.py': 'text/plain',
- '.c': 'text/plain',
- '.h': 'text/plain',
- '.ico': 'image/x-icon',
- })
-
-
-
-def test(HandlerClass = ThemedHTTPRequestHandler,
- ServerClass = BaseHTTPServer.HTTPServer):
- BaseHTTPServer.test(HandlerClass, ServerClass)
-
-
-if __name__ == '__main__':
- test()
Added: z3/deliverance/branches/packaged/deliverance/handtransform.py
==============================================================================
--- (empty file)
+++ z3/deliverance/branches/packaged/deliverance/handtransform.py Mon Oct 9 22:03:44 2006
@@ -0,0 +1,124 @@
+import sys
+from lxml import etree
+from htmlserialize import tostring
+import urllib
+from interpreter import Renderer as PythonRenderer
+from xslt import Renderer as XSLTRenderer
+from optparse import OptionParser
+import re
+import os
+
+"""
+Command line utility to run a deliverance transform
+given the urls of the rules, theme and content.
+
+
+themeurl, rulesfile and baseurl can be rolled into a file specified with -f
+it should contain an element like
+
+
+
+
+"""
+
+
+DEFAULT_BASE_URL = "http://www.example.com"
+
+
+def grab_url(url):
+ f = urllib.urlopen(url)
+ data = f.read()
+ f.close()
+ return data
+
+def do_transform(renderer_type, theme_url, base_url, rules_url, content_url):
+ rules = etree.XML(grab_url(rules_url))
+ theme = etree.HTML(grab_url(theme_url))
+ content = etree.HTML(grab_url(content_url))
+
+ def reference_resolver(href, parse, encoding=None):
+ if not href.startswith('/'):
+ href = os.path.join(os.path.dirname(rules_url),href)
+ text = grab_url(href)
+ if parse == "xml":
+ return etree.XML(text)
+ elif encoding:
+ return text.decode(encoding)
+
+ renderer = None
+ if renderer_type == 'xslt':
+ renderer = XSLTRenderer(theme,base_url,rules,reference_resolver)
+ elif renderer_type == 'py':
+ renderer = PythonRenderer(theme,base_url,rules,reference_resolver)
+ else:
+ print "Unknown renderer type '" + renderer_type + "'"
+ return etree.Element("error")
+
+ return renderer.render(content)
+
+
+def parse_blend_file(filename):
+ b = etree.XML(open(filename).read())
+ return b.get('theme'),b.get('baseurl'),b.get('rules')
+
+
+def die(message,parser):
+ print message
+ parser.print_usage()
+ sys.exit(0)
+
+if __name__ == '__main__':
+
+
+ usage = "usage: %prog [options] "
+ parser = OptionParser(usage=usage)
+ parser.add_option("-t","--theme",dest="theme_url",help="url of theme html")
+ parser.add_option("-b","--baseurl",dest="base_url",
+ help="relative urls in the theme will be made absolute relative to this url [default %default]",
+ default=DEFAULT_BASE_URL)
+ parser.add_option("-r","--rules",dest="rules_file",
+ help="path to file containing the deliverance rules to apply")
+ parser.add_option("-f","--from-file",dest="blend_file",
+ help="take theme, baseurl and rules parameters from the referenced file")
+ parser.add_option("-R","--renderer",dest="renderer_type",
+ help="(xslt|py) [default %default]", default="xslt", choices=['xslt','py'])
+
+ (options,args) = parser.parse_args()
+
+ if len(args) == 0:
+ die("no content url specified.",parser)
+
+ content_url = args[0]
+ theme_url = None
+ base_url = None
+ rules_file = None
+
+
+ if options.blend_file:
+ if (options.rules_file or options.theme_url or options.base_url != DEFAULT_BASE_URL):
+ die("cannot specify base url, rules file or theme url when taking parameters from file.",parser)
+
+ try:
+ theme_url,base_url,rules_file = parse_blend_file(options.blend_file)
+
+ except Exception,message:
+ die(message,parser)
+
+ else:
+ theme_url = options.theme_url
+ rules_file = options.rules_file
+ base_url = options.base_url
+
+
+ if theme_url is None:
+ die("no theme url specified.",parser)
+
+ if rules_file is None:
+ die("no rules file specified.",parser)
+
+ if base_url is None:
+ die("no base url specified",parser)
+
+
+ print tostring(do_transform(options.renderer_type,theme_url,base_url,rules_file,content_url))
+
Added: z3/deliverance/branches/packaged/deliverance/htmlserialize.py
==============================================================================
--- (empty file)
+++ z3/deliverance/branches/packaged/deliverance/htmlserialize.py Mon Oct 9 22:03:44 2006
@@ -0,0 +1,39 @@
+from lxml import etree
+
+
+html_xsl = """
+
+
+
+
+
+
+"""
+
+# TODO: this should do real formatting
+pretty_html_xsl = """
+
+
+
+
+
+
+"""
+
+html_transform = etree.XSLT(etree.XML(html_xsl))
+pretty_html_transform = etree.XSLT(etree.XML(pretty_html_xsl))
+
+
+#
+# creates an HTML string representation of the document given
+#
+# note: this will create a meta http-equiv="Content" tag in the head
+# and may replace any that are present
+#
+def tostring(doc,pretty = False):
+ if pretty:
+ return str(pretty_html_transform(doc))
+ else:
+ return str(html_transform(doc))
+
+
Added: z3/deliverance/branches/packaged/deliverance/interpreter.py
==============================================================================
--- (empty file)
+++ z3/deliverance/branches/packaged/deliverance/interpreter.py Mon Oct 9 22:03:44 2006
@@ -0,0 +1,263 @@
+from lxml import etree
+import xinclude
+import copy
+import utils
+from utils import RuleSyntaxError
+from utils import RendererBase
+
+class Renderer(RendererBase):
+ """
+ implements a deliverance renderer programmatically using
+ lxml api. The rules, theme and content are all processed at
+ render time.
+ """
+
+ def __init__(self, theme, theme_uri, rules, reference_resolver=None):
+ self.theme = self.fixup_links(theme, theme_uri)
+ self.remove_http_equiv_metas(self.theme)
+ self.rules = rules
+ # perform xincludes on the rules
+ if reference_resolver:
+ xinclude.include(self.rules, loader=reference_resolver)
+
+
+ def render(self, content):
+ result = copy.deepcopy(self.theme)
+ input = copy.deepcopy(content)
+ self.remove_http_equiv_metas(input)
+ self.apply_rules(self.rules,result,input)
+ return result
+
+
+ def apply_rules(self,rules,theme,content):
+ for rule in rules:
+ self.apply_rule(rule,theme,content)
+
+
+ def apply_rule(self,rule,theme,content):
+ if rule.tag == self.APPEND_RULE_TAG:
+ self.apply_append(rule,theme,content)
+ elif rule.tag == self.PREPEND_RULE_TAG:
+ self.apply_prepend(rule,theme,content)
+ elif rule.tag == self.REPLACE_RULE_TAG:
+ self.apply_replace(rule,theme,content)
+ elif rule.tag == self.COPY_RULE_TAG:
+ self.apply_copy(rule,theme,content)
+ elif rule.tag == self.APPEND_OR_REPLACE_RULE_TAG:
+ self.apply_append_or_replace(rule,theme,content)
+ elif rule.tag == self.SUBRULES_TAG:
+ self.apply_rules(rule,theme,content)
+ elif rule.tag is etree.Comment:
+ pass
+ else:
+ raise RuleSyntaxError(
+ "Rule %s (%s) not understood" % (
+ rule.tag, etree.tostring(rule)))
+
+ def apply_append(self,rule,theme,content):
+ theme_el = self.get_theme_el(rule,theme)
+ if theme_el is None:
+ return
+
+ content_els = copy.deepcopy(content.xpath(rule.attrib[self.RULE_CONTENT_KEY]))
+
+ if (len(content_els) == 0):
+ return
+
+ non_text_els = self.elements_in(content_els)
+ self.strip_tails(non_text_els)
+
+ # the xpath may return a mixture of strings and elements, handle strings
+ # by attaching them to the proper element
+ if (type(content_els[0]) is type(str())):
+ # if the element we're appending to has children, the text is
+ # appended to the tail of the last child.
+ if len(theme_el):
+ if theme_el[-1].tail:
+ theme_el[-1].tail += content_els[0]
+ else:
+ theme_el[-1].tail = content_els[0]
+ # otherwise, the text is appeded to the text attribute of the
+ # element we're appending to
+ else:
+ if theme_el.text:
+ theme_el.text += content_els[0]
+ else:
+ theme_el.text = content_els[0]
+
+ self.attach_tails(content_els)
+ theme_el.extend(non_text_els)
+
+ def apply_prepend(self,rule,theme,content):
+ theme_el = self.get_theme_el(rule,theme)
+ if theme_el is None:
+ return
+
+ content_els = copy.deepcopy(content.xpath(rule.attrib[self.RULE_CONTENT_KEY]))
+
+ if (len(content_els) == 0):
+ return
+
+ non_text_els = self.elements_in(content_els)
+
+ # if we only get some text, just tack it on and return
+ if len(non_text_els) == 0 and type(content_els[0]) is type(str()):
+ if theme_el.text:
+ theme_el.text = content_els[0] + theme_el.text
+ else:
+ theme_el.text = content_els[0]
+ return
+
+ # here we have some elements and possibly some text
+
+ self.strip_tails(non_text_els)
+
+ # the xpath may return a mixture of strings and elements, handle the
+ # first string by making it the text of the parent element. In any
+ # case if the parent element has text, we need put it after the
+ # elements we're prepending so we save it here
+ old_start_text = theme_el.text
+ if (type(content_els[0]) is type(str())):
+ theme_el.text = content_els[0]
+ else:
+ theme_el.text = None
+
+ self.attach_tails(content_els)
+ for index,el in enumerate(non_text_els):
+ theme_el.insert(index,el)
+
+ # tack on the previous text of the parent element
+ if old_start_text:
+ if (non_text_els[-1].tail):
+ non_text_els[-1].tail += old_start_text
+ else:
+ non_text_els[-1].tail = old_start_text
+
+ def apply_replace(self,rule,theme,content):
+ theme_el = self.get_theme_el(rule,theme)
+ if theme_el is None:
+ return
+
+ content_els = copy.deepcopy(content.xpath(rule.attrib[self.RULE_CONTENT_KEY]))
+
+ if len(content_els) == 0:
+ self.attach_text_to_previous(theme_el,theme_el.tail)
+ theme_el.getparent().remove(theme_el)
+ return
+
+ non_text_els = self.elements_in(content_els)
+ self.strip_tails(non_text_els)
+
+
+ # the xpath may return a mixture of strings and elements, handle strings
+ # by attaching them to the proper element
+ if (type(content_els[0]) is type(str())):
+ # text must be appended to the tail of the most recent sibling or appended
+ # to the text of the parent of the replaced element
+ self.attach_text_to_previous(theme_el,content_els[0])
+
+ if len(non_text_els) == 0:
+ self.attach_text_to_previous(theme_el,theme_el.tail)
+ theme_el.getparent().remove(theme_el)
+ return
+
+ self.attach_tails(content_els)
+
+ # this tail, if there is one, should stick around
+ preserve_tail = non_text_els[0].tail
+
+ self.replace_element(theme_el, non_text_els[0])
+ temptail = non_text_els[0].tail
+ non_text_els[0].tail = None
+ parent = non_text_els[0].getparent()
+ i = parent.index(non_text_els[0])
+ for index,cel in enumerate(non_text_els[1:]):
+ parent.insert(i + index + 1,cel)
+
+ if non_text_els[-1].tail:
+ non_text_els[-1].tail += temptail
+ else:
+ non_text_els[-1].tail = temptail
+
+ # tack in any preserved tail we stored above
+ if preserve_tail:
+ if non_text_els[0].tail:
+ non_text_els[0].tail = preserve_tail + non_text_els[0].tail
+ else:
+ non_text_els[0].tail = preserve_tail
+
+
+ def apply_copy(self,rule,theme,content):
+ theme_el = self.get_theme_el(rule,theme)
+ if theme_el is None:
+ return
+
+ content_els = copy.deepcopy(content.xpath(rule.attrib[self.RULE_CONTENT_KEY]))
+
+ if len(content_els) == 0:
+ return
+
+ non_text_els = self.elements_in(content_els)
+ self.strip_tails(non_text_els)
+ # attach any leading matched text as the text of the element
+ # we're copying into
+ if (type(content_els[0]) is type(str())):
+ theme_el.text = content_els[0]
+ # otherwise knock out any existing text
+ else:
+ theme_el.text = None
+
+ self.attach_tails(content_els)
+ theme_el[:] = non_text_els
+
+ def apply_append_or_replace(self,rule,theme,content):
+ theme_el = self.get_theme_el(rule,theme)
+ if theme_el is None:
+ return
+
+ content_xpath = rule.attrib[self.RULE_CONTENT_KEY]
+ remove_tag = self.get_tag_from_xpath(content_xpath)
+
+ if remove_tag is None:
+ self.add_to_body_start(theme,self.format_error("invalid xpath for content", rule=rule))
+ return
+
+ for el in theme_el:
+ if el.tag == remove_tag:
+ theme_el.remove(el)
+
+ content_els = copy.deepcopy(content.xpath(content_xpath))
+ self.strip_tails(content_els)
+ theme_el.extend(content_els)
+
+
+ def elements_in(self, els):
+ """
+ return a list containing elements from els which are not strings
+ """
+ return [x for x in els if type(x) is not type(str())]
+
+
+
+ def strip_tails(self, els):
+ for el in els:
+ el.tail = None
+
+
+ def attach_tails(self,els):
+ """
+ whereever an lxml element in the list is followed by
+ a string, set the tail of the lxml element to the string
+ """
+ for index,el in enumerate(els):
+ # if we run into a string after the current element,
+ # attach it to the current element as the tail
+ if (type(el) is not type(str()) and
+ index + 1 < len(els) and
+ type(els[index+1]) is type(str())):
+ el.tail = els[index+1]
+
+
+
+
+
Deleted: /z3/deliverance/branches/packaged/deliverance/main.py
==============================================================================
--- /z3/deliverance/branches/packaged/deliverance/main.py Mon Oct 9 22:03:44 2006
+++ (empty file)
@@ -1,157 +0,0 @@
-import os
-from lxml import etree
-from time import time
-from lxml.etree import Namespace, ElementBase, XMLParser
-try:
- from lxml.etree import ElementNamespaceClassLookup
-except ImportError:
- ElementNamespaceClassLookup = None
-
-nsmap = {
- "dv": "http://www.plone.org/deliverance",
- "html": "http://www.w3.org/1999/xhtml",
- "xsl": "http://www.w3.org/1999/XSL/Transform",
- "at": "http://plone.org/archetypes",
- }
-
-parser = XMLParser()
-if ElementNamespaceClassLookup is not None:
- # Earlier versions of lxml did class lookup in all
- # cases; newer versions require this explicit parser
- # setup
- lookup = ElementNamespaceClassLookup()
- parser.setElementClassLookup(lookup)
-
-class AppMap:
-
- def __init__(self, layout_dir):
-
- # Open the appmap file, make a tree, and process XIncludes
- self.module_dir = os.path.dirname(os.path.abspath(__file__))
- self.layout_dir = os.path.join(self.module_dir, layout_dir)
- layoutsfn = os.path.join(self.layout_dir, "appmap.xml")
- self.tree = etree.ElementTree(
- file=layoutsfn, parser=parser)
- self.tree.xinclude()
-
- # Make a themeprocessor to style all outgoing pages. Note that the
- # .processor attribute comes from an lxml namespace binding, meaning it is
- # defined via a custom Python class defined below (class LayoutElement)
- root = self.tree.getroot()
- layout = root.xpath("dv:layouts/dv:layout", nsmap)[0]
- #self.themeprocessor = make_processor(layout)
- self.themeprocessor = layout.processor
-
-
- def publish(self, xmlstring):
- """Given a string of XML, theme it"""
-
- resource = etree.XML(xmlstring)
- response = str(self.themeprocessor(resource))
-
- return response
-
-# The following are extensions based on lxml namespace extensions. It
-# adds Python behavior to XML nodes.
-
-class DVRuleBase(ElementBase):
-
- def getThemeNode(self):
- """Get a node in the theme doc"""
-
- # Current node is a rule, get xpath from the @theme attr
- themedoc = self.xpath("../../dv:theme", nsmap)[0][0]
- xpath = self.get("theme")
- try:
- themenode = themedoc.xpath(xpath, nsmap)[0]
- except IndexError:
- msg = "Themedoc has no node at: %s" % xpath
- print msg
- themenode = None
-
- return themenode
-
-
-class LayoutElement(ElementBase):
-
- def processor(self):
- """Make XSLT processor by changing theme based on rules"""
-
- # Apply all the rules
- for rule in self.xpath("./dv:rules/*", nsmap):
- rule.apply()
-
- # Merge applied rules into compilerdoc
- compilerroot = self.xpath("../dv:compiler/xsl:stylesheet", nsmap)[0]
- themeroot = self.xpath("dv:theme/html:html", nsmap)[0]
- target = compilerroot.xpath("xsl:template[@match='/']", nsmap)[0]
- target.append(themeroot)
-
- #print etree.tostring(compilerroot)
-
- return etree.XSLT(compilerroot)
-
- processor = property(processor)
-
-
-class RuleReplaceElement(DVRuleBase):
-
- def apply(self):
- # TODO: Someething here
- themenode = self.getThemeNode()
- if themenode is None:
- return
- del(themenode[:])
- themenode.text = None
- xslvalueof = etree.SubElement(themenode,
- "{%s}value-of" % nsmap["xsl"])
- xslvalueof.set("select", self.get("content"))
-
-
-class RuleCopyElement(DVRuleBase):
-
- def apply(self):
- themenode = self.getThemeNode()
- if themenode is None:
- return
- del(themenode[:])
- themenode.text = None
- xslvalueof = etree.SubElement(themenode,
- "{%s}apply-templates" % nsmap["xsl"])
- xslvalueof.set("select", self.get("content"))
-
-
-class RuleAppendElement(DVRuleBase):
-
- def apply(self):
- themenode = self.getThemeNode()
- if themenode is None:
- return
- xslvalueof = etree.SubElement(themenode,
- "{%s}apply-templates" % nsmap["xsl"])
- xslvalueof.set("select", self.get("content"))
-
-
-# Bind Python classes for lxml namespace support
-namespace = Namespace(nsmap['dv'])
-namespace['layout'] = LayoutElement
-namespace['replace'] = RuleReplaceElement
-namespace['copy'] = RuleCopyElement
-namespace['append'] = RuleAppendElement
-
-
-def timeit(xmlstring):
- appmap = AppMap()
- start = time()
- iters = 50
- for i in range(iters):
- result = appmap.publish(xmlstring)
- print "*** Average time:", (time() - start) / iters, " ***\n"
- print result[0:2000]
-
-def main():
- xmlstring = open("content/index.html").read()
- timeit(xmlstring)
-
-if __name__ == "__main__":
- main()
Deleted: /z3/deliverance/branches/packaged/deliverance/mpfilter.py
==============================================================================
--- /z3/deliverance/branches/packaged/deliverance/mpfilter.py Mon Oct 9 22:03:44 2006
+++ (empty file)
@@ -1,70 +0,0 @@
-"""
-Deliverance theming for mod_python filters
-
-Deliverance applies a theme to content. This mod_python module acts as an
-Apache "filter", transforming content as it passes through Apache.
-
-This module gets imported by mod_python during its startup. Thus, the
-appmap instance becomes a global, computed only once. If you need to
-recompute the theme, for example, restart the Apache.
-"""
-import time
-from cStringIO import StringIO
-
-from mod_python import apache
-from deliverance.main import AppMap
-appmap = AppMap() # Theme is generated once at module import time
-
-def outputfilter(filter):
- if not hasattr(filter.req, 'notheme'):
- # Check for a flag to not apply theme
- args = filter.req.args
- if args and args.find("notheme") > -1:
- filter.req.notheme = True
- else:
- filter.req.notheme = False
-
- try:
- streambuffer = filter.req.streambuffer
- except AttributeError:
- if filter.req.notheme:
- # pass on if no theme
- filter.pass_on()
- return
- elif not filter.req.headers_out.has_key("content-type"):
- # pass on if no content type specified
- filter.pass_on()
- return
- elif not filter.req.headers_out["content-type"].startswith("text/html"):
- # pass on if not HTML
- filter.pass_on()
- return
-
- filter.req.streambuffer = StringIO()
- streambuffer = filter.req.streambuffer
-
- streamlet = filter.readline()
- while streamlet:
- streambuffer.write(streamlet)
- streamlet = filter.readline()
-
- if streamlet is None:
- output = appmap.publish(streambuffer.getvalue())
- filter.req.headers_out["Content-Length"] = str(len(output))
- filter.write(output)
- filter.close()
-
-
-def handler(req):
- """Basic filter applying to all mime types it is registered for"""
-
- # Get the path, strip off leading slash, and convert to a
- # dotted notation for xml:id compatibility
- path_info = req.path_info[1:]
- dotted_path = path_info.replace("/", ".")
-
- response = appmap.publish(dotted_path)
- req.content_type = "text/html"
- req.write(response)
-
- return apache.OK
Deleted: /z3/deliverance/branches/packaged/deliverance/renderer.xsl
==============================================================================
--- /z3/deliverance/branches/packaged/deliverance/renderer.xsl Mon Oct 9 22:03:44 2006
+++ (empty file)
@@ -1,67 +0,0 @@
-
-
-
- localhello
- /sandboxes/trois/trunk/deliverance/examples/plonenet.py
-
-
-
-
-
-
-
-
-
-
The context of war which characterized the city of Bukavu during several years has caused a procession of violence of all kinds. Its principal victims have been women and young girls. The large number of women and girls that fell victim to violence, rape, prostitution and abandonment is a big issue in the community. Read more...
100 women have united in ?Women Against Violence?. Their ambition is to start a Women Trauma and Care Centre. The Centre will assist women victims of violence and abuse. It will give them a space where they can see hope again, know about their health, learn self-help skills and help each other campaign for their rights. Read more...
+
+
+Welcome to the NYC Streets Renaissance Campaign Headquarters
+
+
The goal of the NYCSR is to tranform New York City streets so that they are safer, more productive and more livable.
+
Streets can be places for neighbors to meet
+
New York City is defined by its vibrant and diverse streets and neighborhoods. Unfortunately, our neighborhoods and business districts are buckling under increasing amounts of dangerous car and truck traffic. Children can no longer
+
+
+
+
+
+
+ play on their own blocks
+
+
+
+
+
+
+
+
+, while parents worry about turning cars smashing into baby carriages. Senior citizens are losing their independence, shut up in their homes for fear of crossing the street. And shoppers and investors are being turned away by chaotic, traffic-choked avenues.
+
This is unacceptable. The time is long overdue for our great city to strike a better balance between traffic and the needs of pedestrians.
+
The NYC Streets Renaissance Campaign aims to:
+
+
+Educate New Yorkers about potential transportation policy changes that will improve quality of life across New York City
+
+Promote a rebalancing of this public space away from private vehicles and toward community needs
+
+Demonstrate the widespread public support for reform on these issues
+
+Tap the potential of New Yorkers to re-imagine their own streets
+
+
+
Our Initiatives:
+
+
Official launch with an exhibition/event at the Municipal Arts Society
+
Preparation of a multimedia information packet, including a inclusion letter, a dvd featuring Clarence's work, and a intro booklet
Welcome to the NYC Streets Renaissance Campaign Headquarters
The goal of the NYCSR is to tranform New York City streets so that they are safer, more productive and more livable.
Streets can be places for neighbors to meet
New York City is defined by its vibrant and diverse streets and neighborhoods. Unfortunately, our neighborhoods and business districts are buckling under increasing amounts of dangerous car and truck traffic. Children can no longer
+
+
+
+
+
+
+ play on their own blocks
+
+
+
+
+
+
+
+
+, while parents worry about turning cars smashing into baby carriages. Senior citizens are losing their independence, shut up in their homes for fear of crossing the street. And shoppers and investors are being turned away by chaotic, traffic-choked avenues.
This is unacceptable. The time is long overdue for our great city to strike a better balance between traffic and the needs of pedestrians.
The NYC Streets Renaissance Campaign aims to:
Educate New Yorkers about potential transportation policy changes that will improve quality of life across New York City
Promote a rebalancing of this public space away from private vehicles and toward community needs
Demonstrate the widespread public support for reform on these issues
Tap the potential of New Yorkers to re-imagine their own streets
Our Initiatives:
Official launch with an exhibition/event at the Municipal Arts Society
Preparation of a multimedia information packet, including a inclusion letter, a dvd featuring Clarence's work, and a intro booklet
Together, we can re-imagine the streets of New York City.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Added: z3/deliverance/branches/packaged/deliverance/test-data/nycsr/standardrules.xml
==============================================================================
--- (empty file)
+++ z3/deliverance/branches/packaged/deliverance/test-data/nycsr/standardrules.xml Mon Oct 9 22:03:44 2006
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
Added: z3/deliverance/branches/packaged/deliverance/test-data/static/example.html
==============================================================================
--- (empty file)
+++ z3/deliverance/branches/packaged/deliverance/test-data/static/example.html Mon Oct 9 22:03:44 2006
@@ -0,0 +1,10 @@
+
+
+ I am a title
+
+
+ Early text
Paragraph one
+
Paragraph two
+ extra
+
+
Added: z3/deliverance/branches/packaged/deliverance/test-data/static/example_expected.html
==============================================================================
--- (empty file)
+++ z3/deliverance/branches/packaged/deliverance/test-data/static/example_expected.html Mon Oct 9 22:03:44 2006
@@ -0,0 +1,14 @@
+
+
+
+ I am a title
+
+
+
+ Some text
+