From 2f9c4e2b6fab3ce08cfbebff79d758196250790c Mon Sep 17 00:00:00 2001 From: Vytis Banaitis Date: Thu, 5 May 2016 20:52:54 +0300 Subject: [PATCH] Fixed #19963 -- Added support for date_hierarchy across relations. --- django/contrib/admin/checks.py | 16 ++++++++----- .../contrib/admin/templatetags/admin_list.py | 5 ++-- docs/ref/checks.txt | 2 +- docs/ref/contrib/admin/index.txt | 9 ++++++++ docs/releases/1.11.txt | 2 +- tests/admin_views/admin.py | 2 +- tests/admin_views/models.py | 1 + tests/admin_views/tests.py | 20 ++++++++++++++++ tests/modeladmin/tests.py | 23 ++++++++++++++++--- 9 files changed, 66 insertions(+), 14 deletions(-) diff --git a/django/contrib/admin/checks.py b/django/contrib/admin/checks.py index 87788c8f308..6d338281a78 100644 --- a/django/contrib/admin/checks.py +++ b/django/contrib/admin/checks.py @@ -840,12 +840,16 @@ class ModelAdminChecks(BaseModelAdminChecks): return [] else: try: - field = obj.model._meta.get_field(obj.date_hierarchy) - except FieldDoesNotExist: - return refer_to_missing_field( - option='date_hierarchy', field=obj.date_hierarchy, - model=obj.model, obj=obj, id='admin.E127', - ) + field = get_fields_from_path(obj.model, obj.date_hierarchy)[-1] + except (NotRelationField, FieldDoesNotExist): + return [ + checks.Error( + "The value of 'date_hierarchy' refers to '%s', which " + "does not refer to a Field." % obj.date_hierarchy, + obj=obj.__class__, + id='admin.E127', + ) + ] else: if not isinstance(field, (models.DateField, models.DateTimeField)): return must_be('a DateField or DateTimeField', option='date_hierarchy', obj=obj, id='admin.E128') diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index c8ebccfd255..63b428ec689 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -5,7 +5,8 @@ import warnings from django.contrib.admin.templatetags.admin_urls import add_preserved_filters from django.contrib.admin.utils import ( - display_for_field, display_for_value, label_for_field, lookup_field, + display_for_field, display_for_value, get_fields_from_path, + label_for_field, lookup_field, ) from django.contrib.admin.views.main import ( ALL_VAR, ORDER_VAR, PAGE_VAR, SEARCH_VAR, @@ -346,7 +347,7 @@ def date_hierarchy(cl): """ if cl.date_hierarchy: field_name = cl.date_hierarchy - field = cl.opts.get_field(field_name) + field = get_fields_from_path(cl.model, field_name)[-1] dates_or_datetimes = 'datetimes' if isinstance(field, models.DateTimeField) else 'dates' year_field = '%s__year' % field_name month_field = '%s__month' % field_name diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index 03bdba97dbe..ac96fbf4120 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -393,7 +393,7 @@ with the admin site: which is not editable through the admin. * **admin.E126**: The value of ``search_fields`` must be a list or tuple. * **admin.E127**: The value of ``date_hierarchy`` refers to ````, - which is not an attribute of ````. + which does not refer to a Field. * **admin.E128**: The value of ``date_hierarchy`` must be a ``DateField`` or ``DateTimeField``. diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index ac696656c28..6984589ee20 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -213,10 +213,19 @@ subclass:: date_hierarchy = 'pub_date' + You can also specify a field on a related model using the ``__`` lookup, + for example:: + + date_hierarchy = 'author__pub_date' + This will intelligently populate itself based on available data, e.g. if all the dates are in one month, it'll show the day-level drill-down only. + .. versionchanged:: 1.11 + + The ability to reference fields on related models was added. + .. note:: ``date_hierarchy`` uses :meth:`QuerySet.datetimes() diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt index ae3f3af4387..918a6157f1a 100644 --- a/docs/releases/1.11.txt +++ b/docs/releases/1.11.txt @@ -50,7 +50,7 @@ Minor features :mod:`django.contrib.admin` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* ... +* :attr:`.ModelAdmin.date_hierarchy` can now reference fields across relations. :mod:`django.contrib.admindocs` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py index 31f41515bbd..2146fa66f29 100644 --- a/tests/admin_views/admin.py +++ b/tests/admin_views/admin.py @@ -973,7 +973,7 @@ site.register(Pizza, PizzaAdmin) site.register(Topping, ToppingAdmin) site.register(Album, AlbumAdmin) site.register(Question) -site.register(Answer) +site.register(Answer, date_hierarchy='question__posted') site.register(PrePopulatedPost, PrePopulatedPostAdmin) site.register(ComplexSortedPerson, ComplexSortedPersonAdmin) site.register(FilteredManager, CustomManagerAdmin) diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py index 301955efd8c..b6ec1b6c319 100644 --- a/tests/admin_views/models.py +++ b/tests/admin_views/models.py @@ -621,6 +621,7 @@ class WorkHour(models.Model): class Question(models.Model): question = models.CharField(max_length=20) + posted = models.DateField(default=datetime.date.today) @python_2_unicode_compatible diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 281084a8a19..6455b210b28 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -5331,6 +5331,26 @@ class DateHierarchyTests(TestCase): self.assert_non_localized_year(response, 2003) self.assert_non_localized_year(response, 2005) + def test_related_field(self): + questions_data = ( + # (posted data, number of answers), + (datetime.date(2001, 1, 30), 0), + (datetime.date(2003, 3, 15), 1), + (datetime.date(2005, 5, 3), 2), + ) + for date, answer_count in questions_data: + question = Question.objects.create(posted=date) + for i in range(answer_count): + question.answer_set.create() + + response = self.client.get(reverse('admin:admin_views_answer_changelist')) + for date, answer_count in questions_data: + link = '?question__posted__year=%d"' % (date.year,) + if answer_count > 0: + self.assertContains(response, link) + else: + self.assertNotContains(response, link) + @override_settings(ROOT_URLCONF='admin_views.urls') class AdminCustomSaveRelatedTests(TestCase): diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index 990ed9ab32f..40ea025baf3 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -1175,9 +1175,10 @@ class DateHierarchyCheckTests(CheckTestCase): self.assertIsInvalid( ValidationTestModelAdmin, ValidationTestModel, - ("The value of 'date_hierarchy' refers to 'non_existent_field', which " - "is not an attribute of 'modeladmin.ValidationTestModel'."), - 'admin.E127') + "The value of 'date_hierarchy' refers to 'non_existent_field', which " + "does not refer to a Field.", + 'admin.E127' + ) def test_invalid_field_type(self): class ValidationTestModelAdmin(ModelAdmin): @@ -1194,6 +1195,22 @@ class DateHierarchyCheckTests(CheckTestCase): self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) + def test_related_valid_case(self): + class ValidationTestModelAdmin(ModelAdmin): + date_hierarchy = 'band__sign_date' + + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) + + def test_related_invalid_field_type(self): + class ValidationTestModelAdmin(ModelAdmin): + date_hierarchy = 'band__name' + + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + "The value of 'date_hierarchy' must be a DateField or DateTimeField.", + 'admin.E128' + ) + class OrderingCheckTests(CheckTestCase):