Repository: carltongibson/django-filter

Issue:
Add a datetimefromtorangefilter
It would be nice to have a datetime range filter in addition to the date/time range filters.



Discussion:
@creynold have you build a solution? I was looking for the same thing

OK. As @newfylox on #369 the [docs say `DateRangeField` already handles `DateTimes` too](http://django-filter.readthedocs.org/en/latest/ref/filters.html#datefromtorangefilter) — but it doesn't.

Looking at #215 — when the field was added — it doesn't seem `DateTime` compatibility is actually tested. 

Two options here: 
1. Add some tests and then compat and docs on usage for `DateTimes` to `DateRangeField`. 
2. Add a separate `DateTimeRangeField` (with docs and tests)

I'm happy for anyone to take that on. 

//cc. @bogdal 

@carltongibson - I believe that the `DateRangeField` is intended to validate date ranges, which in turn can be used to filter date and datetime fields on the model. If you look at the [compress](https://github.com/alex/django-filter/blob/0.12.0/django_filters/fields.py#L41-L49) method, it converts the validated date range into a datetime range that is inclusive of datetimes occuring on the end date.

I don't think the field is intended to validate datetime ranges - the docs are just a little confusing/ambiguous.

@rpkilby Brilliant. Good link. 

This is a documentation issue. Since it's come up more than once, we need a couple of decent examples showing the filtering in action for **BOTH** Date and DateTime fields. 

So to sum up:
- Clarify documentation for `DateRangeField`/`DateFromToRangeFilter`.
- Add tests for above documentation that demonstrate the datetime behavior.
- Add `DateTimeRangeField` & `DateTimeFromToRangeFilter` with tests and docs.

@rpkilby You are absolutely right. Docs could be misleading. Right now I'm working on `DateTimeRangeField` and `DateTimeFromToRangeFilter`. Stay tuned.

@bogdal - sweet. 
Side note,  am still working on the CSV equivalent of these.

@rpkilby — I was hoping you'd say that. :-)

@bogdal — Awesome! Thanks. 


Resolving patch:
diff --git a/django_filters/fields.py b/django_filters/fields.py
--- a/django_filters/fields.py
+++ b/django_filters/fields.py
@@ -49,6 +49,15 @@ def compress(self, data_list):
         return None
 
 
+class DateTimeRangeField(RangeField):
+
+    def __init__(self, *args, **kwargs):
+        fields = (
+            forms.DateTimeField(),
+            forms.DateTimeField())
+        super(DateTimeRangeField, self).__init__(fields, *args, **kwargs)
+
+
 class TimeRangeField(RangeField):
 
     def __init__(self, *args, **kwargs):
diff --git a/django_filters/filters.py b/django_filters/filters.py
--- a/django_filters/filters.py
+++ b/django_filters/filters.py
@@ -12,7 +12,8 @@
 from django.utils.translation import ugettext_lazy as _
 
 from .fields import (
-    RangeField, LookupTypeField, Lookup, DateRangeField, TimeRangeField, IsoDateTimeField)
+    RangeField, LookupTypeField, Lookup, DateRangeField, DateTimeRangeField,
+    TimeRangeField, IsoDateTimeField)
 
 
 __all__ = [
@@ -20,8 +21,8 @@
     'TypedChoiceFilter', 'MultipleChoiceFilter', 'DateFilter',
     'DateTimeFilter', 'IsoDateTimeFilter', 'TimeFilter', 'ModelChoiceFilter',
     'ModelMultipleChoiceFilter', 'NumberFilter', 'NumericRangeFilter', 'RangeFilter',
-    'DateRangeFilter', 'DateFromToRangeFilter', 'TimeRangeFilter',
-    'AllValuesFilter', 'MethodFilter'
+    'DateRangeFilter', 'DateFromToRangeFilter', 'DateTimeFromToRangeFilter',
+    'TimeRangeFilter', 'AllValuesFilter', 'MethodFilter'
 ]
 
 
@@ -310,6 +311,10 @@ class DateFromToRangeFilter(RangeFilter):
     field_class = DateRangeField
 
 
+class DateTimeFromToRangeFilter(RangeFilter):
+    field_class = DateTimeRangeField
+
+
 class TimeRangeFilter(RangeFilter):
     field_class = TimeRangeField
 


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -12,7 +12,8 @@
 
 from django_filters.widgets import RangeWidget
 from django_filters.fields import (
-    RangeField, LookupTypeField, Lookup, DateRangeField, TimeRangeField, IsoDateTimeField)
+    RangeField, LookupTypeField, Lookup, DateRangeField, DateTimeRangeField,
+    TimeRangeField, IsoDateTimeField)
 
 
 def to_d(float_value):
@@ -54,16 +55,32 @@ def test_field(self):
         f = DateRangeField()
         self.assertEqual(len(f.fields), 2)
 
+    @override_settings(USE_TZ=False)
     def test_clean(self):
         w = RangeWidget()
         f = DateRangeField(widget=w)
-
         self.assertEqual(
             f.clean(['2015-01-01', '2015-01-10']),
             slice(datetime(2015, 1, 1, 0, 0, 0),
                   datetime(2015, 1, 10, 23, 59, 59, 999999)))
 
 
+class DateTimeRangeFieldTests(TestCase):
+
+    def test_field(self):
+        f = DateTimeRangeField()
+        self.assertEqual(len(f.fields), 2)
+
+    @override_settings(USE_TZ=False)
+    def test_clean(self):
+        w = RangeWidget()
+        f = DateTimeRangeField(widget=w)
+        self.assertEqual(
+            f.clean(['2015-01-01 10:30', '2015-01-10 8:45']),
+            slice(datetime(2015, 1, 1, 10, 30, 0),
+                  datetime(2015, 1, 10, 8, 45, 0)))
+
+
 class TimeRangeFieldTests(TestCase):
 
     def test_field(self):
diff --git a/tests/test_filters.py b/tests/test_filters.py
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -1,19 +1,20 @@
 from __future__ import absolute_import
 from __future__ import unicode_literals
 
-from datetime import date, time, timedelta
+from datetime import date, time, timedelta, datetime
 import mock
 import unittest
 
 import django
 from django import forms
-from django.test import TestCase
+from django.test import TestCase, override_settings
 
-from django_filters import filters
+from django_filters import filters, FilterSet
 from django_filters.fields import (
     Lookup,
     RangeField,
     DateRangeField,
+    DateTimeRangeField,
     TimeRangeField,
     LookupTypeField)
 from django_filters.filters import (
@@ -32,12 +33,13 @@
     RangeFilter,
     DateRangeFilter,
     DateFromToRangeFilter,
+    DateTimeFromToRangeFilter,
     TimeRangeFilter,
     AllValuesFilter,
     UUIDFilter,
     LOOKUP_TYPES)
 
-from tests.models import Book, User
+from tests.models import Book, User, Article, Comment
 
 
 class FilterTests(TestCase):
@@ -735,6 +737,101 @@ def test_filtering_ignores_lookup_type(self):
         qs.filter.assert_called_once_with(
             None__range=(date(2015, 4, 7), date(2015, 9, 6)))
 
+    def test_filtering_queryset(self):
+        class F(FilterSet):
+            published = DateFromToRangeFilter(name='date')
+            class Meta:
+                model = Comment
+                fields = ['date']
+        adam = User.objects.create(username='adam')
+        kwargs = {'text': 'test', 'author': adam, 'time': '10:00'}
+        Comment.objects.create(date=date(2016, 1, 1), **kwargs)
+        Comment.objects.create(date=date(2016, 1, 2), **kwargs)
+        Comment.objects.create(date=date(2016, 1, 3), **kwargs)
+        Comment.objects.create(date=date(2016, 1, 3), **kwargs)
+        results = F(data={
+            'published_0': '2016-01-02',
+            'published_1': '2016-01-03'})
+        self.assertEqual(len(results.qs), 3)
+
+    @override_settings(USE_TZ=False)
+    def test_filtering_ignores_time(self):
+        class F(FilterSet):
+            published = DateFromToRangeFilter()
+            class Meta:
+                model = Article
+                fields = ['published']
+        Article.objects.create(published=datetime(2016, 1, 1, 10, 0))
+        Article.objects.create(published=datetime(2016, 1, 2, 12, 45))
+        Article.objects.create(published=datetime(2016, 1, 3, 18, 15))
+        Article.objects.create(published=datetime(2016, 1, 3, 19, 30))
+        results = F(data={
+            'published_0': '2016-01-02',
+            'published_1': '2016-01-03'})
+        self.assertEqual(len(results.qs), 3)
+
+
+class DateTimeFromToRangeFilterTests(TestCase):
+
+    def test_default_field(self):
+        f = DateTimeFromToRangeFilter()
+        field = f.field
+        self.assertIsInstance(field, DateTimeRangeField)
+
+    def test_filtering_range(self):
+        qs = mock.Mock(spec=['filter'])
+        value = mock.Mock(
+            start=datetime(2015, 4, 7, 8, 30), stop=datetime(2015, 9, 6, 11, 45))
+        f = DateTimeFromToRangeFilter()
+        f.filter(qs, value)
+        qs.filter.assert_called_once_with(
+            None__range=(datetime(2015, 4, 7, 8, 30), datetime(2015, 9, 6, 11, 45)))
+
+    def test_filtering_start(self):
+        qs = mock.Mock(spec=['filter'])
+        value = mock.Mock(start=datetime(2015, 4, 7, 8, 30), stop=None)
+        f = DateTimeFromToRangeFilter()
+        f.filter(qs, value)
+        qs.filter.assert_called_once_with(None__gte=datetime(2015, 4, 7, 8, 30))
+
+    def test_filtering_stop(self):
+        qs = mock.Mock(spec=['filter'])
+        value = mock.Mock(start=None, stop=datetime(2015, 9, 6, 11, 45))
+        f = DateTimeFromToRangeFilter()
+        f.filter(qs, value)
+        qs.filter.assert_called_once_with(None__lte=datetime(2015, 9, 6, 11, 45))
+
+    def test_filtering_skipped_with_none_value(self):
+        qs = mock.Mock(spec=['filter'])
+        f = DateTimeFromToRangeFilter()
+        result = f.filter(qs, None)
+        self.assertEqual(qs, result)
+
+    def test_filtering_ignores_lookup_type(self):
+        qs = mock.Mock()
+        value = mock.Mock(
+            start=datetime(2015, 4, 7, 8, 30), stop=datetime(2015, 9, 6, 11, 45))
+        f = DateTimeFromToRangeFilter(lookup_type='gte')
+        f.filter(qs, value)
+        qs.filter.assert_called_once_with(
+            None__range=(datetime(2015, 4, 7, 8, 30), datetime(2015, 9, 6, 11, 45)))
+
+    @override_settings(USE_TZ=False)
+    def test_filtering_queryset(self):
+        class F(FilterSet):
+            published = DateTimeFromToRangeFilter()
+            class Meta:
+                model = Article
+                fields = ['published']
+        Article.objects.create(published=datetime(2016, 1, 1, 10, 0))
+        Article.objects.create(published=datetime(2016, 1, 2, 12, 45))
+        Article.objects.create(published=datetime(2016, 1, 3, 18, 15))
+        Article.objects.create(published=datetime(2016, 1, 3, 19, 30))
+        results = F(data={
+            'published_0': '2016-01-02 10:00',
+            'published_1': '2016-01-03 19:00'})
+        self.assertEqual(len(results.qs), 2)
+
 
 class TimeRangeFilterTests(TestCase):