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

1""" 

2Renderers are used to serialize a response into specific media types. 

3 

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. 

6 

7REST framework also provides an HTML renderer that renders the browsable API. 

8""" 

9import base64 

10from collections import OrderedDict 

11from urllib import parse 

12 

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 

21 

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 

33 

34 

35def zero_as_none(value): 

36 return None if value == 0 else value 

37 

38 

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' 

48 

49 def render(self, data, accepted_media_type=None, renderer_context=None): 

50 raise NotImplementedError('Renderer class requires .render() to be implemented') 

51 

52 

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 

63 

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 

69 

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 

80 

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) 

84 

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

91 

92 renderer_context = renderer_context or {} 

93 indent = self.get_indent(accepted_media_type, renderer_context) 

94 

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 

99 

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 ) 

105 

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() 

111 

112 

113class TemplateHTMLRenderer(BaseRenderer): 

114 """ 

115 An HTML renderer for use with templates. 

116 

117 The data supplied to the Response object should be a dictionary that will 

118 be used as context for the template. 

119 

120 The template name is determined by (in order of preference): 

121 

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()`. 

125 

126 For example: 

127 data = {'users': User.objects.all()} 

128 return Response(data, template_name='users.html') 

129 

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' 

140 

141 def render(self, data, accepted_media_type=None, renderer_context=None): 

142 """ 

143 Renders data to HTML, using Django's standard template rendering. 

144 

145 The template name is determined by (in order of preference): 

146 

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

155 

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) 

161 

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) 

168 

169 def resolve_template(self, template_names): 

170 return loader.select_template(template_names) 

171 

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 

177 

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 ) 

190 

191 def get_exception_template(self, response): 

192 template_names = [name % {'status_code': response.status_code} 

193 for name in self.exception_template_names] 

194 

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 

203 

204 

205# Note, subclass TemplateHTMLRenderer simply for the exception behavior 

206class StaticHTMLRenderer(TemplateHTMLRenderer): 

207 """ 

208 An HTML renderer class that simply returns pre-rendered HTML. 

209 

210 The data supplied to the Response object should be a string representing 

211 the pre-rendered HTML content. 

212 

213 For example: 

214 data = '<html><body>example</body></html>' 

215 return Response(data) 

216 

217 For template rendered HTML, see TemplateHTMLRenderer. 

218 """ 

219 media_type = 'text/html' 

220 format = 'html' 

221 charset = 'utf-8' 

222 

223 def render(self, data, accepted_media_type=None, renderer_context=None): 

224 renderer_context = renderer_context or {} 

225 response = renderer_context.get('response') 

226 

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) 

235 

236 return data 

237 

238 

239class HTMLFormRenderer(BaseRenderer): 

240 """ 

241 Renderers serializer data into an HTML form. 

242 

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. 

247 

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' 

255 

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 }) 

327 

328 def render_field(self, field, parent_style): 

329 if isinstance(field._field, serializers.HiddenField): 

330 return '' 

331 

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 

337 

338 # Get a clone of the field with text-only value representation. 

339 field = field.as_form_field() 

340 

341 if style.get('input_type') == 'datetime-local' and isinstance(field.value, str): 

342 field.value = field.value.rstrip('Z') 

343 

344 if 'template' in style: 

345 template_name = style['template'] 

346 else: 

347 template_name = style['template_pack'].strip('/') + '/' + style['base_template'] 

348 

349 template = loader.get_template(template_name) 

350 context = {'field': field, 'style': style} 

351 return template.render(context) 

352 

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 

359 

360 style = renderer_context.get('style', {}) 

361 if 'template_pack' not in style: 

362 style['template_pack'] = self.template_pack 

363 style['renderer'] = self 

364 

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) 

373 

374 

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 

386 

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

396 

397 if not renderers: 

398 return None 

399 elif non_template_renderers: 

400 return non_template_renderers[0]() 

401 return renderers[0]() 

402 

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

411 

412 renderer_context['indent'] = 4 

413 content = renderer.render(data, accepted_media_type, renderer_context) 

414 

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) 

420 

421 return content.decode('utf-8') if isinstance(content, bytes) else content 

422 

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 

429 

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 

437 

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) 

445 

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. 

450 

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 

461 

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 

474 

475 with override_method(view, request, method) as request: 

476 if not self.show_form_for_method(view, method, request, instance): 

477 return 

478 

479 if method in ('DELETE', 'OPTIONS'): 

480 return True # Don't actually need to return a form 

481 

482 has_serializer = getattr(view, 'get_serializer', None) 

483 has_serializer_class = getattr(view, 'serializer_class', None) 

484 

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 

490 

491 if existing_serializer is not None: 

492 try: 

493 return self.render_form_for_serializer(existing_serializer) 

494 except TypeError: 

495 pass 

496 

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) 

510 

511 return self.render_form_for_serializer(serializer) 

512 

513 def render_form_for_serializer(self, serializer): 

514 if hasattr(serializer, 'initial_data'): 

515 serializer.is_valid() 

516 

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 ) 

523 

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 

538 

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 

543 

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. 

550 

551 if method in ('PUT', 'PATCH'): 

552 serializer = view.get_serializer(instance=instance) 

553 else: 

554 serializer = view.get_serializer() 

555 

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 

561 

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 

572 

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] 

578 

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 ) 

592 

593 return GenericContentForm() 

594 

595 def get_name(self, view): 

596 return view.get_view_name() 

597 

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) 

602 

603 def get_breadcrumbs(self, request): 

604 return get_breadcrumbs(request.path, request) 

605 

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 

611 

612 return view.get_extra_action_url_map() 

613 

614 def get_filter_form(self, data, view, request): 

615 if not hasattr(view, 'get_queryset') or not hasattr(view, 'filter_backends'): 

616 return 

617 

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 

629 

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) 

637 

638 if not elements: 

639 return 

640 

641 template = loader.get_template(self.filter_template) 

642 context = {'elements': elements} 

643 return template.render(context) 

644 

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

652 

653 renderer = self.get_default_renderer(view) 

654 

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 

659 

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 

667 

668 if getattr(view, 'paginator', None) and view.paginator.display_page_controls: 

669 paginator = view.paginator 

670 else: 

671 paginator = None 

672 

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('_', '-') 

678 

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, 

694 

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), 

699 

700 'extra_actions': self.get_extra_actions(view, response.status_code), 

701 

702 'filter_form': self.get_filter_form(data, view, request), 

703 

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, 

708 

709 'display_edit_forms': bool(response.status_code != 403), 

710 

711 'api_settings': api_settings, 

712 'csrf_cookie_name': csrf_cookie_name, 

713 'csrf_header_name': csrf_header_name 

714 } 

715 

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 {} 

722 

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

726 

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 

733 

734 return ret 

735 

736 

737class AdminRenderer(BrowsableAPIRenderer): 

738 template = 'rest_framework/admin.html' 

739 format = 'admin' 

740 

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 {} 

744 

745 response = renderer_context['response'] 

746 request = renderer_context['request'] 

747 view = self.renderer_context['view'] 

748 

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

754 

755 with override_method(view, request, 'GET') as request: 

756 response = view.get(request, *view.args, **view.kwargs) 

757 data = response.data 

758 

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

762 

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

768 

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

778 

779 return ret 

780 

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 ) 

788 

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 

797 

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' 

807 

808 columns = [key for key in header if key != 'url'] 

809 details = [key for key in header if key != 'url'] 

810 

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) 

816 

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 

824 

825 def get_result_url(self, result, view): 

826 """ 

827 Attempt to reverse the result's detail view URL. 

828 

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 

835 

836 lookup_field = view.lookup_field 

837 lookup_url_kwarg = getattr(view, 'lookup_url_kwarg', None) or lookup_field 

838 

839 try: 

840 kwargs = {lookup_url_kwarg: result[lookup_field]} 

841 return view.reverse_action('detail', kwargs=kwargs) 

842 except (KeyError, NoReverseMatch): 

843 return 

844 

845 

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

854 

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 } 

864 

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

879 

880 

881class SchemaJSRenderer(BaseRenderer): 

882 media_type = 'application/javascript' 

883 format = 'javascript' 

884 charset = 'utf-8' 

885 template = 'rest_framework/schema.js' 

886 

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

890 

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) 

895 

896 

897class MultiPartRenderer(BaseRenderer): 

898 media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg' 

899 format = 'multipart' 

900 charset = 'utf-8' 

901 BOUNDARY = 'BoUnDaRyStRiNg' 

902 

903 def render(self, data, accepted_media_type=None, renderer_context=None): 

904 from django.test.client import encode_multipart 

905 

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) 

915 

916 

917class CoreJSONRenderer(BaseRenderer): 

918 media_type = 'application/coreapi+json' 

919 charset = None 

920 format = 'corejson' 

921 

922 def __init__(self): 

923 assert coreapi, 'Using CoreJSONRenderer, but `coreapi` is not installed.' 

924 

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) 

929 

930 

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 } 

941 

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 

950 

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 

968 

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) 

972 

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 

985 

986 def get_paths(self, document): 

987 paths = {} 

988 

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) 

995 

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) 

1002 

1003 return paths 

1004 

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 } 

1018 

1019 

1020class CoreAPIOpenAPIRenderer(_BaseOpenAPIRenderer): 

1021 media_type = 'application/vnd.oai.openapi' 

1022 charset = None 

1023 format = 'openapi' 

1024 

1025 def __init__(self): 

1026 assert coreapi, 'Using CoreAPIOpenAPIRenderer, but `coreapi` is not installed.' 

1027 assert yaml, 'Using CoreAPIOpenAPIRenderer, but `pyyaml` is not installed.' 

1028 

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() 

1032 

1033 

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 

1039 

1040 def __init__(self): 

1041 assert coreapi, 'Using CoreAPIJSONOpenAPIRenderer, but `coreapi` is not installed.' 

1042 

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

1048 

1049 

1050class OpenAPIRenderer(BaseRenderer): 

1051 media_type = 'application/vnd.oai.openapi' 

1052 charset = None 

1053 format = 'openapi' 

1054 

1055 def __init__(self): 

1056 assert yaml, 'Using OpenAPIRenderer, but `pyyaml` is not installed.' 

1057 

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

1064 

1065 

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 

1072 

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