From ea10ff8b936ac5bdaf852d48522b996f356c2b0d Mon Sep 17 00:00:00 2001 From: Chris Church Date: Tue, 13 Dec 2016 21:44:09 -0500 Subject: [PATCH] Initial pass at related search fields. --- awx/api/filters.py | 29 +++++++++++++++++++++++++-- awx/api/generics.py | 17 +++++++++++++++- awx/api/metadata.py | 4 ++++ awx/api/templates/api/_list_common.md | 4 ++++ 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/awx/api/filters.py b/awx/api/filters.py index 5146ff0cd2..9128af7e81 100644 --- a/awx/api/filters.py +++ b/awx/api/filters.py @@ -77,7 +77,7 @@ class FieldLookupBackend(BaseFilterBackend): SUPPORTED_LOOKUPS = ('exact', 'iexact', 'contains', 'icontains', 'startswith', 'istartswith', 'endswith', 'iendswith', 'regex', 'iregex', 'gt', 'gte', 'lt', 'lte', 'in', - 'isnull') + 'isnull', 'search') def get_field_from_lookup(self, model, lookup): field = None @@ -148,6 +148,15 @@ class FieldLookupBackend(BaseFilterBackend): re.compile(value) except re.error as e: raise ValueError(e.args[0]) + elif new_lookup.endswith('__search'): + related_model = getattr(field, 'related_model', None) + if not related_model: + raise ValueError('%s is not searchable' % new_lookup[:-8]) + new_lookups = [] + for rm_field in related_model._meta.fields: + if rm_field.name in ('username', 'first_name', 'last_name', 'email', 'name', 'description'): + new_lookups.append('{}__{}__icontains'.format(new_lookup[:-8], rm_field.name)) + return value, new_lookups else: value = self.value_to_python_for_field(field, value) return value, new_lookup @@ -160,6 +169,7 @@ class FieldLookupBackend(BaseFilterBackend): or_filters = [] chain_filters = [] role_filters = [] + search_filters = [] for key, values in request.query_params.lists(): if key in self.RESERVED_NAMES: continue @@ -181,6 +191,16 @@ class FieldLookupBackend(BaseFilterBackend): role_filters.append(values[0]) continue + # Search across related objects. + if key.endswith('__search'): + for value in values: + for search_term in force_text(value).replace(',', ' ').split(): + search_value, new_keys = self.value_to_python(queryset.model, key, search_term) + assert isinstance(new_keys, list) + for new_key in new_keys: + search_filters.append((new_key, search_value)) + continue + # Custom chain__ and or__ filters, mutually exclusive (both can # precede not__). q_chain = False @@ -211,7 +231,7 @@ class FieldLookupBackend(BaseFilterBackend): and_filters.append((q_not, new_key, value)) # Now build Q objects for database query filter. - if and_filters or or_filters or chain_filters or role_filters: + if and_filters or or_filters or chain_filters or role_filters or search_filters: args = [] for n, k, v in and_filters: if n: @@ -234,6 +254,11 @@ class FieldLookupBackend(BaseFilterBackend): else: q |= Q(**{k:v}) args.append(q) + if search_filters: + q = Q() + for k,v in search_filters: + q |= Q(**{k:v}) + args.append(q) for n,k,v in chain_filters: if n: q = ~Q(**{k:v}) diff --git a/awx/api/generics.py b/awx/api/generics.py index 1062135a28..73b92cfcc5 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -267,10 +267,25 @@ class ListAPIView(generics.ListAPIView, GenericAPIView): fields = [] for field in self.model._meta.fields: if field.name in ('username', 'first_name', 'last_name', 'email', - 'name', 'description', 'email'): + 'name', 'description'): fields.append(field.name) return fields + @property + def related_search_fields(self): + fields = [] + for field in self.model._meta.fields: + if field.name.endswith('_role'): + continue + if getattr(field, 'related_model', None): + fields.append('{}__search'.format(field.name)) + for rel in self.model._meta.related_objects: + name = rel.get_accessor_name() + if name.endswith('_set'): + continue + fields.append('{}__search'.format(name)) + return fields + class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView): # Base class for a list view that allows creating new objects. diff --git a/awx/api/metadata.py b/awx/api/metadata.py index 6dd186c9ef..fb5c4d6493 100644 --- a/awx/api/metadata.py +++ b/awx/api/metadata.py @@ -182,6 +182,10 @@ class Metadata(metadata.SimpleMetadata): if getattr(view, 'search_fields', None): metadata['search_fields'] = view.search_fields + # Add related search fields if available from the view. + if getattr(view, 'related_search_fields', None): + metadata['related_search_fields'] = view.related_search_fields + return metadata diff --git a/awx/api/templates/api/_list_common.md b/awx/api/templates/api/_list_common.md index 36e6819276..706ae732a5 100644 --- a/awx/api/templates/api/_list_common.md +++ b/awx/api/templates/api/_list_common.md @@ -56,6 +56,10 @@ within all designated text fields of a model. _Added in AWX 1.4_ +(_Added in Ansible Tower 3.1.0_) Search across related fields: + + ?related__search=findme + ## Filtering Any additional query string parameters may be used to filter the list of