Skip to content

Commit 8c2b433

Browse files
author
Maciej Lewinski
committed
Chained qualifiers implementation
ref T35707
1 parent 77eca30 commit 8c2b433

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
@@ -153,6 +153,8 @@ class FieldFilter(object):
153153
fields = []
154154
# The list of allowed qualifiers
155155
allowed_qualifiers = []
156+
# The mapping of allowed chain qualifiers to the relevant Field
157+
allowed_chain_qualifiers = {}
156158

157159
def __init__(self, field):
158160
self.field = field
@@ -192,12 +194,65 @@ def check_qualifier(self, qualifier):
192194
.format(qualifier, self.__class__.__name__, self.field_description()))
193195

194196

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

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

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

258314
def clean_value(self, qualifier, v):
259315
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):
@@ -275,6 +331,7 @@ def clean_qualifier(self, qualifier, value):
275331
else:
276332
value_type = type(cleaned_value)
277333

334+
# [TODO] Support for chained qualifiers is added, still needed for backwards compat
278335
if issubclass(value_type, date) and not issubclass(value_type, datetime):
279336
if qualifier is None:
280337
qualifier = 'date'
@@ -337,6 +394,7 @@ def clean_value(self, qualifier, v):
337394
class TextFieldFilter(FieldFilter):
338395
fields = [models.CharField, models.TextField]
339396
allowed_qualifiers = [None, 'in', 'iexact', 'contains', 'icontains', 'startswith', 'istartswith', 'endswith', 'iendswith', 'exact', 'isnull']
397+
allowed_chain_qualifiers = {'unaccent': models.TextField}
340398

341399
# Always valid(?)
342400
def clean_value(self, qualifier, v):
@@ -357,22 +415,6 @@ class ArrayFieldFilter(FieldFilter):
357415
fields = [ArrayField]
358416
allowed_qualifiers = [None, 'contains', 'contained_by', 'overlap', 'isnull']
359417

360-
# Some copy/pasta involved....
361-
def get_field_filter(self, field_class, reset=False):
362-
f = not reset and getattr(self, '_field_filter', None)
363-
364-
if not f:
365-
f = None
366-
for field_filter_cls in FieldFilter.__subclasses__():
367-
for field_cls in field_filter_cls.fields:
368-
if field_cls == field_class:
369-
f = field_filter_cls
370-
break
371-
self._field_filter = f
372-
373-
return f
374-
375-
376418
def clean_value(self, qualifier, v):
377419
Filter = self.get_field_filter(self.field.base_field.__class__)
378420
filter = Filter(self.field.base_field)

binder/views.py

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

10531053
if not tail:
10541054
invert = False
1055-
try:
1056-
head, qualifier = head.split(':', 1)
1057-
if qualifier == 'not':
1058-
qualifier = None
1059-
invert = True
1060-
elif qualifier.startswith('not:'):
1061-
qualifier = qualifier[4:]
1062-
invert = True
1063-
except ValueError:
1064-
qualifier = None
1055+
head, *qualifiers = head.split(':')
1056+
if qualifiers and qualifiers[0] == 'not':
1057+
qualifiers = qualifiers[1:]
1058+
invert = True
10651059

1066-
q = self._filter_field(head, qualifier, value, invert, request, include_annotations, partial)
1060+
q = self._filter_field(head, qualifiers, value, invert, request, include_annotations, partial)
10671061
else:
10681062
q = Q()
10691063

@@ -1095,7 +1089,7 @@ def _parse_filter(self, field, value, request, include_annotations, partial=''):
10951089

10961090

10971091

1098-
def _filter_field(self, field_name, qualifier, value, invert, request, include_annotations, partial=''):
1092+
def _filter_field(self, field_name, qualifiers, value, invert, request, include_annotations, partial=''):
10991093
try:
11001094
if field_name in self.hidden_fields:
11011095
raise FieldDoesNotExist()
@@ -1108,7 +1102,7 @@ def _filter_field(self, field_name, qualifier, value, invert, request, include_a
11081102
if partial:
11091103
# NOTE: This creates a subquery; try to avoid this!
11101104
qs = annotate(self.model.objects.all(), request, annotations)
1111-
qs = qs.filter(self._filter_field(field_name, qualifier, value, invert, request, {
1105+
qs = qs.filter(self._filter_field(field_name, qualifiers, value, invert, request, {
11121106
rel_[len(rel) + 1:]: annotations
11131107
for rel_, annotations in include_annotations.items()
11141108
if rel_ == rel or rel_.startswith(rel + '.')
@@ -1121,7 +1115,7 @@ def _filter_field(self, field_name, qualifier, value, invert, request, include_a
11211115
if filter_class:
11221116
filter = filter_class(field)
11231117
try:
1124-
return filter.get_q(qualifier, value, invert, partial)
1118+
return filter.get_q(qualifiers, value, invert, partial)
11251119
except ValidationError as e:
11261120
# TODO: Maybe convert to a BinderValidationError later?
11271121
raise BinderRequestError(e.message)

0 commit comments

Comments
 (0)