1
# -*- test-case-name: nevow.test.test_athena -*-
3
import itertools, os, re, warnings
5
from zope.interface import implements
7
from twisted.internet import defer, error, reactor
8
from twisted.python import log, failure, context
9
from twisted.python.util import sibpath
10
from twisted import plugin
12
from nevow import inevow, plugins, flat, _flat
13
from nevow import rend, loaders, static
14
from nevow import json, util, tags, guard, stan
15
from nevow.util import CachedFile
16
from nevow.useragent import UserAgent, browsers
17
from nevow.url import here, URL
19
from nevow.page import Element, renderer
21
ATHENA_XMLNS_URI = "http://divmod.org/ns/athena/0.7"
22
ATHENA_RECONNECT = "__athena_reconnect__"
26
Allow one or more methods to be invoked by the client::
28
| class Foo(LiveElement):
29
| def twiddle(self, x, y):
31
| def frob(self, a, b):
33
| expose(twiddle, frob)
35
The Widget for Foo will be allowed to invoke C{twiddle} and C{frob}.
40
class OrphanedFragment(Exception):
42
Raised when an operation requiring a parent is attempted on an unattached
48
class LivePageError(Exception):
50
Base exception for LivePage errors.
52
jsClass = u'Divmod.Error'
56
class NoSuchMethod(LivePageError):
58
Raised when an attempt is made to invoke a method which is not defined or
61
jsClass = u'Nevow.Athena.NoSuchMethod'
63
def __init__(self, objectID, methodName):
64
self.objectID = objectID
65
self.methodName = methodName
66
LivePageError.__init__(self, objectID, methodName)
70
def neverEverCache(request):
72
Set headers to indicate that the response to this request should never,
75
request.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate')
76
request.setHeader('Pragma', 'no-cache')
79
def activeChannel(request):
81
Mark this connection as a 'live' channel by setting the Connection: close
82
header and flushing all headers immediately.
84
request.setHeader("Connection", "close")
89
class MappingResource(object):
91
L{inevow.IResource} which looks up segments in a mapping between symbolic
92
names and the files they correspond to.
94
@type mapping: C{dict}
95
@ivar mapping: A map between symbolic, requestable names (eg,
96
'Nevow.Athena') and C{str} instances which name files containing data
97
which should be served in response.
99
implements(inevow.IResource)
101
def __init__(self, mapping):
102
self.mapping = mapping
105
def renderHTTP(self, ctx):
106
return rend.FourOhFour()
109
def resourceFactory(self, fileName):
111
Retrieve an L{inevow.IResource} which will render the contents of
114
return static.File(fileName)
117
def locateChild(self, ctx, segments):
119
impl = self.mapping[segments[0]]
123
return self.resourceFactory(impl), []
127
def _dependencyOrdered(coll, memo):
129
@type coll: iterable of modules
130
@param coll: The initial sequence of modules.
133
@param memo: A dictionary mapping module names to their dependencies that
134
will be used as a mutable cache.
139
class AthenaModule(object):
141
A representation of a chunk of stuff in a file which can depend on other
142
chunks of stuff in other files.
150
def getOrCreate(cls, name, mapping):
151
# XXX This implementation of getOrCreate precludes the
152
# simultaneous co-existence of several different package
154
if name in cls._modules:
155
return cls._modules[name]
156
mod = cls._modules[name] = cls(name, mapping)
158
getOrCreate = classmethod(getOrCreate)
161
def __init__(self, name, mapping):
163
self.mapping = mapping
166
parent = '.'.join(name.split('.')[:-1])
167
self.packageDeps = [self.getOrCreate(parent, mapping)]
169
self._cache = CachedFile(self.mapping[self.name], self._getDeps)
173
return '%s(%r)' % (self.__class__.__name__, self.name,)
176
_importExpression = re.compile('^// import (.+)$', re.MULTILINE)
177
def _extractImports(self, fileObj):
179
for m in self._importExpression.finditer(s):
180
yield self.getOrCreate(m.group(1).decode('ascii'), self.mapping)
184
def _getDeps(self, jsFile):
186
Calculate our dependencies given the path to our source.
188
depgen = self._extractImports(file(jsFile, 'rU'))
189
return self.packageDeps + dict.fromkeys(depgen).keys()
192
def dependencies(self):
194
Return a list of names of other JavaScript modules we depend on.
196
return self._cache.load()
199
def allDependencies(self, memo=None):
201
Return the transitive closure of dependencies, including this module.
203
The transitive dependencies for this module will be ordered such that
204
any particular module is located after all of its dependencies, with no
205
module occurring more than once.
207
The dictionary passed in for C{memo} will be modified in-place; if it
208
is reused across multiple calls, dependencies calculated during a
209
previous invocation will not be recalculated again.
211
@type memo: C{dict} of C{str: list of AthenaModule}
212
@param memo: A dictionary mapping module names to the modules they
213
depend on that will be used as a mutable cache.
215
@rtype: C{list} of C{AthenaModule}
221
def _getDeps(dependent):
222
if dependent.name in memo:
223
deps = memo[dependent.name]
225
memo[dependent.name] = deps = dependent.dependencies()
228
def _insertDep(dependent):
229
if dependent not in ordered:
230
for dependency in _getDeps(dependent):
231
_insertDep(dependency)
232
ordered.append(dependent)
239
class JSModule(AthenaModule):
241
L{AthenaModule} subclass for dealing with Javascript modules.
247
class CSSModule(AthenaModule):
249
L{AthenaModule} subclass for dealing with CSS modules.
255
class JSPackage(object):
257
A Javascript package.
259
@type mapping: C{dict}
260
@ivar mapping: Mapping between JS module names and C{str} representing
261
filesystem paths containing their implementations.
263
implements(plugin.IPlugin, inevow.IJavascriptPackage)
265
def __init__(self, mapping):
266
self.mapping = mapping
270
def _collectPackageBelow(baseDir, extension):
272
Assume a filesystem package hierarchy starting at C{baseDir}. Collect all
273
files within it ending with C{extension} into a mapping between
274
dot-separated symbolic module names and their corresponding filesystem
277
Note that module/package names beginning with . are ignored.
279
@type baseDir: C{str}
280
@param baseDir: A path to the root of a package hierarchy on a filesystem.
282
@type extension: C{str}
283
@param extension: The filename extension we're interested in (e.g. 'css'
287
@return: Mapping between C{unicode} module names and their corresponding
288
C{str} filesystem paths.
291
EMPTY = sibpath(__file__, 'empty-module.' + extension)
293
_revMap = {baseDir: ''}
294
for (root, dirs, filenames) in os.walk(baseDir):
296
dirs[:] = [d for d in dirs if not d.startswith('.')]
300
path = os.path.join(root, dir, '__init__.' + extension)
301
if not os.path.exists(path):
303
mapping[unicode(name, 'ascii')] = path
304
_revMap[os.path.join(root, dir)] = name + '.'
307
if fn.startswith('.'):
310
if fn == '__init__.' + extension:
313
if not fn.endswith('.' + extension):
316
name = stem + fn[:-(len(extension) + 1)]
317
path = os.path.join(root, fn)
318
mapping[unicode(name, 'ascii')] = path
323
class AutoJSPackage(object):
325
A L{inevow.IJavascriptPackage} implementation that scans an on-disk
326
hierarchy locating modules and packages.
328
@type baseDir: C{str}
329
@ivar baseDir: A path to the root of a JavaScript packages/modules
330
filesystem hierarchy.
332
implements(plugin.IPlugin, inevow.IJavascriptPackage)
334
def __init__(self, baseDir):
335
self.mapping = _collectPackageBelow(baseDir, 'js')
339
class AutoCSSPackage(object):
341
Like L{AutoJSPackage}, but for CSS packages. Modules within this package
342
can be referenced by L{LivePage.cssModule} or L{LiveElement.cssModule}.
344
implements(plugin.IPlugin, inevow.ICSSPackage)
346
def __init__(self, baseDir):
347
self.mapping = _collectPackageBelow(baseDir, 'css')
351
def allJavascriptPackages():
353
Return a dictionary mapping JavaScript module names to local filenames
354
which implement those modules. This mapping is constructed from all the
355
C{IJavascriptPackage} plugins available on the system. It also includes
356
C{Nevow.Athena} as a special case.
359
for p in plugin.getPlugIns(inevow.IJavascriptPackage, plugins):
365
def allCSSPackages():
367
Like L{allJavascriptPackages}, but for CSS packages.
370
for p in plugin.getPlugIns(inevow.ICSSPackage, plugins):
376
class JSDependencies(object):
378
Keeps track of which JavaScript files depend on which other
379
JavaScript files (because JavaScript is a very poor language and
380
cannot do this itself).
384
def __init__(self, mapping=None):
387
self._loadPlugins = True
389
self.mapping = mapping
392
def getModuleForName(self, className):
394
Return the L{JSModule} most likely to define the given name.
396
if self._loadPlugins:
397
self.mapping.update(allJavascriptPackages())
398
self._loadPlugins = False
407
jsMod = '.'.join(jsMod.split('.')[:-1])
409
return JSModule.getOrCreate(jsMod, self.mapping)
410
raise RuntimeError("Unknown class: %r" % (className,))
411
getModuleForClass = getModuleForName
414
jsDeps = JSDependencies()
418
class CSSRegistry(object):
420
Keeps track of a set of CSS modules.
422
def __init__(self, mapping=None):
428
self.mapping = mapping
429
self._loadPlugins = loadPlugins
432
def getModuleForName(self, moduleName):
434
Turn a CSS module name into an L{AthenaModule}.
436
@type moduleName: C{unicode}
440
if self._loadPlugins:
441
self.mapping.update(allCSSPackages())
442
self._loadPlugins = False
444
self.mapping[moduleName]
446
raise RuntimeError('Unknown CSS module: %r' % (moduleName,))
447
return CSSModule.getOrCreate(moduleName, self.mapping)
449
_theCSSRegistry = CSSRegistry()
453
class JSException(Exception):
455
Exception class to wrap remote exceptions from JavaScript.
460
class JSCode(object):
462
Class for mock code objects in mock JS frames.
464
def __init__(self, name, filename):
466
self.co_filename = filename
470
class JSFrame(object):
472
Class for mock frame objects in JS client-side traceback wrappers.
474
def __init__(self, func, fname, ln):
478
self.f_code = JSCode(func, fname)
483
class JSTraceback(object):
485
Class for mock traceback objects representing client-side JavaScript
488
def __init__(self, frame, ln):
489
self.tb_frame = frame
495
def parseStack(stack):
497
Extract function name, file name, and line number information from the
498
string representation of a JavaScript trace-back.
501
for line in stack.split('\n'):
504
func, rest = line.split('@', 1)
508
divide = rest.rfind(':')
512
fname, ln = rest[:divide], rest[divide + 1:]
514
frames.insert(0, (func, fname, ln))
519
def buildTraceback(frames, modules):
521
Build a chain of mock traceback objects from a serialized Error (or other
522
exception) object, and return the head of the chain.
526
for func, fname, ln in frames:
527
fname = modules.get(fname.split('/')[-1], fname)
528
frame = JSFrame(func, fname, ln)
529
tb = JSTraceback(frame, ln)
539
def getJSFailure(exc, modules):
541
Convert a serialized client-side exception to a Failure.
543
text = '%s: %s' % (exc[u'name'], exc[u'message'])
547
frames = parseStack(exc[u'stack'])
549
return failure.Failure(JSException(text), exc_tb=buildTraceback(frames, modules))
553
class LivePageTransport(object):
554
implements(inevow.IResource)
556
def __init__(self, messageDeliverer, useActiveChannels=True):
557
self.messageDeliverer = messageDeliverer
558
self.useActiveChannels = useActiveChannels
561
def locateChild(self, ctx, segments):
565
def renderHTTP(self, ctx):
566
req = inevow.IRequest(ctx)
568
if self.useActiveChannels:
571
requestContent = req.content.read()
572
messageData = json.parse(requestContent)
574
response = self.messageDeliverer.basketCaseReceived(ctx, messageData)
575
response.addCallback(json.serialize)
576
req.notifyFinish().addErrback(lambda err: self.messageDeliverer._unregisterDeferredAsOutputChannel(response))
581
class LivePageFactory:
587
def addClient(self, client):
588
clientID = self._newClientID()
589
self.clients[clientID] = client
591
log.msg("Rendered new LivePage %r: %r" % (client, clientID))
594
def getClient(self, clientID):
595
return self.clients[clientID]
597
def removeClient(self, clientID):
598
# State-tracking bugs may make it tempting to make the next line a
599
# 'pop', but it really shouldn't be; if the Page instance with this
600
# client ID is already gone, then it should be gone, which means that
601
# this method can't be called with that argument.
602
del self.clients[clientID]
604
log.msg("Disconnected old LivePage %r" % (clientID,))
606
def _newClientID(self):
607
return guard._sessionCookie()
610
_thePrivateAthenaResource = static.File(util.resource_filename('nevow', 'athena_private'))
613
class ConnectFailed(Exception):
617
class ConnectionLost(Exception):
624
class ReliableMessageDelivery(object):
626
A reliable message delivery abstraction over a possibly unreliable transport.
628
@type livePage: L{LivePage}
629
@ivar livePage: The page this delivery is associated with.
631
@type connectTimeout: C{int}
632
@ivar connectTimeout: The amount of time (in seconds) to wait for the
633
initial connection, before timing out.
635
@type transportlessTimeout: C{int}
636
@ivar transportlessTimeout: The amount of time (in seconds) to wait for
637
another transport to connect if none are currently connected, before
640
@type idleTimeout: C{int}
641
@ivar idleTimeout: The maximum amount of time (in seconds) to leave a
642
connected transport, before sending a noop response.
644
@type connectionLost: callable or C{None}
645
@ivar connectionLost: A callback invoked with a L{failure.Failure} if the
646
connection with the client is lost (due to a timeout, for example).
648
@type scheduler: callable or C{None}
649
@ivar scheduler: If passed, this is used in place of C{reactor.callLater}.
651
@type connectionMade: callable or C{None}
652
@ivar connectionMade: A callback invoked with no arguments when it first
653
becomes possible to to send a message to the client.
659
outgoingAck = -1 # sequence number which has been acknowledged
660
# by this end of the connection.
662
outgoingSeq = -1 # sequence number of the next message to be
663
# added to the outgoing queue.
667
connectTimeout=60, transportlessTimeout=30, idleTimeout=300,
670
connectionMade=None):
671
self.livePage = livePage
674
self.connectTimeout = connectTimeout
675
self.transportlessTimeout = transportlessTimeout
676
self.idleTimeout = idleTimeout
677
if scheduler is None:
678
scheduler = reactor.callLater
679
self.scheduler = scheduler
680
self._transportlessTimeoutCall = self.scheduler(self.connectTimeout, self._connectTimedOut)
681
self.connectionMade = connectionMade
682
self.connectionLost = connectionLost
685
def _connectTimedOut(self):
686
self._transportlessTimeoutCall = None
687
self.connectionLost(failure.Failure(ConnectFailed("Timeout")))
690
def _transportlessTimedOut(self):
691
self._transportlessTimeoutCall = None
692
self.connectionLost(failure.Failure(ConnectionLost("Timeout")))
695
def _idleTimedOut(self):
696
output, timeout = self.outputs.pop(0)
698
self._transportlessTimeoutCall = self.scheduler(self.transportlessTimeout, self._transportlessTimedOut)
699
output([self.outgoingAck, []])
702
def _sendMessagesToOutput(self, output):
703
log.msg(athena_send_messages=True, count=len(self.messages))
704
output([self.outgoingAck, self.messages])
711
def _trySendMessages(self):
713
If we have pending messages and there is an available transport, then
714
consume it to send the messages.
716
if self.messages and self.outputs:
717
output, timeout = self.outputs.pop(0)
720
self._transportlessTimeoutCall = self.scheduler(self.transportlessTimeout, self._transportlessTimedOut)
721
self._sendMessagesToOutput(output)
726
Decrement the pause counter and if the resulting state is not still
727
paused try to flush any pending messages and expend excess outputs.
730
if self._paused == 0:
731
self._trySendMessages()
735
def addMessage(self, msg):
739
self.outgoingSeq += 1
740
self.messages.append((self.outgoingSeq, msg))
741
if not self._paused and self.outputs:
742
output, timeout = self.outputs.pop(0)
745
self._transportlessTimeoutCall = self.scheduler(self.transportlessTimeout, self._transportlessTimedOut)
746
self._sendMessagesToOutput(output)
749
def addOutput(self, output):
750
if not self._connected:
751
self._connected = True
752
self.connectionMade()
753
if self._transportlessTimeoutCall is not None:
754
self._transportlessTimeoutCall.cancel()
755
self._transportlessTimeoutCall = None
756
if not self._paused and self.messages:
757
self._transportlessTimeoutCall = self.scheduler(self.transportlessTimeout, self._transportlessTimedOut)
758
self._sendMessagesToOutput(output)
761
self._sendMessagesToOutput(output)
763
self.outputs.append((output, self.scheduler(self.idleTimeout, self._idleTimedOut)))
767
assert not self._stopped, "Cannot multiply stop ReliableMessageDelivery"
768
self.addMessage((CLOSE, []))
771
output, timeout = self.outputs.pop(0)
773
self._sendMessagesToOutput(output)
775
if self._transportlessTimeoutCall is not None:
776
self._transportlessTimeoutCall.cancel()
777
self._transportlessTimeoutCall = None
780
def _unregisterDeferredAsOutputChannel(self, deferred):
781
for i in xrange(len(self.outputs)):
782
if self.outputs[i][0].im_self is deferred:
783
output, timeout = self.outputs.pop(i)
789
self._transportlessTimeoutCall = self.scheduler(self.transportlessTimeout, self._transportlessTimedOut)
792
def _createOutputDeferred(self):
794
Create a new deferred, attaching it as an output. If the current
795
state is not paused, try to flush any pending messages and expend
799
self.addOutput(d.callback)
800
if not self._paused and self.outputs:
801
self._trySendMessages()
806
def _flushOutputs(self):
808
Use up all except for one output.
810
This provides ideal behavior for the default HTTP client
811
configuration, since only a maximum of two simultaneous connections
812
are allowed. The remaining one output will let us signal the client
813
at will without preventing the client from establishing new
816
if self.outputs is None:
818
while len(self.outputs) > 1:
819
output, timeout = self.outputs.pop(0)
821
output([self.outgoingAck, []])
824
def basketCaseReceived(self, ctx, basketCase):
826
This is called when some random JSON data is received from an HTTP
829
A 'basket case' is currently a data structure of the form [ackNum, [[1,
830
message], [2, message], [3, message]]]
832
Its name is highly informal because unless you are maintaining this
833
exact code path, you should not encounter it. If you do, something has
836
ack, incomingMessages = basketCase
838
outgoingMessages = self.messages
840
# dequeue messages that our client certainly knows about.
841
while outgoingMessages and outgoingMessages[0][0] <= ack:
842
outgoingMessages.pop(0)
845
log.msg(athena_received_messages=True, count=len(incomingMessages))
846
if incomingMessages[0][0] == UNLOAD:
847
# Page-unload messages are special, because they are not part
848
# of the normal message stream: they are a notification that
849
# the message stream can't continue. Browser bugs force us to
850
# handle this as quickly as possible, since the browser can
851
# lock up hard while waiting for a response to this message
852
# (and the user has already navigated away from the page, so
853
# there's no useful communication that can take place any more)
854
# so only one message is allowed. In the actual Athena JS,
855
# only one is ever sent, so there is no need to handle more.
856
# The structure of the packet is preserved for symmetry,
857
# however, if we ever need to expand on it. Realistically, the
858
# only message that can be usefully processed here is CLOSE.
859
msg = incomingMessages[0][1]
860
self.livePage.liveTransportMessageReceived(ctx, msg)
861
return self._createOutputDeferred()
862
elif self.outgoingAck + 1 >= incomingMessages[0][0]:
863
lastSentAck = self.outgoingAck
864
self.outgoingAck = max(incomingMessages[-1][0], self.outgoingAck)
867
for (seq, msg) in incomingMessages:
868
if seq > lastSentAck:
869
self.livePage.liveTransportMessageReceived(ctx, msg)
871
log.msg("Athena transport duplicate message, discarding: %r %r" %
872
(self.livePage.clientID,
874
d = self._createOutputDeferred()
878
d = defer.succeed([self.outgoingAck, []])
880
"Sequence gap! %r went from %s to %s" %
881
(self.livePage.clientID,
883
incomingMessages[0][0]))
885
d = self._createOutputDeferred()
890
BOOTSTRAP_NODE_ID = 'athena:bootstrap'
891
BOOTSTRAP_STATEMENT = ("eval(document.getElementById('" + BOOTSTRAP_NODE_ID +
892
"').getAttribute('payload'));")
894
class _HasJSClass(object):
896
A utility to share some code between the L{LivePage}, L{LiveElement}, and
897
L{LiveFragment} classes which all have a jsClass attribute that represents
900
@ivar jsClass: a JavaScript class.
901
@type jsClass: L{unicode}
904
def _getModuleForClass(self):
906
Get a L{JSModule} object for the class specified by this object's
909
return jsDeps.getModuleForClass(self.jsClass)
912
def _getRequiredModules(self, memo):
914
Return a list of two-tuples containing module names and URLs at which
915
those modules are accessible. All of these modules must be loaded into
916
the page before this Fragment's widget can be instantiated. modules
920
(dep.name, self.page.getJSModuleURL(dep.name))
922
in self._getModuleForClass().allDependencies(memo)
923
if self.page._shouldInclude(dep.name)]
927
def jsModuleDeclaration(name):
929
Generate Javascript for a module declaration.
934
return '%s%s = {"__name__": "%s"};' % (var, name, name)
938
class _HasCSSModule(object):
940
C{cssModule}-handling code common to L{LivePage}, L{LiveElement} and
943
@ivar cssModule: A CSS module name.
944
@type cssModule: C{unicode} or C{NoneType}
946
def _getRequiredCSSModules(self, memo):
948
Return a list of CSS module URLs.
950
@rtype: C{list} of L{url.URL}
952
if self.cssModule is None:
954
module = self.page.cssModules.getModuleForName(self.cssModule)
956
self.page.getCSSModuleURL(dep.name)
957
for dep in module.allDependencies(memo)
958
if self.page._shouldIncludeCSSModule(dep.name)]
961
def getStylesheetStan(self, modules):
963
Get some stan which will include the given modules.
965
@type modules: C{list} or L{url.URL}
971
rel='stylesheet', type='text/css', href=url)
976
class LivePage(rend.Page, _HasJSClass, _HasCSSModule):
978
A resource which can receive messages from and send messages to the client
979
after the initial page load has completed and which can send messages.
981
@ivar requiredBrowserVersions: A dictionary mapping User-Agent browser
982
names to the minimum supported version of those browsers. Clients
983
using these browsers which are below the minimum version will be shown
984
an alternate page explaining this rather than the normal page content.
986
@ivar unsupportedBrowserLoader: A document loader which will be used to
987
generate the content shown to unsupported browsers.
989
@type _cssDepsMemo: C{dict}
990
@ivar _cssDepsMemo: A cache for CSS module dependencies; by default, this
991
will only be shared within a single page instance.
993
@type _jsDepsMemo: C{dict}
994
@ivar _jsDepsMemo: A cache for JS module dependencies; by default, this
995
will only be shared within a single page instance.
997
@type _didConnect: C{bool}
998
@ivar _didConnect: Initially C{False}, set to C{True} if connectionMade has
1001
@type _didDisconnect: C{bool}
1002
@ivar _didDisconnect: Initially C{False}, set to C{True} if _disconnected
1005
@type _localObjects: C{dict} of C{int} : widget
1006
@ivar _localObjects: Mapping from an object ID to a Python object that will
1007
accept messages from the client.
1009
@type _localObjectIDCounter: C{callable} returning C{int}
1010
@ivar _localObjectIDCounter: A callable that will return a new
1011
locally-unique object ID each time it is called.
1013
jsClass = u'Nevow.Athena.PageWidget'
1016
factory = LivePageFactory()
1019
_didDisconnect = False
1021
useActiveChannels = True
1023
# This is the number of seconds that is acceptable for a LivePage to be
1024
# considered 'connected' without any transports still active. In other
1025
# words, if the browser cannot make requests for more than this timeout
1026
# (due to network problems, blocking javascript functions, or broken
1027
# proxies) then deferreds returned from notifyOnDisconnect() will be
1028
# errbacked with ConnectionLost, and the LivePage will be removed from the
1029
# factory's cache, and then likely garbage collected.
1030
TRANSPORTLESS_DISCONNECT_TIMEOUT = 30
1032
# This is the amount of time that each 'transport' request will remain open
1033
# to the server. Although the underlying transport, i.e. the conceptual
1034
# connection established by the sequence of requests, remains alive, it is
1035
# necessary to periodically cancel requests to avoid browser and proxy
1037
TRANSPORT_IDLE_TIMEOUT = 300
1039
page = property(lambda self: self)
1041
# Modules needed to bootstrap
1042
BOOTSTRAP_MODULES = ['Divmod', 'Divmod.Base', 'Divmod.Defer',
1043
'Divmod.Runtime', 'Nevow', 'Nevow.Athena']
1045
# Known minimum working versions of certain browsers.
1046
requiredBrowserVersions = {
1047
browsers.GECKO: (20051111,),
1048
browsers.INTERNET_EXPLORER: (6, 0),
1049
browsers.WEBKIT: (523,),
1050
browsers.OPERA: (9,)}
1052
unsupportedBrowserLoader = loaders.stan(
1055
'Your browser is not supported by the Athena toolkit.']])
1058
def __init__(self, iface=None, rootObject=None, jsModules=None,
1059
jsModuleRoot=None, transportRoot=None, cssModules=None,
1060
cssModuleRoot=None, *a, **kw):
1061
super(LivePage, self).__init__(*a, **kw)
1064
self.rootObject = rootObject
1065
if jsModules is None:
1066
jsModules = JSPackage(jsDeps.mapping)
1067
self.jsModules = jsModules
1068
self.jsModuleRoot = jsModuleRoot
1069
if transportRoot is None:
1070
transportRoot = here
1071
self.transportRoot = transportRoot
1072
self.cssModuleRoot = cssModuleRoot
1073
if cssModules is None:
1074
cssModules = _theCSSRegistry
1075
self.cssModules = cssModules
1076
self.liveFragmentChildren = []
1077
self._includedModules = []
1078
self._includedCSSModules = []
1079
self._disconnectNotifications = []
1080
self._jsDepsMemo = {}
1081
self._cssDepsMemo = {}
1084
def _shouldInclude(self, moduleName):
1085
if moduleName not in self._includedModules:
1086
self._includedModules.append(moduleName)
1091
def _shouldIncludeCSSModule(self, moduleName):
1093
Figure out whether the named CSS module has already been included.
1095
@type moduleName: C{unicode}
1099
if moduleName not in self._includedCSSModules:
1100
self._includedCSSModules.append(moduleName)
1105
# Child lookup may be dependent on the application state
1106
# represented by a LivePage. In this case, it is preferable to
1107
# dispatch child lookup on the same LivePage instance as performed
1108
# the initial rendering of the page. Override the default
1109
# implementation of locateChild to do this where appropriate.
1110
def locateChild(self, ctx, segments):
1112
client = self.factory.getClient(segments[0])
1114
return super(LivePage, self).locateChild(ctx, segments)
1116
return client, segments[1:]
1119
def child___athena_private__(self, ctx):
1120
return _thePrivateAthenaResource
1123
# A note on timeout/disconnect logic: whenever a live client goes from some
1124
# transports to no transports, a timer starts; whenever it goes from no
1125
# transports to some transports, the timer is stopped; if the timer ever
1126
# expires the connection is considered lost; every time a transport is
1127
# added a timer is started; when the transport is used up, the timer is
1128
# stopped; if the timer ever expires, the transport has a no-op sent down
1129
# it; if an idle transport is ever disconnected, the connection is
1130
# considered lost; this lets the server notice clients who actively leave
1131
# (closed window, browser navigates away) and network congestion/errors
1132
# (unplugged ethernet cable, etc)
1133
def _becomeLive(self, location):
1135
Assign this LivePage a clientID, associate it with a factory, and begin
1136
tracking its state. This only occurs when a LivePage is *rendered*,
1137
not when it is instantiated.
1139
self.clientID = self.factory.addClient(self)
1141
if self.jsModuleRoot is None:
1142
self.jsModuleRoot = location.child(self.clientID).child('jsmodule')
1143
if self.cssModuleRoot is None:
1144
self.cssModuleRoot = location.child(self.clientID).child('cssmodule')
1146
self._requestIDCounter = itertools.count().next
1148
self._messageDeliverer = ReliableMessageDelivery(
1150
self.TRANSPORTLESS_DISCONNECT_TIMEOUT * 2,
1151
self.TRANSPORTLESS_DISCONNECT_TIMEOUT,
1152
self.TRANSPORT_IDLE_TIMEOUT,
1154
connectionMade=self._connectionMade)
1155
self._remoteCalls = {}
1156
self._localObjects = {}
1157
self._localObjectIDCounter = itertools.count().next
1159
self.addLocalObject(self)
1162
def _supportedBrowser(self, request):
1164
Determine whether a known-unsupported browser is making a request.
1166
@param request: The L{IRequest} being made.
1169
@return: False if the user agent is known to be unsupported by Athena,
1172
agentString = request.getHeader("user-agent")
1173
if agentString is None:
1175
agent = UserAgent.fromHeaderValue(agentString)
1179
requiredVersion = self.requiredBrowserVersions.get(agent.browser, None)
1180
if requiredVersion is not None:
1181
return agent.version >= requiredVersion
1185
def renderUnsupported(self, ctx):
1187
Render a notification to the user that his user agent is
1188
unsupported by this LivePage.
1190
@param ctx: The current rendering context.
1192
@return: Something renderable (same behavior as L{renderHTTP})
1194
return flat.flatten(self.unsupportedBrowserLoader.load())
1197
def renderHTTP(self, ctx):
1199
Attach this livepage to its transport, and render it and all of its
1200
attached widgets to the browser. During rendering, the page is
1201
attached to its factory, acquires a clientID, and has headers set
1202
appropriately to prevent a browser from ever caching the page, since
1203
the clientID it gives to the browser is transient and changes every
1206
These state changes associated with rendering mean that L{LivePage}s
1207
can only be rendered once, because they are attached to a particular
1208
user's browser, and it must be unambiguous what browser
1209
L{LivePage.callRemote} will invoke the method in.
1211
The page's contents are rendered according to its docFactory, as with a
1212
L{Page}, unless the user-agent requesting this LivePage is determined
1213
to be unsupported by the JavaScript runtime required by Athena. In
1214
that case, a static page is rendered by this page's
1215
C{renderUnsupported} method.
1217
If a special query argument is set in the URL, "__athena_reconnect__",
1218
the page will instead render the JSON-encoded clientID by itself as the
1219
page's content. This allows an existing live page in a browser to
1220
programmatically reconnect without re-rendering and re-loading the
1223
@see: L{LivePage.renderUnsupported}
1225
@see: L{Page.renderHTTP}
1227
@param ctx: a L{WovenContext} with L{IRequest} remembered.
1229
@return: a string (the content of the page) or a Deferred which will
1232
@raise RuntimeError: if the page has already been rendered, or this
1233
page has not been given a factory.
1236
raise RuntimeError("Cannot render a LivePage more than once")
1237
if self.factory is None:
1238
raise RuntimeError("Cannot render a LivePage without a factory")
1240
self._rendered = True
1241
request = inevow.IRequest(ctx)
1242
if not self._supportedBrowser(request):
1243
request.write(self.renderUnsupported(ctx))
1246
self._becomeLive(URL.fromString(flat.flatten(here, ctx)))
1248
neverEverCache(request)
1249
if request.args.get(ATHENA_RECONNECT):
1250
return json.serialize(self.clientID.decode("ascii"))
1251
return rend.Page.renderHTTP(self, ctx)
1254
def _connectionMade(self):
1256
Invoke connectionMade on all attached widgets.
1258
for widget in self._localObjects.values():
1259
widget.connectionMade()
1260
self._didConnect = True
1263
def _disconnected(self, reason):
1265
Callback invoked when the L{ReliableMessageDelivery} is disconnected.
1267
If the page has not already disconnected, fire any deferreds created
1268
with L{notifyOnDisconnect}; if the page was already connected, fire
1269
C{connectionLost} methods on attached widgets.
1271
if not self._didDisconnect:
1272
self._didDisconnect = True
1274
notifications = self._disconnectNotifications
1275
self._disconnectNotifications = None
1276
for d in notifications:
1278
calls = self._remoteCalls
1279
self._remoteCalls = {}
1280
for (reqID, resD) in calls.iteritems():
1281
resD.errback(reason)
1282
if self._didConnect:
1283
for widget in self._localObjects.values():
1284
widget.connectionLost(reason)
1285
self.factory.removeClient(self.clientID)
1288
def connectionMade(self):
1290
Callback invoked when the transport is first connected.
1294
def connectionLost(self, reason):
1296
Callback invoked when the transport is disconnected.
1298
This method will only be called if connectionMade was called.
1304
def addLocalObject(self, obj):
1305
objID = self._localObjectIDCounter()
1306
self._localObjects[objID] = obj
1310
def removeLocalObject(self, objID):
1312
Remove an object from the page's mapping of IDs that can receive
1316
@param objID: The ID returned by L{LivePage.addLocalObject}.
1318
del self._localObjects[objID]
1321
def callRemote(self, methodName, *args):
1322
requestID = u's2c%i' % (self._requestIDCounter(),)
1323
message = (u'call', (unicode(methodName, 'ascii'), requestID, args))
1324
resultD = defer.Deferred()
1325
self._remoteCalls[requestID] = resultD
1326
self.addMessage(message)
1330
def addMessage(self, message):
1331
self._messageDeliverer.addMessage(message)
1334
def notifyOnDisconnect(self):
1336
Return a Deferred which will fire or errback when this LivePage is
1337
no longer connected.
1339
Note that if a LivePage never establishes a connection in the first
1340
place, the Deferreds this returns will never fire.
1342
@rtype: L{defer.Deferred}
1344
d = defer.Deferred()
1345
self._disconnectNotifications.append(d)
1349
def getJSModuleURL(self, moduleName):
1350
return self.jsModuleRoot.child(moduleName)
1353
def getCSSModuleURL(self, moduleName):
1355
Return a URL rooted a L{cssModuleRoot} from which the CSS module named
1356
C{moduleName} can be fetched.
1358
@type moduleName: C{unicode}
1362
return self.cssModuleRoot.child(moduleName)
1365
def getImportStan(self, moduleName):
1366
moduleDef = jsModuleDeclaration(moduleName);
1367
return [tags.script(type='text/javascript')[tags.raw(moduleDef)],
1368
tags.script(type='text/javascript', src=self.getJSModuleURL(moduleName))]
1371
def render_liveglue(self, ctx, data):
1372
bootstrapString = '\n'.join(
1373
[self._bootstrapCall(method, args) for
1374
method, args in self._bootstraps(ctx)])
1376
self.getStylesheetStan(self._getRequiredCSSModules(self._cssDepsMemo)),
1378
# Hit jsDeps.getModuleForName to force it to load some plugins :/
1379
# This really needs to be redesigned.
1380
[self.getImportStan(jsDeps.getModuleForName(name).name)
1382
in self._getRequiredModules(self._jsDepsMemo)],
1383
tags.script(type='text/javascript',
1384
id=BOOTSTRAP_NODE_ID,
1385
payload=bootstrapString)[
1386
BOOTSTRAP_STATEMENT]
1390
def _bootstraps(self, ctx):
1392
Generate a list of 2-tuples of (methodName, arguments) representing the
1393
methods which need to be invoked as soon as all the bootstrap modules
1396
@param: a L{WovenContext} that can render an URL.
1399
("Divmod.bootstrap",
1400
[flat.flatten(self.transportRoot, ctx).decode("ascii")]),
1401
("Nevow.Athena.bootstrap",
1402
[self.jsClass, self.clientID.decode('ascii')])]
1405
def _bootstrapCall(self, methodName, args):
1407
Generate a string to call a 'bootstrap' function in an Athena JavaScript
1410
@param methodName: the name of the method.
1412
@param args: a list of objects that will be JSON-serialized as
1413
arguments to the named method.
1415
return '%s(%s);' % (
1416
methodName, ', '.join([json.serialize(arg) for arg in args]))
1419
def child_jsmodule(self, ctx):
1420
return MappingResource(self.jsModules.mapping)
1423
def child_cssmodule(self, ctx):
1425
Return a L{MappingResource} wrapped around L{cssModules}.
1427
return MappingResource(self.cssModules.mapping)
1430
_transportResource = None
1431
def child_transport(self, ctx):
1432
if self._transportResource is None:
1433
self._transportResource = LivePageTransport(
1434
self._messageDeliverer,
1435
self.useActiveChannels)
1436
return self._transportResource
1439
def locateMethod(self, ctx, methodName):
1440
if methodName in self.iface:
1441
return getattr(self.rootObject, methodName)
1442
raise AttributeError(methodName)
1445
def liveTransportMessageReceived(self, ctx, (action, args)):
1447
A message was received from the reliable transport layer. Process it by
1448
dispatching it first to myself, then later to application code if
1451
method = getattr(self, 'action_' + action)
1455
def action_call(self, ctx, requestId, method, objectID, args, kwargs):
1457
Handle a remote call initiated by the client.
1459
localObj = self._localObjects[objectID]
1461
func = localObj.locateMethod(ctx, method)
1462
except AttributeError:
1463
result = defer.fail(NoSuchMethod(objectID, method))
1465
result = defer.maybeDeferred(func, *args, **kwargs)
1466
def _cbCall(result):
1468
if isinstance(result, failure.Failure):
1469
log.msg("Sending error to browser:")
1472
if result.check(LivePageError):
1474
result.value.jsClass,
1480
result.type.__name__.decode('ascii'),
1481
result.getErrorMessage().decode('ascii'))])
1482
message = (u'respond', (unicode(requestId), success, result))
1483
self.addMessage(message)
1484
result.addBoth(_cbCall)
1487
def action_respond(self, ctx, responseId, success, result):
1489
Handle the response from the client to a call initiated by the server.
1491
callDeferred = self._remoteCalls.pop(responseId)
1493
callDeferred.callback(result)
1495
callDeferred.errback(getJSFailure(result, self.jsModules.mapping))
1498
def action_noop(self, ctx):
1500
Handle noop, used to initialise and ping the live transport.
1504
def action_close(self, ctx):
1506
The client is going away. Clean up after them.
1508
self._messageDeliverer.close()
1509
self._disconnected(error.ConnectionDone("Connection closed"))
1513
handler = stan.Proto('athena:handler')
1514
_handlerFormat = "return Nevow.Athena.Widget.handleEvent(this, %(event)s, %(handler)s);"
1516
def _rewriteEventHandlerToAttribute(tag):
1518
Replace athena:handler children of the given tag with attributes on the tag
1519
which correspond to those event handlers.
1521
if isinstance(tag, stan.Tag):
1522
extraAttributes = {}
1523
for i in xrange(len(tag.children) - 1, -1, -1):
1524
if isinstance(tag.children[i], stan.Tag) and tag.children[i].tagName == 'athena:handler':
1525
info = tag.children.pop(i)
1526
name = info.attributes['event'].encode('ascii')
1527
handler = info.attributes['handler']
1528
extraAttributes[name] = _handlerFormat % {
1529
'handler': json.serialize(handler.decode('ascii')),
1530
'event': json.serialize(name.decode('ascii'))}
1531
tag(**extraAttributes)
1535
def rewriteEventHandlerNodes(root):
1537
Replace all the athena:handler nodes in a given document with onfoo
1540
stan.visit(root, _rewriteEventHandlerToAttribute)
1544
def _mangleId(oldId):
1546
Return a consistently mangled form of an id that is unique to the widget
1547
within which it occurs.
1549
return ['athenaid:', tags.slot('athena:id'), '-', oldId]
1552
def _rewriteAthenaId(tag):
1554
Rewrite id attributes to be prefixed with the ID of the widget the node is
1555
contained by. Also rewrite label "for" attributes which must match the id of
1558
if isinstance(tag, stan.Tag):
1559
elementId = tag.attributes.pop('id', None)
1560
if elementId is not None:
1561
tag.attributes['id'] = _mangleId(elementId)
1562
if tag.tagName == "label":
1563
elementFor = tag.attributes.pop('for', None)
1564
if elementFor is not None:
1565
tag.attributes['for'] = _mangleId(elementFor)
1566
if tag.tagName in ('td', 'th'):
1567
headers = tag.attributes.pop('headers', None)
1568
if headers is not None:
1569
ids = headers.split()
1570
headers = [_mangleId(headerId) for headerId in ids]
1571
for n in xrange(len(headers) - 1, 0, -1):
1572
headers.insert(n, ' ')
1573
tag.attributes['headers'] = headers
1577
def rewriteAthenaIds(root):
1579
Rewrite id attributes to be unique to the widget they're in.
1581
stan.visit(root, _rewriteAthenaId)
1585
class _LiveMixin(_HasJSClass, _HasCSSModule):
1586
jsClass = u'Nevow.Athena.Widget'
1589
preprocessors = [rewriteEventHandlerNodes, rewriteAthenaIds]
1591
fragmentParent = None
1595
# Reference to the result of a call to _structured, if one has been made,
1596
# otherwise None. This is used to make _structured() idempotent.
1597
_structuredCache = None
1599
def __init__(self, *a, **k):
1600
super(_LiveMixin, self).__init__(*a, **k)
1601
self.liveFragmentChildren = []
1605
if self._page is None:
1606
if self.fragmentParent is not None:
1607
self._page = self.fragmentParent.page
1609
def set(self, value):
1612
The L{LivePage} instance which is the topmost container of this
1615
return get, set, None, doc
1616
page = property(*page())
1619
def getInitialArguments(self):
1621
Return a C{tuple} or C{list} of arguments to be passed to this
1622
C{LiveFragment}'s client-side Widget.
1624
This will be called during the rendering process. Whatever it
1625
returns will be serialized into the page and passed to the
1626
C{__init__} method of the widget specified by C{jsClass}.
1628
@rtype: C{list} or C{tuple}
1633
def _prepare(self, tag):
1635
Check for clearly incorrect settings of C{self.jsClass} and
1636
C{self.page}, add this object to the page and fill the I{athena:id}
1637
slot with this object's Athena identifier.
1639
assert isinstance(self.jsClass, unicode), "jsClass must be a unicode string"
1641
if self.page is None:
1642
raise OrphanedFragment(self)
1643
self._athenaID = self.page.addLocalObject(self)
1644
if self.page._didConnect:
1645
self.connectionMade()
1646
tag.fillSlots('athena:id', str(self._athenaID))
1649
def setFragmentParent(self, fragmentParent):
1651
Sets the L{LiveFragment} (or L{LivePage}) which is the logical parent
1652
of this fragment. This should parallel the client-side hierarchy.
1654
All LiveFragments must have setFragmentParent called on them before
1655
they are rendered for the client; otherwise, they will be unable to
1656
properly hook up to the page.
1658
LiveFragments should have their own setFragmentParent called before
1659
calling setFragmentParent on any of their own children. The normal way
1660
to accomplish this is to instantiate your fragment children during the
1663
If that isn't feasible, instead override setFragmentParent and
1664
instantiate your children there.
1666
This architecture might seem contorted, but what it allows that is
1667
interesting is adaptation of foreign objects to LiveFragment. Anywhere
1668
you adapt to LiveFragment, setFragmentParent is the next thing that
1671
self.fragmentParent = fragmentParent
1672
self.page = fragmentParent.page
1673
fragmentParent.liveFragmentChildren.append(self)
1676
def _flatten(self, what):
1678
Synchronously flatten C{what} and return the result as a C{str}.
1680
# Nested import because in a significant stroke of misfortune,
1681
# nevow.testutil already depends on nevow.athena. It makes more sense
1682
# for the dependency to go from nevow.athena to nevow.testutil.
1683
# Perhaps a sane way to fix this would be to move FakeRequest to a
1684
# different module from whence nevow.athena and nevow.testutil could
1685
# import it. -exarkun
1686
from nevow.testutil import FakeRequest
1687
return "".join(_flat.flatten(FakeRequest(), what, False, False))
1690
def _structured(self):
1692
Retrieve an opaque object which may be usable to construct the
1693
client-side Widgets which correspond to this fragment and all of its
1696
if self._structuredCache is not None:
1697
return self._structuredCache
1700
requiredModules = []
1701
requiredCSSModules = []
1703
# Using the context here is terrible but basically necessary given the
1704
# /current/ architecture of Athena and flattening. A better
1705
# implementation which was not tied to the rendering system could avoid
1707
markup = context.call(
1708
{'children': children,
1709
'requiredModules': requiredModules,
1710
'requiredCSSModules': requiredCSSModules},
1711
self._flatten, tags.div(xmlns="http://www.w3.org/1999/xhtml")[self]).decode('utf-8')
1715
self._structuredCache = {
1716
u'requiredModules': [(name, flat.flatten(url).decode('utf-8'))
1717
for (name, url) in requiredModules],
1718
u'requiredCSSModules': [flat.flatten(url).decode('utf-8')
1719
for url in requiredCSSModules],
1720
u'class': self.jsClass,
1721
u'id': self._athenaID,
1722
u'initArguments': tuple(self.getInitialArguments()),
1724
u'children': children}
1725
return self._structuredCache
1728
def liveElement(self, request, tag):
1730
Render framework-level boilerplate for making sure the Widget for this
1731
Element is created and added to the page properly.
1733
requiredModules = self._getRequiredModules(self.page._jsDepsMemo)
1734
requiredCSSModules = self._getRequiredCSSModules(self.page._cssDepsMemo)
1736
# Add required attributes to the top widget node
1737
tag(**{'xmlns:athena': ATHENA_XMLNS_URI,
1738
'id': 'athena:%d' % self._athenaID,
1739
'athena:class': self.jsClass})
1741
# This will only be set if _structured() is being run.
1742
if context.get('children') is not None:
1743
context.get('children').append({
1744
u'class': self.jsClass,
1745
u'id': self._athenaID,
1746
u'initArguments': self.getInitialArguments()})
1747
context.get('requiredModules').extend(requiredModules)
1748
context.get('requiredCSSModules').extend(requiredCSSModules)
1752
self.getStylesheetStan(requiredCSSModules),
1755
[self.getImportStan(name) for (name, url) in requiredModules],
1757
# Dump some data for our client-side __init__ into a text area
1758
# where it can easily be found.
1759
tags.textarea(id='athena-init-args-' + str(self._athenaID),
1760
style="display: none")[
1761
json.serialize(self.getInitialArguments())],
1763
# Arrange to be instantiated
1764
tags.script(type='text/javascript')[
1766
Nevow.Athena.Widget._widgetNodeAdded(%(athenaID)d);
1767
""" % {'athenaID': self._athenaID,
1768
'pythonClass': self.__class__.__name__}],
1770
# Okay, application stuff, plus metadata
1773
renderer(liveElement)
1776
def render_liveFragment(self, ctx, data):
1777
return self.liveElement(inevow.IRequest(ctx), ctx.tag)
1780
def getImportStan(self, moduleName):
1781
return self.page.getImportStan(moduleName)
1784
def locateMethod(self, ctx, methodName):
1785
remoteMethod = expose.get(self, methodName, None)
1786
if remoteMethod is None:
1787
raise AttributeError(self, methodName)
1791
def callRemote(self, methodName, *varargs):
1792
return self.page.callRemote(
1793
"Nevow.Athena.callByAthenaID",
1795
unicode(methodName, 'ascii'),
1799
def _athenaDetachServer(self):
1801
Locally remove this from its parent.
1803
@raise OrphanedFragment: if not attached to a parent.
1805
if self.fragmentParent is None:
1806
raise OrphanedFragment(self)
1807
for ch in self.liveFragmentChildren:
1808
ch._athenaDetachServer()
1809
self.fragmentParent.liveFragmentChildren.remove(self)
1810
self.fragmentParent = None
1813
page.removeLocalObject(self._athenaID)
1814
if page._didConnect:
1815
self.connectionLost(ConnectionLost('Detached'))
1817
expose(_athenaDetachServer)
1822
Remove this from its parent after notifying the client that this is
1825
This function will *not* work correctly if the parent/child
1826
relationships of this widget do not exactly match the parent/child
1827
relationships of the corresponding fragments or elements on the server.
1829
@return: A L{Deferred} which will fire when the detach completes.
1831
d = self.callRemote('_athenaDetachClient')
1832
d.addCallback(lambda ign: self._athenaDetachServer())
1838
Application-level callback invoked when L{detach} succeeds or when the
1839
client invokes the detach logic from its side.
1841
This is invoked after this fragment has been disassociated from its
1842
parent and from the page.
1848
def connectionMade(self):
1850
Callback invoked when the transport is first available to this widget.
1856
def connectionLost(self, reason):
1858
Callback invoked once the transport is no longer available to this
1861
This method will only be called if connectionMade was called.
1868
class LiveFragment(_LiveMixin, rend.Fragment):
1870
This class is deprecated because it relies on context objects
1871
U{which are being removed from Nevow<http://divmod.org/trac/wiki/WitherContext>}.
1873
@see: L{LiveElement}
1875
def __init__(self, *a, **kw):
1876
super(LiveFragment, self).__init__(*a, **kw)
1877
warnings.warn("[v0.10] LiveFragment has been superceded by LiveElement.",
1878
category=DeprecationWarning,
1882
def rend(self, context, data):
1884
Hook into the rendering process in order to check preconditions and
1885
make sure the document will actually be renderable by satisfying
1886
certain Athena requirements.
1888
context = rend.Fragment.rend(self, context, data)
1889
self._prepare(context.tag)
1895
class LiveElement(_LiveMixin, Element):
1897
Base-class for a portion of a LivePage. When being rendered, a LiveElement
1898
has a special ID attribute added to its top-level tag. This attribute is
1899
used to dispatch calls from the client onto the correct object (this one).
1901
A LiveElement must use the `liveElement' renderer somewhere in its document
1902
template. The node given this renderer will be the node used to construct
1903
a Widget instance in the browser (where it will be saved as the `node'
1904
property on the widget object).
1906
JavaScript handlers for elements inside this node can use
1907
C{Nevow.Athena.Widget.get} to retrieve the widget associated with this
1908
LiveElement. For example::
1910
<form onsubmit="Nevow.Athena.Widget.get(this).callRemote('foo', bar); return false;">
1912
Methods of the JavaScript widget class can also be bound as event handlers
1913
using the handler tag type in the Athena namespace::
1915
<form xmlns:athena="http://divmod.org/ns/athena/0.7">
1916
<athena:handler event="onsubmit" handler="doFoo" />
1919
This will invoke the C{doFoo} method of the widget which contains the form
1922
Because this mechanism sets up error handling and otherwise reduces the
1923
required boilerplate for handling events, it is preferred and recommended
1924
over directly including JavaScript in the event handler attribute of a
1927
The C{jsClass} attribute of a LiveElement instance determines the
1928
JavaScript class used to construct its corresponding Widget. This appears
1929
as the 'athena:class' attribute.
1931
JavaScript modules may import other JavaScript modules by using a special
1932
comment which Athena recognizes::
1934
// import Module.Name
1936
Different imports must be placed on different lines. No other comment
1937
style is supported for these directives. Only one space character must
1938
appear between the string 'import' and the name of the module to be
1939
imported. No trailing whitespace or non-whitespace is allowed. There must
1940
be exactly one space between '//' and 'import'. There must be no
1941
preceeding whitespace on the line.
1943
C{Nevow.Athena.Widget.callRemote} can be given permission to invoke methods
1944
on L{LiveElement} instances by passing the functions which implement those
1945
methods to L{nevow.athena.expose} in this way::
1947
class SomeElement(LiveElement):
1948
def someMethod(self, ...):
1952
Only methods exposed in this way will be accessible.
1954
L{LiveElement.callRemote} can be used to invoke any method of the widget on
1957
XML elements with id attributes will be rewritten so that the id is unique
1958
to that particular instance. The client-side
1959
C{Nevow.Athena.Widget.nodeById} API is provided to locate these later
1966
var node = self.nodyById('foo');
1968
On most platforms, this API will be much faster than similar techniques
1969
using C{Nevow.Athena.Widget.nodeByAttribute} etc.
1971
Similarly to how Javascript classes are specified, L{LiveElement}
1972
instances may also identify a CSS module which provides appropriate styles
1973
with the C{cssModule} attribute (a unicode string naming a module within a
1974
L{inevow.ICSSPackage}).
1976
The referenced CSS modules are treated as regular CSS, with the exception
1977
of support for the same::
1979
// import CSSModule.Name
1981
syntax as is provided for Javascript modules.
1983
def render(self, request):
1985
Hook into the rendering process in order to check preconditions and
1986
make sure the document will actually be renderable by satisfying
1987
certain Athena requirements.
1989
document = tags.invisible[Element.render(self, request)]
1990
self._prepare(document)
1995
class IntrospectionFragment(LiveFragment):
1997
Utility for developers which provides detailed information about
1998
the state of a live page.
2001
jsClass = u'Nevow.Athena.IntrospectionWidget'
2003
docFactory = loaders.stan(
2004
tags.span(render=tags.directive('liveFragment'))[
2007
class_='toggle-debug')["Debug"]])
2016
'LivePageError', 'OrphanedFragment', 'ConnectFailed', 'ConnectionLost',
2019
'MappingResource', 'JSModule', 'JSPackage', 'AutoJSPackage',
2020
'allJavascriptPackages', 'JSDependencies', 'JSException', 'JSCode',
2021
'JSFrame', 'JSTraceback',
2024
'CSSRegistry', 'CSSModule',
2027
'LivePage', 'LiveFragment', 'LiveElement', 'IntrospectionFragment',
2030
'expose', 'handler',