12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076 |
- """
- Renderers are used to serialize a response into specific media types.
- They give us a generic way of being able to handle various media types
- 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
- from collections import OrderedDict
- from urllib import parse
- from django import forms
- from django.conf import settings
- from django.core.exceptions import ImproperlyConfigured
- from django.core.paginator import Page
- from django.http.multipartparser import parse_header
- from django.template import engines, loader
- from django.urls import NoReverseMatch
- from django.utils.html import mark_safe
- from rest_framework import VERSION, exceptions, serializers, status
- from rest_framework.compat import (
- INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema,
- pygments_css, yaml
- )
- from rest_framework.exceptions import ParseError
- from rest_framework.request import is_form_media_type, override_method
- from rest_framework.settings import api_settings
- from rest_framework.utils import encoders, json
- from rest_framework.utils.breadcrumbs import get_breadcrumbs
- from rest_framework.utils.field_mapping import ClassLookupDict
- def zero_as_none(value):
- return None if value == 0 else value
- class BaseRenderer:
- """
- All renderers should extend this class, setting the `media_type`
- and `format` attributes, and override the `.render()` method.
- """
- media_type = None
- format = None
- charset = 'utf-8'
- render_style = 'text'
- def render(self, data, accepted_media_type=None, renderer_context=None):
- raise NotImplementedError('Renderer class requires .render() to be implemented')
- class JSONRenderer(BaseRenderer):
- """
- Renderer which serializes to JSON.
- """
- media_type = 'application/json'
- format = 'json'
- encoder_class = encoders.JSONEncoder
- ensure_ascii = not api_settings.UNICODE_JSON
- compact = api_settings.COMPACT_JSON
- strict = api_settings.STRICT_JSON
- # We don't set a charset because JSON is a binary encoding,
- # that can be encoded as utf-8, utf-16 or utf-32.
- # See: https://www.ietf.org/rfc/rfc4627.txt
- # Also: http://lucumr.pocoo.org/2013/7/19/application-mimetypes-and-encodings/
- charset = None
- def get_indent(self, accepted_media_type, renderer_context):
- if accepted_media_type:
- # If the media type looks like 'application/json; indent=4',
- # then pretty print the result.
- # Note that we coerce `indent=0` into `indent=None`.
- base_media_type, params = parse_header(accepted_media_type.encode('ascii'))
- try:
- return zero_as_none(max(min(int(params['indent']), 8), 0))
- except (KeyError, ValueError, TypeError):
- pass
- # If 'indent' is provided in the context, then pretty print the result.
- # E.g. If we're being called by the BrowsableAPIRenderer.
- return renderer_context.get('indent', None)
- def render(self, data, accepted_media_type=None, renderer_context=None):
- """
- Render `data` into JSON, returning a bytestring.
- """
- if data is None:
- return b''
- renderer_context = renderer_context or {}
- indent = self.get_indent(accepted_media_type, renderer_context)
- if indent is None:
- separators = SHORT_SEPARATORS if self.compact else LONG_SEPARATORS
- else:
- separators = INDENT_SEPARATORS
- ret = json.dumps(
- data, cls=self.encoder_class,
- indent=indent, ensure_ascii=self.ensure_ascii,
- allow_nan=not self.strict, separators=separators
- )
- # We always fully escape \u2028 and \u2029 to ensure we output JSON
- # that is a strict javascript subset.
- # See: http://timelessrepo.com/json-isnt-a-javascript-subset
- ret = ret.replace('\u2028', '\\u2028').replace('\u2029', '\\u2029')
- return ret.encode()
- class TemplateHTMLRenderer(BaseRenderer):
- """
- An HTML renderer for use with templates.
- The data supplied to the Response object should be a dictionary that will
- be used as context for the template.
- The template name is determined by (in order of preference):
- 1. An explicit `.template_name` attribute set on the response.
- 2. An explicit `.template_name` attribute set on this class.
- 3. The return result of calling `view.get_template_names()`.
- For example:
- data = {'users': User.objects.all()}
- return Response(data, template_name='users.html')
- For pre-rendered HTML, see StaticHTMLRenderer.
- """
- media_type = 'text/html'
- format = 'html'
- template_name = None
- exception_template_names = [
- '%(status_code)s.html',
- 'api_exception.html'
- ]
- charset = 'utf-8'
- def render(self, data, accepted_media_type=None, renderer_context=None):
- """
- Renders data to HTML, using Django's standard template rendering.
- The template name is determined by (in order of preference):
- 1. An explicit .template_name set on the response.
- 2. An explicit .template_name set on this class.
- 3. The return result of calling view.get_template_names().
- """
- renderer_context = renderer_context or {}
- view = renderer_context['view']
- request = renderer_context['request']
- response = renderer_context['response']
- if response.exception:
- template = self.get_exception_template(response)
- else:
- template_names = self.get_template_names(response, view)
- template = self.resolve_template(template_names)
- if hasattr(self, 'resolve_context'):
- # Fallback for older versions.
- context = self.resolve_context(data, request, response)
- else:
- context = self.get_template_context(data, renderer_context)
- return template.render(context, request=request)
- def resolve_template(self, template_names):
- return loader.select_template(template_names)
- def get_template_context(self, data, renderer_context):
- response = renderer_context['response']
- if response.exception:
- data['status_code'] = response.status_code
- return data
- def get_template_names(self, response, view):
- if response.template_name:
- return [response.template_name]
- elif self.template_name:
- return [self.template_name]
- elif hasattr(view, 'get_template_names'):
- return view.get_template_names()
- elif hasattr(view, 'template_name'):
- return [view.template_name]
- raise ImproperlyConfigured(
- 'Returned a template response with no `template_name` attribute set on either the view or response'
- )
- def get_exception_template(self, response):
- template_names = [name % {'status_code': response.status_code}
- for name in self.exception_template_names]
- try:
- # Try to find an appropriate error template
- return self.resolve_template(template_names)
- except Exception:
- # Fall back to using eg '404 Not Found'
- body = '%d %s' % (response.status_code, response.status_text.title())
- template = engines['django'].from_string(body)
- return template
- # Note, subclass TemplateHTMLRenderer simply for the exception behavior
- class StaticHTMLRenderer(TemplateHTMLRenderer):
- """
- An HTML renderer class that simply returns pre-rendered HTML.
- The data supplied to the Response object should be a string representing
- the pre-rendered HTML content.
- For example:
- data = '<html><body>example</body></html>'
- return Response(data)
- For template rendered HTML, see TemplateHTMLRenderer.
- """
- media_type = 'text/html'
- format = 'html'
- charset = 'utf-8'
- def render(self, data, accepted_media_type=None, renderer_context=None):
- renderer_context = renderer_context or {}
- response = renderer_context.get('response')
- if response and response.exception:
- request = renderer_context['request']
- template = self.get_exception_template(response)
- if hasattr(self, 'resolve_context'):
- context = self.resolve_context(data, request, response)
- else:
- context = self.get_template_context(data, renderer_context)
- return template.render(context, request=request)
- return data
- class HTMLFormRenderer(BaseRenderer):
- """
- Renderers serializer data into an HTML form.
- If the serializer was instantiated without an object then this will
- return an HTML form not bound to any object,
- otherwise it will return an HTML form with the appropriate initial data
- populated from the object.
- Note that rendering of field and form errors is not currently supported.
- """
- media_type = 'text/html'
- format = 'form'
- charset = 'utf-8'
- template_pack = 'rest_framework/vertical/'
- base_template = 'form.html'
- default_style = ClassLookupDict({
- serializers.Field: {
- 'base_template': 'input.html',
- 'input_type': 'text'
- },
- serializers.EmailField: {
- 'base_template': 'input.html',
- 'input_type': 'email'
- },
- serializers.URLField: {
- 'base_template': 'input.html',
- 'input_type': 'url'
- },
- serializers.IntegerField: {
- 'base_template': 'input.html',
- 'input_type': 'number'
- },
- serializers.FloatField: {
- 'base_template': 'input.html',
- 'input_type': 'number'
- },
- serializers.DateTimeField: {
- 'base_template': 'input.html',
- 'input_type': 'datetime-local'
- },
- serializers.DateField: {
- 'base_template': 'input.html',
- 'input_type': 'date'
- },
- serializers.TimeField: {
- 'base_template': 'input.html',
- 'input_type': 'time'
- },
- serializers.FileField: {
- 'base_template': 'input.html',
- 'input_type': 'file'
- },
- serializers.BooleanField: {
- 'base_template': 'checkbox.html'
- },
- serializers.ChoiceField: {
- 'base_template': 'select.html', # Also valid: 'radio.html'
- },
- serializers.MultipleChoiceField: {
- 'base_template': 'select_multiple.html', # Also valid: 'checkbox_multiple.html'
- },
- serializers.RelatedField: {
- 'base_template': 'select.html', # Also valid: 'radio.html'
- },
- serializers.ManyRelatedField: {
- 'base_template': 'select_multiple.html', # Also valid: 'checkbox_multiple.html'
- },
- serializers.Serializer: {
- 'base_template': 'fieldset.html'
- },
- serializers.ListSerializer: {
- 'base_template': 'list_fieldset.html'
- },
- serializers.ListField: {
- 'base_template': 'list_field.html'
- },
- serializers.DictField: {
- 'base_template': 'dict_field.html'
- },
- serializers.FilePathField: {
- 'base_template': 'select.html',
- },
- serializers.JSONField: {
- 'base_template': 'textarea.html',
- },
- })
- def render_field(self, field, parent_style):
- if isinstance(field._field, serializers.HiddenField):
- return ''
- style = self.default_style[field].copy()
- style.update(field.style)
- if 'template_pack' not in style:
- style['template_pack'] = parent_style.get('template_pack', self.template_pack)
- style['renderer'] = self
- # Get a clone of the field with text-only value representation.
- field = field.as_form_field()
- if style.get('input_type') == 'datetime-local' and isinstance(field.value, str):
- field.value = field.value.rstrip('Z')
- if 'template' in style:
- template_name = style['template']
- else:
- template_name = style['template_pack'].strip('/') + '/' + style['base_template']
- template = loader.get_template(template_name)
- context = {'field': field, 'style': style}
- return template.render(context)
- def render(self, data, accepted_media_type=None, renderer_context=None):
- """
- Render serializer data and return an HTML form, as a string.
- """
- renderer_context = renderer_context or {}
- form = data.serializer
- style = renderer_context.get('style', {})
- if 'template_pack' not in style:
- style['template_pack'] = self.template_pack
- style['renderer'] = self
- template_pack = style['template_pack'].strip('/')
- template_name = template_pack + '/' + self.base_template
- template = loader.get_template(template_name)
- context = {
- 'form': form,
- 'style': style
- }
- return template.render(context)
- class BrowsableAPIRenderer(BaseRenderer):
- """
- HTML renderer used to self-document the API.
- """
- media_type = 'text/html'
- format = 'api'
- template = 'rest_framework/api.html'
- filter_template = 'rest_framework/filters/base.html'
- code_style = 'emacs'
- charset = 'utf-8'
- form_renderer_class = HTMLFormRenderer
- def get_default_renderer(self, view):
- """
- Return an instance of the first valid renderer.
- (Don't use another documenting renderer.)
- """
- renderers = [renderer for renderer in view.renderer_classes
- if not issubclass(renderer, BrowsableAPIRenderer)]
- non_template_renderers = [renderer for renderer in renderers
- if not hasattr(renderer, 'get_template_names')]
- if not renderers:
- return None
- elif non_template_renderers:
- return non_template_renderers[0]()
- return renderers[0]()
- def get_content(self, renderer, data,
- accepted_media_type, renderer_context):
- """
- Get the content as if it had been rendered by the default
- non-documenting renderer.
- """
- if not renderer:
- return '[No renderers were found]'
- renderer_context['indent'] = 4
- content = renderer.render(data, accepted_media_type, renderer_context)
- render_style = getattr(renderer, 'render_style', 'text')
- assert render_style in ['text', 'binary'], 'Expected .render_style ' \
- '"text" or "binary", but got "%s"' % render_style
- if render_style == 'binary':
- return '[%d bytes of binary content]' % len(content)
- return content.decode('utf-8') if isinstance(content, bytes) else content
- def show_form_for_method(self, view, method, request, obj):
- """
- Returns True if a form should be shown for this method.
- """
- if method not in view.allowed_methods:
- return # Not a valid method
- try:
- view.check_permissions(request)
- if obj is not None:
- view.check_object_permissions(request, obj)
- except exceptions.APIException:
- return False # Doesn't have permissions
- return True
- def _get_serializer(self, serializer_class, view_instance, request, *args, **kwargs):
- kwargs['context'] = {
- 'request': request,
- 'format': self.format,
- 'view': view_instance
- }
- return serializer_class(*args, **kwargs)
- def get_rendered_html_form(self, data, view, method, request):
- """
- Return a string representing a rendered HTML form, possibly bound to
- either the input or output data.
- In the absence of the View having an associated form then return None.
- """
- # See issue #2089 for refactoring this.
- serializer = getattr(data, 'serializer', None)
- if serializer and not getattr(serializer, 'many', False):
- instance = getattr(serializer, 'instance', None)
- if isinstance(instance, Page):
- instance = None
- else:
- instance = None
- # If this is valid serializer data, and the form is for the same
- # HTTP method as was used in the request then use the existing
- # serializer instance, rather than dynamically creating a new one.
- if request.method == method and serializer is not None:
- try:
- kwargs = {'data': request.data}
- except ParseError:
- kwargs = {}
- existing_serializer = serializer
- else:
- kwargs = {}
- existing_serializer = None
- with override_method(view, request, method) as request:
- if not self.show_form_for_method(view, method, request, instance):
- return
- if method in ('DELETE', 'OPTIONS'):
- return True # Don't actually need to return a form
- has_serializer = getattr(view, 'get_serializer', None)
- 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)
- ):
- return
- if existing_serializer is not None:
- try:
- return self.render_form_for_serializer(existing_serializer)
- except TypeError:
- pass
- if has_serializer:
- if method in ('PUT', 'PATCH'):
- serializer = view.get_serializer(instance=instance, **kwargs)
- else:
- serializer = view.get_serializer(**kwargs)
- else:
- # at this point we must have a serializer_class
- if method in ('PUT', 'PATCH'):
- serializer = self._get_serializer(view.serializer_class, view,
- request, instance=instance, **kwargs)
- else:
- serializer = self._get_serializer(view.serializer_class, view,
- request, **kwargs)
- return self.render_form_for_serializer(serializer)
- def render_form_for_serializer(self, serializer):
- if hasattr(serializer, 'initial_data'):
- serializer.is_valid()
- form_renderer = self.form_renderer_class()
- return form_renderer.render(
- serializer.data,
- self.accepted_media_type,
- {'style': {'template_pack': 'rest_framework/horizontal'}}
- )
- def get_raw_data_form(self, data, view, method, request):
- """
- Returns a form that allows for arbitrary content types to be tunneled
- via standard HTML forms.
- (Which are typically application/x-www-form-urlencoded)
- """
- # See issue #2089 for refactoring this.
- serializer = getattr(data, 'serializer', None)
- if serializer and not getattr(serializer, 'many', False):
- instance = getattr(serializer, 'instance', None)
- if isinstance(instance, Page):
- instance = None
- else:
- instance = None
- with override_method(view, request, method) as request:
- # Check permissions
- if not self.show_form_for_method(view, method, request, instance):
- return
- # If possible, serialize the initial content for the generic form
- default_parser = view.parser_classes[0]
- renderer_class = getattr(default_parser, 'renderer_class', None)
- if hasattr(view, 'get_serializer') and renderer_class:
- # View has a serializer defined and parser class has a
- # corresponding renderer that can be used to render the data.
- if method in ('PUT', 'PATCH'):
- serializer = view.get_serializer(instance=instance)
- else:
- serializer = view.get_serializer()
- # Render the raw data content
- renderer = renderer_class()
- accepted = self.accepted_media_type
- context = self.renderer_context.copy()
- context['indent'] = 4
- # strip HiddenField from output
- data = serializer.data.copy()
- for name, field in serializer.fields.items():
- if isinstance(field, serializers.HiddenField):
- data.pop(name, None)
- content = renderer.render(data, accepted, context)
- # Renders returns bytes, but CharField expects a str.
- content = content.decode()
- else:
- content = None
- # Generate a generic form that includes a content type field,
- # and a content field.
- media_types = [parser.media_type for parser in view.parser_classes]
- choices = [(media_type, media_type) for media_type in media_types]
- initial = media_types[0]
- class GenericContentForm(forms.Form):
- _content_type = forms.ChoiceField(
- label='Media type',
- choices=choices,
- initial=initial,
- widget=forms.Select(attrs={'data-override': 'content-type'})
- )
- _content = forms.CharField(
- label='Content',
- widget=forms.Textarea(attrs={'data-override': 'content'}),
- initial=content,
- required=False
- )
- return GenericContentForm()
- def get_name(self, view):
- return view.get_view_name()
- def get_description(self, view, status_code):
- if status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN):
- return ''
- return view.get_view_description(html=True)
- def get_breadcrumbs(self, request):
- return get_breadcrumbs(request.path, request)
- def get_extra_actions(self, view, status_code):
- if (status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)):
- return None
- elif not hasattr(view, 'get_extra_action_url_map'):
- return None
- return view.get_extra_action_url_map()
- def get_filter_form(self, data, view, request):
- if not hasattr(view, 'get_queryset') or not hasattr(view, 'filter_backends'):
- return
- # Infer if this is a list view or not.
- paginator = getattr(view, 'paginator', None)
- if isinstance(data, list):
- pass
- elif paginator is not None and data is not None:
- try:
- paginator.get_results(data)
- except (TypeError, KeyError):
- return
- elif not isinstance(data, list):
- return
- queryset = view.get_queryset()
- elements = []
- for backend in view.filter_backends:
- if hasattr(backend, 'to_html'):
- html = backend().to_html(request, queryset, view)
- if html:
- elements.append(html)
- if not elements:
- return
- template = loader.get_template(self.filter_template)
- context = {'elements': elements}
- return template.render(context)
- def get_context(self, data, accepted_media_type, renderer_context):
- """
- Returns the context used to render.
- """
- view = renderer_context['view']
- request = renderer_context['request']
- response = renderer_context['response']
- renderer = self.get_default_renderer(view)
- raw_data_post_form = self.get_raw_data_form(data, view, 'POST', request)
- raw_data_put_form = self.get_raw_data_form(data, view, 'PUT', request)
- raw_data_patch_form = self.get_raw_data_form(data, view, 'PATCH', request)
- raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form
- response_headers = OrderedDict(sorted(response.items()))
- renderer_content_type = ''
- if renderer:
- renderer_content_type = '%s' % renderer.media_type
- if renderer.charset:
- renderer_content_type += ' ;%s' % renderer.charset
- response_headers['Content-Type'] = renderer_content_type
- if getattr(view, 'paginator', None) and view.paginator.display_page_controls:
- paginator = view.paginator
- else:
- paginator = None
- csrf_cookie_name = settings.CSRF_COOKIE_NAME
- csrf_header_name = settings.CSRF_HEADER_NAME
- if csrf_header_name.startswith('HTTP_'):
- csrf_header_name = csrf_header_name[5:]
- csrf_header_name = csrf_header_name.replace('_', '-')
- return {
- 'content': self.get_content(renderer, data, accepted_media_type, renderer_context),
- 'code_style': pygments_css(self.code_style),
- 'view': view,
- 'request': request,
- 'response': response,
- 'user': request.user,
- 'description': self.get_description(view, response.status_code),
- 'name': self.get_name(view),
- 'version': VERSION,
- 'paginator': paginator,
- 'breadcrumblist': self.get_breadcrumbs(request),
- 'allowed_methods': view.allowed_methods,
- 'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes],
- 'response_headers': response_headers,
- 'put_form': self.get_rendered_html_form(data, view, 'PUT', request),
- 'post_form': self.get_rendered_html_form(data, view, 'POST', request),
- 'delete_form': self.get_rendered_html_form(data, view, 'DELETE', request),
- 'options_form': self.get_rendered_html_form(data, view, 'OPTIONS', request),
- 'extra_actions': self.get_extra_actions(view, response.status_code),
- 'filter_form': self.get_filter_form(data, view, request),
- 'raw_data_put_form': raw_data_put_form,
- 'raw_data_post_form': raw_data_post_form,
- 'raw_data_patch_form': raw_data_patch_form,
- 'raw_data_put_or_patch_form': raw_data_put_or_patch_form,
- 'display_edit_forms': bool(response.status_code != 403),
- 'api_settings': api_settings,
- 'csrf_cookie_name': csrf_cookie_name,
- 'csrf_header_name': csrf_header_name
- }
- def render(self, data, accepted_media_type=None, renderer_context=None):
- """
- Render the HTML for the browsable API representation.
- """
- self.accepted_media_type = accepted_media_type or ''
- self.renderer_context = renderer_context or {}
- template = loader.get_template(self.template)
- context = self.get_context(data, accepted_media_type, renderer_context)
- ret = template.render(context, request=renderer_context['request'])
- # Munge DELETE Response code to allow us to return content
- # (Do this *after* we've rendered the template so that we include
- # the normal deletion response code in the output)
- response = renderer_context['response']
- if response.status_code == status.HTTP_204_NO_CONTENT:
- response.status_code = status.HTTP_200_OK
- return ret
- class AdminRenderer(BrowsableAPIRenderer):
- template = 'rest_framework/admin.html'
- format = 'admin'
- def render(self, data, accepted_media_type=None, renderer_context=None):
- self.accepted_media_type = accepted_media_type or ''
- self.renderer_context = renderer_context or {}
- response = renderer_context['response']
- request = renderer_context['request']
- view = self.renderer_context['view']
- if response.status_code == status.HTTP_400_BAD_REQUEST:
- # Errors still need to display the list or detail information.
- # The only way we can get at that is to simulate a GET request.
- self.error_form = self.get_rendered_html_form(data, view, request.method, request)
- self.error_title = {'POST': 'Create', 'PUT': 'Edit'}.get(request.method, 'Errors')
- with override_method(view, request, 'GET') as request:
- response = view.get(request, *view.args, **view.kwargs)
- data = response.data
- template = loader.get_template(self.template)
- context = self.get_context(data, accepted_media_type, renderer_context)
- ret = template.render(context, request=renderer_context['request'])
- # Creation and deletion should use redirects in the admin style.
- if response.status_code == status.HTTP_201_CREATED and 'Location' in response:
- response.status_code = status.HTTP_303_SEE_OTHER
- response['Location'] = request.build_absolute_uri()
- ret = ''
- if response.status_code == status.HTTP_204_NO_CONTENT:
- response.status_code = status.HTTP_303_SEE_OTHER
- try:
- # Attempt to get the parent breadcrumb URL.
- response['Location'] = self.get_breadcrumbs(request)[-2][1]
- except KeyError:
- # Otherwise reload current URL to get a 'Not Found' page.
- response['Location'] = request.full_path
- ret = ''
- return ret
- def get_context(self, data, accepted_media_type, renderer_context):
- """
- Render the HTML for the browsable API representation.
- """
- context = super().get_context(
- data, accepted_media_type, renderer_context
- )
- paginator = getattr(context['view'], 'paginator', None)
- if paginator is not None and data is not None:
- try:
- results = paginator.get_results(data)
- except (TypeError, KeyError):
- results = data
- else:
- results = data
- if results is None:
- header = {}
- style = 'detail'
- elif isinstance(results, list):
- header = results[0] if results else {}
- style = 'list'
- else:
- header = results
- style = 'detail'
- columns = [key for key in header if key != 'url']
- details = [key for key in header if key != 'url']
- if isinstance(results, list) and 'view' in renderer_context:
- for result in results:
- url = self.get_result_url(result, context['view'])
- if url is not None:
- result.setdefault('url', url)
- context['style'] = style
- context['columns'] = columns
- context['details'] = details
- context['results'] = results
- context['error_form'] = getattr(self, 'error_form', None)
- context['error_title'] = getattr(self, 'error_title', None)
- return context
- def get_result_url(self, result, view):
- """
- Attempt to reverse the result's detail view URL.
- This only works with views that are generic-like (has `.lookup_field`)
- and viewset-like (has `.basename` / `.reverse_action()`).
- """
- if not hasattr(view, 'reverse_action') or \
- not hasattr(view, 'lookup_field'):
- return
- lookup_field = view.lookup_field
- lookup_url_kwarg = getattr(view, 'lookup_url_kwarg', None) or lookup_field
- try:
- kwargs = {lookup_url_kwarg: result[lookup_field]}
- return view.reverse_action('detail', kwargs=kwargs)
- except (KeyError, NoReverseMatch):
- 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'
- charset = 'utf-8'
- BOUNDARY = 'BoUnDaRyStRiNg'
- def render(self, data, accepted_media_type=None, renderer_context=None):
- from django.test.client import encode_multipart
- if hasattr(data, 'items'):
- for key, value in data.items():
- assert not isinstance(value, dict), (
- "Test data contained a dictionary value for key '%s', "
- "but multipart uploads do not support nested data. "
- "You may want to consider using format='json' in this "
- "test case." % key
- )
- 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
- format = 'openapi'
- def __init__(self):
- assert yaml, 'Using OpenAPIRenderer, but `pyyaml` is not installed.'
- def render(self, data, media_type=None, renderer_context=None):
- # disable yaml advanced feature 'alias' for clean, portable, and readable output
- class Dumper(yaml.Dumper):
- def ignore_aliases(self, data):
- return True
- return yaml.dump(data, default_flow_style=False, sort_keys=False, Dumper=Dumper).encode('utf-8')
- class JSONOpenAPIRenderer(BaseRenderer):
- media_type = 'application/vnd.oai.openapi+json'
- charset = None
- encoder_class = encoders.JSONEncoder
- format = 'openapi-json'
- ensure_ascii = not api_settings.UNICODE_JSON
- def render(self, data, media_type=None, renderer_context=None):
- return json.dumps(
- data, cls=self.encoder_class, indent=2,
- ensure_ascii=self.ensure_ascii).encode('utf-8')
|