Getting Started with Custom Form Validation in Django

Getting Started with Custom Form Validation in Django

an introduction of clean(), and clean_<fieldname>() methods

This is the third part of the "getting started with Django forms" series. Until now I have discussed

  • how to set up a Django project from scratch
  • an overview of forms in Django
  • an overview of forms.Form and forms.ModelForm and their use-cases

Part I | Part II

In this part of the series, I will discuss "what is form validation?", "what are the different ways of validating forms in Django", and I will wrap this part with the discussion of clean() and clean_() methods.

PS. I will be using the same project that I have created in the first part

Let's get started!

What is form validation?

In simple words, form validation is a technique used to make sure that visitor has passed the same values expected by the backend. For example, if you have a login form with an email and a password field that authorizes visitors then you may not want to process the login form if its email field does not contain a "@".

Form validations are generally done in two different steps, also known as types of validation.

1. Client-side form validation

HTML5 comes with the ability to run validations by itself. In this way, if you have defined an email field in a form then HTML5 will validate it on the client-side before sending it to the server. Similarly, sometimes developers may write client-side form validation in JavaScript to validate the form before sending it to the server. User registration can be a use-case, in which with the help of JavaScript developer may check if the password and confirm password fields matched or not. In this way, we can say that if the form validation is run on the browser, it is said to be a client-side form validation. There is no role of the backend in the client-side form validation. Client-side form validation is good for field-level validation, but we cannot fully rely on it for security reasons.

client-side validation should not be considered an exhaustive security measure! Your apps should always perform security checks on any form-submitted data on the server-side as well as the client-side, because client-side validation is too easy to bypass, so malicious users can still easily send bad data through to your server.

2. Server-side form validation

The server-side validation is a type of validation that is run on the server once the form is submitted by the client-side. In this article, I will be discussing how Django handles server-side validation.

But, before that, it's time for a Capsule

Let's understand how does Django process a form?

Before getting started with different validation techniques, it is important to understand how Django processes a form. The below flow chart explains it pretty well.

form handling flow chart.png

Based on the diagram above, the main things that Django's form handling does are:

  1. Display the default form the first time it is requested by the user.

    • Django renders the form (maybe blank or pre-populated, based on desired form action) on the client-side as the first request sent by the user. The form is known as unbounded form.
    • the client fills ups the form fields and hits the submit button as the submit button is pressed client-side validations are run
    • if the form fields are filled correctly, the request is sent to the server, where the form is validated again
  2. Receive data from a submit request and bind it to the form.
    • In this step, the form gets bounded to the session of the tab which submitted the form so that user-entered data and any errors are available when we need to redisplay the form. The form is known as bounded form.

Let's look at the pictures below that describes unbounded, and bounded form

1.png

2.png

3.png

When a user first time sends the request to access the form, Django renders a blank form (in the case of the POST method) known as unbounded form Figure 1 . The unbounded form goes under client-side validation run by the browser itself when the user does not fill the form correctly Figure 2. Once the form surpasses the client-side validation, the server-side validation takes place. Now the form is bounded by session storage. If the server-side validation raises ValidationError then the form will get rerendered with pre-filled values and the error message Figure 3. Note that in case of incorrect form submission, the URL of the form remains the same but error messages are displayed in case of the server-side validation error.

We can use the is_bound attribute to know whether the form is in a bound state or not. If the form is in the bound state then the is_bound returns True, otherwise False.

  1. Data cleaning and server-side validation
    • The cleaning and validation starts after Django encounters is_valid() method
    • Django by default cleans the form data before validating them. Cleaning the data means eliminating the malicious contents from data and converting the data into Python data types (the common task is to remove malicious characters that can harm the server. It eliminates chances of SQL injection like threats). to_python() method is responsible for doing this.
    • After cleaning, server-side validation takes place, validate(), and run_validators() for validating the form and putting all the errors (if any) in one place so that Django can show multiple errors at one time. validate() method runs field-specific validations whereas run_validators() run all fields validators and aggregates all the errors into a single ValidationError.
    • the is_valid() method returns the clean data, which is then inserted into the cleaned_data dictionary of the form.
  2. If any data is invalid (in case of ValidationError raised by any of to_python(), validate(), run_validators() ) re-display the form, this time with any user populated values and error messages for the problem fields.
  3. If all data is valid, perform required actions (e.g. save the data, send an email, return the result of a search, upload a file, etc.)
  4. Once all actions are complete, redirect the user to another page.

Putting it all together: is_valid(), clean(), and clean_fieldname()

When Django finds is_valid() method, it calls the clean() method clean() method calls to_python(), validate(), and run_validate() methods in the correct order and propagating their errors. If at any time, any of the methods raise ValidationError, the validation stops and that error is raised. This method returns the clean data, which is then inserted into the cleaned_data dictionary of the form.

clean_$fieldname$() method can be used to run validation for a single field of a form. $fieldname$ should be replaced by the field name, for example, if a form has an email field named "email_field" and you want to validate it then you can define clean_email_field()

A very simple form validation

A lot of theory on form validation has been covered, now let's write some code, and validate a very simple form.

Let's suppose that we have a contact form (we have already created in the first part of this series) and we want that any message sent by should not be saved, rather tell the user to use any email except this. Add a clean method to the ContactUsForm to validate this. form_without_model/forms.py

from django import forms
from django.core.exceptions import ValidationError # import this
from django.forms.fields import EmailField
from django.forms.widgets import Textarea

class ContactUsForm(forms.Form):
    name = forms.CharField(max_length=100)
    email = forms.EmailField()
    message = forms.CharField(widget=forms.Textarea)

    # add this
    def clean_email(self):
        """
        clean the email field to prevent users from using example@example.com
        """
        email_address = self.cleaned_data['email'] # email is stored in cleaned_data by clean() method called by is_valid()

        if email_address == 'example@example.com':
            raise ValidationError("this email cannot be used, please enter another email address")

        return email_address

Here, the is_valid() method present in contact_us_view at form_without_model/views.py calls the clean() method which runs field level validations such as user has entered a valid email or not and so on. Here, I did not override it. The clean() method, after validating fields return an object named cleaned_data which has key-value pair of all the form fields and their values. Till now, Django has done half of the validation, but we want to prevent users from entering so we have to define a clean method for the email field only. Now, let's run the server to see the output of the above code. Access the page from 127.0.0.1:8000/form-without-model or localhost:8000/form-without-model

image.png

As expected, if a user enters , an error appears asking user to enter another email.

A very simple Model Form validation

We can also validate the model forms in a similar way. Let's validate the form that we had created on PART II. Let's restrict the users from entering heading having characters less than 25.

Modify working_with_modelform/CreateBlogPostModelForm as,

from django.core.exceptions import ValidationError # imported this
from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _  

from .models import BlogPost


class CreateBlogPostModelForm(ModelForm):

    # added content starts
    def clean_title(self):
        title = self.cleaned_data['title']
        if len(title) < 25:
            raise ValidationError("Heading must be 25 characters long!")

        return title
    # added content ends

    class Meta:
        model = BlogPost

        fields = ['title', 'body'] 
        labels = {'title': _('Heading')}  
        help_texts = {
            'title': _('Give your blog post a suitable heading'),
            'body': _('Write the content of the blog')
        }

Always return a value to use as the newly cleaned data, even if the method didn't change it.

Now, let's run the server and access the page 127.0.0.1:8000/posts/create

I have tried creating a new blog with a heading of less than 25 characters as seen in the below image.

image.png

As expected, Django threw an error saying Heading must be 25 characters long!

Alright, that's it for this part. In the next part of the series, we will see Model validation, and how we can reuse validators.