Django Form Customization and Validation - Adding Your Own Validation Rules

(Comments)

We had a basic introduction to Django's Forms and Model forms in my previous post. We will now discuss in detail the validation of form data.

Form validation

Form validation is the main reason that any developer has to use Forms for. The basic security principle of any computer application is "do not trust the user input". That could be because the user passes in malicious data to compromise the application or the user has a made a mistake in providing the needed data which in turn, unintentionally, breaks our application. What ever the case, every piece of user input has to be validated to keep our application secure and un-compromised.


Django validates a form when we run the is_valid() method and the validated data is placed in the cleaned_data attribute of the form. A coding example will clear all of our doubts caused by vague English statements. We first define a registration form as follows:

from django.contrib.auth.models import User

class RegistrationForm(ModelForm):
    re_password = forms.CharField(max_length=128)

    class Meta:
        model = User
        fields = ['first_name', 'last_name', 'email', 'password', 're_password']

This form uses the Django's built in User model to build our model form. This is equivalent to following form in terms of validation except that it does not have a model associated with it or have the save() method.

class RegistrationForm(forms.Form):
    first_name = forms.CharField(max_length=30, required=False)
    last_name = forms.CharField(max_length=30, required=False)
    email = forms.EmailField(required=False)
    password = forms.CharField(max_length=128)
    re_password = forms.CharField(max_length=128)

But the field username in User model is a non-blank field. Hence we can't directly use the built-in save() method without over-riding it. Our form renders into HTML as follows.

Registration Form

I have used the django bootstrap3 to render forms with Bootstrap 3 (and I recommend you do the same, though that is not necessary). My HTML template (that uses django-bootstrap3) looks like this:

{% load bootstrap3 %}

<html>

<head>
    {% bootstrap_css %} 
    {% bootstrap_javascript %} 
    {% bootstrap_messages %}
</head>

<body>
    <div class="container">
        <div class="row">
            <div class="col-sm-offset-3 col-sm-6">
                <form action="" method="post">
                    {% csrf_token %} 
                    {% bootstrap_form form %} 
                    {% buttons %}
                    <button type="submit" class="btn btn-success btn-block">Register</button> 
                    {% endbuttons %}
                </form>
            </div>
        </div>
    </div>
</body>

</html>

I have defined my view to be like this:

from .forms import RegistrationForm
from django.shortcuts import render
from django.contrib import messages

def indexView(request):
    if request.method == 'POST':
        form = RegistrationForm(request.POST)
        if form.is_valid():
            messages.success(request, 'The form is valid.')
        else:
            messages.error(request, 'The form is invalid.')

        return render(request, 'posts/index.html', {'form': form})

    else:
        form = RegistrationForm()
        return render(request, 'posts/index.html', {'form': form})

The view does nothing but validate the form data and show a message stating whether the form is valid or not. This is enough to demonstrate the most functionality of forms.
The only required fields in our form here are password and re_password as all the other included fields are allowed null values in the model. Now fill-in some values in password field and re_password field and click on register. Observe that the password field has been rendered as a text field (revealing the password we entered). But that does not matter now. The response page from the server is:

Valid form

Now that, I think, clears up everything that we need to change in our form now. Here they are as bullet points.

  1. Make all the fields necessary - This is possible with built-in validators.
  2. Make both the password fields rendered as password input elements - This can be achieved with built-in widgets.
  3. Check that the email id belongs to only our company cowhite i.e., the email ends with @cowhite.com - This needs a custom validator to be defined.
  4. Check that the password field is at least 8 characters long - We use built-in validators again.
  5. Finally make sure that the re-entered password is same as the password we entered first - We achieve it with a custom validator.

Once the validation is done, the next thing we do is to override the save() method to save the user model without username and correctly setting password.

Adding validators to model form

Task one. We first make all the fields as required. Before we do the changes in the form let us see what are the validators set for the fields. We can check them in the Django shell which we can start with:

python manage.py shell

Once the shell is up and running, do the following.

In [1]: from posts.forms import RegistrationForm

In [2]: rf = RegistrationForm()

In [3]: fn_field = rf.fields['first_name']

In [4]: fn_field
Out[4]: <django.forms.fields.CharField at 0x7f44b18dfdd8>

In [5]: fn_field.validators
Out[5]: [<django.core.validators.MaxLengthValidator at 0x7f44b228fcf8>]

In [6]: fn_field.max_length
Out[6]: 30

In [7]: fn_field.required
Out[7]: False

We have only checked the status of the first_name form field. All the other fields are also similar. There is only one validator associated with the first_name field and that is MaxLengthValidator. This has be auto-generated due the constraint of the field size in the User model. We can also observe that the required attribute of the field is set to False. To make them as required fields, we change our __init__ method of our form definition to the following:

def __init__(self, *args, **kwargs):
    super(RegistrationForm, self).__init__(*args, **kwargs)
    for field_name, field in self.fields.items():
        field.required = True

That is all we need to do to make the field compulsory. Before we check, we need to exit and restart the Django shell, because the changes in the code are reflected in the shell only after a restart. Now the result of our new form definition is:

In [1]: from posts.forms import RegistrationForm

In [2]: rf = RegistrationForm()

In [3]: fn_field = rf.fields['first_name']

In [4]: fn_field.validators
Out[4]: [<django.core.validators.MaxLengthValidator at 0x7f666a5baba8>]

In [5]: fn_field.required
Out[5]: True

The result when the form is submitted with empty field values is:

All are required fields

Specifying widgets to the form fields

Now that our task one is done, we head to our task two - making both the password fields render as password type HTML element. This is easy. We need to the use the Django's built-in PasswordInput widget. Widgets define the rendering of field into HTML.


On applying the widget, our form definition changes to the following:

from django.contrib.auth.models import User
from django.forms import PasswordInput

class RegistrationForm(ModelForm):
    re_password = forms.CharField(max_length=128, widget=PasswordInput())

    def __init__(self, *args, **kwargs):
        super(RegistrationForm, self).__init__(*args, **kwargs)
        for field_name, field in self.fields.items():
            field.required = True

    class Meta:
        model = User
        fields = ['first_name', 'last_name', 'email', 'password', 're_password']
        widgets = {
            'password': PasswordInput()
        }

The django.forms.PasswordInput widget specifies that the field (though it is a CharField) must be rendered as password type input element. The result is that both the fields are rendered as password inputs.

Password inputs

Observe that we have specified the widget for the password in the form's Meta class, where as for the re_password field we specified it in the field definition itself, i.e. within the line re_password = forms.CharField(max_length=128, widget=PasswordInput()). The catch here is that the widgets of models fields can be overridden in the Meta class's widgets dictionary whose keys map to the field names of the model (and the form) and their values (which must be a widget instance or class) specify the widget to apply to the fields. But the following would not work.

widgets = {
    'password': PasswordInput(),
    're_password': PasswordInput()
}

We cannot override the widget of a field in this way (I mean using a dictionary in the Meta class) if the field is manually defined in the form. Only those fields that are automatically imported from a model can be overwritten in this way. We have specified the widget to our re_password field in its definition itself. And of course, this is not the only way. We can override the widgets in the __init__ method.


Before we jump to our next task, let us change definition of the re_password to the following :

re_password = forms.CharField(max_length=128, widget=PasswordInput(), label='Re-enter password')

As you might have expected, this changes the label of the html field.

Form labels modified

Adding custom validators to forms

We now go to the our next task of checking that the email entered is from cowhite.com. We achieve this by creating a custom validator. Validators for a field can be specified just like we have specified widgets (visit the official documentation of using validators). To validate a field we need to create a method named clean_<field_name>() that raises django.forms.ValidationError error if the data is invalid. It must return the validated field data if it is valid. To validate the email field, we create a method clean_email() in the RegistrationForm like this:

from django.forms import ValidationError

def clean_email(self):
    email = self.cleaned_data['email']

    if not email.endswith('@cowhite.com'):
        raise ValidationError('Domain of email is not valid')

    return email

The result of adding this method can be seen when I used my gmail id in the email field.

Invalid email domain

But the form validates to True when I use my company's email.

Adding additional validators to model fields

Our next requirement is to check whether the input password is at least 8 characters long or not. This is as simple as specifying the max_length parameter to the re_password field. Before making changes to our form let us examine our form in the django shell.

In [1]: from posts.forms import RegistrationForm

In [2]: rf = RegistrationForm()

In [3]: pwd_field = rf.fields['password']

In [4]: pwd_field.validators
Out[4]: [<django.core.validators.MaxLengthValidator at 0x7fe696cc3ac8>]

We only have a MaxLengthValidator for our password field. We can achieve the minimum length check by defining our password field of the RegistrationForm as follows

password = forms.CharField(max_length=40, min_length=8, widget=PasswordInput())

While this works what we are actually doing is re-defining the password field which is already defined in the model. A better way to achieve the same result is to add the validator in the __init__() method.

from django.core.validators import MinLengthValidator

def __init__(self, *args, **kwargs):
    super(RegistrationForm, self).__init__(*args, **kwargs)
    for field_name, field in self.fields.items():
        field.required = True
    # Our previous definition was till this line

    password_field = self.fields['password']
    password_field.validators.append(MinLengthValidator(limit_value=8))

Let us examine the result of this change through the Django shell.

In [1]: from posts.forms import RegistrationForm

In [2]: rf = RegistrationForm()

In [3]: pwd_field = rf.fields['password']

In [4]: pwd_field.validators
Out[4]: 
[<django.core.validators.MaxLengthValidator at 0x7fcbfefac710>,
<django.core.validators.MinLengthValidator at 0x7fcbfefaf0f0>]

We now have two validators associated with the password field. The result in the HTML is:

Short password

Validators that work on multiple fields

Our final requirement is to verify that both the passwords entered are same. The validation of individual field values is done by the clean_<field_name>() methods. The overall validation of the whole data is however, done by the clean() method. We are going to override this method. Define the clean() method of our RegistrationForm as follows:

def clean(self):
    super(RegistrationForm, self).clean()
    # This method will set the `cleaned_data` attribute

    password = self.cleaned_data.get('password')
    re_password = self.cleaned_data.get('re_password')
    if not password == re_password:
        raise ValidationError('Passwords must match')

The result of this in the HTML is:

Password mis-match

One thing we need notice here is that the error is shown on top of the form and not below the Re-enter password field. The take away here is that the validation errors raised by clean() method are associated with the form itself while the errors raised by clean_<field_name>() methods are associated with individual fields.


Though it is not necessary, let us add the 'Passwords must match' error to the Re-enter password field. To do this we use the add_error() method which takes the field name string as the first argument and the error message string as the second argument. Our clean() method now changes to:

def clean(self):
    super(RegistrationForm, self).clean()
    password = self.cleaned_data.get('password')
    re_password = self.cleaned_data.get('re_password')
    if not password == re_password:
        self.add_error('re_password', 'Passwords must match')

The resulting html is:

Password mis-match at field

That is how we can add our own error messages to the form fields.

Bonus - Overriding the save() method of the form

The save() of a django form is used to save the data to the database models. But our form does not quiet match with the User model we have used it with. Hence, we need to change how the save() method works.


If we modify nothing in the save() method, conceptually (not how it exactly works) it would look something like this.

def save(self):
    return User.objects.create(**self.cleaned_data)

But that would raise database errors and the password would be stored as plain-text which we do not want. So we modify our form's save() method to:

def save(self):
    username = email = self.cleaned_data.get('email')
    first_name = self.cleaned_data.get('first_name')
    last_name = self.cleaned_data.get('last_name')
    password = self.cleaned_data.get('password')
    return User.objects.create_user(username, email=email, password=password,
                                    first_name=first_name, last_name=last_name)

We are taking advantage of create_user method of User Manager which takes care of hashing password. Now we can use our form in the view in someway similar to:

def indexView(request):
    if request.method == 'POST':
        form = RegistrationForm(request.POST)
        if form.is_valid():
            user = form.save()
            # Do something with the user
            messages.success(request, 'User saved successfully.')
        else:
            messages.error(request, 'The form is invalid.')

        return render(request, 'posts/index.html', {'form': form})

    else:
        form = RegistrationForm()
        return render(request, 'posts/index.html', {'form': form})

The forms are at the core of Django and they have too many features and options that would require a lot more than a single blog post to cover. I have tried to cover the most commonly used features and problems faced in development. I hope this post gives an insight into most of the practical needs and solutions associated with Django forms.

Comments

Recent Posts

Archive

2022
2021
2020
2019
2018
2017
2016
2015
2014

Tags

Authors

Feeds

RSS / Atom