ShutterFreak
ShutterFreak

Reputation: 141

How to enrich page navigator by adding field from first and last objects in Django paginator?

I am building a Django app form managing the digitalisation process of analog film rolls.

I would like to add the film_roll.from_year field of the FilmRoll model to my page navigation widget in a Django template. This greatly facilitates navigating to the right page by looking at the year_from range below each page.

My view is defined as follows:

def index(request):
    film_roll_list = FilmRoll.objects.order_by(
        "from_year", "from_month", "title"
    ).annotate(
        scan_count=Count("myScanned_VS_NLP", distinct=True),
    )
    paginator = Paginator(film_roll_list, 20)
    page_number = request.GET.get("page")
    try:
        page_obj = paginator.get_page(page_number)
    except PageNotAnInteger:
        # If the page parameter is not an integer, show the first page
        page_obj = paginator.get_page(1)
    except EmptyPage:
        # If the page is out of range, show the last page
        page_obj = paginator.page(paginator.num_pages)

    film_roll_count = paginator.count

    context = {
        "film_roll_list": film_roll_list,
        "page_obj": page_obj,
        "film_roll_count": film_roll_count,
    }
    return render(request, "filmrolls/index.html", context)

Here's the page navigator code in the template:

{# Page navigator: start #}
{% if page_obj.has_other_pages %}
<div class="row">
    <div class="btn-group" role="group" aria-label="Item pagination">
        {% if page_obj.has_previous %}
            <a href="?page={{ page_obj.previous_page_number }}" class="btn btn-outline-primary">&laquo;</a>
        {% endif %}

        {% for page_number in page_obj.paginator.page_range %}
            {% if page_obj.number == page_number %}
                <button class="btn btn-outline-primary active">
                    <span>{{ page_number }} <span class="sr-only"></span></span>
                    # TODO - add 'year_from' range for the current page
                </button>
            {% else %}
                {% if page_number == paginator.ELLIPSIS %}
                <button class="btn">
                    <span>{{ paginator.ELLIPSIS }} <span class="sr-only"></span></span>
                </button>
                {% else %}
                <a href="?page={{ page_number }}" class="btn btn-outline-primary">
                    {{ page_number }}
                    # TODO - add 'year_from' range for each other page
                </a>
                {% endif %}
            {% endif %}
        {% endfor %}

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}" class="btn btn-outline-primary">&raquo;</a>
        {% endif %}
    </div>
</div>
{% endif %}
{# Page navigator: end #}

Since the film roll collection is already ordered by year_from, I could probably fetch year_from from the first and last object in the paginated list.

Is this something that can be added in the Django template, or do I have to compute these properties in the view?

Upvotes: 0

Views: 47

Answers (2)

ShutterFreak
ShutterFreak

Reputation: 141

I found a solution to implement the following: enter image description here

  1. The view:
def index(request):
    # Paginator: max items per page:
    items_per_page = 20

    film_roll_list = FilmRoll.objects.order_by(
        "from_year", "from_month", "title"
    ).annotate(
        vs_nlp_scan_count=Count("myScanned_VS_NLP", distinct=True),
    )

    paginator = Paginator(film_roll_list, items_per_page)

    date_shot_start = {}
    date_shot_end = {}
    p = 1
    while p <= paginator.num_pages:
        index_start = (p - 1) * items_per_page + 1
        index_end = min(p * items_per_page + 1, paginator.count - 1)

        range_start_year = film_roll_list[index_start].from_year
        range_start_month = film_roll_list[index_start].from_month

        range_end_year = film_roll_list[index_end].from_year
        range_end_month = film_roll_list[index_end].from_month

        if range_start_year:
            if range_start_month:
                date_shot_start[p] = f"{range_start_year}-{range_start_month:02d}"
            else:
                date_shot_start[p] = f"{range_start_year}"
        else:
            date_shot_start[p] = "?"

        if range_end_year:
            if range_end_month:
                date_shot_end[p] = f"{range_end_year}-{range_end_month:02d}"
            else:
                date_shot_end[p] = f"{range_end_year}"
        else:
            date_shot_end[p] = "?"

        p += 1

    page_number = request.GET.get("page")
    try:
        page_obj = paginator.get_page(page_number)
    except PageNotAnInteger:
        # If the page parameter is not an integer, show the first page
        page_obj = paginator.get_page(1)
    except EmptyPage:
        # If the page is out of range, show the last page
        page_obj = paginator.page(paginator.num_pages)

    context = {
        "page_obj": page_obj,
        "date_shot_start": year_start,
        "date_shot_end": year_end,
    }
    return render(request, "filmrolls/index.html", context)
  1. The template:
{% load index_tag %}

{# Page navigator: start #}
{% if page_obj.has_other_pages %}
<div class="row">
    <div class="btn-group" role="group" aria-label="Item pagination">
        {% if page_obj.has_previous %}
            <a href="?page={{ page_obj.previous_page_number }}" class="btn btn-outline-primary">&laquo;</a>
        {% endif %}

        {% for page_number in page_obj.paginator.page_range %}
            {% if page_obj.number == page_number %}
                <button class="btn btn-outline-primary active">
                    <span>{{ page_number }} <br />{{ date_shot_start|index:page_number }}<br/>-<br/>{{ date_shot_end|index:page_number }}<span class="sr-only"></span></span>
                </button>
            {% else %}
                {% if page_number == paginator.ELLIPSIS %}
                <button class="btn">
                    <span>{{ paginator.ELLIPSIS }} <span class="sr-only"></span></span>
                </button>
                {% else %}
                <a href="?page={{ page_number }}" class="btn btn-outline-primary">
                    {{ page_number }} <br />{{ date_shot_start|index:page_number }}<br/>-<br/>{{ date_shot_end|index:page_number }}
                </a>
                {% endif %}
            {% endif %}
        {% endfor %}

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}" class="btn btn-outline-primary">&raquo;</a>
        {% endif %}
    </div>
</div>
{% endif %}
{# Page navigator: end #}
  1. Custom index tag (<app>/templatetags/index_tag.py) to index items in a Django template:
from django import template

register = template.Library()


@register.filter
def index(indexable, i):
    return indexable[i]

Upvotes: 0

Vegard
Vegard

Reputation: 4882

The cleanest way is probably to create a custom pagination schema:

from django.core.paginator import Page, Paginator

# Custom page class so you can add the range per page
class MoviePage(Page):
    def __init__(self, object_list, number, paginator):
        super().__init__(object_list, number, paginator)

        # Add the range
        if object_list:
            self.first_item_year = object_list[0].from_year
            self.last_item_year = object_list[-1].from_year

# Custom paginator for the purpose of invoking the custom page class
class MoviePaginator(Paginator):
    def _get_page(self, *args, **kwargs):
        return MoviePage(*args, **kwargs)

The above code will only add the range for the page being loaded into the view. If you're trying to map the range for every page all at once, I'm not sure how feasible that is. Maybe you can .annotate() the queryset with some kind of lookup that selects every n element and adds its from_year attribute in dict with the index of n.

Upvotes: 0

Related Questions