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,
'SHOW_EXTENSIONS': True,
'DEFAULT_MODEL_RENDERING': 'model',
'DEFAULT_MODEL_DEPTH': 2,
'DEFAULT_MODEL_DEPTH': 3,
}
.. 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 -1
View File
@@ -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``
******************
+3 -3
View File
@@ -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
View File
@@ -2,6 +2,6 @@
"name": "drf-yasg",
"dependencies": {
"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,
'SHOW_EXTENSIONS': True,
'DEFAULT_MODEL_RENDERING': 'model',
'DEFAULT_MODEL_DEPTH': 2,
'DEFAULT_MODEL_DEPTH': 3,
}
REDOC_DEFAULTS = {
+14 -31
View File
@@ -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)
+2
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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)
+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
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)),
],
),
]
+1
View File
@@ -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)
+13 -4
View File
@@ -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):
+8 -4
View File
@@ -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'),
),
]
+19 -3
View File
@@ -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):
+5 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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)
+8 -16
View File
@@ -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
+1 -1
View File
@@ -30,7 +30,7 @@ deps =
-rrequirements/test.txt
commands =
pytest --cov-config .coveragerc --cov-append --cov
pytest --cov-config .coveragerc --cov-append --cov {posargs}
[testenv:py36-drfmaster]
pip_pre = True