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 inherits forms.FieldName instead of models.FieldName.

Let's start by creating by creating a AuthorForm class.

Create a new file called forms.py, if not already exists inside 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 corresponds to a Python type.
  2. Both validates data in the form.
  3. Both fields are required by default.
  4. Both type of fields know how to represent them in the templates as HTML. Every form fields is displayed in the browser as a HTML "widget". Each form field is assigned a reasonable Widget class, but you can also override this setting.

Here is an important difference between model field 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, it is in unbound state.

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

If form is in bound state but contains in valid data then the form is bound and in valid. On the other hand, If 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 in bound state or not. If the form is in bound state then is_bound returns True, otherwise False.

Similarly, we can use is_valid() method to check whether the entered data is valid or not. If 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 validates and clean 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 you site. That's why Django validates the form data before you can use them.
  2. Any data user submits through a form will be passed to the server as strings, it doesn't matter which type of form field is used to create the form - IntegerField, SlugField or BooleanField. Eventually, the browser would send 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 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 know as cleaned data. We can access cleaned data via cleaned_data dictionary as follows:

     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 as follows:

(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 not matter what. Similarly, if is_valid() returns True then is_bound must be True.

Calling is_valid() method results in validation and cleaning of form data. In the process, Django creates an attribute called cleaned_data (a dictionary) which contains cleaned data only from the fields which has 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 AttributeError exception.

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

To bind data to a form simply pass a dictionary as 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 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. Here is how to get 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 objects provides two methods to ouput errors in different formats:

Method Explanation
as_data() returns a dictionary with ValidationError objects 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 cleaned_data attribute errors attribute is available to you all the time without first calling 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 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, lets 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 e.g if the field is defined as IntegerField then clean() 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, the cleaned and validated data is insert 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 method 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. As a result, If this method is called, it is guaranteed that the field's data is cleaned and validated at this point. So, you must always access field's data inside this method using cleaned_data['fieldname']. The value returned by this method replaces it's existing value 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 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 - 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/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 - 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 - 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/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 - email field's clean_email() is 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 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 it's override is called. Important thing to remember about the Form's clean() method is that none of the field 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 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 additon 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 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 at the end of AuthorForm class like this:

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 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 9-17, 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 models 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.

Remove redundancy using ModelForm #

ModelForm class allows 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 Meta class's model attribute.

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. 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 = ['title', 'content']  # display only title and content field in the form
fields = '__all__'      # display all the fields in the form

Similarly, There exist 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 fields attribute.

TGDB/django_project/blog/forms.py

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

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 validaton, ModelForm also performs its own validation. What is meant by that ? It simply means that ModelForm perform 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 other Objects #

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

...
from django.template.defaultfilters import slugify

...

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

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 require access to two or more field's at the same time.

In line 51, 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 field is guaranteed to exist.

Then we test the value of the title field. If the title field is not empty, then 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 lot about Django forms. In the next chapter we will learn how to render forms in templates.