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 = """ """