diff --git a/filters/decorators.py b/filters/decorators.py index 2591848..cec99de 100644 --- a/filters/decorators.py +++ b/filters/decorators.py @@ -1,3 +1,18 @@ +from django.db.models import Q + + +def make_query(queryset, method, filters, db_values): + query = Q() + for key, lookup in db_values.items(): + lookup_op = lookup[0] + # If has `IN` already in query to this key, apply it. + if key+'__in' in filters: + methodfn = getattr(queryset, method) + queryset = methodfn((key+'__in', filters[key+'__in'])) + # Combine all lookups. + for value in lookup[1]: + query = query | Q((key + lookup_op, value)) + return query def decorate_get_queryset(f): def decorated(self): @@ -14,5 +29,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) + # This dict will hold filter kwargs subqueries to pass in to Django ORM calls. + db_filters_values = queryset_filters['db_filters_values'] + + # This dict will hold exclude kwargs subqueries to pass in to Django ORM calls. + db_excludes_values = queryset_filters['db_excludes_values'] + + query = make_query(queryset, 'filter', db_filters, db_filters_values) + # Same logic as above, but for excludes. + query_exclude = make_query(queryset, 'exclude', db_excludes, db_excludes_values) + + return queryset.filter(query, **db_filters).exclude(query_exclude, **db_excludes) return decorated diff --git a/filters/mixins.py b/filters/mixins.py index cac7ea1..6e291f7 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 = [] + filters_values = [] + excludes_values = [] if getattr(self, 'filter_mappings', None) and query_params: filter_mappings = self.filter_mappings @@ -53,14 +54,35 @@ 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 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. + + lookups_with_subquery = ('__contains', '__icontains', + '__startswith', '__istartswith', + '__endswith', '__iendswith') + found = False + for lookup_suffix in lookups_with_subquery: + if query_filter.endswith(lookup_suffix): + lookup = (query_filter[:-len(lookup_suffix)], + (lookup_suffix, transformed_value)) + if is_exclude: + excludes_values.append(lookup) + else: + filters_values.append(lookup) + found = True + break + if found: + 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), dict(excludes_values) def __merge_query_params(self, url_params, query_params): ''' @@ -82,12 +104,17 @@ 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] + db_excludes_values = filters[3] return { 'db_filters': db_filters, 'db_excludes': db_excludes, + 'db_filters_values': db_filters_values, + 'db_excludes_values': db_excludes_values } def get_queryset(self):