Better Form Generation with Mako and Pylons
Update - The formhelpers demo is updated and is production ready. See the end of the post for details.
If you're a web developer in New York City, unless you work here, here or maybe here, you're probably not using Python for primary development (if you are, please post your company here). Since I'm not a PHP monkey or a Microsoftie, everywhere I work they're using Struts, which is where the dust has settled around Java Server Pages (with honorable mentions to Spring MVC and JSF).
I'm in a position where I might be able to push Python technology for a project or two, which have until now been built on Struts 1, the most common version of Struts even though Struts 2 is considerably nicer. So I wanted to see where Pylons is at with form rendering and processing these days. Most importantly, I wanted to ensure that layout is accomplished plainly within a template with no module-embedded HTML and no "magic generation" of forms from classes or other datastructures, and that the cycle of data from form to controller back to form again is similarly simple and obvious. Most projects in New York are the kind that get handed off to totally different people when complete, or even just 80% complete since you've been put onto something else, for remaining development and future enhancements. Code handoffs are extremely common, so it can't be overstated how much more important it is to be obvious than to be DRY. This is a big reason that tedious, plodding approaches like Struts and PHP are so popular - through raw verbosity they discourage opaque ways of doing things. Overly clever, data-driven rendering solutions that nobody can understand or extend (and are usually broken, anyway) are basically what gets thrown away and rewritten by the next team.
In developing Mako, a primary goal was to make a super-nice version of a particular "component" pattern which I had used for years primarily with HTML::Mason which for me provides a "sweet spot" of obviousness, agility, and succinctness. The focus is around the ability to create "tag libraries" which interact easily with a server-parsed templating language, and which can be implemented within templates themselves. In JSP development, taglibs are now the standard way to indicate dynamic areas of templates, but while they look pretty clean, they are painful to implement (requiring HTML embedded in hand-crafted classes, a few dozen XML pushups for every tag you add, and the obligatory application restart whenever they change), and the EL and OGNL expressions which are standard within taglibs interact terribly with straight Java code.
Mako allows the creation of tags which can be arbitrarily nested and interactive with one another via the <%def> construct, in combination with the <%call> tag. It's been my observation that the <%call> tag as well as the usage of nesting-aware <%defs> hasn't caught on yet, as the examples in the docs are a little dense, so here I will seek to demystify it a bit.
Pylons currently recommends a decent approach to rendering forms, using Form Tags which are essentially little functions you can embed in your template to render standard form elements. The handling of the form at the controller level uses FormEncode and routes validation errors through htmlfill. My approach modifies this to use Mako tags to build a site-specific taglib around the webhelpers tags and adds an explicit interaction between those tags and the controller, in a manner similar to a Struts form handler, which replaces htmlfill and allows all layout, including the layout of validation error messages, using the same template system. It also adds a preprocessor that illustrates how to build custom tags in Mako which look as nice as the built-in ones.
A tar.gz of the approach, which at this point should be regarded strictly as a proof of concept, can be downloaded here (works against Pylons 0.9.7), which contains three templates each illustrating a different approach to laying out the form. The three approaches are raw webhelpers with htmlfill, the <%call> tag approach, and finally using the "custom tag" approach. The final result, present in the file templates/mako_helpers.html, looks like this:
<%namespace name="form" file="/form_tags.mako"/>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Mako Form Helpers</title>
<link rel="stylesheet" href="/style.css" type="text/css" />
</head>
<body>
<h3>Using Mako Helpers</h3>
<%form:form controller='comment' action='mako_post'>
<table>
<tr>
<th colspan="2">Submit your Comment</th>
</tr>
<tr>
<td>Your Name:</td>
<td><%form:text name="name"/></td>
</tr>
<tr>
<td>How did you hear about this site ?</td>
<td>
<%form:select name="heard">
<%form:option value="" selected="True">None</%form:option>
% for desc, value in c.heard_choices:
<%form:option value="${value}">${desc}</%form:option>
% endfor
</%form:select>
</td>
</tr>
<tr>
<td>Comment:</td>
<td><%form:textarea name="comment"/></td>
</tr>
<tr>
<td colspan="2"><%form:submit/></td>
</tr>
</table>
</%form:form>
</body>
</html>
Where of note are the Mako-like <%form:foo> tags that aren't part of Mako ! A short preprocessor is applied to the source file which turns a tag like <%form:foo> into <%call expr="foo()"> at template compile time. I.e., a tag used above as:
<%form:textarea name="comment"/>
is preprocessed into raw Mako code:
<%call expr="form.textarea(name='comment')"/>
The %call tag invokes the textarea def inside the form namespace, which is defined in the file form_tags.mako. The def for textarea looks like:
<%def name="textarea(name, default=None, **attrs)">\
<%doc>
Render an HTML <textarea></textarea> tag pair with embedded content.
</%doc>\
${form_errors(name)}\
${h.textarea(name, content=request.params.get(name, default), **attrs)}\
</%def>
Above, the $form_errors(name) is a def call used for reporting validation messages. The point of form_tags.mako is that all the form tags and their layout is plainly visible and easily customized. Multiple versions of the file can be used in one application, providing different form layouts for different areas. The fact that it uses the h helper to render the actual HTML for each form control is also arbitrary; you could just as well implement the <textarea> source directly within the def if some special treatment were needed.
The demo also contains a modified version of Pylons' @validate decorator. Usage is mostly the same, except the form parameter is replaced by the more direct input_controller parameter, which is the method used for input:
from formhelpers.lib.mako_forms import validate as mako_validate
class CommentController(BaseController):
def index(self):
return render('/mako_helpers.html')
@mako_validate(schema=CommentForm, input_controller=index)
def mako_post(self):
c.name = self.form_result['name']
return render('/thanks.html')
Where above, mako_post hits the validator, and on an invalid exception the index controller method is called instead. Validation errors are placed in self.form_errors as well as c.form_errors for template access. The validator as well as the preprocessor are defined in lib/mako_forms.py.
That's pretty much all there is to it at this point, a few folks on #pylons seem enthused about it, so perhaps this can be turned into something more formally available and/or recommended when implementing a Pylons/Mako application.
I've refined the formhelpers demo with a @validate function tailored to a single usage pattern (download). Each form now has a name, which identifies its place on c, such as:
<%form:form name="comment_form" controller='comment' action='post'>
The controller now places a comment_form dictionary on c in all cases, which the @validate function takes care of on the post side:
@validate("comment_form", CommentForm, input_controller=index)
def post(self):
c.name = self.form_result['name']
return render('/thanks.html')
The hacky methodology of relating the <%form:option> tag to its parent <%form:select> tag has also been replaced with something reasonable.
This version of formhelpers is production ready.
As far as comments regarding @rest.dispatch_on and forcing a GET to accomodate it, I think that's kind of ugly since I don't really believe in "virtual requests", which is one reason I rewrote the decorator in the first place (i.e., to remove the "re-get" aspect of it). If you are dual-purposing your index() method to display the form on GET as well as to re-display on POST, then index() needs to accommodate both methods. Otherwise you can break index() out into index() and _display_index() and have _display_index() be your input_controller.
Patrick:
Very nice, and very nicely explained. I'm wondering if it is possible to consider generalizing this further to make it accessible from werkzeug as well?
1 July 2008, 3:23 pmShannon -jj Behrens:
Some comments:
In general, it looks fine.
Life in my world is different. I care a lot more about DRY since I end up maintaining the code too.
I like to separate log level "tag" functions from higher level functions that also take care of putting in the error field. I prefer to use the higher level functions, but sometimes, you might need to control the error output and field output separately.
I like to have a way of creating higher level "macros" that might involve multiple fields, such as a date widget.
Remember, my defaults might not come from the request parameters. They might come from a database record or from somewhere else. I like to set the defaults for the form as a whole in a single place using a list of dicts (i.e. first check here, otherwise check here, otherwise fall back to here).
My favorite form toolkit is still Aquarium's http://aquarium.cvs.sourceforge.net/aquarium/aquarium/aquarium/widget/FormUtil.py?revision=1.16&view=markup&pathrev=MAIN because it took a very extensible, plugin-able, macro-like approach. However, I understand that it might be a bit too magical for your situation.
1 July 2008, 3:24 pmShannon -jj Behrens:
One more thing, it's important to be able to create hierarchies of form field values. FormEncode supports this. For instance, I might need to configure three computers each with three services each with one port. PHP, RoR, Aquarium, and FormEncode all have a way of creating such a hierarchy of form field values.
1 July 2008, 3:29 pmzzzeek:
shannon -
easy enough to modify the tags to read defaults from a "default_dict" or somesuch attached to
c.you can also use this approach to make higher-level widgets, in the same manner as the low level widgets. that's the whole point !
Also this supports the formencode hierarchies using "." and "[]" syntaxes; that was already built in to the previous
1 July 2008, 3:30 pmvalidatedecorator (since we are still using `FormEncode` almost identically as before).Shannon -jj Behrens:
"Modifying the tags" was one of the things I worked hard to make unnecessary. That's why I used a "hierarchy of macros" approach. At most, you should only need to subclass.
1 July 2008, 4:04 pmzzzeek:
See, i dont want to make a "subclass" in order to create an HTML widget. Just like I dont want to build a picture-drawing robot just to draw a picture :). If I'm creating an HTML component, I want to use HTML; you know what I'm talking about since you used HTML::Mason at some point if I remember correctly.
1 July 2008, 4:07 pmShannon -jj Behrens:
By the way, allowing people to plugin new "meta widgets" like data widgets via setuptools plugins makes a lot of sense too. That's sort of like ToscaWidgets, but it makes sense to have something specific to your situation.
1 July 2008, 4:12 pmMatt Feifarek:
I love the idea of this; trying to implement a similar thing to play with it and see how it goes, and I get a Mako syntax error:
SyntaxException: (SyntaxError) invalid syntax (, line 1) ...
I tried this with Mako version 1.8 and 2.2
Thinking it might be a version problem, I made a virtualenv without access to site-wide site-packages and installed formhelper into it with dependencies, and ran that.
I get: ImportError: cannot import name StatusCodeRedirect
So I forced the virtualenv to install pylons dev version, which works. But now I can't tell which bit is necessary to try out your custom tag trick.
I can import a namespace, and access its members, but through the old ${newspace.member(name='foo')} syntax, not which still causes a syntax error.
Great idea, I'm looking forward to getting it running ;-)
2 July 2008, 4:40 pmzzzeek:
try running the entire example, with Pylons 0.9.7, as is. There's configuration in the config/environment.py to get the custom tags.
2 July 2008, 4:46 pmKrishgy:
You know what, I am using Mako templates to generate the Java struts projects based on Appfuse struts-spring-hibernate.
Mako does the job very well. Similarly we used mako to generate our device configuration in very nicer XML content which is used for millions for people across the world to configure the flow meters.
Though I hack Pylons, SqlAlchemy and Mako at home, we use Python very rare in our office. I am marketing Python and Mako at our office for code generation.
Thanks a lot Mike.
3 July 2008, 9:50 amMatt Feifarek:
Aha! I didn't catch that you put a hook in environment.py.
Thanks!
3 July 2008, 11:40 amNoah Gift:
Mike,
I would bet there are a LOT of interactive web companies that are looking to use Python in New York. I think that more blogs posts that highlight how Python is a different beast than PHP and Java would be helpful. PHP is pretty efficient for "throw away" projects, but often those projects could be reused later if they were written in Python.
Although I am not currently living in New York, I think it would be good to get the word out that web development with Flash, Javascript and Python is a killer stack. That is currently what I am doing.
Noah
5 July 2008, 7:23 pmOlaf Conradi:
I like this idea, but how do you deal with hiding the validating controller like so:
On submit this leads to a loop between dispatch and input_controller. With @validate(form='index', schema=CommentForm()) on Pylons 0.9.6 this used to work.
Any ideas?
2 August 2008, 4:33 amOlaf Conradi:
The validate decorator should contain:
if errors: request.environ['REQUEST_METHOD'] = 'GET'
2 August 2008, 4:46 amMatt Feifarek:
One possible weirdness...
If one doesn't successfully pass the validation step, ones browser is sitting with a form (with errors) on /comment/post/. This URL is invalid for any followup GET.
If they GET that page, you get a big nasty error: AttributeError: 'CommentController' object has no attribute 'form_result'
Likewise, if they ARE successful, and they reload, or GET that page, the same thing happens. That's easier, as it's a pretty standard practice to redirect upon success (if only to avoid the dual-submit problem).
The former is more troublesome; it could be try/excepted, but that's a little nasty; bad URLs shouldn't be this easy to find.
But, I know where you're coming with formencode switching action dispatch on you. I don't like that either.
I DO like when the same URL is mapped into two different actions via their request method via routes. For example, the url /comment/89/edit can lead to an action called 'edit' for GET (which returns a form) and to an action called 'update" for POST (which does some validation, database stuff, whatever). This ends up working very nicely.
But I've learned from the more important meat of your little project; I like this way of making forms, and I think that we can work out our own quirky url-dispatch neuroses on our own time.
Thanks.
9 September 2008, 5:55 pmMike Lewis:
Hi. I really like the idea of this. Unfortunately, I couldn't get the example working (probably something to do with versions). Anyways, it appears to make the action for the form something like . Anyways, I picked through the source and getting rid of "url or" on like 41 of formtags.mako (just making it have to use urlkwargs) did the trick.
Thanks for the nice writeup and everything else you've done :)
12 April 2009, 11:22 pmChris:
zzzeek, This is excellent stuff, helped me clean up my form gen/processing code quite a bit.
...And in general, I really appreciate your blog posts about web development; tremendous amount of learning for me. If you could only post more often :)
30 December 2009, 3:47 pm