diff --git a/filters/decorators.py b/filters/decorators.py index 2591848..1023ee1 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,18 @@ 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'] + + query = Q() + 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: + 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 cac7ea1..5e213fd 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 @@ -53,14 +54,30 @@ 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): + filters_values.append((query_filter[:-len(lookup_suffix)], + (lookup_suffix, transformed_value))) + 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) def __merge_query_params(self, url_params, query_params): ''' @@ -82,12 +99,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):