diff --git a/awx/api/generics.py b/awx/api/generics.py index e3f553805d..14e430b3bc 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -287,7 +287,13 @@ class GenericAPIView(generics.GenericAPIView, APIView): 'model_verbose_name': smart_text(self.model._meta.verbose_name), 'model_verbose_name_plural': smart_text(self.model._meta.verbose_name_plural), }) - d['serializer_fields'] = self.metadata_class().get_serializer_info(self.get_serializer()) + serializer = self.get_serializer() + for method, key in [ + ('GET', 'serializer_fields'), + ('POST', 'serializer_create_fields'), + ('PUT', 'serializer_update_fields') + ]: + d[key] = self.metadata_class().get_serializer_info(serializer, method=method) d['settings'] = settings d['has_named_url'] = self.model in settings.NAMED_URL_GRAPH return d diff --git a/awx/api/metadata.py b/awx/api/metadata.py index 02e1312c0f..cd1de70a09 100644 --- a/awx/api/metadata.py +++ b/awx/api/metadata.py @@ -17,7 +17,7 @@ from rest_framework.relations import RelatedField, ManyRelatedField from rest_framework.request import clone_request # Ansible Tower -from awx.main.models import InventorySource, NotificationTemplate, CredentialType +from awx.main.models import InventorySource, NotificationTemplate class Metadata(metadata.SimpleMetadata): @@ -117,6 +117,13 @@ class Metadata(metadata.SimpleMetadata): return field_info + def get_serializer_info(self, serializer, method=None): + filterer = getattr(serializer, 'filter_field_metadata', lambda fields, method: fields) + return filterer( + super(Metadata, self).get_serializer_info(serializer), + method + ) + def determine_actions(self, request, view): # Add field information for GET requests (so field names/labels are # available even when we can't POST/PUT). @@ -137,7 +144,7 @@ class Metadata(metadata.SimpleMetadata): # If user has appropriate permissions for the view, include # appropriate metadata about the fields that should be supplied. serializer = view.get_serializer(instance=obj) - actions[method] = self.get_serializer_info(serializer) + actions[method] = self.get_serializer_info(serializer, method=method) finally: view.request = request @@ -149,16 +156,6 @@ class Metadata(metadata.SimpleMetadata): if field == 'type' and hasattr(serializer, 'get_type_choices'): meta['choices'] = serializer.get_type_choices() - # API-created/modified CredentialType kinds are limited to - # `cloud` and `network` - if method != 'GET' and \ - hasattr(serializer, 'Meta') and \ - getattr(serializer.Meta, 'model', None) is CredentialType: - actions[method]['kind']['choices'] = filter( - lambda choice: choice[0] in ('cloud', 'net'), - actions[method]['kind']['choices'] - ) - # For GET method, remove meta attributes that aren't relevant # when reading a field and remove write-only fields. if method == 'GET': diff --git a/awx/api/serializers.py b/awx/api/serializers.py index b11b869c4a..46430d10a0 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -287,6 +287,13 @@ class BaseSerializer(serializers.ModelSerializer): else: return obj.get_absolute_url(request=self.context.get('request')) + def filter_field_metadata(self, fields, method): + """ + Filter field metadata based on the request method. + This it intended to be extended by subclasses. + """ + return fields + def _get_related(self, obj): return {} if obj is None else self.get_related(obj) @@ -1957,6 +1964,16 @@ class CredentialTypeSerializer(BaseSerializer): field['help_text'] = _(field['help_text']) return value + def filter_field_metadata(self, fields, method): + # API-created/modified CredentialType kinds are limited to + # `cloud` and `net` + if method in ('PUT', 'POST'): + fields['kind']['choices'] = filter( + lambda choice: choice[0] in ('cloud', 'net'), + fields['kind']['choices'] + ) + return fields + # TODO: remove when API v1 is removed @six.add_metaclass(BaseSerializerMetaclass) diff --git a/awx/api/templates/api/list_create_api_view.md b/awx/api/templates/api/list_create_api_view.md index 5839f0fbf6..400eeade18 100644 --- a/awx/api/templates/api/list_create_api_view.md +++ b/awx/api/templates/api/list_create_api_view.md @@ -6,7 +6,7 @@ Make a POST request to this resource with the following {{ model_verbose_name }} fields to create a new {{ model_verbose_name }}: {% with write_only=1 %} -{% include "api/_result_fields_common.md" %} +{% include "api/_result_fields_common.md" with serializer_fields=serializer_create_fields %} {% endwith %} {% include "api/_new_in_awx.md" %} diff --git a/awx/api/templates/api/retrieve_update_api_view.md b/awx/api/templates/api/retrieve_update_api_view.md index 49392f1aa7..21e4255bf1 100644 --- a/awx/api/templates/api/retrieve_update_api_view.md +++ b/awx/api/templates/api/retrieve_update_api_view.md @@ -15,7 +15,7 @@ Make a PUT or PATCH request to this resource to update this {{ model_verbose_name }}. The following fields may be modified: {% with write_only=1 %} -{% include "api/_result_fields_common.md" %} +{% include "api/_result_fields_common.md" with serializer_fields=serializer_update_fields %} {% endwith %} For a PUT request, include **all** fields in the request. diff --git a/awx/api/templates/api/retrieve_update_destroy_api_view.md b/awx/api/templates/api/retrieve_update_destroy_api_view.md index 97ca162209..bfc99bb293 100644 --- a/awx/api/templates/api/retrieve_update_destroy_api_view.md +++ b/awx/api/templates/api/retrieve_update_destroy_api_view.md @@ -15,7 +15,7 @@ Make a PUT or PATCH request to this resource to update this {{ model_verbose_name }}. The following fields may be modified: {% with write_only=1 %} -{% include "api/_result_fields_common.md" %} +{% include "api/_result_fields_common.md" with serializer_fields=serializer_update_fields %} {% endwith %} For a PUT request, include **all** fields in the request. diff --git a/awx/main/tests/functional/api/test_credential_type.py b/awx/main/tests/functional/api/test_credential_type.py index 539d8f827b..a8c9d2000f 100644 --- a/awx/main/tests/functional/api/test_credential_type.py +++ b/awx/main/tests/functional/api/test_credential_type.py @@ -12,6 +12,26 @@ def test_list_as_unauthorized_xfail(get): assert response.status_code == 401 +@pytest.mark.django_db +@pytest.mark.parametrize('method, valid', [ + ('GET', sorted(dict(CredentialType.KIND_CHOICES).keys())), + ('POST', ['cloud', 'net']), +]) +def test_options_valid_kinds(method, valid, options, admin): + response = options(reverse('api:credential_type_list'), admin) + choices = sorted(dict(response.data['actions'][method]['kind']['choices']).keys()) + assert valid == choices + + +@pytest.mark.django_db +def test_options_valid_put_kinds(options, admin): + ssh = CredentialType.defaults['ssh']() + ssh.save() + response = options(reverse('api:credential_type_detail', kwargs={'pk': ssh.pk}), admin) + choices = sorted(dict(response.data['actions']['PUT']['kind']['choices']).keys()) + assert ['cloud', 'net'] == choices + + @pytest.mark.django_db def test_list_as_normal_user(get, alice): ssh = CredentialType.defaults['ssh']()