Kurt Peek
Kurt Peek

Reputation: 57741

Django pagination: How do 'paginator' and 'page_obj' get injected into a template's context?

I'm trying to understand some source code which is used in a ListView to generate pagination links at the bottom of the page. For example, on a page listing 'experts', it looks like this:

enter image description here

It seems to be implemented somewhat differently from the way described in https://docs.djangoproject.com/en/2.0/topics/pagination/. There is an inclusion tag called paginator:

from django import template
from django.core.paginator import EmptyPage

from dashboard.views.utils import query_dict_from_params_or_cookies

register = template.Library()


def query_params(params):
    query = params.copy()

    if query.get('page'):
        del query['page']

    return '&' + query.urlencode() if query.urlencode() else ''


@register.inclusion_tag('templatetags/paginator.html', takes_context=True)
def paginator(context, adjacent_pages=2):
    request = context['request']

    page_obj = context['page_obj']
    page_range = context['paginator'].page_range

    page_number = page_obj.number
    last_page_number = len(page_range)

    left_idx = max(0, page_number - adjacent_pages - 1)
    right_idx = min(page_number + adjacent_pages, last_page_number)

    if left_idx == 2:
        left_idx -= 1

    if right_idx == last_page_number - 2:
        right_idx += 1

    window = page_range[left_idx:right_idx]

    try:
        previous_page_number = page_obj.previous_page_number()
    except EmptyPage:
        previous_page_number = None

    try:
        next_page_number = page_obj.next_page_number()
    except EmptyPage:
        next_page_number = None

    params = query_dict_from_params_or_cookies(request)

    return {
        'has_previous': page_obj.has_previous(),
        'has_next': page_obj.has_next(),
        'previous_page_number': previous_page_number,
        'next_page_number': next_page_number,
        'last_page_number': last_page_number,
        'page_range': window,
        'page_number': page_number,
        'show_first': page_number > adjacent_pages + 1,
        'show_left_gap': page_number > adjacent_pages + 3,
        'show_last': page_number < last_page_number - adjacent_pages,
        'show_right_gap': page_number < last_page_number - adjacent_pages - 2,
        'query_params': query_params(params)
    }

The corresponding template, templatetags/paginator.html, looks like this:

<ul class="pagination right"
    {% if has_previous %}data-previous-page="?page={{ previous_page_number }}{{ query_params }}"{% endif %}
    {% if has_next %}data-next-page="?page={{ next_page_number }}{{ query_params }}"{% endif %}>
  {% if has_previous %}
    <li class="waves-effect">
      <a href="?page={{ previous_page_number }}{{ query_params }}">
        <i class="material-icons">chevron_left</i>
      </a>
    </li>
  {% else %}
    <li class="disabled">
      <a><i class="material-icons">chevron_left</i></a>
    </li>
  {% endif %}

  {% if show_first %}
    <li><a href="?page=1{{ query_params }}">1</a></li>
  {% endif %}

  {% if show_left_gap %}
    <li class="disabled"><a>...</a></li>
  {% endif %}

  {% for i in page_range %}
    {% if page_number == i %}
      <li class="active"><a>{{ i }}</a></li>
    {% else %}
      <li class="waves-effect"><a href="?page={{ i }}{{ query_params }}">{{ i }}</a></li>
    {% endif %}
  {% endfor %}

  {% if show_right_gap %}
    <li class="disabled"><a>...</a></li>
  {% endif %}

  {% if show_last %}
    <li>
      <a href="?page={{ last_page_number }}{{ query_params }}">{{ last_page_number }}</a>
    </li>
  {% endif %}

  {% if has_next %}
    <li class="waves-effect">
      <a href="?page={{ next_page_number }}{{ query_params }}">
        <i class="material-icons">chevron_right</i>
      </a>
    </li>
  {% else %}
    <li class="disabled">
      <a><i class="material-icons">chevron_right</i></a>
    </li>
  {% endif %}
</ul>

What I don't quite understand is how paginator and page_obj get inserted into the template's context. For example, I did a project-wide search for page_obj and it does not appear that it is being injected in our own code, which would mean that it is being injected by Django's source code. But how is this being done?

Also, it seems to me like some of the logic essentially replicates what is in Django's Paginator class and this code could be simplified/refactored, would anyone agree?

enter image description here

Upvotes: 3

Views: 2224

Answers (1)

Kurt Peek
Kurt Peek

Reputation: 57741

It would appear that this is indeed done by Django's source code - specifically, by the generic ListView. The get_context_data() method of the MultipleObjectMixin (from which ListView inherits) reads:

def get_context_data(self, *, object_list=None, **kwargs):
    """Get the context for this view."""
    queryset = object_list if object_list is not None else self.object_list
    page_size = self.get_paginate_by(queryset)
    context_object_name = self.get_context_object_name(queryset)
    if page_size:
        paginator, page, queryset, is_paginated = self.paginate_queryset(queryset, page_size)
        context = {
            'paginator': paginator,
            'page_obj': page,
            'is_paginated': is_paginated,
            'object_list': queryset
        }
    else:
        context = {
            'paginator': None,
            'page_obj': None,
            'is_paginated': False,
            'object_list': queryset
        }
    if context_object_name is not None:
        context[context_object_name] = queryset
    context.update(kwargs)
    return super().get_context_data(**context)

Clearly, paginator and page_obj are added to the context here.

Upvotes: 1

Related Questions