mirror of
https://github.com/ansible/awx.git
synced 2026-03-13 15:09:32 -02:30
Merge pull request #1124 from ryanpetrello/swagger
add support for building swagger/OpenAPI JSON
This commit is contained in:
6
Makefile
6
Makefile
@@ -363,6 +363,12 @@ pyflakes: reports
|
||||
pylint: reports
|
||||
@(set -o pipefail && $@ | reports/$@.report)
|
||||
|
||||
swagger: reports
|
||||
@if [ "$(VENV_BASE)" ]; then \
|
||||
. $(VENV_BASE)/awx/bin/activate; \
|
||||
fi; \
|
||||
(set -o pipefail && py.test awx/conf/tests/functional awx/main/tests/functional/api awx/main/tests/docs --release=$(VERSION_TARGET) | tee reports/$@.report)
|
||||
|
||||
check: flake8 pep8 # pyflakes pylint
|
||||
|
||||
TEST_DIRS ?= awx/main/tests/unit awx/main/tests/functional awx/conf/tests awx/sso/tests
|
||||
|
||||
@@ -28,6 +28,7 @@ from rest_framework import status
|
||||
from rest_framework import views
|
||||
|
||||
# AWX
|
||||
from awx.api.swagger import AutoSchema
|
||||
from awx.api.filters import FieldLookupBackend
|
||||
from awx.main.models import * # noqa
|
||||
from awx.main.access import access_registry
|
||||
@@ -93,6 +94,7 @@ def get_view_description(cls, request, html=False):
|
||||
|
||||
class APIView(views.APIView):
|
||||
|
||||
schema = AutoSchema()
|
||||
versioning_class = URLPathVersioning
|
||||
|
||||
def initialize_request(self, request, *args, **kwargs):
|
||||
@@ -176,7 +178,7 @@ class APIView(views.APIView):
|
||||
and in the browsable API.
|
||||
"""
|
||||
func = self.settings.VIEW_DESCRIPTION_FUNCTION
|
||||
return func(self.__class__, self._request, html)
|
||||
return func(self.__class__, getattr(self, '_request', None), html)
|
||||
|
||||
def get_description_context(self):
|
||||
return {
|
||||
@@ -197,6 +199,7 @@ class APIView(views.APIView):
|
||||
'new_in_330': getattr(self, 'new_in_330', False),
|
||||
'new_in_api_v2': getattr(self, 'new_in_api_v2', False),
|
||||
'deprecated': getattr(self, 'deprecated', False),
|
||||
'swagger_method': getattr(self.request, 'swagger_method', None),
|
||||
}
|
||||
|
||||
def get_description(self, request, html=False):
|
||||
@@ -214,7 +217,7 @@ class APIView(views.APIView):
|
||||
context['deprecated'] = True
|
||||
|
||||
description = render_to_string(template_list, context)
|
||||
if context.get('deprecated'):
|
||||
if context.get('deprecated') and context.get('swagger_method') is None:
|
||||
# render deprecation messages at the very top
|
||||
description = '\n'.join([render_to_string('api/_deprecated.md', context), description])
|
||||
return description
|
||||
|
||||
103
awx/api/swagger.py
Normal file
103
awx/api/swagger.py
Normal file
@@ -0,0 +1,103 @@
|
||||
import json
|
||||
import warnings
|
||||
|
||||
from coreapi.document import Object, Link
|
||||
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.renderers import CoreJSONRenderer
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.schemas import SchemaGenerator, AutoSchema as DRFAuthSchema
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from rest_framework_swagger import renderers
|
||||
|
||||
|
||||
class AutoSchema(DRFAuthSchema):
|
||||
|
||||
def get_link(self, path, method, base_url):
|
||||
link = super(AutoSchema, self).get_link(path, method, base_url)
|
||||
try:
|
||||
serializer = self.view.get_serializer()
|
||||
except Exception:
|
||||
serializer = None
|
||||
warnings.warn('{}.get_serializer() raised an exception during '
|
||||
'schema generation. Serializer fields will not be '
|
||||
'generated for {} {}.'
|
||||
.format(self.view.__class__.__name__, method, path))
|
||||
|
||||
link.__dict__['deprecated'] = getattr(self.view, 'deprecated', False)
|
||||
|
||||
# auto-generate a topic/tag for the serializer based on its model
|
||||
if hasattr(self.view, 'swagger_topic'):
|
||||
link.__dict__['topic'] = str(self.view.swagger_topic).title()
|
||||
elif serializer and hasattr(serializer, 'Meta'):
|
||||
link.__dict__['topic'] = str(
|
||||
serializer.Meta.model._meta.verbose_name_plural
|
||||
).title()
|
||||
elif hasattr(self.view, 'model'):
|
||||
link.__dict__['topic'] = str(self.view.model._meta.verbose_name_plural).title()
|
||||
else:
|
||||
warnings.warn('Could not determine a Swagger tag for path {}'.format(path))
|
||||
return link
|
||||
|
||||
def get_description(self, path, method):
|
||||
self.view._request = self.view.request
|
||||
setattr(self.view.request, 'swagger_method', method)
|
||||
description = super(AutoSchema, self).get_description(path, method)
|
||||
return description
|
||||
|
||||
|
||||
class SwaggerSchemaView(APIView):
|
||||
_ignore_model_permissions = True
|
||||
exclude_from_schema = True
|
||||
permission_classes = [AllowAny]
|
||||
renderer_classes = [
|
||||
CoreJSONRenderer,
|
||||
renderers.OpenAPIRenderer,
|
||||
renderers.SwaggerUIRenderer
|
||||
]
|
||||
|
||||
def get(self, request):
|
||||
generator = SchemaGenerator(
|
||||
title='Ansible Tower API',
|
||||
patterns=None,
|
||||
urlconf=None
|
||||
)
|
||||
schema = generator.get_schema(request=request)
|
||||
# python core-api doesn't support the deprecation yet, so track it
|
||||
# ourselves and return it in a response header
|
||||
_deprecated = []
|
||||
|
||||
# By default, DRF OpenAPI serialization places all endpoints in
|
||||
# a single node based on their root path (/api). Instead, we want to
|
||||
# group them by topic/tag so that they're categorized in the rendered
|
||||
# output
|
||||
document = schema._data.pop('api')
|
||||
for path, node in document.items():
|
||||
if isinstance(node, Object):
|
||||
for action in node.values():
|
||||
topic = getattr(action, 'topic', None)
|
||||
if topic:
|
||||
schema._data.setdefault(topic, Object())
|
||||
schema._data[topic]._data[path] = node
|
||||
|
||||
if isinstance(action, Object):
|
||||
for link in action.links.values():
|
||||
if link.deprecated:
|
||||
_deprecated.append(link.url)
|
||||
elif isinstance(node, Link):
|
||||
topic = getattr(node, 'topic', None)
|
||||
if topic:
|
||||
schema._data.setdefault(topic, Object())
|
||||
schema._data[topic]._data[path] = node
|
||||
|
||||
if not schema:
|
||||
raise exceptions.ValidationError(
|
||||
'The schema generator did not return a schema Document'
|
||||
)
|
||||
|
||||
return Response(
|
||||
schema,
|
||||
headers={'X-Deprecated-Paths': json.dumps(_deprecated)}
|
||||
)
|
||||
3
awx/api/templates/api/ad_hoc_command_relaunch.md
Normal file
3
awx/api/templates/api/ad_hoc_command_relaunch.md
Normal file
@@ -0,0 +1,3 @@
|
||||
Relaunch an Ad Hoc Command:
|
||||
|
||||
Make a POST request to this resource to launch a job. If any passwords or variables are required then they should be passed in via POST data. In order to determine what values are required in order to launch a job based on this job template you may make a GET request to this endpoint.
|
||||
@@ -1,4 +1,5 @@
|
||||
Site configuration settings and general information.
|
||||
{% ifmeth GET %}
|
||||
# Site configuration settings and general information
|
||||
|
||||
Make a GET request to this resource to retrieve the configuration containing
|
||||
the following fields (some fields may not be visible to all users):
|
||||
@@ -11,6 +12,10 @@ the following fields (some fields may not be visible to all users):
|
||||
* `license_info`: Information about the current license.
|
||||
* `version`: Version of Ansible Tower package installed.
|
||||
* `eula`: The current End-User License Agreement
|
||||
{% endifmeth %}
|
||||
|
||||
{% ifmeth POST %}
|
||||
# Install or update an existing license
|
||||
|
||||
(_New in Ansible Tower 2.0.0_) Make a POST request to this resource as a super
|
||||
user to install or update the existing license. The license data itself can
|
||||
@@ -18,3 +23,11 @@ be POSTed as a normal json data structure.
|
||||
|
||||
(_New in Ansible Tower 2.1.1_) The POST must include a `eula_accepted` boolean
|
||||
element indicating acceptance of the End-User License Agreement.
|
||||
{% endifmeth %}
|
||||
|
||||
{% ifmeth DELETE %}
|
||||
# Delete an existing license
|
||||
|
||||
(_New in Ansible Tower 2.0.0_) Make a DELETE request to this resource as a super
|
||||
user to delete the existing license
|
||||
{% endifmeth %}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
{% ifmeth POST %}
|
||||
# Generate an Auth Token
|
||||
Make a POST request to this resource with `username` and `password` fields to
|
||||
obtain an authentication token to use for subsequent requests.
|
||||
|
||||
@@ -32,6 +34,10 @@ agent that originally obtained it.
|
||||
Each request that uses the token for authentication will refresh its expiration
|
||||
timestamp and keep it from expiring. A token only expires when it is not used
|
||||
for the configured timeout interval (default 1800 seconds).
|
||||
{% endifmeth %}
|
||||
|
||||
A DELETE request with the token set will cause the token to be invalidated and
|
||||
no further requests can be made with it.
|
||||
{% ifmeth DELETE %}
|
||||
# Delete an Auth Token
|
||||
A DELETE request with the token header set will cause the token to be
|
||||
invalidated and no further requests can be made with it.
|
||||
{% endifmeth %}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
{% ifmeth GET %}
|
||||
# Retrieve {{ model_verbose_name|title }} Variable Data:
|
||||
|
||||
Make a GET request to this resource to retrieve all variables defined for this
|
||||
Make a GET request to this resource to retrieve all variables defined for a
|
||||
{{ model_verbose_name }}.
|
||||
{% endifmeth %}
|
||||
|
||||
{% ifmeth PUT PATCH %}
|
||||
# Update {{ model_verbose_name|title }} Variable Data:
|
||||
|
||||
Make a PUT request to this resource to update variables defined for this
|
||||
Make a PUT or PATCH request to this resource to update variables defined for a
|
||||
{{ model_verbose_name }}.
|
||||
{% endifmeth %}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# View Statistics for Job Runs
|
||||
|
||||
Make a GET request to this resource to retrieve aggregate statistics about job runs suitable for graphing.
|
||||
|
||||
## Parmeters and Filtering
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# List All {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
|
||||
# List All {{ model_verbose_name_plural|title }} for {{ parent_model_verbose_name|title|anora }}:
|
||||
|
||||
Make a GET request to this resource to retrieve a list of all
|
||||
{{ model_verbose_name_plural }} directly or indirectly belonging to this
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# List Potential Child Groups for this {{ parent_model_verbose_name|title }}:
|
||||
# List Potential Child Groups for {{ parent_model_verbose_name|title|anora }}:
|
||||
|
||||
Make a GET request to this resource to retrieve a list of
|
||||
{{ model_verbose_name_plural }} available to be added as children of the
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# List All {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
|
||||
# List All {{ model_verbose_name_plural|title }} for {{ parent_model_verbose_name|title|anora }}:
|
||||
|
||||
Make a GET request to this resource to retrieve a list of all
|
||||
{{ model_verbose_name_plural }} of which the selected
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# List Fact Scans for a Host Specific Host Scan
|
||||
|
||||
Make a GET request to this resource to retrieve system tracking data for a particular scan
|
||||
|
||||
You may filter by datetime:
|
||||
@@ -8,4 +10,4 @@ and module
|
||||
|
||||
`?datetime=2015-06-01&module=ansible`
|
||||
|
||||
{% include "api/_new_in_awx.md" %}
|
||||
{% include "api/_new_in_awx.md" %}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# List Fact Scans for a Host by Module and Date
|
||||
|
||||
Make a GET request to this resource to retrieve system tracking scans by module and date/time
|
||||
|
||||
You may filter scan runs using the `from` and `to` properties:
|
||||
@@ -8,4 +10,4 @@ You may also filter by module
|
||||
|
||||
`?module=packages`
|
||||
|
||||
{% include "api/_new_in_awx.md" %}
|
||||
{% include "api/_new_in_awx.md" %}
|
||||
|
||||
1
awx/api/templates/api/host_insights.md
Normal file
1
awx/api/templates/api/host_insights.md
Normal file
@@ -0,0 +1 @@
|
||||
# List Red Hat Insights for a Host
|
||||
@@ -1,7 +1,9 @@
|
||||
# List Root {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
|
||||
{% ifmeth GET %}
|
||||
# List Root {{ model_verbose_name_plural|title }} for {{ parent_model_verbose_name|title|anora }}:
|
||||
|
||||
Make a GET request to this resource to retrieve a list of root (top-level)
|
||||
{{ model_verbose_name_plural }} associated with this
|
||||
{{ parent_model_verbose_name }}.
|
||||
|
||||
{% include "api/_list_common.md" %}
|
||||
{% endifmeth %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Group Tree for this {{ model_verbose_name|title }}:
|
||||
# Group Tree for {{ model_verbose_name|title|anora }}:
|
||||
|
||||
Make a GET request to this resource to retrieve a hierarchical view of groups
|
||||
associated with the selected {{ model_verbose_name }}.
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
# Cancel Job
|
||||
{% ifmeth GET %}
|
||||
# Determine if a Job can be cancelled
|
||||
|
||||
Make a GET request to this resource to determine if the job can be cancelled.
|
||||
The response will include the following field:
|
||||
|
||||
* `can_cancel`: Indicates whether this job can be canceled (boolean, read-only)
|
||||
{% endifmeth %}
|
||||
|
||||
{% ifmeth POST %}
|
||||
# Cancel a Job
|
||||
Make a POST request to this resource to cancel a pending or running job. The
|
||||
response status code will be 202 if successful, or 405 if the job cannot be
|
||||
canceled.
|
||||
{% endifmeth %}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
Relaunch a job:
|
||||
Relaunch a Job:
|
||||
|
||||
Make a POST request to this resource to launch a job. If any passwords or variables are required then they should be passed in via POST data. In order to determine what values are required in order to launch a job based on this job template you may make a GET request to this endpoint.
|
||||
Make a POST request to this resource to launch a job. If any passwords or variables are required then they should be passed in via POST data. In order to determine what values are required in order to launch a job based on this job template you may make a GET request to this endpoint.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Start Job
|
||||
{% ifmeth GET %}
|
||||
# Determine if a Job can be started
|
||||
|
||||
Make a GET request to this resource to determine if the job can be started and
|
||||
whether any passwords are required to start the job. The response will include
|
||||
@@ -7,10 +8,14 @@ the following fields:
|
||||
* `can_start`: Flag indicating if this job can be started (boolean, read-only)
|
||||
* `passwords_needed_to_start`: Password names required to start the job (array,
|
||||
read-only)
|
||||
{% endifmeth %}
|
||||
|
||||
{% ifmeth POST %}
|
||||
# Start a Job
|
||||
Make a POST request to this resource to start the job. If any passwords are
|
||||
required, they must be passed via POST data.
|
||||
|
||||
If successful, the response status code will be 202. If any required passwords
|
||||
are not provided, a 400 status code will be returned. If the job cannot be
|
||||
started, a 405 status code will be returned.
|
||||
{% endifmeth %}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% ifmeth GET %}
|
||||
# List {{ model_verbose_name_plural|title }}:
|
||||
|
||||
Make a GET request to this resource to retrieve the list of
|
||||
@@ -6,3 +7,4 @@ Make a GET request to this resource to retrieve the list of
|
||||
{% include "api/_list_common.md" %}
|
||||
|
||||
{% include "api/_new_in_awx.md" %}
|
||||
{% endifmeth %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% include "api/list_api_view.md" %}
|
||||
|
||||
# Create {{ model_verbose_name_plural|title }}:
|
||||
# Create {{ model_verbose_name|title|anora }}:
|
||||
|
||||
Make a POST request to this resource with the following {{ model_verbose_name }}
|
||||
fields to create a new {{ model_verbose_name }}:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Retrieve {{ model_verbose_name|title }} Playbooks:
|
||||
|
||||
Make GET request to this resource to retrieve a list of playbooks available
|
||||
for this {{ model_verbose_name }}.
|
||||
for {{ model_verbose_name|anora }}.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
### Note: starting from api v2, this resource object can be accessed via its named URL.
|
||||
{% endif %}
|
||||
|
||||
# Retrieve {{ model_verbose_name|title }}:
|
||||
# Retrieve {{ model_verbose_name|title|anora }}:
|
||||
|
||||
Make GET request to this resource to retrieve a single {{ model_verbose_name }}
|
||||
record containing the following fields:
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
### Note: starting from api v2, this resource object can be accessed via its named URL.
|
||||
{% endif %}
|
||||
|
||||
# Retrieve {{ model_verbose_name|title }}:
|
||||
{% ifmeth GET %}
|
||||
# Retrieve {{ model_verbose_name|title|anora }}:
|
||||
|
||||
Make GET request to this resource to retrieve a single {{ model_verbose_name }}
|
||||
record containing the following fields:
|
||||
|
||||
{% include "api/_result_fields_common.md" %}
|
||||
{% endifmeth %}
|
||||
|
||||
# Delete {{ model_verbose_name|title }}:
|
||||
{% ifmeth DELETE %}
|
||||
# Delete {{ model_verbose_name|title|anora }}:
|
||||
|
||||
Make a DELETE request to this resource to delete this {{ model_verbose_name }}.
|
||||
{% endifmeth %}
|
||||
|
||||
{% include "api/_new_in_awx.md" %}
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
### Note: starting from api v2, this resource object can be accessed via its named URL.
|
||||
{% endif %}
|
||||
|
||||
# Retrieve {{ model_verbose_name|title }}:
|
||||
{% ifmeth GET %}
|
||||
# Retrieve {{ model_verbose_name|title|anora }}:
|
||||
|
||||
Make GET request to this resource to retrieve a single {{ model_verbose_name }}
|
||||
record containing the following fields:
|
||||
|
||||
{% include "api/_result_fields_common.md" %}
|
||||
{% endifmeth %}
|
||||
|
||||
# Update {{ model_verbose_name|title }}:
|
||||
{% ifmeth PUT PATCH %}
|
||||
# Update {{ model_verbose_name|title|anora }}:
|
||||
|
||||
Make a PUT or PATCH request to this resource to update this
|
||||
{{ model_verbose_name }}. The following fields may be modified:
|
||||
@@ -17,9 +20,14 @@ Make a PUT or PATCH request to this resource to update this
|
||||
{% with write_only=1 %}
|
||||
{% include "api/_result_fields_common.md" with serializer_fields=serializer_update_fields %}
|
||||
{% endwith %}
|
||||
{% endifmeth %}
|
||||
|
||||
{% ifmeth PUT %}
|
||||
For a PUT request, include **all** fields in the request.
|
||||
{% endifmeth %}
|
||||
|
||||
{% ifmeth PATCH %}
|
||||
For a PATCH request, include only the fields that are being modified.
|
||||
{% endifmeth %}
|
||||
|
||||
{% include "api/_new_in_awx.md" %}
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
### Note: starting from api v2, this resource object can be accessed via its named URL.
|
||||
{% endif %}
|
||||
|
||||
# Retrieve {{ model_verbose_name|title }}:
|
||||
{% ifmeth GET %}
|
||||
# Retrieve {{ model_verbose_name|title|anora }}:
|
||||
|
||||
Make GET request to this resource to retrieve a single {{ model_verbose_name }}
|
||||
record containing the following fields:
|
||||
|
||||
{% include "api/_result_fields_common.md" %}
|
||||
{% endifmeth %}
|
||||
|
||||
# Update {{ model_verbose_name|title }}:
|
||||
{% ifmeth PUT PATCH %}
|
||||
# Update {{ model_verbose_name|title|anora }}:
|
||||
|
||||
Make a PUT or PATCH request to this resource to update this
|
||||
{{ model_verbose_name }}. The following fields may be modified:
|
||||
@@ -17,13 +20,20 @@ Make a PUT or PATCH request to this resource to update this
|
||||
{% with write_only=1 %}
|
||||
{% include "api/_result_fields_common.md" with serializer_fields=serializer_update_fields %}
|
||||
{% endwith %}
|
||||
{% endifmeth %}
|
||||
|
||||
{% ifmeth PUT %}
|
||||
For a PUT request, include **all** fields in the request.
|
||||
{% endifmeth %}
|
||||
|
||||
{% ifmeth PATCH %}
|
||||
For a PATCH request, include only the fields that are being modified.
|
||||
{% endifmeth %}
|
||||
|
||||
# Delete {{ model_verbose_name|title }}:
|
||||
{% ifmeth DELETE %}
|
||||
# Delete {{ model_verbose_name|title|anora }}:
|
||||
|
||||
Make a DELETE request to this resource to delete this {{ model_verbose_name }}.
|
||||
{% endifmeth %}
|
||||
|
||||
{% include "api/_new_in_awx.md" %}
|
||||
|
||||
1
awx/api/templates/api/setting_logging_test.md
Normal file
1
awx/api/templates/api/setting_logging_test.md
Normal file
@@ -0,0 +1 @@
|
||||
# Test Logging Configuration
|
||||
@@ -1,9 +1,10 @@
|
||||
# List {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
|
||||
{% ifmeth GET %}
|
||||
# List {{ model_verbose_name_plural|title }} for {{ parent_model_verbose_name|title|anora }}:
|
||||
|
||||
Make a GET request to this resource to retrieve a list of
|
||||
{{ model_verbose_name_plural }} associated with the selected
|
||||
{{ parent_model_verbose_name }}.
|
||||
|
||||
{% include "api/_list_common.md" %}
|
||||
|
||||
{% include "api/_new_in_awx.md" %}
|
||||
{% endifmeth %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% include "api/sub_list_api_view.md" %}
|
||||
|
||||
# Create {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
|
||||
# Create {{ model_verbose_name|title|anora }} for {{ parent_model_verbose_name|title|anora }}:
|
||||
|
||||
Make a POST request to this resource with the following {{ model_verbose_name }}
|
||||
fields to create a new {{ model_verbose_name }} associated with this
|
||||
@@ -25,7 +25,7 @@ delete the associated {{ model_verbose_name }}.
|
||||
}
|
||||
|
||||
{% else %}
|
||||
# Add {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
|
||||
# Add {{ model_verbose_name_plural|title }} for {{ parent_model_verbose_name|title|anora }}:
|
||||
|
||||
Make a POST request to this resource with only an `id` field to associate an
|
||||
existing {{ model_verbose_name }} with this {{ parent_model_verbose_name }}.
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
# List Roles for this Team:
|
||||
# List Roles for a Team:
|
||||
|
||||
{% ifmeth GET %}
|
||||
Make a GET request to this resource to retrieve a list of roles associated with the selected team.
|
||||
|
||||
{% include "api/_list_common.md" %}
|
||||
{% endifmeth %}
|
||||
|
||||
{% ifmeth POST %}
|
||||
# Associate Roles with this Team:
|
||||
|
||||
Make a POST request to this resource to add or remove a role from this team. The following fields may be modified:
|
||||
|
||||
* `id`: The Role ID to add to the team. (int, required)
|
||||
* `disassociate`: Provide if you want to remove the role. (any value, optional)
|
||||
{% endifmeth %}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Retrieve Information about the current User
|
||||
|
||||
Make a GET request to retrieve user information about the current user.
|
||||
|
||||
One result should be returned containing the following fields:
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
# List Roles for this User:
|
||||
# List Roles for a User:
|
||||
|
||||
{% ifmeth GET %}
|
||||
Make a GET request to this resource to retrieve a list of roles associated with the selected user.
|
||||
|
||||
{% include "api/_list_common.md" %}
|
||||
{% endifmeth %}
|
||||
|
||||
{% ifmeth POST %}
|
||||
# Associate Roles with this User:
|
||||
|
||||
Make a POST request to this resource to add or remove a role from this user. The following fields may be modified:
|
||||
|
||||
* `id`: The Role ID to add to the user. (int, required)
|
||||
* `disassociate`: Provide if you want to remove the role. (any value, optional)
|
||||
{% endifmeth %}
|
||||
|
||||
44
awx/api/templates/swagger/config.yml
Normal file
44
awx/api/templates/swagger/config.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
# Add categories here for generated Swagger docs; order will be respected
|
||||
# in the generated document.
|
||||
categories:
|
||||
- name: Versioning
|
||||
- name: Authentication
|
||||
- name: Instances
|
||||
- name: Instance Groups
|
||||
- name: System Configuration
|
||||
- name: Settings
|
||||
- name: Dashboard
|
||||
- name: Organizations
|
||||
- name: Users
|
||||
- name: Projects
|
||||
- name: Project Updates
|
||||
- name: Teams
|
||||
- name: Credentials
|
||||
- name: Credential Types
|
||||
- name: Inventories
|
||||
- name: Custom Inventory Scripts
|
||||
- name: Inventory Sources
|
||||
- name: Inventory Updates
|
||||
- name: Groups
|
||||
- name: Hosts
|
||||
- name: Job Templates
|
||||
- name: Jobs
|
||||
- name: Job Events
|
||||
- name: Job Host Summaries
|
||||
- name: Ad Hoc Commands
|
||||
- name: Ad Hoc Command Events
|
||||
- name: System Job Templates
|
||||
- name: System Jobs
|
||||
- name: Schedules
|
||||
- name: Roles
|
||||
- name: Notification Templates
|
||||
- name: Notifications
|
||||
- name: Labels
|
||||
- name: Unified Job Templates
|
||||
- name: Unified Jobs
|
||||
- name: Activity Streams
|
||||
- name: Workflow Job Templates
|
||||
- name: Workflow Jobs
|
||||
- name: Workflow Job Template Nodes
|
||||
- name: Workflow Job Nodes
|
||||
1
awx/api/templates/swagger/description.md
Normal file
1
awx/api/templates/swagger/description.md
Normal file
@@ -0,0 +1 @@
|
||||
The Ansible Tower API Reference Manual provides in-depth documentation for Tower's REST API, including examples on how to integrate with it.
|
||||
@@ -2,7 +2,9 @@
|
||||
# All Rights Reserved.
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
from awx.api.swagger import SwaggerSchemaView
|
||||
|
||||
from awx.api.views import (
|
||||
ApiRootView,
|
||||
@@ -123,5 +125,9 @@ app_name = 'api'
|
||||
urlpatterns = [
|
||||
url(r'^$', ApiRootView.as_view(), name='api_root_view'),
|
||||
url(r'^(?P<version>(v2))/', include(v2_urls)),
|
||||
url(r'^(?P<version>(v1|v2))/', include(v1_urls))
|
||||
url(r'^(?P<version>(v1|v2))/', include(v1_urls)),
|
||||
]
|
||||
if settings.SETTINGS_MODULE == 'awx.settings.development':
|
||||
urlpatterns += [
|
||||
url(r'^swagger/$', SwaggerSchemaView.as_view(), name='swagger_view'),
|
||||
]
|
||||
|
||||
@@ -189,9 +189,10 @@ class ApiRootView(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
view_name = _('REST API')
|
||||
versioning_class = None
|
||||
swagger_topic = 'Versioning'
|
||||
|
||||
def get(self, request, format=None):
|
||||
''' list supported API versions '''
|
||||
''' List supported API versions '''
|
||||
|
||||
v1 = reverse('api:api_v1_root_view', kwargs={'version': 'v1'})
|
||||
v2 = reverse('api:api_v2_root_view', kwargs={'version': 'v2'})
|
||||
@@ -210,9 +211,10 @@ class ApiVersionRootView(APIView):
|
||||
|
||||
authentication_classes = []
|
||||
permission_classes = (AllowAny,)
|
||||
swagger_topic = 'Versioning'
|
||||
|
||||
def get(self, request, format=None):
|
||||
''' list top level resources '''
|
||||
''' List top level resources '''
|
||||
data = OrderedDict()
|
||||
data['authtoken'] = reverse('api:auth_token_view', request=request)
|
||||
data['ping'] = reverse('api:api_v1_ping_view', request=request)
|
||||
@@ -275,9 +277,10 @@ class ApiV1PingView(APIView):
|
||||
authentication_classes = ()
|
||||
view_name = _('Ping')
|
||||
new_in_210 = True
|
||||
swagger_topic = 'System Configuration'
|
||||
|
||||
def get(self, request, format=None):
|
||||
"""Return some basic information about this instance.
|
||||
"""Return some basic information about this instance
|
||||
|
||||
Everything returned here should be considered public / insecure, as
|
||||
this requires no auth and is intended for use by the installer process.
|
||||
@@ -305,6 +308,7 @@ class ApiV1ConfigView(APIView):
|
||||
|
||||
permission_classes = (IsAuthenticated,)
|
||||
view_name = _('Configuration')
|
||||
swagger_topic = 'System Configuration'
|
||||
|
||||
def check_permissions(self, request):
|
||||
super(ApiV1ConfigView, self).check_permissions(request)
|
||||
@@ -312,7 +316,7 @@ class ApiV1ConfigView(APIView):
|
||||
self.permission_denied(request) # Raises PermissionDenied exception.
|
||||
|
||||
def get(self, request, format=None):
|
||||
'''Return various sitewide configuration settings.'''
|
||||
'''Return various sitewide configuration settings'''
|
||||
|
||||
if request.user.is_superuser or request.user.is_system_auditor:
|
||||
license_data = get_license(show_key=True)
|
||||
@@ -407,6 +411,7 @@ class DashboardView(APIView):
|
||||
|
||||
view_name = _("Dashboard")
|
||||
new_in_14 = True
|
||||
swagger_topic = 'Dashboard'
|
||||
|
||||
def get(self, request, format=None):
|
||||
''' Show Dashboard Details '''
|
||||
@@ -506,6 +511,7 @@ class DashboardJobsGraphView(APIView):
|
||||
|
||||
view_name = _("Dashboard Jobs Graphs")
|
||||
new_in_200 = True
|
||||
swagger_topic = 'Jobs'
|
||||
|
||||
def get(self, request, format=None):
|
||||
period = request.query_params.get('period', 'month')
|
||||
@@ -690,6 +696,8 @@ class SchedulePreview(GenericAPIView):
|
||||
|
||||
class ScheduleZoneInfo(APIView):
|
||||
|
||||
swagger_topic = 'System Configuration'
|
||||
|
||||
def get(self, request):
|
||||
from dateutil.zoneinfo import get_zonefile_instance
|
||||
return Response(sorted(get_zonefile_instance().zones.keys()))
|
||||
@@ -746,10 +754,12 @@ class ScheduleUnifiedJobsList(SubListAPIView):
|
||||
|
||||
|
||||
class AuthView(APIView):
|
||||
''' List enabled single-sign-on endpoints '''
|
||||
|
||||
authentication_classes = []
|
||||
permission_classes = (AllowAny,)
|
||||
new_in_240 = True
|
||||
swagger_topic = 'System Configuration'
|
||||
|
||||
def get(self, request):
|
||||
from rest_framework.reverse import reverse
|
||||
@@ -793,6 +803,7 @@ class AuthTokenView(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
serializer_class = AuthTokenSerializer
|
||||
model = AuthToken
|
||||
swagger_topic = 'Authentication'
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
serializer = self.serializer_class(*args, **kwargs)
|
||||
@@ -982,7 +993,7 @@ class OrganizationDetail(RetrieveUpdateDestroyAPIView):
|
||||
def get_serializer_context(self, *args, **kwargs):
|
||||
full_context = super(OrganizationDetail, self).get_serializer_context(*args, **kwargs)
|
||||
|
||||
if not hasattr(self, 'kwargs'):
|
||||
if not hasattr(self, 'kwargs') or 'pk' not in self.kwargs:
|
||||
return full_context
|
||||
org_id = int(self.kwargs['pk'])
|
||||
|
||||
@@ -2680,7 +2691,7 @@ class InventorySourceList(ListCreateAPIView):
|
||||
@property
|
||||
def allowed_methods(self):
|
||||
methods = super(InventorySourceList, self).allowed_methods
|
||||
if get_request_version(self.request) == 1:
|
||||
if get_request_version(getattr(self, 'request', None)) == 1:
|
||||
methods.remove('POST')
|
||||
return methods
|
||||
|
||||
@@ -3994,7 +4005,7 @@ class JobList(ListCreateAPIView):
|
||||
@property
|
||||
def allowed_methods(self):
|
||||
methods = super(JobList, self).allowed_methods
|
||||
if get_request_version(self.request) > 1:
|
||||
if get_request_version(getattr(self, 'request', None)) > 1:
|
||||
methods.remove('POST')
|
||||
return methods
|
||||
|
||||
@@ -4770,6 +4781,7 @@ class NotificationTemplateDetail(RetrieveUpdateDestroyAPIView):
|
||||
|
||||
|
||||
class NotificationTemplateTest(GenericAPIView):
|
||||
'''Test a Notification Template'''
|
||||
|
||||
view_name = _('Notification Template Test')
|
||||
model = NotificationTemplate
|
||||
|
||||
0
awx/main/templatetags/__init__.py
Normal file
0
awx/main/templatetags/__init__.py
Normal file
50
awx/main/templatetags/swagger.py
Normal file
50
awx/main/templatetags/swagger.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import re
|
||||
from django.utils.encoding import force_unicode
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
CONSONANT_SOUND = re.compile(r'''one(![ir])''', re.IGNORECASE|re.VERBOSE) # noqa
|
||||
VOWEL_SOUND = re.compile(r'''[aeio]|u([aeiou]|[^n][^aeiou]|ni[^dmnl]|nil[^l])|h(ier|onest|onou?r|ors\b|our(!i))|[fhlmnrsx]\b''', re.IGNORECASE|re.VERBOSE) # noqa
|
||||
|
||||
|
||||
@register.filter
|
||||
def anora(text):
|
||||
# https://pypi.python.org/pypi/anora
|
||||
# < 10 lines of BSD-3 code, not worth a dependency
|
||||
text = force_unicode(text)
|
||||
anora = 'an' if not CONSONANT_SOUND.match(text) and VOWEL_SOUND.match(text) else 'a'
|
||||
return anora + ' ' + text
|
||||
|
||||
|
||||
@register.tag(name='ifmeth')
|
||||
def ifmeth(parser, token):
|
||||
"""
|
||||
Used to mark template blocks for Swagger/OpenAPI output.
|
||||
If the specified method matches the *current* method in Swagger/OpenAPI
|
||||
generation, show the block. Otherwise, the block is omitted.
|
||||
|
||||
{% ifmeth GET %}
|
||||
Make a GET request to...
|
||||
{% endifmeth %}
|
||||
|
||||
{% ifmeth PUT PATCH %}
|
||||
Make a PUT or PATCH request to...
|
||||
{% endifmeth %}
|
||||
"""
|
||||
allowed_methods = [m.upper() for m in token.split_contents()[1:]]
|
||||
nodelist = parser.parse(('endifmeth',))
|
||||
parser.delete_first_token()
|
||||
return MethodFilterNode(allowed_methods, nodelist)
|
||||
|
||||
|
||||
class MethodFilterNode(template.Node):
|
||||
def __init__(self, allowed_methods, nodelist):
|
||||
self.allowed_methods = allowed_methods
|
||||
self.nodelist = nodelist
|
||||
|
||||
def render(self, context):
|
||||
swagger_method = context.get('swagger_method')
|
||||
if not swagger_method or swagger_method.upper() in self.allowed_methods:
|
||||
return self.nodelist.render(context)
|
||||
return ''
|
||||
0
awx/main/tests/docs/__init__.py
Normal file
0
awx/main/tests/docs/__init__.py
Normal file
13
awx/main/tests/docs/conftest.py
Normal file
13
awx/main/tests/docs/conftest.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from awx.main.tests.functional.conftest import * # noqa
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--release", action="store", help="a release version number, e.g., 3.3.0")
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
# This is called for every test. Only get/set command line arguments
|
||||
# if the argument is specified in the list of test "fixturenames".
|
||||
option_value = metafunc.config.option.release
|
||||
if 'release' in metafunc.fixturenames and option_value is not None:
|
||||
metafunc.parametrize("release", [option_value])
|
||||
185
awx/main/tests/docs/test_swagger_generation.py
Normal file
185
awx/main/tests/docs/test_swagger_generation.py
Normal file
@@ -0,0 +1,185 @@
|
||||
import json
|
||||
import yaml
|
||||
import os
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.utils.functional import Promise
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from coreapi.compat import force_bytes
|
||||
from openapi_codec.encode import generate_swagger_object
|
||||
import pytest
|
||||
|
||||
import awx
|
||||
from awx.api.versioning import drf_reverse
|
||||
|
||||
|
||||
config_dest = os.sep.join([
|
||||
os.path.realpath(os.path.dirname(awx.__file__)),
|
||||
'api', 'templates', 'swagger'
|
||||
])
|
||||
config_file = os.sep.join([config_dest, 'config.yml'])
|
||||
description_file = os.sep.join([config_dest, 'description.md'])
|
||||
|
||||
|
||||
class i18nEncoder(DjangoJSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, Promise):
|
||||
return force_text(obj)
|
||||
return super(i18nEncoder, self).default(obj)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestSwaggerGeneration():
|
||||
"""
|
||||
This class is used to generate a Swagger/OpenAPI document for the awx
|
||||
API. A _prepare fixture generates a JSON blob containing OpenAPI data,
|
||||
individual tests have the ability modify the payload.
|
||||
|
||||
Finally, the JSON content is written to a file, `swagger.json`, in the
|
||||
current working directory.
|
||||
|
||||
$ py.test test_swagger_generation.py --version 3.3.0
|
||||
|
||||
To customize the `info.description` in the generated OpenAPI document,
|
||||
modify the text in `awx.api.templates.swagger.description.md`
|
||||
"""
|
||||
JSON = {}
|
||||
|
||||
@pytest.fixture(autouse=True, scope='function')
|
||||
def _prepare(self, get, admin):
|
||||
if not self.__class__.JSON:
|
||||
url = drf_reverse('api:swagger_view') + '?format=openapi'
|
||||
response = get(url, user=admin)
|
||||
data = generate_swagger_object(response.data)
|
||||
if response.has_header('X-Deprecated-Paths'):
|
||||
data['deprecated_paths'] = json.loads(response['X-Deprecated-Paths'])
|
||||
data.update(response.accepted_renderer.get_customizations() or {})
|
||||
|
||||
data['host'] = None
|
||||
data['schemes'] = ['https']
|
||||
data['consumes'] = ['application/json']
|
||||
|
||||
# Inject a top-level description into the OpenAPI document
|
||||
if os.path.exists(description_file):
|
||||
with open(description_file, 'r') as f:
|
||||
data['info']['description'] = f.read()
|
||||
|
||||
# Write tags in the order we want them sorted
|
||||
if os.path.exists(config_file):
|
||||
with open(config_file, 'r') as f:
|
||||
config = yaml.load(f.read())
|
||||
for category in config.get('categories', []):
|
||||
tag = {'name': category['name']}
|
||||
if 'description' in category:
|
||||
tag['description'] = category['description']
|
||||
data.setdefault('tags', []).append(tag)
|
||||
|
||||
revised_paths = {}
|
||||
deprecated_paths = data.pop('deprecated_paths', [])
|
||||
for path, node in data['paths'].items():
|
||||
# change {version} in paths to the actual default API version (e.g., v2)
|
||||
revised_paths[path.replace(
|
||||
'{version}',
|
||||
settings.REST_FRAMEWORK['DEFAULT_VERSION']
|
||||
)] = node
|
||||
for method in node:
|
||||
if path in deprecated_paths:
|
||||
node[method]['deprecated'] = True
|
||||
if 'description' in node[method]:
|
||||
# Pop off the first line and use that as the summary
|
||||
lines = node[method]['description'].splitlines()
|
||||
node[method]['summary'] = lines.pop(0).strip('#:')
|
||||
node[method]['description'] = '\n'.join(lines)
|
||||
|
||||
# remove the required `version` parameter
|
||||
for param in node[method].get('parameters'):
|
||||
if param['in'] == 'path' and param['name'] == 'version':
|
||||
node[method]['parameters'].remove(param)
|
||||
data['paths'] = revised_paths
|
||||
self.__class__.JSON = data
|
||||
|
||||
def test_sanity(self, release):
|
||||
JSON = self.__class__.JSON
|
||||
JSON['info']['version'] = release
|
||||
|
||||
# Make some basic assertions about the rendered JSON so we can
|
||||
# be sure it doesn't break across DRF upgrades and view/serializer
|
||||
# changes.
|
||||
assert len(JSON['tags'])
|
||||
assert len(JSON['paths'])
|
||||
|
||||
# The number of API endpoints changes over time, but let's just check
|
||||
# for a reasonable number here; if this test starts failing, raise/lower the bounds
|
||||
paths = JSON['paths']
|
||||
assert 250 < len(paths) < 300
|
||||
assert paths['/api/'].keys() == ['get']
|
||||
assert paths['/api/v2/'].keys() == ['get']
|
||||
assert sorted(
|
||||
paths['/api/v2/credentials/'].keys()
|
||||
) == ['get', 'post']
|
||||
assert sorted(
|
||||
paths['/api/v2/credentials/{id}/'].keys()
|
||||
) == ['delete', 'get', 'patch', 'put']
|
||||
assert paths['/api/v2/settings/'].keys() == ['get']
|
||||
assert paths['/api/v2/settings/{category_slug}/'].keys() == [
|
||||
'get', 'put', 'patch', 'delete'
|
||||
]
|
||||
|
||||
# Test deprecated paths
|
||||
assert paths['/api/v2/jobs/{id}/extra_credentials/']['get']['deprecated'] is True
|
||||
|
||||
@pytest.mark.parametrize('path', [
|
||||
'/api/',
|
||||
'/api/v2/',
|
||||
'/api/v2/ping/',
|
||||
'/api/v2/config/',
|
||||
])
|
||||
def test_basic_paths(self, path, get, admin):
|
||||
# hit a couple important endpoints so we always have example data
|
||||
get(path, user=admin, expect=200)
|
||||
|
||||
def test_autogen_response_examples(self, swagger_autogen):
|
||||
for pattern, node in TestSwaggerGeneration.JSON['paths'].items():
|
||||
pattern = pattern.replace('{id}', '[0-9]+')
|
||||
pattern = pattern.replace('{category_slug}', '[a-zA-Z0-9\-]+')
|
||||
for path, result in swagger_autogen.items():
|
||||
if re.match('^{}$'.format(pattern), path):
|
||||
for key, value in result.items():
|
||||
method, status_code = key
|
||||
content_type, resp, request_data = value
|
||||
if method in node:
|
||||
status_code = str(status_code)
|
||||
node[method].setdefault('produces', []).append(content_type)
|
||||
if request_data and status_code.startswith('2'):
|
||||
# DRF builds a schema based on the serializer
|
||||
# fields. This is _pretty good_, but if we
|
||||
# have _actual_ JSON examples, those are even
|
||||
# better and we should use them instead
|
||||
for param in node[method].get('parameters'):
|
||||
if param['in'] == 'body':
|
||||
node[method]['parameters'].remove(param)
|
||||
node[method].setdefault('parameters', []).append({
|
||||
'name': 'data',
|
||||
'in': 'body',
|
||||
'schema': {'example': request_data},
|
||||
})
|
||||
|
||||
# Build response examples
|
||||
if resp:
|
||||
if content_type.startswith('text/html'):
|
||||
continue
|
||||
if content_type == 'application/json':
|
||||
resp = json.loads(resp)
|
||||
node[method]['responses'].setdefault(status_code, {}).setdefault(
|
||||
'examples', {}
|
||||
)[content_type] = resp
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
with open('swagger.json', 'w') as f:
|
||||
f.write(force_bytes(
|
||||
json.dumps(cls.JSON, cls=i18nEncoder, indent=5)
|
||||
))
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
# Python
|
||||
import pytest
|
||||
import mock
|
||||
@@ -47,6 +46,13 @@ from awx.main.models.notifications import (
|
||||
from awx.main.models.workflow import WorkflowJobTemplate
|
||||
from awx.main.models.ad_hoc_commands import AdHocCommand
|
||||
|
||||
__SWAGGER_REQUESTS__ = {}
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def swagger_autogen(requests=__SWAGGER_REQUESTS__):
|
||||
return requests
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_cache():
|
||||
@@ -547,6 +553,9 @@ def _request(verb):
|
||||
assert response.status_code == expect
|
||||
if hasattr(response, 'render'):
|
||||
response.render()
|
||||
__SWAGGER_REQUESTS__.setdefault(request.path, {})[
|
||||
(request.method.lower(), response.status_code)
|
||||
] = (response.get('Content-Type', None), response.content, kwargs.get('data'))
|
||||
return response
|
||||
return rf
|
||||
|
||||
|
||||
@@ -218,6 +218,7 @@ TEMPLATES = [
|
||||
('django.template.loaders.filesystem.Loader',
|
||||
'django.template.loaders.app_directories.Loader',),
|
||||
)],
|
||||
'builtins': ['awx.main.templatetags.swagger'],
|
||||
},
|
||||
'DIRS': [
|
||||
os.path.join(BASE_DIR, 'templates'),
|
||||
|
||||
@@ -101,6 +101,8 @@ if 'django_jenkins' in INSTALLED_APPS:
|
||||
PEP8_RCFILE = "setup.cfg"
|
||||
PYLINT_RCFILE = ".pylintrc"
|
||||
|
||||
INSTALLED_APPS += ('rest_framework_swagger',)
|
||||
|
||||
# Much faster than the default
|
||||
# https://docs.djangoproject.com/en/1.6/topics/auth/passwords/#how-django-stores-passwords
|
||||
PASSWORD_HASHERS = (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
django-debug-toolbar==1.5
|
||||
django-rest-swagger
|
||||
pprofile
|
||||
ipython==5.2.1
|
||||
unittest2
|
||||
|
||||
@@ -7,6 +7,7 @@ env:
|
||||
- AWX_BUILD_TARGET=test
|
||||
- AWX_BUILD_TARGET=ui-test-ci
|
||||
- AWX_BUILD_TARGET="flake8 jshint"
|
||||
- AWX_BUILD_TARGET="swagger"
|
||||
|
||||
branches:
|
||||
only:
|
||||
|
||||
Reference in New Issue
Block a user