'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:
- subclass
ListView
and overridepaginate_queryset
(inherited fromMultipleObjectMixin
), or - subclass
Paginator
and overridevalidate_number
, and setpaginator_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 |