Merge pull request #3967 from jbradberry/upgrade-drf-3.9.4

Upgrade DRF to 3.9.4

Reviewed-by: https://github.com/softwarefactory-project-zuul[bot]
This commit is contained in:
softwarefactory-project-zuul[bot]
2019-06-12 18:41:05 +00:00
committed by GitHub
16 changed files with 258 additions and 599 deletions

View File

@@ -119,39 +119,12 @@ class LoggedLogoutView(auth_views.LogoutView):
return ret return ret
def get_view_name(cls, suffix=None): def get_view_description(view, html=False):
''' '''Wrapper around REST framework get_view_description() to continue
Wrapper around REST framework get_view_name() to support get_name() method to support our historical div.
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(cls, request, html=False):
''' '''
Wrapper around REST framework get_view_description() to support desc = views.get_view_description(view, html=html)
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)
if html: if html:
desc = '<div class="description">%s</div>' % desc desc = '<div class="description">%s</div>' % desc
return mark_safe(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/` # `curl https://user:pass@tower.example.org/api/v2/job_templates/N/launch/`
return 'Bearer realm=api authorization_url=/api/o/authorize/' 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): def get_description_context(self):
return { return {
'view': self, 'view': self,
@@ -280,8 +245,8 @@ class APIView(views.APIView):
'swagger_method': getattr(self.request, 'swagger_method', None), 'swagger_method': getattr(self.request, 'swagger_method', None),
} }
def get_description(self, request, html=False): @property
self.request = request def description(self):
template_list = [] template_list = []
for klass in inspect.getmro(type(self)): for klass in inspect.getmro(type(self)):
template_basename = camelcase_to_underscore(klass.__name__) 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), 'model_verbose_name_plural': smart_text(self.model._meta.verbose_name_plural),
}) })
serializer = self.get_serializer() serializer = self.get_serializer()
metadata = self.metadata_class()
metadata.request = self.request
for method, key in [ for method, key in [
('GET', 'serializer_fields'), ('GET', 'serializer_fields'),
('POST', 'serializer_create_fields'), ('POST', 'serializer_create_fields'),
('PUT', 'serializer_update_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 d['settings'] = settings
return d return d

View File

@@ -53,7 +53,6 @@ class AutoSchema(DRFAuthSchema):
return link return link
def get_description(self, path, method): def get_description(self, path, method):
self.view._request = self.view.request
setattr(self.view.request, 'swagger_method', method) setattr(self.view.request, 'swagger_method', method)
description = super(AutoSchema, self).get_description(path, method) description = super(AutoSchema, self).get_description(path, method)
return description return description

View File

@@ -2,9 +2,9 @@
# All Rights Reserved. # All Rights Reserved.
# Python # Python
import cgi
import dateutil import dateutil
import functools import functools
import html
import logging import logging
import re import re
import requests import requests
@@ -37,7 +37,7 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.renderers import JSONRenderer, StaticHTMLRenderer from rest_framework.renderers import JSONRenderer, StaticHTMLRenderer
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.settings import api_settings 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 from rest_framework import status
# Django REST Framework YAML # Django REST Framework YAML
@@ -69,7 +69,7 @@ from awx.api.generics import (
RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, SimpleListAPIView, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, SimpleListAPIView,
SubDetailAPIView, SubListAPIView, SubListAttachDetachAPIView, SubDetailAPIView, SubListAPIView, SubListAttachDetachAPIView,
SubListCreateAPIView, SubListCreateAttachDetachAPIView, SubListCreateAPIView, SubListCreateAttachDetachAPIView,
SubListDestroyAPIView, get_view_name SubListDestroyAPIView
) )
from awx.api.versioning import reverse from awx.api.versioning import reverse
from awx.conf.license import get_license from awx.conf.license import get_license
@@ -168,7 +168,7 @@ class DashboardView(APIView):
deprecated = True deprecated = True
view_name = _("Dashboard") name = _("Dashboard")
swagger_topic = 'Dashboard' swagger_topic = 'Dashboard'
def get(self, request, format=None): def get(self, request, format=None):
@@ -270,7 +270,7 @@ class DashboardView(APIView):
class DashboardJobsGraphView(APIView): class DashboardJobsGraphView(APIView):
view_name = _("Dashboard Jobs Graphs") name = _("Dashboard Jobs Graphs")
swagger_topic = 'Jobs' swagger_topic = 'Jobs'
def get(self, request, format=None): def get(self, request, format=None):
@@ -320,7 +320,7 @@ class DashboardJobsGraphView(APIView):
class InstanceList(ListAPIView): class InstanceList(ListAPIView):
view_name = _("Instances") name = _("Instances")
model = models.Instance model = models.Instance
serializer_class = serializers.InstanceSerializer serializer_class = serializers.InstanceSerializer
search_fields = ('hostname',) search_fields = ('hostname',)
@@ -328,7 +328,7 @@ class InstanceList(ListAPIView):
class InstanceDetail(RetrieveUpdateAPIView): class InstanceDetail(RetrieveUpdateAPIView):
view_name = _("Instance Detail") name = _("Instance Detail")
model = models.Instance model = models.Instance
serializer_class = serializers.InstanceSerializer serializer_class = serializers.InstanceSerializer
@@ -345,7 +345,7 @@ class InstanceDetail(RetrieveUpdateAPIView):
class InstanceUnifiedJobsList(SubListAPIView): class InstanceUnifiedJobsList(SubListAPIView):
view_name = _("Instance Jobs") name = _("Instance Jobs")
model = models.UnifiedJob model = models.UnifiedJob
serializer_class = serializers.UnifiedJobListSerializer serializer_class = serializers.UnifiedJobListSerializer
parent_model = models.Instance parent_model = models.Instance
@@ -359,7 +359,7 @@ class InstanceUnifiedJobsList(SubListAPIView):
class InstanceInstanceGroupsList(InstanceGroupMembershipMixin, SubListCreateAttachDetachAPIView): class InstanceInstanceGroupsList(InstanceGroupMembershipMixin, SubListCreateAttachDetachAPIView):
view_name = _("Instance's Instance Groups") name = _("Instance's Instance Groups")
model = models.InstanceGroup model = models.InstanceGroup
serializer_class = serializers.InstanceGroupSerializer serializer_class = serializers.InstanceGroupSerializer
parent_model = models.Instance parent_model = models.Instance
@@ -368,7 +368,7 @@ class InstanceInstanceGroupsList(InstanceGroupMembershipMixin, SubListCreateAtta
class InstanceGroupList(ListCreateAPIView): class InstanceGroupList(ListCreateAPIView):
view_name = _("Instance Groups") name = _("Instance Groups")
model = models.InstanceGroup model = models.InstanceGroup
serializer_class = serializers.InstanceGroupSerializer serializer_class = serializers.InstanceGroupSerializer
@@ -376,7 +376,7 @@ class InstanceGroupList(ListCreateAPIView):
class InstanceGroupDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): class InstanceGroupDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
always_allow_superuser = False always_allow_superuser = False
view_name = _("Instance Group Detail") name = _("Instance Group Detail")
model = models.InstanceGroup model = models.InstanceGroup
serializer_class = serializers.InstanceGroupSerializer serializer_class = serializers.InstanceGroupSerializer
permission_classes = (InstanceGroupTowerPermission,) permission_classes = (InstanceGroupTowerPermission,)
@@ -392,7 +392,7 @@ class InstanceGroupDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAP
class InstanceGroupUnifiedJobsList(SubListAPIView): class InstanceGroupUnifiedJobsList(SubListAPIView):
view_name = _("Instance Group Running Jobs") name = _("Instance Group Running Jobs")
model = models.UnifiedJob model = models.UnifiedJob
serializer_class = serializers.UnifiedJobListSerializer serializer_class = serializers.UnifiedJobListSerializer
parent_model = models.InstanceGroup parent_model = models.InstanceGroup
@@ -401,7 +401,7 @@ class InstanceGroupUnifiedJobsList(SubListAPIView):
class InstanceGroupInstanceList(InstanceGroupMembershipMixin, SubListAttachDetachAPIView): class InstanceGroupInstanceList(InstanceGroupMembershipMixin, SubListAttachDetachAPIView):
view_name = _("Instance Group's Instances") name = _("Instance Group's Instances")
model = models.Instance model = models.Instance
serializer_class = serializers.InstanceSerializer serializer_class = serializers.InstanceSerializer
parent_model = models.InstanceGroup parent_model = models.InstanceGroup
@@ -411,7 +411,7 @@ class InstanceGroupInstanceList(InstanceGroupMembershipMixin, SubListAttachDetac
class ScheduleList(ListCreateAPIView): class ScheduleList(ListCreateAPIView):
view_name = _("Schedules") name = _("Schedules")
model = models.Schedule model = models.Schedule
serializer_class = serializers.ScheduleSerializer serializer_class = serializers.ScheduleSerializer
@@ -425,7 +425,7 @@ class ScheduleDetail(RetrieveUpdateDestroyAPIView):
class SchedulePreview(GenericAPIView): class SchedulePreview(GenericAPIView):
model = models.Schedule model = models.Schedule
view_name = _('Schedule Recurrence Rule Preview') name = _('Schedule Recurrence Rule Preview')
serializer_class = serializers.SchedulePreviewSerializer serializer_class = serializers.SchedulePreviewSerializer
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
@@ -508,7 +508,7 @@ class ScheduleUnifiedJobsList(SubListAPIView):
serializer_class = serializers.UnifiedJobListSerializer serializer_class = serializers.UnifiedJobListSerializer
parent_model = models.Schedule parent_model = models.Schedule
relationship = 'unifiedjob_set' relationship = 'unifiedjob_set'
view_name = _('Schedule Jobs List') name = _('Schedule Jobs List')
class AuthView(APIView): class AuthView(APIView):
@@ -704,7 +704,7 @@ class ProjectTeamsList(ListAPIView):
class ProjectSchedulesList(SubListCreateAPIView): class ProjectSchedulesList(SubListCreateAPIView):
view_name = _("Project Schedules") name = _("Project Schedules")
model = models.Schedule model = models.Schedule
serializer_class = serializers.ScheduleSerializer serializer_class = serializers.ScheduleSerializer
@@ -715,7 +715,7 @@ class ProjectSchedulesList(SubListCreateAPIView):
class ProjectScmInventorySources(SubListAPIView): class ProjectScmInventorySources(SubListAPIView):
view_name = _("Project SCM Inventory Sources") name = _("Project SCM Inventory Sources")
model = models.InventorySource model = models.InventorySource
serializer_class = serializers.InventorySourceSerializer serializer_class = serializers.InventorySourceSerializer
parent_model = models.Project parent_model = models.Project
@@ -818,7 +818,7 @@ class ProjectUpdateEventsList(SubListAPIView):
serializer_class = serializers.ProjectUpdateEventSerializer serializer_class = serializers.ProjectUpdateEventSerializer
parent_model = models.ProjectUpdate parent_model = models.ProjectUpdate
relationship = 'project_update_events' relationship = 'project_update_events'
view_name = _('Project Update Events List') name = _('Project Update Events List')
search_fields = ('stdout',) search_fields = ('stdout',)
def finalize_response(self, request, response, *args, **kwargs): def finalize_response(self, request, response, *args, **kwargs):
@@ -832,7 +832,7 @@ class SystemJobEventsList(SubListAPIView):
serializer_class = serializers.SystemJobEventSerializer serializer_class = serializers.SystemJobEventSerializer
parent_model = models.SystemJob parent_model = models.SystemJob
relationship = 'system_job_events' relationship = 'system_job_events'
view_name = _('System Job Events List') name = _('System Job Events List')
search_fields = ('stdout',) search_fields = ('stdout',)
def finalize_response(self, request, response, *args, **kwargs): def finalize_response(self, request, response, *args, **kwargs):
@@ -868,7 +868,7 @@ class ProjectUpdateNotificationsList(SubListAPIView):
class ProjectUpdateScmInventoryUpdates(SubListAPIView): class ProjectUpdateScmInventoryUpdates(SubListAPIView):
view_name = _("Project Update SCM Inventory Updates") name = _("Project Update SCM Inventory Updates")
model = models.InventoryUpdate model = models.InventoryUpdate
serializer_class = serializers.InventoryUpdateListSerializer serializer_class = serializers.InventoryUpdateListSerializer
parent_model = models.ProjectUpdate parent_model = models.ProjectUpdate
@@ -912,7 +912,7 @@ class UserMeList(ListAPIView):
model = models.User model = models.User
serializer_class = serializers.UserSerializer serializer_class = serializers.UserSerializer
view_name = _('Me') name = _('Me')
def get_queryset(self): def get_queryset(self):
return self.model.objects.filter(pk=self.request.user.pk) return self.model.objects.filter(pk=self.request.user.pk)
@@ -920,7 +920,7 @@ class UserMeList(ListAPIView):
class OAuth2ApplicationList(ListCreateAPIView): class OAuth2ApplicationList(ListCreateAPIView):
view_name = _("OAuth 2 Applications") name = _("OAuth 2 Applications")
model = models.OAuth2Application model = models.OAuth2Application
serializer_class = serializers.OAuth2ApplicationSerializer serializer_class = serializers.OAuth2ApplicationSerializer
@@ -929,7 +929,7 @@ class OAuth2ApplicationList(ListCreateAPIView):
class OAuth2ApplicationDetail(RetrieveUpdateDestroyAPIView): class OAuth2ApplicationDetail(RetrieveUpdateDestroyAPIView):
view_name = _("OAuth 2 Application Detail") name = _("OAuth 2 Application Detail")
model = models.OAuth2Application model = models.OAuth2Application
serializer_class = serializers.OAuth2ApplicationSerializer serializer_class = serializers.OAuth2ApplicationSerializer
@@ -942,7 +942,7 @@ class OAuth2ApplicationDetail(RetrieveUpdateDestroyAPIView):
class ApplicationOAuth2TokenList(SubListCreateAPIView): class ApplicationOAuth2TokenList(SubListCreateAPIView):
view_name = _("OAuth 2 Application Tokens") name = _("OAuth 2 Application Tokens")
model = models.OAuth2AccessToken model = models.OAuth2AccessToken
serializer_class = serializers.OAuth2TokenSerializer serializer_class = serializers.OAuth2TokenSerializer
@@ -964,7 +964,7 @@ class OAuth2ApplicationActivityStreamList(SubListAPIView):
class OAuth2TokenList(ListCreateAPIView): class OAuth2TokenList(ListCreateAPIView):
view_name = _("OAuth2 Tokens") name = _("OAuth2 Tokens")
model = models.OAuth2AccessToken model = models.OAuth2AccessToken
serializer_class = serializers.OAuth2TokenSerializer serializer_class = serializers.OAuth2TokenSerializer
@@ -973,7 +973,7 @@ class OAuth2TokenList(ListCreateAPIView):
class OAuth2UserTokenList(SubListCreateAPIView): class OAuth2UserTokenList(SubListCreateAPIView):
view_name = _("OAuth2 User Tokens") name = _("OAuth2 User Tokens")
model = models.OAuth2AccessToken model = models.OAuth2AccessToken
serializer_class = serializers.OAuth2TokenSerializer serializer_class = serializers.OAuth2TokenSerializer
@@ -985,7 +985,7 @@ class OAuth2UserTokenList(SubListCreateAPIView):
class UserAuthorizedTokenList(SubListCreateAPIView): class UserAuthorizedTokenList(SubListCreateAPIView):
view_name = _("OAuth2 User Authorized Access Tokens") name = _("OAuth2 User Authorized Access Tokens")
model = models.OAuth2AccessToken model = models.OAuth2AccessToken
serializer_class = serializers.UserAuthorizedTokenSerializer serializer_class = serializers.UserAuthorizedTokenSerializer
@@ -1000,7 +1000,7 @@ class UserAuthorizedTokenList(SubListCreateAPIView):
class OrganizationApplicationList(SubListCreateAPIView): class OrganizationApplicationList(SubListCreateAPIView):
view_name = _("Organization OAuth2 Applications") name = _("Organization OAuth2 Applications")
model = models.OAuth2Application model = models.OAuth2Application
serializer_class = serializers.OAuth2ApplicationSerializer serializer_class = serializers.OAuth2ApplicationSerializer
@@ -1012,7 +1012,7 @@ class OrganizationApplicationList(SubListCreateAPIView):
class UserPersonalTokenList(SubListCreateAPIView): class UserPersonalTokenList(SubListCreateAPIView):
view_name = _("OAuth2 Personal Access Tokens") name = _("OAuth2 Personal Access Tokens")
model = models.OAuth2AccessToken model = models.OAuth2AccessToken
serializer_class = serializers.UserPersonalTokenSerializer serializer_class = serializers.UserPersonalTokenSerializer
@@ -1027,7 +1027,7 @@ class UserPersonalTokenList(SubListCreateAPIView):
class OAuth2TokenDetail(RetrieveUpdateDestroyAPIView): class OAuth2TokenDetail(RetrieveUpdateDestroyAPIView):
view_name = _("OAuth Token Detail") name = _("OAuth Token Detail")
model = models.OAuth2AccessToken model = models.OAuth2AccessToken
serializer_class = serializers.OAuth2TokenDetailSerializer serializer_class = serializers.OAuth2TokenDetailSerializer
@@ -1373,7 +1373,7 @@ class CredentialExternalTest(SubDetailAPIView):
before saving them. before saving them.
""" """
view_name = _('External Credential Test') name = _('External Credential Test')
model = models.Credential model = models.Credential
serializer_class = serializers.EmptySerializer serializer_class = serializers.EmptySerializer
@@ -1399,7 +1399,7 @@ class CredentialExternalTest(SubDetailAPIView):
class CredentialInputSourceDetail(RetrieveUpdateDestroyAPIView): class CredentialInputSourceDetail(RetrieveUpdateDestroyAPIView):
view_name = _("Credential Input Source Detail") name = _("Credential Input Source Detail")
model = models.CredentialInputSource model = models.CredentialInputSource
serializer_class = serializers.CredentialInputSourceSerializer serializer_class = serializers.CredentialInputSourceSerializer
@@ -1407,7 +1407,7 @@ class CredentialInputSourceDetail(RetrieveUpdateDestroyAPIView):
class CredentialInputSourceList(ListCreateAPIView): class CredentialInputSourceList(ListCreateAPIView):
view_name = _("Credential Input Sources") name = _("Credential Input Sources")
model = models.CredentialInputSource model = models.CredentialInputSource
serializer_class = serializers.CredentialInputSourceSerializer serializer_class = serializers.CredentialInputSourceSerializer
@@ -1415,7 +1415,7 @@ class CredentialInputSourceList(ListCreateAPIView):
class CredentialInputSourceSubList(SubListCreateAPIView): class CredentialInputSourceSubList(SubListCreateAPIView):
view_name = _("Credential Input Sources") name = _("Credential Input Sources")
model = models.CredentialInputSource model = models.CredentialInputSource
serializer_class = serializers.CredentialInputSourceSerializer serializer_class = serializers.CredentialInputSourceSerializer
@@ -1430,7 +1430,7 @@ class CredentialTypeExternalTest(SubDetailAPIView):
saving it. saving it.
""" """
view_name = _('External Credential Type Test') name = _('External Credential Type Test')
model = models.CredentialType model = models.CredentialType
serializer_class = serializers.EmptySerializer serializer_class = serializers.EmptySerializer
@@ -2004,7 +2004,7 @@ class InventoryTreeView(RetrieveAPIView):
class InventoryInventorySourcesList(SubListCreateAPIView): class InventoryInventorySourcesList(SubListCreateAPIView):
view_name = _('Inventory Source List') name = _('Inventory Source List')
model = models.InventorySource model = models.InventorySource
serializer_class = serializers.InventorySourceSerializer serializer_class = serializers.InventorySourceSerializer
@@ -2016,7 +2016,7 @@ class InventoryInventorySourcesList(SubListCreateAPIView):
class InventoryInventorySourcesUpdate(RetrieveAPIView): class InventoryInventorySourcesUpdate(RetrieveAPIView):
view_name = _('Inventory Sources Update') name = _('Inventory Sources Update')
model = models.Inventory model = models.Inventory
obj_permission_type = 'start' obj_permission_type = 'start'
@@ -2079,7 +2079,7 @@ class InventorySourceDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroy
class InventorySourceSchedulesList(SubListCreateAPIView): class InventorySourceSchedulesList(SubListCreateAPIView):
view_name = _("Inventory Source Schedules") name = _("Inventory Source Schedules")
model = models.Schedule model = models.Schedule
serializer_class = serializers.ScheduleSerializer serializer_class = serializers.ScheduleSerializer
@@ -2448,7 +2448,7 @@ class JobTemplateLaunch(RetrieveAPIView):
class JobTemplateSchedulesList(SubListCreateAPIView): class JobTemplateSchedulesList(SubListCreateAPIView):
view_name = _("Job Template Schedules") name = _("Job Template Schedules")
model = models.Schedule model = models.Schedule
serializer_class = serializers.ScheduleSerializer serializer_class = serializers.ScheduleSerializer
@@ -3220,7 +3220,7 @@ class WorkflowJobTemplateJobsList(SubListAPIView):
class WorkflowJobTemplateSchedulesList(SubListCreateAPIView): class WorkflowJobTemplateSchedulesList(SubListCreateAPIView):
view_name = _("Workflow Job Template Schedules") name = _("Workflow Job Template Schedules")
model = models.Schedule model = models.Schedule
serializer_class = serializers.ScheduleSerializer serializer_class = serializers.ScheduleSerializer
@@ -3388,7 +3388,7 @@ class SystemJobTemplateLaunch(GenericAPIView):
class SystemJobTemplateSchedulesList(SubListCreateAPIView): class SystemJobTemplateSchedulesList(SubListCreateAPIView):
view_name = _("System Job Template Schedules") name = _("System Job Template Schedules")
model = models.Schedule model = models.Schedule
serializer_class = serializers.ScheduleSerializer serializer_class = serializers.ScheduleSerializer
@@ -3659,7 +3659,7 @@ class BaseJobHostSummariesList(SubListAPIView):
serializer_class = serializers.JobHostSummarySerializer serializer_class = serializers.JobHostSummarySerializer
parent_model = None # Subclasses must define this attribute. parent_model = None # Subclasses must define this attribute.
relationship = 'job_host_summaries' relationship = 'job_host_summaries'
view_name = _('Job Host Summaries List') name = _('Job Host Summaries List')
search_fields = ('host_name',) search_fields = ('host_name',)
def get_queryset(self): def get_queryset(self):
@@ -3708,7 +3708,7 @@ class JobEventChildrenList(SubListAPIView):
serializer_class = serializers.JobEventSerializer serializer_class = serializers.JobEventSerializer
parent_model = models.JobEvent parent_model = models.JobEvent
relationship = 'children' relationship = 'children'
view_name = _('Job Event Children List') name = _('Job Event Children List')
search_fields = ('stdout',) search_fields = ('stdout',)
def get_queryset(self): def get_queryset(self):
@@ -3724,7 +3724,7 @@ class JobEventHostsList(HostRelatedSearchMixin, SubListAPIView):
serializer_class = serializers.HostSerializer serializer_class = serializers.HostSerializer
parent_model = models.JobEvent parent_model = models.JobEvent
relationship = 'hosts' relationship = 'hosts'
view_name = _('Job Event Hosts List') name = _('Job Event Hosts List')
class BaseJobEventsList(SubListAPIView): class BaseJobEventsList(SubListAPIView):
@@ -3733,7 +3733,7 @@ class BaseJobEventsList(SubListAPIView):
serializer_class = serializers.JobEventSerializer serializer_class = serializers.JobEventSerializer
parent_model = None # Subclasses must define this attribute. parent_model = None # Subclasses must define this attribute.
relationship = 'job_events' relationship = 'job_events'
view_name = _('Job Events List') name = _('Job Events List')
search_fields = ('stdout',) search_fields = ('stdout',)
def finalize_response(self, request, response, *args, **kwargs): def finalize_response(self, request, response, *args, **kwargs):
@@ -3942,7 +3942,7 @@ class BaseAdHocCommandEventsList(SubListAPIView):
serializer_class = serializers.AdHocCommandEventSerializer serializer_class = serializers.AdHocCommandEventSerializer
parent_model = None # Subclasses must define this attribute. parent_model = None # Subclasses must define this attribute.
relationship = 'ad_hoc_command_events' relationship = 'ad_hoc_command_events'
view_name = _('Ad Hoc Command Events List') name = _('Ad Hoc Command Events List')
search_fields = ('stdout',) search_fields = ('stdout',)
@@ -4100,7 +4100,7 @@ class UnifiedJobStdout(RetrieveAPIView):
# Remove any ANSI escape sequences containing job event data. # 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) 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 = { context = {
'title': get_view_name(self.__class__), 'title': get_view_name(self.__class__),
@@ -4195,7 +4195,7 @@ class NotificationTemplateDetail(RetrieveUpdateDestroyAPIView):
class NotificationTemplateTest(GenericAPIView): class NotificationTemplateTest(GenericAPIView):
'''Test a Notification Template''' '''Test a Notification Template'''
view_name = _('Notification Template Test') name = _('Notification Template Test')
model = models.NotificationTemplate model = models.NotificationTemplate
obj_permission_type = 'start' obj_permission_type = 'start'
serializer_class = serializers.EmptySerializer serializer_class = serializers.EmptySerializer

View File

@@ -60,7 +60,7 @@ class InventoryUpdateEventsList(SubListAPIView):
serializer_class = InventoryUpdateEventSerializer serializer_class = InventoryUpdateEventSerializer
parent_model = InventoryUpdate parent_model = InventoryUpdate
relationship = 'inventory_update_events' relationship = 'inventory_update_events'
view_name = _('Inventory Update Events List') name = _('Inventory Update Events List')
search_fields = ('stdout',) search_fields = ('stdout',)
def finalize_response(self, request, response, *args, **kwargs): def finalize_response(self, request, response, *args, **kwargs):

View File

@@ -27,7 +27,7 @@ logger = logging.getLogger('awx.main.analytics')
class MetricsView(APIView): class MetricsView(APIView):
view_name = _('Metrics') name = _('Metrics')
swagger_topic = 'Metrics' swagger_topic = 'Metrics'
renderer_classes = [renderers.PlainTextRenderer, renderer_classes = [renderers.PlainTextRenderer,

View File

@@ -42,7 +42,7 @@ logger = logging.getLogger('awx.api.views.root')
class ApiRootView(APIView): class ApiRootView(APIView):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
view_name = _('REST API') name = _('REST API')
versioning_class = None versioning_class = None
swagger_topic = 'Versioning' swagger_topic = 'Versioning'
@@ -64,7 +64,7 @@ class ApiRootView(APIView):
class ApiOAuthAuthorizationRootView(APIView): class ApiOAuthAuthorizationRootView(APIView):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
view_name = _("API OAuth 2 Authorization Root") name = _("API OAuth 2 Authorization Root")
versioning_class = None versioning_class = None
swagger_topic = 'Authentication' swagger_topic = 'Authentication'
@@ -130,7 +130,7 @@ class ApiVersionRootView(APIView):
class ApiV2RootView(ApiVersionRootView): class ApiV2RootView(ApiVersionRootView):
view_name = _('Version 2') name = _('Version 2')
class ApiV2PingView(APIView): class ApiV2PingView(APIView):
@@ -139,7 +139,7 @@ class ApiV2PingView(APIView):
""" """
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
authentication_classes = () authentication_classes = ()
view_name = _('Ping') name = _('Ping')
swagger_topic = 'System Configuration' swagger_topic = 'System Configuration'
def get(self, request, format=None): def get(self, request, format=None):
@@ -171,7 +171,7 @@ class ApiV2PingView(APIView):
class ApiV2ConfigView(APIView): class ApiV2ConfigView(APIView):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
view_name = _('Configuration') name = _('Configuration')
swagger_topic = 'System Configuration' swagger_topic = 'System Configuration'
def check_permissions(self, request): def check_permissions(self, request):

View File

@@ -41,7 +41,7 @@ class SettingCategoryList(ListAPIView):
model = Setting # Not exactly, but needed for the view. model = Setting # Not exactly, but needed for the view.
serializer_class = SettingCategorySerializer serializer_class = SettingCategorySerializer
filter_backends = [] filter_backends = []
view_name = _('Setting Categories') name = _('Setting Categories')
def get_queryset(self): def get_queryset(self):
setting_categories = [] setting_categories = []
@@ -63,7 +63,7 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
model = Setting # Not exactly, but needed for the view. model = Setting # Not exactly, but needed for the view.
serializer_class = SettingSingletonSerializer serializer_class = SettingSingletonSerializer
filter_backends = [] filter_backends = []
view_name = _('Setting Detail') name = _('Setting Detail')
def get_queryset(self): def get_queryset(self):
self.category_slug = self.kwargs.get('category_slug', 'all') self.category_slug = self.kwargs.get('category_slug', 'all')
@@ -154,7 +154,7 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
class SettingLoggingTest(GenericAPIView): class SettingLoggingTest(GenericAPIView):
view_name = _('Logging Connectivity Test') name = _('Logging Connectivity Test')
model = Setting model = Setting
serializer_class = SettingSingletonSerializer serializer_class = SettingSingletonSerializer
permission_classes = (IsSuperUser,) permission_classes = (IsSuperUser,)

View File

@@ -4,7 +4,11 @@ import json
import os import os
from django.conf import settings 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(): def test_python_and_js_licenses():

View File

@@ -20,10 +20,8 @@ class ApiErrorView(views.APIView):
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
metadata_class = None metadata_class = None
exception_class = exceptions.APIException exception_class = exceptions.APIException
view_name = _('API Error')
def get_view_name(self): name = _('API Error')
return self.view_name
def finalize_response(self, request, response, *args, **kwargs): def finalize_response(self, request, response, *args, **kwargs):
response = super(ApiErrorView, self).finalize_response(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): class APIException(exceptions.APIException):
status_code = status status_code = status
default_detail = kwargs['content'] 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) response = api_error_view(request)
if hasattr(response, 'render'): if hasattr(response, 'render'):
response.render() response.render()

View File

@@ -307,7 +307,6 @@ REST_FRAMEWORK = {
), ),
'DEFAULT_METADATA_CLASS': 'awx.api.metadata.Metadata', 'DEFAULT_METADATA_CLASS': 'awx.api.metadata.Metadata',
'EXCEPTION_HANDLER': 'awx.api.views.api_exception_handler', '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', 'VIEW_DESCRIPTION_FUNCTION': 'awx.api.generics.get_view_description',
'NON_FIELD_ERRORS_KEY': '__all__', 'NON_FIELD_ERRORS_KEY': '__all__',
'DEFAULT_VERSION': 'v2', 'DEFAULT_VERSION': 'v2',

View File

@@ -1,4 +1,5 @@
import collections import collections
import copy
import inspect import inspect
import json import json
import re import re
@@ -8,8 +9,8 @@ import ldap
import awx import awx
# Django # Django
from django.utils import six
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
# Django Auth LDAP # Django Auth LDAP
import django_auth_ldap.config import django_auth_ldap.config
@@ -18,7 +19,8 @@ from django_auth_ldap.config import (
LDAPSearchUnion, 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 # This must be imported so get_subclasses picks it up
from awx.sso.ldap_group_types import PosixUIDGroupType # noqa from awx.sso.ldap_group_types import PosixUIDGroupType # noqa
@@ -74,6 +76,71 @@ class DependsOnMixin():
return res 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): class AuthenticationBackendsField(fields.StringListField):
# Mapping of settings that must be set in order to enable each # Mapping of settings that must be set in order to enable each
@@ -459,70 +526,14 @@ class LDAPDNMapField(fields.StringListBooleanField):
child = LDAPDNField() child = LDAPDNField()
class BaseDictWithChildField(fields.DictField): class LDAPSingleOrganizationMapField(HybridDictField):
default_error_messages = { admins = LDAPDNMapField(allow_null=True, required=False)
'missing_keys': _('Missing key(s): {missing_keys}.'), users = LDAPDNMapField(allow_null=True, required=False)
'invalid_keys': _('Invalid key(s): {invalid_keys}.'), remove_admins = fields.BooleanField(required=False)
} remove_users = fields.BooleanField(required=False)
child_fields = {
# 'key': fields.ChildField(),
}
allow_unknown_keys = False
def __init__(self, *args, **kwargs): child = _Forbidden()
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),
}
class LDAPOrganizationMapField(fields.DictField): class LDAPOrganizationMapField(fields.DictField):
@@ -530,17 +541,13 @@ class LDAPOrganizationMapField(fields.DictField):
child = LDAPSingleOrganizationMapField() child = LDAPSingleOrganizationMapField()
class LDAPSingleTeamMapField(BaseDictWithChildField): class LDAPSingleTeamMapField(HybridDictField):
default_error_messages = { organization = fields.CharField()
'missing_keys': _('Missing required key for team map: {invalid_keys}.'), users = LDAPDNMapField(allow_null=True, required=False)
'invalid_keys': _('Invalid key(s) for team map: {invalid_keys}.'), remove = fields.BooleanField(required=False)
}
child_fields = { child = _Forbidden()
'organization': fields.CharField(),
'users': LDAPDNMapField(allow_null=True, required=False),
'remove': fields.BooleanField(required=False),
}
class LDAPTeamMapField(fields.DictField): class LDAPTeamMapField(fields.DictField):
@@ -614,17 +621,14 @@ class SocialMapField(fields.ListField):
self.fail('type_error', input_type=type(data)) self.fail('type_error', input_type=type(data))
class SocialSingleOrganizationMapField(BaseDictWithChildField): class SocialSingleOrganizationMapField(HybridDictField):
default_error_messages = { admins = SocialMapField(allow_null=True, required=False)
'invalid_keys': _('Invalid key(s) for organization map: {invalid_keys}.'), users = SocialMapField(allow_null=True, required=False)
} remove_admins = fields.BooleanField(required=False)
child_fields = { remove_users = fields.BooleanField(required=False)
'admins': SocialMapField(allow_null=True, required=False),
'users': SocialMapField(allow_null=True, required=False), child = _Forbidden()
'remove_admins': fields.BooleanField(required=False),
'remove_users': fields.BooleanField(required=False),
}
class SocialOrganizationMapField(fields.DictField): class SocialOrganizationMapField(fields.DictField):
@@ -632,17 +636,13 @@ class SocialOrganizationMapField(fields.DictField):
child = SocialSingleOrganizationMapField() child = SocialSingleOrganizationMapField()
class SocialSingleTeamMapField(BaseDictWithChildField): class SocialSingleTeamMapField(HybridDictField):
default_error_messages = { organization = fields.CharField()
'missing_keys': _('Missing required key for team map: {missing_keys}.'), users = SocialMapField(allow_null=True, required=False)
'invalid_keys': _('Invalid key(s) for team map: {invalid_keys}.'), remove = fields.BooleanField(required=False)
}
child_fields = { child = _Forbidden()
'organization': fields.CharField(),
'users': SocialMapField(allow_null=True, required=False),
'remove': fields.BooleanField(required=False),
}
class SocialTeamMapField(fields.DictField): class SocialTeamMapField(fields.DictField):
@@ -650,17 +650,11 @@ class SocialTeamMapField(fields.DictField):
child = SocialSingleTeamMapField() child = SocialSingleTeamMapField()
class SAMLOrgInfoValueField(BaseDictWithChildField): class SAMLOrgInfoValueField(HybridDictField):
default_error_messages = { name = fields.CharField()
'missing_keys': _('Missing required key(s) for org info record: {missing_keys}.'), displayname = fields.CharField()
} url = fields.URLField()
child_fields = {
'name': fields.CharField(),
'displayname': fields.CharField(),
'url': fields.URLField(),
}
allow_unknown_keys = True
class SAMLOrgInfoField(fields.DictField): class SAMLOrgInfoField(fields.DictField):
@@ -683,34 +677,22 @@ class SAMLOrgInfoField(fields.DictField):
return data return data
class SAMLContactField(BaseDictWithChildField): class SAMLContactField(HybridDictField):
default_error_messages = { givenName = fields.CharField()
'missing_keys': _('Missing required key(s) for contact: {missing_keys}.'), emailAddress = fields.EmailField()
}
child_fields = {
'givenName': fields.CharField(),
'emailAddress': fields.EmailField(),
}
allow_unknown_keys = True
class SAMLIdPField(BaseDictWithChildField): class SAMLIdPField(HybridDictField):
default_error_messages = { entity_id = fields.CharField()
'missing_keys': _('Missing required key(s) for IdP: {missing_keys}.'), url = fields.URLField()
} x509cert = fields.CharField(validators=[validate_certificate])
child_fields = { attr_user_permanent_id = fields.CharField(required=False)
'entity_id': fields.CharField(), attr_first_name = fields.CharField(required=False)
'url': fields.URLField(), attr_last_name = fields.CharField(required=False)
'x509cert': fields.CharField(validators=[validate_certificate]), attr_username = fields.CharField(required=False)
'attr_user_permanent_id': fields.CharField(required=False), attr_email = 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
class SAMLEnabledIdPsField(fields.DictField): class SAMLEnabledIdPsField(fields.DictField):
@@ -718,52 +700,49 @@ class SAMLEnabledIdPsField(fields.DictField):
child = SAMLIdPField() child = SAMLIdPField()
class SAMLSecurityField(BaseDictWithChildField): class SAMLSecurityField(HybridDictField):
child_fields = { nameIdEncrypted = fields.BooleanField(required=False)
'nameIdEncrypted': fields.BooleanField(required=False), authnRequestsSigned = fields.BooleanField(required=False)
'authnRequestsSigned': fields.BooleanField(required=False), logoutRequestSigned = fields.BooleanField(required=False)
'logoutRequestSigned': fields.BooleanField(required=False), logoutResponseSigned = fields.BooleanField(required=False)
'logoutResponseSigned': fields.BooleanField(required=False), signMetadata = fields.BooleanField(required=False)
'signMetadata': fields.BooleanField(required=False), wantMessagesSigned = fields.BooleanField(required=False)
'wantMessagesSigned': fields.BooleanField(required=False), wantAssertionsSigned = fields.BooleanField(required=False)
'wantAssertionsSigned': fields.BooleanField(required=False), wantAssertionsEncrypted = fields.BooleanField(required=False)
'wantAssertionsEncrypted': fields.BooleanField(required=False), wantNameId = fields.BooleanField(required=False)
'wantNameId': fields.BooleanField(required=False), wantNameIdEncrypted = fields.BooleanField(required=False)
'wantNameIdEncrypted': fields.BooleanField(required=False), wantAttributeStatement = fields.BooleanField(required=False)
'wantAttributeStatement': fields.BooleanField(required=False), requestedAuthnContext = fields.StringListBooleanField(required=False)
'requestedAuthnContext': fields.StringListBooleanField(required=False), requestedAuthnContextComparison = fields.CharField(required=False)
'requestedAuthnContextComparison': fields.CharField(required=False), metadataValidUntil = fields.CharField(allow_null=True, required=False)
'metadataValidUntil': fields.CharField(allow_null=True, required=False), metadataCacheDuration = fields.CharField(allow_null=True, required=False)
'metadataCacheDuration': fields.CharField(allow_null=True, required=False), signatureAlgorithm = fields.CharField(allow_null=True, required=False)
'signatureAlgorithm': fields.CharField(allow_null=True, required=False), digestAlgorithm = fields.CharField(allow_null=True, required=False)
'digestAlgorithm': fields.CharField(allow_null=True, required=False),
}
allow_unknown_keys = True
class SAMLOrgAttrField(BaseDictWithChildField): class SAMLOrgAttrField(HybridDictField):
child_fields = { remove = fields.BooleanField(required=False)
'remove': fields.BooleanField(required=False), saml_attr = fields.CharField(required=False, allow_null=True)
'saml_attr': fields.CharField(required=False, allow_null=True), remove_admins = fields.BooleanField(required=False)
'remove_admins': fields.BooleanField(required=False), saml_admin_attr = fields.CharField(required=False, allow_null=True)
'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)
'team': fields.CharField(required=True, allow_null=False), organization = 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)
'team_org_map': fields.ListField(required=False, child=SAMLTeamAttrTeamOrgMapField(), allow_null=True), remove = fields.BooleanField(required=False)
'remove': fields.BooleanField(required=False), saml_attr = fields.CharField(required=False, allow_null=True)
'saml_attr': fields.CharField(required=False, allow_null=True),
} child = _Forbidden()

View File

@@ -33,21 +33,23 @@ class TestSAMLOrgAttrField():
@pytest.mark.parametrize("data, expected", [ @pytest.mark.parametrize("data, expected", [
({'remove': 'blah', 'saml_attr': 'foobar'}, ({'remove': 'blah', 'saml_attr': 'foobar'},
ValidationError('"blah" is not a valid boolean.')), {'remove': ['Must be a valid boolean.']}),
({'remove': True, 'saml_attr': False}, ({'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'}, ({'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}, ({'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'}, ({'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): def test_internal_value_invalid(self, data, expected):
field = SAMLOrgAttrField() field = SAMLOrgAttrField()
with pytest.raises(type(expected)) as e: with pytest.raises(ValidationError) as e:
field.to_internal_value(data) field.to_internal_value(data)
assert str(e.value) == str(expected) assert e.value.detail == expected
class TestSAMLTeamAttrField(): class TestSAMLTeamAttrField():
@@ -77,36 +79,38 @@ class TestSAMLTeamAttrField():
@pytest.mark.parametrize("data, expected", [ @pytest.mark.parametrize("data, expected", [
({'remove': True, 'saml_attr': 'foobar', 'team_org_map': [ ({'remove': True, 'saml_attr': 'foobar', 'team_org_map': [
{'team': 'foobar', 'not_a_valid_key': 'blah', 'organization': 'Ansible'}, {'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': [ ({'remove': False, 'saml_attr': 'foobar', 'team_org_map': [
{'organization': 'Ansible'}, {'organization': 'Ansible'},
]}, ValidationError('Missing key(s): "team".')), ]}, {'team_org_map': {0: {'team': ['This field is required.']}}}),
({'remove': False, 'saml_attr': 'foobar', 'team_org_map': [ ({'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): def test_internal_value_invalid(self, data, expected):
field = SAMLTeamAttrField() field = SAMLTeamAttrField()
with pytest.raises(type(expected)) as e: with pytest.raises(ValidationError) as e:
field.to_internal_value(data) field.to_internal_value(data)
assert str(e.value) == str(expected) assert e.value.detail == expected
class TestLDAPGroupTypeParamsField(): class TestLDAPGroupTypeParamsField():
@pytest.mark.parametrize("group_type, data, expected", [ @pytest.mark.parametrize("group_type, data, expected", [
('LDAPGroupType', {'name_attr': 'user', 'bob': ['a', 'b'], 'scooter': 'hello'}, ('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'}, ('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', ('PosixUIDGroupType', {'name_attr': 'user', 'member_attr': 'west', 'ldap_group_user_attr': 'legacyThing',
'bob': ['a', 'b'], 'scooter': 'hello'}, '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): def test_internal_value_invalid(self, group_type, data, expected):
field = LDAPGroupTypeParamsField() field = LDAPGroupTypeParamsField()
field.get_depends_on = mock.MagicMock(return_value=group_type) 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) field.to_internal_value(data)
assert str(e.value) == str(expected) assert e.value.detail == expected

View File

@@ -1,5 +1,5 @@
{% extends 'rest_framework/base.html' %} {% extends 'rest_framework/base.html' %}
{% load i18n staticfiles %} {% load i18n static %}
{% block title %}{{ name }} &middot; {% trans 'AWX REST API' %}{% endblock %} {% block title %}{{ name }} &middot; {% trans 'AWX REST API' %}{% endblock %}
@@ -48,6 +48,15 @@
</div> </div>
{% endblock %} {% endblock %}
{% block content %}
{% if deprecated %}
<div class="breadcrumb" style="background: #FCC;">
<b>This resource has been deprecated and will be removed in a future release.</b>
</div>
{% endif %}
{{ block.super }}
{% endblock content %}
{% block script %} {% block script %}
<div id="footer"> <div id="footer">
<div class="container"> <div class="container">

View File

@@ -1,300 +0,0 @@
{# Copy of base.html from rest_framework with minor AWX change. #}
{% load staticfiles %}
{% load i18n %}
{% 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 %}{% if name %}{{ name }} {% endif %}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>
{% block body %}
<body class="{% block bodyclass %}{% endblock %}">
<div class="wrapper">
{% block navbar %}
<div class="navbar navbar-static-top {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}"
role="navigation" aria-label="{% trans "navbar" %}">
<div class="container">
<span>
{% block branding %}
<a class='navbar-brand' rel="nofollow" href='http://www.django-rest-framework.org'>
Django REST framework
</a>
{% endblock %}
</span>
<ul class="nav navbar-nav pull-right">
{% block userlinks %}
{% if user.is_authenticated %}
{% optional_logout request user %}
{% else %}
{% optional_login request %}
{% endif %}
{% endblock %}
</ul>
</div>
</div>
{% endblock %}
<div class="container">
{% block breadcrumbs %}
<ul class="breadcrumb">
{% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
{% if forloop.last %}
<li class="active"><a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a></li>
{% else %}
<li><a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a></li>
{% endif %}
{% empty %}
{% block breadcrumbs_empty %}&nbsp;{% endblock breadcrumbs_empty %}
{% endfor %}
</ul>
{% endblock %}
<!-- Content -->
<div id="content" role="main" aria-label="{% trans "content" %}">
{% if deprecated %}
<div class="breadcrumb" style="background: #FCC;">
<b>This resource has been deprecated and will be removed in a future release.</b>
</div>
{% endif %}
{% block content %}
<div class="region" aria-label="{% trans "request form" %}">
{% if 'GET' in allowed_methods %}
<form id="get-form" class="pull-right">
<fieldset>
{% if api_settings.URL_FORMAT_OVERRIDE %}
<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>
{% else %}
<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>
{% endif %}
</fieldset>
</form>
{% endif %}
{% if options_form %}
<form class="button-form" action="{{ request.get_full_path }}" data-method="OPTIONS">
<button class="btn btn-primary js-tooltip" title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button>
</form>
{% endif %}
{% if delete_form %}
<button class="btn btn-danger button-form js-tooltip" title="Make a DELETE request on the {{ name }} resource" data-toggle="modal" data-target="#deleteModal">DELETE</button>
<!-- Delete Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
<h4 class="text-center">Are you sure you want to delete this {{ name }}?</h4>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<form class="button-form" action="{{ request.get_full_path }}" data-method="DELETE">
<button class="btn btn-danger">Delete</button>
</form>
</div>
</div>
</div>
</div>
{% endif %}
{% if filter_form %}
<button style="float: right; margin-right: 10px" data-toggle="modal" data-target="#filtersModal" class="btn btn-default">
<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span>
{% trans "Filters" %}
</button>
{% endif %}
</div>
<div class="content-main" role="main" aria-label="{% trans "main content" %}">
<div class="page-header">
<h1>{{ name }}</h1>
</div>
<div style="float:left">
{% block description %}
{{ description }}
{% endblock %}
</div>
{% if paginator %}
<nav style="float: right">
{% get_pagination_html paginator %}
</nav>
{% endif %}
<div class="request-info" style="clear: both" aria-label="{% trans "request info" %}">
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
</div>
<div class="response-info" aria-label="{% trans "response info" %}">
<pre class="prettyprint"><span class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}{% if response_headers %}{% for key, val in response_headers|items %}
<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>{% endfor %}{% endif %}
{# Original line below had the side effect of also escaping content: #}
{# </span>{{ content|urlize_quoted_links }}</pre>{% endautoescape %} #}
{# For AWX, disable automatic URL creation and move content outside of autoescape off block. #}
{% endautoescape %}</span>{{ content }}</pre>
</div>
</div>
{% if display_edit_forms %}
{% 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="#post-object-form" data-toggle="tab">HTML form</a>
</li>
<li>
<a name='raw-tab' href="#post-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="post-object-form">
{% with form=post_form %}
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate>
<fieldset>
{% csrf_token %}
{{ 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="post-generic-content-form">
{% with form=raw_data_post_form %}
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
<fieldset>
{% include "rest_framework/raw_data_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="#put-object-form" data-toggle="tab">HTML form</a>
</li>
<li>
<a name='raw-tab' href="#put-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="put-object-form">
<form action="{{ request.get_full_path }}" data-method="PUT" enctype="multipart/form-data" class="form-horizontal" novalidate>
<fieldset>
{{ put_form }}
<div class="form-actions">
<button class="btn btn-primary js-tooltip" 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="put-generic-content-form">
{% with form=raw_data_put_or_patch_form %}
<form action="{{ request.get_full_path }}" data-method="PUT" class="form-horizontal">
<fieldset>
{% include "rest_framework/raw_data_form.html" %}
<div class="form-actions">
{% if raw_data_put_form %}
<button class="btn btn-primary js-tooltip" title="Make a PUT request on the {{ name }} resource">PUT</button>
{% endif %}
{% if raw_data_patch_form %}
<button data-method="PATCH" class="btn btn-primary js-tooltip" title="Make a PATCH request on the {{ name }} resource">PATCH</button>
{% endif %}
</div>
</fieldset>
</form>
{% endwith %}
</div>
</div>
</div>
{% endif %}
{% endif %}
{% endblock content %}
</div><!-- /.content -->
</div><!-- /.container -->
</div><!-- ./wrapper -->
{% if filter_form %}
{{ filter_form }}
{% endif %}
{% block script %}
<script>
window.drf = {
csrfHeaderName: "{{ csrf_header_name|default:'X-CSRFToken' }}",
csrfCookieName: "{{ csrf_cookie_name|default:'csrftoken' }}"
};
</script>
<script src="{% static "rest_framework/js/jquery-1.12.4.min.js" %}"></script>
<script src="{% static "rest_framework/js/ajax-form.js" %}"></script>
<script src="{% static "rest_framework/js/csrf.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>
<script>
$(document).ready(function() {
$('form').ajaxForm();
});
</script>
{% endblock %}
</body>
{% endblock %}
</html>

View File

@@ -20,7 +20,7 @@ django-radius==1.3.3
django-solo==1.1.3 django-solo==1.1.3
django-split-settings==0.3.0 django-split-settings==0.3.0
django-taggit==0.22.2 django-taggit==0.22.2
djangorestframework==3.7.7 djangorestframework==3.9.4
djangorestframework-yaml==1.0.3 djangorestframework-yaml==1.0.3
irc==16.2 irc==16.2
jinja2==2.10.1 jinja2==2.10.1

View File

@@ -13,9 +13,9 @@ asgi-amqp==1.1.3
asgiref==1.1.2 # via asgi-amqp, channels, daphne asgiref==1.1.2 # via asgi-amqp, channels, daphne
asn1crypto==0.24.0 # via cryptography asn1crypto==0.24.0 # via cryptography
attrs==19.1.0 # via automat, service-identity, twisted attrs==19.1.0 # via automat, service-identity, twisted
autobahn==19.3.3 # via daphne autobahn==19.5.1 # via daphne
automat==0.7.0 # via twisted automat==0.7.0 # via twisted
azure-common==1.1.20 # via azure-keyvault azure-common==1.1.21 # via azure-keyvault
azure-keyvault==1.1.0 azure-keyvault==1.1.0
azure-nspkg==3.0.2 # via azure-keyvault azure-nspkg==3.0.2 # via azure-keyvault
billiard==3.6.0.0 # via celery billiard==3.6.0.0 # via celery
@@ -43,7 +43,7 @@ django-split-settings==0.3.0
django-taggit==0.22.2 django-taggit==0.22.2
django==1.11.20 django==1.11.20
djangorestframework-yaml==1.0.3 djangorestframework-yaml==1.0.3
djangorestframework==3.7.7 djangorestframework==3.9.4
future==0.16.0 # via django-radius future==0.16.0 # via django-radius
hyperlink==19.0.0 # via twisted hyperlink==19.0.0 # via twisted
idna==2.8 # via hyperlink, requests, twisted idna==2.8 # via hyperlink, requests, twisted
@@ -59,7 +59,7 @@ jaraco.logging==2.0 # via irc
jaraco.stream==2.0 # via irc jaraco.stream==2.0 # via irc
jaraco.text==3.0 # via irc, jaraco.collections jaraco.text==3.0 # via irc, jaraco.collections
jinja2==2.10.1 jinja2==2.10.1
jsonpickle==1.1 # via asgi-amqp jsonpickle==1.2 # via asgi-amqp
jsonschema==2.6.0 jsonschema==2.6.0
kombu==4.5.0 # via asgi-amqp, celery kombu==4.5.0 # via asgi-amqp, celery
lockfile==0.12.2 # via python-daemon lockfile==0.12.2 # via python-daemon
@@ -88,7 +88,7 @@ pyjwt==1.7.1 # via adal, social-auth-core, twilio
pyopenssl==19.0.0 # via twisted pyopenssl==19.0.0 # via twisted
pyparsing==2.2.0 pyparsing==2.2.0
pyrad==2.1 # via django-radius pyrad==2.1 # via django-radius
pysocks==1.6.8 # via twilio pysocks==1.7.0 # via twilio
python-daemon==2.2.3 # via ansible-runner python-daemon==2.2.3 # via ansible-runner
python-dateutil==2.7.2 python-dateutil==2.7.2
python-ldap==3.2.0 # via django-auth-ldap python-ldap==3.2.0 # via django-auth-ldap