negotiation.py 3.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. """
  2. Content negotiation deals with selecting an appropriate renderer given the
  3. incoming request. Typically this will be based on the request's Accept header.
  4. """
  5. from django.http import Http404
  6. from rest_framework import HTTP_HEADER_ENCODING, exceptions
  7. from rest_framework.settings import api_settings
  8. from rest_framework.utils.mediatypes import (
  9. _MediaType, media_type_matches, order_by_precedence
  10. )
  11. class BaseContentNegotiation:
  12. def select_parser(self, request, parsers):
  13. raise NotImplementedError('.select_parser() must be implemented')
  14. def select_renderer(self, request, renderers, format_suffix=None):
  15. raise NotImplementedError('.select_renderer() must be implemented')
  16. class DefaultContentNegotiation(BaseContentNegotiation):
  17. settings = api_settings
  18. def select_parser(self, request, parsers):
  19. """
  20. Given a list of parsers and a media type, return the appropriate
  21. parser to handle the incoming request.
  22. """
  23. for parser in parsers:
  24. if media_type_matches(parser.media_type, request.content_type):
  25. return parser
  26. return None
  27. def select_renderer(self, request, renderers, format_suffix=None):
  28. """
  29. Given a request and a list of renderers, return a two-tuple of:
  30. (renderer, media type).
  31. """
  32. # Allow URL style format override. eg. "?format=json
  33. format_query_param = self.settings.URL_FORMAT_OVERRIDE
  34. format = format_suffix or request.query_params.get(format_query_param)
  35. if format:
  36. renderers = self.filter_renderers(renderers, format)
  37. accepts = self.get_accept_list(request)
  38. # Check the acceptable media types against each renderer,
  39. # attempting more specific media types first
  40. # NB. The inner loop here isn't as bad as it first looks :)
  41. # Worst case is we're looping over len(accept_list) * len(self.renderers)
  42. for media_type_set in order_by_precedence(accepts):
  43. for renderer in renderers:
  44. for media_type in media_type_set:
  45. if media_type_matches(renderer.media_type, media_type):
  46. # Return the most specific media type as accepted.
  47. media_type_wrapper = _MediaType(media_type)
  48. if (
  49. _MediaType(renderer.media_type).precedence >
  50. media_type_wrapper.precedence
  51. ):
  52. # Eg client requests '*/*'
  53. # Accepted media type is 'application/json'
  54. full_media_type = ';'.join(
  55. (renderer.media_type,) +
  56. tuple('{}={}'.format(
  57. key, value.decode(HTTP_HEADER_ENCODING))
  58. for key, value in media_type_wrapper.params.items()))
  59. return renderer, full_media_type
  60. else:
  61. # Eg client requests 'application/json; indent=8'
  62. # Accepted media type is 'application/json; indent=8'
  63. return renderer, media_type
  64. raise exceptions.NotAcceptable(available_renderers=renderers)
  65. def filter_renderers(self, renderers, format):
  66. """
  67. If there is a '.json' style format suffix, filter the renderers
  68. so that we only negotiation against those that accept that format.
  69. """
  70. renderers = [renderer for renderer in renderers
  71. if renderer.format == format]
  72. if not renderers:
  73. raise Http404
  74. return renderers
  75. def get_accept_list(self, request):
  76. """
  77. Given the incoming request, return a tokenized list of media
  78. type strings.
  79. """
  80. header = request.META.get('HTTP_ACCEPT', '*/*')
  81. return [token.strip() for token in header.split(',')]