Repository: openwisp/openwisp-utils (language: python)

Issue:
[feature] Add a custom static storage class
Add custom static storage class with option to exclude files based on Unix shell-style wildcards. The code for this new class should reside in `openwisp_utils.storage.py`

Port changes from [storage.py in ansible-openwisp2](https://github.com/openwisp/ansible-openwisp2/blob/d4e05cf502db05c0f48370c37db43585ce39f270/files/storage.py)


Discussion / hints:
Hey, can i work on this issue ?? if yes please assign me.
Hi! @pandafy i would like to work on this issue.
Go ahead @Arsh0023 :+1: 
Hi @pandafy I have created a StaticStoragClass that pretends to ignore files based on Unix shell-style wildcards. So what it actually does is it prevents the files from getting copied to 'STATIC_ROOT'.So first all the files get copied into the 'STATIC_ROOT' and then I've defined a post_process method that deletes the files which were told to be ignored. Is this fine should I create a PR, If not then what changes I should do.
@Arsh0023 you just need to port the code from  [storage.py in ansible-openwisp2](https://github.com/openwisp/ansible-openwisp2/blob/d4e05cf502db05c0f48370c37db43585ce39f270/files/storage.py) which means you can just copy the code for storage class and mixin from there and put it under `openwisp_utils/storage.py`(you'll have to create this file). After that you are supposed to write tests and documentation for the feature. 
@pandafy Oh I see I went a little out of way, thanks for helping I am actually new to open source. I'd be working on what you told

Fix patch:
diff --git a/README.rst b/README.rst
index 28d233a0..b2ba8324 100644
--- a/README.rst
+++ b/README.rst
@@ -46,6 +46,7 @@ Current features
 * `Configurable navigation menu <#main-navigation-menu>`_
 * `OpenAPI / Swagger documentation <#openwisp_api_docs>`_
 * `Model utilities <#model-utilities>`_
+* `Storage utilities <#storage-utilities>`_
 * `Admin utilities <#admin-utilities>`_
 * `Code utilities <#code-utilities>`_
 * `REST API utilities <#rest-api-utilities>`_
@@ -618,6 +619,22 @@ Extends ``collections.SortedDict`` and implements logic to sort inserted
 items based on ``key`` value. Sorting is done at insert operation which
 incurs memory space overhead.
 
+Storage utilities
+-----------------
+
+``openwisp_utils.storage.CompressStaticFilesStorage``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+A static storage backend for compression inheriting from `django-compress-staticfiles's <https://pypi.org/project/django-compress-staticfiles/>`_ ``CompressStaticFilesStorage`` class.
+
+Adds support for excluding file types using `OPENWISP_STATICFILES_VERSIONED_EXCLUDE <#openwisp_staticfiles_versioned_exclude>`_ setting.
+
+To use point ``STATICFILES_STORAGE`` to ``openwisp_utils.storage.CompressStaticFilesStorage`` in ``settings.py``.
+
+.. code-block:: python
+
+    STATICFILES_STORAGE = 'openwisp_utils.storage.CompressStaticFilesStorage'
+
 REST API utilities
 ------------------
 
@@ -1061,6 +1078,23 @@ For more information about optional parameters check the
 It can be used to change the thresholds used by `TimeLoggingTestRunner <#openwisp_utilsteststimeloggingtestrunner>`_
 to detect slow tests (0.3s by default) and highlight the slowest ones (1s by default) amongst them.
 
+``OPENWISP_STATICFILES_VERSIONED_EXCLUDE``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+**default**: ``['leaflet/*/*.png']``
+
+Allows to pass a list of **Unix shell-style wildcards** for files to be excluded by `CompressStaticFilesStorage <#openwisp_utilsstorageCompressStaticFilesStorage>`_.
+
+By default Leaflet PNGs have been excluded to avoid bugs like `openwisp/ansible-openwisp2#232 <https://github.com/openwisp/ansible-openwisp2/issues/232>`_.
+
+Example usage:
+
+.. code-block:: python
+
+    OPENWISP_STATICFILES_VERSIONED_EXCLUDE = [
+        '*png',
+    ]
+
 Installing for development
 --------------------------
 
diff --git a/openwisp_utils/storage.py b/openwisp_utils/storage.py
new file mode 100644
index 00000000..14f00e12
--- /dev/null
+++ b/openwisp_utils/storage.py
@@ -0,0 +1,33 @@
+import fnmatch
+
+from compress_staticfiles.storage import (
+    CompressStaticFilesStorage as BaseCompressStaticFilesStorage,
+)
+from django.conf import settings
+
+
+class FileHashedNameMixin:
+    default_excluded_patterns = ['leaflet/*/*.png']
+    excluded_patterns = default_excluded_patterns + getattr(
+        settings, "OPENWISP_STATICFILES_VERSIONED_EXCLUDE", []
+    )
+
+    def hashed_name(self, name, content=None, filename=None):
+        if not any(
+            fnmatch.fnmatch(name, pattern) for pattern in self.excluded_patterns
+        ):
+            return super().hashed_name(name, content, filename)
+        return name
+
+
+class CompressStaticFilesStorage(
+    FileHashedNameMixin, BaseCompressStaticFilesStorage,
+):
+    """
+    A static files storage backend for compression that inherits from
+    django-compress-staticfiles's CompressStaticFilesStorage class;
+    also adds support for excluding file types using
+    "OPENWISP_STATICFILES_VERSIONED_EXCLUDE" setting.
+    """
+
+    pass
diff --git a/setup.py b/setup.py
index c77239a3..5523e7e4 100644
--- a/setup.py
+++ b/setup.py
@@ -41,7 +41,10 @@
     },
     scripts=['openwisp-qa-check', 'openwisp-qa-format', 'openwisp-pre-push-hook'],
     zip_safe=False,
-    install_requires=['django-model-utils>=4.0.0,<4.1.0'],
+    install_requires=[
+        'django-model-utils>=4.0.0,<4.1.0',
+        'django-compress-staticfiles~=1.0.1b',
+    ],
     extras_require={
         'qa': [
             'black<=19.10b0',


Test patch:
diff --git a/tests/test_project/tests/test_storage.py b/tests/test_project/tests/test_storage.py
new file mode 100644
index 00000000..215361a2
--- /dev/null
+++ b/tests/test_project/tests/test_storage.py
@@ -0,0 +1,53 @@
+import os
+import shutil
+
+from django.conf import settings
+from django.contrib.staticfiles import storage
+from django.core.management import call_command
+from django.test import TestCase, override_settings
+
+
+def create_dir(*paths: str):
+    """Returns Joined path
+
+    joins two or more pathname using os.path.join and creates leaf directory
+    and all the intermidiate ones according to the joined path using os.makedirs
+    """
+    joined_path = os.path.join(*paths)
+    os.makedirs(joined_path)
+    return joined_path
+
+
+@override_settings(
+    STATICFILES_STORAGE='openwisp_utils.storage.CompressStaticFilesStorage',
+    STATIC_ROOT=create_dir(settings.BASE_DIR, 'test_storage_dir', 'test_static_root'),
+    STATICFILES_DIRS=[
+        create_dir(settings.BASE_DIR, 'test_storage_dir', 'test_staticfiles_dir')
+    ],
+    STATICFILES_FINDERS=['django.contrib.staticfiles.finders.FileSystemFinder'],
+    OPENWISP_STATICFILES_VERSIONED_EXCLUDE=['*skip_this.txt'],
+)
+class TestCompressStaticFilesStorage(TestCase):
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        static_dir = settings.STATICFILES_DIRS[0]
+
+        file1 = os.path.join(static_dir, 'skip_this.txt')
+        with open(file1, 'w') as f:
+            f.write('this will not be hashed')
+
+        file2 = os.path.join(static_dir, 'this.txt')
+        with open(file2, 'w') as f:
+            f.write('this will be hashed')
+
+    @classmethod
+    def tearDownClass(cls):
+        super().tearDownClass()
+        shutil.rmtree(os.path.join(settings.BASE_DIR, 'test_storage_dir'))
+
+    def test_hashed_name(self):
+        call_command('collectstatic')
+        hashed_files = storage.staticfiles_storage.hashed_files
+        self.assertEqual(hashed_files['skip_this.txt'], 'skip_this.txt')
+        self.assertNotEqual(hashed_files['this.txt'], 'this.txt')