mirror of
https://github.com/ansible/awx.git
synced 2026-01-16 04:10:44 -03:30
Added API support for OR queries and searching text fields.
This commit is contained in:
parent
3760ce6641
commit
d410f57e08
@ -168,6 +168,8 @@ class GenericAPIView(generics.GenericAPIView, APIView):
|
||||
actions['GET'] = serializer.metadata()
|
||||
if actions:
|
||||
ret['actions'] = actions
|
||||
if getattr(self, 'search_fields', None):
|
||||
ret['search_fields'] = self.search_fields
|
||||
return ret
|
||||
|
||||
class ListAPIView(generics.ListAPIView, GenericAPIView):
|
||||
@ -188,6 +190,15 @@ class ListAPIView(generics.ListAPIView, GenericAPIView):
|
||||
})
|
||||
return d
|
||||
|
||||
@property
|
||||
def search_fields(self):
|
||||
fields = []
|
||||
for field in self.model._meta.fields:
|
||||
if field.name in ('username', 'first_name', 'last_name', 'email',
|
||||
'name', 'description', 'email'):
|
||||
fields.append(field.name)
|
||||
return fields
|
||||
|
||||
class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView):
|
||||
# Base class for a list view that allows creating new objects.
|
||||
|
||||
|
||||
@ -33,7 +33,8 @@ class FieldLookupBackend(BaseFilterBackend):
|
||||
Filter using field lookups provided via query string parameters.
|
||||
'''
|
||||
|
||||
RESERVED_NAMES = ('page', 'page_size', 'format', 'order', 'order_by')
|
||||
RESERVED_NAMES = ('page', 'page_size', 'format', 'order', 'order_by',
|
||||
'search')
|
||||
|
||||
SUPPORTED_LOOKUPS = ('exact', 'iexact', 'contains', 'icontains',
|
||||
'startswith', 'istartswith', 'endswith', 'iendswith',
|
||||
@ -109,33 +110,57 @@ class FieldLookupBackend(BaseFilterBackend):
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
try:
|
||||
# Apply filters and excludes specified via QUERY_PARAMS.
|
||||
filters = {}
|
||||
excludes = {}
|
||||
for key, value in request.QUERY_PARAMS.items():
|
||||
# Apply filters specified via QUERY_PARAMS. Each entry in the lists
|
||||
# below is (negate, field, value).
|
||||
and_filters = []
|
||||
or_filters = []
|
||||
for key, values in request.QUERY_PARAMS.lists():
|
||||
if key in self.RESERVED_NAMES:
|
||||
continue
|
||||
|
||||
# Custom __int filter suffix (internal use only).
|
||||
q_int = False
|
||||
if key.endswith('__int'):
|
||||
key = key[:-5]
|
||||
value = int(value)
|
||||
q_int = True
|
||||
# Custom or__ filter prefix (or__ can precede not__).
|
||||
q_or = False
|
||||
if 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
|
||||
|
||||
# Convert value to python and add to the appropriate dict.
|
||||
value = self.value_to_python(queryset.model, key, value)
|
||||
if q_not:
|
||||
excludes[key] = value
|
||||
else:
|
||||
filters[key] = value
|
||||
|
||||
if filters:
|
||||
queryset = queryset.filter(**filters)
|
||||
if excludes:
|
||||
queryset = queryset.exclude(**excludes)
|
||||
# Convert value(s) to python and add to the appropriate list.
|
||||
for value in values:
|
||||
if q_int:
|
||||
value = int(value)
|
||||
value = self.value_to_python(queryset.model, key, value)
|
||||
if q_or:
|
||||
or_filters.append((q_not, key, value))
|
||||
else:
|
||||
and_filters.append((q_not, key, value))
|
||||
|
||||
# Now build Q objects for database query filter.
|
||||
if and_filters or or_filters:
|
||||
args = []
|
||||
for n, k, v in and_filters:
|
||||
if n:
|
||||
args.append(~Q(**{k:v}))
|
||||
else:
|
||||
args.append(Q(**{k:v}))
|
||||
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)
|
||||
queryset = queryset.filter(*args)
|
||||
return queryset
|
||||
except (FieldError, FieldDoesNotExist, ValueError), e:
|
||||
raise ParseError(e.args[0])
|
||||
|
||||
@ -47,6 +47,15 @@ a particular page of results.
|
||||
The `previous` and `next` links returned with the results will set these query
|
||||
string parameters automatically.
|
||||
|
||||
## Searching
|
||||
|
||||
Use the `search` query string parameter to perform a case-insensitive search
|
||||
within all designated text fields of a model.
|
||||
|
||||
?search=findme
|
||||
|
||||
_New in AWX 1.4_
|
||||
|
||||
## Filtering
|
||||
|
||||
Any additional query string parameters may be used to filter the list of
|
||||
@ -66,6 +75,14 @@ To exclude results matching certain criteria, prefix the field parameter with
|
||||
|
||||
?not__field=value
|
||||
|
||||
(_New in AWX 1.4_) By default, all query string filters are AND'ed together, so
|
||||
only the results matching *all* filters will be returned. To combine results
|
||||
matching *any* one of multiple criteria, prefix each query string parameter
|
||||
with `or__`:
|
||||
|
||||
?or__field=value&or__field=othervalue
|
||||
?or__not__field=value&or__field=othervalue
|
||||
|
||||
Field lookups may also be used for more advanced queries, by appending the
|
||||
lookup to the field name:
|
||||
|
||||
|
||||
@ -125,20 +125,6 @@ class BaseTestMixin(object):
|
||||
))
|
||||
return results
|
||||
|
||||
def check_pagination_and_size(self, data, desired_count, previous=None, next=None):
|
||||
self.assertTrue('results' in data)
|
||||
self.assertEqual(data['count'], desired_count)
|
||||
self.assertEqual(data['previous'], previous)
|
||||
self.assertEqual(data['next'], next)
|
||||
|
||||
def check_list_ids(self, data, queryset, check_order=False):
|
||||
data_ids = [x['id'] for x in data['results']]
|
||||
qs_ids = queryset.values_list('pk', flat=True)
|
||||
if check_order:
|
||||
self.assertEqual(tuple(data_ids), tuple(qs_ids))
|
||||
else:
|
||||
self.assertEqual(set(data_ids), set(qs_ids))
|
||||
|
||||
def setup_users(self, just_super_user=False):
|
||||
# Create a user.
|
||||
self.super_username = 'admin'
|
||||
@ -296,12 +282,34 @@ class BaseTestMixin(object):
|
||||
else:
|
||||
f(url, expect=401)
|
||||
|
||||
def check_pagination_and_size(self, data, desired_count, previous=False,
|
||||
next=False):
|
||||
self.assertTrue('results' in data)
|
||||
self.assertEqual(data['count'], desired_count)
|
||||
if previous:
|
||||
self.assertTrue(data['previous'])
|
||||
else:
|
||||
self.assertFalse(data['previous'])
|
||||
if next:
|
||||
self.assertTrue(data['next'])
|
||||
else:
|
||||
self.assertFalse(data['next'])
|
||||
|
||||
def check_list_ids(self, data, queryset, check_order=False):
|
||||
data_ids = [x['id'] for x in data['results']]
|
||||
qs_ids = queryset.values_list('pk', flat=True)
|
||||
if check_order:
|
||||
self.assertEqual(tuple(data_ids), tuple(qs_ids))
|
||||
else:
|
||||
self.assertEqual(set(data_ids), set(qs_ids))
|
||||
|
||||
def check_get_list(self, url, user, qs, fields=None, expect=200,
|
||||
check_order=False):
|
||||
check_order=False, offset=None, limit=None):
|
||||
'''
|
||||
Check that the given list view URL returns results for the given user
|
||||
that match the given queryset.
|
||||
'''
|
||||
offset = offset or 0
|
||||
with self.current_user(user):
|
||||
if expect == 400:
|
||||
self.options(url, expect=200)
|
||||
@ -311,7 +319,14 @@ class BaseTestMixin(object):
|
||||
response = self.get(url, expect=expect)
|
||||
if expect != 200:
|
||||
return
|
||||
self.check_pagination_and_size(response, qs.count())
|
||||
total = qs.count()
|
||||
if limit is not None:
|
||||
if limit > 0:
|
||||
qs = qs[offset:offset+limit]
|
||||
else:
|
||||
qs = qs.none()
|
||||
self.check_pagination_and_size(response, total, offset > 0,
|
||||
limit and ((offset + limit) < total))
|
||||
self.check_list_ids(response, qs, check_order)
|
||||
if fields:
|
||||
for obj in response['results']:
|
||||
|
||||
@ -9,6 +9,7 @@ import urllib
|
||||
# Django
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.db.models import Q
|
||||
import django.test
|
||||
from django.test.client import Client
|
||||
from django.core.urlresolvers import reverse
|
||||
@ -423,12 +424,36 @@ class UsersTest(BaseTest):
|
||||
url = '%s?username__regex=%s' % (base_url, urllib.quote_plus('['))
|
||||
self.check_get_list(url, self.super_django_user, base_qs, expect=400)
|
||||
|
||||
# Filter by multiple usernames (AND).
|
||||
url = '%s?username=normal&username=nobody' % base_url
|
||||
qs = base_qs.filter(username='normal', username__exact='nobody')
|
||||
self.assertFalse(qs.count())
|
||||
self.check_get_list(url, self.super_django_user, qs)
|
||||
|
||||
# Filter by multiple usernames (OR).
|
||||
url = '%s?or__username=normal&or__username=nobody' % base_url
|
||||
qs = base_qs.filter(Q(username='normal') | Q(username='nobody'))
|
||||
self.assertTrue(qs.count())
|
||||
self.check_get_list(url, self.super_django_user, qs)
|
||||
|
||||
# Exclude by username.
|
||||
url = '%s?not__username=normal' % base_url
|
||||
qs = base_qs.exclude(username='normal')
|
||||
self.assertTrue(qs.count())
|
||||
self.check_get_list(url, self.super_django_user, qs)
|
||||
|
||||
# Exclude by multiple usernames.
|
||||
url = '%s?not__username=normal¬__username=nobody' % base_url
|
||||
qs = base_qs.filter(~Q(username='normal') & ~Q(username='nobody'))
|
||||
self.assertTrue(qs.count())
|
||||
self.check_get_list(url, self.super_django_user, qs)
|
||||
|
||||
# Exclude by multiple usernames with OR.
|
||||
url = '%s?or__not__username=normal&or__not__username=nobody' % base_url
|
||||
qs = base_qs.filter(~Q(username='normal') | ~Q(username='nobody'))
|
||||
self.assertTrue(qs.count())
|
||||
self.check_get_list(url, self.super_django_user, qs)
|
||||
|
||||
# Exclude by username with suffix.
|
||||
url = '%s?not__username__startswith=no' % base_url
|
||||
qs = base_qs.exclude(username__startswith='no')
|
||||
@ -625,6 +650,52 @@ class UsersTest(BaseTest):
|
||||
url = u'%s?user\u2605name=normal' % base_url
|
||||
self.check_get_list(url, self.super_django_user, base_qs, expect=400)
|
||||
|
||||
def test_user_list_pagination(self):
|
||||
base_url = reverse('main:user_list')
|
||||
base_qs = User.objects.distinct()
|
||||
|
||||
# Check list view with page size of 1.
|
||||
url = '%s?order_by=username&page_size=1' % base_url
|
||||
qs = base_qs.order_by('username')
|
||||
self.check_get_list(url, self.super_django_user, qs, check_order=True,
|
||||
limit=1)
|
||||
|
||||
# Check list view with page size of 1, remaining pages.
|
||||
qs = base_qs.order_by('username')
|
||||
for n in xrange(1, base_qs.count()):
|
||||
url = '%s?order_by=username&page_size=1&page=%d' % (base_url, n+1)
|
||||
self.check_get_list(url, self.super_django_user, qs,
|
||||
check_order=True, offset=n, limit=1)
|
||||
|
||||
# Check list view with page size of 2.
|
||||
qs = base_qs.order_by('username')
|
||||
for n in xrange(0, base_qs.count(), 2):
|
||||
url = '%s?order_by=username&page_size=2&page=%d' % (base_url, (n/2)+1)
|
||||
self.check_get_list(url, self.super_django_user, qs,
|
||||
check_order=True, offset=n, limit=2)
|
||||
|
||||
# Check list view with page size of 0 (to allow getting count of items
|
||||
# matching a given filter). # FIXME: Make this work at some point!
|
||||
#url = '%s?order_by=username&page_size=0' % base_url
|
||||
#qs = base_qs.order_by('username')
|
||||
#self.check_get_list(url, self.super_django_user, qs, check_order=True,
|
||||
# limit=0)
|
||||
|
||||
def test_user_list_searching(self):
|
||||
base_url = reverse('main:user_list')
|
||||
base_qs = User.objects.distinct()
|
||||
|
||||
# Check search query parameter.
|
||||
url = '%s?search=no' % base_url
|
||||
qs = base_qs.filter(username__icontains='no')
|
||||
self.check_get_list(url, self.super_django_user, qs)
|
||||
|
||||
# Check search query parameter.
|
||||
url = '%s?search=example' % base_url
|
||||
qs = base_qs.filter(email__icontains='example')
|
||||
self.check_get_list(url, self.super_django_user, qs)
|
||||
|
||||
|
||||
class LdapTest(BaseTest):
|
||||
|
||||
def use_test_setting(self, name, default=None, from_name=None):
|
||||
|
||||
@ -150,6 +150,7 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_FILTER_BACKENDS': (
|
||||
'awx.main.filters.ActiveOnlyBackend',
|
||||
'awx.main.filters.FieldLookupBackend',
|
||||
'rest_framework.filters.SearchFilter',
|
||||
'awx.main.filters.OrderByBackend',
|
||||
),
|
||||
'DEFAULT_PARSER_CLASSES': (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user