Skip to content

Commit bc16118

Browse files
committed
Merge pull request #31 from rpkilby/24-isodatetimezones
Fix timezone-aware datetime handling (#24)
2 parents fe72834 + 362347b commit bc16118

File tree

5 files changed

+62
-65
lines changed

5 files changed

+62
-65
lines changed

.travis.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ env:
1212
- DJANGO="Django>=1.8,<1.9"
1313
install:
1414
- travis_retry pip install -q $DJANGO
15-
- pip install py-dateutil
1615
- python setup.py install
1716
script: python manage.py test rest_framework_filters
1817

rest_framework_filters/fields.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
from django import forms
22

3+
4+
# https://code.djangoproject.com/ticket/19917
5+
class Django14TimeField(forms.TimeField):
6+
input_formats = ['%H:%M:%S', '%H:%M:%S.%f', '%H:%M']
7+
8+
39
class ArrayDecimalField(forms.DecimalField):
410
def clean(self, value):
511
if value is None:

rest_framework_filters/filters.py

Lines changed: 5 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,12 @@
11
from __future__ import absolute_import
22
from __future__ import unicode_literals
33

4+
import django
45
from django.utils import six
5-
6-
from rest_framework.settings import api_settings
7-
import rest_framework.filters
8-
import django_filters
96
from django_filters.filters import *
107

118
from . import fields
129

13-
def subsitute_iso8601(date_type):
14-
from rest_framework import ISO_8601
15-
16-
if date_type == 'datetime':
17-
strptime_iso8601 = '%Y-%m-%dT%H:%M:%S.%f'
18-
formats = api_settings.DATETIME_INPUT_FORMATS
19-
elif date_type == 'date':
20-
strptime_iso8601 = '%Y-%m-%d'
21-
formats = api_settings.DATE_INPUT_FORMATS
22-
elif date_type == 'time':
23-
strptime_iso8601 = '%H:%M:%S.%f'
24-
formats = api_settings.TIME_INPUT_FORMATS
25-
26-
new_formats = []
27-
for f in formats:
28-
if f == ISO_8601:
29-
new_formats.append(strptime_iso8601)
30-
else:
31-
new_formats.append(f)
32-
return new_formats
33-
34-
35-
# In order to support ISO-8601 -- which is the default output for
36-
# DRF -- we need to set up custom date/time input formats.
37-
TIME_INPUT_FORMATS = subsitute_iso8601('time')
38-
DATE_INPUT_FORMATS = subsitute_iso8601('date')
39-
DATETIME_INPUT_FORMATS = subsitute_iso8601('datetime')
40-
4110

4211
class RelatedFilter(ModelChoiceFilter):
4312
def __init__(self, filterset, *args, **kwargs):
@@ -50,7 +19,7 @@ def setup_filterset(self):
5019
# This is a recursive relation, defined via a string, so we need
5120
# to create and import the class here.
5221
items = self.filterset.split('.')
53-
cls = str(items[-1]) # Ensure not unicode on py2.x
22+
cls = str(items[-1]) # Ensure not unicode on py2.x
5423
mod = __import__('.'.join(items[:-1]), fromlist=[cls])
5524
self.filterset = getattr(mod, cls)
5625

@@ -65,22 +34,9 @@ class AllLookupsFilter(Filter):
6534
# Fixed-up versions of some of the default filters
6635
###################################################
6736

68-
class DateFilter(django_filters.DateFilter):
69-
def __init__(self, *args, **kwargs):
70-
super(DateFilter, self).__init__(*args, **kwargs)
71-
self.extra.update({'input_formats': DATE_INPUT_FORMATS})
72-
73-
74-
class DateTimeFilter(django_filters.DateTimeFilter):
75-
def __init__(self, *args, **kwargs):
76-
super(DateTimeFilter, self).__init__(*args, **kwargs)
77-
self.extra.update({'input_formats': DATETIME_INPUT_FORMATS})
78-
79-
80-
class TimeFilter(django_filters.DateTimeFilter):
81-
def __init__(self, *args, **kwargs):
82-
super(TimeFilter, self).__init__(*args, **kwargs)
83-
self.extra.update({'input_formats': TIME_INPUT_FORMATS})
37+
class TimeFilter(TimeFilter):
38+
if django.VERSION < (1, 6):
39+
field_class = fields.Django14TimeField
8440

8541

8642
class InSetNumberFilter(NumberFilter):

rest_framework_filters/filterset.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,19 @@
2424

2525

2626
class FilterSet(django_filters.FilterSet):
27-
# In order to support ISO-8601 -- which is the default output for
28-
# DRF -- we need to set up custom date/time input formats.
2927
filter_overrides = {
28+
29+
# In order to support ISO-8601 -- which is the default output for
30+
# DRF -- we need to use django-filter's IsoDateTimeFilter
3031
models.DateTimeField: {
31-
'filter_class': filters.DateTimeFilter,
32-
},
33-
models.DateField: {
34-
'filter_class': filters.DateFilter,
35-
},
32+
'filter_class': filters.IsoDateTimeFilter,
33+
},
34+
35+
# Django < 1.6 time input formats did not account for microseconds
36+
# https://code.djangoproject.com/ticket/19917
3637
models.TimeField: {
3738
'filter_class': filters.TimeFilter,
38-
},
39+
}
3940
}
4041

4142
LOOKUP_TYPES = django_filters.filters.LOOKUP_TYPES
@@ -68,7 +69,7 @@ def __init__(self, *args, **kwargs):
6869

6970
def fix_filter_field(self, f):
7071
"""
71-
Fix the filter field based on the lookup type.
72+
Fix the filter field based on the lookup type.
7273
"""
7374
lookup_type = f.lookup_type
7475
if lookup_type == 'isnull':

rest_framework_filters/tests.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import time
66
import datetime
77

8-
from dateutil.parser import parse as date_parse
8+
from django.utils.dateparse import parse_time, parse_datetime
99

1010
from django.db import models
1111
from django.test import TestCase
@@ -16,6 +16,12 @@
1616
from .filterset import FilterSet
1717
from .backends import DjangoFilterBackend
1818

19+
try:
20+
from django.test import override_settings
21+
except ImportError:
22+
# TODO: Remove this once Django 1.6 is EOL.
23+
from django.test.utils import override_settings
24+
1925

2026
class Note(models.Model):
2127
title = models.CharField(max_length=100)
@@ -262,7 +268,7 @@ def setUp(self):
262268
n.save()
263269

264270
#######################
265-
# Create notes
271+
# Create notes
266272
#######################
267273
n = Note(
268274
title="Test 2",
@@ -286,7 +292,7 @@ def setUp(self):
286292
n.save()
287293

288294
#######################
289-
# Create posts
295+
# Create posts
290296
#######################
291297
post = Post(
292298
note=Note.objects.get(title="Test 1"),
@@ -360,7 +366,7 @@ def setUp(self):
360366
)
361367
blogpost.save()
362368
blogpost.tags = [Tag.objects.get(name="house")]
363-
369+
364370
################################
365371
# Recursive relations
366372
################################
@@ -600,10 +606,10 @@ class Meta:
600606
date_str = JSONRenderer().render(data['date_joined']).decode('utf-8').strip('"')
601607

602608
# Adjust for imprecise rendering of time
603-
datetime_str = JSONRenderer().render(date_parse(data['datetime_joined']) + datetime.timedelta(seconds=0.6)).decode('utf-8').strip('"')
609+
datetime_str = JSONRenderer().render(parse_datetime(data['datetime_joined']) + datetime.timedelta(seconds=0.6)).decode('utf-8').strip('"')
604610

605611
# Adjust for imprecise rendering of time
606-
dt = datetime.datetime.combine(datetime.date.today(), date_parse(data['time_joined']).time()) + datetime.timedelta(seconds=0.6)
612+
dt = datetime.datetime.combine(datetime.date.today(), parse_time(data['time_joined'])) + datetime.timedelta(seconds=0.6)
607613
time_str = JSONRenderer().render(dt.time()).decode('utf-8').strip('"')
608614

609615
# DateField
@@ -632,6 +638,35 @@ class Meta:
632638
p = list(f)[0]
633639
self.assertEqual(p.name, "John")
634640

641+
@override_settings(USE_TZ=True)
642+
def test_datetime_timezone_awareness(self):
643+
# Addresses issue #24 - ensure that datetime strings terminating
644+
# in 'Z' are correctly handled.
645+
from rest_framework import serializers
646+
from rest_framework.renderers import JSONRenderer
647+
648+
class PersonSerializer(serializers.ModelSerializer):
649+
class Meta:
650+
model = Person
651+
652+
# Figure out what the date strings should look like based on the
653+
# serializer output.
654+
john = Person.objects.get(name="John")
655+
data = PersonSerializer(john).data
656+
datetime_str = JSONRenderer().render(parse_datetime(data['datetime_joined']) + datetime.timedelta(seconds=0.6)).decode('utf-8').strip('"')
657+
658+
# This is more for documentation - DRF appends a 'Z' to timezone aware UTC datetimes when rendering:
659+
# https://github.com/tomchristie/django-rest-framework/blob/3.2.0/rest_framework/fields.py#L1002-L1006
660+
self.assertTrue(datetime_str.endswith('Z'))
661+
662+
GET = {
663+
'datetime_joined__lte': datetime_str,
664+
}
665+
f = AllLookupsPersonDateFilter(GET, queryset=Person.objects.all())
666+
self.assertEqual(len(list(f)), 1)
667+
p = list(f)[0]
668+
self.assertEqual(p.name, "John")
669+
635670
def test_inset_filter(self):
636671
p1 = Person.objects.get(name="John").pk
637672
p2 = Person.objects.get(name="Mark").pk

0 commit comments

Comments
 (0)