Decorating with Pylons
A while ago, I decided to use Pylons to rebuild my site. I even went so far as to name the engine “BonePyl”, which just narrowly edged out “BonesAW” for “Bones’ Awesome Weblog”. While doing this, I’ve obviously had to orient myself with the API, which meant buying The Definitive Guide to Pylons and copious scouring of the web for secondary documentation on SQL Alchemy and FormEncode. It’s a lot to bite off, and I’m having trouble chewing, but considering my current site is a bunch of PHP I threw together back in 1999, I’m obviously in no hurry.
While orienting myself with form validation, I figured it wouldn’t hurt to follow the recommended route and use python validators with a sprinkling of FormEncode’s htmlfill to avoid excessive over-coding of my forms. I decided to start with something simple: tag administration, since all good web-log software apparently has tags and dumps them into a tag cloud. Now, during this process, I followed the examples and produced a form with a submit button to save, and a submit button to cancel.
${h.submit(name="action", value="Save Changes")}
${h.submit(name="action", value="Cancel")}
But I noticed a horrible, glaring problem. Htmlfill actually goes through the rendered form and replaces appropriate elements with whatever the user submitted. This is a great time-saver, but it does this for every submitted value, and Pylons provides no way to instruct the included validation decorator to skip certain fields. I know this because I took a look at the offending code in the Pylons decorator library:
if post_only:
params = request.POST
else:
params = request.params
# The "params" variable is then transformed through an optional unicode
# decoding. Then some other stuff happens...
try:
self.form_result = schema.to_python(decoded, state)
except formencode.Invalid, e:
errors = e.unpack_errors(variable_decode, dict_char, list_char)
# Oops! e.value is ignored. Is it the same as params/decoded? Hmm. Then...
form_content = htmlfill.render(form_content, defaults=params,
errors=errors, **htmlfill_kwargs)
Notice this progression. They get the form values, validate against them, and in the case of an error, use the unfiltered values supplied by the user. This may be the same as the passed-in values, but why take that risk? In addition, with all the parameters the ‘validate’ decorator accepts, none are available to filter out fields. Because of this, htmlfill gleefully replaces both submit buttons with the contents of whichever one was last pressed. It’s easy enough to circumvent code in pylons thanks to its highly modular nature, so I wrote my own ‘validate’ decorator:
from decorator import decorator
from pylons import request
def validate(schema, form):
def wrapper(func, self, *args, **kwargs):
try:
params = dict(request.params)
del(params['action'])
self.form_result = schema.to_python(params)
except fe.Invalid, e:
html = form(self, *args, **kwargs)
return fe.htmlfill.render(html, e.value, e.error_dict or {})
return func(self, *args, **kwargs)
return decorator(wrapper)
This ‘validate’ decorator only takes two parameters: a validation schema instance, and a pointer to a function somewhere that returns the HTML to fill. I did this in case I need to alter the HTML myself or introduce template context values. Otherwise, this is about the bare minimum necessary to evoke a validation: try the validation, inject the default values and errors into the rendered template. And because I did this to scratch my own particular itch, I remove the friggin’ ‘action’ form request parameter. If I decided to take it that far, I could also provide a parameter to specify an array of all fields that should be filtered before rendering defaults and errors–something the Pylons folks should consider adding, themselves.
Now I can safely have fifteen submit buttons without all of them reading “Save Changes” because a validator got uppity and rejected all my hard work, filling in form fields.
Until Tomorrow