Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 73adc49b2c | |||
| 313d122004 | |||
| 443d74b636 | |||
| 43034ddc4b | |||
| 9f6ee4da87 | |||
| f05889292a | |||
| 8a8a9faeeb |
+1
-1
@@ -201,7 +201,7 @@ The possible settings and their default values are as follows:
|
|||||||
'DEEP_LINKING': False,
|
'DEEP_LINKING': False,
|
||||||
'SHOW_EXTENSIONS': True,
|
'SHOW_EXTENSIONS': True,
|
||||||
'DEFAULT_MODEL_RENDERING': 'model',
|
'DEFAULT_MODEL_RENDERING': 'model',
|
||||||
'DEFAULT_MODEL_DEPTH': 2,
|
'DEFAULT_MODEL_DEPTH': 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|||||||
@@ -3,6 +3,17 @@ Changelog
|
|||||||
#########
|
#########
|
||||||
|
|
||||||
|
|
||||||
|
*********
|
||||||
|
**1.0.6**
|
||||||
|
*********
|
||||||
|
|
||||||
|
- **FIXED:** Swagger UI "Try it out!" should now work with Django login
|
||||||
|
- **FIXED:** callable ``default`` values on serializer fields will now be properly called (:pr:`24`, :issue:`25`)
|
||||||
|
- **IMPROVED:** updated ``swagger-ui`` to version 3.8.0
|
||||||
|
- **IMPROVED:** ``PrimaryKeyRelatedField`` and ``SlugRelatedField`` will now have
|
||||||
|
appropriate types based on the related model (:pr:`26`)
|
||||||
|
- **IMPROVED:** mock views will now have a bound request even with ``public=False`` (:pr:`23`)
|
||||||
|
|
||||||
*********
|
*********
|
||||||
**1.0.5**
|
**1.0.5**
|
||||||
*********
|
*********
|
||||||
|
|||||||
+1
-1
@@ -161,7 +161,7 @@ DEFAULT_MODEL_DEPTH
|
|||||||
|
|
||||||
Controls how many levels are expaned by default when showing nested models.
|
Controls how many levels are expaned by default when showing nested models.
|
||||||
|
|
||||||
**Default**: :python:`2` |br|
|
**Default**: :python:`3` |br|
|
||||||
*Maps to parameter*: ``defaultModelExpandDepth``
|
*Maps to parameter*: ``defaultModelExpandDepth``
|
||||||
|
|
||||||
******************
|
******************
|
||||||
|
|||||||
Generated
+3
-3
@@ -312,9 +312,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"swagger-ui-dist": {
|
"swagger-ui-dist": {
|
||||||
"version": "3.7.0",
|
"version": "3.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.8.0.tgz",
|
||||||
"integrity": "sha1-hkLAGUNf1SOE09KzVaHMovEDcoM="
|
"integrity": "sha1-BHfLOagE7a6Wx+COskDNWagEt3U="
|
||||||
},
|
},
|
||||||
"tiny-emitter": {
|
"tiny-emitter": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
|
|||||||
+1
-1
@@ -2,6 +2,6 @@
|
|||||||
"name": "drf-yasg",
|
"name": "drf-yasg",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"redoc": "^1.19.3",
|
"redoc": "^1.19.3",
|
||||||
"swagger-ui-dist": "^3.7.0"
|
"swagger-ui-dist": "^3.8.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ SWAGGER_DEFAULTS = {
|
|||||||
'DEEP_LINKING': False,
|
'DEEP_LINKING': False,
|
||||||
'SHOW_EXTENSIONS': True,
|
'SHOW_EXTENSIONS': True,
|
||||||
'DEFAULT_MODEL_RENDERING': 'model',
|
'DEFAULT_MODEL_RENDERING': 'model',
|
||||||
'DEFAULT_MODEL_DEPTH': 2,
|
'DEFAULT_MODEL_DEPTH': 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
REDOC_DEFAULTS = {
|
REDOC_DEFAULTS = {
|
||||||
|
|||||||
+14
-31
@@ -1,15 +1,13 @@
|
|||||||
import re
|
import re
|
||||||
from collections import defaultdict, OrderedDict
|
from collections import defaultdict, OrderedDict
|
||||||
|
|
||||||
import django.db.models
|
|
||||||
import uritemplate
|
import uritemplate
|
||||||
from coreapi.compat import force_text
|
|
||||||
from rest_framework.schemas.generators import SchemaGenerator, EndpointEnumerator as _EndpointEnumerator
|
from rest_framework.schemas.generators import SchemaGenerator, EndpointEnumerator as _EndpointEnumerator
|
||||||
from rest_framework.schemas.inspectors import get_pk_description
|
|
||||||
|
|
||||||
from . import openapi
|
from . import openapi
|
||||||
from .inspectors import SwaggerAutoSchema
|
from .inspectors import SwaggerAutoSchema
|
||||||
from .openapi import ReferenceResolver
|
from .openapi import ReferenceResolver
|
||||||
|
from .utils import inspect_model_field, get_model_field
|
||||||
|
|
||||||
PATH_PARAMETER_RE = re.compile(r'{(?P<parameter>\w+)}')
|
PATH_PARAMETER_RE = re.compile(r'{(?P<parameter>\w+)}')
|
||||||
|
|
||||||
@@ -82,9 +80,9 @@ class OpenAPISchemaGenerator(object):
|
|||||||
:return: the generated Swagger specification
|
:return: the generated Swagger specification
|
||||||
:rtype: openapi.Swagger
|
:rtype: openapi.Swagger
|
||||||
"""
|
"""
|
||||||
endpoints = self.get_endpoints(None if public else request)
|
endpoints = self.get_endpoints(request)
|
||||||
components = ReferenceResolver(openapi.SCHEMA_DEFINITIONS)
|
components = ReferenceResolver(openapi.SCHEMA_DEFINITIONS)
|
||||||
paths = self.get_paths(endpoints, components)
|
paths = self.get_paths(endpoints, components, public)
|
||||||
|
|
||||||
url = self._gen.url
|
url = self._gen.url
|
||||||
if not url and request is not None:
|
if not url and request is not None:
|
||||||
@@ -114,9 +112,9 @@ class OpenAPISchemaGenerator(object):
|
|||||||
return view
|
return view
|
||||||
|
|
||||||
def get_endpoints(self, request=None):
|
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)])
|
:return: {path: (view_class, list[(http_method, view_instance)])
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
"""
|
"""
|
||||||
@@ -151,11 +149,12 @@ class OpenAPISchemaGenerator(object):
|
|||||||
"""
|
"""
|
||||||
return self._gen.get_keys(subpath, method, view)
|
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.
|
"""Generate the Swagger Paths for the API from the given endpoints.
|
||||||
|
|
||||||
:param dict endpoints: endpoints as returned by get_endpoints
|
:param dict endpoints: endpoints as returned by get_endpoints
|
||||||
:param ReferenceResolver components: resolver/container for Swagger References
|
: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
|
:rtype: openapi.Paths
|
||||||
"""
|
"""
|
||||||
if not endpoints:
|
if not endpoints:
|
||||||
@@ -169,7 +168,7 @@ class OpenAPISchemaGenerator(object):
|
|||||||
path_parameters = self.get_path_parameters(path, view_cls)
|
path_parameters = self.get_path_parameters(path, view_cls)
|
||||||
operations = {}
|
operations = {}
|
||||||
for method, view in methods:
|
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
|
continue
|
||||||
|
|
||||||
operation_keys = self.get_operation_keys(path[len(prefix):], method, view)
|
operation_keys = self.get_operation_keys(path[len(prefix):], method, view)
|
||||||
@@ -209,36 +208,20 @@ class OpenAPISchemaGenerator(object):
|
|||||||
:rtype: list[openapi.Parameter]
|
:rtype: list[openapi.Parameter]
|
||||||
"""
|
"""
|
||||||
parameters = []
|
parameters = []
|
||||||
|
queryset = getattr(view_cls, 'queryset', None)
|
||||||
model = getattr(getattr(view_cls, 'queryset', None), 'model', None)
|
model = getattr(getattr(view_cls, 'queryset', None), 'model', None)
|
||||||
|
|
||||||
for variable in uritemplate.variables(path):
|
for variable in uritemplate.variables(path):
|
||||||
pattern = None
|
model, model_field = get_model_field(queryset, variable)
|
||||||
type = openapi.TYPE_STRING
|
attrs = inspect_model_field(model, model_field)
|
||||||
description = None
|
if hasattr(view_cls, 'lookup_value_regex') and getattr(view_cls, 'lookup_field', None) == variable:
|
||||||
if model is not None:
|
attrs['pattern'] = view_cls.lookup_value_regex
|
||||||
# Attempt to infer a field description if possible.
|
|
||||||
try:
|
|
||||||
model_field = model._meta.get_field(variable)
|
|
||||||
except Exception: # pragma: no cover
|
|
||||||
model_field = None
|
|
||||||
|
|
||||||
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
|
|
||||||
elif isinstance(model_field, django.db.models.AutoField):
|
|
||||||
type = openapi.TYPE_INTEGER
|
|
||||||
|
|
||||||
field = openapi.Parameter(
|
field = openapi.Parameter(
|
||||||
name=variable,
|
name=variable,
|
||||||
required=True,
|
required=True,
|
||||||
in_=openapi.IN_PATH,
|
in_=openapi.IN_PATH,
|
||||||
type=type,
|
**attrs
|
||||||
pattern=pattern,
|
|
||||||
description=description,
|
|
||||||
)
|
)
|
||||||
parameters.append(field)
|
parameters.append(field)
|
||||||
|
|
||||||
|
|||||||
@@ -309,6 +309,7 @@ class Items(SwaggerDict):
|
|||||||
:param .Items items: only valid if `type` is ``array``
|
:param .Items items: only valid if `type` is ``array``
|
||||||
"""
|
"""
|
||||||
super(Items, self).__init__(**extra)
|
super(Items, self).__init__(**extra)
|
||||||
|
assert type is not None, "type is required!"
|
||||||
self.type = type
|
self.type = type
|
||||||
self.format = format
|
self.format = format
|
||||||
self.enum = enum
|
self.enum = enum
|
||||||
@@ -372,6 +373,7 @@ class Schema(SwaggerDict):
|
|||||||
# common error
|
# common error
|
||||||
raise AssertionError(
|
raise AssertionError(
|
||||||
"the `requires` attribute of schema must be an array of required properties, not a boolean!")
|
"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.description = description
|
||||||
self.required = required
|
self.required = required
|
||||||
self.type = type
|
self.type = type
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ class _UIRenderer(BaseRenderer):
|
|||||||
'showExtensions': swagger_settings.SHOW_EXTENSIONS,
|
'showExtensions': swagger_settings.SHOW_EXTENSIONS,
|
||||||
'defaultModelRendering': swagger_settings.DEFAULT_MODEL_RENDERING,
|
'defaultModelRendering': swagger_settings.DEFAULT_MODEL_RENDERING,
|
||||||
'defaultModelExpandDepth': swagger_settings.DEFAULT_MODEL_DEPTH,
|
'defaultModelExpandDepth': swagger_settings.DEFAULT_MODEL_DEPTH,
|
||||||
|
'defaultModelsExpandDepth': swagger_settings.DEFAULT_MODEL_DEPTH,
|
||||||
}
|
}
|
||||||
data = {k: v for k, v in data.items() if v is not None}
|
data = {k: v for k, v in data.items() if v is not None}
|
||||||
if swagger_settings.VALIDATOR_URL != '':
|
if swagger_settings.VALIDATOR_URL != '':
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -163,7 +163,14 @@
|
|||||||
plugins: [
|
plugins: [
|
||||||
SwaggerUIBundle.plugins.DownloadUrl
|
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 = {};
|
var swaggerSettings = {};
|
||||||
|
|||||||
+150
-5
@@ -1,14 +1,20 @@
|
|||||||
|
import logging
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
|
from django.db import models
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.mixins import RetrieveModelMixin, DestroyModelMixin, UpdateModelMixin
|
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.settings import api_settings
|
||||||
|
from rest_framework.utils import json, encoders
|
||||||
|
|
||||||
from . import openapi
|
from . import openapi
|
||||||
from .errors import SwaggerGenerationError
|
from .errors import SwaggerGenerationError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
#: used to forcibly remove the body of a request via :func:`.swagger_auto_schema`
|
#: used to forcibly remove the body of a request via :func:`.swagger_auto_schema`
|
||||||
no_body = object()
|
no_body = object()
|
||||||
|
|
||||||
@@ -157,6 +163,87 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=None, request_bod
|
|||||||
return decorator
|
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:
|
||||||
|
if isinstance(model_field, model_field_class):
|
||||||
|
swagger_type, format = tf
|
||||||
|
break
|
||||||
|
else: # pragma: no cover
|
||||||
|
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):
|
def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **kwargs):
|
||||||
"""Convert a drf Serializer or Field instance into a Swagger object.
|
"""Convert a drf Serializer or Field instance into a Swagger object.
|
||||||
|
|
||||||
@@ -176,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 = 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
|
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:
|
if swagger_object_type == openapi.Parameter and 'required' not in instance_kwargs:
|
||||||
instance_kwargs['required'] = field.required
|
instance_kwargs['required'] = field.required
|
||||||
if swagger_object_type != openapi.Items and 'default' not in instance_kwargs:
|
if swagger_object_type != openapi.Items and 'default' not in instance_kwargs:
|
||||||
default = getattr(field, 'default', serializers.empty)
|
default = getattr(field, 'default', serializers.empty)
|
||||||
if default is not 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: # pragma: no cover
|
||||||
|
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: # pragma: no cover
|
||||||
|
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 swagger_object_type == openapi.Schema and 'read_only' not in instance_kwargs:
|
||||||
if field.read_only:
|
if field.read_only:
|
||||||
instance_kwargs['read_only'] = True
|
instance_kwargs['read_only'] = True
|
||||||
instance_kwargs.update(kwargs)
|
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)
|
return swagger_object_type(title=title, description=description, **instance_kwargs)
|
||||||
|
|
||||||
# arrays in Schema have Schema elements, arrays in Parameter and Items have Items elements
|
# arrays in Schema have Schema elements, arrays in Parameter and Items have Items elements
|
||||||
@@ -238,8 +358,29 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **
|
|||||||
items=child_schema,
|
items=child_schema,
|
||||||
unique_items=True, # is this OK?
|
unique_items=True, # is this OK?
|
||||||
)
|
)
|
||||||
|
elif isinstance(field, serializers.PrimaryKeyRelatedField):
|
||||||
|
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: # pragma: no cover
|
||||||
|
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):
|
elif isinstance(field, serializers.RelatedField):
|
||||||
# TODO: infer type for PrimaryKeyRelatedField?
|
|
||||||
return SwaggerType(type=openapi.TYPE_STRING)
|
return SwaggerType(type=openapi.TYPE_STRING)
|
||||||
# ------ CHOICES
|
# ------ CHOICES
|
||||||
elif isinstance(field, serializers.MultipleChoiceField):
|
elif isinstance(field, serializers.MultipleChoiceField):
|
||||||
@@ -253,7 +394,7 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **
|
|||||||
elif isinstance(field, serializers.ChoiceField):
|
elif isinstance(field, serializers.ChoiceField):
|
||||||
return SwaggerType(type=openapi.TYPE_STRING, enum=list(field.choices.keys()))
|
return SwaggerType(type=openapi.TYPE_STRING, enum=list(field.choices.keys()))
|
||||||
# ------ BOOL
|
# ------ BOOL
|
||||||
elif isinstance(field, serializers.BooleanField):
|
elif isinstance(field, (serializers.BooleanField, serializers.NullBooleanField)):
|
||||||
return SwaggerType(type=openapi.TYPE_BOOLEAN)
|
return SwaggerType(type=openapi.TYPE_BOOLEAN)
|
||||||
# ------ NUMERIC
|
# ------ NUMERIC
|
||||||
elif isinstance(field, (serializers.DecimalField, serializers.FloatField)):
|
elif isinstance(field, (serializers.DecimalField, serializers.FloatField)):
|
||||||
@@ -262,6 +403,8 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **
|
|||||||
elif isinstance(field, serializers.IntegerField):
|
elif isinstance(field, serializers.IntegerField):
|
||||||
# TODO: min_value max_value
|
# TODO: min_value max_value
|
||||||
return SwaggerType(type=openapi.TYPE_INTEGER)
|
return SwaggerType(type=openapi.TYPE_INTEGER)
|
||||||
|
elif isinstance(field, serializers.DurationField):
|
||||||
|
return SwaggerType(type=openapi.TYPE_INTEGER)
|
||||||
# ------ STRING
|
# ------ STRING
|
||||||
elif isinstance(field, serializers.EmailField):
|
elif isinstance(field, serializers.EmailField):
|
||||||
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_EMAIL)
|
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_EMAIL)
|
||||||
@@ -308,8 +451,10 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **
|
|||||||
type=openapi.TYPE_OBJECT,
|
type=openapi.TYPE_OBJECT,
|
||||||
additional_properties=child_schema
|
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
|
# everything else gets string by default
|
||||||
return SwaggerType(type=openapi.TYPE_STRING)
|
return SwaggerType(type=openapi.TYPE_STRING)
|
||||||
|
|||||||
@@ -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
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -8,6 +10,7 @@ class Migration(migrations.Migration):
|
|||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -15,12 +18,13 @@ class Migration(migrations.Migration):
|
|||||||
name='Article',
|
name='Article',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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)),
|
('title', models.CharField(help_text='title model help_text', max_length=255, unique=True)),
|
||||||
('body', models.TextField(help_text='Article content', max_length=5000)),
|
('body', models.TextField(help_text='article model help_text', max_length=5000)),
|
||||||
('slug', models.SlugField(blank=True, help_text='Unique URL slug identifying the article', unique=True)),
|
('slug', models.SlugField(blank=True, help_text='slug model help_text', unique=True)),
|
||||||
('date_created', models.DateTimeField(auto_now_add=True)),
|
('date_created', models.DateTimeField(auto_now_add=True)),
|
||||||
('date_modified', models.DateTimeField(auto_now=True)),
|
('date_modified', models.DateTimeField(auto_now=True)),
|
||||||
('cover', models.ImageField(blank=True, upload_to='article/original/')),
|
('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)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ class Article(models.Model):
|
|||||||
slug = models.SlugField(help_text="slug model help_text", unique=True, blank=True)
|
slug = models.SlugField(help_text="slug model help_text", unique=True, blank=True)
|
||||||
date_created = models.DateTimeField(auto_now_add=True)
|
date_created = models.DateTimeField(auto_now_add=True)
|
||||||
date_modified = models.DateTimeField(auto_now=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)
|
cover = models.ImageField(upload_to='article/original/', blank=True)
|
||||||
|
|||||||
@@ -7,17 +7,26 @@ class ArticleSerializer(serializers.ModelSerializer):
|
|||||||
references = serializers.DictField(
|
references = serializers.DictField(
|
||||||
help_text="this is a really bad example",
|
help_text="this is a really bad example",
|
||||||
child=serializers.URLField(help_text="but i needed to test these 2 fields somehow"),
|
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)
|
cover_name = serializers.FileField(use_url=False, source='cover', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Article
|
model = Article
|
||||||
fields = ('title', 'body', 'slug', 'date_created', 'date_modified',
|
fields = ('title', 'author', 'body', 'slug', 'date_created', 'date_modified',
|
||||||
'references', 'uuid', 'cover', 'cover_name')
|
'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'
|
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):
|
class ImageUploadSerializer(serializers.Serializer):
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ class NoPagingAutoSchema(SwaggerAutoSchema):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class ArticlePagination(LimitOffsetPagination):
|
||||||
|
default_limit = 5
|
||||||
|
max_limit = 25
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(name='list', decorator=swagger_auto_schema(
|
@method_decorator(name='list', decorator=swagger_auto_schema(
|
||||||
operation_description="description from swagger_auto_schema via method_decorator"
|
operation_description="description from swagger_auto_schema via method_decorator"
|
||||||
))
|
))
|
||||||
@@ -41,12 +46,11 @@ class ArticleViewSet(viewsets.ModelViewSet):
|
|||||||
lookup_value_regex = r'[a-z0-9]+(?:-[a-z0-9]+)'
|
lookup_value_regex = r'[a-z0-9]+(?:-[a-z0-9]+)'
|
||||||
serializer_class = serializers.ArticleSerializer
|
serializer_class = serializers.ArticleSerializer
|
||||||
|
|
||||||
pagination_class = LimitOffsetPagination
|
pagination_class = ArticlePagination
|
||||||
max_page_size = 5
|
|
||||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||||
filter_fields = ('title',)
|
filter_fields = ('title',)
|
||||||
ordering_fields = ('date_modified',)
|
ordering_fields = ('date_modified', 'date_created')
|
||||||
ordering = ('username',)
|
ordering = ('date_created',)
|
||||||
|
|
||||||
@swagger_auto_schema(auto_schema=NoPagingAutoSchema)
|
@swagger_auto_schema(auto_schema=NoPagingAutoSchema)
|
||||||
@list_route(methods=['get'])
|
@list_route(methods=['get'])
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1,27 +0,0 @@
|
|||||||
# Generated by Django 2.0 on 2017-12-05 04:05
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
('snippets', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='snippet',
|
|
||||||
name='owner',
|
|
||||||
field=models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, related_name='snippets', to=settings.AUTH_USER_MODEL),
|
|
||||||
preserve_default=False,
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='snippet',
|
|
||||||
name='code',
|
|
||||||
field=models.TextField(help_text='code model help text'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES
|
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES
|
||||||
@@ -23,20 +24,35 @@ class SnippetSerializer(serializers.Serializer):
|
|||||||
create: docstring for create from serializer classdoc
|
create: docstring for create from serializer classdoc
|
||||||
"""
|
"""
|
||||||
id = serializers.IntegerField(read_only=True, help_text="id serializer help text")
|
id = serializers.IntegerField(read_only=True, help_text="id serializer help text")
|
||||||
owner = serializers.ReadOnlyField(source='owner.username')
|
owner = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=get_user_model().objects.all(),
|
||||||
|
default=serializers.CurrentUserDefault(),
|
||||||
|
help_text="The ID of the user that created this snippet; if none is provided, "
|
||||||
|
"defaults to the currently logged in user."
|
||||||
|
)
|
||||||
|
owner_as_string = serializers.PrimaryKeyRelatedField(
|
||||||
|
help_text="The ID of the user that created this snippet.",
|
||||||
|
pk_field=serializers.CharField(help_text="this help text should not show up"),
|
||||||
|
read_only=True,
|
||||||
|
source='owner',
|
||||||
|
)
|
||||||
title = serializers.CharField(required=False, allow_blank=True, max_length=100)
|
title = serializers.CharField(required=False, allow_blank=True, max_length=100)
|
||||||
code = serializers.CharField(style={'base_template': 'textarea.html'})
|
code = serializers.CharField(style={'base_template': 'textarea.html'})
|
||||||
linenos = serializers.BooleanField(required=False)
|
linenos = serializers.BooleanField(required=False)
|
||||||
language = LanguageSerializer(help_text="Sample help text for language")
|
language = LanguageSerializer(help_text="Sample help text for language")
|
||||||
styles = serializers.MultipleChoiceField(choices=STYLE_CHOICES, default=['friendly'])
|
styles = serializers.MultipleChoiceField(choices=STYLE_CHOICES, default=['friendly'])
|
||||||
lines = serializers.ListField(child=serializers.IntegerField(), allow_empty=True, allow_null=True, required=False)
|
lines = serializers.ListField(child=serializers.IntegerField(), allow_empty=True, allow_null=True, required=False)
|
||||||
example_projects = serializers.ListSerializer(child=ExampleProjectSerializer())
|
example_projects = serializers.ListSerializer(child=ExampleProjectSerializer(), read_only=True)
|
||||||
difficulty_factor = serializers.FloatField(help_text="this is here just to test FloatField")
|
difficulty_factor = serializers.FloatField(help_text="this is here just to test FloatField",
|
||||||
|
read_only=True, default=lambda: 6.9)
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
"""
|
"""
|
||||||
Create and return a new `Snippet` instance, given the validated data.
|
Create and return a new `Snippet` instance, given the validated data.
|
||||||
"""
|
"""
|
||||||
|
del validated_data['styles']
|
||||||
|
del validated_data['lines']
|
||||||
|
del validated_data['difficulty_factor']
|
||||||
return Snippet.objects.create(**validated_data)
|
return Snippet.objects.create(**validated_data)
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
|
|||||||
@@ -6,14 +6,16 @@ from snippets.models import Snippet
|
|||||||
|
|
||||||
class UserSerializerrr(serializers.ModelSerializer):
|
class UserSerializerrr(serializers.ModelSerializer):
|
||||||
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
|
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
|
||||||
|
article_slugs = serializers.SlugRelatedField(read_only=True, slug_field='slug', many=True, source='articlessss')
|
||||||
last_connected_ip = serializers.IPAddressField(help_text="i'm out of ideas", protocol='ipv4', read_only=True)
|
last_connected_ip = serializers.IPAddressField(help_text="i'm out of ideas", protocol='ipv4', read_only=True)
|
||||||
last_connected_at = serializers.DateField(help_text="really?", read_only=True)
|
last_connected_at = serializers.DateField(help_text="really?", read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = ('id', 'username', 'email', 'snippets', 'last_connected_ip', 'last_connected_at')
|
fields = ('id', 'username', 'email', 'articles', 'snippets',
|
||||||
|
'last_connected_ip', 'last_connected_at', 'article_slugs')
|
||||||
|
|
||||||
|
|
||||||
class UserListQuerySerializer(serializers.Serializer):
|
class UserListQuerySerializer(serializers.Serializer):
|
||||||
username = serializers.CharField(help_text="this field is generated from a query_serializer")
|
username = serializers.CharField(help_text="this field is generated from a query_serializer", required=False)
|
||||||
is_staff = serializers.BooleanField(help_text="this one too!")
|
is_staff = serializers.BooleanField(help_text="this one too!", required=False)
|
||||||
|
|||||||
+21
-10
@@ -3,6 +3,9 @@ import json
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from rest_framework.test import APIRequestFactory
|
||||||
|
from rest_framework.views import APIView
|
||||||
from ruamel import yaml
|
from ruamel import yaml
|
||||||
|
|
||||||
from drf_yasg import openapi, codecs
|
from drf_yasg import openapi, codecs
|
||||||
@@ -10,11 +13,16 @@ from drf_yasg.generators import OpenAPISchemaGenerator
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def generator():
|
def mock_schema_request(db):
|
||||||
return OpenAPISchemaGenerator(
|
from rest_framework.test import force_authenticate
|
||||||
info=openapi.Info(title="Test generator", default_version="v1"),
|
|
||||||
version="v2",
|
factory = APIRequestFactory()
|
||||||
)
|
user = User.objects.create_user(username='admin', is_staff=True, is_superuser=True)
|
||||||
|
|
||||||
|
request = factory.get('/swagger.json')
|
||||||
|
force_authenticate(request, user=user)
|
||||||
|
request = APIView().initialize_request(request)
|
||||||
|
return request
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -28,19 +36,22 @@ def codec_yaml():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def swagger(generator):
|
def swagger(mock_schema_request):
|
||||||
return generator.get_schema(None, True)
|
generator = OpenAPISchemaGenerator(
|
||||||
|
info=openapi.Info(title="Test generator", default_version="v1"),
|
||||||
|
version="v2",
|
||||||
|
)
|
||||||
|
return generator.get_schema(mock_schema_request, True)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def swagger_dict(generator):
|
def swagger_dict(swagger):
|
||||||
swagger = generator.get_schema(None, True)
|
|
||||||
json_bytes = codec_json().encode(swagger)
|
json_bytes = codec_json().encode(swagger)
|
||||||
return json.loads(json_bytes.decode('utf-8'))
|
return json.loads(json_bytes.decode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def validate_schema():
|
def validate_schema(db):
|
||||||
def validate_schema(swagger):
|
def validate_schema(swagger):
|
||||||
from flex.core import parse as validate_flex
|
from flex.core import parse as validate_flex
|
||||||
from swagger_spec_validator.validator20 import validate_spec as validate_ssv
|
from swagger_spec_validator.validator20 import validate_spec as validate_ssv
|
||||||
|
|||||||
+35
-8
@@ -178,6 +178,7 @@ paths:
|
|||||||
description: slug model help_text
|
description: slug model help_text
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
format: slug
|
||||||
pattern: '[a-z0-9]+(?:-[a-z0-9]+)'
|
pattern: '[a-z0-9]+(?:-[a-z0-9]+)'
|
||||||
/articles/{slug}/image/:
|
/articles/{slug}/image/:
|
||||||
get:
|
get:
|
||||||
@@ -231,6 +232,7 @@ paths:
|
|||||||
description: slug model help_text
|
description: slug model help_text
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
format: slug
|
||||||
pattern: '[a-z0-9]+(?:-[a-z0-9]+)'
|
pattern: '[a-z0-9]+(?:-[a-z0-9]+)'
|
||||||
/plain/:
|
/plain/:
|
||||||
get:
|
get:
|
||||||
@@ -355,12 +357,12 @@ paths:
|
|||||||
- name: username
|
- name: username
|
||||||
in: query
|
in: query
|
||||||
description: this field is generated from a query_serializer
|
description: this field is generated from a query_serializer
|
||||||
required: true
|
required: false
|
||||||
type: string
|
type: string
|
||||||
- name: is_staff
|
- name: is_staff
|
||||||
in: query
|
in: query
|
||||||
description: this one too!
|
description: this one too!
|
||||||
required: true
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -459,13 +461,16 @@ definitions:
|
|||||||
required:
|
required:
|
||||||
- title
|
- title
|
||||||
- body
|
- body
|
||||||
- references
|
|
||||||
- uuid
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
title:
|
title:
|
||||||
description: title model help_text
|
description: title model help_text
|
||||||
type: string
|
type: string
|
||||||
|
author:
|
||||||
|
description: The ID of the user that created this article; if none is provided,
|
||||||
|
defaults to the currently logged in user.
|
||||||
|
type: integer
|
||||||
|
default: 1
|
||||||
body:
|
body:
|
||||||
description: body serializer help_text
|
description: body serializer help_text
|
||||||
type: string
|
type: string
|
||||||
@@ -489,14 +494,16 @@ definitions:
|
|||||||
description: but i needed to test these 2 fields somehow
|
description: but i needed to test these 2 fields somehow
|
||||||
type: string
|
type: string
|
||||||
format: uri
|
format: uri
|
||||||
|
readOnly: true
|
||||||
uuid:
|
uuid:
|
||||||
description: should articles have UUIDs?
|
description: should articles have UUIDs?
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
|
readOnly: true
|
||||||
cover:
|
cover:
|
||||||
type: string
|
type: string
|
||||||
format: uri
|
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
format: uri
|
||||||
cover_name:
|
cover_name:
|
||||||
type: string
|
type: string
|
||||||
readOnly: true
|
readOnly: true
|
||||||
@@ -516,8 +523,6 @@ definitions:
|
|||||||
required:
|
required:
|
||||||
- code
|
- code
|
||||||
- language
|
- language
|
||||||
- example_projects
|
|
||||||
- difficulty_factor
|
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
@@ -525,6 +530,12 @@ definitions:
|
|||||||
type: integer
|
type: integer
|
||||||
readOnly: true
|
readOnly: true
|
||||||
owner:
|
owner:
|
||||||
|
description: The ID of the user that created this snippet; if none is provided,
|
||||||
|
defaults to the currently logged in user.
|
||||||
|
type: integer
|
||||||
|
default: 1
|
||||||
|
owner_as_string:
|
||||||
|
description: The ID of the user that created this snippet.
|
||||||
type: string
|
type: string
|
||||||
readOnly: true
|
readOnly: true
|
||||||
title:
|
title:
|
||||||
@@ -1020,12 +1031,16 @@ definitions:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/Project'
|
$ref: '#/definitions/Project'
|
||||||
|
readOnly: true
|
||||||
difficulty_factor:
|
difficulty_factor:
|
||||||
description: this is here just to test FloatField
|
description: this is here just to test FloatField
|
||||||
type: number
|
type: number
|
||||||
|
default: 6.9
|
||||||
|
readOnly: true
|
||||||
UserSerializerrr:
|
UserSerializerrr:
|
||||||
required:
|
required:
|
||||||
- username
|
- username
|
||||||
|
- articles
|
||||||
- snippets
|
- snippets
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -1039,10 +1054,15 @@ definitions:
|
|||||||
email:
|
email:
|
||||||
type: string
|
type: string
|
||||||
format: email
|
format: email
|
||||||
|
articles:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
uniqueItems: true
|
||||||
snippets:
|
snippets:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: integer
|
||||||
uniqueItems: true
|
uniqueItems: true
|
||||||
last_connected_ip:
|
last_connected_ip:
|
||||||
description: i'm out of ideas
|
description: i'm out of ideas
|
||||||
@@ -1054,6 +1074,13 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
format: date
|
format: date
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
article_slugs:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
uniqueItems: true
|
||||||
|
readOnly: true
|
||||||
securityDefinitions:
|
securityDefinitions:
|
||||||
basic:
|
basic:
|
||||||
type: basic
|
type: basic
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from datadiff.tools import assert_equal
|
|||||||
|
|
||||||
|
|
||||||
def test_reference_schema(swagger_dict, reference_schema):
|
def test_reference_schema(swagger_dict, reference_schema):
|
||||||
# formatted better than pytest diff
|
|
||||||
swagger_dict = dict(swagger_dict)
|
swagger_dict = dict(swagger_dict)
|
||||||
reference_schema = dict(reference_schema)
|
reference_schema = dict(reference_schema)
|
||||||
ignore = ['info', 'host', 'schemes', 'basePath', 'securityDefinitions']
|
ignore = ['info', 'host', 'schemes', 'basePath', 'securityDefinitions']
|
||||||
@@ -10,4 +9,5 @@ def test_reference_schema(swagger_dict, reference_schema):
|
|||||||
swagger_dict.pop(attr, None)
|
swagger_dict.pop(attr, None)
|
||||||
reference_schema.pop(attr, None)
|
reference_schema.pop(attr, None)
|
||||||
|
|
||||||
|
# formatted better than pytest diff
|
||||||
assert_equal(swagger_dict, reference_schema)
|
assert_equal(swagger_dict, reference_schema)
|
||||||
|
|||||||
@@ -7,16 +7,11 @@ from drf_yasg import openapi, codecs
|
|||||||
from drf_yasg.generators import OpenAPISchemaGenerator
|
from drf_yasg.generators import OpenAPISchemaGenerator
|
||||||
|
|
||||||
|
|
||||||
def test_schema_generates_without_errors(generator):
|
def test_schema_is_valid(swagger, codec_yaml):
|
||||||
generator.get_schema(None, True)
|
|
||||||
|
|
||||||
|
|
||||||
def test_schema_is_valid(generator, codec_yaml):
|
|
||||||
swagger = generator.get_schema(request=None, public=False)
|
|
||||||
codec_yaml.encode(swagger)
|
codec_yaml.encode(swagger)
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_schema_fails(codec_json):
|
def test_invalid_schema_fails(codec_json, mock_schema_request):
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
bad_generator = OpenAPISchemaGenerator(
|
bad_generator = OpenAPISchemaGenerator(
|
||||||
info=openapi.Info(
|
info=openapi.Info(
|
||||||
@@ -26,40 +21,37 @@ def test_invalid_schema_fails(codec_json):
|
|||||||
version="v2",
|
version="v2",
|
||||||
)
|
)
|
||||||
|
|
||||||
swagger = bad_generator.get_schema(None, True)
|
swagger = bad_generator.get_schema(mock_schema_request, True)
|
||||||
with pytest.raises(codecs.SwaggerValidationError):
|
with pytest.raises(codecs.SwaggerValidationError):
|
||||||
codec_json.encode(swagger)
|
codec_json.encode(swagger)
|
||||||
|
|
||||||
|
|
||||||
def test_json_codec_roundtrip(codec_json, generator, validate_schema):
|
def test_json_codec_roundtrip(codec_json, swagger, validate_schema):
|
||||||
swagger = generator.get_schema(None, True)
|
|
||||||
json_bytes = codec_json.encode(swagger)
|
json_bytes = codec_json.encode(swagger)
|
||||||
validate_schema(json.loads(json_bytes.decode('utf-8')))
|
validate_schema(json.loads(json_bytes.decode('utf-8')))
|
||||||
|
|
||||||
|
|
||||||
def test_yaml_codec_roundtrip(codec_yaml, generator, validate_schema):
|
def test_yaml_codec_roundtrip(codec_yaml, swagger, validate_schema):
|
||||||
swagger = generator.get_schema(None, True)
|
|
||||||
yaml_bytes = codec_yaml.encode(swagger)
|
yaml_bytes = codec_yaml.encode(swagger)
|
||||||
assert b'omap' not in yaml_bytes # ensure no ugly !!omap is outputted
|
assert b'omap' not in yaml_bytes # ensure no ugly !!omap is outputted
|
||||||
assert b'&id' not in yaml_bytes and b'*id' not in yaml_bytes # ensure no YAML references are generated
|
assert b'&id' not in yaml_bytes and b'*id' not in yaml_bytes # ensure no YAML references are generated
|
||||||
validate_schema(yaml.safe_load(yaml_bytes.decode('utf-8')))
|
validate_schema(yaml.safe_load(yaml_bytes.decode('utf-8')))
|
||||||
|
|
||||||
|
|
||||||
def test_yaml_and_json_match(codec_yaml, codec_json, generator):
|
def test_yaml_and_json_match(codec_yaml, codec_json, swagger):
|
||||||
swagger = generator.get_schema(None, True)
|
|
||||||
yaml_schema = yaml.safe_load(codec_yaml.encode(swagger).decode('utf-8'))
|
yaml_schema = yaml.safe_load(codec_yaml.encode(swagger).decode('utf-8'))
|
||||||
json_schema = json.loads(codec_json.encode(swagger).decode('utf-8'))
|
json_schema = json.loads(codec_json.encode(swagger).decode('utf-8'))
|
||||||
assert yaml_schema == json_schema
|
assert yaml_schema == json_schema
|
||||||
|
|
||||||
|
|
||||||
def test_basepath_only():
|
def test_basepath_only(mock_schema_request):
|
||||||
generator = OpenAPISchemaGenerator(
|
generator = OpenAPISchemaGenerator(
|
||||||
info=openapi.Info(title="Test generator", default_version="v1"),
|
info=openapi.Info(title="Test generator", default_version="v1"),
|
||||||
version="v2",
|
version="v2",
|
||||||
url='/basepath/',
|
url='/basepath/',
|
||||||
)
|
)
|
||||||
|
|
||||||
swagger = generator.get_schema(None, public=True)
|
swagger = generator.get_schema(mock_schema_request, public=True)
|
||||||
assert 'host' not in swagger
|
assert 'host' not in swagger
|
||||||
assert 'schemes' not in swagger
|
assert 'schemes' not in swagger
|
||||||
assert swagger['basePath'] == '/' # base path is not implemented for now
|
assert swagger['basePath'] == '/' # base path is not implemented for now
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ deps =
|
|||||||
-rrequirements/test.txt
|
-rrequirements/test.txt
|
||||||
|
|
||||||
commands =
|
commands =
|
||||||
pytest --cov-config .coveragerc --cov-append --cov
|
pytest --cov-config .coveragerc --cov-append --cov {posargs}
|
||||||
|
|
||||||
[testenv:py36-drfmaster]
|
[testenv:py36-drfmaster]
|
||||||
pip_pre = True
|
pip_pre = True
|
||||||
|
|||||||
Reference in New Issue
Block a user