(Comments)
We had a basic introduction to Django's Forms and Model forms in my previous post. We will now discuss in detail the validation of form data.
Form validation is the main reason that any developer has to use Forms for. The basic security principle of any computer application is "do not trust the user input". That could be because the user passes in malicious data to compromise the application or the user has a made a mistake in providing the needed data which in turn, unintentionally, breaks our application. What ever the case, every piece of user input has to be validated to keep our application secure and un-compromised.
Django validates a form when we run the is_valid()
method and the validated data is placed in the cleaned_data
attribute of the form. A coding example will clear all of our doubts caused by vague English statements. We first define a registration form as follows:
from django.contrib.auth.models import User class RegistrationForm(ModelForm): re_password = forms.CharField(max_length=128) class Meta: model = User fields = ['first_name', 'last_name', 'email', 'password', 're_password']
This form uses the Django's built in User
model to build our model form. This is equivalent to following form in terms of validation except that it does not have a model associated with it or have the save()
method.
class RegistrationForm(forms.Form): first_name = forms.CharField(max_length=30, required=False) last_name = forms.CharField(max_length=30, required=False) email = forms.EmailField(required=False) password = forms.CharField(max_length=128) re_password = forms.CharField(max_length=128)
But the field username
in User
model is a non-blank field. Hence we can't directly use the built-in save()
method without over-riding it. Our form renders into HTML as follows.
I have used the django bootstrap3
to render forms with Bootstrap 3 (and I recommend you do the same, though that is not necessary). My HTML template (that uses django-bootstrap3
) looks like this:
{% load bootstrap3 %} <html> <head> {% bootstrap_css %} {% bootstrap_javascript %} {% bootstrap_messages %} </head> <body> <div class="container"> <div class="row"> <div class="col-sm-offset-3 col-sm-6"> <form action="" method="post"> {% csrf_token %} {% bootstrap_form form %} {% buttons %} <button type="submit" class="btn btn-success btn-block">Register</button> {% endbuttons %} </form> </div> </div> </div> </body> </html>
I have defined my view to be like this:
from .forms import RegistrationForm from django.shortcuts import render from django.contrib import messages def indexView(request): if request.method == 'POST': form = RegistrationForm(request.POST) if form.is_valid(): messages.success(request, 'The form is valid.') else: messages.error(request, 'The form is invalid.') return render(request, 'posts/index.html', {'form': form}) else: form = RegistrationForm() return render(request, 'posts/index.html', {'form': form})
The view does nothing but validate the form data and show a message stating whether the form is valid or not. This is enough to demonstrate the most functionality of forms.
The only required fields in our form here are password
and re_password
as all the other included fields are allowed null values in the model. Now fill-in some values in password field and re_password field and click on register. Observe that the password field has been rendered as a text field (revealing the password we entered). But that does not matter now. The response page from the server is:
Now that, I think, clears up everything that we need to change in our form now. Here they are as bullet points.
@cowhite.com
- This needs a custom validator to be defined.Once the validation is done, the next thing we do is to override the save()
method to save the user model without username and correctly setting password.
Task one. We first make all the fields as required. Before we do the changes in the form let us see what are the validators set for the fields. We can check them in the Django shell which we can start with:
python manage.py shell
Once the shell is up and running, do the following.
In [1]: from posts.forms import RegistrationForm In [2]: rf = RegistrationForm() In [3]: fn_field = rf.fields['first_name'] In [4]: fn_field Out[4]: <django.forms.fields.CharField at 0x7f44b18dfdd8> In [5]: fn_field.validators Out[5]: [<django.core.validators.MaxLengthValidator at 0x7f44b228fcf8>] In [6]: fn_field.max_length Out[6]: 30 In [7]: fn_field.required Out[7]: False
We have only checked the status of the first_name
form field. All the other fields are also similar. There is only one validator associated with the first_name
field and that is MaxLengthValidator
. This has be auto-generated due the constraint of the field size in the User
model. We can also observe that the required
attribute of the field is set to False
. To make them as required fields, we change our __init__
method of our form definition to the following:
def __init__(self, *args, **kwargs): super(RegistrationForm, self).__init__(*args, **kwargs) for field_name, field in self.fields.items(): field.required = True
That is all we need to do to make the field compulsory. Before we check, we need to exit and restart the Django shell, because the changes in the code are reflected in the shell only after a restart. Now the result of our new form definition is:
In [1]: from posts.forms import RegistrationForm In [2]: rf = RegistrationForm() In [3]: fn_field = rf.fields['first_name'] In [4]: fn_field.validators Out[4]: [<django.core.validators.MaxLengthValidator at 0x7f666a5baba8>] In [5]: fn_field.required Out[5]: True
The result when the form is submitted with empty field values is:
Now that our task one is done, we head to our task two - making both the password fields render as password type HTML element. This is easy. We need to the use the Django's built-in PasswordInput
widget. Widgets define the rendering of field into HTML.
On applying the widget, our form definition changes to the following:
from django.contrib.auth.models import User from django.forms import PasswordInput class RegistrationForm(ModelForm): re_password = forms.CharField(max_length=128, widget=PasswordInput()) def __init__(self, *args, **kwargs): super(RegistrationForm, self).__init__(*args, **kwargs) for field_name, field in self.fields.items(): field.required = True class Meta: model = User fields = ['first_name', 'last_name', 'email', 'password', 're_password'] widgets = { 'password': PasswordInput() }
The django.forms.PasswordInput
widget specifies that the field (though it is a CharField) must be rendered as password type input element. The result is that both the fields are rendered as password inputs.
Observe that we have specified the widget for the password
in the form's Meta class, where as for the re_password
field we specified it in the field definition itself, i.e. within the line re_password = forms.CharField(max_length=128, widget=PasswordInput())
. The catch here is that the widgets of models fields can be overridden in the Meta class's widgets
dictionary whose keys map to the field names of the model (and the form) and their values (which must be a widget instance or class) specify the widget to apply to the fields. But the following would not work.
widgets = { 'password': PasswordInput(), 're_password': PasswordInput() }
We cannot override the widget of a field in this way (I mean using a dictionary in the Meta class) if the field is manually defined in the form. Only those fields that are automatically imported from a model can be overwritten in this way. We have specified the widget to our re_password
field in its definition itself. And of course, this is not the only way. We can override the widgets in the __init__
method.
Before we jump to our next task, let us change definition of the re_password
to the following :
re_password = forms.CharField(max_length=128, widget=PasswordInput(), label='Re-enter password')
As you might have expected, this changes the label of the html field.
We now go to the our next task of checking that the email entered is from cowhite.com. We achieve this by creating a custom validator. Validators for a field can be specified just like we have specified widgets (visit the official documentation of using validators). To validate a field we need to create a method named clean_<field_name>()
that raises django.forms.ValidationError
error if the data is invalid. It must return the validated field data if it is valid. To validate the email
field, we create a method clean_email()
in the RegistrationForm like this:
from django.forms import ValidationError def clean_email(self): email = self.cleaned_data['email'] if not email.endswith('@cowhite.com'): raise ValidationError('Domain of email is not valid') return email
The result of adding this method can be seen when I used my gmail id in the email field.
But the form validates to True
when I use my company's email.
Our next requirement is to check whether the input password is at least 8 characters long or not. This is as simple as specifying the max_length
parameter to the re_password
field. Before making changes to our form let us examine our form in the django shell.
In [1]: from posts.forms import RegistrationForm In [2]: rf = RegistrationForm() In [3]: pwd_field = rf.fields['password'] In [4]: pwd_field.validators Out[4]: [<django.core.validators.MaxLengthValidator at 0x7fe696cc3ac8>]
We only have a MaxLengthValidator
for our password field. We can achieve the minimum length check by defining our password
field of the RegistrationForm as follows
password = forms.CharField(max_length=40, min_length=8, widget=PasswordInput())
While this works what we are actually doing is re-defining the password field which is already defined in the model. A better way to achieve the same result is to add the validator in the __init__()
method.
from django.core.validators import MinLengthValidator def __init__(self, *args, **kwargs): super(RegistrationForm, self).__init__(*args, **kwargs) for field_name, field in self.fields.items(): field.required = True # Our previous definition was till this line password_field = self.fields['password'] password_field.validators.append(MinLengthValidator(limit_value=8))
Let us examine the result of this change through the Django shell.
In [1]: from posts.forms import RegistrationForm In [2]: rf = RegistrationForm() In [3]: pwd_field = rf.fields['password'] In [4]: pwd_field.validators Out[4]: [<django.core.validators.MaxLengthValidator at 0x7fcbfefac710>, <django.core.validators.MinLengthValidator at 0x7fcbfefaf0f0>]
We now have two validators associated with the password field. The result in the HTML is:
Our final requirement is to verify that both the passwords entered are same. The validation of individual field values is done by the clean_<field_name>()
methods. The overall validation of the whole data is however, done by the clean()
method. We are going to override this method. Define the clean()
method of our RegistrationForm
as follows:
def clean(self): super(RegistrationForm, self).clean() # This method will set the `cleaned_data` attribute password = self.cleaned_data.get('password') re_password = self.cleaned_data.get('re_password') if not password == re_password: raise ValidationError('Passwords must match')
The result of this in the HTML is:
One thing we need notice here is that the error is shown on top of the form and not below the Re-enter password field. The take away here is that the validation errors raised by clean()
method are associated with the form itself while the errors raised by clean_<field_name>()
methods are associated with individual fields.
Though it is not necessary, let us add the 'Passwords must match'
error to the Re-enter password field. To do this we use the add_error()
method which takes the field name string as the first argument and the error message string as the second argument. Our clean()
method now changes to:
def clean(self): super(RegistrationForm, self).clean() password = self.cleaned_data.get('password') re_password = self.cleaned_data.get('re_password') if not password == re_password: self.add_error('re_password', 'Passwords must match')
The resulting html is:
That is how we can add our own error messages to the form fields.
save()
method of the formThe save()
of a django form is used to save the data to the database models. But our form does not quiet match with the User
model we have used it with. Hence, we need to change how the save()
method works.
If we modify nothing in the save()
method, conceptually (not how it exactly works) it would look something like this.
def save(self): return User.objects.create(**self.cleaned_data)
But that would raise database errors and the password would be stored as plain-text which we do not want. So we modify our form's save()
method to:
def save(self): username = email = self.cleaned_data.get('email') first_name = self.cleaned_data.get('first_name') last_name = self.cleaned_data.get('last_name') password = self.cleaned_data.get('password') return User.objects.create_user(username, email=email, password=password, first_name=first_name, last_name=last_name)
We are taking advantage of create_user method of User Manager which takes care of hashing password. Now we can use our form in the view in someway similar to:
def indexView(request): if request.method == 'POST': form = RegistrationForm(request.POST) if form.is_valid(): user = form.save() # Do something with the user messages.success(request, 'User saved successfully.') else: messages.error(request, 'The form is invalid.') return render(request, 'posts/index.html', {'form': form}) else: form = RegistrationForm() return render(request, 'posts/index.html', {'form': form})
The forms are at the core of Django and they have too many features and options that would require a lot more than a single blog post to cover. I have tried to cover the most commonly used features and problems faced in development. I hope this post gives an insight into most of the practical needs and solutions associated with Django forms.
We develop web applications to our customers using python/django/angular.
Contact us at hello@cowhite.com
Comments