Skip to content

Commit d452bb3

Browse files
committed
Finished implementation of NestedAutoRQLFilterClass
* refactored NestedAutoRQLFilterClass._get_init_filters to reduce complexity * added support for automatic queryset optimization * wrote the minimum number of needed tests (max depth=3)
1 parent d16aebd commit d452bb3

File tree

3 files changed

+233
-41
lines changed

3 files changed

+233
-41
lines changed

dj_rql/filter_cls.py

Lines changed: 91 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@
2828
from dj_rql.exceptions import RQLFilterLookupError, RQLFilterParsingError, RQLFilterValueError
2929
from dj_rql.openapi import RQLFilterClassSpecification
3030
from dj_rql.parser import RQLParser
31-
from dj_rql.qs import Annotation
31+
from dj_rql.qs import Annotation, NPR, NSR
3232
from dj_rql.transformer import RQLToDjangoORMTransformer
3333

34-
from django.db.models import Model, Q
34+
from django.db.models import ForeignKey, ManyToManyField, Model, OneToOneField, OneToOneRel, Q
3535
from django.utils.dateparse import parse_date, parse_datetime
36+
from django.utils.functional import cached_property
3637

3738
from lark.exceptions import LarkError
3839

@@ -1040,8 +1041,7 @@ class AutoRQLFilterClass(RQLFilterClass):
10401041
"""This class will collect all simple model fields except the ones in this field."""
10411042

10421043
def _get_init_filters(self):
1043-
described_filters = tuple(self.FILTERS) if self.FILTERS else ()
1044-
1044+
described_filters = self._described_filters
10451045
filters = tuple(
10461046
{
10471047
'filter': f.name,
@@ -1058,6 +1058,10 @@ def _get_init_filters(self):
10581058

10591059
return described_filters + filters
10601060

1061+
@cached_property
1062+
def _described_filters(self):
1063+
return tuple(self.FILTERS) if self.FILTERS else ()
1064+
10611065

10621066
class NestedAutoRQLFilterClass(AutoRQLFilterClass):
10631067
"""
@@ -1076,37 +1080,90 @@ def _get_init_filters(self):
10761080
if self.DEPTH == 0:
10771081
return super()._get_init_filters()
10781082

1079-
described_filters = tuple(self.FILTERS) if self.FILTERS else ()
1080-
filters = []
1081-
10821083
depth = 0
1083-
models = [(self.MODEL, None)]
1084-
1085-
while depth <= self.DEPTH and models:
1086-
related_models = []
1087-
for model, prefix in models:
1088-
for field in model._meta.get_fields():
1089-
field_name = field.name
1090-
if prefix:
1091-
rel_f_name = '.'.join((prefix, field_name))
1092-
else:
1093-
rel_f_name = field_name
1094-
1095-
if rel_f_name in self.EXCLUDE_FILTERS or rel_f_name in described_filters:
1096-
continue
1097-
1098-
if field.is_relation:
1099-
related_models.append((field.related_model, rel_f_name))
1100-
continue
1101-
1102-
if isinstance(field, SUPPORTED_FIELD_TYPES):
1103-
filters.append({
1104-
'filter': rel_f_name,
1105-
'ordering': True,
1106-
'search': FilterTypes.field_filter_type(field) == FilterTypes.STRING,
1107-
})
1084+
global_namespace = []
1085+
iterator = [(self.MODEL, global_namespace, None, None)]
11081086

1087+
while depth <= self.DEPTH and iterator:
1088+
iterator = self._iter_models_to_get_filters(depth, iterator)
11091089
depth += 1
1110-
models = related_models
11111090

1112-
return filters
1091+
return self._described_filters + tuple(global_namespace)
1092+
1093+
def _iter_models_to_get_filters(self, depth, iterator):
1094+
related_models = []
1095+
1096+
for model_data in iterator:
1097+
related_models.extend(self._iter_model_to_get_filters(depth, model_data))
1098+
1099+
return related_models
1100+
1101+
def _iter_model_to_get_filters(self, depth, model_data):
1102+
model, namespace, circular_related_name, prefix = model_data
1103+
through_models = set()
1104+
model_related_models = []
1105+
1106+
for field in model._meta.get_fields():
1107+
rel_f_name = self._get_relative_field_name(field, circular_related_name, prefix)
1108+
if not rel_f_name:
1109+
continue
1110+
1111+
if field.is_relation:
1112+
if self._is_through_field(field):
1113+
through_models.add(field.through)
1114+
1115+
relation_data = self._add_relation_to_iterated_models(depth, field, namespace)
1116+
model_related_models.append(relation_data + (rel_f_name,))
1117+
continue
1118+
1119+
if isinstance(field, SUPPORTED_FIELD_TYPES):
1120+
namespace.append({
1121+
'filter': field.name,
1122+
'ordering': True,
1123+
'search': FilterTypes.field_filter_type(field) == FilterTypes.STRING,
1124+
})
1125+
1126+
return [i for i in model_related_models if i[0] not in through_models]
1127+
1128+
def _add_relation_to_iterated_models(self, depth, field, namespace):
1129+
if isinstance(field, (ForeignKey, ManyToManyField)):
1130+
circular_related_name = field.remote_field.name
1131+
else:
1132+
circular_related_name = field.field.name
1133+
1134+
field_name = field.name
1135+
qs = None
1136+
if isinstance(field, (ForeignKey, OneToOneField, OneToOneRel)):
1137+
qs = NSR(field_name)
1138+
elif not self._is_through_field(field):
1139+
qs = NPR(field_name)
1140+
1141+
namespace_filters = []
1142+
if depth < self.DEPTH:
1143+
namespace.append({
1144+
'namespace': field_name,
1145+
'filters': namespace_filters,
1146+
'qs': qs,
1147+
})
1148+
1149+
return field.related_model, namespace_filters, circular_related_name
1150+
1151+
def _get_relative_field_name(self, field, circular_related_name, prefix):
1152+
field_name = field.name
1153+
if circular_related_name and field_name == circular_related_name:
1154+
# This is needed to avoid circular dependencies
1155+
return
1156+
1157+
if prefix:
1158+
rel_f_name = '.'.join((prefix, field_name))
1159+
else:
1160+
rel_f_name = field_name
1161+
1162+
if rel_f_name in self.EXCLUDE_FILTERS or rel_f_name in self._described_filters:
1163+
return
1164+
1165+
return rel_f_name
1166+
1167+
@staticmethod
1168+
def _is_through_field(field):
1169+
return getattr(field, 'through', None)

tests/dj_rf/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ class FKRelated1(models.Model):
8383

8484

8585
class FKRelated2(models.Model):
86-
related21 = models.ForeignKey(FKRelated1, null=True, on_delete=models.PROTECT)
86+
related21 = models.ForeignKey(FKRelated1, null=True, on_delete=models.PROTECT, related_name='r')
8787

8888

8989
class OneTOneRelated(models.Model):
@@ -123,7 +123,7 @@ class ReverseManyToManyRelated(models.Model):
123123

124124
class ReverseManyToManyTroughRelated(models.Model):
125125
auto = models.ManyToManyField(
126-
AutoMain, through='Through', through_fields=('mtm', 'auto'), related_name='reverse_MtM'
126+
AutoMain, through='Through', through_fields=('mtm', 'auto'), related_name='reverse_MtM',
127127
)
128128

129129

tests/test_filter_cls/test_initialization.py

Lines changed: 140 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from tests.dj_rf.filters import AUTHOR_FILTERS, BooksFilterClass
1515
from tests.dj_rf.models import Author, AutoMain, Book
1616

17-
1817
empty_qs = Author.objects.none()
1918

2019

@@ -388,10 +387,146 @@ class Cls(AutoRQLFilterClass):
388387
)
389388

390389

391-
def test_nested_auto_building_filters():
390+
def test_nested_auto_building_filters_depth_0():
391+
class Cls(NestedAutoRQLFilterClass):
392+
MODEL = AutoMain
393+
DEPTH = 0
394+
395+
assert set(Cls(AutoMain.objects.all()).filters.keys()) == {'id', 'common_int', 'common_str'}
396+
397+
398+
def test_nested_auto_building_filters_depth_1_check_structure():
399+
class Cls(NestedAutoRQLFilterClass):
400+
MODEL = AutoMain
401+
402+
assert set(Cls(AutoMain.objects.all()).filters.keys()) == {
403+
'id',
404+
'common_int',
405+
'common_str',
406+
'parent.id',
407+
'parent.common_int',
408+
'parent.common_str',
409+
'self.id',
410+
'self.common_int',
411+
'self.common_str',
412+
'related1.id',
413+
'related2.id',
414+
'one_to_one.id',
415+
'many_to_many.id',
416+
'reverse_OtM.id',
417+
'reverse_OtO.id',
418+
'reverse_MtM.id',
419+
}
420+
421+
422+
@pytest.mark.django_db
423+
def test_nested_auto_building_filters_depth_2_check_select():
392424
class Cls(NestedAutoRQLFilterClass):
393425
MODEL = AutoMain
394-
DEPTH = 5
426+
DEPTH = 2
427+
428+
filter_cls = Cls(AutoMain.objects.all())
429+
assert set(filter_cls.filters.keys()) == {
430+
'common_int',
431+
'common_str',
432+
'id',
433+
'many_to_many.id',
434+
'one_to_one.id',
435+
'parent.common_int',
436+
'parent.common_str',
437+
'parent.id',
438+
'parent.many_to_many.id',
439+
'parent.one_to_one.id',
440+
'parent.parent.common_int',
441+
'parent.parent.common_str',
442+
'parent.parent.id',
443+
'parent.related1.id',
444+
'parent.related2.id',
445+
'parent.reverse_MtM.id',
446+
'parent.reverse_OtM.id',
447+
'parent.reverse_OtO.id',
448+
'related1.r.id',
449+
'related1.id',
450+
'related2.id',
451+
'related2.related21.id',
452+
'reverse_MtM.id',
453+
'reverse_MtM.through.common_int',
454+
'reverse_MtM.through.id',
455+
'reverse_OtM.auto2.common_int',
456+
'reverse_OtM.auto2.common_str',
457+
'reverse_OtM.auto2.id',
458+
'reverse_OtM.id',
459+
'reverse_OtO.id',
460+
'self.common_int',
461+
'self.common_str',
462+
'self.id',
463+
'self.many_to_many.id',
464+
'self.one_to_one.id',
465+
'self.related1.id',
466+
'self.related2.id',
467+
'self.reverse_MtM.id',
468+
'self.reverse_OtM.id',
469+
'self.reverse_OtO.id',
470+
'self.self.common_int',
471+
'self.self.common_str',
472+
'self.self.id',
473+
}
395474

396-
result = Cls(AutoMain.objects.all())
397-
print(1)
475+
_, qs = filter_cls.apply_filters('')
476+
assert qs.query.select_related == {
477+
'reverse_OtO': {},
478+
'self': {
479+
'reverse_OtO': {},
480+
'self': {},
481+
'related1': {},
482+
'related2': {},
483+
'one_to_one': {},
484+
},
485+
'related1': {},
486+
'related2': {
487+
'related21': {},
488+
},
489+
'one_to_one': {},
490+
}
491+
assert set(qs._prefetch_related_lookups) == {
492+
'parent',
493+
'parent__parent',
494+
'parent__reverse_OtM',
495+
'parent__reverse_OtO',
496+
'parent__through',
497+
'parent__related1',
498+
'parent__related2',
499+
'parent__one_to_one',
500+
'parent__many_to_many',
501+
'reverse_OtM',
502+
'reverse_OtM__auto2',
503+
'through',
504+
'self__reverse_OtM',
505+
'self__through',
506+
'self__many_to_many',
507+
'related1__r',
508+
'many_to_many',
509+
}
510+
assert list(qs.all()) == []
511+
512+
513+
def test_nested_auto_building_filters_depth_3_described_and_exclusions():
514+
class Cls(NestedAutoRQLFilterClass):
515+
MODEL = AutoMain
516+
DEPTH = 3
517+
EXCLUDE_FILTERS = ('common_int', 'parent.parent', 'related1')
518+
FILTERS = ({
519+
'filter': 'random',
520+
'source': 'id',
521+
},)
522+
523+
filter_set = set(Cls(AutoMain.objects.all()).filters.keys())
524+
525+
assert {
526+
'random',
527+
'common_str',
528+
'self.self.self.common_int',
529+
'self.self.self.common_str',
530+
'self.self.self.id',
531+
}.issubset(filter_set)
532+
assert {'parent.parent.id', 'related1.id', 'common_int'}.isdisjoint(filter_set)

0 commit comments

Comments
 (0)