# -*- 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 = '''
<html>
<head>
<title> Form Error View </title>
<style>
.formDescription {color: green}
.formError {color: red; font-weight: bold}
.formInputError {color: #900}
</style>
</head>
<body>
Error: <span model="description" />
<form model=".">
</form>
</body>
</html>
'''
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 = '''
<table model="hack" style="background-color: #99f">
<tr pattern="listItem" view="Widget">
<td model="0" style="font-weight: bold">
</td>
<td model="1">
</td>
</tr>
</table>
'''
model = _RequestHack()
else:
bodyStr = '<div model="." />'
v = view.View(model)
v.template = '''
<html>
<head>
<title> Thank You </title>
</head>
<body>
<h1>Thank You for Using Woven</h1>
%s
</body>
</html>
''' % 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)
syntax highlighted by Code2HTML, v. 0.9.1