1
# -*- test-case-name: nevow.test.test_livepage -*-
2
# Copyright (c) 2004 Divmod.
3
# See LICENSE for details.
6
Previous generation Nevow Comet support. Do not use this module.
11
import itertools, types
14
from zope.interface import implements, Interface
16
from twisted.internet import defer, error
17
from twisted.internet.task import LoopingCall
18
from twisted.python import log
20
from nevow import tags, inevow, context, static, flat, rend, url, util, stan
22
# If you need to debug livepage itself or your livepage app, set this to true
30
_jslog = file("js.log", "w")
31
_jslog.write("**********\n")
42
class JavascriptContext(context.WovenContext):
43
def __init__(self, parent=None, tag=None, isAttrib=None,
44
inJSSingleQuoteString=None, remembrances=None):
45
super(JavascriptContext, self).__init__(
46
parent, tag, inJS=True, isAttrib=isAttrib,
47
inJSSingleQuoteString=inJSSingleQuoteString,
51
class TimeoutException(Exception):
55
class ClientSideException(Exception):
59
class SingleQuote(object):
60
def __init__(self, children):
61
self.children = children
64
return "%s(%s)" % (type(self).__name__, self.children)
67
def flattenSingleQuote(singleQuote, ctx):
68
new = JavascriptContext(ctx, tags.invisible[singleQuote], inJSSingleQuoteString=True)
69
return flat.serialize(singleQuote.children, new)
70
flat.registerFlattener(flattenSingleQuote, SingleQuote)
75
Stan for Javascript. There is a convenience instance of this
76
class named "js" in the livepage module which you should use
77
instead of the _js class directly.
79
Marker indicating literal Javascript should be rendered.
80
No escaping will be performed.
82
When inside a JavascriptContext, Nevow will automatically put
83
apostrophe quote marks around any Python strings it renders.
84
This makes turning a Python string into a JavaScript string very
85
easy. However, there are often situations where you wish to
86
generate some literal Javascript code and do not wish quote
87
marks to be placed around it. In this situation, the js object
90
The simplest usage is to simply pass a python string to js.
91
When the js object is rendered, the python string will be
92
rendered as if it were literal javascript. For example::
94
client.send(js(\"alert('hello')\"))
96
However, to make the generation of Javascript more
97
convenient, the js object also provides safe implementations
98
of __getattr__, __call__, and __getitem__. See the following
99
examples to get an idea of how to use it. The Python code
100
is to the left of the -> and the Javascript which results is
103
js(\"alert('any javascript you like')\") -> alert('any javascript you like')
105
js.window.title -> window.title
107
js.document.getElementById('foo') -> document.getElementById('foo')
109
js.myFunction('my argument') -> myFunction('my argument')
111
js.myFunction(True, 5, \"it's a beautiful day\") -> myFunction(true, 5, 'it\\'s a beautiful day')
113
js.document.all[\"something\"] -> document.all['something']
117
XXX TODO support javascript object literals somehow? (They look like dicts)
120
js[\"one\": 1, \"two\": 2] -> {\"one\": 1, \"two\": 2}
122
The livepage module includes many convenient instances of the js object.
123
It includes the literals::
130
It includes shorthand for commonly called javascript functions::
133
get -> document.getElementById
135
append -> nevow_appendNode
136
prepend -> nevow.prependNode
137
insert -> nevow.insertNode
139
It includes convenience calls against the client-side server object::
141
server.handle('callMe') -> server.handle('callMe')
143
It includes commonly-used fragments of javascript::
145
stop -> ; return false;
148
Stop is used to prevent the browser from executing it's default
149
event handler. For example::
151
button(onclick=[server.handle('click'), stop]) -> <button onclick=\"server.handle('click'); return false;\" />
153
EOL is currently required to separate statements (this requirement
154
may go away in the future). For example::
160
XXX TODO: investigate whether rendering a \\n between list elements
161
in a JavascriptContext has any ill effects.
164
def __init__(self, name=None):
167
if isinstance(name, str):
168
name = [stan.raw(name)]
169
self._children = name
171
def __getattr__(self, name):
173
raise RuntimeError("Can't clone")
175
newchildren = self._children[:]
176
newchildren.append(stan.raw('.'+name))
177
return self.__class__(newchildren)
178
return self.__class__(name)
180
def __call__(self, *args):
181
if not self._children:
182
return self.__class__(args[0])
183
newchildren = self._children[:]
187
basestring, stan.Tag, types.FunctionType,
188
types.MethodType, types.UnboundMethodType)):
189
x = stan.raw("'"), SingleQuote(x), stan.raw("'")
190
stuff.append((x, stan.raw(',')))
192
stuff[-1] = stuff[-1][0]
193
newchildren.extend([stan.raw('('), stuff, stan.raw(')')])
194
return self.__class__(newchildren)
196
def __getitem__(self, args):
197
if not isinstance(args, (tuple, list)):
199
newchildren = self._children[:]
200
stuff = [(x, stan.raw(',')) for x in args]
202
stuff[-1] = stuff[-1][0]
203
newchildren.extend([stan.raw("["), stuff, stan.raw("]")])
204
return self.__class__(newchildren)
207
"""Prevent an infinite loop if someone tries to do
210
raise NotImplementedError, "js instances are not iterable. (%r)" % (self, )
213
return "%s(%r)" % (type(self).__name__, self._children)
214
def flattenJS(theJS, ctx):
215
new = JavascriptContext(ctx, tags.invisible[theJS])
216
return flat.serialize(theJS._children, new)
217
flat.registerFlattener(flattenJS, _js)
221
document = _js('document')
222
get = document.getElementById
223
window = _js('window')
226
server = _js('server')
228
stop = _js('; return false;')
231
set = js.nevow_setNode
232
append = js.nevow_appendNode
233
prepend = js.nevow_prependNode
234
insert = js.nevow_insertNode
237
def assign(where, what):
238
"""Assign what to where. Equivalent to
241
return _js([where, stan.raw(" = "), what])
247
def var(where, what):
248
"""Define local variable 'where' and assign 'what' to it.
249
Equivalent to var where = what;
251
return _js([stan.raw("var "), where, stan.raw(" = "), what, stan.raw(";")])
254
def anonymous(block):
256
Turn block (any stan) into an anonymous JavaScript function
257
which takes no arguments. Equivalent to::
263
return _js([stan.raw("function() {\n"), block, stan.raw("\n}")])
266
class IClientHandle(Interface):
267
def hookupOutput(output, finisher=None):
268
"""hook up an output conduit to this live evil instance.
272
"""send a script through the output conduit to the browser.
273
If no output conduit is yet hooked up, buffer the script
277
def handleInput(identifier, *args):
278
"""route some input from the browser to the appropriate
283
class IHandlerFactory(Interface):
284
def locateHandler(ctx, name):
285
"""Locate a handler callable with the given name.
289
class _transient(object):
290
def __init__(self, transientId, arguments=None):
291
self.transientId = transientId
292
if arguments is None:
294
elif isinstance(arguments, tuple):
295
arguments = list(arguments)
297
raise TypeError, "Arguments must be None or tuple"
298
self.arguments = arguments
300
def __call__(self, *arguments):
301
return type(self)(self.transientId, arguments)
304
def flattenTransient(transient, ctx):
305
thing = js.server.handle("--transient.%s" % (transient.transientId, ), *transient.arguments)
306
return flat.serialize(thing, ctx)
307
flat.registerFlattener(flattenTransient, _transient)
310
class ClientHandle(object):
311
"""An object which represents the client-side webbrowser.
313
implements(IClientHandle)
317
def __init__(self, livePage, handleId, refreshInterval, targetTimeoutCount):
318
self.refreshInterval = refreshInterval
319
self.targetTimeoutCount = targetTimeoutCount
320
self.timeoutCount = 0
321
self.livePage = livePage
322
self.handleId = handleId
323
self.outputBuffer = []
324
self.bufferDeferreds = []
326
self.closeNotifications = []
327
self.firstTime = True
328
self.timeoutLoop = LoopingCall(self.checkTimeout)
330
self.timeoutLoop.start(self.refreshInterval)
331
self._transients = {}
332
self.transientCounter = itertools.count().next
333
self.nextId = itertools.count().next ## For backwards compatibility with handler
335
def transient(self, what, *args):
336
"""Register a transient event handler, 'what'.
337
The callable 'what' can only be invoked by the
338
client once before being garbage collected.
339
Additional attempts to invoke the handler
342
transientId = str(self.transientCounter())
343
self._transients[transientId] = what
344
return _transient(transientId, args)
346
def popTransient(self, transientId):
347
"""Remove a transient previously registered
348
by a call to transient. Normally, this will be done
349
automatically when the transient is invoked.
350
However, you can invoke it yourself if you wish
351
to revoke the client's capability to call the
354
if DEBUG: print "TRANSIENTS", self._transients
355
return self._transients.pop(transientId)
357
def _actuallySend(self, scripts):
358
output = self.outputConduit
361
#print "WRITER", write
362
written.append(write)
363
def finisher(finish):
364
towrite = '\n'.join(written)
365
jslog("<<<<<<\n%s\n" % towrite)
366
output.callback(towrite)
367
flat.flattenFactory(scripts, self.outputContext, writer, finisher)
368
self.outputConduit = None
369
self.outputContext = None
371
def send(self, *script):
372
"""Send the stan "script", which can be flattened to javascript,
373
to the browser which is connected to this handle, and evaluate
374
it in the context of the browser window.
376
if self.outputConduit:
377
self._actuallySend(script)
379
self.outputBuffer.append(script)
380
self.outputBuffer.append(eol)
382
def setOutput(self, ctx, output):
383
self.timeoutCount = 0
384
self.outputContext = ctx
385
self.outputConduit = output
386
if self.outputBuffer:
387
if DEBUG: print "SENDING BUFFERED", self.outputBuffer
388
self._actuallySend(self.outputBuffer)
389
self.outputBuffer = []
391
def _actuallyPassed(self, result, deferreds):
395
def _actuallyFailed(self, failure, deferreds):
399
def checkTimeout(self):
400
if self.outputConduit is not None:
401
## The browser is waiting for us, send a noop.
402
self.send(_js('null;'))
404
self.timeoutCount += 1
405
if self.timeoutCount >= self.targetTimeoutCount:
406
## This connection timed out.
409
"This connection did not ACK in at least %s seconds." % (
410
self.targetTimeoutCount * self.refreshInterval, )))
412
def outputGone(self, failure, output):
413
# assert output == self.outputConduit
414
# Twisted errbacks with a ConnectionDone when the client closes the
415
# connection cleanly. Pretend it didn't happen and carry on.
416
self.outputConduit = None
417
if failure.check(error.ConnectionDone):
418
self._closeComplete()
420
self._closeComplete(failure)
423
def _closeComplete(self, failure=None):
427
self.timeoutLoop.stop()
428
self.timeoutLoop = None
429
for notify in self.closeNotifications[:]:
430
if failure is not None:
431
notify.errback(failure)
433
notify.callback(None)
434
self.closeNotifications = []
436
def notifyOnClose(self):
437
"""This will return a Deferred that will be fired when the
438
connection is closed 'normally', i.e. in response to handle.close()
439
. If the connection is lost in any other way (because the browser
440
navigated to another page, the browser was shut down, the network
441
connection was lost, or the timeout was reached), this will errback
444
self.closeNotifications.append(d)
447
def close(self, executeScriptBeforeClose=""):
448
if DEBUG: print "CLOSE WAS CALLED"
449
d = self.notifyOnClose()
450
self.send(js.nevow_closeLive(executeScriptBeforeClose))
453
def set(self, where, what):
454
self.send(js.nevow_setNode(where, what))
456
def prepend(self, where, what):
457
self.send(js.nevow_prependNode(where, what))
459
def append(self, where, what):
460
self.send(js.nevow_appendNode(where, what))
462
def alert(self, what):
463
self.send(js.alert(what))
465
def call(self, what, *args):
466
self.send(js(what)(*args))
468
def sendScript(self, string):
470
"[0.5] nevow.livepage.ClientHandle.sendScript is deprecated, use send instead.",
476
class DefaultClientHandleFactory(object):
477
clientHandleClass = ClientHandle
480
self.clientHandles = {}
481
self.handleCounter = itertools.count().next
483
def newClientHandle(self, livePage, refreshInterval, targetTimeoutCount):
484
handleid = str(self.handleCounter())
485
handle = self.clientHandleClass(
486
livePage, handleid, refreshInterval, targetTimeoutCount)
487
self.clientHandles[handleid] = handle
488
handle.notifyOnClose().addBoth(lambda ign: self.deleteHandle(handleid))
491
def deleteHandle(self, handleid):
492
del self.clientHandles[handleid]
494
def getHandleForId(self, handleId):
495
"""Override this to restore old handles on demand.
497
return self.clientHandles[handleId]
499
theDefaultClientHandleFactory = DefaultClientHandleFactory()
502
class OutputHandlerResource:
503
implements(inevow.IResource)
505
def __init__(self, clientHandle):
506
self.clientHandle = clientHandle
508
def locateChild(self, ctx, segments):
509
raise NotImplementedError()
511
def renderHTTP(self, ctx):
512
request = inevow.IRequest(ctx)
513
neverEverCache(request)
514
activeChannel(request)
515
ctx.remember(jsExceptionHandler, inevow.ICanHandleException)
516
request.channel._savedTimeOut = None # XXX TODO
518
request.notifyFinish().addErrback(self.clientHandle.outputGone, d)
519
jsContext = JavascriptContext(ctx, tags.invisible())
520
self.clientHandle.livePage.rememberStuff(jsContext)
521
jsContext.remember(self.clientHandle, IClientHandle)
522
if self.clientHandle.firstTime:
523
self.clientHandle.livePage.goingLive(jsContext, self.clientHandle)
524
self.clientHandle.firstTime = False
525
self.clientHandle.setOutput(jsContext, d)
529
class InputHandlerResource:
530
implements(inevow.IResource)
532
def __init__(self, clientHandle):
533
self.clientHandle = clientHandle
535
def locateChild(self, ctx, segments):
536
raise NotImplementedError()
538
def renderHTTP(self, ctx):
539
self.clientHandle.timeoutCount = 0
541
request = inevow.IRequest(ctx)
542
neverEverCache(request)
543
activeChannel(request)
544
ctx.remember(self.clientHandle, IClientHandle)
545
ctx.remember(jsExceptionHandler, inevow.ICanHandleException)
546
self.clientHandle.livePage.rememberStuff(ctx)
548
handlerName = request.args['handler-name'][0]
549
arguments = request.args.get('arguments', ())
550
jslog(">>>>>>\n%s %s\n" % (handlerName, arguments))
551
if handlerName.startswith('--transient.'):
552
handler = self.clientHandle.popTransient(handlerName.split('.')[-1])
554
handler = self.clientHandle.livePage.locateHandler(
555
ctx, request.args['handler-path'],
558
jsContext = JavascriptContext(ctx, tags.invisible[handler])
567
writestr = ''.join(towrite)
568
jslog("<><><>\n%s\n" % (writestr, ))
569
request.write(writestr)
573
result = handler(jsContext, *arguments)
574
jslog("RESULT ", result)
577
return defer.succeed('')
578
return self.clientHandle.livePage.flattenFactory(result, jsContext,
583
class DefaultClientHandlesResource(object):
584
implements(inevow.IResource)
587
'input': InputHandlerResource,
588
'output': OutputHandlerResource,
591
clientFactory = theDefaultClientHandleFactory
593
def locateChild(self, ctx, segments):
594
handleId = segments[0]
595
handlerType = segments[1]
596
client = self.clientFactory.clientHandles[handleId]
598
return self.clientResources[handlerType](client), segments[2:]
600
theDefaultClientHandlesResource = DefaultClientHandlesResource()
602
class attempt(defer.Deferred):
604
Attempt to do 'stuff' in the browser. callback on the server
605
if 'stuff' executes without raising an exception. errback on the
606
server if 'stuff' raises a JavaScript exception in the client.
613
C = IClientHandle(ctx)
615
attempt(js("1+1")).addCallback(printIt))
618
attempt(js("thisWillFail")).addErrback(printIt))
620
def __init__(self, stuff):
622
defer.Deferred.__init__(self)
625
def flattenAttemptDeferred(d, ctx):
626
def attemptComplete(ctx, result, reason=None):
627
if result == 'success':
630
d.errback(ClientSideException(reason))
631
transient = IClientHandle(ctx).transient(attemptComplete)
632
return flat.serialize([
638
transient('success'),
642
transient('failure', js.e),
645
flat.registerFlattener(flattenAttemptDeferred, attempt)
648
class IOutputEvent(Interface):
651
class IInputEvent(Interface):
655
class ExceptionHandler(object):
656
def renderHTTP_exception(self, ctx, failure):
657
log.msg("Exception during input event:")
659
request = inevow.IRequest(ctx)
660
request.write("throw new Error('Server side error: %s')" % (failure.getErrorMessage().replace("'", "\\'").replace("\n", "\\\n"), ))
663
def renderInlineException(self, ctx, reason):
664
"""TODO: I don't even think renderInlineException is ever called by anybody
669
jsExceptionHandler = ExceptionHandler()
672
def neverEverCache(request):
673
""" Set headers to indicate that the response to this request should never,
676
request.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate')
677
request.setHeader('Pragma', 'no-cache')
679
def activeChannel(request):
680
"""Mark this connection as a 'live' channel by setting the Connection: close
681
header and flushing all headers immediately.
683
request.setHeader("Connection", "close")
686
class LivePage(rend.Page):
688
A Page which is Live provides asynchronous, bidirectional RPC between
689
Python on the server and JavaScript in the client browser. A LivePage must
690
include the "liveglue" JavaScript which includes a unique identifier which
691
is assigned to every page render of a LivePage and the JavaScript required
692
for the client to communicate asynchronously with the server.
694
A LivePage grants the client browser the capability of calling server-side
695
Python methods using a small amount of JavaScript code. There are two
696
types of Python handler methods, persistent handlers and transient handlers.
698
- To grant the client the capability to call a persistent handler over and over
699
as many times as it wishes, subclass LivePage and provide handle_foo
700
methods. The client can then call handle_foo by executing the following
705
handle_foo will be invoked because the default implementation of
706
locateHandler looks for a method prefixed handle_*. To change this,
707
override locateHandler to do what you wish.
709
- To grant the client the capability of calling a handler once and
710
exactly once, use ClientHandle.transient to register a callable and
711
embed the return result in a page to render JavaScript which will
712
invoke the transient handler when executed. For example::
714
def render_clickable(self, ctx, data):
716
return livepage.alert(\"Hello, world. You can only click me once.\")
718
return ctx.tag(onclick=IClientHandle(ctx).transient(hello))
720
The return result of transient can also be called to pass additional
721
arguments to the transient handler. For example::
723
def render_choice(self, ctx, data):
724
def chosen(ctx, choseWhat):
727
[\"Thanks for choosing \", choseWhat])
729
chooser = IClientHandle(ctx).transient(chosen)
731
return span(id=\"choosable\")[
733
p(onclick=chooser(\"one\"))[\"One\"],
734
p(onclick=chooser(\"two\"))[\"Two\"]]
736
Note that the above situation displays temporary UI to the
737
user. When the user invokes the chosen handler, the UI which
738
allowed the user to invoke the chosen handler is removed from
739
the client. Thus, it is important that the transient registration
740
is deleted once it is invoked, otherwise uncollectable garbage
741
would accumulate in the handler dictionary. It is also important
742
that either the one or the two button consume the same handler,
743
since it is an either/or choice. If two handlers were registered,
744
the untaken choice would be uncollectable garbage.
747
targetTimeoutCount = 3
749
clientFactory = theDefaultClientHandleFactory
751
def renderHTTP(self, ctx):
752
if not self.cacheable:
753
neverEverCache(inevow.IRequest(ctx))
754
return rend.Page.renderHTTP(self, ctx)
756
def locateHandler(self, ctx, path, name):
757
### XXX TODO: Handle path
758
return getattr(self, 'handle_%s' % (name, ))
760
def goingLive(self, ctx, handle):
761
"""This particular LivePage instance is 'going live' from the
762
perspective of the ClientHandle 'handle'. Override this to
763
get notified when a new browser window observes this page.
765
This means that a new user is now looking at the page, an old
766
user has refreshed the page, or an old user has opened a new
767
window or tab onto the page.
769
This is the first time the ClientHandle instance is available
770
for general use by the server. This Page may wish to keep
771
track of the ClientHandle instances depending on how your
772
application is set up.
776
def child_livepage_client(self, ctx):
777
return theDefaultClientHandlesResource
779
# child_nevow_glue.js = static.File # see below
781
def render_liveid(self, ctx, data):
782
warnings.warn("You don't need a liveid renderer any more; just liveglue is fine.",
786
cacheable = False # Set this to true to use ***HIGHLY***
787
# EXPERIMENTAL lazy ID allocation feature,
788
# which will allow your LivePage instances to
789
# be cached by clients.
791
def render_liveglue(self, ctx, data):
792
if not self.cacheable:
793
handleId = "'", self.clientFactory.newClientHandle(
795
self.refreshInterval,
796
self.targetTimeoutCount).handleId, "'"
801
tags.script(type="text/javascript")[
802
"var nevow_clientHandleId = ", handleId ,";"],
803
tags.script(type="text/javascript",
804
src=url.here.child('nevow_glue.js'))
810
'child_nevow_glue.js',
812
util.resource_filename('nevow', 'liveglue.js'),
816
glue = tags.directive('liveglue')
821
##### BACKWARDS COMPATIBILITY CODE
824
ctsTemplate = "nevow_clientToServerEvent('%s',this,''%s)%s"
825
handledEventPostlude = '; return false;'
828
class handler(object):
832
def __init__(self, *args, **kw):
833
"""**DEPRECATED** [0.5]
835
Handler is now deprecated. To expose server-side code to the client
836
to be called by JavaScript, read the LivePage docstring.
839
"[0.5] livepage.handler is deprecated; Provide handle_foo methods (or override locateHandler) on your LivePage and use (in javascript) server.handle('foo'), or use ClientHandle.transient to register a one-shot handler capability.",
842
## Handle working like a 2.4 decorator where calling handler returns a decorator
843
if not callable(args[0]) or isinstance(args[0], _js):
846
self.callme = args[0]
847
self(*args[1:], **kw)
849
def __call__(self, *args, **kw):
850
if self.callme is None:
851
self.callme = args[0]
855
self.outsideAttribute = kw.get('outsideAttribute')
856
bubble = kw.get('bubble')
860
self.postlude = handledEventPostlude
862
if 'identifier' in kw:
863
self.identifier = kw['identifier']
867
content = property(lambda self: flt(self))
870
def flattenHandler(handler, ctx):
871
client = IClientHandle(ctx)
872
iden = handler.identifier
874
iden = client.nextId()
875
iden = '--handler-%s' % (iden, )
876
## TODO this should be the IHandlerFactory instead of IResource
877
setattr(IHandlerFactory(ctx), 'handle_%s' % (iden, ), handler.callme)
878
isAttrib = not handler.outsideAttribute
879
new = JavascriptContext(ctx, tags.invisible[handler.args], isAttrib=isAttrib)
882
js.nevow_clientToServerEvent(*(iden, this, '') + handler.args),
884
rv += handler.postlude
886
flat.registerFlattener(flattenHandler, handler)
889
def flt(stan, quote=True, client=None, handlerFactory=None):
890
"""Flatten some stan to a string suitable for embedding in a javascript
893
If quote is True, apostrophe, quote, and newline will be quoted
895
warnings.warn("[0.5] livepage.flt is deprecated. Don't use it.", DeprecationWarning, 2)
896
from nevow import testutil
897
fr = testutil.FakeRequest()
898
ctx = context.RequestContext(tag=fr)
899
ctx.remember(client, IClientHandle)
900
ctx.remember(handlerFactory, IHandlerFactory)
901
ctx.remember(None, inevow.IData)
903
fl = flat.flatten(stan, ctx=ctx)
905
fl = fl.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n')