diff --git a/README.rst b/README.rst
index 7c2f634..0a3498e 100644
--- a/README.rst
+++ b/README.rst
@@ -57,6 +57,9 @@ Configure the following settings::
# ...
)
+ # The subdirectory of STATIC_ROOT that you wish to use for compiled bundles
+ BUNDLES_DIR = 'bundled'
+
# This is the important part.
MINIFY_BUNDLES = {
'css': {},
diff --git a/jingo_minify/helpers.py b/jingo_minify/helpers.py
index d049345..dc44016 100644
--- a/jingo_minify/helpers.py
+++ b/jingo_minify/helpers.py
@@ -70,7 +70,10 @@ def get_js_urls(bundle, debug=None):
bundle_full = 'js:%s' % bundle
if bundle_full in BUNDLE_HASHES:
build_id = BUNDLE_HASHES[bundle_full]
- return (_get_item_path('js/%s-min.js?build=%s' % (bundle, build_id,)),)
+ return (_get_item_path(
+ os.path.join(
+ getattr(settings, 'BUNDLES_DIR', ''),
+ 'js/%s-min.js?build=%s' % (bundle, build_id,))),)
def _get_compiled_css_url(item):
@@ -120,8 +123,9 @@ def get_css_urls(bundle, debug=None):
bundle_full = 'css:%s' % bundle
if bundle_full in BUNDLE_HASHES:
build_id = BUNDLE_HASHES[bundle_full]
- return (_get_item_path('css/%s-min.css?build=%s' %
- (bundle, build_id)),)
+ item = 'css/%s-min.css?build=%s' % (bundle, build_id)
+ item = os.path.join(getattr(settings, 'BUNDLES_DIR', ''), item)
+ return (_get_item_path(item),)
@register.function
diff --git a/jingo_minify/management/commands/compress_assets.py b/jingo_minify/management/commands/compress_assets.py
index 7467ae1..b46529a 100644
--- a/jingo_minify/management/commands/compress_assets.py
+++ b/jingo_minify/management/commands/compress_assets.py
@@ -32,10 +32,12 @@ class Command(BaseCommand): # pragma: no cover
checked_hash = {}
bundle_hashes = {}
+ tmp_files = []
missing_files = 0
minify_skipped = 0
cmd_errors = False
ext_media_path = os.path.join(get_media_root(), 'external')
+ bundles_dir = getattr(settings, 'BUNDLES_DIR', '.')
def update_hashes(self, update=False):
def media_git_id(media_path):
@@ -76,12 +78,17 @@ def handle(self, **options):
for ftype, bundle in settings.MINIFY_BUNDLES.iteritems():
for name, files in bundle.iteritems():
# Set the paths to the files.
- concatted_file = path(ftype, '%s-all.%s' % (name, ftype,))
- compressed_file = path(ftype, '%s-min.%s' % (name, ftype,))
+ concatted_file = path(self.bundles_dir, ftype,
+ '%s-all.%s' % (name, ftype,))
+ compressed_file = path(self.bundles_dir, ftype,
+ '%s-min.%s' % (name, ftype,))
+
+ if not os.path.exists(path(self.bundles_dir, ftype)):
+ os.makedirs(path(self.bundles_dir, ftype))
files_all = []
for fn in files:
- processed = self._preprocess_file(fn)
+ processed = self._preprocess_file(fn, compressed_file)
# If the file can't be processed, we skip it.
if processed is not None:
files_all.append(processed)
@@ -111,6 +118,16 @@ def handle(self, **options):
else:
self.minify_skipped += 1
+ # Clean up.
+ for file in self.tmp_files:
+ try:
+ os.remove(path(file))
+
+ # Try to remove the temporary directory (only removed if empty)
+ os.rmdir(os.path.dirname(path(file)))
+ except OSError:
+ pass
+
# Write out the hashes
self.update_hashes(options.get('add_timestamp', False))
@@ -134,11 +151,11 @@ def _get_url_or_path(self, item):
"""
if item.startswith('//'):
return 'http:%s' % item
- elif item.startswith(('http', 'https')):
+ elif item.startswith(('http:', 'https:')):
return item
return None
- def _preprocess_file(self, filename):
+ def _preprocess_file(self, filename, compressed_file):
"""Preprocess files and return new filenames."""
url = self._get_url_or_path(filename)
if url:
@@ -186,6 +203,12 @@ def _preprocess_file(self, filename):
(settings.STYLUS_BIN, os.path.dirname(fp), fp, fp),
shell=True, stdout=PIPE)
filename = '%s.css' % filename
+
+ if not url:
+ # Fix relative paths.
+ filename = self._fix_urls(filename, compressed_file)
+ self.tmp_files.append(filename)
+
return path(filename.lstrip('/'))
def _is_changed(self, concatted_file):
@@ -205,6 +228,38 @@ def _clean_tmp(self, concatted_file):
os.remove(concatted_file)
os.rename(tmp_concatted, concatted_file)
+ def _fix_urls(self, filename, compressed_file):
+ """Fix the relative URLs in css files."""
+ css_content = ''
+ with open(path(filename)) as css_in:
+ css_content = css_in.read()
+
+ relpath = os.path.relpath(os.path.dirname(compressed_file),
+ path(os.path.dirname(filename)))
+
+ parse = lambda url: self._fix_urls_regex(url, relpath)
+
+ css_parsed = re.sub(r'url\(([^)]*?)\)', parse, css_content)
+
+ # Write to a temporary file.
+ out_file = path(os.path.join(self.bundles_dir, '.tmp',
+ '%s.tmp' % filename))
+
+ if not os.path.exists(os.path.dirname(out_file)):
+ os.makedirs(os.path.dirname(out_file))
+
+ with open(out_file, 'w') as css_out:
+ css_out.write(css_parsed)
+
+ return os.path.relpath(out_file, path('.'))
+
+ def _fix_urls_regex(self, url, relpath):
+ """Run over the regex; Fix relative URL's"""
+ url = url.group(1).strip('"\'')
+ if not url.startswith(('data:', 'http:', 'https:')):
+ url = os.path.relpath(url, relpath)
+ return 'url(%s)' % url
+
def _cachebust(self, css_file, bundle_name):
"""Cache bust images. Return a new bundle hash."""
print "Cache busting images in %s" % re.sub('.tmp$', '', css_file)
@@ -268,7 +323,7 @@ def _file_hash(self, url):
def _cachebust_regex(self, img, parent):
"""Run over the regex; img is the structural regex object."""
url = img.group(1).strip('"\'')
- if url.startswith('data:') or url.startswith('http'):
+ if url.startswith(('data:', 'http:', 'https:')):
return "url(%s)" % url
url = url.split('?')[0]
diff --git a/jingo_minify/tests.py b/jingo_minify/tests.py
index 80c9c69..5507f32 100644
--- a/jingo_minify/tests.py
+++ b/jingo_minify/tests.py
@@ -1,11 +1,16 @@
+import os
+import tempfile
+
from django.conf import settings
from django.test.utils import override_settings
import jingo
+
from mock import ANY, call, patch
from nose.tools import eq_
from .utils import get_media_root, get_media_url
+from .management.commands.compress_assets import Command
try:
from build import BUILD_ID_CSS, BUILD_ID_JS
@@ -66,8 +71,9 @@ def test_js_helper(getmtime, time):
t = env.from_string("{{ js('common_protocol_less_url', debug=False) }}")
s = t.render()
- eq_(s, '' % (settings.STATIC_URL, BUILD_ID_JS))
+ eq_(s,
+ '' % (settings.STATIC_URL, BUILD_ID_JS))
t = env.from_string("{{ js('common_bundle', debug=True) }}")
s = t.render()
@@ -81,7 +87,7 @@ def test_js_helper(getmtime, time):
s = t.render()
eq_(s, '' %
- (settings.STATIC_URL, BUILD_ID_JS))
+ (settings.STATIC_URL, BUILD_ID_JS))
@patch('jingo_minify.helpers.time.time')
@@ -163,6 +169,33 @@ def test_css_helper(getmtime, time):
(settings.STATIC_URL, BUILD_ID_CSS))
+@patch('jingo_minify.helpers.time.time')
+@patch('jingo_minify.helpers.os.path.getmtime')
+@override_settings(STATIC_ROOT='static',
+ MEDIA_ROOT='media',
+ STATIC_URL='http://example.com/static/',
+ MEDIA_URL='http://example.com/media/',
+ BUNDLES_DIR='bundled')
+def test_bundles_dir(getmtime, time):
+ getmtime.return_value = 1
+ time.return_value = 1
+ env = jingo.env
+
+ t = env.from_string("{{ js('common', debug=False) }}")
+ s = t.render()
+
+ eq_(s, '' %
+ (settings.STATIC_URL, BUILD_ID_JS))
+
+ t = env.from_string("{{ css('common', debug=False) }}")
+ s = t.render()
+
+ eq_(s,
+ ''
+ % (settings.STATIC_URL, BUILD_ID_CSS))
+
+
def test_inline_css_helper():
env = jingo.env
t = env.from_string("{{ inline_css('common', debug=True) }}")
@@ -296,3 +329,72 @@ def test_js(getmtime, time):
for j in settings.MINIFY_BUNDLES['js']['common']])
eq_(s, expected)
+
+
+@override_settings(STATIC_ROOT='static',
+ MEDIA_ROOT='media',
+ STATIC_URL='http://example.com/static/',
+ MEDIA_URL='http://example.com/media/')
+def test_fix_urls():
+ """Make sure relative URLs are fixed correctly."""
+ command = Command()
+
+ tmp = tempfile.mkstemp()[1]
+
+ tmp_file = open(tmp, 'w')
+ tmp_file.write('url(../img/test.png);')
+ tmp_file.close()
+
+ # Test files in same directory
+ compressed = command._fix_urls(tmp,
+ os.path.join(os.path.dirname(tmp), 'compiled'))
+ compressed_file = open(compressed, 'r')
+ eq_(compressed_file.read(), 'url(../img/test.png);')
+ compressed_file.close()
+
+ # Test files in different directory
+ compressed = command._fix_urls(tmp,
+ os.path.join(os.path.dirname(tmp), 'dir', 'compiled'))
+ compressed_file = open(compressed, 'r')
+ eq_(compressed_file.read(), 'url(../../img/test.png);')
+ compressed_file.close()
+
+ # Remove temporary files
+ os.remove(compressed)
+ os.remove(tmp)
+
+ # Test data urls are unchanged
+ data_url = 'url(data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulr);'
+
+ tmp = tempfile.mkstemp()[1]
+ tmp_file = open(tmp, 'w')
+ tmp_file.write(data_url)
+ tmp_file.close()
+
+ compressed = command._fix_urls(tmp,
+ os.path.join(os.path.dirname(tmp), 'compiled'))
+ compressed_file = open(compressed, 'r')
+ eq_(compressed_file.read(), data_url)
+ compressed_file.close()
+
+ # Remove temporary files
+ os.remove(compressed)
+ os.remove(tmp)
+
+ # Test absolute paths are unchanged
+ content = ('url(http://example.com/image.gif); '
+ 'url(https://example.com/image.png);')
+ tmp = tempfile.mkstemp()[1]
+ tmp_file = open(tmp, 'w')
+ tmp_file.write(content)
+ tmp_file.close()
+
+ compressed = command._fix_urls(tmp,
+ os.path.join(os.path.dirname(tmp), 'compiled'))
+ compressed_file = open(compressed, 'r')
+ eq_(compressed_file.read(), content)
+ compressed_file.close()
+
+ # Remove temporary files
+ os.remove(compressed)
+ os.remove(tmp)
diff --git a/requirements.txt b/requirements.txt
index 8c9140b..596451a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,3 +2,4 @@ Django==1.4.5
jingo==0.4
-e git://github.com/jbalogh/django-nose.git@83c7867c3f90ff3c7c7471716da91b643e8b2c01#egg=django_nose-dev
mock==1.0b1
+GitPython==0.1.7