mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-07-06 21:33:34 +03:00
Feat: drop SchemaJSRenderer/DocumentationRenderer/CoreJSONRenderer/_BaseOpenAPIRenderer/CoreAPIOpenAPIRenderer/CoreAPIJSONOpenAPIRenderer class
This commit is contained in:
parent
f610a05cc4
commit
5873060ea9
|
@ -1,88 +0,0 @@
|
|||
from django.urls import include, path
|
||||
|
||||
from rest_framework.renderers import (
|
||||
CoreJSONRenderer, DocumentationRenderer, SchemaJSRenderer
|
||||
)
|
||||
from rest_framework.schemas import SchemaGenerator, get_schema_view
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
|
||||
def get_docs_view(
|
||||
title=None, description=None, schema_url=None, urlconf=None,
|
||||
public=True, patterns=None, generator_class=SchemaGenerator,
|
||||
authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES,
|
||||
permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES,
|
||||
renderer_classes=None):
|
||||
|
||||
if renderer_classes is None:
|
||||
renderer_classes = [DocumentationRenderer, CoreJSONRenderer]
|
||||
|
||||
return get_schema_view(
|
||||
title=title,
|
||||
url=schema_url,
|
||||
urlconf=urlconf,
|
||||
description=description,
|
||||
renderer_classes=renderer_classes,
|
||||
public=public,
|
||||
patterns=patterns,
|
||||
generator_class=generator_class,
|
||||
authentication_classes=authentication_classes,
|
||||
permission_classes=permission_classes,
|
||||
)
|
||||
|
||||
|
||||
def get_schemajs_view(
|
||||
title=None, description=None, schema_url=None, urlconf=None,
|
||||
public=True, patterns=None, generator_class=SchemaGenerator,
|
||||
authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES,
|
||||
permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES):
|
||||
renderer_classes = [SchemaJSRenderer]
|
||||
|
||||
return get_schema_view(
|
||||
title=title,
|
||||
url=schema_url,
|
||||
urlconf=urlconf,
|
||||
description=description,
|
||||
renderer_classes=renderer_classes,
|
||||
public=public,
|
||||
patterns=patterns,
|
||||
generator_class=generator_class,
|
||||
authentication_classes=authentication_classes,
|
||||
permission_classes=permission_classes,
|
||||
)
|
||||
|
||||
|
||||
def include_docs_urls(
|
||||
title=None, description=None, schema_url=None, urlconf=None,
|
||||
public=True, patterns=None, generator_class=SchemaGenerator,
|
||||
authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES,
|
||||
permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES,
|
||||
renderer_classes=None):
|
||||
docs_view = get_docs_view(
|
||||
title=title,
|
||||
description=description,
|
||||
schema_url=schema_url,
|
||||
urlconf=urlconf,
|
||||
public=public,
|
||||
patterns=patterns,
|
||||
generator_class=generator_class,
|
||||
authentication_classes=authentication_classes,
|
||||
renderer_classes=renderer_classes,
|
||||
permission_classes=permission_classes,
|
||||
)
|
||||
schema_js_view = get_schemajs_view(
|
||||
title=title,
|
||||
description=description,
|
||||
schema_url=schema_url,
|
||||
urlconf=urlconf,
|
||||
public=public,
|
||||
patterns=patterns,
|
||||
generator_class=generator_class,
|
||||
authentication_classes=authentication_classes,
|
||||
permission_classes=permission_classes,
|
||||
)
|
||||
urls = [
|
||||
path('', docs_view, name='docs-index'),
|
||||
path('schema.js', schema_js_view, name='schema-js')
|
||||
]
|
||||
return include((urls, 'api-docs'), namespace='api-docs')
|
|
@ -7,10 +7,8 @@ on the response, such as JSON encoded data or HTML output.
|
|||
REST framework also provides an HTML renderer that renders the browsable API.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import contextlib
|
||||
import datetime
|
||||
from urllib import parse
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
|
@ -18,14 +16,12 @@ from django.core.exceptions import ImproperlyConfigured
|
|||
from django.core.paginator import Page
|
||||
from django.template import engines, loader
|
||||
from django.urls import NoReverseMatch
|
||||
from django.utils.html import mark_safe
|
||||
from django.utils.http import parse_header_parameters
|
||||
from django.utils.safestring import SafeString
|
||||
|
||||
from rest_framework import VERSION, exceptions, serializers, status
|
||||
from rest_framework.compat import (
|
||||
INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema,
|
||||
pygments_css, yaml
|
||||
INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, pygments_css, yaml
|
||||
)
|
||||
from rest_framework.exceptions import ParseError
|
||||
from rest_framework.request import is_form_media_type, override_method
|
||||
|
@ -418,7 +414,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
|
||||
render_style = getattr(renderer, 'render_style', 'text')
|
||||
assert render_style in ['text', 'binary'], 'Expected .render_style ' \
|
||||
'"text" or "binary", but got "%s"' % render_style
|
||||
'"text" or "binary", but got "%s"' % render_style
|
||||
if render_style == 'binary':
|
||||
return '[%d bytes of binary content]' % len(content)
|
||||
|
||||
|
@ -487,8 +483,8 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
has_serializer_class = getattr(view, 'serializer_class', None)
|
||||
|
||||
if (
|
||||
(not has_serializer and not has_serializer_class) or
|
||||
not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)
|
||||
(not has_serializer and not has_serializer_class) or
|
||||
not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)
|
||||
):
|
||||
return
|
||||
|
||||
|
@ -837,7 +833,7 @@ class AdminRenderer(BrowsableAPIRenderer):
|
|||
and viewset-like (has `.basename` / `.reverse_action()`).
|
||||
"""
|
||||
if not hasattr(view, 'reverse_action') or \
|
||||
not hasattr(view, 'lookup_field'):
|
||||
not hasattr(view, 'lookup_field'):
|
||||
return
|
||||
|
||||
lookup_field = view.lookup_field
|
||||
|
@ -850,57 +846,6 @@ class AdminRenderer(BrowsableAPIRenderer):
|
|||
return
|
||||
|
||||
|
||||
class DocumentationRenderer(BaseRenderer):
|
||||
media_type = 'text/html'
|
||||
format = 'html'
|
||||
charset = 'utf-8'
|
||||
template = 'rest_framework/docs/index.html'
|
||||
error_template = 'rest_framework/docs/error.html'
|
||||
code_style = 'emacs'
|
||||
languages = ['shell', 'javascript', 'python']
|
||||
|
||||
def get_context(self, data, request):
|
||||
return {
|
||||
'document': data,
|
||||
'langs': self.languages,
|
||||
'lang_htmls': ["rest_framework/docs/langs/%s.html" % language for language in self.languages],
|
||||
'lang_intro_htmls': ["rest_framework/docs/langs/%s-intro.html" % language for language in self.languages],
|
||||
'code_style': pygments_css(self.code_style),
|
||||
'request': request
|
||||
}
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
if isinstance(data, coreapi.Document):
|
||||
template = loader.get_template(self.template)
|
||||
context = self.get_context(data, renderer_context['request'])
|
||||
return template.render(context, request=renderer_context['request'])
|
||||
else:
|
||||
template = loader.get_template(self.error_template)
|
||||
context = {
|
||||
"data": data,
|
||||
"request": renderer_context['request'],
|
||||
"response": renderer_context['response'],
|
||||
"debug": settings.DEBUG,
|
||||
}
|
||||
return template.render(context, request=renderer_context['request'])
|
||||
|
||||
|
||||
class SchemaJSRenderer(BaseRenderer):
|
||||
media_type = 'application/javascript'
|
||||
format = 'javascript'
|
||||
charset = 'utf-8'
|
||||
template = 'rest_framework/schema.js'
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
codec = coreapi.codecs.CoreJSONCodec()
|
||||
schema = base64.b64encode(codec.encode(data)).decode('ascii')
|
||||
|
||||
template = loader.get_template(self.template)
|
||||
context = {'schema': mark_safe(schema)}
|
||||
request = renderer_context['request']
|
||||
return template.render(context, request=request)
|
||||
|
||||
|
||||
class MultiPartRenderer(BaseRenderer):
|
||||
media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
|
||||
format = 'multipart'
|
||||
|
@ -921,139 +866,6 @@ class MultiPartRenderer(BaseRenderer):
|
|||
return encode_multipart(self.BOUNDARY, data)
|
||||
|
||||
|
||||
class CoreJSONRenderer(BaseRenderer):
|
||||
media_type = 'application/coreapi+json'
|
||||
charset = None
|
||||
format = 'corejson'
|
||||
|
||||
def __init__(self):
|
||||
assert coreapi, 'Using CoreJSONRenderer, but `coreapi` is not installed.'
|
||||
|
||||
def render(self, data, media_type=None, renderer_context=None):
|
||||
indent = bool(renderer_context.get('indent', 0))
|
||||
codec = coreapi.codecs.CoreJSONCodec()
|
||||
return codec.dump(data, indent=indent)
|
||||
|
||||
|
||||
class _BaseOpenAPIRenderer:
|
||||
def get_schema(self, instance):
|
||||
CLASS_TO_TYPENAME = {
|
||||
coreschema.Object: 'object',
|
||||
coreschema.Array: 'array',
|
||||
coreschema.Number: 'number',
|
||||
coreschema.Integer: 'integer',
|
||||
coreschema.String: 'string',
|
||||
coreschema.Boolean: 'boolean',
|
||||
}
|
||||
|
||||
schema = {}
|
||||
if instance.__class__ in CLASS_TO_TYPENAME:
|
||||
schema['type'] = CLASS_TO_TYPENAME[instance.__class__]
|
||||
schema['title'] = instance.title
|
||||
schema['description'] = instance.description
|
||||
if hasattr(instance, 'enum'):
|
||||
schema['enum'] = instance.enum
|
||||
return schema
|
||||
|
||||
def get_parameters(self, link):
|
||||
parameters = []
|
||||
for field in link.fields:
|
||||
if field.location not in ['path', 'query']:
|
||||
continue
|
||||
parameter = {
|
||||
'name': field.name,
|
||||
'in': field.location,
|
||||
}
|
||||
if field.required:
|
||||
parameter['required'] = True
|
||||
if field.description:
|
||||
parameter['description'] = field.description
|
||||
if field.schema:
|
||||
parameter['schema'] = self.get_schema(field.schema)
|
||||
parameters.append(parameter)
|
||||
return parameters
|
||||
|
||||
def get_operation(self, link, name, tag):
|
||||
operation_id = "%s_%s" % (tag, name) if tag else name
|
||||
parameters = self.get_parameters(link)
|
||||
|
||||
operation = {
|
||||
'operationId': operation_id,
|
||||
}
|
||||
if link.title:
|
||||
operation['summary'] = link.title
|
||||
if link.description:
|
||||
operation['description'] = link.description
|
||||
if parameters:
|
||||
operation['parameters'] = parameters
|
||||
if tag:
|
||||
operation['tags'] = [tag]
|
||||
return operation
|
||||
|
||||
def get_paths(self, document):
|
||||
paths = {}
|
||||
|
||||
tag = None
|
||||
for name, link in document.links.items():
|
||||
path = parse.urlparse(link.url).path
|
||||
method = link.action.lower()
|
||||
paths.setdefault(path, {})
|
||||
paths[path][method] = self.get_operation(link, name, tag=tag)
|
||||
|
||||
for tag, section in document.data.items():
|
||||
for name, link in section.links.items():
|
||||
path = parse.urlparse(link.url).path
|
||||
method = link.action.lower()
|
||||
paths.setdefault(path, {})
|
||||
paths[path][method] = self.get_operation(link, name, tag=tag)
|
||||
|
||||
return paths
|
||||
|
||||
def get_structure(self, data):
|
||||
return {
|
||||
'openapi': '3.0.0',
|
||||
'info': {
|
||||
'version': '',
|
||||
'title': data.title,
|
||||
'description': data.description
|
||||
},
|
||||
'servers': [{
|
||||
'url': data.url
|
||||
}],
|
||||
'paths': self.get_paths(data)
|
||||
}
|
||||
|
||||
|
||||
class CoreAPIOpenAPIRenderer(_BaseOpenAPIRenderer):
|
||||
media_type = 'application/vnd.oai.openapi'
|
||||
charset = None
|
||||
format = 'openapi'
|
||||
|
||||
def __init__(self):
|
||||
assert coreapi, 'Using CoreAPIOpenAPIRenderer, but `coreapi` is not installed.'
|
||||
assert yaml, 'Using CoreAPIOpenAPIRenderer, but `pyyaml` is not installed.'
|
||||
|
||||
def render(self, data, media_type=None, renderer_context=None):
|
||||
structure = self.get_structure(data)
|
||||
return yaml.dump(structure, default_flow_style=False).encode()
|
||||
|
||||
|
||||
class CoreAPIJSONOpenAPIRenderer(_BaseOpenAPIRenderer):
|
||||
media_type = 'application/vnd.oai.openapi+json'
|
||||
charset = None
|
||||
format = 'openapi-json'
|
||||
ensure_ascii = not api_settings.UNICODE_JSON
|
||||
|
||||
def __init__(self):
|
||||
assert coreapi, 'Using CoreAPIJSONOpenAPIRenderer, but `coreapi` is not installed.'
|
||||
|
||||
def render(self, data, media_type=None, renderer_context=None):
|
||||
structure = self.get_structure(data)
|
||||
return json.dumps(
|
||||
structure, indent=4,
|
||||
ensure_ascii=self.ensure_ascii).encode('utf-8')
|
||||
|
||||
|
||||
class OpenAPIRenderer(BaseRenderer):
|
||||
media_type = 'application/vnd.oai.openapi'
|
||||
charset = None
|
||||
|
@ -1067,6 +879,7 @@ class OpenAPIRenderer(BaseRenderer):
|
|||
class Dumper(yaml.Dumper):
|
||||
def ignore_aliases(self, data):
|
||||
return True
|
||||
|
||||
Dumper.add_representer(SafeString, Dumper.represent_str)
|
||||
Dumper.add_representer(datetime.timedelta, encoders.CustomScalar.represent_timedelta)
|
||||
return yaml.dump(data, default_flow_style=False, sort_keys=False, Dumper=Dumper).encode('utf-8')
|
||||
|
|
|
@ -5,18 +5,16 @@ import pytest
|
|||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.http.request import HttpRequest
|
||||
from django.template import loader
|
||||
from django.test import TestCase, override_settings
|
||||
from django.urls import include, path, re_path
|
||||
from django.utils.safestring import SafeText
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework import permissions, serializers, status
|
||||
from rest_framework.compat import coreapi
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.renderers import (
|
||||
AdminRenderer, BaseRenderer, BrowsableAPIRenderer, DocumentationRenderer,
|
||||
HTMLFormRenderer, JSONRenderer, SchemaJSRenderer, StaticHTMLRenderer
|
||||
AdminRenderer, BaseRenderer, BrowsableAPIRenderer, HTMLFormRenderer,
|
||||
JSONRenderer, StaticHTMLRenderer
|
||||
)
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
@ -871,61 +869,3 @@ class AdminRendererTests(TestCase):
|
|||
self.assertEqual(results[1]['url'], '/example')
|
||||
self.assertEqual(results[2]['url'], None)
|
||||
self.assertNotIn('url', results[3])
|
||||
|
||||
|
||||
@pytest.mark.skipif(not coreapi, reason='coreapi is not installed')
|
||||
class TestDocumentationRenderer(TestCase):
|
||||
|
||||
def test_document_with_link_named_data(self):
|
||||
"""
|
||||
Ref #5395: Doc's `document.data` would fail with a Link named "data".
|
||||
As per #4972, use templatetag instead.
|
||||
"""
|
||||
document = coreapi.Document(
|
||||
title='Data Endpoint API',
|
||||
url='https://api.example.org/',
|
||||
content={
|
||||
'data': coreapi.Link(
|
||||
url='/data/',
|
||||
action='get',
|
||||
fields=[],
|
||||
description='Return data.'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
factory = APIRequestFactory()
|
||||
request = factory.get('/')
|
||||
|
||||
renderer = DocumentationRenderer()
|
||||
|
||||
html = renderer.render(document, accepted_media_type="text/html", renderer_context={"request": request})
|
||||
assert '<h1>Data Endpoint API</h1>' in html
|
||||
|
||||
def test_shell_code_example_rendering(self):
|
||||
template = loader.get_template('rest_framework/docs/langs/shell.html')
|
||||
context = {
|
||||
'document': coreapi.Document(url='https://api.example.org/'),
|
||||
'link_key': 'testcases > list',
|
||||
'link': coreapi.Link(url='/data/', action='get', fields=[]),
|
||||
}
|
||||
html = template.render(context)
|
||||
assert 'testcases<span class="w"> </span>list' in html
|
||||
|
||||
|
||||
@pytest.mark.skipif(not coreapi, reason='coreapi is not installed')
|
||||
class TestSchemaJSRenderer(TestCase):
|
||||
|
||||
def test_schemajs_output(self):
|
||||
"""
|
||||
Test output of the SchemaJS renderer as per #5608. Django 2.0 on Py3 prints binary data as b'xyz' in templates,
|
||||
and the base64 encoding used by SchemaJSRenderer outputs base64 as binary. Test fix.
|
||||
"""
|
||||
factory = APIRequestFactory()
|
||||
request = factory.get('/')
|
||||
|
||||
renderer = SchemaJSRenderer()
|
||||
|
||||
output = renderer.render('data', renderer_context={"request": request})
|
||||
assert "'ImRhdGEi'" in output
|
||||
assert "'b'ImRhdGEi''" not in output
|
||||
|
|
Loading…
Reference in New Issue
Block a user