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:
parent
cf493e5c81
commit
d66d72f956
@ -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()]
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -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']
|
||||||
|
101
tests/requests/test_accept_header.py
Normal file
101
tests/requests/test_accept_header.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user