1
0
mirror of https://github.com/django/django.git synced 2025-06-05 11:39:13 +00:00

Refs #30997 -- Added HttpRequest.accepts().

This commit is contained in:
Claude Paroz 2019-11-17 13:24:10 +01:00 committed by Mariusz Felisiak
parent cf493e5c81
commit d66d72f956
5 changed files with 192 additions and 12 deletions

View File

@ -20,6 +20,8 @@ from django.utils.functional import cached_property
from django.utils.http import is_same_domain, limited_parse_qsl from django.utils.http import is_same_domain, limited_parse_qsl
from django.utils.regex_helper import _lazy_re_compile from django.utils.regex_helper import _lazy_re_compile
from .multipartparser import parse_header
RAISE_ERROR = object() RAISE_ERROR = object()
host_validation_re = _lazy_re_compile(r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9\.:]+\])(:\d+)?$") host_validation_re = _lazy_re_compile(r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9\.:]+\])(:\d+)?$")
@ -71,6 +73,17 @@ class HttpRequest:
def headers(self): def headers(self):
return HttpHeaders(self.META) return HttpHeaders(self.META)
@cached_property
def accepted_types(self):
"""Return a list of MediaType instances."""
return parse_accept_header(self.headers.get('Accept', '*/*'))
def accepts(self, media_type):
return any(
accepted_type.match(media_type)
for accepted_type in self.accepted_types
)
def _set_content_type_params(self, meta): def _set_content_type_params(self, meta):
"""Set content_type, content_params, and encoding.""" """Set content_type, content_params, and encoding."""
self.content_type, self.content_params = cgi.parse_header(meta.get('CONTENT_TYPE', '')) self.content_type, self.content_params = cgi.parse_header(meta.get('CONTENT_TYPE', ''))
@ -557,6 +570,40 @@ class QueryDict(MultiValueDict):
return '&'.join(output) return '&'.join(output)
class MediaType:
def __init__(self, media_type_raw_line):
full_type, self.params = parse_header(
media_type_raw_line.encode('ascii') if media_type_raw_line else b''
)
self.main_type, _, self.sub_type = full_type.partition('/')
def __str__(self):
params_str = ''.join(
'; %s=%s' % (k, v.decode('ascii'))
for k, v in self.params.items()
)
return '%s%s%s' % (
self.main_type,
('/%s' % self.sub_type) if self.sub_type else '',
params_str,
)
def __repr__(self):
return '<%s: %s>' % (self.__class__.__qualname__, self)
@property
def is_all_types(self):
return self.main_type == '*' and self.sub_type == '*'
def match(self, other):
if self.is_all_types:
return True
other = MediaType(other)
if self.main_type == other.main_type and self.sub_type in {'*', other.sub_type}:
return True
return False
# It's neither necessary nor appropriate to use # It's neither necessary nor appropriate to use
# django.utils.encoding.force_str() for parsing URLs and form inputs. Thus, # django.utils.encoding.force_str() for parsing URLs and form inputs. Thus,
# this slightly more restricted function, used by QueryDict. # this slightly more restricted function, used by QueryDict.
@ -612,3 +659,7 @@ def validate_host(host, allowed_hosts):
Return ``True`` for a valid host, ``False`` otherwise. Return ``True`` for a valid host, ``False`` otherwise.
""" """
return any(pattern == '*' or is_same_domain(host, pattern) for pattern in allowed_hosts) return any(pattern == '*' or is_same_domain(host, pattern) for pattern in allowed_hosts)
def parse_accept_header(header):
return [MediaType(token) for token in header.split(',') if token.strip()]

View File

@ -406,6 +406,29 @@ Methods
Returns ``True`` if the request is secure; that is, if it was made with Returns ``True`` if the request is secure; that is, if it was made with
HTTPS. HTTPS.
.. method:: HttpRequest.accepts(mime_type)
.. versionadded:: 3.1
Returns ``True`` if the request ``Accept`` header matches the ``mime_type``
argument::
>>> request.accepts('text/html')
True
Most browsers send ``Accept: */*`` by default, so this would return
``True`` for all content types. Setting an explicit ``Accept`` header in
API requests can be useful for returning a different content type for those
consumers only. See :ref:`content-negotiation-example` of using
``accepts()`` to return different content to API consumers.
If a response varies depending on the content of the ``Accept`` header and
you are using some form of caching like Django's :mod:`cache middleware
<django.middleware.cache>`, you should decorate the view with
:func:`vary_on_headers('Accept')
<django.views.decorators.vary.vary_on_headers>` so that the responses are
properly cached.
.. method:: HttpRequest.is_ajax() .. method:: HttpRequest.is_ajax()
Returns ``True`` if the request was made via an ``XMLHttpRequest``, by Returns ``True`` if the request was made via an ``XMLHttpRequest``, by

View File

@ -282,6 +282,9 @@ Requests and Responses
now allow using ``samesite='None'`` (string) to explicitly state that the now allow using ``samesite='None'`` (string) to explicitly state that the
cookie is sent with all same-site and cross-site requests. cookie is sent with all same-site and cross-site requests.
* The new :meth:`.HttpRequest.accepts` method returns whether the request
accepts the given MIME type according to the ``Accept`` HTTP header.
Serialization Serialization
~~~~~~~~~~~~~ ~~~~~~~~~~~~~

View File

@ -222,41 +222,43 @@ to edit, and override
aren't logged in from accessing the form. If you omit that, you'll need to aren't logged in from accessing the form. If you omit that, you'll need to
handle unauthorized users in :meth:`~.ModelFormMixin.form_valid()`. handle unauthorized users in :meth:`~.ModelFormMixin.form_valid()`.
AJAX example .. _content-negotiation-example:
============
Content negotiation example
===========================
Here is an example showing how you might go about implementing a form that Here is an example showing how you might go about implementing a form that
works for AJAX requests as well as 'normal' form POSTs:: works with an API-based workflow as well as 'normal' form POSTs::
from django.http import JsonResponse from django.http import JsonResponse
from django.views.generic.edit import CreateView from django.views.generic.edit import CreateView
from myapp.models import Author from myapp.models import Author
class AjaxableResponseMixin: class JsonableResponseMixin:
""" """
Mixin to add AJAX support to a form. Mixin to add JSON support to a form.
Must be used with an object-based FormView (e.g. CreateView) Must be used with an object-based FormView (e.g. CreateView)
""" """
def form_invalid(self, form): def form_invalid(self, form):
response = super().form_invalid(form) response = super().form_invalid(form)
if self.request.is_ajax(): if self.request.accepts('text/html'):
return JsonResponse(form.errors, status=400)
else:
return response return response
else:
return JsonResponse(form.errors, status=400)
def form_valid(self, form): def form_valid(self, form):
# We make sure to call the parent's form_valid() method because # We make sure to call the parent's form_valid() method because
# it might do some processing (in the case of CreateView, it will # it might do some processing (in the case of CreateView, it will
# call form.save() for example). # call form.save() for example).
response = super().form_valid(form) response = super().form_valid(form)
if self.request.is_ajax(): if self.request.accepts('text/html'):
return response
else:
data = { data = {
'pk': self.object.pk, 'pk': self.object.pk,
} }
return JsonResponse(data) return JsonResponse(data)
else:
return response
class AuthorCreate(AjaxableResponseMixin, CreateView): class AuthorCreate(JsonableResponseMixin, CreateView):
model = Author model = Author
fields = ['name'] fields = ['name']

View File

@ -0,0 +1,101 @@
from unittest import TestCase
from django.http import HttpRequest
from django.http.request import MediaType
class MediaTypeTests(TestCase):
def test_empty(self):
for empty_media_type in (None, ''):
with self.subTest(media_type=empty_media_type):
media_type = MediaType(empty_media_type)
self.assertIs(media_type.is_all_types, False)
self.assertEqual(str(media_type), '')
self.assertEqual(repr(media_type), '<MediaType: >')
def test_str(self):
self.assertEqual(str(MediaType('*/*; q=0.8')), '*/*; q=0.8')
self.assertEqual(str(MediaType('application/xml')), 'application/xml')
def test_repr(self):
self.assertEqual(repr(MediaType('*/*; q=0.8')), '<MediaType: */*; q=0.8>')
self.assertEqual(
repr(MediaType('application/xml')),
'<MediaType: application/xml>',
)
def test_is_all_types(self):
self.assertIs(MediaType('*/*').is_all_types, True)
self.assertIs(MediaType('*/*; q=0.8').is_all_types, True)
self.assertIs(MediaType('text/*').is_all_types, False)
self.assertIs(MediaType('application/xml').is_all_types, False)
def test_match(self):
tests = [
('*/*; q=0.8', '*/*'),
('*/*', 'application/json'),
(' */* ', 'application/json'),
('application/*', 'application/json'),
('application/xml', 'application/xml'),
(' application/xml ', 'application/xml'),
('application/xml', ' application/xml '),
]
for accepted_type, mime_type in tests:
with self.subTest(accepted_type, mime_type=mime_type):
self.assertIs(MediaType(accepted_type).match(mime_type), True)
def test_no_match(self):
tests = [
(None, '*/*'),
('', '*/*'),
('; q=0.8', '*/*'),
('application/xml', 'application/html'),
('application/xml', '*/*'),
]
for accepted_type, mime_type in tests:
with self.subTest(accepted_type, mime_type=mime_type):
self.assertIs(MediaType(accepted_type).match(mime_type), False)
class AcceptHeaderTests(TestCase):
def test_no_headers(self):
"""Absence of Accept header defaults to '*/*'."""
request = HttpRequest()
self.assertEqual(
[str(accepted_type) for accepted_type in request.accepted_types],
['*/*'],
)
def test_accept_headers(self):
request = HttpRequest()
request.META['HTTP_ACCEPT'] = (
'text/html, application/xhtml+xml,application/xml ;q=0.9,*/*;q=0.8'
)
self.assertEqual(
[str(accepted_type) for accepted_type in request.accepted_types],
[
'text/html',
'application/xhtml+xml',
'application/xml; q=0.9',
'*/*; q=0.8',
],
)
def test_request_accepts_any(self):
request = HttpRequest()
request.META['HTTP_ACCEPT'] = '*/*'
self.assertIs(request.accepts('application/json'), True)
def test_request_accepts_none(self):
request = HttpRequest()
request.META['HTTP_ACCEPT'] = ''
self.assertIs(request.accepts('application/json'), False)
self.assertEqual(request.accepted_types, [])
def test_request_accepts_some(self):
request = HttpRequest()
request.META['HTTP_ACCEPT'] = 'text/html,application/xhtml+xml,application/xml;q=0.9'
self.assertIs(request.accepts('text/html'), True)
self.assertIs(request.accepts('application/xhtml+xml'), True)
self.assertIs(request.accepts('application/xml'), True)
self.assertIs(request.accepts('application/json'), False)