11import json
22
33from django .core .exceptions import ValidationError
4+ from django .db .models import Q
45from django .http import QueryDict
5- from django_filters .rest_framework import DjangoFilterBackend
6+ from django_filters .utils import translate_validation
67from rest_framework import filters
78
89
@@ -38,7 +39,6 @@ def filter_queryset(self, request, queryset, view, filter_data=None):
3839 # Propagate validation errors unchanged
3940 raise
4041 except Exception as e :
41- raise
4242 # Convert unexpected errors to ValidationError to keep response consistent
4343 raise ValidationError (f"Filter error: { str (e )} " )
4444
@@ -58,7 +58,7 @@ def _normalize_filter_data(self, raw_filter, source_label):
5858 raise ValidationError (f"Invalid JSON for '{ source_label } '. Expected a valid JSON object." )
5959
6060 def _apply_json_filter (self , queryset , filter_data , view ):
61- """Process a JSON filter structure using OR/AND/NOT set operations ."""
61+ """Process a JSON filter structure using Q object composition ."""
6262 if not filter_data :
6363 return queryset
6464
@@ -69,11 +69,13 @@ def _apply_json_filter(self, queryset, filter_data, view):
6969 # Validate against the view's FilterSet (only declared filters are allowed)
7070 self ._validate_fields (filter_data , view )
7171
72- # Build combined queryset using FilterSet-driven leaf evaluation
73- combined_qs = self ._evaluate_node (filter_data , queryset , view )
74- if combined_qs is None :
72+ # Build combined Q object from the filter tree
73+ combined_q = self ._evaluate_node (filter_data , view , queryset )
74+ if combined_q is None :
7575 return queryset
76- return combined_qs
76+
77+ # Apply the combined Q object to the queryset once
78+ return queryset .filter (combined_q )
7779
7880 def _validate_fields (self , filter_data , view ):
7981 """Validate that filtered fields are defined in the view's FilterSet."""
@@ -115,108 +117,76 @@ def _extract_field_names(self, filter_data):
115117 return fields
116118 return []
117119
118- def _evaluate_node (self , node , base_queryset , view ):
120+ def _evaluate_node (self , node , view , queryset ):
119121 """
120- Recursively evaluate a JSON node into a combined queryset using branch-based filtering .
122+ Recursively evaluate a JSON node into a combined Q object .
121123
122124 Rules:
123- - leaf dict → evaluated through DjangoFilterBackend as a mini-querystring
124- - {"or": [...]} → union (|) of children
125- - {"and": [...]} → collect field conditions per branch and apply together
126- - {"not": {...}} → exclude child's rows from the base queryset
127- (complement within base scope)
125+ - leaf dict → evaluated through FilterSet to produce a Q object
126+ - {"or": [...]} → Q() | Q() | ... (OR of children)
127+ - {"and": [...]} → Q() & Q() & ... (AND of children)
128+ - {"not": {...}} → ~Q() (negation of child)
129+
130+ Returns a Q object that can be applied to a queryset.
128131 """
129132 if not isinstance (node , dict ):
130133 return None
131134
132- # 'or' combination - requires set operations between children
135+ # 'or' combination - OR of child Q objects
133136 if "or" in node :
134137 children = node ["or" ]
135138 if not isinstance (children , list ) or not children :
136139 return None
137- combined = None
140+ combined_q = Q ()
138141 for child in children :
139- child_qs = self ._evaluate_node (child , base_queryset , view )
140- if child_qs is None :
142+ child_q = self ._evaluate_node (child , view , queryset )
143+ if child_q is None :
141144 continue
142- combined = child_qs if combined is None else ( combined | child_qs )
143- return combined
145+ combined_q |= child_q
146+ return combined_q
144147
145- # 'and' combination - collect field conditions per branch
148+ # 'and' combination - AND of child Q objects
146149 if "and" in node :
147150 children = node ["and" ]
148151 if not isinstance (children , list ) or not children :
149152 return None
150- return self ._evaluate_and_branch (children , base_queryset , view )
153+ combined_q = Q ()
154+ for child in children :
155+ child_q = self ._evaluate_node (child , view , queryset )
156+ if child_q is None :
157+ continue
158+ combined_q &= child_q
159+ return combined_q
151160
152- # 'not' negation
161+ # 'not' negation - negate the child Q object
153162 if "not" in node :
154163 child = node ["not" ]
155164 if not isinstance (child , dict ):
156165 return None
157- child_qs = self ._evaluate_node (child , base_queryset , view )
158- if child_qs is None :
159- return None
160- # Use subquery instead of pk__in for better performance
161- # This avoids evaluating child_qs and creating large IN clauses
162- return base_queryset .exclude (pk__in = child_qs .values ("pk" ))
163-
164- # Leaf dict: evaluate via DjangoFilterBackend using FilterSet
165- return self ._filter_leaf_via_backend (node , base_queryset , view )
166-
167- def _evaluate_and_branch (self , children , base_queryset , view ):
168- """
169- Evaluate an AND branch by collecting field conditions and applying them together.
170-
171- This approach is more efficient than individual leaf evaluation because:
172- - Field conditions within the same AND branch are collected and applied together
173- - Only logical operation children require separate evaluation and set intersection
174- - Reduces the number of intermediate querysets and database queries
175- """
176- collected_conditions = {}
177- logical_querysets = []
178-
179- # Separate field conditions from logical operations
180- for child in children :
181- if not isinstance (child , dict ):
182- continue
183-
184- # Check if this child contains logical operators
185- has_logical = any (k .lower () in ("or" , "and" , "not" ) for k in child .keys () if isinstance (k , str ))
186-
187- if has_logical :
188- # This child has logical operators, evaluate separately
189- child_qs = self ._evaluate_node (child , base_queryset , view )
190- if child_qs is not None :
191- logical_querysets .append (child_qs )
192- else :
193- # This is a leaf with field conditions, collect them
194- collected_conditions .update (child )
195-
196- # Start with base queryset
197- result_qs = base_queryset
198-
199- # Apply collected field conditions together if any exist
200- if collected_conditions :
201- result_qs = self ._filter_leaf_via_backend (collected_conditions , result_qs , view )
202- if result_qs is None :
166+ child_q = self ._evaluate_node (child , view , queryset )
167+ if child_q is None :
203168 return None
169+ return ~ child_q
204170
205- # Intersect with any logical operation results
206- for logical_qs in logical_querysets :
207- result_qs = result_qs & logical_qs
171+ # Leaf dict: evaluate via FilterSet to get a Q object
172+ return self ._build_leaf_q (node , view , queryset )
208173
209- return result_qs
174+ def _build_leaf_q (self , leaf_conditions , view , queryset ):
175+ """Build a Q object from leaf filter conditions using the view's FilterSet.
210176
211- def _filter_leaf_via_backend (self , leaf_conditions , base_queryset , view ):
212- """Evaluate a leaf dict by delegating to DjangoFilterBackend once.
177+ We serialize the leaf dict into a QueryDict and let the view's
178+ filterset_class perform validation and build a combined Q object
179+ from all the field filters.
213180
214- We serialize the leaf dict into a mini querystring and let the view's
215- filterset_class perform validation, conversion, and filtering. This returns
216- a lazy queryset suitable for set-operations with siblings.
181+ Returns a Q object representing all the field conditions in the leaf.
217182 """
218183 if not leaf_conditions :
219- return None
184+ return Q ()
185+
186+ # Get the filterset class from the view
187+ filterset_class = getattr (view , "filterset_class" , None )
188+ if not filterset_class :
189+ raise ValidationError ("Filtering requires a filterset_class to be defined on the view" )
220190
221191 # Build a QueryDict from the leaf conditions
222192 qd = QueryDict (mutable = True )
@@ -231,17 +201,18 @@ def _filter_leaf_via_backend(self, leaf_conditions, base_queryset, view):
231201 qd = qd .copy ()
232202 qd ._mutable = False
233203
234- # Temporarily patch request.GET and delegate to DjangoFilterBackend
235- backend = DjangoFilterBackend ()
236- request = view .request
237- original_get = request ._request .GET if hasattr (request , "_request" ) else None
238- try :
239- if hasattr (request , "_request" ):
240- request ._request .GET = qd
241- return backend .filter_queryset (request , base_queryset , view )
242- finally :
243- if hasattr (request , "_request" ) and original_get is not None :
244- request ._request .GET = original_get
204+ # Instantiate the filterset with the actual queryset
205+ # Custom filter methods may need access to the queryset for filtering
206+ fs = filterset_class (data = qd , queryset = queryset )
207+
208+ if not fs .is_valid ():
209+ raise translate_validation (fs .errors )
210+
211+ # Build and return the combined Q object
212+ if not hasattr (fs , "build_combined_q" ):
213+ raise ValidationError ("FilterSet must have build_combined_q method for complex filtering" )
214+
215+ return fs .build_combined_q ()
245216
246217 def _get_max_depth (self , view ):
247218 """Return the maximum allowed nesting depth for complex filters.
0 commit comments