From 8a8a9faeeb8271df6bd1a643819be0e040e8b5d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristi=20V=C3=AEjdea?= Date: Thu, 21 Dec 2017 17:12:58 +0100 Subject: [PATCH 1/4] Fix swagger Try it out and make endpoints work --- .../templates/drf-yasg/swagger-ui.html | 9 +++++- .../migrations/0002_auto_20171221_1635.py | 28 ++++++++++++++++++ testproj/articles/serializers.py | 6 ++-- testproj/articles/views.py | 4 +-- testproj/db.sqlite3 | Bin 155648 -> 163840 bytes testproj/snippets/serializers.py | 6 ++-- tests/reference.yaml | 10 +++---- 7 files changed, 51 insertions(+), 12 deletions(-) create mode 100644 testproj/articles/migrations/0002_auto_20171221_1635.py diff --git a/src/drf_yasg/templates/drf-yasg/swagger-ui.html b/src/drf_yasg/templates/drf-yasg/swagger-ui.html index 12ac96a..0a2ad2a 100644 --- a/src/drf_yasg/templates/drf-yasg/swagger-ui.html +++ b/src/drf_yasg/templates/drf-yasg/swagger-ui.html @@ -163,7 +163,14 @@ plugins: [ SwaggerUIBundle.plugins.DownloadUrl ], - layout: "StandaloneLayout" + layout: "StandaloneLayout", + filter: true, + requestInterceptor: function(request) { + console.log(request); + var headers = request.headers || {}; + headers["X-CSRFToken"] = document.querySelector("[name=csrfmiddlewaretoken]").value; + return request; + } }; var swaggerSettings = {}; diff --git a/testproj/articles/migrations/0002_auto_20171221_1635.py b/testproj/articles/migrations/0002_auto_20171221_1635.py new file mode 100644 index 0000000..f21f376 --- /dev/null +++ b/testproj/articles/migrations/0002_auto_20171221_1635.py @@ -0,0 +1,28 @@ +# Generated by Django 2.0 on 2017-12-21 15:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('articles', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='article', + name='body', + field=models.TextField(help_text='article model help_text', max_length=5000), + ), + migrations.AlterField( + model_name='article', + name='slug', + field=models.SlugField(blank=True, help_text='slug model help_text', unique=True), + ), + migrations.AlterField( + model_name='article', + name='title', + field=models.CharField(help_text='title model help_text', max_length=255, unique=True), + ), + ] diff --git a/testproj/articles/serializers.py b/testproj/articles/serializers.py index a01ad8c..d488ec9 100644 --- a/testproj/articles/serializers.py +++ b/testproj/articles/serializers.py @@ -7,15 +7,17 @@ class ArticleSerializer(serializers.ModelSerializer): references = serializers.DictField( help_text="this is a really bad example", child=serializers.URLField(help_text="but i needed to test these 2 fields somehow"), + read_only=True, ) - uuid = serializers.UUIDField(help_text="should articles have UUIDs?") + uuid = serializers.UUIDField(help_text="should articles have UUIDs?", read_only=True) cover_name = serializers.FileField(use_url=False, source='cover', read_only=True) class Meta: model = Article fields = ('title', 'body', 'slug', 'date_created', 'date_modified', 'references', 'uuid', 'cover', 'cover_name') - read_only_fields = ('date_created', 'date_modified') + read_only_fields = ('date_created', 'date_modified', + 'references', 'uuid', 'cover_name') lookup_field = 'slug' extra_kwargs = {'body': {'help_text': 'body serializer help_text'}} diff --git a/testproj/articles/views.py b/testproj/articles/views.py index 3a15d9d..10eb2ce 100644 --- a/testproj/articles/views.py +++ b/testproj/articles/views.py @@ -45,8 +45,8 @@ class ArticleViewSet(viewsets.ModelViewSet): max_page_size = 5 filter_backends = (DjangoFilterBackend, OrderingFilter) filter_fields = ('title',) - ordering_fields = ('date_modified',) - ordering = ('username',) + ordering_fields = ('date_modified','date_created') + ordering = ('date_created',) @swagger_auto_schema(auto_schema=NoPagingAutoSchema) @list_route(methods=['get']) diff --git a/testproj/db.sqlite3 b/testproj/db.sqlite3 index 40d2009a93a115d5dd87efdc84bd0a12dc0691f0..18280dff6bba2fcf1b5770f8b06f9a0e64f826b3 100644 GIT binary patch delta 1541 zcmah}&2Jk;6yM1@j@OPePKp{r6&z?vVp2Q1vsr&6CCxTpa_HKPas08UIJVc$uI*hr zj+?|jkPlTH5m6-e01^_aassK;NI?RL68->sK&S^!5#mCHP!HwMiZZs#heCnfc~7(R ze!utT&3m)kx2f%I^E+p{y#zrFV;R6gV!5(+o(0}dmTy3^$FjTMVLRJq*{w6Sh-GTB zr=94Z;FINwk}hP{XqrZeX3qOYk@J$$mdvLG*!azT?KMP5Z=@0*jCzC{?SddOxNhh@*9(6c+ zIxVD~!g1xqwY5wcegVHEPT8>aB(Huj`o^*mz!(8z_3q^W+}b$@ti+Ckr+TfU6vbOh zsl1d$i8VQiIQJlf6Qb$CNNIC6#dx!;i!-&s)yP6#+!Quvg$?@E8Ga;?$qMWm+8kNp z=*5+}4Sz81D@euF#j%Z7P9)BbBi7|+y{sE0B~{Uq5{LCvbM9*De-LQS#j(ieX3e+| z0RBqgVa8kk37So0FV!Sj>!9@5e(R3GW&~YFMAs2MxzD<`b|*l)aJ`)`c`8MOADUgS z<;ts7X}wfdvSZnDHD5z&epARr)IfM~BT$Q|VpymONL0m~EC<7RCSD8W<29ZZ!y#Fm zk8|-bPYXFoj)zMgf59h9u~0F_MrvYgA)AaYDnZ4+=~p(C7@FH+q=5$8*CN|n|xtAX{D7OEzr{%i4AgicO#f8x?rE|5Z@LTYwFj-yH;r7kL$ zrx&tK+?q+=|WA9au4piWHvC+~Abh2Zcoj!o^A3VV=HF3;SadwOg_j)|w zI$ueq3Yn*Zp1P=iqyl?;GuS?D;@;K31$|iqN!_P`!Nc1Um@paeetCUL1ta>)D(E41 zFJ02NRd61h(?3?hYx+wXcu&8Z20c2|z%!)d;!Go1X9L`A8Q!t=ck~9GV29BYMR5A? zSOd@R>|N=g+OS(Ymj1(Ty6qSEX`8`j0`PmHF>s&Xfv&TpK>c42w{FKV0go}~E*$PB zdFnqZi{k2Fp^WoRMaf zbY=MU;UO)lW`@oW43(-Cxu^{}t~#=1C8J4&Y75>vjt2yMfFs?5&rxI_b<#o%;e^j& K`;XB4m;HB~)w$;Y delta 484 zcmZuu!E4iC9Q}T2*2bpw_XVvLN}DWmVsT|1GK*N&u^o1-qPv>Lgb$2+_i1nFRSX}6$M2_ZLQ%*KEi zYyG)9Q2o;Sgb76+4N`O_oYvmU;i#}IUhR`4Hf4N<-|z_c@f9|)8BSO4;nbCEq9P3* z&zH+eMoy@bxz&AcZ27E%$M}`tZ<2$ttp+=*oSKj; zj4l6-nB{j(C{2wGgQ~R1-} diff --git a/testproj/snippets/serializers.py b/testproj/snippets/serializers.py index c149917..6a0d268 100644 --- a/testproj/snippets/serializers.py +++ b/testproj/snippets/serializers.py @@ -30,13 +30,15 @@ class SnippetSerializer(serializers.Serializer): language = LanguageSerializer(help_text="Sample help text for language") styles = serializers.MultipleChoiceField(choices=STYLE_CHOICES, default=['friendly']) lines = serializers.ListField(child=serializers.IntegerField(), allow_empty=True, allow_null=True, required=False) - example_projects = serializers.ListSerializer(child=ExampleProjectSerializer()) - difficulty_factor = serializers.FloatField(help_text="this is here just to test FloatField") + example_projects = serializers.ListSerializer(child=ExampleProjectSerializer(), read_only=True) + difficulty_factor = serializers.FloatField(help_text="this is here just to test FloatField", read_only=True) def create(self, validated_data): """ Create and return a new `Snippet` instance, given the validated data. """ + del validated_data['styles'] + del validated_data['lines'] return Snippet.objects.create(**validated_data) def update(self, instance, validated_data): diff --git a/tests/reference.yaml b/tests/reference.yaml index ac7f253..24d4af2 100644 --- a/tests/reference.yaml +++ b/tests/reference.yaml @@ -459,8 +459,6 @@ definitions: required: - title - body - - references - - uuid type: object properties: title: @@ -489,14 +487,16 @@ definitions: description: but i needed to test these 2 fields somehow type: string format: uri + readOnly: true uuid: description: should articles have UUIDs? type: string format: uuid + readOnly: true cover: type: string - format: uri readOnly: true + format: uri cover_name: type: string readOnly: true @@ -516,8 +516,6 @@ definitions: required: - code - language - - example_projects - - difficulty_factor type: object properties: id: @@ -1020,9 +1018,11 @@ definitions: type: array items: $ref: '#/definitions/Project' + readOnly: true difficulty_factor: description: this is here just to test FloatField type: number + readOnly: true UserSerializerrr: required: - username From f05889292abf04dbf4148e575d35a56850d5e32a Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Fri, 22 Dec 2017 21:00:13 +0100 Subject: [PATCH 2/4] Add type inspection for PrimaryKeyRelatedField (#26) Guess the type for PrimaryKeyRelatedField from the related ModelField. --- src/drf_yasg/generators.py | 7 +++---- src/drf_yasg/utils.py | 11 ++++++++++- tests/conftest.py | 2 +- tests/reference.yaml | 2 +- tox.ini | 2 +- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/drf_yasg/generators.py b/src/drf_yasg/generators.py index 522d27d..6eeef64 100644 --- a/src/drf_yasg/generators.py +++ b/src/drf_yasg/generators.py @@ -1,7 +1,6 @@ import re from collections import defaultdict, OrderedDict -import django.db.models import uritemplate from coreapi.compat import force_text from rest_framework.schemas.generators import SchemaGenerator, EndpointEnumerator as _EndpointEnumerator @@ -10,6 +9,7 @@ from rest_framework.schemas.inspectors import get_pk_description from . import openapi from .inspectors import SwaggerAutoSchema from .openapi import ReferenceResolver +from .utils import get_schema_type_from_model_field PATH_PARAMETER_RE = re.compile(r'{(?P\w+)}') @@ -221,6 +221,8 @@ class OpenAPISchemaGenerator(object): model_field = model._meta.get_field(variable) except Exception: # pragma: no cover model_field = None + else: + type = get_schema_type_from_model_field(model_field) if model_field is not None and model_field.help_text: description = force_text(model_field.help_text) @@ -229,9 +231,6 @@ class OpenAPISchemaGenerator(object): if hasattr(view_cls, 'lookup_value_regex') and getattr(view_cls, 'lookup_field', None) == variable: pattern = view_cls.lookup_value_regex - elif isinstance(model_field, django.db.models.AutoField): - type = openapi.TYPE_INTEGER - field = openapi.Parameter( name=variable, required=True, diff --git a/src/drf_yasg/utils.py b/src/drf_yasg/utils.py index 3a74ae8..cab2beb 100644 --- a/src/drf_yasg/utils.py +++ b/src/drf_yasg/utils.py @@ -1,6 +1,7 @@ from collections import OrderedDict from django.core.validators import RegexValidator +from django.db import models from django.utils.encoding import force_text from rest_framework import serializers from rest_framework.mixins import RetrieveModelMixin, DestroyModelMixin, UpdateModelMixin @@ -13,6 +14,12 @@ from .errors import SwaggerGenerationError no_body = object() +def get_schema_type_from_model_field(model_field): + if isinstance(model_field, models.AutoField): + return openapi.TYPE_INTEGER + return openapi.TYPE_STRING + + def is_list_view(path, method, view): """Check if the given path/method appears to represent a list view (as opposed to a detail/instance view). @@ -238,8 +245,10 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, ** items=child_schema, unique_items=True, # is this OK? ) + elif isinstance(field, serializers.PrimaryKeyRelatedField): + model = field.queryset.model + return SwaggerType(type=get_schema_type_from_model_field(model._meta.pk)) elif isinstance(field, serializers.RelatedField): - # TODO: infer type for PrimaryKeyRelatedField? return SwaggerType(type=openapi.TYPE_STRING) # ------ CHOICES elif isinstance(field, serializers.MultipleChoiceField): diff --git a/tests/conftest.py b/tests/conftest.py index d4efdb1..d390a8f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -40,7 +40,7 @@ def swagger_dict(generator): @pytest.fixture -def validate_schema(): +def validate_schema(db): def validate_schema(swagger): from flex.core import parse as validate_flex from swagger_spec_validator.validator20 import validate_spec as validate_ssv diff --git a/tests/reference.yaml b/tests/reference.yaml index 24d4af2..9c04800 100644 --- a/tests/reference.yaml +++ b/tests/reference.yaml @@ -1042,7 +1042,7 @@ definitions: snippets: type: array items: - type: string + type: integer uniqueItems: true last_connected_ip: description: i'm out of ideas diff --git a/tox.ini b/tox.ini index a254d67..5d5c09f 100644 --- a/tox.ini +++ b/tox.ini @@ -30,7 +30,7 @@ deps = -rrequirements/test.txt commands = - pytest --cov-config .coveragerc --cov-append --cov + pytest --cov-config .coveragerc --cov-append --cov {posargs} [testenv:py36-drfmaster] pip_pre = True From 9f6ee4da87faef36fb3d93715bdb5cdb65b3cd99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristi=20V=C3=AEjdea?= Date: Sat, 23 Dec 2017 11:52:31 +0100 Subject: [PATCH 3/4] Improve RelatedField and callable default handling - callable default values will now be properly called - PrimaryKeyRelatedField and SlugRelatedField will now return an appropriate type based on the relation model's Field - mock views now have a request object bound even when public is True --- src/drf_yasg/generators.py | 44 ++--- src/drf_yasg/openapi.py | 2 + src/drf_yasg/utils.py | 160 ++++++++++++++++-- testproj/articles/migrations/0001_initial.py | 12 +- .../migrations/0002_auto_20171221_1635.py | 28 --- testproj/articles/models.py | 1 + testproj/articles/serializers.py | 11 +- testproj/articles/views.py | 8 +- testproj/db.sqlite3 | Bin 163840 -> 155648 bytes testproj/snippets/migrations/0001_initial.py | 10 +- .../migrations/0002_auto_20171205_0505.py | 27 --- testproj/snippets/serializers.py | 18 +- testproj/users/serializers.py | 9 +- tests/conftest.py | 29 +++- tests/reference.yaml | 31 +++- tests/test_reference_schema.py | 2 +- tests/test_schema_generator.py | 24 +-- 17 files changed, 274 insertions(+), 142 deletions(-) delete mode 100644 testproj/articles/migrations/0002_auto_20171221_1635.py delete mode 100644 testproj/snippets/migrations/0002_auto_20171205_0505.py diff --git a/src/drf_yasg/generators.py b/src/drf_yasg/generators.py index 6eeef64..3f1979f 100644 --- a/src/drf_yasg/generators.py +++ b/src/drf_yasg/generators.py @@ -2,14 +2,12 @@ import re from collections import defaultdict, OrderedDict import uritemplate -from coreapi.compat import force_text from rest_framework.schemas.generators import SchemaGenerator, EndpointEnumerator as _EndpointEnumerator -from rest_framework.schemas.inspectors import get_pk_description from . import openapi from .inspectors import SwaggerAutoSchema from .openapi import ReferenceResolver -from .utils import get_schema_type_from_model_field +from .utils import inspect_model_field, get_model_field PATH_PARAMETER_RE = re.compile(r'{(?P\w+)}') @@ -82,9 +80,9 @@ class OpenAPISchemaGenerator(object): :return: the generated Swagger specification :rtype: openapi.Swagger """ - endpoints = self.get_endpoints(None if public else request) + endpoints = self.get_endpoints(request) components = ReferenceResolver(openapi.SCHEMA_DEFINITIONS) - paths = self.get_paths(endpoints, components) + paths = self.get_paths(endpoints, components, public) url = self._gen.url if not url and request is not None: @@ -114,9 +112,9 @@ class OpenAPISchemaGenerator(object): return view def get_endpoints(self, request=None): - """Iterate over all the registered endpoints in the API. + """Iterate over all the registered endpoints in the API and return a fake view with the right parameters. - :param rest_framework.request.Request request: used for returning only endpoints available to the given request + :param rest_framework.request.Request request: request to bind to the endpoint views :return: {path: (view_class, list[(http_method, view_instance)]) :rtype: dict """ @@ -151,11 +149,12 @@ class OpenAPISchemaGenerator(object): """ return self._gen.get_keys(subpath, method, view) - def get_paths(self, endpoints, components): + def get_paths(self, endpoints, components, public): """Generate the Swagger Paths for the API from the given endpoints. :param dict endpoints: endpoints as returned by get_endpoints :param ReferenceResolver components: resolver/container for Swagger References + :param bool public: if True, all endpoints are included regardless of access through `request` :rtype: openapi.Paths """ if not endpoints: @@ -169,7 +168,7 @@ class OpenAPISchemaGenerator(object): path_parameters = self.get_path_parameters(path, view_cls) operations = {} for method, view in methods: - if not self._gen.has_view_permissions(path, method, view): + if not public and not self._gen.has_view_permissions(path, method, view): continue operation_keys = self.get_operation_keys(path[len(prefix):], method, view) @@ -209,35 +208,20 @@ class OpenAPISchemaGenerator(object): :rtype: list[openapi.Parameter] """ parameters = [] + queryset = getattr(view_cls, 'queryset', None) model = getattr(getattr(view_cls, 'queryset', None), 'model', None) for variable in uritemplate.variables(path): - pattern = None - type = openapi.TYPE_STRING - description = None - if model is not None: - # Attempt to infer a field description if possible. - try: - model_field = model._meta.get_field(variable) - except Exception: # pragma: no cover - model_field = None - else: - type = get_schema_type_from_model_field(model_field) + model, model_field = get_model_field(queryset, variable) + attrs = inspect_model_field(model, model_field) + if hasattr(view_cls, 'lookup_value_regex') and getattr(view_cls, 'lookup_field', None) == variable: + attrs['pattern'] = view_cls.lookup_value_regex - if model_field is not None and model_field.help_text: - description = force_text(model_field.help_text) - elif model_field is not None and model_field.primary_key: - description = get_pk_description(model, model_field) - - if hasattr(view_cls, 'lookup_value_regex') and getattr(view_cls, 'lookup_field', None) == variable: - pattern = view_cls.lookup_value_regex field = openapi.Parameter( name=variable, required=True, in_=openapi.IN_PATH, - type=type, - pattern=pattern, - description=description, + **attrs ) parameters.append(field) diff --git a/src/drf_yasg/openapi.py b/src/drf_yasg/openapi.py index 7e6e34b..8b3d3b0 100644 --- a/src/drf_yasg/openapi.py +++ b/src/drf_yasg/openapi.py @@ -309,6 +309,7 @@ class Items(SwaggerDict): :param .Items items: only valid if `type` is ``array`` """ super(Items, self).__init__(**extra) + assert type is not None, "type is required!" self.type = type self.format = format self.enum = enum @@ -372,6 +373,7 @@ class Schema(SwaggerDict): # common error raise AssertionError( "the `requires` attribute of schema must be an array of required properties, not a boolean!") + assert type is not None, "type is required!" self.description = description self.required = required self.type = type diff --git a/src/drf_yasg/utils.py b/src/drf_yasg/utils.py index cab2beb..27c713e 100644 --- a/src/drf_yasg/utils.py +++ b/src/drf_yasg/utils.py @@ -1,3 +1,4 @@ +import logging from collections import OrderedDict from django.core.validators import RegexValidator @@ -5,21 +6,19 @@ from django.db import models from django.utils.encoding import force_text from rest_framework import serializers from rest_framework.mixins import RetrieveModelMixin, DestroyModelMixin, UpdateModelMixin +from rest_framework.schemas.inspectors import get_pk_description from rest_framework.settings import api_settings +from rest_framework.utils import json, encoders from . import openapi from .errors import SwaggerGenerationError +logger = logging.getLogger(__name__) + #: used to forcibly remove the body of a request via :func:`.swagger_auto_schema` no_body = object() -def get_schema_type_from_model_field(model_field): - if isinstance(model_field, models.AutoField): - return openapi.TYPE_INTEGER - return openapi.TYPE_STRING - - def is_list_view(path, method, view): """Check if the given path/method appears to represent a list view (as opposed to a detail/instance view). @@ -164,6 +163,87 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=None, request_bod return decorator +def get_model_field(queryset, field_name): + """Try to get information about a model and model field from a queryset. + + :param queryset: the queryset + :param field_name: the target field name + :returns: the model and target field from the queryset as a 2-tuple; both elements can be ``None`` + :rtype: tuple + """ + model = getattr(queryset, 'model', None) + try: + model_field = model._meta.get_field(field_name) + except Exception: # pragma: no cover + model_field = None + + return model, model_field + + +model_field_to_swagger_type = { + models.AutoField: (openapi.TYPE_INTEGER, None), + models.BinaryField: (openapi.TYPE_STRING, openapi.FORMAT_BINARY), + models.BooleanField: (openapi.TYPE_BOOLEAN, None), + models.NullBooleanField: (openapi.TYPE_BOOLEAN, None), + models.DateTimeField: (openapi.TYPE_STRING, openapi.FORMAT_DATETIME), + models.DateField: (openapi.TYPE_STRING, openapi.FORMAT_DATE), + models.DecimalField: (openapi.TYPE_NUMBER, None), + models.DurationField: (openapi.TYPE_INTEGER, None), + models.FloatField: (openapi.TYPE_NUMBER, None), + models.IntegerField: (openapi.TYPE_INTEGER, None), + models.IPAddressField: (openapi.TYPE_STRING, openapi.FORMAT_IPV4), + models.GenericIPAddressField: (openapi.TYPE_STRING, openapi.FORMAT_IPV6), + models.SlugField: (openapi.TYPE_STRING, openapi.FORMAT_SLUG), + models.TextField: (openapi.TYPE_STRING, None), + models.TimeField: (openapi.TYPE_STRING, None), + models.UUIDField: (openapi.TYPE_STRING, openapi.FORMAT_UUID), + models.CharField: (openapi.TYPE_STRING, None), +} + + +def inspect_model_field(model, model_field): + """Extract information from a django model field instance. + + :param model: the django model + :param model_field: a field on the model + :return: description, type, format and pattern extracted from the model field + :rtype: OrderedDict + """ + if model is not None and model_field is not None: + for model_field_class, tf in model_field_to_swagger_type.items(): + if isinstance(model_field, model_field_class): + swagger_type, format = tf + break + else: + swagger_type, format = None, None + + if format is None or format == openapi.FORMAT_SLUG: + pattern = find_regex(model_field) + else: + pattern = None + + if model_field.help_text: + description = force_text(model_field.help_text) + elif model_field.primary_key: + description = get_pk_description(model, model_field) + else: + description = None + else: + description = None + swagger_type = None + format = None + pattern = None + + result = OrderedDict([ + ('description', description), + ('type', swagger_type or openapi.TYPE_STRING), + ('format', format), + ('pattern', pattern) + ]) + # TODO: filter none + return result + + def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **kwargs): """Convert a drf Serializer or Field instance into a Swagger object. @@ -183,17 +263,50 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, ** description = force_text(field.help_text) if field.help_text else None description = description if swagger_object_type != openapi.Items else None # Items has no description either - def SwaggerType(**instance_kwargs): + def SwaggerType(existing_object=None, **instance_kwargs): if swagger_object_type == openapi.Parameter and 'required' not in instance_kwargs: instance_kwargs['required'] = field.required if swagger_object_type != openapi.Items and 'default' not in instance_kwargs: default = getattr(field, 'default', serializers.empty) if default is not serializers.empty: - instance_kwargs['default'] = default + if callable(default): + try: + if hasattr(default, 'set_context'): + default.set_context(field) + default = default() + except Exception as e: + logger.warning("default for %s is callable but it raised an exception when " + "called; 'default' field will not be added to schema", field, exc_info=True) + default = None + + if default is not None: + try: + default = field.to_representation(default) + # JSON roundtrip ensures that the value is valid JSON; + # for example, sets get transformed into lists + default = json.loads(json.dumps(default, cls=encoders.JSONEncoder)) + except Exception as e: + logger.warning("'default' on schema for %s will not be set because " + "to_representation raised an exception", field, exc_info=True) + default = None + + if default is not None: + instance_kwargs['default'] = default + if swagger_object_type == openapi.Schema and 'read_only' not in instance_kwargs: if field.read_only: instance_kwargs['read_only'] = True instance_kwargs.update(kwargs) + instance_kwargs.pop('title', None) + instance_kwargs.pop('description', None) + + if existing_object is not None: + existing_object.title = title + existing_object.description = description + for attr, val in instance_kwargs.items(): + setattr(existing_object, attr, val) + return existing_object + return swagger_object_type(title=title, description=description, **instance_kwargs) # arrays in Schema have Schema elements, arrays in Parameter and Items have Items elements @@ -246,8 +359,27 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, ** unique_items=True, # is this OK? ) elif isinstance(field, serializers.PrimaryKeyRelatedField): - model = field.queryset.model - return SwaggerType(type=get_schema_type_from_model_field(model._meta.pk)) + if field.pk_field: + result = serializer_field_to_swagger(field.pk_field, swagger_object_type, definitions, **kwargs) + return SwaggerType(existing_object=result) + + attrs = {'type': openapi.TYPE_STRING} + try: + model = field.queryset.model + pk_field = model._meta.pk + except Exception: + logger.warning("an exception was raised when attempting to extract the primary key related to %s; " + "falling back to plain string" % field, exc_info=True) + else: + attrs.update(inspect_model_field(model, pk_field)) + + return SwaggerType(**attrs) + elif isinstance(field, serializers.HyperlinkedRelatedField): + return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_URI) + elif isinstance(field, serializers.SlugRelatedField): + model, model_field = get_model_field(field.queryset, field.slug_field) + attrs = inspect_model_field(model, model_field) + return SwaggerType(**attrs) elif isinstance(field, serializers.RelatedField): return SwaggerType(type=openapi.TYPE_STRING) # ------ CHOICES @@ -262,7 +394,7 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, ** elif isinstance(field, serializers.ChoiceField): return SwaggerType(type=openapi.TYPE_STRING, enum=list(field.choices.keys())) # ------ BOOL - elif isinstance(field, serializers.BooleanField): + elif isinstance(field, (serializers.BooleanField, serializers.NullBooleanField)): return SwaggerType(type=openapi.TYPE_BOOLEAN) # ------ NUMERIC elif isinstance(field, (serializers.DecimalField, serializers.FloatField)): @@ -271,6 +403,8 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, ** elif isinstance(field, serializers.IntegerField): # TODO: min_value max_value return SwaggerType(type=openapi.TYPE_INTEGER) + elif isinstance(field, serializers.DurationField): + return SwaggerType(type=openapi.TYPE_INTEGER) # ------ STRING elif isinstance(field, serializers.EmailField): return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_EMAIL) @@ -317,8 +451,10 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, ** type=openapi.TYPE_OBJECT, additional_properties=child_schema ) + elif isinstance(field, serializers.ModelField): + return SwaggerType(type=openapi.TYPE_STRING) - # TODO unhandled fields: TimeField DurationField HiddenField ModelField NullBooleanField? JSONField + # TODO unhandled fields: TimeField HiddenField JSONField # everything else gets string by default return SwaggerType(type=openapi.TYPE_STRING) diff --git a/testproj/articles/migrations/0001_initial.py b/testproj/articles/migrations/0001_initial.py index 75452fd..6d82083 100644 --- a/testproj/articles/migrations/0001_initial.py +++ b/testproj/articles/migrations/0001_initial.py @@ -1,6 +1,8 @@ -# Generated by Django 2.0 on 2017-12-05 04:05 +# Generated by Django 2.0 on 2017-12-23 09:07 +from django.conf import settings from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): @@ -8,6 +10,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -15,12 +18,13 @@ class Migration(migrations.Migration): name='Article', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(help_text='Main article headline', max_length=255, unique=True)), - ('body', models.TextField(help_text='Article content', max_length=5000)), - ('slug', models.SlugField(blank=True, help_text='Unique URL slug identifying the article', unique=True)), + ('title', models.CharField(help_text='title model help_text', max_length=255, unique=True)), + ('body', models.TextField(help_text='article model help_text', max_length=5000)), + ('slug', models.SlugField(blank=True, help_text='slug model help_text', unique=True)), ('date_created', models.DateTimeField(auto_now_add=True)), ('date_modified', models.DateTimeField(auto_now=True)), ('cover', models.ImageField(blank=True, upload_to='article/original/')), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='articles', to=settings.AUTH_USER_MODEL)), ], ), ] diff --git a/testproj/articles/migrations/0002_auto_20171221_1635.py b/testproj/articles/migrations/0002_auto_20171221_1635.py deleted file mode 100644 index f21f376..0000000 --- a/testproj/articles/migrations/0002_auto_20171221_1635.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 2.0 on 2017-12-21 15:35 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('articles', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='article', - name='body', - field=models.TextField(help_text='article model help_text', max_length=5000), - ), - migrations.AlterField( - model_name='article', - name='slug', - field=models.SlugField(blank=True, help_text='slug model help_text', unique=True), - ), - migrations.AlterField( - model_name='article', - name='title', - field=models.CharField(help_text='title model help_text', max_length=255, unique=True), - ), - ] diff --git a/testproj/articles/models.py b/testproj/articles/models.py index 3031616..24878b8 100644 --- a/testproj/articles/models.py +++ b/testproj/articles/models.py @@ -7,5 +7,6 @@ class Article(models.Model): slug = models.SlugField(help_text="slug model help_text", unique=True, blank=True) date_created = models.DateTimeField(auto_now_add=True) date_modified = models.DateTimeField(auto_now=True) + author = models.ForeignKey('auth.User', related_name='articles', on_delete=models.CASCADE) cover = models.ImageField(upload_to='article/original/', blank=True) diff --git a/testproj/articles/serializers.py b/testproj/articles/serializers.py index d488ec9..ee90e2c 100644 --- a/testproj/articles/serializers.py +++ b/testproj/articles/serializers.py @@ -14,12 +14,19 @@ class ArticleSerializer(serializers.ModelSerializer): class Meta: model = Article - fields = ('title', 'body', 'slug', 'date_created', 'date_modified', + fields = ('title', 'author', 'body', 'slug', 'date_created', 'date_modified', 'references', 'uuid', 'cover', 'cover_name') read_only_fields = ('date_created', 'date_modified', 'references', 'uuid', 'cover_name') lookup_field = 'slug' - extra_kwargs = {'body': {'help_text': 'body serializer help_text'}} + extra_kwargs = { + 'body': {'help_text': 'body serializer help_text'}, + 'author': { + 'default': serializers.CurrentUserDefault(), + 'help_text': "The ID of the user that created this article; if none is provided, " + "defaults to the currently logged in user." + }, + } class ImageUploadSerializer(serializers.Serializer): diff --git a/testproj/articles/views.py b/testproj/articles/views.py index 10eb2ce..d66e058 100644 --- a/testproj/articles/views.py +++ b/testproj/articles/views.py @@ -20,6 +20,11 @@ class NoPagingAutoSchema(SwaggerAutoSchema): return False +class ArticlePagination(LimitOffsetPagination): + default_limit = 5 + max_limit = 25 + + @method_decorator(name='list', decorator=swagger_auto_schema( operation_description="description from swagger_auto_schema via method_decorator" )) @@ -41,8 +46,7 @@ class ArticleViewSet(viewsets.ModelViewSet): lookup_value_regex = r'[a-z0-9]+(?:-[a-z0-9]+)' serializer_class = serializers.ArticleSerializer - pagination_class = LimitOffsetPagination - max_page_size = 5 + pagination_class = ArticlePagination filter_backends = (DjangoFilterBackend, OrderingFilter) filter_fields = ('title',) ordering_fields = ('date_modified','date_created') diff --git a/testproj/db.sqlite3 b/testproj/db.sqlite3 index 18280dff6bba2fcf1b5770f8b06f9a0e64f826b3..532719e25144fa0be7b7ed911e26561061456079 100644 GIT binary patch delta 1884 zcmah~eQXnD7{BMPYuB$`%RcI$UAwZu#^`qM-Syg=B9yKBf?gTx7_BBPd7_-QeF-*h#{q4nwl#C8X?-l z)JVK_4S+nIUe?VsbiYx`THSv$$^hlz8E>CKDp}bIz*A;;cj=`ER=xt@c#3B@pHA9s z>#770&%4o1t#r~xRRC_j9DGN$bko*Z3D63_^=PE}CR72i9K*Y*0ksrq;wk{f%PWk5 zD(PGkx9;JVC_0$(a#^X_j+)j^(;TO$pol8W(&~%FGh$XUTkC8UCP>l4cHvc6y<##~ zI8e5z@QoB0C4; zPS3^)%52_LixaNu)#+tH(R6NlCA-wsF@v74YnMIE<6=xND$ z_r@j)T+dXoFPRDs#l`%g-ca#iB+$1%KRhHBM2|4qzh`D*WFWAOrjW;lXxFM{z8kTf zoQLx=l=S6UyLlxaz1=L`$+2#Z;TB*0LfDt1&|GRNhGH{`0^+zSmdhq$VkS4Ao-DZM z4v9ihI1uR-GJ#||84eAi;MgD%Li>_|Vu}hB{YV%aMd8p+G15PDAQBo%g^QDigT0dD z&WX`MHWKnPfw56W=no$bXTr474oCX^DA1cu3dJEY*gr}KLi;j8DCJfHq=e)oJ(#7+ zO1v%#xhQVAhL)9hXym0mx+NHYV1Iqaq`ZqWU~*~xz|7bcjBKg7p>NZFq`js7yn4H# zL)}mnR=uK96Enu!Mmzq7V)6~cL4(`?HO!9x~J(a zbqv8%pw{2TwguazPxZF-<%5YvbLn^#Jv^x8vA%S9Hw=T+roW~#HeioSzw15njzRcQ zs~oS^(%ufoOMEXZLFtA7UFHt`8EhRUW4b%Kr?sc5uWHV!ud7>B^C~}af(YUtVlsXR z=g4W}cg9^3GNp9KtgOc3a=~*cxi15)&@87jFd{#ggeT;$Gw{5+yqw&ubWpchcJ#m& zqJ29jpPB%d{9YE!#K!IJ^5{vGbD@rrBMI0B4*BH-i~%j*O+dHGzQ7^5a|%jTl-lCA zm&MAOX;=qcrS;SB4nT|aE72osMVN$*az=!k;FNty*a9{=orL?!g2WVDGdMIBhJk1K zLTn-}?@2?Qq^qfw<7tp}3nrV!5_G`=lA}3jD1D!WCQxA*Tr9odgLi>ksO2@5Va32L zX5)!@*_;5UIiD40lyuBTS38?zA^{=d7~z$*G1v;NG8coBy2)HT;dB%d^9A{G3_d>6 zzR=*&SVk0Q{Iau0i#WQnUr{k0rQCch5%a_!HEHSD>-UBHjzOW(iF$A^h#9udKB+EuaN!(HaiIj delta 2049 zcmah~S!@$m6!o>^-SOC2WCC&Q5cc5Y%|2t6#3UhXc1UoX#%<*|apEQMLSjfr)k0E) zstQWek(j^IQq-UJ14tXIq7_T2sF1oq_kQt#`qQYiRevg4(PxJx4oDr(lg96yb7#)= zx#J_3^+%2vUSDAq6bi*UyqtKc@Y*}Gkp|&>-&tZ^nQ>Y{TqjNu5u(;SZ$4vw##~-{ zv-CviF4MoJ4^1N`-lQ~MHBQfxmVoi_)-tuizR43F&Bg~t2FDOWR49BPn+{Qke zO_MAjNm*T2v3Q?jXnbFqkW$sPMF1;^BG0s&rFW{?Vt}F5&5_X#; z#x52Cw8-JpW0a=sjl}?k1WMRukgnNL5kLt%O)<@SNmtWQ1dt*{Q*4V)+Eqgq12jV+ zafenqS;G|ph+{Yg?a)X+Ed@~id1$DSxmk}yn_b@r32N$7GdL&X_s&eTcW+L?9f;g_I1XUwM z65&)d9ZJTdqv32kof<1M8C4E_{^>#o#s)`;kBLi)atp4ow05~#RSyMTW^gpAjgO6C z`7-HbbUx`CqwnxmwzPdls& z1484BH?TB?V|L-K8@@n>&!#DSAo}2BVs!tZ$id8Le25tuJv2N?C5I=xi9pil?;Q6{ z29iO)_mGzgCWDDstiwM{_D*&U_fC3H(BBmcw)e8Veh=~{BC%e-!m-$iVHW!CJI-V*_`HxQv23uyF$J;8e9PQqHkdv#9y8SIn{{t!ztdJ~rq%N$ zf0c-;5ls0}(Kh3;$!*OrA-~iLP4drPcs2J)Gkl=TIlXWJa+wzJt0a}`*Id&Mcot@_ zX=s@JLEj0|MI$S%GfKHzK4`QyD!)=#L8BzP%~ws0#v#L2eM0xC)~30msaOA|?g357 zmD0`1*UVE3ABnVluXbi4o+m9z7D_B z*S%-Ydegn87L9G6OS)pLl27$REwR>g5B|(YW~WDEd!By?SXSmE9io<|v5<5p)|_6% zT~pwaPvtr=*Kgx_IO>t#?t{t|g$hrl+U4tgP@^sog`6EKi0ZA_u}yTe5zt ze3R@;!d6%%pGe}SPE~O7#eUcVb@Jc+(1R771K=)MImI#Z$pOfzX^pMTfj!yfSOm&| z%DoVQ_W;&Pf2dmI#1IU?8u`m1_y$(XXQHqHYUMAY@DDD3BL<(F>a;eRhG{Vy?jMoQ z$Du+pn9JqQ;vj2q!#t1MXpsl|!D@6)Z`-D|xm<9R&wE{st71p{^2V)p)F$R~4VuU45`bmyHryth z|2J}Tzr$U^`b6~SkWypOoKq0z6svH}vxLD1Iz?5L$5UD*UnZ25B%W~2jVL$b8Yr6P zL~ijLp%@vc2bxn|}2ghSt9*H Date: Sat, 23 Dec 2017 12:04:02 +0100 Subject: [PATCH 4/4] Fixed assumption of dict ordering --- src/drf_yasg/utils.py | 42 +++++++++++++++++------------------ testproj/articles/views.py | 2 +- testproj/users/serializers.py | 1 - 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/drf_yasg/utils.py b/src/drf_yasg/utils.py index 27c713e..01a2642 100644 --- a/src/drf_yasg/utils.py +++ b/src/drf_yasg/utils.py @@ -180,25 +180,25 @@ def get_model_field(queryset, field_name): return model, model_field -model_field_to_swagger_type = { - models.AutoField: (openapi.TYPE_INTEGER, None), - models.BinaryField: (openapi.TYPE_STRING, openapi.FORMAT_BINARY), - models.BooleanField: (openapi.TYPE_BOOLEAN, None), - models.NullBooleanField: (openapi.TYPE_BOOLEAN, None), - models.DateTimeField: (openapi.TYPE_STRING, openapi.FORMAT_DATETIME), - models.DateField: (openapi.TYPE_STRING, openapi.FORMAT_DATE), - models.DecimalField: (openapi.TYPE_NUMBER, None), - models.DurationField: (openapi.TYPE_INTEGER, None), - models.FloatField: (openapi.TYPE_NUMBER, None), - models.IntegerField: (openapi.TYPE_INTEGER, None), - models.IPAddressField: (openapi.TYPE_STRING, openapi.FORMAT_IPV4), - models.GenericIPAddressField: (openapi.TYPE_STRING, openapi.FORMAT_IPV6), - models.SlugField: (openapi.TYPE_STRING, openapi.FORMAT_SLUG), - models.TextField: (openapi.TYPE_STRING, None), - models.TimeField: (openapi.TYPE_STRING, None), - models.UUIDField: (openapi.TYPE_STRING, openapi.FORMAT_UUID), - models.CharField: (openapi.TYPE_STRING, None), -} +model_field_to_swagger_type = [ + (models.AutoField, (openapi.TYPE_INTEGER, None)), + (models.BinaryField, (openapi.TYPE_STRING, openapi.FORMAT_BINARY)), + (models.BooleanField, (openapi.TYPE_BOOLEAN, None)), + (models.NullBooleanField, (openapi.TYPE_BOOLEAN, None)), + (models.DateTimeField, (openapi.TYPE_STRING, openapi.FORMAT_DATETIME)), + (models.DateField, (openapi.TYPE_STRING, openapi.FORMAT_DATE)), + (models.DecimalField, (openapi.TYPE_NUMBER, None)), + (models.DurationField, (openapi.TYPE_INTEGER, None)), + (models.FloatField, (openapi.TYPE_NUMBER, None)), + (models.IntegerField, (openapi.TYPE_INTEGER, None)), + (models.IPAddressField, (openapi.TYPE_STRING, openapi.FORMAT_IPV4)), + (models.GenericIPAddressField, (openapi.TYPE_STRING, openapi.FORMAT_IPV6)), + (models.SlugField, (openapi.TYPE_STRING, openapi.FORMAT_SLUG)), + (models.TextField, (openapi.TYPE_STRING, None)), + (models.TimeField, (openapi.TYPE_STRING, None)), + (models.UUIDField, (openapi.TYPE_STRING, openapi.FORMAT_UUID)), + (models.CharField, (openapi.TYPE_STRING, None)), +] def inspect_model_field(model, model_field): @@ -210,7 +210,7 @@ def inspect_model_field(model, model_field): :rtype: OrderedDict """ if model is not None and model_field is not None: - for model_field_class, tf in model_field_to_swagger_type.items(): + for model_field_class, tf in model_field_to_swagger_type: if isinstance(model_field, model_field_class): swagger_type, format = tf break @@ -285,7 +285,7 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, ** # JSON roundtrip ensures that the value is valid JSON; # for example, sets get transformed into lists default = json.loads(json.dumps(default, cls=encoders.JSONEncoder)) - except Exception as e: + except Exception: logger.warning("'default' on schema for %s will not be set because " "to_representation raised an exception", field, exc_info=True) default = None diff --git a/testproj/articles/views.py b/testproj/articles/views.py index d66e058..860e681 100644 --- a/testproj/articles/views.py +++ b/testproj/articles/views.py @@ -49,7 +49,7 @@ class ArticleViewSet(viewsets.ModelViewSet): pagination_class = ArticlePagination filter_backends = (DjangoFilterBackend, OrderingFilter) filter_fields = ('title',) - ordering_fields = ('date_modified','date_created') + ordering_fields = ('date_modified', 'date_created') ordering = ('date_created',) @swagger_auto_schema(auto_schema=NoPagingAutoSchema) diff --git a/testproj/users/serializers.py b/testproj/users/serializers.py index 6af803e..87cc87c 100644 --- a/testproj/users/serializers.py +++ b/testproj/users/serializers.py @@ -1,7 +1,6 @@ from django.contrib.auth.models import User from rest_framework import serializers -from articles.models import Article from snippets.models import Snippet