pax_global_header00006660000000000000000000000064152442166740014525gustar00rootroot0000000000000052 comment=c9efba387b2cb48eeb68e3f2a2c86e6322c27c71 feincms-django-js-asset-e73dd4d/000077500000000000000000000000001524421667400166405ustar00rootroot00000000000000feincms-django-js-asset-e73dd4d/.editorconfig000066400000000000000000000003101524421667400213070ustar00rootroot00000000000000# top-most EditorConfig file root = true [*] end_of_line = lf insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true indent_style = space indent_size = 2 [*.py] indent_size = 4 feincms-django-js-asset-e73dd4d/.github/000077500000000000000000000000001524421667400202005ustar00rootroot00000000000000feincms-django-js-asset-e73dd4d/.github/dependabot.yml000066400000000000000000000005241524421667400230310ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" groups: github-actions: patterns: - "*" - package-ecosystem: "pip" directory: "/" schedule: interval: "monthly" groups: python-deps: patterns: - "*" feincms-django-js-asset-e73dd4d/.github/workflows/000077500000000000000000000000001524421667400222355ustar00rootroot00000000000000feincms-django-js-asset-e73dd4d/.github/workflows/publish.yml000066400000000000000000000014171524421667400244310ustar00rootroot00000000000000name: Publish to PyPI on: push: tags: - '*' jobs: build: name: Build distribution runs-on: ubuntu-latest if: github.event.repository.fork == false steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v7 with: python-version: 3.x - name: Build run: | python -m pip install --upgrade build python -m build - uses: actions/upload-artifact@v7 with: name: dist path: dist/ publish: name: Publish to PyPI needs: build runs-on: ubuntu-latest environment: release permissions: id-token: write steps: - uses: actions/download-artifact@v8 with: name: dist path: dist/ - uses: pypa/gh-action-pypi-publish@release/v1 feincms-django-js-asset-e73dd4d/.github/workflows/tests.yml000066400000000000000000000016131524421667400241230ustar00rootroot00000000000000name: Tests on: push: branches: [main] pull_request: branches: [main] schedule: - cron: 37 1 1 * * jobs: tests: name: Python ${{ matrix.python-version }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: - '3.10' - '3.11' - '3.12' - '3.13' - '3.14' steps: - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip wheel setuptools tox - name: Run tox targets for ${{ matrix.python-version }} run: | ENV_PREFIX=$(tr -C -d "0-9" <<< "${{ matrix.python-version }}") TOXENV=$(tox --listenvs | grep "^py$ENV_PREFIX" | tr '\n' ',') python -m tox feincms-django-js-asset-e73dd4d/.gitignore000066400000000000000000000002301524421667400206230ustar00rootroot00000000000000*.py? *~ *.sw? \#*# /secrets.py .DS_Store ._* /*.egg-info /MANIFEST /_build /build /dist tests/test.zip /docs/_build /.eggs .coverage htmlcov venv .tox feincms-django-js-asset-e73dd4d/.pre-commit-config.yaml000066400000000000000000000016711524421667400231260ustar00rootroot00000000000000exclude: ".yarn/|yarn.lock|\\.min\\.(css|js)$" repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: check-added-large-files - id: check-builtin-literals - id: check-executables-have-shebangs - id: check-merge-conflict - id: check-toml - id: check-yaml - id: detect-private-key - id: end-of-file-fixer - id: mixed-line-ending - id: trailing-whitespace - repo: https://github.com/adamchainz/django-upgrade rev: 1.32.0 hooks: - id: django-upgrade args: [--target-version, "3.2"] - repo: https://github.com/astral-sh/ruff-pre-commit rev: "v0.16.4" hooks: - id: ruff - id: ruff-format - repo: https://github.com/tox-dev/pyproject-fmt rev: v2.28.1 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject rev: '0.26' hooks: - id: validate-pyproject feincms-django-js-asset-e73dd4d/.readthedocs.yaml000066400000000000000000000003311524421667400220640ustar00rootroot00000000000000# Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 build: os: ubuntu-24.04 tools: python: "3.13" sphinx: configuration: docs/conf.py feincms-django-js-asset-e73dd4d/AGENTS.md000066400000000000000000000150271524421667400201500ustar00rootroot00000000000000# django-js-asset — agent notes `JS`, `CSS`, `JSON` support for `django.forms.Media`, plus importmap and CSP nonce support. ## Running tests Use **tox** (configured with `tox-uv`, so it provisions the right Python/Django automatically — do not hand-roll a venv): ```bash tox -e py312-dj42 # lowest supported combination tox -e py314-dj62 # a recent combination tox run-parallel # the whole matrix ``` The matrix lives in `tox.ini` (`tests/manage.py test testapp`). ## Compatibility (hard constraint) - Python `>=3.10`, Django `>=4.2`. **Django 6.2+ is NOT an acceptable floor.** - `JS`/`CSS` *produce* Django's own `Script`/`Stylesheet` (see Layout). Where Django lacks them they are **backported** in `js_asset/_compat.py`, so the 4.2 floor holds — we provide the machinery, we don't *depend* on Django having it. Availability in real Django: `Script` (5.2+), `Stylesheet` + `MediaAsset.render(attrs=)` (6.1+), attribute-aware `__eq__`/`__hash__` (6.2+). The `<5.2` backport mirrors the **6.2** contract. - Don't rely on Django's `MediaAsset.render(attrs=...)` existing: it is absent on 5.2/6.0. `media.py:_render_asset` injects the nonce itself by rebuilding the tag from `element_template` + `flatatt`, which works on every version. - Cross-version gotcha: Django >= 6.1 wraps bare js/css path strings into `Script`/`Stylesheet` in `Media._js`/`._css`; older Django keeps raw strings. `media.py:_render_{js,css}` wrap any leftover strings via `JS()`/`CSS()`, so `_render_asset` always sees a `MediaAsset` (or `JSON`/`ImportMap`). - **Never test an asset with `isinstance(item, str)`.** `SafeString` is a `str` subclass, so `mark_safe('')` — a complete tag that must render verbatim — would be resolved through `static()` and percent-encoded. The predicate is `hasattr(item, "__html__")`; only bare paths get wrapped. Django made exactly this mistake in 6.1 (ticket #37262, fixed on its 6.1.x branch for 6.1.1), and we had it independently in `_render_{js,css}`. - Because Django 6.1 mangles such strings in `forms.Media.__init__` itself, `media.py` also overrides `_normalize_{js,css}` with the `__html__` predicate — those hooks are called through `self`, so a subclass can fix them, and `_compat` gives us `Script`/`Stylesheet` on every supported Django so the wrapped output is unchanged. This covers media built *through* our class; assets adopted from a foreign `forms.Media` (`from_media`, `__add__`, and Django's widget `media_property`, which always instantiates `forms.Media`) arrive already normalized and cannot be recovered on 6.1. `test_html_safe_strings_adopted_from_foreign_media` pins that boundary via the `DJANGO_KEEPS_HTML_SAFE_STRINGS` probe. - `tests/testapp/test_media.py` carries `JS_ASSETS`/`CSS_ASSETS`: one row per asset kind, each with its exact rendering with and without a nonce. **Add a row whenever a new asset kind appears** — that table is the guard against a rendering branch quietly mishandling one of them. `test_matches_django_rendering` compares us against stock `forms.Media` for path assets; it is what catches Django changing its normalization or tag format under our overrides. ## Layout - `js_asset/_compat.py` — `MediaAsset`/`Script`/`Stylesheet`: imported from Django where present, backported (with the 6.2 contract) below 5.2/6.1. - `js_asset/js.py` — `JS`/`CSS` are **factories** (a `_ProducesAsset` metaclass): calling them returns a Django `Script`/`Stylesheet`/`InlineStyle` so they dedup in `forms.Media.merge` against native assets *and* bare path strings; `isinstance(x, JS)` still works via `__instancecheck__`. `JSON` and `ImportMap` have no Django counterpart and stay standalone `@html_safe` objects with `render(*, nonce="")`. Output is byte-identical to native Django assets (flatatt sorts attributes), which the exact-string tests depend on. Equality is Django's, so dedup is attribute-aware on 4.2-5.1 + 6.2+ and path-only on 5.2-6.1 (`test_set` derives its expectation from this). `InlineStyle` renders its CSS **verbatim** (`path` returns `mark_safe`): ` Inline CSS is rendered verbatim -- a ``` block whose `src` is *CSS text*, not a path; `Stylesheet` always renders `` and runs `_path` through `static()`. So `CSS` is a factory that returns a small dedicated `InlineStyle(MediaAsset)` with a `` template and a `path` override that returns the source verbatim. It is still a `MediaAsset`, so it flows through the same render path; its `_path`-keyed equality is on CSS text, which is odd but harmless (inline blocks are rarely deduped). 2. **`JSON` and `ImportMap` stay custom.** No Django counterpart (`json_script` / `' def __init__(self, src, **attributes): # Alter the signature to allow src to be passed as a keyword argument. super().__init__(src, **attributes) if django.VERSION >= (6, 1): from django.forms.widgets import Stylesheet else: class Stylesheet(MediaAsset): element_template = '' def __init__(self, href, **attributes): super().__init__(href, rel="stylesheet", **attributes) feincms-django-js-asset-e73dd4d/js_asset/js.py000066400000000000000000000130121524421667400214360ustar00rootroot00000000000000from __future__ import annotations import json from dataclasses import dataclass, field from typing import Any from django.core.serializers.json import DjangoJSONEncoder from django.forms.utils import flatatt from django.templatetags.static import static from django.utils.functional import lazy from django.utils.html import html_safe, json_script, mark_safe from js_asset._compat import MediaAsset, Script, Stylesheet __all__ = [ "CSS", "JS", "JSON", "ImportMap", "InlineStyle", "MediaAsset", "Script", "Stylesheet", "static", "static_lazy", ] static_lazy = lazy(static, str) def _canonical_hash(data): # Hash an order-insensitive canonical form so the hash stays consistent # with dict equality (``a == b`` must imply ``hash(a) == hash(b)``). # ``DjangoJSONEncoder`` resolves lazy values, e.g. ``static_lazy`` paths. return hash(json.dumps(data, sort_keys=True, cls=DjangoJSONEncoder)) class InlineStyle(MediaAsset): """ An inline ``" def __init__(self, css, *, media="all", **attributes): # ``', ) def test_inline_css_is_not_escaped(self): # A ``', ) def test_inline_css_cannot_close_the_style_element(self): # The one sequence which would let the CSS escape its element. for css in ("a{}", "a{}"): with self.subTest(css=css), self.assertRaises(ValueError): CSS(css, inline=True) def test_js_produces_script(self): asset = JS("app/asset.js", {"id": "x"}) # ``JS`` is a factory producing a Django ``Script`` ... self.assertIsInstance(asset, Script) self.assertNotIsInstance(asset, Stylesheet) # ... while ``isinstance(x, JS)`` keeps answering truthfully. self.assertIsInstance(asset, JS) self.assertNotIsInstance(CSS("app/style.css"), JS) def test_css_produces_stylesheet_or_inline(self): link = CSS("app/style.css") inline = CSS("p{color:red}", inline=True) self.assertIsInstance(link, Stylesheet) self.assertIsInstance(inline, InlineStyle) # Both flavours answer ``isinstance(x, CSS)``. self.assertIsInstance(link, CSS) self.assertIsInstance(inline, CSS) self.assertNotIsInstance(JS("app/asset.js"), CSS) def test_json(self): self.assertEqual( str(JSON({"hello": "world"}, id="hello")), '', ) self.assertEqual( str(JSON({"hello": "world"})), '', ) feincms-django-js-asset-e73dd4d/tests/testapp/test_media.py000066400000000000000000000531111524421667400241530ustar00rootroot00000000000000from unittest import skipIf from django.forms import Media as DjangoMedia from django.test import TestCase from django.utils.functional import SimpleLazyObject, empty from django.utils.html import html_safe from django.utils.safestring import mark_safe from js_asset import CSS, JS, JSON, ImportMap, Media, Script, Stylesheet try: # Django >= 6.1 from django.utils.csp import LazyNonce as DjangoLazyNonce, nonce_attr except ImportError: DjangoLazyNonce = nonce_attr = None @html_safe class HTMLOnlyAsset: """ A media asset using Django's plain ``__html__`` contract -- not a ``MediaAsset`` and not one of our ``ImportMap``/``JSON`` types. Stock ``forms.Media`` renders such assets via ``__html__()``. """ def __init__(self, markup): self._markup = markup def __str__(self): return self._markup # Django 6.1 normalizes *every* js/css string -- html-safe ones included -- # into ``Script``/``Stylesheet`` inside ``forms.Media.__init__`` (ticket #37262, # fixed on the 6.1.x branch for 6.1.1). Our ``Media`` overrides the normalizers # so media built through *our* class is unaffected, but assets adopted from a # foreign ``forms.Media`` arrive already mangled. This probe marks that window. _PROBE = mark_safe('') DJANGO_KEEPS_HTML_SAFE_STRINGS = str(DjangoMedia(js=[_PROBE])) == _PROBE HTML_SAFE_JS = mark_safe('') HTML_SAFE_CSS = mark_safe( '' ) # (label, asset, rendering without a nonce, rendering with nonce="n0nce"). # # One row per kind of asset a ``Media`` can carry. Add a row whenever a new # asset kind appears -- this is the guard against a rendering branch (such as # the "is it a bare path?" test in ``Media._render_js``/``._render_css``) # quietly mishandling one of them. JS_ASSETS = [ ( "bare path string", "app.js", '', '', ), ( "JS factory", JS("app.js"), '', '', ), ( "JS factory with attributes", JS("app.js", {"type": "module"}), '', '', ), ( "Script object", Script("app.js"), '', '', ), ( "JSON block", JSON({"a": 1}, id="cfg"), # Data, not executed script: no nonce either way. '', '', ), ( "ImportMap", ImportMap({"imports": {"a": "/static/a.js"}}), '', ( '' ), ), ( "object with only __html__", HTMLOnlyAsset(''), # Opaque markup: rendered verbatim, and the nonce cannot be threaded in # (same as stock ``forms.Media``). '', '', ), ( "html-safe string", HTML_SAFE_JS, # A ``SafeString`` is a ``str``, but it is a complete tag rather than a # path: it must never be resolved through ``static()``. HTML_SAFE_JS, HTML_SAFE_JS, ), ] CSS_ASSETS = [ ( "bare path string", "app.css", '', '', ), ( "CSS factory", CSS("app.css"), '', '', ), ( # Django's ``Stylesheet`` has no implicit ``media``; unlike the ``CSS`` # factory it only emits the attribute when one is passed. The dict key # it is filed under does not add one. "Stylesheet object", Stylesheet("app.css"), '', '', ), ( "inline CSS", CSS("body{color:red}", inline=True), '', '', ), ( # ``', '', ), ( "object with only __html__", HTMLOnlyAsset(''), '', '', ), ( "html-safe string", HTML_SAFE_CSS, HTML_SAFE_CSS, HTML_SAFE_CSS, ), ] class AssetRenderingTest(TestCase): """ Exhaustive per-asset-kind rendering, with and without a CSP nonce. """ def _check(self, rows, kwargs_for): for label, asset, plain, with_nonce in rows: with self.subTest(asset=label): self.assertEqual(Media(**kwargs_for(asset)).render(), plain) self.assertEqual( Media(nonce="n0nce", **kwargs_for(asset)).render(), with_nonce ) def test_js_assets(self): self._check(JS_ASSETS, lambda asset: {"js": [asset]}) def test_css_assets(self): self._check(CSS_ASSETS, lambda asset: {"css": {"all": [asset]}}) def test_matches_django_rendering(self): # Our rendering must not drift from stock ``forms.Media`` for the asset # kinds Django itself understands -- this is what catches a future # change to Django's normalization or tag format that our overridden # ``_normalize_{js,css}`` would otherwise silently skip. (Excludes # ``ImportMap``, which we deliberately merge and hoist, and html-safe # strings, which Django 6.1 itself gets wrong.) kwargs = { "css": {"all": ["a.css", CSS("b.css"), Stylesheet("c.css")]}, "js": ["a.js", JS("b.js"), Script("c.js"), JS("d.js", {"defer": True})], } self.assertEqual(str(Media(**kwargs)), str(DjangoMedia(**kwargs))) class MediaTest(TestCase): def test_nonce_applied_to_assets(self): media = Media( nonce="r@nd0m", css={"all": [CSS("app.css")]}, js=[JS("app.js")], ) html = media.render() self.assertInHTML( '', html, ) self.assertInHTML( '', html, ) def test_no_nonce_keeps_plain_output(self): media = Media(js=[JS("app.js")]) self.assertEqual(media.render(), '') def test_importmaps_are_merged_and_rendered_first(self): media = Media( nonce="r@nd0m", js=[ ImportMap({"imports": {"a": "/static/a.js"}}), JS("app.js", {"type": "module"}), ImportMap({"imports": {"b": "/static/b.js"}}), ], ) self.assertEqual( media.render(), '\n' '', ) def test_type_and_nonce_preserved_when_merging(self): ours = Media(nonce="abc", js=[ImportMap({"imports": {"a": "/static/a.js"}})]) plain = DjangoMedia(js=["app.js"]) # Our Media on the right-hand side (the case plain forms.Media drops). merged = plain + ours self.assertIsInstance(merged, Media) self.assertEqual(merged.nonce, "abc") # ... and on the left-hand side. merged = ours + plain self.assertIsInstance(merged, Media) self.assertEqual(merged.nonce, "abc") def test_bare_string_assets_get_nonce(self): # Bare paths stay strings on Django < 6.2 and are wrapped into # MediaAsset objects on >= 6.2; both paths must receive the nonce. media = Media(nonce="n0nce", css={"all": ["plain.css"]}, js=["plain.js"]) html = media.render() self.assertInHTML( '', html, ) self.assertInHTML( '', html, ) def test_merging_combines_css_and_js(self): ours = Media(nonce="n", css={"all": [CSS("a.css")]}, js=[JS("a.js")]) plain = DjangoMedia(css={"all": ["b.css"]}, js=["b.js"]) merged = plain + ours # via __radd__ self.assertIsInstance(merged, Media) html = merged.render() for snippet in ( '', '', '', '', ): self.assertInHTML(snippet, html) def test_render_accepts_attrs_nonce(self): # Django >= 6.2's CSP integration calls media.render(attrs={"nonce": ...}). media = Media(js=[JS("app.js")]) self.assertInHTML( '', media.render(attrs={"nonce": "from-attrs"}), ) # A passed nonce overrides the stored one. media = Media(nonce="stored", js=[JS("app.js")]) self.assertInHTML( '', media.render(attrs={"nonce": "from-attrs"}), ) def test_html_only_asset_renders_via_html(self): # A plain ``__html__``-only asset (e.g. a build-tool helper) is neither # a MediaAsset nor one of our types; it must fall back to ``__html__()`` # the way stock ``forms.Media`` does, instead of crashing on a missing # ``render`` method. media = Media(js=[HTMLOnlyAsset('')]) self.assertEqual(media.render(), '') def test_html_only_asset_renders_with_nonce_set(self): # The nonce cannot be threaded into an opaque ``__html__`` asset (same # as stock Django), but its presence must not break rendering. media = Media( nonce="n0nce", js=[HTMLOnlyAsset('')], ) self.assertEqual(media.render(), '') def test_json_asset_rendered_through_media(self): # A JSON block carried as a JS asset renders via its own ``render``. # It is data, not executed script, so it stays nonce-free even when a # nonce is set on the media. media = Media(nonce="n0nce", js=[JSON({"a": 1}, id="cfg")]) self.assertInHTML( '', media.render(), ) def test_adding_non_media_is_not_supported(self): with self.assertRaises(TypeError): Media() + 3 # The reverse case dispatches to Media.__radd__, which must also return # NotImplemented (a clean TypeError) rather than raise AttributeError. with self.assertRaises(TypeError): 3 + Media() def test_from_media_wraps_existing_instance(self): plain = DjangoMedia( js=[ImportMap({"imports": {"a": "/static/a.js"}}), JS("app.js")] ) wrapped = Media.from_media(plain, nonce="w") self.assertIsInstance(wrapped, Media) self.assertEqual(wrapped.nonce, "w") html = wrapped.render() self.assertInHTML( '', html, ) self.assertInHTML( '', html, ) def test_with_nonce_returns_a_copy(self): media = Media(js=[JS("app.js")]) request_media = media.with_nonce("xyz") self.assertIsInstance(request_media, Media) self.assertEqual(request_media.nonce, "xyz") # The shared (e.g. cached widget) media is left untouched. self.assertEqual(media.nonce, "") self.assertInHTML( '', request_media.render(), ) def test_html_safe_strings_dedupe_and_merge(self): # Mirrors Django's own #37262 tests: html-safe strings survive # deduplication and media merging alongside regular path assets. first = Media( css={"all": [HTML_SAFE_CSS, "a.css"]}, js=["a.js", HTML_SAFE_JS], ) second = Media( nonce="n0nce", css={"all": [HTML_SAFE_CSS]}, js=[HTML_SAFE_JS, JS("b.js")], ) merged = first + second self.assertEqual(merged.nonce, "n0nce") self.assertEqual( merged.render(), f"{HTML_SAFE_CSS}\n" '\n' '\n' f"{HTML_SAFE_JS}\n" '', ) def test_html_safe_strings_normalized_through_our_class(self): # We override ``_normalize_{js,css}`` so html-safe strings survive # construction even on Django 6.1, whose own normalizers mangle them. self.assertEqual(Media(js=[HTML_SAFE_JS]).render(), HTML_SAFE_JS) self.assertEqual( Media(media=type("Def", (), {"js": [HTML_SAFE_JS]})).render(), HTML_SAFE_JS, ) def test_html_safe_strings_adopted_from_foreign_media(self): # ``from_media`` adopts assets a plain ``forms.Media`` already # normalized, so on Django 6.1 the damage is done before we see them. # Nothing to fix from here -- just pin which side the boundary is on. adopted = Media.from_media(DjangoMedia(js=[HTML_SAFE_JS])) if DJANGO_KEEPS_HTML_SAFE_STRINGS: self.assertEqual(adopted.render(), HTML_SAFE_JS) else: self.assertNotEqual(adopted.render(), HTML_SAFE_JS) self.assertEqual(adopted.render(), str(DjangoMedia(js=[HTML_SAFE_JS]))) class LazyNonce(SimpleLazyObject): """ A stand-in for ``django.utils.csp.LazyNonce`` (Django >= 6.2), which is *falsy* until the nonce has been generated so that a nonce is only added to the CSP header if the template actually used it. """ def __init__(self, value): super().__init__(lambda: value) def __bool__(self): return self._wrapped is not empty class LazyNonceTest(TestCase): def test_lazy_nonce_from_attrs(self): # Django >= 6.2 renders media as ``media.render(attrs={"nonce": nonce})`` # with the lazy nonce straight from the template context. Truth-testing # it says "no nonce" as long as it has not been read yet, so the nonce # must be resolved instead of checked for truthiness. media = Media(js=[JS("app.js")]) self.assertEqual( media.render(attrs={"nonce": LazyNonce("l@zy")}), '', ) def test_lazy_nonce_stored_on_the_media(self): media = Media(nonce=LazyNonce("l@zy"), js=[JS("app.js")]) self.assertEqual( media.render(), '', ) self.assertEqual( media.with_nonce(LazyNonce("l@zy")).render(), '', ) def test_lazy_nonce_applied_to_importmaps(self): media = Media(js=[ImportMap({"imports": {"a": "/static/a.js"}})]) self.assertEqual( media.render(nonce=LazyNonce("l@zy")), '', ) def test_lazy_nonce_untouched_for_empty_media(self): # Nothing to render, so the nonce must not be generated: on Django the # CSP header only carries a nonce if one was actually used. nonce = LazyNonce("l@zy") self.assertEqual(Media().render(attrs={"nonce": nonce}), "") self.assertFalse(nonce) @skipIf(nonce_attr is None, "Django < 6.1 has no built-in CSP support") def test_csp_nonce_attr_template_tag(self): # The real thing, on the Django versions which have it. nonce = DjangoLazyNonce() html = nonce_attr( {"csp_nonce": nonce}, Media(js=[ImportMap({"imports": {"a": "/static/a.js"}}), JS("app.js")]), ) self.assertTrue(nonce) self.assertEqual( html, f'\n' f'', ) class RenderPartsTest(TestCase): """ ``render_css()``/``render_js()`` are ``forms.Media`` API in their own right; they must behave like ``render()`` regarding the nonce and import maps. """ def setUp(self): self.media = Media( nonce="n0nce", css={"all": [CSS("app.css")]}, js=[ ImportMap({"imports": {"a": "/static/a.js"}}), JS("app.js"), ImportMap({"imports": {"b": "/static/b.js"}}), ], ) def test_render_css_applies_the_nonce(self): self.assertEqual( list(self.media.render_css()), [ '' ], ) def test_render_js_applies_the_nonce_and_merges_importmaps(self): self.assertEqual( list(self.media.render_js()), [ ( '' ), '', ], ) def test_render_parts_accept_attrs(self): media = Media(css={"all": [CSS("app.css")]}, js=[JS("app.js")]) attrs = {"nonce": "from-attrs"} self.assertEqual( list(media.render_css(attrs=attrs)), [ ( '' ) ], ) self.assertEqual( list(media.render_js(attrs=attrs)), [''], ) class GetItemTest(TestCase): """ ``media["css"]`` / ``media["js"]`` -- reached as ``{{ media.css }}`` and ``{{ media.js }}`` from templates -- must keep our type and its nonce. """ def setUp(self): self.media = Media( nonce="n0nce", css={"all": [CSS("app.css")]}, js=[ ImportMap({"imports": {"a": "/static/a.js"}}), JS("app.js"), ImportMap({"imports": {"b": "/static/b.js"}}), ], ) def test_css_subset(self): subset = self.media["css"] self.assertIsInstance(subset, Media) self.assertEqual(subset.nonce, "n0nce") self.assertEqual( subset.render(), '', ) def test_js_subset(self): subset = self.media["js"] self.assertIsInstance(subset, Media) self.assertEqual( subset.render(), '\n' '', ) def test_unknown_media_type(self): with self.assertRaises(KeyError): self.media["json"] @skipIf(nonce_attr is None, "Django < 6.1 has no built-in CSP support") def test_csp_nonce_attr_on_a_subset(self): # What Django's own admin templates do: {% csp_nonce_attr media.js %}. nonce = DjangoLazyNonce() media = Media(js=[ImportMap({"imports": {"a": "/static/a.js"}}), JS("app.js")]) self.assertEqual( nonce_attr({"csp_nonce": nonce}, media["js"]), f'\n' f'', ) feincms-django-js-asset-e73dd4d/tox.ini000066400000000000000000000012161524421667400201530ustar00rootroot00000000000000[tox] envlist = py{310,311}-dj{42} py{312}-dj{42,52,60,main} py{313}-dj{52,60,61,main} py{314}-dj{52,60,61,main} [testenv] usedevelop = true extras = tests commands = python -Wd {envbindir}/coverage run tests/manage.py test -v2 --keepdb {posargs:testapp} coverage report -m deps = dj42: Django>=4.2,<5.0 dj50: Django>=5.0,<5.1 dj51: Django>=5.1,<5.2 dj52: Django>=5.2,<6.0 dj60: Django>=6.0,<6.1 dj61: Django>=6.1a1,<6.2 djmain: https://github.com/django/django/archive/main.tar.gz [testenv:docs] deps = Sphinx changedir = docs commands = make html skip_install = true allowlist_externals = make