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,
|
||||
'SHOW_EXTENSIONS': True,
|
||||
'DEFAULT_MODEL_RENDERING': 'model',
|
||||
'DEFAULT_MODEL_DEPTH': 2,
|
||||
'DEFAULT_MODEL_DEPTH': 3,
|
||||
}
|
||||
|
||||
.. 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
-1
@@ -161,7 +161,7 @@ DEFAULT_MODEL_DEPTH
|
||||
|
||||
Controls how many levels are expaned by default when showing nested models.
|
||||
|
||||
**Default**: :python:`2` |br|
|
||||
**Default**: :python:`3` |br|
|
||||
*Maps to parameter*: ``defaultModelExpandDepth``
|
||||
|
||||
******************
|
||||
|
||||
Generated
+3
-3
@@ -312,9 +312,9 @@
|
||||
}
|
||||
},
|
||||
"swagger-ui-dist": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.7.0.tgz",
|
||||
"integrity": "sha1-hkLAGUNf1SOE09KzVaHMovEDcoM="
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.8.0.tgz",
|
||||
"integrity": "sha1-BHfLOagE7a6Wx+COskDNWagEt3U="
|
||||
},
|
||||
"tiny-emitter": {
|
||||
"version": "2.0.2",
|
||||
|
||||
+1
-1
@@ -2,6 +2,6 @@
|
||||
"name": "drf-yasg",
|
||||
"dependencies": {
|
||||
"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,
|
||||
'SHOW_EXTENSIONS': True,
|
||||
'DEFAULT_MODEL_RENDERING': 'model',
|
||||
'DEFAULT_MODEL_DEPTH': 2,
|
||||
'DEFAULT_MODEL_DEPTH': 3,
|
||||
}
|
||||
|
||||
REDOC_DEFAULTS = {
|
||||
|
||||
+14
-31
@@ -1,15 +1,13 @@
|
||||
import re
|
||||
from collections import defaultdict, OrderedDict
|
||||
|
||||
import django.db.models
|
||||
import uritemplate
|
||||
from coreapi.compat import force_text
|
||||
from rest_framework.schemas.generators import SchemaGenerator, EndpointEnumerator as _EndpointEnumerator
|
||||
from rest_framework.schemas.inspectors import get_pk_description
|
||||
|
||||
from . import openapi
|
||||
from .inspectors import SwaggerAutoSchema
|
||||
from .openapi import ReferenceResolver
|
||||
from .utils import inspect_model_field, get_model_field
|
||||
|
||||
PATH_PARAMETER_RE = re.compile(r'{(?P<parameter>\w+)}')
|
||||
|
||||
@@ -82,9 +80,9 @@ class OpenAPISchemaGenerator(object):
|
||||
:return: the generated Swagger specification
|
||||
:rtype: openapi.Swagger
|
||||
"""
|
||||
endpoints = self.get_endpoints(None if public else request)
|
||||
endpoints = self.get_endpoints(request)
|
||||
components = ReferenceResolver(openapi.SCHEMA_DEFINITIONS)
|
||||
paths = self.get_paths(endpoints, components)
|
||||
paths = self.get_paths(endpoints, components, public)
|
||||
|
||||
url = self._gen.url
|
||||
if not url and request is not None:
|
||||
@@ -114,9 +112,9 @@ class OpenAPISchemaGenerator(object):
|
||||
return view
|
||||
|
||||
def get_endpoints(self, request=None):
|
||||
"""Iterate over all the registered endpoints in the API.
|
||||
"""Iterate over all the registered endpoints in the API and return a fake view with the right parameters.
|
||||
|
||||
:param rest_framework.request.Request request: used for returning only endpoints available to the given request
|
||||
:param rest_framework.request.Request request: request to bind to the endpoint views
|
||||
:return: {path: (view_class, list[(http_method, view_instance)])
|
||||
:rtype: dict
|
||||
"""
|
||||
@@ -151,11 +149,12 @@ class OpenAPISchemaGenerator(object):
|
||||
"""
|
||||
return self._gen.get_keys(subpath, method, view)
|
||||
|
||||
def get_paths(self, endpoints, components):
|
||||
def get_paths(self, endpoints, components, public):
|
||||
"""Generate the Swagger Paths for the API from the given endpoints.
|
||||
|
||||
:param dict endpoints: endpoints as returned by get_endpoints
|
||||
:param ReferenceResolver components: resolver/container for Swagger References
|
||||
:param bool public: if True, all endpoints are included regardless of access through `request`
|
||||
:rtype: openapi.Paths
|
||||
"""
|
||||
if not endpoints:
|
||||
@@ -169,7 +168,7 @@ class OpenAPISchemaGenerator(object):
|
||||
path_parameters = self.get_path_parameters(path, view_cls)
|
||||
operations = {}
|
||||
for method, view in methods:
|
||||
if not self._gen.has_view_permissions(path, method, view):
|
||||
if not public and not self._gen.has_view_permissions(path, method, view):
|
||||
continue
|
||||
|
||||
operation_keys = self.get_operation_keys(path[len(prefix):], method, view)
|
||||
@@ -209,36 +208,20 @@ class OpenAPISchemaGenerator(object):
|
||||
:rtype: list[openapi.Parameter]
|
||||
"""
|
||||
parameters = []
|
||||
queryset = getattr(view_cls, 'queryset', None)
|
||||
model = getattr(getattr(view_cls, 'queryset', None), 'model', None)
|
||||
|
||||
for variable in uritemplate.variables(path):
|
||||
pattern = None
|
||||
type = openapi.TYPE_STRING
|
||||
description = None
|
||||
if model is not None:
|
||||
# Attempt to infer a field description if possible.
|
||||
try:
|
||||
model_field = model._meta.get_field(variable)
|
||||
except Exception: # pragma: no cover
|
||||
model_field = None
|
||||
|
||||
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
|
||||
model, model_field = get_model_field(queryset, variable)
|
||||
attrs = inspect_model_field(model, model_field)
|
||||
if hasattr(view_cls, 'lookup_value_regex') and getattr(view_cls, 'lookup_field', None) == variable:
|
||||
attrs['pattern'] = view_cls.lookup_value_regex
|
||||
|
||||
field = openapi.Parameter(
|
||||
name=variable,
|
||||
required=True,
|
||||
in_=openapi.IN_PATH,
|
||||
type=type,
|
||||
pattern=pattern,
|
||||
description=description,
|
||||
**attrs
|
||||
)
|
||||
parameters.append(field)
|
||||
|
||||
|
||||
@@ -309,6 +309,7 @@ class Items(SwaggerDict):
|
||||
:param .Items items: only valid if `type` is ``array``
|
||||
"""
|
||||
super(Items, self).__init__(**extra)
|
||||
assert type is not None, "type is required!"
|
||||
self.type = type
|
||||
self.format = format
|
||||
self.enum = enum
|
||||
@@ -372,6 +373,7 @@ class Schema(SwaggerDict):
|
||||
# common error
|
||||
raise AssertionError(
|
||||
"the `requires` attribute of schema must be an array of required properties, not a boolean!")
|
||||
assert type is not None, "type is required!"
|
||||
self.description = description
|
||||
self.required = required
|
||||
self.type = type
|
||||
|
||||
@@ -84,6 +84,7 @@ class _UIRenderer(BaseRenderer):
|
||||
'showExtensions': swagger_settings.SHOW_EXTENSIONS,
|
||||
'defaultModelRendering': swagger_settings.DEFAULT_MODEL_RENDERING,
|
||||
'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}
|
||||
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: [
|
||||
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 = {};
|
||||
|
||||
+150
-5
@@ -1,14 +1,20 @@
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.utils.encoding import force_text
|
||||
from rest_framework import serializers
|
||||
from rest_framework.mixins import RetrieveModelMixin, DestroyModelMixin, UpdateModelMixin
|
||||
from rest_framework.schemas.inspectors import get_pk_description
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.utils import json, encoders
|
||||
|
||||
from . import openapi
|
||||
from .errors import SwaggerGenerationError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#: used to forcibly remove the body of a request via :func:`.swagger_auto_schema`
|
||||
no_body = object()
|
||||
|
||||
@@ -157,6 +163,87 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=None, request_bod
|
||||
return decorator
|
||||
|
||||
|
||||
def get_model_field(queryset, field_name):
|
||||
"""Try to get information about a model and model field from a queryset.
|
||||
|
||||
:param queryset: the queryset
|
||||
:param field_name: the target field name
|
||||
:returns: the model and target field from the queryset as a 2-tuple; both elements can be ``None``
|
||||
:rtype: tuple
|
||||
"""
|
||||
model = getattr(queryset, 'model', None)
|
||||
try:
|
||||
model_field = model._meta.get_field(field_name)
|
||||
except Exception: # pragma: no cover
|
||||
model_field = None
|
||||
|
||||
return model, model_field
|
||||
|
||||
|
||||
model_field_to_swagger_type = [
|
||||
(models.AutoField, (openapi.TYPE_INTEGER, None)),
|
||||
(models.BinaryField, (openapi.TYPE_STRING, openapi.FORMAT_BINARY)),
|
||||
(models.BooleanField, (openapi.TYPE_BOOLEAN, None)),
|
||||
(models.NullBooleanField, (openapi.TYPE_BOOLEAN, None)),
|
||||
(models.DateTimeField, (openapi.TYPE_STRING, openapi.FORMAT_DATETIME)),
|
||||
(models.DateField, (openapi.TYPE_STRING, openapi.FORMAT_DATE)),
|
||||
(models.DecimalField, (openapi.TYPE_NUMBER, None)),
|
||||
(models.DurationField, (openapi.TYPE_INTEGER, None)),
|
||||
(models.FloatField, (openapi.TYPE_NUMBER, None)),
|
||||
(models.IntegerField, (openapi.TYPE_INTEGER, None)),
|
||||
(models.IPAddressField, (openapi.TYPE_STRING, openapi.FORMAT_IPV4)),
|
||||
(models.GenericIPAddressField, (openapi.TYPE_STRING, openapi.FORMAT_IPV6)),
|
||||
(models.SlugField, (openapi.TYPE_STRING, openapi.FORMAT_SLUG)),
|
||||
(models.TextField, (openapi.TYPE_STRING, None)),
|
||||
(models.TimeField, (openapi.TYPE_STRING, None)),
|
||||
(models.UUIDField, (openapi.TYPE_STRING, openapi.FORMAT_UUID)),
|
||||
(models.CharField, (openapi.TYPE_STRING, None)),
|
||||
]
|
||||
|
||||
|
||||
def inspect_model_field(model, model_field):
|
||||
"""Extract information from a django model field instance.
|
||||
|
||||
:param model: the django model
|
||||
:param model_field: a field on the model
|
||||
:return: description, type, format and pattern extracted from the model field
|
||||
:rtype: OrderedDict
|
||||
"""
|
||||
if model is not None and model_field is not None:
|
||||
for model_field_class, tf in model_field_to_swagger_type:
|
||||
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):
|
||||
"""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 = description if swagger_object_type != openapi.Items else None # Items has no description either
|
||||
|
||||
def SwaggerType(**instance_kwargs):
|
||||
def SwaggerType(existing_object=None, **instance_kwargs):
|
||||
if swagger_object_type == openapi.Parameter and 'required' not in instance_kwargs:
|
||||
instance_kwargs['required'] = field.required
|
||||
if swagger_object_type != openapi.Items and 'default' not in instance_kwargs:
|
||||
default = getattr(field, 'default', serializers.empty)
|
||||
if default is not serializers.empty:
|
||||
instance_kwargs['default'] = default
|
||||
if callable(default):
|
||||
try:
|
||||
if hasattr(default, 'set_context'):
|
||||
default.set_context(field)
|
||||
default = default()
|
||||
except Exception as e: # 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 field.read_only:
|
||||
instance_kwargs['read_only'] = True
|
||||
instance_kwargs.update(kwargs)
|
||||
instance_kwargs.pop('title', None)
|
||||
instance_kwargs.pop('description', None)
|
||||
|
||||
if existing_object is not None:
|
||||
existing_object.title = title
|
||||
existing_object.description = description
|
||||
for attr, val in instance_kwargs.items():
|
||||
setattr(existing_object, attr, val)
|
||||
return existing_object
|
||||
|
||||
return swagger_object_type(title=title, description=description, **instance_kwargs)
|
||||
|
||||
# arrays in Schema have Schema elements, arrays in Parameter and Items have Items elements
|
||||
@@ -238,8 +358,29 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **
|
||||
items=child_schema,
|
||||
unique_items=True, # is this OK?
|
||||
)
|
||||
elif isinstance(field, serializers.PrimaryKeyRelatedField):
|
||||
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):
|
||||
# TODO: infer type for PrimaryKeyRelatedField?
|
||||
return SwaggerType(type=openapi.TYPE_STRING)
|
||||
# ------ CHOICES
|
||||
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):
|
||||
return SwaggerType(type=openapi.TYPE_STRING, enum=list(field.choices.keys()))
|
||||
# ------ BOOL
|
||||
elif isinstance(field, serializers.BooleanField):
|
||||
elif isinstance(field, (serializers.BooleanField, serializers.NullBooleanField)):
|
||||
return SwaggerType(type=openapi.TYPE_BOOLEAN)
|
||||
# ------ NUMERIC
|
||||
elif isinstance(field, (serializers.DecimalField, serializers.FloatField)):
|
||||
@@ -262,6 +403,8 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **
|
||||
elif isinstance(field, serializers.IntegerField):
|
||||
# TODO: min_value max_value
|
||||
return SwaggerType(type=openapi.TYPE_INTEGER)
|
||||
elif isinstance(field, serializers.DurationField):
|
||||
return SwaggerType(type=openapi.TYPE_INTEGER)
|
||||
# ------ STRING
|
||||
elif isinstance(field, serializers.EmailField):
|
||||
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_EMAIL)
|
||||
@@ -308,8 +451,10 @@ def serializer_field_to_swagger(field, swagger_object_type, definitions=None, **
|
||||
type=openapi.TYPE_OBJECT,
|
||||
additional_properties=child_schema
|
||||
)
|
||||
elif isinstance(field, serializers.ModelField):
|
||||
return SwaggerType(type=openapi.TYPE_STRING)
|
||||
|
||||
# TODO unhandled fields: TimeField DurationField HiddenField ModelField NullBooleanField? JSONField
|
||||
# TODO unhandled fields: TimeField HiddenField JSONField
|
||||
|
||||
# everything else gets string by default
|
||||
return SwaggerType(type=openapi.TYPE_STRING)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Generated by Django 2.0 on 2017-12-05 04:05
|
||||
# Generated by Django 2.0 on 2017-12-23 09:07
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -8,6 +10,7 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
@@ -15,12 +18,13 @@ class Migration(migrations.Migration):
|
||||
name='Article',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(help_text='Main article headline', max_length=255, unique=True)),
|
||||
('body', models.TextField(help_text='Article content', max_length=5000)),
|
||||
('slug', models.SlugField(blank=True, help_text='Unique URL slug identifying the article', unique=True)),
|
||||
('title', models.CharField(help_text='title model help_text', max_length=255, unique=True)),
|
||||
('body', models.TextField(help_text='article model help_text', max_length=5000)),
|
||||
('slug', models.SlugField(blank=True, help_text='slug model help_text', unique=True)),
|
||||
('date_created', models.DateTimeField(auto_now_add=True)),
|
||||
('date_modified', models.DateTimeField(auto_now=True)),
|
||||
('cover', models.ImageField(blank=True, upload_to='article/original/')),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='articles', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -7,5 +7,6 @@ class Article(models.Model):
|
||||
slug = models.SlugField(help_text="slug model help_text", unique=True, blank=True)
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
date_modified = models.DateTimeField(auto_now=True)
|
||||
author = models.ForeignKey('auth.User', related_name='articles', on_delete=models.CASCADE)
|
||||
|
||||
cover = models.ImageField(upload_to='article/original/', blank=True)
|
||||
|
||||
@@ -7,17 +7,26 @@ class ArticleSerializer(serializers.ModelSerializer):
|
||||
references = serializers.DictField(
|
||||
help_text="this is a really bad example",
|
||||
child=serializers.URLField(help_text="but i needed to test these 2 fields somehow"),
|
||||
read_only=True,
|
||||
)
|
||||
uuid = serializers.UUIDField(help_text="should articles have UUIDs?")
|
||||
uuid = serializers.UUIDField(help_text="should articles have UUIDs?", read_only=True)
|
||||
cover_name = serializers.FileField(use_url=False, source='cover', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = ('title', 'body', 'slug', 'date_created', 'date_modified',
|
||||
fields = ('title', 'author', 'body', 'slug', 'date_created', 'date_modified',
|
||||
'references', 'uuid', 'cover', 'cover_name')
|
||||
read_only_fields = ('date_created', 'date_modified')
|
||||
read_only_fields = ('date_created', 'date_modified',
|
||||
'references', 'uuid', 'cover_name')
|
||||
lookup_field = 'slug'
|
||||
extra_kwargs = {'body': {'help_text': 'body serializer help_text'}}
|
||||
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):
|
||||
|
||||
@@ -20,6 +20,11 @@ class NoPagingAutoSchema(SwaggerAutoSchema):
|
||||
return False
|
||||
|
||||
|
||||
class ArticlePagination(LimitOffsetPagination):
|
||||
default_limit = 5
|
||||
max_limit = 25
|
||||
|
||||
|
||||
@method_decorator(name='list', decorator=swagger_auto_schema(
|
||||
operation_description="description from swagger_auto_schema via method_decorator"
|
||||
))
|
||||
@@ -41,12 +46,11 @@ class ArticleViewSet(viewsets.ModelViewSet):
|
||||
lookup_value_regex = r'[a-z0-9]+(?:-[a-z0-9]+)'
|
||||
serializer_class = serializers.ArticleSerializer
|
||||
|
||||
pagination_class = LimitOffsetPagination
|
||||
max_page_size = 5
|
||||
pagination_class = ArticlePagination
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
filter_fields = ('title',)
|
||||
ordering_fields = ('date_modified',)
|
||||
ordering = ('username',)
|
||||
ordering_fields = ('date_modified', 'date_created')
|
||||
ordering = ('date_created',)
|
||||
|
||||
@swagger_auto_schema(auto_schema=NoPagingAutoSchema)
|
||||
@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 snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES
|
||||
@@ -23,20 +24,35 @@ class SnippetSerializer(serializers.Serializer):
|
||||
create: docstring for create from serializer classdoc
|
||||
"""
|
||||
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)
|
||||
code = serializers.CharField(style={'base_template': 'textarea.html'})
|
||||
linenos = serializers.BooleanField(required=False)
|
||||
language = LanguageSerializer(help_text="Sample help text for language")
|
||||
styles = serializers.MultipleChoiceField(choices=STYLE_CHOICES, default=['friendly'])
|
||||
lines = serializers.ListField(child=serializers.IntegerField(), allow_empty=True, allow_null=True, required=False)
|
||||
example_projects = serializers.ListSerializer(child=ExampleProjectSerializer())
|
||||
difficulty_factor = serializers.FloatField(help_text="this is here just to test FloatField")
|
||||
example_projects = serializers.ListSerializer(child=ExampleProjectSerializer(), read_only=True)
|
||||
difficulty_factor = serializers.FloatField(help_text="this is here just to test FloatField",
|
||||
read_only=True, default=lambda: 6.9)
|
||||
|
||||
def create(self, 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)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
|
||||
@@ -6,14 +6,16 @@ from snippets.models import Snippet
|
||||
|
||||
class UserSerializerrr(serializers.ModelSerializer):
|
||||
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_at = serializers.DateField(help_text="really?", read_only=True)
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
username = serializers.CharField(help_text="this field is generated from a query_serializer")
|
||||
is_staff = serializers.BooleanField(help_text="this one too!")
|
||||
username = serializers.CharField(help_text="this field is generated from a query_serializer", required=False)
|
||||
is_staff = serializers.BooleanField(help_text="this one too!", required=False)
|
||||
|
||||
+21
-10
@@ -3,6 +3,9 @@ import json
|
||||
import os
|
||||
|
||||
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 drf_yasg import openapi, codecs
|
||||
@@ -10,11 +13,16 @@ from drf_yasg.generators import OpenAPISchemaGenerator
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def generator():
|
||||
return OpenAPISchemaGenerator(
|
||||
info=openapi.Info(title="Test generator", default_version="v1"),
|
||||
version="v2",
|
||||
)
|
||||
def mock_schema_request(db):
|
||||
from rest_framework.test import force_authenticate
|
||||
|
||||
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
|
||||
@@ -28,19 +36,22 @@ def codec_yaml():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def swagger(generator):
|
||||
return generator.get_schema(None, True)
|
||||
def swagger(mock_schema_request):
|
||||
generator = OpenAPISchemaGenerator(
|
||||
info=openapi.Info(title="Test generator", default_version="v1"),
|
||||
version="v2",
|
||||
)
|
||||
return generator.get_schema(mock_schema_request, True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def swagger_dict(generator):
|
||||
swagger = generator.get_schema(None, True)
|
||||
def swagger_dict(swagger):
|
||||
json_bytes = codec_json().encode(swagger)
|
||||
return json.loads(json_bytes.decode('utf-8'))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def validate_schema():
|
||||
def validate_schema(db):
|
||||
def validate_schema(swagger):
|
||||
from flex.core import parse as validate_flex
|
||||
from swagger_spec_validator.validator20 import validate_spec as validate_ssv
|
||||
|
||||
+35
-8
@@ -178,6 +178,7 @@ paths:
|
||||
description: slug model help_text
|
||||
required: true
|
||||
type: string
|
||||
format: slug
|
||||
pattern: '[a-z0-9]+(?:-[a-z0-9]+)'
|
||||
/articles/{slug}/image/:
|
||||
get:
|
||||
@@ -231,6 +232,7 @@ paths:
|
||||
description: slug model help_text
|
||||
required: true
|
||||
type: string
|
||||
format: slug
|
||||
pattern: '[a-z0-9]+(?:-[a-z0-9]+)'
|
||||
/plain/:
|
||||
get:
|
||||
@@ -355,12 +357,12 @@ paths:
|
||||
- name: username
|
||||
in: query
|
||||
description: this field is generated from a query_serializer
|
||||
required: true
|
||||
required: false
|
||||
type: string
|
||||
- name: is_staff
|
||||
in: query
|
||||
description: this one too!
|
||||
required: true
|
||||
required: false
|
||||
type: boolean
|
||||
responses:
|
||||
'200':
|
||||
@@ -459,13 +461,16 @@ definitions:
|
||||
required:
|
||||
- title
|
||||
- body
|
||||
- references
|
||||
- uuid
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
description: title model help_text
|
||||
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:
|
||||
description: body serializer help_text
|
||||
type: string
|
||||
@@ -489,14 +494,16 @@ definitions:
|
||||
description: but i needed to test these 2 fields somehow
|
||||
type: string
|
||||
format: uri
|
||||
readOnly: true
|
||||
uuid:
|
||||
description: should articles have UUIDs?
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
cover:
|
||||
type: string
|
||||
format: uri
|
||||
readOnly: true
|
||||
format: uri
|
||||
cover_name:
|
||||
type: string
|
||||
readOnly: true
|
||||
@@ -516,8 +523,6 @@ definitions:
|
||||
required:
|
||||
- code
|
||||
- language
|
||||
- example_projects
|
||||
- difficulty_factor
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
@@ -525,6 +530,12 @@ definitions:
|
||||
type: integer
|
||||
readOnly: true
|
||||
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
|
||||
readOnly: true
|
||||
title:
|
||||
@@ -1020,12 +1031,16 @@ definitions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Project'
|
||||
readOnly: true
|
||||
difficulty_factor:
|
||||
description: this is here just to test FloatField
|
||||
type: number
|
||||
default: 6.9
|
||||
readOnly: true
|
||||
UserSerializerrr:
|
||||
required:
|
||||
- username
|
||||
- articles
|
||||
- snippets
|
||||
type: object
|
||||
properties:
|
||||
@@ -1039,10 +1054,15 @@ definitions:
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
articles:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
uniqueItems: true
|
||||
snippets:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
type: integer
|
||||
uniqueItems: true
|
||||
last_connected_ip:
|
||||
description: i'm out of ideas
|
||||
@@ -1054,6 +1074,13 @@ definitions:
|
||||
type: string
|
||||
format: date
|
||||
readOnly: true
|
||||
article_slugs:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
readOnly: true
|
||||
uniqueItems: true
|
||||
readOnly: true
|
||||
securityDefinitions:
|
||||
basic:
|
||||
type: basic
|
||||
|
||||
@@ -2,7 +2,6 @@ from datadiff.tools import assert_equal
|
||||
|
||||
|
||||
def test_reference_schema(swagger_dict, reference_schema):
|
||||
# formatted better than pytest diff
|
||||
swagger_dict = dict(swagger_dict)
|
||||
reference_schema = dict(reference_schema)
|
||||
ignore = ['info', 'host', 'schemes', 'basePath', 'securityDefinitions']
|
||||
@@ -10,4 +9,5 @@ def test_reference_schema(swagger_dict, reference_schema):
|
||||
swagger_dict.pop(attr, None)
|
||||
reference_schema.pop(attr, None)
|
||||
|
||||
# formatted better than pytest diff
|
||||
assert_equal(swagger_dict, reference_schema)
|
||||
|
||||
@@ -7,16 +7,11 @@ from drf_yasg import openapi, codecs
|
||||
from drf_yasg.generators import OpenAPISchemaGenerator
|
||||
|
||||
|
||||
def test_schema_generates_without_errors(generator):
|
||||
generator.get_schema(None, True)
|
||||
|
||||
|
||||
def test_schema_is_valid(generator, codec_yaml):
|
||||
swagger = generator.get_schema(request=None, public=False)
|
||||
def test_schema_is_valid(swagger, codec_yaml):
|
||||
codec_yaml.encode(swagger)
|
||||
|
||||
|
||||
def test_invalid_schema_fails(codec_json):
|
||||
def test_invalid_schema_fails(codec_json, mock_schema_request):
|
||||
# noinspection PyTypeChecker
|
||||
bad_generator = OpenAPISchemaGenerator(
|
||||
info=openapi.Info(
|
||||
@@ -26,40 +21,37 @@ def test_invalid_schema_fails(codec_json):
|
||||
version="v2",
|
||||
)
|
||||
|
||||
swagger = bad_generator.get_schema(None, True)
|
||||
swagger = bad_generator.get_schema(mock_schema_request, True)
|
||||
with pytest.raises(codecs.SwaggerValidationError):
|
||||
codec_json.encode(swagger)
|
||||
|
||||
|
||||
def test_json_codec_roundtrip(codec_json, generator, validate_schema):
|
||||
swagger = generator.get_schema(None, True)
|
||||
def test_json_codec_roundtrip(codec_json, swagger, validate_schema):
|
||||
json_bytes = codec_json.encode(swagger)
|
||||
validate_schema(json.loads(json_bytes.decode('utf-8')))
|
||||
|
||||
|
||||
def test_yaml_codec_roundtrip(codec_yaml, generator, validate_schema):
|
||||
swagger = generator.get_schema(None, True)
|
||||
def test_yaml_codec_roundtrip(codec_yaml, swagger, validate_schema):
|
||||
yaml_bytes = codec_yaml.encode(swagger)
|
||||
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
|
||||
validate_schema(yaml.safe_load(yaml_bytes.decode('utf-8')))
|
||||
|
||||
|
||||
def test_yaml_and_json_match(codec_yaml, codec_json, generator):
|
||||
swagger = generator.get_schema(None, True)
|
||||
def test_yaml_and_json_match(codec_yaml, codec_json, swagger):
|
||||
yaml_schema = yaml.safe_load(codec_yaml.encode(swagger).decode('utf-8'))
|
||||
json_schema = json.loads(codec_json.encode(swagger).decode('utf-8'))
|
||||
assert yaml_schema == json_schema
|
||||
|
||||
|
||||
def test_basepath_only():
|
||||
def test_basepath_only(mock_schema_request):
|
||||
generator = OpenAPISchemaGenerator(
|
||||
info=openapi.Info(title="Test generator", default_version="v1"),
|
||||
version="v2",
|
||||
url='/basepath/',
|
||||
)
|
||||
|
||||
swagger = generator.get_schema(None, public=True)
|
||||
swagger = generator.get_schema(mock_schema_request, public=True)
|
||||
assert 'host' not in swagger
|
||||
assert 'schemes' not in swagger
|
||||
assert swagger['basePath'] == '/' # base path is not implemented for now
|
||||
|
||||
Reference in New Issue
Block a user