Add a mixin for pagination
- Dominant language
- No language data
- Stars
- 188
- Forks
- 7
- PR merge metrics
- No merged PRs in 30d
Description
### Code of Conduct
- [x] I agree to follow Django's Code of Conduct
### Feature Description
A mixin designed for paginating multiple objects outside of Django views. In this case, it applies to Wagtail CMS page models, where the existing MultipleObjectMixin conflicts with the model.
### Problem
This addresses the challenge of paginating multiple objects outside of a Django view context, particularly within the scope of Wagtail page models.
### Request or proposal
request
### Additional Details
_No response_
### Implementation Suggestions
The current MultipleObjectMixin can be refactored to isolate and extract the new mixin.
```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
class PaginationMixin:
"""A mixin for views paginating multiple objects."""
allow_empty = True
paginate_by = None
paginate_orphans = 0
paginator_class = Paginator
page_kwarg = "page"
def paginate_queryset(self, queryset, page_size, *args, **kwargs):
"""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
kw_args = self.kwargs if self.kwargs else kwargs
request = self.request if self.request else kwargs.get("request")
page = kw_args.get(page_kwarg) or 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_pagination_context(self, queryset, *args, **kwargs):
"""Get context for pagination."""
page_size = self.get_paginate_by(queryset)
if page_size:
paginator, page, queryset, is_paginated = self.paginate_queryset(
queryset, page_size, *args, **kwargs
)
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,
}
return context
class MultipleObjectMixin(PaginationMixin, ContextMixin):
"""A mixin for views manipulating multiple objects."""
queryset = None
model = None
context_object_name = None
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 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
context_object_name = self.get_context_object_name(queryset)
context = self.get_pagination_context(queryset, **kwargs)
if context_object_name is not None:
context[context_object_name] = context.get("object_list", queryset)
context.update(kwargs)
return super().get_context_data(**context)
```
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by comparing the proposed PaginationMixin with Django's current MultipleObjectMixin, especially paginate_queryset and get_context_data. Check how the mixin would work for Wagtail page models outside Django views and preserve existing multiple-object behavior. Done means pagination can be reused without the stated model conflict, with behavior verified by relevant tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100