Repository: carltongibson/django-filter

Issue:
Add support for ISO 8601 format dates/datetimes
See [Filtering on Date Fields With `DjangoFilterBackend` Doesn't Quite Work. · Issue #1338 · tomchristie/django-rest-framework](https://github.com/tomchristie/django-rest-framework/issues/1338) over on DRF. 

It [looks like there's no fix coming in Django soon](https://code.djangoproject.com/ticket/23448) so we should add something here. 

:loudspeaker: @copitux — sorry for the interruption — Would we be able to incorporate your code from [Iso 8601 DateTimeFilter to Django Rest Framework and Django-Filter](https://gist.github.com/copitux/5773821)? (It doesn't provide licence info.)



Discussion:
Yes of course. I've just updated it with [MIT](http://opensource.org/licenses/mit-license.php) :wink:

@copitux 👍 — Good man! (Thanks!)

+1, this coming in anytime soon?

Actually I found out that using either Arrow or Delorean for parsing the dates is way better then adding support for a specific format.
I'd be willing to provide form fields that parses dates with those libraries.
The form fields won't introduce hard dependencies but will allow the client to filter with any format that Arrow or Delorean are familiar with.

Hi @thedrow. 

This sounds great. What would it look like from the declaration point of view, and would it cover all the cases?

I'm happy to recommend a dependency if the benefit is there...

I have no idea actually. Maybe we should consider introducing a datetime_parser keyword argument that will determine the form field's datetime parser.


Resolving patch:
diff --git a/django_filters/fields.py b/django_filters/fields.py
--- a/django_filters/fields.py
+++ b/django_filters/fields.py
@@ -5,6 +5,13 @@
 from collections import namedtuple
 
 from django import forms
+from django.utils.dateparse import parse_datetime
+
+# TODO: Remove this once Django 1.4 is EOL.
+try:
+    from django.utils.encoding import force_str
+except ImportError:
+    force_str = None
 
 from .widgets import RangeWidget, LookupTypeWidget
 
@@ -71,3 +78,28 @@ def compress(self, data_list):
         if len(data_list)==2:
             return Lookup(value=data_list[0], lookup_type=data_list[1] or 'exact')
         return Lookup(value=None, lookup_type='exact')
+
+
+class IsoDateTimeField(forms.DateTimeField):
+    """
+    Supports 'iso-8601' date format too which is out the scope of
+    the ``datetime.strptime`` standard library
+
+    # ISO 8601: ``http://www.w3.org/TR/NOTE-datetime``
+
+    Based on Gist example by David Medina https://gist.github.com/copitux/5773821
+    """
+    ISO_8601 = 'iso-8601'
+    input_formats = [ISO_8601]
+
+    def strptime(self, value, format):
+        # TODO: Remove this once Django 1.4 is EOL.
+        if force_str is not None:
+            value = force_str(value)
+
+        if format == self.ISO_8601:
+            parsed = parse_datetime(value)
+            if parsed is None:  # Continue with other formats if doesn't match
+                raise ValueError
+            return parsed
+        return super(IsoDateTimeField, self).strptime(value, format)
diff --git a/django_filters/filters.py b/django_filters/filters.py
--- a/django_filters/filters.py
+++ b/django_filters/filters.py
@@ -12,13 +12,13 @@
 from django.utils.translation import ugettext_lazy as _
 
 from .fields import (
-    RangeField, LookupTypeField, Lookup, DateRangeField, TimeRangeField)
+    RangeField, LookupTypeField, Lookup, DateRangeField, TimeRangeField, IsoDateTimeField)
 
 
 __all__ = [
     'Filter', 'CharFilter', 'BooleanFilter', 'ChoiceFilter',
     'TypedChoiceFilter', 'MultipleChoiceFilter', 'DateFilter',
-    'DateTimeFilter', 'TimeFilter', 'ModelChoiceFilter',
+    'DateTimeFilter', 'IsoDateTimeFilter', 'TimeFilter', 'ModelChoiceFilter',
     'ModelMultipleChoiceFilter', 'NumberFilter', 'NumericRangeFilter', 'RangeFilter',
     'DateRangeFilter', 'DateFromToRangeFilter', 'TimeRangeFilter',
     'AllValuesFilter', 'MethodFilter'
@@ -169,6 +169,17 @@ class DateFilter(Filter):
 class DateTimeFilter(Filter):
     field_class = forms.DateTimeField
 
+class IsoDateTimeFilter(DateTimeFilter):
+    """
+    Uses IsoDateTimeField to support filtering on ISO 8601 formated datetimes.
+
+    For context see:
+
+    * https://code.djangoproject.com/ticket/23448
+    * https://github.com/tomchristie/django-rest-framework/issues/1338
+    * https://github.com/alex/django-filter/pull/264
+    """
+    field_class = IsoDateTimeField
 
 class TimeFilter(Filter):
     field_class = forms.TimeField


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -16,7 +16,7 @@
 
 from django_filters.widgets import RangeWidget
 from django_filters.fields import (
-    RangeField, LookupTypeField, Lookup, DateRangeField, TimeRangeField)
+    RangeField, LookupTypeField, Lookup, DateRangeField, TimeRangeField, IsoDateTimeField)
 
 def to_d(float_value):
     return decimal.Decimal('%.2f' % float_value)
@@ -117,3 +117,16 @@ def test_render_used_html5(self):
                 <option value="gt">gt</option>
                 <option selected="selected" value="lt">lt</option>
             </select>""")
+
+
+class IsoDateTimeFieldTests(TestCase):
+
+    def test_datetime_string_is_parsed(self):
+        f = IsoDateTimeField()
+        d = f.strptime("2015-07-19T13:34:51.759", IsoDateTimeField.ISO_8601)
+        self.assertTrue(isinstance(d, datetime))
+
+    def test_datetime_string_with_timezone_is_parsed(self):
+        f = IsoDateTimeField()
+        d = f.strptime("2015-07-19T13:34:51.759+01:00", IsoDateTimeField.ISO_8601)
+        self.assertTrue(isinstance(d, datetime))
\ No newline at end of file