I've now written, worked with and generally explored form handling in web development a great deal more than I've enjoyed. I haven't seen anything that satisfies me.
You can code basic form handling in minutes, but then sharing with anyone else is lost. The intricate messy details of formatting/layout of the GUI widgets, the placement of error text, the integration of internationalization, handling complex widgets, and getting all of the data to the right spot when the time comes to render... they're all simple problems, but together they're the kind of thing that no-one wants to spend their lives doing.
The shared form-handling libraries, however, seem to grow so complex as to make them ridiculously complex to debug when they fall down. Your data doesn't show up in the widget, nor your error messages... well, you'd darn well better feel like spelunking down 20 levels deep into the meta-programming to figure out what went wrong.
Sometimes it will be easy, like "oh, it's trying to take the len of a None because there's no 'name' field on the widget and an error was encountered for that field value". Sometimes it just won't work no matter what you do. And sometimes you'll find yourself thinking it would take 1/10th the time to just write your own bloody validation routine for these 4 fields on two forms... but then you're back into maintaining the tarmac forever.
My big projects have all wound up paving new tarmac. I tend to want complete control over rendering, so I create template functions that render each given control, they take the value, higher-level functions render table-rows with labels, error messages, with a couple of layouts, and then the template decides what rows to render, dispatching the values passed into the widget-renderers.
I normally do custom FormEncode validators, but I often find that using schemas/compound validators seems to make the code very obtuse feeling, so I wind up doing per-field validation with FormEncode/TG and then separate validation for multi-field consistency tests... but now I need to hack TG to re-hook that non-standard validation into the mainline error-reporting/handling machinery, and we still hit issues with TG's validation/error_handler stuff where we'll wind up with un-validated results showing up. Blech.
It feels like there should be something better available, something simple, obvious, easily debugged, readily composable and decomposable, loosely coupled... or maybe it's just that at 4:30am I'm tired of debugging what I was intending to have finished by midnight. It feels like it should be some sort of tool-set of functions that are all explicitly invoked in the order you want them in a function that you write for each controller (you couldn't believe how much time I spent one day tracking down a bug due to an "All" constraint being applied in reversed order)...
def validator( req ):
login = req.set( 'login', req.re_match(
req.longer_than(
req.as_text( req.get( login) ),
5,
),
re.compile( r'^\w+$', re.I|re.U),
message = _('Only non-white-space characters allowed' ),
))
password = req.set( 'password',req.equals(
req.strong_password(
req.longer_than(
req.as_text( req.get( 'password' ) ),
6,
)
),
req.as_text( req.get( 'confirm' )),
message = _('Password and confirmation do not match'),
))
if not req.errors:
# no errors so far, so do more checking...
# here we see custom code, rather than something
# composed into a reusable piece...
other = req.db_lookup( User, User.id, login )
if other:
req.set( 'login', req.error( _('That login is in use')))
The idea being that each little helper function/method of the request is doing some trivial operation. It returns either the converted/validated result or an Invalid object. The method wrapping for the helper functions then short-circuits if there's already an Invalid as the value and returns the original Invalid. You set the converted value for the parameter with the .set() method. If it's an Invalid, it registers as an error, otherwise it's a converted value. Helpers where it makes sense to allow message customization can just let you pass in the messages as arguments. Extra arguments (e.g. limits, or other parameters) can similarly be passed in.
You'd keep the helper functions as simple as you can, something like:
def longer_than(
req, value,
minimum=1,
message=_('Require value greater than %(minimum)s in length'),
type_message=_('Unable to determine length of %(typ)s value'),
):
"""Validation function, takes value, checks it, returns checked/converted value or Invalid instance"""
try:
length = len( value )
except (ValueError,TypeError), err:
return req.error( type_message%dict(
typ = type(value),
))
else:
if len < minimum:
return req.error(message%dict(
value = value,
minimum = minimum,
))
return value
The request would look something like:
class Request( object ):
def get( self, key, default=None, required=False ):
"""Retrieve argument for key"""
def set( self, key, value=None ):
"""Set a (partially) validated value"""
if isinstance( value, Invalid ):
self.errors[key] = value
self._values[key] = value
def error( self, message=_('Error')):
"""Create an error message for the given key
These are sentinel values. for sanity-checking, would likely
make them report an error if they are deallocated without being
set() on the request?
"""
def rawvalue( self, key, default=None ):
"""Retrieve original, untouched value for key (for user to correct)"""
def register_helper( cls, name, helper ):
"""Register a validation helper function (and wrap)"""
Of course now that I've written all that, it's not really any different than the dozens of other validation/conversion mechanisms out there. So now it's 1.25 hours later and I'm just going to stop writing and accept that web development is a messy mess of mess and that tomorrow whatever it is that's making FormEncode/TG fail will have just magically disappeared.


Comments
2010-07-25 14:02
> and would have no Trac integ ration The trac-bzr plugin[ 1] seems to provide good integ ration between bzr and t [...]
2010-07-13 21:47
I've always been fascinated wi th the Asterisk AMI interface. So much so that I married tha t fascination with the [...]
2010-07-03 21:32
Yes, only references in dicti onaries are replaced, so hold ing references in lists, tuple s, etceteras keeps them alive.
2010-07-03 11:18
They hold references to remove and install?
2010-06-24 08:34
There's higher-level objects w hich are tracking what is repl aced (the actual Mock objects) . They hold references [...]
2010-06-24 08:23
I haven't tried it, but it see ms to me like this approach ha s one fundamental problem: If you replace all refs o [...]
2010-06-24 08:22
That's the "magic" that made m e go "ooh shiny"
2010-06-24 06:03
That's even more evil than the mock patch decorator...
2010-06-06 18:33
blush Oh.
2010-06-06 11:07
That's what the module does (a utomatically), but on a per-te st-run basis, and only for the process being tested (i [...]
2010-06-06 02:43
Maybe I'm missing something im portant here, but why not just write small scripts to mimic whatever dangerous utili [...]
2010-06-05 15:17
I thought about stubbing out t he python call to the process in the current process, but I want something which stu [...]
2010-06-05 14:47
Hmm... if Mock isn't flexibl e enough to handle mocking pro cesses adequately then I'd lik e to know how it could b [...]
2010-05-19 10:27
Hey, maybe it's a stupid new bie question, but where and ho w exactly should the patching of the core take place? [...]
2010-05-04 14:36
I used Qemu and VirtualBox pre tty extensively back when I wa s working for the OLPC, but mo st of the stuff we were [...]