* Populate browsable API raw data form with submitted request data in response to an update.

* Remove fields from browsable API raw data that are set implicitly based on URL / parent object.
* Fix issue where a group/host could be assigned to a different inventory.
* Update validation to load values from existing instance if not present in new data; allows PATCH requests to succeed.
* Remove job_args, job_cwd, job_env, result_stdout and result_traceback fields from job listings.
This commit is contained in:
Chris Church
2016-02-16 17:49:34 -05:00
parent 6434171977
commit 4873e2413f
6 changed files with 170 additions and 55 deletions

View File

@@ -463,24 +463,6 @@ class BaseSerializer(serializers.ModelSerializer):
raise ValidationError(d)
return attrs
def to_representation(self, obj):
# FIXME: Doesn't get called anymore for an new raw data form!
# When rendering the raw data form, create an instance of the model so
# that the model defaults will be filled in.
view = self.context.get('view', None)
parent_key = getattr(view, 'parent_key', None)
if not obj and hasattr(view, '_raw_data_form_marker'):
obj = self.Meta.model()
# FIXME: Would be nice to include any posted data for the raw data
# form, so that a submission with errors can be modified in place
# and resubmitted.
ret = super(BaseSerializer, self).to_representation(obj)
# Remove parent key from raw form data, since it will be automatically
# set by the sub list create view.
if parent_key and hasattr(view, '_raw_data_form_marker'):
ret.pop(parent_key, None)
return ret
class BaseFactSerializer(DocumentSerializer):
@@ -611,6 +593,12 @@ class UnifiedJobListSerializer(UnifiedJobSerializer):
class Meta:
fields = ('*', '-job_args', '-job_cwd', '-job_env', '-result_traceback', '-result_stdout')
def get_field_names(self, declared_fields, info):
field_names = super(UnifiedJobListSerializer, self).get_field_names(declared_fields, info)
# Meta multiple inheritance and -field_name options don't seem to be
# taking effect above, so remove the undesired fields here.
return tuple(x for x in field_names if x not in ('job_args', 'job_cwd', 'job_env', 'result_traceback', 'result_stdout'))
def get_types(self):
if type(self) is UnifiedJobListSerializer:
return ['project_update', 'inventory_update', 'job', 'ad_hoc_command', 'system_job']
@@ -995,6 +983,14 @@ class HostSerializer(BaseSerializerWithVariables):
'last_job_host_summary')
read_only_fields = ('last_job', 'last_job_host_summary')
def build_relational_field(self, field_name, relation_info):
field_class, field_kwargs = super(HostSerializer, self).build_relational_field(field_name, relation_info)
# Inventory is read-only unless creating a new host.
if self.instance and field_name == 'inventory':
field_kwargs['read_only'] = True
field_kwargs.pop('queryset', None)
return field_class, field_kwargs
def get_related(self, obj):
res = super(HostSerializer, self).get_related(obj)
res.update(dict(
@@ -1053,15 +1049,12 @@ class HostSerializer(BaseSerializerWithVariables):
return value
def validate(self, attrs):
name = force_text(attrs.get('name', ''))
name = force_text(attrs.get('name', self.instance and self.instance.name or ''))
host, port = self._get_host_port_from_name(name)
if port:
attrs['name'] = host
if self.instance:
variables = force_text(attrs.get('variables', self.instance.variables) or '')
else:
variables = force_text(attrs.get('variables', ''))
variables = force_text(attrs.get('variables', self.instance and self.instance.variables or ''))
try:
vars_dict = json.loads(variables.strip() or '{}')
vars_dict['ansible_ssh_port'] = port
@@ -1099,6 +1092,14 @@ class GroupSerializer(BaseSerializerWithVariables):
'total_hosts', 'hosts_with_active_failures', 'total_groups',
'groups_with_active_failures', 'has_inventory_sources')
def build_relational_field(self, field_name, relation_info):
field_class, field_kwargs = super(GroupSerializer, self).build_relational_field(field_name, relation_info)
# Inventory is read-only unless creating a new group.
if self.instance and field_name == 'inventory':
field_kwargs['read_only'] = True
field_kwargs.pop('queryset', None)
return field_class, field_kwargs
def get_related(self, obj):
res = super(GroupSerializer, self).get_related(obj)
res.update(dict(
@@ -1247,9 +1248,10 @@ class InventorySourceOptionsSerializer(BaseSerializer):
# TODO: Validate source, validate source_regions
errors = {}
source_script = attrs.get('source_script', None)
if 'source' in attrs and attrs.get('source', '') == 'custom':
if source_script is None or source_script == '':
source = attrs.get('source', self.instance and self.instance.source or '')
source_script = attrs.get('source_script', self.instance and self.instance.source_script or '')
if source == 'custom':
if not source_script is None or source_script == '':
errors['source_script'] = 'source_script must be provided'
else:
try:
@@ -1403,15 +1405,19 @@ class PermissionSerializer(BaseSerializer):
def validate(self, attrs):
# Can only set either user or team.
if attrs.get('user', None) and attrs.get('team', None):
user = attrs.get('user', self.instance and self.instance.user or None)
team = attrs.get('team', self.instance and self.instance.team or None)
if user and team:
raise serializers.ValidationError('permission can only be assigned'
' to a user OR a team, not both')
# Cannot assign admit/read/write permissions for a project.
if attrs.get('permission_type', None) in ('admin', 'read', 'write') and attrs.get('project', None):
permission_type = attrs.get('permission_type', self.instance and self.instance.permission_type or None)
project = attrs.get('project', self.instance and self.instance.project or None)
if permission_type in ('admin', 'read', 'write') and project:
raise serializers.ValidationError('project cannot be assigned for '
'inventory-only permissions')
# Project is required when setting deployment permissions.
if attrs.get('permission_type', None) in ('run', 'check') and not attrs.get('project', None):
if permission_type in ('run', 'check') and not project:
raise serializers.ValidationError('project is required when '
'assigning deployment permissions')
@@ -1522,9 +1528,10 @@ class JobOptionsSerializer(BaseSerializer):
def validate(self, attrs):
if 'project' in self.fields and 'playbook' in self.fields:
project = attrs.get('project', None)
playbook = attrs.get('playbook', '')
if not project and attrs.get('job_type') != PERM_INVENTORY_SCAN:
project = attrs.get('project', self.instance and self.instance.project or None)
playbook = attrs.get('playbook', self.instance and self.instance.playbook or '')
job_type = attrs.get('job_type', self.instance and self.instance.job_type or None)
if not project and job_type != PERM_INVENTORY_SCAN:
raise serializers.ValidationError({'project': 'This field is required.'})
if project and playbook and force_text(playbook) not in project.playbooks:
raise serializers.ValidationError({'playbook': 'Playbook not found for project'})
@@ -1578,8 +1585,8 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer):
return d
def validate(self, attrs):
survey_enabled = attrs.get('survey_enabled', False)
job_type = attrs.get('job_type', None)
survey_enabled = attrs.get('survey_enabled', self.instance and self.instance.survey_enabled or False)
job_type = attrs.get('job_type', self.instance and self.instance.job_type or None)
if survey_enabled and job_type == PERM_INVENTORY_SCAN:
raise serializers.ValidationError({'survey_enabled': 'Survey Enabled can not be used with scan jobs'})
@@ -1737,8 +1744,8 @@ class AdHocCommandSerializer(UnifiedJobSerializer):
def get_field_names(self, declared_fields, info):
field_names = super(AdHocCommandSerializer, self).get_field_names(declared_fields, info)
# Meta inheritance and -field_name options don't seem to be taking
# effect above, so remove the undesired fields here.
# Meta multiple inheritance and -field_name options don't seem to be
# taking effect above, so remove the undesired fields here.
return tuple(x for x in field_names if x not in ('unified_job_template', 'description'))
def build_standard_field(self, field_name, model_field):
@@ -1770,19 +1777,7 @@ class AdHocCommandSerializer(UnifiedJobSerializer):
return res
def to_representation(self, obj):
# In raw data form, populate limit field from host/group name.
view = self.context.get('view', None)
parent_model = getattr(view, 'parent_model', None)
if not (obj and obj.pk) and view and hasattr(view, '_raw_data_form_marker'):
if not obj:
obj = self.Meta.model()
ret = super(AdHocCommandSerializer, self).to_representation(obj)
# Hide inventory and limit fields from raw data, since they will be set
# automatically by sub list create view.
if not (obj and obj.pk) and view and hasattr(view, '_raw_data_form_marker'):
if parent_model in (Host, Group):
ret.pop('inventory', None)
ret.pop('limit', None)
if 'inventory' in ret and (not obj.inventory or not obj.inventory.active):
ret['inventory'] = None
if 'credential' in ret and (not obj.credential or not obj.credential.active):
@@ -1993,7 +1988,7 @@ class JobLaunchSerializer(BaseSerializer):
obj = self.context.get('obj')
data = self.context.get('data')
credential = attrs.get('credential', None) or (obj and obj.credential)
credential = attrs.get('credential', obj and obj.credential or None)
if not credential or not credential.active:
errors['credential'] = 'Credential not provided'