diff --git a/CHANGELOG.md b/CHANGELOG.md index 438a4b12f..39bd975aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to - 🐛(frontend) hide Leave in the doc menu when not logged in #2626 - 🐛(backend) allow to configure settings DATA_UPLOAD_MAX_MEMORY_SIZE +- 🐛(backend) serve the static files of the admin without whitenoise ## [v5.6.0] - 2026-09-03 diff --git a/documentation/env.md b/documentation/env.md index 70218f300..99a50bf43 100644 --- a/documentation/env.md +++ b/documentation/env.md @@ -132,6 +132,7 @@ These are the environment variables you can set for the `impress-backend` contai | SEARCH_INDEXER_SECRET | Token required for indexation queries | | | INDEXING_URL | Find application endpoint for indexation | | | SENTRY_DSN | Sentry host | | +| SERVE_STATIC_FILES | Serve the files collected in STATIC_ROOT from Django. Disable it when a web server or a CDN serves /static/ instead | True | | SESSION_COOKIE_AGE | duration of the cookie session | 60*60*12 | | SIGNUP_NEW_USER_TO_MARKETING_EMAIL | Register new user to the marketing onboarding. If True, see env LASUITE_MARKETING_* system | False | | SILK_ENABLED | Enable the django-silk request/SQL/cProfile profiler and its /silk/ UI. OFF by default; never enable against production with real users. See documentation/profiling.md | False | diff --git a/src/backend/core/tests/test_urls_static.py b/src/backend/core/tests/test_urls_static.py new file mode 100644 index 000000000..2687d60af --- /dev/null +++ b/src/backend/core/tests/test_urls_static.py @@ -0,0 +1,55 @@ +"""Test the serving of the files collected in STATIC_ROOT.""" + +import pytest + +from core.tests.utils.urls import reload_urls + +pytestmark = pytest.mark.django_db + + +@pytest.fixture(autouse=True) +def static_root(settings, tmp_path): + """Point STATIC_ROOT to a directory holding a single collected file.""" + static_file = tmp_path / "admin" / "css" / "login.css" + static_file.parent.mkdir(parents=True) + static_file.write_text("body {}", encoding="utf-8") + + settings.STATIC_ROOT = str(tmp_path) + + yield tmp_path + + # The URLs are built from the settings, so restore the settings and reload + # them to leave the URLs as the next tests expect them. + settings.finalize() + reload_urls() + + +def test_urls_static_served(client, settings): + """Collected static files are served, the admin has no assets otherwise.""" + settings.SERVE_STATIC_FILES = True + reload_urls() + + response = client.get("/static/admin/css/login.css") + + assert response.status_code == 200 + assert b"".join(response.streaming_content) == b"body {}" + + +def test_urls_static_unknown_file(client, settings): + """A file missing from STATIC_ROOT is a 404.""" + settings.SERVE_STATIC_FILES = True + reload_urls() + + response = client.get("/static/admin/css/unknown.css") + + assert response.status_code == 404 + + +def test_urls_static_disabled(client, settings): + """Serving static files can be delegated to a web server or a CDN.""" + settings.SERVE_STATIC_FILES = False + reload_urls() + + response = client.get("/static/admin/css/login.css") + + assert response.status_code == 404 diff --git a/src/backend/impress/settings.py b/src/backend/impress/settings.py index 3b4dbf9dd..74b5f619d 100755 --- a/src/backend/impress/settings.py +++ b/src/backend/impress/settings.py @@ -146,6 +146,13 @@ class Base(Configuration): # Static files (CSS, JavaScript, Images) STATIC_URL = "/static/" STATIC_ROOT = os.path.join(DATA_DIR, "static") + # Let Django itself serve the files collected in STATIC_ROOT. The admin is + # unusable without them and, since whitenoise was removed, nothing else + # serves STATIC_URL. Disable it when a web server or a CDN is placed in + # front of the application to serve them. + SERVE_STATIC_FILES = values.BooleanValue( + True, environ_name="SERVE_STATIC_FILES", environ_prefix=None + ) MEDIA_URL = "/media/" MEDIA_ROOT = os.path.join(DATA_DIR, "media") MEDIA_BASE_URL = values.Value( @@ -1371,6 +1378,7 @@ class Test(Base): # Static files are not used in the test environment # Tests are raising warnings because the /data/static directory does not exist STATIC_ROOT = None + SERVE_STATIC_FILES = False CELERY_TASK_ALWAYS_EAGER = values.BooleanValue(True) diff --git a/src/backend/impress/urls.py b/src/backend/impress/urls.py index fd594dc61..be727ca40 100644 --- a/src/backend/impress/urls.py +++ b/src/backend/impress/urls.py @@ -1,10 +1,13 @@ """URL configuration for the impress project""" +import re + from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.urls import include, path, re_path +from django.views.static import serve as serve_static from drf_spectacular.views import ( SpectacularJSONAPIView, @@ -29,6 +32,18 @@ if settings.DEBUG: + staticfiles_urlpatterns() + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ) +elif settings.SERVE_STATIC_FILES: + # Whitenoise used to serve the files collected in STATIC_ROOT. It was removed, + # so serve them from Django instead, otherwise the admin has no assets at all. + # Django's `static()` helper only builds this route when DEBUG is on, hence the + # explicit pattern. + urlpatterns += [ + re_path( + rf"^{re.escape(settings.STATIC_URL.lstrip('/'))}(?P.*)$", + serve_static, + {"document_root": settings.STATIC_ROOT}, + ), + ] if settings.USE_SWAGGER or settings.DEBUG: