Forms play a vital role in web applications, serving as a communication channel between you and your visitors. By collecting inputs from the visitors and transmitting them to the backend, forms enable effective interaction and collaboration.
Previously, we demonstrated how to build a basic CRUD operation using raw HTML forms. If you followed the linked tutorial, you will see that it is not an easy task. You need to deal with different data types, validate the user input, match the user input to different model fields, set up CSRF protection, and so on. This will increase the difficulty for future maintenance, especially when you need to reuse the same form in multiple webpages. Django's built-in form functionality can significantly simplify this process and automate the majority of your work.
In this article, we are going to discuss how to create forms the Django way, how to create new resources, update existing resources, as well as how to upload files via Django forms.
Creating new resources using Django form
Let's start easy and consider this basic Post
model:
class Post(models.Model):
title = models.CharField(max_length=255)
content = models.TextField()
is_published = models.BooleanField(default=False)
def __str__(self):
return self.title
Instead of building the corresponding form using raw HTML, let's create a forms.py
file and add the following code:
from django import forms
class PostForm(forms.Form):
title = forms.CharField(
max_length=255,
)
content = forms.CharField(
widget=forms.Textarea(),
)
is_published = forms.BooleanField(
required=False,
)
Here we created three form fields, each corresponds to one model field in the Post
model. For the title
field, forms.CharField
matches models.CharField
, with the same constraint (max_length=255
). This form field also corresponds to an HTML input field in the rendered webpage, which you'll see later.
The content
field is a bit more complex, as you must define an extra widget. For Django forms, you must define both a form field and a widget in order to properly render a field. The form field tells Django what data type it will be dealing with, and the widget tells Django what HTML input element the field should use.
These two concepts seem like the same thing, but Django separates them because sometimes the same form field requires different widgets. For example, the forms.CharField
tells Django it should be expecting characters and texts, and by default, this field is tied to the forms.TextInput
widget. That is the right widget for the title
field, but for content
, we should use a large textbox, so its widget is set to forms.Textarea
.
Lastly, for the is_published
field, forms.BooleanField
uses the CheckboxInput
widget. Since this field indicates the status of the post, either published or not published, you must allow the checkbox to be unchecked by setting required=False
. This way the is_published
field can return either True
or False
, instead of forcing it to be checked.
Besides the CharField
and BooleanField
, Django also offers other form fields such as: DateTimeField
, EmailField
, FileField
, ImageField
, ChoiceField
, MultipleChoiceField
, and so on. Please refer to the documentation for a full list of form fields available in Django.
Each of these fields also takes a number of extra arguments, such as the max_length
, required
, and widget
arguments we just discussed. Different form fields may take different arguments, but there are some core arguments that are available to all fields.
-
required
: by default, each field assumes the value is required. If the user pass an empty value, aValidationError
will raise. If you want to accept an empty value, setrequired=False
. -
label
: allows you to define a custom label for the field.
<label for="id_title">Custom Label:</label>
-
label_suffix
: by default, the label suffix is a colon (:
), but you can overwrite it by settinglabel_suffix
to a different value. -
initial
: sets the initial value for the field when rendering the form. This is especially useful when you are creating a form for updating existing resources. We will discuss more about this later. -
widget
: specifies the corresponding widget for the field. -
help_text
: include a help text when rendering the form.
<span class="helptext">100 characters max.</span>
-
error_messages
: The default error message isThis field is required.
, and this argument allows you to overwrite it. -
validators
: allows you to specify a custom validate method. -
localize
: enables the localization of form data input. -
disabled
: if set toTrue
, the field will be rendered with adisabled
attribute.
To render the form we just defined, create a view function.
from django.shortcuts import render
from .forms import PostForm
def create(request):
if request.method == "GET":
return render(request, "post/create.html", {"form": PostForm})
And in the create.html
template, print this form like a regular variable.
{% extends 'layout.html' %}
{% block title %}
<title>Create</title>
{% endblock %}
{% block content %}
<div class="w-96 mx-auto my-8">
<h2 class="text-2xl font-semibold underline mb-4">Create new post</h2>
<form action="{% url 'create' %}" method="POST">
{% csrf_token %}
{{ form }}
<button type="submit" class=". . .">Submit</button>
</form>
</div>
{% endblock %}
The form will be outputted like this:
<form action=". . ." method="POST">
<label for="id_title">Title:</label>
<input type="text" name="title" maxlength="255" required="" id="id_title">
<label for="id_content">Content:</label>
<textarea name="content" cols="40" rows="10" required="" id="id_content"></textarea>
<label for="id_is_published">Is published:</label>
<input type="checkbox" name="is_published" id="id_is_published">
<button type="submit" class=". . .">Submit</button>
</form>
Styling your Django form
As you can see, the form does not look ideal. To improve that, you must edit the widget so that the rendered HTML input element would include class names.
from django import forms
class PostForm(forms.Form):
title = forms.CharField(
max_length=255,
widget=forms.TextInput(
attrs={
"class": "mb-4 p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300"
}
),
)
content = forms.CharField(
widget=forms.Textarea(
attrs={
"class": ". . ."
}
),
)
is_published = forms.BooleanField(
required=False,
widget=forms.CheckboxInput(
attrs={
"class": ". . ."
}
),
)
Notice that we added an attrs
key, which stands for attributes. The specified attributes will be rendered as a part of the HTML input element. Of course, it does not have to be class
, you can also add id
, size
, or something else, depending on the widget you are using.
The revised form should give an improved look:
<form action="/create/" method="POST" enctype="multipart/form-data">
<input type="hidden" name="csrfmiddlewaretoken" value=". . .">
<label for="id_title">Title:</label>
<input type="text" name="title"
class="mb-4 p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300"
maxlength="255" required="" id="id_title">
<label for="id_content">Content:</label>
<textarea name="content" cols="40" rows="10" class=". . ." required="" id="id_content"></textarea>
<label for="id_is_published">Is published:</label>
<input type="checkbox" name="is_published" class="mb-4" id="id_is_published">
<button type="submit" class=". . .">Submit</button>
</form>
Form submission
Of course, just rendering the form is not enough. You also need to deal with the user input and handle form submissions. To accomplish this, make sure the form has method=POST
, then go back to the view method, and create a condition where a POST
request is received.
def create(request):
if request.method == "GET":
return render(request, "post/create.html", {"form": PostForm})
elif request.method == "POST":
form = PostForm(request.POST)
if form.is_valid():
post = Post(
title=form.cleaned_data["title"],
content=form.cleaned_data["content"],
is_published=form.cleaned_data["is_published"],
)
post.save()
return redirect("list")
The form.is_valid()
method will validate the form inputs, making sure they match the requirements. Remember this step is necessary, or you will not be able to retrieve the inputs using form.cleaned_data[. . .]
. And next, post=Post(. . .)
creates a new instance of Post
, and post.save()
saves it to the database.
Dealing with relations
In a real-life application, it is very common for one model to have relations with other models. For example, our Post
could belong to a User
, and have multiple Tag
s attached.
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class Tag(models.Model):
name = models.CharField(max_length=255)
def __str__(self):
return self.name
class Post(models.Model):
title = models.CharField(max_length=255)
content = models.TextField()
is_published = models.BooleanField(default=False)
tags = models.ManyToManyField(Tag)
user = models.ForeignKey(User, on_delete=models.CASCADE, blank=True)
def __str__(self):
return self.title
How can we adjust the form so that it allows us to define relations? Django offers two form fields, ModelChoiceField
and ModelMultipleChoiceField
for this purpose. They are variants of the ChoiceField
and the MultipleChoiceField
. The first one has the default widget Select
, which creates a single selection field.
single_select = forms.ChoiceField(
choices=[
("FR", "Freshman"),
("SO", "Sophomore"),
("JR", "Junior"),
("SR", "Senior"),
("GR", "Graduate"),
],
widget=forms.Select(
attrs={. . .}
),
)
<label for="id_single_select">Single select:</label>
<select name="single_select" class=". . ." id="id_single_select">
<option value="FR">Freshman</option>
<option value="SO">Sophomore</option>
<option value="JR">Junior</option>
<option value="SR">Senior</option>
<option value="GR">Graduate</option>
</select>
The latter has the default widget SelectMultiple
, which creates a multi-select field.
multi_select = forms.MultipleChoiceField(
choices=[
("FR", "Freshman"),
("SO", "Sophomore"),
("JR", "Junior"),
("SR", "Senior"),
("GR", "Graduate"),
],
widget=forms.SelectMultiple(
attrs={. . .}
),
)
<label for="id_multi_select">Multi select:</label>
<select name="multi_select" class=". . ." required="" id="id_multi_select" multiple="">
<option value="FR">Freshman</option>
<option value="SO">Sophomore</option>
<option value="JR">Junior</option>
<option value="SR">Senior</option>
<option value="GR">Graduate</option>
</select>
The ModelChoiceField
and ModelMultipleChoiceField
are based on these choice fields, but instead of defining a choices
argument, they can directly pull available choices from the database through models by specifying a queryset
argument.
user = forms.ModelChoiceField(
queryset=User.objects.all(),
widget=forms.Select(
attrs={
"class": "mb-4 p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300"
}
),
)
tags = forms.ModelMultipleChoiceField(
queryset=Tag.objects.all(),
widget=forms.SelectMultiple(
attrs={
"class": "mb-4 p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300"
}
),
)
Don't forget to edit the view function so that the relations can be saved.
def create(request):
if request.method == "GET":
return render(request, "post/create.html", {"form": PostForm})
elif request.method == "POST":
form = PostForm(request.POST)
if form.is_valid():
post = Post(
title=form.cleaned_data["title"],
content=form.cleaned_data["content"],
is_published=form.cleaned_data["is_published"],
)
post.save()
user = form.cleaned_data["user"]
user.post_set.add(post)
post.tags.set(form.cleaned_data["tags"])
return redirect("list")
Uploading files using Django form
Sometimes when publishing a post, you might want to include a cover image to attract more audience. Django also provides an ImageField
that enables you to upload images to your server.
image = forms.ImageField(
widget=forms.ClearableFileInput(
attrs={. . .}
),
)
Of course, you cannot save the image in the database. The image is stored on your server, and the path that points to the image will be saved to the database. To accomplish this, it is best to create a helper function.
import uuid
def upload_file(f):
path = "uploads/images/" + str(uuid.uuid4()) + ".png"
with open(path, "wb+") as destination:
for chunk in f.chunks():
destination.write(chunk)
return path
This function takes a file (f
) as the input. It generates a random name for the file, saves it under the directory uploads/images/
, and returns the file path as the output. You should make sure the directory exists, or the function will give an error.
Next, edit the view function like this:
def create(request):
if request.method == "GET":
return render(request, "post/create.html", {"form": PostForm})
elif request.method == "POST":
form = PostForm(request.POST, request.FILES)
if form.is_valid():
path = upload_file(request.FILES["image"])
post = Post(
title=form.cleaned_data["title"],
content=form.cleaned_data["content"],
is_published=form.cleaned_data["is_published"],
image=path,
)
post.save()
user = form.cleaned_data["user"]
user.post_set.add(post)
post.tags.set(form.cleaned_data["tags"])
return redirect("list")
Line 5, files are transferred separately from the POST
body, so here you must also include request.FILES
.
Line 7, use the helper function to upload the file, the file path should be saved to the variable path
.
Line 12, save the path to the database.
Lastly, you also need to ensure your form has enctype="multipart/form-data"
, or uploading files will not be allowed.
<form action="{% url 'create' %}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
<button type="submit" class=". . .">Submit</button>
</form>
Updating existing resources using Django form
So far, we've only been talking about how to create new resources using Django forms, but what if you need to update existing resources? There are two major problems we need to tackle in order to achieve this. First of all, when displaying the form, we must include data from the old resource by setting the initial
argument.
def update(request, id):
post = Post.objects.get(pk=id)
if request.method == "GET":
return render(
request,
"post/update.html",
{
"form": PostForm(
initial={
"title": post.title,
"content": post.content,
"image": post.image,
"is_published": post.is_published,
"user": post.user,
"tags": post.tags.all,
}
),
"post": post,
},
)
Post.objects.get(pk=id)
retrieves the requested post based on its id
.
The outputted form should look like this:
The second problem with this form is that sometimes you don't need to update the image, but if you don't, Django will return a validation error. As we have mentioned before, Django assumes all form fields are required.
<ul class="errorlist"><li>image<ul class="errorlist"><li>This field is required.</li></ul></li></ul>
So you'll have to set required=False
for the image field.
is_published = forms.BooleanField(
required=False,
widget=forms.CheckboxInput(attrs={. . .}),
)
And then add the condition request.method == "POST"
for the view function.
def update(request, id):
post = Post.objects.get(pk=id)
if request.method == "GET":
return render(
request,
"post/update.html",
{
"form": PostForm(
initial={
"title": post.title,
"content": post.content,
"image": post.image,
"is_published": post.is_published,
"user": post.user,
"tags": post.tags.all,
}
),
"post": post,
},
)
elif request.method == "POST":
form = PostForm(request.POST, request.FILES)
if form.is_valid():
path = (
upload_file(request.FILES["image"])
if "image" in request.FILES
else post.image
)
Post.objects.update_or_create(
pk=id,
defaults={
"title": form.cleaned_data["title"],
"content": form.cleaned_data["content"],
"is_published": form.cleaned_data["is_published"],
"image": path,
},
)
user = form.cleaned_data["user"]
user.post_set.add(post)
post.tags.set(form.cleaned_data["tags"])
return redirect("list")
Line 24 to 28, since image
might be None
in this case, you have to account for this condition. If image
is in request.FILES
, the image is uploaded, and the file path is stored in the variable path
. If image
is not in request.FILES
, path
is set to the original file path.
ModelForm
, a shortcut
As we've mentioned at the beginning of this article, the whole point of using Django forms is to simplify the form-building process. But as you can see, the create()
and update()
views demonstrated in this tutorial are not simple at all, even though we only have a very basic form.
Luckily, Django offers a shortcut, ModelForm
, which allows you to create forms directly from models, and when saving the form, all you need to do is form.save()
, without having to retrieve the user inputs one by one, and match them with each model field. And the best part is, it also works for relations and file uploads. Let's take a look at this example:
class PostModelForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "content", "image", "is_published", "tags", "user"]
Instead of setting up each field, you only need to tell Django the corresponding model, as well as the model fields you wish to be included in the form.
The view functions are a lot simpler too:
def create(request):
if request.method == "GET":
return render(request, "post/create.html", {"form": PostModelForm})
elif request.method == "POST":
form = PostModelForm(request.POST, request.FILES)
if form.is_valid():
form.save()
return redirect("list")
Line 7, this is all you need to do to save the form input to the database.
def update(request, id):
post = Post.objects.get(pk=id)
if request.method == "GET":
return render(
request,
"post/update.html",
{
"form": PostModelForm(instance=post),
"post": post,
},
)
elif request.method == "POST":
form = PostModelForm(request.POST, request.FILES, instance=post)
if form.is_valid():
form.save()
return redirect("list")
Line 8 and 13, pass the existing resource to the form.
In this article, we went over the basics of building web forms using Django's Form
and ModelForm
classes, which should significantly simplify your form-building process. If you liked this article, please also take a look at my other tutorials on Django.
Top comments (0)