Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Fixed #28933 -- Custom range-based filter for date_hierarchy
  • Loading branch information
hakib committed Dec 16, 2017
1 parent 6fd6d83 commit ad87051
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 4 deletions.
6 changes: 2 additions & 4 deletions django/contrib/admin/templatetags/admin_list.py
Expand Up @@ -367,8 +367,7 @@ def link(filters):
'choices': [{'title': capfirst(formats.date_format(day, 'MONTH_DAY_FORMAT'))}]
}
elif year_lookup and month_lookup:
days = cl.queryset.filter(**{year_field: year_lookup, month_field: month_lookup})
days = getattr(days, 'dates')(field_name, 'day')
days = getattr(cl.queryset, 'dates')(field_name, 'day')
return {
'show': True,
'back': {
Expand All @@ -381,8 +380,7 @@ def link(filters):
} for day in days]
}
elif year_lookup:
months = cl.queryset.filter(**{year_field: year_lookup})
months = getattr(months, 'dates')(field_name, 'month')
months = getattr(cl.queryset, 'dates')(field_name, 'month')
return {
'show': True,
'back': {
Expand Down
29 changes: 29 additions & 0 deletions django/contrib/admin/views/main.py
@@ -1,5 +1,7 @@
import datetime
from collections import OrderedDict

from django.conf import settings
from django.contrib.admin import FieldListFilter
from django.contrib.admin.exceptions import (
DisallowedModelAdminLookup, DisallowedModelAdminToField,
Expand All @@ -17,6 +19,7 @@
from django.db import models
from django.urls import reverse
from django.utils.http import urlencode
from django.utils.timezone import make_aware
from django.utils.translation import gettext

# Changelist settings
Expand Down Expand Up @@ -135,6 +138,32 @@ def get_filters(self, request):
if spec and spec.has_output():
filter_specs.append(spec)

if self.date_hierarchy:
year = lookup_params.pop('{}__year'.format(self.date_hierarchy), None)
if year is not None:
month = lookup_params.pop('{}__month'.format(self.date_hierarchy), None)
day = lookup_params.pop('{}__day'.format(self.date_hierarchy), None)

from_date = datetime.datetime(
int(year),
int(month if month is not None else 1),
int(day if day is not None else 1),
)
if settings.USE_TZ:
from_date = make_aware(from_date)

if day:
to_date = from_date + datetime.timedelta(days=1)

elif month:
to_date = (from_date + datetime.timedelta(days=32)).replace(day=1)

else:
to_date = from_date.replace(year=from_date.year + 1)

lookup_params['{}__gte'.format(self.date_hierarchy)] = from_date
lookup_params['{}__lt'.format(self.date_hierarchy)] = to_date

# At this point, all the parameters used by the various ListFilters
# have been removed from lookup_params, which now only contains other
# parameters passed via the query string. We now loop through the
Expand Down
98 changes: 98 additions & 0 deletions tests/admin_filters/tests.py
Expand Up @@ -7,10 +7,12 @@
RelatedOnlyFieldListFilter, SimpleListFilter, site,
)
from django.contrib.admin.options import IncorrectLookupParameters
from django.contrib.admin.templatetags.admin_list import date_hierarchy
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from django.core.exceptions import ImproperlyConfigured
from django.test import RequestFactory, TestCase, override_settings
from django.utils import timezone

from .models import Book, Bookmark, Department, Employee, TaggedItem

Expand Down Expand Up @@ -244,6 +246,10 @@ class BookmarkAdminGenericRelation(ModelAdmin):
list_filter = ['tags__tag']


class BookAdminWithDateHierarchy(ModelAdmin):
date_hierarchy = 'date_registered'


class ListFiltersTests(TestCase):

def setUp(self):
Expand Down Expand Up @@ -1099,3 +1105,95 @@ def test_list_filter_queryset_filtered_by_default(self):
changelist = modeladmin.get_changelist_instance(request)
changelist.get_results(request)
self.assertEqual(changelist.full_result_count, 4)

def test_should_filter_by_date_hierarchy(self):
modeladmin = BookAdminWithDateHierarchy(Book, site)

def _test_date_hierarchy(query, expected_from_date, expected_to_date):
request = self.request_factory.get('/', query)
changelist = modeladmin.get_changelist_instance(request)
_, _, lookup_params, _ = changelist.get_filters(request)
self.assertEqual(lookup_params['date_registered__gte'], expected_from_date)
self.assertEqual(lookup_params['date_registered__lt'], expected_to_date)

_test_date_hierarchy(
{'date_registered__year': 2017},
datetime.datetime(2017, 1, 1, 0, 0, 0),
datetime.datetime(2018, 1, 1, 0, 0, 0),
)

_test_date_hierarchy(
{'date_registered__year': 2017, 'date_registered__month': 2},
datetime.datetime(2017, 2, 1, 0, 0, 0),
datetime.datetime(2017, 3, 1, 0, 0, 0),
)

_test_date_hierarchy(
{'date_registered__year': 2017, 'date_registered__month': 12},
datetime.datetime(2017, 12, 1, 0, 0, 0),
datetime.datetime(2018, 1, 1, 0, 0, 0),
)

_test_date_hierarchy(
{'date_registered__year': 2017, 'date_registered__month': 12, 'date_registered__day': 15},
datetime.datetime(2017, 12, 15, 0, 0, 0),
datetime.datetime(2017, 12, 16, 0, 0, 0),
)

_test_date_hierarchy(
{'date_registered__year': 2017, 'date_registered__month': 12, 'date_registered__day': 31},
datetime.datetime(2017, 12, 31, 0, 0, 0),
datetime.datetime(2018, 1, 1, 0, 0, 0),
)

_test_date_hierarchy(
{'date_registered__year': 2017, 'date_registered__month': 2, 'date_registered__day': 28},
datetime.datetime(2017, 2, 28, 0, 0, 0),
datetime.datetime(2017, 3, 1, 0, 0, 0),
)

with override_settings(USE_TZ=True, TIME_ZONE='Asia/Jerusalem'):
tz = timezone.get_default_timezone()
_test_date_hierarchy(
{'date_registered__year': 2017, 'date_registered__month': 2, 'date_registered__day': 28},
tz.localize(datetime.datetime(2017, 2, 28, 0, 0, 0)),
tz.localize(datetime.datetime(2017, 3, 1, 0, 0, 0)),
)

def test_should_show_date_hierarchy_choices(self):
modeladmin = BookAdminWithDateHierarchy(Book, site)

Book.objects.all().delete()
for date_registered in (
datetime.date(2017, 10, 1),
datetime.date(2017, 12, 15),
datetime.date(2017, 12, 31),
datetime.date(2018, 2, 1),
):
Book.objects.create(title='title', date_registered=date_registered)

def _test_date_hierarch_choices(query, expected_choices):
request = self.request_factory.get('/', query)
changelist = modeladmin.get_changelist_instance(request)
spec = date_hierarchy(changelist)
choices = [choice['link'] for choice in spec['choices']]
self.assertEqual(choices, expected_choices)

_test_date_hierarch_choices({}, [
'?date_registered__year=2017',
'?date_registered__year=2018',
])

_test_date_hierarch_choices({'date_registered__year': 2016}, [])

_test_date_hierarch_choices({'date_registered__year': 2017}, [
'?date_registered__month=10&date_registered__year=2017',
'?date_registered__month=12&date_registered__year=2017',
])

_test_date_hierarch_choices({'date_registered__year': 2017, 'date_registered__month': 9}, [])

_test_date_hierarch_choices({'date_registered__year': 2017, 'date_registered__month': 12}, [
'?date_registered__day=15&date_registered__month=12&date_registered__year=2017',
'?date_registered__day=31&date_registered__month=12&date_registered__year=2017',
])

0 comments on commit ad87051

Please sign in to comment.