~certify-web-dev/twisted/certify-trunk

« back to all changes in this revision

Viewing changes to twisted/web/woven/widgets.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2007-01-17 14:52:35 UTC
  • mfrom: (1.1.5 upstream) (2.1.2 etch)
  • Revision ID: james.westby@ubuntu.com-20070117145235-btmig6qfmqfen0om
Tags: 2.5.0-0ubuntu1
New upstream version, compatible with python2.5.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- test-case-name: twisted.web.test.test_woven -*-
 
2
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
 
3
# See LICENSE for details.
 
4
 
 
5
 
 
6
# DOMWidgets
 
7
 
 
8
from __future__ import nested_scopes
 
9
 
 
10
import urllib
 
11
import warnings
 
12
from twisted.web.microdom import parseString, Element, Node
 
13
from twisted.web import domhelpers
 
14
 
 
15
 
 
16
#sibling imports
 
17
import model
 
18
import template
 
19
import view
 
20
import utils
 
21
import interfaces
 
22
 
 
23
from twisted.python import components, failure
 
24
from twisted.python import reflect
 
25
from twisted.python import log
 
26
from twisted.internet import defer
 
27
 
 
28
viewFactory = view.viewFactory
 
29
document = parseString("<xml />", caseInsensitive=0, preserveCase=0)
 
30
 
 
31
missingPattern = Element("div", caseInsensitive=0, preserveCase=0)
 
32
missingPattern.setAttribute("style", "border: dashed red 1px; margin: 4px")
 
33
 
 
34
"""
 
35
DOMWidgets are views which can be composed into bigger views.
 
36
"""
 
37
 
 
38
DEBUG = 0
 
39
 
 
40
_RAISE = 1
 
41
 
 
42
class Dummy:
 
43
    pass
 
44
 
 
45
class Widget(view.View):
 
46
    """
 
47
    A Widget wraps an object, its model, for display. The model can be a
 
48
    simple Python object (string, list, etc.) or it can be an instance
 
49
    of L{model.Model}.  (The former case is for interface purposes, so that
 
50
    the rest of the code does not have to treat simple objects differently
 
51
    from Model instances.)
 
52
 
 
53
    If the model is-a Model, there are two possibilities:
 
54
 
 
55
       - we are being called to enable an operation on the model
 
56
       - we are really being called to enable an operation on an attribute
 
57
         of the model, which we will call the submodel
 
58
 
 
59
    @cvar tagName: The tag name of the element that this widget creates. If this
 
60
          is None, then the original Node will be cloned.
 
61
    @cvar wantsAllNotifications: Indicate that this widget wants to recieve every
 
62
          change notification from the main model, not just notifications that affect
 
63
          its model.
 
64
    @ivar model: If the current model is an L{model.Model}, then the result of
 
65
          model.getData(). Otherwise the original object itself.
 
66
    """
 
67
    # wvupdate_xxx method signature: request, widget, data; returns None
 
68
 
 
69
    # Don't do lots of work setting up my stacks; they will be passed to me
 
70
    setupStacks = 0
 
71
    
 
72
    # Should we clear the node before we render the widget?
 
73
    clearNode = 0
 
74
    
 
75
    # If some code has to ask if a widget is livePage, the answer is yes
 
76
    livePage = 1
 
77
    
 
78
    tagName = None
 
79
    def __init__(self, model = None, submodel = None, setup = None, controller = None, viewStack=None, *args, **kwargs):
 
80
        """
 
81
        @type model: L{interfaces.IModel}
 
82
 
 
83
        @param submodel: see L{Widget.setSubmodel}
 
84
        @type submodel: String
 
85
 
 
86
        @type setup: Callable
 
87
        """
 
88
        self.errorFactory = Error
 
89
        self.controller = controller
 
90
        self.become = None
 
91
        self._reset()
 
92
        view.View.__init__(self, model)
 
93
        self.node = None
 
94
        self.templateNode = None
 
95
        if submodel:
 
96
            self.submodel = submodel
 
97
        else:
 
98
            self.submodel = ""
 
99
        if setup:
 
100
            self.setupMethods = [setup]
 
101
        else:
 
102
            self.setupMethods = []
 
103
        self.viewStack = viewStack
 
104
        self.initialize(*args, **kwargs)
 
105
 
 
106
    def _reset(self):
 
107
        self.attributes = {}
 
108
        self.slots = {}
 
109
        self._children = []
 
110
 
 
111
    def initialize(self, *args, **kwargs):
 
112
        """
 
113
        Use this method instead of __init__ to initialize your Widget, so you
 
114
        don't have to deal with calling the __init__ of the superclass.
 
115
        """
 
116
        pass
 
117
 
 
118
    def setSubmodel(self, submodel):
 
119
        """
 
120
        I use the submodel to know which attribute in self.model I am responsible for
 
121
        """
 
122
        self.submodel = submodel
 
123
 
 
124
    def getData(self, request=None):
 
125
        """
 
126
        I have a model; however since I am a widget I am only responsible
 
127
        for a portion of that model. This method returns the portion I am
 
128
        responsible for.
 
129
 
 
130
        The return value of this may be a Deferred; if it is, then
 
131
        L{setData} will be called once the result is available.
 
132
        """
 
133
        return self.model.getData(request)
 
134
 
 
135
    def setData(self, request=None, data=None):
 
136
        """
 
137
        If the return value of L{getData} is a Deferred, I am called
 
138
        when the result of the Deferred is available.
 
139
        """
 
140
        self.model.setData(request, data)
 
141
 
 
142
    def add(self, item):
 
143
        """
 
144
        Add `item' to the children of the resultant DOM Node of this widget.
 
145
 
 
146
        @type item: A DOM node or L{Widget}.
 
147
        """
 
148
        self._children.append(item)
 
149
 
 
150
    def appendChild(self, item):
 
151
        """
 
152
        Add `item' to the children of the resultant DOM Node of this widget.
 
153
 
 
154
        @type item: A DOM node or L{Widget}.
 
155
        """
 
156
        self._children.append(item)
 
157
 
 
158
    def insert(self, index, item):
 
159
        """
 
160
        Insert `item' at `index' in the children list of the resultant DOM Node
 
161
        of this widget.
 
162
 
 
163
        @type item: A DOM node or L{Widget}.
 
164
        """
 
165
        self._children.insert(index, item)
 
166
 
 
167
    def setNode(self, node):
 
168
        """
 
169
        Set a node for this widget to use instead of creating one programatically.
 
170
        Useful for looking up a node in a template and using that.
 
171
        """
 
172
        # self.templateNode should always be the original, unmutated
 
173
        # node that was in the HTML template.
 
174
        if self.templateNode == None:
 
175
            self.templateNode = node
 
176
        self.node = node
 
177
 
 
178
    def cleanNode(self, node):
 
179
        """
 
180
        Do your part, prevent infinite recursion!
 
181
        """
 
182
        if not DEBUG:
 
183
            if node.attributes.has_key('model'):
 
184
                del node.attributes['model']
 
185
            if node.attributes.has_key('view'):
 
186
                del node.attributes['view']
 
187
            if node.attributes.has_key('controller'):
 
188
                del node.attributes['controller']
 
189
        return node
 
190
 
 
191
    def generate(self, request, node):
 
192
        data = self.getData(request)
 
193
        if isinstance(data, defer.Deferred):
 
194
            data.addCallback(self.setDataCallback, request, node)
 
195
            data.addErrback(utils.renderFailure, request)
 
196
            return data
 
197
        return self._regenerate(request, node, data)
 
198
 
 
199
    def _regenerate(self, request, node, data):
 
200
        self._reset()
 
201
        self.setUp(request, node, data)
 
202
        for setupMethod in self.setupMethods:
 
203
            setupMethod(request, self, data)
 
204
        # generateDOM should always get a reference to the
 
205
        # templateNode from the original HTML
 
206
        result = self.generateDOM(request, self.templateNode or node)
 
207
        if DEBUG:
 
208
            result.attributes['woven_class'] = reflect.qual(self.__class__)
 
209
        return result
 
210
 
 
211
    def setDataCallback(self, result, request, node):
 
212
        if isinstance(self.getData(request), defer.Deferred):
 
213
            self.setData(request, result)
 
214
        data = self.getData(request)
 
215
        if isinstance(data, defer.Deferred):
 
216
            import warnings
 
217
            warnings.warn("%r has returned a Deferred multiple times for the "
 
218
                          "same request; this is a potential infinite loop."
 
219
                          % self.getData)
 
220
            data.addCallback(self.setDataCallback, request, node)
 
221
            data.addErrback(utils.renderFailure, request)
 
222
            return data
 
223
 
 
224
        newNode = self._regenerate(request, node, result)
 
225
        returnNode = self.dispatchResult(request, node, newNode)
 
226
        # isinstance(Element) added because I was having problems with
 
227
        # this code trying to call setAttribute on my RawTexts -radix 2003-5-28
 
228
        if hasattr(self, 'outgoingId') and isinstance(returnNode, Element):
 
229
            returnNode.attributes['id'] = self.outgoingId
 
230
        self.handleNewNode(request, returnNode)
 
231
        self.handleOutstanding(request)
 
232
        if self.subviews:
 
233
            self.getTopModel().subviews.update(self.subviews)
 
234
        self.controller.domChanged(request, self, returnNode)
 
235
 
 
236
        ## We need to return the result along the callback chain
 
237
        ## so that any other views which added a setDataCallback
 
238
        ## to the same deferred will get the correct data.
 
239
        return result
 
240
 
 
241
    def setUp(self, request, node, data):
 
242
        """
 
243
        Override this method to set up your Widget prior to generateDOM. This
 
244
        is a good place to call methods like L{add}, L{insert}, L{__setitem__}
 
245
        and L{__getitem__}.
 
246
 
 
247
        Overriding this method obsoletes overriding generateDOM directly, in
 
248
        most cases.
 
249
 
 
250
        @type request: L{twisted.web.server.Request}.
 
251
        @param node: The DOM node which this Widget is operating on.
 
252
        @param data: The Model data this Widget is meant to operate upon.
 
253
        """
 
254
        pass
 
255
 
 
256
    def generateDOM(self, request, node):
 
257
        """
 
258
        @returns: A DOM Node to replace the Node in the template that this
 
259
                  Widget handles. This Node is created based on L{tagName},
 
260
                  L{children}, and L{attributes} (You should populate these
 
261
                  in L{setUp}, probably).
 
262
        """
 
263
        if self.become:
 
264
            #print "becoming"
 
265
            become = self.become
 
266
            self.become = None
 
267
            parent = node.parentNode
 
268
            node.parentNode = None
 
269
            old = node.cloneNode(1)
 
270
            node.parentNode = parent
 
271
            gen = become.generateDOM(request, node)
 
272
            if old.attributes.has_key('model'):
 
273
                del old.attributes['model']
 
274
            del old.attributes['controller']
 
275
            gen.appendChild(old)
 
276
            self.node = gen
 
277
            return gen
 
278
        if DEBUG:
 
279
            template = node.toxml()
 
280
            log.msg(template)
 
281
        if not self.tagName:
 
282
            self.tagName = self.templateNode.tagName
 
283
        if node is not self.templateNode or self.tagName != self.templateNode.tagName:
 
284
            parent = node.parentNode
 
285
            node = document.createElement(self.tagName, caseInsensitive=0, preserveCase=0)
 
286
            node.parentNode = parent
 
287
        else:
 
288
            parentNode = node.parentNode
 
289
            node.parentNode = None
 
290
            if self.clearNode:
 
291
                new = node.cloneNode(0)
 
292
            else:
 
293
                new = node.cloneNode(1)
 
294
            node.parentNode = parentNode
 
295
            node = self.cleanNode(new)
 
296
        #print "NICE CLEAN NODE", node.toxml(), self._children
 
297
        node.attributes.update(self.attributes)
 
298
        for item in self._children:
 
299
            if hasattr(item, 'generate'):
 
300
                parentNode = node.parentNode
 
301
                node.parentNode = None
 
302
                item = item.generate(request, node.cloneNode(1))
 
303
                node.parentNode = parentNode
 
304
            node.appendChild(item)
 
305
        #print "WE GOT A NODE", node.toxml()
 
306
        self.node = node
 
307
        return self.node
 
308
 
 
309
    def modelChanged(self, payload):
 
310
        request = payload.get('request', None)
 
311
        if request is None:
 
312
            request = Dummy()
 
313
            request.d = document
 
314
        oldNode = self.node
 
315
        if payload.has_key(self.submodel):
 
316
            data = payload[self.submodel]
 
317
        else:
 
318
            data = self.getData(request)
 
319
        newNode = self._regenerate(request, oldNode, data)
 
320
        returnNode = self.dispatchResult(request, oldNode, newNode)
 
321
        # shot in the dark: this seems to make *my* code work.  probably will
 
322
        # break if returnNode returns a Deferred, as it's supposed to be able
 
323
        # to do -glyph
 
324
#        self.viewStack.push(self)
 
325
#        self.controller.controllerStack.push(self.controller)
 
326
        self.handleNewNode(request, returnNode)
 
327
        self.handleOutstanding(request)
 
328
        self.controller.domChanged(request, self, returnNode)
 
329
 
 
330
    def __setitem__(self, item, value):
 
331
        """
 
332
        Convenience syntax for adding attributes to the resultant DOM Node of
 
333
        this widget.
 
334
        """
 
335
        assert value is not None
 
336
        self.attributes[item] = value
 
337
 
 
338
    setAttribute = __setitem__
 
339
 
 
340
    def __getitem__(self, item):
 
341
        """
 
342
        Convenience syntax for getting an attribute from the resultant DOM Node
 
343
        of this widget.
 
344
        """
 
345
        return self.attributes[item]
 
346
 
 
347
    getAttribute = __getitem__
 
348
 
 
349
    def setError(self, request, message):
 
350
        """
 
351
        Convenience method for allowing a Controller to report an error to the
 
352
        user. When this is called, a Widget of class self.errorFactory is instanciated
 
353
        and set to self.become. When generate is subsequently called, self.become
 
354
        will be responsible for mutating the DOM instead of this widget.
 
355
        """
 
356
        #print "setError called", self
 
357
        id = self.attributes.get('id', '')
 
358
        
 
359
        self.become = self.errorFactory(self.model, message)
 
360
        self.become['id'] = id
 
361
#        self.modelChanged({'request': request})
 
362
 
 
363
    def getTopModel(self):
 
364
        """Get a reference to this page's top model object.
 
365
        """
 
366
        top = self.model
 
367
        while top.parent is not None:
 
368
            top = top.parent
 
369
        return top
 
370
 
 
371
    def getAllPatterns(self, name, default=missingPattern, clone=1, deep=1):
 
372
        """Get all nodes below this one which have a matching pattern attribute.
 
373
        """
 
374
        if self.slots.has_key(name):
 
375
            slots = self.slots[name]
 
376
        else:
 
377
            sm = self.submodel.split('/')[-1]
 
378
            slots = domhelpers.locateNodes(self.templateNode, name + 'Of', sm)
 
379
            if not slots:
 
380
#                slots = domhelpers.locateNodes(self.templateNode, "pattern", name, noNesting=1)
 
381
                matcher = lambda n, name=name: isinstance(n, Element) and \
 
382
                            n.attributes.has_key("pattern") and n.attributes["pattern"] == name
 
383
                recurseMatcher = lambda n: isinstance(n, Element) and not n.attributes.has_key("view") and not n.attributes.has_key('model')
 
384
                slots = domhelpers.findNodesShallowOnMatch(self.templateNode, matcher, recurseMatcher)
 
385
                if not slots:
 
386
                    msg = 'WARNING: No template nodes were found '\
 
387
                              '(tagged %s="%s"'\
 
388
                              ' or pattern="%s") for node %s (full submodel path %s)' % (name + "Of",
 
389
                                            sm, name, self.templateNode, `self.submodel`)
 
390
                    if default is _RAISE:
 
391
                        raise Exception(msg)
 
392
                    if DEBUG:
 
393
                        warnings.warn(msg)
 
394
                    if default is missingPattern:
 
395
                        newNode = missingPattern.cloneNode(1)
 
396
                        newNode.appendChild(document.createTextNode(msg))
 
397
                        return [newNode]
 
398
                    if default is None:
 
399
                        return None
 
400
                    return [default]
 
401
            self.slots[name] = slots
 
402
        if clone:
 
403
            return [x.cloneNode(deep) for x in slots]
 
404
        return slots
 
405
 
 
406
    def getPattern(self, name, default=missingPattern, clone=1, deep=1):
 
407
        """Get a named slot from the incoming template node. Returns a copy
 
408
        of the node and all its children. If there was more than one node with
 
409
        the same slot identifier, they will be returned in a round-robin fashion.
 
410
        """
 
411
        slots = self.getAllPatterns(name, default=default, clone=0)
 
412
        if slots is None:
 
413
            return None
 
414
        slot = slots.pop(0)
 
415
        slots.append(slot)
 
416
        if clone:
 
417
            parentNode = slot.parentNode
 
418
            slot.parentNode = None
 
419
            clone = slot.cloneNode(deep)
 
420
            if clone.attributes.has_key('pattern'):
 
421
                del clone.attributes['pattern']
 
422
            elif clone.attributes.has_key(name + 'Of'):
 
423
                del clone.attributes[name + 'Of']
 
424
            slot.parentNode = parentNode
 
425
            if DEBUG:
 
426
                clone.attributes['ofPattern'] = name + 'Of'
 
427
                clone.attributes['nameOf'] = self.submodel.split('/')[-1]
 
428
            return clone
 
429
        if DEBUG:
 
430
            slot.attributes['ofPattern'] = name + 'Of'
 
431
            slot.attributes['nameOf'] = self.submodel.split('/')[-1]
 
432
        return slot
 
433
 
 
434
    def addUpdateMethod(self, updateMethod):
 
435
        """Add a method to this widget that will be called when the widget
 
436
        is being rendered. The signature for this method should be
 
437
        updateMethod(request, widget, data) where widget will be the
 
438
        instance you are calling addUpdateMethod on.
 
439
        """
 
440
        self.setupMethods.append(updateMethod)
 
441
 
 
442
    def addEventHandler(self, eventName, handler, *args):
 
443
        """Add an event handler to this widget. eventName is a string
 
444
        indicating which javascript event handler should cause this
 
445
        handler to fire. Handler is a callable that has the signature
 
446
        handler(request, widget, *args).
 
447
        """
 
448
        def handlerUpdateStep(request, widget, data):
 
449
            extraArgs = ''
 
450
            for x in args:
 
451
                extraArgs += " ,'" + x.replace("'", "\\'") + "'"
 
452
            widget[eventName] = "return woven_eventHandler('%s', this%s)" % (eventName, extraArgs)
 
453
            setattr(self, 'wevent_' + eventName, handler)
 
454
        self.addUpdateMethod(handlerUpdateStep)
 
455
        
 
456
    def onEvent(self, request, eventName, *args):
 
457
        """Dispatch a client-side event to an event handler that was
 
458
        registered using addEventHandler.
 
459
        """
 
460
        eventHandler = getattr(self, 'wevent_' + eventName, None)
 
461
        if eventHandler is None:
 
462
            raise NotImplementedError("A client side '%s' event occurred,"
 
463
                    " but there was no event handler registered on %s." % 
 
464
                    (eventName, self))
 
465
                
 
466
        eventHandler(request, self, *args)
 
467
 
 
468
 
 
469
class DefaultWidget(Widget):
 
470
    def generate(self, request, node):
 
471
        """
 
472
        By default, we just return the node unchanged
 
473
        """
 
474
        self.cleanNode(node)
 
475
        if self.become:
 
476
            become = self.become
 
477
            self.become = None
 
478
            parent = node.parentNode
 
479
            node.parentNode = None
 
480
            old = node.cloneNode(1)
 
481
            node.parentNode = parent
 
482
            gen = become.generateDOM(request, node)
 
483
            del old.attributes['model']
 
484
            gen.appendChild(self.cleanNode(old))
 
485
            return gen
 
486
        return node
 
487
 
 
488
    def modelChanged(self, payload):
 
489
        """We're not concerned if the model has changed.
 
490
        """
 
491
        pass
 
492
 
 
493
 
 
494
class Attributes(Widget):
 
495
    """Set attributes on a node.
 
496
 
 
497
    Assumes model is a dictionary of attributes.
 
498
    """
 
499
 
 
500
    def setUp(self, request, node, data):
 
501
        for k, v in data.items():
 
502
            self[k] = v
 
503
 
 
504
 
 
505
class Text(Widget):
 
506
    """
 
507
    A simple Widget that renders some text.
 
508
    """
 
509
    def __init__(self, model, raw=0, clear=1, *args, **kwargs):
 
510
        """
 
511
        @param model: The text to render.
 
512
        @type model: A string or L{model.Model}.
 
513
        @param raw: A boolean that specifies whether to render the text as
 
514
              a L{domhelpers.RawText} or as a DOM TextNode.
 
515
        """
 
516
        self.raw = raw
 
517
        self.clearNode = clear
 
518
        Widget.__init__(self, model, *args, **kwargs)
 
519
 
 
520
    def generate(self, request, node):
 
521
        if self.templateNode is None:
 
522
            if self.raw:
 
523
                return domhelpers.RawText(str(self.getData(request)))
 
524
            else:
 
525
                return document.createTextNode(str(self.getData(request)))
 
526
        return Widget.generate(self, request, node)
 
527
 
 
528
    def setUp(self, request, node, data):
 
529
        if self.raw:
 
530
            textNode = domhelpers.RawText(str(data))
 
531
        else:
 
532
            textNode = document.createTextNode(str(data))
 
533
        self.appendChild(textNode)
 
534
 
 
535
 
 
536
class ParagraphText(Widget):
 
537
    """
 
538
    Like a normal text widget, but it takes line breaks in the text and
 
539
    formats them as HTML paragraphs.
 
540
    """
 
541
    def setUp(self, request, node, data):
 
542
        nSplit = data.split('\n')
 
543
        for line in nSplit:
 
544
            if line.strip():
 
545
                para = request.d.createElement('p', caseInsensitive=0, preserveCase=0)
 
546
                para.appendChild(request.d.createTextNode(line))
 
547
                self.add(para)
 
548
 
 
549
class Image(Widget):
 
550
    """
 
551
    A simple Widget that creates an `img' tag.
 
552
    """
 
553
    tagName = 'img'
 
554
    border = '0'
 
555
    def setUp(self, request, node, data):
 
556
        self['border'] = self.border
 
557
        self['src'] = data
 
558
 
 
559
 
 
560
class Error(Widget):
 
561
    tagName = 'span'
 
562
    def __init__(self, model, message="", *args, **kwargs):
 
563
        Widget.__init__(self, model, *args, **kwargs)
 
564
        self.message = message
 
565
 
 
566
    def generateDOM(self, request, node):
 
567
        self['style'] = 'color: red'
 
568
        self.add(Text(" " + self.message))
 
569
        return Widget.generateDOM(self, request, node)
 
570
 
 
571
 
 
572
class Div(Widget):
 
573
    tagName = 'div'
 
574
 
 
575
 
 
576
class Span(Widget):
 
577
    tagName = 'span'
 
578
 
 
579
 
 
580
class Br(Widget):
 
581
    tagName = 'br'
 
582
 
 
583
 
 
584
class Input(Widget):
 
585
    tagName = 'input'
 
586
    def setSubmodel(self, submodel):
 
587
        self.submodel = submodel
 
588
        self['name'] = submodel
 
589
 
 
590
    def setUp(self, request, node, data):
 
591
        if not self.attributes.has_key('name') and not node.attributes.get('name'):
 
592
            if self.submodel:
 
593
                id = self.submodel
 
594
            else:
 
595
                id = self.attributes.get('id', node.attributes.get('id'))
 
596
            self['name'] = id
 
597
        if data is None:
 
598
            data = ''
 
599
        if not self.attributes.has_key('value'):
 
600
            self['value'] = str(data)
 
601
 
 
602
 
 
603
class CheckBox(Input):
 
604
    def setUp(self, request, node, data):
 
605
        self['type'] = 'checkbox'
 
606
        Input.setUp(self, request, node, data)
 
607
 
 
608
 
 
609
class RadioButton(Input):
 
610
    def setUp(self, request, node, data):
 
611
        self['type'] = 'radio'
 
612
        Input.setUp(self, request, node, data)
 
613
 
 
614
 
 
615
class File(Input):
 
616
    def setUp(self, request, node, data):
 
617
        self['type'] = 'file'
 
618
        Input.setUp(self, request, node, data)
 
619
 
 
620
 
 
621
class Hidden(Input):
 
622
    def setUp(self, request, node, data):
 
623
        self['type'] = 'hidden'
 
624
        Input.setUp(self, request, node, data)
 
625
 
 
626
 
 
627
class InputText(Input):
 
628
    def setUp(self, request, node, data):
 
629
        self['type'] = 'text'
 
630
        Input.setUp(self, request, node, data)
 
631
 
 
632
 
 
633
class PasswordText(Input):
 
634
    """
 
635
    I render a password input field.
 
636
    """
 
637
    def setUp(self, request, node, data):
 
638
        self['type'] = 'password'
 
639
        Input.setUp(self, request, node, data)
 
640
 
 
641
 
 
642
class Button(Input):
 
643
    def setUp(self, request, node, data):
 
644
        self['type'] = 'button'
 
645
        Input.setUp(self, request, node, data)
 
646
 
 
647
 
 
648
class Select(Input):
 
649
    tagName = 'select'
 
650
 
 
651
 
 
652
class Option(Widget):
 
653
    tagName = 'option'
 
654
    def initialize(self):
 
655
        self.text = ''
 
656
 
 
657
    def setText(self, text):
 
658
        """
 
659
        Set the text to be displayed within the select menu.
 
660
        """
 
661
        self.text = text
 
662
 
 
663
    def setValue(self, value):
 
664
        self['value'] = str(value)
 
665
 
 
666
    def setUp(self, request, node, data):
 
667
        self.add(Text(self.text or data))
 
668
        if data is None:
 
669
            data = ''
 
670
        if not self.attributes.has_key('value'):
 
671
            self['value'] = str(data)
 
672
 
 
673
class Anchor(Widget):
 
674
    tagName = 'a'
 
675
    trailingSlash = ''
 
676
    def initialize(self):
 
677
        self.baseHREF = ''
 
678
        self.parameters = {}
 
679
        self.raw = 0
 
680
        self.text = ''
 
681
 
 
682
    def setRaw(self, raw):
 
683
        self.raw = raw
 
684
 
 
685
    def setLink(self, href):
 
686
        self.baseHREF= href
 
687
 
 
688
    def setParameter(self, key, value):
 
689
        self.parameters[key] = value
 
690
 
 
691
    def setText(self, text):
 
692
        self.text = text
 
693
 
 
694
    def setUp(self, request, node, data):
 
695
        href = self.baseHREF
 
696
        params = urllib.urlencode(self.parameters)
 
697
        if params:
 
698
            href = href + '?' + params
 
699
        self['href'] = href or str(data) + self.trailingSlash
 
700
        if data is None:
 
701
            data = ""
 
702
        self.add(Text(self.text or data, self.raw, 0))
 
703
 
 
704
 
 
705
class SubAnchor(Anchor):
 
706
    def initialize(self):
 
707
        warnings.warn(
 
708
            "SubAnchor is deprecated, you might want either Anchor or DirectoryAnchor",
 
709
            DeprecationWarning)
 
710
        Anchor.initialize(self)
 
711
 
 
712
 
 
713
 
 
714
class DirectoryAnchor(Anchor):
 
715
    trailingSlash = '/'
 
716
 
 
717
 
 
718
def appendModel(newNode, modelName):
 
719
    if newNode is None: return
 
720
    curModel = newNode.attributes.get('model')
 
721
    if curModel is None:
 
722
        newModel = str(modelName)
 
723
    else:
 
724
        newModel = '/'.join((curModel, str(modelName)))
 
725
    newNode.attributes['model'] = newModel
 
726
 
 
727
 
 
728
class List(Widget):
 
729
    """
 
730
    I am a widget which knows how to generateDOM for a python list.
 
731
 
 
732
    A List should be specified in the template HTML as so::
 
733
 
 
734
       | <ul model="blah" view="List">
 
735
       |     <li pattern="emptyList">This will be displayed if the list
 
736
       |         is empty.</li>
 
737
       |     <li pattern="listItem" view="Text">Foo</li>
 
738
       | </ul>
 
739
 
 
740
    If you have nested lists, you may also do something like this::
 
741
 
 
742
       | <table model="blah" view="List">
 
743
       |     <tr pattern="listHeader"><th>A</th><th>B</th></tr>
 
744
       |     <tr pattern="emptyList"><td colspan='2'>***None***</td></tr>
 
745
       |     <tr pattern="listItem">
 
746
       |         <td><span view="Text" model="1" /></td>
 
747
       |         <td><span view="Text" model="2" /></td>
 
748
       |     </tr>
 
749
       |     <tr pattern="listFooter"><td colspan="2">All done!</td></tr>
 
750
       | </table>
 
751
 
 
752
    Where blah is the name of a list on the model; eg::
 
753
 
 
754
       | self.model.blah = ['foo', 'bar']
 
755
 
 
756
    """
 
757
    tagName = None
 
758
    defaultItemView = "DefaultWidget"
 
759
    def generateDOM(self, request, node):
 
760
        node = Widget.generateDOM(self, request, node)
 
761
        listHeaders = self.getAllPatterns('listHeader', None)
 
762
        listFooters = self.getAllPatterns('listFooter', None)
 
763
        emptyLists = self.getAllPatterns('emptyList', None)
 
764
        domhelpers.clearNode(node)
 
765
        if listHeaders:
 
766
            node.childNodes.extend(listHeaders)
 
767
            for n in listHeaders: n.parentNode = node
 
768
        data = self.getData(request)
 
769
        if data:
 
770
            self._iterateData(node, self.submodel, data)
 
771
        elif emptyLists:
 
772
            node.childNodes.extend(emptyLists)
 
773
            for n in emptyLists: n.parentNode = node
 
774
        if listFooters:
 
775
            node.childNodes.extend(listFooters)
 
776
            for n in listFooters: n.parentNode = node
 
777
        return node
 
778
 
 
779
    def _iterateData(self, parentNode, submodel, data):
 
780
        currentListItem = 0
 
781
        retVal = [None] * len(data)
 
782
        for itemNum in range(len(data)):
 
783
            # theory: by appending copies of the li node
 
784
            # each node will be handled once we exit from
 
785
            # here because handleNode will then recurse into
 
786
            # the newly appended nodes
 
787
 
 
788
            newNode = self.getPattern('listItem')
 
789
            if newNode.getAttribute('model') == '.':
 
790
                newNode.removeAttribute('model')
 
791
            elif not newNode.attributes.get("view"):
 
792
                newNode.attributes["view"] = self.defaultItemView
 
793
            appendModel(newNode, itemNum)
 
794
            retVal[itemNum] = newNode
 
795
            newNode.parentNode = parentNode
 
796
#            parentNode.appendChild(newNode)
 
797
        parentNode.childNodes.extend(retVal)
 
798
 
 
799
 
 
800
class KeyedList(List):
 
801
    """
 
802
    I am a widget which knows how to display the values stored within a
 
803
    Python dictionary..
 
804
 
 
805
    A KeyedList should be specified in the template HTML as so::
 
806
 
 
807
       | <ul model="blah" view="KeyedList">
 
808
       |     <li pattern="emptyList">This will be displayed if the list is
 
809
       |         empty.</li>
 
810
       |     <li pattern="keyedListItem" view="Text">Foo</li>
 
811
       | </ul>
 
812
 
 
813
    I can take advantage of C{listHeader}, C{listFooter} and C{emptyList}
 
814
    items just as a L{List} can.
 
815
    """
 
816
    def _iterateData(self, parentNode, submodel, data):
 
817
        """
 
818
        """
 
819
        currentListItem = 0
 
820
        keys = data.keys()
 
821
        # Keys may be a tuple, if this is not a true dictionary but a dictionary-like object
 
822
        if hasattr(keys, 'sort'):
 
823
            keys.sort()
 
824
        for key in keys:
 
825
            newNode = self.getPattern('keyedListItem')
 
826
            if not newNode:
 
827
                newNode = self.getPattern('item', _RAISE)
 
828
                if newNode:
 
829
                    warnings.warn("itemOf= is deprecated, "
 
830
                                        "please use listItemOf instead",
 
831
                                        DeprecationWarning)
 
832
 
 
833
            appendModel(newNode, key)
 
834
            if not newNode.attributes.get("view"):
 
835
                newNode.attributes["view"] = "DefaultWidget"
 
836
            parentNode.appendChild(newNode)
 
837
 
 
838
 
 
839
class ColumnList(Widget):
 
840
    def __init__(self, model, columns=1, start=0, end=0, *args, **kwargs):
 
841
        Widget.__init__(self, model, *args, **kwargs)
 
842
        self.columns = columns
 
843
        self.start = start
 
844
        self.end = end
 
845
 
 
846
    def setColumns(self, columns):
 
847
        self.columns = columns
 
848
 
 
849
    def setStart(self, start):
 
850
        self.start = start
 
851
 
 
852
    def setEnd(self, end):
 
853
        self.end = end
 
854
 
 
855
    def setUp(self, request, node, data):
 
856
        pattern = self.getPattern('columnListRow', clone=0)
 
857
        if self.end:
 
858
            listSize = self.end - self.start
 
859
            if listSize > len(data):
 
860
                listSize = len(data)
 
861
        else:
 
862
            listSize = len(data)
 
863
        for itemNum in range(listSize):
 
864
            if itemNum % self.columns == 0:
 
865
                row = self.getPattern('columnListRow')
 
866
                domhelpers.clearNode(row)
 
867
                node.appendChild(row)
 
868
 
 
869
            newNode = self.getPattern('columnListItem')
 
870
 
 
871
            appendModel(newNode, itemNum + self.start)
 
872
            if not newNode.attributes.get("view"):
 
873
                newNode.attributes["view"] = "DefaultWidget"
 
874
            row.appendChild(newNode)
 
875
        node.removeChild(pattern)
 
876
        return node
 
877
 
 
878
 
 
879
class Bold(Widget):
 
880
    tagName = 'b'
 
881
 
 
882
 
 
883
class Table(Widget):
 
884
    tagName = 'table'
 
885
 
 
886
 
 
887
class Row(Widget):
 
888
    tagName = 'tr'
 
889
 
 
890
 
 
891
class Cell(Widget):
 
892
    tagName = 'td'
 
893
 
 
894
 
 
895
class RawText(Widget):
 
896
    def generateDOM(self, request, node):
 
897
        self.node = domhelpers.RawText(self.getData(request))
 
898
        return self.node
 
899
 
 
900
from types import StringType
 
901
 
 
902
class Link(Widget):
 
903
    """A utility class for generating <a href='foo'>bar</a> tags.
 
904
    """
 
905
    tagName = 'a'
 
906
    def setUp(self, request, node, data):
 
907
        # TODO: we ought to support Deferreds here for both text and href!
 
908
        if isinstance(data, StringType):
 
909
            node.tagName = self.tagName
 
910
            node.attributes["href"] = data
 
911
        else:
 
912
            data = self.model
 
913
            txt = data.getSubmodel(request, "text").getData(request)
 
914
            if not isinstance(txt, Node):
 
915
                txt = document.createTextNode(txt)
 
916
            lnk = data.getSubmodel(request, "href").getData(request)
 
917
            self['href'] = lnk
 
918
            node.tagName = self.tagName
 
919
            domhelpers.clearNode(node)
 
920
            node.appendChild(txt)
 
921
 
 
922
class RootRelativeLink(Link):
 
923
    """
 
924
    Just like a regular Link, only it makes the href relative to the
 
925
    appRoot (that is, request.getRootURL()).
 
926
    """
 
927
    def setUp(self, request, node, data):
 
928
        # hack, hack: some juggling so I can type less and share more
 
929
        # code with Link
 
930
        st = isinstance(data, StringType)
 
931
        if st:
 
932
            data = request.getRootURL() + '/' + data
 
933
        Link.setUp(self, request, node, data)
 
934
        if not st:
 
935
            self['href'] = request.getRootURL() + '/' + self['href']
 
936
 
 
937
class ExpandMacro(Widget):
 
938
    """A Macro expansion widget modeled after the METAL expander
 
939
    in ZPT/TAL/METAL. Usage:
 
940
    
 
941
    In the Page that is being rendered, place the ExpandMacro widget
 
942
    on the node you want replaced with the Macro, and provide nodes
 
943
    tagged with fill-slot= attributes which will fill slots in the Macro::
 
944
    
 
945
        def wvfactory_myMacro(self, request, node, model):
 
946
            return ExpandMacro(
 
947
                model,
 
948
                macroFile="MyMacro.html",
 
949
                macroName="main")
 
950
        
 
951
        <div view="myMacro">
 
952
            <span fill-slot="greeting">Hello</span>
 
953
            <span fill-slot="greetee">World</span>
 
954
        </div>
 
955
    
 
956
    Then, in your Macro template file ("MyMacro.html" in the above
 
957
    example) designate a node as the macro node, and nodes
 
958
    inside that as the slot nodes::
 
959
    
 
960
        <div macro="main">
 
961
            <h3><span slot="greeting" />, <span slot="greetee" />!</h3>
 
962
        </div>
 
963
    """
 
964
    def __init__(self, model, macroTemplate = "", macroFile="", macroFileDirectory="", macroName="", **kwargs):
 
965
        self.macroTemplate = macroTemplate
 
966
        self.macroFile=macroFile
 
967
        self.macroFileDirectory=macroFileDirectory
 
968
        self.macroName=macroName
 
969
        Widget.__init__(self, model, **kwargs)
 
970
 
 
971
    def generate(self, request, node):
 
972
        if self.macroTemplate:
 
973
            templ = view.View(
 
974
                self.model,
 
975
                template = self.macroTemplate).lookupTemplate(request)
 
976
        else:
 
977
            templ = view.View(
 
978
                self.model,
 
979
                templateFile=self.macroFile,
 
980
                templateDirectory=self.macroFileDirectory).lookupTemplate(request)
 
981
 
 
982
        ## We are going to return the macro node from the metatemplate,
 
983
        ## after replacing any slot= nodes in it with fill-slot= nodes from `node'
 
984
        macrolist = domhelpers.locateNodes(templ.childNodes, "macro", self.macroName)
 
985
        assert len(macrolist) == 1, ("No macro or more than "
 
986
            "one macro named %s found." % self.macroName)
 
987
 
 
988
        macro = macrolist[0]
 
989
        del macro.attributes['macro']
 
990
        slots = domhelpers.findElementsWithAttributeShallow(macro, "slot")
 
991
        for slot in slots:
 
992
            slotName = slot.attributes.get("slot")
 
993
            fillerlist = domhelpers.locateNodes(node.childNodes, "fill-slot", slotName)
 
994
            assert len(fillerlist) <= 1, "More than one fill-slot found with name %s" % slotName
 
995
            if len(fillerlist):
 
996
                filler = fillerlist[0]
 
997
                filler.tagName = filler.endTagName = slot.tagName
 
998
                del filler.attributes['fill-slot']
 
999
                del slot.attributes['slot']
 
1000
                filler.attributes.update(slot.attributes)
 
1001
                slot.parentNode.replaceChild(filler, slot)
 
1002
 
 
1003
        return macro
 
1004
 
 
1005
class DeferredWidget(Widget):
 
1006
    def setDataCallback(self, result, request, node):
 
1007
        model = result
 
1008
        view = None
 
1009
        if isinstance(model, components.Componentized):
 
1010
            view = model.getAdapter(interfaces.IView)
 
1011
        if not view and hasattr(model, '__class__'):
 
1012
            view = interfaces.IView(model, None)
 
1013
        
 
1014
        if view:
 
1015
            view["id"] = self.attributes.get('id', '')
 
1016
            view.templateNode = node
 
1017
            view.controller = self.controller
 
1018
            return view.setDataCallback(result, request, node)
 
1019
        else:
 
1020
            return Widget.setDataCallback(self, result, request, node)
 
1021
 
 
1022
 
 
1023
class Break(Widget):
 
1024
    """Break into pdb when this widget is rendered. Mildly
 
1025
    useful for debugging template structure, model stacks,
 
1026
    etc.
 
1027
    """
 
1028
    def setUp(self, request, node, data):
 
1029
        import pdb; pdb.set_trace()
 
1030
 
 
1031
 
 
1032
view.registerViewForModel(Text, model.StringModel)
 
1033
view.registerViewForModel(List, model.ListModel)
 
1034
view.registerViewForModel(KeyedList, model.DictionaryModel)
 
1035
view.registerViewForModel(Link, model.Link)
 
1036
view.registerViewForModel(DeferredWidget, model.DeferredWrapper)