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 copy
|
||||
import operator
|
||||
from io import BytesIO
|
||||
from itertools import chain
|
||||
from urllib.parse import parse_qsl, quote, urlencode, urljoin, urlsplit
|
||||
|
@ -89,13 +90,47 @@ class HttpRequest:
|
|||
|
||||
@cached_property
|
||||
def accepted_types(self):
|
||||
"""Return a list of MediaType instances."""
|
||||
return parse_accept_header(self.headers.get("Accept", "*/*"))
|
||||
"""Return a list of MediaType instances, in order of preference."""
|
||||
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):
|
||||
return any(
|
||||
accepted_type.match(media_type) for accepted_type in self.accepted_types
|
||||
)
|
||||
"""Does the client accept a response in the given media type?"""
|
||||
return self.accepted_type(media_type) is not None
|
||||
|
||||
def _set_content_type_params(self, meta):
|
||||
"""Set content_type, content_params, and encoding."""
|
||||
|
@ -678,9 +713,37 @@ class MediaType:
|
|||
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
|
||||
return self.main_type == other.main_type and self.sub_type in {
|
||||
"*",
|
||||
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
|
||||
|
@ -732,7 +795,3 @@ def validate_host(host, 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()]
|
||||
|
|
|
@ -425,10 +425,48 @@ Methods
|
|||
Returns ``True`` if the request is secure; that is, if it was made with
|
||||
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)
|
||||
|
||||
Returns ``True`` if the request ``Accept`` header matches the ``mime_type``
|
||||
argument:
|
||||
Returns ``True`` if the request's ``Accept`` header matches the
|
||||
``mime_type`` argument:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
|
@ -436,17 +474,10 @@ Methods
|
|||
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.
|
||||
``True`` for all content types.
|
||||
|
||||
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.
|
||||
See :ref:`content-negotiation-example` for an example of using
|
||||
``accepts()`` to return different content based on the ``Accept`` header.
|
||||
|
||||
.. method:: HttpRequest.read(size=None)
|
||||
.. method:: HttpRequest.readline()
|
||||
|
|
|
@ -226,7 +226,8 @@ Models
|
|||
Requests and Responses
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* ...
|
||||
* The new :meth:`.HttpRequest.get_preferred_type` method can be used to query
|
||||
the preferred media type the client accepts.
|
||||
|
||||
Security
|
||||
~~~~~~~~
|
||||
|
@ -309,6 +310,9 @@ Miscellaneous
|
|||
|
||||
* 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:
|
||||
|
||||
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):
|
||||
model = Author
|
||||
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):
|
||||
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):
|
||||
def test_no_headers(self):
|
||||
|
@ -69,13 +98,14 @@ class AcceptHeaderTests(TestCase):
|
|||
def test_accept_headers(self):
|
||||
request = HttpRequest()
|
||||
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(
|
||||
[str(accepted_type) for accepted_type in request.accepted_types],
|
||||
[
|
||||
"text/html",
|
||||
"application/xhtml+xml",
|
||||
"text/*",
|
||||
"application/xml; q=0.9",
|
||||
"*/*; q=0.8",
|
||||
],
|
||||
|
@ -85,12 +115,20 @@ class AcceptHeaderTests(TestCase):
|
|||
request = HttpRequest()
|
||||
request.META["HTTP_ACCEPT"] = "*/*"
|
||||
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):
|
||||
request = HttpRequest()
|
||||
request.META["HTTP_ACCEPT"] = ""
|
||||
self.assertIs(request.accepts("application/json"), False)
|
||||
self.assertEqual(request.accepted_types, [])
|
||||
self.assertIsNone(
|
||||
request.get_preferred_type(["application/json", "text/plain"])
|
||||
)
|
||||
|
||||
def test_request_accepts_some(self):
|
||||
request = HttpRequest()
|
||||
|
@ -101,3 +139,39 @@ class AcceptHeaderTests(TestCase):
|
|||
self.assertIs(request.accepts("application/xhtml+xml"), True)
|
||||
self.assertIs(request.accepts("application/xml"), True)
|
||||
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