Newforms: Is the current method of multiple field comparison the best way?

0 views
Skip to first unread message

Ian

unread,
Mar 16, 2007, 2:27:42 PM3/16/07
to Django users
The purpose of web development frameworks is to make common web
development tasks easier, or give them to you for free. A forms
framework which requires custom code for password comparisons does not
satisfy this fundamental requirement.

I've seen the test codes that tell you how to compare multiple fields
by creating custom clean methods. Why should one have to do that
every time you need to compare fields? It's a consequence of
Newforms' architecture, and I think it should be changed.

>From my inspection of the code, the architecture is as follows:
Forms have fields, and forms have data. When a form has fields /and/
data, it also has BoundFields, special little objects that have access
to both the field information and the data.

This means that the field itself has no access to the data, or to the
form, or to itself, which in turn means that common operations that
require access to form data cannot be done from within the Field,
which I feel is the proper place to put it to ensure reusability.

What's wrong with modifying the Newforms architecture to enable fields
to have access to form data? There are many ways of doing this, but
I've implemented a naive method as below:

class AwareForm(forms.Form):
"""
This behaves as a regular form, except it makes all fields aware
of their
corresponding forms.BoundField object through a bound_field
attribute.
"""
def is_valid(self):
for bound_field in self:
bound_field.field.bound_field = bound_field
return forms.Form.is_valid(self)

This form enables fields to do all sorts of fun tricks, like the
following example that could work for Password confirmation (warning,
following example is not as DRY as it could be):

class ConfirmCharField(forms.CharField):
"""
This field behaves exactly the same as the CharField that is given
to the constructor as the field parameter, and it also validates
that it matches the value of the field with the same name as this
field minus "confirm_"

This field must be named "confirm_<field>", where <field> is the
name
of the field to confirm.

Since this relies on the name of the field and on the form data,
this
must be in an AwareForm.
"""
def __init__(self, field, error=u'This field must match the %s
field.'):
self.field = field
self.error = error
super(ConfirmCharField,
self).__init__(max_length=field.max_length,

min_length=field.min_length,

required=field.required,
widget=field.widget,
label=field.label,
initial=field.initial,

help_text=field.help_text)

def clean(self, value):
"Validates that this field's value is the same as the field to
confirm against"
super(ConfirmCharField, self).clean(value)
name = self.bound_field.name.replace('confirm_','')
if value != self.bound_field.form[name].data:
raise ValidationError(self.error)
return value

and this next example, which could work for unique username
validation.

class UniqueCharField(forms.CharField):
"""
This field behaves exactly the same as a CharField, except that it
also
validates that an object does not already exist in the database

Since this relies on the name of the field, this must be in an
AwareForm.
"""
def __init__(self, cls, error=u'This field must be unique.',
*args, **kwargs):
self.cls = cls
self.error = error
super(UniqueCharField, self).__init__(*args, **kwargs)

def clean(self, value):
"""
Validates that this field's value does not already exist in
the database
for the given type
"""
super(UniqueCharField, self).clean(value)
column = self.bound_field.name
if len(self.cls.objects.extra(where=[column+'=\''+value
+'\''])) != 0:
raise ValidationError(self.error)
return value

Then you could have a form to, say, register a new user:

class JoinForm(AwareForm):
"""
This is the form that prospective members use to sign up to the
site
"""
username = UniqueCharField(cls=User, error="Another user has this
username")
email = forms.EmailField()
password =
forms.CharField(widget=PasswordInput(render_value=False))
confirm_password = ConfirmCharField(field=password, error="The
passwords must match")

Anyone have any thoughts?

Thanks,
Ian

James Bennett

unread,
Mar 16, 2007, 3:18:26 PM3/16/07
to django...@googlegroups.com
On 3/16/07, Ian <ian.t...@gmail.com> wrote:
> The purpose of web development frameworks is to make common web
> development tasks easier, or give them to you for free. A forms
> framework which requires custom code for password comparisons does not
> satisfy this fundamental requirement.

While I can understand feeling frustrated at writing custom validation
methods, I personally I like the idea that the
thing-which-accepts-data and the thing-which-validates-data are
separate from one another; after spending a couple years working with
the old manipulator system -- where "knowledge" of validation is
literally scattered all over the place -- it's like a breath of fresh
air, even if it means I occasionally have to write a short validation
method at the Form level instead of putting it directly on a Field
(I've done exactly that for password confirmation, btw -- the method
to do it is four lines long).

And that distinction -- between Field-level validation and Form-level
validation -- also makes intuitive sense to me; it feels right that a
Field only needs to know about itself, while a Form is responsible for
knowing about all its constituent Fields and their interactions (and,
hence, validation which involves multiple Fields goes at the level of
the Form instead of the level of the Field).

But I'm just thinking out loud here, and I'm far from being an expert
on newforms, so I'll defer to someone more knowledgeable.

(also, django-developers is probably a better place to discuss this,
since it's a design issue in Django itself)


--
"Bureaucrat Conrad, you are technically correct -- the best kind of correct."

Rubic

unread,
Mar 16, 2007, 4:28:42 PM3/16/07
to Django users
On Mar 16, 2:18 pm, "James Bennett" <ubernost...@gmail.com> wrote:
> And that distinction -- between Field-level
> validation and Form-level validation -- also
> makes intuitive sense to me; it feels right
> that a Field only needs to know about itself,

I'm not particularly concerned about field
validation and it generally works okay for me
at the form level, but ...

> while a Form is responsible for knowing about
> all its constituent Fields and their interactions
> (and, hence, validation which involves multiple
> Fields goes at the level of the Form instead of
> the level of the Field).

But I think I understand a bit of Ian's frustration,
though he may not have chosen the best use case to
advance his argument. There are times that it would
be convenient to access bound info at the field
level. Witness the hack I perform in here in creating
a BoundField instance, then assigning it as a faux
attribute to a field:

http://www.djangosnippets.org/snippets/82/

--
Jeff Bauer
Rubicon, Inc.

Ian

unread,
Mar 16, 2007, 4:43:04 PM3/16/07
to Django users
James,

> While I can understand feeling frustrated at writing custom validation
> methods,

To clarify, I only feel frustrated at writing custom code for
ubiquitous validation needs.

> And that distinction -- between Field-level validation and Form-level
> validation -- also makes intuitive sense to me; it feels right that a
> Field only needs to know about itself, while a Form is responsible for
> knowing about all its constituent Fields and their interactions (and,
> hence, validation which involves multiple Fields goes at the level of
> the Form instead of the level of the Field).

I agree with you in theory. ;-) In the case of password
confirmation, though, I suppose it depends on what you view fails the
validation. Does field A fail validation because it doesn't match
field B, or does the form fail? Where is the error best communicated
to the user -- at the form level, or beside the confirming field? I
tend to think that the error is clearest at the field level, which
implies that it's field validation.

That said, there is certainly the case to be made that it should be at
the form level. In that case the form architecture should still
support common tasks in a way that a) gives it to you for free, and b)
makes sense from the callers' perspective (in this case the subclass).

Here's another implementation of another form class that at the form
level handles confirmation of all fields that start with "confirm_" (a
little hacky to do it that way, maybe, but maybe not). Forms that
desire to have confirmation fields can just subclass this form.

class ConfirmForm(forms.Form):
"""
...
"""
def clean(self):
failing_fields = []
for name in self.fields.keys():
if name.startswith('confirm_'):
compare_field = name.replace('confirm_','')
if self[name].data != self[compare_field].data:
failing_fields.append(compare_field)
if len(failing_fields):
raise ValidationError('The %s fields must match' % ',
'.join(failing_fields))
return super(ConfirmForm,self).clean()

Now you can have a form that looks like this:
class JoinForm(ConfirmForm):
...
email = forms.EmailField()
confirm_email = forms.EmailField()


password =
forms.CharField(widget=PasswordInput(render_value=False))
confirm_password =

forms.CharField(widget=PasswordInput(render_value=False))

> (also, django-developers is probably a better place to discuss this,
> since it's a design issue in Django itself)

You're right, thanks. I'll post a note there.

Ian

Ian

unread,
Mar 16, 2007, 5:17:08 PM3/16/07
to Django users
Here's another brainstorm that's sort of a combination. :) It's a
Form that handles confirmation validation, but does so at the field
level. Code will help illuminate what I mean:

class ConfirmForm(forms.Form):
"""
...
"""
def __init__(self, *args, **kwargs):
super(ConfirmForm2,self).__init__(*args,**kwargs)

for name in self.fields.keys():
if name.startswith('confirm_'):
compare_field = name.replace('confirm_','')

setattr(self,'clean_%s' % name,
self._make_confirmation_method(compare_field,
name))

def _make_confirmation_method(self, field_name,
confirm_field_name):
return lambda: self._confirm_fields_match(field_name,
confirm_field_name)
def _confirm_fields_match(self, field_name, confirm_field_name):
if self[field_name].data != self[confirm_field_name].data:
raise ValidationError('This field must match the %s field'
\
% field_name)
return self.clean_data.get(confirm_field_name)

Ian

unread,
Mar 16, 2007, 5:18:40 PM3/16/07
to Django users
> super(ConfirmForm2,self).__init__(*args,**kwargs)

Copy paste edit: ConfirmForm, not with the 2. :)

Reply all
Reply to author
Forward
0 new messages