list.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. from django.core.exceptions import ImproperlyConfigured
  2. from django.core.paginator import InvalidPage, Paginator
  3. from django.db.models import QuerySet
  4. from django.http import Http404
  5. from django.utils.translation import gettext as _
  6. from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
  7. class MultipleObjectMixin(ContextMixin):
  8. """A mixin for views manipulating multiple objects."""
  9. allow_empty = True
  10. queryset = None
  11. model = None
  12. paginate_by = None
  13. paginate_orphans = 0
  14. context_object_name = None
  15. paginator_class = Paginator
  16. page_kwarg = "page"
  17. ordering = None
  18. def get_queryset(self):
  19. """
  20. Return the list of items for this view.
  21. The return value must be an iterable and may be an instance of
  22. `QuerySet` in which case `QuerySet` specific behavior will be enabled.
  23. """
  24. if self.queryset is not None:
  25. queryset = self.queryset
  26. if isinstance(queryset, QuerySet):
  27. queryset = queryset.all()
  28. elif self.model is not None:
  29. queryset = self.model._default_manager.all()
  30. else:
  31. raise ImproperlyConfigured(
  32. "%(cls)s is missing a QuerySet. Define "
  33. "%(cls)s.model, %(cls)s.queryset, or override "
  34. "%(cls)s.get_queryset()." % {"cls": self.__class__.__name__}
  35. )
  36. ordering = self.get_ordering()
  37. if ordering:
  38. if isinstance(ordering, str):
  39. ordering = (ordering,)
  40. queryset = queryset.order_by(*ordering)
  41. return queryset
  42. def get_ordering(self):
  43. """Return the field or fields to use for ordering the queryset."""
  44. return self.ordering
  45. def paginate_queryset(self, queryset, page_size):
  46. """Paginate the queryset, if needed."""
  47. paginator = self.get_paginator(
  48. queryset,
  49. page_size,
  50. orphans=self.get_paginate_orphans(),
  51. allow_empty_first_page=self.get_allow_empty(),
  52. )
  53. page_kwarg = self.page_kwarg
  54. page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1
  55. try:
  56. page_number = int(page)
  57. except ValueError:
  58. if page == "last":
  59. page_number = paginator.num_pages
  60. else:
  61. raise Http404(
  62. _("Page is not “last”, nor can it be converted to an int.")
  63. )
  64. try:
  65. page = paginator.page(page_number)
  66. return (paginator, page, page.object_list, page.has_other_pages())
  67. except InvalidPage as e:
  68. raise Http404(
  69. _("Invalid page (%(page_number)s): %(message)s")
  70. % {"page_number": page_number, "message": str(e)}
  71. )
  72. def get_paginate_by(self, queryset):
  73. """
  74. Get the number of items to paginate by, or ``None`` for no pagination.
  75. """
  76. return self.paginate_by
  77. def get_paginator(
  78. self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs
  79. ):
  80. """Return an instance of the paginator for this view."""
  81. return self.paginator_class(
  82. queryset,
  83. per_page,
  84. orphans=orphans,
  85. allow_empty_first_page=allow_empty_first_page,
  86. **kwargs,
  87. )
  88. def get_paginate_orphans(self):
  89. """
  90. Return the maximum number of orphans extend the last page by when
  91. paginating.
  92. """
  93. return self.paginate_orphans
  94. def get_allow_empty(self):
  95. """
  96. Return ``True`` if the view should display empty lists and ``False``
  97. if a 404 should be raised instead.
  98. """
  99. return self.allow_empty
  100. def get_context_object_name(self, object_list):
  101. """Get the name of the item to be used in the context."""
  102. if self.context_object_name:
  103. return self.context_object_name
  104. elif hasattr(object_list, "model"):
  105. return "%s_list" % object_list.model._meta.model_name
  106. else:
  107. return None
  108. def get_context_data(self, *, object_list=None, **kwargs):
  109. """Get the context for this view."""
  110. queryset = object_list if object_list is not None else self.object_list
  111. page_size = self.get_paginate_by(queryset)
  112. context_object_name = self.get_context_object_name(queryset)
  113. if page_size:
  114. paginator, page, queryset, is_paginated = self.paginate_queryset(
  115. queryset, page_size
  116. )
  117. context = {
  118. "paginator": paginator,
  119. "page_obj": page,
  120. "is_paginated": is_paginated,
  121. "object_list": queryset,
  122. }
  123. else:
  124. context = {
  125. "paginator": None,
  126. "page_obj": None,
  127. "is_paginated": False,
  128. "object_list": queryset,
  129. }
  130. if context_object_name is not None:
  131. context[context_object_name] = queryset
  132. context.update(kwargs)
  133. return super().get_context_data(**context)
  134. class BaseListView(MultipleObjectMixin, View):
  135. """A base view for displaying a list of objects."""
  136. def get(self, request, *args, **kwargs):
  137. self.object_list = self.get_queryset()
  138. allow_empty = self.get_allow_empty()
  139. if not allow_empty:
  140. # When pagination is enabled and object_list is a queryset,
  141. # it's better to do a cheap query than to load the unpaginated
  142. # queryset in memory.
  143. if self.get_paginate_by(self.object_list) is not None and hasattr(
  144. self.object_list, "exists"
  145. ):
  146. is_empty = not self.object_list.exists()
  147. else:
  148. is_empty = not self.object_list
  149. if is_empty:
  150. raise Http404(
  151. _("Empty list and “%(class_name)s.allow_empty” is False.")
  152. % {
  153. "class_name": self.__class__.__name__,
  154. }
  155. )
  156. context = self.get_context_data()
  157. return self.render_to_response(context)
  158. class MultipleObjectTemplateResponseMixin(TemplateResponseMixin):
  159. """Mixin for responding with a template and list of objects."""
  160. template_name_suffix = "_list"
  161. def get_template_names(self):
  162. """
  163. Return a list of template names to be used for the request. Must return
  164. a list. May not be called if render_to_response is overridden.
  165. """
  166. try:
  167. names = super().get_template_names()
  168. except ImproperlyConfigured:
  169. # If template_name isn't specified, it's not a problem --
  170. # we just start with an empty list.
  171. names = []
  172. # If the list is a queryset, we'll invent a template name based on the
  173. # app and model name. This name gets put at the end of the template
  174. # name list so that user-supplied names override the automatically-
  175. # generated ones.
  176. if hasattr(self.object_list, "model"):
  177. opts = self.object_list.model._meta
  178. names.append(
  179. "%s/%s%s.html"
  180. % (opts.app_label, opts.model_name, self.template_name_suffix)
  181. )
  182. elif not names:
  183. raise ImproperlyConfigured(
  184. "%(cls)s requires either a 'template_name' attribute "
  185. "or a get_queryset() method that returns a QuerySet."
  186. % {
  187. "cls": self.__class__.__name__,
  188. }
  189. )
  190. return names
  191. class ListView(MultipleObjectTemplateResponseMixin, BaseListView):
  192. """
  193. Render some list of objects, set by `self.model` or `self.queryset`.
  194. `self.queryset` can actually be any iterable of items, not just a queryset.
  195. """