From d3c4696596822281c0a5d4b9e3ee27732a4ce092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Wygoda?= Date: Wed, 2 Jun 2021 21:44:59 +0200 Subject: [PATCH] Fixed #27590 -- Allowed customizing a manifest file storage in ManifestFilesMixin. --- AUTHORS | 1 + django/contrib/staticfiles/storage.py | 13 ++++--- docs/ref/contrib/staticfiles.txt | 20 ++++++++++ docs/releases/4.0.txt | 5 +++ tests/staticfiles_tests/test_storage.py | 52 ++++++++++++++++++++++++- 5 files changed, 85 insertions(+), 6 deletions(-) diff --git a/AUTHORS b/AUTHORS index 3a8df03ca3..2d9d23f73e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -423,6 +423,7 @@ answer newbie questions, and generally made Django that much better: Jan Rademaker Jarek Głowacki Jarek Zgoda + Jarosław Wygoda Jason Davies (Esaj) Jason Huggins Jason McBrayer diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py index fe13423749..7cf43ed6ab 100644 --- a/django/contrib/staticfiles/storage.py +++ b/django/contrib/staticfiles/storage.py @@ -401,13 +401,16 @@ class ManifestFilesMixin(HashedFilesMixin): manifest_strict = True keep_intermediate_files = False - def __init__(self, *args, **kwargs): + def __init__(self, *args, manifest_storage=None, **kwargs): super().__init__(*args, **kwargs) + if manifest_storage is None: + manifest_storage = self + self.manifest_storage = manifest_storage self.hashed_files = self.load_manifest() def read_manifest(self): try: - with self.open(self.manifest_name) as manifest: + with self.manifest_storage.open(self.manifest_name) as manifest: return manifest.read().decode() except FileNotFoundError: return None @@ -435,10 +438,10 @@ class ManifestFilesMixin(HashedFilesMixin): def save_manifest(self): payload = {'paths': self.hashed_files, 'version': self.manifest_version} - if self.exists(self.manifest_name): - self.delete(self.manifest_name) + if self.manifest_storage.exists(self.manifest_name): + self.manifest_storage.delete(self.manifest_name) contents = json.dumps(payload).encode() - self._save(self.manifest_name, ContentFile(contents)) + self.manifest_storage._save(self.manifest_name, ContentFile(contents)) def stored_name(self, name): parsed_name = urlsplit(unquote(name)) diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt index 14c739b3ea..46a73e7c54 100644 --- a/docs/ref/contrib/staticfiles.txt +++ b/docs/ref/contrib/staticfiles.txt @@ -313,6 +313,20 @@ For example, the ``'css/styles.css'`` file with this content: @import url("../admin/css/base.27e20196a850.css"); +You can change the location of the manifest file by using a custom +``ManifestStaticFilesStorage`` subclass that sets the ``manifest_storage`` +argument. For example:: + + from django.conf import settings + from django.contrib.staticfiles.storage import ( + ManifestStaticFilesStorage, StaticFilesStorage, + ) + + class MyManifestStaticFilesStorage(ManifestStaticFilesStorage): + def __init__(self, *args, **kwargs): + manifest_storage = StaticFilesStorage(location=settings.BASE_DIR) + super().__init__(*args, manifest_storage=manifest_storage, **kwargs) + .. versionchanged:: 4.0 Support for finding paths in the source map comments was added. @@ -320,6 +334,8 @@ For example, the ``'css/styles.css'`` file with this content: Support for finding paths to JavaScript modules in ``import`` and ``export`` statements was added. + The ``manifest_storage`` argument was added. + .. attribute:: storage.ManifestStaticFilesStorage.max_post_process_passes Since static files might reference other static files that need to have their @@ -384,6 +400,10 @@ hashing algorithm. Use this mixin with a custom storage to append the MD5 hash of the file's content to the filename as :class:`~storage.ManifestStaticFilesStorage` does. +.. versionchanged:: 4.0 + + The ``manifest_storage`` argument was added. + Finders Module ============== diff --git a/docs/releases/4.0.txt b/docs/releases/4.0.txt index 390c561638..0fa87b2810 100644 --- a/docs/releases/4.0.txt +++ b/docs/releases/4.0.txt @@ -174,6 +174,11 @@ Minor features replaces paths to JavaScript modules in ``import`` and ``export`` statements with their hashed counterparts. +* The new ``manifest_storage`` argument of + :class:`~django.contrib.staticfiles.storage.ManifestFilesMixin` and + :class:`~django.contrib.staticfiles.storage.ManifestStaticFilesStorage` + allows customizing the manifest file storage. + :mod:`django.contrib.syndication` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/staticfiles_tests/test_storage.py b/tests/staticfiles_tests/test_storage.py index d5aa1c1c9d..0a34f64394 100644 --- a/tests/staticfiles_tests/test_storage.py +++ b/tests/staticfiles_tests/test_storage.py @@ -1,3 +1,4 @@ +import json import os import shutil import sys @@ -13,7 +14,7 @@ from django.contrib.staticfiles.management.commands.collectstatic import ( Command as CollectstaticCommand, ) from django.core.management import call_command -from django.test import override_settings +from django.test import SimpleTestCase, override_settings from .cases import CollectionTestCase from .settings import TEST_ROOT @@ -499,6 +500,55 @@ class TestCollectionSimpleStorage(CollectionTestCase): self.assertIn(b"other.deploy12345.css", content) +class CustomManifestStorage(storage.ManifestStaticFilesStorage): + def __init__(self, *args, manifest_storage=None, **kwargs): + manifest_storage = storage.StaticFilesStorage( + location=kwargs.pop('manifest_location'), + ) + super().__init__(*args, manifest_storage=manifest_storage, **kwargs) + + +class TestCustomManifestStorage(SimpleTestCase): + def setUp(self): + self.manifest_path = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.manifest_path) + + self.staticfiles_storage = CustomManifestStorage( + manifest_location=self.manifest_path, + ) + self.manifest_file = self.manifest_path / self.staticfiles_storage.manifest_name + # Manifest without paths. + self.manifest = {'version': self.staticfiles_storage.manifest_version} + with self.manifest_file.open('w') as manifest_file: + json.dump(self.manifest, manifest_file) + + def test_read_manifest(self): + self.assertEqual( + self.staticfiles_storage.read_manifest(), + json.dumps(self.manifest), + ) + + def test_read_manifest_nonexistent(self): + os.remove(self.manifest_file) + self.assertIsNone(self.staticfiles_storage.read_manifest()) + + def test_save_manifest_override(self): + self.assertIs(self.manifest_file.exists(), True) + self.staticfiles_storage.save_manifest() + self.assertIs(self.manifest_file.exists(), True) + new_manifest = json.loads(self.staticfiles_storage.read_manifest()) + self.assertIn('paths', new_manifest) + self.assertNotEqual(new_manifest, self.manifest) + + def test_save_manifest_create(self): + os.remove(self.manifest_file) + self.staticfiles_storage.save_manifest() + self.assertIs(self.manifest_file.exists(), True) + new_manifest = json.loads(self.staticfiles_storage.read_manifest()) + self.assertIn('paths', new_manifest) + self.assertNotEqual(new_manifest, self.manifest) + + class CustomStaticFilesStorage(storage.StaticFilesStorage): """ Used in TestStaticFilePermissions