[Checkins] SVN: zope3org/trunk/src/zorg/ajax/livepage.py Removed
obsolete livepage implementation. See zorg.live
Uwe Oestermeier
uwe_oestermeier at iwm-kmrc.de
Fri May 26 03:45:39 EDT 2006
Log message for revision 68301:
Removed obsolete livepage implementation. See zorg.live
Changed:
D zope3org/trunk/src/zorg/ajax/livepage.py
-=-
Deleted: zope3org/trunk/src/zorg/ajax/livepage.py
===================================================================
--- zope3org/trunk/src/zorg/ajax/livepage.py 2006-05-26 04:35:25 UTC (rev 68300)
+++ zope3org/trunk/src/zorg/ajax/livepage.py 2006-05-26 07:45:39 UTC (rev 68301)
@@ -1,803 +0,0 @@
-##############################################################################
-#
-# 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: livepage.py 39651 2005-10-26 18:36:17Z oestermeier $
-"""
-__docformat__ = 'restructuredtext'
-
-import os, itertools, unittest, time, transaction
-from string import Template
-from thread import allocate_lock
-
-from zope.testing import doctest
-
-from zope.app import zapi
-from zope.interface import implements
-from zope.component import adapts
-from zope.publisher.browser import TestRequest
-from zope.publisher.interfaces import IRequest
-from zope.app.session.interfaces import ISession
-from zope.security.proxy import removeSecurityProxy
-from zope.security.checker import defineChecker, NoProxy
-from zope.security.checker import ProxyFactory
-
-from zope.app.traversing.interfaces import TraversalError
-from zope.app.traversing.interfaces import ITraversable
-from zope.publisher.interfaces import IPublishTraverse
-from zope.publisher.interfaces import NotFound
-from zope.app.publisher.browser import BrowserView
-from zope.app.keyreference.interfaces import IKeyReference
-
-from zope.interface import implements
-from zope.interface import Interface
-from zorg.ajax.page import AjaxPage, ComposedAjaxPage
-
-
-from zorg.ajax.interfaces import ILiveChanges
-from zorg.ajax.interfaces import ILivePage
-from zope.publisher.http import DirectResult
-
-from twisted.web2.wsgi import FileWrapper
-
-from interfaces import ILivePageClients
-from interfaces import ILivePageClient
-
-from zorg.edition.interfaces import IUUIDGenerator
-
-
-class Cache(object) :
- """ Simple cache that uses least recently accessed time to trim size """
-
- def __init__(self, size=100):
- self.data = {}
- self.size = size
-
- def resize(self):
- """ trim cache to no more than 95% of desired size """
- trim = max(0, int(len(self.data)-0.95*self.size))
- if trim:
- # don't want self.items() because we must sort list by access time
- values = map(None, self.data.values(), self.data.keys())
- values.sort()
- for val,k in values[0:trim]:
- try :
- del self.data[k]
- except KeyError:
- pass
-
- def __delitem__(self, key) :
- try :
- del self.data[key]
- except KeyError:
- pass
-
- def __setitem__(self,key,val):
- if (not self.data.has_key(key) and
- len(self.data) >= self.size):
- self.resize()
- self.data[key] = (time.time(), val)
-
- def __getitem__(self,key):
- """ like normal __getitem__ but updates time of fetched entry """
- val = self.data[key][1]
- self.data[key] = (time.time(),val)
- return val
-
- def get(self,key,default=None):
- """ like normal __getitem__ but updates time of fetched entry """
- try :
- return self[key]
- except KeyError:
- return default
-
-
-class LivePageClients(object) :
- """ Groups collections of LivePages.
-
- Holds a dict of dict of LivePageClients with group_ids and
- client uuids as keys and clients as values.
-
- Makes all write operations thread safe.
- """
-
- implements(ILivePageClients)
-
- writelock = allocate_lock() # A writelock for clients
- checkInterval = 10 # check for dead clients in seconds
- maxClients = 3 # max clients per user
-
- def __init__(self) :
- self.groups = {}
- self.lastCheck = 0
- self.results = Cache(size=200)
-
- def cacheResult(self, result) :
- """ Caches a result for efficient access.
-
- Returns a UUID which can be used to retrive the result.
-
- >>> clients = LivePageClients()
- >>> uuid = clients.cacheResult(42)
- >>> clients.fetchResult(uuid)
- 42
-
- """
-
- uuid = zapi.getUtility(IUUIDGenerator)()
- self.writelock.acquire()
- try :
- self.results[uuid] = result
- finally:
- self.writelock.release()
- return uuid
-
- def fetchResult(self, uuid, clear=True) :
- """ Accesses the result and clears the memory
-
- >>> clients = LivePageClients()
- >>> uuid = clients.cacheResult(42)
- >>> clients.fetchResult(uuid)
- 42
-
- """
- result = self.results.get(uuid, None)
- if clear :
- del self.results[uuid]
- return result
-
- def _group(self, group_id) :
- """ Internal method that returns a dict of uuid, client tuples
- that belong to a group specified by a group id.
-
- >>> clients = LivePageClients()
- >>> clients._group(42)
- {}
-
- """
-
- return self.groups.setdefault(group_id, {})
-
- def register(self, client, uuid=None) :
- """ Register a client. Uses the provided uuid or generates a new one.
-
- """
-
-
- self.writelock.acquire()
- try:
- if uuid is None :
- uuid = zapi.getUtility(IUUIDGenerator)()
- else :
- # an existing uuid indicates a renewed registration of an
- # old page. We do not know what happened so a reload
- # would make sense
- client.addOutput("reload")
-
- existing = self.getClientsFor(client.principal.id)
- if len(existing) > self.maxClients :
- for c in existing :
- self.unregister(c)
- client.addOutput("limited")
-
- group_id = client.group_id
- client.handleid = uuid
- if uuid in self._group(group_id) :
- print "***WARNING: already registered"
- else :
- self._group(group_id)[uuid] = client
- finally:
- self.writelock.release()
-
- def unregister(self, client) :
- self.writelock.acquire()
- try:
- group_id = client.group_id
- try :
- del self._group(group_id)[client.handleid]
- print "***Info: unregistered client for %s." % client.principal.title
- except KeyError :
- print "***Info: client already unregistered."
- finally:
- self.writelock.release()
-
- def getClientsFor(self, user_id, group_id=None) :
- """ Get clients for a specific user.
- """
- return [c for c in self._iterClients(group_id) if c.principal.id == user_id]
-
- def get(self, uuid, default=None) :
- for mapping in self.groups.values() :
- if uuid in mapping :
- client = mapping[uuid]
- self.writelock.acquire()
- try :
- client.touched = time.time()
- finally :
- self.writelock.release()
- return client
- return default
-
- def checkAlive(self, verbose=False) :
- """ Checks whether clients are alive. """
- if time.time() < self.lastCheck + self.checkInterval :
- return
-
- if verbose :
- for mapping in self.groups.values() :
- for client in mapping.values() :
- print "Client", client.principal.id
-
- for mapping in self.groups.values() :
- for client in mapping.values() :
- if time.time() > client.touched + client.targetTimeout :
- self.unregister(client)
- self.writelock.acquire()
- try :
- self.lastCheck = time.time()
- finally :
- self.writelock.release()
-
- def _iterClients(self, group_id=None) :
- """
-
-
- """
- if group_id is None :
- for mapping in self.groups.values() :
- for client in mapping.values() :
- yield client
- else :
- for client in self._group(group_id).values() :
- yield client
-
-
- def __iter__(self) :
- """ Iterates over all clients which are still alive.
- Unregisters all unnecessary clients.
- """
- self.checkAlive()
- return self._iterClients()
-
-
- def addOutput(self, output, group_id=None, recipients="all") :
- self.checkAlive()
- for client in self._iterClients(group_id) :
- if recipients == "all" or client.principal.id in recipients :
- client.addOutput(output)
-
- def whoIsOnline(self, group_id) :
- """ Returns the ids of the principals that are using livepages. """
- self.checkAlive()
- online = set()
- for client in self._iterClients(group_id) :
- online.add(client.principal.id)
- return sorted(online)
-
-
-
-def livePageSubscriber(event) :
- """ A subscriber that allows the clients to respond to
- Zope3 events.
-
- It checkes whether the clients are still alive (via
- the __iter__ method of the clients).
-
- The events are send to the notify **classmethod**
- of the page classes.
-
- """
- global clients
-
- page_classes = set()
- for client in clients :
- page_classes.add(client.page_class)
-
- for cls in page_classes :
- cls.notify(event)
-
-
-
-# A global dictionary shared between threads
-clients = LivePageClients()
-
-class LivePageClient(object):
- """An object which represents the client-side webbrowser of a LivePage.
- """
-
- implements(ILivePageClients)
-
- refreshInterval = 10 # seconds
- targetTimeout = 20
-
- def __init__(self, page, uuid=None) :
- global clients
- self.group_id = page.getGroupId()
- self.page_class = page.__class__
- self.outbox = []
- self.principal = page.request.principal
- self.writelock = allocate_lock()
- self.touched = time.time()
- clients.register(self, uuid)
-
- def popOutput(self) :
- """ Returns an output block for processing in the browser side client.
- Touches the client to indicate that the connection is still alive.
- """
- self.writelock.acquire()
- try :
- if self.outbox :
- result = self.outbox.pop()
- if result.endswith("timestamp=") :
- result += "%s\n" % time.time()
- #enforce that the URLs with dummy timestamps are really reloaded
- #we add the dummy as late as possible to avoid redundant calls
- else :
- result = None
- self.touched = time.time()
- finally :
- self.writelock.release()
- return result
-
-
- def addOutput(self, output) :
- """ Adds a output block for further client side processing to the
- clients message queue.
- """
-
- self.writelock.acquire()
- try:
- if output not in self.outbox :
- self.outbox.append(output)
- finally:
- self.writelock.release()
-
- def output(self, outputNum=0) :
- end = time.time() + self.refreshInterval
- while time.time() < end :
- output = self.popOutput()
- if output :
- time.sleep(0.5)
- #print "sending", output
- return output
- time.sleep(0.1)
- return "idle"
-
- def input(self, handler_name, arguments) :
- js = getattr(self, handler_name)(arguments)
- global clients
- clients.addOutput(js)
- return ''
-
- def alert(self, arguments) :
- args = arguments.split(",")
- return "javascript alert('%s')" % args[0]
-
- def update(self, arguments) :
- args = arguments.split(",")
- return "javascript update('%s')" % args[0]
-
- def append(self, arguments) :
- args = arguments.split(",")
- return "append %s\n%s" % (args[0], args[1])
-
-
-class LivePage(ComposedAjaxPage) :
- """ A Zope3 substitute for the newov.livepage.LivePage
-
- The initial call of a LivePage (without parameters) returns a page
- that immediately connects a client to the server. After that the client
- asks again and again for new available output. The output consists of
- JavaScript snippets that are evaluated by the browser.
-
- Here we simulate the startup of two clients of the same factory type:
-
- >>> factory='zorg.ajax.livepage.LivePage'
-
- Note that the factory must be application specific because
- the access and notification method build their own instances.
-
- >>> class Principal(object) :
- ... def __init__(self, id, title) :
- ... self.id = id
- ... self.title = title
-
- >>> user1 = Principal('zorg.member.uwe', u'Uwe Oestmeier')
- >>> user2 = Principal('zorg.member.dominik', u'Dominik Huber')
-
- >>> request = TestRequest()
- >>> request.setPrincipal(user1)
- >>> page = LivePage(None, request)
- >>> group_id = page.getGroupId()
- >>> print page.render()
- <html>
- <head>
- <script src="http://127.0.0.1/++resource++zorgajax/prototype.js" type="text/javascript"></script>
- <script type="text/javascript">var livePageUUID = 'uuid1';</script>
- <script src="http://127.0.0.1/++resource++zorgajax/livepage.js" type="text/javascript"></script>
- </head>
- <body onload="startClient()">
- <p>Input some text.</p>
- <input onchange="sendLivePage('append', 'target', this.value)" type="text" />
- <p id="target">Text goes here:</p>
- </body>
- </html>
-
-
- >>> request.setPrincipal(user2)
- >>> page = LivePage(None, request)
- >>> print page.render()
- <html>
- <head>
- <script src="http://127.0.0.1/++resource++zorgajax/prototype.js" type="text/javascript"></script>
- <script type="text/javascript">var livePageUUID = 'uuid2';</script>
- <script src="http://127.0.0.1/++resource++zorgajax/livepage.js" type="text/javascript"></script>
- </head>
- <body onload="startClient()">
- <p>Input some text.</p>
- <input onchange="sendLivePage('append', 'target', this.value)" type="text" />
- <p id="target">Text goes here:</p>
- </body>
- </html>
-
- The global clients dictionary can be asked for all online users:
-
- >>> for client in clients :
- ... print client.principal.id
- zorg.member.dominik
- zorg.member.uwe
-
- >>> clients.whoIsOnline(group_id)
- ['zorg.member.dominik', 'zorg.member.uwe']
-
- For test purposes we set the refresh interval (i.e. the interval in which
- output calls are renewed) to 0.1 seconds :
-
- >>> LivePageClient.refreshInterval = 0.1
-
- After that the page can be called by the javascript glue as follows
-
- - @@livepage.html/@@client/uuid0/output?outputNum=0
-
- Call the output method and ask for special output that is intended
- for the specified client
-
- - @@livepage.html/@@client/uuid0/input?handler-name=change&arguments=42
-
- Call an input method that produces output for various clients.
-
- After the startup we ask for some output. Since nothing happened, the
- output is 'idle':
-
- >>> page1 = LivePage(None, TestRequest())
- >>> page1.output(uuid='uuid1', outputNum=0)
- 'idle'
-
- Now we can send some input :
-
- >>> page2 = LivePage(None, TestRequest())
- >>> page2.input(uuid='uuid1', handler_name='append', arguments="target,42")
- ''
-
- After that the next call of the output returns javascript snippets:
-
- >>> print page1.output(uuid='uuid1', outputNum=1)
- append target
- 42
-
-
- And this again and again if we provide new input :
-
- >>> page2.input(uuid='uuid1', handler_name='append', arguments="target,43")
- ''
-
- >>> print page1.output(uuid='uuid1', outputNum=2)
- append target
- 43
-
-
- """
-
- implements(ILivePage)
-
- clientFactory = LivePageClient
-
- def notify(self, event) :
- """ Default implementation of an event handler. Must be specialized. """
- pass
-
- notify = classmethod(notify)
-
-
- def nextClientId(self) :
- """ Returns a new client id. """
- #print "***nextClientId called"
- return self.clientFactory(self).handleid
-
- def getGroupId(self) :
- """ Returns a group id that allows to share different livepages
- in different contexts.
-
- The default implementation returns the IKeyReference of the
- LivePage context.
- """
-
- return IKeyReference(self.context, None) or 0
-
-
- def output(self, uuid, outputNum) :
- """ Convenience function that accesses a specific client.
-
- """
- request = self.request
- method = Output(self, request).publishTraverse(request, uuid)
- return method(outputNum)
-
- def input(self, uuid, handler_name, arguments) :
- """ Convenience function that accesses a specific client.
-
- """
- request = self.request
- method = Input(self, request).publishTraverse(request, uuid)
- return method(handler_name, arguments)
-
- def sendResponse(cls, response, group_id=None, recipients="all") :
- """ Sends a livepage response to all clients.
- A response consits of a leading command line
- and optional html body data.
- """
- global clients
- assert isinstance(response, str)
- clients.addOutput(response, group_id, recipients)
- return ''
-
- sendResponse = classmethod(sendResponse)
-
- def render(self) :
- """ A test method that calls the livepage.
-
- Must be overwritten. The method must call self.nextClientId to
- ensure that a unique key is generated.
-
- """
-
- url = zapi.absoluteURL(self.context, self.request)
- url += "/++resource++zorgajax"
-
- template = Template("""<html>
- <head>
- <script src="$url/prototype.js" type="text/javascript"></script>
- <script type="text/javascript">var livePageUUID = '$id';</script>
- <script src="$url/livepage.js" type="text/javascript"></script>
- </head>
- <body onload="startClient()">
- <p>Input some text.</p>
- <input onchange="sendLivePage('append', 'target', this.value)" type="text" />
- <p id="target">Text goes here:</p>
- </body>
-</html>""")
-
- return template.substitute(id=self.nextClientId(), url=url)
-
-
-defineChecker(LivePage, NoProxy)
-
-
-class ClientIO(BrowserView) :
- """ A view that represents the input and
- output of a single client. """
-
- def __init__(self, context, request) :
- super(BrowserView, self).__init__(context, request)
- self.view = removeSecurityProxy(context)
-
- def traverseClient(self, request, uuid) :
- global clients
- client = clients.get(uuid)
- if client is None :
- client = self.view.clientFactory(self.view, uuid)
- return client
-
-class Output(ClientIO) :
- """ A view of a LivePage view that allows to traverse to a specific
- client.
-
- """
-
- implements(IPublishTraverse)
-
- def publishTraverse(self, request, uuid) :
- #print "Output call", request.URL, uuid
- client = self.traverseClient(request, uuid)
- return client.output
-
-class Input(ClientIO) :
- """ A view of a LivePage view that allows to traverse to a specific
- client.
-
- """
-
- implements(IPublishTraverse)
-
- def publishTraverse(self, request, uuid) :
- client = self.traverseClient(request, uuid)
- return client.input
-
-
-
-class LiveOutputStream(object) :
- """ The iterable output stream. """
-
- counter = 0
- limit = 100
-
- def __init__(self, channel, client) :
- self.channel = channel
- self.client = client
-
- def __iter__(self) :
- if self.client.output :
- result = self.client.output.pop()
- yield result
-
- self.counter += 1
- if self.counter > self.limit :
- raise StopIteration
-
-
-defineChecker(LiveOutputStream, NoProxy)
-
-
-
-### Experimental Streaming Stuff. Seems to be unnecessary for live pages ###
-
-
-class IIterable(Interface) :
-
- def __iter__() :
- """ Defines an iterable. """
-
-
-class FileBuffer(object):
- """ A buffer for shared read and write operations. """
- closed = False
- count = 0
-
- newline = '\n<p>.</p>'
- end = "</body></html>"
-
- def __init__(self):
- self.buf = ''
-
- def write(self, str):
- self.buf += str
-
- def read(self, size):
-
- data = self.buf[:size]
- self.buf = self.buf[len(data):]
- if data :
- return data
- elif self.closed :
- return None
- if self.count > 100 :
- self.close()
-
- self.count += 1
- time.sleep(0.1)
- return self.newline
-
- def __iter__(self) :
- data = self.read(100)
- if data is None :
- raise StopIteration
- return data
-
- def close(self) :
- self.write(self.end)
- self.closed = True
-
- def open(self) :
- self.closed = False
- self.count = 0
-
-
-class IterBuffer(FileBuffer) :
-
- newline = '\n'
- end = ""
-
-class LiveChanges(AjaxPage) :
- """ Base class that implements live changes.
-
- The server provides a shared stream. The clients can
- subscribe to the stream events via an potentially infinite
- streamEvents request and and fire an event with a writeEvent
- operation.
-
- Let's set up a consumer :
-
- >>> consumer = LiveChanges(None, TestRequest())
- >>> result = consumer.streamEvents()
- >>> stream = result.body
-
- Now we create a writer and fire two events :
-
- >>> writer = LiveChanges(None, TestRequest())
- >>> writer.writeEvent("1")
- >>> writer.writeEvent("2")
-
- The consumer can access them in one call :
-
- >>> print stream.next()
- <html><body><p>1</p>
-
-
- If all events are consumed we get a dummy :
-
- >>> print stream.next()
- <p>2</p>
-
-
- >>> writer.writeEvent("3")
- >>> print stream.next()
- <p>3</p>
-
-
- We can also decide to close the connection :
-
- >>> LiveChanges(None, TestRequest()).closeStream()
- >>> print stream.next()
- </body></html>
-
-
- """
-
- implements(ILiveChanges)
-
-
- buffer = FileBuffer() # shared file buffer
-
-
- def streamEvents(self) :
- """ Dummy implementation of an unlimited stream of change events.
-
- Should be overwritten by the target class.
-
- """
-
- from zope.publisher.http import IResult
- from zope.publisher.http import DirectResult
-
- from twisted.web2.wsgi import FileWrapper
-
- self.buffer.open()
- body = FileWrapper(self.buffer, 20)
-
- self.buffer.write("<html><body>")
- headers = [('Content-Type', 'text/html')]
- return DirectResult(body, headers)
-
- def writeEvent(self, data) :
- """ Default implementation of a write operation
- to a shared stream.
- """
-
- self.buffer.write('<p>' + data + '</p>')
-
- def closeStream(self) :
- """ Default implementation of a write operation
- to a shared stream.
- """
- self.buffer.close()
More information about the Checkins
mailing list