-
Notifications
You must be signed in to change notification settings - Fork 130
json complex backend #321
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
json complex backend #321
Changes from 11 commits
ec90ed2
cee0727
deb0a21
ad0b1ca
ab5d0a9
2df4b12
b5191d5
599bc5e
73c5661
14e1224
201c48f
6252a94
84a1a5a
c51d12e
ad5c2e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
import json | ||
from contextlib import contextmanager | ||
|
||
from django.db.models import QuerySet | ||
from django.http import QueryDict | ||
from django_filters import compat | ||
from django_filters.rest_framework import backends | ||
|
@@ -8,6 +10,8 @@ | |
from .complex_ops import combine_complex_queryset, decode_complex_ops | ||
from .filterset import FilterSet | ||
|
||
COMPLEX_JSON_OPERATORS = {"and": QuerySet.__and__, "or": QuerySet.__or__} | ||
|
||
|
||
class RestFrameworkFilterBackend(backends.DjangoFilterBackend): | ||
filterset_base = FilterSet | ||
|
@@ -94,3 +98,60 @@ def get_filtered_querysets(self, querystrings, request, queryset, view): | |
if errors: | ||
raise ValidationError(errors) | ||
return querysets | ||
|
||
|
||
class ComplexJsonFilterBackend(RestFrameworkFilterBackend): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ComplexJsonFilterBackend -> ComplexJSONFilterBackend |
||
complex_filter_param = "json_filters" | ||
|
||
def filter_queryset(self, request, queryset, view): | ||
res = super().filter_queryset(request, queryset, view) | ||
if self.complex_filter_param not in request.query_params: | ||
return res | ||
|
||
encoded_querystring = request.query_params[self.complex_filter_param] | ||
try: | ||
complex_ops = json.loads(encoded_querystring) | ||
return self.combine_filtered_querysets(complex_ops, request, res, view) | ||
except ValidationError as exc: | ||
raise ValidationError({self.complex_filter_param: exc.detail}) | ||
except json.decoder.JSONDecodeError: | ||
raise ValidationError({self.complex_filter_param: "unable to parse json."}) | ||
|
||
def combine_filtered_querysets(self, complex_filter, request, queryset, view): | ||
""" | ||
Function used recursively to filter the complex filter boolean logic | ||
Args: | ||
complex_filter: the json complex filter | ||
request: request | ||
queryset: starting queryset, unfiltered | ||
view: the view | ||
|
||
Returns: | ||
queryset | ||
""" | ||
operator = None | ||
combined_queryset = None | ||
for symbol, complex_operator in COMPLEX_JSON_OPERATORS.items(): | ||
if operator is None and symbol in complex_filter: | ||
operator = complex_operator | ||
for sub_filter in complex_filter[symbol]: | ||
filtered_queryset = self.combine_filtered_querysets(sub_filter, request, queryset, view) | ||
if combined_queryset is None: | ||
combined_queryset = filtered_queryset | ||
else: | ||
combined_queryset = complex_operator(combined_queryset, filtered_queryset) | ||
if operator: | ||
return combined_queryset | ||
|
||
return self.get_filtered_queryset( | ||
"&".join(["{k}={v}".format(k=k, v=v) for k, v in complex_filter.items()]), request, queryset, view | ||
) | ||
|
||
def get_filtered_queryset(self, querystring, request, queryset, view): | ||
original_GET = request._request.GET | ||
request._request.GET = QueryDict(querystring) | ||
try: | ||
res = super().filter_queryset(request, queryset, view) | ||
finally: | ||
request._request.GET = original_GET | ||
return res |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
import json | ||
from urllib.parse import quote, urlencode | ||
|
||
import django_filters | ||
|
@@ -464,3 +465,114 @@ def test_pagination_compatibility(self): | |
[r['username'] for r in response.data['results']], | ||
['user3'] | ||
) | ||
|
||
|
||
class ComplexJsonFilterBackendTests(APITestCase): | ||
ferrants marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@classmethod | ||
def setUpTestData(cls): | ||
models.User.objects.create(username="user1", email="[email protected]") | ||
models.User.objects.create(username="user2", email="[email protected]") | ||
models.User.objects.create(username="user3", email="[email protected]") | ||
models.User.objects.create(username="user4", email="[email protected]") | ||
|
||
def test_valid(self): | ||
readable = json.dumps({ | ||
"or": [ | ||
{ | ||
"username": "user1" | ||
}, | ||
{ | ||
"email__contains": "example.org" | ||
} | ||
] | ||
}) | ||
response = self.client.get('/ffjsoncomplex-users/?json_filters=' + quote(readable), content_type='json') | ||
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
self.assertListEqual( | ||
[r['username'] for r in response.data], | ||
['user1', 'user3', 'user4'] | ||
) | ||
|
||
def test_invalid(self): | ||
readable = json.dumps({ | ||
"or": [ | ||
{ | ||
"username": "user1" | ||
}, | ||
{ | ||
"email__contains": "example.org" | ||
} | ||
] | ||
})[0:10] | ||
response = self.client.get('/ffjsoncomplex-users/?json_filters=' + quote(readable), content_type='json') | ||
|
||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) | ||
self.assertDictEqual(response.json(), { | ||
'json_filters': "unable to parse json.", | ||
}) | ||
|
||
def test_invalid_filterset_errors(self): | ||
readable = json.dumps({ | ||
"or": [ | ||
{ | ||
"id": "foo" | ||
}, | ||
{ | ||
"id": "bar" | ||
} | ||
] | ||
}) | ||
response = self.client.get('/ffjsoncomplex-users/?json_filters=' + quote(readable), content_type='json') | ||
|
||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) | ||
self.assertDictEqual(response.json(), { | ||
'json_filters': { | ||
'id': ["Enter a number."], | ||
}, | ||
}) | ||
|
||
def test_pagination_compatibility(self): | ||
""" | ||
Ensure that complex-filtering does not interfere with additional query param processing. | ||
""" | ||
readable = json.dumps({ | ||
"or": [ | ||
{ | ||
"email__contains": "example.org" | ||
} | ||
] | ||
}) | ||
|
||
# sanity check w/o pagination | ||
response = self.client.get('/ffjsoncomplex-users/?json_filters=' + quote(readable), content_type='json') | ||
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
self.assertListEqual( | ||
[r['username'] for r in response.data], | ||
['user3', 'user4'] | ||
) | ||
|
||
# sanity check w/o complex-filtering | ||
response = self.client.get('/ffjsoncomplex-users/?page_size=1', content_type='json') | ||
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
self.assertIn('results', response.data) | ||
self.assertListEqual( | ||
[r['username'] for r in response.data['results']], | ||
['user1'] | ||
) | ||
|
||
# pagination + complex-filtering | ||
response = self.client.get( | ||
'/ffjsoncomplex-users/?page_size=1&json_filters=' + quote(readable), | ||
content_type='json' | ||
) | ||
|
||
self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
self.assertIn('results', response.data) | ||
self.assertListEqual( | ||
[r['username'] for r in response.data['results']], | ||
['user3'] | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -52,6 +52,19 @@ class pagination_class(pagination.PageNumberPagination): | |
page_size_query_param = 'page_size' | ||
|
||
|
||
class ComplexJsonFilterFieldsUserViewSet(FilterFieldsUserViewSet): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PEP-8 nitpick: |
||
queryset = User.objects.order_by('pk') | ||
filter_backends = (backends.ComplexJsonFilterBackend, ) | ||
filterset_fields = { | ||
'id': '__all__', | ||
'username': '__all__', | ||
'email': '__all__', | ||
} | ||
|
||
class pagination_class(pagination.PageNumberPagination): | ||
page_size_query_param = 'page_size' | ||
|
||
|
||
class UserViewSet(viewsets.ModelViewSet): | ||
queryset = User.objects.all() | ||
serializer_class = UserSerializer | ||
|
Uh oh!
There was an error while loading. Please reload this page.