Merge branch 'release/1.5'

This commit is contained in:
Mathieu Pillard
2015-04-20 16:18:13 +02:00
36 changed files with 535 additions and 285 deletions

View File

@@ -3,20 +3,25 @@ before_install:
- sudo apt-get update
- sudo apt-get install csstidy libxml2-dev libxslt-dev
install:
- pip install tox coveralls
- pip install tox
script:
- tox
env:
- TOXENV=py33-1.6.X
- TOXENV=py32-1.6.X
- TOXENV=py27-1.6.X
- TOXENV=py26-1.6.X
- TOXENV=py33-1.5.X
- TOXENV=py32-1.5.X
- TOXENV=py27-1.5.X
- TOXENV=py26-1.4.X
- TOXENV=py26-1.5.X
- TOXENV=py27-1.4.X
- TOXENV=py26-1.4.X
- TOXENV=py27-1.5.X
- TOXENV=py26-1.6.X
- TOXENV=py27-1.6.X
- TOXENV=py32-1.6.X
- TOXENV=py33-1.6.X
- TOXENV=py27-1.7.X
- TOXENV=py32-1.7.X
- TOXENV=py33-1.7.X
- TOXENV=py34-1.7.X
- TOXENV=py27-1.8.X
- TOXENV=py32-1.8.X
- TOXENV=py33-1.8.X
- TOXENV=py34-1.8.X
notifications:
irc: "irc.freenode.org#django-compressor"
after_success: coveralls

View File

@@ -29,6 +29,7 @@ Bojan Mihelac
Boris Shemigon
Brad Whittington
Bruno Renié
Carlton Gibson
Cassus Adam Banko
Chris Adams
Chris Streeter

View File

@@ -4,16 +4,19 @@ Django Compressor
.. image:: https://coveralls.io/repos/django-compressor/django-compressor/badge.png?branch=develop
:target: https://coveralls.io/r/django-compressor/django-compressor?branch=develop
.. image:: https://pypip.in/v/django_compressor/badge.png
.. image:: https://pypip.in/v/django_compressor/badge.svg
:target: https://pypi.python.org/pypi/django_compressor
.. image:: https://pypip.in/d/django_compressor/badge.png
.. image:: https://pypip.in/d/django_compressor/badge.svg
:target: https://pypi.python.org/pypi/django_compressor
.. image:: https://secure.travis-ci.org/django-compressor/django-compressor.png?branch=develop
.. image:: https://secure.travis-ci.org/django-compressor/django-compressor.svg?branch=develop
:alt: Build Status
:target: http://travis-ci.org/django-compressor/django-compressor
.. image:: https://caniusepython3.com/project/django_compressor.svg
:target: https://caniusepython3.com/project/django_compressor
Django Compressor combines and compresses linked and inline Javascript
or CSS in a Django template into cacheable static files by using the
``compress`` template tag.

View File

@@ -5,7 +5,10 @@ import codecs
from django.core.files.base import ContentFile
from django.template import Context
from django.template.loader import render_to_string
from django.utils.importlib import import_module
try:
from importlib import import_module
except:
from django.utils.importlib import import_module
from django.utils.safestring import mark_safe
try:
@@ -140,6 +143,9 @@ class Compressor(object):
"""
Reads file contents using given `charset` and returns it as text.
"""
if charset == 'utf-8':
# Removes BOM
charset = 'utf-8-sig'
with codecs.open(filename, 'r', charset) as fd:
try:
return fd.read()
@@ -247,7 +253,7 @@ class Compressor(object):
mod_name, cls_name = get_mod_func(filter_or_command)
try:
mod = import_module(mod_name)
except ImportError:
except (ImportError, TypeError):
filter = CompilerFilter(
content, filter_type=self.type, filename=filename,
charset=charset, command=filter_or_command)

View File

@@ -4,11 +4,21 @@ import os
import socket
import time
from django.core.cache import get_cache
try:
from django.core.cache import caches
def get_cache(name):
return caches[name]
except ImportError:
from django.core.cache import get_cache
from django.core.files.base import ContentFile
from django.utils.encoding import force_text, smart_bytes
from django.utils.functional import SimpleLazyObject
from django.utils.importlib import import_module
try:
from importlib import import_module
except:
from django.utils.importlib import import_module
from compressor.conf import settings
from compressor.storage import default_storage
@@ -39,7 +49,7 @@ def get_cachekey(*args, **kwargs):
mod_name, func_name = get_mod_func(
settings.COMPRESS_CACHE_KEY_FUNCTION)
_cachekey_func = getattr(import_module(mod_name), func_name)
except (AttributeError, ImportError) as e:
except (AttributeError, ImportError, TypeError) as e:
raise ImportError("Couldn't import cache key function %s: %s" %
(settings.COMPRESS_CACHE_KEY_FUNCTION, e))
return _cachekey_func(*args, **kwargs)

View File

@@ -45,6 +45,8 @@ class CompressorConf(AppConf):
YUGLIFY_BINARY = 'yuglify'
YUGLIFY_CSS_ARGUMENTS = '--terminal'
YUGLIFY_JS_ARGUMENTS = '--terminal'
CLEAN_CSS_BINARY = 'cleancss'
CLEAN_CSS_ARGUMENTS = ''
DATA_URI_MAX_SIZE = 1024
# the cache backend to use

View File

@@ -34,7 +34,7 @@ class CssCompressor(Compressor):
self.media_nodes[-1][1].split_content.append(data)
else:
node = self.__class__(content=self.parser.elem_str(elem),
context=self.context)
context=self.context)
node.split_content.append(data)
self.media_nodes.append((media, node))
return self.split_content

View File

@@ -3,9 +3,27 @@ import io
import logging
import subprocess
from platform import system
if system() != "Windows":
try:
from shlex import quote as shell_quote # Python 3
except ImportError:
from pipes import quote as shell_quote # Python 2
else:
from subprocess import list2cmdline
def shell_quote(s):
# shlex.quote/pipes.quote is not compatible with Windows
return list2cmdline([s])
from django.core.exceptions import ImproperlyConfigured
from django.core.files.temp import NamedTemporaryFile
from django.utils.importlib import import_module
try:
from importlib import import_module
except ImportError:
from django.utils.importlib import import_module
from django.utils.encoding import smart_text
from django.utils import six
@@ -26,7 +44,7 @@ class FilterBase(object):
"""
def __init__(self, content, filter_type=None, filename=None, verbose=0,
charset=None):
self.type = filter_type
self.type = filter_type or getattr(self, 'type', None)
self.content = content
self.verbose = verbose or settings.COMPRESS_VERBOSE
self.logger = logger
@@ -65,7 +83,7 @@ class CallbackOutputFilter(FilterBase):
try:
mod_name, func_name = get_mod_func(self.callback)
func = getattr(import_module(mod_name), func_name)
except ImportError:
except (ImportError, TypeError):
if self.dependencies:
if len(self.dependencies) == 1:
warning = "dependency (%s) is" % self.dependencies[0]
@@ -147,6 +165,12 @@ class CompilerFilter(FilterBase):
self.outfile = NamedTemporaryFile(mode='r+', suffix=ext)
options["outfile"] = self.outfile.name
# Quote infile and outfile for spaces etc.
if "infile" in options:
options["infile"] = shell_quote(options["infile"])
if "outfile" in options:
options["outfile"] = shell_quote(options["outfile"])
try:
command = self.command.format(**options)
proc = subprocess.Popen(

View File

@@ -0,0 +1,10 @@
from compressor.conf import settings
from compressor.filters import CompilerFilter
class CleanCSSFilter(CompilerFilter):
command = "{binary} {args} -o {outfile} {infile}"
options = (
("binary", settings.COMPRESS_CLEAN_CSS_BINARY),
("args", settings.COMPRESS_CLEAN_CSS_ARGUMENTS),
)

View File

@@ -5,7 +5,6 @@ import posixpath
from compressor.cache import get_hashed_mtime, get_hashed_content
from compressor.conf import settings
from compressor.filters import FilterBase, FilterError
from compressor.utils import staticfiles
URL_PATTERN = re.compile(r'url\(([^\)]+)\)')
SRC_PATTERN = re.compile(r'src=([\'"])(.+?)\1')
@@ -22,10 +21,7 @@ class CssAbsoluteFilter(FilterBase):
self.has_scheme = False
def input(self, filename=None, basename=None, **kwargs):
if filename is not None:
filename = os.path.normcase(os.path.abspath(filename))
if (not (filename and filename.startswith(self.root)) and
not self.find(basename)):
if not filename:
return self.content
self.path = basename.replace(os.sep, '/')
self.path = self.path.lstrip('/')
@@ -40,10 +36,6 @@ class CssAbsoluteFilter(FilterBase):
return SRC_PATTERN.sub(self.src_converter,
URL_PATTERN.sub(self.url_converter, self.content))
def find(self, basename):
if settings.DEBUG and basename and staticfiles.finders:
return staticfiles.finders.find(basename)
def guess_filename(self, url):
local_path = url
if self.has_scheme:
@@ -70,6 +62,8 @@ class CssAbsoluteFilter(FilterBase):
suffix = get_hashed_mtime(filename)
elif settings.COMPRESS_CSS_HASHING_METHOD in ("hash", "content"):
suffix = get_hashed_content(filename)
elif settings.COMPRESS_CSS_HASHING_METHOD is None:
suffix = None
else:
raise FilterError('COMPRESS_CSS_HASHING_METHOD is configured '
'with an unknown method (%s).' %

View File

@@ -12,14 +12,42 @@ class JsCompressor(Compressor):
def split_contents(self):
if self.split_content:
return self.split_content
self.extra_nodes = []
for elem in self.parser.js_elems():
attribs = self.parser.elem_attribs(elem)
if 'src' in attribs:
basename = self.get_basename(attribs['src'])
filename = self.get_filename(basename)
content = (SOURCE_FILE, filename, basename, elem)
self.split_content.append(content)
else:
content = self.parser.elem_content(elem)
self.split_content.append((SOURCE_HUNK, content, None, elem))
content = (SOURCE_HUNK, self.parser.elem_content(elem), None, elem)
self.split_content.append(content)
if 'async' in attribs:
extra = ' async'
elif 'defer' in attribs:
extra = ' defer'
else:
extra = ''
# Append to the previous node if it had the same attribute
append_to_previous = (self.extra_nodes and
self.extra_nodes[-1][0] == extra)
if append_to_previous and settings.COMPRESS_ENABLED:
self.extra_nodes[-1][1].split_content.append(content)
else:
node = self.__class__(content=self.parser.elem_str(elem),
context=self.context)
node.split_content.append(content)
self.extra_nodes.append((extra, node))
return self.split_content
def output(self, *args, **kwargs):
if (settings.COMPRESS_ENABLED or settings.COMPRESS_PRECOMPILERS or
kwargs.get('forced', False)):
self.split_contents()
if hasattr(self, 'extra_nodes'):
ret = []
for extra, subnode in self.extra_nodes:
subnode.extra_context.update({'extra': extra})
ret.append(subnode.output(*args, **kwargs))
return '\n'.join(ret)
return super(JsCompressor, self).output(*args, **kwargs)

View File

@@ -5,6 +5,7 @@ import sys
from fnmatch import fnmatch
from optparse import make_option
import django
from django.core.management.base import NoArgsCommand, CommandError
import django.template
from django.template import Context
@@ -53,24 +54,30 @@ class Command(NoArgsCommand):
requires_model_validation = False
def get_loaders(self):
from django.template.loader import template_source_loaders
if template_source_loaders is None:
try:
from django.template.loader import (
find_template as finder_func)
except ImportError:
from django.template.loader import (
find_template_source as finder_func) # noqa
try:
# Force django to calculate template_source_loaders from
# TEMPLATE_LOADERS settings, by asking to find a dummy template
source, name = finder_func('test')
except django.template.TemplateDoesNotExist:
pass
# Reload template_source_loaders now that it has been calculated ;
# it should contain the list of valid, instanciated template loaders
# to use.
if django.VERSION < (1, 8):
from django.template.loader import template_source_loaders
if template_source_loaders is None:
try:
from django.template.loader import (
find_template as finder_func)
except ImportError:
from django.template.loader import (
find_template_source as finder_func) # noqa
try:
# Force django to calculate template_source_loaders from
# TEMPLATE_LOADERS settings, by asking to find a dummy template
source, name = finder_func('test')
except django.template.TemplateDoesNotExist:
pass
# Reload template_source_loaders now that it has been calculated ;
# it should contain the list of valid, instanciated template loaders
# to use.
from django.template.loader import template_source_loaders
else:
from django.template import engines
template_source_loaders = []
for e in engines.all():
template_source_loaders.extend(e.engine.get_template_loaders(e.engine.loaders))
loaders = []
# If template loader is CachedTemplateLoader, return the loaders
# that it wraps around. So if we have
@@ -130,7 +137,7 @@ class Command(NoArgsCommand):
if get_template_sources is None:
get_template_sources = loader.get_template_sources
paths.update(list(get_template_sources('')))
except (ImportError, AttributeError):
except (ImportError, AttributeError, TypeError):
# Yeah, this didn't work out so well, let's move on
pass
if not paths:

View File

@@ -1,13 +1,13 @@
from __future__ import absolute_import
import io
from copy import copy
import django
from django import template
from django.conf import settings
from django.template import Template
from django.template import Context
from django.template.base import Node, VariableNode, TextNode, NodeList
from django.template.defaulttags import IfNode
from django.template.loader import get_template
from django.template.loader_tags import ExtendsNode, BlockNode, BlockContext
@@ -15,7 +15,7 @@ from compressor.exceptions import TemplateSyntaxError, TemplateDoesNotExist
from compressor.templatetags.compress import CompressorNode
def handle_extendsnode(extendsnode, block_context=None):
def handle_extendsnode(extendsnode, block_context=None, original=None):
"""Create a copy of Node tree of a derived template replacing
all blocks tags with the nodes of appropriate blocks.
Also handles {{ block.super }} tags.
@@ -27,6 +27,9 @@ def handle_extendsnode(extendsnode, block_context=None):
block_context.add_blocks(blocks)
context = Context(settings.COMPRESS_OFFLINE_CONTEXT)
if original is not None:
context.template = original
compiled_parent = extendsnode.get_parent(context)
parent_nodelist = compiled_parent.nodelist
# If the parent template has an ExtendsNode it is not the root.
@@ -34,7 +37,7 @@ def handle_extendsnode(extendsnode, block_context=None):
# The ExtendsNode has to be the first non-text node.
if not isinstance(node, TextNode):
if isinstance(node, ExtendsNode):
return handle_extendsnode(node, block_context)
return handle_extendsnode(node, block_context, original)
break
# Add blocks of the root template to block context.
blocks = dict((n.name, n) for n in
@@ -55,6 +58,8 @@ def remove_block_nodes(nodelist, block_stack, block_context):
if not block_stack:
continue
node = block_context.get_block(block_stack[-1].name)
if not node:
continue
if isinstance(node, BlockNode):
expanded_block = expand_blocknode(node, block_stack, block_context)
new_nodelist.extend(expanded_block)
@@ -93,13 +98,15 @@ class DjangoParser(object):
self.charset = charset
def parse(self, template_name):
with io.open(template_name, mode='rb') as file:
try:
return Template(file.read().decode(self.charset))
except template.TemplateSyntaxError as e:
raise TemplateSyntaxError(str(e))
except template.TemplateDoesNotExist as e:
raise TemplateDoesNotExist(str(e))
try:
if django.VERSION < (1, 8):
return get_template(template_name)
else:
return get_template(template_name).template
except template.TemplateSyntaxError as e:
raise TemplateSyntaxError(str(e))
except template.TemplateDoesNotExist as e:
raise TemplateDoesNotExist(str(e))
def process_template(self, template, context):
return True
@@ -111,15 +118,17 @@ class DjangoParser(object):
pass
def render_nodelist(self, template, context, node):
if django.VERSION >= (1, 8):
context.template = template
return node.nodelist.render(context)
def render_node(self, template, context, node):
return node.render(context, forced=True)
def get_nodelist(self, node):
def get_nodelist(self, node, original=None):
if isinstance(node, ExtendsNode):
try:
return handle_extendsnode(node)
return handle_extendsnode(node, block_context=None, original=original)
except template.TemplateSyntaxError as e:
raise TemplateSyntaxError(str(e))
except template.TemplateDoesNotExist as e:
@@ -134,10 +143,12 @@ class DjangoParser(object):
nodelist = getattr(node, 'nodelist', [])
return nodelist
def walk_nodes(self, node):
for node in self.get_nodelist(node):
def walk_nodes(self, node, original=None):
if django.VERSION >= (1, 8) and original is None:
original = node
for node in self.get_nodelist(node, original):
if isinstance(node, CompressorNode) and node.is_offline_compression_enabled(forced=True):
yield node
else:
for node in self.walk_nodes(node):
for node in self.walk_nodes(node, original):
yield node

View File

@@ -1,6 +1,9 @@
from django.utils import six
from django.utils.functional import LazyObject
from django.utils.importlib import import_module
try:
from importlib import import_module
except ImportError:
from django.utils.importlib import import_module
# support legacy parser module usage
from compressor.parser.base import ParserBase # noqa
@@ -30,5 +33,5 @@ class AutoSelectParser(LazyObject):
import_module(dependency)
self._wrapped = parser(content)
break
except ImportError:
except (ImportError, TypeError):
continue

View File

@@ -1 +1 @@
<script type="text/javascript" src="{{ compressed.url }}"></script>
<script type="text/javascript" src="{{ compressed.url }}"{{ compressed.extra }}></script>

View File

@@ -3,7 +3,13 @@ import django
TEST_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'tests')
COMPRESS_CACHE_BACKEND = 'locmem://'
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'unique-snowflake'
}
}
DATABASES = {
'default': {
@@ -13,9 +19,17 @@ DATABASES = {
}
INSTALLED_APPS = [
'django.contrib.staticfiles',
'compressor',
'coffin',
'jingo',
]
if django.VERSION < (1, 8):
INSTALLED_APPS.append('jingo')
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder',
]
STATIC_URL = '/static/'
@@ -38,3 +52,5 @@ SECRET_KEY = "iufoj=mibkpdz*%bob952x(%49rqgv8gg45k36kjcg76&-y5=!"
PASSWORD_HASHERS = (
'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
)
MIDDLEWARE_CLASSES = []

View File

@@ -7,11 +7,11 @@ import sys
def main():
p = optparse.OptionParser()
p.add_option('-f', '--file', action="store",
type="string", dest="filename",
help="File to read from, defaults to stdin", default=None)
type="string", dest="filename",
help="File to read from, defaults to stdin", default=None)
p.add_option('-o', '--output', action="store",
type="string", dest="outfile",
help="File to write to, defaults to stdout", default=None)
type="string", dest="outfile",
help="File to write to, defaults to stdout", default=None)
options, arguments = p.parse_args()

View File

@@ -0,0 +1 @@
body { background:#424242; }

View File

@@ -0,0 +1 @@
.compress-test {color: red;}

View File

@@ -0,0 +1 @@
hermanos = {}

View File

@@ -0,0 +1 @@
pollos = {}

View File

@@ -12,11 +12,13 @@ from django.core.cache.backends import locmem
from django.test import SimpleTestCase
from django.test.utils import override_settings
from compressor.base import SOURCE_HUNK, SOURCE_FILE
from compressor import cache as cachemod
from compressor.base import SOURCE_FILE, SOURCE_HUNK
from compressor.cache import get_cachekey
from compressor.conf import settings
from compressor.css import CssCompressor
from compressor.exceptions import FilterDoesNotExist, FilterError
from compressor.js import JsCompressor
from compressor.exceptions import FilterDoesNotExist
def make_soup(markup):
@@ -112,6 +114,14 @@ class CompressorTestCase(SimpleTestCase):
hunks = '\n'.join([h for h in self.css_node.hunks()])
self.assertEqual(out, hunks)
def test_css_output_with_bom_input(self):
out = 'body { background:#990; }\n.compress-test {color: red;}'
css = ("""<link rel="stylesheet" href="/static/css/one.css" type="text/css" />
<link rel="stylesheet" href="/static/css/utf-8_with-BOM.css" type="text/css" />""")
css_node_with_bom = CssCompressor(css)
hunks = '\n'.join([h for h in css_node_with_bom.hunks()])
self.assertEqual(out, hunks)
def test_css_mtimes(self):
is_date = re.compile(r'^\d{10}[\.\d]+$')
for date in self.css_node.mtimes:
@@ -208,6 +218,14 @@ class CompressorTestCase(SimpleTestCase):
css_node = CssCompressor(css)
self.assertRaises(FilterDoesNotExist, css_node.output, 'inline')
@override_settings(COMPRESS_PRECOMPILERS=(
('text/foobar', './foo -I ./bar/baz'),
), COMPRESS_ENABLED=True)
def test_command_with_dot_precompiler(self):
css = '<style type="text/foobar">p { border:10px solid red;}</style>'
css_node = CssCompressor(css)
self.assertRaises(FilterError, css_node.output, 'inline')
class CssMediaTestCase(SimpleTestCase):
def setUp(self):
@@ -267,4 +285,49 @@ class CacheBackendTestCase(CompressorTestCase):
def test_correct_backend(self):
from compressor.cache import cache
self.assertEqual(cache.__class__, locmem.CacheClass)
self.assertEqual(cache.__class__, locmem.LocMemCache)
class JsAsyncDeferTestCase(SimpleTestCase):
def setUp(self):
self.js = """\
<script src="/static/js/one.js" type="text/javascript"></script>
<script src="/static/js/two.js" type="text/javascript" async></script>
<script src="/static/js/three.js" type="text/javascript" defer></script>
<script type="text/javascript">obj.value = "value";</script>
<script src="/static/js/one.js" type="text/javascript" async></script>
<script src="/static/js/two.js" type="text/javascript" async></script>
<script src="/static/js/three.js" type="text/javascript"></script>"""
def test_js_output(self):
def extract_attr(tag):
if tag.has_attr('async'):
return 'async'
if tag.has_attr('defer'):
return 'defer'
js_node = JsCompressor(self.js)
output = [None, 'async', 'defer', None, 'async', None]
if six.PY3:
scripts = make_soup(js_node.output()).find_all('script')
attrs = [extract_attr(i) for i in scripts]
else:
scripts = make_soup(js_node.output()).findAll('script')
attrs = [s.get('async') or s.get('defer') for s in scripts]
self.assertEqual(output, attrs)
class CacheTestCase(SimpleTestCase):
def setUp(self):
cachemod._cachekey_func = None
def test_get_cachekey_basic(self):
self.assertEqual(get_cachekey("foo"), "django_compressor.foo")
@override_settings(COMPRESS_CACHE_KEY_FUNCTION='.leading.dot')
def test_get_cachekey_leading_dot(self):
self.assertRaises(ImportError, lambda: get_cachekey("foo"))
@override_settings(COMPRESS_CACHE_KEY_FUNCTION='invalid.module')
def test_get_cachekey_invalid_mod(self):
self.assertRaises(ImportError, lambda: get_cachekey("foo"))

View File

@@ -1,4 +1,5 @@
from __future__ import with_statement, unicode_literals
from collections import defaultdict
import io
import os
import sys
@@ -17,9 +18,18 @@ from compressor.filters.base import CompilerFilter
from compressor.filters.cssmin import CSSMinFilter
from compressor.filters.css_default import CssAbsoluteFilter
from compressor.filters.template import TemplateFilter
from compressor.filters.closure import ClosureCompilerFilter
from compressor.filters.csstidy import CSSTidyFilter
from compressor.filters.yuglify import YUglifyCSSFilter, YUglifyJSFilter
from compressor.filters.yui import YUICSSFilter, YUIJSFilter
from compressor.filters.cleancss import CleanCSSFilter
from compressor.tests.test_base import test_dir
def blankdict(*args, **kwargs):
return defaultdict(lambda: '', *args, **kwargs)
@unittest.skipIf(find_command(settings.COMPRESS_CSSTIDY_BINARY) is None,
'CSStidy binary %r not found' % settings.COMPRESS_CSSTIDY_BINARY)
class CssTidyTestCase(TestCase):
@@ -30,7 +40,6 @@ class CssTidyTestCase(TestCase):
color: black;
}
""")
from compressor.filters.csstidy import CSSTidyFilter
ret = CSSTidyFilter(content).input()
self.assertIsInstance(ret, six.text_type)
self.assertEqual(
@@ -39,10 +48,13 @@ class CssTidyTestCase(TestCase):
class PrecompilerTestCase(TestCase):
def setUp(self):
self.filename = os.path.join(test_dir, 'static/css/one.css')
self.test_precompiler = os.path.join(test_dir, 'precompiler.py')
self.setup_infile()
def setup_infile(self, filename='static/css/one.css'):
self.filename = os.path.join(test_dir, filename)
with io.open(self.filename, encoding=settings.FILE_CHARSET) as file:
self.content = file.read()
self.test_precompiler = os.path.join(test_dir, 'precompiler.py')
def test_precompiler_infile_outfile(self):
command = '%s %s -f {infile} -o {outfile}' % (sys.executable, self.test_precompiler)
@@ -51,6 +63,14 @@ class PrecompilerTestCase(TestCase):
charset=settings.FILE_CHARSET, command=command)
self.assertEqual("body { color:#990; }", compiler.input())
def test_precompiler_infile_with_spaces(self):
self.setup_infile('static/css/filename with spaces.css')
command = '%s %s -f {infile} -o {outfile}' % (sys.executable, self.test_precompiler)
compiler = CompilerFilter(
content=self.content, filename=self.filename,
charset=settings.FILE_CHARSET, command=command)
self.assertEqual("body { color:#424242; }", compiler.input())
def test_precompiler_infile_stdout(self):
command = '%s %s -f {infile}' % (sys.executable, self.test_precompiler)
compiler = CompilerFilter(
@@ -99,8 +119,8 @@ class CssMinTestCase(TestCase):
class CssAbsolutizingTestCase(TestCase):
hashing_method = 'mtime'
hashing_func = staticmethod(get_hashed_mtime)
content = ("p { background: url('../../img/python.png') }"
"p { filter: Alpha(src='../../img/python.png') }")
template = ("p { background: url('%(url)simg/python.png%(query)s%(hash)s%(frag)s') }"
"p { filter: Alpha(src='%(url)simg/python.png%(query)s%(hash)s%(frag)s') }")
def setUp(self):
self.old_enabled = settings.COMPRESS_ENABLED
@@ -120,40 +140,55 @@ class CssAbsolutizingTestCase(TestCase):
settings.COMPRESS_URL = self.old_url
settings.COMPRESS_CSS_HASHING_METHOD = self.old_hashing_method
def test_css_no_hash(self):
settings.COMPRESS_CSS_HASHING_METHOD = None
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
content = self.template % blankdict(url='../../')
params = blankdict({
'url': settings.COMPRESS_URL,
})
output = self.template % params
filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
settings.COMPRESS_URL = params['url'] = 'http://static.example.com/'
output = self.template % params
filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
def test_css_absolute_filter(self):
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
params = {
content = self.template % blankdict(url='../../')
params = blankdict({
'url': settings.COMPRESS_URL,
'hash': self.hashing_func(imagefilename),
}
output = ("p { background: url('%(url)simg/python.png?%(hash)s') }"
"p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params
filter = CssAbsoluteFilter(self.content)
'hash': '?' + self.hashing_func(imagefilename),
})
output = self.template % params
filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
settings.COMPRESS_URL = params['url'] = 'http://static.example.com/'
filter = CssAbsoluteFilter(self.content)
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
output = ("p { background: url('%(url)simg/python.png?%(hash)s') }"
"p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params
output = self.template % params
filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
def test_css_absolute_filter_url_fragment(self):
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
params = {
content = self.template % blankdict(url='../../', frag='#foo')
params = blankdict({
'url': settings.COMPRESS_URL,
'hash': self.hashing_func(imagefilename),
}
content = "p { background: url('../../img/python.png#foo') }"
output = "p { background: url('%(url)simg/python.png?%(hash)s#foo') }" % params
'hash': '?' + self.hashing_func(imagefilename),
'frag': '#foo',
})
output = self.template % params
filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
settings.COMPRESS_URL = params['url'] = 'http://media.example.com/'
output = self.template % params
filter = CssAbsoluteFilter(content)
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
output = "p { background: url('%(url)simg/python.png?%(hash)s#foo') }" % params
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
def test_css_absolute_filter_only_url_fragment(self):
@@ -161,64 +196,78 @@ class CssAbsolutizingTestCase(TestCase):
content = "p { background: url('#foo') }"
filter = CssAbsoluteFilter(content)
self.assertEqual(content, 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')
self.assertEqual(content, filter.input(filename=filename, basename='css/url/test.css'))
def test_css_absolute_filter_querystring(self):
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
params = {
content = self.template % blankdict(url='../../', query='?foo')
params = blankdict({
'url': settings.COMPRESS_URL,
'hash': self.hashing_func(imagefilename),
}
content = "p { background: url('../../img/python.png?foo') }"
output = "p { background: url('%(url)simg/python.png?foo&%(hash)s') }" % params
'query': '?foo',
'hash': '&' + self.hashing_func(imagefilename),
})
output = self.template % params
filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
settings.COMPRESS_URL = params['url'] = 'http://media.example.com/'
output = self.template % params
filter = CssAbsoluteFilter(content)
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
output = "p { background: url('%(url)simg/python.png?foo&%(hash)s') }" % params
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
def test_css_absolute_filter_https(self):
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
params = {
content = self.template % blankdict(url='../../')
params = blankdict({
'url': settings.COMPRESS_URL,
'hash': self.hashing_func(imagefilename),
}
output = ("p { background: url('%(url)simg/python.png?%(hash)s') }"
"p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params
filter = CssAbsoluteFilter(self.content)
'hash': '?' + self.hashing_func(imagefilename),
})
output = self.template % params
filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
settings.COMPRESS_URL = params['url'] = 'https://static.example.com/'
filter = CssAbsoluteFilter(self.content)
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
output = ("p { background: url('%(url)simg/python.png?%(hash)s') }"
"p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params
output = self.template % params
filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
def test_css_absolute_filter_relative_path(self):
filename = os.path.join(settings.TEST_DIR, 'whatever', '..', 'static', 'whatever/../css/url/test.css')
imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
params = {
content = self.template % blankdict(url='../../')
params = blankdict({
'url': settings.COMPRESS_URL,
'hash': self.hashing_func(imagefilename),
}
output = ("p { background: url('%(url)simg/python.png?%(hash)s') }"
"p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params
filter = CssAbsoluteFilter(self.content)
'hash': '?' + self.hashing_func(imagefilename),
})
output = self.template % params
filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
settings.COMPRESS_URL = params['url'] = 'https://static.example.com/'
filter = CssAbsoluteFilter(self.content)
output = ("p { background: url('%(url)simg/python.png?%(hash)s') }"
"p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params
output = self.template % params
filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
def test_css_absolute_filter_filename_outside_compress_root(self):
filename = '/foo/bar/baz/test.css'
content = self.template % blankdict(url='../qux/')
params = blankdict({
'url': settings.COMPRESS_URL + 'bar/qux/',
})
output = self.template % params
filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename, basename='bar/baz/test.css'))
settings.COMPRESS_URL = 'https://static.example.com/'
params['url'] = settings.COMPRESS_URL + 'bar/qux/'
output = self.template % params
filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename, basename='bar/baz/test.css'))
def test_css_hunks(self):
hash_dict = {
'hash1': self.hashing_func(os.path.join(settings.COMPRESS_ROOT, 'img/python.png')),
@@ -253,14 +302,6 @@ class CssAbsolutizingTestCaseWithHash(CssAbsolutizingTestCase):
hashing_method = 'content'
hashing_func = staticmethod(get_hashed_content)
def setUp(self):
super(CssAbsolutizingTestCaseWithHash, self).setUp()
self.css = """
<link rel="stylesheet" href="/static/css/url/url1.css" type="text/css" charset="utf-8">
<link rel="stylesheet" href="/static/css/url/2/url2.css" type="text/css" charset="utf-8">
"""
self.css_node = CssCompressor(self.css)
class CssDataUriTestCase(TestCase):
def setUp(self):
@@ -301,3 +342,38 @@ class TemplateTestCase(TestCase):
#footer {font-weight: bold;}
"""
self.assertEqual(input, TemplateFilter(content).input())
class SpecializedFiltersTest(TestCase):
"""
Test to check the Specializations of filters.
"""
def test_closure_filter(self):
filter = ClosureCompilerFilter('')
self.assertEqual(filter.options, (('binary', six.text_type('java -jar compiler.jar')), ('args', six.text_type(''))))
def test_csstidy_filter(self):
filter = CSSTidyFilter('')
self.assertEqual(filter.options, (('binary', six.text_type('csstidy')), ('args', six.text_type('--template=highest'))))
def test_yuglify_filters(self):
filter = YUglifyCSSFilter('')
self.assertEqual(filter.command, '{binary} {args} --type=css')
self.assertEqual(filter.options, (('binary', six.text_type('yuglify')), ('args', six.text_type('--terminal'))))
filter = YUglifyJSFilter('')
self.assertEqual(filter.command, '{binary} {args} --type=js')
self.assertEqual(filter.options, (('binary', six.text_type('yuglify')), ('args', six.text_type('--terminal'))))
def test_yui_filters(self):
filter = YUICSSFilter('')
self.assertEqual(filter.command, '{binary} {args} --type=css')
self.assertEqual(filter.options, (('binary', six.text_type('java -jar yuicompressor.jar')), ('args', six.text_type(''))))
filter = YUIJSFilter('', verbose=1)
self.assertEqual(filter.command, '{binary} {args} --type=js --verbose')
self.assertEqual(filter.options, (('binary', six.text_type('java -jar yuicompressor.jar')), ('args', six.text_type('')), ('verbose', 1)))
def test_clean_css_filter(self):
filter = CleanCSSFilter('')
self.assertEqual(filter.options, (('binary', six.text_type('cleancss')), ('args', six.text_type(''))))

View File

@@ -65,8 +65,7 @@ class TestJinja2CompressorExtension(TestCase):
self.assertEqual(tag_body, template.render())
def test_empty_tag(self):
template = self.env.from_string("""{% compress js %}{% block js %}
{% endblock %}{% endcompress %}""")
template = self.env.from_string("""{% compress js %}{% block js %}{% endblock %}{% endcompress %}""")
context = {'STATIC_URL': settings.COMPRESS_URL}
self.assertEqual('', template.render(context))

View File

@@ -3,6 +3,7 @@ import io
import os
import sys
import django
from django.core.management.base import CommandError
from django.template import Template, Context
from django.test import TestCase
@@ -44,10 +45,6 @@ class OfflineTestCaseMixin(object):
engines = ("django",)
def setUp(self):
self._old_compress = settings.COMPRESS_ENABLED
self._old_compress_offline = settings.COMPRESS_OFFLINE
self._old_template_dirs = settings.TEMPLATE_DIRS
self._old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
self.log = StringIO()
# Reset template dirs, because it enables us to force compress to
@@ -58,11 +55,18 @@ class OfflineTestCaseMixin(object):
# template to be skipped over.
django_template_dir = os.path.join(settings.TEST_DIR, 'test_templates', self.templates_dir)
jinja2_template_dir = os.path.join(settings.TEST_DIR, 'test_templates_jinja2', self.templates_dir)
settings.TEMPLATE_DIRS = (django_template_dir, jinja2_template_dir)
# Enable offline compress
settings.COMPRESS_ENABLED = True
settings.COMPRESS_OFFLINE = True
override_settings = {
'TEMPLATE_DIRS': (django_template_dir, jinja2_template_dir,),
'COMPRESS_ENABLED': True,
'COMPRESS_OFFLINE': True
}
if "jinja2" in self.engines:
override_settings["COMPRESS_JINJA2_GET_ENVIRONMENT"] = lambda: self._get_jinja2_env()
self.override_settings = self.settings(**override_settings)
self.override_settings.__enter__()
if "django" in self.engines:
self.template_path = os.path.join(django_template_dir, self.template_name)
@@ -70,22 +74,16 @@ class OfflineTestCaseMixin(object):
with io.open(self.template_path, encoding=settings.FILE_CHARSET) as file:
self.template = Template(file.read())
self._old_jinja2_get_environment = settings.COMPRESS_JINJA2_GET_ENVIRONMENT
if "jinja2" in self.engines:
# Setup Jinja2 settings.
settings.COMPRESS_JINJA2_GET_ENVIRONMENT = lambda: self._get_jinja2_env()
jinja2_env = settings.COMPRESS_JINJA2_GET_ENVIRONMENT()
jinja2_env = override_settings["COMPRESS_JINJA2_GET_ENVIRONMENT"]()
self.template_path_jinja2 = os.path.join(jinja2_template_dir, self.template_name)
with io.open(self.template_path_jinja2, encoding=settings.FILE_CHARSET) as file:
self.template_jinja2 = jinja2_env.from_string(file.read())
def tearDown(self):
settings.COMPRESS_JINJA2_GET_ENVIRONMENT = self._old_jinja2_get_environment
settings.COMPRESS_ENABLED = self._old_compress
settings.COMPRESS_OFFLINE = self._old_compress_offline
settings.TEMPLATE_DIRS = self._old_template_dirs
self.override_settings.__exit__(None, None, None)
manifest_path = os.path.join('CACHE', 'manifest.json')
if default_storage.exists(manifest_path):
default_storage.delete(manifest_path)
@@ -454,6 +452,8 @@ class OfflineGenerationComplexTestCase(OfflineTestCaseMixin, TestCase):
# It seems there is no evidence nor indicated support for Python 3+.
@unittest.skipIf(sys.version_info >= (3, 2),
"Coffin does not support 3.2+")
@unittest.skipIf(django.VERSION >= (1, 8),
"Import error on 1.8")
class OfflineGenerationCoffinTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = "test_coffin"
expected_hash = "32c8281e3346"
@@ -478,6 +478,8 @@ class OfflineGenerationCoffinTestCase(OfflineTestCaseMixin, TestCase):
# is also evident in its tox.ini file.
@unittest.skipIf(sys.version_info >= (3, 2) and sys.version_info < (3, 3),
"Jingo does not support 3.2")
@unittest.skipIf(django.VERSION >= (1, 8),
"Import error on 1.8")
class OfflineGenerationJingoTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = "test_jingo"
expected_hash = "61ec584468eb"

View File

@@ -5,4 +5,9 @@
<script type="text/javascript">
alert("this alert shouldn't be alone!");
</script>
{% block orphan %}
{{ block.super }}
An 'orphan' block that refers to a non-existent super block.
Contents of this block are ignored.
{% endblock %}
{% endspaceless %}{% endblock %}

View File

@@ -4,20 +4,10 @@ from django.core.exceptions import ImproperlyConfigured
from compressor.conf import settings
INSTALLED = ("staticfiles" in settings.INSTALLED_APPS or
"django.contrib.staticfiles" in settings.INSTALLED_APPS)
if "django.contrib.staticfiles" in settings.INSTALLED_APPS:
from django.contrib.staticfiles import finders # noqa
if INSTALLED:
if "django.contrib.staticfiles" in settings.INSTALLED_APPS:
from django.contrib.staticfiles import finders
else:
try:
from staticfiles import finders # noqa
except ImportError:
# Old (pre 1.0) and incompatible version of staticfiles
INSTALLED = False
if (INSTALLED and "compressor.finders.CompressorFinder"
if ("compressor.finders.CompressorFinder"
not in settings.STATICFILES_FINDERS):
raise ImproperlyConfigured(
"When using Django Compressor together with staticfiles, "

View File

@@ -1,12 +1,46 @@
Changelog
=========
v1.4
----
v1.5 (03/27/2015)
-----------------
`Full Changelog <https://github.com/django-compressor/django-compressor/compare/1.4...HEAD>`_
- Fix compress command and run automated tests for Django 1.8
- Fix Django 1.8 warnings
- Handle TypeError from import_module
- Fix reading UTF-8 files which have BOM
- Fix incompatibility with Windows (shell_quote is not supported)
- Run automated tests on Django 1.7
- Ignore non-existent {{ block.super }} in offline compression instead of raising AttributeError
- Support for clean-css
- Fix link markup
- Add support for COMPRESS_CSS_HASHING_METHOD = None
- Remove compatibility with old 'staticfiles' app
- In compress command, use get_template() instead of opening template files manually, fixing compatibility issues with custom template loaders
- Fix FilterBase so that does not override self.type for subclasses if filter_type is not specified at init
- Remove unnecessary filename and existence checks in CssAbsoluteFilter
v1.4 (06/20/2014)
-----------------
- Added Python 3 compatibility.
- Added compatibility with Django 1.6.x.
- Added compatibility with Django 1.6.x and dropped support for Django 1.3.X.
- Fixed compatibility with html5lib 1.0.
@@ -46,7 +80,7 @@ v1.3 (03/18/2013)
- Dropped support for Python 2.5. Removed ``any`` and ``walk`` compatibility
functions in ``compressor.utils``.
- Removed compatibility with Django 1.2 for default values of some settings:
- Removed compatibility with some old django setttings:
- :attr:`~COMPRESS_ROOT` no longer uses ``MEDIA_ROOT`` if ``STATIC_ROOT`` is
not defined. It expects ``STATIC_ROOT`` to be defined instead.

View File

@@ -9,11 +9,12 @@ tidy, everybody has to follow a few rules (nothing major, I promise :) )
Community
---------
People interested in developing for the Django Compressor should head
over to #django-compressor on the `freenode`_ IRC network for help and to
discuss the development.
People interested in developing for the Django Compressor should:
1. Head over to #django-compressor on the `freenode`_ IRC network for help and to
discuss the development.
2. Open an issue on GitHub explaining your ideas.
You may also be interested in following `@jezdez`_ on Twitter.
In a nutshell
-------------
@@ -143,7 +144,7 @@ Documentation should be:
- Accessible. You should assume the reader to be moderately familiar with
Python and Django, but not anything else. Link to documentation of libraries
you use, for example, even if they are "obvious" to you. A brief
description of what it does is also welcome.
description of what it does is also welcome.
Pulling of documentation is pretty fast and painless. Usually somebody goes
over your text and merges it, since there are no "breaks" and that github

View File

@@ -3,12 +3,12 @@
django-sekizai Support
======================
Django Compressor comes with support for _django-sekizai via an extension.
_django-sekizai provides the ability to include template code, from within
Django Compressor comes with support for django-sekizai_ via an extension.
django-sekizai provides the ability to include template code, from within
any block, to a parent block. It is primarily used to include js/css from
included templates to the master template.
It requires _django-sekizai to installed. Refer to the _django-sekizai _docs
It requires django-sekizai to be installed. Refer to the `django-sekizai docs`_
for how to use ``render_block``
Usage
@@ -21,4 +21,4 @@ Usage
.. _django-sekizai: https://github.com/ojii/django-sekizai
.. _docs: http://django-sekizai.readthedocs.org/en/latest/
.. _django-sekizai docs: http://django-sekizai.readthedocs.org/en/latest/

View File

@@ -42,13 +42,13 @@ Jinja2 Offline Compression Support
==================================
You'd need to configure ``COMPRESS_JINJA2_GET_ENVIRONMENT`` so that
Compressor can retrieve the Jinja2 environment for rendering.
This can be a lamda or function that returns a Jinja2 environment.
This can be a lambda or function that returns a Jinja2 environment.
Usage
-----
Run the following compress command along with an ``-engine`` parameter. The
Run the following compress command along with an ``--engine`` parameter. The
parameter can be either jinja2 or django (default). For example,
"./manage.py compress -engine jinja2".
``./manage.py compress --engine jinja2``.
Using both Django and Jinja2 templates
--------------------------------------
@@ -60,9 +60,9 @@ template safely. (Vice versa for Django parser).
A typical usage could be :
- "./manage.py compress" for processing Django templates first, skipping
- ``./manage.py compress`` for processing Django templates first, skipping
Jinja2 templates.
- "./manage.py compress -engine jinja2" for processing Jinja2 templates,
- ``./manage.py compress --engine jinja2`` for processing Jinja2 templates,
skipping Django templates.
However, it is still recommended that you do not mix Django and Jinja2
@@ -172,4 +172,3 @@ Jinja2 alone (with custom loader) are tested and work on Python 2.6, 2.7 and
.. _Jinja2: http://jinja.pocoo.org/docs/
.. _Coffin: http://pypi.python.org/pypi/Coffin
.. _Jingo: https://jingo.readthedocs.org/en/latest/

View File

@@ -18,10 +18,8 @@ Installation
* See the list of :ref:`settings` to modify Django Compressor's
default behaviour and make adjustments for your website.
* In case you use Django's staticfiles_ contrib app (or its standalone
counterpart django-staticfiles_) you have to add Django Compressor's file
finder to the ``STATICFILES_FINDERS`` setting, for example with
``django.contrib.staticfiles``:
* In case you use Django's staticfiles_ contrib app you have to add Django
Compressor's file finder to the ``STATICFILES_FINDERS`` setting, like this:
.. code-block:: python
@@ -95,6 +93,6 @@ Optional
.. _lxml: http://codespeak.net/lxml/
.. _libxml2: http://xmlsoft.org/
.. _html5lib: http://code.google.com/p/html5lib/
.. _`Slim It`: http://slimit.org/
.. _`Slim It`: https://github.com/rspivak/slimit
.. _django-appconf: http://pypi.python.org/pypi/django-appconf/
.. _versiontools: http://pypi.python.org/pypi/versiontools/

View File

@@ -39,12 +39,11 @@ The storage backend to save the compressed files needs to be changed, too::
Using staticfiles
^^^^^^^^^^^^^^^^^
If you are using Django's staticfiles_ contrib app or the standalone
app django-staticfiles_, you'll need to use a temporary filesystem cache
for Django Compressor to know which files to compress. Since staticfiles
provides a management command to collect static files from various
locations which uses a storage backend, this is where both apps can be
integrated.
If you are using Django's staticfiles_ contrib app, you'll need to use a
temporary filesystem cache for Django Compressor to know which files to
compress. Since staticfiles provides a management command to collect static
files from various locations which uses a storage backend, this is where both
apps can be integrated.
#. Make sure the :attr:`~django.conf.settings.COMPRESS_ROOT` and STATIC_ROOT_
settings are equal since both apps need to look at the same directories
@@ -84,7 +83,6 @@ integrated.
.. _Amazon S3: https://s3.amazonaws.com/
.. _boto: http://boto.cloudhackers.com/
.. _django-storages: http://code.welldev.org/django-storages/
.. _django-staticfiles: http://github.com/jezdez/django-staticfiles/
.. _staticfiles: http://docs.djangoproject.com/en/dev/howto/static-files/
.. _STATIC_ROOT: http://docs.djangoproject.com/en/dev/ref/settings/#static-root
.. _STATIC_URL: http://docs.djangoproject.com/en/dev/ref/settings/#static-url

View File

@@ -81,10 +81,11 @@ Backend settings
.. attribute:: COMPRESS_CSS_HASHING_METHOD
The method to use when calculating the hash to append to
processed URLs. Either ``'mtime'`` (default) or ``'content'``.
Use the latter in case you're using multiple server to serve your
static files.
The method to use when calculating the suffix to append to URLs in
your processed CSS files. Either ``None``, ``'mtime'`` (default) or
``'content'``. Use the ``None`` if you want to completely disable that
feature, and the ``'content'`` in case you're using multiple servers
to serve your content.
- ``compressor.filters.csstidy.CSSTidyFilter``
@@ -136,9 +137,24 @@ Backend settings
A filter that uses Zachary Voase's Python port of the YUI CSS compression
algorithm cssmin_.
- ``compressor.filters.cleancss.CleanCSSFilter``
A filter that passes the CSS content to the `clean-css`_ tool.
.. attribute:: CLEAN_CSS_BINARY
The clean-css binary filesystem path.
.. attribute:: CLEAN_CSS_ARGUMENTS
The arguments passed to clean-css.
.. _CSSTidy: http://csstidy.sourceforge.net/
.. _`data: URIs`: http://en.wikipedia.org/wiki/Data_URI_scheme
.. _cssmin: http://pypi.python.org/pypi/cssmin/
.. _`clean-css`: https://github.com/GoalSmashers/clean-css/
- ``compressor.filters.template.TemplateFilter``
@@ -220,7 +236,7 @@ Backend settings
.. _`Google Closure compiler`: http://code.google.com/closure/compiler/
.. _`YUI compressor`: http://developer.yahoo.com/yui/compressor/
.. _`yUglify compressor`: https://github.com/yui/yuglify
.. _`Slim It`: http://slimit.org/
.. _`Slim It`: https://github.com/rspivak/slimit
.. attribute:: COMPRESS_PRECOMPILERS
@@ -305,7 +321,7 @@ Backend settings
<link rel="stylesheet" href="/static/CACHE/css/8ccf8d877f18.css" type="text/css" charset="utf-8">
.. _less: http://lesscss.org/
.. _CoffeeScript: http://jashkenas.github.com/coffee-script/
.. _CoffeeScript: http://coffeescript.org/
.. attribute:: COMPRESS_STORAGE

View File

@@ -48,7 +48,7 @@ Which would be rendered something like:
.. note::
Remember that django-compressor will try to :ref:`group ouputs by media <css_notes>`.
Remember that django-compressor will try to :ref:`group outputs by media <css_notes>`.
Linked files **must** be accessible via
:attr:`~django.conf.settings.COMPRESS_URL`.

97
tox.ini
View File

@@ -33,89 +33,34 @@ three_two =
[tox]
envlist =
py33-1.6.X,
py32-1.6.X,
py27-1.6.X,
py26-1.6.X,
py33-1.5.X,
py32-1.5.X,
py27-1.5.X,
py26-1.5.X,
py27-1.4.X,
py26-1.4.X
{py26,py27}-{1.4.X,1.5.X},
{py26,py27,py32,py33}-{1.6.X},
{py27,py32,py33,py34}-{1.7.X},
{py27,py32,py33,py34}-{1.8.X}
[testenv]
basepython =
py26: python2.6
py27: python2.7
py32: python3.2
py33: python3.3
py34: python3.4
usedevelop = true
setenv =
CPPFLAGS=-O0
usedevelop = true
whitelist_externals = /usr/bin/make
downloadcache = {toxworkdir}/_download/
commands =
django-admin.py --version
make test
[testenv:py33-1.6.X]
basepython = python3.3
deps =
Django>=1.6,<1.7
{[deps]three}
[testenv:py32-1.6.X]
basepython = python3.2
deps =
Django>=1.6,<1.7
{[deps]three_two}
[testenv:py27-1.6.X]
basepython = python2.7
deps =
Django>=1.6,<1.7
{[deps]two}
[testenv:py26-1.6.X]
basepython = python2.6
deps =
Django>=1.6,<1.7
{[deps]two}
[testenv:py33-1.5.X]
basepython = python3.3
deps =
Django>=1.5,<1.6
1.4.X: Django>=1.4,<1.5
1.5.X: Django>=1.5,<1.6
1.6.X: Django>=1.6,<1.7
1.7.X: Django>=1.7,<1.8
1.8.X: Django>=1.8,<1.9
py26: {[deps]two}
py27: {[deps]two}
py32: {[deps]three_two}
py33: {[deps]three}
py34: {[deps]three}
django-discover-runner
{[deps]three}
[testenv:py32-1.5.X]
basepython = python3.2
deps =
Django>=1.5,<1.6
django-discover-runner
{[deps]three_two}
[testenv:py27-1.5.X]
basepython = python2.7
deps =
Django>=1.5,<1.6
django-discover-runner
{[deps]two}
[testenv:py26-1.5.X]
basepython = python2.6
deps =
Django>=1.5,<1.6
django-discover-runner
{[deps]two}
[testenv:py27-1.4.X]
basepython = python2.7
deps =
Django>=1.4,<1.5
django-discover-runner
{[deps]two}
[testenv:py26-1.4.X]
basepython = python2.6
deps =
Django>=1.4,<1.5
django-discover-runner
{[deps]two}