Defying Classification

by Malcolm Tredinnick

Mon 22 Dec 2008

Django Tip: A Never-Redisplay Form Widget

Posted at 16:59 +1100 (last edited: 22 Dec 2008, 19:42)

One situation, when working with Django's HTML form module, has come up a couple of times recently: how to construct a form where redisplaying the form after validation errors have been found, will always blank out a particular field. This might come up if you always require a commenter to fill in a captcha field, even when they are just correcting errors.

Just for fun, here are a couple of solutions.

The Problem

Typically, the people encountering this problem have deleted the value they don't want to re-render from the dictionary of data that they pass to the form. Something like this (let's suppose the field in question is called "captcha"):

def form_handler(request, ...):
    if request.method == "POST":
        data = request.POST.copy()
        del data["captcha"]
        form = InputForm(data)
        ...

The main problem I see with this sort of approach (and there are few variations, including removing the data in the form's rendering method) is that when all the other data is correct, this special field is never validated. It could almost have been left it out of the form in the first place, except that the form creator wanted it displayed on the HTML page.

Another concern I have with this — and I suspect this is why a lot of people feel that there might be a "nicer" way — is that the data manipulation is separate from the rest of the form processing. Normally, you would pass all the submitted data to the form and that object nicely encapsulates the whole validation and rendering processed. In the above example, however, there's this extra, out of band surgery that's being performed on the input data and if you forget to couple that step with the format validation, things will behave strangely, but the code won't necessarily look wrong, so debugging could be time-consuming.

The General Solution

Two solutions come to mind for this problem. Really, they're both variations on the same theme: creating a custom widget for the form field that simply doesn't render the data it contains. Once we have such a widget, we can happily pass in the non-renderable data to the form, have it validated as per normal and still have it not displayed should the form have to be sent back for error correction.

In general, suppose our widget is called NonRenderWidget, we could create a form like so:

from django import forms

class InputForm(forms.Form):
    catpcha = forms.CharField(widget=NoRenderWidget())
    ...

and the form handling view becomes much simpler:

def form_handler(request, ...):
    if request.method == "POST":
        form = InputForm(request.POST)
        ...

No pre-processing manipulation of request.POST is necessary. Cleaner and safer.

First Variation (Text Fields Only)

I suspect that most of the time somebody wants this functionality, it's for a text field. Turns out, there's a very quick way to achieve the desired goal for text fields. An HTML text input field (not a textarea field type) is exactly the same as a password input field, except with a type of text, rather than password. So we can subclass Django's existing PasswordInput widget, since that already has a parameter for not rendering the passed-in value (although it appears this option is undocumented at present).

class NonRenderWidget(forms.PasswordInput):
    input_type = 'text'

This can be used like this:

captcha = forms.CharField(widget=NonRenderWidget(render_value=False))

(I'm only giving the form field line here, not the full form definition.) Passing in render_value=False is the secret sauce.

Purists will argue that this solution has a slight flaw beyond only being applicable to text input fields. It relies on the implementation and behaviour of PasswordInput being exactly the same as the normal TextInput widget. This is true. However, for a simple thing that gets the job done and can be easily tested to verify the behaviour won't change, it's not too horrible.

Second Variation

If the quick-and-dirty version doesn't do the job for you (and I don't really like giving it that name, since it's not a bad idea for what it does), here's a more general solution. No (great) reliance on implementation details and works for any widget type (select drop-downs that are forgotten when the form is rerendered, anybody?).

The generic approach I'm using here is to write a mixin that has the effect of wrapping the render() method for the widget it is mixed into.

class NoRenderMixin(object):
    def render(self, name, value, *args, **kwargs):
        # Ignore "value". None ==> no value given.
        return super(NoRenderMixin, self).render(name, None,
                *args, **kwargs)

I can use this to modify an existing widget as follows. The thing to remember here, if you aren't too familiar with mixin usage, is that Python's resolution strategy for looking up methods is essentially to start with the child class, then left-to-right amongst the ancestors. It's a little more complex than that for complex inheritance hierarchies, but the rule-of-thumb works for most practical cases (if you care about the details, go and read PEP 253).

# The same effect as the "first variation" solution
class NRTextInput(NoRenderMixin, forms.TextInput):
    pass

# A non-rendering select dropdown (why? I have no idea!)
class NRSelect(NoRenderMixin, forms.Select):
    pass

I've prefixed the class names with NR here (for non-rendering), merely for conciseness. Again, these widgets are used in the same way as when you normally specify a custom widget for a form field.

captcha = forms.CharField(widget=NRTextInput())

I guess this version is slightly simpler than the first one, in that the render_value parameter isn't present or needed. So you could even pass in only the widget class (not an instance). That is, this is also valid (and behaves the same way):

captcha = forms.CharField(widget=NRTextInput)

Worth It?

If I ever needed to write some form handling code that behaved in this way (not showing a previously submitted value upon redisplay), I think I would use the code I've given here. Probably the second variation, just for neatness purposes. I really feel that the benefits of keeping the form object as the only thing that needs to worry about validating and/or ignoring submitted data makes it worthwhile to control the display of data at the right place: the form widget. Not by modifying the data passed to the form.

Of course, opinions vary and if your solution solves your problem without making a mess on my lawn, go for it. I'm throwing these options out there mostly as link bait for future wonderers.

Topics: software/django/tips