Skip to content

Commit 35c60e5

Browse files
authored
Merge pull request #49 from cloudblue/LITE-24280_support_get_rql_filter_class
LITE-24280: Add support for get_rql_filter_class() in views
2 parents 283d376 + dbef712 commit 35c60e5

File tree

9 files changed

+219
-23
lines changed

9 files changed

+219
-23
lines changed

Diff for: dj_rql/drf/backend.py

+26-5
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,26 @@ def clear(cls):
2222
class RQLFilterBackend(BaseFilterBackend):
2323
""" RQL filter backend for DRF GenericAPIViews.
2424
25-
Examples:
25+
Set the backend filter for the ``GenericAPIView`` class-based view, and set the
26+
``rql_filter_class`` class attribute to the ``RQLFilterClass`` to use:
27+
28+
.. code-block:: python
29+
2630
class ViewSet(mixins.ListModelMixin, GenericViewSet):
2731
filter_backends = (RQLFilterBackend,)
2832
rql_filter_class = ModelFilterClass
33+
34+
Yo can also add a ``get_rql_filter_class()`` method to the view to get the filter class:
35+
36+
.. code-block:: python
37+
38+
class ViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, GenericViewSet):
39+
filter_backends = (RQLFilterBackend,)
40+
41+
def get_rql_filter_class(self):
42+
if self.action == 'retrieve':
43+
return ModelDetailFilterClass
44+
return ModelFilterClass
2945
"""
3046
OPENAPI_RETRIEVE_SPECIFICATION = False
3147

@@ -85,6 +101,8 @@ def get_schema_operation_parameters(self, view):
85101

86102
@staticmethod
87103
def get_filter_class(view):
104+
if hasattr(view, 'get_rql_filter_class') and callable(view.get_rql_filter_class):
105+
return view.get_rql_filter_class()
88106
return getattr(view, 'rql_filter_class', None)
89107

90108
@classmethod
@@ -93,14 +111,14 @@ def get_query(cls, filter_instance, request, view):
93111

94112
@classmethod
95113
def _get_or_init_cache(cls, filter_class, view):
96-
qual_name = cls._get_filter_cls_qual_name(view)
114+
qual_name = cls._get_filter_cls_qual_name(view, filter_class)
97115
return cls._CACHES.setdefault(
98116
qual_name, filter_class.QUERIES_CACHE_BACKEND(int(filter_class.QUERIES_CACHE_SIZE)),
99117
)
100118

101119
@classmethod
102120
def _get_filter_instance(cls, filter_class, queryset, view):
103-
qual_name = cls._get_filter_cls_qual_name(view)
121+
qual_name = cls._get_filter_cls_qual_name(view, filter_class)
104122

105123
filter_instance = _FilterClassCache.CACHE.get(qual_name)
106124
if filter_instance:
@@ -111,5 +129,8 @@ def _get_filter_instance(cls, filter_class, queryset, view):
111129
return filter_instance
112130

113131
@staticmethod
114-
def _get_filter_cls_qual_name(view):
115-
return '{0}.{1}'.format(view.__class__.__module__, view.__class__.__name__)
132+
def _get_filter_cls_qual_name(view, filter_class):
133+
return '{0}.{1}+{2}.{3}'.format(
134+
view.__class__.__module__, view.__class__.__name__,
135+
filter_class.__module__, filter_class.__name__,
136+
)

Diff for: setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def read_file(name):
2626
include_package_data=True,
2727
install_requires=read_file('requirements/dev.txt').splitlines(),
2828
tests_require=read_file('requirements/test.txt').splitlines(),
29-
setup_requires=['setuptools_scm', 'pytest-runner', 'wheel'],
29+
setup_requires=['setuptools_scm<7', 'pytest-runner', 'wheel'],
3030
extras_require={
3131
'drf': read_file('requirements/extra.txt').splitlines(),
3232
},

Diff for: tests/dj_rf/filters.py

+17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#
22
# Copyright © 2022 Ingram Micro Inc. All rights reserved.
33
#
4+
from copy import deepcopy
5+
46
from cachetools import LFUCache, LRUCache
57

68
from dj_rql.fields import SelectField
@@ -48,6 +50,7 @@ class BooksFilterClass(RQLFilterClass):
4850
'openapi': {
4951
'required': True,
5052
},
53+
'hidden': True,
5154
}, {
5255
'filter': 'author__email',
5356
'search': True,
@@ -67,6 +70,7 @@ class BooksFilterClass(RQLFilterClass):
6770
'filters': AUTHOR_FILTERS,
6871
'distinct': True,
6972
'qs': SR('author', 'author__publisher'),
73+
'hidden': True,
7074
}, {
7175
'namespace': 'page',
7276
'source': 'pages',
@@ -89,6 +93,7 @@ class BooksFilterClass(RQLFilterClass):
8993
'filter': 'amazon_rating',
9094
'lookups': {FilterLookups.GE, FilterLookups.LT},
9195
'null_values': {'random'},
96+
'hidden': True,
9297
}, {
9398
'filter': 'url',
9499
'source': 'publishing_url',
@@ -196,3 +201,15 @@ class SelectBooksFilterClass(BooksFilterClass):
196201
SELECT = True
197202
QUERIES_CACHE_BACKEND = LRUCache
198203
QUERIES_CACHE_SIZE = 100
204+
205+
206+
class SelectDetailedBooksFilterClass(SelectBooksFilterClass):
207+
208+
def __make_filters():
209+
result = deepcopy(BooksFilterClass.FILTERS)
210+
result[4]['hidden'] = False # status
211+
result[7]['hidden'] = False # author
212+
result[12]['hidden'] = False # amazon_rating
213+
return result
214+
215+
FILTERS = __make_filters()

Diff for: tests/dj_rf/serializers.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright © 2021 Ingram Micro Inc. All rights reserved.
2+
# Copyright © 2022 Ingram Micro Inc. All rights reserved.
33
#
44

55
from dj_rql.drf.serializers import RQLMixin
@@ -68,6 +68,8 @@ class Meta:
6868
'author_ref', # One level reference field (FK)
6969
'author', # Deep nested fields (FK)
7070
'pages', # List of backrefs
71+
'status',
72+
'amazon_rating',
7173
)
7274

7375
def get_author(self, obj):

Diff for: tests/dj_rf/urls.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright © 2021 Ingram Micro Inc. All rights reserved.
2+
# Copyright © 2022 Ingram Micro Inc. All rights reserved.
33
#
44

55
from django.conf.urls import include
@@ -8,7 +8,8 @@
88
from rest_framework.routers import SimpleRouter
99

1010
from tests.dj_rf.view import (
11-
AutoViewSet, DRFViewSet, DjangoFiltersViewSet, NoFilterClsViewSet, SelectViewSet,
11+
AutoViewSet, DRFViewSet, DjangoFiltersViewSet, DynamicFilterClsViewSet, NoFilterClsViewSet,
12+
SelectViewSet,
1213
)
1314

1415

@@ -18,6 +19,7 @@
1819
router.register(r'select', SelectViewSet, basename='select')
1920
router.register(r'nofiltercls', NoFilterClsViewSet, basename='nofiltercls')
2021
router.register(r'auto', AutoViewSet, basename='auto')
22+
router.register(r'dynamicfiltercls', DynamicFilterClsViewSet, basename='dynamicfiltercls')
2123

2224
urlpatterns = [
2325
re_path(r'^', include(router.urls)),

Diff for: tests/dj_rf/view.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright © 2021 Ingram Micro Inc. All rights reserved.
2+
# Copyright © 2022 Ingram Micro Inc. All rights reserved.
33
#
44

55
from dj_rql.drf.backend import RQLFilterBackend
@@ -14,7 +14,9 @@
1414
from rest_framework.response import Response
1515
from rest_framework.viewsets import GenericViewSet
1616

17-
from tests.dj_rf.filters import BooksFilterClass, SelectBooksFilterClass
17+
from tests.dj_rf.filters import (
18+
BooksFilterClass, SelectBooksFilterClass, SelectDetailedBooksFilterClass,
19+
)
1820
from tests.dj_rf.models import Book
1921
from tests.dj_rf.serializers import BookSerializer, SelectBookSerializer
2022

@@ -56,6 +58,15 @@ class SelectViewSet(mixins.RetrieveModelMixin, DRFViewSet):
5658
rql_filter_class = SelectBooksFilterClass
5759

5860

61+
class DynamicFilterClsViewSet(mixins.RetrieveModelMixin, DRFViewSet):
62+
serializer_class = SelectBookSerializer
63+
64+
def get_rql_filter_class(self):
65+
if self.action == 'retrieve':
66+
return SelectDetailedBooksFilterClass
67+
return SelectBooksFilterClass
68+
69+
5970
class NoFilterClsViewSet(DRFViewSet):
6071
rql_filter_class = None
6172

Diff for: tests/test_drf/test_common_drf_backend.py

+77-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright © 2021 Ingram Micro Inc. All rights reserved.
2+
# Copyright © 2022 Ingram Micro Inc. All rights reserved.
33
#
44
from cachetools import LFUCache, LRUCache
55

@@ -12,7 +12,7 @@
1212
import pytest
1313

1414
from rest_framework.reverse import reverse
15-
from rest_framework.status import HTTP_200_OK
15+
from rest_framework.status import HTTP_200_OK, HTTP_404_NOT_FOUND
1616

1717
from tests.dj_rf.models import Book
1818

@@ -103,7 +103,7 @@ def test_filter_cls_cache(api_client, clear_cache):
103103
response = api_client.get('{0}?{1}'.format(reverse('book-list'), 'title=F'))
104104
assert response.data == [{'id': books[0].pk}]
105105

106-
expected_cache_key = 'tests.dj_rf.view.DRFViewSet'
106+
expected_cache_key = 'tests.dj_rf.view.DRFViewSet+tests.dj_rf.filters.BooksFilterClass'
107107
assert expected_cache_key in _FilterClassCache.CACHE
108108
cache_item_id = id(_FilterClassCache.CACHE[expected_cache_key])
109109

@@ -117,6 +117,42 @@ def test_filter_cls_cache(api_client, clear_cache):
117117
assert _FilterClassCache.CACHE == {}
118118

119119

120+
@pytest.mark.django_db
121+
def test_dynamic_filter_cls_cache(api_client, clear_cache):
122+
books = [
123+
Book.objects.create(title='F'),
124+
Book.objects.create(title='G'),
125+
]
126+
127+
list_cache_key = '{0}+{1}'.format(
128+
'tests.dj_rf.view.DynamicFilterClsViewSet',
129+
'tests.dj_rf.filters.SelectBooksFilterClass',
130+
)
131+
detail_cache_key = '{0}+{1}'.format(
132+
'tests.dj_rf.view.DynamicFilterClsViewSet',
133+
'tests.dj_rf.filters.SelectDetailedBooksFilterClass',
134+
)
135+
136+
assert _FilterClassCache.CACHE == {}
137+
138+
api_client.get('{0}?{1}'.format(reverse('dynamicfiltercls-list'), 'title=F'))
139+
assert list_cache_key in _FilterClassCache.CACHE
140+
141+
list_cache_item_id = id(_FilterClassCache.CACHE[list_cache_key])
142+
api_client.get('{0}?{1}'.format(reverse('dynamicfiltercls-list'), 'title=G'))
143+
assert len(_FilterClassCache.CACHE) == 1
144+
assert id(_FilterClassCache.CACHE[list_cache_key]) == list_cache_item_id
145+
146+
api_client.get(reverse('dynamicfiltercls-detail', [books[0].pk]))
147+
assert len(_FilterClassCache.CACHE) == 2
148+
assert detail_cache_key in _FilterClassCache.CACHE
149+
150+
detail_cache_item_id = id(_FilterClassCache.CACHE[detail_cache_key])
151+
api_client.get(reverse('dynamicfiltercls-detail', [books[1].pk]))
152+
assert len(_FilterClassCache.CACHE) == 2
153+
assert id(_FilterClassCache.CACHE[detail_cache_key]) == detail_cache_item_id
154+
155+
120156
@pytest.mark.django_db
121157
def test_query_cache(api_client, clear_cache, django_assert_num_queries):
122158
books = [
@@ -136,13 +172,45 @@ def test_query_cache(api_client, clear_cache, django_assert_num_queries):
136172
assert response.status_code == HTTP_200_OK
137173
assert 'id' not in response.data[0]
138174

175+
response = api_client.get('{0}?{1}'.format(reverse('dynamicfiltercls-list'), 'title=F'))
176+
assert response.data[0]['id'] == books[0].pk
177+
178+
response = api_client.get(reverse('dynamicfiltercls-list') + '?select(author)')
179+
assert len(response.data) == 2
180+
181+
response = api_client.get('{0}?{1}'.format(reverse('dynamicfiltercls-list'), 'title=X'))
182+
assert response.data == []
183+
184+
response = api_client.get(reverse('dynamicfiltercls-detail', [books[0].pk]))
185+
assert response.data['id'] == books[0].pk
186+
187+
response = api_client.get(reverse('dynamicfiltercls-detail', ['non-exists']))
188+
assert response.status_code == HTTP_404_NOT_FOUND
189+
139190
caches = RQLFilterBackend._CACHES
140-
assert isinstance(caches['tests.dj_rf.view.DRFViewSet'], LFUCache)
141-
assert caches['tests.dj_rf.view.DRFViewSet'].currsize == 2
142-
assert caches['tests.dj_rf.view.DRFViewSet'].maxsize == 20
143-
assert isinstance(caches['tests.dj_rf.view.SelectViewSet'], LRUCache)
144-
assert caches['tests.dj_rf.view.SelectViewSet'].currsize == 1
145-
assert caches['tests.dj_rf.view.SelectViewSet'].maxsize == 100
191+
cache = caches['tests.dj_rf.view.DRFViewSet+tests.dj_rf.filters.BooksFilterClass']
192+
assert isinstance(cache, LFUCache)
193+
assert cache.currsize == 2
194+
assert cache.maxsize == 20
195+
196+
cache = caches['tests.dj_rf.view.SelectViewSet+tests.dj_rf.filters.SelectBooksFilterClass']
197+
assert isinstance(cache, LRUCache)
198+
assert cache.currsize == 1
199+
assert cache.maxsize == 100
200+
201+
cache = caches[
202+
'tests.dj_rf.view.DynamicFilterClsViewSet'
203+
'+tests.dj_rf.filters.SelectBooksFilterClass'
204+
]
205+
assert isinstance(cache, LRUCache)
206+
assert cache.currsize == 3
207+
208+
cache = caches[
209+
'tests.dj_rf.view.DynamicFilterClsViewSet'
210+
'+tests.dj_rf.filters.SelectDetailedBooksFilterClass'
211+
]
212+
assert isinstance(cache, LRUCache)
213+
assert cache.currsize == 1
146214

147215

148216
@pytest.mark.django_db

Diff for: tests/test_drf/test_dynamic_filter.py

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#
2+
# Copyright © 2022 Ingram Micro Inc. All rights reserved.
3+
#
4+
5+
import pytest
6+
7+
from rest_framework.reverse import reverse
8+
from rest_framework.status import HTTP_200_OK
9+
10+
from tests.dj_rf.models import Author, Book, Publisher
11+
12+
13+
@pytest.mark.django_db
14+
def test_detail_default(api_client, clear_cache):
15+
publisher = Publisher.objects.create(name='publisher')
16+
author = Author.objects.create(name='auth', publisher=publisher)
17+
book = Book.objects.create(author=author, status=Book.PLANNING, amazon_rating=5.0)
18+
19+
response = api_client.get(reverse('dynamicfiltercls-detail', [book.pk]))
20+
21+
assert response.status_code == HTTP_200_OK
22+
assert 'author' in response.data
23+
assert 'status' in response.data
24+
assert 'amazon_rating' in response.data
25+
26+
27+
@pytest.mark.django_db
28+
def test_detail_exclude_fields(api_client, clear_cache):
29+
publisher = Publisher.objects.create(name='publisher')
30+
author = Author.objects.create(name='auth', publisher=publisher)
31+
book = Book.objects.create(author=author, status=Book.PLANNING, amazon_rating=5.0)
32+
33+
response = api_client.get(
34+
reverse('dynamicfiltercls-detail', [book.pk])
35+
+ '?select(-author,-status,-amazon_rating)',
36+
)
37+
38+
assert response.status_code == HTTP_200_OK
39+
assert 'author' not in response.data
40+
assert 'status' not in response.data
41+
assert 'amazon_rating' not in response.data
42+
43+
44+
@pytest.mark.django_db
45+
def test_list_default(api_client, clear_cache):
46+
publisher = Publisher.objects.create(name='publisher')
47+
author = Author.objects.create(name='auth', publisher=publisher)
48+
Book.objects.create(author=author, status=Book.PLANNING, amazon_rating=5.0)
49+
50+
response = api_client.get(reverse('dynamicfiltercls-list'))
51+
52+
assert response.status_code == HTTP_200_OK
53+
assert 'author' not in response.data[0]
54+
assert 'status' not in response.data[0]
55+
assert 'amazon_rating' not in response.data[0]
56+
57+
58+
@pytest.mark.django_db
59+
def test_list_include_fields(api_client, clear_cache):
60+
publisher = Publisher.objects.create(name='publisher')
61+
author = Author.objects.create(name='auth', publisher=publisher)
62+
Book.objects.create(author=author, status=Book.PLANNING, amazon_rating=5.0)
63+
64+
response = api_client.get(
65+
reverse('dynamicfiltercls-list')
66+
+ '?select(author,status,amazon_rating)',
67+
)
68+
69+
assert response.status_code == HTTP_200_OK
70+
assert 'author' in response.data[0]
71+
assert 'status' in response.data[0]
72+
assert 'amazon_rating' in response.data[0]

0 commit comments

Comments
 (0)