| 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 from __future__ import nested_scopes | |
| 8 | |
| 9 __version__ = "$Revision: 1.67 $"[11:-2] | |
| 10 | |
| 11 import os | |
| 12 import cgi | |
| 13 import types | |
| 14 | |
| 15 from twisted.python import log | |
| 16 from twisted.python import components | |
| 17 from twisted.python import failure | |
| 18 from zope.interface import implements | |
| 19 from twisted.web import resource, server, static | |
| 20 from twisted.web.woven import interfaces, utils | |
| 21 from twisted.web import woven | |
| 22 from twisted.web import microdom | |
| 23 from twisted.web.static import redirectTo, addSlash | |
| 24 | |
| 25 import warnings | |
| 26 from time import time as now | |
| 27 | |
| 28 def controllerFactory(controllerClass): | |
| 29 return lambda request, node, model: controllerClass(model) | |
| 30 | |
| 31 def controllerMethod(controllerClass): | |
| 32 return lambda self, request, node, model: controllerClass(model) | |
| 33 | |
| 34 | |
| 35 class Controller(resource.Resource): | |
| 36 """ | |
| 37 A Controller which handles to events from the user. Such events | |
| 38 are `web request', `form submit', etc. | |
| 39 | |
| 40 I should be the IResource implementor for your Models (and | |
| 41 L{registerControllerForModel} makes this so). | |
| 42 """ | |
| 43 | |
| 44 implements(interfaces.IController) | |
| 45 setupStacks = 1 | |
| 46 addSlash = 1 # Should this controller add a slash to the url automatically? | |
| 47 controllerLibraries = [] | |
| 48 viewFactory = None | |
| 49 templateDirectory = "" | |
| 50 def __init__(self, m, inputhandlers=None, view=None, controllers=None, templ
ateDirectory = None): | |
| 51 #self.start = now() | |
| 52 resource.Resource.__init__(self) | |
| 53 self.model = m | |
| 54 # It's the responsibility of the calling code to make sure setView is | |
| 55 # called on this controller before it's rendered. | |
| 56 self.view = None | |
| 57 self.subcontrollers = [] | |
| 58 if self.setupStacks: | |
| 59 self.setupControllerStack() | |
| 60 if inputhandlers is None and controllers is None: | |
| 61 self._inputhandlers = [] | |
| 62 elif inputhandlers: | |
| 63 print "The inputhandlers arg is deprecated, please use controllers i
nstead" | |
| 64 self._inputhandlers = inputhandlers | |
| 65 else: | |
| 66 self._inputhandlers = controllers | |
| 67 if templateDirectory is not None: | |
| 68 self.templateDirectory = templateDirectory | |
| 69 self._valid = {} | |
| 70 self._invalid = {} | |
| 71 self._process = {} | |
| 72 self._parent = None | |
| 73 | |
| 74 def setupControllerStack(self): | |
| 75 self.controllerStack = utils.Stack([]) | |
| 76 from twisted.web.woven import input | |
| 77 if input not in self.controllerLibraries: | |
| 78 self.controllerLibraries.append(input) | |
| 79 for library in self.controllerLibraries: | |
| 80 self.importControllerLibrary(library) | |
| 81 self.controllerStack.push(self) | |
| 82 | |
| 83 def importControllerLibrary(self, namespace): | |
| 84 if not hasattr(namespace, 'getSubcontroller'): | |
| 85 namespace.getSubcontroller = utils.createGetFunction(namespace) | |
| 86 self.controllerStack.push(namespace) | |
| 87 | |
| 88 def getSubcontroller(self, request, node, model, controllerName): | |
| 89 controller = None | |
| 90 cm = getattr(self, 'wcfactory_' + | |
| 91 controllerName, None) | |
| 92 if cm is None: | |
| 93 cm = getattr(self, 'factory_' + | |
| 94 controllerName, None) | |
| 95 if cm is not None: | |
| 96 warnings.warn("factory_ methods are deprecated; please use " | |
| 97 "wcfactory_ instead", DeprecationWarning) | |
| 98 if cm: | |
| 99 if cm.func_code.co_argcount == 1 and not type(cm) == types.LambdaTyp
e: | |
| 100 warnings.warn("A Controller Factory takes " | |
| 101 "(request, node, model) " | |
| 102 "now instead of (model)", DeprecationWarning) | |
| 103 controller = controllerFactory(model) | |
| 104 else: | |
| 105 controller = cm(request, node, model) | |
| 106 return controller | |
| 107 | |
| 108 def setSubcontrollerFactory(self, name, factory, setup=None): | |
| 109 setattr(self, "wcfactory_" + name, lambda request, node, m: | |
| 110 factory(m)) | |
| 111 | |
| 112 def setView(self, view): | |
| 113 self.view = view | |
| 114 | |
| 115 def setNode(self, node): | |
| 116 self.node = node | |
| 117 | |
| 118 def setUp(self, request, *args): | |
| 119 """ | |
| 120 @type request: L{twisted.web.server.Request} | |
| 121 """ | |
| 122 pass | |
| 123 | |
| 124 def getChild(self, name, request): | |
| 125 """ | |
| 126 Look for a factory method to create the object to handle the | |
| 127 next segment of the URL. If a wchild_* method is found, it will | |
| 128 be called to produce the Resource object to handle the next | |
| 129 segment of the path. If a wchild_* method is not found, | |
| 130 getDynamicChild will be called with the name and request. | |
| 131 | |
| 132 @param name: The name of the child being requested. | |
| 133 @type name: string | |
| 134 @param request: The HTTP request being handled. | |
| 135 @type request: L{twisted.web.server.Request} | |
| 136 """ | |
| 137 if not name: | |
| 138 method = "index" | |
| 139 else: | |
| 140 method = name.replace('.', '_') | |
| 141 f = getattr(self, "wchild_%s" % method, None) | |
| 142 if f: | |
| 143 return f(request) | |
| 144 else: | |
| 145 child = self.getDynamicChild(name, request) | |
| 146 if child is None: | |
| 147 return resource.Resource.getChild(self, name, request) | |
| 148 else: | |
| 149 return child | |
| 150 | |
| 151 def getDynamicChild(self, name, request): | |
| 152 """ | |
| 153 This method is called when getChild cannot find a matching wchild_* | |
| 154 method in the Controller. Override me if you wish to have dynamic | |
| 155 handling of child pages. Should return a Resource if appropriate. | |
| 156 Return None to indicate no resource found. | |
| 157 | |
| 158 @param name: The name of the child being requested. | |
| 159 @type name: string | |
| 160 @param request: The HTTP request being handled. | |
| 161 @type request: L{twisted.web.server.Request} | |
| 162 """ | |
| 163 pass | |
| 164 | |
| 165 def wchild_index(self, request): | |
| 166 """By default, we return ourself as the index. | |
| 167 Override this to provide different behavior | |
| 168 for a URL that ends in a slash. | |
| 169 """ | |
| 170 self.addSlash = 0 | |
| 171 return self | |
| 172 | |
| 173 def render(self, request): | |
| 174 """ | |
| 175 Trigger any inputhandlers that were passed in to this Page, | |
| 176 then delegate to the View for traversing the DOM. Finally, | |
| 177 call gatheredControllers to deal with any InputHandlers that | |
| 178 were constructed from any controller= tags in the | |
| 179 DOM. gatheredControllers will render the page to the browser | |
| 180 when it is done. | |
| 181 """ | |
| 182 if self.addSlash and request.uri.split('?')[0][-1] != '/': | |
| 183 return redirectTo(addSlash(request), request) | |
| 184 # Handle any inputhandlers that were passed in to the controller first | |
| 185 for ih in self._inputhandlers: | |
| 186 ih._parent = self | |
| 187 ih.handle(request) | |
| 188 self._inputhandlers = [] | |
| 189 for key, value in self._valid.items(): | |
| 190 key.commit(request, None, value) | |
| 191 self._valid = {} | |
| 192 return self.renderView(request) | |
| 193 | |
| 194 def makeView(self, model, templateFile=None, parentCount=0): | |
| 195 if self.viewFactory is None: | |
| 196 self.viewFactory = self.__class__ | |
| 197 v = self.viewFactory(model, templateFile=templateFile, templateDirectory
=self.templateDirectory) | |
| 198 v.parentCount = parentCount | |
| 199 v.tapestry = self | |
| 200 v.importViewLibrary(self) | |
| 201 return v | |
| 202 | |
| 203 def renderView(self, request): | |
| 204 if self.view is None: | |
| 205 if self.viewFactory is not None: | |
| 206 self.setView(self.makeView(self.model, None)) | |
| 207 else: | |
| 208 self.setView(interfaces.IView(self.model, None)) | |
| 209 self.view.setController(self) | |
| 210 return self.view.render(request, doneCallback=self.gatheredControllers) | |
| 211 | |
| 212 def gatheredControllers(self, v, d, request): | |
| 213 process = {} | |
| 214 request.args = {} | |
| 215 for key, value in self._valid.items(): | |
| 216 key.commit(request, None, value) | |
| 217 process[key.submodel] = value | |
| 218 self.process(request, **process) | |
| 219 #log.msg("Sending page!") | |
| 220 self.pageRenderComplete(request) | |
| 221 utils.doSendPage(v, d, request) | |
| 222 #v.unlinkViews() | |
| 223 | |
| 224 #print "Page time: ", now() - self.start | |
| 225 #return view.View.render(self, request, block=0) | |
| 226 | |
| 227 def aggregateValid(self, request, input, data): | |
| 228 self._valid[input] = data | |
| 229 | |
| 230 def aggregateInvalid(self, request, input, data): | |
| 231 self._invalid[input] = data | |
| 232 | |
| 233 def process(self, request, **kwargs): | |
| 234 if kwargs: | |
| 235 log.msg("Processing results: ", kwargs) | |
| 236 | |
| 237 def setSubmodel(self, submodel): | |
| 238 self.submodel = submodel | |
| 239 | |
| 240 def handle(self, request): | |
| 241 """ | |
| 242 By default, we don't do anything | |
| 243 """ | |
| 244 pass | |
| 245 | |
| 246 def exit(self, request): | |
| 247 """We are done handling the node to which this controller was attached. | |
| 248 """ | |
| 249 pass | |
| 250 | |
| 251 def domChanged(self, request, widget, node): | |
| 252 parent = getattr(self, '_parent', None) | |
| 253 if parent is not None: | |
| 254 parent.domChanged(request, widget, node) | |
| 255 | |
| 256 def pageRenderComplete(self, request): | |
| 257 """Override this to recieve notification when the view rendering | |
| 258 process is complete. | |
| 259 """ | |
| 260 pass | |
| 261 | |
| 262 WOVEN_PATH = os.path.split(woven.__file__)[0] | |
| 263 | |
| 264 class LiveController(Controller): | |
| 265 """A Controller that encapsulates logic that makes it possible for this | |
| 266 page to be "Live". A live page can have it's content updated after the | |
| 267 page has been sent to the browser, and can translate client-side | |
| 268 javascript events into server-side events. | |
| 269 """ | |
| 270 pageSession = None | |
| 271 def render(self, request): | |
| 272 """First, check to see if this request is attempting to hook up the | |
| 273 output conduit. If so, do it. Otherwise, unlink the current session's | |
| 274 View from the MVC notification infrastructure, then render the page | |
| 275 normally. | |
| 276 """ | |
| 277 # Check to see if we're hooking up an output conduit | |
| 278 sess = request.getSession(interfaces.IWovenLivePage) | |
| 279 #print "REQUEST.ARGS", request.args | |
| 280 if request.args.has_key('woven_hookupOutputConduitToThisFrame'): | |
| 281 sess.hookupOutputConduit(request) | |
| 282 return server.NOT_DONE_YET | |
| 283 if request.args.has_key('woven_clientSideEventName'): | |
| 284 try: | |
| 285 request.d = microdom.parseString('<xml/>', caseInsensitive=0, pr
eserveCase=0) | |
| 286 eventName = request.args['woven_clientSideEventName'][0] | |
| 287 eventTarget = request.args['woven_clientSideEventTarget'][0] | |
| 288 eventArgs = request.args.get('woven_clientSideEventArguments', [
]) | |
| 289 #print "EVENT", eventName, eventTarget, eventArgs | |
| 290 return self.clientToServerEvent(request, eventName, eventTarget,
eventArgs) | |
| 291 except: | |
| 292 fail = failure.Failure() | |
| 293 self.view.renderFailure(fail, request) | |
| 294 return server.NOT_DONE_YET | |
| 295 | |
| 296 # Unlink the current page in this user's session from MVC notifications | |
| 297 page = sess.getCurrentPage() | |
| 298 #request.currentId = getattr(sess, 'currentId', 0) | |
| 299 if page is not None: | |
| 300 page.view.unlinkViews() | |
| 301 sess.setCurrentPage(None) | |
| 302 #print "PAGE SESSION IS NONE" | |
| 303 self.pageSession = None | |
| 304 return Controller.render(self, request) | |
| 305 | |
| 306 def clientToServerEvent(self, request, eventName, eventTarget, eventArgs): | |
| 307 """The client sent an asynchronous event to the server. | |
| 308 Locate the View object targeted by this event and attempt | |
| 309 to call onEvent on it. | |
| 310 """ | |
| 311 sess = request.getSession(interfaces.IWovenLivePage) | |
| 312 self.view = sess.getCurrentPage().view | |
| 313 #request.d = self.view.d | |
| 314 print "clientToServerEvent", eventTarget | |
| 315 target = self.view.subviews[eventTarget] | |
| 316 print "target, parent", target, target.parent | |
| 317 #target.parent = self.view | |
| 318 #target.controller._parent = self | |
| 319 | |
| 320 ## From the time we call onEvent until it returns, we want all | |
| 321 ## calls to IWovenLivePage.sendScript to be appended to this | |
| 322 ## list so we can spit them out in the response, immediately | |
| 323 ## below. | |
| 324 scriptOutput = [] | |
| 325 orig = sess.sendScript | |
| 326 sess.sendScript = scriptOutput.append | |
| 327 target.onEvent(request, eventName, *eventArgs) | |
| 328 sess.sendScript = orig | |
| 329 | |
| 330 scriptOutput.append('parent.woven_clientToServerEventComplete()')
| |
| 331 | |
| 332 #print "GATHERED JS", scriptOutput | |
| 333 | |
| 334 return '''<html> | |
| 335 <body> | |
| 336 <script language="javascript"> | |
| 337 %s | |
| 338 </script> | |
| 339 %s event sent to %s (%s) with arguments %s. | |
| 340 </body> | |
| 341 </html>''' % ('\n'.join(scriptOutput), eventName, cgi.escape(str(target)), event
Target, eventArgs) | |
| 342 | |
| 343 def gatheredControllers(self, v, d, request): | |
| 344 Controller.gatheredControllers(self, v, d, request) | |
| 345 sess = request.getSession(interfaces.IWovenLivePage) | |
| 346 self.pageSession = sess | |
| 347 sess.setCurrentPage(self) | |
| 348 sess.currentId = request.currentId | |
| 349 | |
| 350 def domChanged(self, request, widget, node): | |
| 351 sess = request.getSession(interfaces.IWovenLivePage) | |
| 352 print "domchanged" | |
| 353 if sess is not None: | |
| 354 if not hasattr(node, 'getAttribute'): | |
| 355 return | |
| 356 page = sess.getCurrentPage() | |
| 357 if page is None: | |
| 358 return | |
| 359 nodeId = node.getAttribute('id') | |
| 360 #logger.warn("DOM for %r is changing to %s", nodeId, node.toprettyxm
l()) | |
| 361 nodeXML = node.toxml() | |
| 362 nodeXML = nodeXML.replace("\\", "\\\\") | |
| 363 nodeXML = nodeXML.replace("'", "\\'") | |
| 364 nodeXML = nodeXML.replace('"', '\\"') | |
| 365 nodeXML = nodeXML.replace('\n', '\\n') | |
| 366 nodeXML = nodeXML.replace('\r', ' ') | |
| 367 nodeXML = nodeXML.replace('\b', ' ') | |
| 368 nodeXML = nodeXML.replace('\t', ' ') | |
| 369 nodeXML = nodeXML.replace('\000', ' ') | |
| 370 nodeXML = nodeXML.replace('\v', ' ') | |
| 371 nodeXML = nodeXML.replace('\f', ' ') | |
| 372 | |
| 373 js = "parent.woven_replaceElement('%s', '%s')" % (nodeId, nodeXML) | |
| 374 #for key in widget.subviews.keys(): | |
| 375 # view.subviews[key].unlinkViews() | |
| 376 oldNode = page.view.subviews[nodeId] | |
| 377 for id, subview in oldNode.subviews.items(): | |
| 378 subview.unlinkViews() | |
| 379 topSubviews = page.view.subviews | |
| 380 #print "Widgetid, subviews", id(widget), widget.subviews | |
| 381 if widget.subviews: | |
| 382 def recurseSubviews(w): | |
| 383 #print "w.subviews", w.subviews | |
| 384 topSubviews.update(w.subviews) | |
| 385 for id, sv in w.subviews.items(): | |
| 386 recurseSubviews(sv) | |
| 387 #print "recursing" | |
| 388 recurseSubviews(widget) | |
| 389 #page.view.subviews.update(widget.subviews) | |
| 390 sess.sendScript(js) | |
| 391 | |
| 392 def wchild_WebConduit2_js(self, request): | |
| 393 #print "returning js file" | |
| 394 h = request.getHeader("user-agent") | |
| 395 if h.count("MSIE"): | |
| 396 fl = "WebConduit2_msie.js" | |
| 397 else: | |
| 398 fl = "WebConduit2_mozilla.js" | |
| 399 | |
| 400 return static.File(os.path.join(WOVEN_PATH, fl)) | |
| 401 | |
| 402 def wchild_FlashConduit_swf(self, request): | |
| 403 #print "returning flash file" | |
| 404 h = request.getHeader("user-agent") | |
| 405 if h.count("MSIE"): | |
| 406 fl = "FlashConduit.swf" | |
| 407 else: | |
| 408 fl = "FlashConduit.swf" | |
| 409 return static.File(os.path.join(WOVEN_PATH, fl)) | |
| 410 | |
| 411 def wchild_input_html(self, request): | |
| 412 return BlankPage() | |
| 413 | |
| 414 | |
| 415 class BlankPage(resource.Resource): | |
| 416 def render(self, request): | |
| 417 return "<html>This space intentionally left blank</html>" | |
| 418 | |
| 419 | |
| 420 WController = Controller | |
| 421 | |
| 422 def registerControllerForModel(controller, model): | |
| 423 """ | |
| 424 Registers `controller' as an adapter of `model' for IController, and | |
| 425 optionally registers it for IResource, if it implements it. | |
| 426 | |
| 427 @param controller: A class that implements L{interfaces.IController}, usuall
y a | |
| 428 L{Controller} subclass. Optionally it can implement | |
| 429 L{resource.IResource}. | |
| 430 @param model: Any class, but probably a L{twisted.web.woven.model.Model} | |
| 431 subclass. | |
| 432 """ | |
| 433 components.registerAdapter(controller, model, interfaces.IController) | |
| 434 if resource.IResource.implementedBy(controller): | |
| 435 components.registerAdapter(controller, model, resource.IResource) | |
| 436 | |
| OLD | NEW |