Skip to content

Commit 42d1969

Browse files
authored
Merge pull request #51 from cloudblue/feature/LITE-24359
LITE-24359 Adapted library for integration of various django extensions
2 parents fd707b9 + 94c7cc7 commit 42d1969

File tree

4 files changed

+99
-60
lines changed

4 files changed

+99
-60
lines changed

Diff for: dj_rql/constants.py

+15-14
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,25 @@
3131

3232

3333
class FilterTypes(FT):
34+
mapper = [
35+
(models.AutoField, FT.INT),
36+
(models.BooleanField, FT.BOOLEAN),
37+
(models.NullBooleanField, FT.BOOLEAN),
38+
(models.DateTimeField, FT.DATETIME),
39+
(models.DateField, FT.DATE),
40+
(models.DecimalField, FT.DECIMAL),
41+
(models.FloatField, FT.FLOAT),
42+
(models.IntegerField, FT.INT),
43+
(models.TextField, FT.STRING),
44+
(models.UUIDField, FT.STRING),
45+
(models.CharField, FT.STRING),
46+
]
47+
3448
@classmethod
3549
def field_filter_type(cls, field):
36-
mapper = [
37-
(models.AutoField, cls.INT),
38-
(models.BooleanField, cls.BOOLEAN),
39-
(models.NullBooleanField, cls.BOOLEAN),
40-
(models.DateTimeField, cls.DATETIME),
41-
(models.DateField, cls.DATE),
42-
(models.DecimalField, cls.DECIMAL),
43-
(models.FloatField, cls.FLOAT),
44-
(models.IntegerField, cls.INT),
45-
(models.TextField, cls.STRING),
46-
(models.UUIDField, cls.STRING),
47-
(models.CharField, cls.STRING),
48-
]
4950
return next(
5051
(
51-
filter_type for base_cls, filter_type in mapper
52+
filter_type for base_cls, filter_type in cls.mapper
5253
if issubclass(field.__class__, base_cls)
5354
),
5455
cls.STRING,

Diff for: dj_rql/filter_cls.py

+64-31
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ class RQLFilterClass:
6969
QUERIES_CACHE_SIZE = 20
7070
"""Default number of cached queries."""
7171

72+
Q_CLS = Q
73+
"""Class for building nodes of the query, generated by django."""
74+
75+
FILTER_TYPES_CLS = FilterTypes
76+
"""Class for the mapping of model field types to filter types."""
77+
7278
def __init__(self, queryset, instance=None):
7379
self.queryset = queryset
7480
self._is_distinct = self.DISTINCT
@@ -82,9 +88,13 @@ def __init__(self, queryset, instance=None):
8288
self._validate_init()
8389
self._default_init(self._get_init_filters())
8490

91+
@classmethod
92+
def _is_valid_model_cls(cls, model):
93+
return issubclass(model, Model)
94+
8595
def _validate_init(self):
8696
e = 'Django model must be set for Filter Class.'
87-
assert self.MODEL and issubclass(self.MODEL, Model), e
97+
assert self.MODEL and self._is_valid_model_cls(self.MODEL), e
8898

8999
e = 'Wrong filter settings type for Filter Class.'
90100
assert (self.FILTERS is None) or isinstance(self.FILTERS, iterable_types), e
@@ -258,7 +268,7 @@ def build_q_for_filter(self, data):
258268

259269
base_item = self.get_filter_base_item(filter_name)
260270
if not base_item:
261-
return Q()
271+
return self.Q_CLS()
262272

263273
if base_item.get('distinct'):
264274
self._is_distinct = True
@@ -311,7 +321,7 @@ def build_q_for_filter(self, data):
311321
return self._build_django_q(filter_item, django_lookup, filter_lookup, typed_value)
312322

313323
# filter has different DB field 'sources'
314-
q = Q()
324+
q = self.Q_CLS()
315325
for item in filter_item:
316326
item_q = self._build_django_q(item, django_lookup, filter_lookup, typed_value)
317327
if filter_lookup == FilterLookups.NE:
@@ -432,7 +442,7 @@ def _build_q_for_search(self, operator, str_value):
432442

433443
unquoted_value = self.remove_quotes(str_value)
434444
if not unquoted_value:
435-
return Q()
445+
return self.Q_CLS()
436446

437447
if not unquoted_value.startswith(RQL_ANY_SYMBOL):
438448
unquoted_value = '*' + unquoted_value
@@ -449,7 +459,7 @@ def _build_q_for_search(self, operator, str_value):
449459
return q
450460

451461
def _build_q_for_extended_search(self, str_value):
452-
q = Q()
462+
q = self.Q_CLS()
453463
extended_search_filter_lookup = FilterLookups.I_LIKE
454464

455465
for django_orm_route in self.EXTENDED_SEARCH_ORM_ROUTES:
@@ -591,9 +601,9 @@ def _build_filters(self, filters, **kwargs):
591601
orm_field_name = item.get('source', namespace)
592602
related_orm_route = '{0}{1}__'.format(orm_route, orm_field_name)
593603

594-
related_model = self._get_field(
604+
related_model = self._get_field_related_model(self._get_field(
595605
_model, orm_field_name, get_related=True,
596-
).related_model
606+
))
597607

598608
qs = item.get('qs')
599609
tree, p_qs = self._fill_select_tree(
@@ -735,6 +745,14 @@ def _extend_annotations(self):
735745

736746
self.annotations.update(dict(extended_annotations))
737747

748+
@classmethod
749+
def _is_field_supported(cls, field):
750+
return isinstance(field, SUPPORTED_FIELD_TYPES)
751+
752+
@classmethod
753+
def _get_field_related_model(cls, field):
754+
return field.related_model
755+
738756
@classmethod
739757
def _get_field(cls, base_model, field_name, get_related=False):
740758
""" Django ORM field getter.
@@ -750,10 +768,10 @@ def _get_field(cls, base_model, field_name, get_related=False):
750768
current_field = cls._get_model_field(current_model, part)
751769
if index == field_name_parts_length:
752770
e = 'Unsupported field type: {0}.'.format(field_name)
753-
assert get_related or isinstance(current_field, SUPPORTED_FIELD_TYPES), e
771+
assert get_related or cls._is_field_supported(current_field), e
754772

755773
return current_field
756-
current_model = current_field.related_model
774+
current_model = cls._get_field_related_model(current_field)
757775

758776
@staticmethod
759777
def _get_field_name_parts(field_name):
@@ -762,6 +780,10 @@ def _get_field_name_parts(field_name):
762780

763781
return field_name.split('.' if '.' in field_name else '__')
764782

783+
@classmethod
784+
def _is_field_nullable(cls, field):
785+
return field.null or cls._is_pk_field(field)
786+
765787
@classmethod
766788
def _build_mapped_item(cls, field, field_orm_route, **kwargs):
767789
lookups = kwargs.get('lookups')
@@ -771,8 +793,8 @@ def _build_mapped_item(cls, field, field_orm_route, **kwargs):
771793
openapi = kwargs.get('openapi')
772794
hidden = kwargs.get('hidden')
773795

774-
possible_lookups = lookups or FilterTypes.default_field_filter_lookups(field)
775-
if not (field.null or cls._is_pk_field(field)):
796+
possible_lookups = lookups or cls.FILTER_TYPES_CLS.default_field_filter_lookups(field)
797+
if not cls._is_field_nullable(field):
776798
possible_lookups.discard(FilterLookups.NULL)
777799

778800
result = {
@@ -924,40 +946,50 @@ def _escape_regex_special_symbols(str_value):
924946

925947
@classmethod
926948
def _convert_value(cls, django_field, str_value, use_repr=False):
949+
ft_cls = cls.FILTER_TYPES_CLS
927950
val = cls.remove_quotes(str_value)
928-
filter_type = FilterTypes.field_filter_type(django_field)
951+
filter_type = ft_cls.field_filter_type(django_field)
929952

930-
if filter_type == FilterTypes.FLOAT:
953+
if filter_type == ft_cls.FLOAT:
931954
return float(val)
932955

933-
elif filter_type == FilterTypes.DECIMAL:
934-
if '.' in val:
935-
integer_part, fractional_part = val.split('.', 1)
936-
val = integer_part + '.' + fractional_part[:django_field.decimal_places]
937-
return decimal.Decimal(val)
956+
elif filter_type == ft_cls.DECIMAL:
957+
return cls._convert_decimal_value(val, django_field)
938958

939-
elif filter_type == FilterTypes.DATE:
959+
elif filter_type == ft_cls.DATE:
940960
return cls._convert_date_value(val)
941961

942-
elif filter_type == FilterTypes.DATETIME:
962+
elif filter_type == ft_cls.DATETIME:
943963
return cls._convert_datetime_value(val)
944964

945-
elif filter_type == FilterTypes.BOOLEAN:
965+
elif filter_type == ft_cls.BOOLEAN:
946966
return cls._convert_boolean_value(val)
947967

948968
if val == RQL_EMPTY:
949-
if (filter_type == FilterTypes.INT) or (not django_field.blank):
969+
if (filter_type == ft_cls.INT) or (not django_field.blank):
950970
raise ValueError
951971
return ''
952972

953973
choices = getattr(django_field, 'choices', None)
954974
if not choices:
955-
if filter_type == FilterTypes.INT:
975+
if filter_type == ft_cls.INT:
956976
return int(val)
957977
return val
958978

959979
return cls._get_choices_field_db_value(val, choices, filter_type, use_repr)
960980

981+
@classmethod
982+
def _convert_decimal_value(cls, value, field):
983+
if '.' in value:
984+
integer_part, fractional_part = value.split('.', 1)
985+
value = integer_part + '.' + fractional_part[:cls._get_decimal_field_precision(field)]
986+
987+
return decimal.Decimal(value)
988+
989+
@classmethod
990+
def _get_decimal_field_precision(cls, field):
991+
return field.decimal_places
992+
961993
@staticmethod
962994
def _convert_date_value(value):
963995
dt = parse_date(value)
@@ -1002,8 +1034,8 @@ def _get_choices_field_db_value(cls, value, choices, filter_type, use_repr):
10021034
except StopIteration:
10031035
raise ValueError
10041036

1005-
@staticmethod
1006-
def _get_choice_class_db_value(value, choices, filter_type, use_repr):
1037+
@classmethod
1038+
def _get_choice_class_db_value(cls, value, choices, filter_type, use_repr):
10071039
if use_repr:
10081040
try:
10091041
db_value = next(
@@ -1013,7 +1045,7 @@ def _get_choice_class_db_value(value, choices, filter_type, use_repr):
10131045
except StopIteration:
10141046
raise ValueError
10151047

1016-
if filter_type == FilterTypes.INT:
1048+
if filter_type == cls.FILTER_TYPES_CLS.INT:
10171049
db_value = int(value)
10181050
else:
10191051
db_value = value
@@ -1024,8 +1056,8 @@ def _get_choice_class_db_value(value, choices, filter_type, use_repr):
10241056
return db_value
10251057

10261058
def _build_django_q(self, filter_item, django_lookup, filter_lookup, typed_value):
1027-
kwargs = {'{0}__{1}'.format(filter_item['orm_route'], django_lookup): typed_value}
1028-
return ~Q(**kwargs) if filter_lookup == FilterLookups.NE else Q(**kwargs)
1059+
q = self.Q_CLS(**{'{0}__{1}'.format(filter_item['orm_route'], django_lookup): typed_value})
1060+
return ~q if filter_lookup == FilterLookups.NE else q
10291061

10301062
@staticmethod
10311063
def _get_filter_lookup_by_operator(grammar_operator):
@@ -1082,9 +1114,10 @@ def _check_dynamic(filter_item, filter_name, filter_route):
10821114
e = "{0}: common filters can't have 'field' set.".format(filter_name)
10831115
assert not filter_item.get('custom', False) and field is None, e
10841116

1085-
@staticmethod
1086-
def _check_search(filter_item, filter_name, field):
1087-
is_non_string_field_type = FilterTypes.field_filter_type(field) != FilterTypes.STRING
1117+
@classmethod
1118+
def _check_search(cls, filter_item, filter_name, field):
1119+
ft_cls = cls.FILTER_TYPES_CLS
1120+
is_non_string_field_type = ft_cls.field_filter_type(field) != ft_cls.STRING
10881121

10891122
e = "{0}: 'search' can be applied only to text filters.".format(filter_name)
10901123
assert not (filter_item.get('search') and is_non_string_field_type), e

Diff for: dj_rql/transformer.py

+19-14
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
from dj_rql._dataclasses import FilterArgs
66

7-
from django.db.models import Q
8-
97
from lark import Tree
108

119
from py_rql.constants import (
@@ -44,6 +42,10 @@ def __init__(self, filter_cls_instance):
4442

4543
self.__visit_tokens__ = False
4644

45+
@property
46+
def _q(self):
47+
return self._filter_cls_instance.Q_CLS
48+
4749
def _push_namespace(self, tree):
4850
if tree.data in self.NAMESPACE_PROVIDERS:
4951
self._namespace.append(None)
@@ -69,12 +71,11 @@ def _transform_tree(self, tree):
6971
self._pop_namespace(tree)
7072
return ret_value
7173

72-
@staticmethod
73-
def _get_value(obj):
74+
def _get_value(self, obj):
7475
while isinstance(obj, Tree):
7576
obj = obj.children[0]
7677

77-
if isinstance(obj, Q):
78+
if isinstance(obj, self._q):
7879
return obj
7980

8081
return obj.value
@@ -95,7 +96,7 @@ def start(self, args):
9596
def comp(self, args):
9697
prop, operation, value = self._extract_comparison(args)
9798

98-
if isinstance(value, Q):
99+
if isinstance(value, self._q):
99100
if operation == ComparisonOperators.EQ:
100101
return value
101102
else:
@@ -106,17 +107,21 @@ def comp(self, args):
106107
return self._filter_cls_instance.build_q_for_filter(filter_args)
107108

108109
def tuple(self, args):
109-
return Q(*args)
110+
return self._q(*args)
110111

111112
def logical(self, args):
112113
operation = args[0].data
113114
children = args[0].children
114115
if operation == LogicalOperators.get_grammar_key(LogicalOperators.NOT):
115-
return ~Q(children[0])
116+
return ~children[0]
117+
118+
q = self._q()
116119
if operation == LogicalOperators.get_grammar_key(LogicalOperators.AND):
117-
return Q(*children)
120+
for child in children:
121+
q &= child
122+
123+
return q
118124

119-
q = Q()
120125
for child in children:
121126
q |= child
122127

@@ -127,10 +132,10 @@ def listing(self, args):
127132
operation, prop = self._get_value(args[0]), self._get_value(args[1])
128133
f_op = ComparisonOperators.EQ if operation == ListOperators.IN else ComparisonOperators.NE
129134

130-
q = Q()
135+
q = self._q()
131136
for value_tree in args[2:]:
132137
value = self._get_value(value_tree)
133-
if isinstance(value, Q):
138+
if isinstance(value, self._q):
134139
if f_op == ComparisonOperators.EQ:
135140
field_q = value
136141
else:
@@ -164,7 +169,7 @@ def ordering(self, args):
164169
for prop in props:
165170
self._filtered_props.add(prop.replace('-', '').replace('+', ''))
166171

167-
return Q()
172+
return self._q()
168173

169174
def select(self, args):
170175
assert not self._select
@@ -177,7 +182,7 @@ def select(self, args):
177182
if not prop.startswith('-'):
178183
self._filtered_props.add(prop.replace('+', ''))
179184

180-
return Q()
185+
return self._q()
181186

182187

183188
class RQLLimitOffsetTransformer(BaseRQLTransformer):

Diff for: requirements/dev.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
lib-rql==1.1.2
1+
lib-rql>=1.1.3,<2
22
Django>=2.2.19

0 commit comments

Comments
 (0)