Form Handling, is there a better path?
Written by
on
.
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
Comments are closed.
Pingbacks
Pingbacks are closed.
Web templates on 01/22/2010 2:39 p.m. #
Wow, thank you for so informative post. I am just a beginner in programming and now tried to run a similar type of script but I always get no success in password lines. Now I think I understand that from your example. Maybe there would be a possibility to write you and get help about my script? I could pay you for your help. Thank you very much
Mike Fletcher on 01/22/2010 4:43 p.m. #
Hmm, hard to tell if this is a spam post or not (the use of the search terms for the name makes me suspect it is, combined with not seeming to have read the post). If not, well, the code posted wasn't intended to be executable, it's just pseudo-code.