Tutorial on how to use form validation in Django
This story will show how to use form validation in Django.
Prerequisites
I'm working on an Ubuntu 20.04, Django 3.1.1 and django-extensions 3.0.9 for good measure.
This will not be a complete from zero to hero tutorial, rather I'll assume that you know enough basic Django to be able to apply the shown yourself. Additionally, this story will lean on the topics covered in the first forms story.
Setup
We have initially these two models:
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
class Shop(models.Model):
name = models.CharField(max_length=100)
updated_by = models.ForeignKey(User, on_delete=models.DO_NOTHING, default=None, blank=True, null=True)
def __str__(self):
return self.name
class Product(models.Model):
shop = models.ForeignKey(Shop, related_name="products", on_delete=models.CASCADE)
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=10, decimal_places=2)
and a CreateView to create a shop:
class CreateShopView(CreateView):
template_name = "create_shop.html"
form_class = ShopForm
success_url = "/"
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.updated_by = self.request.user
self.object.save()
return HttpResponseRedirect(self.get_success_url())
def get_form_kwargs(self, *args, **kwargs):
kwargs = super().get_form_kwargs(*args, **kwargs)
kwargs['user'] = self.request.user
return kwargs
and a ShopForm:
from django import forms
from .models import Shop
class ShopForm(forms.ModelForm):
class Meta:
model = Shop
fields = ['name', ]
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
Validation use cases
Shop name validation
Lets assume I want to restrict what names can be entered in the shop's name field. For some reason, I don't want the user to be able to create shops with the name "The Galactic Empire". How would I do this?
There are several ways to do this, one way is to use the clean_FIELDNAME(...)
method:
from django import forms
from .models import Shop
from django.core.exceptions import ValidationError
class ShopForm(forms.ModelForm):
class Meta:
model = Shop
fields = ['name', ]
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
def clean_name(self):
cleaned_name = self.cleaned_data['name']
if cleaned_name == "The Galactic Empire":
raise ValidationError("You cannot create The Galactic Empire")
return cleaned_name
Now, if you enter "The Galactic Empire" in the form and submit, you will get the error message in the template that is raised with the ValidationError.
What if we want the same validation to apply to multiple different fields, say the name field on the Shop model and the name field on the Product model, how would we do this without duplicate code? The answer is with a custom validator, which is a function that takes the cleaned field data and either raises a ValidationError or does nothing.
We update the form to:
def validate_name(value):
if value == "The Galactic Empire":
raise ValidationError("You cannot create The Galactic Empire")
class ShopForm(forms.ModelForm):
name = forms.CharField(validators=[validate_name,])
class Meta:
model = Shop
fields = ['name', ]
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
And again, we can't create The Galactic Empire.
Validate multiple conditions in one go
What if we have multiple fields we wanna validate at the same time, for example 2 password fields where both fields have to have the same value? In this case, we would use the clean
method. Lets first update our Shop model to have an extra field, in this case a ticker field:
class Shop(models.Model):
name = models.CharField(max_length=100)
ticker = models.CharField(max_length=100, blank=True)
updated_by = models.ForeignKey(User, on_delete=models.DO_NOTHING, default=None, blank=True, null=True)
def __str__(self):
return self.name
And the form we update to include this field:
class ShopForm(forms.ModelForm):
name = forms.CharField(validators=[validate_name,])
class Meta:
model = Shop
fields = ['name', 'ticker', ]
...
...
...
def clean(self):
name = self.cleaned_data['name']
ticker = self.cleaned_data['ticker']
if name == "Alderaan":
if ticker == "":
raise ValidationError("Alderaan must have a ticker symbol")
return self.cleaned_data
The clean method takes out the data from the two fields and makes some logic involving both fields. And this is how this kind of multiple field validation is made.
References
Django forms official documentation