Django Forms

Posted by Daksh on Tuesday, August 9, 2022

What are forms?

Forms are a way to collect user input. They are used to validate data, display errors, and save data to a database. Django provides a form library that handles all of this for you. Admin is meant for developers, forms are meant for users. Forms are exposed to website visitors, and are used to collect data from them.

Let’s start by building a simple form. We’ll create a form that asks for a user’s name and email address.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Your Review</title>
</head>
<body>
<form action="">
    <label for="user-name">Your Name</label>
    <input id="user-name" name="username" type="text">
    <button type="submit">Send</button>
</form>
</body>
</html>

When the button with type=“submit: is clicked, the page refreshes and the URL changes to include the form data. For example, if the user enters “Daksh” in the name field and clicks the button, the URL will change to http://localhost:8000/?username=Daksh. The form data is sent to the server as a query string, if the request method is POST.

If the button is of type=“button”, the page will not refresh. The form data will not be sent to the server. By default the button is of type=“submit”.

The request object is GET by default.

To send POST request, add method=“POST” to the form tag.

<form action="" method="POST">

To bypass the CSRF(Cross-Site Request Forgery) check, add {% csrf_token %} to the form.

<form action="" method="POST">
    {% csrf_token %}
</form>

View function gets triggered for both GET and POST requests. Inside the form, we can use action="/some/url/" to send the form data(request) to “/some/url/”.

Inside the view function, we can use request.POST to get the form data. request.POST is a dictionary-like object that lets you access submitted data by key name. In our example, we can access the user’s name with request.POST['username']. The key is the name attribute of the input field.

Generally, we don’t want to return html on a POST request, therefore although we can use the below code, but it’s not a good practice.

def review(request):
    if request.method == "POST":
        entered_username = request.POST['username']
        return render(request, "reviews/thank-you.html")
    elif request.method == "GET":
        return render(request, "reviews/review.html")

Instead we can use HttpResponseRedirect to redirect the user to a different page. This redirection is a GET request. This also helps to avoid the resubmission of the form data on page refresh.

from django.shortcuts import render
from django.http import HttpResponseRedirect

def review(request):
    if request.method == "POST":
        entered_username = request.POST['username']
        print(entered_username)
        return HttpResponseRedirect("/thank-you")

    elif request.method == "GET":
        return render(request, "reviews/review.html")


def thank_you(request):
    return render(request, "reviews/thank-you.html")

Form Class

Inside the app directory, create a file named forms.py. Inside the file, create a class named ReviewForm that inherits from forms.Form. This class will be used to create a form that has required input fields and validation checks. By convention, the name of the form class should be the required name with the word “Form” appended to the end. The fieds of the form are very similar to the fields of the model.

from django import forms

class ReviewForm(forms.Form):
    user_name = forms.CharField(label="Your Name", max_length=100)

# inside the view function
# instantiate the form class, without passing any data for GET request
# and with the POST data for POST request
def review(request):
    if request.method == 'POST':
        form = ReviewForm(request.POST)

        # validity check
        if form.is_valid():
            # it will validate all the input fields
            print(form.changed_data)  # prints out cleaned and validate data
            return HttpResponseRedirect("/thank-you")

    else:
        # if request is GET
        # render blank form
        form = ReviewForm()

    # if validation fails, it still saves user data, and give error message for invalid fields
    # this way user has to enter invalid fields only, and all other fields are prepopulated
    return render(request, "reviews/review.html", {
        "form": form
    })

And Inside the template, we can use the form variable to render the form.

<form action="/" method="POST">
    {% csrf_token %}
    {{ form }}
    <button>Send</button>
</form>

This automatically renders the form fields. The form variable is an instance of the ReviewForm class. It has a __str__ method that returns the HTML for the form. The form variable also has a __iter__ method that returns an iterator over the form fields. This is how the {{ form }} template tag works.

Form validation

Form validation should ideally occur on both the client side (using JavaScript) and the server side (using the appropriate server-side framework or library, such as Django).

Client-side validation is useful because it can provide instant feedback to the user without the need for a round-trip to the server. For example, checking whether a required field is empty or whether a field is of the correct format (e.g. email address or phone number) can be done using JavaScript. This can improve the user experience and make the form more user-friendly.

However, client-side validation alone is not sufficient for security reasons, as it can be bypassed by malicious users or by users who have disabled JavaScript in their browser. Therefore, server-side validation is essential to ensure that the submitted data is valid and safe to process. Server-side validation can also perform more complex checks and can interact with databases or other resources to validate the data.

In summary, while client-side validation can improve user experience, server-side validation is necessary for security and to ensure that the data is valid and safe to process.

Customizing the Form

Refer to Django Form Fields for more information.

class ReviewForm(forms.Form):
    user_name = forms.CharField(label="Enter your name", max_length=100, required=False, error_messages={
        "required": "Your name must not be empty",
        "max_length": "Please enter a shorter name!"
    })
    # to display a text area instead of a single line input field use widget=forms.Textarea
    review_text = forms.CharField(label="Your Feedback", widget=forms.Textarea, max_length=200)
    rating = forms.IntegerField(label="Your Rating", max_value=5, min_value=1)

Customizing Form’s appearance, manually rendering specific form elements. One of the key advantages of manually rendering the form is that we can add custom styling to the form by wrapping a div around the form field.

<form action="/" method="POST">
    {% csrf_token %}
    {# div with a conditional styling class - "errors" #}
    <div class="form-control {% if form.user_name.errors %}errors{% endif %}">
    {{ form.user_name.label_tag }}
    {{ form.user_name }}
    {{ form.user_name.errors }}
    </div>
    <button>Send</button>
</form>

We can also use a for loop to render the form fields.

<form action="/" method="POST">
    {% csrf_token %}
    {% for field in form %}
    <div class="form-control {% if field.errors %}errors{% endif %}">
        {{ field.label_tag }}
        {{ field }}
        {{ field.errors }}
    </div>
    {% endfor %}
    <button>Send</button>
</form>

Django sends back all the errors in a list. We can use {{ form.user_name.errors }} to display the errors. We can also use {{ form.user_name.errors.as_ul }} to display the errors as a list. Inside the sent html, there is a class named errorlist that we can use to style the error list.

Saving the Form Data

Forms are used to fetch data from the user and models are used to store data in the database. We can use the form data to create a new model instance and save it to the database. Note that the data fetched from the form is already cleaned and validated. We can use the cleaned_data attribute of the form to access the data.

# inside the model.py
class Review(models.Model):
    user_name = models.CharField(max_length=100)
    review_text = models.TextField()
    rating = models.IntegerField()

# inside the view.py
review = Review(user_name=form.changed_data['user_name'],
                review_text=form.changed_data['review_text'],
                rating=form.changed_data['rating'])

review.save()

There is another method to do the exact same thing using something called ModelForm. ModelForm is a special type of form that is used to create a form from a model. It automatically creates the form fields for the model’s fields. It also automatically validates the data and saves the data to the database. We can use the ModelForm class to create a form for the Review model.

# inside the forms.py
from django import forms
from .models import Review

class ReviewForm(forms.ModelForm):
    class Meta:
        # Do not instantiate it, just point to the model
        model = Review
        # fields to be included in the form
        # fields = '__all__'  # all fields
        fields = ['user_name', 'review_text', 'rating']
        # exclude = ['user_name']  # all fields except user_name
        # labels = { "key_from_model": "label_for_form" }
        # error_messages = { "key_from_model": { "error_type": "error_message" } }

# inside the view.py
# method 1
form = ReviewForm(request.POST)
if form.is_valid():
    review = Review(user_name=form.cleaned_data['user_name'],
                    review_text=form.cleaned_data['review_text'],
                    rating=form.cleaned_data['rating'])
    review.save()
    return HttpResponseRedirect("/thank-you")

# method 2
form = ReviewForm(request.POST)
if form.is_valid():
    form.save()
    return HttpResponseRedirect("/thank-you")

# if you want to modify existing data
existing_review = Review.objects.get(pk=1)
form = ReviewForm(request.POST, instance=existing_review)
if form.is_valid():
    form.save()
    return HttpResponseRedirect("/thank-you")

Class Based Views

# method 1
# inside the views.py
from django.views import View

class ReviewView(View):
    def get(self, request):
        form = ReviewForm()
        return render(request, "reviews/review.html", {"form": form})

    def post(self, request):
        form = ReviewForm(request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect("/thank-you")
        return render(request, "reviews/review.html", {"form": form})


# method 2
# inside the views.py
from django.views.generic import FormView

class ReviewView(FormView):
    template_name = "reviews/review.html"
    form_class = ReviewForm
    success_url = "/thank-you"

    def form_valid(self, form):
        form.save()
        return super().form_valid(form)

# inside the urls.py
from .views import ReviewView
urlpatterns = [
    path("", ReviewView.as_view(), name="review"),
]

Important Form Tags

  • {{ form.as_p }} - renders the form as a paragraph
  • {{ form.as_table }} - renders the form as a table
  • {{ form.as_ul }} - renders the form as a list
  • {{ form.non_field_errors }} - renders the non field errors
  • {{ form.non_field_errors.as_ul }} - renders the non field errors as a list
  • {{ form.non_field_errors.as_text }} - renders the non field errors as text
  • {{ form.non_field_errors.as_json }} - renders the non field errors as json
  • {{ form.as_div }} - renders the form as a div