Browse Source

Fixed #19963 -- Added support for date_hierarchy across relations.

Vytis Banaitis 9 years ago
parent
commit
2f9c4e2b6f

+ 10 - 6
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')

+ 3 - 2
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

+ 1 - 1
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 ``<field name>``,
-  which is not an attribute of ``<model>``.
+  which does not refer to a Field.
 * **admin.E128**: The value of ``date_hierarchy`` must be a ``DateField`` or
   ``DateTimeField``.
 

+ 9 - 0
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()

+ 1 - 1
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`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+ 1 - 1
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)

+ 1 - 0
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

+ 20 - 0
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):

+ 20 - 3
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):