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:
- Django form inherits from
forms.Form
instead ofmodels.Model
. - Each form fields inherit
forms.FieldName
instead ofmodels.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:
The first step is to call Field's
clean()
method. Every form field has aclean()
method, which does the following two things: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 theclean()
method will convert the data to Pythonint
, if it fails to do so, it raises aValidationError
exception.Validate the data received from the step 1. If validation succeeds, the data is inserted into the
cleaned_data
attribute. If it fails aValidationError
is raised. We usually don't override field'sclean()
method.
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 whenValidationError
is not raised by the Field'sclean()
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 usingcleaned_data['fieldname']
. The value returned by this method replaces the existing value of the field in thecleaned_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.
- Prevent users from creating language named
"djangobin"
and"DJANGOBIN"
. - Save the slug in lowercase only. At this point, nothing is stopping us to save the slug in uppercase.
- 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:
- Change inheritance of the form class from
forms.Form
toforms.ModelForm
. - Inform the form class in
forms.py
which model to use by using themodel
attribute of theMeta
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.
Model.clean_fields()
- This method validates all the fields in the modelModel.clean()
- Works just like Form'sclean()
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.
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.
Load Comments