33
44from collections import OrderedDict
55import copy
6+ import warnings
67
78from django .db import models
89from django .db .models .constants import LOOKUP_SEP
@@ -48,19 +49,40 @@ def __new__(cls, name, bases, attrs):
4849
4950 return new_class
5051
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+
5164
5265class FilterSet (six .with_metaclass (FilterSetMetaclass , filterset .FilterSet )):
5366 filter_overrides = {
67+ models .BooleanField : {
68+ 'filter_class' : filters .BooleanFilter ,
69+ },
5470
5571 # In order to support ISO-8601 -- which is the default output for
5672 # DRF -- we need to use django-filter's IsoDateTimeFilter
5773 models .DateTimeField : {
5874 'filter_class' : filters .IsoDateTimeFilter ,
5975 },
6076 }
77+ _subset_cache = {}
6178
6279 def __init__ (self , * args , ** kwargs ):
63- self ._related_filterset_cache = kwargs .pop ('cache' , {})
80+ if 'cache' in kwargs :
81+ warnings .warn (
82+ "'cache' argument is deprecated. Override '_subset_cache' instead." ,
83+ DeprecationWarning , stacklevel = 2
84+ )
85+ self .__class__ ._subset_cache = kwargs .pop ('cache' , None )
6486
6587 super (FilterSet , self ).__init__ (* args , ** kwargs )
6688
@@ -79,74 +101,68 @@ def get_filters(self):
79101 Build a set of filters based on the requested data. The resulting set
80102 will walk `RelatedFilter`s to recursively build the set of filters.
81103 """
82- requested_filters = OrderedDict ()
83-
84- # Add plain lookup filters if match. ie, `username__icontains`
85- for filter_key , filter_value in six .iteritems (self .filters ):
86- exclude_key = '%s!' % filter_key
87-
88- if filter_key in self .data :
89- requested_filters [filter_key ] = filter_value
90-
91- if exclude_key in self .data :
92- filter_value = copy .deepcopy (filter_value )
93- filter_value .exclude = not filter_value .exclude
94- requested_filters [exclude_key ] = filter_value
95-
96- # build a map of potential {rel: {filter: value}} data
97- related_data = OrderedDict ()
98- for filter_key , value in six .iteritems (self .data ):
99- if filter_key not in self .filters :
100-
101- # skip non lookup/related keys
102- if LOOKUP_SEP not in filter_key :
103- continue
104-
105- rel_name , filter_key = filter_key .split (LOOKUP_SEP , 1 )
106-
107- related_data .setdefault (rel_name , OrderedDict ())
108- related_data [rel_name ][filter_key ] = value
109-
110- # walk the related lookup data. If the filter is a RelatedFilter,
111- # then instantiate its filterset and append its filters.
112- for rel_name , rel_data in related_data .items ():
113- related_filter = self .filters .get (rel_name , None )
114-
115- # skip non-`RelatedFilter`s
116- if not isinstance (related_filter , filters .RelatedFilter ):
104+ # build param data for related filters: {rel: {param: value}}
105+ related_data = OrderedDict (
106+ [(name , OrderedDict ()) for name in self .__class__ .related_filters ]
107+ )
108+ for param , value in six .iteritems (self .data ):
109+ filter_name , related_param = self .get_related_filter_param (param )
110+
111+ # skip non lookup/related keys
112+ if filter_name is None :
117113 continue
118114
119- # get known filter names
120- filterset_class = related_filter .filterset
121- filter_names = [filterset_class .get_filter_name (param ) for param in rel_data .keys ()]
122-
123- # filter out empty values - indicates an unknown field (author__foobar__isnull)
124- filter_names = [f for f in filter_names if f is not None ]
125-
126- # attempt to retrieve related filterset subset from the cache
127- key = self .cache_key (filterset_class , filter_names )
128- subset_class = self .cache_get (key )
115+ if filter_name in related_data :
116+ related_data [filter_name ][related_param ] = value
129117
130- # otherwise build and insert it into the cache
131- if subset_class is None :
132- subset_class = related_filter .filterset .get_subset (filter_names )
133- self .cache_set (key , subset_class )
134-
135- # initialize and copy filters
136- filterset = subset_class (data = rel_data )
137- rel_filters = filterset .get_filters ()
138- for filter_key , filter_value in six .iteritems (rel_filters ):
139- # modify filter name to account for relationship
140- rel_filter_key = LOOKUP_SEP .join ([rel_name , filter_key ])
141- filter_value .name = LOOKUP_SEP .join ([related_filter .name , filter_value .name ])
142- requested_filters [rel_filter_key ] = filter_value
118+ # build the compiled set of all filters
119+ requested_filters = OrderedDict ()
120+ for filter_name , f in six .iteritems (self .filters ):
121+ exclude_name = '%s!' % filter_name
122+
123+ # Add plain lookup filters if match. ie, `username__icontains`
124+ if filter_name in self .data :
125+ requested_filters [filter_name ] = f
126+
127+ # include exclusion keys
128+ if exclude_name in self .data :
129+ f = copy .deepcopy (f )
130+ f .exclude = not f .exclude
131+ requested_filters [exclude_name ] = f
132+
133+ # include filters from related subsets
134+ if isinstance (f , filters .RelatedFilter ) and filter_name in related_data :
135+ subset_data = related_data [filter_name ]
136+ subset_class = f .filterset .get_subset (subset_data )
137+ filterset = subset_class (data = subset_data )
138+
139+ # modify filter names to account for relationship
140+ for related_name , related_f in six .iteritems (filterset .get_filters ()):
141+ related_name = LOOKUP_SEP .join ([filter_name , related_name ])
142+ related_f .name = LOOKUP_SEP .join ([f .name , related_f .name ])
143+ requested_filters [related_name ] = related_f
143144
144145 return requested_filters
145146
146147 @classmethod
147148 def get_filter_name (cls , param ):
148149 """
149150 Get the filter name for the request data parameter.
151+
152+ ex::
153+
154+ # regular attribute filters
155+ name = FilterSet.get_filter_name('email')
156+ assert name == 'email'
157+
158+ # exclusion filters
159+ name = FilterSet.get_filter_name('email!')
160+ assert name == 'email'
161+
162+ # related filters
163+ name = FilterSet.get_filter_name('author__email')
164+ assert name == 'author'
165+
150166 """
151167 # Attempt to match against filters with lookups first. (username__endswith)
152168 if param in cls .base_filters :
@@ -156,40 +172,97 @@ def get_filter_name(cls, param):
156172 if param [- 1 ] == '!' and param [:- 1 ] in cls .base_filters :
157173 return param [:- 1 ]
158174
159- # Fallback to matching against relationships. (author__username__endswith)
160- related_param = param .split (LOOKUP_SEP , 1 )[0 ]
161- f = cls .base_filters .get (related_param , None )
162- if isinstance (f , filters .RelatedFilter ):
163- return related_param
175+ # Fallback to matching against relationships. (author__username__endswith).
176+ related_filters = cls .related_filters .keys ()
177+
178+ # preference more specific filters. eg, `note__author` over `note`.
179+ for name in sorted (related_filters )[::- 1 ]:
180+ # we need to match against '__' to prevent eager matching against
181+ # like names. eg, note vs note2. Exact matches are handled above.
182+ if param .startswith ("%s%s" % (name , LOOKUP_SEP )):
183+ return name
164184
165185 @classmethod
166- def get_subset (cls , filter_names ):
186+ def get_related_filter_param (cls , param ):
167187 """
168- Returns a FilterSet subclass that contains the subset of filters
169- specified in `filter_names`. This is useful for creating FilterSets
170- used across relationships, as it minimizes the deepcopy overhead
171- incurred when instantiating the FilterSet.
188+ Get a tuple of (filter name, related param).
189+
190+ ex::
191+
192+ name, param = FilterSet.get_filter_name('author__email__foobar')
193+ assert name == 'author'
194+ assert param = 'email__foobar'
195+
196+ name, param = FilterSet.get_filter_name('author')
197+ assert name is None
198+ assert param is None
199+
172200 """
173- class FilterSetSubset (cls ):
174- pass
201+ related_filters = cls .related_filters .keys ()
175202
176- FilterSetSubset .__name__ = str ('%sSubset' % (cls .__name__ ))
177- FilterSetSubset .base_filters = OrderedDict ([
178- (name , f )
179- for name , f in six .iteritems (cls .base_filters )
180- if name in filter_names
181- ])
203+ # preference more specific filters. eg, `note__author` over `note`.
204+ for name in sorted (related_filters )[::- 1 ]:
205+ # we need to match against '__' to prevent eager matching against
206+ # like names. eg, note vs note2. Exact matches are handled above.
207+ if param .startswith ("%s%s" % (name , LOOKUP_SEP )):
208+ # strip param + LOOKUP_SET from param
209+ related_param = param [len (name ) + len (LOOKUP_SEP ):]
210+ return name , related_param
182211
183- return FilterSetSubset
212+ # not a related param
213+ return None , None
184214
185- def cache_key (self , filterset , filter_names ):
186- return '%sSubset-%s' % (filterset .__name__ , '-' .join (sorted (filter_names )), )
215+ @classmethod
216+ def get_subset (cls , params ):
217+ """
218+ Returns a FilterSubset class that contains the subset of filters
219+ specified in the requested `params`. This is useful for creating
220+ FilterSets that traverse relationships, as it helps to minimize
221+ the deepcopy overhead incurred when instantiating the FilterSet.
222+ """
223+ # Determine names of filters from query params and remove empty values.
224+ # param names that traverse relations are translated to just the local
225+ # filter names. eg, `author__username` => `author`. Empty values are
226+ # removed, as they indicate an unknown field eg, author__foobar__isnull
227+ filter_names = [cls .get_filter_name (param ) for param in params ]
228+ filter_names = [f for f in filter_names if f is not None ]
229+
230+ # attempt to retrieve related filterset subset from the cache
231+ key = cls .cache_key (filter_names )
232+ subset_class = cls .cache_get (key )
233+
234+ # if no cached subset, then derive base_filters and create new subset
235+ if subset_class is not None :
236+ return subset_class
237+
238+ class FilterSubsetMetaclass (FilterSetMetaclass ):
239+ def __new__ (cls , name , bases , attrs ):
240+ new_class = super (FilterSubsetMetaclass , cls ).__new__ (cls , name , bases , attrs )
241+ new_class .base_filters = OrderedDict ([
242+ (name , f )
243+ for name , f in six .iteritems (new_class .base_filters )
244+ if name in filter_names
245+ ])
246+ return new_class
247+
248+ class FilterSubset (six .with_metaclass (FilterSubsetMetaclass , cls )):
249+ pass
187250
188- def cache_get (self , key ):
189- return self ._related_filterset_cache .get (key )
251+ FilterSubset .__name__ = str ('%sSubset' % (cls .__name__ , ))
252+ cls .cache_set (key , FilterSubset )
253+ return FilterSubset
190254
191- def cache_set (self , key , value ):
192- self ._related_filterset_cache [key ] = value
255+ @classmethod
256+ def cache_key (cls , filter_names ):
257+ return '%sSubset-%s' % (cls .__name__ , '-' .join (sorted (filter_names )), )
258+
259+ @classmethod
260+ def cache_get (cls , key ):
261+ return cls ._subset_cache .get (key )
262+
263+ @classmethod
264+ def cache_set (cls , key , value ):
265+ cls ._subset_cache [key ] = value
193266
194267 @property
195268 def qs (self ):
0 commit comments