Skip to main content

Django Class-based Views

Django Class Views Banner

Learning Objectives

Students Will Be Able To:
Use Class-based Views (CBVs) in place of View Functions
Use a CBV to Create data
Use a CBV to Delete data
Use a CBV to Update data
Use a ModelForm to Render a Form's Inputs

Road Map

  1. Setup
  2. Practice Exercise
  3. What are Class-based Views (CBVs)?
  4. Why use CBVs?
  5. Creating Data Using a CBV
  6. Updating & Deleting Data Using a CBV
  7. Essential Questions
  8. Challenge Exercises
  9. Further Study

1. Setup

This lesson continues to build-out Cat Collector beginning from where the Intro to Django Models lesson left off. If your code is complete and working, you don't need to sync, otherwise: git fetch --all then git reset --hard origin/main

There are three <a> tags within the nav in base.html that have their href's hardcoded:

<ul>
<li><a href="/" class="left brand-logo">&nbsp;&nbsp;Cat Collector</a></li>
</ul>
<ul class="right">
<li><a href="/about">About</a></li>
<li><a href="/cats">View All My Cats</a></li>
</ul>

Update the href's to use the url template tag instead of hard-coding the URL in the links.

3. What are Class-based Views?

First, what's a Django View?

Class-based Views are classes defined in the Django framework that we can extend and use instead of view functions. For example, let's say we wanted to implement the index (view all) functionality for a Books data resource:

# views.py
from django.views.generic import ListView

class BookList(ListView):
model = Book

ListView is a built-in generic CBV designed to implement the "index" (list) functionality for a particular Model.

We extended the generic ListView to create a BookList and used

    model = Book

to inform BookList that it is to access the Book Model.

Then, in urls.py, we invoke the as_view() class method of the BookList CBV (which returns a view function) and connect it to a route as usual:

# urls.py
from django.urls import path
from books.views import BookList

urlpatterns = [
path('books/', BookList.as_view(), name='books_index'),
]

Notice that there's not even a call to render? By default, the CBV will expect to render a template named templates/\<name of app>/book_list.html. This convention can of course be overridden.

In addition to the ListView used to display the index page for a Model, there are also:

  • DetailView - used to implement the "detail" page for an instance of a Model
  • CreateView - used to create an instance of a Model
  • DeleteView - used to delete an instance of a Model
  • UpdateView - used to update an instance of a Model

In this lesson, we will be extending the CreateView, UpdateView and DeleteView CBVs to CUD cats!

We also could easily replace the existing cats_index and cats_detail view functions with CBVs, however, we won't as a reminder of how they are structured.

4. Why use Class-based Views?

Much of the code we write in CRUD applications repeats certain patterns again and again.

As you saw in the above example, we can leverage CBVs to avoid having to write the same repeating code over and over.

CBVs can save time thus making us more productive developers.

CBVs are highly configurable by adding class attributes, overriding methods and using decorators (see the Further Study section).

For example, we set the template_name attribute to render a template other than the default:

class BookList(ListView):
model = Book
template_name = 'books/index.html'

5. Creating Data Using a CBV

We need a way to create cats in Cat Collector!

Add the Route

If this was an Express app, we would need to code two separate routes and controller actions like:

  • router.get('/cats/new', catsCtrl.new); to view the form, and
  • router.post('/cats', catsCtrl.create); to add the cat to the database and then redirect

However, using Django, we only need a single URL-based route because a CreateView CBV will automatically:

  • Create a Django ModelForm used to automatically create the form's inputs for the Model.
  • If the request is a GET, render a template (where we'll include a <form>) and
  • In the case of a POST, use the posted form's contents to automatically and transparently create data and perform a redirect.

Let's add the new URL pattern to main_app/urls.py

urlpatterns = [
path('', views.home, name='home'),
path('about/', views.about, name='about'),
path('cats/', views.cats_index, name='index'),
path('cats/<int:cat_id>/', views.cats_detail, name='detail'),
# new route used to show a form and create a cat
path('cats/create/', views.CatCreate.as_view(), name='cats_create'),
]

The path() function still needs a view function as its second argument, not a class, and that's what a CBV's as_view() method returns.

Did you notice that we didn't have to put that route above the 'cats/<int:cat_id>/'? Unlike Express, Django won't match that route unless there's something that looks like an integer in the second segment, thus it ignores cats/create/

We'll need to add the views.CatCreate CBV to make the server happy, but first let's add a link to the nav for adding a cat...

Update the UI

Now that we know the path that's used to BOTH:

  • View a form for entering cat info; and
  • Create the cat when the form is submitted

let's update base.html to add a link to the nav:

<li><a href="{% url 'about' %}">About</a></li>
<!-- new link below -->
<li><a href="{% url 'cats_create' %}">Add a Cat</a></li>
<li><a href="{% url 'index' %}">View All My Cats</a></li>

On to the view!

Extending the Generic CreateView

First, to use anything in Django, it needs to be imported:

# views.py

from django.shortcuts import render
# Add the following import
from django.views.generic.edit import CreateView
from .models import Cat

Now we can inherit from CreateView to create our own CBV used to create cats:

class CatCreate(CreateView):
model = Cat
fields = '__all__'

The fields attribute is required for a CreateView and can be used to limit or change the ordering of the attributes from the Cat model are generated in the ModelForm passed to the template.

We've taken advantage of the special '__all__' value to specify that the form should contain all of the Cat Model's attributes. Alternatively, we could have listed the fields in a list like this:

class CatCreate(CreateView):
model = Cat
fields = ['name', 'breed', 'description', 'age']

That's all of the code we need to write to display a template with the form that's automatically passed to it and create a Cat when form is submitted (the request is a POST instead of a GET).

It won't always be quite this easy as we'll see in the future when we need to add some additional class attributes or override methods in the CBV.

On to the template!

Create the Template for Creating Cats

By convention, the CatCreate CBV will look to render a template named templates/main_app/cat_form.html.

All CBVs by default will use a folder inside of the templates folder with a name the same as the app, in our case main_app.

Let's give CatCreate the template it wants by first creating the templates/main_app folder:

mkdir main_app/templates/main_app

Now create the template file:

touch main_app/templates/main_app/cat_form.html

Note that the CreateView renders a default template named as follows: <name of model>_form.html (lowercased, of course)

There's not too much so we'll review as we type it in:

{% extends 'base.html' %}

{% block content %}
<h1>Add Cat</h1>
<!-- Leaving the action empty makes the form post to the same url used to display it -->
<form action="" method="POST">
<!-- Django requires the following for security purposes -->
{% csrf_token %}
<table>
<!-- Render the inputs inside of <tr>s & <td>s -->
{{ form.as_table }}
</table>
<input type="submit" value="Add Cat!" class="btn">
</form>
{% endblock %}

The {% csrf_token %} template tag is a security measure that makes it difficult to perform a cross-site-request-forgery by writing a CSRF (pronounced "see-surf") token that is validated on the server.

The form variable is an instance of the ModelForm class instantiated automatically by the CreateView and passed to the template (via the context dictionary). We'll learn more about ModelForms in a later lesson.

Let's refresh and click the Add a Cat link.

Not too bad:

Add Cat Page

Use Devtools to explore the DOM. You'll see how Django's ModelForm wrote the inputs in table rows and columns because we used {{ form.as_table }}. Other options include:

  • {{ form }} - No wrapper around the <label> & <input> tags
  • {{ form.as_p }} - Wraps a <p> tag around the <label> & <input> tags
  • {{ form.as_ul }} - Wraps an <li> tag around the <label> & <input> tags

Note: To ease custom styling, you can add an id or class to your <table> and/or <form> tags. Also note how Django automatically assigns an id to each input.

Redirecting

If we submit the form to create a cat, the cat will be created, however, we'll receive an error because Django doesn't know where we want our app to redirect to.

One way to fix this is by adding a success_url class attribute to the CBV like this:

class CatCreate(CreateView):
model = Cat
fields = '__all__'
success_url = '/cats/'

However, we would need to write a method if we wanted to redirect to the cat that was just created, and for better code reuse, Django recommends adding a get_absolute_url method to the Model instead.

Let's take Django's advice by updating the Cat model:

class Cat(models.Model):
name = models.CharField(max_length=100)
breed = models.CharField(max_length=100)
description = models.TextField(max_length=250)
age = models.IntegerField()

def __str__(self):
return self.name

# Add this method
def get_absolute_url(self):
return reverse('detail', kwargs={'cat_id': self.id})

The reverse above will return the correct path for the detail named route. However, since that route requires a cat_id route parameter, its value must provided as a shown above.

Let's not forget to import reverse in models.py:

from django.db import models
# Import the reverse function
from django.urls import reverse

Before moving on to Update & Delete operations, here's a few review questions...

❓ Review Questions - Creating Data with a CBV

  1. To use a CBV to create data, our class must inherit from what class?

  2. True or False: A CreateView performs different behavior depending upon the HTTP request being a GET or a POST?

  3. True or False: In the case of a GET request, a CreateView automatically renders a template matching this format templates/<name of app>/<name of model>_form.html

6. Updating & Deleting Data Using a CBV

Here are the user stories we want to implement:

  • AAU, when viewing a cat's detail page, I want to click EDIT to update that cat's information.
  • AAU, when viewing a cat's detail page, I want to click DELETE to remove that cat from the database.

We've covered all of the fundamentals of class-based views above so we'll be a little more brief while adding the functionality to delete and update cats by combining their steps together...

Add the Routes

Let's add the two new routes in urls.py:

path('cats/create/', views.CatCreate.as_view(), name='cats_create'),
# Add the new routes below
path('cats/<int:pk>/update/', views.CatUpdate.as_view(), name='cats_update'),
path('cats/<int:pk>/delete/', views.CatDelete.as_view(), name='cats_delete'),

IMPORTANT: By convention, CBVs that work with individual model instances will expect to find a named parameter of pk. This is why we didn't use cat_id as we did in the detail entry.

Update the Detail UI

We need to see EDIT and DELETE links on a cat's detail page.

Let's update templates/cats/detail.html by adding to a cat's "card" a <div> with a Materialize class of card-action:

      <p>Age: Kitten</p>
{% endif %}
</div>
<!-- Add the following markup -->
<div class="card-action">
<a href="{% url 'cats_update' cat.id %}">Edit</a>
<a href="{% url 'cats_delete' cat.id %}">Delete</a>
</div>
<!-- New markup above -->
</div>

Now for the views...

Subclass the UpdateView & DeleteView

We've referenced the new CatUpdate and CatDelete views in the routes so we need to add them to views.py.

But first we'll need to import the two additional generic CBVs to extend from:

# Add UdpateView & DeleteView
from django.views.generic.edit import CreateView, UpdateView, DeleteView

Now for the amazingly minimal new code:

class CatUpdate(UpdateView):
model = Cat
# Let's disallow the renaming of a cat by excluding the name field!
fields = ['breed', 'description', 'age']

class CatDelete(DeleteView):
model = Cat
success_url = '/cats/'

Note that if we delete a cat, we'll need to redirect to the cats index page since that cat doesn't exist anymore.

Now the server should be back up and clicking on a cat should result in a page that looks something like this:

Cat Card with Edit and Delete

Updating a cat is working, however, we'll need to add an extra template to implement delete functionality, which we'll do in a moment.

First though, to enhance the UX, it would be nice to see the name of the cat we're editing...

Customize the cat_form.html Template

Since we didn't include 'name' in the fields list in CatUpdate, the name attribute isn't listed in the form.

We're going to customize cat_form.html to show the cat's name that is being edited, and learn a little more about Django in the process.

The CBVs that work with a single model instance automatically pass as part of the context two attributes that are assigned the model instance. The attributes are named object and, in this case, cat (the lowercase name of the Model).

object will be set to None in a CreateView, which makes sense.

FYI, in a ListView, the queryset of model instances will be available via attributes named object_list and cat_list (again, the lowercase name of the Model with _list appended to it).

Let's leverage this new knowledge to modify templates/main_app/cat_form.html to show the cat's name only when we are editing, not creating, a cat:

{% block content %}
<!-- New if/else block -->
{% if object %}
<h1>Edit <span class="teal-text">{{object.name}}</span></h1>
{% else %}
<h1>Add Cat</h1>
{% endif %}

Looking good:

Edit Cat

Okay, let's get delete functionality working then we're done!

Adding a cat_confirm_delete.html Template

It's usually wise to perform some type of confirmation when deleting data and Django makes it easy when we subclass a DeleteView by automatically rendering a confirmation template.

All we have to do is create it named as follows:

touch main_app/templates/main_app/cat_confirm_delete.html

Now for its content:

{% extends 'base.html' %}

{% block content %}
<h1>Delete Cat</h1>

<h4>Are you sure you want to delete <span class="teal-text">{{ object.name }}</span>?</h4>

<form action="" method="POST">
{% csrf_token %}
<input type="submit" value="Yes - Delete!" class="btn">
<a href="{% url 'detail' cat.id %}">Cancel</a>
</form>
{% endblock %}

Note how we are allowing the user to cancel the delete by providing a link back to the detail page.

Let's check it out by refreshing:

Delete Cat

Congrats for implementing full CRUD for cats!

In the next lesson we'll see how to work with One-Many data relationships.

As usual, your lab is to implement the same class-based views in you Finch Collector project.

7. ❓ Essential Questions

  1. To be more productive, Class-based Views can be used in lieu of __ __.

  2. What generic class do we extend with our own CBV for deleting model instances?

  3. What class attribute must we provide to every CBV to inform the CBV what Model its for?

8. Challenge Exercise

This challenge exercise is for those of you who don't need to rest and would like some additional reps in addition to the Finch Collector lab.

So far in the Cat Collector, we've implemented full-CRUD for the Cat Model. Tomorrow we will be learning how to implement a many-to-many association between cats and toys: Cat >---< Toy

The starter code for the many-to-many lesson will have full-CRUD implemented for the Toy Model.

For practice, feel free to implement full-CRUD on the Toy Model yourself. The Toy Model has the following attributes:


class Toy(models.Model):
name = models.CharField(max_length=50)
color = models.CharField(max_length=20)

# Other goodness such as 'def __str__():' below

Enjoy!

9. Further Study - Customizing Class-based Views

So, we now know that Django provides us with a set of very useful classes that make development in Django easier and more productive.

You should consider class-based views as the go to vs. function-based views.

There might come a time though, when you might believe that class-based views can't be used in situations that require more flexibility. However, Django's class-based views are designed to be easily customizable by overriding their methods and assigning values to attributes.

Your app's requirements may, or may not, need to depend upon customization of the built-in CBVs, however, you should take time to investigate how and learn more about OOP along the way...

Start with the docs here

Great Examples in the Docs

This section of the docs shows how to override a CBV's get_context_data() method to provide additional data/context to templates.

This section of the docs shows how to override a CBV's get_queryset() method to provide a custom/dynamic queryset to a ListView. For example, in that method, you can access the logged in user via self.request.user to use to filter data, etc.

Similar to the above this section of the docs shows how to override a CBV's get_object() method to provide a custom/dynamic object to a DetailView and/or perform "extra work".

References

Built-in Class-based Views

Working with Forms in Django

How Python's import system works - Modules & Packages