mirror of https://github.com/django/django.git
Fixed #35631 -- Added HttpRequest.get_preferred_type().
This commit is contained in:
parent
826ef00668
commit
e161bd4657
|
@ -1,5 +1,6 @@
|
||||||
import codecs
|
import codecs
|
||||||
import copy
|
import copy
|
||||||
|
import operator
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from urllib.parse import parse_qsl, quote, urlencode, urljoin, urlsplit
|
from urllib.parse import parse_qsl, quote, urlencode, urljoin, urlsplit
|
||||||
|
@ -89,13 +90,47 @@ class HttpRequest:
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def accepted_types(self):
|
def accepted_types(self):
|
||||||
"""Return a list of MediaType instances."""
|
"""Return a list of MediaType instances, in order of preference."""
|
||||||
return parse_accept_header(self.headers.get("Accept", "*/*"))
|
header_value = self.headers.get("Accept", "*/*")
|
||||||
|
return sorted(
|
||||||
|
(MediaType(token) for token in header_value.split(",") if token.strip()),
|
||||||
|
key=operator.attrgetter("quality", "specificity"),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def accepted_type(self, media_type):
|
||||||
|
"""
|
||||||
|
Return the preferred MediaType instance which matches the given media type.
|
||||||
|
"""
|
||||||
|
return next(
|
||||||
|
(
|
||||||
|
accepted_type
|
||||||
|
for accepted_type in self.accepted_types
|
||||||
|
if accepted_type.match(media_type)
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_preferred_type(self, media_types):
|
||||||
|
"""Select the preferred media type from the provided options."""
|
||||||
|
if not media_types or not self.accepted_types:
|
||||||
|
return None
|
||||||
|
|
||||||
|
desired_types = [
|
||||||
|
(accepted_type, media_type)
|
||||||
|
for media_type in media_types
|
||||||
|
if (accepted_type := self.accepted_type(media_type)) is not None
|
||||||
|
]
|
||||||
|
|
||||||
|
if not desired_types:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Of the desired media types, select the one which is most desirable.
|
||||||
|
return min(desired_types, key=lambda t: self.accepted_types.index(t[0]))[1]
|
||||||
|
|
||||||
def accepts(self, media_type):
|
def accepts(self, media_type):
|
||||||
return any(
|
"""Does the client accept a response in the given media type?"""
|
||||||
accepted_type.match(media_type) for accepted_type in self.accepted_types
|
return self.accepted_type(media_type) is not None
|
||||||
)
|
|
||||||
|
|
||||||
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."""
|
||||||
|
@ -678,9 +713,37 @@ class MediaType:
|
||||||
if self.is_all_types:
|
if self.is_all_types:
|
||||||
return True
|
return True
|
||||||
other = MediaType(other)
|
other = MediaType(other)
|
||||||
if self.main_type == other.main_type and self.sub_type in {"*", other.sub_type}:
|
return self.main_type == other.main_type and self.sub_type in {
|
||||||
return True
|
"*",
|
||||||
return False
|
other.sub_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def quality(self):
|
||||||
|
try:
|
||||||
|
quality = float(self.params.get("q", 1))
|
||||||
|
except ValueError:
|
||||||
|
# Discard invalid values.
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Valid quality values must be between 0 and 1.
|
||||||
|
if quality < 0 or quality > 1:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return round(quality, 3)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def specificity(self):
|
||||||
|
"""
|
||||||
|
Return a value from 0-3 for how specific the media type is.
|
||||||
|
"""
|
||||||
|
if self.main_type == "*":
|
||||||
|
return 0
|
||||||
|
elif self.sub_type == "*":
|
||||||
|
return 1
|
||||||
|
elif self.quality == 1:
|
||||||
|
return 2
|
||||||
|
return 3
|
||||||
|
|
||||||
|
|
||||||
# It's neither necessary nor appropriate to use
|
# It's neither necessary nor appropriate to use
|
||||||
|
@ -732,7 +795,3 @@ def validate_host(host, allowed_hosts):
|
||||||
return any(
|
return any(
|
||||||
pattern == "*" or is_same_domain(host, pattern) for pattern in allowed_hosts
|
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()]
|
|
||||||
|
|
|
@ -425,10 +425,48 @@ 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.get_preferred_type(media_types)
|
||||||
|
|
||||||
|
.. versionadded:: 5.2
|
||||||
|
|
||||||
|
Returns the preferred mime type from ``media_types``, based on the
|
||||||
|
``Accept`` header, or ``None`` if the client does not accept any of the
|
||||||
|
provided types.
|
||||||
|
|
||||||
|
Assuming the client sends an ``Accept`` header of
|
||||||
|
``text/html,application/json;q=0.8``:
|
||||||
|
|
||||||
|
.. code-block:: pycon
|
||||||
|
|
||||||
|
>>> request.get_preferred_type(["text/html", "application/json"])
|
||||||
|
"text/html"
|
||||||
|
>>> request.get_preferred_type(["application/json", "text/plain"])
|
||||||
|
"application/json"
|
||||||
|
>>> request.get_preferred_type(["application/xml", "text/plain"])
|
||||||
|
None
|
||||||
|
|
||||||
|
Most browsers send ``Accept: */*`` by default, meaning they don't have a
|
||||||
|
preference, in which case the first item in ``media_types`` would be
|
||||||
|
returned.
|
||||||
|
|
||||||
|
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` for an example of returning
|
||||||
|
different content based on the ``Accept`` header.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
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.accepts(mime_type)
|
.. method:: HttpRequest.accepts(mime_type)
|
||||||
|
|
||||||
Returns ``True`` if the request ``Accept`` header matches the ``mime_type``
|
Returns ``True`` if the request's ``Accept`` header matches the
|
||||||
argument:
|
``mime_type`` argument:
|
||||||
|
|
||||||
.. code-block:: pycon
|
.. code-block:: pycon
|
||||||
|
|
||||||
|
@ -436,17 +474,10 @@ Methods
|
||||||
True
|
True
|
||||||
|
|
||||||
Most browsers send ``Accept: */*`` by default, so this would return
|
Most browsers send ``Accept: */*`` by default, so this would return
|
||||||
``True`` for all content types. Setting an explicit ``Accept`` header in
|
``True`` for all content types.
|
||||||
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
|
See :ref:`content-negotiation-example` for an example of using
|
||||||
you are using some form of caching like Django's :mod:`cache middleware
|
``accepts()`` to return different content based on the ``Accept`` header.
|
||||||
<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.read(size=None)
|
.. method:: HttpRequest.read(size=None)
|
||||||
.. method:: HttpRequest.readline()
|
.. method:: HttpRequest.readline()
|
||||||
|
|
|
@ -226,7 +226,8 @@ Models
|
||||||
Requests and Responses
|
Requests and Responses
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
* ...
|
* The new :meth:`.HttpRequest.get_preferred_type` method can be used to query
|
||||||
|
the preferred media type the client accepts.
|
||||||
|
|
||||||
Security
|
Security
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
@ -309,6 +310,9 @@ Miscellaneous
|
||||||
|
|
||||||
* The minimum supported version of ``gettext`` is increased from 0.15 to 0.19.
|
* The minimum supported version of ``gettext`` is increased from 0.15 to 0.19.
|
||||||
|
|
||||||
|
* ``HttpRequest.accepted_types`` is now sorted by the client's preference, based
|
||||||
|
on the request's ``Accept`` header.
|
||||||
|
|
||||||
.. _deprecated-features-5.2:
|
.. _deprecated-features-5.2:
|
||||||
|
|
||||||
Features deprecated in 5.2
|
Features deprecated in 5.2
|
||||||
|
|
|
@ -273,3 +273,56 @@ works with an API-based workflow as well as 'normal' form POSTs::
|
||||||
class AuthorCreateView(JsonableResponseMixin, CreateView):
|
class AuthorCreateView(JsonableResponseMixin, CreateView):
|
||||||
model = Author
|
model = Author
|
||||||
fields = ["name"]
|
fields = ["name"]
|
||||||
|
|
||||||
|
The above example assumes that if the client supports ``text/html``, that they
|
||||||
|
would prefer it. However, this may not always be true. When requesting a
|
||||||
|
``.css`` file, many browsers will send the header
|
||||||
|
``Accept: text/css,*/*;q=0.1``, indicating that they would prefer CSS, but
|
||||||
|
anything else is fine. This means ``request.accepts("text/html") will be
|
||||||
|
``True``.
|
||||||
|
|
||||||
|
To determine the correct format, taking into consideration the client's
|
||||||
|
preference, use :func:`django.http.HttpRequest.get_preferred_type`::
|
||||||
|
|
||||||
|
class JsonableResponseMixin:
|
||||||
|
"""
|
||||||
|
Mixin to add JSON support to a form.
|
||||||
|
Must be used with an object-based FormView (e.g. CreateView).
|
||||||
|
"""
|
||||||
|
|
||||||
|
accepted_media_types = ["text/html", "application/json"]
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
if request.get_preferred_type(self.accepted_media_types) is None:
|
||||||
|
# No format in common.
|
||||||
|
return HttpResponse(
|
||||||
|
status_code=406, headers={"Accept": ",".join(self.accepted_media_types)}
|
||||||
|
)
|
||||||
|
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def form_invalid(self, form):
|
||||||
|
response = super().form_invalid(form)
|
||||||
|
accepted_type = request.get_preferred_type(self.accepted_media_types)
|
||||||
|
if accepted_type == "text/html":
|
||||||
|
return response
|
||||||
|
elif accepted_type == "application/json":
|
||||||
|
return JsonResponse(form.errors, status=400)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
# We make sure to call the parent's form_valid() method because
|
||||||
|
# it might do some processing (in the case of CreateView, it will
|
||||||
|
# call form.save() for example).
|
||||||
|
response = super().form_valid(form)
|
||||||
|
accepted_type = request.get_preferred_type(self.accepted_media_types)
|
||||||
|
if accepted_type == "text/html":
|
||||||
|
return response
|
||||||
|
elif accepted_type == "application/json":
|
||||||
|
data = {
|
||||||
|
"pk": self.object.pk,
|
||||||
|
}
|
||||||
|
return JsonResponse(data)
|
||||||
|
|
||||||
|
.. versionchanged:: 5.2
|
||||||
|
|
||||||
|
The :meth:`.HttpRequest.get_preferred_type` method was added.
|
||||||
|
|
|
@ -56,6 +56,35 @@ class MediaTypeTests(TestCase):
|
||||||
with self.subTest(accepted_type, mime_type=mime_type):
|
with self.subTest(accepted_type, mime_type=mime_type):
|
||||||
self.assertIs(MediaType(accepted_type).match(mime_type), False)
|
self.assertIs(MediaType(accepted_type).match(mime_type), False)
|
||||||
|
|
||||||
|
def test_quality(self):
|
||||||
|
tests = [
|
||||||
|
("*/*; q=0.8", 0.8),
|
||||||
|
("*/*; q=0.0001", 0),
|
||||||
|
("*/*; q=0.12345", 0.123),
|
||||||
|
("*/*; q=0.1", 0.1),
|
||||||
|
("*/*; q=-1", 1),
|
||||||
|
("*/*; q=2", 1),
|
||||||
|
("*/*; q=h", 1),
|
||||||
|
("*/*", 1),
|
||||||
|
]
|
||||||
|
for accepted_type, quality in tests:
|
||||||
|
with self.subTest(accepted_type, quality=quality):
|
||||||
|
self.assertEqual(MediaType(accepted_type).quality, quality)
|
||||||
|
|
||||||
|
def test_specificity(self):
|
||||||
|
tests = [
|
||||||
|
("*/*", 0),
|
||||||
|
("*/*;q=0.5", 0),
|
||||||
|
("text/*", 1),
|
||||||
|
("text/*;q=0.5", 1),
|
||||||
|
("text/html", 2),
|
||||||
|
("text/html;q=1", 2),
|
||||||
|
("text/html;q=0.5", 3),
|
||||||
|
]
|
||||||
|
for accepted_type, specificity in tests:
|
||||||
|
with self.subTest(accepted_type, specificity=specificity):
|
||||||
|
self.assertEqual(MediaType(accepted_type).specificity, specificity)
|
||||||
|
|
||||||
|
|
||||||
class AcceptHeaderTests(TestCase):
|
class AcceptHeaderTests(TestCase):
|
||||||
def test_no_headers(self):
|
def test_no_headers(self):
|
||||||
|
@ -69,13 +98,14 @@ class AcceptHeaderTests(TestCase):
|
||||||
def test_accept_headers(self):
|
def test_accept_headers(self):
|
||||||
request = HttpRequest()
|
request = HttpRequest()
|
||||||
request.META["HTTP_ACCEPT"] = (
|
request.META["HTTP_ACCEPT"] = (
|
||||||
"text/html, application/xhtml+xml,application/xml ;q=0.9,*/*;q=0.8"
|
"text/*,text/html, application/xhtml+xml,application/xml ;q=0.9,*/*;q=0.8,"
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
[str(accepted_type) for accepted_type in request.accepted_types],
|
[str(accepted_type) for accepted_type in request.accepted_types],
|
||||||
[
|
[
|
||||||
"text/html",
|
"text/html",
|
||||||
"application/xhtml+xml",
|
"application/xhtml+xml",
|
||||||
|
"text/*",
|
||||||
"application/xml; q=0.9",
|
"application/xml; q=0.9",
|
||||||
"*/*; q=0.8",
|
"*/*; q=0.8",
|
||||||
],
|
],
|
||||||
|
@ -85,12 +115,20 @@ class AcceptHeaderTests(TestCase):
|
||||||
request = HttpRequest()
|
request = HttpRequest()
|
||||||
request.META["HTTP_ACCEPT"] = "*/*"
|
request.META["HTTP_ACCEPT"] = "*/*"
|
||||||
self.assertIs(request.accepts("application/json"), True)
|
self.assertIs(request.accepts("application/json"), True)
|
||||||
|
self.assertIsNone(request.get_preferred_type([]))
|
||||||
|
self.assertEqual(
|
||||||
|
request.get_preferred_type(["application/json", "text/plain"]),
|
||||||
|
"application/json",
|
||||||
|
)
|
||||||
|
|
||||||
def test_request_accepts_none(self):
|
def test_request_accepts_none(self):
|
||||||
request = HttpRequest()
|
request = HttpRequest()
|
||||||
request.META["HTTP_ACCEPT"] = ""
|
request.META["HTTP_ACCEPT"] = ""
|
||||||
self.assertIs(request.accepts("application/json"), False)
|
self.assertIs(request.accepts("application/json"), False)
|
||||||
self.assertEqual(request.accepted_types, [])
|
self.assertEqual(request.accepted_types, [])
|
||||||
|
self.assertIsNone(
|
||||||
|
request.get_preferred_type(["application/json", "text/plain"])
|
||||||
|
)
|
||||||
|
|
||||||
def test_request_accepts_some(self):
|
def test_request_accepts_some(self):
|
||||||
request = HttpRequest()
|
request = HttpRequest()
|
||||||
|
@ -101,3 +139,39 @@ class AcceptHeaderTests(TestCase):
|
||||||
self.assertIs(request.accepts("application/xhtml+xml"), True)
|
self.assertIs(request.accepts("application/xhtml+xml"), True)
|
||||||
self.assertIs(request.accepts("application/xml"), True)
|
self.assertIs(request.accepts("application/xml"), True)
|
||||||
self.assertIs(request.accepts("application/json"), False)
|
self.assertIs(request.accepts("application/json"), False)
|
||||||
|
|
||||||
|
def test_accept_header_priority(self):
|
||||||
|
request = HttpRequest()
|
||||||
|
request.META["HTTP_ACCEPT"] = (
|
||||||
|
"text/html,application/xml;q=0.9,*/*;q=0.1,text/*;q=0.5"
|
||||||
|
)
|
||||||
|
|
||||||
|
tests = [
|
||||||
|
(["text/html", "application/xml"], "text/html"),
|
||||||
|
(["application/xml", "application/json"], "application/xml"),
|
||||||
|
(["application/json"], "application/json"),
|
||||||
|
(["application/json", "text/plain"], "text/plain"),
|
||||||
|
]
|
||||||
|
for types, preferred_type in tests:
|
||||||
|
with self.subTest(types, preferred_type=preferred_type):
|
||||||
|
self.assertEqual(str(request.get_preferred_type(types)), preferred_type)
|
||||||
|
|
||||||
|
def test_accept_header_priority_overlapping_mime(self):
|
||||||
|
request = HttpRequest()
|
||||||
|
request.META["HTTP_ACCEPT"] = "text/*;q=0.8,text/html;q=0.8"
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[str(accepted_type) for accepted_type in request.accepted_types],
|
||||||
|
[
|
||||||
|
"text/html; q=0.8",
|
||||||
|
"text/*; q=0.8",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_no_matching_accepted_type(self):
|
||||||
|
request = HttpRequest()
|
||||||
|
request.META["HTTP_ACCEPT"] = "text/html"
|
||||||
|
|
||||||
|
self.assertIsNone(
|
||||||
|
request.get_preferred_type(["application/json", "text/plain"])
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue