[Checkins] SVN: zope3org/trunk/src/zorg/live/ Added a first implementation for an upload progress widget

Uwe Oestermeier uwe_oestermeier at iwm-kmrc.de
Wed Apr 5 15:26:27 EDT 2006


Log message for revision 66575:
  Added a first implementation for an upload progress widget

Changed:
  U   zope3org/trunk/src/zorg/live/configure.zcml
  A   zope3org/trunk/src/zorg/live/demo/upload/
  A   zope3org/trunk/src/zorg/live/demo/upload/README.txt
  A   zope3org/trunk/src/zorg/live/demo/upload/__init__.py
  A   zope3org/trunk/src/zorg/live/demo/upload/configure.zcml
  A   zope3org/trunk/src/zorg/live/demo/upload/tests.py
  A   zope3org/trunk/src/zorg/live/demo/upload/upload.pt
  A   zope3org/trunk/src/zorg/live/demo/upload/upload.py
  U   zope3org/trunk/src/zorg/live/page/client.js
  U   zope3org/trunk/src/zorg/live/page/event.py
  U   zope3org/trunk/src/zorg/live/page/interfaces.py
  U   zope3org/trunk/src/zorg/live/page/page.py
  U   zope3org/trunk/src/zorg/live/server.py

-=-
Modified: zope3org/trunk/src/zorg/live/configure.zcml
===================================================================
--- zope3org/trunk/src/zorg/live/configure.zcml	2006-04-05 19:23:55 UTC (rev 66574)
+++ zope3org/trunk/src/zorg/live/configure.zcml	2006-04-05 19:26:25 UTC (rev 66575)
@@ -4,6 +4,7 @@
 
     <include package="zorg.live.page" />
     <include package="zorg.live.demo.comment" />
+    <include package="zorg.live.demo.upload" />
     
     <utility
       name="LiveServerHTTP"

Copied: zope3org/trunk/src/zorg/live/demo/upload/README.txt (from rev 65517, zope3org/trunk/src/zorg/live/demo/comment/README.txt)
===================================================================
--- zope3org/trunk/src/zorg/live/demo/comment/README.txt	2006-02-27 16:01:55 UTC (rev 65517)
+++ zope3org/trunk/src/zorg/live/demo/upload/README.txt	2006-04-05 19:26:25 UTC (rev 66575)
@@ -0,0 +1,99 @@
+Upload Demo
+===========
+
+This demo shows how LivePages can be used to provide better feedback to users.
+On file upload the user is informed about the progress of the upload task, i.e.
+the amout of transfered bytes.
+
+
+    >>> from zorg.live.page.manager import LivePageManager
+    >>> from zorg.live.page.interfaces import ILivePageManager
+    >>> manager = LivePageManager()
+    >>> zope.component.provideUtility(manager, ILivePageManager)
+
+Additionally we must add a subscriber that observes Zope3 events and 
+LivePageEvents :
+
+    >>> from zorg.live.page.manager import livePageSubscriber
+    >>> zope.event.subscribers.append(livePageSubscriber)
+
+For test purposes we set the refresh interval (i.e. the interval in which
+output calls are renewed) to 0.1 seconds :
+    
+    >>> from zorg.live.page.client import LivePageClient
+    >>> LivePageClient.refreshInterval = 0.1
+
+Now we simulate the startup of a client. We need a folder that allows us
+to upload a file and a user :
+
+    >>> from zorg.live.demo.upload.tests import buildTestFolder
+    >>> folder = buildTestFolder()
+    >>> class Principal(object) :
+    ...     def __init__(self, id, title) :
+    ...         self.id = id
+    ...         self.title = title
+    
+    >>> user = Principal('zorg.member.uwe', u'Uwe Oestmeier')
+
+Note that the startup is simulated by the nextClientId() call. We need
+a second event handler who shows what happens :
+    
+    >>> def printEvent(event) :
+    ...     print "Event:", event.__class__.__name__
+    >>> zope.event.subscribers.append(printEvent)
+    
+And here is the simulated page request that starts the live session:
+
+    >>> from zorg.live.demo.upload.upload import LiveFileAdd
+    >>> request = TestRequest()
+    >>> request.setPrincipal(user)
+    >>> page = LiveFileAdd(folder, request)
+    >>> clientID = page.nextClientId()
+    Event: LoginEvent
+    >>> client = manager.get(clientID)
+    
+Now we upload the file. If we upload only a small file the view mimics the
+standard Add File view:
+    
+    >>> page.update_object("Some text", "text/plain")
+    Event: ObjectCreatedEvent
+    Event: ObjectAddedEvent
+    Event: ContainerModifiedEvent
+    ''
+    
+The interesting part is handled by the WSGI LivePage server, which buffers
+the uploaded file content in an input stream before the actual Zope handler
+is called. The progress is reported by a LiveInputStream wrapper that set
+up by the request handler. Here we simply simulate such an request by
+repeately calling the wrapping write method:
+
+    >>> class DummyStream(object) :
+    ...     def write(self, data) : pass
+    >>> dummy = DummyStream()
+    >>> import time    
+    >>> from zorg.live.server import LiveInputStream
+    >>> stream = LiveInputStream(dummy, client, content_length=100)
+    >>> for i in range(1, 10) :
+    ...     time.sleep(0.1)
+    ...     stream.write("Some Data")
+    
+    >>> event = client.nextEvent()
+    >>> while event :
+    ...     if event.name == "progress" :
+    ...         event.pprint()
+    ...     event = client.nextEvent()
+    name : 'progress'
+    percent : 18
+    ...
+    name : 'progress'
+    percent : 27
+    ...
+    name : 'progress'
+    percent : 36
+    ...
+
+Clean up:
+
+    >>> zope.event.subscribers.remove(livePageSubscriber)
+    >>> zope.event.subscribers.remove(printEvent)
+    >>> zope.event.subscribers = []
\ No newline at end of file

Added: zope3org/trunk/src/zorg/live/demo/upload/__init__.py
===================================================================
--- zope3org/trunk/src/zorg/live/demo/upload/__init__.py	2006-04-05 19:23:55 UTC (rev 66574)
+++ zope3org/trunk/src/zorg/live/demo/upload/__init__.py	2006-04-05 19:26:25 UTC (rev 66575)
@@ -0,0 +1 @@
+# needed to make this a package
\ No newline at end of file

Copied: zope3org/trunk/src/zorg/live/demo/upload/configure.zcml (from rev 65517, zope3org/trunk/src/zorg/live/demo/comment/configure.zcml)
===================================================================
--- zope3org/trunk/src/zorg/live/demo/comment/configure.zcml	2006-02-27 16:01:55 UTC (rev 65517)
+++ zope3org/trunk/src/zorg/live/demo/upload/configure.zcml	2006-04-05 19:26:25 UTC (rev 66575)
@@ -0,0 +1,32 @@
+<configure 
+   xmlns="http://namespaces.zope.org/browser"
+   xmlns:zope="http://namespaces.zope.org/zope"
+   i18n_domain="zope"
+   >
+     
+   <pages		
+      
+      for="zope.app.folder.interfaces.IFolder"
+      class=".upload.LiveFileAdd"
+      permission="zope.View"
+      >
+      
+      <page
+        name="liveaddfile.html"
+        template="./upload.pt"
+        title="LiveUpload"
+        menu="zmi_views" />
+        
+      <page
+        name="liveprogress.html"
+        template="./progress.pt"
+        />
+        
+      <page
+        name="liveUpload"
+        attribute="errors"
+        />
+        
+    </pages>
+ 
+</configure>

Copied: zope3org/trunk/src/zorg/live/demo/upload/tests.py (from rev 65517, zope3org/trunk/src/zorg/live/demo/comment/tests.py)
===================================================================
--- zope3org/trunk/src/zorg/live/demo/comment/tests.py	2006-02-27 16:01:55 UTC (rev 65517)
+++ zope3org/trunk/src/zorg/live/demo/upload/tests.py	2006-04-05 19:26:25 UTC (rev 66575)
@@ -0,0 +1,59 @@
+import unittest
+
+import zope
+import zope.component
+from zope.interface import classImplements
+from zope.app import zapi
+
+from zope.testing import doctest, doctestunit
+from zope.app.testing import ztapi
+from zope.app.testing.setup import placefulSetUp, placefulTearDown
+
+from zope.app.folder import rootFolder
+
+from zorg.live.testing import livePageSetUp
+from zorg.comment.testing import commentSetUp
+from zorg.comment import IAttributeAnnotableComments
+
+
+def buildTestFolder() :
+    """ Returns a file that is located in a site. """
+    return rootFolder()
+    
+def setUpBrowserTests(test) :
+
+    placefulSetUp()
+    commentSetUp()
+    livePageSetUp()
+        
+     
+def tearDownBrowserTests(test) :
+
+    placefulTearDown()
+    
+    
+def test_suite():
+    optionflags = doctest.NORMALIZE_WHITESPACE+doctest.ELLIPSIS
+    globs = {'zope': zope,
+             'zapi': zope.app.zapi,
+             'pprint': doctestunit.pprint,
+             'TestRequest': zope.publisher.browser.TestRequest}
+ 
+    return unittest.TestSuite((
+        doctest.DocFileSuite('README.txt', 
+                                setUp=setUpBrowserTests, 
+                                tearDown=tearDownBrowserTests,
+                                globs=globs,
+                                optionflags=optionflags
+                             ),
+                             
+        doctest.DocTestSuite("zorg.live.demo.upload.upload", 
+                                setUp=setUpBrowserTests, 
+                                tearDown=tearDownBrowserTests,
+                                optionflags=optionflags
+                             ),
+                             
+        ))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')

Copied: zope3org/trunk/src/zorg/live/demo/upload/upload.pt (from rev 65517, zope3org/trunk/src/zorg/live/demo/comment/templates/comments.pt)
===================================================================
--- zope3org/trunk/src/zorg/live/demo/comment/templates/comments.pt	2006-02-27 16:01:55 UTC (rev 65517)
+++ zope3org/trunk/src/zorg/live/demo/upload/upload.pt	2006-04-05 19:26:25 UTC (rev 66575)
@@ -0,0 +1,102 @@
+<html metal:use-macro="context/@@standard_macros/page"
+    i18n:domain="zope">
+<body>
+
+<div metal:fill-slot="user_details">
+    <div id="userDetails">
+        <span>Online:</span>
+        <span id="online">
+            <tal:block replace="view/whoIsOnline">
+                  User
+            </tal:block>
+        </span>
+        &nbsp;&nbsp;&nbsp;
+        <span tal:omit-tag="" i18n:translate="">User:</span>
+        <tal:block replace="request/principal/title">
+                User
+        </tal:block>
+    </div>
+</div>
+
+<div metal:fill-slot="body">
+
+    <script type="text/javascript" src="prototype.js" 
+       tal:attributes="src string:${context/++resource++zorgajax/prototype.js}">
+    </script>
+    <script type="text/javascript" src="livepage.js" 
+       tal:attributes="src string:${context/++resource++livepage.js}">
+    </script>
+    <script type="text/javascript" src="livecomment.js" 
+       tal:attributes="src string:${context/++resource++livecomment.js}">
+    </script>
+    
+    <script type="text/javascript" 
+            tal:content="string:LivePage.uuid = '${view/nextClientId}';">
+            LivePage.uuid = '0';
+    </script>
+    
+     <script type="text/javascript">
+        LivePage.startClient();
+        window.onunload = LivePage.stopClient
+    </script>  
+
+  <form action="./liveUpload?progress=uuid"
+        tal:attributes="action string:./liveUpload?progress=${view/clientUUID}"
+        method="post" enctype="multipart/form-data">
+
+    <h3 i18n:translate="">LivePage File Upload</h3>
+    
+    <iframe src="./liveprogress.html"></iframe>
+
+    <div tal:define="errors view/errors" tal:content="errors"
+        i18n:translate=""/>
+
+    <div class="row">
+      <div class="label">
+        <label for="field.contentType"
+               title="The content type identifies the type of data."
+               i18n:attributes="title" i18n:translate="">Content Type</label>
+      </div>
+      <div class="field">
+        <input class="textType"
+               id="field.contentType"
+               name="field.contentType"
+               size="20"
+               type="text"
+               value="" /></div>
+    </div>
+
+    <div class="row">
+      <div class="label">
+        <label for="field.data"
+               title="The actual content of the object."
+               i18n:attributes="title" i18n:translate="">Data</label>
+      </div>
+      <div class="field">
+        <input class="fileType"
+               id="field.data"
+               name="field.data"
+               size="20"
+               type="file" /></div>
+               
+    
+    </div>
+
+    <div class="row">
+      <div class="controls"><hr />
+
+        <input type="submit" i18n:attributes="value add-button"
+            value="Add" name="UPDATE_SUBMIT" />
+
+        &nbsp;&nbsp;<b i18n:translate="">Object Name</b>&nbsp;&nbsp;
+        <input type="text" name="add_input_name" value="" />
+
+      </div>
+    </div>
+
+  </form>
+
+</div>
+</body>
+
+</html>
\ No newline at end of file

Copied: zope3org/trunk/src/zorg/live/demo/upload/upload.py (from rev 65517, zope3org/trunk/src/zorg/live/demo/comment/comment.py)
===================================================================
--- zope3org/trunk/src/zorg/live/demo/comment/comment.py	2006-02-27 16:01:55 UTC (rev 65517)
+++ zope3org/trunk/src/zorg/live/demo/upload/upload.py	2006-04-05 19:26:25 UTC (rev 66575)
@@ -0,0 +1,68 @@
+##############################################################################
+#
+# Copyright (c) 2005 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.
+#
+##############################################################################
+"""
+$Id: upload.py 38895 2005-10-07 15:09:36Z dominikhuber $
+"""
+__docformat__ = 'restructuredtext'
+
+import zope
+from zope.app import zapi
+from zope.app.file.browser.file import FileUpdateView
+from zope.app.file import File
+from zope.app.event import objectevent
+from zope.app.container.browser.adding import Adding
+
+from zorg.live.page.client import LivePageClient
+from zorg.live.page.page import LivePage
+
+from zorg.live.page.interfaces import ILivePageManager
+from zorg.live.page.interfaces import IPersonEvent
+from zorg.live.page.event import Update
+
+from zorg.live.globals import getRequest
+from zorg.live.globals import getFullName
+
+
+class LiveFileAdd(LivePage, FileUpdateView) :
+    """ A specialization of the traditional upload view that shows the
+        progress of the upload task.
+    """
+    
+    
+    def update_object(self, data, contenttype):
+        print "XXX"
+        f = File(data, contenttype)
+        zope.event.notify(objectevent.ObjectCreatedEvent(f))
+        
+        adding = Adding(self.context, self.request)
+        adding.add(f)
+        self.request.response.redirect(adding.nextURL())
+        return ''
+
+    def whoIsOnline(self) :
+        """ Returns a comma seperated list of names of online users. """
+        manager = zapi.getUtility(ILivePageManager)
+        ids = manager.whoIsOnline(self.getLocationId())
+        return ", ".join([getFullName(id) for id in ids])
+   
+
+    def notify(cls, event) :
+
+        if IPersonEvent.providedBy(event) :
+            manager = zapi.getUtility(ILivePageManager)
+            repr = manager.whoIsOnline(event.where)
+            update = Update(id="online", html=repr)
+            cls.sendEvent(update)
+                    
+    notify = classmethod(notify)

Modified: zope3org/trunk/src/zorg/live/page/client.js
===================================================================
--- zope3org/trunk/src/zorg/live/page/client.js	2006-04-05 19:23:55 UTC (rev 66574)
+++ zope3org/trunk/src/zorg/live/page/client.js	2006-04-05 19:26:25 UTC (rev 66575)
@@ -172,6 +172,13 @@
                 $("livepage_status").innerHTML = "idle";
                 }
             return;
+            },
+            
+    onProgress : function(event) {
+            if ($("livepage_progress")) {
+                $("livepage_progress").innerHTML = event.percent + "%";
+                }
+            return;
             }
             
     }

Modified: zope3org/trunk/src/zorg/live/page/event.py
===================================================================
--- zope3org/trunk/src/zorg/live/page/event.py	2006-04-05 19:23:55 UTC (rev 66574)
+++ zope3org/trunk/src/zorg/live/page/event.py	2006-04-05 19:26:25 UTC (rev 66575)
@@ -35,6 +35,8 @@
 from zorg.live.page.interfaces import IUpdate
 from zorg.live.page.interfaces import ISetAttribute
 from zorg.live.page.interfaces import IModifyElementEvent
+from zorg.live.page.interfaces import IProgressEvent
+
 from zorg.live.page.interfaces import IClientEventFactory
 
 
@@ -178,6 +180,26 @@
         
 directlyProvides(ReloadEvent, IClientEventFactory)
 
+
+class ProgressEvent(LivePageEvent) :
+    """ Indicates the progress of a long enduring task.
+
+        >>> event = Progress(percent=20)
+        >>> event.pprint()
+        html : '<div id="comment1"></div>'
+        id : 'comments'
+        name : 'update'
+        recipients : 'all'
+        where : None
+    
+    """
+    
+    implements(IProgressEvent)
+    
+    name = "progress"
+
+directlyProvides(ProgressEvent, IClientEventFactory)
+
        
 class CloseEvent(LivePageEvent) :
     """ A user has closed the browser window
@@ -324,6 +346,7 @@
 directlyProvides(Update, IClientEventFactory)
 
     
+
 def dict2event(args) :
     """ Converts a dict into an event.
     

Modified: zope3org/trunk/src/zorg/live/page/interfaces.py
===================================================================
--- zope3org/trunk/src/zorg/live/page/interfaces.py	2006-04-05 19:23:55 UTC (rev 66574)
+++ zope3org/trunk/src/zorg/live/page/interfaces.py	2006-04-05 19:26:25 UTC (rev 66575)
@@ -80,10 +80,18 @@
     uuid = Attribute("Identifies the client browser page.")
 
 class IErrorEvent(ILivePageEvent) :
-    """ Am error event that can be used to inform the clients about
+    """ An error event that can be used to inform the clients about
         server errors.
     """
+
+class IProgressEvent(ILivePageEvent) :
+    """ An event that can be used to inform the clients about
+        the progress of long enduring tasks.
+    """
     
+    percent = Attribute("Indicates the progress of the task.")
+
+    
 class ILoginEvent(ILocationEvent, IPersonEvent) :
     """ A login event that can be used to notify about new users. 
     """

Modified: zope3org/trunk/src/zorg/live/page/page.py
===================================================================
--- zope3org/trunk/src/zorg/live/page/page.py	2006-04-05 19:23:55 UTC (rev 66574)
+++ zope3org/trunk/src/zorg/live/page/page.py	2006-04-05 19:26:25 UTC (rev 66575)
@@ -56,7 +56,8 @@
     implements(ILivePage)
     
     clientFactory = LivePageClient
-        
+    clientUUID = None
+    
     def notify(self, event) :
         """ Default implementation of an event handler. Must be specialized. """
         pass
@@ -66,9 +67,14 @@
                    
     def nextClientId(self) :
         """ Returns a new client id. """
-
         return self.clientFactory(self).uuid
         
+    def clientUUID(self) :
+        manager = zapi.getUtility(ILivePageManager)
+        for client in manager._iterClients(self.getLocationId()) :
+            return client.uuid
+        return None
+        
     def getLocationId(self) :
         """ Returns a group id that allows to share different livepages
             in different contexts.

Modified: zope3org/trunk/src/zorg/live/server.py
===================================================================
--- zope3org/trunk/src/zorg/live/server.py	2006-04-05 19:23:55 UTC (rev 66574)
+++ zope3org/trunk/src/zorg/live/server.py	2006-04-05 19:26:25 UTC (rev 66575)
@@ -38,6 +38,7 @@
 from zorg.live.page.interfaces import ILivePageManager
 from zorg.live.page.event import IdleEvent
 from zorg.live.page.event import ErrorEvent
+from zorg.live.page.event import ProgressEvent
 from zorg.live.page.event import dict2event
 
 
@@ -69,27 +70,27 @@
         except IndexError :
             raise ExtractionError
     
-    def cachedUUID(self, uri) :
+    def queryUUID(self, uri, key) :
         """ Extracts the uuid from the livepage call or raises an IndexError
         
         >>> extract = Extractor(None)
-        >>> extract.cachedUUID("/exp/imageMap/cached=uuid")
+        >>> extract.queryUUID("/exp/imageMap/cached=uuid", "cached")
         'uuid'
         
-        >>> extract.cachedUUID("/exp/@@out/uuid")
+        >>> extract.queryUUID("/exp/@@out/uuid", "cached")
         Traceback (most recent call last):
         ...
         ExtractionError
         
         """
-        splitted = uri.split("cached=")
+        splitted = uri.split(key + "=")
         if len(splitted) < 2 :
             raise ExtractionError
         try : 
             return splitted[-1].split("&")[0]
         except IndexError :
             raise ExtractionError
-            
+                  
     def readEvent(self) :
         """ Deserializes the event from the input stream. """
         input = str(self.context.request.stream.read())
@@ -129,10 +130,9 @@
             except ExtractionError :
                 pass
        
-         
         if "cached=" in uri :
             try :
-                uuid = handler.uuid = self.cachedUUID(uri)
+                uuid = handler.uuid = self.queryUUID(uri, "cached")
                 handler.result = manager.fetchResult(uuid, clear=True)
                 if handler.result :
                     handler.liverequest = True
@@ -270,7 +270,87 @@
 #             LogWrapperResource.hook(self, ctx)    
 
 
+from cStringIO import StringIO
+import tempfile
+from twisted.web2 import iweb, resource, stream
 
+max_stringio = 100*1000 # Should this be configurable?
+
+
+class LiveInputStream(object) :
+    """ A wrapper for live input streams. This wrapper
+        can report the progress of the upload tasks.
+    """
+    
+    def __init__(self, stream, client=None, content_length=None) :
+        self.stream = stream
+        self.received_bytes = 0
+        self.expected_length = content_length or 1.0
+        self.client = client
+        self.reported = time.time()
+        self.interval = LivePageWSGIHandler.idleInterval * 2
+        
+    def write(self, data) :
+        self.received_bytes += len(data)
+        self.stream.write(data)
+        
+        if self.client :
+            if time.time() > (self.reported + self.interval) :
+                ratio = float(self.received_bytes) / float(self.expected_length)
+                percent = int(ratio * 100.0)
+                
+                event = ProgressEvent(percent=percent)
+                manager = zapi.getUtility(ILivePageManager)
+                print "addEvent", percent
+                manager.addEvent(event)
+                print "added"
+    
+class LivePrebuffer(resource.WrapperResource):
+
+    def hook(self, ctx):
+        req = iweb.IRequest(ctx)
+
+        content_length = req.headers.getHeader('content-length')
+        if content_length is not None and int(content_length) > max_stringio:
+            temp = tempfile.TemporaryFile()
+            def done(_):
+                temp.seek(0)
+                # Replace the request's stream object with the tempfile
+                req.stream = stream.FileStream(temp, useMMap=False)
+                # Hm, this shouldn't be required:
+                req.stream.doStartReading = None
+
+        else:
+            temp = StringIO()
+            def done(_):
+                # Replace the request's stream object with the tempfile
+                req.stream = stream.MemoryStream(temp.getvalue())
+                # Hm, this shouldn't be required:
+                req.stream.doStartReading = None
+        
+        
+        print "LivePrebuffer.uri", req.uri
+        try :
+            uuid = Extractor(ctx).queryUUID(req.uri, "progress")
+            print "Found uuid", uuid
+            manager = zapi.getUtility(ILivePageManager)
+            client = manager.get(uuid)
+        except ExtractionError :
+            client = None
+        
+        live = LiveInputStream(temp, client, content_length)
+        
+        return stream.readStream(req.stream, live.write).addCallback(done)
+
+    # Oops, fix missing () in lambda in WrapperResource
+    def locateChild(self, ctx, segments):
+        x = self.hook(ctx)
+        if x is not None:
+            return x.addCallback(lambda data: (self.res, segments))
+        return self.res, segments
+
+
+    
 def createHTTPFactory(db):
 
     reactor.threadpool.adjustPoolsize(10, 20)
@@ -278,7 +358,7 @@
     resource = WSGIPublisherApplication(db)
     resource = LivePageWSGIResource(resource)
     resource = LiveLogWrapperResource(resource)
-    resource = Prebuffer(resource)
+    resource = LivePrebuffer(resource)
     return HTTPFactory(Site(resource))
 
 



More information about the Checkins mailing list