This article is the final installment of the Django Class-Based Views (CBV) exploration series, introducing how to implement commonly used features in real-world applications such as pagination, search, and sorting by making full use of ListView. If you’ve followed the previous seven posts, which covered everything from the basics of CBV to the use of Mixins, this part will combine those insights and demonstrate how to elegantly handle common requirements encountered in actual web development using CBV.

Click the link below to read the previous post!

Exploring Class-Based Views (CBV) Series ⑦ - Utilizing Mixins and Managing Permissions


“Extend Django ListView to implement powerful listing features and improve user experience!”


1. ListView: Beyond simple possibilities

ListView is a very useful Generic View for displaying a list of data. However, in real web applications, it is essential to provide features like pagination to efficiently manage large datasets, search to quickly find desired information, and sorting based on user preferences.

In this chapter, we will explore how to extend ListView to implement these practical requirements easily and elegantly, leveraging the advantages of CBV.


2. Implementing Pagination

Displaying a large amount of data at once can lead to poor user experience. Pagination helps reduce the loading burden on users by dividing the data across multiple pages and assisting them in finding information more easily.

2.1 Basic Pagination Setup

ListView provides built-in pagination functionality. You just need to set the paginate_by attribute in your view class.

Example in views.py

from django.views.generic import ListView
from .models import Article

class ArticleListView(ListView):
    model = Article
    template_name = 'articles/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10  # Display 10 Article objects per page
  • paginate_by = 10: By setting this, ListView will automatically divide the results into pages of 10 items, and pass pagination-related objects (paginator, page_obj, is_paginated) to the template context.

2.2 Implementing Pagination UI in the Template

In the template, you can use the page_obj passed in the context to display page number links and implement previous/next page buttons.

Example in articles/article_list.html

<ul>
    {% for article in articles %}
        <li>{{ article.title }} - {{ article.created_at }}</li>
    {% empty %}
        <li>No articles have been published yet.</li>
    {% endfor %}
</ul>

<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?page=1">&laquo; First</a>
            <a href="?page={{ page_obj.previous_page_number }}">Previous</a>
        {% endif %}

        <span class="current">
            Page {{ page_obj.number }} / {{ paginator.num_pages }}
        </span>

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}">Next</a>
            <a href="?page={{ paginator.num_pages }}">Last &raquo;</a>
        {% endif %}
    </span>
</div>
  • page_obj: This object holds the data for the current page. It provides properties like has_previous, has_next, previous_page_number, next_page_number, and number.
  • paginator: This object contains information related to the entire set of pages. It provides properties like num_pages (total number of pages).

2.3 Maintaining URL Connections

You can observe that when generating pagination links, the ?page=2 query parameter is utilized. ListView automatically recognizes this parameter to display data for the corresponding page.


Quickly finding desired information on a website is crucial. We will extend ListView to implement search functionality.

3.1 Defining a Form

First, let’s define a simple form to receive the search term.

Example in forms.py

from django import forms

class ArticleSearchForm(forms.Form):
    search_term = forms.CharField(label='Search Term', required=False)

3.2 Modifying ListView

We will override the get_queryset() method of ListView to implement the search functionality.

Example in views.py

from django.views.generic import ListView
from django.db.models import Q
from .models import Article
from .forms import ArticleSearchForm

class ArticleListView(ListView):
    model = Article
    template_name = 'articles/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10

    def get_queryset(self):
        queryset = super().get_queryset()
        self.form = ArticleSearchForm(self.request.GET)  # Get form data from the GET request

        if self.form.is_valid():
            search_term = self.form.cleaned_data.get('search_term')
            if search_term:
                # Filter Articles that contain the search term in the title or content
                queryset = queryset.filter(
                    Q(title__icontains=search_term) | Q(content__icontains=search_term)
                )
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['search_form'] = self.form  # Pass form to the template
        return context
  • get_queryset(): This method is overridden to modify the base queryset.
  • An instance of the form is created, and if it passes validation, the search term is extracted from cleaned_data.
  • Using Q object, filter the Article objects that contain the search term in the title (title) or content (content) (the __icontains operator checks for inclusion irrespective of case).
  • get_context_data(): This adds the form instance to the context so it can be rendered in the template.

3.3 Displaying the Search Form and Maintaining Results in the Template

Let’s add the search form to the template and structure the URL to allow for pagination while maintaining search results.

articles/article_list.html

<form method="get">
    {{ search_form }}
    <button type="submit">Search</button>
</form>

<ul>
    {% for article in articles %}
        <li>{{ article.title }} - {{ article.created_at }}</li>
    {% empty %}
        <li>No search results found.</li>
    {% endfor %}
</ul>

<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?page=1&search_term={{ search_form.search_term.value|default:'' }}">&laquo; First</a>
            <a href="?page={{ page_obj.previous_page_number }}&search_term={{ search_form.search_term.value|default:'' }}">Previous</a>
        {% endif %}

        <span class="current">
            Page {{ page_obj.number }} / {{ paginator.num_pages }}
        </span>

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}&search_term={{ search_form.search_term.value|default:'' }}">Next</a>
            <a href="?page={{ paginator.num_pages }}&search_term={{ search_form.search_term.value|default:'' }}">Last &raquo;</a>
        {% endif %}
    </span>
</div>
  • Set the form's method to get to pass the search term as a URL query parameter.
  • When creating page links, include the current search term (search_form.search_term.value) in the URL parameters to maintain the search results while paginating. The |default:'' filter is used to provide an empty string if no search term is present.

Example ListView screen – List page with search and sorting UI


4. Adding Sorting Functionality

Let's add sorting functionality so users can view data sorted by their preferred criteria.

4.1 Adding a Sorting Selection Form (Optional)

You can also add a dropdown form for selecting sorting criteria. However, you can simply handle it via URL parameters. Here, we will describe the method using URL parameters.

4.2 Modifying ListView

We will modify the get_queryset() method to apply order_by().

Example in views.py (including search functionality)

from django.views.generic import ListView
from django.db.models import Q
from .models import Article
from .forms import ArticleSearchForm

class ArticleListView(ListView):
    model = Article
    template_name = 'articles/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10

    def get_queryset(self):
        queryset = super().get_queryset()
        self.form = ArticleSearchForm(self.request.GET)
        ordering = self.request.GET.get('ordering', '-created_at')  # Default sorting: latest first

        if self.form.is_valid():
            search_term = self.form.cleaned_data.get('search_term')
            if search_term:
                queryset = queryset.filter(
                    Q(title__icontains=search_term) | Q(content__icontains=search_term)
                )

        queryset = queryset.order_by(ordering)
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['search_form'] = self.form
        context['ordering'] = self.request.GET.get('ordering', '-created_at')  # Pass the current sorting criteria to the template
        return context
  • self.request.GET.get('ordering', '-created_at'): Retrieves the ordering value from the URL query parameter. If there is no value, it defaults to -created_at (latest first).
  • queryset = queryset.order_by(ordering): This sorts the queryset using the retrieved ordering value. Prefixing the model field name with - sorts in reverse order.

4.3 Providing Sorting Links in the Template

The template should provide links for sorting by each field.

Example in articles/article_list.html

<form method="get">
    {{ search_form }}
    <button type="submit">Search</button>
</form>

<table>
    <thead>
        <tr>
            <th><a href="?ordering=title&search_term={{ search_form.search_term.value|default:'' }}">Title</a></th>
            <th><a href="?ordering=-created_at&search_term={{ search_form.search_term.value|default:'' }}">Created Date</a></th>
            </tr>
    </thead>
    <tbody>
        {% for article in articles %}
            <tr>
                <td>{{ article.title }}</td>
                <td>{{ article.created_at }}</td>
            </tr>
        {% empty %}
            <tr><td colspan="2">No search results found.</td></tr>
        {% endfor %}
    </tbody>
</table>

<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?page=1&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">&laquo; First</a>
            <a href="?page={{ page_obj.previous_page_number }}&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">Previous</a>
        {% endif %}

        <span class="current">
            Page {{ page_obj.number }} / {{ paginator.num_pages }}
        </span>

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">Next</a>
            <a href="?page={{ paginator.num_pages }}&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">Last &raquo;</a>
        {% endif %}
    </span>
</div>
  • Provide links for each table header that includes the sorting criteria (ordering) parameter.
  • When generating page links, include not only the current search term but also the current sorting criteria (ordering) in the URL parameters to ensure pagination maintains the sorting state.

Search/Sorting Process Diagram – Visualization of ListView extension flow


5. Combining Functionality with Mixins

By utilizing Mixins, you can manage pagination, search, and sorting functionality more cleanly. For example, you can separate the search functionality into a Mixin.

Example in mixins.py

from django.db.models import Q
from .forms import ArticleSearchForm

class ArticleSearchMixin:
    def get_queryset(self):
        queryset = super().get_queryset()
        self.form = ArticleSearchForm(self.request.GET)

        if self.form.is_valid():
            search_term = self.form.cleaned_data.get('search_term')
            if search_term:
                queryset = queryset.filter(
                    Q(title__icontains=search_term) | Q(content__icontains=search_term)
                )
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['search_form'] = getattr(self, 'form', ArticleSearchForm())  # Use default form if none exists
        return context

Example in views.py (Applying Mixin)

from django.views.generic import ListView
from .models import Article
from .mixins import ArticleSearchMixin

class ArticleListView(ArticleSearchMixin, ListView):
    model = Article
    template_name = 'articles/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10

    def get_queryset(self):
        queryset = super().get_queryset()
        ordering = self.request.GET.get('ordering', '-created_at')
        queryset = queryset.order_by(ordering)
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['ordering'] = self.request.GET.get('ordering', '-created_at')
        return context
  • The ArticleSearchMixin encapsulates the search-related logic.
  • ArticleListView inherits from both ArticleSearchMixin and ListView, allowing it to possess both search and listing functionalities.

6. Advantages and Limitations of CBV (Series Conclusion)

Throughout the eight installments of the CBV exploration series, we have examined reasons for transitioning from function-based views (FBV) to class-based views (CBV), the basic structure and usage of CBVs, the application of various Generic Views, and functional expansion through Mixins.

Key Advantages of CBV:

  • Code Reusability: Through Generic Views and Mixins, repetitive code can be minimized leading to more efficient development.
  • Structured Code: Class-based code clearly separates and manages logic, making it easier to comprehend.
  • Extensibility: Inheritance and Mixins allow for easy expansion of existing functionalities and addition of new features.
  • Maintainability: Modularized code simplifies maintenance and reduces the impact of changes across the entire codebase.
  • Improved Development Productivity: Utilizing various Generic Views and Mixins provided by Django allows for expedited development.

Limitations and Considerations for CBV:

  • Initial Learning Curve: Understanding the class-based structure may require more effort compared to FBV.
  • Excessive Inheritance: Using too many Mixins can complicate the flow of code, making it harder to follow. It’s important to use Mixins at a suitable level.
  • Overengineering for Simple Logic: For very simple pages or functionalities, FBVs might be more concise. It’s advisable to choose the appropriate view style as per the context.

In conclusion, Django CBVs are a powerful and beneficial tool for developing web applications that require complex and diverse functionalities. Understanding and appropriately leveraging the advantages of CBVs can lead to writing more efficient and maintainable code.

Thank you for reading the Class-Based Views (CBV) exploration series! I hope this series has been helpful in understanding Django CBVs and applying them in real projects.


Review Previous Posts

  1. Exploring Class-Based Views (CBV) Series #1 – The Reason for Moving from FBV to CBV and Developer Mindset
  2. Exploring Class-Based Views (CBV) Series #2 – Understanding Django's Basic View Classes
  3. Exploring Class-Based Views (CBV) Series #3 – Simplifying Form Handling with FormView
  4. Exploring Class-Based Views (CBV) Series #4 – Using ListView & DetailView
  5. Exploring Class-Based Views (CBV) Series #5 – Implementing CRUD with CreateView, UpdateView, and DeleteView
  6. Exploring Class-Based Views (CBV) Series #6 – Utilizing TemplateView & RedirectView
  7. Exploring Class-Based Views (CBV) Series #7 – Utilizing Mixins and Managing Permissions

“Implement richer and more user-friendly listing features by extending ListView, and upgrade your Django development skills by mastering CBVs!”