Django class-based views with pagination

Introduction

The final project, I am using a phonebook application – Github repo

Applications grow in terms of complexity and we will end up repeating certain functions or patterns again and again.

Django tries to take away some of that monotonous at the model and template layers, but Web developers also experience this boredom at the view level.

Built-in class-based generic views

Django’s generic views were developed to ease that pain. They take certain common idioms and patterns found in view development and abstract them so that you can quickly write common views of data without having to write too much code.

We can recognize certain common tasks, like displaying a list of objects and write code that displays a list of any object. Then the model in question can be passed as an extra argument to the URLconf.

Django ships with generic views to do the following:

  • Display list and detail pages for a single object. If we were creating an application to manage conferences then a TalkListView and a RegisteredUserListView would be examples of list views. A single talk page is an example of what we call a « detail » view.
  • Present date-based objects in year/month/day archive pages, associated detail, and « latest » pages.
  • Allow users to create, update, and delete objects – with or without authorization.

Taken together, these views provide easy interfaces to perform the most common tasks developers encounter.

Generic views of objects

TemplateView certainly is useful, but Django’s generic views really shine when it comes to presenting views of your database content. Because it’s such a common task, Django comes with a handful of built-in generic views that make generating list and detail views of objects incredibly easy.

Let’s start by looking at some examples of showing a list of objects or an individual object.

Here is our models:

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

# Create your models here.


class Contact(models.Model):
    first_name = models.CharField(max_length=150)
    last_name = models.CharField(max_length=150)
    phone = models.CharField(max_length=150)
    email = models.CharField(max_length=150, blank=True)
    created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.phone

Here are our function-based views, we'll change it later

# views.py
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, get_object_or_404, redirect
from .models import Contact


# Contact list


def contact_list(request):
    user = request.user
    contacts = Contact.objects.filter(created_by=None)
    if user.is_authenticated:
        contacts = Contact.objects.filter(created_by=user)
    return render(request, "contact/contact_list.html", {"contacts": contacts})

You see that these views do one thing display a list of Phonebook

To change this into a class-based views, add this

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


# Contact list

class ContactList(ListView):
    model = Contact
...

You don't need to change the template name. To display the data we need to loop throught a viraibale called object_list.

<!-- contact-list.html -->
...
<div class="row">
  <div class="col-md-6 m-auto">
    <div class="card card-body">
      {% if object_list %}
      <ul class="list-group list-group-flush">
        {% for contact in object_list %}
        <li class="list-group-item">
          <a href="{% url 'details' contact.id %}"> {{ contact.email }}</a>
        </li>
        {% endfor %}
      </ul>
      {% else %}
      <p>No contact</p>
      {% endif %}
    </div>
  </div>
</div>

Making « friendly » template contexts

Instead to use the variable object_list we can use a friendly name contacts. The context_object_name attribute on a generic view specifies the context variable to use:

# views.py
class ContactList(ListView):
    model = Contact
    context_object_name = "contacts"
<!-- contact-list.html -->
...
<div class="row">
  <div class="col-md-6 m-auto">
    <div class="card card-body">
      {% if contacts %}
      <ul class="list-group list-group-flush">
        {% for contact in contacts %}
        <li class="list-group-item">
          <a href="{% url 'details' contact.id %}"> {{ contact.email }}</a>
        </li>
        {% endfor %}
      </ul>
      {% else %}
      <p>No contact</p>
      {% endif %}
    </div>
  </div>
</div>

Providing a useful context_object_name is always a good idea. Your coworkers who design templates will thank you.

Filtering

Another common need is filtering, if we wanted, we could use self.request.user to filter using the current user, or other more complex logic.

# views.py
...
class ContactList(ListView):
    context_object_name = "contacts"

    def get_queryset(self):
        user = self.request.user
        if user.is_authenticated:
            return Contact.objects.filter(created_by=self.request.user)
        return Contact.objects.filter(created_by=None)
...

Adding pagination

For some performance reason, we need to paginate our app, that will make it load faster. Fortunately, Django comes with built-in pagination classes for managing paginating data of your application.

# views.py
...
class ContactList(ListView):
    context_object_name = "contacts"
    paginate_by = 4 # add this

    def get_queryset(self):
        user = self.request.user
        if user.is_authenticated:
            return Contact.objects.filter(created_by=self.request.user)
        return Contact.objects.filter(created_by=None)
...

Our template

<!-- contact-list.html -->
...
<div class="row">
  <div class="col-md-6 m-auto">
    <div class="card card-body">
      {% if contacts %}
      <ul class="list-group list-group-flush">
        {% for contact in contacts %}
        <li class="list-group-item">
          <a href="{% url 'details' contact.id %}"> {{ contact.email }}</a>
        </li>
        {% endfor %}
      </ul>
      {% else %}
      <p>No contact</p>
      {% endif %}
    </div>
    <nav aria-label="Page navigation example">
      {% if is_paginated %}
      <ul class="pagination">
        {% if page_obj.has_previous %}
        <li class="page-item">
          <a class="page-link" href="?page={{page_obj.previous_page_number}}"
            >&laquo;</a
          >
        </li>
        {% else %}
        <li class="page-item disabled">
          <a class="page-link" href="#">&laquo;</a>
        </li>
        {% endif %} {% for i in paginator.page_range %} {% if page_obj.number ==
        i %}
        <li class="page-item"><a class="page-link active">{{ i }}</a></li>
        {% else %}
        <li class="page-item">
          <a class="page-link" href="?page={{ i }}">{{ i }}</a>
        </li>
        {% endif %} {% endfor %} {% if page_obj.has_next %}
        <li class="page-item">
          <a href="?page={{page_obj.next_page_number}}" class="page-link"
            >&raquo;</a
          >
        </li>
        {% else %}
        <li class="page-item disabled">
          <a class="page-link">&raquo;</a>
        </li>
        {% endif %}
      </ul>
      {% endif %}
    </nav>
  </div>
</div>

Now run the server and visit http://127.0.0.1:8000/ you should see the page navigation buttons below the contacts.

python manage.py runserver

Conclusion

In this tutorial, we've learned how to work with django class-based views. You can do more stuff with like Form submitting, we'll cover them next.

Thanks for reading, see you next.