Compare commits

...

7 Commits

Author SHA1 Message Date
Cristi Vîjdea 73adc49b2c Prepare release 1.0.6 2017-12-23 12:29:34 +01:00
Cristi Vîjdea 313d122004 Update swagger-ui to 3.8.0 2017-12-23 12:29:19 +01:00
Cristi Vîjdea 443d74b636 Merge pull request #27 from axnsan12/fix/callable-default
Fix callable default handling
2017-12-23 12:22:35 +01:00
Cristi Vîjdea 43034ddc4b Fixed assumption of dict ordering 2017-12-23 12:04:02 +01:00
Cristi Vîjdea 9f6ee4da87 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
2017-12-23 11:52:57 +01:00
Nicolas Delaby f05889292a Add type inspection for PrimaryKeyRelatedField (#26)
Guess the type for PrimaryKeyRelatedField from the related ModelField.
2017-12-22 21:00:13 +01:00
Cristi Vîjdea 8a8a9faeeb Fix swagger Try it out and make endpoints work 2017-12-21 17:20:46 +01:00
30 changed files with 339 additions and 149 deletions
+1 -1
View File
@@ -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
+11
View File
@@ -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
View File
@@ -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``
****************** ******************
+3 -3
View File
@@ -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
View File
@@ -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"
} }
} }
+1 -1
View File
@@ -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
View File
@@ -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)
+2
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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)
+8 -4
View File
@@ -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)),
], ],
), ),
] ]
+1
View File
@@ -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)
+13 -4
View File
@@ -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):
+8 -4
View File
@@ -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'),
),
]
+19 -3
View File
@@ -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):
+5 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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)
+8 -16
View File
@@ -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
+1 -1
View File
@@ -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