diff --git a/rest_framework/documentation.py b/rest_framework/documentation.py deleted file mode 100644 index 53e5ab551..000000000 --- a/rest_framework/documentation.py +++ /dev/null @@ -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') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index b81f9ab46..74677dbd8 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -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') diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 1b396575d..6b51ff34c 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -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 '

Data Endpoint API

' 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 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