Skip to content

Commit 5cfe1f8

Browse files
author
Ryan P Kilby
committed
Simplify get_filters
1 parent 8358724 commit 5cfe1f8

File tree

2 files changed

+155
-55
lines changed

2 files changed

+155
-55
lines changed

rest_framework_filters/filterset.py

Lines changed: 81 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ def __new__(cls, name, bases, attrs):
4949

5050
return new_class
5151

52+
@property
53+
def related_filters(self):
54+
# check __dict__ instead of use hasattr. we *don't* want to check
55+
# parents for existence of existing cache. eg, we do not want
56+
# FilterSet.get_subset([...]) to return the same cache.
57+
if '_related_filters' not in self.__dict__:
58+
self._related_filters = OrderedDict([
59+
(name, f) for name, f in six.iteritems(self.base_filters)
60+
if isinstance(f, filters.RelatedFilter)
61+
])
62+
return self._related_filters
63+
5264

5365
class FilterSet(six.with_metaclass(FilterSetMetaclass, filterset.FilterSet)):
5466
filter_overrides = {
@@ -86,53 +98,46 @@ def get_filters(self):
8698
Build a set of filters based on the requested data. The resulting set
8799
will walk `RelatedFilter`s to recursively build the set of filters.
88100
"""
89-
requested_filters = OrderedDict()
90-
91-
# Add plain lookup filters if match. ie, `username__icontains`
92-
for filter_key, filter_value in six.iteritems(self.filters):
93-
exclude_key = '%s!' % filter_key
94-
95-
if filter_key in self.data:
96-
requested_filters[filter_key] = filter_value
97-
98-
if exclude_key in self.data:
99-
filter_value = copy.deepcopy(filter_value)
100-
filter_value.exclude = not filter_value.exclude
101-
requested_filters[exclude_key] = filter_value
102-
103-
# build a map of potential {rel: {filter: value}} data
104-
related_data = OrderedDict()
105-
for filter_key, value in six.iteritems(self.data):
106-
if filter_key not in self.filters:
107-
108-
# skip non lookup/related keys
109-
if LOOKUP_SEP not in filter_key:
110-
continue
111-
112-
rel_name, filter_key = filter_key.split(LOOKUP_SEP, 1)
113-
114-
related_data.setdefault(rel_name, OrderedDict())
115-
related_data[rel_name][filter_key] = value
116-
117-
# walk the related lookup data. If the filter is a RelatedFilter,
118-
# then instantiate its filterset and append its filters.
119-
for rel_name, rel_data in related_data.items():
120-
related_filter = self.filters.get(rel_name, None)
121-
122-
# skip non-`RelatedFilter`s
123-
if not isinstance(related_filter, filters.RelatedFilter):
101+
# build param data for related filters: {rel: {param: value}}
102+
related_data = OrderedDict(
103+
[(name, OrderedDict()) for name in self.__class__.related_filters]
104+
)
105+
for param, value in six.iteritems(self.data):
106+
filter_name, related_param = self.get_related_filter_param(param)
107+
108+
# skip non lookup/related keys
109+
if filter_name is None:
124110
continue
125111

126-
subset_class = related_filter.filterset.get_subset(rel_data)
112+
if filter_name in related_data:
113+
related_data[filter_name][related_param] = value
127114

128-
# initialize and copy filters
129-
filterset = subset_class(data=rel_data)
130-
rel_filters = filterset.get_filters()
131-
for filter_key, filter_value in six.iteritems(rel_filters):
132-
# modify filter name to account for relationship
133-
rel_filter_key = LOOKUP_SEP.join([rel_name, filter_key])
134-
filter_value.name = LOOKUP_SEP.join([related_filter.name, filter_value.name])
135-
requested_filters[rel_filter_key] = filter_value
115+
# build the compiled set of all filters
116+
requested_filters = OrderedDict()
117+
for filter_name, f in six.iteritems(self.filters):
118+
exclude_name = '%s!' % filter_name
119+
120+
# Add plain lookup filters if match. ie, `username__icontains`
121+
if filter_name in self.data:
122+
requested_filters[filter_name] = f
123+
124+
# include exclusion keys
125+
if exclude_name in self.data:
126+
f = copy.deepcopy(f)
127+
f.exclude = not f.exclude
128+
requested_filters[exclude_name] = f
129+
130+
# include filters from related subsets
131+
if isinstance(f, filters.RelatedFilter) and filter_name in related_data:
132+
subset_data = related_data[filter_name]
133+
subset_class = f.filterset.get_subset(subset_data)
134+
filterset = subset_class(data=subset_data)
135+
136+
# modify filter names to account for relationship
137+
for related_name, related_f in six.iteritems(filterset.get_filters()):
138+
related_name = LOOKUP_SEP.join([filter_name, related_name])
139+
related_f.name = LOOKUP_SEP.join([f.name, related_f.name])
140+
requested_filters[related_name] = related_f
136141

137142
return requested_filters
138143

@@ -165,18 +170,45 @@ def get_filter_name(cls, param):
165170
return param[:-1]
166171

167172
# Fallback to matching against relationships. (author__username__endswith).
168-
related_filters = [
169-
name for name, f in six.iteritems(cls.base_filters)
170-
if isinstance(f, filters.RelatedFilter)
171-
]
173+
related_filters = cls.related_filters.keys()
172174

173175
# preference more specific filters. eg, `note__author` over `note`.
174176
for name in sorted(related_filters)[::-1]:
175177
# we need to match against '__' to prevent eager matching against
176178
# like names. eg, note vs note2. Exact matches are handled above.
177-
if param.startswith("%s__" % name):
179+
if param.startswith("%s%s" % (name, LOOKUP_SEP)):
178180
return name
179181

182+
@classmethod
183+
def get_related_filter_param(cls, param):
184+
"""
185+
Get a tuple of (filter name, related param).
186+
187+
ex::
188+
189+
name, param = FilterSet.get_filter_name('author__email__foobar')
190+
assert name == 'author'
191+
assert param = 'email__foobar'
192+
193+
name, param = FilterSet.get_filter_name('author')
194+
assert name is None
195+
assert param is None
196+
197+
"""
198+
related_filters = cls.related_filters.keys()
199+
200+
# preference more specific filters. eg, `note__author` over `note`.
201+
for name in sorted(related_filters)[::-1]:
202+
# we need to match against '__' to prevent eager matching against
203+
# like names. eg, note vs note2. Exact matches are handled above.
204+
if param.startswith("%s%s" % (name, LOOKUP_SEP)):
205+
# strip param + LOOKUP_SET from param
206+
related_param = param[len(name) + len(LOOKUP_SEP):]
207+
return name, related_param
208+
209+
# not a related param
210+
return None, None
211+
180212
@classmethod
181213
def get_subset(cls, params):
182214
"""

tests/test_filterset.py

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,31 @@ def test_nonexistent_related_field(self):
323323
f = NoteFilterWithRelated(GET, queryset=Note.objects.all())
324324
self.assertEqual(len(list(f)), 4)
325325

326+
def test_related_filters_caching(self):
327+
filters = PostFilter.related_filters
328+
329+
self.assertEqual(len(filters), 1)
330+
self.assertIn('note', filters)
331+
self.assertIn('_related_filters', PostFilter.__dict__)
332+
333+
# subset should not use parent's cached related filters.
334+
PostSubset = PostFilter.get_subset(['title'])
335+
self.assertNotIn('_related_filters', PostSubset.__dict__)
336+
337+
filters = PostSubset.related_filters
338+
self.assertIn('_related_filters', PostFilter.__dict__)
339+
340+
self.assertEqual(len(filters), 0)
341+
342+
# ensure subsets don't interact
343+
PostSubset = PostFilter.get_subset(['note'])
344+
self.assertNotIn('_related_filters', PostSubset.__dict__)
345+
346+
filters = PostSubset.related_filters
347+
self.assertIn('_related_filters', PostFilter.__dict__)
348+
349+
self.assertEqual(len(filters), 1)
350+
326351

327352
class GetFilterNameTests(TestCase):
328353

@@ -378,28 +403,71 @@ class Meta:
378403
self.assertEqual('note__author', name)
379404

380405
def test_name_hiding(self):
381-
class PostFilterWithDirectAuthor(PostFilter):
406+
class PostFilterNameHiding(PostFilter):
382407
note__author = filters.RelatedFilter(UserFilter)
383408
note = filters.RelatedFilter(NoteFilterWithAll)
384409
note2 = filters.RelatedFilter(NoteFilterWithAll)
385410

386411
class Meta:
387412
model = Post
388413

389-
name = PostFilterWithDirectAuthor.get_filter_name('note__author')
414+
name = PostFilterNameHiding.get_filter_name('note__author')
390415
self.assertEqual('note__author', name)
391416

392-
name = PostFilterWithDirectAuthor.get_filter_name('note__title')
417+
name = PostFilterNameHiding.get_filter_name('note__title')
418+
self.assertEqual('note', name)
419+
420+
name = PostFilterNameHiding.get_filter_name('note')
393421
self.assertEqual('note', name)
394422

395-
name = PostFilterWithDirectAuthor.get_filter_name('note')
423+
name = PostFilterNameHiding.get_filter_name('note2')
424+
self.assertEqual('note2', name)
425+
426+
name = PostFilterNameHiding.get_filter_name('note2__author')
427+
self.assertEqual('note2', name)
428+
429+
430+
class GetRelatedFilterParamTests(TestCase):
431+
432+
def test_regular_filter(self):
433+
name, param = NoteFilterWithRelated.get_related_filter_param('title')
434+
self.assertIsNone(name)
435+
self.assertIsNone(param)
436+
437+
def test_related_filter_exact(self):
438+
name, param = NoteFilterWithRelated.get_related_filter_param('author')
439+
self.assertIsNone(name)
440+
self.assertIsNone(param)
441+
442+
def test_related_filter_param(self):
443+
name, param = NoteFilterWithRelated.get_related_filter_param('author__email')
444+
self.assertEqual('author', name)
445+
self.assertEqual('email', param)
446+
447+
def test_name_hiding(self):
448+
class PostFilterNameHiding(PostFilter):
449+
note__author = filters.RelatedFilter(UserFilter)
450+
note = filters.RelatedFilter(NoteFilterWithAll)
451+
note2 = filters.RelatedFilter(NoteFilterWithAll)
452+
453+
class Meta:
454+
model = Post
455+
456+
name, param = PostFilterNameHiding.get_related_filter_param('note__author__email')
457+
self.assertEqual('note__author', name)
458+
self.assertEqual('email', param)
459+
460+
name, param = PostFilterNameHiding.get_related_filter_param('note__title')
396461
self.assertEqual('note', name)
462+
self.assertEqual('title', param)
397463

398-
name = PostFilterWithDirectAuthor.get_filter_name('note2')
464+
name, param = PostFilterNameHiding.get_related_filter_param('note2__title')
399465
self.assertEqual('note2', name)
466+
self.assertEqual('title', param)
400467

401-
name = PostFilterWithDirectAuthor.get_filter_name('note2__author')
468+
name, param = PostFilterNameHiding.get_related_filter_param('note2__author')
402469
self.assertEqual('note2', name)
470+
self.assertEqual('author', param)
403471

404472

405473
class FilterSubsetTests(TestCase):

0 commit comments

Comments
 (0)