mirror of
https://github.com/ZwareBear/awx.git
synced 2026-04-23 00:11:48 -05:00
Merge remote-tracking branch 'tower/release_3.2.2' into devel
This commit is contained in:
@@ -166,7 +166,13 @@ class FieldLookupBackend(BaseFilterBackend):
|
||||
elif isinstance(field, models.BooleanField):
|
||||
return to_python_boolean(value)
|
||||
elif isinstance(field, (ForeignObjectRel, ManyToManyField, GenericForeignKey, ForeignKey)):
|
||||
return self.to_python_related(value)
|
||||
try:
|
||||
return self.to_python_related(value)
|
||||
except ValueError:
|
||||
raise ParseError(_('Invalid {field_name} id: {field_id}').format(
|
||||
field_name=getattr(field, 'name', 'related field'),
|
||||
field_id=value)
|
||||
)
|
||||
else:
|
||||
return field.to_python(value)
|
||||
|
||||
@@ -243,11 +249,10 @@ class FieldLookupBackend(BaseFilterBackend):
|
||||
# Search across related objects.
|
||||
if key.endswith('__search'):
|
||||
for value in values:
|
||||
for search_term in force_text(value).replace(',', ' ').split():
|
||||
search_value, new_keys = self.value_to_python(queryset.model, key, search_term)
|
||||
assert isinstance(new_keys, list)
|
||||
for new_key in new_keys:
|
||||
search_filters.append((new_key, search_value))
|
||||
search_value, new_keys = self.value_to_python(queryset.model, key, force_text(value))
|
||||
assert isinstance(new_keys, list)
|
||||
for new_key in new_keys:
|
||||
search_filters.append((new_key, search_value))
|
||||
continue
|
||||
|
||||
# Custom chain__ and or__ filters, mutually exclusive (both can
|
||||
|
||||
@@ -21,7 +21,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
# Django REST Framework
|
||||
from rest_framework.authentication import get_authorization_header
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.exceptions import PermissionDenied, AuthenticationFailed
|
||||
from rest_framework import generics
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
@@ -38,9 +38,10 @@ from awx.api.metadata import SublistAttachDetatchMetadata
|
||||
|
||||
__all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
|
||||
'ListCreateAPIView', 'SubListAPIView', 'SubListCreateAPIView',
|
||||
'SubListDestroyAPIView',
|
||||
'SubListCreateAttachDetachAPIView', 'RetrieveAPIView',
|
||||
'RetrieveUpdateAPIView', 'RetrieveDestroyAPIView',
|
||||
'RetrieveUpdateDestroyAPIView', 'DestroyAPIView',
|
||||
'RetrieveUpdateDestroyAPIView',
|
||||
'SubDetailAPIView',
|
||||
'ResourceAccessList',
|
||||
'ParentMixin',
|
||||
@@ -115,6 +116,10 @@ class APIView(views.APIView):
|
||||
|
||||
drf_request = super(APIView, self).initialize_request(request, *args, **kwargs)
|
||||
request.drf_request = drf_request
|
||||
try:
|
||||
request.drf_request_user = getattr(drf_request, 'user', False)
|
||||
except AuthenticationFailed:
|
||||
request.drf_request_user = None
|
||||
return drf_request
|
||||
|
||||
def finalize_response(self, request, response, *args, **kwargs):
|
||||
@@ -140,7 +145,6 @@ class APIView(views.APIView):
|
||||
response['X-API-Query-Count'] = len(q_times)
|
||||
response['X-API-Query-Time'] = '%0.3fs' % sum(q_times)
|
||||
|
||||
analytics_logger.info("api response", extra=dict(python_objects=dict(request=request, response=response)))
|
||||
return response
|
||||
|
||||
def get_authenticate_header(self, request):
|
||||
@@ -442,6 +446,41 @@ class SubListAPIView(ParentMixin, ListAPIView):
|
||||
return qs & sublist_qs
|
||||
|
||||
|
||||
class DestroyAPIView(generics.DestroyAPIView):
|
||||
|
||||
def has_delete_permission(self, obj):
|
||||
return self.request.user.can_access(self.model, 'delete', obj)
|
||||
|
||||
def perform_destroy(self, instance, check_permission=True):
|
||||
if check_permission and not self.has_delete_permission(instance):
|
||||
raise PermissionDenied()
|
||||
super(DestroyAPIView, self).perform_destroy(instance)
|
||||
|
||||
|
||||
class SubListDestroyAPIView(DestroyAPIView, SubListAPIView):
|
||||
"""
|
||||
Concrete view for deleting everything related by `relationship`.
|
||||
"""
|
||||
check_sub_obj_permission = True
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
instance_list = self.get_queryset()
|
||||
if (not self.check_sub_obj_permission and
|
||||
not request.user.can_access(self.parent_model, 'delete', self.get_parent_object())):
|
||||
raise PermissionDenied()
|
||||
self.perform_list_destroy(instance_list)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def perform_list_destroy(self, instance_list):
|
||||
if self.check_sub_obj_permission:
|
||||
# Check permissions for all before deleting, avoiding half-deleted lists
|
||||
for instance in instance_list:
|
||||
if self.has_delete_permission(instance):
|
||||
raise PermissionDenied()
|
||||
for instance in instance_list:
|
||||
self.perform_destroy(instance, check_permission=False)
|
||||
|
||||
|
||||
class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
|
||||
# Base class for a sublist view that allows for creating subobjects
|
||||
# associated with the parent object.
|
||||
@@ -680,22 +719,11 @@ class RetrieveUpdateAPIView(RetrieveAPIView, generics.RetrieveUpdateAPIView):
|
||||
pass
|
||||
|
||||
|
||||
class RetrieveDestroyAPIView(RetrieveAPIView, generics.RetrieveDestroyAPIView):
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
# somewhat lame that delete has to call it's own permissions check
|
||||
obj = self.get_object()
|
||||
if not request.user.can_access(self.model, 'delete', obj):
|
||||
raise PermissionDenied()
|
||||
obj.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class RetrieveUpdateDestroyAPIView(RetrieveUpdateAPIView, RetrieveDestroyAPIView):
|
||||
class RetrieveDestroyAPIView(RetrieveAPIView, DestroyAPIView):
|
||||
pass
|
||||
|
||||
|
||||
class DestroyAPIView(GenericAPIView, generics.DestroyAPIView):
|
||||
class RetrieveUpdateDestroyAPIView(RetrieveUpdateAPIView, DestroyAPIView):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -345,7 +345,9 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
continue
|
||||
summary_fields[fk] = OrderedDict()
|
||||
for field in related_fields:
|
||||
if field == 'credential_type_id' and fk == 'credential' and self.version < 2: # TODO: remove version check in 3.3
|
||||
if (
|
||||
self.version < 2 and field == 'credential_type_id' and
|
||||
fk in ['credential', 'vault_credential']): # TODO: remove version check in 3.3
|
||||
continue
|
||||
|
||||
fval = getattr(fkval, field, None)
|
||||
@@ -1111,8 +1113,13 @@ class ProjectUpdateSerializer(UnifiedJobSerializer, ProjectOptionsSerializer):
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(ProjectUpdateSerializer, self).get_related(obj)
|
||||
try:
|
||||
res.update(dict(
|
||||
project = self.reverse('api:project_detail', kwargs={'pk': obj.project.pk}),
|
||||
))
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
res.update(dict(
|
||||
project = self.reverse('api:project_detail', kwargs={'pk': obj.project.pk}),
|
||||
cancel = self.reverse('api:project_update_cancel', kwargs={'pk': obj.pk}),
|
||||
scm_inventory_updates = self.reverse('api:project_update_scm_inventory_updates', kwargs={'pk': obj.pk}),
|
||||
notifications = self.reverse('api:project_update_notifications_list', kwargs={'pk': obj.pk}),
|
||||
@@ -1726,8 +1733,15 @@ class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSeri
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(InventoryUpdateSerializer, self).get_related(obj)
|
||||
try:
|
||||
res.update(dict(
|
||||
inventory_source = self.reverse(
|
||||
'api:inventory_source_detail', kwargs={'pk': obj.inventory_source.pk}
|
||||
),
|
||||
))
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
res.update(dict(
|
||||
inventory_source = self.reverse('api:inventory_source_detail', kwargs={'pk': obj.inventory_source.pk}),
|
||||
cancel = self.reverse('api:inventory_update_cancel', kwargs={'pk': obj.pk}),
|
||||
notifications = self.reverse('api:inventory_update_notifications_list', kwargs={'pk': obj.pk}),
|
||||
))
|
||||
@@ -2125,7 +2139,7 @@ class CredentialSerializer(BaseSerializer):
|
||||
|
||||
def to_internal_value(self, data):
|
||||
# TODO: remove when API v1 is removed
|
||||
if 'credential_type' not in data:
|
||||
if 'credential_type' not in data and self.version == 1:
|
||||
# If `credential_type` is not provided, assume the payload is a
|
||||
# v1 credential payload that specifies a `kind` and a flat list
|
||||
# of field values
|
||||
@@ -2162,10 +2176,22 @@ class CredentialSerializer(BaseSerializer):
|
||||
|
||||
def validate_credential_type(self, credential_type):
|
||||
if self.instance and credential_type.pk != self.instance.credential_type.pk:
|
||||
raise ValidationError(
|
||||
_('You cannot change the credential type of the credential, as it may break the functionality'
|
||||
' of the resources using it.'),
|
||||
)
|
||||
for rel in (
|
||||
'ad_hoc_commands',
|
||||
'insights_inventories',
|
||||
'inventorysources',
|
||||
'inventoryupdates',
|
||||
'jobs',
|
||||
'jobtemplates',
|
||||
'projects',
|
||||
'projectupdates',
|
||||
'workflowjobnodes'
|
||||
):
|
||||
if getattr(self.instance, rel).count() > 0:
|
||||
raise ValidationError(
|
||||
_('You cannot change the credential type of the credential, as it may break the functionality'
|
||||
' of the resources using it.'),
|
||||
)
|
||||
return credential_type
|
||||
|
||||
|
||||
@@ -2346,14 +2372,30 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
|
||||
def get_related(self, obj):
|
||||
res = super(JobOptionsSerializer, self).get_related(obj)
|
||||
res['labels'] = self.reverse('api:job_template_label_list', kwargs={'pk': obj.pk})
|
||||
if obj.inventory:
|
||||
res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk})
|
||||
if obj.project:
|
||||
res['project'] = self.reverse('api:project_detail', kwargs={'pk': obj.project.pk})
|
||||
if obj.credential:
|
||||
res['credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.credential})
|
||||
if obj.vault_credential:
|
||||
res['vault_credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.vault_credential})
|
||||
try:
|
||||
if obj.inventory:
|
||||
res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk})
|
||||
except ObjectDoesNotExist:
|
||||
setattr(obj, 'inventory', None)
|
||||
try:
|
||||
if obj.project:
|
||||
res['project'] = self.reverse('api:project_detail', kwargs={'pk': obj.project.pk})
|
||||
except ObjectDoesNotExist:
|
||||
setattr(obj, 'project', None)
|
||||
try:
|
||||
if obj.credential:
|
||||
res['credential'] = self.reverse(
|
||||
'api:credential_detail', kwargs={'pk': obj.credential.pk}
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
setattr(obj, 'credential', None)
|
||||
try:
|
||||
if obj.vault_credential:
|
||||
res['vault_credential'] = self.reverse(
|
||||
'api:credential_detail', kwargs={'pk': obj.vault_credential.pk}
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
setattr(obj, 'vault_credential', None)
|
||||
if self.version > 1:
|
||||
if isinstance(obj, UnifiedJobTemplate):
|
||||
res['extra_credentials'] = self.reverse(
|
||||
@@ -2608,15 +2650,23 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
||||
notifications = self.reverse('api:job_notifications_list', kwargs={'pk': obj.pk}),
|
||||
labels = self.reverse('api:job_label_list', kwargs={'pk': obj.pk}),
|
||||
))
|
||||
if obj.job_template:
|
||||
res['job_template'] = self.reverse('api:job_template_detail',
|
||||
kwargs={'pk': obj.job_template.pk})
|
||||
try:
|
||||
if obj.job_template:
|
||||
res['job_template'] = self.reverse('api:job_template_detail',
|
||||
kwargs={'pk': obj.job_template.pk})
|
||||
except ObjectDoesNotExist:
|
||||
setattr(obj, 'job_template', None)
|
||||
if (obj.can_start or True) and self.version == 1: # TODO: remove in 3.3
|
||||
res['start'] = self.reverse('api:job_start', kwargs={'pk': obj.pk})
|
||||
if obj.can_cancel or True:
|
||||
res['cancel'] = self.reverse('api:job_cancel', kwargs={'pk': obj.pk})
|
||||
if obj.project_update:
|
||||
res['project_update'] = self.reverse('api:project_update_detail', kwargs={'pk': obj.project_update.pk})
|
||||
try:
|
||||
if obj.project_update:
|
||||
res['project_update'] = self.reverse(
|
||||
'api:project_update_detail', kwargs={'pk': obj.project_update.pk}
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
res['create_schedule'] = self.reverse('api:job_create_schedule', kwargs={'pk': obj.pk})
|
||||
res['relaunch'] = self.reverse('api:job_relaunch', kwargs={'pk': obj.pk})
|
||||
return res
|
||||
@@ -2756,8 +2806,10 @@ class JobRelaunchSerializer(BaseSerializer):
|
||||
|
||||
def validate(self, attrs):
|
||||
obj = self.context.get('obj')
|
||||
if not obj.credential:
|
||||
raise serializers.ValidationError(dict(credential=[_("Credential not found or deleted.")]))
|
||||
if not obj.credential and not obj.vault_credential:
|
||||
raise serializers.ValidationError(
|
||||
dict(credential=[_("Neither credential nor vault credential provided.")])
|
||||
)
|
||||
if obj.project is None:
|
||||
raise serializers.ValidationError(dict(errors=[_("Job Template Project is missing or undefined.")]))
|
||||
if obj.inventory is None or obj.inventory.pending_deletion:
|
||||
@@ -3820,6 +3872,7 @@ class InstanceSerializer(BaseSerializer):
|
||||
|
||||
class InstanceGroupSerializer(BaseSerializer):
|
||||
|
||||
committed_capacity = serializers.SerializerMethodField()
|
||||
consumed_capacity = serializers.SerializerMethodField()
|
||||
percent_capacity_remaining = serializers.SerializerMethodField()
|
||||
jobs_running = serializers.SerializerMethodField()
|
||||
@@ -3827,7 +3880,8 @@ class InstanceGroupSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = InstanceGroup
|
||||
fields = ("id", "type", "url", "related", "name", "created", "modified", "capacity", "consumed_capacity",
|
||||
fields = ("id", "type", "url", "related", "name", "created", "modified",
|
||||
"capacity", "committed_capacity", "consumed_capacity",
|
||||
"percent_capacity_remaining", "jobs_running", "instances", "controller")
|
||||
|
||||
def get_related(self, obj):
|
||||
@@ -3856,7 +3910,10 @@ class InstanceGroupSerializer(BaseSerializer):
|
||||
return self.context['capacity_map']
|
||||
|
||||
def get_consumed_capacity(self, obj):
|
||||
return self.get_capacity_dict()[obj.name]['consumed_capacity']
|
||||
return self.get_capacity_dict()[obj.name]['running_capacity']
|
||||
|
||||
def get_committed_capacity(self, obj):
|
||||
return self.get_capacity_dict()[obj.name]['committed_capacity']
|
||||
|
||||
def get_percent_capacity_remaining(self, obj):
|
||||
if not obj.capacity:
|
||||
@@ -3954,6 +4011,11 @@ class ActivityStreamSerializer(BaseSerializer):
|
||||
|
||||
if fk == 'schedule':
|
||||
rel['unified_job_template'] = thisItem.unified_job_template.get_absolute_url(self.context.get('request'))
|
||||
if obj.setting and obj.setting.get('category', None):
|
||||
rel['setting'] = self.reverse(
|
||||
'api:setting_singleton_detail',
|
||||
kwargs={'category_slug': obj.setting['category']}
|
||||
)
|
||||
return rel
|
||||
|
||||
def _get_rel(self, obj, fk):
|
||||
@@ -4005,6 +4067,8 @@ class ActivityStreamSerializer(BaseSerializer):
|
||||
username = obj.actor.username,
|
||||
first_name = obj.actor.first_name,
|
||||
last_name = obj.actor.last_name)
|
||||
if obj.setting:
|
||||
summary_fields['setting'] = [obj.setting]
|
||||
return summary_fields
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
The resulting data structure contains:
|
||||
|
||||
{
|
||||
"count": 99,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"count": 99,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
...
|
||||
]
|
||||
@@ -60,6 +60,10 @@ _Added in AWX 1.4_
|
||||
|
||||
?related__search=findme
|
||||
|
||||
Note: If you want to provide more than one search terms, please use multiple
|
||||
search fields with the same key, like `?related__search=foo&related__search=bar`,
|
||||
All search terms with the same key will be ORed together.
|
||||
|
||||
## Filtering
|
||||
|
||||
Any additional query string parameters may be used to filter the list of
|
||||
@@ -70,7 +74,7 @@ in the specified value should be url-encoded. For example:
|
||||
?field=value%20xyz
|
||||
|
||||
Fields may also span relations, only for fields and relationships defined in
|
||||
the database:
|
||||
the database:
|
||||
|
||||
?other__field=value
|
||||
|
||||
|
||||
6
awx/api/templates/api/sub_list_destroy_api_view.md
Normal file
6
awx/api/templates/api/sub_list_destroy_api_view.md
Normal file
@@ -0,0 +1,6 @@
|
||||
{% include "api/sub_list_create_api_view.md" %}
|
||||
|
||||
# Delete all {{ model_verbose_name_plural }} of this {{ parent_model_verbose_name|title }}:
|
||||
|
||||
Make a DELETE request to this resource to delete all {{ model_verbose_name_plural }} show in the list.
|
||||
The {{ parent_model_verbose_name|title }} will not be deleted by this request.
|
||||
102
awx/api/views.py
102
awx/api/views.py
@@ -14,6 +14,7 @@ import logging
|
||||
import requests
|
||||
from base64 import b64encode
|
||||
from collections import OrderedDict, Iterable
|
||||
import six
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
@@ -72,6 +73,7 @@ from awx.main.utils import (
|
||||
extract_ansible_vars,
|
||||
decrypt_field,
|
||||
)
|
||||
from awx.main.utils.encryption import encrypt_value
|
||||
from awx.main.utils.filters import SmartFilter
|
||||
from awx.main.utils.insights import filter_insights_api_response
|
||||
|
||||
@@ -1967,7 +1969,17 @@ class InventoryJobTemplateList(SubListAPIView):
|
||||
return qs.filter(inventory=parent)
|
||||
|
||||
|
||||
class HostList(ListCreateAPIView):
|
||||
class HostRelatedSearchMixin(object):
|
||||
|
||||
@property
|
||||
def related_search_fields(self):
|
||||
# Edge-case handle: https://github.com/ansible/ansible-tower/issues/7712
|
||||
ret = super(HostRelatedSearchMixin, self).related_search_fields
|
||||
ret.append('ansible_facts')
|
||||
return ret
|
||||
|
||||
|
||||
class HostList(HostRelatedSearchMixin, ListCreateAPIView):
|
||||
|
||||
always_allow_superuser = False
|
||||
model = Host
|
||||
@@ -2004,7 +2016,7 @@ class HostAnsibleFactsDetail(RetrieveAPIView):
|
||||
new_in_api_v2 = True
|
||||
|
||||
|
||||
class InventoryHostsList(SubListCreateAttachDetachAPIView):
|
||||
class InventoryHostsList(HostRelatedSearchMixin, SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Host
|
||||
serializer_class = HostSerializer
|
||||
@@ -2274,7 +2286,9 @@ class GroupPotentialChildrenList(SubListAPIView):
|
||||
return qs.exclude(pk__in=except_pks)
|
||||
|
||||
|
||||
class GroupHostsList(ControlledByScmMixin, SubListCreateAttachDetachAPIView):
|
||||
class GroupHostsList(HostRelatedSearchMixin,
|
||||
ControlledByScmMixin,
|
||||
SubListCreateAttachDetachAPIView):
|
||||
''' the list of hosts directly below a group '''
|
||||
|
||||
model = Host
|
||||
@@ -2301,7 +2315,7 @@ class GroupHostsList(ControlledByScmMixin, SubListCreateAttachDetachAPIView):
|
||||
return super(GroupHostsList, self).create(request, *args, **kwargs)
|
||||
|
||||
|
||||
class GroupAllHostsList(SubListAPIView):
|
||||
class GroupAllHostsList(HostRelatedSearchMixin, SubListAPIView):
|
||||
''' the list of all hosts below a group, even including subgroups '''
|
||||
|
||||
model = Host
|
||||
@@ -2419,6 +2433,8 @@ class InventoryScriptView(RetrieveAPIView):
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
hostname = request.query_params.get('host', '')
|
||||
hostvars = bool(request.query_params.get('hostvars', ''))
|
||||
towervars = bool(request.query_params.get('towervars', ''))
|
||||
show_all = bool(request.query_params.get('all', ''))
|
||||
if hostname:
|
||||
hosts_q = dict(name=hostname)
|
||||
@@ -2607,23 +2623,25 @@ class InventorySourceNotificationTemplatesSuccessList(InventorySourceNotificatio
|
||||
relationship = 'notification_templates_success'
|
||||
|
||||
|
||||
class InventorySourceHostsList(SubListAPIView):
|
||||
class InventorySourceHostsList(HostRelatedSearchMixin, SubListDestroyAPIView):
|
||||
|
||||
model = Host
|
||||
serializer_class = HostSerializer
|
||||
parent_model = InventorySource
|
||||
relationship = 'hosts'
|
||||
new_in_148 = True
|
||||
check_sub_obj_permission = False
|
||||
capabilities_prefetch = ['inventory.admin']
|
||||
|
||||
|
||||
class InventorySourceGroupsList(SubListAPIView):
|
||||
class InventorySourceGroupsList(SubListDestroyAPIView):
|
||||
|
||||
model = Group
|
||||
serializer_class = GroupSerializer
|
||||
parent_model = InventorySource
|
||||
relationship = 'groups'
|
||||
new_in_148 = True
|
||||
check_sub_obj_permission = False
|
||||
|
||||
|
||||
class InventorySourceUpdatesList(SubListAPIView):
|
||||
@@ -2918,13 +2936,8 @@ class JobTemplateSurveySpec(GenericAPIView):
|
||||
if not feature_enabled('surveys'):
|
||||
raise LicenseForbids(_('Your license does not allow '
|
||||
'adding surveys.'))
|
||||
survey_spec = obj.survey_spec
|
||||
for pos, field in enumerate(survey_spec.get('spec', [])):
|
||||
if field.get('type') == 'password':
|
||||
if 'default' in field and field['default']:
|
||||
field['default'] = '$encrypted$'
|
||||
|
||||
return Response(survey_spec)
|
||||
return Response(obj.display_survey_spec())
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
@@ -2937,7 +2950,14 @@ class JobTemplateSurveySpec(GenericAPIView):
|
||||
|
||||
if not request.user.can_access(self.model, 'change', obj, None):
|
||||
raise PermissionDenied()
|
||||
new_spec = request.data
|
||||
response = self._validate_spec_data(request.data, obj.survey_spec)
|
||||
if response:
|
||||
return response
|
||||
obj.survey_spec = request.data
|
||||
obj.save(update_fields=['survey_spec'])
|
||||
return Response()
|
||||
|
||||
def _validate_spec_data(self, new_spec, old_spec):
|
||||
if "name" not in new_spec:
|
||||
return Response(dict(error=_("'name' missing from survey spec.")), status=status.HTTP_400_BAD_REQUEST)
|
||||
if "description" not in new_spec:
|
||||
@@ -2949,9 +2969,9 @@ class JobTemplateSurveySpec(GenericAPIView):
|
||||
if len(new_spec["spec"]) < 1:
|
||||
return Response(dict(error=_("'spec' doesn't contain any items.")), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
idx = 0
|
||||
variable_set = set()
|
||||
for survey_item in new_spec["spec"]:
|
||||
old_spec_dict = JobTemplate.pivot_spec(old_spec)
|
||||
for idx, survey_item in enumerate(new_spec["spec"]):
|
||||
if not isinstance(survey_item, dict):
|
||||
return Response(dict(error=_("Survey question %s is not a json object.") % str(idx)), status=status.HTTP_400_BAD_REQUEST)
|
||||
if "type" not in survey_item:
|
||||
@@ -2968,21 +2988,41 @@ class JobTemplateSurveySpec(GenericAPIView):
|
||||
if "required" not in survey_item:
|
||||
return Response(dict(error=_("'required' missing from survey question %s.") % str(idx)), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if survey_item["type"] == "password":
|
||||
if survey_item.get("default") and survey_item["default"].startswith('$encrypted$'):
|
||||
if not obj.survey_spec:
|
||||
return Response(dict(error=_("$encrypted$ is reserved keyword and may not be used as a default for password {}.".format(str(idx)))),
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
old_spec = obj.survey_spec
|
||||
for old_item in old_spec['spec']:
|
||||
if old_item['variable'] == survey_item['variable']:
|
||||
survey_item['default'] = old_item['default']
|
||||
idx += 1
|
||||
if survey_item["type"] == "password" and "default" in survey_item:
|
||||
if not isinstance(survey_item['default'], six.string_types):
|
||||
return Response(dict(error=_(
|
||||
"Value {question_default} for '{variable_name}' expected to be a string."
|
||||
).format(
|
||||
question_default=survey_item["default"], variable_name=survey_item["variable"])
|
||||
), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
obj.survey_spec = new_spec
|
||||
obj.save(update_fields=['survey_spec'])
|
||||
return Response()
|
||||
if ("default" in survey_item and isinstance(survey_item['default'], six.string_types) and
|
||||
survey_item['default'].startswith('$encrypted$')):
|
||||
# Submission expects the existence of encrypted DB value to replace given default
|
||||
if survey_item["type"] != "password":
|
||||
return Response(dict(error=_(
|
||||
"$encrypted$ is a reserved keyword for password question defaults, "
|
||||
"survey question {question_position} is type {question_type}."
|
||||
).format(
|
||||
question_position=str(idx), question_type=survey_item["type"])
|
||||
), status=status.HTTP_400_BAD_REQUEST)
|
||||
old_element = old_spec_dict.get(survey_item['variable'], {})
|
||||
encryptedish_default_exists = False
|
||||
if 'default' in old_element:
|
||||
old_default = old_element['default']
|
||||
if isinstance(old_default, six.string_types):
|
||||
if old_default.startswith('$encrypted$'):
|
||||
encryptedish_default_exists = True
|
||||
elif old_default == "": # unencrypted blank string is allowed as DB value as special case
|
||||
encryptedish_default_exists = True
|
||||
if not encryptedish_default_exists:
|
||||
return Response(dict(error=_(
|
||||
"$encrypted$ is a reserved keyword, may not be used for new default in position {question_position}."
|
||||
).format(question_position=str(idx))), status=status.HTTP_400_BAD_REQUEST)
|
||||
survey_item['default'] = old_element['default']
|
||||
elif survey_item["type"] == "password" and 'default' in survey_item:
|
||||
# Submission provides new encrypted default
|
||||
survey_item['default'] = encrypt_value(survey_item['default'])
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
@@ -4121,7 +4161,7 @@ class JobEventChildrenList(SubListAPIView):
|
||||
view_name = _('Job Event Children List')
|
||||
|
||||
|
||||
class JobEventHostsList(SubListAPIView):
|
||||
class JobEventHostsList(HostRelatedSearchMixin, SubListAPIView):
|
||||
|
||||
model = Host
|
||||
serializer_class = HostSerializer
|
||||
@@ -4141,7 +4181,7 @@ class BaseJobEventsList(SubListAPIView):
|
||||
search_fields = ('stdout',)
|
||||
|
||||
def finalize_response(self, request, response, *args, **kwargs):
|
||||
response['X-UI-Max-Events'] = settings.RECOMMENDED_MAX_EVENTS_DISPLAY_HEADER
|
||||
response['X-UI-Max-Events'] = settings.MAX_UI_JOB_EVENTS
|
||||
return super(BaseJobEventsList, self).finalize_response(request, response, *args, **kwargs)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user