From 03bd7f1170aa46754af124c0d75e91801cf35dd1 Mon Sep 17 00:00:00 2001 From: Manuel Raynaud Date: Fri, 4 Sep 2026 15:08:40 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(backend)=20serve=20the=20static=20?= =?UTF-8?q?files=20of=20the=20admin=20without=20whitenoise?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We previously removed whitenoise because its middleware is not compatible is not compatible with the ASGI application. But without it, static file for the admin are not served anymore. This commit allow to serve them using the static view from django. This view should not be used in production but we are only using it for the admin, which is not used a lot and a settings is also added to disable it if you want to serve static from a dedicated nginx for example. --- CHANGELOG.md | 1 + documentation/env.md | 1 + src/backend/core/tests/test_urls_static.py | 55 ++++++++++++++++++++++ src/backend/impress/settings.py | 8 ++++ src/backend/impress/urls.py | 15 ++++++ 5 files changed, 80 insertions(+) create mode 100644 src/backend/core/tests/test_urls_static.py 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: