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 |