@@ -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
5365class 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 """
0 commit comments