Django Pagination with Bootstrap

There are some great Django pagination examples out there but none of them quite worked for me, so here's another one. If you are first starting out, pagination is "the process of dividing a document into discrete pages, either electronic pages or printed pages." The From the official Django documentation, to Simple Is Better Than Complex, I was able to get the basics, but none worked perfectly. This Medium blog post by sumitlni has a great solution but I had some issues with it.

This is how I render my blog list page:

def blog_posts(request, subject=None):
    """Display all blog posts"""

    if subject:
        qs = BlogPost.objects.filter(subject__subject=subject, published=True)
    else:
        qs = BlogPost.objects.filter(published=True)

    subjects = Subject.objects.all()

    keywords = request.GET.get('q', None)

    if keywords:
        query = SearchQuery(keywords)
        title_vector = SearchVector('title', weight='A')
        tag_vector = SearchVector(StringAgg('tag__tag', delimiter=' '), weight='B')
        content_vector = SearchVector('body', weight='C')
        vectors = title_vector + tag_vector + content_vector
        qs = qs.annotate(search=vectors).filter(search=query).distinct()
        posts = qs.annotate(rank=SearchRank(vectors, query)).distinct().order_by('-rank')

    else:
        posts = qs.order_by('-created_date')

    paginator = Paginator(posts, 20)
    page_number = request.GET.get('page', 1)
    page_obj = paginator.get_page(page_number)

    return render(request, 'blog.html', {'page_obj': page_obj, 'keywords': keywords, 'subjects': subjects, })

Here, I set the paginator to 20 posts and get the requested page:

...
paginator = Paginator(posts, 20)
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)

return render(request, 'blog.html', {'page_obj': page_obj, 'keywords': keywords, 'subjects': subjects, })

Next, the required Bootstrap HTML. I am only showing the pagination if there is more than 1 page with {% if page_obj.paginator.num_pages > 1 %} and then I am loading the custom pagination tags url_replace and proper_paginate that I will describe below with {% load pagination %}. The rest is semi-custom styling, so please ask any questions if something doesn't make sense. Here is the completed Bootstrap pagination:

{% if page_obj.paginator.num_pages > 1 %}
    {% load pagination %}
    <ul class="mt-3 pagination justify-content-center">
        {% if page_obj.number != 1 %}
            <li><a class="page-link" href="?{% url_replace request 'page' 1 %}">⇤</a></li>
        {% endif %}
        {% if page_obj.has_previous %}
            <li><a class="page-link"
                   href="?{% url_replace request 'page' page_obj.previous_page_number %}">&laquo;</a>
            </li>
        {% endif %}
        {% for i in page_obj.paginator|proper_paginate:page_obj.number %}
            {% if page_obj.number == i %}
                <li class="page-item active"><a class="page-link"
                                                href="?{% url_replace request 'page' i %}">{{ i }}</a>
                </li>
            {% else %}
                <li class="page-item"><a class="page-link"
                                         href="?{% url_replace request 'page' i %}">{{ i }}</a></li>
            {% endif %}
        {% endfor %}
        {% if page_obj.has_next %}
            <li><a class="page-link" href="?{% url_replace request 'page' page_obj.next_page_number %}">&raquo;</a>
            </li>
        {% endif %}
        {% if page_obj.number != page_obj.paginator.num_pages %}
            <li><a class="page-link"
                   href="?{% url_replace request 'page' page_obj.paginator.num_pages %}">⇥</a></li>
        {% endif %}
    </ul>
{% endif %}

Finally, the custom pagination tags as described by sumitlni "calculates the start and end indices" and "generates the proper URL based on which page number we wish to go to":

from django import template

register = template.Library()


@register.filter(name='proper_paginate')
def proper_paginate(paginator, current_page, neighbors=2):
    if paginator.num_pages > 2 * neighbors:
        start_index = max(1, current_page - neighbors)
        end_index = min(paginator.num_pages, current_page + neighbors)
        if end_index < start_index + 2 * neighbors:
            end_index = start_index + 2 * neighbors
        elif start_index > end_index - 2 * neighbors:
            start_index = end_index - 2 * neighbors
        if start_index < 1:
            end_index -= start_index
            start_index = 1
        elif end_index > paginator.num_pages:
            start_index -= (end_index - paginator.num_pages)
            end_index = paginator.num_pages
        page_list = [f for f in range(start_index, end_index + 1)]
        return page_list[:(2 * neighbors + 1)]
    return paginator.page_range


@register.simple_tag
def url_replace(request, field, value):
    query_string = request.GET.copy()
    query_string[field] = value

    return query_string.urlencode()

The issues I had with the code and blog post above were:

1) I put them both in a single file called pagination.py

2) I didn't put it in a sub-directory named templatetags

Note, thanks to this question at Stack Overflow

After changing {% load filter_method_name %} to {% load filename %} and moving pagination.py to a folder named templatetags at the same level as my apps models.py, it worked!

May 4, 2020, 9:50 a.m.