Repository: carltongibson/django-filter

Issue:
Use DRF BooleanFilter as default for DRF FilterSet
When using Django-filter together with Django Rest Framework, shouldn't the `BooleanFilter` for DRF be used as default for a DRF `FilterSet`?

Currently (**Python 2.7.14**, **Django 1.11.6**, **DRF 3.7.0**, **Django-Filter 1.1.0**) I have to manually specify my boolean fields to use the DRF BooleanFilter to accept lowercase `true`/`false`.


Discussion:
It is. 🙂

I'm guessing you're using something like: 

```
import django_filters

class ProductFilter(django_filters.FilterSet):
      ...
```

You want to be using the DRF specific base FilterSet: 

```
from django_filters import rest_framework as filters

class ProductFilter(filters.FilterSet):
    ...
```

...as per the [Integration with DRF docs topic](http://django-filter.readthedocs.io/en/1.1.0/guide/rest_framework.html#integration-with-drf)
No, I am using the DRF specific FilterSet, this is a repo I'm testing with: https://github.com/OskarPersson/django-filter-drf

```
$ ./manage.py shell
Python 2.7.14 (default, Sep 29 2017, 15:33:24)
[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.37)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from books.models import Book
>>> Book.objects.create(title="foo", boolean=False, null_boolean=False)
<Book: Book object>
>>> Book.objects.create(title="bar", boolean=True, null_boolean=True)
<Book: Book object>
>>> exit()

$ curl http://localhost:8004/books/?format=json&boolean=false
[
    {
        "boolean": false,
        "id": 1,
        "null_boolean": false,
        "title": "foo"
    },
    {
        "boolean": true,
        "id": 2,
        "null_boolean": true,
        "title": "bar"
    }
]

$ curl "http://localhost:8004/books/?format=json&boolean=False"
[
    {
        "boolean": false,
        "id": 1,
        "null_boolean": false,
        "title": "foo"
    }
]
```
Well the DRF `FilterSet` uses the DRF `BooleanFilter`: 

https://github.com/carltongibson/django-filter/blob/a88c2bc98d28686b3b0f2ba5ae9528f38c8bb5a6/django_filters/rest_framework/filterset.py#L11-L15

So what's the `Filter` on the generated `FilterSet`? What else is happening such that it's not what we're expecting?

If you could create a failing test case on the project re-creating your issue we'd be able to see exactly what's going on. 

Note, there's already a test checking the right `Filter` subclass is automatically generated: 

https://github.com/carltongibson/django-filter/blob/a88c2bc98d28686b3b0f2ba5ae9528f38c8bb5a6/tests/rest_framework/test_filterset.py#L28-L31
I have added some tests here: https://github.com/OskarPersson/django-filter-drf/commit/d9365de19fdf53ce3650e0bf537f8dfd390bd317

Here are the results on my machine. I feel like I'm missing something obvious :/

```
$ ./manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F.F
======================================================================
FAIL: test_filter_lowercase (books.tests.FilterTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/Oskar/git/django_filter_drf/books/tests.py", line 36, in test_filter_lowercase
    self.assertEqual(len(res.data), 1)
AssertionError: 2 != 1

======================================================================
FAIL: test_filterset (books.tests.FilterTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/Oskar/git/django_filter_drf/books/tests.py", line 24, in test_filterset
    self.assertIsInstance(result, filters.BooleanFilter)
AssertionError: <django_filters.filters.BooleanFilter object at 0x10af2fdd0> is not an instance of <class 'django_filters.rest_framework.filters.BooleanFilter'>

----------------------------------------------------------------------
Ran 3 tests in 0.016s

FAILED (failures=2)
```
Hmmm. I kind of meant add a test to the DF test suite recreating the issue there, so it’s clear what’s going on. Either you recreate the issue, open a PR and we find the problem or you see what’s happening in your project. 

Resolving patch:
diff --git a/django_filters/rest_framework/filterset.py b/django_filters/rest_framework/filterset.py
--- a/django_filters/rest_framework/filterset.py
+++ b/django_filters/rest_framework/filterset.py
@@ -16,6 +16,7 @@
 FILTER_FOR_DBFIELD_DEFAULTS.update({
     models.DateTimeField: {'filter_class': IsoDateTimeFilter},
     models.BooleanField: {'filter_class': BooleanFilter},
+    models.NullBooleanField: {'filter_class': BooleanFilter},
 })
 
 


Test patch:
diff --git a/tests/models.py b/tests/models.py
--- a/tests/models.py
+++ b/tests/models.py
@@ -51,6 +51,7 @@ class User(models.Model):
     status = models.IntegerField(choices=STATUS_CHOICES, default=0)
 
     is_active = models.BooleanField(default=False)
+    is_employed = models.NullBooleanField(default=False)
 
     favorite_books = models.ManyToManyField('Book', related_name='lovers')
 
diff --git a/tests/rest_framework/test_filterset.py b/tests/rest_framework/test_filterset.py
--- a/tests/rest_framework/test_filterset.py
+++ b/tests/rest_framework/test_filterset.py
@@ -31,6 +31,12 @@ def test_booleanfilter_widget(self):
         self.assertIsInstance(result, filters.BooleanFilter)
         self.assertEqual(result.extra['widget'], BooleanWidget)
 
+    def test_booleanfilter_widget_nullbooleanfield(self):
+        field = User._meta.get_field('is_employed')
+        result = FilterSet.filter_for_field(field, 'is_employed')
+        self.assertIsInstance(result, filters.BooleanFilter)
+        self.assertEqual(result.extra['widget'], BooleanWidget)
+
 
 @skipIf(is_crispy(), 'django_crispy_forms must be installed')
 @override_settings(INSTALLED_APPS=settings.INSTALLED_APPS + ('crispy_forms', ))