Cat Collector Django Class-Based Views

Learning objective: By the end of this lesson, students will be able to create new pages using Django’s built in Class-based Views.

What are class-based views?

Class-based Views (CBVs) in Django provide a structured approach to creating views by using classes rather than functions. These classes are built into Django’s views module, enabling you to organize your views and reuse code by extending Django’s predefined base classes. Let’s step away from cat collector for a moment and think about an application performing CRUD on a Book resource.

For example, let’s say we wanted to implement index (view all) functionality using class-based views. First, we would import and extend the built in ListView class:

from django.views.generic import ListView

class BookList(ListView):
    model = Book

Here, ListView is a predefined generic class-based view that abstracts common patterns into a reusable format. We extend ListView to create our own BookList class, and tell it which model to use:

    model = Book

This tells BookList to fetch all records from the Book model.

Next, we connect this class-based view to a URL in urls.py using the as_view() method of the CBV (which returns a view function) and connect it to a route as usual:

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

urlpatterns = [
    path('books/', BookList.as_view(), name='book-index'),
]

Notice that there’s no call to render? Unlike function-based views, CBVs don’t require explicit render calls. By default, ListView will look for a template named templates/<app_name>/book_list.html, but this can be customized with additional attributes.

Types of class-based views

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

Everything you would need to perform full crud on a resource!

In this lesson, we will be utilizing the CreateView, UpdateView and DeleteView CBVs to complete our our C U D functionality on cats! We could also replace the existing cat_index and cat_detail view functions with Class-based Views (CBVs). However, we will keep these as function-based views to serve as examples of their structure and functionality.

Why use Class-based Views?

Django developed class-based views to minimize the redundancy of common view patterns found in web applications, effectively DRYing up our code base. You can read more about the rationale from Django’s documentation on 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.

CBVs can save time, making us more productive developers. CBVs are also highly configurable by adding class attributes or overriding methods and using decorators.

For example, to change the default template for a list view, set the template_name attribute:

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

Creating Data Using a CBV

Let’s explore class-based views as we add create functionality for cats!

In Django, the naming convention for class-based views (CBVs) that handle creating objects typically involves using the name of the model followed by the type of action the view handles. For creating objects, the common practice is to append “Create” to the model name. For a model called Cat the convention would be CatCreate.

Add the Route

In an Express application, you typically need to define two separate routes and their corresponding controller actions to handle a form:

  1. A GET route (cats/new) to serve the form page.
  2. A POST route (/cats) to process the form data and add a new cat to the database.

In Django, using the class-based view CreateView simplifies this process by combining these steps:

Let’s add a new URL pattern to main_app/urls.py for this classed-based view:

urlpatterns = [
    path('', views.home, name='home'),
    path('about/', views.about, name='about'),
    path('cats/', views.cat_index, name='cat-index'),
    path('cats/<int:cat_id>/', views.cat_detail, name='cat-detail'),
    # new route used to create a cat
    path('cats/create/', views.CatCreate.as_view(), name='cat-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, therefore it ignores cats/create/

Now we 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 used to both:

Let’s update base.html to add a link to the nav:

<ul>
  <li><a href="{% url 'cat-index' %}">All Cats</a></li>
  <li><a href="{% url 'cat-create' %}">Add a Cat</a></li>
  <li><a href="{% url 'about' %}">About</a></li>
</ul>

On to the view!

Extending the generic CreateView

First, to use any class-based view in Django, it needs to be imported:

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:

# main-app/views.py

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

The fields attribute is required 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:

# main-app/views.py

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

This is all the code necessary to display a template containing a form that’s automatically provided, and to create a Cat when the form is submitted (when the request is a POST rather than a GET).

On to the template!

Create the template for creating cats

Consider the following wireframe:

Add a Cat wireframe

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 in the terminal:

mkdir main_app/templates/main_app

Now create the template file in the terminal:

touch main_app/templates/main_app/cat_form.html

The CreateView expects a template named <model_name>_form.html by default, where <model_name> is the lowercase name of the model. This convention is suggested in the Django documentation to standardize form templates. You can customize this by explicitly setting template_name in your view class.

We’ll discuss the following code as we type it:

{% extends 'base.html' %} 
{% load static %} 
{% block head %}
<link rel="stylesheet" href="{% static 'css/form.css' %}" />
{% endblock %} 
{% block content %}

<div class="page-header">
  <h1>Add a Cat</h1>
  <img src="{% static 'images/nerd-cat.svg' %}" alt="A cat using a computer" />
</div>

<form action="" method="post" class="form-container">
  {% csrf_token %}
  <table>
    {{ form.as_table }}
  </table>
  <button type="submit" class="btn submit">Submit!</button>
</form>

{% endblock %}
  1. Setting the action attribute to an empty string (action="") means the form will submit to the same URL that served it. This is typical for CreateView, which handles both displaying the form (on GET requests) and processing the form submission (on POST requests).

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

  3. The form variable represents a Django ModelForm instance that is automatically created by CreateView. This form is linked directly to your model and includes all the fields specified in the form’s Meta class or passed explicitly to the view. {{ form.as_table }} renders the form fields within a table layout. This method is one of several Django provides for rendering forms (others include as_p for paragraph tags and as_ul for unordered lists).

Add page styles

A new page means new CSS! We’ll be using this same form CSS throughout this app, so we’re just going to put it in the main css directory for this application. Run the following command in the terminal:

touch main_app/static/css/form.css

Add the following styles to that file:

.form-container {
  display: flex;
  flex-direction: column;
  width: 100%;
}

table {
  padding: 0 40px;
  width: 100%;
  border-spacing: 0 20px;
}

th {
  text-align: left;
  padding: 6px 20px 0 0;
  font-weight: normal;
  vertical-align: top;
  font-size: var(--font-reg);
}

td {
  max-width: 60%;
}

td > * {
  width: 100%;
  padding: 2px 4px;
  font-size: var(--font-l);
}

td > textarea {
  height: calc(4 * var(--font-l) + 8px);
  font-family: inherit;
}

.btn {
  align-self: flex-end;
  margin-right: 40px;
}

Let’s refresh the browser and click the Add a Cat link.

Looks great!

Create Page UI

Use the 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:

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

Currently, if you submit the form to create a new cat, while the cat will be successfully created, you will encounter an error. This error occurs because Django does not know where to redirect the user after the form submission.

To resolve this, you need to specify a success_url attribute in your class-based view (CBV). This attribute tells Django the URL to redirect to once the form has been successfully processed.

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

However, this approach always redirects users to the general /cats/ page after a cat is created. Instead, it is often more useful to redirect to the specific page of the cat that was just created.

Redirecting to a newly created cat object

Rather than redirecting to a static page, Django allows us to dynamically redirect to the newly created object using the get_absolute_url method on the model.

Let’s update the Cat model to include this method:

from django.db import models
from django.urls import reverse

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

    # Define a method to get the URL for this particular cat instance
    def get_absolute_url(self):
        # Use the 'reverse' function to dynamically find the URL for viewing this cat's details
        return reverse('cat-detail', kwargs={'cat_id': self.id})

The reverse method above will return the correct path for the cat-detail named route. However, since that route requires a cat_id route parameter, its value must be provided as an argument.

Don’t forget to import reverse in models.py:

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

Since CreateView automatically calls get_absolute_url on the model instance if success_url is not explicitly set, we must remove the previously added success_url from the CatCreate view:

class CatCreate(CreateView):
    model = Cat
    fields = '__all__'
    # Remove success_url so Django uses get_absolute_url from Cat

By removing success_url, Django will now use the get_absolute_url method defined in the Cat model, ensuring the user is redirected to the newly created cat’s detail page.

Updating & deleting cats with class-based views

Let’s implement the following user stories using class-based view:

Add the routes

Let’s add two new routes for update and delete in main_app/urls.py:

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

By convention, CBVs that work with individual model instances will expect to find a named parameter of pk for “primary key”. This is why we didn’t use cat_id as we did in the detail entry.

Update the UI

Now we need to add EDIT and DELETE links on a cat’s details page.

Let’s update templates/cats/detail.html by adding a new <div> we’ll label with the class "cat-actions". This will contain our link buttons to Edit and Delete routes.

Add the new “cat-actions” <div> inside the detail.html template:

<div class="cat-details">
  <h1>{{ cat.name }}</h1>
  {% if cat.age > 0 %}
    <h2>A {{ cat.age }} year old {{ cat.breed }}</h2>
  {% else %}
    <h2>A {{ cat.breed }} kitten.</h2>
  {% endif %}
  <p>{{ cat.description }}</p>

  <div class="cat-actions">
    <a href="{% url 'cat-update' cat.id %}" class="btn warn">Edit</a>
    <a href="{% url 'cat-delete' cat.id %}" class="btn danger">Delete</a>
  </div>
</div>

Now for the views!

Create subclasses from Django’s UpdateView & DeleteView

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

First, import Django’s UpdateView and DeleteView CBVs to extend from:

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

Now we can utilize these minimal view classes:

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 when we delete a cat, we’ll need to redirect to the cats index page since that cat doesn’t exist anymore.

Now we should be able to refresh the page and see our buttons rendered.

Edit and Delete Buttons UI

Adding the templates

At this point, the UPDATE functionality is ready for testing, but to fully implement the DELETE functionality, we need to take a few more steps. For both UPDATE and DELETE actions, if we want to introduce customizations or confirmations, modifications to existing templates are necessary.

Instead of creating a new template for the UPDATE action, we will edit the existing cat_form.html template to add custom UI elements. This ensures consistency across different actions like UPDATE and DELETE, where DELETE will require a separate confirmation template to safeguard against accidental deletions.

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 name of the Cat being edited, and learn a little more about Django in the process:

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:

<div class="page-header">
  <!-- Check if a cat object exists to determine if we're editing -->
  {% if cat %}
    <h1>Edit {{ cat.name }}</h1>
  {% else %}
    <h1>Add a Cat</h1>
  {% endif %}
</div>

We could also do:

<div class="page-header">
  <!-- Using the generic 'object' variable -->
  {% if object %}
    <h1>Edit {{ object.name }}</h1>
  {% else %}
    <h1>Add a Cat</h1>
  {% endif %}
</div>

These two blocks are functionally equivalent and we could use either.

Refresh the browser to see your changes. Looks great!

Edit Page UI

Creating a confirmation template for deleting a cat

When you want to delete data, it’s best practice to ask for confirmation to prevent accidental deletions. Django simplifies this process in its class-based DeleteView by looking for a specific confirmation template.

First, create the necessary confirmation template. By default, Django expects this template to be named following a particular convention. For a model named Cat, the template should be named cat_confirm_delete.html.

Run the following in the terminal:

touch main_app/templates/main_app/cat_confirm_delete.html

Next, define the contents of the template. This template will extend your base layout and include a confirmation form:

{% extends 'base.html' %}  
{% load static %} 
{% block content %}

<div class="page-header">
  <h1>Delete Cat?</h1>
  <img src="{% static 'images/nerd-cat.svg' %}" alt="A cat using a computer" />
</div>

<h2>Are you sure you want to delete {{ cat.name }}?</h2>

<form action="" method="post" class="form">
  {% csrf_token %}
  <a href="{% url 'cat-detail' cat.id %}" class="btn secondary">Cancel</a>
  <button type="submit" class="btn danger">Yes - Delete!</button>
</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 our progress by refreshing the browser and hitting the delete button.

Delete Confirm Page

🎉 Congrats, you have implemented full CRUD for cats!