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():
if not isinstance(meta, dict):
continue
if meta.get('read_only', False):
if meta.pop('read_only', False):
fields.pop(field)
if 'GET' in self.allowed_methods:
cloned_request = clone_request(request, 'GET')
@ -242,12 +242,19 @@ class GenericAPIView(generics.GenericAPIView, APIView):
serializer = self.get_serializer()
actions['GET'] = serializer.metadata()
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()
# 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:
ret['actions'] = actions
if getattr(self, 'search_fields', None):

View File

@ -2,6 +2,7 @@
# All Rights Reserved.
# Python
import functools
import json
import re
import logging
@ -86,6 +87,25 @@ SUMMARIZABLE_FK_FIELDS = {
'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):
def __init__(self, *args, **kwargs):
@ -93,7 +113,7 @@ class ChoiceField(fields.ChoiceField):
if not self.required:
# Remove extra blank option if one is already present (for writable
# 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:
self.choices = [x for x in self.choices
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)
elif key == 'type':
field.help_text = 'Data type for this %s.' % unicode(opts.verbose_name)
field.type_label = 'string'
field.type_label = 'multiple choice'
elif key == 'url':
field.help_text = 'URL for this %s.' % unicode(opts.verbose_name)
field.type_label = 'string'
@ -371,6 +391,17 @@ class BaseSerializer(serializers.ModelSerializer):
ret.pop(parent_key, None)
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):
@ -413,8 +444,9 @@ class UnifiedJobTemplateSerializer(BaseSerializer):
class UnifiedJobSerializer(BaseSerializer):
result_stdout = serializers.Field(source='result_stdout')
unified_job_template = serializers.Field(source='unified_job_template_id')
result_stdout = serializers.CharField(source='result_stdout', label='result stdout', read_only=True)
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:
model = UnifiedJob
@ -521,9 +553,9 @@ class UnifiedJobStdoutSerializer(UnifiedJobSerializer):
class UserSerializer(BaseSerializer):
password = serializers.WritableField(required=False, default='',
help_text='Write-only field used to change the password.')
ldap_dn = serializers.Field(source='profile.ldap_dn')
password = serializers.CharField(required=False, default='', write_only=True,
help_text='Write-only field used to change the password.')
ldap_dn = serializers.CharField(source='profile.ldap_dn', read_only=True)
class Meta:
model = User
@ -664,10 +696,10 @@ class ProjectOptionsSerializer(BaseSerializer):
class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
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)
last_update_failed = serializers.Field(source='last_update_failed')
last_updated = serializers.Field(source='last_updated')
last_update_failed = serializers.BooleanField(source='last_update_failed', read_only=True)
last_updated = serializers.DateTimeField(source='last_updated', read_only=True)
class Meta:
model = Project
@ -1114,8 +1146,8 @@ class InventorySourceOptionsSerializer(BaseSerializer):
class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOptionsSerializer):
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_updated = serializers.Field(source='last_updated')
last_update_failed = serializers.BooleanField(source='last_update_failed', read_only=True)
last_updated = serializers.DateTimeField(source='last_updated', read_only=True)
class Meta:
model = InventorySource
@ -1273,11 +1305,11 @@ class CredentialSerializer(BaseSerializer):
# FIXME: may want to make some of these filtered based on user accessing
password = serializers.WritableField(required=False, default='')
ssh_key_data = serializers.WritableField(required=False, default='')
ssh_key_unlock = serializers.WritableField(required=False, default='')
become_password = serializers.WritableField(required=False, default='')
vault_password = serializers.WritableField(required=False, default='')
password = serializers.CharField(required=False, default='')
ssh_key_data = serializers.CharField(required=False, default='')
ssh_key_unlock = serializers.CharField(required=False, default='')
become_password = serializers.CharField(required=False, default='')
vault_password = serializers.CharField(required=False, default='')
class Meta:
model = Credential
@ -1512,6 +1544,7 @@ class JobCancelSerializer(JobSerializer):
class JobRelaunchSerializer(JobSerializer):
passwords_needed_to_start = serializers.SerializerMethodField('get_passwords_needed_to_start')
class Meta:
@ -1687,8 +1720,8 @@ class JobHostSummarySerializer(BaseSerializer):
class JobEventSerializer(BaseSerializer):
event_display = serializers.Field(source='get_event_display2')
event_level = serializers.Field(source='event_level')
event_display = serializers.CharField(source='get_event_display2', read_only=True)
event_level = serializers.IntegerField(source='event_level', read_only=True)
class Meta:
model = JobEvent
@ -1724,7 +1757,7 @@ class JobEventSerializer(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:
model = AdHocCommandEvent
@ -1742,8 +1775,9 @@ class AdHocCommandEventSerializer(BaseSerializer):
return res
class JobLaunchSerializer(BaseSerializer):
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')
credential_needed_to_start = serializers.SerializerMethodField('get_credential_needed_to_start')
survey_enabled = serializers.SerializerMethodField('get_survey_enabled')

View File

@ -1,6 +1,6 @@
{% for fn, fm in serializer_fields.items %}{% spaceless %}
{% 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 %})
{% 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 %}
- `{% 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 %}
{% endfor %}

View File

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

View File

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

View File

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