From a8a4e46e4254d16d0835a3736a3e83596c400b51 Mon Sep 17 00:00:00 2001 From: Guy D Date: Tue, 20 Mar 2018 08:18:25 +0200 Subject: [PATCH 1/8] there are cases in which the decision if to render a path is not related to the Permission Model SoI decoupled that decision and you are now able to set the name of actions you want to render --- drf_openapi/entities.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/drf_openapi/entities.py b/drf_openapi/entities.py index 70bba2d..438fa6a 100644 --- a/drf_openapi/entities.py +++ b/drf_openapi/entities.py @@ -84,8 +84,9 @@ def get(cls, request_version): class OpenApiSchemaGenerator(SchemaGenerator): - def __init__(self, version, title=None, url=None, description=None, patterns=None, urlconf=None): + def __init__(self, version, actions=None, title=None, url=None, description=None, patterns=None, urlconf=None): self.version = version + self.actions = None super(OpenApiSchemaGenerator, self).__init__(title, url, description, patterns, urlconf) def get_schema(self, request=None, public=False): @@ -108,6 +109,7 @@ def get_schema(self, request=None, public=False): url=url, content=links ) + def get_links(self, request=None): """ Return a dictionary containing all the links that should be @@ -122,6 +124,16 @@ def get_links(self, request=None): view = self.create_view(callback, method, request) if getattr(view, 'exclude_from_schema', False): continue + + action = None + if hasattr(view, "action"): + action = view.action + # In case actions is defined we want to render only actions in the actions list + if self.actions is not None and action not in self.actions: + continue + + + path = self.coerce_path(path, method, view) paths.append(path) view_endpoints.append((path, method, view)) From 9902cde1e015c811b809729c044a72e6fff1d3e3 Mon Sep 17 00:00:00 2001 From: Guy D Date: Tue, 20 Mar 2018 08:20:27 +0200 Subject: [PATCH 2/8] there are cases we want to add more fields that filter, pagination, serializer and path In such cases add to the genrator a method called custom_field --- drf_openapi/entities.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drf_openapi/entities.py b/drf_openapi/entities.py index 438fa6a..713fa8b 100644 --- a/drf_openapi/entities.py +++ b/drf_openapi/entities.py @@ -173,6 +173,11 @@ def get_link(self, path, method, view, version=None): fields += view.schema.get_pagination_fields(path, method) fields += view.schema.get_filter_fields(path, method) + if hasattr(view.schema, "get_custom_fields"): + fields += view.schema.get_custom_fields(path, method) + + + if fields and any([field.location in ('form', 'body') for field in fields]): encoding = view.schema.get_encoding(path, method) else: From 5cc53fdda47fa1971cde9a021111ee33c98b6858 Mon Sep 17 00:00:00 2001 From: Guy D Date: Tue, 20 Mar 2018 08:24:39 +0200 Subject: [PATCH 3/8] I wanted to add the ability to set a page result not only on list --- drf_openapi/entities.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/drf_openapi/entities.py b/drf_openapi/entities.py index 713fa8b..cd1eda1 100644 --- a/drf_openapi/entities.py +++ b/drf_openapi/entities.py @@ -18,6 +18,8 @@ from drf_openapi.codec import _get_parameters +class PaginatedListSerializer: + pass class VersionedSerializers: """Adapted from https://github.com/avanov/Rhetoric/ :) @@ -198,14 +200,12 @@ def get_link(self, path, method, view, version=None): description = description + '\n\n**Response Description:**\n' + res_doc response_serializer_class = response_serializer_class.get(version) - if not response_serializer_class and method_name in ('list', 'retrieve'): - if hasattr(view, 'get_serializer_class'): - response_serializer_class = view.get_serializer_class() - elif hasattr(view, 'serializer_class'): - response_serializer_class = view.serializer_class - if response_serializer_class and method_name == 'list': - response_serializer_class = self.get_paginator_serializer( - view, response_serializer_class) + if response_serializer_class and issubclass(response_serializer_class, PaginatedListSerializer): + response_serializer_class = self._default_response_class("list", view) + + + if response_serializer_class is None: + response_serializer_class = self._default_response_class(method_name, view) response_schema, error_status_codes = self.get_response_object( response_serializer_class, method_func.__doc__) if response_serializer_class else ({}, {}) @@ -219,6 +219,19 @@ def get_link(self, path, method, view, version=None): description=description ) + def _default_response_class(self, method_name, view): + response_serializer_class = None + if method_name in ('list', 'retrieve'): + if hasattr(view, 'get_serializer_class'): + response_serializer_class = view.get_serializer_class() + elif hasattr(view, 'serializer_class'): + response_serializer_class = view.serializer_class + if response_serializer_class and method_name == 'list': + response_serializer_class = self.get_paginator_serializer(view, response_serializer_class) + return response_serializer_class + + + def get_paginator_serializer(self, view, child_serializer_class): class BaseFakeListSerializer(serializers.Serializer): results = child_serializer_class(many=True) From 5e59445ba6eb6e211888a52c95008a4b34b87827 Mon Sep 17 00:00:00 2001 From: Guy D Date: Tue, 20 Mar 2018 08:28:10 +0200 Subject: [PATCH 4/8] return serializers fields only on create and update --- drf_openapi/entities.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drf_openapi/entities.py b/drf_openapi/entities.py index cd1eda1..9bd79fe 100644 --- a/drf_openapi/entities.py +++ b/drf_openapi/entities.py @@ -347,6 +347,11 @@ def get_serializer_fields(self, path, method, view, version=None, method_func=No else: location = 'query' + method_name = getattr(view, 'action', method.lower()) + # I don't see reason to return the serializers fields in other actions + if method_name not in ['update', 'create']: + return [] + serializer_class = self.get_serializer_class(view, method_func) if not serializer_class: return [] From f12190a966435f6e0ca1e65cc61d459281024600 Mon Sep 17 00:00:00 2001 From: Guy D Date: Tue, 20 Mar 2018 08:30:23 +0200 Subject: [PATCH 5/8] avoid rendering write only fields in response --- drf_openapi/entities.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/drf_openapi/entities.py b/drf_openapi/entities.py index 9bd79fe..928e323 100644 --- a/drf_openapi/entities.py +++ b/drf_openapi/entities.py @@ -397,6 +397,10 @@ def get_response_object(self, response_serializer_class, description): nested_obj = {} for field in serializer.fields.values(): + # we don't want to render write only fields in the response + if field.write_only: + continue + # If field is a serializer, attempt to get its schema. if isinstance(field, serializers.Serializer): subfield_schema = self.get_response_object(field.__class__, None)[0].get('schema') From 9eb6ca2d67ca869b3cf20cbcf811778cb46ed140 Mon Sep 17 00:00:00 2001 From: Guy D Date: Tue, 20 Mar 2018 08:36:37 +0200 Subject: [PATCH 6/8] fixing syntax issues --- drf_openapi/entities.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drf_openapi/entities.py b/drf_openapi/entities.py index 928e323..f8eb03b 100644 --- a/drf_openapi/entities.py +++ b/drf_openapi/entities.py @@ -347,9 +347,9 @@ def get_serializer_fields(self, path, method, view, version=None, method_func=No else: location = 'query' - method_name = getattr(view, 'action', method.lower()) - # I don't see reason to return the serializers fields in other actions - if method_name not in ['update', 'create']: + method_name = getattr(view, 'action', method.lower()) + # I don't see reason to return the serializers fields in other actions + if method_name not in ['update', 'create']: return [] serializer_class = self.get_serializer_class(view, method_func) From a73d1da3227ca564737555cd36760f2b4448e89d Mon Sep 17 00:00:00 2001 From: Guy D Date: Tue, 20 Mar 2018 08:38:42 +0200 Subject: [PATCH 7/8] fixing actions --- drf_openapi/entities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drf_openapi/entities.py b/drf_openapi/entities.py index f8eb03b..ce8fb97 100644 --- a/drf_openapi/entities.py +++ b/drf_openapi/entities.py @@ -88,7 +88,7 @@ def get(cls, request_version): class OpenApiSchemaGenerator(SchemaGenerator): def __init__(self, version, actions=None, title=None, url=None, description=None, patterns=None, urlconf=None): self.version = version - self.actions = None + self.actions = actions super(OpenApiSchemaGenerator, self).__init__(title, url, description, patterns, urlconf) def get_schema(self, request=None, public=False): From c7bf9c2c78302317b92f89baa767085d7966fe28 Mon Sep 17 00:00:00 2001 From: Guy D Date: Tue, 20 Mar 2018 10:08:08 +0200 Subject: [PATCH 8/8] supprting write_only fields --- drf_openapi/entities.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/drf_openapi/entities.py b/drf_openapi/entities.py index ce8fb97..825c6e8 100644 --- a/drf_openapi/entities.py +++ b/drf_openapi/entities.py @@ -15,7 +15,7 @@ from rest_framework.schemas import SchemaGenerator from rest_framework.schemas.generators import insert_into, distribute_links, LinkNode from rest_framework.schemas.inspectors import get_pk_description, field_to_schema - +import copy from drf_openapi.codec import _get_parameters class PaginatedListSerializer: @@ -390,28 +390,39 @@ def get_serializer_fields(self, path, method, view, version=None, method_func=No return fields + def remove_write_only_fields(self, field): + if isinstance(field, serializers.ListSerializer): + fields = [key for key, value in field.child.fields.items() if value.write_only] + for field_name in fields: + field.child.fields.pop(field_name) + + for sub_field in field.child.fields: + self.remove_write_only_fields(sub_field) + + + def get_response_object(self, response_serializer_class, description): fields = [] serializer = response_serializer_class() nested_obj = {} - + # I copied the serializer so I will be able to alter and not to affect other behaviours + serializer = copy.deepcopy(serializer) for field in serializer.fields.values(): + self.remove_write_only_fields(field) + # we don't want to render write only fields in the response if field.write_only: - continue - + continue # If field is a serializer, attempt to get its schema. if isinstance(field, serializers.Serializer): subfield_schema = self.get_response_object(field.__class__, None)[0].get('schema') - # If the schema exists, use it as the nested_obj if subfield_schema is not None: nested_obj[field.field_name] = subfield_schema nested_obj[field.field_name]['description'] = field.help_text continue - - # Otherwise, carry-on and use the field's schema. + # Otherwise, carry-on and use the field's schema.get_filter_fields fallback_schema = self.fallback_schema_from_field(field) fields.append(Field( name=field.field_name, @@ -419,9 +430,7 @@ def get_response_object(self, response_serializer_class, description): required=field.required, schema=fallback_schema if fallback_schema else field_to_schema(field), )) - res = _get_parameters(Link(fields=fields), None) - if not res: if nested_obj: return { @@ -447,7 +456,6 @@ def get_response_object(self, response_serializer_class, description): for status_code, description in getattr(response_meta, 'error_status_codes', {}).items(): error_status_codes[status_code] = {'description': description} - return response_schema, error_status_codes