diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index b24f133124..220b60e1dc 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -231,6 +231,16 @@ def date_hierarchy(cl): link = lambda d: cl.get_query_string(d, [field_generic]) + if not (year_lookup or month_lookup or day_lookup): + # select appropriate start level + date_range = cl.query_set.aggregate(first=models.Min(field_name), + last=models.Max(field_name)) + if date_range['first'] and date_range['last']: + if date_range['first'].year == date_range['last'].year: + year_lookup = date_range['first'].year + if date_range['first'].month == date_range['last'].month: + month_lookup = date_range['first'].month + if year_lookup and month_lookup and day_lookup: day = datetime.date(int(year_lookup), int(month_lookup), int(day_lookup)) return { diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 97ea03bad2..7d650f52ba 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -107,6 +107,12 @@ subclass:: date_hierarchy = 'pub_date' + .. versionadded:: 1.3 + + 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. + .. attribute:: ModelAdmin.form By default a ``ModelForm`` is dynamically created for your model. It is diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py index 61ca33904c..12b92d80a4 100644 --- a/tests/regressiontests/admin_views/models.py +++ b/tests/regressiontests/admin_views/models.py @@ -320,7 +320,7 @@ class Podcast(Media): class PodcastAdmin(admin.ModelAdmin): list_display = ('name', 'release_date') list_editable = ('release_date',) - + date_hierarchy = 'release_date' ordering = ('name',) class Vodcast(Media): diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index db2d7f9be3..0c9f03b933 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -6,6 +6,7 @@ import datetime from django.conf import settings from django.core import mail from django.core.files import temp as tempfile +from django.core.urlresolvers import reverse # Register auth models with the admin. from django.contrib.auth import REDIRECT_FIELD_NAME, admin from django.contrib.auth.models import User, Permission, UNUSABLE_PASSWORD @@ -2395,3 +2396,105 @@ class ValidXHTMLTests(TestCase): response = self.client.get('/test_admin/%s/admin_views/' % self.urlbit) self.assertFalse(' lang=""' in response.content) self.assertFalse(' xml:lang=""' in response.content) + + +class DateHierarchyTests(TestCase): + fixtures = ['admin-views-users.xml'] + + def setUp(self): + self.client.login(username='super', password='secret') + + def assert_contains_year_link(self, response, date): + self.assertContains(response, '?release_date__year=%d"' % (date.year,)) + + def assert_contains_month_link(self, response, date): + self.assertContains( + response, '?release_date__year=%d&release_date__month=%d"' % ( + date.year, date.month)) + + def assert_contains_day_link(self, response, date): + self.assertContains( + response, '?release_date__year=%d&' + 'release_date__month=%d&release_date__day=%d"' % ( + date.year, date.month, date.day)) + + def test_empty(self): + """ + Ensure that no date hierarchy links display with empty changelist. + """ + response = self.client.get( + reverse('admin:admin_views_podcast_changelist')) + self.assertNotContains(response, 'release_date__year=') + self.assertNotContains(response, 'release_date__month=') + self.assertNotContains(response, 'release_date__day=') + + def test_single(self): + """ + Ensure that single day-level date hierarchy appears for single object. + """ + DATE = datetime.date(2000, 6, 30) + Podcast.objects.create(release_date=DATE) + response = self.client.get( + reverse('admin:admin_views_podcast_changelist')) + self.assert_contains_day_link(response, DATE) + + def test_within_month(self): + """ + Ensure that day-level links appear for changelist within single month. + """ + DATES = (datetime.date(2000, 6, 30), + datetime.date(2000, 6, 15), + datetime.date(2000, 6, 3)) + for date in DATES: + Podcast.objects.create(release_date=date) + response = self.client.get( + reverse('admin:admin_views_podcast_changelist')) + for date in DATES: + self.assert_contains_day_link(response, date) + + def test_within_year(self): + """ + Ensure that month-level links appear for changelist within single year. + """ + DATES = (datetime.date(2000, 1, 30), + datetime.date(2000, 3, 15), + datetime.date(2000, 5, 3)) + for date in DATES: + Podcast.objects.create(release_date=date) + response = self.client.get( + reverse('admin:admin_views_podcast_changelist')) + # no day-level links + self.assertNotContains(response, 'release_date__day=') + for date in DATES: + self.assert_contains_month_link(response, date) + + def test_multiple_years(self): + """ + Ensure that year-level links appear for year-spanning changelist. + """ + DATES = (datetime.date(2001, 1, 30), + datetime.date(2003, 3, 15), + datetime.date(2005, 5, 3)) + for date in DATES: + Podcast.objects.create(release_date=date) + response = self.client.get( + reverse('admin:admin_views_podcast_changelist')) + # no day/month-level links + self.assertNotContains(response, 'release_date__day=') + self.assertNotContains(response, 'release_date__month=') + for date in DATES: + self.assert_contains_year_link(response, date) + + # and make sure GET parameters still behave correctly + for date in DATES: + response = self.client.get( + '%s?release_date__year=%d' % ( + reverse('admin:admin_views_podcast_changelist'), + date.year)) + self.assert_contains_month_link(response, date) + + response = self.client.get( + '%s?release_date__year=%d&release_date__month=%d' % ( + reverse('admin:admin_views_podcast_changelist'), + date.year, date.month)) + self.assert_contains_day_link(response, date)