Django Form Basics

The syntax of creating forms in Django is almost similar to that of creating models, the only differences are:

  1. Django form inherits from forms.Form instead of models.Model.
  2. Each form fields inherit forms.FieldName instead of models.FieldName.

Let's start off by creating an AuthorForm class.

Create a new file called forms.py, if not already exists in the blog app i.e TGDB/django_project/blog (same place where models.py file is located) and add the following code to it.

TGDB/django_project/blog/forms.py

from django import forms


class AuthorForm(forms.Form):
    name = forms.CharField(max_length=50)
    email = forms.EmailField()
    active = forms.BooleanField(required=False) # required=False makes the field optional
    created_on = forms.DateTimeField()
    last_logged_in = forms.DateTimeField()

Form fields are similar to model fields in the following ways:

  1. Both correspond to a Python type.
  2. Both validate data in the form.
  3. Both fields are required by default.
  4. Both types of fields know how to represent them in the templates as HTML. Every form fields are displayed in the browser as an HTML widget. Each form field is assigned a reasonable Widget class, but you can also override this setting.

Here is an important difference between the model fields and form fields.

Model fields know how to represent themselves in the database whereas form fields do not.

Form States #

A form in Django can be either in a Bound state or Unbound state. What is Bound and Unbound state?

Unbound State: In Unbound state the form has no data associated with it. For example, an empty form displayed for the first time is in unbound state.

Bound State: The act of giving data to the form is called binding a form. A form is in the bound state if it has user submitted data. It doesn't matter whether the data is valid or not.

If the form is in bound state but contains invalid data then the form is bound and invalid. On the other hand, if the form is bound and data is valid then the form is bound and valid.

is_bound attribute and is_valid() method #

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

Similarly, we can use the is_valid() method to check whether the entered data is valid or not. If the data is valid then is_valid() returns True, otherwise False. It is important to note that if is_valid() returns True then is_bound attribute is bound to return True.

Accessing Cleaned Data #

When a user enter submits the data via form Django first validate and clean the data. Does this mean that the data entered by the user were not clean? Yes, for the two good reasons:

  1. When it comes to submitting data using forms you must never trust the user. It takes a single malicious user to wreak havoc on your site. That's why Django validates the form data before you can use them.

  2. Any data the user submits through a form will be passed to the server as strings. It doesn't matter which type of form field was used to create the form. Eventually, the browser would will everything as strings. When Django cleans the data it automatically converts data to the appropriate type. For example IntegerField data would be converted to an integer, CharField data would be converted to a string, BooleanField data would be converted to a bool i.e True or False and so on. In Django, this cleaned and validated data is commonly known as cleaned data. We can access cleaned data via cleaned_data dictionary:

    cleaned_date['field_name']

    You must never access the data directly using self.field_name as it may not be safe.

Django Forms in Django Shell #

In this section we will learn how to bind data and validate a form using Django Shell. Start Django Shell by typing python manage.py shell command in the command prompt or terminal. Next, import the AuthorForm class and instantiate a AuthorForm object:

(env) C:\Users\Q\TGDB\django_project>python manage.py shell
Python 3.4.4 (v3.4.4:737efcadf5a6, Dec 20 2015, 20:20:57) [MSC v.1600 64 bit (AM
D64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from blog.forms import AuthorForm
>>> f = AuthorForm()
>>>

At this point, our form object i.e f is unbound because there is no data in the form. We can verify this fact by using is_bound attribute.

>>>
>>> f.is_bound
False
>>>

As expected is_bound attribute returns False. We can also check whether the form is valid or not by calling is_valid() method.

>>>
>>> f.is_valid()
False
>>>

It is important to understand that is_bound and is_valid() are related. If is_bound is False then is_valid() will always return False no matter what. Similarly, if is_valid() returns True then is_bound must be True.

Calling is_valid() method results in validation and cleaning of the form data. In the process, Django creates an attribute called cleaned_data, a dictionary which contains cleaned data only from the fields which have passed the validation tests. Note that cleaned_data attribute will only be available to you after you have invoked the is_valid() method. Trying to access cleaned_data before invoking is_valid() will throw an AttributeError exception.

Obviously, now the question arises "How do we bind data to the form"?

To bind data to a form simply pass a dictionary as an argument to the form class(in this case AuthorForm) while creating a new form object.

>>>
>>> data = {
... 'name': 'jon',
... 'created_on': 'today',
... 'active': True,
... }
>>>
>>>
>>> f = AuthorForm(data)
>>>

Our form object f has data now, so we can say that it is bound. Let's verify that by using is_bound attribute.

>>>
>>> f.is_bound
True
>>>

As expected, our form is bound now. We could also get a bound form by passing an empty dictionary ({}).

>>>
>>> data = {}
>>> f2 = AuthorForm(data)
>>> f2.is_bound
True
>>>

Okay let's now try accessing cleaned_data attribute before invoking is_valid() method.

>>>
>>> f.cleaned_data
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'CategoryForm' object has no attribute 'cleaned_data'
>>>
>>>

As expected, we got an AttributeError exception. Now we will validate the form by calling is_valid() method.

>>>
>>> f.is_valid()
False
>>>
>>> f.cleaned_data
{'active': True, 'name': 'jon'}
>>>

Our validation fails but we now have cleaned_data dictionary available. Notice that there is no created_on key in the cleaned_data dictionary because Django failed to validate this field. In addition to that, the form validation also failed to validate email and last_logged_in field of the AuthorForm because we haven't provided any data to it.

Always remember that the cleaned_data attribute will only contain validated and cleaned data, nothing else.

To access errors, the form object provides an errors attribute which is an object of type ErrorDict, but for the most part you can use it as a dictionary. Here is how it works:

>>> 
>>> f.errors
{'created_on': ['Enter a valid date/time.'], 'email': ['This field is required.'
], 'last_logged_in': ['This field is required.']}
>>>
>>>

Notice that there are three fields which failed the validation process. By default, f.errors returns error messages for all the fields which failed to pass validation. Here is how to get the error message for a particular field.

>>>
>>> f.errors['email']
['This field is required.']
>>>
>>>
>>> f.errors['created_on']
['Enter a valid date/time.']
>>>
>>>
>>> f.errors['last_logged_in']
['This field is required.']
>>>

The errors object provides two methods to ouput errors in different formats:

Method Explanation
as_data() returns a dictionary with ValidationError object instead of a string.
as_json() returns errors as JSON
>>>
>>>
>>> f.errors
{'created_on': ['Enter a valid date/time.'], 'email': ['This field is required.'
], 'last_logged_in': ['This field is required.']}
>>>
>>>
>>> f.errors.as_data()
{'created_on': [ValidationError(['Enter a valid date/time.'])], 'email': [Valida
tionError(['This field is required.'])], 'last_logged_in': [ValidationError(['Th
is field is required.'])]}
>>>
>>>
>>>
>>> f.errors.as_json()
'{"created_on": [{"code": "invalid", "message": "Enter a valid date/time."}], "e
mail": [{"code": "required", "message": "This field is required."}], "last_logge
d_in": [{"code": "required", "message": "This field is required."}]}'
>>>
>>>

Note that unlike the cleaned_data attribute the errors attribute is available to you all the time without first calling the is_valid() method. But there is a caveat, trying to access errors attribute before calling is_valid() method results in validation and cleaning of form data first, consequently creating cleaned_data attribute in the process. In other words, trying to access errors attribute first will result in a call to is_valid() method implicitly. However, in your code should always call is_valid() method explicitly.

To demonstrate the whole process one more time, let's create another form object, but this time we will bind the form with data that will pass the validation.

>>>
>>> import datetime
>>>
>>>
>>> data = {
...  'name': 'tim',
...  'email': 'tim@mail.com',
...  'active': True,
...  'created_on': datetime.datetime.now(),
...  'last_logged_in': datetime.datetime.now()
... }
>>>
>>>
>>> f = AuthorForm(data)
>>>
>>>
>>> f.is_bound
True
>>>
>>>
>>> f.is_valid()
True
>>>
>>>
>>> f.cleaned_data
{'name': 'tim', 'created_on': datetime.datetime(2017, 4, 29, 14, 11, 59, 433661,
 tzinfo=<UTC>), 'last_logged_in': datetime.datetime(2017, 4, 29, 14, 11, 59, 433
661, tzinfo=<UTC>), 'email': 'tim@mail.com', 'active': True}
>>>
>>>
>>> f.errors
{}
>>>

Digging deep into Form Validation #

When is_valid() method is called Django does the following things behind the scenes:

  1. The first step is to call Field's clean() method. Every form field has a clean() method, which does the following two things:

    1. Convert the field data (recall that the data is sent by the browser as a string to the server) to the appropriate Python type. For example, if the field is defined as IntegerField then the clean() method will convert the data to Python int, if it fails to do so, it raises a ValidationError exception.

    2. Validate the converted data received from the step 1. If validation succeeds, cleaned and validated data is inserted into the cleaned_data attribute. If it fails a ValidationError is raised. We usually don't override field's clean() method.

  2. In step 2, Field's clean_<fieldname>() method is called to provide some additional validation to the field. Notice that <fieldname> is a placeholder, it is not an actual Python method. By default, Django doesn't define these methods. These methods are usually written by developers to provide some additional validation to the field. They do not accept any arguments, but they must return the new value of the field. This method is called only when ValidationError is not raised by the Field's clean() method. That means, if this method is called, it is guaranteed that the field's data is cleaned and validated. Consequently, you must always access field's data inside this method using cleaned_data['fieldname']. The value returned by this method replaces the existing value of the field in the cleaned_data dictionary.

Django repeats step 1 and 2 for all form fields.

Finally, Form's class clean() method is called. If you want to perform validation which requires access to multiple fields override this method in your form class.

Note: This is an oversimplified view of Django Validation Process. The reality is much more involved but that's enough, to begin with.

Let's take an example to understand how Django performs cleaning and validation when is_valid() method is called on AuthorForm class.

1st step - The name field's clean() method is called to clean and validate data. On success, it puts the clean and validated data into the cleaned_data dictionary. If cleaning or validation failed ValidationError exception is raised and call to clean_name() is skipped. Nonetheless, the clean() method of the following field will be called.

2nd step - The name field's clean_name() method is called (assuming this method is defined in the form class and ValidationError is not raised in step 1) to perform some additional validations.

3rd step - The email field's clean() method is called to clean and validate data. On success, it puts the clean and validated data into the cleaned_data dictionary. If cleaning or validation failed ValidationError exception is raised and call to clean_email() method is skipped. Nonetheless, the clean() method of the following field will be called.

4th step - The email field's clean_email() is a method called (assuming this method is defined in the form class and ValidationError is not raised in step 3) to perform some additional validation. At this point, it is guaranteed that email field is cleaned and validated, so the following code is perfectly valid inside clean_email() method.

email= self.cleaned_data['email']; ## that's okay

However, there is no guarantee that the data from other fields, for example, the name field is available inside clean_email() method. So, you should not attempt to access name field inside clean_email() method like this:

name = self.cleaned_data['name']; # Not good, you may get an error for doing this

If you want to provide additional validation to the name field, do it in the clean_name() method because it is guaranteed to be available there.

This process repeats for every form field. At last, Form's clean() method or its override is called. An important thing to remember about the Form's clean() method is that none of the fields is guaranteed to exists here. To access field data you must always use dictionary's object get() method like this:

self.cleaned_data.get('name')

If the name key is not available in the cleaned_data dictionary then get() method will return None.

Implementing Custom Validators #

In this section, we are going to implement some custom validators in our AuthorForm class. Here are things we want to achieve.

  1. Prevent users to create Author named "admin" and "author".
  2. Save the email in lowercase only. At this point, nothing is stopping us to save the email in uppercase.

Open forms.py and modify the code as follows:

TGDB/django_project/blog/forms.py

from django import forms
from django.core.exceptions import ValidationError


class AuthorForm(forms.Form):
    #...
    last_logged_in = forms.DateTimeField()

    def clean_name(self):
        name = self.cleaned_data['name']
        name_l = name.lower()
        if name_l == "admin" or name_l == "author":
            raise ValidationError("Author name can't be 'admin/author'")
        return name

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

Restart the Django shell for the changes to take effect and then enter the following code. Here we are trying to validate a form where author name is "author".

>>>
>>> from blog.forms import AuthorForm
>>>
>>> import datetime
>>>
>>>
>>> data = {
...  'name': 'author',
...  'email': 'TIM@MAIL.COM',
...  'active': True,
...  'created_on': datetime.datetime.now(),
...  'last_logged_in': datetime.datetime.now()
... }
>>>
>>>
>>> f = AuthorForm(data)
>>>
>>>
>>> f.is_bound
True
>>>
>>>
>>> f.is_valid()
False
>>>
>>>
>>> f.cleaned_data
{'last_logged_in': datetime.datetime(2017, 9, 12, 22, 17, 26, 441359, tzinfo=<UT
C>), 'created_on': datetime.datetime(2017, 9, 12, 22, 17, 26, 441359, tzinfo=<UT
C>), 'active': True, 'email': 'tim@mail.com'}
>>>
>>>
>>> f.errors
{'name': ["Author name can't be 'admin/author'"]}
>>>
>>>

As expected, form validation failed because "author" is not a valid author name. In addition to that cleaned_data contains email in lowercase, thanks to the clean_email() method.

Notice that form's errors attribute returns the same error message we specified in the clean_name() method. Let's try validating form data once more, this time we will provide valid data in every field.

>>>
>>>
>>> data = {
...  'name': 'Mike',
...  'email': 'mike@mail.com',
...  'active': True,
...  'created_on': datetime.datetime.now(),
...  'last_logged_in': datetime.datetime.now()
...  }
>>>
>>>
>>> f = AuthorForm(data)
>>>
>>>
>>> f.is_bound
True
>>>
>>>
>>> f.is_valid()
True
>>>
>>>
>>> f.cleaned_data
{'last_logged_in': datetime.datetime(2017, 9, 12, 22, 20, 25, 935625, tzinfo=<UT
C>), 'name': 'Mike', 'created_on': datetime.datetime(2017, 9, 12, 22, 20, 25, 93
5625, tzinfo=<UTC>), 'active': True, 'email': 'mike@mail.com'}
>>>
>>>
>>> f.errors
{}
>>>
>>>

This time validation succeeds because data in every field is correct.

Saving the form data to the database #

So, how do we save data received via the form to the database? Earlier in this chapter, we have already discussed that unlike models fields, form fields don't know how to represent themselves in the database. Further, unlike models.Model class, the forms.Form class doesn't provide save() method to save the form data to the database.

The solution is to implement our own save() method. There is no restriction on method name you can call it anything you like. Open forms.py file and add save() method towards the end of AuthorForm class:

TGDB/django_project/blog/forms.py

from django import forms
from django.core.exceptions import ValidationError
from .models import Author, Tag, Category, Post


class AuthorForm(forms.Form):
    #...

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

    def save(self):
        new_author = Author.objects.create(
            name = self.cleaned_data['name'],
            email = self.cleaned_data['email'],
            active = self.cleaned_data['active'],
            created_on = self.cleaned_data['created_on'],
            last_logged_in = self.cleaned_data['last_logged_in'],
        )
        return new_author

Nothing new here, in line 3, we are importing models from the blog app. In lines 12-20, we are defining the save() method which uses form data to create a new Author object. Notice that while creating new Author object we are accessing form data via cleaned_data dictionary.

Restart the Django shell again and Let's try creating a new Author via AuthorForm.

>>>
>>> from blog.forms import AuthorForm
>>>
>>> import datetime
>>>
>>> data = {
...  'name': 'jetson',
...  'email': 'jetson@mail.com',
...  'active': True,
...  'created_on': datetime.datetime.now(),
...  'last_logged_in': datetime.datetime.now()
...  }
>>>
>>> f = AuthorForm(data)
>>>
>>> f.is_bound
True
>>>
>>> f.is_valid()
True
>>>
>>> f.save()
<Author: jetson : jetson@mail.com>
>>>
>>>
>>> from blog.models import Author
>>>
>>> a = Author.objects.get(name='jetson')
>>>
>>> a.pk
11
>>>
>>> a
<Author: jetson : jetson@mail.com>
>>>
>>>

Sure enough, our newly created category object is now saved in the database.

Our form is fully functional. At this point, we could move on to create form classes for the rest of the objects like Post, Tag etc; but there is a big problem.

The problem with this approach is that fields in the AuthorForm class map closely to that of Author models. As a result, redefining them in the AuthorForm is redundant. If we add or modify any field in the Author model then we would have to update our AuthorForm class accordingly.

Further, as you might have noticed there are few differences in the way we have defined model fields and form fields. For example:

The email field in Author model is defined like this:

email = models.EmailField(unique=True)

On the other hand, the same field in AuthorForm is defined like this:

email = forms.EmailField()

Notice that the email field in AuthorForm doesn't have unique=True attribute because unique=True attribute is only defined for the model fields, not for the form fields. One way to solve this problem is to create a custom validator by implementing clean_email() method like this:

from django import forms
from .models import Author, Tag, Category, Post

class AuthorForm(forms.Form):
    #...

    def clean_email(self):
        email = self.cleaned_data['email'].lower()
        r = Author.objects.filter(email=email)
        if r.count:
            raise ValidationError("{0} already exists".format(email))

        return email.lower()

Similarly, Form fields don't provide default, auto_add_now and auto_now parameters. If you want to implement functionalities provided by these attributes then you would need the write custom validation method for each of these fields.

As you can see, for each functionality provided by the Django models, we would have to add various cleaning methods as well as custom validators. Certainly, this involves a lot of work. We can avoid all these issues by using ModelForm.

Removing redundancy using ModelForm #

The ModelForm class allows us to connect a Form class to the Model class.

To use ModelForm do the following:

  1. Change inheritance of the form class from forms.Form to forms.ModelForm.
  2. Inform the form class in forms.py which model to use by using the model attribute of the Meta class.

After these two steps, we can remove all the form fields we have defined in the AuthorForm class. Furthermore, we can remove the save() method too, because ModelForm provides this method. It is important to mention here that the save() method we implemented earlier in this chapter can only create objects, it can't update them. On the other hand, the save() method coming from ModelForm can do both.

Here is the modified code.

TGDB/django_project/blog/forms.py

from django import forms
from django.core.exceptions import ValidationError
from .models import Author, Tag, Category, Post


class AuthorForm(forms.ModelForm):
    class Meta:
        model = Author        

    def clean_name(self):
        name = self.cleaned_data['name']
        name_l = name.lower()
        if name_l == "admin" or name_l == "author":
            raise ValidationError("Author name can't be 'admin/author'")
        return name

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

There is still one thing missing in our AuthorForm class. We have to tell AuthorForm class which fields we want to show in the form. To do that we use fields attribute of the Meta class. It accepts a list or tuple of field names you want to show in the form. If you want to show all the fields just use "__all__" (that's double underscore).

fields = '__all__'      # display all the fields in the form
fields = ['title', 'content']  # display only title and content field in the form

Similarly, there exists a complementary attribute called exclude which accepts a list of field names which you don't want to show in the form.

exclude = ['slug', 'pub_date'] # show all the fields except slug and pub_date

Let's update our code to use the fields attribute.

TGDB/django_project/blog/forms.py

from django import forms
from django.core.exceptions import ValidationError
from .models import Author, Tag, Category, Post


class AuthorForm(forms.Form):
    class Meta:
        model = Author
        fields = '__all__'

    def clean_name(self):
        name = self.cleaned_data['name']
        name_l = name.lower()
        if name_l == "admin" or name_l == "author":
            raise ValidationError("Author name can't be 'admin/author'")
        return name

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

Notice that we haven't changed clean_name() and clean_email() method because they work with ModelForm too.

Additional Validation in ModelForm #

In addition to Form validation, ModelForm also performs its own validation. What is meant by that? It simply means that ModelForm performs validation at the database level.

ModelForm Validation takes place in 3 steps.

Model.clean_fields() - This method validates all the fields in the model

Model.clean() - Works just like Form's clean() method. If you want to perform some validation at the database level which requires access to multiple fields override this method in the Model class. By default, this method does nothing.

Model.validate_unique() - This method checks uniqueness constraints imposed on your model (using unique parameter).

Which validation occurs first Form validation or Model validation?

Form Validation occurs first.

How do I trigger this Model validation?

Just call is_valid() method as usual and Django will run Form validation followed by ModelForm validation.

Creating Form classes for the remaning objects #

Before we move ahead, let's create PostForm, CategoryForm and TagForm class in the forms.py file.

TGDB/django_project/blog/forms.py

#...
from .models import Author, Tag, Category, Post
from django.template.defaultfilters import slugify

#...


class AuthorForm(forms.ModelForm):
    #...

class TagForm(forms.ModelForm):

    class Meta:
        model = Tag
        fields = '__all__'

    def clean_name(self):
        n = self.cleaned_data['name']
        if n.lower() == "tag" or n.lower() == "add" or n.lower() == "update":
            raise ValidationError("Tag name can't be '{}'".format(n))
        return n

    def clean_slug(self):
        return self.cleaned_data['slug'].lower()


class CategoryForm(forms.ModelForm):

    class Meta:
        model = Category
        fields = '__all__'

    def clean_name(self):
        n = self.cleaned_data['name']
        if n.lower() == "tag" or n.lower() == "add" or n.lower() == "update":
            raise ValidationError("Category name can't be '{}'".format(n))
        return n

    def clean_slug(self):
        return self.cleaned_data['slug'].lower()


class PostForm(forms.ModelForm):

    class Meta:
        model = Post
        fields = ('title', 'content', 'author', 'category', 'tags',)

    def clean_name(self):
        n = self.cleaned_data['title']
        if n.lower() == "post" or n.lower() == "add" or n.lower() == "update":
            raise ValidationError("Post name can't be '{}'".format(n))
        return n

    def clean(self):
        cleaned_data = super(PostForm, self).clean() # call the parent clean method
        title  = cleaned_data.get('title')
        # if title exists create slug from title
        if title:
            cleaned_data['slug'] = slugify(title)
        return cleaned_data

The TagForm and CategoryForm are very similar to AuthorForm class but PostForm is a little different. In PostForm, we are overriding Form's clean() method for the first time. Recall that we commonly use Form's clean() method when we want to perform some validation which requires access to two or more fields at the same time.

In line 56, we are calling Form's parent clean() method which by itself does nothing, except returning cleaned_data dictionary.

Next, we are using dictionary object's get() method to access the title field of the PostForm, recall that in the Form's clean() method none of the fields is guaranteed to exist.

Then we test the value of the title field. If the title field is not empty, then we use slugify() method to create slug from the title field and assign the result to cleaned_data['slug']. At last, we return cleaned_data from the clean() method.

It is important to note that by the time Form's clean() method is called, clean() methods of the individual field would have already been executed.

That'all for now. This chapter was quite long. Nonetheless, we have learned a lot about Django forms. In the next chapter, we will learn how to render forms in templates.