# -*- test-case-name: twisted.test.test_woven -*- # # WORK IN PROGRESS: HARD HAT REQUIRED # from __future__ import nested_scopes # Twisted Imports from twisted.python import formmethod, failure from twisted.python.components import registerAdapter, getAdapter from twisted.web import domhelpers, resource, util from twisted.internet import defer # Sibling Imports from twisted.web.woven import model, view, controller, widgets, input, interfaces from twisted.web.microdom import parseString, lmx, Element #other imports import math # map formmethod.Argument to functions that render them: _renderers = {} def registerRenderer(argumentClass, renderer): """Register a renderer for a given argument class. The renderer function should act in the same way as the 'input_XXX' methods of C{FormFillerWidget}. """ assert callable(renderer) global _renderers _renderers[argumentClass] = renderer class FormFillerWidget(widgets.Widget): SPANNING_TYPES = ["hidden", "submit"] def getValue(self, request, argument): """Return value for form input.""" if not self.model.alwaysDefault: values = request.args.get(argument.name, None) if values: try: return argument.coerce(values[0]) except formmethod.InputError: return values[0] return argument.default def getValues(self, request, argument): """Return values for form input.""" if not self.model.alwaysDefault: values = request.args.get(argument.name, None) if values: try: return argument.coerce(values) except formmethod.InputError: return values return argument.default def createShell(self, request, node, data): """Create a `shell' node that will hold the additional form elements, if one is required. """ return lmx(node).table(border="0") def input_single(self, request, content, model, templateAttributes={}): """ Returns a text input node built based upon the node model. Optionally takes an already-coded DOM node merges that information with the model's information. Returns a new (??) lmx node. """ #in a text field, only the following options are allowed (well, more #are, but they're not supported yet - can add them in later) attribs = ['type', 'name', 'value', 'size', 'maxlength', 'readonly'] #only MSIE recognizes readonly and disabled arguments = {} for attrib in attribs: #model hints and values override anything in the template val = model.getHint(attrib, templateAttributes.get(attrib, None)) if val: arguments[attrib] = str(val) value = self.getValue(request, model) if value: arguments["value"] = str(value) arguments["type"] = "text" #these are default arguments["name"] = model.name return content.input(**arguments) def input_string(self, request, content, model, templateAttributes={}): if not templateAttributes.has_key("size"): templateAttributes["size"] = '60' return self.input_single(request, content, model, templateAttributes) input_integer = input_single input_integerrange = input_single input_float = input_single def input_text(self, request, content, model, templateAttributes={}): r = content.textarea( cols=str(model.getHint('cols', templateAttributes.get('cols', '60'))), rows=str(model.getHint('rows', templateAttributes.get('rows', '10'))), name=model.name, wrap=str(model.getHint('wrap', templateAttributes.get('wrap', "virtual")))) r.text(str(self.getValue(request, model))) return r def input_hidden(self, request, content, model, templateAttributes={}): return content.input(type="hidden", name=model.name, value=str(self.getValue(request, model))) def input_submit(self, request, content, model, templateAttributes={}): arguments = {} val = model.getHint("onClick", templateAttributes.get("onClick", None)) if val: arguments["onClick"] = val arguments["type"] = "submit" arguments["name"] = model.name div = content.div() for tag, value, desc in model.choices: args = arguments.copy() args["value"] = tag div.input(**args) div.text(" ") if model.reset: div.input(type="reset") return div def input_choice(self, request, content, model, templateAttributes={}): # am I not evil? allow onChange js events arguments = {} val = model.getHint("onChange", templateAttributes.get("onChange", None)) if val: arguments["onChange"] = val arguments["name"] = model.name s = content.select(**arguments) default = self.getValues(request, model) for tag, value, desc in model.choices: kw = {} if value in default: kw = {'selected' : '1'} s.option(value=tag, **kw).text(desc) return s def input_group(self, request, content, model, groupValues, inputType, templateAttributes={}): """ Base code for a group of objects. Checkgroup will use this, as well as radiogroup. In the attributes, rows means how many rows the group should be arranged into, cols means how many cols the group should be arranged into. Columns take precedence over rows: if both are specified, the output will always generate the correct number of columns. However, if the number of elements in the group exceed (or is smaller than) rows*cols, then the number of rows will be off. A cols attribute of 1 will mean that all the elements will be listed one underneath another. The default is a rows attribute of 1: everything listed next to each other. """ rows = model.getHint('rows', templateAttributes.get('rows', None)) cols = model.getHint('cols', templateAttributes.get('cols', None)) if rows: rows = int(rows) if cols: cols = int(cols) defaults = self.getValues(request, model) if (rows and rows>1) or (cols and cols>1): #build a table s = content.table(border="0") if cols: breakat = cols else: breakat = math.ceil(float(len(groupValues))/rows) for i in range(0, len(groupValues), breakat): tr = s.tr() for j in range(0, breakat): if i+j >= len(groupValues): break tag, value, desc = groupValues[i+j] kw = {} if value in defaults: kw = {'checked' : '1'} tr.td().input(type=inputType, name=model.name, value=tag, **kw).text(desc) else: s = content.div() for tag, value, desc in groupValues: kw = {} if value in defaults: kw = {'checked' : '1'} s.input(type=inputType, name=model.name, value=tag, **kw).text(desc) if cols: s.br() return s def input_checkgroup(self, request, content, model, templateAttributes={}): return self.input_group(request, content, model, model.flags, "checkbox", templateAttributes) def input_radiogroup(self, request, content, model, templateAttributes={}): return self.input_group(request, content, model, model.choices, "radio", templateAttributes) #I don't know why they're the same, but they were. So I removed the #excess code. Maybe someone should look into removing it entirely. input_flags = input_checkgroup def input_boolean(self, request, content, model, templateAttributes={}): kw = {} if self.getValue(request, model): kw = {'checked' : '1'} return content.input(type="checkbox", name=model.name, **kw) def input_file(self, request, content, model, templateAttributes={}): kw = {} for attrib in ['size', 'accept']: val = model.getHint(attrib, templateAttributes.get(attrib, None)) if val: kw[attrib] = str(val) return content.input(type="file", name=model.name, **kw) def input_date(self, request, content, model, templateAttributes={}): breakLines = model.getHint('breaklines', 1) date = self.getValues(request, model) if date == None: year, month, day = "", "", "" else: year, month, day = date div = content.div() div.text("Year: ") div.input(type="text", size="4", maxlength="4", name=model.name, value=str(year)) if breakLines: div.br() div.text("Month: ") div.input(type="text", size="2", maxlength="2", name=model.name, value=str(month)) if breakLines: div.br() div.text("Day: ") div.input(type="text", size="2", maxlength="2", name=model.name, value=str(day)) return div def input_password(self, request, content, model, templateAttributes={}): return content.input( type="password", size=str(templateAttributes.get('size', "60")), name=model.name) def input_verifiedpassword(self, request, content, model, templateAttributes={}): breakLines = model.getHint('breaklines', 1) values = self.getValues(request, model) if isinstance(values, (str, unicode)): values = (values, values) if not values: p1, p2 = "", "" elif len(values) == 1: p1, p2 = values, "" elif len(values) == 2: p1, p2 = values else: p1, p2 = "", "" div = content.div() div.text("Password: ") div.input(type="password", size="20", name=model.name, value=str(p1)) if breakLines: div.br() div.text("Verify: ") div.input(type="password", size="20", name=model.name, value=str(p2)) return div def convergeInput(self, request, content, model, templateNode): name = model.__class__.__name__.lower() if _renderers.has_key(model.__class__): imeth = _renderers[model.__class__] else: imeth = getattr(self,"input_"+name) return imeth(request, content, model, templateNode.attributes).node def createInput(self, request, shell, model, templateAttributes={}): name = model.__class__.__name__.lower() if _renderers.has_key(model.__class__): imeth = _renderers[model.__class__] else: imeth = getattr(self,"input_"+name) if name in self.SPANNING_TYPES: td = shell.tr().td(valign="top", colspan="2") return (imeth(request, td, model).node, shell.tr().td(colspan="2").node) else: if model.allowNone: required = "" else: required = " *" tr = shell.tr() tr.td(align="right", valign="top").text(model.getShortDescription()+":"+required) content = tr.td(valign="top") return (imeth(request, content, model).node, content.div(_class="formDescription"). # because class is a keyword text(model.getLongDescription()).node) def setUp(self, request, node, data): # node = widgets.Widget.generateDOM(self,request,node) lmn = lmx(node) if not node.hasAttribute('action'): lmn['action'] = (request.prepath+request.postpath)[-1] if not node.hasAttribute("method"): lmn['method'] = 'post' lmn['enctype'] = 'multipart/form-data' self.errorNodes = errorNodes = {} # name: nodes which trap errors self.inputNodes = inputNodes = {} for errorNode in domhelpers.findElementsWithAttribute(node, 'errorFor'): errorNodes[errorNode.getAttribute('errorFor')] = errorNode argz={} # list to figure out which nodes are in the template already and which aren't hasSubmit = 0 argList = self.model.fmethod.getArgs() for arg in argList: if isinstance(arg, formmethod.Submit): hasSubmit = 1 argz[arg.name] = arg inNodes = domhelpers.findElements( node, lambda n: n.tagName.lower() in ('textarea', 'select', 'input', 'div')) for inNode in inNodes: t = inNode.getAttribute("type") if t and t.lower() == "submit": hasSubmit = 1 if not inNode.hasAttribute("name"): continue nName = inNode.getAttribute("name") if argz.has_key(nName): #send an empty content shell - we just want the node inputNodes[nName] = self.convergeInput(request, lmx(), argz[nName], inNode) inNode.parentNode.replaceChild(inputNodes[nName], inNode) del argz[nName] # TODO: # * some arg types should only have a single node (text, string, etc) # * some should have multiple nodes (choice, checkgroup) # * some have a bunch of ancillary nodes that are possible values (menu, radiogroup) # these should all be taken into account when walking through the template if argz: shell = self.createShell(request, node, data) # create inputs, in the same order they were passed to us: for remArg in [arg for arg in argList if argz.has_key(arg.name)]: inputNode, errorNode = self.createInput(request, shell, remArg) errorNodes[remArg.name] = errorNode inputNodes[remArg.name] = inputNode if not hasSubmit: lmn.input(type="submit") class FormErrorWidget(FormFillerWidget): def setUp(self, request, node, data): FormFillerWidget.setUp(self, request, node, data) for k, f in self.model.err.items(): en = self.errorNodes[k] tn = self.inputNodes[k] en.setAttribute('class', 'formError') tn.setAttribute('class', 'formInputError') en.childNodes[:]=[] # gurfle, CLEAR IT NOW!@# if isinstance(f, failure.Failure): f = f.getErrorMessage() lmx(en).text(str(f)) class FormDisplayModel(model.MethodModel): def initialize(self, fmethod, alwaysDefault=False): self.fmethod = fmethod self.alwaysDefault = alwaysDefault class FormErrorModel(FormDisplayModel): def initialize(self, fmethod, args, err): FormDisplayModel.initialize(self, fmethod) self.args = args if isinstance(err, failure.Failure): err = err.value if isinstance(err, Exception): self.err = getattr(err, "descriptions", {}) self.desc = err else: self.err = err self.desc = "Please try again" def wmfactory_description(self, request): return str(self.desc) class _RequestHack(model.MethodModel): def wmfactory_hack(self, request): rv = [[str(a), repr(b)] for (a, b) in request._outDict.items()] #print 'hack', rv return rv class FormProcessor(resource.Resource): def __init__(self, formMethod, callback=None, errback=None): resource.Resource.__init__(self) self.formMethod = formMethod if callback is None: callback = self.viewFactory self.callback = callback if errback is None: errback = self.errorViewFactory self.errback = errback def getArgs(self, request): """Return the formmethod.Arguments. Overridable hook to allow pre-processing, e.g. if we want to enable on them depending on one of the inputs. """ return self.formMethod.getArgs() def render(self, request): outDict = {} errDict = {} for methodArg in self.getArgs(request): valmethod = getattr(self,"mangle_"+ (methodArg.__class__.__name__.lower()), None) tmpval = request.args.get(methodArg.name) if valmethod: # mangle the argument to a basic datatype that coerce will like tmpval = valmethod(tmpval) # coerce it try: cv = methodArg.coerce(tmpval) outDict[methodArg.name] = cv except: errDict[methodArg.name] = failure.Failure() if errDict: # there were problems processing the form return self.errback(self.errorModelFactory( request.args, outDict, errDict)).render(request) else: try: if self.formMethod.takesRequest: outObj = self.formMethod.call(request=request, **outDict) else: outObj = self.formMethod.call(**outDict) except formmethod.FormException, e: err = request.errorInfo = self.errorModelFactory( request.args, outDict, e) return self.errback(err).render(request) else: request._outDict = outDict # CHOMP CHOMP! # I wanted better default behavior for debugging, so I could # see the arguments passed, but there is no channel for this in # the existing callback structure. So, here it goes. if isinstance(outObj, defer.Deferred): def _ebModel(err): if err.trap(formmethod.FormException): mf = self.errorModelFactory(request.args, outDict, err.value) return self.errback(mf) raise err (outObj .addCallback(self.modelFactory) .addCallback(self.callback) .addErrback(_ebModel)) return util.DeferredResource(outObj).render(request) else: return self.callback(self.modelFactory(outObj)).render( request) def errorModelFactory(self, args, out, err): return FormErrorModel(self.formMethod, args, err) def errorViewFactory(self, m): v = view.View(m) v.template = ''' Form Error View Error:
''' return v def modelFactory(self, outObj): adapt = getAdapter(outObj, interfaces.IModel, outObj) # print 'factorizing', adapt return adapt def viewFactory(self, model): # return getAdapter(model, interfaces.IView) if model is None: bodyStr = '''
''' model = _RequestHack() else: bodyStr = '
' v = view.View(model) v.template = ''' Thank You

Thank You for Using Woven

%s ''' % bodyStr return v # manglizers def mangle_single(self, args): if args: return args[0] else: return '' mangle_string = mangle_single mangle_text = mangle_single mangle_integer = mangle_single mangle_password = mangle_single mangle_integerrange = mangle_single mangle_float = mangle_single mangle_choice = mangle_single mangle_boolean = mangle_single mangle_hidden = mangle_single mangle_submit = mangle_single mangle_file = mangle_single mangle_radiogroup = mangle_single def mangle_multi(self, args): if args is None: return [] return args mangle_checkgroup = mangle_multi mangle_flags = mangle_multi from twisted.python.formmethod import FormMethod view.registerViewForModel(FormFillerWidget, FormDisplayModel) view.registerViewForModel(FormErrorWidget, FormErrorModel) registerAdapter(FormDisplayModel, FormMethod, interfaces.IModel)