mirror of
https://github.com/django/django.git
synced 2025-06-05 11:39:13 +00:00
Fixed #35886 -- Added support for object-based form media script assets.
This commit is contained in:
parent
b322319f9d
commit
989329344a
@ -9,7 +9,7 @@ from collections import defaultdict
|
|||||||
from graphlib import CycleError, TopologicalSorter
|
from graphlib import CycleError, TopologicalSorter
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
from django.forms.utils import to_current_timezone
|
from django.forms.utils import flatatt, to_current_timezone
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
from django.utils import formats
|
from django.utils import formats
|
||||||
from django.utils.choices import normalize_choices
|
from django.utils.choices import normalize_choices
|
||||||
@ -23,6 +23,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from .renderers import get_default_renderer
|
from .renderers import get_default_renderer
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
"Script",
|
||||||
"Media",
|
"Media",
|
||||||
"MediaDefiningClass",
|
"MediaDefiningClass",
|
||||||
"Widget",
|
"Widget",
|
||||||
@ -61,6 +62,53 @@ class MediaOrderConflictWarning(RuntimeWarning):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@html_safe
|
||||||
|
class MediaAsset:
|
||||||
|
element_template = "{path}"
|
||||||
|
|
||||||
|
def __init__(self, path, **attributes):
|
||||||
|
self._path = path
|
||||||
|
self.attributes = attributes
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
# Compare the path only, to ensure performant comparison in Media.merge.
|
||||||
|
return (self.__class__ is other.__class__ and self.path == other.path) or (
|
||||||
|
isinstance(other, str) and self._path == other
|
||||||
|
)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
# Hash the path only, to ensure performant comparison in Media.merge.
|
||||||
|
return hash(self._path)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return format_html(
|
||||||
|
self.element_template,
|
||||||
|
path=self.path,
|
||||||
|
attributes=flatatt(self.attributes),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{type(self).__qualname__}({self._path!r})"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
"""
|
||||||
|
Ensure an absolute path.
|
||||||
|
Relative paths are resolved via the {% static %} template tag.
|
||||||
|
"""
|
||||||
|
if self._path.startswith(("http://", "https://", "/")):
|
||||||
|
return self._path
|
||||||
|
return static(self._path)
|
||||||
|
|
||||||
|
|
||||||
|
class Script(MediaAsset):
|
||||||
|
element_template = '<script src="{path}"{attributes}></script>'
|
||||||
|
|
||||||
|
def __init__(self, src, **attributes):
|
||||||
|
# Alter the signature to allow src to be passed as a keyword argument.
|
||||||
|
super().__init__(src, **attributes)
|
||||||
|
|
||||||
|
|
||||||
@html_safe
|
@html_safe
|
||||||
class Media:
|
class Media:
|
||||||
def __init__(self, media=None, css=None, js=None):
|
def __init__(self, media=None, css=None, js=None):
|
||||||
|
@ -259,6 +259,10 @@ Forms
|
|||||||
* An :attr:`~django.forms.BoundField.aria_describedby` property is added to
|
* An :attr:`~django.forms.BoundField.aria_describedby` property is added to
|
||||||
``BoundField`` to ease use of this HTML attribute in templates.
|
``BoundField`` to ease use of this HTML attribute in templates.
|
||||||
|
|
||||||
|
* The new asset object :class:`~django.forms.Script` is available for adding
|
||||||
|
custom HTML-attributes to JavaScript in form media. See
|
||||||
|
:ref:`paths as objects <form-media-asset-objects>` for more details.
|
||||||
|
|
||||||
Generic Views
|
Generic Views
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
Form Assets (the ``Media`` class)
|
Form Assets (the ``Media`` class)
|
||||||
=================================
|
=================================
|
||||||
|
|
||||||
|
.. currentmodule:: django.forms
|
||||||
|
|
||||||
Rendering an attractive and easy-to-use web form requires more than just
|
Rendering an attractive and easy-to-use web form requires more than just
|
||||||
HTML - it also requires CSS stylesheets, and if you want to use fancy widgets,
|
HTML - it also requires CSS stylesheets, and if you want to use fancy widgets,
|
||||||
you may also need to include some JavaScript on each page. The exact
|
you may also need to include some JavaScript on each page. The exact
|
||||||
@ -130,6 +132,24 @@ A tuple describing the required JavaScript files. See :ref:`the
|
|||||||
section on paths <form-asset-paths>` for details of how to specify
|
section on paths <form-asset-paths>` for details of how to specify
|
||||||
paths to these files.
|
paths to these files.
|
||||||
|
|
||||||
|
``Script`` objects
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. versionadded:: 5.2
|
||||||
|
|
||||||
|
.. class:: Script(src, **attributes)
|
||||||
|
|
||||||
|
Represents a script file.
|
||||||
|
|
||||||
|
The first parameter, ``src``, is the string path to the script file. See
|
||||||
|
:ref:`the section on paths <form-asset-paths>` for details on how to
|
||||||
|
specify paths to these files.
|
||||||
|
|
||||||
|
The optional keyword arguments, ``**attributes``, are HTML attributes that
|
||||||
|
are set on the rendered ``<script>`` tag.
|
||||||
|
|
||||||
|
See :ref:`form-media-asset-objects` for usage examples.
|
||||||
|
|
||||||
``extend``
|
``extend``
|
||||||
----------
|
----------
|
||||||
|
|
||||||
@ -271,29 +291,38 @@ 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="https://othersite.com/actions.js"></script>
|
<script src="https://othersite.com/actions.js"></script>
|
||||||
|
|
||||||
|
.. _form-media-asset-objects:
|
||||||
|
|
||||||
Paths as objects
|
Paths as objects
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
Asset paths may also be given as hashable objects implementing an
|
Assets may also be object-based, using :class:`.Script`.
|
||||||
``__html__()`` method. The ``__html__()`` method is typically added using the
|
Furthermore, these allow you to pass custom HTML attributes::
|
||||||
:func:`~django.utils.html.html_safe` decorator. The object is responsible for
|
|
||||||
outputting the complete HTML ``<script>`` or ``<link>`` tag content:
|
|
||||||
|
|
||||||
.. code-block:: pycon
|
class Media:
|
||||||
|
js = [
|
||||||
|
Script(
|
||||||
|
"https://cdn.example.com/something.min.js",
|
||||||
|
**{
|
||||||
|
"crossorigin": "anonymous",
|
||||||
|
"async": True,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
>>> from django import forms
|
If this Media definition were to be rendered, it would become the following
|
||||||
>>> from django.utils.html import html_safe
|
HTML:
|
||||||
>>>
|
|
||||||
>>> @html_safe
|
|
||||||
... class JSPath:
|
|
||||||
... def __str__(self):
|
|
||||||
... return '<script src="https://example.org/asset.js" defer>'
|
|
||||||
...
|
|
||||||
|
|
||||||
>>> class SomeWidget(forms.TextInput):
|
.. code-block:: html+django
|
||||||
... class Media:
|
|
||||||
... js = [JSPath()]
|
<script src="https://cdn.example.com/something.min.js"
|
||||||
...
|
crossorigin="anonymous"
|
||||||
|
async>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
.. versionchanged:: 5.2
|
||||||
|
|
||||||
|
The object class ``Script`` was added.
|
||||||
|
|
||||||
``Media`` objects
|
``Media`` objects
|
||||||
=================
|
=================
|
||||||
|
@ -1,8 +1,94 @@
|
|||||||
from django.forms import CharField, Form, Media, MultiWidget, TextInput
|
from django.forms import CharField, Form, Media, MultiWidget, TextInput
|
||||||
|
from django.forms.widgets import MediaAsset, Script
|
||||||
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
|
from django.utils.html import html_safe
|
||||||
|
|
||||||
|
|
||||||
|
class CSS(MediaAsset):
|
||||||
|
element_template = '<link href="{path}"{attributes}>'
|
||||||
|
|
||||||
|
def __init__(self, href, **attributes):
|
||||||
|
super().__init__(href, **attributes)
|
||||||
|
self.attributes["rel"] = "stylesheet"
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(STATIC_URL="http://media.example.com/static/")
|
||||||
|
class MediaAssetTestCase(SimpleTestCase):
|
||||||
|
def test_init(self):
|
||||||
|
attributes = {"media": "all", "is": "magic-css"}
|
||||||
|
asset = MediaAsset("path/to/css", **attributes)
|
||||||
|
self.assertEqual(asset._path, "path/to/css")
|
||||||
|
self.assertEqual(asset.attributes, attributes)
|
||||||
|
self.assertIsNot(asset.attributes, attributes)
|
||||||
|
|
||||||
|
def test_eq(self):
|
||||||
|
self.assertEqual(MediaAsset("path/to/css"), MediaAsset("path/to/css"))
|
||||||
|
self.assertEqual(MediaAsset("path/to/css"), "path/to/css")
|
||||||
|
self.assertEqual(
|
||||||
|
MediaAsset("path/to/css", media="all"), MediaAsset("path/to/css")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertNotEqual(MediaAsset("path/to/css"), MediaAsset("path/to/other.css"))
|
||||||
|
self.assertNotEqual(MediaAsset("path/to/css"), "path/to/other.css")
|
||||||
|
self.assertNotEqual(MediaAsset("path/to/css", media="all"), CSS("path/to/css"))
|
||||||
|
|
||||||
|
def test_hash(self):
|
||||||
|
self.assertEqual(hash(MediaAsset("path/to/css")), hash("path/to/css"))
|
||||||
|
self.assertEqual(
|
||||||
|
hash(MediaAsset("path/to/css")), hash(MediaAsset("path/to/css"))
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_str(self):
|
||||||
|
self.assertEqual(
|
||||||
|
str(MediaAsset("path/to/css")),
|
||||||
|
"http://media.example.com/static/path/to/css",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
str(MediaAsset("http://media.other.com/path/to/css")),
|
||||||
|
"http://media.other.com/path/to/css",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_repr(self):
|
||||||
|
self.assertEqual(repr(MediaAsset("path/to/css")), "MediaAsset('path/to/css')")
|
||||||
|
self.assertEqual(
|
||||||
|
repr(MediaAsset("http://media.other.com/path/to/css")),
|
||||||
|
"MediaAsset('http://media.other.com/path/to/css')",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_path(self):
|
||||||
|
asset = MediaAsset("path/to/css")
|
||||||
|
self.assertEqual(asset.path, "http://media.example.com/static/path/to/css")
|
||||||
|
|
||||||
|
asset = MediaAsset("http://media.other.com/path/to/css")
|
||||||
|
self.assertEqual(asset.path, "http://media.other.com/path/to/css")
|
||||||
|
|
||||||
|
asset = MediaAsset("https://secure.other.com/path/to/css")
|
||||||
|
self.assertEqual(asset.path, "https://secure.other.com/path/to/css")
|
||||||
|
|
||||||
|
asset = MediaAsset("/absolute/path/to/css")
|
||||||
|
self.assertEqual(asset.path, "/absolute/path/to/css")
|
||||||
|
|
||||||
|
asset = MediaAsset("//absolute/path/to/css")
|
||||||
|
self.assertEqual(asset.path, "//absolute/path/to/css")
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(STATIC_URL="http://media.example.com/static/")
|
||||||
|
class ScriptTestCase(SimpleTestCase):
|
||||||
|
def test_init_with_src_kwarg(self):
|
||||||
|
self.assertEqual(
|
||||||
|
Script(src="path/to/js").path, "http://media.example.com/static/path/to/js"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_str(self):
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
str(Script("path/to/js")),
|
||||||
|
'<script src="http://media.example.com/static/path/to/js"></script>',
|
||||||
|
)
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
str(Script("path/to/js", **{"async": True, "deferred": False})),
|
||||||
|
'<script src="http://media.example.com/static/path/to/js" async></script>',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
@ -714,63 +800,6 @@ class FormsMediaTestCase(SimpleTestCase):
|
|||||||
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(
|
@override_settings(
|
||||||
STATIC_URL="http://media.example.com/static/",
|
STATIC_URL="http://media.example.com/static/",
|
||||||
)
|
)
|
||||||
@ -779,17 +808,22 @@ class FormsMediaObjectTestCase(SimpleTestCase):
|
|||||||
|
|
||||||
def test_construction(self):
|
def test_construction(self):
|
||||||
m = Media(
|
m = Media(
|
||||||
css={"all": (CSS("path/to/css1", "all"), CSS("/path/to/css2", "all"))},
|
css={
|
||||||
|
"all": (
|
||||||
|
CSS("path/to/css1", media="all"),
|
||||||
|
CSS("/path/to/css2", media="all"),
|
||||||
|
)
|
||||||
|
},
|
||||||
js=(
|
js=(
|
||||||
JS("/path/to/js1"),
|
Script("/path/to/js1"),
|
||||||
JS("http://media.other.com/path/to/js2"),
|
Script("http://media.other.com/path/to/js2"),
|
||||||
JS(
|
Script(
|
||||||
"https://secure.other.com/path/to/js3",
|
"https://secure.other.com/path/to/js3",
|
||||||
integrity="9d947b87fdeb25030d56d01f7aa75800",
|
integrity="9d947b87fdeb25030d56d01f7aa75800",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertHTMLEqual(
|
||||||
str(m),
|
str(m),
|
||||||
'<link href="http://media.example.com/static/path/to/css1" media="all" '
|
'<link href="http://media.example.com/static/path/to/css1" media="all" '
|
||||||
'rel="stylesheet">\n'
|
'rel="stylesheet">\n'
|
||||||
@ -801,9 +835,9 @@ class FormsMediaObjectTestCase(SimpleTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
repr(m),
|
repr(m),
|
||||||
"Media(css={'all': ['path/to/css1', '/path/to/css2']}, "
|
"Media(css={'all': [CSS('path/to/css1'), CSS('/path/to/css2')]}, "
|
||||||
"js=['/path/to/js1', 'http://media.other.com/path/to/js2', "
|
"js=[Script('/path/to/js1'), Script('http://media.other.com/path/to/js2'), "
|
||||||
"'https://secure.other.com/path/to/js3'])",
|
"Script('https://secure.other.com/path/to/js3')])",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_simplest_class(self):
|
def test_simplest_class(self):
|
||||||
@ -823,22 +857,24 @@ class FormsMediaObjectTestCase(SimpleTestCase):
|
|||||||
def test_combine_media(self):
|
def test_combine_media(self):
|
||||||
class MyWidget1(TextInput):
|
class MyWidget1(TextInput):
|
||||||
class Media:
|
class Media:
|
||||||
css = {"all": (CSS("path/to/css1", "all"), "/path/to/css2")}
|
css = {"all": (CSS("path/to/css1", media="all"), "/path/to/css2")}
|
||||||
js = (
|
js = (
|
||||||
"/path/to/js1",
|
"/path/to/js1",
|
||||||
"http://media.other.com/path/to/js2",
|
"http://media.other.com/path/to/js2",
|
||||||
"https://secure.other.com/path/to/js3",
|
"https://secure.other.com/path/to/js3",
|
||||||
JS("/path/to/js4", integrity="9d947b87fdeb25030d56d01f7aa75800"),
|
Script(
|
||||||
|
"/path/to/js4", integrity="9d947b87fdeb25030d56d01f7aa75800"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
class MyWidget2(TextInput):
|
class MyWidget2(TextInput):
|
||||||
class Media:
|
class Media:
|
||||||
css = {"all": (CSS("/path/to/css2", "all"), "/path/to/css3")}
|
css = {"all": (CSS("/path/to/css2", media="all"), "/path/to/css3")}
|
||||||
js = (JS("/path/to/js1"), "/path/to/js4")
|
js = (Script("/path/to/js1"), "/path/to/js4")
|
||||||
|
|
||||||
w1 = MyWidget1()
|
w1 = MyWidget1()
|
||||||
w2 = MyWidget2()
|
w2 = MyWidget2()
|
||||||
self.assertEqual(
|
self.assertHTMLEqual(
|
||||||
str(w1.media + w2.media),
|
str(w1.media + w2.media),
|
||||||
'<link href="http://media.example.com/static/path/to/css1" media="all" '
|
'<link href="http://media.example.com/static/path/to/css1" media="all" '
|
||||||
'rel="stylesheet">\n'
|
'rel="stylesheet">\n'
|
||||||
@ -857,14 +893,14 @@ class FormsMediaObjectTestCase(SimpleTestCase):
|
|||||||
media = Media(
|
media = Media(
|
||||||
css={
|
css={
|
||||||
"all": (
|
"all": (
|
||||||
CSS("/path/to/css1", "all"),
|
CSS("/path/to/css1", media="all"),
|
||||||
CSS("/path/to/css1", "all"),
|
CSS("/path/to/css1", media="all"),
|
||||||
"/path/to/css1",
|
"/path/to/css1",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
js=(JS("/path/to/js1"), JS("/path/to/js1"), "/path/to/js1"),
|
js=(Script("/path/to/js1"), Script("/path/to/js1"), "/path/to/js1"),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertHTMLEqual(
|
||||||
str(media),
|
str(media),
|
||||||
'<link href="/path/to/css1" media="all" rel="stylesheet">\n'
|
'<link href="/path/to/css1" media="all" rel="stylesheet">\n'
|
||||||
'<script src="/path/to/js1"></script>',
|
'<script src="/path/to/js1"></script>',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user