I add a boolean filter to my viewset using django_filters and if works only when uppercase booleans are passed like enabled=False and not enabled=false.
Isn't REST API supposed to be JSON-based?
This detail seem to leak pythonic nature of django-rest-framework.
Or maybe I miss some obvious way around this "feature"?
http://www.django-rest-framework.org/api-guide/filtering references NullBooleanSelect which is weird - why could API conventions depend on form widgets code?
Yes, the implementation of django-filter there ends up tying internal Django implementation details to the allowed querystring values, and yes that's imperfect. You might consider opening an issue on django-filters or attempt to address it as a PR - I've not looked at django-filter so I'm not sure how complicated that would be, or if the maintainers would consider the issue in-scope or not.
I'm happy to look at any issues/pulls on Django Filter. Please open a ticket there! :)
I'm not sure that it is a problem of Django Filter itself as it knows nothing about REST API, but probably it should include more options, my current solution is:
class TypedChoiceFilter(django_filters.Filter):
from django import forms
field_class = forms.TypedChoiceField
and
from distutils.util import strtobool
class MyModel
...
flag = TypedChoiceFilter(choices=[('true','true'),('false','false')], coerce=strtobool)
a bit hacky but at least it resolves the problem
I'm not sure that it is a problem of Django Filter itself as it knows nothing about REST API
A feature request to allow more relaxed querystring inputs is still valid against that package.
Pragmatically it's also a reasonable thing to do as Django REST framework support by that package is one of the current drivers behind it's continued maintenance.
@tomchristie actually it should be not only relaxed but also limited as currently NullBooleanSelect allows filters like flag=3 and flag=2 which is almost meaningless
@tomchristie Indeed. :)
@qrilka — If you can write up your thoughts and open a ticket on django filter I'm really happy to take a look. (I'm looking to do a release this week so, if you're quick with tests and such... :)
I'm still having this issue. I saw the issue was addressed in django filter but I'm still having the issue. I'm using 0.9.2 of django filter and 3.0.5 of DRF. Any ideas? Thank you
This is still open on Django Filter. alex/django-filter#187
OK. I've just closed this on Django Filter.
The bottom line is that if you need different matching behaviour then you should use a custom widget. Best bet is (probably) to subclass NullBooleanSelect and override value_from_datadict. There's not much work in doing that; it should be the recommended approach.
why could API conventions depend on form widgets code?
The key is that the widgets encapsulate the mapping of query params to an expected value type: strings to bools, lists, ints, ... you name it. Each widget type wraps its own logic in value_from_datadict, making something pretty messy into something quite clean. Django itself covers at least the 80% here; it's easily extensible if your use-case differs. I don't think we should have any qualms about leveraging it.
This issue comes up a lot when you have a REST service that has to support non-Python clients. Most other languages serialize True/False as true/false (all lower case). In addition to carltongibson's solution, this works:
import django_filters
class CaseInsensitiveBooleanFilter(django_filters.Filter):
def filter(self, qs, value):
if value is not None:
lc_value = value.lower()
if lc_value == "true":
value = True
elif lc_value == "false":
value = False
return qs.filter(**{self.name: value})
return qs
Could you please consider re-opening this? It's a problem for REST. I'm using DRF as a backend to Ember, and JS true isn't being unserialised to Python True correctly. It took ages to track down and fix. Thanks to @diamondap for his fix. Note that the fix defaults to searching for True if value.lower() has any value other than "false", which probably isn't what you want.
More code bits to save other people time:
# views.py
import django_filters
class CaseInsensitiveBooleanFilter(django_filters.Filter):
...
class IssueFilter(django_filters.FilterSet):
most_common = CaseInsensitiveBooleanFilter(name="most_common", lookup_type='eq')
class Meta:
model = Issue
fields = ['most_common']
class IssueViewSet(viewsets.ModelViewSet):
filter_class = IssueFilter
@marauder37 regardless of whether it's right or not you could make it a 3rd party library targeting the compatibility with Ember and/or JS.
Two things:
BooleanWidget — added for this kind of case. On 2, the issue is, as ever, time. PRs always welcomed.
For anyone stumbling on this thread, here is a little enhancement to @diamondap code to support the exclude parameter:
class CaseInsensitiveBooleanFilter(django_filters.Filter):
def filter(self, qs, value):
if value is not None:
lc_value = value.lower()
if lc_value == "true":
value = True
elif lc_value == "false":
value = False
return self.get_method(qs)(**{self.name: value})
return qs
@jroeland - this ticket is pretty out of date, and the desired behavior is now supported by django-filter. The recommended usage is:
from django_filters import rest_framework as filters
# declarative syntax
class MyFilterSet(filters.FilterSet):
my_field__isnull = filters.BooleanFilter(name='my_field', lookup_expr='isnull')
class Meta:
model = MyModel
# or with Meta.fields
class MyFilterSet(filters.FilterSet):
class Meta:
model = MyModel
fields = {'my_field': ['isnull'], }
The DRF-compatible filters use the API-friendly BooleanWidget mentioned by carltongibson above. It is no longer necessary to provide a custom filter implementation.
@rpkilby: i'm using the latest version of django-rest (3.5.3) and django-filter (1.0.2)
The snippet above works if I filter by True / False but not when filtering by true / false
However, it does work if I pass the BooleanWidget:
class MyFilterSet(django_filters.FilterSet):
my_field__isnull = django_filters.BooleanFilter(name="my_field", lookup_expr="isnull", widget=BooleanWidget)
class Meta:
model = MyModel
fields = {
'number': ['exact'],
'timestamp': ['exact', 'gt', 'gte', 'lt', 'lte'],
'date': ['exact', 'gt', 'gte', 'lt', 'lte'],
'value': ['exact', 'gt', 'gte', 'lt', 'lte'],
'estimation': ['exact', ],
'my_field': ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull'],
}
Is this how it should be done or am i missing something? :)
@jroeland - note that the import is different.
from django_filters import rest_framework as filters
For people wondering about above examples, it's good enough to import from the rest framework filters. If your field is already a bool you can get away with just doing this:
from django_filters import rest_framework as rest_filters
from .models import MyModel
class MyFilter(rest_filters.FilterSet):
class Meta:
model = MyModel
fields = ('my_field',)
Most helpful comment
This issue comes up a lot when you have a REST service that has to support non-Python clients. Most other languages serialize True/False as true/false (all lower case). In addition to carltongibson's solution, this works: