[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>
+
+ <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" />
+
+ <b i18n:translate="">Object Name</b>
+ <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