django / django/new-features

Add a mixin for pagination

Open
#120 3 comments 4 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.