diff --git a/compressor/conf.py b/compressor/conf.py
index 7ab381b..2011531 100644
--- a/compressor/conf.py
+++ b/compressor/conf.py
@@ -24,6 +24,8 @@ class CompressorSettings(AppSettings):
ROOT = None
CSS_FILTERS = ['compressor.filters.css_default.CssAbsoluteFilter']
+ CSS_HASHING_METHOD = 'mtime'
+
JS_FILTERS = ['compressor.filters.jsmin.JSMinFilter']
PRECOMPILERS = (
# ('text/coffeescript', 'coffee --compile --stdio'),
diff --git a/compressor/filters/__init__.py b/compressor/filters/__init__.py
index b4d80fc..96f0c87 100644
--- a/compressor/filters/__init__.py
+++ b/compressor/filters/__init__.py
@@ -1 +1,2 @@
-from compressor.filters.base import (FilterBase, CallbackOutputFilter, CompilerFilter, FilterError)
+from compressor.filters.base import (FilterBase, CallbackOutputFilter,
+ CompilerFilter, FilterError)
diff --git a/compressor/filters/css_default.py b/compressor/filters/css_default.py
index d5556c1..6f2ffb9 100644
--- a/compressor/filters/css_default.py
+++ b/compressor/filters/css_default.py
@@ -2,7 +2,7 @@ import os
import re
import posixpath
-from compressor.cache import get_hashed_mtime
+from compressor.cache import get_hexdigest, get_hashed_mtime
from compressor.conf import settings
from compressor.filters import FilterBase
from compressor.utils import staticfiles
@@ -27,7 +27,6 @@ class CssAbsoluteFilter(FilterBase):
return self.content
self.path = basename.replace(os.sep, '/')
self.path = self.path.lstrip('/')
- self.mtime = get_hashed_mtime(filename)
if self.url.startswith(('http://', 'https://')):
self.has_scheme = True
parts = self.url.split('/')
@@ -54,24 +53,36 @@ class CssAbsoluteFilter(FilterBase):
filename = os.path.join(self.root, local_path.lstrip(os.sep))
return os.path.exists(filename) and filename
- def add_mtime(self, url):
+ def add_suffix(self, url):
filename = self.guess_filename(url)
- mtime = filename and get_hashed_mtime(filename) or self.mtime
- if mtime is None:
+ suffix = None
+ if filename:
+ if settings.COMPRESS_CSS_HASHING_METHOD == "mtime":
+ suffix = get_hashed_mtime(filename)
+ elif settings.COMPRESS_CSS_HASHING_METHOD == "hash":
+ hash_file = open(filename)
+ try:
+ suffix = get_hexdigest(hash_file.read(), 12)
+ finally:
+ hash_file.close()
+ else:
+ raise FilterError('COMPRESS_CSS_HASHING_METHOD is configured '
+ 'with an unknown method (%s).')
+ if suffix is None:
return url
if url.startswith(('http://', 'https://', '/')):
if "?" in url:
- url = "%s&%s" % (url, mtime)
+ url = "%s&%s" % (url, suffix)
else:
- url = "%s?%s" % (url, mtime)
+ url = "%s?%s" % (url, suffix)
return url
def url_converter(self, matchobj):
url = matchobj.group(1)
url = url.strip(' \'"')
if url.startswith(('http://', 'https://', '/', 'data:')):
- return "url('%s')" % self.add_mtime(url)
+ return "url('%s')" % self.add_suffix(url)
full_url = posixpath.normpath('/'.join([str(self.directory_name), url]))
if self.has_scheme:
full_url = "%s%s" % (self.protocol, full_url)
- return "url('%s')" % self.add_mtime(full_url)
+ return "url('%s')" % self.add_suffix(full_url)
diff --git a/docs/settings.txt b/docs/settings.txt
index 2e86863..fe1f6ae 100644
--- a/docs/settings.txt
+++ b/docs/settings.txt
@@ -70,6 +70,11 @@ Possible options are:
A filter that normalizes the URLs used in ``url()`` CSS statements.
+- ``COMPRESS_CSS_HASHING_METHOD`` -- The method to use when calculating
+ the hash to append to processed URLs. Either ``'mtime'`` (default) or
+ ``'hash'``. Use the latter in case you're using multiple server to serve
+ your static files.
+
``compressor.filters.csstidy.CSSTidyFilter``
""""""""""""""""""""""""""""""""""""""""""""
diff --git a/tests/media/css/url/2/url2.css b/tests/media/css/url/2/url2.css
index 1114622..48e20a5 100644
--- a/tests/media/css/url/2/url2.css
+++ b/tests/media/css/url/2/url2.css
@@ -1,4 +1,4 @@
-p { background: url('../../../images/test.png'); }
-p { background: url(../../../images/test.png); }
-p { background: url( ../../../images/test.png ); }
-p { background: url( '../../../images/test.png' ); }
+p { background: url('../../../img/add.png'); }
+p { background: url(../../../img/add.png); }
+p { background: url( ../../../img/add.png ); }
+p { background: url( '../../../img/add.png' ); }
diff --git a/tests/media/css/url/url1.css b/tests/media/css/url/url1.css
index d202a47..e77e922 100644
--- a/tests/media/css/url/url1.css
+++ b/tests/media/css/url/url1.css
@@ -1,4 +1,4 @@
-p { background: url('../../images/test.png'); }
-p { background: url(../../images/test.png); }
-p { background: url( ../../images/test.png ); }
-p { background: url( '../../images/test.png' ); }
+p { background: url('../../img/python.png'); }
+p { background: url(../../img/python.png); }
+p { background: url( ../../img/python.png ); }
+p { background: url( '../../img/python.png' ); }
diff --git a/tests/tests/filters.py b/tests/tests/filters.py
index ee8dac0..f6cf287 100644
--- a/tests/tests/filters.py
+++ b/tests/tests/filters.py
@@ -86,62 +86,88 @@ class CssAbsolutizingTestCase(TestCase):
def setUp(self):
settings.COMPRESS_ENABLED = True
settings.COMPRESS_URL = '/media/'
+ settings.COMPRESS_CSS_HASHING_METHOD = 'mtime'
self.css = """
"""
self.css_node = CssCompressor(self.css)
+ def suffix_method(self, filename):
+ return get_hashed_mtime(filename)
+
def test_css_absolute_filter(self):
from compressor.filters.css_default import CssAbsoluteFilter
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
- content = "p { background: url('../../images/image.gif') }"
- output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
+ imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
+ content = "p { background: url('../../img/python.png') }"
+ output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.suffix_method(imagefilename))
filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
settings.COMPRESS_URL = 'http://media.example.com/'
filter = CssAbsoluteFilter(content)
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
- output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
+ output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.suffix_method(imagefilename))
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
def test_css_absolute_filter_https(self):
from compressor.filters.css_default import CssAbsoluteFilter
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
- content = "p { background: url('../../images/image.gif') }"
- output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
+ imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
+ content = "p { background: url('../../img/python.png') }"
+ output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.suffix_method(imagefilename))
filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
settings.COMPRESS_URL = 'https://media.example.com/'
filter = CssAbsoluteFilter(content)
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
- output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
+ output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.suffix_method(imagefilename))
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
def test_css_absolute_filter_relative_path(self):
from compressor.filters.css_default import CssAbsoluteFilter
filename = os.path.join(settings.TEST_DIR, 'whatever', '..', 'media', 'whatever/../css/url/test.css')
- content = "p { background: url('../../images/image.gif') }"
- output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
+ imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
+ content = "p { background: url('../../img/python.png') }"
+ output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.suffix_method(imagefilename))
filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
settings.COMPRESS_URL = 'https://media.example.com/'
filter = CssAbsoluteFilter(content)
- output = "p { background: url('%simages/image.gif?%s') }" % (settings.COMPRESS_URL, get_hashed_mtime(filename))
+ output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.suffix_method(imagefilename))
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
def test_css_hunks(self):
hash_dict = {
- 'hash1': get_hashed_mtime(os.path.join(settings.COMPRESS_ROOT, 'css/url/url1.css')),
- 'hash2': get_hashed_mtime(os.path.join(settings.COMPRESS_ROOT, 'css/url/2/url2.css')),
+ 'hash1': self.suffix_method(os.path.join(settings.COMPRESS_ROOT, 'img/python.png')),
+ 'hash2': self.suffix_method(os.path.join(settings.COMPRESS_ROOT, 'img/add.png')),
}
- out = [u"p { background: url('/media/images/test.png?%(hash1)s'); }\np { background: url('/media/images/test.png?%(hash1)s'); }\np { background: url('/media/images/test.png?%(hash1)s'); }\np { background: url('/media/images/test.png?%(hash1)s'); }\n" % hash_dict,
- u"p { background: url('/media/images/test.png?%(hash2)s'); }\np { background: url('/media/images/test.png?%(hash2)s'); }\np { background: url('/media/images/test.png?%(hash2)s'); }\np { background: url('/media/images/test.png?%(hash2)s'); }\n" % hash_dict]
+ out = [u"p { background: url('/media/img/python.png?%(hash1)s'); }\np { background: url('/media/img/python.png?%(hash1)s'); }\np { background: url('/media/img/python.png?%(hash1)s'); }\np { background: url('/media/img/python.png?%(hash1)s'); }\n" % hash_dict,
+ u"p { background: url('/media/img/add.png?%(hash2)s'); }\np { background: url('/media/img/add.png?%(hash2)s'); }\np { background: url('/media/img/add.png?%(hash2)s'); }\np { background: url('/media/img/add.png?%(hash2)s'); }\n" % hash_dict]
hunks = [h for m, h in self.css_node.hunks()]
self.assertEqual(out, hunks)
+class CssAbsolutizingTestCaseWithHash(CssAbsolutizingTestCase):
+
+ def setUp(self):
+ settings.COMPRESS_ENABLED = True
+ settings.COMPRESS_URL = '/media/'
+ settings.COMPRESS_CSS_HASHING_METHOD = 'hash'
+ self.css = """
+
+
+ """
+ self.css_node = CssCompressor(self.css)
+
+ def suffix_method(self, filename):
+ f = open(filename)
+ suffix = "H%s" % (get_hexdigest(f.read(), 12), )
+ f.close()
+ return suffix
+
+
class CssDataUriTestCase(TestCase):
def setUp(self):
settings.COMPRESS_ENABLED = True
@@ -150,6 +176,7 @@ class CssDataUriTestCase(TestCase):
'compressor.filters.datauri.CssDataUriFilter',
]
settings.COMPRESS_URL = '/media/'
+ settings.COMPRESS_CSS_HASHING_METHOD = 'mtime'
self.css = """
"""