Tutorial on how to use forms in Django
This story will show how to use forms 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.
Setup
If you wanna follow along, setup a simple django project:
python3 -m venv ./venv
source venv/bin/activate
pip install django django-extensions
django-admin startproject config .
python manage.py startapp products
update the settings.py file with the new app:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'products',
]
...
TEMPLATE_DIR = os.path.join(BASE_DIR,"templates")
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [TEMPLATE_DIR],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
Models
We are setting up a project to handle sales from shops; To that extend, we need a Shop and Product model:
from django.db import models
class Shop(models.Model):
name = models.CharField(max_length=100)
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)
Forms
We will start by creating a simple form so we are able to create a Shop in the system. We will begin with a function based view and a manually created form, and from there gradually use more and more of Django's included functionality.
Shop form in a function based view
in a forms.py
file in the app folder, create the form like this:
from django import forms
class ShopForm(forms.Form):
name = forms.CharField(label='The shop name', max_length=100)
We also need a view:
from django.http import HttpResponseRedirect
from django.shortcuts import render
from .forms import ShopForm
def create_shop(request):
if request.method == 'POST':
form = ShopForm(request.POST)
if form.is_valid():
return HttpResponseRedirect('/')
else:
form = ShopForm()
return render(request, 'create_shop.html', {'form': form})
Setup the urls in a urls.py:
from django.urls import path
from . import views
urlpatterns = [
path('shop/create', views.create_shop, name='create_shop'),
]
And finally, a template:
<form action="/your-name/" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Submit">
</form>
We should now have:
Right now, if you enter anything in the shop form and click submit, nothing really happens, you are simply redirected to /
as specified in the:
if form.is_valid():
return HttpResponseRedirect('/')
But, we really wanna save the new shop and we do this after the form.is_valid()
and before the return HttpResponseRedirect('/')
:
if form.is_valid():
name = form.cleaned_data.get('name', None)
if name:
new_shop = Shop(name=name)
new_shop.save()
return HttpResponseRedirect('/')
After the form.is_valid()
has been executed, the form will contain a dictionary called cleaned_data
with all the form data in it, after it has been validated and 'cleaned' (eg. made into Python types). If you printed out the form.cleaned_data, you would have gotten:
{'name': 'abc'}
If we enter a name now and submit the form, we would create an entry in the database.
Just for fun, what does the request.POST
contain that we initialize the form with? if we print it out we get:
<QueryDict: {'csrfmiddlewaretoken': ['mGIi8M3YJ2X21sBRDoMr1jVRHZ3ar2jl3akkwQrIgKSXAHYGW804QXVw3PqeAInj'], 'name': ['ccc']}>
So, it contains the CSRF token as well as the 'uncleaned' data.
What if instead, we wanted to have the object creation logic (eg. create the new shop) stay in the form instead of in the view? we could create a .save() method on the form and call that from the view. The form could look like:
class ShopForm(forms.Form):
name = forms.CharField(label='The shop name', max_length=100)
def save(self):
name = self.cleaned_data.get('name', None)
new_shop = Shop(name=name)
new_shop.save()
and we update the view to be:
if form.is_valid():
name = form.cleaned_data.get('name', None)
if name:
form.save()
return HttpResponseRedirect('/')
If we try and create a new shop, it will still work.
is_valid()
Lets take a closer look at what the is_valid()
method actually does.
1 If the form has bound data, then the form.full_clean() is called.
2 form.full_clean() iterates over the form fields and each fields also iterates over:
--- to_python()
is called, creating the python type of the raw data.
--- Data is validated with field specific rules which includes custom validators.
--- Any custom made clean_FIELD(...) methods are called.
3 form.full_clean() then executes the form.clean() method, where validation that require access to multiple fields can take place (for example in a password form, ensure that password1 and password2 matches)
Failures in any of these steps raises a ValidationError(...)
.
If no ValidationErrors are raised then the method returns True, and False otherwise.
Modelform instead of Form
Lets swap the ShopForm we created for a ModelForm. This saves us a few lines of code.
class ShopForm(forms.ModelForm):
class Meta:
model = Shop
fields = ['name', ]
and we will update our view as well:
def create_shop(request):
if request.method == 'POST':
form = ShopForm(request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect('/')
else:
form = ShopForm()
return render(request, 'create_shop.html', {'form': form})
Everything will work just like before, but we don't have to reimplement the .save()
method. Additionally, the form will inherent the validation logic from the model which will also save us some lines of code.
Class based FormView
Lets try and get the modelform to work with a class based view, in this case a FormView.
class CreateShopView(FormView):
form_class = ShopForm
template_name = "create_shop.html"
success_url = "/"
def form_valid(self, form):
form.save()
return super().form_valid(form)
We also need to update our url a bit:
urlpatterns = [
path('shop/create', views.CreateShopView.as_view(), name='create_shop'),
]
And again, everything works as expected.
Class based CreateView
We can make it even simpler by using a CreateView, a generic view, which will make our view even simpler:
from django.views.generic.edit import CreateView
class CreateShopView(CreateView):
template_name = "create_shop.html"
form_class = ShopForm
success_url = "/"
Again, everything will work as before. And with CreateView we can make it even simpler by removing the form altogether and simply adding the model to the view:
class CreateShopView(CreateView):
template_name = "create_shop.html"
model = Shop
fields = "__all__"
success_url = "/"
No need for a separate form in this case. But, typically, it makes sense to have a separate form as you probably want to have some custom logic in your form anyway.
Extra kwargs for the form with class based views
Imagine we have a slightly different model for the Shop:
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
We now have a updated_by
field that we want set whenever the model instance is updated or created (in our case). Now, in the form, we want this done automatically, eg. the logged in user should simply be added to the instance.
In our CreateView version, we could do this by capturing the logged in user and pass this information to the form where it will be saved.
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['updated_by'] = self.request.user
return kwargs
If we just use this without updating the form we will get an error when trying to create a Shop. This is because, the form doesn't have a 'updated_by' field but is still passed a updated_by
keyword argument. So, we need to remove this updated by
kwarg before the form gets initialized which can be done by overriding the form's init method:
class ShopForm(forms.ModelForm):
class Meta:
model = Shop
fields = ['name', ]
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('updated_by')
super().__init__(*args, **kwargs)
References
Django forms official documentation