'Display last page of paginated results instead of 404 using ListView

The Django docs show how to return the last page of a paginated queryset using a function-based view by catching the EmptyPage exception.

What's the easiest way to achieve the same thing using generic class-based views, for example ListView?

I first thought that the allow_empty setting for MultipleObjectMixin would do what I need, but examining the code shows that it only prevents a 404 error if there are zero objects in the queryset, rather than zero objects on the page requested.

Two options seem to be:

  1. subclass ListView and override paginate_queryset (inherited from MultipleObjectMixin), or
  2. subclass Paginator and override validate_number, and set paginator_class to the subclass in the view.

Is there a better way to achieve this?



Solution 1:[1]

Here's what option 2 looks like:

from django.core.paginator import EmptyPage, Paginator
from django.views.generic import ListView

class SafePaginator(Paginator):
    def validate_number(self, number):
        try:
            return super(SafePaginator, self).validate_number(number)
        except EmptyPage:
            if number > 1:
                return self.num_pages
            else:
                raise

class MyView(ListView):
    paginator_class = SafePaginator
    paginate_by = 25

    [...]

This seems like the best option to me at the moment.

Solution 2:[2]

This is very old i know, but i needed it and i went for the first option Here is how 1st option looks like:

class MyView(ListView):
    .....
    .....
    def paginate_queryset(self, queryset, page_size):
        """Paginate the queryset, if needed."""
        paginator = self.get_paginator(
            queryset, page_size, orphans=self.get_paginate_orphans(),
            allow_empty_first_page=self.get_allow_empty())
        page_kwarg = self.page_kwarg
        page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1
        try:
            page_number = int(page)
        except ValueError:
            if page == 'last':
                page_number = paginator.num_pages
            else:
                # This was modified to prevent value error related stuff
                page_number = 1
        try:
            page = paginator.page(page_number)
            return paginator, page, page.object_list, page.has_other_pages()
        except InvalidPage as e:
            # this is where you should do the invalid Page error handling
            if page_number < 1:
                page_number = 1
            else:
                page_number = paginator.num_pages
            page = paginator.page(page_number)
            return paginator, page, page.object_list, page.has_other_pages()

TBH this seems very verbose, if there is a better option i ll vouch for it

Solution 3:[3]

If you use TimB's answer, and you need or want to redirect to the last valid page, here is one way of doing that:

implement a get (or post) method in the relevant view if you haven't already.
Here is mine (with placeholder variable names):

    def get(self, *args, **kwargs):
        result = super(YourClassHere, self).get(*args, **kwargs)
        # get the current page number if present
        page = int(self.request.GET.get("page")) if self.request.GET.get("page") else 1

        # get the maximum page number
        total_page = result.context_data['context_item_that_conatins_the_paginator'][0].num_pages

        if page > total_page:
            # make an http response
            response = redirect(reverse('namespace:pageName'))

            # set the page
            response['Location'] += f'?page={total_page}'

            # Maintain get variables if present 
            for var_title, var_data in self.request.GET.items():
                if var_title != "page":
                    response['Location'] += f"&{var_title}={var_data}"

            return response
        return result

in order to define the 'context_item_that_conatins_the_paginator' you will probably need to implement get_context_data as well if you haven't already

also, depending on your implementation your paginator may be in an entirely different part of your context_data - and you might have to use a debugger or other methods to see what's in it in order to find the num_pages variable

note: this method was done with Django 4.0

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 TimB
Solution 2 fire bombahakuna
Solution 3