OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.web.test.test_woven -*- | |
2 # | |
3 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
4 # See LICENSE for details. | |
5 | |
6 | |
7 """ | |
8 DOMTemplate | |
9 | |
10 Most templating systems provide commands that you embed | |
11 in the HTML to repeat elements, include fragments from other | |
12 files, etc. This works fairly well for simple constructs and people | |
13 tend to get a false sense of simplicity from this. However, in my | |
14 experience, as soon as the programmer wants to make the logic | |
15 even slightly more complicated, the templating system must be | |
16 bent and abused in ways it was never meant to be used. | |
17 | |
18 The theory behind DOMTemplate is that Python code instead | |
19 of template syntax in the HTML should be used to manipulate | |
20 the structure of the HTML. DOMTemplate uses the DOM, a w3c | |
21 standard tree-based representation of an HTML document that | |
22 provides an API that allows you to traverse nodes in the tree, | |
23 examine their attributes, move, add, and delete them. It is a | |
24 fairly low level API, meaning it takes quite a bit of code to get | |
25 a bit done, but it is standard -- learn the DOM once, you can | |
26 use it from ActionScript, JavaScript, Java, C++, whatever. | |
27 | |
28 A DOMTemplate subclass must do two things: indicate which | |
29 template it wants to use, and indicate which elements it is | |
30 interested in. | |
31 | |
32 A short example:: | |
33 | |
34 | class Test(DOMTemplate): | |
35 | template = ''' | |
36 | <html><head><title>Foo</title></head><body> | |
37 | | |
38 | <div view="Test"> | |
39 | This test node will be replaced | |
40 | </div> | |
41 | | |
42 | </body></html> | |
43 | ''' | |
44 | | |
45 | def factory_test(self, request, node): | |
46 | ''' | |
47 | The test method will be called with the request and the | |
48 | DOM node that the test method was associated with. | |
49 | ''' | |
50 | # self.d has been bound to the main DOM "document" object | |
51 | newNode = self.d.createTextNode("Testing, 1,2,3") | |
52 | | |
53 | # Replace the test node with our single new text node | |
54 | return newNode | |
55 """ | |
56 | |
57 import warnings | |
58 | |
59 try: | |
60 import cPickle as pickle | |
61 except ImportError: | |
62 import pickle | |
63 | |
64 import string, os, sys, stat, types | |
65 from twisted.web import microdom | |
66 | |
67 from twisted.python import components | |
68 from twisted.web import resource, html | |
69 from twisted.web.resource import Resource | |
70 from twisted.web.woven import controller, utils, interfaces | |
71 | |
72 from twisted.internet import defer | |
73 from twisted.python import failure | |
74 from twisted.internet import reactor, defer | |
75 from twisted.python import log | |
76 from zope.interface import implements, Interface | |
77 | |
78 from twisted.web.server import NOT_DONE_YET | |
79 STOP_RENDERING = 1 | |
80 RESTART_RENDERING = 2 | |
81 | |
82 | |
83 | |
84 class INodeMutator(Interface): | |
85 """A component that implements NodeMutator knows how to mutate | |
86 DOM based on the instructions in the object it wraps. | |
87 """ | |
88 def generate(request, node): | |
89 """The generate method should do the work of mutating the DOM | |
90 based on the object this adapter wraps. | |
91 """ | |
92 pass | |
93 | |
94 | |
95 class NodeMutator: | |
96 implements(INodeMutator) | |
97 def __init__(self, data): | |
98 self.data = data | |
99 | |
100 class NodeNodeMutator(NodeMutator): | |
101 """A NodeNodeMutator replaces the node that is passed in to generate | |
102 with the node it adapts. | |
103 """ | |
104 def __init__(self, data): | |
105 assert data is not None | |
106 NodeMutator.__init__(self, data) | |
107 | |
108 def generate(self, request, node): | |
109 if self.data is not node: | |
110 parent = node.parentNode | |
111 if parent: | |
112 parent.replaceChild(self.data, node) | |
113 else: | |
114 log.msg("Warning: There was no parent for node %s; node not muta
ted." % node) | |
115 return self.data | |
116 | |
117 | |
118 class NoneNodeMutator(NodeMutator): | |
119 def generate(self, request, node): | |
120 return node # do nothing | |
121 child = request.d.createTextNode("None") | |
122 node.parentNode.replaceChild(child, node) | |
123 | |
124 | |
125 class StringNodeMutator(NodeMutator): | |
126 """A StringNodeMutator replaces the node that is passed in to generate | |
127 with the string it adapts. | |
128 """ | |
129 def generate(self, request, node): | |
130 if self.data: | |
131 try: | |
132 child = microdom.parseString(self.data) | |
133 except Exception, e: | |
134 log.msg("Error parsing return value, probably invalid xml:", e) | |
135 child = request.d.createTextNode(self.data) | |
136 else: | |
137 child = request.d.createTextNode(self.data) | |
138 nodeMutator = NodeNodeMutator(child) | |
139 return nodeMutator.generate(request, node) | |
140 | |
141 | |
142 components.registerAdapter(NodeNodeMutator, microdom.Node, INodeMutator) | |
143 components.registerAdapter(NoneNodeMutator, type(None), INodeMutator) | |
144 components.registerAdapter(StringNodeMutator, type(""), INodeMutator) | |
145 | |
146 | |
147 class DOMTemplate(Resource): | |
148 """A resource that renders pages using DOM.""" | |
149 | |
150 isLeaf = 1 | |
151 templateFile = '' | |
152 templateDirectory = '' | |
153 template = '' | |
154 _cachedTemplate = None | |
155 | |
156 def __init__(self, templateFile = None): | |
157 """ | |
158 @param templateFile: The name of a file containing a template. | |
159 @type templateFile: String | |
160 """ | |
161 Resource.__init__(self) | |
162 if templateFile: | |
163 self.templateFile = templateFile | |
164 | |
165 self.outstandingCallbacks = 0 | |
166 self.failed = 0 | |
167 | |
168 def render(self, request): | |
169 template = self.getTemplate(request) | |
170 if template: | |
171 self.d = microdom.parseString(template) | |
172 else: | |
173 if not self.templateFile: | |
174 raise AttributeError, "%s does not define self.templateFile to o
perate on" % self.__class__ | |
175 self.d = self.lookupTemplate(request) | |
176 self.handleDocument(request, self.d) | |
177 return NOT_DONE_YET | |
178 | |
179 def getTemplate(self, request): | |
180 """ | |
181 Override this if you want to have your subclass look up its template | |
182 using a different method. | |
183 """ | |
184 return self.template | |
185 | |
186 def lookupTemplate(self, request): | |
187 """ | |
188 Use acquisition to look up the template named by self.templateFile, | |
189 located anywhere above this object in the heirarchy, and use it | |
190 as the template. The first time the template is used it is cached | |
191 for speed. | |
192 """ | |
193 if self.template: | |
194 return microdom.parseString(self.template) | |
195 if not self.templateDirectory: | |
196 mod = sys.modules[self.__module__] | |
197 if hasattr(mod, '__file__'): | |
198 self.templateDirectory = os.path.split(mod.__file__)[0] | |
199 # First see if templateDirectory + templateFile is a file | |
200 templatePath = os.path.join(self.templateDirectory, self.templateFile) | |
201 # Check to see if there is an already compiled copy of it | |
202 templateName = os.path.splitext(self.templateFile)[0] | |
203 compiledTemplateName = '.' + templateName + '.pxp' | |
204 compiledTemplatePath = os.path.join(self.templateDirectory, compiledTemp
lateName) | |
205 # No? Compile and save it | |
206 if (not os.path.exists(compiledTemplatePath) or | |
207 os.stat(compiledTemplatePath)[stat.ST_MTIME] < os.stat(templatePath)[sta
t.ST_MTIME]): | |
208 compiledTemplate = microdom.parse(templatePath) | |
209 pickle.dump(compiledTemplate, open(compiledTemplatePath, 'wb'), 1) | |
210 else: | |
211 compiledTemplate = pickle.load(open(compiledTemplatePath, "rb")) | |
212 return compiledTemplate | |
213 | |
214 def setUp(self, request, document): | |
215 pass | |
216 | |
217 def handleDocument(self, request, document): | |
218 """ | |
219 Handle the root node, and send the page if there are no | |
220 outstanding callbacks when it returns. | |
221 """ | |
222 try: | |
223 request.d = document | |
224 self.setUp(request, document) | |
225 # Don't let outstandingCallbacks get to 0 until the | |
226 # entire tree has been recursed | |
227 # If you don't do this, and any callback has already | |
228 # completed by the time the dispatchResultCallback | |
229 # is added in dispachResult, then sendPage will be | |
230 # called prematurely within dispatchResultCallback | |
231 # resulting in much gnashing of teeth. | |
232 self.outstandingCallbacks += 1 | |
233 for node in document.childNodes: | |
234 request.currentParent = node | |
235 self.handleNode(request, node) | |
236 self.outstandingCallbacks -= 1 | |
237 if not self.outstandingCallbacks: | |
238 return self.sendPage(request) | |
239 except: | |
240 self.renderFailure(None, request) | |
241 | |
242 def dispatchResult(self, request, node, result): | |
243 """ | |
244 Check a given result from handling a node and hand it to a process* | |
245 method which will convert the result into a node and insert it | |
246 into the DOM tree. Return the new node. | |
247 """ | |
248 if not isinstance(result, defer.Deferred): | |
249 adapter = INodeMutator(result, None) | |
250 if adapter is None: | |
251 raise NotImplementedError( | |
252 "Your factory method returned %s, but there is no " | |
253 "INodeMutator adapter registerred for %s." % | |
254 (result, getattr(result, "__class__", | |
255 None) or type(result))) | |
256 result = adapter.generate(request, node) | |
257 if isinstance(result, defer.Deferred): | |
258 self.outstandingCallbacks += 1 | |
259 result.addCallback(self.dispatchResultCallback, request, node) | |
260 result.addErrback(self.renderFailure, request) | |
261 # Got to wait until the callback comes in | |
262 return result | |
263 | |
264 def recurseChildren(self, request, node): | |
265 """ | |
266 If this node has children, handle them. | |
267 """ | |
268 request.currentParent = node | |
269 if not node: return | |
270 if type(node.childNodes) == type(""): return | |
271 if node.hasChildNodes(): | |
272 for child in node.childNodes: | |
273 self.handleNode(request, child) | |
274 | |
275 def dispatchResultCallback(self, result, request, node): | |
276 """ | |
277 Deal with a callback from a deferred, dispatching the result | |
278 and recursing children. | |
279 """ | |
280 self.outstandingCallbacks -= 1 | |
281 node = self.dispatchResult(request, node, result) | |
282 self.recurseChildren(request, node) | |
283 if not self.outstandingCallbacks: | |
284 return self.sendPage(request) | |
285 | |
286 def handleNode(self, request, node): | |
287 """ | |
288 Handle a single node by looking up a method for it, calling the method | |
289 and dispatching the result. | |
290 | |
291 Also, handle all childNodes of this node using recursion. | |
292 """ | |
293 if not hasattr(node, 'getAttribute'): # text node? | |
294 return node | |
295 | |
296 viewName = node.getAttribute('view') | |
297 if viewName: | |
298 method = getattr(self, "factory_" + viewName, None) | |
299 if not method: | |
300 raise NotImplementedError, "You specified view name %s on a node
, but no factory_%s method was found." % (viewName, viewName) | |
301 | |
302 result = method(request, node) | |
303 node = self.dispatchResult(request, node, result) | |
304 | |
305 if not isinstance(node, defer.Deferred): | |
306 self.recurseChildren(request, node) | |
307 | |
308 def sendPage(self, request): | |
309 """ | |
310 Send the results of the DOM mutation to the browser. | |
311 """ | |
312 page = str(self.d.toxml()) | |
313 request.write(page) | |
314 request.finish() | |
315 return page | |
316 | |
317 def renderFailure(self, failure, request): | |
318 try: | |
319 xml = request.d.toxml() | |
320 except: | |
321 xml = "" | |
322 # if not hasattr(request, 'channel'): | |
323 # log.msg("The request got away from me before I could render an err
or page.") | |
324 # log.err(failure) | |
325 # return failure | |
326 if not self.failed: | |
327 self.failed = 1 | |
328 if failure: | |
329 request.write("<html><head><title>%s: %s</title></head><body>\n"
% (html.escape(str(failure.type)), html.escape(str(failure.value)))) | |
330 else: | |
331 request.write("<html><head><title>Failure!</title></head><body>\
n") | |
332 utils.renderFailure(failure, request) | |
333 request.write("<h3>Here is the partially processed DOM:</h3>") | |
334 request.write("\n<pre>\n") | |
335 request.write(html.escape(xml)) | |
336 request.write("\n</pre>\n") | |
337 request.write("</body></html>") | |
338 request.finish() | |
339 return failure | |
340 | |
341 ########################################## | |
342 # Deprecation zone | |
343 # Wear a hard hat | |
344 ########################################## | |
345 | |
346 | |
347 # DOMView is now deprecated since the functionality was merged into domtemplate | |
348 DOMView = DOMTemplate | |
349 | |
350 # DOMController is now renamed woven.controller.Controller | |
351 class DOMController(controller.Controller, Resource): | |
352 """ | |
353 A simple controller that automatically passes responsibility on to the view | |
354 class registered for the model. You can override render to perform | |
355 more advanced template lookup logic. | |
356 """ | |
357 | |
358 def __init__(self, *args, **kwargs): | |
359 log.msg("DeprecationWarning: DOMController is deprecated; it has been re
named twisted.web.woven.controller.Controller.\n") | |
360 controller.Controller.__init__(self, *args, **kwargs) | |
361 Resource.__init__(self) | |
362 | |
363 def setUp(self, request): | |
364 pass | |
365 | |
366 def render(self, request): | |
367 self.setUp(request) | |
368 self.view = interfaces.IView(self.model, None) | |
369 self.view.setController(self) | |
370 return self.view.render(request) | |
371 | |
372 def process(self, request, **kwargs): | |
373 log.msg("Processing results: ", kwargs) | |
374 return RESTART_RENDERING | |
OLD | NEW |