def filter_queryset(self, request, queryset, view): try: # Apply filters specified via query_params. Each entry in the lists # below is (negate, field, value). and_filters = [] or_filters = [] chain_filters = [] role_filters = [] search_filters = {} # Can only have two values: 'AND', 'OR' # If 'AND' is used, an iterm must satisfy all condition to show up in the results. # If 'OR' is used, an item just need to satisfy one condition to appear in results. search_filter_relation = 'OR' for key, values in request.query_params.lists(): if key in self.RESERVED_NAMES: continue # HACK: Make job event filtering by host name mostly work even # when not capturing job event hosts M2M. if queryset.model._meta.object_name == 'JobEvent' and key.startswith( 'hosts__name'): key = key.replace('hosts__name', 'or__host__name') or_filters.append((False, 'host__name__isnull', True)) # Custom __int filter suffix (internal use only). q_int = False if key.endswith('__int'): key = key[:-5] q_int = True # RBAC filtering if key == 'role_level': role_filters.append(values[0]) continue # Search across related objects. if key.endswith('__search'): if values and ',' in values[0]: search_filter_relation = 'AND' values = reduce(lambda list1, list2: list1 + list2, [i.split(',') for i in values]) for value in values: search_value, new_keys = self.value_to_python( queryset.model, key, force_text(value)) assert isinstance(new_keys, list) search_filters[search_value] = new_keys continue # Custom chain__ and or__ filters, mutually exclusive (both can # precede not__). q_chain = False q_or = False if key.startswith('chain__'): key = key[7:] q_chain = True elif key.startswith('or__'): key = key[4:] q_or = True # Custom not__ filter prefix. q_not = False if key.startswith('not__'): key = key[5:] q_not = True # Make legacy v1 Job/Template fields work for backwards compatability # TODO: remove after API v1 deprecation period if queryset.model._meta.object_name in ( 'JobTemplate', 'Job') and key in ( 'credential', 'vault_credential', 'cloud_credential', 'network_credential' ) or queryset.model._meta.object_name in ( 'InventorySource', 'InventoryUpdate') and key == 'credential': key = 'credentials' # Make legacy v1 Credential fields work for backwards compatability # TODO: remove after API v1 deprecation period # # convert v1 `Credential.kind` queries to `Credential.credential_type__pk` if queryset.model._meta.object_name == 'Credential' and key == 'kind': key = key.replace('kind', 'credential_type') if 'ssh' in values: # In 3.2, SSH and Vault became separate credential types, but in the v1 API, # they're both still "kind=ssh" # under the hood, convert `/api/v1/credentials/?kind=ssh` to # `/api/v1/credentials/?or__credential_type=<ssh_pk>&or__credential_type=<vault_pk>` values = set(values) values.add('vault') values = list(values) q_or = True for i, kind in enumerate(values): if kind == 'vault': type_ = CredentialType.objects.get(kind=kind) else: type_ = CredentialType.from_v1_kind(kind) if type_ is None: raise ParseError( _('cannot filter on kind %s') % kind) values[i] = type_.pk # Convert value(s) to python and add to the appropriate list. for value in values: if q_int: value = int(value) value, new_key = self.value_to_python( queryset.model, key, value) if q_chain: chain_filters.append((q_not, new_key, value)) elif q_or: or_filters.append((q_not, new_key, value)) else: 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 or search_filters: args = [] for n, k, v in and_filters: if n: args.append(~Q(**{k: v})) else: args.append(Q(**{k: v})) for role_name in role_filters: if not hasattr(queryset.model, 'accessible_pk_qs'): raise ParseError( _('Cannot apply role_level filter to this list because its model ' 'does not use roles for access control.')) args.append( Q(pk__in=queryset.model.accessible_pk_qs( request.user, role_name))) if or_filters: q = Q() for n, k, v in or_filters: if n: q |= ~Q(**{k: v}) else: q |= Q(**{k: v}) args.append(q) if search_filters and search_filter_relation == 'OR': q = Q() for term, constrains in search_filters.items(): for constrain in constrains: q |= Q(**{constrain: term}) args.append(q) elif search_filters and search_filter_relation == 'AND': for term, constrains in search_filters.items(): q_chain = Q() for constrain in constrains: q_chain |= Q(**{constrain: term}) queryset = queryset.filter(q_chain) for n, k, v in chain_filters: if n: q = ~Q(**{k: v}) else: q = Q(**{k: v}) queryset = queryset.filter(q) queryset = queryset.filter(*args).distinct() return queryset except (FieldError, FieldDoesNotExist, ValueError, TypeError) as e: raise ParseError(e.args[0]) except ValidationError as e: raise ParseError(json.dumps(e.messages, ensure_ascii=False))
def filter_queryset(self, request, queryset, view): try: # Apply filters specified via query_params. Each entry in the lists # below is (negate, field, value). and_filters = [] or_filters = [] chain_filters = [] role_filters = [] search_filters = [] for key, values in request.query_params.lists(): if key in self.RESERVED_NAMES: continue # HACK: Make job event filtering by host name mostly work even # when not capturing job event hosts M2M. if queryset.model._meta.object_name == 'JobEvent' and key.startswith( 'hosts__name'): key = key.replace('hosts__name', 'or__host__name') or_filters.append((False, 'host__name__isnull', True)) # Custom __int filter suffix (internal use only). q_int = False if key.endswith('__int'): key = key[:-5] q_int = True # RBAC filtering if key == 'role_level': 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 q_or = False if key.startswith('chain__'): key = key[7:] q_chain = True elif key.startswith('or__'): key = key[4:] q_or = True # Custom not__ filter prefix. q_not = False if key.startswith('not__'): key = key[5:] q_not = True # Make legacy v1 Job/Template fields work for backwards compatability # TODO: remove after API v1 deprecation period if queryset.model._meta.object_name in ( 'JobTemplate', 'Job') and key in ('credential', 'vault_credential', 'cloud_credential', 'network_credential'): key = 'credentials' # Make legacy v1 Credential fields work for backwards compatability # TODO: remove after API v1 deprecation period # # convert v1 `Credential.kind` queries to `Credential.credential_type__pk` if queryset.model._meta.object_name == 'Credential' and key == 'kind': key = key.replace('kind', 'credential_type') if 'ssh' in values: # In 3.2, SSH and Vault became separate credential types, but in the v1 API, # they're both still "kind=ssh" # under the hood, convert `/api/v1/credentials/?kind=ssh` to # `/api/v1/credentials/?or__credential_type=<ssh_pk>&or__credential_type=<vault_pk>` values = set(values) values.add('vault') values = list(values) q_or = True for i, kind in enumerate(values): if kind == 'vault': type_ = CredentialType.objects.get(kind=kind) else: type_ = CredentialType.from_v1_kind(kind) if type_ is None: raise ParseError( _('cannot filter on kind %s') % kind) values[i] = type_.pk # Convert value(s) to python and add to the appropriate list. for value in values: if q_int: value = int(value) value, new_key = self.value_to_python( queryset.model, key, value) if q_chain: chain_filters.append((q_not, new_key, value)) elif q_or: or_filters.append((q_not, new_key, value)) else: 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 or search_filters: args = [] for n, k, v in and_filters: if n: args.append(~Q(**{k: v})) else: args.append(Q(**{k: v})) for role_name in role_filters: args.append( Q(pk__in=RoleAncestorEntry.objects.filter( ancestor__in=request.user.roles.all(), content_type_id=ContentType.objects.get_for_model( queryset.model).id, role_field=role_name).values_list( 'object_id').distinct())) if or_filters: q = Q() for n, k, v in or_filters: if n: q |= ~Q(**{k: v}) 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}) else: q = Q(**{k: v}) queryset = queryset.filter(q) queryset = queryset.filter(*args).distinct() return queryset except (FieldError, FieldDoesNotExist, ValueError, TypeError) as e: raise ParseError(e.args[0]) except ValidationError as e: raise ParseError(json.dumps(e.messages, ensure_ascii=False))