1
0
mirror of https://github.com/ansible/awx.git synced 2024-11-02 18:21:12 +03:00

Merge remote branch 'upstream/master'

This commit is contained in:
chouseknecht 2013-10-27 08:38:38 +00:00
commit 6f6a7dbf8b
8 changed files with 414 additions and 34 deletions

View File

@ -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.

View File

@ -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])

View File

@ -418,7 +418,11 @@ class Command(NoArgsCommand):
self.logger = logging.getLogger('awx.main.commands.inventory_import')
self.logger.setLevel(log_levels.get(self.verbosity, 0))
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')
class Formatter(logging.Formatter):
def format(self, record):
record.relativeSeconds = record.relativeCreated / 1000.0
return super(Formatter, self).format(record)
formatter = Formatter('%(relativeSeconds)9.3f %(levelname)-8s %(message)s')
handler.setFormatter(formatter)
self.logger.addHandler(handler)
self.logger.propagate = False

View File

@ -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:

View File

@ -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']:

View File

@ -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&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 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):

View File

@ -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': (

View File

@ -0,0 +1,236 @@
{# Copy of base.html from rest_framework with minor AWX change. #}
{% load url from future %}
{% load rest_framework %}
<!DOCTYPE html>
<html>
<head>
{% block head %}
{% block meta %}
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="robots" content="NONE,NOARCHIVE" />
{% endblock %}
<title>{% block title %}Django REST framework{% endblock %}</title>
{% block style %}
{% block bootstrap_theme %}
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
{% endblock %}
{% endblock %}
</head>
<body class="{% block bodyclass %}{% endblock %} container">
<div class="wrapper">
{% block navbar %}
<div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
<div class="navbar-inner">
<div class="container-fluid">
<span href="/">
{% block branding %}<a class='brand' href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
</span>
<ul class="nav pull-right">
{% block userlinks %}
{% if user.is_authenticated %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
{{ user }}
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li>{% optional_logout request %}</li>
</ul>
</li>
{% else %}
<li>{% optional_login request %}</li>
{% endif %}
{% endblock %}
</ul>
</div>
</div>
</div>
{% endblock %}
{% block breadcrumbs %}
<ul class="breadcrumb">
{% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
<li>
<a href="{{ breadcrumb_url }}" {% if forloop.last %}class="active"{% endif %}>{{ breadcrumb_name }}</a> {% if not forloop.last %}<span class="divider">&rsaquo;</span>{% endif %}
</li>
{% endfor %}
</ul>
{% endblock %}
<!-- Content -->
<div id="content">
{% if 'GET' in allowed_methods %}
<form id="get-form" class="pull-right">
<fieldset>
<div class="btn-group format-selection">
<a class="btn btn-primary js-tooltip" href='{{ request.get_full_path }}' rel="nofollow" title="Make a GET request on the {{ name }} resource">GET</a>
<button class="btn btn-primary dropdown-toggle js-tooltip" data-toggle="dropdown" title="Specify a format for the GET request">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% for format in available_formats %}
<li>
<a class="js-tooltip format-option" href='{% add_query_param request api_settings.URL_FORMAT_OVERRIDE format %}' rel="nofollow" title="Make a GET request on the {{ name }} resource with the format set to `{{ format }}`">{{ format }}</a>
</li>
{% endfor %}
</ul>
</div>
</fieldset>
</form>
{% endif %}
{% if options_form %}
<form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right">
{% csrf_token %}
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" />
<button class="btn btn-primary js-tooltip" title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button>
</form>
{% endif %}
{% if delete_form %}
<form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right">
{% csrf_token %}
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" />
<button class="btn btn-danger js-tooltip" title="Make a DELETE request on the {{ name }} resource">DELETE</button>
</form>
{% endif %}
<div class="content-main">
<div class="page-header"><h1>{{ name }}</h1></div>
{{ description }}
<div class="request-info" style="clear: both" >
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
</div>
<div class="response-info">
<pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
{% for key, val in response.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>
{% endfor %}
{# Original line below had content|urlize_quoted_links; for AWX disable automatic URL creation here. #}
</div>{{ content }}</pre>{% endautoescape %}
</div>
</div>
{% if response.status_code != 403 %}
{% if post_form or raw_data_post_form %}
<div {% if post_form %}class="tabbable"{% endif %}>
{% if post_form %}
<ul class="nav nav-tabs form-switcher">
<li><a name='html-tab' href="#object-form" data-toggle="tab">HTML form</a></li>
<li><a name='raw-tab' href="#generic-content-form" data-toggle="tab">Raw data</a></li>
</ul>
{% endif %}
<div class="well tab-content">
{% if post_form %}
<div class="tab-pane" id="object-form">
{% with form=post_form %}
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal">
<fieldset>
{{ post_form }}
<div class="form-actions">
<button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
</div>
</fieldset>
</form>
{% endwith %}
</div>
{% endif %}
<div {% if post_form %}class="tab-pane"{% endif %} id="generic-content-form">
{% with form=raw_data_post_form %}
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
<fieldset>
{% include "rest_framework/form.html" %}
<div class="form-actions">
<button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
</div>
</fieldset>
</form>
{% endwith %}
</div>
</div>
</div>
{% endif %}
{% if put_form or raw_data_put_form or raw_data_patch_form %}
<div {% if put_form %}class="tabbable"{% endif %}>
{% if put_form %}
<ul class="nav nav-tabs form-switcher">
<li><a name='html-tab' href="#object-form" data-toggle="tab">HTML form</a></li>
<li><a name='raw-tab' href="#generic-content-form" data-toggle="tab">Raw data</a></li>
</ul>
{% endif %}
<div class="well tab-content">
{% if put_form %}
<div class="tab-pane" id="object-form">
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal">
<fieldset>
{{ put_form }}
<div class="form-actions">
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
</div>
</fieldset>
</form>
</div>
{% endif %}
<div {% if put_form %}class="tab-pane"{% endif %} id="generic-content-form">
{% with form=raw_data_put_or_patch_form %}
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
<fieldset>
{% include "rest_framework/form.html" %}
<div class="form-actions">
{% if raw_data_put_form %}
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
{% endif %}
{% if raw_data_patch_form %}
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PATCH" title="Make a PATCH request on the {{ name }} resource">PATCH</button>
{% endif %}
</div>
</fieldset>
</form>
{% endwith %}
</div>
</div>
</div>
{% endif %}
{% endif %}
</div>
<!-- END content-main -->
</div>
<!-- END Content -->
<div id="push"></div>
</div>
</div><!-- ./wrapper -->
{% block footer %}
<!--<div id="footer">
<a class="powered-by" href='http://django-rest-framework.org'>Django REST framework</a>
</div>-->
{% endblock %}
{% block script %}
<script src="{% static "rest_framework/js/jquery-1.8.1-min.js" %}"></script>
<script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script>
<script src="{% static "rest_framework/js/prettify-min.js" %}"></script>
<script src="{% static "rest_framework/js/default.js" %}"></script>
{% endblock %}
</body>
</html>