Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 28

Handling Form Submissions and Validation in CBVs

Recommended resources

Hello! Welcome to your next lesson on the Django framework.

In our last session, we saw how CreateView and UpdateView dramatically simplify our code by handling the entire GET/POST cycle for creating and editing model objects. We configured them with attributes like model and form_class, and they automatically saved the data and redirected on success.

But what happens when the default behavior isn't quite enough? For example, how do you associate a new blog post with the currently logged-in user who created it? The view needs to intervene in the process.

Today's lesson focuses on exactly that. Our goal is to process form submissions and handle validation within Class-Based Views (CBVs). You'll learn how to hook into the form processing lifecycle to execute custom logic, which is a crucial skill for building real-world applications. Given your background in object-oriented programming, you'll find the method overriding approach here quite natural.


The Form Processing Lifecycle in a CBV

When you submit a form to a CreateView or UpdateView, a POST request is sent to the server. The view doesn't just blindly save the data. It follows a precise, predictable sequence of steps. Understanding this sequence is key to customizing it.

The process is managed by methods inherited from Django's ProcessFormView class, which is an ancestor of CreateView and UpdateView.

To see the core logic, let's explore a resource that digs into the source code.

Digging up Django class-based views - 3

This article, 'Digging up Django class-based views - 3' by The Digital Cat Online, provides an excellent, clear look under the hood. It will show you the exact post method that orchestrates form handling.

Please read the section titled 'Processing GET and POST requests'. Focus on the code block showing the post method of ProcessFormView. This is the heart of today's lesson.

As you saw in the article, the post method's logic can be summarized as:

# Simplified logic within a CBV's post method
form = self.get_form()  # Instantiates the form with request.POST data
if form.is_valid():
    return self.form_valid(form)
else:
    return self.form_invalid(form)

This reveals two critical "hook" methods:

  1. form_valid(form): This method is called only when the submitted data passes all validation rules. Its default job is to save the model instance and redirect to the success_url. This is where we will add our custom logic.
  2. form_invalid(form): This method is called when the data fails validation. Its default job is to re-render the template, passing back the form object which now contains the validation errors to be displayed to the user.

This flow is beautifully illustrated in the following diagram.

Django CreateView Form Submission Flow
This diagram shows the data flow for a `CreateView`. Note the central role of the `form_valid` method, which is executed after successful validation and before the database record is created and the user is redirected.


Handling Successful Submissions with form_valid()

The most common reason to customize a form-handling view is to perform an action after the form is successfully validated but before the final redirect. The form_valid() method is the perfect place for this.

A classic example is assigning the currently logged-in user to a created_by field on a new object. CreateView doesn't know about your specific user relationships, so you have to tell it what to do.

The official Django documentation provides a clear, concise example of this.

Form handling with class-based views

The official Django documentation on 'Form handling with class-based views' provides the canonical example for this task. It shows how to add a created_by field to a model and then populate it in the view.

Read the section titled 'Models and request.user'. It's short and directly to the point. Pay close attention to how the form_valid() method is overridden.

Let's break down the implementation for our portfolio project.

Step 1: Update the Model
First, we'd add a ForeignKey to the User model in our Project model (don't forget to import User).

# portfolio/models.py
from django.db import models
from django.contrib.auth.models import User

class Project(models.Model):
    # ... other fields
    author = models.ForeignKey(User, on_delete=models.CASCADE)

After changing the model, you would need to run makemigrations and migrate.

Step 2: Override form_valid in the View
Now, we override the form_valid method in our ProjectCreateView.

# portfolio/views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import CreateView
from .models import Project

class ProjectCreateView(LoginRequiredMixin, CreateView):
    model = Project
    fields = ['title', 'description', 'technology'] # 'author' is excluded!
    template_name = 'portfolio/project_form.html'
    # success_url or get_absolute_url() on model...

    def form_valid(self, form):
        # Assign the currently logged-in user to the author field
        form.instance.author = self.request.user
        # Now, let the parent class do its work: save the object and redirect
        return super().form_valid(form)

Let's analyze this:

  • LoginRequiredMixin: We add this mixin to ensure only logged-in users can access this view.
  • fields: Notice that 'author' is not in the list of fields. We don't want the user to choose the author from a dropdown; we want to set it programmatically.
  • def form_valid(self, form):: We define our own version of the method.
  • form.instance.author = self.request.user: This is the core of our custom logic. The form object passed to form_valid has an instance attribute, which is the Project object about to be saved. We can modify it directly. The current user is available via self.request.user.
  • return super().form_valid(form): This is extremely important. After we're done with our custom logic, we call the form_valid method from the parent class (CreateView). This parent method contains the logic that actually saves the model (form.save()) and returns the HttpResponseRedirect. If you forget this line, the form will not be saved!

To see this exact pattern in a live-coding context, the following video is very helpful.

Django Class Based Views Tutorial

This video from JustDjango demonstrates converting a function-based view to a CreateView and overriding form_valid to set the author. It's a great practical walkthrough of the concept we just discussed.

Watch from 21:15 to 24:20. The instructor implements a PostCreateView and overrides form_valid to set the post's author, which is identical to the user-project association we're discussing.



Handling Validation Errors with form_invalid()

What happens when form.is_valid() returns False? The form_invalid() method is called.

By default, form_invalid() simply re-renders the template (self.render_to_response()) with the same context, which includes the form object. However, this time the form object is "bound" with the submitted data and also contains a dictionary of errors. The template can then access these errors and display them next to the corresponding fields.

You've already built a ModelForm, so you know that Django automatically provides validation based on your model's field types (e.g., CharField with max_length, IntegerField, etc.). Let's see how FormView (the parent of CreateView) handles these errors automatically.

Learn Django Class Based Views - FormView - Theory and Examples

This video from Very Academy provides a clear demonstration of Django's default validation error handling within a form view.

Watch from 07:00 to 08:46. The presenter intentionally submits invalid data (a string into an integer field) and shows how the view automatically redisplays the form with a validation error message, without any custom code.

This default behavior is usually exactly what you want. While you can override form_invalid() to add custom logging or flash a special message, it's far less common than overriding form_valid(). The key takeaway is that Django's CBVs handle the validation failure case for you out of the box.

Test your understanding!

You have a ProjectCreateView. A user tries to submit the form but leaves the title field blank, which is a required field in your Project model. Which method in your view gets called, and what is its default action?

Show answer

The form_invalid(form) method gets called because the form fails validation (title is required). Its default action is to re-render the same template, passing the form context variable back. The template can then display the error message associated with the title field.

The same principles for overriding form_valid() apply directly to UpdateView as well. You might override it to set a last_modified_by field or to create a log entry every time an object is changed. The pattern remains the same: modify form.instance and then call super().form_valid(form).



Conclusion

You've now unlocked the ability to customize the form processing logic within Django's powerful Class-Based Views. This moves you beyond basic configuration and into building more complex, real-world application features.

Key Takeaways:

  • CBVs like CreateView and UpdateView use a post method that orchestrates form validation.
  • If a form is valid, the form_valid() method is called. This is the primary hook for adding custom logic.
  • A common pattern in form_valid() is to modify the unsaved model instance (form.instance) before calling the parent method with super().form_valid(form) to complete the save and redirect operations.
  • If a form is invalid, the form_invalid() method is called, which by default redisplays the form with error messages.

In our next lesson, we will build upon this foundation to tackle another common requirement: handling file uploads using forms and models. You'll see that the form_valid() method is, once again, a valuable place to perform custom actions.