我设法创建了一个自定义分页器,它显示当前页面编号、一个下一步按钮和一个显示完整计数链接。如果需要,它允许使用原始分页器。
使用的技巧是per_page + 1
从 db 中获取元素,以查看是否有更多元素,然后提供虚假计数。
假设我们想要第三页并且页面有 25 个元素 => 我们想要object_list[50:75]
。调用查询集时Paginator.count
,将对查询集进行评估object_list[50:76]
(请注意,我们采用75+1 个元素),然后如果我们从 db 获得 25+1 个元素,则返回计数为 76,如果没有收到 26,则返回 50 + 收到的元素数元素。
TL;DR:我为以下内容创建了一个 mixin ModelAdmin
:
from django.core.paginator import Paginator
from django.utils.functional import cached_property
class FastCountPaginator(Paginator):
"""A faster paginator implementation than the Paginator. Paginator is slow
mainly because QuerySet.count() is expensive on large queries.
The idea is to use the requested page to generate a 'fake' count. In
order to see if the page is the final one it queries n+1 elements
from db then reports the count as page_number * per_page + received_elements.
"""
use_fast_pagination = True
def __init__(self, page_number, *args, **kwargs):
self.page_number = page_number
super(FastCountPaginator, self).__init__(*args, **kwargs)
@cached_property
def count(self):
# Populate the object list when count is called. As this is a cached property,
# it will be called only once per instance
return self.populate_object_list()
def page(self, page_number):
"""Return a Page object for the given 1-based page number."""
page_number = self.validate_number(page_number)
return self._get_page(self.object_list, page_number, self)
def populate_object_list(self):
# converts queryset object_list to a list and return the number of elements until there
# the trick is to get per_page elements + 1 in order to see if the next page exists.
bottom = self.page_number * self.per_page
# get one more object than needed to see if we should show next page
top = bottom + self.per_page + 1
object_list = list(self.object_list[bottom:top])
# not the last page
if len(object_list) == self.per_page + 1:
object_list = object_list[:-1]
else:
top = bottom + len(object_list)
self.object_list = object_list
return top
class ModelAdminFastPaginationMixin:
show_full_result_count = False # prevents root_queryset.count() call
def changelist_view(self, request, extra_context=None):
# strip count_all query parameter from the request before it is processed
# this allows all links to be generated like this parameter was not present and without raising errors
request.GET = request.GET.copy()
request.GET.paginator_count_all = request.GET.pop('count_all', False)
return super().changelist_view(request, extra_context)
def get_paginator(self, request, queryset, per_page, orphans=0, allow_empty_first_page=True):
# use the normal paginator if we want to count all the ads
if hasattr(request.GET, 'paginator_count_all') and request.GET.paginator_count_all:
return Paginator(queryset, per_page, orphans, allow_empty_first_page)
page = self._validate_page_number(request.GET.get('p', '0'))
return FastCountPaginator(page, queryset, per_page, orphans, allow_empty_first_page)
def _validate_page_number(self, number):
# taken from Paginator.validate_number and adjusted
try:
if isinstance(number, float) and not number.is_integer():
raise ValueError
number = int(number)
except (TypeError, ValueError):
return 0
if number < 1:
number = 0
return number
pagination.html
模板:
{% if cl and cl.paginator and cl.paginator.use_fast_pagination %}
{# Fast paginator with only next button and show the total number of results#}
{% load admin_list %}
{% load i18n %}
{% load admin_templatetags %}
<p class="paginator">
{% if pagination_required %}
{% for i in page_range %}
{% if forloop.last %}
{% fast_paginator_number cl i 'Next' %}
{% else %}
{% fast_paginator_number cl i %}
{% endif %}
{% endfor %}
{% endif %}
{% show_count_all_link cl "showall" %}
</p>
{% else %}
{# use the default pagination template if we are not using the FastPaginator #}
{% include "admin/pagination.html" %}
{% endif %}
和使用的模板标签:
from django import template
from django.contrib.admin.views.main import PAGE_VAR
from django.utils.html import format_html
from django.utils.safestring import mark_safe
register = template.Library()
DOT = '.'
@register.simple_tag
def fast_paginator_number(cl, i, text_display=None):
"""Generate an individual page index link in a paginated list.
Allows to change the link text by setting text_display
"""
if i == DOT:
return '… '
elif i == cl.page_num:
return format_html('<span class="this-page">{}</span> ', i + 1)
else:
return format_html(
'<a href="{}"{}>{}</a> ',
cl.get_query_string({PAGE_VAR: i}),
mark_safe(' class="end"' if i == cl.paginator.num_pages - 1 else ''),
text_display if text_display else i + 1,
)
@register.simple_tag
def show_count_all_link(cl, css_class='', text_display='Show the total number of results'):
"""Generate a button that toggles between FastPaginator and the normal
Paginator."""
return format_html(
'<a href="{}"{}>{}</a> ',
cl.get_query_string({PAGE_VAR: cl.page_num, 'count_all': True}),
mark_safe(f' class="{css_class}"' if css_class else ''),
text_display,
)
你可以这样使用它:
class MyVeryLargeModelAdmin(ModelAdminFastPaginationMixin, admin.ModelAdmin):
# ...
或者更简单的版本,不显示Next按钮和Show the total number of results:
from django.core.paginator import Paginator
from django.utils.functional import cached_property
class FastCountPaginator(Paginator):
"""A faster paginator implementation than the Paginator. Paginator is slow
mainly because QuerySet.count() is expensive on large queries.
The idea is to use the requested page to generate a 'fake' count. In
order to see if the page is the final one it queries n+1 elements
from db then reports the count as page_number * per_page + received_elements.
"""
use_fast_pagination = True
def __init__(self, page_number, *args, **kwargs):
self.page_number = page_number
super(FastCountPaginator, self).__init__(*args, **kwargs)
@cached_property
def count(self):
# Populate the object list when count is called. As this is a cached property,
# it will be called only once per instance
return self.populate_object_list()
def page(self, page_number):
"""Return a Page object for the given 1-based page number."""
page_number = self.validate_number(page_number)
return self._get_page(self.object_list, page_number, self)
def populate_object_list(self):
# converts queryset object_list to a list and return the number of elements until there
# the trick is to get per_page elements + 1 in order to see if the next page exists.
bottom = self.page_number * self.per_page
# get one more object than needed to see if we should show next page
top = bottom + self.per_page + 1
object_list = list(self.object_list[bottom:top])
# not the last page
if len(object_list) == self.per_page + 1:
object_list = object_list[:-1]
else:
top = bottom + len(object_list)
self.object_list = object_list
return top
class ModelAdminFastPaginationMixin:
show_full_result_count = False # prevents root_queryset.count() call
def get_paginator(self, request, queryset, per_page, orphans=0, allow_empty_first_page=True):
page = self._validate_page_number(request.GET.get('p', '0'))
return FastCountPaginator(page, queryset, per_page, orphans, allow_empty_first_page)
def _validate_page_number(self, number):
# taken from Paginator.validate_number and adjusted
try:
if isinstance(number, float) and not number.is_integer():
raise ValueError
number = int(number)
except (TypeError, ValueError):
return 0
if number < 1:
number = 0
return number