Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions src/django_bpp/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@


class TolerantManifestStaticFilesStorage(ManifestStaticFilesStorage):
# Runtime: brakujący wpis w manifeście → zwróć nazwę nie-hashowaną zamiast
# rzucać "Missing staticfiles manifest entry". Chroni dev/testy bez
# collectstatic (choć tam i tak działa short-circuit DEBUG w `.url()`)
# oraz 132 vendored-refy, które zostały niezahashowane na buildzie.
# Runtime: brakujący wpis w manifeście → nie rzucaj "Missing staticfiles
# manifest entry". Potrzebne dla 132 vendored-refów niezahashowanych na
# buildzie. UWAGA: samo `manifest_strict=False` NIE wystarcza dla dev/testów
# bez collectstatic — vanilla fallback liczy wtedy hash w locie (martwy URL,
# bo hashowanej kopii nie ma na dysku). Pod pytest `DEBUG=False`, więc
# short-circuit DEBUG w `.url()` też nie ratuje. Realnie chroni nas dopiero
# override `stored_name` poniżej.
manifest_strict = False

def hashed_name(self, name, content=None, filename=None):
Expand All @@ -41,3 +44,27 @@ def hashed_name(self, name, content=None, filename=None):
# / sprite, którego targetu nie ma w zebranych statykach). Zostaw
# nazwę oryginalną zamiast przerywać collectstatic — issue #269.
return name

def stored_name(self, name):
# Runtime lookup używany przez `url()`. Gdy manifestu NIE MA w ogóle
# (`staticfiles.json` nie istnieje → `hashed_files` puste — dev/testy
# bez collectstatic, albo stary `staticroot` sprzed Manifestu),
# vanilla `ManifestFilesMixin.stored_name` z `manifest_strict=False`
# liczy content-hash w locie przez `hashed_name()`. Dla pliku, który
# PRZYPADKIEM leży w STATIC_ROOT, daje to `name.<hash>.ext` — nazwę,
# której fizycznej kopii nikt nigdy nie wygenerował (stary
# collectstatic zrobił tylko nie-hashowaną). URL jest martwy:
# django-compressor rzuca `UncompressableFileError`, a bezpośredni GET
# (webtest) dostaje 404. Manifest jest jedynym źródłem prawdy dla
# hashy — bez niego serwujemy nazwę oryginalną; realny plik źródłowy
# zaserwuje finder / runserver / whitenoise.
#
# Gating na PUSTY manifest (nie per-wpis) jest celowo wąski: gdy
# collectstatic wygenerował manifest (produkcja, `.baked`),
# `hashed_files` jest niepuste → delegujemy do vanilla bez zmian, więc
# cache-busting długiego ogona działa 1:1 jak dotąd. Build-time
# `post_process` używa osobnej ścieżki (`_stored_name`, force=True),
# więc ten override go nie dotyka.
if not self.hashed_files:
return name
return super().stored_name(name)
42 changes: 42 additions & 0 deletions src/django_bpp/tests/test_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,48 @@ def test_missing_manifest_entry_falls_back_instead_of_raising():
assert TolerantManifestStaticFilesStorage().url(probe) == "/static/" + probe


@override_settings(DEBUG=False)
def test_existing_file_without_manifest_is_not_hashed_on_the_fly(tmp_path):
# Regression (bug: testy padały lokalnie, przechodziły na CI). Gdy
# STATIC_ROOT zawiera plik fizyczny, ale NIE ma manifestu
# (`staticfiles.json`) — typowy stan lokalny ze starym `staticroot`
# sprzed wprowadzenia Manifestu — vanilla `ManifestFilesMixin.stored_name`
# z `manifest_strict=False` liczy content-hash w locie i zwraca
# `foo.<hash>.css`. Plik z tą nazwą NIE istnieje na dysku (stary
# collectstatic wyprodukował tylko nie-hashowaną kopię), więc
# django-compressor i webtest dostają `could not be found` / 404.
#
# Bez manifestu (jedyne źródło prawdy dla hashy jest puste) musimy
# serwować nazwę ORYGINALNĄ — realny plik zaserwuje finder / runserver /
# whitenoise. Kontrast z `ManifestStaticFilesStorage` (poniżej) dowodzi,
# że to nasza subklasa, nie domyślne zachowanie Django, daje ten wynik.
(tmp_path / "foo.css").write_text("body{color:red}")
storage = TolerantManifestStaticFilesStorage(location=str(tmp_path))
assert storage.hashed_files == {} # brak manifestu

# Vanilla z manifest_strict=False (nasza konfiguracja) fabrykuje martwą
# nazwę hashowaną (foo.<hash>.css) — to jest dokładnie bug:
vanilla = ManifestStaticFilesStorage(location=str(tmp_path))
vanilla.manifest_strict = False
assert vanilla.stored_name("foo.css") != "foo.css"

# Subklasa zwraca nazwę oryginalną — i runtime'owy url() też:
assert storage.stored_name("foo.css") == "foo.css"
assert storage.url("foo.css") == "/static/foo.css"


@override_settings(DEBUG=False)
def test_present_manifest_still_hashes(tmp_path):
# Strona przeciwna do regresji: gdy manifest ISTNIEJE (produkcja, `.baked`
# po collectstatic), cache-busting długiego ogona musi działać 1:1 jak w
# vanilla — `stored_name` deleguje do super i zwraca nazwę z manifestu.
# Override z `not self.hashed_files` jest wąski i NIE dotyka tej ścieżki.
storage = TolerantManifestStaticFilesStorage(location=str(tmp_path))
storage.hashed_files = {"foo.css": "foo.deadbeef0123.css"}
assert storage.stored_name("foo.css") == "foo.deadbeef0123.css"
assert storage.url("foo.css") == "/static/foo.deadbeef0123.css"


@override_settings(DEBUG=True)
def test_url_in_debug_returns_unhashed_without_manifest():
# Keystone "jedna klasa storage wszędzie": w DEBUG `HashedFilesMixin.url()`
Expand Down
Loading