Django Forms 2 Validation

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

Django views official documentation

Django docs on validators