[z3-checkins] r32995 - z3/jsonserver/trunk

jwashin at codespeak.net jwashin at codespeak.net
Sat Oct 7 23:20:50 CEST 2006


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("""
+    ... <configure
+    ...   xmlns="http://namespaces.zope.org/zope"
+    ...   xmlns:browser="http://namespaces.zope.org/browser"
+    ...   >
+    ...  <!-- allow browser directives here -->
+    ...  <include package="zope.app.publisher.browser" file="meta.zcml" />
+    ...  <browser:page
+    ...     name="folderlist"
+    ...     for="zope.app.folder.folder.IFolder"
+    ...     class="jsonserver.JSONViews.FolderListing"
+    ...     permission="zope.ManageContent"
+    ...     />
+    ... </configure>
+    ... """)
+
+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("""
+    ... <configure
+    ...   xmlns="http://namespaces.zope.org/zope"
+    ...   xmlns:browser="http://namespaces.zope.org/browser"
+    ...   >
+    ...  <!-- allow browser directives here -->
+    ...  <include package="zope.app.publisher.browser" file="meta.zcml" />
+    ...  <browser:page
+    ...     name="sum"
+    ...     for="zope.app.folder.folder.IFolder"
+    ...     class="jsonserver.JSONViews.FolderStupidSum"
+    ...     permission="zope.ManageContent"
+    ...     />
+    ... </configure>
+    ... """)
+
+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.


More information about the z3-checkins mailing list