Django

Django Class Based Views Pagination with Bootstrap 4

Pagination is an essential part of platforms where one needs to list many items. Instead of displaying all the times, say 50,000 records, pagination allows the user to browse through the entire collection in steps, only seeing a part of the entire collection.

In this article, we take a look at how to get pagination working in Django via Class Based Views. Thus, if you use ListView, for example, using pagination can give us results like this:

With that in mind, let’s get straight to it.

Our ListView Django Class Based View

This view class is responsible for doubling as a mere listing of items in the Package model class, plus displaying search results. Here

class PackageCustomList(ListView):
  model = Package
  template_name = 'package/custom_list.html'
  paginate_by = 3

  def get_queryset(self, *args, **kwargs):
    if self.kwargs:
      return Package.objects.filter(category=self.kwargs['category']).order_by('-createdAt')
    else:
      query = Package.objects.all().order_by('-createdAt')
      return query

The key take away point from the above snippet is the part, paginate_by. The paginate_by attribute is available with the MultipleObjectMixin, (docs)which is a class ListView generic list class inherits from.

At the fundamental level, here’s what the paginate_by field on the ListView class is doing:

  • Take the returned Queryset of the model, Package
  • Break them into separate pages, with each page holding a number of, in the case above, 3 items.
  • Provide handles for switching back and forth between the different pages generated holding the items.

Therefore, if the returned Queryset of the model, Package is made of 50 items, and the paginate_by value is 10, then we will have 5 pages, each holding 10 items.

Enabling the paginate_by attribute on our ListView class gives us certain options and callbacks for free.

See a basic example of Django Pagination from the docs

Django Pagination

Our Template

In our template, let’s play around with our page_objwhich is thrown into the template should pagination be enabled.

Remember: The page_obj will be available in our template if the page is ‘paginable’. As in, if you have a total of 5 items in the returned Queryset, yet you’ve stated a paginate_by value of 100, there’s nothing to paginate, thus the page_objwon’t be there in the template to make use of.

In fact, a boolean, is_paginatedis trued if the returned list is worthy of paginating. We make use of that boolean next.

{% if is_paginated %}
<hr>
<nav aria-label="Page navigation example">
    <ul class="pagination justify-content-center pagination-sm">
        {% if page_obj.has_previous %}
          <!-- If it ain't a search result display, don't append the search query
               to the URL. -->
          {% if not search %}
          <li class="page-item">
              <a class="page-link" href="{% url 'package_list' %}?page={{ page_obj.previous_page_number }}" tabindex="-1">Previous</a>
          </li>
          {% else %}
          <!-- Append the searched query to the URL, so that on a search results page,
               the pagination don't revert to listing all the listview items. -->
            <li class="page-item">
              <a class="page-link" href="{% url 'package_list' %}?{{search}}&page={{ page_obj.previous_page_number }}" tabindex="-1">Previous</a>
          </li>
          {% endif %}
        {% else %}
        <li class="page-item disabled">
            <a class="page-link" href="#" tabindex="-1">Previous</a>
        </li>
        {% endif %} 
        {% for object in page_obj.paginator.page_range %}
            <li class="page-item"><a class="page-link" href="{% url 'package_list' %}?page={{ forloop.counter }}">{{ forloop.counter }}</a></li>
        {% endfor %} 
        {% if page_obj.has_next %}
          {% if not search %}
          <li class="page-item">
              <a class="page-link" href="{% url 'package_list' %}?page={{ page_obj.next_page_number }}">Next</a>
          </li>
          {% else %}
            <li class="page-item">
              <a class="page-link" href="{% url 'package_list' %}?{{search}}&page={{ page_obj.next_page_number }}">Next</a>
          </li>
          {% endif %}
        {% else %}
        <li class="page-item disabled">
            <a class="page-link" href="#">Next</a>
        </li>
        {% endif %}
    </ul>
</nav>
{% endif %}

The code snippet from above is from Bootstrap 4. It has only be altered to reflect the Django template parts, and gives us this visual look:

Django Pagination Bootstrap 4 Look

We make use of the:

  • is_paginated bool to first check whether we have pagination available. We don’t want a pagination related UI showing without any functionality.
  • page_obj comes with various methods that make our job easier, such as
    • has_next andhas_previous
    • previous_page_number and next_page_number

The above is fairly straightforward as to what they do. However, one feature of our pagination is the fact that we have the number of pages listed, and numbered. We use the .page_range method available from the paginator class to achieve this.

This part:

{% for object in page_obj.paginator.page_range %}
    <li class="page-item">
        <a class="page-link" href="{% url 'package_list' %}?page={{ forloop.counter }}">{{ forloop.counter }}</a>
    </li>
{% endfor %}

We loop through the page_range, which gives a result of say, range(1,10) accordingly.

The page_range holds the total number of pages available. That is, if we had 50 returned queryset, and we’re paginate_by 10, our page_rangevalue will be range(1,6)

Using the forloop.counter to count how many times we’re going through and displaying the value, helps us to mimic accessing the individual values of the range.

Think of it as a replacement for:

for i in range(1, 6):
    print(i)

in the template.

This way, we get to display the 1, 2, 3 etc number of pages, providing links to the respective pages.

Conclusion

As seen in the results video above, our pagination behaves this way:

  • If there’s no previous page, disable previous page link
  • If there’s no next page, disable next page link
  • Display the number of pages and link to each of them

Bonus

Throw in this:

<nav aria-label="Page navigation example">
    <ul class="pagination justify-content-center pagination-sm">
        <li class="page-item disabled">
            <a class="page-link" href="#" tabindex="-1"><small>On page <strong>{{ page_obj.number }}</strong>. Showing <strong>{{ page_obj.object_list.count }}</strong> of total <strong>{{ page_obj.paginator.count }}</strong> items.</small></a>
        </li>

    </ul>
</nav>

at the top of the page, to get something like this:

I hope you enjoyed this piece. See you again in the next one.

 

Related Articles

Back to top button