Django pagination: EmptyPage: That page contains no results - python

When using Django CBV ListView with pagination:
class Proposals(ListView):
model = Proposal
ordering = "id"
paginate_by = 10
In the browser, if I provide a page that is out of range, I get an error:
I would like to have a different behaviour: to fallback to the last existing page if the provided page is out of range.
I dug into Django source code paginator.py file and was surprised to find some code that does exactly this:
So using paginator.get_page(page) (and not paginator.page(page)) would be the way to go. However, ListView does not use it as you can see here:
What is the best way to deal with this?
Thanks.

The only solution I found is by overriding the paginate_queryset method.
However I don't like it as I'm forced to rewrite the whole logic while I just want to change a single line.
Open to any better suggestion.
class PermissivePaginationListView(ListView):
def paginate_queryset(self, queryset, page_size):
"""
This is an exact copy of the original method, jut changing `page` to `get_page` method to prevent errors with out of range pages.
This is useful with HTMX, when the last row of the table is deleted, as the current page in URL is not valid anymore because there is no result in it.
"""
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:
raise Http404(_("Page is not “last”, nor can it be converted to an int."))
try:
page = paginator.get_page(page_number)
return (paginator, page, page.object_list, page.has_other_pages())
except InvalidPage as e:
raise Http404(
_("Invalid page (%(page_number)s): %(message)s")
% {"page_number": page_number, "message": str(e)}
)

Related

django - how can I add more to my URL in my views.py?

I have a url, http://127.0.0.1:8000/lesson/riff-lab/1305/pentab-wow/
When a user navigates to the above url, I want to change it to http://127.0.0.1:8000/lesson/riff-lab/1305/pentab-wow/?d:a3ugm6eyko59qhr/pentab-Track_1.js
The appended part is needed in order to load something that I want to load, but the specifics are not important for this question.
Here's what I have tried.
def my_view(request, pk):
context = {}
page = Page.objects.get(pk=pk)
request.GET._mutable = True
request.GET['?d:%s/%s' % (page.dropbox_key, page.dropbox_js_file_name)] = ""
return render(request, template, context)
Also
def my_view(request, pk):
context = {}
page = Page.objects.get(pk=pk)
request.GET = request.GET.copy()
request.GET['?d:%s/%s' % (page.dropbox_key, page.dropbox_js_file_name)] = ""
return render(request, template, context)
These do not change the url.
Can anyone help? Thanks in advance.
You trying to change the aim of a shell that already has hit it's target.
URL comes first, then view routed to it is processed, there is no way to change it without making another request, e.g. returning a redirect response "please open this url now" to the client.
You can easily find it in django docs by this keywords, but what you are trying to do generally doesn't look very reasonable, if you know beforehand how you need to construct url, why change it mid-way or why change it at all if view has all required data? I don't know your context, but it's probable that you need to reconsider your approach.

Why does my django pagination keeps returning the same items infinitely?

I am using django and ajax to create a infinite scroll on my website. I would like it to load more items from the database (that are in the next page) when the user scrolls to the bottom of the page. Everything is working perfectly except that it keeps returning the first page's items infinitely. For example if a user scrolls down to the end of a page instead of adding page 2's elements it just add the same elements from the first page to the bottom again. I am very sure this issue is from my views.py. I can't seem to figure out what's wrong.
def feed(request):
queryset2 = Store_detail.objects.filter(store_lat__gte=lat1, store_lat__lte=lat2)\
.filter(store_lng__gte=lng1, store_lng__lte=lng2)
queryset3 = Paginator(queryset2, 4)
page = request.GET.get('page',1)
try:
queryset = queryset3.page(1)
except PageNotAnInteger:
queryset = queryset3.page(page)
except EmptyPage:
queryset = ""
context = {
"location":location,
"queryset":queryset,
}
# return HttpResponse(template.render(context,request))
return render(request, 'main/feed.html', {'queryset': queryset,
'location':location,})
So basically I would like to load the next page when a user scrolls to the end of the screen and if there are no more items in the next page or the next page does not exist then stop adding items.
The pagination logic is a bit off. You paginate with:
try:
# you first try to retrieve page 1
queryset = queryset3.page(1)
except PageNotAnInteger:
queryset = queryset3.page(page)
except EmptyPage:
queryset = ""
This thus means that you first aim to fetch the first page, and only if 1 is not an integer, you will fetch a page with the page querystring parameter. You should swap these, like:
try:
queryset = queryset3.page(page)
except PageNotAnInteger:
queryset = queryset3.page(1)
except EmptyPage:
queryset = Store_detail.objects.none()

How can i get object_list in django middleware

I am writing one middleware class to handle pagination in django.
I am getting issue when user delete entry in ListView and page number is lost. So, i have to check how many pages for that request and adjust page number to reduce issue 404 error. I can get ClassBase Name, model name but cant get object_list data.
my code is:
url = request.path
resolver_match = resolve(url)
func = resolver_match.func
module = func.__module__
view_name = func.__name__
clss = get_class( '{0}.{1}'.format( module, view_name ) )
I want to count the current page of that request.
Please suggest to get it.
Thanks,
ThanhTruong
It would probably be better to handle pagination in a base view or a mixin. See ListView for an example.

Template tag for conditionally rendering or including another template if the path or paths exist

I am implementing a generic dropdown menu within a Django application. In most cases, the pages are generic and they have simple generic 'children' menus. However in some cases, I want to be able to include or render a special set of children, or custom content from a completely different template, based upon the page slug (page.slug).
I have considered implementing an inclusion tag but I am not sure about how to do this in a template tag. I know in views you can implement it like so:
blog_post = get_object_or_404(blog_posts, slug=slug)
...
templates = [u"blog/blog_post_detail_%s.html" % str(slug), template]
return render(request, templates, context)
My design pattern for the tag would be simple:
Look within pages/SLUG/dropdown.html
Try pages/dropdown_SLUG.html
Render nothing if none of those files exist.
How can I do this from a template tag? Is an inclusion tag the right way, or a render_tag?
This is the solution I came up with:
#register.render_tag
def page_menu_special(context, token):
page = None
template_name = None
menu_templates = []
parts = token.split_contents()[1:]
for part in parts:
part = Variable(part).resolve(context)
if isinstance(part, str):
template_name = part
elif isinstance(part, Page) or isinstance(part, CustomPage):
page = part
if page and page.slug:
page_template = str(page.slug) if page.slug != home_slug() else "index"
method_template = page.get_content_model().get_template_name()
menu_templates.extend([
u'pages/menus/dropdown_%s.html' % (page_template),
u'pages/menus/dropdown/%s.html' % (page_template),
])
if method_template:
menu_templates.append(method_template)
if template_name:
menu_templates.insert(0, template_name)
try:
t = select_template(menu_templates)
return t.render(Context(context))
except TemplateDoesNotExist:
return u''
It is rather specific to Mezzanine, but the logic can be reused:
Build paths to potential templates.
Pass the list to select_template
Catch exception if none exist, and return empty or suitable rendered content.
I'm still trying to figure out how to default to another template tag, from within this definition. So I can call page_menu_special and if this one fails, it reverts back to a call to page_menu with the same arguments and that saves us an if/else block.

How to cache a paginated Django queryset

How do you cache a paginated Django queryset, specifically in a ListView?
I noticed one query was taking a long time to run, so I'm attempting to cache it. The queryset is huge (over 100k records), so I'm attempting to only cache paginated subsections of it. I can't cache the entire view or template because there are sections that are user/session specific and need to change constantly.
ListView has a couple standard methods for retrieving the queryset, get_queryset(), which returns the non-paginated data, and paginate_queryset(), which filters it by the current page.
I first tried caching the query in get_queryset(), but quickly realized calling cache.set(my_query_key, super(MyView, self).get_queryset()) was causing the entire query to be serialized.
So then I tried overriding paginate_queryset() like:
import time
from functools import partial
from django.core.cache import cache
from django.views.generic import ListView
class MyView(ListView):
...
def paginate_queryset(self, queryset, page_size):
cache_key = 'myview-queryset-%s-%s' % (self.page, page_size)
print 'paginate_queryset.cache_key:',cache_key
t0 = time.time()
ret = cache.get(cache_key)
if ret is None:
print 're-caching'
ret = super(MyView, self).paginate_queryset(queryset, page_size)
cache.set(cache_key, ret, 60*60)
td = time.time() - t0
print 'paginate_queryset.time.seconds:',td
(paginator, page, object_list, other_pages) = ret
print 'total objects:',len(object_list)
return ret
However, this takes almost a minute to run, even though only 10 objects are retrieved, and every requests shows "re-caching", implying nothing is being saved to cache.
My settings.CACHE looks like:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': '127.0.0.1:11211',
}
}
and service memcached status shows memcached is running and tail -f /var/log/memcached.log shows absolutely nothing.
What am I doing wrong? What is the proper way to cache a paginated query so that the entire queryset isn't retrieved?
Edit: I think their may be a bug in either memcached or the Python wrapper. Django appears to support two different memcached backends, one using python-memcached and one using pylibmc. The python-memcached seems to silently hide the error caching the paginate_queryset() value. When I switched to the pylibmc backend, now I get an explicit error message "error 10 from memcached_set: SERVER ERROR" tracing back to django/core/cache/backends/memcached.py in set, line 78.
You can extend the Paginator to support caching by a provided cache_key.
A blog post about usage and implementation of a such CachedPaginator can be found here. The source code is posted at djangosnippets.org (here is a web-acrhive link because the original is not working).
However I will post a slightly modificated example from the original version, which can not only cache objects per page, but the total count too. (sometimes even the count can be an expensive operation).
from django.core.cache import cache
from django.utils.functional import cached_property
from django.core.paginator import Paginator, Page, PageNotAnInteger
class CachedPaginator(Paginator):
"""A paginator that caches the results on a page by page basis."""
def __init__(self, object_list, per_page, orphans=0, allow_empty_first_page=True, cache_key=None, cache_timeout=300):
super(CachedPaginator, self).__init__(object_list, per_page, orphans, allow_empty_first_page)
self.cache_key = cache_key
self.cache_timeout = cache_timeout
#cached_property
def count(self):
"""
The original django.core.paginator.count attribute in Django1.8
is not writable and cant be setted manually, but we would like
to override it when loading data from cache. (instead of recalculating it).
So we make it writable via #cached_property.
"""
return super(CachedPaginator, self).count
def set_count(self, count):
"""
Override the paginator.count value (to prevent recalculation)
and clear num_pages and page_range which values depend on it.
"""
self.count = count
# if somehow we have stored .num_pages or .page_range (which are cached properties)
# this can lead to wrong page calculations (because they depend on paginator.count value)
# so we clear their values to force recalculations on next calls
try:
del self.num_pages
except AttributeError:
pass
try:
del self.page_range
except AttributeError:
pass
#cached_property
def num_pages(self):
"""This is not writable in Django1.8. We want to make it writable"""
return super(CachedPaginator, self).num_pages
#cached_property
def page_range(self):
"""This is not writable in Django1.8. We want to make it writable"""
return super(CachedPaginator, self).page_range
def page(self, number):
"""
Returns a Page object for the given 1-based page number.
This will attempt to pull the results out of the cache first, based on
the requested page number. If not found in the cache,
it will pull a fresh list and then cache that result + the total result count.
"""
if self.cache_key is None:
return super(CachedPaginator, self).page(number)
# In order to prevent counting the queryset
# we only validate that the provided number is integer
# The rest of the validation will happen when we fetch fresh data.
# so if the number is invalid, no cache will be setted
# number = self.validate_number(number)
try:
number = int(number)
except (TypeError, ValueError):
raise PageNotAnInteger('That page number is not an integer')
page_cache_key = "%s:%s:%s" % (self.cache_key, self.per_page, number)
page_data = cache.get(page_cache_key)
if page_data is None:
page = super(CachedPaginator, self).page(number)
#cache not only the objects, but the total count too.
page_data = (page.object_list, self.count)
cache.set(page_cache_key, page_data, self.cache_timeout)
else:
cached_object_list, cached_total_count = page_data
self.set_count(cached_total_count)
page = Page(cached_object_list, number, self)
return page
The problem turned out to be a combination of factors. Mainly, the result returned by the paginate_queryset() contains a reference to the unlimited queryset, meaning it's essentially uncachable. When I called cache.set(mykey, (paginator, page, object_list, other_pages)), it was trying to serialize thousands of records instead of just the page_size number of records I was expecting, causing the cached item to exceed memcached's limits and fail.
The other factor was the horrible default error reporting in the memcached/python-memcached, which silently hides all errors and turns cache.set() into a nop if anything goes wrong, making it very time-consuming to track down the problem.
I fixed this by essentially rewriting paginate_queryset() to ditch Django's builtin paginator functionality altogether and calculate the queryset myself with:
object_list = queryset[page_size*(page-1):page_size*(page-1)+page_size]
and then caching that object_list.
I wanted to paginate my infinite scrolling view on my home page and this is the solution I came up with. It's a mix of Django CCBVs and the author's initial solution.
The response times, however, didn't improve as much as I would've hoped for but that's probably because I am testing it on my local with just 6 posts and 2 users haha.
# Import
from django.core.cache import cache
from django.core.paginator import InvalidPage
from django.views.generic.list import ListView
from django.http Http404
class MyListView(ListView):
template_name = 'MY TEMPLATE NAME'
model = MY POST MODEL
paginate_by = 10
def paginate_queryset(self, queryset, page_size):
"""Paginate the queryset"""
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:
raise Http404(_("Page is not 'last', nor can it be converted to an int."))
try:
page = paginator.page(page_number)
cache_key = 'mylistview-%s-%s' % (page_number, page_size)
retreive_cache = cache.get(cache_key)
if retreive_cache is None:
print('re-caching')
retreive_cache = super(MyListView, self).paginate_queryset(queryset, page_size)
# Caching for 1 day
cache.set(cache_key, retreive_cache, 86400)
return retreive_cache
except InvalidPage as e:
raise Http404(_('Invalid page (%(page_number)s): %(message)s') % {
'page_number': page_number,
'message': str(e)
})

Categories

Resources