Repository: django-compressor/django-compressor

Issue:
problem with relative path in less file
I'm using django-compressor with lessc in order to compile twitter-bootstrap less framework.

The issue is the following:

The compressor is not converting the relative path pointing to an image file to it's absolute path... the result is that the django server is trying to get the file from the CACHE folder:

[01/Aug/2012 21:12:16] "GET /static/CACHE/img/glyphicons-halflings.png HTTP/1.1" 404 1706

If I change manually the relative path:

@iconSpritePath:          "../img/glyphicons-halflings.png";

to:

@iconSpritePath:          "/static/bootstrap/img/glyphicons-halflings.png";

The image is loaded, but this is not a DRY solution.

I've reproduced this with 1.1.2 and 1.2a2

Cheers,
David



Discussion:
This is probably related to https://github.com/jezdez/django_compressor/issues/226, I think.

This means that it probably works fine in production, when you collect all the static files in that CACHE dir and serve it from there? And that it fails in development?

http://stackoverflow.com/a/17033883/1330615


Resolving patch:
diff --git a/compressor/base.py b/compressor/base.py
--- a/compressor/base.py
+++ b/compressor/base.py
@@ -21,6 +21,7 @@
 from compressor.exceptions import (CompressorError, UncompressableFileError,
         FilterDoesNotExist)
 from compressor.filters import CachedCompilerFilter
+from compressor.filters.css_default import CssAbsoluteFilter
 from compressor.storage import compressor_file_storage
 from compressor.signals import post_compress
 from compressor.utils import get_class, get_mod_func, staticfiles
@@ -211,20 +212,24 @@ def hunks(self, forced=False):
                 precompiled, value = self.precompile(value, **options)
 
             if enabled:
-                yield self.filter(value, **options)
+                yield self.filter(value, self.cached_filters, **options)
+            elif precompiled:
+                # since precompiling moves files around, it breaks url()
+                # statements in css files. therefore we run the absolute filter
+                # on precompiled css files even if compression is disabled.
+                if CssAbsoluteFilter in self.cached_filters:
+                    value = self.filter(value, [CssAbsoluteFilter], **options)
+                yield self.handle_output(kind, value, forced=True,
+                                         basename=basename)
             else:
-                if precompiled:
-                    yield self.handle_output(kind, value, forced=True,
-                                             basename=basename)
-                else:
-                    yield self.parser.elem_str(elem)
+                yield self.parser.elem_str(elem)
 
     def filter_output(self, content):
         """
         Passes the concatenated content to the 'output' methods
         of the compressor filters.
         """
-        return self.filter(content, method=METHOD_OUTPUT)
+        return self.filter(content, self.cached_filters, method=METHOD_OUTPUT)
 
     def filter_input(self, forced=False):
         """
@@ -275,8 +280,8 @@ def precompile(self, content, kind=None, elem=None, filename=None,
             filename=filename)
         return True, filter.input(**kwargs)
 
-    def filter(self, content, method, **kwargs):
-        for filter_cls in self.cached_filters:
+    def filter(self, content, filters, method, **kwargs):
+        for filter_cls in filters:
             filter_func = getattr(
                 filter_cls(content, filter_type=self.type), method)
             try:


Test patch:
diff --git a/compressor/tests/static/css/relative_url.css b/compressor/tests/static/css/relative_url.css
new file mode 100644
--- /dev/null
+++ b/compressor/tests/static/css/relative_url.css
@@ -0,0 +1 @@
+p { background: url('../img/python.png'); }
\ No newline at end of file
diff --git a/compressor/tests/test_base.py b/compressor/tests/test_base.py
--- a/compressor/tests/test_base.py
+++ b/compressor/tests/test_base.py
@@ -48,9 +48,55 @@ def input(self, **kwargs):
         return 'OUTPUT'
 
 
+class PassthroughPrecompiler(object):
+    """A filter whose outputs the input unmodified """
+    def __init__(self, content, attrs, filter_type=None, filename=None,
+                 charset=None):
+        self.content = content
+
+    def input(self, **kwargs):
+        return self.content
+
+
 test_dir = os.path.abspath(os.path.join(os.path.dirname(__file__)))
 
 
+class PrecompilerAndAbsoluteFilterTestCase(SimpleTestCase):
+
+    def setUp(self):
+        self.html_orig = '<link rel="stylesheet" href="/static/css/relative_url.css" type="text/css" />'
+        self.html_link_to_precompiled_css = '<link rel="stylesheet" href="/static/CACHE/css/relative_url.41a74f6d5864.css" type="text/css" />'
+        self.html_link_to_absolutized_css = '<link rel="stylesheet" href="/static/CACHE/css/relative_url.9b8fd415e521.css" type="text/css" />'
+        self.css_orig = "p { background: url('../img/python.png'); }" # content of relative_url.css
+        self.css_absolutized = "p { background: url('/static/img/python.png?c2281c83670e'); }"
+
+    def helper(self, enabled, use_precompiler, use_absolute_filter, expected_output):
+        precompiler = (('text/css', 'compressor.tests.test_base.PassthroughPrecompiler'),) if use_precompiler else ()
+        filters = ('compressor.filters.css_default.CssAbsoluteFilter',) if use_absolute_filter else ()
+
+        with self.settings(COMPRESS_ENABLED=enabled, COMPRESS_PRECOMPILERS=precompiler, COMPRESS_CSS_FILTERS=filters):
+            css_node = CssCompressor(self.html_orig)
+            output = list(css_node.hunks())[0]
+            self.assertEqual(output, expected_output)
+
+    @override_settings(COMPRESS_CSS_HASHING_METHOD="content")
+    def test_precompiler_enables_absolute(self):
+        """
+        Tests whether specifying a precompiler also runs the CssAbsoluteFilter even if
+        compression is disabled, but only if the CssAbsoluteFilter is actually contained
+        in the filters setting.
+        While at it, ensure that everything runs as expected when compression is enabled.
+        """
+        self.helper(enabled=False, use_precompiler=False, use_absolute_filter=False, expected_output=self.html_orig)
+        self.helper(enabled=False, use_precompiler=False, use_absolute_filter=True, expected_output=self.html_orig)
+        self.helper(enabled=False, use_precompiler=True, use_absolute_filter=False, expected_output=self.html_link_to_precompiled_css)
+        self.helper(enabled=False, use_precompiler=True, use_absolute_filter=True, expected_output=self.html_link_to_absolutized_css)
+        self.helper(enabled=True, use_precompiler=False, use_absolute_filter=False, expected_output=self.css_orig)
+        self.helper(enabled=True, use_precompiler=False, use_absolute_filter=True, expected_output=self.css_absolutized)
+        self.helper(enabled=True, use_precompiler=True, use_absolute_filter=False, expected_output=self.css_orig)
+        self.helper(enabled=True, use_precompiler=True, use_absolute_filter=True, expected_output=self.css_absolutized)
+
+
 class CompressorTestCase(SimpleTestCase):
 
     def setUp(self):