Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 548489a539 | |||
| 887b53300a | |||
| f692fe7c98 | |||
| 017ae3d240 | |||
| b57413023b | |||
| 4014c69689 | |||
| 7bb4700003 | |||
| 86c1675c58 | |||
| 81f0b1a2ea | |||
| 298a9745df | |||
| 652a33a54d | |||
| 340a60324c | |||
| f348084d85 | |||
| 904c43a167 | |||
| 4c78a683f4 | |||
| 4da09830ac | |||
| 34ed1e20a2 | |||
| 62d97a80bc | |||
| e108ddbb48 | |||
| 75a5d866be | |||
| f189426901 | |||
| b4900ebd6a | |||
| c593b3fcfb | |||
| 9caeed781e | |||
| 3377ef08ea | |||
| 5c2c39c82d | |||
| e538e0713a | |||
| 76c8fe0646 | |||
| 3d43ee6748 | |||
| 583e404ed8 | |||
| d62243599b | |||
| 6df3523675 |
+5
-10
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Generated
+793
-81
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
+33
-32
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
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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):
|
||||
|
||||
@@ -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},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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__'
|
||||
|
||||
@@ -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()),
|
||||
]
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user