OLD | NEW |
| (Empty) |
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 ever
y | |
62 change notification from the main model, not just notifications that a
ffect | |
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 responsibl
e 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 programaticall
y. | |
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.ta
gName: | |
284 parent = node.parentNode | |
285 node = document.createElement(self.tagName, caseInsensitive=0, prese
rveCase=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 instan
ciated | |
353 and set to self.become. When generate is subsequently called, self.becom
e | |
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", na
me, noNesting=1) | |
381 matcher = lambda n, name=name: isinstance(n, Element) and \ | |
382 n.attributes.has_key("pattern") and n.attributes["pa
ttern"] == name | |
383 recurseMatcher = lambda n: isinstance(n, Element) and not n.attr
ibutes.has_key("view") and not n.attributes.has_key('model') | |
384 slots = domhelpers.findNodesShallowOnMatch(self.templateNode, ma
tcher, 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.s
ubmodel`) | |
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)" % (eve
ntName, 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, preserveC
ase=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 DirectoryA
nchor", | |
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="", macroFileDirecto
ry="", 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(reques
t) | |
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 `nod
e' | |
984 macrolist = domhelpers.locateNodes(templ.childNodes, "macro", self.macro
Name) | |
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", sl
otName) | |
994 assert len(fillerlist) <= 1, "More than one fill-slot found with nam
e %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) | |
OLD | NEW |