| 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 |