~divmod-dev/divmod.org/trunk

« back to all changes in this revision

Viewing changes to Nevow/nevow/athena.py

  • Committer: Jean-Paul Calderone
  • Date: 2014-06-29 20:33:04 UTC
  • mfrom: (2749.1.1 remove-epsilon-1325289)
  • Revision ID: exarkun@twistedmatrix.com-20140629203304-gdkmbwl1suei4m97
mergeĀ lp:~exarkun/divmod.org/remove-epsilon-1325289

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- test-case-name: nevow.test.test_athena -*-
2
 
 
3
 
import itertools, os, re, warnings
4
 
 
5
 
from zope.interface import implements
6
 
 
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
11
 
 
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
18
 
 
19
 
from nevow.page import Element, renderer
20
 
 
21
 
ATHENA_XMLNS_URI = "http://divmod.org/ns/athena/0.7"
22
 
ATHENA_RECONNECT = "__athena_reconnect__"
23
 
 
24
 
expose = util.Expose(
25
 
    """
26
 
    Allow one or more methods to be invoked by the client::
27
 
 
28
 
    | class Foo(LiveElement):
29
 
    |     def twiddle(self, x, y):
30
 
    |         ...
31
 
    |     def frob(self, a, b):
32
 
    |         ...
33
 
    |     expose(twiddle, frob)
34
 
 
35
 
    The Widget for Foo will be allowed to invoke C{twiddle} and C{frob}.
36
 
    """)
37
 
 
38
 
 
39
 
 
40
 
class OrphanedFragment(Exception):
41
 
    """
42
 
    Raised when an operation requiring a parent is attempted on an unattached
43
 
    child.
44
 
    """
45
 
 
46
 
 
47
 
 
48
 
class LivePageError(Exception):
49
 
    """
50
 
    Base exception for LivePage errors.
51
 
    """
52
 
    jsClass = u'Divmod.Error'
53
 
 
54
 
 
55
 
 
56
 
class NoSuchMethod(LivePageError):
57
 
    """
58
 
    Raised when an attempt is made to invoke a method which is not defined or
59
 
    exposed.
60
 
    """
61
 
    jsClass = u'Nevow.Athena.NoSuchMethod'
62
 
 
63
 
    def __init__(self, objectID, methodName):
64
 
        self.objectID = objectID
65
 
        self.methodName = methodName
66
 
        LivePageError.__init__(self, objectID, methodName)
67
 
 
68
 
 
69
 
 
70
 
def neverEverCache(request):
71
 
    """
72
 
    Set headers to indicate that the response to this request should never,
73
 
    ever be cached.
74
 
    """
75
 
    request.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate')
76
 
    request.setHeader('Pragma', 'no-cache')
77
 
 
78
 
 
79
 
def activeChannel(request):
80
 
    """
81
 
    Mark this connection as a 'live' channel by setting the Connection: close
82
 
    header and flushing all headers immediately.
83
 
    """
84
 
    request.setHeader("Connection", "close")
85
 
    request.write('')
86
 
 
87
 
 
88
 
 
89
 
class MappingResource(object):
90
 
    """
91
 
    L{inevow.IResource} which looks up segments in a mapping between symbolic
92
 
    names and the files they correspond to. 
93
 
 
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.
98
 
    """
99
 
    implements(inevow.IResource)
100
 
 
101
 
    def __init__(self, mapping):
102
 
        self.mapping = mapping
103
 
 
104
 
 
105
 
    def renderHTTP(self, ctx):
106
 
        return rend.FourOhFour()
107
 
 
108
 
 
109
 
    def resourceFactory(self, fileName):
110
 
        """
111
 
        Retrieve an L{inevow.IResource} which will render the contents of
112
 
        C{fileName}.
113
 
        """
114
 
        return static.File(fileName)
115
 
 
116
 
 
117
 
    def locateChild(self, ctx, segments):
118
 
        try:
119
 
            impl = self.mapping[segments[0]]
120
 
        except KeyError:
121
 
            return rend.NotFound
122
 
        else:
123
 
            return self.resourceFactory(impl), []
124
 
 
125
 
 
126
 
 
127
 
def _dependencyOrdered(coll, memo):
128
 
    """
129
 
    @type coll: iterable of modules
130
 
    @param coll: The initial sequence of modules.
131
 
 
132
 
    @type memo: C{dict}
133
 
    @param memo: A dictionary mapping module names to their dependencies that
134
 
                 will be used as a mutable cache.
135
 
    """
136
 
 
137
 
 
138
 
 
139
 
class AthenaModule(object):
140
 
    """
141
 
    A representation of a chunk of stuff in a file which can depend on other
142
 
    chunks of stuff in other files.
143
 
    """
144
 
    _modules = {}
145
 
 
146
 
    lastModified = 0
147
 
    deps = None
148
 
    packageDeps = []
149
 
 
150
 
    def getOrCreate(cls, name, mapping):
151
 
        # XXX This implementation of getOrCreate precludes the
152
 
        # simultaneous co-existence of several different package
153
 
        # namespaces.
154
 
        if name in cls._modules:
155
 
            return cls._modules[name]
156
 
        mod = cls._modules[name] = cls(name, mapping)
157
 
        return mod
158
 
    getOrCreate = classmethod(getOrCreate)
159
 
 
160
 
 
161
 
    def __init__(self, name, mapping):
162
 
        self.name = name
163
 
        self.mapping = mapping
164
 
 
165
 
        if '.' in name:
166
 
            parent = '.'.join(name.split('.')[:-1])
167
 
            self.packageDeps = [self.getOrCreate(parent, mapping)]
168
 
 
169
 
        self._cache = CachedFile(self.mapping[self.name], self._getDeps)
170
 
 
171
 
 
172
 
    def __repr__(self):
173
 
        return '%s(%r)' % (self.__class__.__name__, self.name,)
174
 
 
175
 
 
176
 
    _importExpression = re.compile('^// import (.+)$', re.MULTILINE)
177
 
    def _extractImports(self, fileObj):
178
 
        s = fileObj.read()
179
 
        for m in self._importExpression.finditer(s):
180
 
            yield self.getOrCreate(m.group(1).decode('ascii'), self.mapping)
181
 
 
182
 
 
183
 
 
184
 
    def _getDeps(self, jsFile):
185
 
        """
186
 
        Calculate our dependencies given the path to our source.
187
 
        """
188
 
        depgen = self._extractImports(file(jsFile, 'rU'))
189
 
        return self.packageDeps + dict.fromkeys(depgen).keys()
190
 
 
191
 
 
192
 
    def dependencies(self):
193
 
        """
194
 
        Return a list of names of other JavaScript modules we depend on.
195
 
        """
196
 
        return self._cache.load()
197
 
 
198
 
 
199
 
    def allDependencies(self, memo=None):
200
 
        """
201
 
        Return the transitive closure of dependencies, including this module.
202
 
 
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.
206
 
 
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.
210
 
 
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.
214
 
 
215
 
        @rtype: C{list} of C{AthenaModule}
216
 
        """
217
 
        if memo is None:
218
 
            memo = {}
219
 
        ordered = []
220
 
 
221
 
        def _getDeps(dependent):
222
 
            if dependent.name in memo:
223
 
                deps = memo[dependent.name]
224
 
            else:
225
 
                memo[dependent.name] = deps = dependent.dependencies()
226
 
            return deps
227
 
 
228
 
        def _insertDep(dependent):
229
 
            if dependent not in ordered:
230
 
                for dependency in _getDeps(dependent):
231
 
                    _insertDep(dependency)
232
 
                ordered.append(dependent)
233
 
 
234
 
        _insertDep(self)
235
 
        return ordered
236
 
 
237
 
 
238
 
 
239
 
class JSModule(AthenaModule):
240
 
    """
241
 
    L{AthenaModule} subclass for dealing with Javascript modules.
242
 
    """
243
 
    _modules= {}
244
 
 
245
 
 
246
 
 
247
 
class CSSModule(AthenaModule):
248
 
    """
249
 
    L{AthenaModule} subclass for dealing with CSS modules.
250
 
    """
251
 
    _modules = {}
252
 
 
253
 
 
254
 
 
255
 
class JSPackage(object):
256
 
    """
257
 
    A Javascript package.
258
 
 
259
 
    @type mapping: C{dict}
260
 
    @ivar mapping: Mapping between JS module names and C{str} representing
261
 
    filesystem paths containing their implementations.
262
 
    """
263
 
    implements(plugin.IPlugin, inevow.IJavascriptPackage)
264
 
 
265
 
    def __init__(self, mapping):
266
 
        self.mapping = mapping
267
 
 
268
 
 
269
 
 
270
 
def _collectPackageBelow(baseDir, extension):
271
 
    """
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
275
 
    paths.
276
 
 
277
 
    Note that module/package names beginning with . are ignored.
278
 
 
279
 
    @type baseDir: C{str}
280
 
    @param baseDir: A path to the root of a package hierarchy on a filesystem.
281
 
 
282
 
    @type extension: C{str}
283
 
    @param extension: The filename extension we're interested in (e.g. 'css'
284
 
    or 'js').
285
 
 
286
 
    @rtype: C{dict}
287
 
    @return: Mapping between C{unicode} module names and their corresponding
288
 
    C{str} filesystem paths.
289
 
    """
290
 
    mapping = {}
291
 
    EMPTY = sibpath(__file__, 'empty-module.' + extension)
292
 
 
293
 
    _revMap = {baseDir: ''}
294
 
    for (root, dirs, filenames) in os.walk(baseDir):
295
 
        stem = _revMap[root]
296
 
        dirs[:] = [d for d in dirs if not d.startswith('.')]
297
 
 
298
 
        for dir in dirs:
299
 
            name = stem + dir
300
 
            path = os.path.join(root, dir, '__init__.' + extension)
301
 
            if not os.path.exists(path):
302
 
                path = EMPTY
303
 
            mapping[unicode(name, 'ascii')] = path
304
 
            _revMap[os.path.join(root, dir)] = name + '.'
305
 
 
306
 
        for fn in filenames:
307
 
            if fn.startswith('.'):
308
 
                continue
309
 
 
310
 
            if fn == '__init__.' + extension:
311
 
                continue
312
 
 
313
 
            if not fn.endswith('.' + extension):
314
 
                continue
315
 
 
316
 
            name = stem + fn[:-(len(extension) + 1)]
317
 
            path = os.path.join(root, fn)
318
 
            mapping[unicode(name, 'ascii')] = path
319
 
    return mapping
320
 
 
321
 
 
322
 
 
323
 
class AutoJSPackage(object):
324
 
    """
325
 
    A L{inevow.IJavascriptPackage} implementation that scans an on-disk
326
 
    hierarchy locating modules and packages.
327
 
 
328
 
    @type baseDir: C{str}
329
 
    @ivar baseDir: A path to the root of a JavaScript packages/modules
330
 
    filesystem hierarchy.
331
 
    """
332
 
    implements(plugin.IPlugin, inevow.IJavascriptPackage)
333
 
 
334
 
    def __init__(self, baseDir):
335
 
        self.mapping = _collectPackageBelow(baseDir, 'js')
336
 
 
337
 
 
338
 
 
339
 
class AutoCSSPackage(object):
340
 
    """
341
 
    Like L{AutoJSPackage}, but for CSS packages.  Modules within this package
342
 
    can be referenced by L{LivePage.cssModule} or L{LiveElement.cssModule}.
343
 
    """
344
 
    implements(plugin.IPlugin, inevow.ICSSPackage)
345
 
 
346
 
    def __init__(self, baseDir):
347
 
        self.mapping = _collectPackageBelow(baseDir, 'css')
348
 
 
349
 
 
350
 
 
351
 
def allJavascriptPackages():
352
 
    """
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.
357
 
    """
358
 
    d = {}
359
 
    for p in plugin.getPlugIns(inevow.IJavascriptPackage, plugins):
360
 
        d.update(p.mapping)
361
 
    return d
362
 
 
363
 
 
364
 
 
365
 
def allCSSPackages():
366
 
    """
367
 
    Like L{allJavascriptPackages}, but for CSS packages.
368
 
    """
369
 
    d = {}
370
 
    for p in plugin.getPlugIns(inevow.ICSSPackage, plugins):
371
 
        d.update(p.mapping)
372
 
    return d
373
 
 
374
 
 
375
 
 
376
 
class JSDependencies(object):
377
 
    """
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).
381
 
    """
382
 
    _loadPlugins = False
383
 
 
384
 
    def __init__(self, mapping=None):
385
 
        if mapping is None:
386
 
            self.mapping = {}
387
 
            self._loadPlugins = True
388
 
        else:
389
 
            self.mapping = mapping
390
 
 
391
 
 
392
 
    def getModuleForName(self, className):
393
 
        """
394
 
        Return the L{JSModule} most likely to define the given name.
395
 
        """
396
 
        if self._loadPlugins:
397
 
            self.mapping.update(allJavascriptPackages())
398
 
            self._loadPlugins = False
399
 
 
400
 
        jsMod = className
401
 
        while jsMod:
402
 
            try:
403
 
                self.mapping[jsMod]
404
 
            except KeyError:
405
 
                if '.' not in jsMod:
406
 
                    break
407
 
                jsMod = '.'.join(jsMod.split('.')[:-1])
408
 
            else:
409
 
                return JSModule.getOrCreate(jsMod, self.mapping)
410
 
        raise RuntimeError("Unknown class: %r" % (className,))
411
 
    getModuleForClass = getModuleForName
412
 
 
413
 
 
414
 
jsDeps = JSDependencies()
415
 
 
416
 
 
417
 
 
418
 
class CSSRegistry(object):
419
 
    """
420
 
    Keeps track of a set of CSS modules.
421
 
    """
422
 
    def __init__(self, mapping=None):
423
 
        if mapping is None:
424
 
            mapping = {}
425
 
            loadPlugins = True
426
 
        else:
427
 
            loadPlugins = False
428
 
        self.mapping = mapping
429
 
        self._loadPlugins = loadPlugins
430
 
 
431
 
 
432
 
    def getModuleForName(self, moduleName):
433
 
        """
434
 
        Turn a CSS module name into an L{AthenaModule}.
435
 
 
436
 
        @type moduleName: C{unicode}
437
 
 
438
 
        @rtype: L{CSSModule}
439
 
        """
440
 
        if self._loadPlugins:
441
 
            self.mapping.update(allCSSPackages())
442
 
            self._loadPlugins = False
443
 
        try:
444
 
            self.mapping[moduleName]
445
 
        except KeyError:
446
 
            raise RuntimeError('Unknown CSS module: %r' % (moduleName,))
447
 
        return CSSModule.getOrCreate(moduleName, self.mapping)
448
 
 
449
 
_theCSSRegistry = CSSRegistry()
450
 
 
451
 
 
452
 
 
453
 
class JSException(Exception):
454
 
    """
455
 
    Exception class to wrap remote exceptions from JavaScript.
456
 
    """
457
 
 
458
 
 
459
 
 
460
 
class JSCode(object):
461
 
    """
462
 
    Class for mock code objects in mock JS frames.
463
 
    """
464
 
    def __init__(self, name, filename):
465
 
        self.co_name = name
466
 
        self.co_filename = filename
467
 
 
468
 
 
469
 
 
470
 
class JSFrame(object):
471
 
    """
472
 
    Class for mock frame objects in JS client-side traceback wrappers.
473
 
    """
474
 
    def __init__(self, func, fname, ln):
475
 
        self.f_back = None
476
 
        self.f_locals = {}
477
 
        self.f_globals = {}
478
 
        self.f_code = JSCode(func, fname)
479
 
        self.f_lineno = ln
480
 
 
481
 
 
482
 
 
483
 
class JSTraceback(object):
484
 
    """
485
 
    Class for mock traceback objects representing client-side JavaScript
486
 
    tracebacks.
487
 
    """
488
 
    def __init__(self, frame, ln):
489
 
        self.tb_frame = frame
490
 
        self.tb_lineno = ln
491
 
        self.tb_next = None
492
 
 
493
 
 
494
 
 
495
 
def parseStack(stack):
496
 
    """
497
 
    Extract function name, file name, and line number information from the
498
 
    string representation of a JavaScript trace-back.
499
 
    """
500
 
    frames = []
501
 
    for line in stack.split('\n'):
502
 
        if '@' not in line:
503
 
            continue
504
 
        func, rest = line.split('@', 1)
505
 
        if ':' not in rest:
506
 
            continue
507
 
 
508
 
        divide = rest.rfind(':')
509
 
        if divide == -1:
510
 
            fname, ln = rest, ''
511
 
        else:
512
 
            fname, ln = rest[:divide], rest[divide + 1:]
513
 
        ln = int(ln)
514
 
        frames.insert(0, (func, fname, ln))
515
 
    return frames
516
 
 
517
 
 
518
 
 
519
 
def buildTraceback(frames, modules):
520
 
    """
521
 
    Build a chain of mock traceback objects from a serialized Error (or other
522
 
    exception) object, and return the head of the chain.
523
 
    """
524
 
    last = None
525
 
    first = None
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)
530
 
        if last:
531
 
            last.tb_next = tb
532
 
        else:
533
 
            first = tb
534
 
        last = tb
535
 
    return first
536
 
 
537
 
 
538
 
 
539
 
def getJSFailure(exc, modules):
540
 
    """
541
 
    Convert a serialized client-side exception to a Failure.
542
 
    """
543
 
    text = '%s: %s' % (exc[u'name'], exc[u'message'])
544
 
 
545
 
    frames = []
546
 
    if u'stack' in exc:
547
 
        frames = parseStack(exc[u'stack'])
548
 
 
549
 
    return failure.Failure(JSException(text), exc_tb=buildTraceback(frames, modules))
550
 
 
551
 
 
552
 
 
553
 
class LivePageTransport(object):
554
 
    implements(inevow.IResource)
555
 
 
556
 
    def __init__(self, messageDeliverer, useActiveChannels=True):
557
 
        self.messageDeliverer = messageDeliverer
558
 
        self.useActiveChannels = useActiveChannels
559
 
 
560
 
 
561
 
    def locateChild(self, ctx, segments):
562
 
        return rend.NotFound
563
 
 
564
 
 
565
 
    def renderHTTP(self, ctx):
566
 
        req = inevow.IRequest(ctx)
567
 
        neverEverCache(req)
568
 
        if self.useActiveChannels:
569
 
            activeChannel(req)
570
 
 
571
 
        requestContent = req.content.read()
572
 
        messageData = json.parse(requestContent)
573
 
 
574
 
        response = self.messageDeliverer.basketCaseReceived(ctx, messageData)
575
 
        response.addCallback(json.serialize)
576
 
        req.notifyFinish().addErrback(lambda err: self.messageDeliverer._unregisterDeferredAsOutputChannel(response))
577
 
        return response
578
 
 
579
 
 
580
 
 
581
 
class LivePageFactory:
582
 
    noisy = True
583
 
 
584
 
    def __init__(self):
585
 
        self.clients = {}
586
 
 
587
 
    def addClient(self, client):
588
 
        clientID = self._newClientID()
589
 
        self.clients[clientID] = client
590
 
        if self.noisy:
591
 
            log.msg("Rendered new LivePage %r: %r" % (client, clientID))
592
 
        return clientID
593
 
 
594
 
    def getClient(self, clientID):
595
 
        return self.clients[clientID]
596
 
 
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]
603
 
        if self.noisy:
604
 
            log.msg("Disconnected old LivePage %r" % (clientID,))
605
 
 
606
 
    def _newClientID(self):
607
 
        return guard._sessionCookie()
608
 
 
609
 
 
610
 
_thePrivateAthenaResource = static.File(util.resource_filename('nevow', 'athena_private'))
611
 
 
612
 
 
613
 
class ConnectFailed(Exception):
614
 
    pass
615
 
 
616
 
 
617
 
class ConnectionLost(Exception):
618
 
    pass
619
 
 
620
 
 
621
 
CLOSE = u'close'
622
 
UNLOAD = u'unload'
623
 
 
624
 
class ReliableMessageDelivery(object):
625
 
    """
626
 
    A reliable message delivery abstraction over a possibly unreliable transport.
627
 
 
628
 
    @type livePage: L{LivePage}
629
 
    @ivar livePage: The page this delivery is associated with.
630
 
 
631
 
    @type connectTimeout: C{int}
632
 
    @ivar connectTimeout: The amount of time (in seconds) to wait for the
633
 
        initial connection, before timing out.
634
 
 
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
638
 
        timing out.
639
 
 
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.
643
 
 
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).
647
 
 
648
 
    @type scheduler: callable or C{None}
649
 
    @ivar scheduler: If passed, this is used in place of C{reactor.callLater}.
650
 
 
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.
654
 
    """
655
 
    _paused = 0
656
 
    _stopped = False
657
 
    _connected = False
658
 
 
659
 
    outgoingAck = -1            # sequence number which has been acknowledged
660
 
                                # by this end of the connection.
661
 
 
662
 
    outgoingSeq = -1            # sequence number of the next message to be
663
 
                                # added to the outgoing queue.
664
 
 
665
 
    def __init__(self,
666
 
                 livePage,
667
 
                 connectTimeout=60, transportlessTimeout=30, idleTimeout=300,
668
 
                 connectionLost=None,
669
 
                 scheduler=None,
670
 
                 connectionMade=None):
671
 
        self.livePage = livePage
672
 
        self.messages = []
673
 
        self.outputs = []
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
683
 
 
684
 
 
685
 
    def _connectTimedOut(self):
686
 
        self._transportlessTimeoutCall = None
687
 
        self.connectionLost(failure.Failure(ConnectFailed("Timeout")))
688
 
 
689
 
 
690
 
    def _transportlessTimedOut(self):
691
 
        self._transportlessTimeoutCall = None
692
 
        self.connectionLost(failure.Failure(ConnectionLost("Timeout")))
693
 
 
694
 
 
695
 
    def _idleTimedOut(self):
696
 
        output, timeout = self.outputs.pop(0)
697
 
        if not self.outputs:
698
 
            self._transportlessTimeoutCall = self.scheduler(self.transportlessTimeout, self._transportlessTimedOut)
699
 
        output([self.outgoingAck, []])
700
 
 
701
 
 
702
 
    def _sendMessagesToOutput(self, output):
703
 
        log.msg(athena_send_messages=True, count=len(self.messages))
704
 
        output([self.outgoingAck, self.messages])
705
 
 
706
 
 
707
 
    def pause(self):
708
 
        self._paused += 1
709
 
 
710
 
 
711
 
    def _trySendMessages(self):
712
 
        """
713
 
        If we have pending messages and there is an available transport, then
714
 
        consume it to send the messages.
715
 
        """
716
 
        if self.messages and self.outputs:
717
 
            output, timeout = self.outputs.pop(0)
718
 
            timeout.cancel()
719
 
            if not self.outputs:
720
 
                self._transportlessTimeoutCall = self.scheduler(self.transportlessTimeout, self._transportlessTimedOut)
721
 
            self._sendMessagesToOutput(output)
722
 
 
723
 
 
724
 
    def unpause(self):
725
 
        """
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.
728
 
        """
729
 
        self._paused -= 1
730
 
        if self._paused == 0:
731
 
            self._trySendMessages()
732
 
            self._flushOutputs()
733
 
 
734
 
 
735
 
    def addMessage(self, msg):
736
 
        if self._stopped:
737
 
            return
738
 
 
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)
743
 
            timeout.cancel()
744
 
            if not self.outputs:
745
 
                self._transportlessTimeoutCall = self.scheduler(self.transportlessTimeout, self._transportlessTimedOut)
746
 
            self._sendMessagesToOutput(output)
747
 
 
748
 
 
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)
759
 
        else:
760
 
            if self._stopped:
761
 
                self._sendMessagesToOutput(output)
762
 
            else:
763
 
                self.outputs.append((output, self.scheduler(self.idleTimeout, self._idleTimedOut)))
764
 
 
765
 
 
766
 
    def close(self):
767
 
        assert not self._stopped, "Cannot multiply stop ReliableMessageDelivery"
768
 
        self.addMessage((CLOSE, []))
769
 
        self._stopped = True
770
 
        while self.outputs:
771
 
            output, timeout = self.outputs.pop(0)
772
 
            timeout.cancel()
773
 
            self._sendMessagesToOutput(output)
774
 
        self.outputs = None
775
 
        if self._transportlessTimeoutCall is not None:
776
 
            self._transportlessTimeoutCall.cancel()
777
 
            self._transportlessTimeoutCall = None
778
 
 
779
 
 
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)
784
 
                timeout.cancel()
785
 
                break
786
 
        else:
787
 
            return
788
 
        if not self.outputs:
789
 
            self._transportlessTimeoutCall = self.scheduler(self.transportlessTimeout, self._transportlessTimedOut)
790
 
 
791
 
 
792
 
    def _createOutputDeferred(self):
793
 
        """
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
796
 
        any excess outputs.
797
 
        """
798
 
        d = defer.Deferred()
799
 
        self.addOutput(d.callback)
800
 
        if not self._paused and self.outputs:
801
 
            self._trySendMessages()
802
 
            self._flushOutputs()
803
 
        return d
804
 
 
805
 
 
806
 
    def _flushOutputs(self):
807
 
        """
808
 
        Use up all except for one output.
809
 
 
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
814
 
        connections.
815
 
        """
816
 
        if self.outputs is None:
817
 
            return
818
 
        while len(self.outputs) > 1:
819
 
            output, timeout = self.outputs.pop(0)
820
 
            timeout.cancel()
821
 
            output([self.outgoingAck, []])
822
 
 
823
 
 
824
 
    def basketCaseReceived(self, ctx, basketCase):
825
 
        """
826
 
        This is called when some random JSON data is received from an HTTP
827
 
        request.
828
 
 
829
 
        A 'basket case' is currently a data structure of the form [ackNum, [[1,
830
 
        message], [2, message], [3, message]]]
831
 
 
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
834
 
        gone *badly* wrong.
835
 
        """
836
 
        ack, incomingMessages = basketCase
837
 
 
838
 
        outgoingMessages = self.messages
839
 
 
840
 
        # dequeue messages that our client certainly knows about.
841
 
        while outgoingMessages and outgoingMessages[0][0] <= ack:
842
 
            outgoingMessages.pop(0)
843
 
 
844
 
        if incomingMessages:
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)
865
 
                self.pause()
866
 
                try:
867
 
                    for (seq, msg) in incomingMessages:
868
 
                        if seq > lastSentAck:
869
 
                            self.livePage.liveTransportMessageReceived(ctx, msg)
870
 
                        else:
871
 
                            log.msg("Athena transport duplicate message, discarding: %r %r" %
872
 
                                    (self.livePage.clientID,
873
 
                                     seq))
874
 
                    d = self._createOutputDeferred()
875
 
                finally:
876
 
                    self.unpause()
877
 
            else:
878
 
                d = defer.succeed([self.outgoingAck, []])
879
 
                log.msg(
880
 
                    "Sequence gap! %r went from %s to %s" %
881
 
                    (self.livePage.clientID,
882
 
                     self.outgoingAck,
883
 
                     incomingMessages[0][0]))
884
 
        else:
885
 
            d = self._createOutputDeferred()
886
 
 
887
 
        return d
888
 
 
889
 
 
890
 
BOOTSTRAP_NODE_ID = 'athena:bootstrap'
891
 
BOOTSTRAP_STATEMENT = ("eval(document.getElementById('" + BOOTSTRAP_NODE_ID +
892
 
                       "').getAttribute('payload'));")
893
 
 
894
 
class _HasJSClass(object):
895
 
    """
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
898
 
    a JavaScript class.
899
 
 
900
 
    @ivar jsClass: a JavaScript class.
901
 
    @type jsClass: L{unicode}
902
 
    """
903
 
 
904
 
    def _getModuleForClass(self):
905
 
        """
906
 
        Get a L{JSModule} object for the class specified by this object's
907
 
        jsClass string.
908
 
        """
909
 
        return jsDeps.getModuleForClass(self.jsClass)
910
 
 
911
 
 
912
 
    def _getRequiredModules(self, memo):
913
 
        """
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
917
 
        are accessible.
918
 
        """
919
 
        return [
920
 
            (dep.name, self.page.getJSModuleURL(dep.name))
921
 
            for dep
922
 
            in self._getModuleForClass().allDependencies(memo)
923
 
            if self.page._shouldInclude(dep.name)]
924
 
 
925
 
 
926
 
 
927
 
def jsModuleDeclaration(name):
928
 
    """
929
 
    Generate Javascript for a module declaration.
930
 
    """
931
 
    var = ''
932
 
    if '.' not in name:
933
 
        var = 'var '
934
 
    return '%s%s = {"__name__": "%s"};' % (var, name, name)
935
 
 
936
 
 
937
 
 
938
 
class _HasCSSModule(object):
939
 
    """
940
 
    C{cssModule}-handling code common to L{LivePage}, L{LiveElement} and
941
 
    L{LiveFragment}.
942
 
 
943
 
    @ivar cssModule: A CSS module name.
944
 
    @type cssModule: C{unicode} or C{NoneType}
945
 
    """
946
 
    def _getRequiredCSSModules(self, memo):
947
 
        """
948
 
        Return a list of CSS module URLs.
949
 
 
950
 
        @rtype: C{list} of L{url.URL}
951
 
        """
952
 
        if self.cssModule is None:
953
 
            return []
954
 
        module = self.page.cssModules.getModuleForName(self.cssModule)
955
 
        return [
956
 
            self.page.getCSSModuleURL(dep.name)
957
 
            for dep in module.allDependencies(memo)
958
 
            if self.page._shouldIncludeCSSModule(dep.name)]
959
 
 
960
 
 
961
 
    def getStylesheetStan(self, modules):
962
 
        """
963
 
        Get some stan which will include the given modules.
964
 
 
965
 
        @type modules: C{list} or L{url.URL}
966
 
 
967
 
        @rtype: Stan
968
 
        """
969
 
        return [
970
 
            tags.link(
971
 
                rel='stylesheet', type='text/css', href=url)
972
 
            for url in modules]
973
 
 
974
 
 
975
 
 
976
 
class LivePage(rend.Page, _HasJSClass, _HasCSSModule):
977
 
    """
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.
980
 
 
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.
985
 
 
986
 
    @ivar unsupportedBrowserLoader: A document loader which will be used to
987
 
        generate the content shown to unsupported browsers.
988
 
 
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.
992
 
 
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.
996
 
 
997
 
    @type _didConnect: C{bool}
998
 
    @ivar _didConnect: Initially C{False}, set to C{True} if connectionMade has
999
 
        been invoked.
1000
 
 
1001
 
    @type _didDisconnect: C{bool}
1002
 
    @ivar _didDisconnect: Initially C{False}, set to C{True} if _disconnected
1003
 
        has been invoked.
1004
 
 
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.
1008
 
 
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.
1012
 
    """
1013
 
    jsClass = u'Nevow.Athena.PageWidget'
1014
 
    cssModule = None
1015
 
 
1016
 
    factory = LivePageFactory()
1017
 
    _rendered = False
1018
 
    _didConnect = False
1019
 
    _didDisconnect = False
1020
 
 
1021
 
    useActiveChannels = True
1022
 
 
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
1031
 
 
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
1036
 
    # bugs.
1037
 
    TRANSPORT_IDLE_TIMEOUT = 300
1038
 
 
1039
 
    page = property(lambda self: self)
1040
 
 
1041
 
    # Modules needed to bootstrap
1042
 
    BOOTSTRAP_MODULES = ['Divmod', 'Divmod.Base', 'Divmod.Defer',
1043
 
                         'Divmod.Runtime', 'Nevow', 'Nevow.Athena']
1044
 
 
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,)}
1051
 
 
1052
 
    unsupportedBrowserLoader = loaders.stan(
1053
 
        tags.html[
1054
 
            tags.body[
1055
 
                'Your browser is not supported by the Athena toolkit.']])
1056
 
 
1057
 
 
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)
1062
 
 
1063
 
        self.iface = iface
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 = {}
1082
 
 
1083
 
 
1084
 
    def _shouldInclude(self, moduleName):
1085
 
        if moduleName not in self._includedModules:
1086
 
            self._includedModules.append(moduleName)
1087
 
            return True
1088
 
        return False
1089
 
 
1090
 
 
1091
 
    def _shouldIncludeCSSModule(self, moduleName):
1092
 
        """
1093
 
        Figure out whether the named CSS module has already been included.
1094
 
 
1095
 
        @type moduleName: C{unicode}
1096
 
 
1097
 
        @rtype: C{bool}
1098
 
        """
1099
 
        if moduleName not in self._includedCSSModules:
1100
 
            self._includedCSSModules.append(moduleName)
1101
 
            return True
1102
 
        return False
1103
 
 
1104
 
 
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):
1111
 
        try:
1112
 
            client = self.factory.getClient(segments[0])
1113
 
        except KeyError:
1114
 
            return super(LivePage, self).locateChild(ctx, segments)
1115
 
        else:
1116
 
            return client, segments[1:]
1117
 
 
1118
 
 
1119
 
    def child___athena_private__(self, ctx):
1120
 
        return _thePrivateAthenaResource
1121
 
 
1122
 
 
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):
1134
 
        """
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.
1138
 
        """
1139
 
        self.clientID = self.factory.addClient(self)
1140
 
 
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')
1145
 
 
1146
 
        self._requestIDCounter = itertools.count().next
1147
 
 
1148
 
        self._messageDeliverer = ReliableMessageDelivery(
1149
 
            self,
1150
 
            self.TRANSPORTLESS_DISCONNECT_TIMEOUT * 2,
1151
 
            self.TRANSPORTLESS_DISCONNECT_TIMEOUT,
1152
 
            self.TRANSPORT_IDLE_TIMEOUT,
1153
 
            self._disconnected,
1154
 
            connectionMade=self._connectionMade)
1155
 
        self._remoteCalls = {}
1156
 
        self._localObjects = {}
1157
 
        self._localObjectIDCounter = itertools.count().next
1158
 
 
1159
 
        self.addLocalObject(self)
1160
 
 
1161
 
 
1162
 
    def _supportedBrowser(self, request):
1163
 
        """
1164
 
        Determine whether a known-unsupported browser is making a request.
1165
 
 
1166
 
        @param request: The L{IRequest} being made.
1167
 
 
1168
 
        @rtype: C{bool}
1169
 
        @return: False if the user agent is known to be unsupported by Athena,
1170
 
            True otherwise.
1171
 
        """
1172
 
        agentString = request.getHeader("user-agent")
1173
 
        if agentString is None:
1174
 
            return True
1175
 
        agent = UserAgent.fromHeaderValue(agentString)
1176
 
        if agent is None:
1177
 
            return True
1178
 
 
1179
 
        requiredVersion = self.requiredBrowserVersions.get(agent.browser, None)
1180
 
        if requiredVersion is not None:
1181
 
            return agent.version >= requiredVersion
1182
 
        return True
1183
 
 
1184
 
 
1185
 
    def renderUnsupported(self, ctx):
1186
 
        """
1187
 
        Render a notification to the user that his user agent is
1188
 
        unsupported by this LivePage.
1189
 
 
1190
 
        @param ctx: The current rendering context.
1191
 
 
1192
 
        @return: Something renderable (same behavior as L{renderHTTP})
1193
 
        """
1194
 
        return flat.flatten(self.unsupportedBrowserLoader.load())
1195
 
 
1196
 
 
1197
 
    def renderHTTP(self, ctx):
1198
 
        """
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
1204
 
        time.
1205
 
 
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.
1210
 
 
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.
1216
 
 
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
1221
 
        entire page.
1222
 
 
1223
 
        @see: L{LivePage.renderUnsupported}
1224
 
 
1225
 
        @see: L{Page.renderHTTP}
1226
 
 
1227
 
        @param ctx: a L{WovenContext} with L{IRequest} remembered.
1228
 
 
1229
 
        @return: a string (the content of the page) or a Deferred which will
1230
 
        fire with the same.
1231
 
 
1232
 
        @raise RuntimeError: if the page has already been rendered, or this
1233
 
        page has not been given a factory.
1234
 
        """
1235
 
        if self._rendered:
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")
1239
 
 
1240
 
        self._rendered = True
1241
 
        request = inevow.IRequest(ctx)
1242
 
        if not self._supportedBrowser(request):
1243
 
            request.write(self.renderUnsupported(ctx))
1244
 
            return ''
1245
 
 
1246
 
        self._becomeLive(URL.fromString(flat.flatten(here, ctx)))
1247
 
 
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)
1252
 
 
1253
 
 
1254
 
    def _connectionMade(self):
1255
 
        """
1256
 
        Invoke connectionMade on all attached widgets.
1257
 
        """
1258
 
        for widget in self._localObjects.values():
1259
 
            widget.connectionMade()
1260
 
        self._didConnect = True
1261
 
 
1262
 
 
1263
 
    def _disconnected(self, reason):
1264
 
        """
1265
 
        Callback invoked when the L{ReliableMessageDelivery} is disconnected.
1266
 
 
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.
1270
 
        """
1271
 
        if not self._didDisconnect:
1272
 
            self._didDisconnect = True
1273
 
 
1274
 
            notifications = self._disconnectNotifications
1275
 
            self._disconnectNotifications = None
1276
 
            for d in notifications:
1277
 
                d.errback(reason)
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)
1286
 
 
1287
 
 
1288
 
    def connectionMade(self):
1289
 
        """
1290
 
        Callback invoked when the transport is first connected.
1291
 
        """
1292
 
 
1293
 
 
1294
 
    def connectionLost(self, reason):
1295
 
        """
1296
 
        Callback invoked when the transport is disconnected.
1297
 
 
1298
 
        This method will only be called if connectionMade was called.
1299
 
 
1300
 
        Override this.
1301
 
        """
1302
 
 
1303
 
 
1304
 
    def addLocalObject(self, obj):
1305
 
        objID = self._localObjectIDCounter()
1306
 
        self._localObjects[objID] = obj
1307
 
        return objID
1308
 
 
1309
 
 
1310
 
    def removeLocalObject(self, objID):
1311
 
        """
1312
 
        Remove an object from the page's mapping of IDs that can receive
1313
 
        messages.
1314
 
 
1315
 
        @type  objID: C{int}
1316
 
        @param objID: The ID returned by L{LivePage.addLocalObject}.
1317
 
        """
1318
 
        del self._localObjects[objID]
1319
 
 
1320
 
 
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)
1327
 
        return resultD
1328
 
 
1329
 
 
1330
 
    def addMessage(self, message):
1331
 
        self._messageDeliverer.addMessage(message)
1332
 
 
1333
 
 
1334
 
    def notifyOnDisconnect(self):
1335
 
        """
1336
 
        Return a Deferred which will fire or errback when this LivePage is
1337
 
        no longer connected.
1338
 
 
1339
 
        Note that if a LivePage never establishes a connection in the first
1340
 
        place, the Deferreds this returns will never fire.
1341
 
 
1342
 
        @rtype: L{defer.Deferred}
1343
 
        """
1344
 
        d = defer.Deferred()
1345
 
        self._disconnectNotifications.append(d)
1346
 
        return d
1347
 
 
1348
 
 
1349
 
    def getJSModuleURL(self, moduleName):
1350
 
        return self.jsModuleRoot.child(moduleName)
1351
 
 
1352
 
 
1353
 
    def getCSSModuleURL(self, moduleName):
1354
 
        """
1355
 
        Return a URL rooted a L{cssModuleRoot} from which the CSS module named
1356
 
        C{moduleName} can be fetched.
1357
 
 
1358
 
        @type moduleName: C{unicode}
1359
 
 
1360
 
        @rtype: C{str}
1361
 
        """
1362
 
        return self.cssModuleRoot.child(moduleName)
1363
 
 
1364
 
 
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))]
1369
 
 
1370
 
 
1371
 
    def render_liveglue(self, ctx, data):
1372
 
        bootstrapString = '\n'.join(
1373
 
            [self._bootstrapCall(method, args) for
1374
 
             method, args in self._bootstraps(ctx)])
1375
 
        return ctx.tag[
1376
 
            self.getStylesheetStan(self._getRequiredCSSModules(self._cssDepsMemo)),
1377
 
 
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)
1381
 
             for (name, url)
1382
 
             in self._getRequiredModules(self._jsDepsMemo)],
1383
 
            tags.script(type='text/javascript',
1384
 
                        id=BOOTSTRAP_NODE_ID,
1385
 
                        payload=bootstrapString)[
1386
 
                BOOTSTRAP_STATEMENT]
1387
 
        ]
1388
 
 
1389
 
 
1390
 
    def _bootstraps(self, ctx):
1391
 
        """
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
1394
 
        are loaded.
1395
 
 
1396
 
        @param: a L{WovenContext} that can render an URL.
1397
 
        """
1398
 
        return [
1399
 
            ("Divmod.bootstrap",
1400
 
             [flat.flatten(self.transportRoot, ctx).decode("ascii")]),
1401
 
            ("Nevow.Athena.bootstrap",
1402
 
             [self.jsClass, self.clientID.decode('ascii')])]
1403
 
 
1404
 
 
1405
 
    def _bootstrapCall(self, methodName, args):
1406
 
        """
1407
 
        Generate a string to call a 'bootstrap' function in an Athena JavaScript
1408
 
        module client-side.
1409
 
 
1410
 
        @param methodName: the name of the method.
1411
 
 
1412
 
        @param args: a list of objects that will be JSON-serialized as
1413
 
        arguments to the named method.
1414
 
        """
1415
 
        return '%s(%s);' % (
1416
 
            methodName, ', '.join([json.serialize(arg) for arg in args]))
1417
 
 
1418
 
 
1419
 
    def child_jsmodule(self, ctx):
1420
 
        return MappingResource(self.jsModules.mapping)
1421
 
 
1422
 
 
1423
 
    def child_cssmodule(self, ctx):
1424
 
        """
1425
 
        Return a L{MappingResource} wrapped around L{cssModules}.
1426
 
        """
1427
 
        return MappingResource(self.cssModules.mapping)
1428
 
 
1429
 
 
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
1437
 
 
1438
 
 
1439
 
    def locateMethod(self, ctx, methodName):
1440
 
        if methodName in self.iface:
1441
 
            return getattr(self.rootObject, methodName)
1442
 
        raise AttributeError(methodName)
1443
 
 
1444
 
 
1445
 
    def liveTransportMessageReceived(self, ctx, (action, args)):
1446
 
        """
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
1449
 
        applicable.
1450
 
        """
1451
 
        method = getattr(self, 'action_' + action)
1452
 
        method(ctx, *args)
1453
 
 
1454
 
 
1455
 
    def action_call(self, ctx, requestId, method, objectID, args, kwargs):
1456
 
        """
1457
 
        Handle a remote call initiated by the client.
1458
 
        """
1459
 
        localObj = self._localObjects[objectID]
1460
 
        try:
1461
 
            func = localObj.locateMethod(ctx, method)
1462
 
        except AttributeError:
1463
 
            result = defer.fail(NoSuchMethod(objectID, method))
1464
 
        else:
1465
 
            result = defer.maybeDeferred(func, *args, **kwargs)
1466
 
        def _cbCall(result):
1467
 
            success = True
1468
 
            if isinstance(result, failure.Failure):
1469
 
                log.msg("Sending error to browser:")
1470
 
                log.err(result)
1471
 
                success = False
1472
 
                if result.check(LivePageError):
1473
 
                    result = (
1474
 
                        result.value.jsClass,
1475
 
                        result.value.args)
1476
 
                else:
1477
 
                    result = (
1478
 
                        u'Divmod.Error',
1479
 
                        [u'%s: %s' % (
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)
1485
 
 
1486
 
 
1487
 
    def action_respond(self, ctx, responseId, success, result):
1488
 
        """
1489
 
        Handle the response from the client to a call initiated by the server.
1490
 
        """
1491
 
        callDeferred = self._remoteCalls.pop(responseId)
1492
 
        if success:
1493
 
            callDeferred.callback(result)
1494
 
        else:
1495
 
            callDeferred.errback(getJSFailure(result, self.jsModules.mapping))
1496
 
 
1497
 
 
1498
 
    def action_noop(self, ctx):
1499
 
        """
1500
 
        Handle noop, used to initialise and ping the live transport.
1501
 
        """
1502
 
 
1503
 
 
1504
 
    def action_close(self, ctx):
1505
 
        """
1506
 
        The client is going away.  Clean up after them.
1507
 
        """
1508
 
        self._messageDeliverer.close()
1509
 
        self._disconnected(error.ConnectionDone("Connection closed"))
1510
 
 
1511
 
 
1512
 
 
1513
 
handler = stan.Proto('athena:handler')
1514
 
_handlerFormat = "return Nevow.Athena.Widget.handleEvent(this, %(event)s, %(handler)s);"
1515
 
 
1516
 
def _rewriteEventHandlerToAttribute(tag):
1517
 
    """
1518
 
    Replace athena:handler children of the given tag with attributes on the tag
1519
 
    which correspond to those event handlers.
1520
 
    """
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)
1532
 
    return tag
1533
 
 
1534
 
 
1535
 
def rewriteEventHandlerNodes(root):
1536
 
    """
1537
 
    Replace all the athena:handler nodes in a given document with onfoo
1538
 
    attributes.
1539
 
    """
1540
 
    stan.visit(root, _rewriteEventHandlerToAttribute)
1541
 
    return root
1542
 
 
1543
 
 
1544
 
def _mangleId(oldId):
1545
 
    """
1546
 
    Return a consistently mangled form of an id that is unique to the widget
1547
 
    within which it occurs.
1548
 
    """
1549
 
    return ['athenaid:', tags.slot('athena:id'), '-', oldId]
1550
 
 
1551
 
 
1552
 
def _rewriteAthenaId(tag):
1553
 
    """
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
1556
 
    their form element.
1557
 
    """
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
1574
 
    return tag
1575
 
 
1576
 
 
1577
 
def rewriteAthenaIds(root):
1578
 
    """
1579
 
    Rewrite id attributes to be unique to the widget they're in.
1580
 
    """
1581
 
    stan.visit(root, _rewriteAthenaId)
1582
 
    return root
1583
 
 
1584
 
 
1585
 
class _LiveMixin(_HasJSClass, _HasCSSModule):
1586
 
    jsClass = u'Nevow.Athena.Widget'
1587
 
    cssModule = None
1588
 
 
1589
 
    preprocessors = [rewriteEventHandlerNodes, rewriteAthenaIds]
1590
 
 
1591
 
    fragmentParent = None
1592
 
 
1593
 
    _page = None
1594
 
 
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
1598
 
 
1599
 
    def __init__(self, *a, **k):
1600
 
        super(_LiveMixin, self).__init__(*a, **k)
1601
 
        self.liveFragmentChildren = []
1602
 
 
1603
 
    def page():
1604
 
        def get(self):
1605
 
            if self._page is None:
1606
 
                if self.fragmentParent is not None:
1607
 
                    self._page = self.fragmentParent.page
1608
 
            return self._page
1609
 
        def set(self, value):
1610
 
            self._page = value
1611
 
        doc = """
1612
 
        The L{LivePage} instance which is the topmost container of this
1613
 
        fragment.
1614
 
        """
1615
 
        return get, set, None, doc
1616
 
    page = property(*page())
1617
 
 
1618
 
 
1619
 
    def getInitialArguments(self):
1620
 
        """
1621
 
        Return a C{tuple} or C{list} of arguments to be passed to this
1622
 
        C{LiveFragment}'s client-side Widget.
1623
 
 
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}.
1627
 
 
1628
 
        @rtype: C{list} or C{tuple}
1629
 
        """
1630
 
        return ()
1631
 
 
1632
 
 
1633
 
    def _prepare(self, tag):
1634
 
        """
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.
1638
 
        """
1639
 
        assert isinstance(self.jsClass, unicode), "jsClass must be a unicode string"
1640
 
 
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))
1647
 
 
1648
 
 
1649
 
    def setFragmentParent(self, fragmentParent):
1650
 
        """
1651
 
        Sets the L{LiveFragment} (or L{LivePage}) which is the logical parent
1652
 
        of this fragment.  This should parallel the client-side hierarchy.
1653
 
 
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.
1657
 
 
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
1661
 
        render pass.
1662
 
 
1663
 
        If that isn't feasible, instead override setFragmentParent and
1664
 
        instantiate your children there.
1665
 
 
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
1669
 
        should be called.
1670
 
        """
1671
 
        self.fragmentParent = fragmentParent
1672
 
        self.page = fragmentParent.page
1673
 
        fragmentParent.liveFragmentChildren.append(self)
1674
 
 
1675
 
 
1676
 
    def _flatten(self, what):
1677
 
        """
1678
 
        Synchronously flatten C{what} and return the result as a C{str}.
1679
 
        """
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))
1688
 
 
1689
 
 
1690
 
    def _structured(self):
1691
 
        """
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
1694
 
        children.
1695
 
        """
1696
 
        if self._structuredCache is not None:
1697
 
            return self._structuredCache
1698
 
 
1699
 
        children = []
1700
 
        requiredModules = []
1701
 
        requiredCSSModules = []
1702
 
 
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
1706
 
        # this.
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')
1712
 
 
1713
 
        del children[0]
1714
 
 
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()),
1723
 
            u'markup': markup,
1724
 
            u'children': children}
1725
 
        return self._structuredCache
1726
 
 
1727
 
 
1728
 
    def liveElement(self, request, tag):
1729
 
        """
1730
 
        Render framework-level boilerplate for making sure the Widget for this
1731
 
        Element is created and added to the page properly.
1732
 
        """
1733
 
        requiredModules = self._getRequiredModules(self.page._jsDepsMemo)
1734
 
        requiredCSSModules = self._getRequiredCSSModules(self.page._cssDepsMemo)
1735
 
 
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})
1740
 
 
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)
1749
 
            return tag
1750
 
 
1751
 
        return (
1752
 
            self.getStylesheetStan(requiredCSSModules),
1753
 
 
1754
 
            # Import stuff
1755
 
            [self.getImportStan(name) for (name, url) in requiredModules],
1756
 
 
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())],
1762
 
 
1763
 
            # Arrange to be instantiated
1764
 
            tags.script(type='text/javascript')[
1765
 
                """
1766
 
                Nevow.Athena.Widget._widgetNodeAdded(%(athenaID)d);
1767
 
                """ % {'athenaID': self._athenaID,
1768
 
                       'pythonClass': self.__class__.__name__}],
1769
 
 
1770
 
            # Okay, application stuff, plus metadata
1771
 
            tag,
1772
 
            )
1773
 
    renderer(liveElement)
1774
 
 
1775
 
 
1776
 
    def render_liveFragment(self, ctx, data):
1777
 
        return self.liveElement(inevow.IRequest(ctx), ctx.tag)
1778
 
 
1779
 
 
1780
 
    def getImportStan(self, moduleName):
1781
 
        return self.page.getImportStan(moduleName)
1782
 
 
1783
 
 
1784
 
    def locateMethod(self, ctx, methodName):
1785
 
        remoteMethod = expose.get(self, methodName, None)
1786
 
        if remoteMethod is None:
1787
 
            raise AttributeError(self, methodName)
1788
 
        return remoteMethod
1789
 
 
1790
 
 
1791
 
    def callRemote(self, methodName, *varargs):
1792
 
        return self.page.callRemote(
1793
 
            "Nevow.Athena.callByAthenaID",
1794
 
            self._athenaID,
1795
 
            unicode(methodName, 'ascii'),
1796
 
            varargs)
1797
 
 
1798
 
 
1799
 
    def _athenaDetachServer(self):
1800
 
        """
1801
 
        Locally remove this from its parent.
1802
 
 
1803
 
        @raise OrphanedFragment: if not attached to a parent.
1804
 
        """
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
1811
 
        page = self.page
1812
 
        self.page = None
1813
 
        page.removeLocalObject(self._athenaID)
1814
 
        if page._didConnect:
1815
 
            self.connectionLost(ConnectionLost('Detached'))
1816
 
        self.detached()
1817
 
    expose(_athenaDetachServer)
1818
 
 
1819
 
 
1820
 
    def detach(self):
1821
 
        """
1822
 
        Remove this from its parent after notifying the client that this is
1823
 
        happening.
1824
 
 
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.
1828
 
 
1829
 
        @return: A L{Deferred} which will fire when the detach completes.
1830
 
        """
1831
 
        d = self.callRemote('_athenaDetachClient')
1832
 
        d.addCallback(lambda ign: self._athenaDetachServer())
1833
 
        return d
1834
 
 
1835
 
 
1836
 
    def detached(self):
1837
 
        """
1838
 
        Application-level callback invoked when L{detach} succeeds or when the
1839
 
        client invokes the detach logic from its side.
1840
 
 
1841
 
        This is invoked after this fragment has been disassociated from its
1842
 
        parent and from the page.
1843
 
 
1844
 
        Override this.
1845
 
        """
1846
 
 
1847
 
 
1848
 
    def connectionMade(self):
1849
 
        """
1850
 
        Callback invoked when the transport is first available to this widget.
1851
 
 
1852
 
        Override this.
1853
 
        """
1854
 
 
1855
 
 
1856
 
    def connectionLost(self, reason):
1857
 
        """
1858
 
        Callback invoked once the transport is no longer available to this
1859
 
        widget.
1860
 
 
1861
 
        This method will only be called if connectionMade was called.
1862
 
 
1863
 
        Override this.
1864
 
        """
1865
 
 
1866
 
 
1867
 
 
1868
 
class LiveFragment(_LiveMixin, rend.Fragment):
1869
 
    """
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>}.
1872
 
 
1873
 
    @see: L{LiveElement}
1874
 
    """
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,
1879
 
                      stacklevel=2)
1880
 
 
1881
 
 
1882
 
    def rend(self, context, data):
1883
 
        """
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.
1887
 
        """
1888
 
        context = rend.Fragment.rend(self, context, data)
1889
 
        self._prepare(context.tag)
1890
 
        return context
1891
 
 
1892
 
 
1893
 
 
1894
 
 
1895
 
class LiveElement(_LiveMixin, Element):
1896
 
    """
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).
1900
 
 
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).
1905
 
 
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::
1909
 
 
1910
 
        <form onsubmit="Nevow.Athena.Widget.get(this).callRemote('foo', bar); return false;">
1911
 
 
1912
 
    Methods of the JavaScript widget class can also be bound as event handlers
1913
 
    using the handler tag type in the Athena namespace::
1914
 
 
1915
 
        <form xmlns:athena="http://divmod.org/ns/athena/0.7">
1916
 
            <athena:handler event="onsubmit" handler="doFoo" />
1917
 
        </form>
1918
 
 
1919
 
    This will invoke the C{doFoo} method of the widget which contains the form
1920
 
    node.
1921
 
 
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
1925
 
    node.
1926
 
 
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.
1930
 
 
1931
 
    JavaScript modules may import other JavaScript modules by using a special
1932
 
    comment which Athena recognizes::
1933
 
 
1934
 
        // import Module.Name
1935
 
 
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.
1942
 
 
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::
1946
 
 
1947
 
        class SomeElement(LiveElement):
1948
 
            def someMethod(self, ...):
1949
 
                ...
1950
 
            expose(someMethod)
1951
 
 
1952
 
    Only methods exposed in this way will be accessible.
1953
 
 
1954
 
    L{LiveElement.callRemote} can be used to invoke any method of the widget on
1955
 
    the client.
1956
 
 
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
1960
 
    on. For example::
1961
 
 
1962
 
        <div id="foo" />
1963
 
 
1964
 
    and then::
1965
 
 
1966
 
        var node = self.nodyById('foo');
1967
 
 
1968
 
    On most platforms, this API will be much faster than similar techniques
1969
 
    using C{Nevow.Athena.Widget.nodeByAttribute} etc.
1970
 
 
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}).
1975
 
 
1976
 
    The referenced CSS modules are treated as regular CSS, with the exception
1977
 
    of support for the same::
1978
 
 
1979
 
        // import CSSModule.Name
1980
 
 
1981
 
    syntax as is provided for Javascript modules.
1982
 
    """
1983
 
    def render(self, request):
1984
 
        """
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.
1988
 
        """
1989
 
        document = tags.invisible[Element.render(self, request)]
1990
 
        self._prepare(document)
1991
 
        return document
1992
 
 
1993
 
 
1994
 
 
1995
 
class IntrospectionFragment(LiveFragment):
1996
 
    """
1997
 
    Utility for developers which provides detailed information about
1998
 
    the state of a live page.
1999
 
    """
2000
 
 
2001
 
    jsClass = u'Nevow.Athena.IntrospectionWidget'
2002
 
 
2003
 
    docFactory = loaders.stan(
2004
 
        tags.span(render=tags.directive('liveFragment'))[
2005
 
        tags.a(
2006
 
        href="#DEBUG_ME",
2007
 
        class_='toggle-debug')["Debug"]])
2008
 
 
2009
 
 
2010
 
 
2011
 
__all__ = [
2012
 
    # Constants
2013
 
    'ATHENA_XMLNS_URI',
2014
 
 
2015
 
    # Errors
2016
 
    'LivePageError', 'OrphanedFragment', 'ConnectFailed', 'ConnectionLost',
2017
 
 
2018
 
    # JS support
2019
 
    'MappingResource', 'JSModule', 'JSPackage', 'AutoJSPackage',
2020
 
    'allJavascriptPackages', 'JSDependencies', 'JSException', 'JSCode',
2021
 
    'JSFrame', 'JSTraceback',
2022
 
 
2023
 
    # CSS support
2024
 
    'CSSRegistry', 'CSSModule',
2025
 
 
2026
 
    # Core objects
2027
 
    'LivePage', 'LiveFragment', 'LiveElement', 'IntrospectionFragment',
2028
 
 
2029
 
    # Decorators
2030
 
    'expose', 'handler',
2031
 
    ]