@@ -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
0 commit comments