221 lines
7.8 KiB
Python
221 lines
7.8 KiB
Python
from django.core.exceptions import ImproperlyConfigured
|
|
from django.core.paginator import InvalidPage, Paginator
|
|
from django.db.models import QuerySet
|
|
from django.http import Http404
|
|
from django.utils.translation import gettext as _
|
|
from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
|
|
|
|
|
|
class MultipleObjectMixin(ContextMixin):
|
|
"""A mixin for views manipulating multiple objects."""
|
|
|
|
allow_empty = True
|
|
queryset = None
|
|
model = None
|
|
paginate_by = None
|
|
paginate_orphans = 0
|
|
context_object_name = None
|
|
paginator_class = Paginator
|
|
page_kwarg = "page"
|
|
ordering = None
|
|
|
|
def get_queryset(self):
|
|
"""
|
|
Return the list of items for this view.
|
|
|
|
The return value must be an iterable and may be an instance of
|
|
`QuerySet` in which case `QuerySet` specific behavior will be enabled.
|
|
"""
|
|
if self.queryset is not None:
|
|
queryset = self.queryset
|
|
if isinstance(queryset, QuerySet):
|
|
queryset = queryset.all()
|
|
elif self.model is not None:
|
|
queryset = self.model._default_manager.all()
|
|
else:
|
|
raise ImproperlyConfigured(
|
|
"%(cls)s is missing a QuerySet. Define "
|
|
"%(cls)s.model, %(cls)s.queryset, or override "
|
|
"%(cls)s.get_queryset()." % {"cls": self.__class__.__name__}
|
|
)
|
|
ordering = self.get_ordering()
|
|
if ordering:
|
|
if isinstance(ordering, str):
|
|
ordering = (ordering,)
|
|
queryset = queryset.order_by(*ordering)
|
|
|
|
return queryset
|
|
|
|
def get_ordering(self):
|
|
"""Return the field or fields to use for ordering the queryset."""
|
|
return self.ordering
|
|
|
|
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:
|
|
raise Http404(
|
|
_("Page is not “last”, nor can it be converted to an int.")
|
|
)
|
|
try:
|
|
page = paginator.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)}
|
|
)
|
|
|
|
def get_paginate_by(self, queryset):
|
|
"""
|
|
Get the number of items to paginate by, or ``None`` for no pagination.
|
|
"""
|
|
return self.paginate_by
|
|
|
|
def get_paginator(
|
|
self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs
|
|
):
|
|
"""Return an instance of the paginator for this view."""
|
|
return self.paginator_class(
|
|
queryset,
|
|
per_page,
|
|
orphans=orphans,
|
|
allow_empty_first_page=allow_empty_first_page,
|
|
**kwargs,
|
|
)
|
|
|
|
def get_paginate_orphans(self):
|
|
"""
|
|
Return the maximum number of orphans extend the last page by when
|
|
paginating.
|
|
"""
|
|
return self.paginate_orphans
|
|
|
|
def get_allow_empty(self):
|
|
"""
|
|
Return ``True`` if the view should display empty lists and ``False``
|
|
if a 404 should be raised instead.
|
|
"""
|
|
return self.allow_empty
|
|
|
|
def get_context_object_name(self, object_list):
|
|
"""Get the name of the item to be used in the context."""
|
|
if self.context_object_name:
|
|
return self.context_object_name
|
|
elif hasattr(object_list, "model"):
|
|
return "%s_list" % object_list.model._meta.model_name
|
|
else:
|
|
return None
|
|
|
|
def get_context_data(self, *, object_list=None, **kwargs):
|
|
"""Get the context for this view."""
|
|
queryset = object_list if object_list is not None else self.object_list
|
|
page_size = self.get_paginate_by(queryset)
|
|
context_object_name = self.get_context_object_name(queryset)
|
|
if page_size:
|
|
paginator, page, queryset, is_paginated = self.paginate_queryset(
|
|
queryset, page_size
|
|
)
|
|
context = {
|
|
"paginator": paginator,
|
|
"page_obj": page,
|
|
"is_paginated": is_paginated,
|
|
"object_list": queryset,
|
|
}
|
|
else:
|
|
context = {
|
|
"paginator": None,
|
|
"page_obj": None,
|
|
"is_paginated": False,
|
|
"object_list": queryset,
|
|
}
|
|
if context_object_name is not None:
|
|
context[context_object_name] = queryset
|
|
context.update(kwargs)
|
|
return super().get_context_data(**context)
|
|
|
|
|
|
class BaseListView(MultipleObjectMixin, View):
|
|
"""A base view for displaying a list of objects."""
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
self.object_list = self.get_queryset()
|
|
allow_empty = self.get_allow_empty()
|
|
|
|
if not allow_empty:
|
|
# When pagination is enabled and object_list is a queryset,
|
|
# it's better to do a cheap query than to load the unpaginated
|
|
# queryset in memory.
|
|
if self.get_paginate_by(self.object_list) is not None and hasattr(
|
|
self.object_list, "exists"
|
|
):
|
|
is_empty = not self.object_list.exists()
|
|
else:
|
|
is_empty = not self.object_list
|
|
if is_empty:
|
|
raise Http404(
|
|
_("Empty list and “%(class_name)s.allow_empty” is False.")
|
|
% {
|
|
"class_name": self.__class__.__name__,
|
|
}
|
|
)
|
|
context = self.get_context_data()
|
|
return self.render_to_response(context)
|
|
|
|
|
|
class MultipleObjectTemplateResponseMixin(TemplateResponseMixin):
|
|
"""Mixin for responding with a template and list of objects."""
|
|
|
|
template_name_suffix = "_list"
|
|
|
|
def get_template_names(self):
|
|
"""
|
|
Return a list of template names to be used for the request. Must return
|
|
a list. May not be called if render_to_response is overridden.
|
|
"""
|
|
try:
|
|
names = super().get_template_names()
|
|
except ImproperlyConfigured:
|
|
# If template_name isn't specified, it's not a problem --
|
|
# we just start with an empty list.
|
|
names = []
|
|
|
|
# If the list is a queryset, we'll invent a template name based on the
|
|
# app and model name. This name gets put at the end of the template
|
|
# name list so that user-supplied names override the automatically-
|
|
# generated ones.
|
|
if hasattr(self.object_list, "model"):
|
|
opts = self.object_list.model._meta
|
|
names.append(
|
|
"%s/%s%s.html"
|
|
% (opts.app_label, opts.model_name, self.template_name_suffix)
|
|
)
|
|
elif not names:
|
|
raise ImproperlyConfigured(
|
|
"%(cls)s requires either a 'template_name' attribute "
|
|
"or a get_queryset() method that returns a QuerySet."
|
|
% {
|
|
"cls": self.__class__.__name__,
|
|
}
|
|
)
|
|
return names
|
|
|
|
|
|
class ListView(MultipleObjectTemplateResponseMixin, BaseListView):
|
|
"""
|
|
Render some list of objects, set by `self.model` or `self.queryset`.
|
|
`self.queryset` can actually be any iterable of items, not just a queryset.
|
|
"""
|