DEV Community

Cover image for Working with Django Forms
Eric Hu
Eric Hu

Posted on • Originally published at ericsdevblog.com

Working with Django Forms

Download source code here ⬅️

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
Enter fullscreen mode Exit fullscreen mode

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,
    )
Enter fullscreen mode Exit fullscreen mode

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, a ValidationError will raise. If you want to accept an empty value, set required=False.
  • label: allows you to define a custom label for the field.
  <label for="id_title">Custom Label:</label>
Enter fullscreen mode Exit fullscreen mode
  • label_suffix: by default, the label suffix is a colon (:), but you can overwrite it by setting label_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>
Enter fullscreen mode Exit fullscreen mode
  • error_messages: The default error message is This 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 to True, the field will be rendered with a disabled 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})
Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode

The form will be outputted like this:

Django forms

<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>
Enter fullscreen mode Exit fullscreen mode

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": ". . ."
            }
        ),
    )
Enter fullscreen mode Exit fullscreen mode

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 with style

<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>
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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.

create new post

new post admin panel

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 Tags 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
Enter fullscreen mode Exit fullscreen mode

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={. . .}
    ),
)
Enter fullscreen mode Exit fullscreen mode

single select

<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>
Enter fullscreen mode Exit fullscreen mode

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={. . .}
    ),
)
Enter fullscreen mode Exit fullscreen mode

multi select

<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>
Enter fullscreen mode Exit fullscreen mode

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"
        }
    ),
)
Enter fullscreen mode Exit fullscreen mode

relation fields

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")
Enter fullscreen mode Exit fullscreen mode

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={. . .}
    ),
)
Enter fullscreen mode Exit fullscreen mode

image field

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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,
            },
        )
Enter fullscreen mode Exit fullscreen mode

Post.objects.get(pk=id) retrieves the requested post based on its id.

The outputted form should look like this:

update form

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>
Enter fullscreen mode Exit fullscreen mode

So you'll have to set required=False for the image field.

is_published = forms.BooleanField(
    required=False,
    widget=forms.CheckboxInput(attrs={. . .}),
)
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)