Compare commits

..

32 Commits

Author SHA1 Message Date
Cristi Vijdea 548489a539 Add missing migration for test 2019-04-01 03:29:10 +03:00
Cristi Vijdea 887b53300a Add 1.15.0 changelog 2019-04-01 03:29:03 +03:00
Joe Korbel f692fe7c98 Update openapi.rst (#327)
Minor typo on "because"
2019-04-01 03:21:16 +03:00
Cristi Vijdea 017ae3d240 Update swagger-ui to 3.22.0 and ReDoc to 2.0.0-rc.4 2019-04-01 03:19:26 +03:00
Cristi Vijdea b57413023b Fix isort 2019-04-01 02:58:42 +03:00
Cristi Vijdea 4014c69689 Add test for #340 2019-04-01 02:58:07 +03:00
Cristi Vijdea 7bb4700003 Apply to_representation on choices
Fixes #340
2019-04-01 02:58:07 +03:00
Dimas Ari 86c1675c58 make sure all ListModelMixin views considered as list view (#306)
* make sure all ListModelMixin views considered as list view
* test to make sure all ListModelMixin views considered as list view added
2019-04-01 02:57:14 +03:00
AndreaFox93 81f0b1a2ea Use getattr for 'help_text' (#342) 2019-04-01 02:24:00 +03:00
Cristi Vijdea 298a9745df Add is_list_view and has_list_response methods 2019-03-22 14:27:15 +02:00
Cristi Vijdea 652a33a54d Fix lint errors 2019-03-04 00:49:28 +02:00
Cristi Vijdea 340a60324c Add special exclusion for FileUploadParser
Fixes #288
2019-03-04 00:48:55 +02:00
Cristi Vijdea f348084d85 Fix Python 2.7 compat 2019-03-04 00:46:00 +02:00
Cristi Vijdea 904c43a167 Add 1.14.0 changelog 2019-03-04 00:08:19 +02:00
Cristi Vijdea 4c78a683f4 Remove Background section from readme 2019-03-03 23:59:12 +02:00
Cristi Vijdea 4da09830ac Add BMaC button (#323) 2019-03-03 23:59:05 +02:00
Cristi Vijdea 34ed1e20a2 Update swagger-ui to 3.21.0 2019-03-03 18:08:25 +02:00
Cristi Vijdea 62d97a80bc Fix _serializer pickle bugs 2019-03-03 17:23:04 +02:00
Cristi Vijdea e108ddbb48 Add force_init argument to ReferenceResolver constructor
Closes #325
2019-03-03 16:40:19 +02:00
Cristi Vijdea 75a5d866be Run makemigrations 2019-03-02 05:51:15 +02:00
Cristi Vijdea f189426901 Update swagger-ui to 3.20.9 2019-03-02 05:42:39 +02:00
Cristi Vijdea b4900ebd6a Add tests for #310 2019-03-02 05:35:10 +02:00
Cristi Vijdea c593b3fcfb Remove _summary_and_description_compat 2019-02-27 21:51:01 +02:00
Cristi Vijdea 9caeed781e Fix duplicate ref_name detection and promote to error 2019-02-27 21:47:31 +02:00
Cristi Vijdea 3377ef08ea Fix Schema limits for ListField and ListSerializer
Fixes #324
2019-02-27 21:23:25 +02:00
Daniel Hahler 5c2c39c82d Travis: use Ubuntu Xenial (#302)
Closes #302
2019-02-27 21:23:09 +02:00
Roman Sichny e538e0713a Support RecursiveField with 'many' option (#321) 2019-02-25 20:59:35 +02:00
Roman Sichny 76c8fe0646 Fix union type hint checks (#318)
Fix some obscure edge cases related to typing.Union type args.

Fixes #304.
2019-02-22 01:00:14 +02:00
Paul Wayper 3d43ee6748 Fix items field conflict with dict.items() (#308)
Closes #307
2019-02-22 00:57:52 +02:00
Cristi Vijdea 583e404ed8 Add xenial hacks back
Travis is still retarded
2019-01-29 09:43:09 +02:00
Cristi Vijdea d62243599b Remove xenial incantations 2019-01-29 09:28:22 +02:00
Cristi Vijdea 6df3523675 Use python 3.7 for lint and Django master tests 2019-01-29 09:26:56 +02:00
35 changed files with 1337 additions and 272 deletions
+5 -10
View File
@@ -1,25 +1,22 @@
language: python
sudo: false
python:
- '2.7'
- '3.4'
- '3.5'
- '3.6'
- '3.7'
dist: xenial
cache: pip
jobs:
include:
- # workaround for python 3.7 on travis https://github.com/travis-ci/travis-ci/issues/9815#issuecomment-401756442
stage: test
python: '3.7'
dist: xenial
sudo: required
- python: '3.6'
env: TOXENV=docs
- python: '3.6'
- python: '3.7'
env: TOXENV=djmaster
- python: '3.6'
- python: '3.7'
env: TOXENV=lint
- stage: publish
@@ -66,7 +63,6 @@ after_success:
branches:
only:
- master
- /^release\/.*$/
- /^v?\d+\.\d+(\.\d+)?(-?\S+)?$/
stages:
@@ -74,7 +70,6 @@ stages:
- name: publish
if: tag IS present
notifications:
email:
on_success: always
+11 -42
View File
@@ -7,6 +7,8 @@ drf-yasg - Yet another Swagger generator
|travis| |nbsp| |codecov| |nbsp| |rtd-badge| |nbsp| |pypi-version|
|bmac-button|
Generate **real** Swagger/OpenAPI 2.0 specifications from a Django Rest Framework API.
Compatible with
@@ -29,9 +31,7 @@ Resources:
* **Changelog**: https://drf-yasg.readthedocs.io/en/stable/changelog.html
* **Live demo**: https://drf-yasg-demo.herokuapp.com/
.. image:: https://www.herokucdn.com/deploy/button.svg
:target: https://heroku.com/deploy?template=https://github.com/axnsan12/drf-yasg
:alt: heroku deploy button
|heroku-button|
********
Features
@@ -306,45 +306,6 @@ For additional usage examples, you can take a look at the test project in the ``
(venv) $ python manage.py runserver
(venv) $ firefox localhost:8000/swagger/
**********
Background
**********
``OpenAPI 2.0``/``Swagger`` is a format designed to encode information about a Web API into an easily parsable schema
that can then be used for rendering documentation, generating code, etc.
More details are available on `swagger.io <https://swagger.io/>`__ and on the `OpenAPI 2.0 specification
page <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md>`__.
From here on, the terms “OpenAPI” and “Swagger” are used interchangeably.
Swagger in Django Rest Framework
================================
Since Django Rest Framework 3.7, there is now `built in support <http://www.django-rest-framework.org/api-guide/schemas/>`__
for automatic OpenAPI 2.0 schema generation. However, this generation is based on the `coreapi <http://www.coreapi.org/>`__
standard, which for the moment is vastly inferior to OpenAPI in both features and tooling support. In particular,
the OpenAPI codec/compatibility layer provided has a few major problems:
* there is no support for documenting response schemas and status codes
* nested schemas do not work properly
* does not handle more complex fields such as ``FileField``, ``ChoiceField``, …
In short this makes the generated schema unusable for code generation, and mediocre at best for documentation.
Other libraries
===============
There are currently two decent Swagger schema generators that I could find for django-rest-framework:
* `django-rest-swagger <https://github.com/marcgibbons/django-rest-swagger>`__
* `drf-openapi <https://github.com/limdauto/drf_openapi>`__
``django-rest-swagger`` is just a wrapper around DRF 3.7 schema generation with an added UI, and
thus presents the same problems, while also being unmaintained. ``drf-openapi`` was
`discontinued by the author <https://github.com/limdauto/drf_openapi/commit/1673c6e039eec7f089336a83bdc31613f32f7e21>`_
on April 3rd, 2018.
************************
Third-party integrations
************************
@@ -378,5 +339,13 @@ provided out of the box - if you have ``djangorestframework-recursive`` installe
:target: https://drf-yasg.readthedocs.io/
:alt: ReadTheDocs
.. |bmac-button| image:: https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png
:target: https://www.buymeacoffee.com/cvijdea
:alt: Buy Me A Coffee
.. |heroku-button| image:: https://www.herokucdn.com/deploy/button.svg
:target: https://heroku.com/deploy?template=https://github.com/axnsan12/drf-yasg
:alt: Heroku deploy button
.. |nbsp| unicode:: 0xA0
:trim:
+27 -1
View File
@@ -3,13 +3,39 @@ Changelog
#########
**********
**1.15.0**
**********
*Release date: Apr 01, 2019*
- **ADDED:** added ``is_list_view`` and ``has_list_response`` extension points to ``SwaggerAutoSchema`` (:issue:`331`)
- **IMPROVED:** updated ``swagger-ui`` to version 3.22.0
- **IMPROVED:** updated ``ReDoc`` to version 2.0.0-rc.4
- **FIXED:** ``ListModelMixin`` will now always be treated as a list view (:issue:`306`)
- **FIXED:** non-primtive values in field ``choices`` will now be handled properly (:issue:`340`)
**********
**1.14.0**
**********
*Release date: Mar 04, 2019*
- **IMPROVED:** updated ``swagger-ui`` to version 3.21.0
- **FIXED:** implicit ``ref_name`` collisions will now throw an exception
- **FIXED:** ``RecursiveField`` will now also work as a child of ``ListSerializer`` (:pr:`321`)
- **FIXED:** fixed ``minLength`` and ``maxLength`` for ``ListSerializer`` and ``ListField``
- **FIXED:** the ``items`` property of ``Schema``, ``Parameter`` and ``Items`` objects was renamed to ``items_``; this
is a *mildly breaking change* and was needed to fix the collision with the ``items`` method of ``dict`` (:pr:`308`)
- **REMOVED:** the ``get_summary`` and ``get_description`` methods have been removed (previously deprecated in 1.12.0)
**********
**1.13.0**
**********
*Release date: Jan 29, 2019*
- **IMPROVED:** type hint inspection is now supported for collections and``Optional`` (:pr:`272`)
- **IMPROVED:** type hint inspection is now supported for collections and ``Optional`` (:pr:`272`)
- **IMPROVED:** updated ``swagger-ui`` to version 3.20.5
- **IMPROVED:** updated ``ReDoc`` to version 2.0.0-rc.2
- **DEPRECATED:** quietly dropped support for the ``flex`` validator; it will still work if the library is installed,
+1 -1
View File
@@ -14,7 +14,7 @@ This library generates OpenAPI 2.0 documents. The authoritative specification fo
be the official documentation over at `swagger.io <https://swagger.io/>`__ and the `OpenAPI 2.0 specification
page <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md>`__.
Beause the above specifications are a bit heavy and convoluted, here is a general overview of how the specification
Because the above specifications are a bit heavy and convoluted, here is a general overview of how the specification
is structured, starting from the root ``Swagger`` object.
* :class:`.Swagger` object
+793 -81
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,8 +1,8 @@
{
"name": "drf-yasg",
"dependencies": {
"redoc": "^2.0.0-rc.2",
"swagger-ui-dist": "^3.20.5"
"redoc": "^2.0.0-rc.4",
"swagger-ui-dist": "^3.22.0"
},
"repository": {
"type": "git",
+1 -1
View File
@@ -238,7 +238,7 @@ class OpenAPISchemaGenerator(object):
:rtype: openapi.Swagger
"""
endpoints = self.get_endpoints(request)
components = ReferenceResolver(openapi.SCHEMA_DEFINITIONS)
components = ReferenceResolver(openapi.SCHEMA_DEFINITIONS, force_init=True)
self.consumes = get_consumes(api_settings.DEFAULT_PARSER_CLASSES)
self.produces = get_produces(api_settings.DEFAULT_RENDERER_CLASSES)
paths, prefix = self.get_paths(endpoints, components, request, public)
+24 -16
View File
@@ -272,7 +272,8 @@ class FieldInspector(BaseInspector):
assert not isinstance(field, openapi.SwaggerDict), "passed field is already a SwaggerDict object"
title = force_real_str(field.label) if field.label else None
title = title if swagger_object_type == openapi.Schema else None # only Schema has title
description = force_real_str(field.help_text) if field.help_text else None
help_text = getattr(field, 'help_text', None)
description = force_real_str(help_text) if help_text else None
description = description if swagger_object_type != openapi.Items else None # Items has no description either
def SwaggerType(existing_object=None, **instance_kwargs):
@@ -341,6 +342,9 @@ class ViewInspector(BaseInspector):
#: methods that are assumed to require a request body determined by the view's ``serializer_class``
implicit_body_methods = ('PUT', 'PATCH', 'POST')
#: methods which are assumed to return a list of objects when present on non-detail endpoints
implicit_list_response_methods = ('GET',)
# real values set in __init__ to prevent import errors
field_inspectors = [] #:
filter_inspectors = [] #:
@@ -374,20 +378,30 @@ class ViewInspector(BaseInspector):
"""
raise NotImplementedError("ViewInspector must implement get_operation()!")
# methods below provided as default implementations for probing inspectors
def is_list_view(self):
"""Determine whether this view is a list or a detail view. The difference between the two is that
detail views depend on a pk/id path parameter. Note that a non-detail view does not necessarily imply a list
reponse (:meth:`.has_list_response`), nor are list responses limited to non-detail views.
For example, one might have a `/topic/<pk>/posts` endpoint which is a detail view that has a list response.
:rtype: bool"""
return is_list_view(self.path, self.method, self.view)
def has_list_response(self):
"""Determine whether this view returns multiple objects. By default this is any non-detail view
(see :meth:`.is_list_view`) whose request method is one of :attr:`.implicit_list_response_methods`.
:rtype: bool
"""
return self.is_list_view() and (self.method.upper() in self.implicit_list_response_methods)
def should_filter(self):
"""Determine whether filter backend parameters should be included for this request.
:rtype: bool
"""
if not getattr(self.view, 'filter_backends', None):
return False
if self.method.lower() not in ["get", "delete"]:
return False
return is_list_view(self.path, self.method, self.view)
return getattr(self.view, 'filter_backends', None) and self.has_list_response()
def get_filter_parameters(self):
"""Return the parameters added to the view by its filter backends.
@@ -408,13 +422,7 @@ class ViewInspector(BaseInspector):
:rtype: bool
"""
if not getattr(self.view, 'paginator', None):
return False
if self.method.lower() != 'get':
return False
return is_list_view(self.path, self.method, self.view)
return getattr(self.view, 'paginator', None) and self.has_list_response()
def get_pagination_parameters(self):
"""Return the parameters added to the view by its paginator.
+55 -12
View File
@@ -13,7 +13,9 @@ from rest_framework.settings import api_settings as rest_framework_settings
from .. import openapi
from ..errors import SwaggerGenerationError
from ..utils import decimal_as_float, filter_none, get_serializer_class, get_serializer_ref_name
from ..utils import (
decimal_as_float, field_value_to_representation, filter_none, get_serializer_class, get_serializer_ref_name
)
from .base import FieldInspector, NotHandled, SerializerInspector, call_view_method
try:
@@ -72,14 +74,20 @@ class InlineSerializerInspector(SerializerInspector):
def get_serializer_ref_name(self, serializer):
return get_serializer_ref_name(serializer)
def _has_ref_name(self, serializer):
serializer_meta = getattr(serializer, 'Meta', None)
return hasattr(serializer_meta, 'ref_name')
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
if isinstance(field, (serializers.ListSerializer, serializers.ListField)):
child_schema = self.probe_field_inspectors(field.child, ChildSwaggerType, use_references)
limits = find_limits(field) or {}
return SwaggerType(
type=openapi.TYPE_ARRAY,
items=child_schema,
**limits
)
elif isinstance(field, serializers.Serializer):
if swagger_object_type != openapi.Schema:
@@ -116,6 +124,7 @@ class InlineSerializerInspector(SerializerInspector):
# it is better to just remove title from inline models
del result.title
setattr(result, '_NP_serializer', get_serializer_class(serializer))
return result
if not ref_name or not use_references:
@@ -125,11 +134,15 @@ class InlineSerializerInspector(SerializerInspector):
actual_schema = definitions.setdefault(ref_name, make_schema_definition)
actual_schema._remove_read_only()
actual_serializer = get_serializer_class(getattr(actual_schema, '_serializer', None))
actual_serializer = getattr(actual_schema, '_NP_serializer', None)
this_serializer = get_serializer_class(field)
if actual_serializer and actual_serializer != this_serializer: # pragma: no cover
logger.warning("Schema for %s will override distinct serializer %s because they "
"share the same ref_name", actual_serializer, this_serializer)
explicit_refs = self._has_ref_name(actual_serializer) and self._has_ref_name(this_serializer)
if not explicit_refs:
raise SwaggerGenerationError(
"Schema for %s would override distinct serializer %s because they implicitly share the same "
"ref_name; explicitly set the ref_name atribute on both serializers' Meta classes"
% (actual_serializer, this_serializer))
return openapi.SchemaRef(definitions, ref_name)
@@ -490,6 +503,20 @@ if typing:
hinting_type_info.append(((typing.Sequence, typing.AbstractSet), inspect_collection_hint_class))
def _get_union_types(hint_class):
if typing:
origin_type = get_origin_type(hint_class)
if origin_type is typing.Union:
return hint_class.__args__
try:
# python 3.5.2 and lower compatibility
if issubclass(origin_type, typing.Union):
return hint_class.__union_params__
except TypeError:
pass
return None
def get_basic_type_info_from_hint(hint_class):
"""Given a class (eg from a SerializerMethodField's return type hint,
return its basic type information - ``type``, ``format``, ``pattern``,
@@ -499,11 +526,11 @@ def get_basic_type_info_from_hint(hint_class):
:return: the extracted attributes as a dictionary, or ``None`` if the field type is not known
:rtype: OrderedDict
"""
if typing and get_origin_type(hint_class) == typing.Union:
union_types = _get_union_types(hint_class)
if typing and union_types:
# Optional is implemented as Union[T, None]
if len(hint_class.__args__) == 2 and hint_class.__args__[1] == type(None): # noqa: E721
child_type = hint_class.__args__[0]
result = get_basic_type_info_from_hint(child_type)
if len(union_types) == 2 and isinstance(None, union_types[1]):
result = get_basic_type_info_from_hint(union_types[0])
result['x-nullable'] = True
return result
@@ -610,7 +637,14 @@ class ChoiceFieldInspector(FieldInspector):
if isinstance(field, serializers.ChoiceField):
enum_type = openapi.TYPE_STRING
enum_values = list(field.choices.keys())
enum_values = []
for choice in field.choices.keys():
if isinstance(field, serializers.MultipleChoiceField):
choice = field_value_to_representation(field, [choice])[0]
else:
choice = field_value_to_representation(field, choice)
enum_values.append(choice)
# for ModelSerializer, try to infer the type from the associated model field
serializer = get_parent_serializer(field)
@@ -780,10 +814,19 @@ else:
if isinstance(field, RecursiveField) and swagger_object_type == openapi.Schema:
assert use_references is True, "Can not create schema for RecursiveField when use_references is False"
ref_name = get_serializer_ref_name(field.proxied)
assert ref_name is not None, "Can't create RecursiveField schema for inline " + str(type(field.proxied))
proxied = field.proxied
if isinstance(field.proxied, serializers.ListSerializer):
proxied = proxied.child
ref_name = get_serializer_ref_name(proxied)
assert ref_name is not None, "Can't create RecursiveField schema for inline " + str(type(proxied))
definitions = self.components.with_scope(openapi.SCHEMA_DEFINITIONS)
return openapi.SchemaRef(definitions, ref_name, ignore_unresolved=True)
ref = openapi.SchemaRef(definitions, ref_name, ignore_unresolved=True)
if isinstance(field.proxied, serializers.ListSerializer):
ref = openapi.Items(type=openapi.TYPE_ARRAY, items=ref)
return ref
return NotHandled
+1 -28
View File
@@ -21,17 +21,6 @@ class SwaggerAutoSchema(ViewInspector):
super(SwaggerAutoSchema, self).__init__(view, path, method, components, request, overrides)
self._sch = AutoSchema()
self._sch.view = view
self._summary_and_description_compat()
def _summary_and_description_compat(self):
# TODO: remove in 1.14
base_methods = (SwaggerAutoSchema.get_summary, SwaggerAutoSchema.get_description)
self_methods = (type(self).get_summary, type(self).get_description)
if self_methods != base_methods:
raise NotImplementedError(
"`SwaggerAutoSchema` methods `get_summary` and `get_description` were removed in "
"drf-yasg 1.13 and will have no effect. Override `get_summary_and_description` instead."
)
def get_operation(self, operation_keys):
consumes = self.get_consumes()
@@ -219,7 +208,7 @@ class SwaggerAutoSchema(ViewInspector):
default_schema = self.serializer_to_schema(default_schema) or ''
if default_schema:
if is_list_view(self.path, self.method, self.view) and self.method.lower() == 'get':
if self.has_list_response():
default_schema = openapi.Schema(type=openapi.TYPE_ARRAY, items=default_schema)
if self.should_page():
default_schema = self.get_paginated_response(default_schema) or default_schema
@@ -362,22 +351,6 @@ class SwaggerAutoSchema(ViewInspector):
return summary, description
def get_summary(self):
"""Return a summary description for this operation.
:return: the summary
:rtype: str
"""
return self.get_summary_and_description()[0]
def get_description(self):
"""Return an operation description determined as appropriate from the view's method and class docstrings.
:return: the operation description
:rtype: str
"""
return self.get_summary_and_description()[1]
def get_security(self):
"""Return a list of security requirements for this operation.
+18 -7
View File
@@ -167,7 +167,8 @@ class SwaggerDict(OrderedDict):
def __reduce__(self):
# for pickle supprt; this skips calls to all SwaggerDict __init__ methods and relies
# on the already set attributes instead
return _bare_SwaggerDict, (type(self),), vars(self), None, iter(self.items())
attrs = {k: v for k, v in vars(self).items() if not k.startswith('_NP_')}
return _bare_SwaggerDict, (type(self),), attrs, None, iter(self.items())
class Contact(SwaggerDict):
@@ -400,7 +401,7 @@ class Items(SwaggerDict):
self.format = format
self.enum = enum
self.pattern = pattern
self.items = items
self.items_ = items
self._insert_extras__()
_check_type(type, format, enum, pattern, items, self.__class__)
@@ -434,7 +435,7 @@ class Parameter(SwaggerDict):
self.format = format
self.enum = enum
self.pattern = pattern
self.items = items
self.items_ = items
self.default = default
self._insert_extras__()
if (not schema and not type) or (schema and type):
@@ -492,7 +493,7 @@ class Schema(SwaggerDict):
self.format = format
self.enum = enum
self.pattern = pattern
self.items = items
self.items_ = items
self.read_only = read_only
self.default = default
self._insert_extras__()
@@ -617,16 +618,26 @@ class ReferenceResolver(object):
::
> components = ReferenceResolver('definitions', 'parameters')
> definitions = ReferenceResolver.with_scope('definitions')
> definitions = components.with_scope('definitions')
> definitions.set('Article', Schema(...))
> print(components)
{'definitions': OrderedDict([('Article', Schema(...)]), 'parameters': OrderedDict()}
"""
def __init__(self, *scopes):
def __init__(self, *scopes, **kwargs):
"""
:param str scopes: an enumeration of the valid scopes this resolver will contain
"""
force_init = kwargs.pop('force_init', False)
if not force_init:
raise AssertionError(
"Creating an instance of ReferenceResolver almost certainly won't do what you want it to do.\n"
"See https://github.com/axnsan12/drf-yasg/issues/211, "
"https://github.com/axnsan12/drf-yasg/issues/271, "
"https://github.com/axnsan12/drf-yasg/issues/325.\n"
"Pass `force_init=True` to override this."
)
self._objects = OrderedDict()
self._force_scope = None
for scope in scopes:
@@ -641,7 +652,7 @@ class ReferenceResolver(object):
:rtype: .ReferenceResolver
"""
assert scope in self.scopes, "unknown scope %s" % scope
ret = ReferenceResolver()
ret = ReferenceResolver(force_init=True)
ret._objects = self._objects
ret._force_scope = scope
return ret
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 628 B

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
+27 -7
View File
@@ -2,11 +2,13 @@ import inspect
import logging
import sys
from collections import OrderedDict
from decimal import Decimal
from django.db import models
from django.utils.encoding import force_text
from rest_framework import serializers, status
from rest_framework.mixins import DestroyModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.mixins import DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.parsers import FileUploadParser
from rest_framework.request import is_form_media_type
from rest_framework.settings import api_settings as rest_framework_settings
from rest_framework.utils import encoders, json
@@ -225,6 +227,9 @@ def is_list_view(path, method, view):
# a detail action is surely not a list route
return False
if isinstance(view, ListModelMixin):
return True
# for GenericAPIView, if it's a detail view it can't also be a list view
if isinstance(view, (RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin)):
return False
@@ -366,6 +371,7 @@ def get_consumes(parser_classes):
:rtype: list[str]
"""
parser_classes = get_object_classes(parser_classes)
parser_classes = [pc for pc in parser_classes if not issubclass(pc, FileUploadParser)]
media_types = [parser.media_type for parser in parser_classes or []]
non_form_media_types = [encoding for encoding in media_types if not is_form_media_type(encoding)]
if len(non_form_media_types) == 0:
@@ -435,6 +441,25 @@ def force_real_str(s, encoding='utf-8', strings_only=False, errors='strict'):
return s
def field_value_to_representation(field, value):
"""Convert a python value related to a field (default, choices, etc.) into its OpenAPI-compatible representation.
:param serializers.Field field: field associated with the value
:param object value: value
:return: the converted value
"""
value = field.to_representation(value)
if isinstance(value, Decimal):
if decimal_as_float(field):
value = float(value)
else:
value = str(value)
# JSON roundtrip ensures that the value is valid JSON;
# for example, sets and tuples get transformed into lists
return json.loads(json.dumps(value, cls=encoders.JSONEncoder))
def get_field_default(field):
"""
Get the default value for a field, converted to a JSON-compatible value while properly handling callables.
@@ -456,12 +481,7 @@ def get_field_default(field):
if default is not serializers.empty and default is not None:
try:
default = field.to_representation(default)
# JSON roundtrip ensures that the value is valid JSON;
# for example, sets and tuples get transformed into lists
default = json.loads(json.dumps(default, cls=encoders.JSONEncoder))
if decimal_as_float(field):
default = float(default)
default = field_value_to_representation(field, default)
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)
@@ -0,0 +1,18 @@
# Generated by Django 2.1.5 on 2019-03-02 03:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('articles', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='article',
name='read_only_nullable',
field=models.CharField(blank=True, max_length=20, null=True),
),
]
+1
View File
@@ -20,6 +20,7 @@ class Article(models.Model):
on_delete=models.PROTECT)
original_group = models.ForeignKey('ArticleGroup', related_name='articles_as_original', blank=True, default=None,
on_delete=models.PROTECT)
read_only_nullable = models.CharField(max_length=20, null=True, blank=True)
class ArticleGroup(models.Model):
+3 -3
View File
@@ -17,10 +17,9 @@ class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ('title', 'author', 'body', 'slug', 'date_created', 'date_modified',
fields = ('title', 'author', 'body', 'slug', 'date_created', 'date_modified', 'read_only_nullable',
'references', 'uuid', 'cover', 'cover_name', 'article_type', 'group', 'original_group', )
read_only_fields = ('date_created', 'date_modified',
'references', 'uuid', 'cover_name')
read_only_fields = ('date_created', 'date_modified', 'references', 'uuid', 'cover_name', 'read_only_nullable')
lookup_field = 'slug'
extra_kwargs = {
'body': {'help_text': 'body serializer help_text'},
@@ -29,6 +28,7 @@ class ArticleSerializer(serializers.ModelSerializer):
'help_text': _("The ID of the user that created this article; if none is provided, "
"defaults to the currently logged in user.")
},
'read_only_nullable': {'allow_null': True},
}
+2 -2
View File
@@ -7,7 +7,7 @@ from rest_framework import viewsets
# noinspection PyDeprecation
from rest_framework.filters import OrderingFilter
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.parsers import MultiPartParser
from rest_framework.parsers import FileUploadParser, MultiPartParser
from rest_framework.response import Response
from articles import serializers
@@ -118,7 +118,7 @@ class ArticleViewSet(viewsets.ModelViewSet):
type=openapi.TYPE_INTEGER,
description="this should not crash (form parameter on DELETE method)"
)])
@detail_route(methods=['get', 'post', 'delete'], parser_classes=(MultiPartParser,))
@detail_route(methods=['get', 'post', 'delete'], parser_classes=(MultiPartParser, FileUploadParser))
def image(self, request, slug=None):
"""
image method docstring
@@ -0,0 +1,24 @@
# Generated by Django 2.1.7 on 2019-03-16 14:06
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', '0002_auto_20181219_1016'),
]
operations = [
migrations.CreateModel(
name='SnippetViewer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('snippet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='viewers', to='snippets.Snippet')),
('viewer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='snippet_views', to=settings.AUTH_USER_MODEL)),
],
),
]
+5
View File
@@ -18,3 +18,8 @@ class Snippet(models.Model):
class Meta:
ordering = ('created',)
class SnippetViewer(models.Model):
snippet = models.ForeignKey(Snippet, on_delete=models.CASCADE, related_name='viewers')
viewer = models.ForeignKey('auth.User', related_name='snippet_views', on_delete=models.CASCADE)
+12 -3
View File
@@ -2,14 +2,15 @@ from decimal import Decimal
from django.contrib.auth import get_user_model
from rest_framework import serializers
from rest_framework.compat import MinValueValidator
from rest_framework.compat import MaxLengthValidator, MinValueValidator
from snippets.models import LANGUAGE_CHOICES, STYLE_CHOICES, Snippet
from snippets.models import LANGUAGE_CHOICES, STYLE_CHOICES, Snippet, SnippetViewer
class LanguageSerializer(serializers.Serializer):
name = serializers.ChoiceField(
choices=LANGUAGE_CHOICES, default='python', help_text='The name of the programming language')
read_only_nullable = serializers.CharField(read_only=True, allow_null=True)
class Meta:
ref_name = None
@@ -65,11 +66,13 @@ class SnippetSerializer(serializers.Serializer):
)
title = serializers.CharField(required=False, allow_blank=True, max_length=100)
code = serializers.CharField(style={'base_template': 'textarea.html'})
tags = serializers.ListField(child=serializers.CharField(min_length=2), min_length=3, max_length=15)
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(), read_only=True)
example_projects = serializers.ListSerializer(child=ExampleProjectSerializer(), read_only=True,
validators=[MaxLengthValidator(100)])
difficulty_factor = serializers.FloatField(help_text="this is here just to test FloatField",
read_only=True, default=lambda: 6.9)
rate_as_string = serializers.DecimalField(max_digits=6, decimal_places=3, default=Decimal('0.0'),
@@ -97,3 +100,9 @@ class SnippetSerializer(serializers.Serializer):
instance.style = validated_data.get('style', instance.style)
instance.save()
return instance
class SnippetViewerSerializer(serializers.ModelSerializer):
class Meta:
model = SnippetViewer
fields = '__all__'
+3
View File
@@ -8,10 +8,13 @@ if django.VERSION[:2] >= (2, 0):
urlpatterns = [
path('', views.SnippetList.as_view()),
path('<int:pk>/', views.SnippetDetail.as_view()),
path('views/<int:snippet_pk>/', views.SnippetViewerList.as_view()),
]
else:
from django.conf.urls import url
urlpatterns = [
url('^$', views.SnippetList.as_view()),
url(r'^(?P<pk>\d+)/$', views.SnippetDetail.as_view()),
url(r'^views/(?P<snippet_pk>\d+)/$', views.SnippetViewerList.as_view()),
]
+34 -4
View File
@@ -2,13 +2,15 @@ from djangorestframework_camel_case.parser import CamelCaseJSONParser
from djangorestframework_camel_case.render import CamelCaseJSONRenderer
from inflection import camelize
from rest_framework import generics, status
from rest_framework.parsers import FormParser
from rest_framework.generics import get_object_or_404
from rest_framework.pagination import PageNumberPagination
from rest_framework.parsers import FileUploadParser, FormParser
from drf_yasg import openapi
from drf_yasg.inspectors import SwaggerAutoSchema
from drf_yasg.utils import swagger_auto_schema
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from snippets.models import Snippet, SnippetViewer
from snippets.serializers import SnippetSerializer, SnippetViewerSerializer
class CamelCaseOperationIDAutoSchema(SwaggerAutoSchema):
@@ -22,7 +24,7 @@ class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
parser_classes = (FormParser, CamelCaseJSONParser,)
parser_classes = (FormParser, CamelCaseJSONParser, FileUploadParser)
renderer_classes = (CamelCaseJSONRenderer,)
swagger_schema = CamelCaseOperationIDAutoSchema
@@ -93,3 +95,31 @@ class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
def delete(self, request, *args, **kwargs):
"""delete method docstring"""
return super(SnippetDetail, self).patch(request, *args, **kwargs)
class SnippetViewerList(generics.ListAPIView):
"""SnippetViewerList classdoc"""
serializer_class = SnippetViewerSerializer
pagination_class = PageNumberPagination
parser_classes = (FormParser, CamelCaseJSONParser, FileUploadParser)
renderer_classes = (CamelCaseJSONRenderer,)
swagger_schema = CamelCaseOperationIDAutoSchema
lookup_url_kwarg = 'snippet_pk'
def get_object(self):
queryset = Snippet.objects.all()
# Perform the lookup filtering.
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
obj = get_object_or_404(queryset, **filter_kwargs)
# May raise a permission denied
self.check_object_permissions(self.request, obj)
return obj
def get_queryset(self):
return SnippetViewer.objects.filter(snippet=self.get_object())
+21
View File
@@ -0,0 +1,21 @@
# Generated by Django 2.1.5 on 2019-04-01 00:28
from decimal import Decimal
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('todo', '0002_todotree'),
]
operations = [
migrations.CreateModel(
name='Pack',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('size_code', models.DecimalField(choices=[(Decimal('50'), '5x10'), (Decimal('100'), '10x10'), (Decimal('200'), '10x20')], decimal_places=3, default=Decimal('200'), max_digits=7)),
],
),
]
+18
View File
@@ -1,3 +1,5 @@
from decimal import Decimal
from django.db import models
@@ -18,3 +20,19 @@ class TodoYetAnother(models.Model):
class TodoTree(models.Model):
parent = models.ForeignKey('self', on_delete=models.CASCADE, related_name='children', null=True)
title = models.CharField(max_length=50)
class Pack(models.Model):
SIZE_10x20 = Decimal(200.000)
SIZE_10x10 = Decimal(100.000)
SIZE_5x10 = Decimal(50.000)
size_code_choices = (
(SIZE_5x10, '5x10'),
(SIZE_10x10, '10x10'),
(SIZE_10x20, '10x20'),
)
size_code = models.DecimalField(max_digits=7,
decimal_places=3,
choices=size_code_choices,
default=SIZE_10x20)
+14 -2
View File
@@ -4,7 +4,7 @@ from django.utils import timezone
from rest_framework import serializers
from rest_framework_recursive.fields import RecursiveField
from .models import Todo, TodoAnother, TodoTree, TodoYetAnother
from .models import Pack, Todo, TodoAnother, TodoTree, TodoYetAnother
class TodoSerializer(serializers.ModelSerializer):
@@ -41,10 +41,11 @@ class TodoYetAnotherSerializer(serializers.ModelSerializer):
class TodoTreeSerializer(serializers.ModelSerializer):
children = serializers.ListField(child=RecursiveField(), source='children.all')
many_children = RecursiveField(many=True, source='children')
class Meta:
model = TodoTree
fields = ('id', 'title', 'children')
fields = ('id', 'title', 'children', 'many_children')
class TodoRecursiveSerializer(serializers.ModelSerializer):
@@ -56,3 +57,14 @@ class TodoRecursiveSerializer(serializers.ModelSerializer):
class Meta:
model = TodoTree
fields = ('id', 'title', 'parent', 'parent_id')
class HarvestSerializer(serializers.ModelSerializer):
class Meta:
model = Pack
fields = (
'size_code',
)
read_only_fields = (
'size_code',
)
+1
View File
@@ -9,6 +9,7 @@ router.register(r'another', views.TodoAnotherViewSet)
router.register(r'yetanother', views.TodoYetAnotherViewSet)
router.register(r'tree', views.TodoTreeView)
router.register(r'recursive', views.TodoRecursiveView)
router.register(r'harvest', views.HarvestViewSet)
urlpatterns = router.urls
+18 -3
View File
@@ -1,11 +1,13 @@
from rest_framework import viewsets
from rest_framework import mixins, permissions, viewsets
from rest_framework.authentication import TokenAuthentication
from rest_framework.generics import RetrieveAPIView
from drf_yasg.utils import swagger_auto_schema
from .models import Todo, TodoAnother, TodoTree, TodoYetAnother
from .models import Pack, Todo, TodoAnother, TodoTree, TodoYetAnother
from .serializer import (
TodoAnotherSerializer, TodoRecursiveSerializer, TodoSerializer, TodoTreeSerializer, TodoYetAnotherSerializer
HarvestSerializer, TodoAnotherSerializer, TodoRecursiveSerializer, TodoSerializer, TodoTreeSerializer,
TodoYetAnotherSerializer
)
@@ -75,3 +77,16 @@ class TodoRecursiveView(viewsets.ModelViewSet):
@swagger_auto_schema(responses={200: TodoRecursiveSerializer(many=True)})
def list(self, request, *args, **kwargs):
return super(TodoRecursiveView, self).list(request, *args, **kwargs)
class HarvestViewSet(mixins.ListModelMixin,
mixins.UpdateModelMixin,
viewsets.GenericViewSet):
queryset = Pack.objects.all()
serializer_class = HarvestSerializer
permission_classes = [permissions.IsAuthenticated]
authentication_classes = [TokenAuthentication]
def perform_update(self, serializer):
pass
+146
View File
@@ -440,6 +440,46 @@ paths:
tags:
- snippets
parameters: []
/snippets/views/{snippet_pk}/:
get:
operationId: snippetsViewsRead
description: SnippetViewerList classdoc
parameters:
- name: page
in: query
description: A page number within the paginated result set.
required: false
type: integer
responses:
'200':
description: ''
schema:
required:
- count
- results
type: object
properties:
count:
type: integer
next:
type: string
format: uri
x-nullable: true
previous:
type: string
format: uri
x-nullable: true
results:
type: array
items:
$ref: '#/definitions/SnippetViewer'
tags:
- snippets
parameters:
- name: snippet_pk
in: path
required: true
type: string
/snippets/{id}/:
get:
operationId: snippetsRead
@@ -552,6 +592,60 @@ paths:
description: A unique integer value identifying this todo another.
required: true
type: integer
/todo/harvest/:
get:
operationId: todo_harvest_list
description: ''
parameters: []
responses:
'200':
description: ''
schema:
type: array
items:
$ref: '#/definitions/Harvest'
tags:
- todo
parameters: []
/todo/harvest/{id}/:
put:
operationId: todo_harvest_update
description: ''
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/Harvest'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/Harvest'
tags:
- todo
patch:
operationId: todo_harvest_partial_update
description: ''
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/Harvest'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/Harvest'
tags:
- todo
parameters:
- name: id
in: path
description: A unique integer value identifying this pack.
required: true
type: integer
/todo/recursive/:
get:
operationId: todo_recursive_list
@@ -902,6 +996,11 @@ definitions:
type: string
format: date-time
readOnly: true
read_only_nullable:
type: string
readOnly: true
minLength: 1
x-nullable: true
references:
description: this is a really bad example
type: object
@@ -1023,6 +1122,7 @@ definitions:
Snippet:
required:
- code
- tags
- language
type: object
properties:
@@ -1057,6 +1157,13 @@ definitions:
title: Code
type: string
minLength: 1
tags:
type: array
items:
type: string
minLength: 2
maxItems: 15
minItems: 3
linenos:
title: Linenos
type: boolean
@@ -1507,6 +1614,12 @@ definitions:
- yaml+jinja
- zephir
default: python
readOnlyNullable:
title: Read only nullable
type: string
readOnly: true
minLength: 1
x-nullable: true
styles:
type: array
items:
@@ -1553,6 +1666,7 @@ definitions:
items:
$ref: '#/definitions/Project'
readOnly: true
maxItems: 100
difficultyFactor:
title: Difficulty factor
description: this is here just to test FloatField
@@ -1570,6 +1684,22 @@ definitions:
format: decimal
default: 0.0
minimum: 0.0
SnippetViewer:
required:
- snippet
- viewer
type: object
properties:
id:
title: ID
type: integer
readOnly: true
snippet:
title: Snippet
type: integer
viewer:
title: Viewer
type: integer
Todo:
required:
- title
@@ -1593,6 +1723,17 @@ definitions:
minLength: 1
todo:
$ref: '#/definitions/Todo'
Harvest:
type: object
properties:
size_code:
title: Size code
type: string
enum:
- '50'
- '100'
- '200'
readOnly: true
TodoRecursive:
required:
- title
@@ -1617,6 +1758,7 @@ definitions:
required:
- title
- children
- many_children
type: object
properties:
id:
@@ -1632,6 +1774,10 @@ definitions:
type: array
items:
$ref: '#/definitions/TodoTree'
many_children:
type: array
items:
$ref: '#/definitions/TodoTree'
TodoYetAnother:
required:
- title
@@ -33,6 +33,10 @@ if typing:
# Following cases are not 100% correct, but it should work somehow and not crash.
(Union[int, float], None),
(List, {'type': openapi.TYPE_ARRAY, 'items': openapi.Items(openapi.TYPE_STRING)}),
('SomeType', None),
(type('SomeType', (object,), {}), None),
(None, None),
(6, None),
])
def test_get_basic_type_info_from_hint(hint_class, expected_swagger_type_info):
type_info = get_basic_type_info_from_hint(hint_class)
+2 -2
View File
@@ -5,7 +5,7 @@ from drf_yasg.openapi import ReferenceResolver
def test_basic():
scopes = ['s1', 's2']
rr = ReferenceResolver(*scopes)
rr = ReferenceResolver(*scopes, force_init=True)
assert scopes == rr.scopes == list(rr.keys()) == list(rr)
rr.set('o1', 1, scope='s1')
assert rr.has('o1', scope='s1')
@@ -25,7 +25,7 @@ def test_basic():
def test_scoped():
scopes = ['s1', 's2']
rr = ReferenceResolver(*scopes)
rr = ReferenceResolver(*scopes, force_init=True)
r1 = rr.with_scope('s1')
r2 = rr.with_scope('s2')
with pytest.raises(AssertionError):
+1 -1
View File
@@ -1,7 +1,7 @@
[tox]
minversion = 3.3.0
isolated_build = true
isolated_build_env=.package
isolated_build_env = .package
# https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django
envlist =