Fixed #29490 -- Added support for object-based Media CSS and JS paths.

This commit is contained in:
Claude Paroz 2022-01-22 17:21:57 +01:00 committed by Mariusz Felisiak
parent cda81b79f2
commit 4c76ffc2d6
4 changed files with 196 additions and 3 deletions

View File

@ -101,7 +101,9 @@ class Media:
def render_js(self): def render_js(self):
return [ return [
format_html('<script src="{}"></script>', self.absolute_path(path)) path.__html__()
if hasattr(path, "__html__")
else format_html('<script src="{}"></script>', self.absolute_path(path))
for path in self._js for path in self._js
] ]
@ -111,7 +113,9 @@ class Media:
media = sorted(self._css) media = sorted(self._css)
return chain.from_iterable( return chain.from_iterable(
[ [
format_html( path.__html__()
if hasattr(path, "__html__")
else format_html(
'<link href="{}" media="{}" rel="stylesheet">', '<link href="{}" media="{}" rel="stylesheet">',
self.absolute_path(path), self.absolute_path(path),
medium, medium,

View File

@ -192,6 +192,11 @@ Forms
* The new ``edit_only`` argument for :func:`.modelformset_factory` and * The new ``edit_only`` argument for :func:`.modelformset_factory` and
:func:`.inlineformset_factory` allows preventing new objects creation. :func:`.inlineformset_factory` allows preventing new objects creation.
* The ``js`` and ``css`` class attributes of :doc:`Media </topics/forms/media>`
now allow using hashable objects, not only path strings, as long as those
objects implement the ``__html__()`` method (typically when decorated with
the :func:`~django.utils.html.html_safe` decorator).
Generic Views Generic Views
~~~~~~~~~~~~~ ~~~~~~~~~~~~~

View File

@ -206,7 +206,10 @@ return values for dynamic ``media`` properties.
Paths in asset definitions Paths in asset definitions
========================== ==========================
Paths used to specify assets can be either relative or absolute. If a Paths as strings
----------------
String paths used to specify assets can be either relative or absolute. If a
path starts with ``/``, ``http://`` or ``https://``, it will be path starts with ``/``, ``http://`` or ``https://``, it will be
interpreted as an absolute path, and left as-is. All other paths will interpreted as an absolute path, and left as-is. All other paths will
be prepended with the value of the appropriate prefix. If the be prepended with the value of the appropriate prefix. If the
@ -254,6 +257,28 @@ Or if :mod:`~django.contrib.staticfiles` is configured using the
<script src="https://static.example.com/animations.27e20196a850.js"></script> <script src="https://static.example.com/animations.27e20196a850.js"></script>
<script src="http://othersite.com/actions.js"></script> <script src="http://othersite.com/actions.js"></script>
Paths as objects
----------------
.. versionadded:: 4.1
Asset paths may also be given as hashable objects implementing an
``__html__()`` method. The ``__html__()`` method is typically added using the
:func:`~django.utils.html.html_safe` decorator. The object is responsible for
outputting the complete HTML ``<script>`` or ``<link>`` tag content::
>>> from django import forms
>>> from django.utils.html import html_safe
>>>
>>> @html_safe
>>> class JSPath:
... def __str__(self):
... return '<script src="https://example.org/asset.js" rel="stylesheet">'
>>> class SomeWidget(forms.TextInput):
... class Media:
... js = (JSPath(),)
``Media`` objects ``Media`` objects
================= =================

View File

@ -1,6 +1,8 @@
from django.forms import CharField, Form, Media, MultiWidget, TextInput from django.forms import CharField, Form, Media, MultiWidget, TextInput
from django.template import Context, Template from django.template import Context, Template
from django.templatetags.static import static
from django.test import SimpleTestCase, override_settings from django.test import SimpleTestCase, override_settings
from django.utils.html import format_html, html_safe
@override_settings( @override_settings(
@ -710,3 +712,160 @@ class FormsMediaTestCase(SimpleTestCase):
merged = media + empty_media merged = media + empty_media
self.assertEqual(merged._css_lists, [{"screen": ["a.css"]}]) self.assertEqual(merged._css_lists, [{"screen": ["a.css"]}])
self.assertEqual(merged._js_lists, [["a"]]) self.assertEqual(merged._js_lists, [["a"]])
@html_safe
class Asset:
def __init__(self, path):
self.path = path
def __eq__(self, other):
return (self.__class__ == other.__class__ and self.path == other.path) or (
other.__class__ == str and self.path == other
)
def __hash__(self):
return hash(self.path)
def __str__(self):
return self.absolute_path(self.path)
def absolute_path(self, path):
"""
Given a relative or absolute path to a static asset, return an absolute
path. An absolute path will be returned unchanged while a relative path
will be passed to django.templatetags.static.static().
"""
if path.startswith(("http://", "https://", "/")):
return path
return static(path)
def __repr__(self):
return f"{self.path!r}"
class CSS(Asset):
def __init__(self, path, medium):
super().__init__(path)
self.medium = medium
def __str__(self):
path = super().__str__()
return format_html(
'<link href="{}" media="{}" rel="stylesheet">',
self.absolute_path(path),
self.medium,
)
class JS(Asset):
def __init__(self, path, integrity=None):
super().__init__(path)
self.integrity = integrity or ""
def __str__(self, integrity=None):
path = super().__str__()
template = '<script src="{}"%s></script>' % (
' integrity="{}"' if self.integrity else "{}"
)
return format_html(template, self.absolute_path(path), self.integrity)
@override_settings(
STATIC_URL="http://media.example.com/static/",
)
class FormsMediaObjectTestCase(SimpleTestCase):
"""Media handling when media are objects instead of raw strings."""
def test_construction(self):
m = Media(
css={"all": (CSS("path/to/css1", "all"), CSS("/path/to/css2", "all"))},
js=(
JS("/path/to/js1"),
JS("http://media.other.com/path/to/js2"),
JS(
"https://secure.other.com/path/to/js3",
integrity="9d947b87fdeb25030d56d01f7aa75800",
),
),
)
self.assertEqual(
str(m),
'<link href="http://media.example.com/static/path/to/css1" media="all" '
'rel="stylesheet">\n'
'<link href="/path/to/css2" media="all" rel="stylesheet">\n'
'<script src="/path/to/js1"></script>\n'
'<script src="http://media.other.com/path/to/js2"></script>\n'
'<script src="https://secure.other.com/path/to/js3" '
'integrity="9d947b87fdeb25030d56d01f7aa75800"></script>',
)
self.assertEqual(
repr(m),
"Media(css={'all': ['path/to/css1', '/path/to/css2']}, "
"js=['/path/to/js1', 'http://media.other.com/path/to/js2', "
"'https://secure.other.com/path/to/js3'])",
)
def test_simplest_class(self):
@html_safe
class SimpleJS:
"""The simplest possible asset class."""
def __str__(self):
return '<script src="https://example.org/asset.js" rel="stylesheet">'
m = Media(js=(SimpleJS(),))
self.assertEqual(
str(m),
'<script src="https://example.org/asset.js" rel="stylesheet">',
)
def test_combine_media(self):
class MyWidget1(TextInput):
class Media:
css = {"all": (CSS("path/to/css1", "all"), "/path/to/css2")}
js = (
"/path/to/js1",
"http://media.other.com/path/to/js2",
"https://secure.other.com/path/to/js3",
JS("/path/to/js4", integrity="9d947b87fdeb25030d56d01f7aa75800"),
)
class MyWidget2(TextInput):
class Media:
css = {"all": (CSS("/path/to/css2", "all"), "/path/to/css3")}
js = (JS("/path/to/js1"), "/path/to/js4")
w1 = MyWidget1()
w2 = MyWidget2()
self.assertEqual(
str(w1.media + w2.media),
'<link href="http://media.example.com/static/path/to/css1" media="all" '
'rel="stylesheet">\n'
'<link href="/path/to/css2" media="all" rel="stylesheet">\n'
'<link href="/path/to/css3" media="all" rel="stylesheet">\n'
'<script src="/path/to/js1"></script>\n'
'<script src="http://media.other.com/path/to/js2"></script>\n'
'<script src="https://secure.other.com/path/to/js3"></script>\n'
'<script src="/path/to/js4" integrity="9d947b87fdeb25030d56d01f7aa75800">'
"</script>",
)
def test_media_deduplication(self):
# The deduplication doesn't only happen at the point of merging two or
# more media objects.
media = Media(
css={
"all": (
CSS("/path/to/css1", "all"),
CSS("/path/to/css1", "all"),
"/path/to/css1",
)
},
js=(JS("/path/to/js1"), JS("/path/to/js1"), "/path/to/js1"),
)
self.assertEqual(
str(media),
'<link href="/path/to/css1" media="all" rel="stylesheet">\n'
'<script src="/path/to/js1"></script>',
)