feat: add django-whitenoise
This commit is contained in:
parent
9a795dc3c9
commit
f986105cf5
20
README.md
20
README.md
@ -34,6 +34,26 @@ DJANGO_CSRF_TRUSTED_ORIGINS=http://192.168.1.x:8000 uv run python manage.py runs
|
|||||||
|
|
||||||
Sessions last a year, so your phone stays logged in.
|
Sessions last a year, so your phone stays logged in.
|
||||||
|
|
||||||
|
## Static files
|
||||||
|
|
||||||
|
WhiteNoise serves `/static/` from Django itself, so there is no nginx or S3 to
|
||||||
|
set up. `collectstatic` hashes every file's contents into its name and writes
|
||||||
|
gzip/brotli copies alongside, and WhiteNoise serves those with a ten-year
|
||||||
|
immutable cache header — worth having on a phone over a slow connection.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python manage.py collectstatic --noinput
|
||||||
|
```
|
||||||
|
|
||||||
|
Run that whenever an icon or CSS file changes. In `DEBUG=1` Django serves the
|
||||||
|
files directly and you can skip it; with `DEBUG=0` the hashed manifest must
|
||||||
|
exist or pages referencing `{% static %}` will fail.
|
||||||
|
|
||||||
|
`manifest.webmanifest` and `sw.js` are templates rather than static files: the
|
||||||
|
manifest so `{% static %}` resolves the icons to their hashed names (the
|
||||||
|
staticfiles storage only rewrites URLs inside CSS and JS), and the worker
|
||||||
|
because it has to be served from `/` to control the `/admin/` pages it caches.
|
||||||
|
|
||||||
## Installing it on Android (PWA)
|
## Installing it on Android (PWA)
|
||||||
|
|
||||||
The Today page is a progressive web app: Chrome will offer *Add to home screen*
|
The Today page is a progressive web app: Chrome will offer *Add to home screen*
|
||||||
|
|||||||
@ -54,6 +54,9 @@ INSTALLED_APPS = [
|
|||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
# Directly below SecurityMiddleware and above everything else, per WhiteNoise's
|
||||||
|
# docs: it serves /static/ itself so no nginx/S3 is needed in front of Django.
|
||||||
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
@ -132,6 +135,15 @@ STATIC_ROOT = BASE_DIR / "static"
|
|||||||
# PWA icons and manifest live here; STATIC_ROOT is only the collectstatic output.
|
# PWA icons and manifest live here; STATIC_ROOT is only the collectstatic output.
|
||||||
STATICFILES_DIRS = [BASE_DIR / "assets"]
|
STATICFILES_DIRS = [BASE_DIR / "assets"]
|
||||||
|
|
||||||
|
STORAGES = {
|
||||||
|
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||||
|
"staticfiles": {
|
||||||
|
# Hashes each file's contents into its name and gzip/brotli-compresses it,
|
||||||
|
# so WhiteNoise can serve it with a far-future cache header.
|
||||||
|
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,16 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="service-worker",
|
name="service-worker",
|
||||||
),
|
),
|
||||||
|
# A template, not a static file, so {% static %} resolves the icons to their
|
||||||
|
# hashed names — .webmanifest is not URL-rewritten by the staticfiles storage.
|
||||||
|
path(
|
||||||
|
"manifest.webmanifest",
|
||||||
|
TemplateView.as_view(
|
||||||
|
template_name="manifest.webmanifest",
|
||||||
|
content_type="application/manifest+json",
|
||||||
|
),
|
||||||
|
name="manifest",
|
||||||
|
),
|
||||||
# Sits under /admin/ so it inherits the admin's styling and login, but must
|
# Sits under /admin/ so it inherits the admin's styling and login, but must
|
||||||
# come before the admin's own catch-all patterns.
|
# come before the admin's own catch-all patterns.
|
||||||
path("admin/", include("tracker.urls")),
|
path("admin/", include("tracker.urls")),
|
||||||
|
|||||||
@ -7,6 +7,7 @@ requires-python = ">=3.14"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"django>=5.2",
|
"django>=5.2",
|
||||||
"django-ninja>=1.4",
|
"django-ninja>=1.4",
|
||||||
|
"whitenoise>=6.12.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{% block extrahead %}
|
{% block extrahead %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<link rel="manifest" href="{% static 'manifest.webmanifest' %}">
|
<link rel="manifest" href="{% url 'manifest' %}">
|
||||||
<meta name="theme-color" content="#202a35">
|
<meta name="theme-color" content="#202a35">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<link rel="apple-touch-icon" href="{% static 'icons/icon-192.png' %}">
|
<link rel="apple-touch-icon" href="{% static 'icons/icon-192.png' %}">
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
{
|
{% load static %}{
|
||||||
"name": "Osiris",
|
"name": "Osiris",
|
||||||
"short_name": "Osiris",
|
"short_name": "Osiris",
|
||||||
"description": "Medication and pulse tracking for Osiris the cat.",
|
"description": "Medication and pulse tracking for Osiris the cat.",
|
||||||
@ -10,19 +10,19 @@
|
|||||||
"theme_color": "#202a35",
|
"theme_color": "#202a35",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/static/icons/icon-192.png",
|
"src": "{% static 'icons/icon-192.png' %}",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any"
|
"purpose": "any"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/static/icons/icon-512.png",
|
"src": "{% static 'icons/icon-512.png' %}",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any"
|
"purpose": "any"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/static/icons/icon-maskable-512.png",
|
"src": "{% static 'icons/icon-maskable-512.png' %}",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "maskable"
|
"purpose": "maskable"
|
||||||
@ -1,5 +1,7 @@
|
|||||||
|
import json
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -246,6 +248,18 @@ class PwaTests(TrackerTestCase):
|
|||||||
self.assertContains(response, 'rel="manifest"')
|
self.assertContains(response, 'rel="manifest"')
|
||||||
self.assertContains(response, "serviceWorker.register")
|
self.assertContains(response, "serviceWorker.register")
|
||||||
|
|
||||||
|
def test_the_manifest_is_valid_json_pointing_at_real_icons(self):
|
||||||
|
response = self.client.get(reverse("manifest"))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response["Content-Type"], "application/manifest+json")
|
||||||
|
|
||||||
|
manifest = json.loads(response.content)
|
||||||
|
self.assertEqual(manifest["start_url"], reverse("tracker:today"))
|
||||||
|
self.assertEqual(len(manifest["icons"]), 3)
|
||||||
|
for icon in manifest["icons"]:
|
||||||
|
self.assertTrue(icon["src"].startswith(settings.STATIC_URL))
|
||||||
|
|
||||||
def test_the_login_page_is_installable_too(self):
|
def test_the_login_page_is_installable_too(self):
|
||||||
# The app opens on the login screen when the session lapses.
|
# The app opens on the login screen when the session lapses.
|
||||||
response = self.client.get("/admin/login/")
|
response = self.client.get("/admin/login/")
|
||||||
|
|||||||
11
uv.lock
11
uv.lock
@ -160,6 +160,7 @@ source = { virtual = "." }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
{ name = "django-ninja" },
|
{ name = "django-ninja" },
|
||||||
|
{ name = "whitenoise" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
@ -173,6 +174,7 @@ dev = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "django", specifier = ">=5.2" },
|
{ name = "django", specifier = ">=5.2" },
|
||||||
{ name = "django-ninja", specifier = ">=1.4" },
|
{ name = "django-ninja", specifier = ">=1.4" },
|
||||||
|
{ name = "whitenoise", specifier = ">=6.12.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
@ -415,3 +417,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288be
|
|||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" },
|
{ url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "whitenoise"
|
||||||
|
version = "6.12.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/cb/2a/55b3f3a4ec326cd077c1c3defeee656b9298372a69229134d930151acd01/whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad", size = 26841, upload-time = "2026-02-27T00:05:42.028Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/eb/d5583a11486211f3ebd4b385545ae787f32363d453c19fffd81106c9c138/whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2", size = 20302, upload-time = "2026-02-27T00:05:40.086Z" },
|
||||||
|
]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user