Browse Source

Fixed #27590 -- Allowed customizing a manifest file storage in ManifestFilesMixin.

Jarosław Wygoda 3 years ago
parent
commit
d3c4696596

+ 1 - 0
AUTHORS

@@ -423,6 +423,7 @@ answer newbie questions, and generally made Django that much better:
     Jan Rademaker
     Jarek Głowacki <jarekwg@gmail.com>
     Jarek Zgoda <jarek.zgoda@gmail.com>
+    Jarosław Wygoda <jaroslaw@wygoda.me>
     Jason Davies (Esaj) <https://www.jasondavies.com/>
     Jason Huggins <http://www.jrandolph.com/blog/>
     Jason McBrayer <http://www.carcosa.net/jason/>

+ 8 - 5
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))

+ 20 - 0
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
 ==============
 

+ 5 - 0
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`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

+ 51 - 1
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