diff --git a/django/views/generic/dates.py b/django/views/generic/dates.py index b80b7ffd18..602aa8d14b 100644 --- a/django/views/generic/dates.py +++ b/django/views/generic/dates.py @@ -343,7 +343,14 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View): """ raise NotImplementedError('A DateView must provide an implementation of get_dated_items()') - def get_dated_queryset(self, ordering=None, **lookup): + def get_ordering(self): + """ + Returns the field or fields to use for ordering the queryset; uses the + date field by default. + """ + return '-%s' % self.get_date_field() if self.ordering is None else self.ordering + + def get_dated_queryset(self, **lookup): """ Get a queryset properly filtered according to `allow_future` and any extra lookup kwargs. @@ -354,9 +361,6 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View): allow_empty = self.get_allow_empty() paginate_by = self.get_paginate_by(qs) - if ordering is not None: - qs = qs.order_by(ordering) - if not allow_future: now = timezone.now() if self.uses_datetime_field else timezone_today() qs = qs.filter(**{'%s__lte' % date_field: now}) @@ -412,7 +416,7 @@ class BaseArchiveIndexView(BaseDateListView): """ Return (date_list, items, extra_context) for this request. """ - qs = self.get_dated_queryset(ordering='-%s' % self.get_date_field()) + qs = self.get_dated_queryset() date_list = self.get_date_list(qs, ordering='DESC') if not date_list: @@ -451,7 +455,7 @@ class BaseYearArchiveView(YearMixin, BaseDateListView): '%s__lt' % date_field: until, } - qs = self.get_dated_queryset(ordering='-%s' % date_field, **lookup_kwargs) + qs = self.get_dated_queryset(**lookup_kwargs) date_list = self.get_date_list(qs) if not self.get_make_object_list(): diff --git a/django/views/generic/list.py b/django/views/generic/list.py index 0df46ec8f4..2243bcec4a 100644 --- a/django/views/generic/list.py +++ b/django/views/generic/list.py @@ -4,6 +4,7 @@ from django.core.paginator import Paginator, InvalidPage from django.core.exceptions import ImproperlyConfigured from django.db.models.query import QuerySet from django.http import Http404 +from django.utils import six from django.utils.translation import ugettext as _ from django.views.generic.base import TemplateResponseMixin, ContextMixin, View @@ -20,6 +21,7 @@ class MultipleObjectMixin(ContextMixin): context_object_name = None paginator_class = Paginator page_kwarg = 'page' + ordering = None def get_queryset(self): """ @@ -42,8 +44,20 @@ class MultipleObjectMixin(ContextMixin): 'cls': self.__class__.__name__ } ) + ordering = self.get_ordering() + if ordering: + if isinstance(ordering, six.string_types): + 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. diff --git a/docs/ref/class-based-views/flattened-index.txt b/docs/ref/class-based-views/flattened-index.txt index be14a34a7c..8f7c743dd4 100644 --- a/docs/ref/class-based-views/flattened-index.txt +++ b/docs/ref/class-based-views/flattened-index.txt @@ -118,6 +118,7 @@ ListView * :attr:`~django.views.generic.list.MultipleObjectMixin.context_object_name` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_context_object_name`] * :attr:`~django.views.generic.base.View.http_method_names` * :attr:`~django.views.generic.list.MultipleObjectMixin.model` +* :attr:`~django.views.generic.list.MultipleObjectMixin.ordering` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_ordering`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_orphans` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_orphans`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class` @@ -301,6 +302,7 @@ ArchiveIndexView * :attr:`~django.views.generic.dates.DateMixin.date_field` [:meth:`~django.views.generic.dates.DateMixin.get_date_field`] * :attr:`~django.views.generic.base.View.http_method_names` * :attr:`~django.views.generic.list.MultipleObjectMixin.model` +* :attr:`~django.views.generic.list.MultipleObjectMixin.ordering` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_ordering`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_orphans` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_orphans`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class` @@ -338,6 +340,7 @@ YearArchiveView * :attr:`~django.views.generic.base.View.http_method_names` * :attr:`~django.views.generic.dates.YearArchiveView.make_object_list` [:meth:`~django.views.generic.dates.YearArchiveView.get_make_object_list`] * :attr:`~django.views.generic.list.MultipleObjectMixin.model` +* :attr:`~django.views.generic.list.MultipleObjectMixin.ordering` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_ordering`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_orphans` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_orphans`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class` @@ -378,6 +381,7 @@ MonthArchiveView * :attr:`~django.views.generic.list.MultipleObjectMixin.model` * :attr:`~django.views.generic.dates.MonthMixin.month` [:meth:`~django.views.generic.dates.MonthMixin.get_month`] * :attr:`~django.views.generic.dates.MonthMixin.month_format` [:meth:`~django.views.generic.dates.MonthMixin.get_month_format`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.ordering` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_ordering`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_orphans` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_orphans`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class` @@ -418,6 +422,7 @@ WeekArchiveView * :attr:`~django.views.generic.dates.DateMixin.date_field` [:meth:`~django.views.generic.dates.DateMixin.get_date_field`] * :attr:`~django.views.generic.base.View.http_method_names` * :attr:`~django.views.generic.list.MultipleObjectMixin.model` +* :attr:`~django.views.generic.list.MultipleObjectMixin.ordering` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_ordering`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_orphans` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_orphans`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class` @@ -462,6 +467,7 @@ DayArchiveView * :attr:`~django.views.generic.list.MultipleObjectMixin.model` * :attr:`~django.views.generic.dates.MonthMixin.month` [:meth:`~django.views.generic.dates.MonthMixin.get_month`] * :attr:`~django.views.generic.dates.MonthMixin.month_format` [:meth:`~django.views.generic.dates.MonthMixin.get_month_format`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.ordering` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_ordering`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_orphans` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_orphans`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class` @@ -508,6 +514,7 @@ TodayArchiveView * :attr:`~django.views.generic.list.MultipleObjectMixin.model` * :attr:`~django.views.generic.dates.MonthMixin.month` [:meth:`~django.views.generic.dates.MonthMixin.get_month`] * :attr:`~django.views.generic.dates.MonthMixin.month_format` [:meth:`~django.views.generic.dates.MonthMixin.get_month_format`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.ordering` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_ordering`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_orphans` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_orphans`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class` diff --git a/docs/ref/class-based-views/mixins-multiple-object.txt b/docs/ref/class-based-views/mixins-multiple-object.txt index c5acff6208..193203be6d 100644 --- a/docs/ref/class-based-views/mixins-multiple-object.txt +++ b/docs/ref/class-based-views/mixins-multiple-object.txt @@ -71,6 +71,13 @@ MultipleObjectMixin retrieve it with :meth:`get_queryset` which takes care of the cloning behind the scenes. + .. attribute:: ordering + + .. versionadded:: 1.8 + + A string or list of strings specifying the ordering to apply to the ``queryset``. + Valid values are the same as those for :meth:`~django.db.models.query.QuerySet.order_by`. + .. attribute:: paginate_by An integer specifying how many objects should be displayed per page. If @@ -110,6 +117,15 @@ MultipleObjectMixin Get the list of items for this view. This must be an iterable and may be a queryset (in which queryset-specific behavior will be enabled). + .. method:: get_ordering() + + .. versionadded:: 1.8 + + Returns a string (or iterable of strings) that defines the ordering that + will be applied to the ``queryset``. + + Returns :attr:`ordering` by default. + .. method:: paginate_queryset(queryset, page_size) Returns a 4-tuple containing (``paginator``, ``page``, ``object_list``, diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index 22faffd330..1f34623811 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -175,6 +175,15 @@ Forms will also update ``UploadedFile.content_type`` with the image's content type as determined by Pillow. +Generic Views +^^^^^^^^^^^^^ + +* Generic views that use :class:`~django.views.generic.list.MultipleObjectMixin` + may now specify the ordering applied to the + :attr:`~django.views.generic.list.MultipleObjectMixin.queryset` by setting + :attr:`~django.views.generic.list.MultipleObjectMixin.ordering` or overriding + :meth:`~django.views.generic.list.MultipleObjectMixin.get_ordering()`. + Internationalization ^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/generic_views/test_dates.py b/tests/generic_views/test_dates.py index 7b3d74bc8a..643a9c2456 100644 --- a/tests/generic_views/test_dates.py +++ b/tests/generic_views/test_dates.py @@ -120,6 +120,22 @@ class ArchiveIndexViewTests(TestCase): self.assertEqual(res.status_code, 200) self.assertEqual(list(res.context['date_list']), list(reversed(sorted(res.context['date_list'])))) + def test_archive_view_custom_sorting(self): + Book.objects.create(name="Zebras for Dummies", pages=600, pubdate=datetime.date(2007, 5, 1)) + res = self.client.get('/dates/books/sortedbyname/') + self.assertEqual(res.status_code, 200) + self.assertEqual(list(res.context['date_list']), list(Book.objects.dates('pubdate', 'year', 'DESC'))) + self.assertEqual(list(res.context['latest']), list(Book.objects.order_by('name').all())) + self.assertTemplateUsed(res, 'generic_views/book_archive.html') + + def test_archive_view_custom_sorting_dec(self): + Book.objects.create(name="Zebras for Dummies", pages=600, pubdate=datetime.date(2007, 5, 1)) + res = self.client.get('/dates/books/sortedbynamedec/') + self.assertEqual(res.status_code, 200) + self.assertEqual(list(res.context['date_list']), list(Book.objects.dates('pubdate', 'year', 'DESC'))) + self.assertEqual(list(res.context['latest']), list(Book.objects.order_by('-name').all())) + self.assertTemplateUsed(res, 'generic_views/book_archive.html') + @override_settings(ROOT_URLCONF='generic_views.urls') class YearArchiveViewTests(TestCase): @@ -178,6 +194,26 @@ class YearArchiveViewTests(TestCase): self.assertEqual(list(res.context['object_list']), list(Book.objects.filter(pubdate__year=2006))) self.assertTemplateUsed(res, 'generic_views/book_archive_year.html') + def test_year_view_custom_sort_order(self): + # Zebras comes after Dreaming by name, but before on '-pubdate' which is the default sorting + Book.objects.create(name="Zebras for Dummies", pages=600, pubdate=datetime.date(2006, 9, 1)) + res = self.client.get('/dates/books/2006/sortedbyname/') + self.assertEqual(res.status_code, 200) + self.assertEqual(list(res.context['date_list']), [datetime.date(2006, 5, 1), datetime.date(2006, 9, 1)]) + self.assertEqual(list(res.context['book_list']), list(Book.objects.filter(pubdate__year=2006).order_by('name'))) + self.assertEqual(list(res.context['object_list']), list(Book.objects.filter(pubdate__year=2006).order_by('name'))) + self.assertTemplateUsed(res, 'generic_views/book_archive_year.html') + + def test_year_view_two_custom_sort_orders(self): + Book.objects.create(name="Zebras for Dummies", pages=300, pubdate=datetime.date(2006, 9, 1)) + Book.objects.create(name="Hunting Hippos", pages=400, pubdate=datetime.date(2006, 3, 1)) + res = self.client.get('/dates/books/2006/sortedbypageandnamedec/') + self.assertEqual(res.status_code, 200) + self.assertEqual(list(res.context['date_list']), [datetime.date(2006, 3, 1), datetime.date(2006, 5, 1), datetime.date(2006, 9, 1)]) + self.assertEqual(list(res.context['book_list']), list(Book.objects.filter(pubdate__year=2006).order_by('pages', '-name'))) + self.assertEqual(list(res.context['object_list']), list(Book.objects.filter(pubdate__year=2006).order_by('pages', '-name'))) + self.assertTemplateUsed(res, 'generic_views/book_archive_year.html') + def test_year_view_invalid_pattern(self): res = self.client.get('/dates/books/no_year/') self.assertEqual(res.status_code, 404) diff --git a/tests/generic_views/test_list.py b/tests/generic_views/test_list.py index 8c5c023e4c..f5b91c74bb 100644 --- a/tests/generic_views/test_list.py +++ b/tests/generic_views/test_list.py @@ -1,11 +1,13 @@ from __future__ import unicode_literals +import datetime + from django.core.exceptions import ImproperlyConfigured from django.test import TestCase, override_settings from django.views.generic.base import View from django.utils.encoding import force_str -from .models import Author, Artist +from .models import Author, Artist, Book @override_settings(ROOT_URLCONF='generic_views.urls') @@ -200,6 +202,20 @@ class ListViewTests(TestCase): with self.assertNumQueries(3): self.client.get('/list/authors/notempty/paginated/') + def test_explicitly_ordered_list_view(self): + Book.objects.create(name="Zebras for Dummies", pages=800, pubdate=datetime.date(2006, 9, 1)) + res = self.client.get('/list/books/sorted/') + self.assertEqual(res.status_code, 200) + self.assertEqual(res.context['object_list'][0].name, '2066') + self.assertEqual(res.context['object_list'][1].name, 'Dreaming in Code') + self.assertEqual(res.context['object_list'][2].name, 'Zebras for Dummies') + + res = self.client.get('/list/books/sortedbypagesandnamedec/') + self.assertEqual(res.status_code, 200) + self.assertEqual(res.context['object_list'][0].name, 'Dreaming in Code') + self.assertEqual(res.context['object_list'][1].name, 'Zebras for Dummies') + self.assertEqual(res.context['object_list'][2].name, '2066') + @override_settings(DEBUG=True) def test_paginated_list_view_returns_useful_message_on_invalid_page(self): # test for #19240 diff --git a/tests/generic_views/urls.py b/tests/generic_views/urls.py index a14eb6a9f4..b88b33892e 100644 --- a/tests/generic_views/urls.py +++ b/tests/generic_views/urls.py @@ -121,6 +121,11 @@ urlpatterns = [ views.BookArchive.as_view(date_list_period='month')), url(r'^dates/booksignings/$', views.BookSigningArchive.as_view()), + url(r'^dates/books/sortedbyname/$', + views.BookArchive.as_view(ordering='name')), + url(r'^dates/books/sortedbynamedec/$', + views.BookArchive.as_view(ordering='-name')), + # ListView url(r'^list/dict/$', @@ -159,6 +164,10 @@ urlpatterns = [ views.AuthorList.as_view(paginate_by=30, page_kwarg='pagina')), url(r'^list/authors/paginated/custom_constructor/$', views.AuthorListCustomPaginator.as_view()), + url(r'^list/books/sorted/$', + views.BookList.as_view(ordering='name')), + url(r'^list/books/sortedbypagesandnamedec/$', + views.BookList.as_view(ordering=('pages', '-name'))), # YearArchiveView # Mixing keyword and positional captures below is intentional; the views @@ -173,6 +182,10 @@ urlpatterns = [ views.BookYearArchive.as_view(allow_future=True)), url(r'^dates/books/(?P[0-9]{4})/paginated/$', views.BookYearArchive.as_view(make_object_list=True, paginate_by=30)), + url(r'^dates/books/(?P\d{4})/sortedbyname/$', + views.BookYearArchive.as_view(make_object_list=True, ordering='name')), + url(r'^dates/books/(?P\d{4})/sortedbypageandnamedec/$', + views.BookYearArchive.as_view(make_object_list=True, ordering=('pages', '-name'))), url(r'^dates/books/no_year/$', views.BookYearArchive.as_view()), url(r'^dates/books/(?P[0-9]{4})/reverse/$', diff --git a/tests/generic_views/views.py b/tests/generic_views/views.py index fc91799f56..27ae5b2ca9 100644 --- a/tests/generic_views/views.py +++ b/tests/generic_views/views.py @@ -57,6 +57,10 @@ class AuthorList(generic.ListView): queryset = Author.objects.all() +class BookList(generic.ListView): + model = Book + + class CustomPaginator(Paginator): def __init__(self, queryset, page_size, orphans=0, allow_empty_first_page=True): super(CustomPaginator, self).__init__(