OverIQ.com

Django Form Basics

Last updated on July 27, 2020


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 a LanguageForm class.

Create a new file called forms.py, if not already exists in the djangobin app and add the following code to it.

djangobin/django_project/djangobin/forms.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from django import forms


class LanguageForm(forms.Form):
    name = forms.CharField(max_length=100)
    lang_code = forms.CharField()
    slug = forms.SlugField()  
    mime = forms.CharField()
    created_on = forms.DateTimeField()
    updated_on = forms.DateTimeField()

Form Fields #

Form fields are responsible for validating data and converting it to a Python type. However, unlike model fields, the form fields do not have a corresponding SQL Type. In other words, the form fields do not know how to represent themselves in the database.

Widget #

A widget is an HTML representation of a form field. Every form field is assigned a reasonable Widget class, but we can easily override this setting.

Form States #

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

Unbound State: If the form has no data associated with it then it is in Unbound state. For example, an empty form displayed for the first time to the user 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 (valid or invalid).

is_bound attribute and is_valid() method #

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

Similarly, we can use the is_valid() method to check whether the form 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 submits the data via a form, Django cleans the data and then validates it.

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 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 a string, BooleanField data would be converted to a bool (True or False) and so on. Once the data is cleaned and validated, Django makes it available 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 ./manage.py shell command in the terminal or command prompt. Next, import the LanguageForm class and instantiate it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ ./manage.py shell
Python 3.5.2 (default, Nov 23 2017, 16:37:01) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>
>>> from djangobin.forms import LanguageForm
>>>
>>> f = LanguageForm()
>>>

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

1
2
3
4
>>>
>>> f.is_bound
False
>>>

As expected is_bound attribute returns False. Although meaningless, we could also check whether the form is valid or not by calling is_valid() method.

1
2
3
4
>>>
>>> f.is_valid()
False
>>>

Calling is_valid() method results in cleaning and validation 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() method 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 containing form data to the Form constructor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>>
>>> data = {
... 'name': 'ruby',
... 'lang_code': 'ruby',
... 'slug': 'ruby lang', 
... 'mime': 'text/plain',
... }
>>>
>>>
>>> f = LanguageForm(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.

1
2
3
4
>>>
>>> f.is_bound
True
>>>

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

1
2
3
4
5
6
>>>
>>> f2 = LanguageForm({})
>>>
>>> f2.is_bound
True
>>>

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

1
2
3
4
5
6
7
>>>
>>> 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 get an AttributeError exception. Now we will validate the form by calling is_valid() method.

1
2
3
4
5
6
7
>>>
>>> f.is_valid()
False
>>>
>>> f.cleaned_data
{'lang_code': 'ruby', 'mime': 'text/plain', 'name': 'ruby'}
>>>

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

Always remember that the cleaned_data attribute will only contain cleaned and validated 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.

1
2
3
4
5
6
7
>>> 
>>> f.errors
{'created_on': ['This field is required.'],
 'slug': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."],
 'updated_on': ['This field is required.']}
>>>
>>>

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>>
>>> f.errors['slug']
["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]
>>>
>>>
>>> f.errors['updated_on']
['This field is required.']
>>>
>>>
>>> f.errors['created_on']
['Enter a valid date/time.']
>>>
>>>

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

Method Explanation
as_data() returns a dictionary with ValidationError object instead of a string.
as_json() returns errors as JSON
1
2
3
4
5
6
7
>>>
>>> f.errors.as_data()
{'created_on': [ValidationError(['This field is required.'])],
 'slug': [ValidationError(["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."])],
 'updated_on': [ValidationError(['This field is required.'])]}
>>>
>>>
1
2
3
4
5
6
7
8
>>>
>>> f.errors.as_json()
('{"created_on": [{"message": "This field is required.", "code": "required"}], '
 '"slug": [{"message": "Enter a valid \'slug\' consisting of letters, numbers, '
 'underscores or hyphens.", "code": "invalid"}], "updated_on": [{"message": '
 '"This field is required.", "code": "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 cleaning and validation of form data, 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. In your code, however, you 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
>>>
>>> from datetime import datetime
>>>
>>>
>>> data = {
...     'name': 'ruby',
...     'lang_code': 'ruby',
...     'slug': 'ruby', 
...     'mime': 'text/plain',
...     'created_on': datetime.now(),
...     'updated_on': datetime.now(),
... }
>>>
>>>
>>> f = LanguageForm(data)
>>>
>>>
>>> f.is_bound
True
>>>
>>>
>>> f.is_valid()
True
>>>
>>>
>>> f.cleaned_data
{'created_on': datetime.datetime(2018, 4, 5, 18, 9, 21, 244298, tzinfo=<UTC>),
 'lang_code': 'ruby',
 'mime': 'text/plain',
 'name': 'ruby',
 'slug': 'ruby',
 'updated_on': datetime.datetime(2018, 4, 5, 18, 9, 21, 244317, tzinfo=<UTC>)}
>>>
>>>
>>> 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 to the appropriate Python type (recall that the data is sent by the browser as a string to the server). 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 data received from the step 1. If validation succeeds, the 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 routine 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 the 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.

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

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 lang_code 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 lang_code field's clean_lang_code() 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 lang_code field is cleaned and validated, so the following code is perfectly valid inside clean_lang_code() method.

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

However, there is no guarantee that the data from other fields, for example, the name field is available inside clean_lang_code() method. So, you should not attempt to access name field inside clean_lang_code() 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 LanguageForm class. Here are the things we want to achieve.

  1. Prevent users from creating language named "djangobin" and "DJANGOBIN".
  2. Save the slug in lowercase only. At this point, nothing is stopping us to save the slug in uppercase.
  3. Value of slug and mime shouldn't be same.

Note: Off course these are superficial validations. The whole point of implementing them is to show you how to perform custom validations.

Open forms.py and modify the code as follows:

djangobin/django_project/djangobin/forms.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from django import forms
from django.core.exceptions import ValidationError


class LanguageForm(forms.Form):
    name = forms.CharField(max_length=100)
    lang_code = forms.CharField()
    slug = forms.SlugField()
    mime = forms.CharField()
    created_on = forms.DateTimeField()
    updated_on = forms.DateTimeField()

    def clean_name(self):
        name = self.cleaned_data['name']
        if name == 'djangobin' or name == 'DJANGOBIN':
            raise ValidationError("name can't be {}.".format(name))

        # Always return the data
        return name

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

    def clean(self):
        cleaned_data = super(LanguageForm, self).clean()
        slug = cleaned_data.get('slug')
        mime = cleaned_data.get('mime')

        if slug == mime:
            raise ValidationError("Slug and MIME shouldn't be same.")

        # Always return the data
        return cleaned_data

Here we have created three custom validators:

The clean_name() method ensures that the name of the language shouldn't be djangobin or DJANGOBIN. This method will be called only when the clean() method of the name field doesn't raise ValidationError exception.

The clean_slug() method converts the slug to lowercase. Just like the clean_name() method, it will be called only when the slug field's clean() method doesn't raise a ValidationError.

Finally, we have the overridden the form's clean() method. By the time this method is called, clean() methods of the individual field would have already been executed. Errors raised by this method will not be associated with a particular field. They will go into a separate field called __all__. Such errors are known as Non-Field Errors.

Let's put the custom validators to test.

Restart the Django shell for the changes to take effect and then enter the following code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
>>>
>>> from djangobin.forms import LanguageForm
>>>
>>> from datetime  import datetime
>>>
>>>
>>> data = {
...     'name': 'djangobin',
...     'lang_code': 'ruby',
...     'slug': 'RUBY', 
...     'mime': 'ruby',
...     'created_on': datetime.now(),
...     'updated_on': datetime.now(),
... }
>>>
>>>
>>> f = LanguageForm(data)
>>>
>>>
>>> f.is_bound
True
>>>
>>>
>>> f.is_valid()
False
>>>
>>>
>>> f.cleaned_data
{'created_on': datetime.datetime(2018, 4, 6, 6, 0, 57, 261639, tzinfo=<UTC>),
 'lang_code': 'ruby',
 'mime': 'ruby',
 'slug': 'ruby',
 'updated_on': datetime.datetime(2018, 4, 6, 6, 0, 57, 261658, tzinfo=<UTC>)}
>>>
>>>
>>> f.errors
{'__all__': ["Slug and MIME shouldn't be same."],
 'name': ["name can't be djangobin."]}
>>>
>>>

As expected, the form validation failed because djangobin is not a valid language name and the value of slug name mime fields are same. Notice that the cleaned_data dictionary contains slug in lowercase, thanks to clean_slug() method.

Let's try validating form once more, this time we will provide valid data in every field.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
>>>
>>>
>>> data = {
...     'name': 'ruby',
...     'lang_code': 'ruby',
...     'slug': 'RUBY', 
...     'mime': 'text/plain',
...     'created_on': datetime.now(),
...     'updated_on': datetime.now(),
... }
>>>
>>> 
>>> f = LanguageForm(data)
>>> 
>>>
>>> f.is_bound
True
>>> 
>>> f.is_valid()
True
>>>
>>>
>>> f.cleaned_data
{'created_on': datetime.datetime(2018, 4, 6, 6, 13, 41, 437553, tzinfo=<UTC>),
 'lang_code': 'ruby',
 'mime': 'text/plain',
 'name': 'ruby',
 'slug': 'ruby',
 'updated_on': datetime.datetime(2018, 4, 6, 6, 13, 41, 437569, tzinfo=<UTC>)}
>>>
>>>
>>> f.errors
{}
>>>
>>>

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

Common Form field Options #

In this section, we will go over some core field options that can be used with all types of form fields.

required #

By default, you are required to enter data in every form field. If you don't provide any data to the field while submitting a form then the clean() method of the field will raise a ValidationError exception.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>>
>>> from django import forms
>>>
>>> f = forms.CharField()
>>> 
>>> f.clean(100)   
'100'
>>> 
>>> f.clean("")    # An empty string ("") signifies no data.
...      
django.core.exceptions.ValidationError: ['This field is required.']
>>>
Traceback (most recent call last):

We can make the field optional by passing required=False to the field constructor.

1
2
3
4
5
6
>>> 
>>> f = forms.CharField(required=False)
>>> 
>>> f.clean("")
''
>>>

label #

The label specifies a human-friendly name for the form field. This will appear inside the <label> tag. If you don't specify this option Django will create default label by taking field name, capitalizing it, converting underscores to spaces and appending a trailing colon (:).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>>
>>> class PostForm(forms.Form):
...     title = forms.CharField(label="Enter title")
...     content = forms.CharField()
... 
>>> 
>>> f = PostForm()
>>> 
>>> print(f)
<tr>
    <th>
        <label for="id_title">Enter title:</label>
    </th>
    <td>
        <input type="text" name="title" required id="id_title" />
    </td>
</tr>
<tr>
    <th>
        <label for="id_content">Content:</label>
    </th>
    <td>
    <input type="text" name="content" required id="id_content" />
    </td>
</tr>

initial #

The initial parameter sets the initial data to display when rendering a field.

1
2
3
4
5
6
7
>>> 
>>> class PostForm(forms.Form):
...     title = forms.CharField(initial="Enter title")
...     content = forms.CharField()
... 
>>> 
>>>

Note that the providing initial data to a field doesn't magically put the form in the bound state.

1
2
3
4
5
6
>>> 
>>> f = PostForm()
>>>
>>> f.is_bound
False
>>>

But if we now render the form, the title field will have the value of Enter title.

1
2
3
4
5
6
7
8
9
>>> 
>>> f
<PostForm bound=False, valid=Unknown, fields=(title;content)>
>>>
>>> print(f)
<tr><th><label for="id_title">Title:</label></th><td><input type="text" name="title" value="Enter title" required id="id_title" /></td></tr>
<tr><th><label for="id_content">Content:</label></th><td><input type="text" name="content" required id="id_content" /></td></tr>
>>> 
>>>

We can also set the initial values while instantiating a form. This allows us to set initial values for multiple fields at once. For example:

1
2
3
4
5
6
7
8
>>> 
>>> f = PostForm(initial={'title': 'Some title', 'content': 'Some content'})
>>> 
>>> print(f)
<tr><th><label for="id_title">Title:</label></th><td><input type="text" name="title" value="Some title" required id="id_title" /></td></tr>
<tr><th><label for="id_content">Content:</label></th><td><input type="text" name="content" value="Some content" required id="id_content" /></td></tr>
>>> 
>>>

This method of providing initial values is quite flexible because it allows us to dynamically set initial data. We will see an example of this in upcoming lessons.

widget #

This parameter allows us to change default widget used by the form field.

Each form field is assigned a default widget. That's how the field knows how to render itself as HTML.

Formally, a widget is a class which controls rendering of a particular form element in HTML and extraction of GET/POST data from it. Just like model and form fields, Django provides built-in widget classes representing common form elements. The following table lists some common Widget class and the form element they represent.

Widget class HTML
TextInput <input type="text" ...>
PasswordInput <input type="password" ...>
EmailInput <input type="email" ...>
Textarea <textarea>...</textarea>
URLInput <input type="url" ...>
DateTimeInput <input type="text" ...>

And just for your quick reference following table lists common form fields and the default widget they use.

Form Field Default widget class
CharField TextInput
EmailField EmailInput
SlugField TextInput
DateTimeField DateTimeInput

Note: You can view the complete list of default field-widget pairing at this URL.

To change the default widget simply assign a new widget class to the widget parameter while defining the field. For example:

1
2
3
4
5
6
>>>
>>> class PostForm(forms.Form):
...     title = forms.CharField()
...     content = forms.CharField(widget=forms.Textarea)
... 
>>>

By default, CharField renders itself as TextInput ( or <input type="text" ...> ). In the preceeding code, we force Django to render content field as Textarea ( i.e <textarea>...</textarea>) instead of TextInput.

1
2
3
4
5
6
7
8
>>> 
>>> f = PostForm()
>>>
>>> print(f)
<tr><th><label for="id_title">Title:</label></th><td><input type="text" name="title" required id="id_title" /></td></tr>
<tr><th><label for="id_content">Content:</label></th><td><textarea name="content" required rows="10" cols="40" id="id_content">
</textarea></td></tr>
>>>

We can also set additional attributes to the form element using the attrs attribute:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>> 
>>> class PostForm(forms.Form):
...     title = forms.CharField()
...     content = forms.CharField(widget=forms.Textarea(attrs={'class': 'content', 'row':'5', 'cols':'10'}))
... 
>>> 
>>> f = PostForm()
>>> 
>>> print(f)
<tr><th><label for="id_title">Title:</label></th><td><input type="text" name="title" required id="id_title" /></td></tr>
<tr><th><label for="id_content">Content:</label></th><td><textarea name="content" required row="5" cols="10" rows="10" id="id_content" class="content">
</textarea></td></tr>
>>> 
>>>

help_text #

This parameter is used to specify some important information about the field.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>> 
>>> class PostForm(forms.Form):
...     title = forms.CharField()
...     content = forms.CharField(help_text="Good stuff")
... 
>>> 
>>> f = PostForm()
>>> 
>>> 
>>> print(f)
<tr><th><label for="id_title">Title:</label></th><td><input type="text" name="title" required id="id_title" /></td></tr>
<tr><th><label for="id_content">Content:</label></th><td><input type="text" name="content" required id="id_content" /><br /><span class="helptext">Good stuff</span></td></tr>
>>> 
>>>

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 LanguageForm class:

djangobin/django_project/djangobin/forms.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from django import forms
from django.core.exceptions import ValidationError
from .models import Language


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

    def clean(self):
        #...

    def save(self):
        new_lang = Language.objects.create(
            name = self.cleaned_data['name'],
            lang_code = self.cleaned_data['lang_code'],
            slug = self.cleaned_data['slug'],
            mime = self.cleaned_data['mime'],
            created_on = self.cleaned_data['created_on'],
            updated_on = self.cleaned_data['updated_on'],
        )
        return new_lang

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

Restart the Django shell again and Let's try creating a new Language via LanguageForm.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
>>>
>>> from djangobin.forms import LanguageForm
>>>
>>> from datetime import datetime
>>>
>>> data = {
...     'name': 'Go',
...     'lang_code': 'go',
...     'slug': 'go', 
...     'mime': 'text/x-gosrc',
...     'created_on': datetime.now(),
...     'updated_on': datetime.now(),
... }
>>>
>>> f = LanguageForm(data)
>>>
>>> f.is_bound
True
>>>
>>> f.is_valid()
True
>>>
>>> f.save()
<Language: Go>
>>> 
>>>
>>> from djangobin.models import Language
>>>
>>> l = Language.objects.get(name='Go')
>>> 
>>> l
<Language: Go>
>>> 
>>> l.pk
17
>>> 
>>>

Sure enough, our newly created Language 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 application; but there is a big problem.

The problem with this approach is that the fields in the LanguageForm class map closely to the fields in the Language model. As a result, redefining them in the LanguageForm is redundant. If we add or modify any field in the Language model then we would have to update our LanguageForm 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 slug field in Language model is defined like this:

slug = models.SlugField(max_length=100, unique=True)

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

slug = forms.SlugField(max_length=100)

Notice that the slug field in LanguageForm doesn't have unique=True attribute. This is because unique=True 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_slug() method like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#...
class LanguageForm(forms.Form):
    #...

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

        return slug.lower()

Similarly, Form fields don't provide, auto_add_now, and auto_now parameters to automatically populate date and time. We can implement the functionalities provided by these parameters by overriding the form's clean() method.

As you can see, for each functionality provided by the Django models, we would have to add various custom validators, override form's clean() method and so on. Certainly, this involves a lot of work. We can avoid all these issues by using ModelForm.

Removing redundancy using ModelForm #

Most of the time, you will want to have a form that matches closely to your model class. This is where ModelForm class comes into play. The ModelForm lets you build forms that are based on the fields of the model class. It also gives you options to customize the form.

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 LanguageForm 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.

djangobin/django_project/djangobin/forms.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from django import forms
from django.core.exceptions import ValidationError
from .models import Language


class LanguageForm(forms.ModelForm):
    class Meta:
        model = Language        

    def clean_name(self):
        name = self.cleaned_data['name']
        name_l = name.lower()
        if name == 'djangobin' or name == 'DJANGOBIN':
            raise ValidationError("name can't be {}.".format(name))
        return name

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

    def clean(self):
        cleaned_data = super(LanguageForm, self).clean()
        slug = cleaned_data.get('slug')
        mime = cleaned_data.get('mime')

        if slug == mime:
            raise ValidationError("Slug and MIME shouldn't be same.")

        # Always return the data
        return cleaned_data

Next, we have to tell LanguageForm 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 a double underscore).

1
2
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.

djangobin/django_project/djangobin/forms.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from django import forms
from django.core.exceptions import ValidationError
from .models import Language


class LanguageForm(forms.Form):
    class Meta:
        model = Language
        fields = '__all__'

    def clean_name(self):
        name = self.cleaned_data['name']
        if name == 'djangobin' or name == 'DJANGOBIN':
            raise ValidationError("name can't be {}.".format(name))

        # Always return the data
        return name
    
    def clean_slug(self):
        return self.cleaned_data['slug'].lower()

    def clean(self):
        cleaned_data = super(LanguageForm, self).clean()
        slug = cleaned_data.get('slug')
        mime = cleaned_data.get('mime')

        if slug == mime:
            raise ValidationError("Slug and MIME shouldn't be same.")

        # Always return the data
        return cleaned_data

Notice that we haven't changed our custom validators at all because they work with ModelForm too.

Model Validation #

In addition to form validation, ModelForm also performs model validation. The model validation validates data at the database level.

The model validation takes place in 3 steps.

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

  2. 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.

  3. Model.validate_unique() - This method checks uniqueness constraints imposed on your model.

The model validation starts right after the form's clean() method is called.

Now, here comes the most important statement to keep in mind:

By Default, Django doesn't use model validation.

Note: The reason for this puzzling behavior is backward compatibility.

That means Django allows you to screw up the data in the database. We can verify this fact by saving an object containing invalid data into the database.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
>>>
>>> l = Language(
...     name='racket',
...     slug='this is an invalid slug',
...     mime='text/plain',
...     lang_code='racket',
...     file_extension='*.rkt'    
... )
>>> 
>>> 
>>> l.save()
>>> 
>>> l.pk
9
>>> 
>>> l
<Language: racket>
>>>
>>>

Here the slug field contains invalid data but Django still saved the object into the database.

We can manually trigger the model validation using the full_clean() method as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
>>> 
>>> l = Language(
...     name='Haskell',
...     slug='Hask ell',
...     mime='text/x-haskell',
...     lang_code='hs',    
...     file_extension='*.hs'
... )
>>> 
>>> 
>>> l.full_clean()
Traceback (most recent call last):
  ...  
django.core.exceptions.ValidationError: {'slug': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]}
>>> 
>>>

Calling full_clean() method only triggers model validation, it doesn't save the object into the database.

Since ModelForm automatically triggers model validation, we don't need to worry about calling full_clean() method manually.

Overriding the default fields in ModelForm #

ModelForm's utility is not just limited to quickly creating forms. It also gives you a handful of Meta attributes to customize the form fields. For example, the widgets attribute allows us to override default widget or add additional attributes to the form element.

1
2
3
4
5
6
7
8
9
from django.forms import ModelForm, Textarea

class LanguageForm(forms.ModelForm):
    class Meta:
        model = Language
        fields = '__all__'
        widgets = {
            'file_extension': Textarea(attrs={'rows': 5, 'cols': 10}),
        }

Similarly, we can add a label and help text using labels and help_texts attributes of inner Meta class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from django.forms import ModelForm, Textarea

class LanguageForm(forms.ModelForm):
    class Meta:
        model = Language
        fields = '__all__'
        labels = {
            'mime': 'MIME Type',
            'lang_code': 'Language Code',
        },
        help_texts = {            
            'lang_code': 'Short name of the Pygment lexer to use',
            'file_extension': 'Specify extension like *.txt, *.md etc;'
        },

That's 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.