Skip to content

Commit 4c89a57

Browse files
Maciej Lewinskimacias
authored andcommitted
Chained qualifiers implementation
ref T35707
1 parent 3388534 commit 4c89a57

File tree

2 files changed

+70
-34
lines changed

2 files changed

+70
-34
lines changed

binder/models.py

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ class FieldFilter(object):
154154
fields = []
155155
# The list of allowed qualifiers
156156
allowed_qualifiers = []
157+
# The mapping of allowed chain qualifiers to the relevant Field
158+
allowed_chain_qualifiers = {}
157159

158160
def __init__(self, field):
159161
self.field = field
@@ -193,12 +195,65 @@ def check_qualifier(self, qualifier):
193195
.format(qualifier, self.__class__.__name__, self.field_description()))
194196

195197

198+
# This returns a (cached) filterclass for a field class.
199+
def get_field_filter(self, field_class, reset=False):
200+
f = not reset and getattr(self, '_field_filters', None)
201+
202+
if not f:
203+
f = {}
204+
for field_filter_cls in FieldFilter.__subclasses__():
205+
for field_cls in field_filter_cls.fields:
206+
if f.get(field_cls):
207+
raise ValueError('Field-Filter mapping conflict: {} vs {}'.format(field_filter_cls.name, field_cls.name))
208+
else:
209+
f[field_cls] = field_filter_cls
210+
211+
self._field_filters = f
212+
213+
return f.get(field_class)
214+
215+
216+
217+
def get_q(self, qualifiers, value, invert, partial=''):
218+
i = 0
219+
field_filter = self
220+
221+
# First we try to handle chain qualifiers
222+
while (
223+
# If its not the last qualifier it has to be a chain qualifier
224+
i < len(qualifiers) - 1 or
225+
# For the last one we check if it is in chain qualifiers
226+
(i < len(qualifiers) and qualifiers[i] in field_filter.allowed_chain_qualifiers)
227+
):
228+
chain_qualifier = qualifiers[i]
229+
i += 1
230+
231+
field_cls = field_filter.allowed_chain_qualifiers[chain_qualifier]
232+
if field_cls is None:
233+
raise BinderRequestError(
234+
'Qualifier {} not supported for type {} ({}).'
235+
.format(chain_qualifier, field_filter.__class__.__name__, field_filter.field_description())
236+
)
196237

197-
def get_q(self, qualifier, value, invert, partial=''):
198-
self.check_qualifier(qualifier)
199-
qualifier, cleaned_value = self.clean_qualifier(qualifier, value)
238+
field = field_cls()
239+
field.model = self.field.model
240+
field.name = self.field.name + ':' + chain_qualifier
200241

201-
suffix = '__' + qualifier if qualifier else ''
242+
field_filter_cls = self.get_field_filter(field_cls)
243+
field_filter = field_filter_cls(field)
244+
245+
try:
246+
qualifier = qualifiers[i]
247+
except IndexError:
248+
qualifier = None
249+
250+
field_filter.check_qualifier(qualifier)
251+
qualifier, cleaned_value = field_filter.clean_qualifier(qualifier, value)
252+
253+
if 0 <= i < len(qualifiers):
254+
qualifiers[i] = qualifier
255+
256+
suffix = ''.join('__' + qualifier for qualifier in qualifiers)
202257
if invert:
203258
return ~Q(**{partial + self.field.name + suffix: cleaned_value})
204259
else:
@@ -265,6 +320,7 @@ class DateTimeFieldFilter(FieldFilter):
265320
fields = [models.DateTimeField]
266321
# Maybe allow __startswith? And __year etc?
267322
allowed_qualifiers = [None, 'in', 'gt', 'gte', 'lt', 'lte', 'range', 'isnull']
323+
allowed_chain_qualifiers = {'date': models.DateField}
268324

269325
def clean_value(self, qualifier, v):
270326
if re.match('^[0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9]{2}:[0-9]{2}:[0-9]{2}([.][0-9]+)?([A-Za-z]+|[+-][0-9]{1,4})$', v):
@@ -286,6 +342,7 @@ def clean_qualifier(self, qualifier, value):
286342
else:
287343
value_type = type(cleaned_value)
288344

345+
# [TODO] Support for chained qualifiers is added, still needed for backwards compat
289346
if issubclass(value_type, date) and not issubclass(value_type, datetime):
290347
if qualifier is None:
291348
qualifier = 'date'
@@ -348,6 +405,7 @@ def clean_value(self, qualifier, v):
348405
class TextFieldFilter(FieldFilter):
349406
fields = [models.CharField, models.TextField]
350407
allowed_qualifiers = [None, 'in', 'iexact', 'contains', 'icontains', 'startswith', 'istartswith', 'endswith', 'iendswith', 'exact', 'isnull']
408+
allowed_chain_qualifiers = {'unaccent': models.TextField}
351409

352410
# Always valid(?)
353411
def clean_value(self, qualifier, v):
@@ -368,22 +426,6 @@ class ArrayFieldFilter(FieldFilter):
368426
fields = [ArrayField]
369427
allowed_qualifiers = [None, 'contains', 'contained_by', 'overlap', 'isnull']
370428

371-
# Some copy/pasta involved....
372-
def get_field_filter(self, field_class, reset=False):
373-
f = not reset and getattr(self, '_field_filter', None)
374-
375-
if not f:
376-
f = None
377-
for field_filter_cls in FieldFilter.__subclasses__():
378-
for field_cls in field_filter_cls.fields:
379-
if field_cls == field_class:
380-
f = field_filter_cls
381-
break
382-
self._field_filter = f
383-
384-
return f
385-
386-
387429
def clean_value(self, qualifier, v):
388430
Filter = self.get_field_filter(self.field.base_field.__class__)
389431
filter = Filter(self.field.base_field)

binder/views.py

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,18 +1250,12 @@ def _parse_filter(self, field, value, request, include_annotations, partial=''):
12501250

12511251
if not tail:
12521252
invert = False
1253-
try:
1254-
head, qualifier = head.split(':', 1)
1255-
if qualifier == 'not':
1256-
qualifier = None
1257-
invert = True
1258-
elif qualifier.startswith('not:'):
1259-
qualifier = qualifier[4:]
1260-
invert = True
1261-
except ValueError:
1262-
qualifier = None
1253+
head, *qualifiers = head.split(':')
1254+
if qualifiers and qualifiers[0] == 'not':
1255+
qualifiers = qualifiers[1:]
1256+
invert = True
12631257

1264-
q = self._filter_field(head, qualifier, value, invert, request, include_annotations, partial)
1258+
q = self._filter_field(head, qualifiers, value, invert, request, include_annotations, partial)
12651259
else:
12661260
q = Q()
12671261

@@ -1293,7 +1287,7 @@ def _parse_filter(self, field, value, request, include_annotations, partial=''):
12931287

12941288

12951289

1296-
def _filter_field(self, field_name, qualifier, value, invert, request, include_annotations, partial=''):
1290+
def _filter_field(self, field_name, qualifiers, value, invert, request, include_annotations, partial=''):
12971291
try:
12981292
if field_name in self.hidden_fields:
12991293
raise FieldDoesNotExist()
@@ -1306,7 +1300,7 @@ def _filter_field(self, field_name, qualifier, value, invert, request, include_a
13061300
if partial:
13071301
# NOTE: This creates a subquery; try to avoid this!
13081302
qs = annotate(self.model.objects.all(), request, annotations)
1309-
qs = qs.filter(self._filter_field(field_name, qualifier, value, invert, request, {
1303+
qs = qs.filter(self._filter_field(field_name, qualifiers, value, invert, request, {
13101304
rel_[len(rel) + 1:]: annotations
13111305
for rel_, annotations in include_annotations.items()
13121306
if rel_ == rel or rel_.startswith(rel + '.')
@@ -1319,7 +1313,7 @@ def _filter_field(self, field_name, qualifier, value, invert, request, include_a
13191313
if filter_class:
13201314
filter = filter_class(field)
13211315
try:
1322-
return filter.get_q(qualifier, value, invert, partial)
1316+
return filter.get_q(qualifiers, value, invert, partial)
13231317
except ValidationError as e:
13241318
# TODO: Maybe convert to a BinderValidationError later?
13251319
raise BinderRequestError(e.message)

0 commit comments

Comments
 (0)