Working with Django Forms
Tags: django, python, webdev, tutorial
Published: 2023-07-25 19:07:39

> [Download source code here](https://www.ericsdevblog.com/posts/working-with-django-forms/) ⬅️

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](https://www.ericsdevblog.com/posts/django-for-beginners-2/) 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:

```python
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:

```python
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](https://docs.djangoproject.com/en/4.2/ref/forms/fields/#built-in-field-classes) 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](https://docs.djangoproject.com/en/4.2/ref/forms/fields/#core-field-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.

  ```html
  <label for="id_title">Custom Label:</label>
  ```

- `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](https://docs.djangoproject.com/en/4.2/ref/forms/widgets/) for the field.
- `help_text`: include a help text when rendering the form.

  ```html
  <span class="helptext">100 characters max.</span>
  ```

- `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](https://docs.djangoproject.com/en/4.2/ref/validators/).
- `localize`: enables the [localization](https://docs.djangoproject.com/en/4.2/topics/i18n/formatting/) 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.

```python
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.

```html
{% 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:

![Django forms](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ko42x79a477xru5vhl2d.png)

```html
<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.

```python
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 with style](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/of8z48rd7kax3jqhp8ut.png)

```html
<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.

```python
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.

![create new post](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dd5ofge8a8tcrqb2nr2o.png)

![new post admin panel](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/la4ghv1j9wbqy6437any.png)

## 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.

```python
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.

```python
single_select = forms.ChoiceField(
    choices=[
        ("FR", "Freshman"),
        ("SO", "Sophomore"),
        ("JR", "Junior"),
        ("SR", "Senior"),
        ("GR", "Graduate"),
    ],
    widget=forms.Select(
        attrs={. . .}
    ),
)
```

![single select](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d2c36yt2qhrte81a002b.png)

```html
<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.

```python
multi_select = forms.MultipleChoiceField(
    choices=[
        ("FR", "Freshman"),
        ("SO", "Sophomore"),
        ("JR", "Junior"),
        ("SR", "Senior"),
        ("GR", "Graduate"),
    ],
    widget=forms.SelectMultiple(
        attrs={. . .}
    ),
)
```

![multi select](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m5b2q5tli5c3ngf7j1sw.png)

```html
<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.

```python
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"
        }
    ),
)
```

![relation fields](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/74fz846wf543g0aig740.png)

Don't forget to edit the view function so that the relations can be saved.

```python
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.

```python
image = forms.ImageField(
    widget=forms.ClearableFileInput(
        attrs={. . .}
    ),
)
```

![image field](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rj8xv11p6wln66u3iuu3.png)

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.

```python
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:

```python
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.

```html
<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.

```python
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:

![update form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/27c5caqatkxkg5gtbga3.png)

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.

```html
<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.

```python
is_published = forms.BooleanField(
    required=False,
    widget=forms.CheckboxInput(attrs={. . .}),
)
```

And then add the condition `request.method == "POST"` for the view function.

```python
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:

```python
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:

```python
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.

```python
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.

- [Create a Modern Application with Django and Vue](https://www.ericsdevblog.com/posts/create-a-modern-application-with-django-and-vue-1/)
- [How to Deploy a Django Project](https://www.ericsdevblog.com/posts/how-to-deploy-a-django-project/)
- [How to Send Emails with Django](https://www.ericsdevblog.com/posts/how-to-send-emails-with-django/)