Update serializers to remove empty choices, provide default values for fields, and better indicate the field type when possible for OPTIONS requests and browsable API docs.

This commit is contained in:
Chris Church
2015-05-11 18:25:30 -04:00
parent 27b06313a7
commit e017270201
6 changed files with 73 additions and 29 deletions

View File

@@ -217,7 +217,7 @@ class GenericAPIView(generics.GenericAPIView, APIView):
for field, meta in fields.items(): for field, meta in fields.items():
if not isinstance(meta, dict): if not isinstance(meta, dict):
continue continue
if meta.get('read_only', False): if meta.pop('read_only', False):
fields.pop(field) fields.pop(field)
if 'GET' in self.allowed_methods: if 'GET' in self.allowed_methods:
cloned_request = clone_request(request, 'GET') cloned_request = clone_request(request, 'GET')
@@ -242,12 +242,19 @@ class GenericAPIView(generics.GenericAPIView, APIView):
serializer = self.get_serializer() serializer = self.get_serializer()
actions['GET'] = serializer.metadata() actions['GET'] = serializer.metadata()
if hasattr(serializer, 'get_types'): if hasattr(serializer, 'get_types'):
# Inject the type field choices into GET options as well as on
# the metadata itself.
if 'type' in actions['GET']:
actions['GET']['type']['type'] = 'multiple choice'
actions['GET']['type']['choices'] = serializer.get_type_choices()
ret['types'] = serializer.get_types() ret['types'] = serializer.get_types()
# Remove fields labeled as write_only, remove field attributes
# that aren't relevant for retrieving data.
for field, meta in actions['GET'].items():
if not isinstance(meta, dict):
continue
meta.pop('required', None)
meta.pop('read_only', None)
meta.pop('default', None)
meta.pop('min_length', None)
meta.pop('max_length', None)
if meta.pop('write_only', False):
actions['GET'].pop(field)
if actions: if actions:
ret['actions'] = actions ret['actions'] = actions
if getattr(self, 'search_fields', None): if getattr(self, 'search_fields', None):

View File

@@ -2,6 +2,7 @@
# All Rights Reserved. # All Rights Reserved.
# Python # Python
import functools
import json import json
import re import re
import logging import logging
@@ -86,6 +87,25 @@ SUMMARIZABLE_FK_FIELDS = {
'source_script': ('name', 'description'), 'source_script': ('name', 'description'),
} }
# Monkeypatch REST framework to include default value and write_only flag in
# field metadata.
def add_metadata_default(f):
@functools.wraps(f)
def _add_metadata_default(self, *args, **kwargs):
metadata = f(self, *args, **kwargs)
if hasattr(self, 'get_default_value'):
default = self.get_default_value()
if default is None and metadata.get('type', '') != 'field':
default = getattr(self, 'empty', None)
if default or not getattr(self, 'required', False):
metadata['default'] = default
if getattr(self, 'write_only', False):
metadata['write_only'] = True
return metadata
return _add_metadata_default
fields.Field.metadata = add_metadata_default(fields.Field.metadata)
class ChoiceField(fields.ChoiceField): class ChoiceField(fields.ChoiceField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -93,7 +113,7 @@ class ChoiceField(fields.ChoiceField):
if not self.required: if not self.required:
# Remove extra blank option if one is already present (for writable # Remove extra blank option if one is already present (for writable
# field) or if present at all for read-only fields. # field) or if present at all for read-only fields.
if ([x[0] for x in self.choices].count(u'') > 1 or self.read_only) \ if ([x[0] for x in self.choices].count(u'') > 1 or self.get_default_value() != u'' or self.read_only) \
and BLANK_CHOICE_DASH[0] in self.choices: and BLANK_CHOICE_DASH[0] in self.choices:
self.choices = [x for x in self.choices self.choices = [x for x in self.choices
if x != BLANK_CHOICE_DASH[0]] if x != BLANK_CHOICE_DASH[0]]
@@ -205,7 +225,7 @@ class BaseSerializer(serializers.ModelSerializer):
field.help_text = 'Database ID for this %s.' % unicode(opts.verbose_name) field.help_text = 'Database ID for this %s.' % unicode(opts.verbose_name)
elif key == 'type': elif key == 'type':
field.help_text = 'Data type for this %s.' % unicode(opts.verbose_name) field.help_text = 'Data type for this %s.' % unicode(opts.verbose_name)
field.type_label = 'string' field.type_label = 'multiple choice'
elif key == 'url': elif key == 'url':
field.help_text = 'URL for this %s.' % unicode(opts.verbose_name) field.help_text = 'URL for this %s.' % unicode(opts.verbose_name)
field.type_label = 'string' field.type_label = 'string'
@@ -371,6 +391,17 @@ class BaseSerializer(serializers.ModelSerializer):
ret.pop(parent_key, None) ret.pop(parent_key, None)
return ret return ret
def metadata(self):
fields = super(BaseSerializer, self).metadata()
for field, meta in fields.items():
if not isinstance(meta, dict):
continue
if field == 'type':
meta['choices'] = self.get_type_choices()
#if meta.get('type', '') == 'field':
# meta['type'] = 'id'
return fields
class UnifiedJobTemplateSerializer(BaseSerializer): class UnifiedJobTemplateSerializer(BaseSerializer):
@@ -413,8 +444,9 @@ class UnifiedJobTemplateSerializer(BaseSerializer):
class UnifiedJobSerializer(BaseSerializer): class UnifiedJobSerializer(BaseSerializer):
result_stdout = serializers.Field(source='result_stdout') result_stdout = serializers.CharField(source='result_stdout', label='result stdout', read_only=True)
unified_job_template = serializers.Field(source='unified_job_template_id') unified_job_template = serializers.Field(source='unified_job_template_id', label='unified job template')
job_env = serializers.CharField(source='job_env', label='job env', read_only=True)
class Meta: class Meta:
model = UnifiedJob model = UnifiedJob
@@ -521,9 +553,9 @@ class UnifiedJobStdoutSerializer(UnifiedJobSerializer):
class UserSerializer(BaseSerializer): class UserSerializer(BaseSerializer):
password = serializers.WritableField(required=False, default='', password = serializers.CharField(required=False, default='', write_only=True,
help_text='Write-only field used to change the password.') help_text='Write-only field used to change the password.')
ldap_dn = serializers.Field(source='profile.ldap_dn') ldap_dn = serializers.CharField(source='profile.ldap_dn', read_only=True)
class Meta: class Meta:
model = User model = User
@@ -664,10 +696,10 @@ class ProjectOptionsSerializer(BaseSerializer):
class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer): class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
playbooks = serializers.Field(source='playbooks', help_text='Array of playbooks available within this project.') playbooks = serializers.Field(source='playbooks', help_text='Array of playbooks available within this project.')
scm_delete_on_next_update = serializers.Field(source='scm_delete_on_next_update') scm_delete_on_next_update = serializers.BooleanField(source='scm_delete_on_next_update', read_only=True)
status = ChoiceField(source='status', choices=Project.PROJECT_STATUS_CHOICES, read_only=True, required=False) status = ChoiceField(source='status', choices=Project.PROJECT_STATUS_CHOICES, read_only=True, required=False)
last_update_failed = serializers.Field(source='last_update_failed') last_update_failed = serializers.BooleanField(source='last_update_failed', read_only=True)
last_updated = serializers.Field(source='last_updated') last_updated = serializers.DateTimeField(source='last_updated', read_only=True)
class Meta: class Meta:
model = Project model = Project
@@ -1114,8 +1146,8 @@ class InventorySourceOptionsSerializer(BaseSerializer):
class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOptionsSerializer): class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOptionsSerializer):
status = ChoiceField(source='status', choices=InventorySource.INVENTORY_SOURCE_STATUS_CHOICES, read_only=True, required=False) status = ChoiceField(source='status', choices=InventorySource.INVENTORY_SOURCE_STATUS_CHOICES, read_only=True, required=False)
last_update_failed = serializers.Field(source='last_update_failed') last_update_failed = serializers.BooleanField(source='last_update_failed', read_only=True)
last_updated = serializers.Field(source='last_updated') last_updated = serializers.DateTimeField(source='last_updated', read_only=True)
class Meta: class Meta:
model = InventorySource model = InventorySource
@@ -1273,11 +1305,11 @@ class CredentialSerializer(BaseSerializer):
# FIXME: may want to make some of these filtered based on user accessing # FIXME: may want to make some of these filtered based on user accessing
password = serializers.WritableField(required=False, default='') password = serializers.CharField(required=False, default='')
ssh_key_data = serializers.WritableField(required=False, default='') ssh_key_data = serializers.CharField(required=False, default='')
ssh_key_unlock = serializers.WritableField(required=False, default='') ssh_key_unlock = serializers.CharField(required=False, default='')
become_password = serializers.WritableField(required=False, default='') become_password = serializers.CharField(required=False, default='')
vault_password = serializers.WritableField(required=False, default='') vault_password = serializers.CharField(required=False, default='')
class Meta: class Meta:
model = Credential model = Credential
@@ -1512,6 +1544,7 @@ class JobCancelSerializer(JobSerializer):
class JobRelaunchSerializer(JobSerializer): class JobRelaunchSerializer(JobSerializer):
passwords_needed_to_start = serializers.SerializerMethodField('get_passwords_needed_to_start') passwords_needed_to_start = serializers.SerializerMethodField('get_passwords_needed_to_start')
class Meta: class Meta:
@@ -1687,8 +1720,8 @@ class JobHostSummarySerializer(BaseSerializer):
class JobEventSerializer(BaseSerializer): class JobEventSerializer(BaseSerializer):
event_display = serializers.Field(source='get_event_display2') event_display = serializers.CharField(source='get_event_display2', read_only=True)
event_level = serializers.Field(source='event_level') event_level = serializers.IntegerField(source='event_level', read_only=True)
class Meta: class Meta:
model = JobEvent model = JobEvent
@@ -1724,7 +1757,7 @@ class JobEventSerializer(BaseSerializer):
class AdHocCommandEventSerializer(BaseSerializer): class AdHocCommandEventSerializer(BaseSerializer):
event_display = serializers.Field(source='get_event_display') event_display = serializers.CharField(source='get_event_display', read_only=True)
class Meta: class Meta:
model = AdHocCommandEvent model = AdHocCommandEvent
@@ -1742,8 +1775,9 @@ class AdHocCommandEventSerializer(BaseSerializer):
return res return res
class JobLaunchSerializer(BaseSerializer): class JobLaunchSerializer(BaseSerializer):
passwords_needed_to_start = serializers.Field(source='passwords_needed_to_start') passwords_needed_to_start = serializers.Field(source='passwords_needed_to_start')
can_start_without_user_input = serializers.Field(source='can_start_without_user_input') can_start_without_user_input = serializers.BooleanField(source='can_start_without_user_input', read_only=True)
variables_needed_to_start = serializers.Field(source='variables_needed_to_start') variables_needed_to_start = serializers.Field(source='variables_needed_to_start')
credential_needed_to_start = serializers.SerializerMethodField('get_credential_needed_to_start') credential_needed_to_start = serializers.SerializerMethodField('get_credential_needed_to_start')
survey_enabled = serializers.SerializerMethodField('get_survey_enabled') survey_enabled = serializers.SerializerMethodField('get_survey_enabled')

View File

@@ -1,6 +1,6 @@
{% for fn, fm in serializer_fields.items %}{% spaceless %} {% for fn, fm in serializer_fields.items %}{% spaceless %}
{% if not write_only or not fm.read_only %} {% if not write_only or not fm.read_only %}
* `{{ fn }}`: {{ fm.help_text|capfirst }} ({{ fm.type }}{% if fm.required %}, required{% endif %}{% if fm.read_only %}, read-only{% endif %}) * `{{ fn }}`: {{ fm.help_text|capfirst }} ({{ fm.type }}{% if write_only and fm.required %}, required{% endif %}{% if write_only and fm.read_only %}, read-only{% endif %}{% if write_only and not fm.choices and not fm.required %}, default=`{% if fm.type == "string" or fm.type == "email" %}"{% firstof fm.default "" %}"{% else %}{{ fm.default }}{% endif %}`{% endif %}){% if fm.choices %}{% for c in fm.choices %}
{% endif %} - `{% if c.0 == "" %}""{% else %}{{ c.0 }}{% endif %}`{% if c.1 != c.0 %}: {{ c.1 }}{% endif %}{% if write_only and c.0 == fm.default %} (default){% endif %}{% endfor %}{% endif %}{% endif %}
{% endspaceless %} {% endspaceless %}
{% endfor %} {% endfor %}

View File

@@ -734,6 +734,7 @@ class InventorySourceOptions(BaseModel):
''' '''
SOURCE_CHOICES = [ SOURCE_CHOICES = [
('', _('Manual')),
('file', _('Local File, Directory or Script')), ('file', _('Local File, Directory or Script')),
('rax', _('Rackspace Cloud Servers')), ('rax', _('Rackspace Cloud Servers')),
('ec2', _('Amazon EC2')), ('ec2', _('Amazon EC2')),

View File

@@ -41,6 +41,7 @@ class JobOptions(BaseModel):
job_type = models.CharField( job_type = models.CharField(
max_length=64, max_length=64,
choices=JOB_TYPE_CHOICES, choices=JOB_TYPE_CHOICES,
default='run',
) )
inventory = models.ForeignKey( inventory = models.ForeignKey(
'Inventory', 'Inventory',

View File

@@ -203,6 +203,7 @@ class Project(UnifiedJobTemplate, ProjectOptions):
) )
scm_update_cache_timeout = models.PositiveIntegerField( scm_update_cache_timeout = models.PositiveIntegerField(
default=0, default=0,
blank=True,
) )
@classmethod @classmethod