script tag with additional attributes for django.forms.Media
Project description
Note! Django 5.2 adds its own support for JavaScript objects. This library has a slightly different API and also supports much older versions of Django, and it also supports CSS and JSON tags. As of the next version JS and CSS actually produce Django’s own Script and Stylesheet objects (backported on Django versions that lack them), so js_asset assets, plain path strings and Django’s native assets share the same de-duplication buckets in forms.Media – see Deduplication below.
Usage
Use this to insert a script tag via forms.Media containing additional attributes (such as id and data-* for CSP-compatible data injection.):
from js_asset import JS
forms.Media(js=[
JS("asset.js", {
"id": "asset-script",
"data-answer": "42",
}),
])
The rendered media tag (via {{ media.js }} or {{ media }} will now contain a script tag as follows, without line breaks:
<script type="text/javascript" src="/static/asset.js"
data-answer="42" id="asset-script"></script>
The attributes are automatically escaped. The data attributes may now be accessed inside asset.js:
let answer = document.querySelector("#asset-script").dataset.answer;
Also, because the implementation of static differs between supported Django versions (older do not take the presence of django.contrib.staticfiles in INSTALLED_APPS into account), a js_asset.static function is provided which does the right thing automatically.
CSS and JSON support
Since 3.0 django-js-asset also ships a CSS and JSON media object which can be used to ship stylesheets, inline styles and JSON blobs to the frontend. It’s recommended to pass those through forms.Media(js=[]) as well since js is a simple list while css uses a dictionary keyed with the media to use for the stylesheet.
So, you can add everything at once:
from js_asset import CSS, JS, JSON
forms.Media(js=[
JSON({"configuration": 42}, id="widget-configuration"),
CSS("widget/style.css"),
CSS("p{color:red;}", inline=True),
JS("widget/script.js", {"type": "module"}),
])
This produces:
<script id="widget-configuration" type="application/json">{"configuration": 42}</script>
<link href="/static/widget/style.css" media="all" rel="stylesheet">
<style media="all">p{color:red;}</style>
<script src="/static/widget/script.js" type="module"></script>
Compatibility
At the time of writing this app is compatible with Django 4.2 and better (up to and including the Django main branch), but have a look at the tox configuration for definitive answers.
Deduplication
JS and CSS produce Django’s own Script and Stylesheet objects (backported on Django versions that lack them), so they de-duplicate against each other, against plain path strings, and against Django’s native assets when forms.Media merges the media of several forms and widgets. The rules for what counts as a duplicate are Django’s, and Django changed them between releases:
Django 4.2 - 5.1 and 6.2+: two assets are equal when both the path and the attributes match, so JS("a.js", {"id": "x"}) and JS("a.js", {"id": "y"}) are distinct and both render.
Django 5.2 - 6.1: Django compares the path only, so those same two assets de-duplicate to one (whichever wins the merge) – exactly as Django’s own Script/Stylesheet behave on those versions.
A given file referenced both as a bare string and via JS/CSS always de-duplicates, and nothing is ever rendered twice for the same path. The per-version difference only affects the unusual case of the same path carried with different attributes in a single merge.
Import maps
django-js-asset can ship import maps. They let your modules import short names (import { Stuff } from "my-library") and have the browser resolve them to the real, possibly hashed, URLs produced by Django’s ManifestStaticFilesStorage – without rewriting the imports in your JavaScript.
Browsers do not reliably support more than one import map per page, so all of them have to be merged into one. js_asset.Media does this for you: drop ImportMap objects into your media wherever they are relevant – typically next to the module that needs them – and they are combined into a single <script type="importmap"> rendered before every other script, no matter how many media objects were added together to get there:
from js_asset import ImportMap, JS, Media, static_lazy
media = Media(js=[
ImportMap({"imports": {"my-library": static_lazy("my-library.js")}}),
JS("code.js", {"type": "module"}),
])
See CSP nonces below for per-request nonces.
Rendering media in views and the admin
js_asset.Media renders exactly like forms.Media: {{ media }} (i.e. str(media)) in a template emits the merged import map followed by every stylesheet and script – the import map first, so the modules can rely on it. What changes from one situation to the next is only how you obtain the right Media object.
In a front-facing view
When you own the template, build or collect the media in the view and put it in the context:
from django.shortcuts import render
from js_asset import ImportMap, JS, Media, static
def dashboard(request):
media = Media(js=[
ImportMap({"imports": {"chart": static("chart/index.js")}}),
JS("dashboard.js", {"type": "module"}),
])
return render(request, "dashboard.html", {"media": media})
form.media is already a js_asset.Media as soon as one of its widgets returns one, so you can render it directly. Combine your own assets with a form’s by simply adding them – as long as one side is a js_asset.Media the result stays one (regardless of order) and merges import maps. If neither side is, wrap one with Media.from_media first (Media(form.media) does not work, because forms.Media copies from a media definition, not an instance):
def edit(request):
form = MyForm()
page = Media(js=[
ImportMap({"imports": {"editor": static("editor/index.js")}}),
JS("editor/init.js", {"type": "module"}),
])
media = page + form.media # or: Media.from_media(form.media)
return render(request, "edit.html", {"form": form, "media": media})
{# edit.html #}
<head>{{ media }}</head>
...
{{ form }}
Attach a per-request CSP nonce with media.with_nonce(request.csp_nonce) – see CSP nonces below.
In the Django admin
In the admin you write no view code: the admin collects ModelAdmin.media together with every form’s and widget’s media itself and renders it in the change form. All you have to do is make sure the widget that needs the import map returns a js_asset.Media:
from django.contrib import admin
from django import forms
from js_asset import ImportMap, JS, Media, static
class EditorWidget(forms.Textarea):
@property
def media(self):
return Media(js=[
ImportMap({"imports": {"editor": static("editor/index.js")}}),
JS("editor/init.js", {"type": "module"}),
])
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = "__all__"
widgets = {"body": EditorWidget}
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
form = ArticleForm
Because js_asset.Media keeps its type through the additions Django performs while combining ModelAdmin.media with the form media, the object the admin finally renders is a js_asset.Media – so the import maps your widgets contribute are merged into the single tag automatically, ahead of the admin’s own scripts. The same widget works unchanged outside the admin.
CSP nonces
A CSP nonce is request-scoped – it must change on every response, while widget media is usually built once at class-definition time – so the nonce is applied when the media is rendered, not when it is constructed. js_asset.Media stores an optional nonce and applies it to every script and stylesheet it renders (a JSON block is data, not executable, and deliberately gets none). There are three ways to get the nonce in, depending on your Django version.
Django 6.2 and newer (built-in CSP)
Django 6.2 ships CSP support, and js_asset.Media plugs straight into it – no extra wiring. Configure CSP as usual:
# settings.py
from django.utils.csp import CSP
MIDDLEWARE = [
# ...
"django.middleware.csp.ContentSecurityPolicyMiddleware",
]
SECURE_CSP = {
"default-src": [CSP.SELF],
"script-src": [CSP.SELF, CSP.NONCE],
"style-src": [CSP.SELF, CSP.NONCE],
}
TEMPLATES = [{
# ...
"OPTIONS": {
"context_processors": [
# ...
"django.template.context_processors.csp",
],
},
}]
Then render the media with the built-in {% csp_nonce_attr %} tag, which calls media.render(attrs={"nonce": ...}) for you:
{% csp_nonce_attr form.media %}
That single tag emits the merged import map and every script/stylesheet, each carrying the per-request nonce.
Django 4.2 to 6.1 (with django-csp)
Older Django has no built-in nonce, so use the third-party django-csp package. Install it, add its middleware, and make sure the nonce is part of the relevant directives.
With django-csp 4.x the policy is a single setting and the nonce is a sentinel from csp.constants:
# settings.py (django-csp 4.x)
from csp.constants import NONCE, SELF
MIDDLEWARE = [
# ...
"csp.middleware.CSPMiddleware",
]
CONTENT_SECURITY_POLICY = {
"DIRECTIVES": {
"default-src": [SELF],
"script-src": [SELF, NONCE],
"style-src": [SELF, NONCE],
},
}
django-csp 3.x uses individual settings instead, and you opt the nonce into directives with CSP_INCLUDE_NONCE_IN:
# settings.py (django-csp 3.x)
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'",)
CSP_INCLUDE_NONCE_IN = ("script-src", "style-src")
Either way the middleware exposes the per-request nonce as request.csp_nonce. It is lazy: django-csp only adds the nonce to the response header once the value has actually been used. Rendering the media with it counts as using it, so you do not have to do anything special – just make sure you render through js_asset. Attach the nonce in the view and render the copy:
def my_view(request):
form = MyForm()
return render(request, "page.html", {
"form_media": form.media.with_nonce(request.csp_nonce),
})
{{ form_media }}
with_nonce() returns a copy, so a shared/cached widget media object is never mutated and one request’s nonce can never leak into another. If you would rather stay in the template, drop in a small tag (the request context processor must be enabled). It also copes with a plain forms.Media – Media(form.media) does not work, because forms.Media copies assets from a media definition, not an instance, so use from_media:
# yourapp/templatetags/js_asset_csp.py
from django import template
from js_asset import Media
register = template.Library()
@register.simple_tag(takes_context=True)
def media_with_nonce(context, media):
nonce = getattr(context.get("request"), "csp_nonce", "")
if not isinstance(media, Media):
media = Media.from_media(media)
return media.with_nonce(nonce).render()
{% load js_asset_csp %}
{% media_with_nonce form.media %}
Anywhere: set the nonce explicitly
You can always set the nonce yourself, either on construction or per request:
Media(nonce=the_nonce, js=[...]) # at construction
some_media.with_nonce(the_nonce) # copy with a nonce
some_media.render(nonce=the_nonce) # one-off render
Notes
The merged import map is always rendered first, so a module added in the same media can rely on it.
Browsers honour only the first import map on a page; make sure everything that needs the same map ends up in one merged Media – a second <script type="importmap"> reaching the page some other way is silently ignored.
Import maps are subject to script-src; make sure CSP.NONCE is present there (and in style-src if you render stylesheets).
A stylesheet placed in js=[...] is only de-duplicated against that list. forms.Media keeps the css={...} dictionary in a separate slot, so the same file listed in both renders twice – pick one.
JSON blocks are data and deliberately get no nonce.
Browser support for import maps is still uneven; merging into a single map is currently the only portable way to use them in production.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_js_asset-4.0.0.tar.gz.
File metadata
- Download URL: django_js_asset-4.0.0.tar.gz
- Upload date:
- Size: 26.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c349cf5a08ba8eabad47689546de4f25ea927968f8b929b1d3cb683b6f424653
|
|
| MD5 |
ba6c0c3a729bf972026641da1e44faa2
|
|
| BLAKE2b-256 |
fe304f3999a9dcc4ddeabac4b40fa98ba8cc2085e396c0b33fcd7b840cb0373e
|
Provenance
The following attestation bundles were made for django_js_asset-4.0.0.tar.gz:
Publisher:
publish.yml on feincms/django-js-asset
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_js_asset-4.0.0.tar.gz -
Subject digest:
c349cf5a08ba8eabad47689546de4f25ea927968f8b929b1d3cb683b6f424653 - Sigstore transparency entry: 1790599813
- Sigstore integration time:
-
Permalink:
feincms/django-js-asset@b3c515c793185d96d43124a8239267a535bdd18f -
Branch / Tag:
refs/tags/4.0 - Owner: https://github.com/feincms
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b3c515c793185d96d43124a8239267a535bdd18f -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_js_asset-4.0.0-py3-none-any.whl.
File metadata
- Download URL: django_js_asset-4.0.0-py3-none-any.whl
- Upload date:
- Size: 13.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ad3b32778a918daa67d9bff0759da2959514866c45b30203f2769e3ec01b3e67
|
|
| MD5 |
9ed7307592e8a74d893ba4ebe36eb6e8
|
|
| BLAKE2b-256 |
1697790afa149580543d397dbc3ea925214e544fcd2053248089310303fd6500
|
Provenance
The following attestation bundles were made for django_js_asset-4.0.0-py3-none-any.whl:
Publisher:
publish.yml on feincms/django-js-asset
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_js_asset-4.0.0-py3-none-any.whl -
Subject digest:
ad3b32778a918daa67d9bff0759da2959514866c45b30203f2769e3ec01b3e67 - Sigstore transparency entry: 1790599872
- Sigstore integration time:
-
Permalink:
feincms/django-js-asset@b3c515c793185d96d43124a8239267a535bdd18f -
Branch / Tag:
refs/tags/4.0 - Owner: https://github.com/feincms
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b3c515c793185d96d43124a8239267a535bdd18f -
Trigger Event:
push
-
Statement type: