Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/rest_framework/renderers.py: 27%
555 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1"""
2Renderers are used to serialize a response into specific media types.
4They give us a generic way of being able to handle various media types
5on the response, such as JSON encoded data or HTML output.
7REST framework also provides an HTML renderer that renders the browsable API.
8"""
9import base64
10from collections import OrderedDict
11from urllib import parse
13from django import forms
14from django.conf import settings
15from django.core.exceptions import ImproperlyConfigured
16from django.core.paginator import Page
17from django.http.multipartparser import parse_header
18from django.template import engines, loader
19from django.urls import NoReverseMatch
20from django.utils.html import mark_safe
22from rest_framework import VERSION, exceptions, serializers, status
23from rest_framework.compat import (
24 INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, coreschema,
25 pygments_css, yaml
26)
27from rest_framework.exceptions import ParseError
28from rest_framework.request import is_form_media_type, override_method
29from rest_framework.settings import api_settings
30from rest_framework.utils import encoders, json
31from rest_framework.utils.breadcrumbs import get_breadcrumbs
32from rest_framework.utils.field_mapping import ClassLookupDict
35def zero_as_none(value):
36 return None if value == 0 else value
39class BaseRenderer:
40 """
41 All renderers should extend this class, setting the `media_type`
42 and `format` attributes, and override the `.render()` method.
43 """
44 media_type = None
45 format = None
46 charset = 'utf-8'
47 render_style = 'text'
49 def render(self, data, accepted_media_type=None, renderer_context=None):
50 raise NotImplementedError('Renderer class requires .render() to be implemented')
53class JSONRenderer(BaseRenderer):
54 """
55 Renderer which serializes to JSON.
56 """
57 media_type = 'application/json'
58 format = 'json'
59 encoder_class = encoders.JSONEncoder
60 ensure_ascii = not api_settings.UNICODE_JSON
61 compact = api_settings.COMPACT_JSON
62 strict = api_settings.STRICT_JSON
64 # We don't set a charset because JSON is a binary encoding,
65 # that can be encoded as utf-8, utf-16 or utf-32.
66 # See: https://www.ietf.org/rfc/rfc4627.txt
67 # Also: http://lucumr.pocoo.org/2013/7/19/application-mimetypes-and-encodings/
68 charset = None
70 def get_indent(self, accepted_media_type, renderer_context):
71 if accepted_media_type: 71 ↛ 83line 71 didn't jump to line 83, because the condition on line 71 was never false
72 # If the media type looks like 'application/json; indent=4',
73 # then pretty print the result.
74 # Note that we coerce `indent=0` into `indent=None`.
75 base_media_type, params = parse_header(accepted_media_type.encode('ascii'))
76 try:
77 return zero_as_none(max(min(int(params['indent']), 8), 0))
78 except (KeyError, ValueError, TypeError):
79 pass
81 # If 'indent' is provided in the context, then pretty print the result.
82 # E.g. If we're being called by the BrowsableAPIRenderer.
83 return renderer_context.get('indent', None)
85 def render(self, data, accepted_media_type=None, renderer_context=None):
86 """
87 Render `data` into JSON, returning a bytestring.
88 """
89 if data is None:
90 return b''
92 renderer_context = renderer_context or {}
93 indent = self.get_indent(accepted_media_type, renderer_context)
95 if indent is None: 95 ↛ 98line 95 didn't jump to line 98, because the condition on line 95 was never false
96 separators = SHORT_SEPARATORS if self.compact else LONG_SEPARATORS
97 else:
98 separators = INDENT_SEPARATORS
100 ret = json.dumps(
101 data, cls=self.encoder_class,
102 indent=indent, ensure_ascii=self.ensure_ascii,
103 allow_nan=not self.strict, separators=separators
104 )
106 # We always fully escape \u2028 and \u2029 to ensure we output JSON
107 # that is a strict javascript subset.
108 # See: http://timelessrepo.com/json-isnt-a-javascript-subset
109 ret = ret.replace('\u2028', '\\u2028').replace('\u2029', '\\u2029')
110 return ret.encode()
113class TemplateHTMLRenderer(BaseRenderer):
114 """
115 An HTML renderer for use with templates.
117 The data supplied to the Response object should be a dictionary that will
118 be used as context for the template.
120 The template name is determined by (in order of preference):
122 1. An explicit `.template_name` attribute set on the response.
123 2. An explicit `.template_name` attribute set on this class.
124 3. The return result of calling `view.get_template_names()`.
126 For example:
127 data = {'users': User.objects.all()}
128 return Response(data, template_name='users.html')
130 For pre-rendered HTML, see StaticHTMLRenderer.
131 """
132 media_type = 'text/html'
133 format = 'html'
134 template_name = None
135 exception_template_names = [
136 '%(status_code)s.html',
137 'api_exception.html'
138 ]
139 charset = 'utf-8'
141 def render(self, data, accepted_media_type=None, renderer_context=None):
142 """
143 Renders data to HTML, using Django's standard template rendering.
145 The template name is determined by (in order of preference):
147 1. An explicit .template_name set on the response.
148 2. An explicit .template_name set on this class.
149 3. The return result of calling view.get_template_names().
150 """
151 renderer_context = renderer_context or {}
152 view = renderer_context['view']
153 request = renderer_context['request']
154 response = renderer_context['response']
156 if response.exception:
157 template = self.get_exception_template(response)
158 else:
159 template_names = self.get_template_names(response, view)
160 template = self.resolve_template(template_names)
162 if hasattr(self, 'resolve_context'):
163 # Fallback for older versions.
164 context = self.resolve_context(data, request, response)
165 else:
166 context = self.get_template_context(data, renderer_context)
167 return template.render(context, request=request)
169 def resolve_template(self, template_names):
170 return loader.select_template(template_names)
172 def get_template_context(self, data, renderer_context):
173 response = renderer_context['response']
174 if response.exception:
175 data['status_code'] = response.status_code
176 return data
178 def get_template_names(self, response, view):
179 if response.template_name:
180 return [response.template_name]
181 elif self.template_name:
182 return [self.template_name]
183 elif hasattr(view, 'get_template_names'):
184 return view.get_template_names()
185 elif hasattr(view, 'template_name'):
186 return [view.template_name]
187 raise ImproperlyConfigured(
188 'Returned a template response with no `template_name` attribute set on either the view or response'
189 )
191 def get_exception_template(self, response):
192 template_names = [name % {'status_code': response.status_code}
193 for name in self.exception_template_names]
195 try:
196 # Try to find an appropriate error template
197 return self.resolve_template(template_names)
198 except Exception:
199 # Fall back to using eg '404 Not Found'
200 body = '%d %s' % (response.status_code, response.status_text.title())
201 template = engines['django'].from_string(body)
202 return template
205# Note, subclass TemplateHTMLRenderer simply for the exception behavior
206class StaticHTMLRenderer(TemplateHTMLRenderer):
207 """
208 An HTML renderer class that simply returns pre-rendered HTML.
210 The data supplied to the Response object should be a string representing
211 the pre-rendered HTML content.
213 For example:
214 data = '<html><body>example</body></html>'
215 return Response(data)
217 For template rendered HTML, see TemplateHTMLRenderer.
218 """
219 media_type = 'text/html'
220 format = 'html'
221 charset = 'utf-8'
223 def render(self, data, accepted_media_type=None, renderer_context=None):
224 renderer_context = renderer_context or {}
225 response = renderer_context.get('response')
227 if response and response.exception:
228 request = renderer_context['request']
229 template = self.get_exception_template(response)
230 if hasattr(self, 'resolve_context'):
231 context = self.resolve_context(data, request, response)
232 else:
233 context = self.get_template_context(data, renderer_context)
234 return template.render(context, request=request)
236 return data
239class HTMLFormRenderer(BaseRenderer):
240 """
241 Renderers serializer data into an HTML form.
243 If the serializer was instantiated without an object then this will
244 return an HTML form not bound to any object,
245 otherwise it will return an HTML form with the appropriate initial data
246 populated from the object.
248 Note that rendering of field and form errors is not currently supported.
249 """
250 media_type = 'text/html'
251 format = 'form'
252 charset = 'utf-8'
253 template_pack = 'rest_framework/vertical/'
254 base_template = 'form.html'
256 default_style = ClassLookupDict({
257 serializers.Field: {
258 'base_template': 'input.html',
259 'input_type': 'text'
260 },
261 serializers.EmailField: {
262 'base_template': 'input.html',
263 'input_type': 'email'
264 },
265 serializers.URLField: {
266 'base_template': 'input.html',
267 'input_type': 'url'
268 },
269 serializers.IntegerField: {
270 'base_template': 'input.html',
271 'input_type': 'number'
272 },
273 serializers.FloatField: {
274 'base_template': 'input.html',
275 'input_type': 'number'
276 },
277 serializers.DateTimeField: {
278 'base_template': 'input.html',
279 'input_type': 'datetime-local'
280 },
281 serializers.DateField: {
282 'base_template': 'input.html',
283 'input_type': 'date'
284 },
285 serializers.TimeField: {
286 'base_template': 'input.html',
287 'input_type': 'time'
288 },
289 serializers.FileField: {
290 'base_template': 'input.html',
291 'input_type': 'file'
292 },
293 serializers.BooleanField: {
294 'base_template': 'checkbox.html'
295 },
296 serializers.ChoiceField: {
297 'base_template': 'select.html', # Also valid: 'radio.html'
298 },
299 serializers.MultipleChoiceField: {
300 'base_template': 'select_multiple.html', # Also valid: 'checkbox_multiple.html'
301 },
302 serializers.RelatedField: {
303 'base_template': 'select.html', # Also valid: 'radio.html'
304 },
305 serializers.ManyRelatedField: {
306 'base_template': 'select_multiple.html', # Also valid: 'checkbox_multiple.html'
307 },
308 serializers.Serializer: {
309 'base_template': 'fieldset.html'
310 },
311 serializers.ListSerializer: {
312 'base_template': 'list_fieldset.html'
313 },
314 serializers.ListField: {
315 'base_template': 'list_field.html'
316 },
317 serializers.DictField: {
318 'base_template': 'dict_field.html'
319 },
320 serializers.FilePathField: {
321 'base_template': 'select.html',
322 },
323 serializers.JSONField: {
324 'base_template': 'textarea.html',
325 },
326 })
328 def render_field(self, field, parent_style):
329 if isinstance(field._field, serializers.HiddenField):
330 return ''
332 style = self.default_style[field].copy()
333 style.update(field.style)
334 if 'template_pack' not in style:
335 style['template_pack'] = parent_style.get('template_pack', self.template_pack)
336 style['renderer'] = self
338 # Get a clone of the field with text-only value representation.
339 field = field.as_form_field()
341 if style.get('input_type') == 'datetime-local' and isinstance(field.value, str):
342 field.value = field.value.rstrip('Z')
344 if 'template' in style:
345 template_name = style['template']
346 else:
347 template_name = style['template_pack'].strip('/') + '/' + style['base_template']
349 template = loader.get_template(template_name)
350 context = {'field': field, 'style': style}
351 return template.render(context)
353 def render(self, data, accepted_media_type=None, renderer_context=None):
354 """
355 Render serializer data and return an HTML form, as a string.
356 """
357 renderer_context = renderer_context or {}
358 form = data.serializer
360 style = renderer_context.get('style', {})
361 if 'template_pack' not in style:
362 style['template_pack'] = self.template_pack
363 style['renderer'] = self
365 template_pack = style['template_pack'].strip('/')
366 template_name = template_pack + '/' + self.base_template
367 template = loader.get_template(template_name)
368 context = {
369 'form': form,
370 'style': style
371 }
372 return template.render(context)
375class BrowsableAPIRenderer(BaseRenderer):
376 """
377 HTML renderer used to self-document the API.
378 """
379 media_type = 'text/html'
380 format = 'api'
381 template = 'rest_framework/api.html'
382 filter_template = 'rest_framework/filters/base.html'
383 code_style = 'emacs'
384 charset = 'utf-8'
385 form_renderer_class = HTMLFormRenderer
387 def get_default_renderer(self, view):
388 """
389 Return an instance of the first valid renderer.
390 (Don't use another documenting renderer.)
391 """
392 renderers = [renderer for renderer in view.renderer_classes
393 if not issubclass(renderer, BrowsableAPIRenderer)]
394 non_template_renderers = [renderer for renderer in renderers
395 if not hasattr(renderer, 'get_template_names')]
397 if not renderers:
398 return None
399 elif non_template_renderers:
400 return non_template_renderers[0]()
401 return renderers[0]()
403 def get_content(self, renderer, data,
404 accepted_media_type, renderer_context):
405 """
406 Get the content as if it had been rendered by the default
407 non-documenting renderer.
408 """
409 if not renderer:
410 return '[No renderers were found]'
412 renderer_context['indent'] = 4
413 content = renderer.render(data, accepted_media_type, renderer_context)
415 render_style = getattr(renderer, 'render_style', 'text')
416 assert render_style in ['text', 'binary'], 'Expected .render_style ' \
417 '"text" or "binary", but got "%s"' % render_style
418 if render_style == 'binary':
419 return '[%d bytes of binary content]' % len(content)
421 return content.decode('utf-8') if isinstance(content, bytes) else content
423 def show_form_for_method(self, view, method, request, obj):
424 """
425 Returns True if a form should be shown for this method.
426 """
427 if method not in view.allowed_methods:
428 return # Not a valid method
430 try:
431 view.check_permissions(request)
432 if obj is not None:
433 view.check_object_permissions(request, obj)
434 except exceptions.APIException:
435 return False # Doesn't have permissions
436 return True
438 def _get_serializer(self, serializer_class, view_instance, request, *args, **kwargs):
439 kwargs['context'] = {
440 'request': request,
441 'format': self.format,
442 'view': view_instance
443 }
444 return serializer_class(*args, **kwargs)
446 def get_rendered_html_form(self, data, view, method, request):
447 """
448 Return a string representing a rendered HTML form, possibly bound to
449 either the input or output data.
451 In the absence of the View having an associated form then return None.
452 """
453 # See issue #2089 for refactoring this.
454 serializer = getattr(data, 'serializer', None)
455 if serializer and not getattr(serializer, 'many', False):
456 instance = getattr(serializer, 'instance', None)
457 if isinstance(instance, Page):
458 instance = None
459 else:
460 instance = None
462 # If this is valid serializer data, and the form is for the same
463 # HTTP method as was used in the request then use the existing
464 # serializer instance, rather than dynamically creating a new one.
465 if request.method == method and serializer is not None:
466 try:
467 kwargs = {'data': request.data}
468 except ParseError:
469 kwargs = {}
470 existing_serializer = serializer
471 else:
472 kwargs = {}
473 existing_serializer = None
475 with override_method(view, request, method) as request:
476 if not self.show_form_for_method(view, method, request, instance):
477 return
479 if method in ('DELETE', 'OPTIONS'):
480 return True # Don't actually need to return a form
482 has_serializer = getattr(view, 'get_serializer', None)
483 has_serializer_class = getattr(view, 'serializer_class', None)
485 if (
486 (not has_serializer and not has_serializer_class) or
487 not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)
488 ):
489 return
491 if existing_serializer is not None:
492 try:
493 return self.render_form_for_serializer(existing_serializer)
494 except TypeError:
495 pass
497 if has_serializer:
498 if method in ('PUT', 'PATCH'):
499 serializer = view.get_serializer(instance=instance, **kwargs)
500 else:
501 serializer = view.get_serializer(**kwargs)
502 else:
503 # at this point we must have a serializer_class
504 if method in ('PUT', 'PATCH'):
505 serializer = self._get_serializer(view.serializer_class, view,
506 request, instance=instance, **kwargs)
507 else:
508 serializer = self._get_serializer(view.serializer_class, view,
509 request, **kwargs)
511 return self.render_form_for_serializer(serializer)
513 def render_form_for_serializer(self, serializer):
514 if hasattr(serializer, 'initial_data'):
515 serializer.is_valid()
517 form_renderer = self.form_renderer_class()
518 return form_renderer.render(
519 serializer.data,
520 self.accepted_media_type,
521 {'style': {'template_pack': 'rest_framework/horizontal'}}
522 )
524 def get_raw_data_form(self, data, view, method, request):
525 """
526 Returns a form that allows for arbitrary content types to be tunneled
527 via standard HTML forms.
528 (Which are typically application/x-www-form-urlencoded)
529 """
530 # See issue #2089 for refactoring this.
531 serializer = getattr(data, 'serializer', None)
532 if serializer and not getattr(serializer, 'many', False):
533 instance = getattr(serializer, 'instance', None)
534 if isinstance(instance, Page):
535 instance = None
536 else:
537 instance = None
539 with override_method(view, request, method) as request:
540 # Check permissions
541 if not self.show_form_for_method(view, method, request, instance):
542 return
544 # If possible, serialize the initial content for the generic form
545 default_parser = view.parser_classes[0]
546 renderer_class = getattr(default_parser, 'renderer_class', None)
547 if hasattr(view, 'get_serializer') and renderer_class:
548 # View has a serializer defined and parser class has a
549 # corresponding renderer that can be used to render the data.
551 if method in ('PUT', 'PATCH'):
552 serializer = view.get_serializer(instance=instance)
553 else:
554 serializer = view.get_serializer()
556 # Render the raw data content
557 renderer = renderer_class()
558 accepted = self.accepted_media_type
559 context = self.renderer_context.copy()
560 context['indent'] = 4
562 # strip HiddenField from output
563 data = serializer.data.copy()
564 for name, field in serializer.fields.items():
565 if isinstance(field, serializers.HiddenField):
566 data.pop(name, None)
567 content = renderer.render(data, accepted, context)
568 # Renders returns bytes, but CharField expects a str.
569 content = content.decode()
570 else:
571 content = None
573 # Generate a generic form that includes a content type field,
574 # and a content field.
575 media_types = [parser.media_type for parser in view.parser_classes]
576 choices = [(media_type, media_type) for media_type in media_types]
577 initial = media_types[0]
579 class GenericContentForm(forms.Form):
580 _content_type = forms.ChoiceField(
581 label='Media type',
582 choices=choices,
583 initial=initial,
584 widget=forms.Select(attrs={'data-override': 'content-type'})
585 )
586 _content = forms.CharField(
587 label='Content',
588 widget=forms.Textarea(attrs={'data-override': 'content'}),
589 initial=content,
590 required=False
591 )
593 return GenericContentForm()
595 def get_name(self, view):
596 return view.get_view_name()
598 def get_description(self, view, status_code):
599 if status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN):
600 return ''
601 return view.get_view_description(html=True)
603 def get_breadcrumbs(self, request):
604 return get_breadcrumbs(request.path, request)
606 def get_extra_actions(self, view, status_code):
607 if (status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)):
608 return None
609 elif not hasattr(view, 'get_extra_action_url_map'):
610 return None
612 return view.get_extra_action_url_map()
614 def get_filter_form(self, data, view, request):
615 if not hasattr(view, 'get_queryset') or not hasattr(view, 'filter_backends'):
616 return
618 # Infer if this is a list view or not.
619 paginator = getattr(view, 'paginator', None)
620 if isinstance(data, list):
621 pass
622 elif paginator is not None and data is not None:
623 try:
624 paginator.get_results(data)
625 except (TypeError, KeyError):
626 return
627 elif not isinstance(data, list):
628 return
630 queryset = view.get_queryset()
631 elements = []
632 for backend in view.filter_backends:
633 if hasattr(backend, 'to_html'):
634 html = backend().to_html(request, queryset, view)
635 if html:
636 elements.append(html)
638 if not elements:
639 return
641 template = loader.get_template(self.filter_template)
642 context = {'elements': elements}
643 return template.render(context)
645 def get_context(self, data, accepted_media_type, renderer_context):
646 """
647 Returns the context used to render.
648 """
649 view = renderer_context['view']
650 request = renderer_context['request']
651 response = renderer_context['response']
653 renderer = self.get_default_renderer(view)
655 raw_data_post_form = self.get_raw_data_form(data, view, 'POST', request)
656 raw_data_put_form = self.get_raw_data_form(data, view, 'PUT', request)
657 raw_data_patch_form = self.get_raw_data_form(data, view, 'PATCH', request)
658 raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form
660 response_headers = OrderedDict(sorted(response.items()))
661 renderer_content_type = ''
662 if renderer:
663 renderer_content_type = '%s' % renderer.media_type
664 if renderer.charset:
665 renderer_content_type += ' ;%s' % renderer.charset
666 response_headers['Content-Type'] = renderer_content_type
668 if getattr(view, 'paginator', None) and view.paginator.display_page_controls:
669 paginator = view.paginator
670 else:
671 paginator = None
673 csrf_cookie_name = settings.CSRF_COOKIE_NAME
674 csrf_header_name = settings.CSRF_HEADER_NAME
675 if csrf_header_name.startswith('HTTP_'):
676 csrf_header_name = csrf_header_name[5:]
677 csrf_header_name = csrf_header_name.replace('_', '-')
679 return {
680 'content': self.get_content(renderer, data, accepted_media_type, renderer_context),
681 'code_style': pygments_css(self.code_style),
682 'view': view,
683 'request': request,
684 'response': response,
685 'user': request.user,
686 'description': self.get_description(view, response.status_code),
687 'name': self.get_name(view),
688 'version': VERSION,
689 'paginator': paginator,
690 'breadcrumblist': self.get_breadcrumbs(request),
691 'allowed_methods': view.allowed_methods,
692 'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes],
693 'response_headers': response_headers,
695 'put_form': self.get_rendered_html_form(data, view, 'PUT', request),
696 'post_form': self.get_rendered_html_form(data, view, 'POST', request),
697 'delete_form': self.get_rendered_html_form(data, view, 'DELETE', request),
698 'options_form': self.get_rendered_html_form(data, view, 'OPTIONS', request),
700 'extra_actions': self.get_extra_actions(view, response.status_code),
702 'filter_form': self.get_filter_form(data, view, request),
704 'raw_data_put_form': raw_data_put_form,
705 'raw_data_post_form': raw_data_post_form,
706 'raw_data_patch_form': raw_data_patch_form,
707 'raw_data_put_or_patch_form': raw_data_put_or_patch_form,
709 'display_edit_forms': bool(response.status_code != 403),
711 'api_settings': api_settings,
712 'csrf_cookie_name': csrf_cookie_name,
713 'csrf_header_name': csrf_header_name
714 }
716 def render(self, data, accepted_media_type=None, renderer_context=None):
717 """
718 Render the HTML for the browsable API representation.
719 """
720 self.accepted_media_type = accepted_media_type or ''
721 self.renderer_context = renderer_context or {}
723 template = loader.get_template(self.template)
724 context = self.get_context(data, accepted_media_type, renderer_context)
725 ret = template.render(context, request=renderer_context['request'])
727 # Munge DELETE Response code to allow us to return content
728 # (Do this *after* we've rendered the template so that we include
729 # the normal deletion response code in the output)
730 response = renderer_context['response']
731 if response.status_code == status.HTTP_204_NO_CONTENT:
732 response.status_code = status.HTTP_200_OK
734 return ret
737class AdminRenderer(BrowsableAPIRenderer):
738 template = 'rest_framework/admin.html'
739 format = 'admin'
741 def render(self, data, accepted_media_type=None, renderer_context=None):
742 self.accepted_media_type = accepted_media_type or ''
743 self.renderer_context = renderer_context or {}
745 response = renderer_context['response']
746 request = renderer_context['request']
747 view = self.renderer_context['view']
749 if response.status_code == status.HTTP_400_BAD_REQUEST:
750 # Errors still need to display the list or detail information.
751 # The only way we can get at that is to simulate a GET request.
752 self.error_form = self.get_rendered_html_form(data, view, request.method, request)
753 self.error_title = {'POST': 'Create', 'PUT': 'Edit'}.get(request.method, 'Errors')
755 with override_method(view, request, 'GET') as request:
756 response = view.get(request, *view.args, **view.kwargs)
757 data = response.data
759 template = loader.get_template(self.template)
760 context = self.get_context(data, accepted_media_type, renderer_context)
761 ret = template.render(context, request=renderer_context['request'])
763 # Creation and deletion should use redirects in the admin style.
764 if response.status_code == status.HTTP_201_CREATED and 'Location' in response:
765 response.status_code = status.HTTP_303_SEE_OTHER
766 response['Location'] = request.build_absolute_uri()
767 ret = ''
769 if response.status_code == status.HTTP_204_NO_CONTENT:
770 response.status_code = status.HTTP_303_SEE_OTHER
771 try:
772 # Attempt to get the parent breadcrumb URL.
773 response['Location'] = self.get_breadcrumbs(request)[-2][1]
774 except KeyError:
775 # Otherwise reload current URL to get a 'Not Found' page.
776 response['Location'] = request.full_path
777 ret = ''
779 return ret
781 def get_context(self, data, accepted_media_type, renderer_context):
782 """
783 Render the HTML for the browsable API representation.
784 """
785 context = super().get_context(
786 data, accepted_media_type, renderer_context
787 )
789 paginator = getattr(context['view'], 'paginator', None)
790 if paginator is not None and data is not None:
791 try:
792 results = paginator.get_results(data)
793 except (TypeError, KeyError):
794 results = data
795 else:
796 results = data
798 if results is None:
799 header = {}
800 style = 'detail'
801 elif isinstance(results, list):
802 header = results[0] if results else {}
803 style = 'list'
804 else:
805 header = results
806 style = 'detail'
808 columns = [key for key in header if key != 'url']
809 details = [key for key in header if key != 'url']
811 if isinstance(results, list) and 'view' in renderer_context:
812 for result in results:
813 url = self.get_result_url(result, context['view'])
814 if url is not None:
815 result.setdefault('url', url)
817 context['style'] = style
818 context['columns'] = columns
819 context['details'] = details
820 context['results'] = results
821 context['error_form'] = getattr(self, 'error_form', None)
822 context['error_title'] = getattr(self, 'error_title', None)
823 return context
825 def get_result_url(self, result, view):
826 """
827 Attempt to reverse the result's detail view URL.
829 This only works with views that are generic-like (has `.lookup_field`)
830 and viewset-like (has `.basename` / `.reverse_action()`).
831 """
832 if not hasattr(view, 'reverse_action') or \
833 not hasattr(view, 'lookup_field'):
834 return
836 lookup_field = view.lookup_field
837 lookup_url_kwarg = getattr(view, 'lookup_url_kwarg', None) or lookup_field
839 try:
840 kwargs = {lookup_url_kwarg: result[lookup_field]}
841 return view.reverse_action('detail', kwargs=kwargs)
842 except (KeyError, NoReverseMatch):
843 return
846class DocumentationRenderer(BaseRenderer):
847 media_type = 'text/html'
848 format = 'html'
849 charset = 'utf-8'
850 template = 'rest_framework/docs/index.html'
851 error_template = 'rest_framework/docs/error.html'
852 code_style = 'emacs'
853 languages = ['shell', 'javascript', 'python']
855 def get_context(self, data, request):
856 return {
857 'document': data,
858 'langs': self.languages,
859 'lang_htmls': ["rest_framework/docs/langs/%s.html" % language for language in self.languages],
860 'lang_intro_htmls': ["rest_framework/docs/langs/%s-intro.html" % language for language in self.languages],
861 'code_style': pygments_css(self.code_style),
862 'request': request
863 }
865 def render(self, data, accepted_media_type=None, renderer_context=None):
866 if isinstance(data, coreapi.Document):
867 template = loader.get_template(self.template)
868 context = self.get_context(data, renderer_context['request'])
869 return template.render(context, request=renderer_context['request'])
870 else:
871 template = loader.get_template(self.error_template)
872 context = {
873 "data": data,
874 "request": renderer_context['request'],
875 "response": renderer_context['response'],
876 "debug": settings.DEBUG,
877 }
878 return template.render(context, request=renderer_context['request'])
881class SchemaJSRenderer(BaseRenderer):
882 media_type = 'application/javascript'
883 format = 'javascript'
884 charset = 'utf-8'
885 template = 'rest_framework/schema.js'
887 def render(self, data, accepted_media_type=None, renderer_context=None):
888 codec = coreapi.codecs.CoreJSONCodec()
889 schema = base64.b64encode(codec.encode(data)).decode('ascii')
891 template = loader.get_template(self.template)
892 context = {'schema': mark_safe(schema)}
893 request = renderer_context['request']
894 return template.render(context, request=request)
897class MultiPartRenderer(BaseRenderer):
898 media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
899 format = 'multipart'
900 charset = 'utf-8'
901 BOUNDARY = 'BoUnDaRyStRiNg'
903 def render(self, data, accepted_media_type=None, renderer_context=None):
904 from django.test.client import encode_multipart
906 if hasattr(data, 'items'): 906 ↛ 914line 906 didn't jump to line 914, because the condition on line 906 was never false
907 for key, value in data.items():
908 assert not isinstance(value, dict), (
909 "Test data contained a dictionary value for key '%s', "
910 "but multipart uploads do not support nested data. "
911 "You may want to consider using format='json' in this "
912 "test case." % key
913 )
914 return encode_multipart(self.BOUNDARY, data)
917class CoreJSONRenderer(BaseRenderer):
918 media_type = 'application/coreapi+json'
919 charset = None
920 format = 'corejson'
922 def __init__(self):
923 assert coreapi, 'Using CoreJSONRenderer, but `coreapi` is not installed.'
925 def render(self, data, media_type=None, renderer_context=None):
926 indent = bool(renderer_context.get('indent', 0))
927 codec = coreapi.codecs.CoreJSONCodec()
928 return codec.dump(data, indent=indent)
931class _BaseOpenAPIRenderer:
932 def get_schema(self, instance):
933 CLASS_TO_TYPENAME = {
934 coreschema.Object: 'object',
935 coreschema.Array: 'array',
936 coreschema.Number: 'number',
937 coreschema.Integer: 'integer',
938 coreschema.String: 'string',
939 coreschema.Boolean: 'boolean',
940 }
942 schema = {}
943 if instance.__class__ in CLASS_TO_TYPENAME:
944 schema['type'] = CLASS_TO_TYPENAME[instance.__class__]
945 schema['title'] = instance.title
946 schema['description'] = instance.description
947 if hasattr(instance, 'enum'):
948 schema['enum'] = instance.enum
949 return schema
951 def get_parameters(self, link):
952 parameters = []
953 for field in link.fields:
954 if field.location not in ['path', 'query']:
955 continue
956 parameter = {
957 'name': field.name,
958 'in': field.location,
959 }
960 if field.required:
961 parameter['required'] = True
962 if field.description:
963 parameter['description'] = field.description
964 if field.schema:
965 parameter['schema'] = self.get_schema(field.schema)
966 parameters.append(parameter)
967 return parameters
969 def get_operation(self, link, name, tag):
970 operation_id = "%s_%s" % (tag, name) if tag else name
971 parameters = self.get_parameters(link)
973 operation = {
974 'operationId': operation_id,
975 }
976 if link.title:
977 operation['summary'] = link.title
978 if link.description:
979 operation['description'] = link.description
980 if parameters:
981 operation['parameters'] = parameters
982 if tag:
983 operation['tags'] = [tag]
984 return operation
986 def get_paths(self, document):
987 paths = {}
989 tag = None
990 for name, link in document.links.items():
991 path = parse.urlparse(link.url).path
992 method = link.action.lower()
993 paths.setdefault(path, {})
994 paths[path][method] = self.get_operation(link, name, tag=tag)
996 for tag, section in document.data.items():
997 for name, link in section.links.items():
998 path = parse.urlparse(link.url).path
999 method = link.action.lower()
1000 paths.setdefault(path, {})
1001 paths[path][method] = self.get_operation(link, name, tag=tag)
1003 return paths
1005 def get_structure(self, data):
1006 return {
1007 'openapi': '3.0.0',
1008 'info': {
1009 'version': '',
1010 'title': data.title,
1011 'description': data.description
1012 },
1013 'servers': [{
1014 'url': data.url
1015 }],
1016 'paths': self.get_paths(data)
1017 }
1020class CoreAPIOpenAPIRenderer(_BaseOpenAPIRenderer):
1021 media_type = 'application/vnd.oai.openapi'
1022 charset = None
1023 format = 'openapi'
1025 def __init__(self):
1026 assert coreapi, 'Using CoreAPIOpenAPIRenderer, but `coreapi` is not installed.'
1027 assert yaml, 'Using CoreAPIOpenAPIRenderer, but `pyyaml` is not installed.'
1029 def render(self, data, media_type=None, renderer_context=None):
1030 structure = self.get_structure(data)
1031 return yaml.dump(structure, default_flow_style=False).encode()
1034class CoreAPIJSONOpenAPIRenderer(_BaseOpenAPIRenderer):
1035 media_type = 'application/vnd.oai.openapi+json'
1036 charset = None
1037 format = 'openapi-json'
1038 ensure_ascii = not api_settings.UNICODE_JSON
1040 def __init__(self):
1041 assert coreapi, 'Using CoreAPIJSONOpenAPIRenderer, but `coreapi` is not installed.'
1043 def render(self, data, media_type=None, renderer_context=None):
1044 structure = self.get_structure(data)
1045 return json.dumps(
1046 structure, indent=4,
1047 ensure_ascii=self.ensure_ascii).encode('utf-8')
1050class OpenAPIRenderer(BaseRenderer):
1051 media_type = 'application/vnd.oai.openapi'
1052 charset = None
1053 format = 'openapi'
1055 def __init__(self):
1056 assert yaml, 'Using OpenAPIRenderer, but `pyyaml` is not installed.'
1058 def render(self, data, media_type=None, renderer_context=None):
1059 # disable yaml advanced feature 'alias' for clean, portable, and readable output
1060 class Dumper(yaml.Dumper):
1061 def ignore_aliases(self, data):
1062 return True
1063 return yaml.dump(data, default_flow_style=False, sort_keys=False, Dumper=Dumper).encode('utf-8')
1066class JSONOpenAPIRenderer(BaseRenderer):
1067 media_type = 'application/vnd.oai.openapi+json'
1068 charset = None
1069 encoder_class = encoders.JSONEncoder
1070 format = 'openapi-json'
1071 ensure_ascii = not api_settings.UNICODE_JSON
1073 def render(self, data, media_type=None, renderer_context=None):
1074 return json.dumps(
1075 data, cls=self.encoder_class, indent=2,
1076 ensure_ascii=self.ensure_ascii).encode('utf-8')