diff --git a/awx/api/generics.py b/awx/api/generics.py
index 0a33b889cd..7c17799d11 100644
--- a/awx/api/generics.py
+++ b/awx/api/generics.py
@@ -119,39 +119,12 @@ class LoggedLogoutView(auth_views.LogoutView):
return ret
-def get_view_name(cls, suffix=None):
- '''
- Wrapper around REST framework get_view_name() to support get_name() method
- and view_name property on a view class.
- '''
- name = ''
- if hasattr(cls, 'get_name') and callable(cls.get_name):
- name = cls().get_name()
- elif hasattr(cls, 'view_name'):
- if callable(cls.view_name):
- name = cls.view_name()
- else:
- name = cls.view_name
- if name:
- return ('%s %s' % (name, suffix)) if suffix else name
- return views.get_view_name(cls, suffix=None)
+def get_view_description(view, html=False):
+ '''Wrapper around REST framework get_view_description() to continue
+ to support our historical div.
-
-def get_view_description(cls, request, html=False):
'''
- Wrapper around REST framework get_view_description() to support
- get_description() method and view_description property on a view class.
- '''
- if hasattr(cls, 'get_description') and callable(cls.get_description):
- desc = cls().get_description(request, html=html)
- cls = type(cls.__name__, (object,), {'__doc__': desc})
- elif hasattr(cls, 'view_description'):
- if callable(cls.view_description):
- view_desc = cls.view_description()
- else:
- view_desc = cls.view_description
- cls = type(cls.__name__, (object,), {'__doc__': view_desc})
- desc = views.get_view_description(cls, html=html)
+ desc = views.get_view_description(view, html=html)
if html:
desc = '
%s
' % desc
return mark_safe(desc)
@@ -264,14 +237,6 @@ class APIView(views.APIView):
# `curl https://user:pass@tower.example.org/api/v2/job_templates/N/launch/`
return 'Bearer realm=api authorization_url=/api/o/authorize/'
- def get_view_description(self, html=False):
- """
- Return some descriptive text for the view, as used in OPTIONS responses
- and in the browsable API.
- """
- func = self.settings.VIEW_DESCRIPTION_FUNCTION
- return func(self.__class__, getattr(self, '_request', None), html)
-
def get_description_context(self):
return {
'view': self,
@@ -280,8 +245,8 @@ class APIView(views.APIView):
'swagger_method': getattr(self.request, 'swagger_method', None),
}
- def get_description(self, request, html=False):
- self.request = request
+ @property
+ def description(self):
template_list = []
for klass in inspect.getmro(type(self)):
template_basename = camelcase_to_underscore(klass.__name__)
@@ -383,12 +348,14 @@ class GenericAPIView(generics.GenericAPIView, APIView):
'model_verbose_name_plural': smart_text(self.model._meta.verbose_name_plural),
})
serializer = self.get_serializer()
+ metadata = self.metadata_class()
+ metadata.request = self.request
for method, key in [
('GET', 'serializer_fields'),
('POST', 'serializer_create_fields'),
('PUT', 'serializer_update_fields')
]:
- d[key] = self.metadata_class().get_serializer_info(serializer, method=method)
+ d[key] = metadata.get_serializer_info(serializer, method=method)
d['settings'] = settings
return d
diff --git a/awx/api/swagger.py b/awx/api/swagger.py
index ae774e78f0..fd54928251 100644
--- a/awx/api/swagger.py
+++ b/awx/api/swagger.py
@@ -53,7 +53,6 @@ class AutoSchema(DRFAuthSchema):
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
diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py
index c6f51ac493..9f85854cdd 100644
--- a/awx/api/views/__init__.py
+++ b/awx/api/views/__init__.py
@@ -2,9 +2,9 @@
# All Rights Reserved.
# Python
-import cgi
import dateutil
import functools
+import html
import logging
import re
import requests
@@ -37,7 +37,7 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.renderers import JSONRenderer, StaticHTMLRenderer
from rest_framework.response import Response
from rest_framework.settings import api_settings
-from rest_framework.views import exception_handler
+from rest_framework.views import exception_handler, get_view_name
from rest_framework import status
# Django REST Framework YAML
@@ -69,7 +69,7 @@ from awx.api.generics import (
RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, SimpleListAPIView,
SubDetailAPIView, SubListAPIView, SubListAttachDetachAPIView,
SubListCreateAPIView, SubListCreateAttachDetachAPIView,
- SubListDestroyAPIView, get_view_name
+ SubListDestroyAPIView
)
from awx.api.versioning import reverse
from awx.conf.license import get_license
@@ -168,7 +168,7 @@ class DashboardView(APIView):
deprecated = True
- view_name = _("Dashboard")
+ name = _("Dashboard")
swagger_topic = 'Dashboard'
def get(self, request, format=None):
@@ -270,7 +270,7 @@ class DashboardView(APIView):
class DashboardJobsGraphView(APIView):
- view_name = _("Dashboard Jobs Graphs")
+ name = _("Dashboard Jobs Graphs")
swagger_topic = 'Jobs'
def get(self, request, format=None):
@@ -320,7 +320,7 @@ class DashboardJobsGraphView(APIView):
class InstanceList(ListAPIView):
- view_name = _("Instances")
+ name = _("Instances")
model = models.Instance
serializer_class = serializers.InstanceSerializer
search_fields = ('hostname',)
@@ -328,7 +328,7 @@ class InstanceList(ListAPIView):
class InstanceDetail(RetrieveUpdateAPIView):
- view_name = _("Instance Detail")
+ name = _("Instance Detail")
model = models.Instance
serializer_class = serializers.InstanceSerializer
@@ -345,7 +345,7 @@ class InstanceDetail(RetrieveUpdateAPIView):
class InstanceUnifiedJobsList(SubListAPIView):
- view_name = _("Instance Jobs")
+ name = _("Instance Jobs")
model = models.UnifiedJob
serializer_class = serializers.UnifiedJobListSerializer
parent_model = models.Instance
@@ -359,7 +359,7 @@ class InstanceUnifiedJobsList(SubListAPIView):
class InstanceInstanceGroupsList(InstanceGroupMembershipMixin, SubListCreateAttachDetachAPIView):
- view_name = _("Instance's Instance Groups")
+ name = _("Instance's Instance Groups")
model = models.InstanceGroup
serializer_class = serializers.InstanceGroupSerializer
parent_model = models.Instance
@@ -368,7 +368,7 @@ class InstanceInstanceGroupsList(InstanceGroupMembershipMixin, SubListCreateAtta
class InstanceGroupList(ListCreateAPIView):
- view_name = _("Instance Groups")
+ name = _("Instance Groups")
model = models.InstanceGroup
serializer_class = serializers.InstanceGroupSerializer
@@ -376,7 +376,7 @@ class InstanceGroupList(ListCreateAPIView):
class InstanceGroupDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
always_allow_superuser = False
- view_name = _("Instance Group Detail")
+ name = _("Instance Group Detail")
model = models.InstanceGroup
serializer_class = serializers.InstanceGroupSerializer
permission_classes = (InstanceGroupTowerPermission,)
@@ -392,7 +392,7 @@ class InstanceGroupDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAP
class InstanceGroupUnifiedJobsList(SubListAPIView):
- view_name = _("Instance Group Running Jobs")
+ name = _("Instance Group Running Jobs")
model = models.UnifiedJob
serializer_class = serializers.UnifiedJobListSerializer
parent_model = models.InstanceGroup
@@ -401,7 +401,7 @@ class InstanceGroupUnifiedJobsList(SubListAPIView):
class InstanceGroupInstanceList(InstanceGroupMembershipMixin, SubListAttachDetachAPIView):
- view_name = _("Instance Group's Instances")
+ name = _("Instance Group's Instances")
model = models.Instance
serializer_class = serializers.InstanceSerializer
parent_model = models.InstanceGroup
@@ -411,7 +411,7 @@ class InstanceGroupInstanceList(InstanceGroupMembershipMixin, SubListAttachDetac
class ScheduleList(ListCreateAPIView):
- view_name = _("Schedules")
+ name = _("Schedules")
model = models.Schedule
serializer_class = serializers.ScheduleSerializer
@@ -425,7 +425,7 @@ class ScheduleDetail(RetrieveUpdateDestroyAPIView):
class SchedulePreview(GenericAPIView):
model = models.Schedule
- view_name = _('Schedule Recurrence Rule Preview')
+ name = _('Schedule Recurrence Rule Preview')
serializer_class = serializers.SchedulePreviewSerializer
permission_classes = (IsAuthenticated,)
@@ -508,7 +508,7 @@ class ScheduleUnifiedJobsList(SubListAPIView):
serializer_class = serializers.UnifiedJobListSerializer
parent_model = models.Schedule
relationship = 'unifiedjob_set'
- view_name = _('Schedule Jobs List')
+ name = _('Schedule Jobs List')
class AuthView(APIView):
@@ -704,7 +704,7 @@ class ProjectTeamsList(ListAPIView):
class ProjectSchedulesList(SubListCreateAPIView):
- view_name = _("Project Schedules")
+ name = _("Project Schedules")
model = models.Schedule
serializer_class = serializers.ScheduleSerializer
@@ -715,7 +715,7 @@ class ProjectSchedulesList(SubListCreateAPIView):
class ProjectScmInventorySources(SubListAPIView):
- view_name = _("Project SCM Inventory Sources")
+ name = _("Project SCM Inventory Sources")
model = models.InventorySource
serializer_class = serializers.InventorySourceSerializer
parent_model = models.Project
@@ -818,7 +818,7 @@ class ProjectUpdateEventsList(SubListAPIView):
serializer_class = serializers.ProjectUpdateEventSerializer
parent_model = models.ProjectUpdate
relationship = 'project_update_events'
- view_name = _('Project Update Events List')
+ name = _('Project Update Events List')
search_fields = ('stdout',)
def finalize_response(self, request, response, *args, **kwargs):
@@ -832,7 +832,7 @@ class SystemJobEventsList(SubListAPIView):
serializer_class = serializers.SystemJobEventSerializer
parent_model = models.SystemJob
relationship = 'system_job_events'
- view_name = _('System Job Events List')
+ name = _('System Job Events List')
search_fields = ('stdout',)
def finalize_response(self, request, response, *args, **kwargs):
@@ -868,7 +868,7 @@ class ProjectUpdateNotificationsList(SubListAPIView):
class ProjectUpdateScmInventoryUpdates(SubListAPIView):
- view_name = _("Project Update SCM Inventory Updates")
+ name = _("Project Update SCM Inventory Updates")
model = models.InventoryUpdate
serializer_class = serializers.InventoryUpdateListSerializer
parent_model = models.ProjectUpdate
@@ -912,7 +912,7 @@ class UserMeList(ListAPIView):
model = models.User
serializer_class = serializers.UserSerializer
- view_name = _('Me')
+ name = _('Me')
def get_queryset(self):
return self.model.objects.filter(pk=self.request.user.pk)
@@ -920,7 +920,7 @@ class UserMeList(ListAPIView):
class OAuth2ApplicationList(ListCreateAPIView):
- view_name = _("OAuth 2 Applications")
+ name = _("OAuth 2 Applications")
model = models.OAuth2Application
serializer_class = serializers.OAuth2ApplicationSerializer
@@ -929,7 +929,7 @@ class OAuth2ApplicationList(ListCreateAPIView):
class OAuth2ApplicationDetail(RetrieveUpdateDestroyAPIView):
- view_name = _("OAuth 2 Application Detail")
+ name = _("OAuth 2 Application Detail")
model = models.OAuth2Application
serializer_class = serializers.OAuth2ApplicationSerializer
@@ -942,7 +942,7 @@ class OAuth2ApplicationDetail(RetrieveUpdateDestroyAPIView):
class ApplicationOAuth2TokenList(SubListCreateAPIView):
- view_name = _("OAuth 2 Application Tokens")
+ name = _("OAuth 2 Application Tokens")
model = models.OAuth2AccessToken
serializer_class = serializers.OAuth2TokenSerializer
@@ -964,7 +964,7 @@ class OAuth2ApplicationActivityStreamList(SubListAPIView):
class OAuth2TokenList(ListCreateAPIView):
- view_name = _("OAuth2 Tokens")
+ name = _("OAuth2 Tokens")
model = models.OAuth2AccessToken
serializer_class = serializers.OAuth2TokenSerializer
@@ -973,7 +973,7 @@ class OAuth2TokenList(ListCreateAPIView):
class OAuth2UserTokenList(SubListCreateAPIView):
- view_name = _("OAuth2 User Tokens")
+ name = _("OAuth2 User Tokens")
model = models.OAuth2AccessToken
serializer_class = serializers.OAuth2TokenSerializer
@@ -985,7 +985,7 @@ class OAuth2UserTokenList(SubListCreateAPIView):
class UserAuthorizedTokenList(SubListCreateAPIView):
- view_name = _("OAuth2 User Authorized Access Tokens")
+ name = _("OAuth2 User Authorized Access Tokens")
model = models.OAuth2AccessToken
serializer_class = serializers.UserAuthorizedTokenSerializer
@@ -1000,7 +1000,7 @@ class UserAuthorizedTokenList(SubListCreateAPIView):
class OrganizationApplicationList(SubListCreateAPIView):
- view_name = _("Organization OAuth2 Applications")
+ name = _("Organization OAuth2 Applications")
model = models.OAuth2Application
serializer_class = serializers.OAuth2ApplicationSerializer
@@ -1012,7 +1012,7 @@ class OrganizationApplicationList(SubListCreateAPIView):
class UserPersonalTokenList(SubListCreateAPIView):
- view_name = _("OAuth2 Personal Access Tokens")
+ name = _("OAuth2 Personal Access Tokens")
model = models.OAuth2AccessToken
serializer_class = serializers.UserPersonalTokenSerializer
@@ -1027,7 +1027,7 @@ class UserPersonalTokenList(SubListCreateAPIView):
class OAuth2TokenDetail(RetrieveUpdateDestroyAPIView):
- view_name = _("OAuth Token Detail")
+ name = _("OAuth Token Detail")
model = models.OAuth2AccessToken
serializer_class = serializers.OAuth2TokenDetailSerializer
@@ -1373,7 +1373,7 @@ class CredentialExternalTest(SubDetailAPIView):
before saving them.
"""
- view_name = _('External Credential Test')
+ name = _('External Credential Test')
model = models.Credential
serializer_class = serializers.EmptySerializer
@@ -1399,7 +1399,7 @@ class CredentialExternalTest(SubDetailAPIView):
class CredentialInputSourceDetail(RetrieveUpdateDestroyAPIView):
- view_name = _("Credential Input Source Detail")
+ name = _("Credential Input Source Detail")
model = models.CredentialInputSource
serializer_class = serializers.CredentialInputSourceSerializer
@@ -1407,7 +1407,7 @@ class CredentialInputSourceDetail(RetrieveUpdateDestroyAPIView):
class CredentialInputSourceList(ListCreateAPIView):
- view_name = _("Credential Input Sources")
+ name = _("Credential Input Sources")
model = models.CredentialInputSource
serializer_class = serializers.CredentialInputSourceSerializer
@@ -1415,7 +1415,7 @@ class CredentialInputSourceList(ListCreateAPIView):
class CredentialInputSourceSubList(SubListCreateAPIView):
- view_name = _("Credential Input Sources")
+ name = _("Credential Input Sources")
model = models.CredentialInputSource
serializer_class = serializers.CredentialInputSourceSerializer
@@ -1430,7 +1430,7 @@ class CredentialTypeExternalTest(SubDetailAPIView):
saving it.
"""
- view_name = _('External Credential Type Test')
+ name = _('External Credential Type Test')
model = models.CredentialType
serializer_class = serializers.EmptySerializer
@@ -2004,7 +2004,7 @@ class InventoryTreeView(RetrieveAPIView):
class InventoryInventorySourcesList(SubListCreateAPIView):
- view_name = _('Inventory Source List')
+ name = _('Inventory Source List')
model = models.InventorySource
serializer_class = serializers.InventorySourceSerializer
@@ -2016,7 +2016,7 @@ class InventoryInventorySourcesList(SubListCreateAPIView):
class InventoryInventorySourcesUpdate(RetrieveAPIView):
- view_name = _('Inventory Sources Update')
+ name = _('Inventory Sources Update')
model = models.Inventory
obj_permission_type = 'start'
@@ -2079,7 +2079,7 @@ class InventorySourceDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroy
class InventorySourceSchedulesList(SubListCreateAPIView):
- view_name = _("Inventory Source Schedules")
+ name = _("Inventory Source Schedules")
model = models.Schedule
serializer_class = serializers.ScheduleSerializer
@@ -2448,7 +2448,7 @@ class JobTemplateLaunch(RetrieveAPIView):
class JobTemplateSchedulesList(SubListCreateAPIView):
- view_name = _("Job Template Schedules")
+ name = _("Job Template Schedules")
model = models.Schedule
serializer_class = serializers.ScheduleSerializer
@@ -3220,7 +3220,7 @@ class WorkflowJobTemplateJobsList(SubListAPIView):
class WorkflowJobTemplateSchedulesList(SubListCreateAPIView):
- view_name = _("Workflow Job Template Schedules")
+ name = _("Workflow Job Template Schedules")
model = models.Schedule
serializer_class = serializers.ScheduleSerializer
@@ -3388,7 +3388,7 @@ class SystemJobTemplateLaunch(GenericAPIView):
class SystemJobTemplateSchedulesList(SubListCreateAPIView):
- view_name = _("System Job Template Schedules")
+ name = _("System Job Template Schedules")
model = models.Schedule
serializer_class = serializers.ScheduleSerializer
@@ -3659,7 +3659,7 @@ class BaseJobHostSummariesList(SubListAPIView):
serializer_class = serializers.JobHostSummarySerializer
parent_model = None # Subclasses must define this attribute.
relationship = 'job_host_summaries'
- view_name = _('Job Host Summaries List')
+ name = _('Job Host Summaries List')
search_fields = ('host_name',)
def get_queryset(self):
@@ -3708,7 +3708,7 @@ class JobEventChildrenList(SubListAPIView):
serializer_class = serializers.JobEventSerializer
parent_model = models.JobEvent
relationship = 'children'
- view_name = _('Job Event Children List')
+ name = _('Job Event Children List')
search_fields = ('stdout',)
def get_queryset(self):
@@ -3724,7 +3724,7 @@ class JobEventHostsList(HostRelatedSearchMixin, SubListAPIView):
serializer_class = serializers.HostSerializer
parent_model = models.JobEvent
relationship = 'hosts'
- view_name = _('Job Event Hosts List')
+ name = _('Job Event Hosts List')
class BaseJobEventsList(SubListAPIView):
@@ -3733,7 +3733,7 @@ class BaseJobEventsList(SubListAPIView):
serializer_class = serializers.JobEventSerializer
parent_model = None # Subclasses must define this attribute.
relationship = 'job_events'
- view_name = _('Job Events List')
+ name = _('Job Events List')
search_fields = ('stdout',)
def finalize_response(self, request, response, *args, **kwargs):
@@ -3942,7 +3942,7 @@ class BaseAdHocCommandEventsList(SubListAPIView):
serializer_class = serializers.AdHocCommandEventSerializer
parent_model = None # Subclasses must define this attribute.
relationship = 'ad_hoc_command_events'
- view_name = _('Ad Hoc Command Events List')
+ name = _('Ad Hoc Command Events List')
search_fields = ('stdout',)
@@ -4100,7 +4100,7 @@ class UnifiedJobStdout(RetrieveAPIView):
# Remove any ANSI escape sequences containing job event data.
content = re.sub(r'\x1b\[K(?:[A-Za-z0-9+/=]+\x1b\[\d+D)+\x1b\[K', '', content)
- body = ansiconv.to_html(cgi.escape(content))
+ body = ansiconv.to_html(html.escape(content))
context = {
'title': get_view_name(self.__class__),
@@ -4195,7 +4195,7 @@ class NotificationTemplateDetail(RetrieveUpdateDestroyAPIView):
class NotificationTemplateTest(GenericAPIView):
'''Test a Notification Template'''
- view_name = _('Notification Template Test')
+ name = _('Notification Template Test')
model = models.NotificationTemplate
obj_permission_type = 'start'
serializer_class = serializers.EmptySerializer
diff --git a/awx/api/views/inventory.py b/awx/api/views/inventory.py
index 15daa55232..dfa3f6627a 100644
--- a/awx/api/views/inventory.py
+++ b/awx/api/views/inventory.py
@@ -60,7 +60,7 @@ class InventoryUpdateEventsList(SubListAPIView):
serializer_class = InventoryUpdateEventSerializer
parent_model = InventoryUpdate
relationship = 'inventory_update_events'
- view_name = _('Inventory Update Events List')
+ name = _('Inventory Update Events List')
search_fields = ('stdout',)
def finalize_response(self, request, response, *args, **kwargs):
diff --git a/awx/api/views/metrics.py b/awx/api/views/metrics.py
index 5d60747260..2c15c4e8c8 100644
--- a/awx/api/views/metrics.py
+++ b/awx/api/views/metrics.py
@@ -27,7 +27,7 @@ logger = logging.getLogger('awx.main.analytics')
class MetricsView(APIView):
- view_name = _('Metrics')
+ name = _('Metrics')
swagger_topic = 'Metrics'
renderer_classes = [renderers.PlainTextRenderer,
diff --git a/awx/api/views/root.py b/awx/api/views/root.py
index 2e4312d613..50e7ada0d6 100644
--- a/awx/api/views/root.py
+++ b/awx/api/views/root.py
@@ -42,7 +42,7 @@ logger = logging.getLogger('awx.api.views.root')
class ApiRootView(APIView):
permission_classes = (AllowAny,)
- view_name = _('REST API')
+ name = _('REST API')
versioning_class = None
swagger_topic = 'Versioning'
@@ -64,7 +64,7 @@ class ApiRootView(APIView):
class ApiOAuthAuthorizationRootView(APIView):
permission_classes = (AllowAny,)
- view_name = _("API OAuth 2 Authorization Root")
+ name = _("API OAuth 2 Authorization Root")
versioning_class = None
swagger_topic = 'Authentication'
@@ -130,7 +130,7 @@ class ApiVersionRootView(APIView):
class ApiV2RootView(ApiVersionRootView):
- view_name = _('Version 2')
+ name = _('Version 2')
class ApiV2PingView(APIView):
@@ -139,7 +139,7 @@ class ApiV2PingView(APIView):
"""
permission_classes = (AllowAny,)
authentication_classes = ()
- view_name = _('Ping')
+ name = _('Ping')
swagger_topic = 'System Configuration'
def get(self, request, format=None):
@@ -171,7 +171,7 @@ class ApiV2PingView(APIView):
class ApiV2ConfigView(APIView):
permission_classes = (IsAuthenticated,)
- view_name = _('Configuration')
+ name = _('Configuration')
swagger_topic = 'System Configuration'
def check_permissions(self, request):
diff --git a/awx/conf/views.py b/awx/conf/views.py
index bab468e0eb..13b72a926f 100644
--- a/awx/conf/views.py
+++ b/awx/conf/views.py
@@ -41,7 +41,7 @@ class SettingCategoryList(ListAPIView):
model = Setting # Not exactly, but needed for the view.
serializer_class = SettingCategorySerializer
filter_backends = []
- view_name = _('Setting Categories')
+ name = _('Setting Categories')
def get_queryset(self):
setting_categories = []
@@ -63,7 +63,7 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
model = Setting # Not exactly, but needed for the view.
serializer_class = SettingSingletonSerializer
filter_backends = []
- view_name = _('Setting Detail')
+ name = _('Setting Detail')
def get_queryset(self):
self.category_slug = self.kwargs.get('category_slug', 'all')
@@ -154,7 +154,7 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
class SettingLoggingTest(GenericAPIView):
- view_name = _('Logging Connectivity Test')
+ name = _('Logging Connectivity Test')
model = Setting
serializer_class = SettingSingletonSerializer
permission_classes = (IsSuperUser,)
diff --git a/awx/main/tests/functional/test_licenses.py b/awx/main/tests/functional/test_licenses.py
index ec4da1ad91..6c34321f8d 100644
--- a/awx/main/tests/functional/test_licenses.py
+++ b/awx/main/tests/functional/test_licenses.py
@@ -4,7 +4,11 @@ import json
import os
from django.conf import settings
-from pip._internal.req import parse_requirements
+
+try:
+ from pip._internal.req import parse_requirements
+except ImportError:
+ from pip.req import parse_requirements
def test_python_and_js_licenses():
diff --git a/awx/main/views.py b/awx/main/views.py
index c8bcfa304f..6cf27c0476 100644
--- a/awx/main/views.py
+++ b/awx/main/views.py
@@ -20,10 +20,8 @@ class ApiErrorView(views.APIView):
permission_classes = (permissions.AllowAny,)
metadata_class = None
exception_class = exceptions.APIException
- view_name = _('API Error')
- def get_view_name(self):
- return self.view_name
+ name = _('API Error')
def finalize_response(self, request, response, *args, **kwargs):
response = super(ApiErrorView, self).finalize_response(request, response, *args, **kwargs)
@@ -46,7 +44,7 @@ def handle_error(request, status=404, **kwargs):
class APIException(exceptions.APIException):
status_code = status
default_detail = kwargs['content']
- api_error_view = ApiErrorView.as_view(view_name=kwargs['name'], exception_class=APIException)
+ api_error_view = ApiErrorView.as_view(exception_class=APIException)
response = api_error_view(request)
if hasattr(response, 'render'):
response.render()
diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py
index 8341875aa5..b577e86ae5 100644
--- a/awx/settings/defaults.py
+++ b/awx/settings/defaults.py
@@ -307,7 +307,6 @@ REST_FRAMEWORK = {
),
'DEFAULT_METADATA_CLASS': 'awx.api.metadata.Metadata',
'EXCEPTION_HANDLER': 'awx.api.views.api_exception_handler',
- 'VIEW_NAME_FUNCTION': 'awx.api.generics.get_view_name',
'VIEW_DESCRIPTION_FUNCTION': 'awx.api.generics.get_view_description',
'NON_FIELD_ERRORS_KEY': '__all__',
'DEFAULT_VERSION': 'v2',
diff --git a/awx/sso/fields.py b/awx/sso/fields.py
index 8220feed5b..09b87fff02 100644
--- a/awx/sso/fields.py
+++ b/awx/sso/fields.py
@@ -1,4 +1,5 @@
import collections
+import copy
import inspect
import json
import re
@@ -8,8 +9,8 @@ import ldap
import awx
# Django
+from django.utils import six
from django.utils.translation import ugettext_lazy as _
-from django.core.exceptions import ValidationError
# Django Auth LDAP
import django_auth_ldap.config
@@ -18,7 +19,8 @@ from django_auth_ldap.config import (
LDAPSearchUnion,
)
-from rest_framework.fields import empty
+from rest_framework.exceptions import ValidationError
+from rest_framework.fields import empty, Field, SkipField
# This must be imported so get_subclasses picks it up
from awx.sso.ldap_group_types import PosixUIDGroupType # noqa
@@ -74,6 +76,71 @@ class DependsOnMixin():
return res
+class _Forbidden(Field):
+ default_error_messages = {
+ 'invalid': _('Invalid field.'),
+ }
+
+ def run_validation(self, value):
+ self.fail('invalid')
+
+
+class HybridDictField(fields.DictField):
+ """A DictField, but with defined fixed Fields for certain keys.
+ """
+
+ def __init__(self, *args, **kwargs):
+ self.allow_blank = kwargs.pop('allow_blank', False)
+
+ fields = [
+ (field_name, obj)
+ for field_name, obj in self.__class__.__dict__.items()
+ if isinstance(obj, Field) and field_name != 'child'
+ ]
+ fields.sort(key=lambda x: x[1]._creation_counter)
+ self._declared_fields = collections.OrderedDict(fields)
+
+ super().__init__(*args, **kwargs)
+
+ def to_representation(self, value):
+ fields = copy.deepcopy(self._declared_fields)
+ return {
+ key: field.to_representation(val) if val is not None else None
+ for key, val, field in (
+ (six.text_type(key), val, fields.get(key, self.child))
+ for key, val in value.items()
+ )
+ if not field.write_only
+ }
+
+ def run_child_validation(self, data):
+ result = {}
+
+ if not data and self.allow_blank:
+ return result
+
+ errors = collections.OrderedDict()
+ fields = copy.deepcopy(self._declared_fields)
+ keys = set(fields.keys()) | set(data.keys())
+
+ for key in keys:
+ value = data.get(key, empty)
+ key = six.text_type(key)
+ field = fields.get(key, self.child)
+ try:
+ if field.read_only:
+ continue # Ignore read_only fields, as Serializer seems to do.
+ result[key] = field.run_validation(value)
+ except ValidationError as e:
+ errors[key] = e.detail
+ except SkipField:
+ pass
+
+ if not errors:
+ return result
+ raise ValidationError(errors)
+
+
class AuthenticationBackendsField(fields.StringListField):
# Mapping of settings that must be set in order to enable each
@@ -459,70 +526,14 @@ class LDAPDNMapField(fields.StringListBooleanField):
child = LDAPDNField()
-class BaseDictWithChildField(fields.DictField):
+class LDAPSingleOrganizationMapField(HybridDictField):
- default_error_messages = {
- 'missing_keys': _('Missing key(s): {missing_keys}.'),
- 'invalid_keys': _('Invalid key(s): {invalid_keys}.'),
- }
- child_fields = {
- # 'key': fields.ChildField(),
- }
- allow_unknown_keys = False
+ admins = LDAPDNMapField(allow_null=True, required=False)
+ users = LDAPDNMapField(allow_null=True, required=False)
+ remove_admins = fields.BooleanField(required=False)
+ remove_users = fields.BooleanField(required=False)
- def __init__(self, *args, **kwargs):
- self.allow_blank = kwargs.pop('allow_blank', False)
- super(BaseDictWithChildField, self).__init__(*args, **kwargs)
-
- def to_representation(self, value):
- value = super(BaseDictWithChildField, self).to_representation(value)
- for k, v in value.items():
- child_field = self.child_fields.get(k, None)
- if child_field:
- value[k] = child_field.to_representation(v)
- elif self.allow_unknown_keys:
- value[k] = v
- return value
-
- def to_internal_value(self, data):
- data = super(BaseDictWithChildField, self).to_internal_value(data)
- missing_keys = set()
- for key, child_field in self.child_fields.items():
- if not child_field.required:
- continue
- elif key not in data:
- missing_keys.add(key)
- missing_keys = sorted(list(missing_keys))
- if missing_keys and (data or not self.allow_blank):
- missing_keys = sorted(list(missing_keys))
- keys_display = json.dumps(missing_keys).lstrip('[').rstrip(']')
- self.fail('missing_keys', missing_keys=keys_display)
- if not self.allow_unknown_keys:
- invalid_keys = set(data.keys()) - set(self.child_fields.keys())
- if invalid_keys:
- invalid_keys = sorted(list(invalid_keys))
- keys_display = json.dumps(invalid_keys).lstrip('[').rstrip(']')
- self.fail('invalid_keys', invalid_keys=keys_display)
- for k, v in data.items():
- child_field = self.child_fields.get(k, None)
- if child_field:
- data[k] = child_field.run_validation(v)
- elif self.allow_unknown_keys:
- data[k] = v
- return data
-
-
-class LDAPSingleOrganizationMapField(BaseDictWithChildField):
-
- default_error_messages = {
- 'invalid_keys': _('Invalid key(s) for organization map: {invalid_keys}.'),
- }
- child_fields = {
- 'admins': LDAPDNMapField(allow_null=True, required=False),
- 'users': LDAPDNMapField(allow_null=True, required=False),
- 'remove_admins': fields.BooleanField(required=False),
- 'remove_users': fields.BooleanField(required=False),
- }
+ child = _Forbidden()
class LDAPOrganizationMapField(fields.DictField):
@@ -530,17 +541,13 @@ class LDAPOrganizationMapField(fields.DictField):
child = LDAPSingleOrganizationMapField()
-class LDAPSingleTeamMapField(BaseDictWithChildField):
+class LDAPSingleTeamMapField(HybridDictField):
- default_error_messages = {
- 'missing_keys': _('Missing required key for team map: {invalid_keys}.'),
- 'invalid_keys': _('Invalid key(s) for team map: {invalid_keys}.'),
- }
- child_fields = {
- 'organization': fields.CharField(),
- 'users': LDAPDNMapField(allow_null=True, required=False),
- 'remove': fields.BooleanField(required=False),
- }
+ organization = fields.CharField()
+ users = LDAPDNMapField(allow_null=True, required=False)
+ remove = fields.BooleanField(required=False)
+
+ child = _Forbidden()
class LDAPTeamMapField(fields.DictField):
@@ -614,17 +621,14 @@ class SocialMapField(fields.ListField):
self.fail('type_error', input_type=type(data))
-class SocialSingleOrganizationMapField(BaseDictWithChildField):
+class SocialSingleOrganizationMapField(HybridDictField):
- default_error_messages = {
- 'invalid_keys': _('Invalid key(s) for organization map: {invalid_keys}.'),
- }
- child_fields = {
- 'admins': SocialMapField(allow_null=True, required=False),
- 'users': SocialMapField(allow_null=True, required=False),
- 'remove_admins': fields.BooleanField(required=False),
- 'remove_users': fields.BooleanField(required=False),
- }
+ admins = SocialMapField(allow_null=True, required=False)
+ users = SocialMapField(allow_null=True, required=False)
+ remove_admins = fields.BooleanField(required=False)
+ remove_users = fields.BooleanField(required=False)
+
+ child = _Forbidden()
class SocialOrganizationMapField(fields.DictField):
@@ -632,17 +636,13 @@ class SocialOrganizationMapField(fields.DictField):
child = SocialSingleOrganizationMapField()
-class SocialSingleTeamMapField(BaseDictWithChildField):
+class SocialSingleTeamMapField(HybridDictField):
- default_error_messages = {
- 'missing_keys': _('Missing required key for team map: {missing_keys}.'),
- 'invalid_keys': _('Invalid key(s) for team map: {invalid_keys}.'),
- }
- child_fields = {
- 'organization': fields.CharField(),
- 'users': SocialMapField(allow_null=True, required=False),
- 'remove': fields.BooleanField(required=False),
- }
+ organization = fields.CharField()
+ users = SocialMapField(allow_null=True, required=False)
+ remove = fields.BooleanField(required=False)
+
+ child = _Forbidden()
class SocialTeamMapField(fields.DictField):
@@ -650,17 +650,11 @@ class SocialTeamMapField(fields.DictField):
child = SocialSingleTeamMapField()
-class SAMLOrgInfoValueField(BaseDictWithChildField):
+class SAMLOrgInfoValueField(HybridDictField):
- default_error_messages = {
- 'missing_keys': _('Missing required key(s) for org info record: {missing_keys}.'),
- }
- child_fields = {
- 'name': fields.CharField(),
- 'displayname': fields.CharField(),
- 'url': fields.URLField(),
- }
- allow_unknown_keys = True
+ name = fields.CharField()
+ displayname = fields.CharField()
+ url = fields.URLField()
class SAMLOrgInfoField(fields.DictField):
@@ -683,34 +677,22 @@ class SAMLOrgInfoField(fields.DictField):
return data
-class SAMLContactField(BaseDictWithChildField):
+class SAMLContactField(HybridDictField):
- default_error_messages = {
- 'missing_keys': _('Missing required key(s) for contact: {missing_keys}.'),
- }
- child_fields = {
- 'givenName': fields.CharField(),
- 'emailAddress': fields.EmailField(),
- }
- allow_unknown_keys = True
+ givenName = fields.CharField()
+ emailAddress = fields.EmailField()
-class SAMLIdPField(BaseDictWithChildField):
+class SAMLIdPField(HybridDictField):
- default_error_messages = {
- 'missing_keys': _('Missing required key(s) for IdP: {missing_keys}.'),
- }
- child_fields = {
- 'entity_id': fields.CharField(),
- 'url': fields.URLField(),
- 'x509cert': fields.CharField(validators=[validate_certificate]),
- 'attr_user_permanent_id': fields.CharField(required=False),
- 'attr_first_name': fields.CharField(required=False),
- 'attr_last_name': fields.CharField(required=False),
- 'attr_username': fields.CharField(required=False),
- 'attr_email': fields.CharField(required=False),
- }
- allow_unknown_keys = True
+ entity_id = fields.CharField()
+ url = fields.URLField()
+ x509cert = fields.CharField(validators=[validate_certificate])
+ attr_user_permanent_id = fields.CharField(required=False)
+ attr_first_name = fields.CharField(required=False)
+ attr_last_name = fields.CharField(required=False)
+ attr_username = fields.CharField(required=False)
+ attr_email = fields.CharField(required=False)
class SAMLEnabledIdPsField(fields.DictField):
@@ -718,52 +700,49 @@ class SAMLEnabledIdPsField(fields.DictField):
child = SAMLIdPField()
-class SAMLSecurityField(BaseDictWithChildField):
+class SAMLSecurityField(HybridDictField):
- child_fields = {
- 'nameIdEncrypted': fields.BooleanField(required=False),
- 'authnRequestsSigned': fields.BooleanField(required=False),
- 'logoutRequestSigned': fields.BooleanField(required=False),
- 'logoutResponseSigned': fields.BooleanField(required=False),
- 'signMetadata': fields.BooleanField(required=False),
- 'wantMessagesSigned': fields.BooleanField(required=False),
- 'wantAssertionsSigned': fields.BooleanField(required=False),
- 'wantAssertionsEncrypted': fields.BooleanField(required=False),
- 'wantNameId': fields.BooleanField(required=False),
- 'wantNameIdEncrypted': fields.BooleanField(required=False),
- 'wantAttributeStatement': fields.BooleanField(required=False),
- 'requestedAuthnContext': fields.StringListBooleanField(required=False),
- 'requestedAuthnContextComparison': fields.CharField(required=False),
- 'metadataValidUntil': fields.CharField(allow_null=True, required=False),
- 'metadataCacheDuration': fields.CharField(allow_null=True, required=False),
- 'signatureAlgorithm': fields.CharField(allow_null=True, required=False),
- 'digestAlgorithm': fields.CharField(allow_null=True, required=False),
- }
- allow_unknown_keys = True
+ nameIdEncrypted = fields.BooleanField(required=False)
+ authnRequestsSigned = fields.BooleanField(required=False)
+ logoutRequestSigned = fields.BooleanField(required=False)
+ logoutResponseSigned = fields.BooleanField(required=False)
+ signMetadata = fields.BooleanField(required=False)
+ wantMessagesSigned = fields.BooleanField(required=False)
+ wantAssertionsSigned = fields.BooleanField(required=False)
+ wantAssertionsEncrypted = fields.BooleanField(required=False)
+ wantNameId = fields.BooleanField(required=False)
+ wantNameIdEncrypted = fields.BooleanField(required=False)
+ wantAttributeStatement = fields.BooleanField(required=False)
+ requestedAuthnContext = fields.StringListBooleanField(required=False)
+ requestedAuthnContextComparison = fields.CharField(required=False)
+ metadataValidUntil = fields.CharField(allow_null=True, required=False)
+ metadataCacheDuration = fields.CharField(allow_null=True, required=False)
+ signatureAlgorithm = fields.CharField(allow_null=True, required=False)
+ digestAlgorithm = fields.CharField(allow_null=True, required=False)
-class SAMLOrgAttrField(BaseDictWithChildField):
+class SAMLOrgAttrField(HybridDictField):
- child_fields = {
- 'remove': fields.BooleanField(required=False),
- 'saml_attr': fields.CharField(required=False, allow_null=True),
- 'remove_admins': fields.BooleanField(required=False),
- 'saml_admin_attr': fields.CharField(required=False, allow_null=True),
- }
+ remove = fields.BooleanField(required=False)
+ saml_attr = fields.CharField(required=False, allow_null=True)
+ remove_admins = fields.BooleanField(required=False)
+ saml_admin_attr = fields.CharField(required=False, allow_null=True)
+
+ child = _Forbidden()
-class SAMLTeamAttrTeamOrgMapField(BaseDictWithChildField):
+class SAMLTeamAttrTeamOrgMapField(HybridDictField):
- child_fields = {
- 'team': fields.CharField(required=True, allow_null=False),
- 'organization': fields.CharField(required=True, allow_null=False),
- }
+ team = fields.CharField(required=True, allow_null=False)
+ organization = fields.CharField(required=True, allow_null=False)
+
+ child = _Forbidden()
-class SAMLTeamAttrField(BaseDictWithChildField):
+class SAMLTeamAttrField(HybridDictField):
- child_fields = {
- 'team_org_map': fields.ListField(required=False, child=SAMLTeamAttrTeamOrgMapField(), allow_null=True),
- 'remove': fields.BooleanField(required=False),
- 'saml_attr': fields.CharField(required=False, allow_null=True),
- }
+ team_org_map = fields.ListField(required=False, child=SAMLTeamAttrTeamOrgMapField(), allow_null=True)
+ remove = fields.BooleanField(required=False)
+ saml_attr = fields.CharField(required=False, allow_null=True)
+
+ child = _Forbidden()
diff --git a/awx/sso/tests/unit/test_fields.py b/awx/sso/tests/unit/test_fields.py
index fbd3a04781..95cd774b98 100644
--- a/awx/sso/tests/unit/test_fields.py
+++ b/awx/sso/tests/unit/test_fields.py
@@ -33,21 +33,23 @@ class TestSAMLOrgAttrField():
@pytest.mark.parametrize("data, expected", [
({'remove': 'blah', 'saml_attr': 'foobar'},
- ValidationError('"blah" is not a valid boolean.')),
+ {'remove': ['Must be a valid boolean.']}),
({'remove': True, 'saml_attr': False},
- ValidationError('Not a valid string.')),
+ {'saml_attr': ['Not a valid string.']}),
({'remove': True, 'saml_attr': False, 'foo': 'bar', 'gig': 'ity'},
- ValidationError('Invalid key(s): "foo", "gig".')),
+ {'saml_attr': ['Not a valid string.'],
+ 'foo': ['Invalid field.'],
+ 'gig': ['Invalid field.']}),
({'remove_admins': True, 'saml_admin_attr': False},
- ValidationError('Not a valid string.')),
+ {'saml_admin_attr': ['Not a valid string.']}),
({'remove_admins': 'blah', 'saml_admin_attr': 'foobar'},
- ValidationError('"blah" is not a valid boolean.')),
+ {'remove_admins': ['Must be a valid boolean.']}),
])
def test_internal_value_invalid(self, data, expected):
field = SAMLOrgAttrField()
- with pytest.raises(type(expected)) as e:
+ with pytest.raises(ValidationError) as e:
field.to_internal_value(data)
- assert str(e.value) == str(expected)
+ assert e.value.detail == expected
class TestSAMLTeamAttrField():
@@ -77,36 +79,38 @@ class TestSAMLTeamAttrField():
@pytest.mark.parametrize("data, expected", [
({'remove': True, 'saml_attr': 'foobar', 'team_org_map': [
{'team': 'foobar', 'not_a_valid_key': 'blah', 'organization': 'Ansible'},
- ]}, ValidationError('Invalid key(s): "not_a_valid_key".')),
+ ]}, {'team_org_map': {0: {'not_a_valid_key': ['Invalid field.']}}}),
({'remove': False, 'saml_attr': 'foobar', 'team_org_map': [
{'organization': 'Ansible'},
- ]}, ValidationError('Missing key(s): "team".')),
+ ]}, {'team_org_map': {0: {'team': ['This field is required.']}}}),
({'remove': False, 'saml_attr': 'foobar', 'team_org_map': [
{},
- ]}, ValidationError('Missing key(s): "organization", "team".')),
+ ]}, {'team_org_map': {
+ 0: {'organization': ['This field is required.'],
+ 'team': ['This field is required.']}}}),
])
def test_internal_value_invalid(self, data, expected):
field = SAMLTeamAttrField()
- with pytest.raises(type(expected)) as e:
+ with pytest.raises(ValidationError) as e:
field.to_internal_value(data)
- assert str(e.value) == str(expected)
+ assert e.value.detail == expected
class TestLDAPGroupTypeParamsField():
@pytest.mark.parametrize("group_type, data, expected", [
('LDAPGroupType', {'name_attr': 'user', 'bob': ['a', 'b'], 'scooter': 'hello'},
- ValidationError('Invalid key(s): "bob", "scooter".')),
+ ['Invalid key(s): "bob", "scooter".']),
('MemberDNGroupType', {'name_attr': 'user', 'member_attr': 'west', 'bob': ['a', 'b'], 'scooter': 'hello'},
- ValidationError('Invalid key(s): "bob", "scooter".')),
+ ['Invalid key(s): "bob", "scooter".']),
('PosixUIDGroupType', {'name_attr': 'user', 'member_attr': 'west', 'ldap_group_user_attr': 'legacyThing',
'bob': ['a', 'b'], 'scooter': 'hello'},
- ValidationError('Invalid key(s): "bob", "member_attr", "scooter".')),
+ ['Invalid key(s): "bob", "member_attr", "scooter".']),
])
def test_internal_value_invalid(self, group_type, data, expected):
field = LDAPGroupTypeParamsField()
field.get_depends_on = mock.MagicMock(return_value=group_type)
- with pytest.raises(type(expected)) as e:
+ with pytest.raises(ValidationError) as e:
field.to_internal_value(data)
- assert str(e.value) == str(expected)
+ assert e.value.detail == expected
diff --git a/awx/templates/rest_framework/api.html b/awx/templates/rest_framework/api.html
index eba78531cc..64b1dfa0c2 100644
--- a/awx/templates/rest_framework/api.html
+++ b/awx/templates/rest_framework/api.html
@@ -1,5 +1,5 @@
{% extends 'rest_framework/base.html' %}
-{% load i18n staticfiles %}
+{% load i18n static %}
{% block title %}{{ name }} · {% trans 'AWX REST API' %}{% endblock %}
@@ -48,6 +48,15 @@
{% endblock %}
+{% block content %}
+ {% if deprecated %}
+
+ This resource has been deprecated and will be removed in a future release.
+
+ {% endif %}
+{{ block.super }}
+{% endblock content %}
+
{% block script %}