Metadata-Version: 2.4
Name: django-include-media
Version: 0.2.1
Summary: Include Django Media assets into <head> using template tags
Author: Django Include Media Contributors
License-Expression: BSD-3-Clause
Project-URL: Homepage, https://github.com/blighj/django-include-media
Project-URL: Repository, https://github.com/blighj/django-include-media
Project-URL: Issues, https://github.com/blighj/django-include-media/issues
Keywords: django,media,assets,templatetags,css,javascript
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django>=5.2
Provides-Extra: dev
Requires-Dist: black>=22.0; extra == "dev"
Requires-Dist: coverage>=7.0; extra == "dev"
Requires-Dist: Django>=5.2; extra == "dev"
Requires-Dist: flake8>=4.0; extra == "dev"
Requires-Dist: isort>=5.0; extra == "dev"
Requires-Dist: pre-commit>=3.0; extra == "dev"
Dynamic: license-file

# Django include media

An app for Django that allows templates and views to add Script/Stylesheets to
a page using Django's `forms.Media` object, with automatic collection and
deduplication, outputting assets into `<head>`. Inspired by django-sekizai.

## Installation

```bash
pip install django-include-media
```

Add to `INSTALLED_APPS`:

```python
INSTALLED_APPS = [
    ...
    "include_media",
]
```

## Usage

Place `{% include_media %}` in `<head>` of your base template. Then use the
`use_media` templatetag or `page_media` context to add the assets you need.

All your sub-templates or templates from templatetags can now reliably add
assets to the page.

```html
{# base.html #}
{% load include_media_tags %}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    {% include_media %}
</head>
<body>
    {% block content %}{% endblock %}
</body>
</html>
```

### Component and template assets

Declare assets inline with `{% use_media %}`. Assets are deduplicated by
object identity, including the same component twice only renders its
assets once:

```html
{% load include_media_tags %}
{% use_media form.media %}
{% use_media css="myapp/widget.css" %}
{% use_media js="myapp/script.js" %}
```

Extra HTML attributes can be passed as keyword arguments and are forwarded
to the rendered tag. Add `csp_nonce_attr` to opt a specific asset into
Django's CSP nonce (Django 6.0+); the nonce is applied if `csp_nonce` is
present in the template context and is a no-op otherwise:

```html
{% use_media js="myapp/widget.js" type="module" %}
{% use_media js="myapp/widget.js" type="module" csp_nonce_attr %}
{% use_media css="myapp/widget.css" media="print" %}
{% use_media form.media csp_nonce_attr %}
```

### View-level assets

Pass `page_media` via `get_context_data`. If a site-wide context processor
also sets `page_media`, the two are merged automatically:

```python
from django.forms import Media
from django.forms.widgets import Script
from include_media import Stylesheet
from django.views.generic import TemplateView

class DatePickerView(TemplateView):
    template_name = "datepicker.html"

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["page_media"] = Media(
            css={"all":[Stylesheet("datepicker/datepicker.css")]},
            js=[Script("datepicker/datepicker.js", type="module")],
        )
        return ctx
```

### Site-wide assets

Declare assets required on every page in a context processor:

```python
# myproject/context_processors.py
from django.forms import Media
from django.forms.widgets import Script
from include_media import Stylesheet

def site_media(request):
    nonce = getattr(request, "csp_nonce", None)
    attrs = {"nonce": nonce} if nonce else {}
    media = Media(
        css={"all":[Stylesheet("base.css", **attrs)]},
        js=[Script("base.js", type="module", **attrs)],
    )
    if request.user.is_authenticated:
        media += Media(js=[Script("dashboard.js", type="module", **attrs)])
    return {"page_media": media}
```

```python
TEMPLATES = [{
    "OPTIONS": {
        "context_processors": [
            ...
            "myproject.context_processors.site_media",
        ],
    },
}]
```

### App-level registration

Apps can use the `register()` method to add app level assets that are linked to
a `registered_media` context_processor.

```python
# myapp/apps.py
from django.apps import AppConfig

class MyAppConfig(AppConfig):
    name = "myapp"

    def ready(self):
        from django.forms import Media
        from django.forms.widgets import Script
        from include_media import register, Stylesheet

        register(Media(
            css={"all": [Stylesheet("myapp/base.css")]},
            js=[Script("myapp/base.js", type="module")],
        ))
```

Add the single bundled context processor to `TEMPLATES` once, regardless of
how many apps use `register()`:

```python
TEMPLATES = [{
    "OPTIONS": {
        "context_processors": [
            ...
            "include_media.context_processors.registered_media",
        ],
    },
}]
```

### Import maps

`{% include_media %}` can generate a `<script type="importmap">` tag,
letting templates and reusable components declare their ES module specifiers
in the same places they already declare CSS and JS assets. All entries from
across the template hierarchy are merged into a single importmap tag, placed
before other assets in `<head>`.

**Template tag** — for one-off or inline declarations:

```html
{% use_media js="vendor/htmx.js" importmap="htmx" %}
{% use_media js="https://cdn.example.com/lodash.js" importmap="lodash" %}
```

**`ImportmapScript`** — for reusable widgets and forms that need a module
specifier wherever they are used:

```python
from django.forms import Form
from include_media import ImportmapScript

class DatePickerForm(Form):
    class Media:
        js = [
            ImportmapScript("vendor/pikaday.js", specifier="pikaday"),
            Script("datepicker/widget.js", type="module"),
        ]
```

To hook up a JS build system's manifest, you could add `ImportmapScript`
entries to `page_media` from a context processor:

```python
import json
from pathlib import Path
from django.forms import Media
from include_media import ImportmapScript

_manifest = json.loads((BASE_DIR / "static/dist/manifest.json").read_text())

def importmap(request):
    return {
        "page_media": Media(js=[
            ImportmapScript(f"/static/dist/{entry['file']}", specifier=name)
            for name, entry in _manifest.items()
        ])
    }
```

**Merging and precedence** — all sources (template tags, `ImportmapScript` in
`page_media`) are merged into one `<script type="importmap">` tag with
first-wins semantics: `page_media` is processed before template tags, so
view-level declarations take precedence when the same specifier appears in
multiple places.

### Asset post-processing

Set `INCLUDE_MEDIA_POSTPROCESSOR` to a dotted Python path to intercept the
collected asset HTML before it is written to the page.  The callable receives
the fully-rendered asset tags — after deduplication and nonce application —
and must return a string:

```python
# settings.py
INCLUDE_MEDIA_POSTPROCESSOR = "myproject.assets.postprocess"
```

```python
# myproject/assets.py
def postprocess(assets_html: str, context) -> str:
    # assets_html contains the rendered <link> / <script> / importmap tags.
    # context is the template Context, giving access to request, user, etc.
    return assets_html  # return a modified string
```

`context` is the full Django template `Context`.  Access the current request
via `context.get("request")` if you need per-request information (user,
headers, etc.).

The setting is validated at startup by Django's system check framework; a
misconfigured path or non-callable target raises an error before the first
request is served.

## Compatibility

- Python 3.10+
- Django 5.2, 6.0+ (`Stylesheet` is backported for Django < 6.1)

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.
The aim of this repo is to explore this idea and if it feels right to
propose it back to django core, where it could be implemented cleaner.
Any feedback is welcome.

## License

BSD 3-Clause License

## Changelog

### 0.2.0
- Add a post processing hook
- Add an app level register for media assets

### 0.1.0 — (Initial version)

- Provide template tags `{% include_media %}` and `{% use_media %}` for collecting Django Media assets into `<head>`
- Supports CSS `<link rel="stylesheet">` and JavaScript `<script>` tags and setting any attributes.
- Sets correct path with `django.contrib.staticfiles` if relevant.
- CSP nonce support available via `csp_nonce_attr` flag on `{% use_media %}`
- Import map support via `importmap=` keyword and `ImportmapScript`
- Site-wide media via `page_media` context variable; merges with template-level assets
- `{% use_media %}` falls back to rendering inline with debug-mode warning, if the include is not there.
