From 34c00644a9f87d1d3487c59a3a56352319b4eed9 Mon Sep 17 00:00:00 2001 From: JoaoEdison Date: Thu, 21 Aug 2025 17:31:44 -0300 Subject: [PATCH 1/7] Fix CSV passed as value not being treated as in 'IN' query. --- filters/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/filters/mixins.py b/filters/mixins.py index cac7ea1..43365ab 100644 --- a/filters/mixins.py +++ b/filters/mixins.py @@ -53,7 +53,7 @@ def __get_queryset_filters(self, query_params, *args, **kwargs): transform_value = value_transformations.get(query, lambda val: val) transformed_value = transform_value(value) # [2] multiple options is filter values will execute as `IN` query - if isinstance(value, list) and not query_filter.endswith('__in'): + if ',' in value and not query_filter.endswith('__in'): query_filter += '__in' if is_exclude: excludes.append((query_filter, transformed_value)) From fdf4e0d55e3cd19b430c12293d8a5b8c8a0fe553 Mon Sep 17 00:00:00 2001 From: JoaoEdison Date: Thu, 21 Aug 2025 23:10:19 -0300 Subject: [PATCH 2/7] Add suport to contains with query. --- filters/decorators.py | 14 +++++++++++++- filters/mixins.py | 23 ++++++++++++++++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/filters/decorators.py b/filters/decorators.py index 2591848..7668b1c 100644 --- a/filters/decorators.py +++ b/filters/decorators.py @@ -1,3 +1,5 @@ +from django.db.models import Q + def decorate_get_queryset(f): def decorated(self): @@ -14,5 +16,15 @@ def decorated(self): # This dict will hold exclude kwargs to pass in to Django ORM calls. db_excludes = queryset_filters['db_excludes'] - return queryset.filter(**db_filters).exclude(**db_excludes) + query = Q() + for key, lookup in queryset_filters['db_filters_values'].items(): + lookup_op = lookup[0] + # If has `IN` already in query to this key, apply it. + if key+'__in' in db_filters: + queryset = queryset.filter((key+'__in', db_filters[key+'__in'])) + # Combine all lookups. + for value in lookup[1]: + query = query | Q((key + lookup_op, value)) + + return queryset.filter(query, **db_filters).exclude(**db_excludes) return decorated diff --git a/filters/mixins.py b/filters/mixins.py index 43365ab..2197337 100644 --- a/filters/mixins.py +++ b/filters/mixins.py @@ -22,9 +22,10 @@ def __get_queryset_filters(self, query_params, *args, **kwargs): [2] when a CSV is passed as value to a query params make a filter with 'IN' query. ''' - filters = [] excludes = [] + # TODO: excludes_values. + filters_values = [] if getattr(self, 'filter_mappings', None) and query_params: filter_mappings = self.filter_mappings @@ -54,13 +55,22 @@ def __get_queryset_filters(self, query_params, *args, **kwargs): transformed_value = transform_value(value) # [2] multiple options is filter values will execute as `IN` query if ',' in value and not query_filter.endswith('__in'): + # If lookup uses contains and is a CSV, needs to apply + # contains separately with each value. + + contains_str = '__contains' + if query_filter.endswith(contains_str): + values = [v for v in value.split(',') if v != ''] + query_filter = query_filter[:-len(contains_str)] + filters_values.append((query_filter, (contains_str, values))) + continue query_filter += '__in' if is_exclude: excludes.append((query_filter, transformed_value)) else: filters.append((query_filter, transformed_value)) - return dict(filters), dict(excludes) + return dict(filters), dict(excludes), dict(filters_values) def __merge_query_params(self, url_params, query_params): ''' @@ -82,12 +92,15 @@ def get_db_filters(self, url_params, query_params): query_params = self.__merge_query_params(url_params, query_params) # get queryset filters - db_filters = self.__get_queryset_filters(query_params)[0] - db_excludes = self.__get_queryset_filters(query_params)[1] + filters = self.__get_queryset_filters(query_params) + db_filters = filters[0] + db_excludes = filters[1] + db_filters_values = filters[2] return { 'db_filters': db_filters, 'db_excludes': db_excludes, + 'db_filters_values': db_filters_values, } def get_queryset(self): @@ -96,4 +109,4 @@ def get_queryset(self): # (and hence the metaclass would not have been # able to decorate it with the filtering logic.) - return super(FiltersMixin, self).get_queryset() + return super().get_queryset() From 941c1f51039d83cb7831b915d5553a37972c747e Mon Sep 17 00:00:00 2001 From: JoaoEdison Date: Thu, 21 Aug 2025 23:40:11 -0300 Subject: [PATCH 3/7] Little fix. --- filters/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/filters/mixins.py b/filters/mixins.py index 2197337..3a30c77 100644 --- a/filters/mixins.py +++ b/filters/mixins.py @@ -109,4 +109,4 @@ def get_queryset(self): # (and hence the metaclass would not have been # able to decorate it with the filtering logic.) - return super().get_queryset() + return super(FiltersMixin, self).get_queryset() From eff6f3cc0bae44145972cadf1effdd50e67514da Mon Sep 17 00:00:00 2001 From: JoaoEdison Date: Fri, 22 Aug 2025 13:18:42 -0300 Subject: [PATCH 4/7] Add suport to __icontains with CSV value. --- filters/mixins.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/filters/mixins.py b/filters/mixins.py index 3a30c77..703ded1 100644 --- a/filters/mixins.py +++ b/filters/mixins.py @@ -58,13 +58,19 @@ def __get_queryset_filters(self, query_params, *args, **kwargs): # If lookup uses contains and is a CSV, needs to apply # contains separately with each value. - contains_str = '__contains' - if query_filter.endswith(contains_str): - values = [v for v in value.split(',') if v != ''] - query_filter = query_filter[:-len(contains_str)] - filters_values.append((query_filter, (contains_str, values))) + lookups_with_subquery = ('__contains', '__icontains') + found = False + for lookup_suffix in lookups_with_subquery: + if query_filter.endswith(lookup_suffix): + values = [v for v in value.split(',') if v != ''] + query_filter = query_filter[:-len(lookup_suffix)] + filters_values.append((query_filter, (lookup_suffix, values))) + found = True + break + if found: continue query_filter += '__in' + if is_exclude: excludes.append((query_filter, transformed_value)) else: From f785c53df45e201161037dc851507c0714fdfdc7 Mon Sep 17 00:00:00 2001 From: JoaoEdison Date: Fri, 22 Aug 2025 18:50:24 -0300 Subject: [PATCH 5/7] CSV parsing needs to be done in filter_value_transformations. --- filters/decorators.py | 5 ++++- filters/mixins.py | 7 +++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/filters/decorators.py b/filters/decorators.py index 7668b1c..1023ee1 100644 --- a/filters/decorators.py +++ b/filters/decorators.py @@ -16,8 +16,11 @@ def decorated(self): # This dict will hold exclude kwargs to pass in to Django ORM calls. db_excludes = queryset_filters['db_excludes'] + # This dict will hold filter kwargs subqueries to pass in to Django ORM calls. + db_filters_values = queryset_filters['db_filters_values'] + query = Q() - for key, lookup in queryset_filters['db_filters_values'].items(): + for key, lookup in db_filters_values.items(): lookup_op = lookup[0] # If has `IN` already in query to this key, apply it. if key+'__in' in db_filters: diff --git a/filters/mixins.py b/filters/mixins.py index 703ded1..ea2fdc4 100644 --- a/filters/mixins.py +++ b/filters/mixins.py @@ -54,7 +54,7 @@ def __get_queryset_filters(self, query_params, *args, **kwargs): transform_value = value_transformations.get(query, lambda val: val) transformed_value = transform_value(value) # [2] multiple options is filter values will execute as `IN` query - if ',' in value and not query_filter.endswith('__in'): + if isinstance(transformed_value, list) and not query_filter.endswith('__in'): # If lookup uses contains and is a CSV, needs to apply # contains separately with each value. @@ -62,9 +62,8 @@ def __get_queryset_filters(self, query_params, *args, **kwargs): found = False for lookup_suffix in lookups_with_subquery: if query_filter.endswith(lookup_suffix): - values = [v for v in value.split(',') if v != ''] - query_filter = query_filter[:-len(lookup_suffix)] - filters_values.append((query_filter, (lookup_suffix, values))) + filters_values.append((query_filter[:-len(lookup_suffix)], + (lookup_suffix, transformed_value))) found = True break if found: From ebb3bb7cad78c264c3834e9f1f0d3292aa843374 Mon Sep 17 00:00:00 2001 From: JoaoEdison Date: Fri, 5 Sep 2025 15:56:15 -0300 Subject: [PATCH 6/7] Adds '__startswith' and '__endswith' support to 'IN' query. --- filters/mixins.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/filters/mixins.py b/filters/mixins.py index ea2fdc4..0923c1e 100644 --- a/filters/mixins.py +++ b/filters/mixins.py @@ -58,7 +58,8 @@ def __get_queryset_filters(self, query_params, *args, **kwargs): # If lookup uses contains and is a CSV, needs to apply # contains separately with each value. - lookups_with_subquery = ('__contains', '__icontains') + lookups_with_subquery = ('__contains', '__icontains', + '__startswith', '__endswith') found = False for lookup_suffix in lookups_with_subquery: if query_filter.endswith(lookup_suffix): From 1ea7030d391147fa628c56e0bfee75f85cbee7b6 Mon Sep 17 00:00:00 2001 From: JoaoEdison Date: Mon, 15 Sep 2025 23:47:59 -0300 Subject: [PATCH 7/7] __istartswith and __iendswith. --- filters/mixins.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/filters/mixins.py b/filters/mixins.py index 0923c1e..5e213fd 100644 --- a/filters/mixins.py +++ b/filters/mixins.py @@ -59,7 +59,8 @@ def __get_queryset_filters(self, query_params, *args, **kwargs): # contains separately with each value. lookups_with_subquery = ('__contains', '__icontains', - '__startswith', '__endswith') + '__startswith', '__istartswith', + '__endswith', '__iendswith') found = False for lookup_suffix in lookups_with_subquery: if query_filter.endswith(lookup_suffix):