OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.web.test.test_woven -*- | |
2 # | |
3 # WORK IN PROGRESS: HARD HAT REQUIRED | |
4 # | |
5 | |
6 from __future__ import nested_scopes | |
7 | |
8 # Twisted Imports | |
9 | |
10 from twisted.python import formmethod, failure | |
11 from twisted.python.components import registerAdapter | |
12 from twisted.web import domhelpers, resource, util | |
13 from twisted.internet import defer | |
14 | |
15 # Sibling Imports | |
16 from twisted.web.woven import model, view, controller, widgets, input, interface
s | |
17 | |
18 from twisted.web.microdom import parseString, lmx, Element | |
19 | |
20 | |
21 #other imports | |
22 import math | |
23 | |
24 # map formmethod.Argument to functions that render them: | |
25 _renderers = {} | |
26 | |
27 def registerRenderer(argumentClass, renderer): | |
28 """Register a renderer for a given argument class. | |
29 | |
30 The renderer function should act in the same way | |
31 as the 'input_XXX' methods of C{FormFillerWidget}. | |
32 """ | |
33 assert callable(renderer) | |
34 global _renderers | |
35 _renderers[argumentClass] = renderer | |
36 | |
37 | |
38 class FormFillerWidget(widgets.Widget): | |
39 | |
40 SPANNING_TYPES = ["hidden", "submit"] | |
41 | |
42 def getValue(self, request, argument): | |
43 """Return value for form input.""" | |
44 if not self.model.alwaysDefault: | |
45 values = request.args.get(argument.name, None) | |
46 if values: | |
47 try: | |
48 return argument.coerce(values[0]) | |
49 except formmethod.InputError: | |
50 return values[0] | |
51 return argument.default | |
52 | |
53 def getValues(self, request, argument): | |
54 """Return values for form input.""" | |
55 if not self.model.alwaysDefault: | |
56 values = request.args.get(argument.name, None) | |
57 if values: | |
58 try: | |
59 return argument.coerce(values) | |
60 except formmethod.InputError: | |
61 return values | |
62 return argument.default | |
63 | |
64 def createShell(self, request, node, data): | |
65 """Create a `shell' node that will hold the additional form | |
66 elements, if one is required. | |
67 """ | |
68 return lmx(node).table(border="0") | |
69 | |
70 def input_single(self, request, content, model, templateAttributes={}): | |
71 """ | |
72 Returns a text input node built based upon the node model. | |
73 Optionally takes an already-coded DOM node merges that | |
74 information with the model's information. Returns a new (??) | |
75 lmx node. | |
76 """ | |
77 #in a text field, only the following options are allowed (well, more | |
78 #are, but they're not supported yet - can add them in later) | |
79 attribs = ['type', 'name', 'value', 'size', 'maxlength', | |
80 'readonly'] #only MSIE recognizes readonly and disabled | |
81 | |
82 arguments = {} | |
83 for attrib in attribs: | |
84 #model hints and values override anything in the template | |
85 val = model.getHint(attrib, templateAttributes.get(attrib, None)) | |
86 if val: | |
87 arguments[attrib] = str(val) | |
88 | |
89 value = self.getValue(request, model) | |
90 if value: | |
91 arguments["value"] = str(value) | |
92 | |
93 arguments["type"] = "text" #these are default | |
94 arguments["name"] = model.name | |
95 | |
96 return content.input(**arguments) | |
97 | |
98 def input_string(self, request, content, model, templateAttributes={}): | |
99 if not templateAttributes.has_key("size"): | |
100 templateAttributes["size"] = '60' | |
101 return self.input_single(request, content, model, templateAttributes) | |
102 | |
103 input_integer = input_single | |
104 input_integerrange = input_single | |
105 input_float = input_single | |
106 | |
107 def input_text(self, request, content, model, templateAttributes={}): | |
108 r = content.textarea( | |
109 cols=str(model.getHint('cols', | |
110 templateAttributes.get('cols', '60'))), | |
111 rows=str(model.getHint('rows', | |
112 templateAttributes.get('rows', '10'))), | |
113 name=model.name, | |
114 wrap=str(model.getHint('wrap', | |
115 templateAttributes.get('wrap', "virtual")))) | |
116 r.text(str(self.getValue(request, model))) | |
117 return r | |
118 | |
119 def input_hidden(self, request, content, model, templateAttributes={}): | |
120 return content.input(type="hidden", | |
121 name=model.name, | |
122 value=str(self.getValue(request, model))) | |
123 | |
124 def input_submit(self, request, content, model, templateAttributes={}): | |
125 arguments = {} | |
126 val = model.getHint("onClick", templateAttributes.get("onClick", None)) | |
127 if val: | |
128 arguments["onClick"] = val | |
129 arguments["type"] = "submit" | |
130 arguments["name"] = model.name | |
131 div = content.div() | |
132 for tag, value, desc in model.choices: | |
133 args = arguments.copy() | |
134 args["value"] = tag | |
135 div.input(**args) | |
136 div.text(" ") | |
137 if model.reset: | |
138 div.input(type="reset") | |
139 return div | |
140 | |
141 def input_choice(self, request, content, model, templateAttributes={}): | |
142 # am I not evil? allow onChange js events | |
143 arguments = {} | |
144 val = model.getHint("onChange", templateAttributes.get("onChange", None)
) | |
145 if val: | |
146 arguments["onChange"] = val | |
147 arguments["name"] = model.name | |
148 s = content.select(**arguments) | |
149 default = self.getValues(request, model) | |
150 for tag, value, desc in model.choices: | |
151 kw = {} | |
152 if value in default: | |
153 kw = {'selected' : '1'} | |
154 s.option(value=tag, **kw).text(desc) | |
155 return s | |
156 | |
157 def input_group(self, request, content, model, groupValues, inputType, | |
158 templateAttributes={}): | |
159 """ | |
160 Base code for a group of objects. Checkgroup will use this, as | |
161 well as radiogroup. In the attributes, rows means how many rows | |
162 the group should be arranged into, cols means how many cols the | |
163 group should be arranged into. Columns take precedence over | |
164 rows: if both are specified, the output will always generate the | |
165 correct number of columns. However, if the number of elements | |
166 in the group exceed (or is smaller than) rows*cols, then the | |
167 number of rows will be off. A cols attribute of 1 will mean that | |
168 all the elements will be listed one underneath another. The | |
169 default is a rows attribute of 1: everything listed next to each | |
170 other. | |
171 """ | |
172 rows = model.getHint('rows', templateAttributes.get('rows', None)) | |
173 cols = model.getHint('cols', templateAttributes.get('cols', None)) | |
174 if rows: | |
175 rows = int(rows) | |
176 if cols: | |
177 cols = int(cols) | |
178 | |
179 defaults = self.getValues(request, model) | |
180 if (rows and rows>1) or (cols and cols>1): #build a table | |
181 s = content.table(border="0") | |
182 if cols: | |
183 breakat = cols | |
184 else: | |
185 breakat = math.ceil(float(len(groupValues))/rows) | |
186 for i in range(0, len(groupValues), breakat): | |
187 tr = s.tr() | |
188 for j in range(0, breakat): | |
189 if i+j >= len(groupValues): | |
190 break | |
191 tag, value, desc = groupValues[i+j] | |
192 kw = {} | |
193 if value in defaults: | |
194 kw = {'checked' : '1'} | |
195 tr.td().input(type=inputType, name=model.name, | |
196 value=tag, **kw).text(desc) | |
197 | |
198 else: | |
199 s = content.div() | |
200 for tag, value, desc in groupValues: | |
201 kw = {} | |
202 if value in defaults: | |
203 kw = {'checked' : '1'} | |
204 s.input(type=inputType, name=model.name, | |
205 value=tag, **kw).text(desc) | |
206 if cols: | |
207 s.br() | |
208 | |
209 return s | |
210 | |
211 def input_checkgroup(self, request, content, model, templateAttributes={}): | |
212 return self.input_group(request, content, model, model.flags, | |
213 "checkbox", templateAttributes) | |
214 | |
215 def input_radiogroup(self, request, content, model, templateAttributes={}): | |
216 return self.input_group(request, content, model, model.choices, | |
217 "radio", templateAttributes) | |
218 | |
219 #I don't know why they're the same, but they were. So I removed the | |
220 #excess code. Maybe someone should look into removing it entirely. | |
221 input_flags = input_checkgroup | |
222 | |
223 def input_boolean(self, request, content, model, templateAttributes={}): | |
224 kw = {} | |
225 if self.getValue(request, model): | |
226 kw = {'checked' : '1'} | |
227 return content.input(type="checkbox", name=model.name, **kw) | |
228 | |
229 def input_file(self, request, content, model, templateAttributes={}): | |
230 kw = {} | |
231 for attrib in ['size', 'accept']: | |
232 val = model.getHint(attrib, templateAttributes.get(attrib, None)) | |
233 if val: | |
234 kw[attrib] = str(val) | |
235 return content.input(type="file", name=model.name, **kw) | |
236 | |
237 def input_date(self, request, content, model, templateAttributes={}): | |
238 breakLines = model.getHint('breaklines', 1) | |
239 date = self.getValues(request, model) | |
240 if date == None: | |
241 year, month, day = "", "", "" | |
242 else: | |
243 year, month, day = date | |
244 div = content.div() | |
245 div.text("Year: ") | |
246 div.input(type="text", size="4", maxlength="4", name=model.name, value=s
tr(year)) | |
247 if breakLines: | |
248 div.br() | |
249 div.text("Month: ") | |
250 div.input(type="text", size="2", maxlength="2", name=model.name, value=s
tr(month)) | |
251 if breakLines: | |
252 div.br() | |
253 div.text("Day: ") | |
254 div.input(type="text", size="2", maxlength="2", name=model.name, value=s
tr(day)) | |
255 return div | |
256 | |
257 def input_password(self, request, content, model, templateAttributes={}): | |
258 return content.input( | |
259 type="password", | |
260 size=str(templateAttributes.get('size', "60")), | |
261 name=model.name) | |
262 | |
263 def input_verifiedpassword(self, request, content, model, templateAttributes
={}): | |
264 breakLines = model.getHint('breaklines', 1) | |
265 values = self.getValues(request, model) | |
266 if isinstance(values, (str, unicode)): | |
267 values = (values, values) | |
268 if not values: | |
269 p1, p2 = "", "" | |
270 elif len(values) == 1: | |
271 p1, p2 = values, "" | |
272 elif len(values) == 2: | |
273 p1, p2 = values | |
274 else: | |
275 p1, p2 = "", "" | |
276 div = content.div() | |
277 div.text("Password: ") | |
278 div.input(type="password", size="20", name=model.name, value=str(p1)) | |
279 if breakLines: | |
280 div.br() | |
281 div.text("Verify: ") | |
282 div.input(type="password", size="20", name=model.name, value=str(p2)) | |
283 return div | |
284 | |
285 | |
286 def convergeInput(self, request, content, model, templateNode): | |
287 name = model.__class__.__name__.lower() | |
288 if _renderers.has_key(model.__class__): | |
289 imeth = _renderers[model.__class__] | |
290 else: | |
291 imeth = getattr(self,"input_"+name) | |
292 | |
293 return imeth(request, content, model, templateNode.attributes).node | |
294 | |
295 def createInput(self, request, shell, model, templateAttributes={}): | |
296 name = model.__class__.__name__.lower() | |
297 if _renderers.has_key(model.__class__): | |
298 imeth = _renderers[model.__class__] | |
299 else: | |
300 imeth = getattr(self,"input_"+name) | |
301 if name in self.SPANNING_TYPES: | |
302 td = shell.tr().td(valign="top", colspan="2") | |
303 return (imeth(request, td, model).node, shell.tr().td(colspan="2").n
ode) | |
304 else: | |
305 if model.allowNone: | |
306 required = "" | |
307 else: | |
308 required = " *" | |
309 tr = shell.tr() | |
310 tr.td(align="right", valign="top").text(model.getShortDescription()+
":"+required) | |
311 content = tr.td(valign="top") | |
312 return (imeth(request, content, model).node, | |
313 content.div(_class="formDescription"). # because class is a
keyword | |
314 text(model.getLongDescription()).node) | |
315 | |
316 def setUp(self, request, node, data): | |
317 # node = widgets.Widget.generateDOM(self,request,node) | |
318 lmn = lmx(node) | |
319 if not node.hasAttribute('action'): | |
320 lmn['action'] = (request.prepath+request.postpath)[-1] | |
321 if not node.hasAttribute("method"): | |
322 lmn['method'] = 'post' | |
323 lmn['enctype'] = 'multipart/form-data' | |
324 self.errorNodes = errorNodes = {} # name: nodes whic
h trap errors | |
325 self.inputNodes = inputNodes = {} | |
326 for errorNode in domhelpers.findElementsWithAttribute(node, 'errorFor'): | |
327 errorNodes[errorNode.getAttribute('errorFor')] = errorNode | |
328 argz={} | |
329 # list to figure out which nodes are in the template already and which a
ren't | |
330 hasSubmit = 0 | |
331 argList = self.model.fmethod.getArgs() | |
332 for arg in argList: | |
333 if isinstance(arg, formmethod.Submit): | |
334 hasSubmit = 1 | |
335 argz[arg.name] = arg | |
336 inNodes = domhelpers.findElements( | |
337 node, | |
338 lambda n: n.tagName.lower() in ('textarea', 'select', 'input', | |
339 'div')) | |
340 for inNode in inNodes: | |
341 t = inNode.getAttribute("type") | |
342 if t and t.lower() == "submit": | |
343 hasSubmit = 1 | |
344 if not inNode.hasAttribute("name"): | |
345 continue | |
346 nName = inNode.getAttribute("name") | |
347 if argz.has_key(nName): | |
348 #send an empty content shell - we just want the node | |
349 inputNodes[nName] = self.convergeInput(request, lmx(), | |
350 argz[nName], inNode) | |
351 inNode.parentNode.replaceChild(inputNodes[nName], inNode) | |
352 del argz[nName] | |
353 # TODO: | |
354 # * some arg types should only have a single node (text, string, etc
) | |
355 # * some should have multiple nodes (choice, checkgroup) | |
356 # * some have a bunch of ancillary nodes that are possible values (m
enu, radiogroup) | |
357 # these should all be taken into account when walking through the te
mplate | |
358 if argz: | |
359 shell = self.createShell(request, node, data) | |
360 # create inputs, in the same order they were passed to us: | |
361 for remArg in [arg for arg in argList if argz.has_key(arg.name)]: | |
362 inputNode, errorNode = self.createInput(request, shell, remArg) | |
363 errorNodes[remArg.name] = errorNode | |
364 inputNodes[remArg.name] = inputNode | |
365 | |
366 if not hasSubmit: | |
367 lmn.input(type="submit") | |
368 | |
369 | |
370 class FormErrorWidget(FormFillerWidget): | |
371 def setUp(self, request, node, data): | |
372 FormFillerWidget.setUp(self, request, node, data) | |
373 for k, f in self.model.err.items(): | |
374 en = self.errorNodes[k] | |
375 tn = self.inputNodes[k] | |
376 en.setAttribute('class', 'formError') | |
377 tn.setAttribute('class', 'formInputError') | |
378 en.childNodes[:]=[] # gurfle, CLEAR IT NOW!@# | |
379 if isinstance(f, failure.Failure): | |
380 f = f.getErrorMessage() | |
381 lmx(en).text(str(f)) | |
382 | |
383 | |
384 class FormDisplayModel(model.MethodModel): | |
385 def initialize(self, fmethod, alwaysDefault=False): | |
386 self.fmethod = fmethod | |
387 self.alwaysDefault = alwaysDefault | |
388 | |
389 class FormErrorModel(FormDisplayModel): | |
390 def initialize(self, fmethod, args, err): | |
391 FormDisplayModel.initialize(self, fmethod) | |
392 self.args = args | |
393 if isinstance(err, failure.Failure): | |
394 err = err.value | |
395 if isinstance(err, Exception): | |
396 self.err = getattr(err, "descriptions", {}) | |
397 self.desc = err | |
398 else: | |
399 self.err = err | |
400 self.desc = "Please try again" | |
401 | |
402 def wmfactory_description(self, request): | |
403 return str(self.desc) | |
404 | |
405 class _RequestHack(model.MethodModel): | |
406 def wmfactory_hack(self, request): | |
407 rv = [[str(a), repr(b)] for (a, b) | |
408 in request._outDict.items()] | |
409 #print 'hack', rv | |
410 return rv | |
411 | |
412 class FormProcessor(resource.Resource): | |
413 def __init__(self, formMethod, callback=None, errback=None): | |
414 resource.Resource.__init__(self) | |
415 self.formMethod = formMethod | |
416 if callback is None: | |
417 callback = self.viewFactory | |
418 self.callback = callback | |
419 if errback is None: | |
420 errback = self.errorViewFactory | |
421 self.errback = errback | |
422 | |
423 def getArgs(self, request): | |
424 """Return the formmethod.Arguments. | |
425 | |
426 Overridable hook to allow pre-processing, e.g. if we want to enable | |
427 on them depending on one of the inputs. | |
428 """ | |
429 return self.formMethod.getArgs() | |
430 | |
431 def render(self, request): | |
432 outDict = {} | |
433 errDict = {} | |
434 for methodArg in self.getArgs(request): | |
435 valmethod = getattr(self,"mangle_"+ | |
436 (methodArg.__class__.__name__.lower()), None) | |
437 tmpval = request.args.get(methodArg.name) | |
438 if valmethod: | |
439 # mangle the argument to a basic datatype that coerce will like | |
440 tmpval = valmethod(tmpval) | |
441 # coerce it | |
442 try: | |
443 cv = methodArg.coerce(tmpval) | |
444 outDict[methodArg.name] = cv | |
445 except: | |
446 errDict[methodArg.name] = failure.Failure() | |
447 if errDict: | |
448 # there were problems processing the form | |
449 return self.errback(self.errorModelFactory( | |
450 request.args, outDict, errDict)).render(request) | |
451 else: | |
452 try: | |
453 if self.formMethod.takesRequest: | |
454 outObj = self.formMethod.call(request=request, **outDict) | |
455 else: | |
456 outObj = self.formMethod.call(**outDict) | |
457 except formmethod.FormException, e: | |
458 err = request.errorInfo = self.errorModelFactory( | |
459 request.args, outDict, e) | |
460 return self.errback(err).render(request) | |
461 else: | |
462 request._outDict = outDict # CHOMP CHOMP! | |
463 # I wanted better default behavior for debugging, so I could | |
464 # see the arguments passed, but there is no channel for this in | |
465 # the existing callback structure. So, here it goes. | |
466 if isinstance(outObj, defer.Deferred): | |
467 def _ebModel(err): | |
468 if err.trap(formmethod.FormException): | |
469 mf = self.errorModelFactory(request.args, outDict, | |
470 err.value) | |
471 return self.errback(mf) | |
472 raise err | |
473 (outObj | |
474 .addCallback(self.modelFactory) | |
475 .addCallback(self.callback) | |
476 .addErrback(_ebModel)) | |
477 return util.DeferredResource(outObj).render(request) | |
478 else: | |
479 return self.callback(self.modelFactory(outObj)).render( | |
480 request) | |
481 | |
482 def errorModelFactory(self, args, out, err): | |
483 return FormErrorModel(self.formMethod, args, err) | |
484 | |
485 def errorViewFactory(self, m): | |
486 v = view.View(m) | |
487 v.template = ''' | |
488 <html> | |
489 <head> | |
490 <title> Form Error View </title> | |
491 <style> | |
492 .formDescription {color: green} | |
493 .formError {color: red; font-weight: bold} | |
494 .formInputError {color: #900} | |
495 </style> | |
496 </head> | |
497 <body> | |
498 Error: <span model="description" /> | |
499 <form model="."> | |
500 </form> | |
501 </body> | |
502 </html> | |
503 ''' | |
504 return v | |
505 | |
506 def modelFactory(self, outObj): | |
507 adapt = interfaces.IModel(outObj, outObj) | |
508 # print 'factorizing', adapt | |
509 return adapt | |
510 | |
511 def viewFactory(self, model): | |
512 # return interfaces.IView(model) | |
513 if model is None: | |
514 bodyStr = ''' | |
515 <table model="hack" style="background-color: #99f"> | |
516 <tr pattern="listItem" view="Widget"> | |
517 <td model="0" style="font-weight: bold"> | |
518 </td> | |
519 <td model="1"> | |
520 </td> | |
521 </tr> | |
522 </table> | |
523 ''' | |
524 model = _RequestHack() | |
525 else: | |
526 bodyStr = '<div model="." />' | |
527 v = view.View(model) | |
528 v.template = ''' | |
529 <html> | |
530 <head> | |
531 <title> Thank You </title> | |
532 </head> | |
533 <body> | |
534 <h1>Thank You for Using Woven</h1> | |
535 %s | |
536 </body> | |
537 </html> | |
538 ''' % bodyStr | |
539 return v | |
540 | |
541 # manglizers | |
542 | |
543 def mangle_single(self, args): | |
544 if args: | |
545 return args[0] | |
546 else: | |
547 return '' | |
548 | |
549 mangle_string = mangle_single | |
550 mangle_text = mangle_single | |
551 mangle_integer = mangle_single | |
552 mangle_password = mangle_single | |
553 mangle_integerrange = mangle_single | |
554 mangle_float = mangle_single | |
555 mangle_choice = mangle_single | |
556 mangle_boolean = mangle_single | |
557 mangle_hidden = mangle_single | |
558 mangle_submit = mangle_single | |
559 mangle_file = mangle_single | |
560 mangle_radiogroup = mangle_single | |
561 | |
562 def mangle_multi(self, args): | |
563 if args is None: | |
564 return [] | |
565 return args | |
566 | |
567 mangle_checkgroup = mangle_multi | |
568 mangle_flags = mangle_multi | |
569 | |
570 from twisted.python.formmethod import FormMethod | |
571 | |
572 view.registerViewForModel(FormFillerWidget, FormDisplayModel) | |
573 view.registerViewForModel(FormErrorWidget, FormErrorModel) | |
574 registerAdapter(FormDisplayModel, FormMethod, interfaces.IModel) | |
575 | |
OLD | NEW |