diff --git a/AUTHORS b/AUTHORS
index 7b9a571..39e21d6 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -70,6 +70,7 @@ Matthew Tretter
Mehmet S. Catalbas
Michael van de Waeter
Mike Yumatov
+Nick Pope
Nicolas Charlot
Niran Babalola
Paul McMillan
diff --git a/compressor/management/commands/compress.py b/compressor/management/commands/compress.py
index ac555e9..f2061d1 100644
--- a/compressor/management/commands/compress.py
+++ b/compressor/management/commands/compress.py
@@ -22,6 +22,7 @@ from compressor.conf import settings
from compressor.exceptions import (OfflineGenerationError, TemplateSyntaxError,
TemplateDoesNotExist)
from compressor.templatetags.compress import CompressorNode
+from compressor.utils import get_mod_func
if six.PY3:
# there is an 'io' module in python 2.6+, but io.StringIO does not
@@ -211,44 +212,58 @@ class Command(NoArgsCommand):
"\n\t".join((t.template_name
for t in compressor_nodes.keys())) + "\n")
+ contexts = settings.COMPRESS_OFFLINE_CONTEXT
+ if isinstance(contexts, six.string_types):
+ try:
+ module, function = get_mod_func(contexts)
+ contexts = getattr(import_module(module), function)()
+ except (AttributeError, ImportError, TypeError) as e:
+ raise ImportError("Couldn't import offline context function %s: %s" %
+ (settings.COMPRESS_OFFLINE_CONTEXT, e))
+ elif not isinstance(contexts, (list, tuple)):
+ contexts = [contexts]
+
log.write("Compressing... ")
- count = 0
+ block_count = context_count = 0
results = []
offline_manifest = SortedDict()
- init_context = parser.get_init_context(settings.COMPRESS_OFFLINE_CONTEXT)
- for template, nodes in compressor_nodes.items():
- context = Context(init_context)
- template._log = log
- template._log_verbosity = verbosity
+ for context_dict in contexts:
+ context_count += 1
+ init_context = parser.get_init_context(context_dict)
- if not parser.process_template(template, context):
- continue
+ for template, nodes in compressor_nodes.items():
+ context = Context(init_context)
+ template._log = log
+ template._log_verbosity = verbosity
- for node in nodes:
- context.push()
- parser.process_node(template, context, node)
- rendered = parser.render_nodelist(template, context, node)
- key = get_offline_hexdigest(rendered)
-
- if key in offline_manifest:
+ if not parser.process_template(template, context):
continue
- try:
- result = parser.render_node(template, context, node)
- except Exception as e:
- raise CommandError("An error occurred during rendering %s: "
- "%s" % (template.template_name, e))
- offline_manifest[key] = result
- context.pop()
- results.append(result)
- count += 1
+ for node in nodes:
+ context.push()
+ parser.process_node(template, context, node)
+ rendered = parser.render_nodelist(template, context, node)
+ key = get_offline_hexdigest(rendered)
+
+ if key in offline_manifest:
+ continue
+
+ try:
+ result = parser.render_node(template, context, node)
+ except Exception as e:
+ raise CommandError("An error occurred during rendering %s: "
+ "%s" % (template.template_name, e))
+ offline_manifest[key] = result
+ context.pop()
+ results.append(result)
+ block_count += 1
write_offline_manifest(offline_manifest)
- log.write("done\nCompressed %d block(s) from %d template(s).\n" %
- (count, len(compressor_nodes)))
- return count, results
+ log.write("done\nCompressed %d block(s) from %d template(s) for %d context(s).\n" %
+ (block_count, len(compressor_nodes), context_count))
+ return block_count, results
def handle_extensions(self, extensions=('html',)):
"""
diff --git a/compressor/tests/test_offline.py b/compressor/tests/test_offline.py
index 71f91c0..09bc3bd 100644
--- a/compressor/tests/test_offline.py
+++ b/compressor/tests/test_offline.py
@@ -8,12 +8,14 @@ from django.core.management.base import CommandError
from django.template import Template, Context
from django.test import TestCase
from django.utils import six, unittest
+from django.utils.importlib import import_module
from compressor.cache import flush_offline_manifest, get_offline_manifest
from compressor.conf import settings
from compressor.exceptions import OfflineGenerationError
from compressor.management.commands.compress import Command as CompressCommand
from compressor.storage import default_storage
+from compressor.utils import get_mod_func
if six.PY3:
# there is an 'io' module in python 2.6+, but io.StringIO does not
@@ -32,6 +34,11 @@ else:
_TEST_JINJA2 = not(sys.version_info[0] == 3 and sys.version_info[1] == 2)
+def offline_context_generator():
+ for i in range(1, 4):
+ yield {'content': 'OK %d!' % i}
+
+
class OfflineTestCaseMixin(object):
template_name = "test_compressor_offline.html"
verbosity = 0
@@ -88,22 +95,30 @@ class OfflineTestCaseMixin(object):
if default_storage.exists(manifest_path):
default_storage.delete(manifest_path)
+ def _prepare_contexts(self, engine):
+ if engine == 'django':
+ return [Context(settings.COMPRESS_OFFLINE_CONTEXT)]
+ if engine == 'jinja2':
+ return [settings.COMPRESS_OFFLINE_CONTEXT]
+ return None
+
def _render_template(self, engine):
- if engine == "django":
- return self.template.render(Context(settings.COMPRESS_OFFLINE_CONTEXT))
- elif engine == "jinja2":
- return self.template_jinja2.render(settings.COMPRESS_OFFLINE_CONTEXT) + "\n"
- else:
- return None
+ contexts = self._prepare_contexts(engine)
+ if engine == 'django':
+ return ''.join(self.template.render(c) for c in contexts)
+ if engine == 'jinja2':
+ return '\n'.join(self.template_jinja2.render(c) for c in contexts) + "\n"
+ return None
def _test_offline(self, engine):
+ hashes = self.expected_hash
+ if not isinstance(hashes, (list, tuple)):
+ hashes = [hashes]
count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
- self.assertEqual(1, count)
- self.assertEqual([
- '' % (self.expected_hash, ),
- ], result)
+ self.assertEqual(len(hashes), count)
+ self.assertEqual(['' % h for h in hashes], result)
rendered_template = self._render_template(engine)
- self.assertEqual(rendered_template, "".join(result) + "\n")
+ self.assertEqual(rendered_template, '\n'.join(result) + '\n')
def test_offline(self):
for engine in self.engines:
@@ -247,6 +262,84 @@ class OfflineGenerationTestCaseWithContext(OfflineTestCaseMixin, TestCase):
super(OfflineGenerationTestCaseWithContext, self).tearDown()
+class OfflineGenerationTestCaseWithContextList(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_with_context'
+ expected_hash = ['f8bcaea049b3', 'db12749b1e80', 'e9f4a0054a06']
+
+ def setUp(self):
+ self.old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
+ settings.COMPRESS_OFFLINE_CONTEXT = [{'content': 'OK %d!' % i} for i in range(1, 4)]
+ super(OfflineGenerationTestCaseWithContextList, self).setUp()
+
+ def tearDown(self):
+ settings.COMPRESS_OFFLINE_CONTEXT = self.old_offline_context
+ super(OfflineGenerationTestCaseWithContextList, self).tearDown()
+
+ def _prepare_contexts(self, engine):
+ if engine == 'django':
+ return [Context(c) for c in settings.COMPRESS_OFFLINE_CONTEXT]
+ if engine == 'jinja2':
+ return settings.COMPRESS_OFFLINE_CONTEXT
+ return None
+
+
+class OfflineGenerationTestCaseWithContextGenerator(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_with_context'
+ expected_hash = ['f8bcaea049b3', 'db12749b1e80', 'e9f4a0054a06']
+
+ def setUp(self):
+ self.old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
+ settings.COMPRESS_OFFLINE_CONTEXT = 'compressor.tests.test_offline.offline_context_generator'
+ super(OfflineGenerationTestCaseWithContextGenerator, self).setUp()
+
+ def tearDown(self):
+ settings.COMPRESS_OFFLINE_CONTEXT = self.old_offline_context
+ super(OfflineGenerationTestCaseWithContextGenerator, self).tearDown()
+
+ def _prepare_contexts(self, engine):
+ module, function = get_mod_func(settings.COMPRESS_OFFLINE_CONTEXT)
+ contexts = getattr(import_module(module), function)()
+ if engine == 'django':
+ return (Context(c) for c in contexts)
+ if engine == 'jinja2':
+ return contexts
+ return None
+
+
+class OfflineGenerationTestCaseWithContextGeneratorImportError(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_with_context'
+
+ def _test_offline(self, engine):
+ old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
+
+ try:
+ # Path with invalid module name -- ImportError:
+ settings.COMPRESS_OFFLINE_CONTEXT = 'invalid_module.invalid_function'
+ self.assertRaises(ImportError, CompressCommand().compress, engine=engine)
+
+ # Module name only without function -- AttributeError:
+ settings.COMPRESS_OFFLINE_CONTEXT = 'compressor'
+ self.assertRaises(ImportError, CompressCommand().compress, engine=engine)
+
+ # Path with invalid function name -- AttributeError:
+ settings.COMPRESS_OFFLINE_CONTEXT = 'compressor.tests.invalid_function'
+ self.assertRaises(ImportError, CompressCommand().compress, engine=engine)
+
+ # Path without function attempts call on module -- TypeError:
+ settings.COMPRESS_OFFLINE_CONTEXT = 'compressor.tests.test_offline'
+ self.assertRaises(ImportError, CompressCommand().compress, engine=engine)
+
+ # Valid path to generator function -- no ImportError:
+ settings.COMPRESS_OFFLINE_CONTEXT = 'compressor.tests.test_offline.offline_context_generator'
+ try:
+ CompressCommand().compress(engine=engine)
+ except ImportError:
+ self.fail("Valid path to offline context generator mustn't raise ImportError.")
+
+ finally:
+ settings.COMPRESS_OFFLINE_CONTEXT = old_offline_context
+
+
class OfflineGenerationTestCaseErrors(OfflineTestCaseMixin, TestCase):
templates_dir = "test_error_handling"
diff --git a/docs/settings.txt b/docs/settings.txt
index 4acf418..e0cb6f2 100644
--- a/docs/settings.txt
+++ b/docs/settings.txt
@@ -492,6 +492,37 @@ Offline settings
If available, the ``STATIC_URL`` setting is also added to the context.
+ .. note::
+
+ It is also possible to perform offline compression for multiple
+ contexts by providing a list or tuple of dictionaries, or by providing
+ a dotted string pointing to a generator function.
+
+ This makes it easier to generate contexts dynamically for situations
+ where a user might be able to select a different theme in their user
+ profile, or be served different stylesheets based on other criteria.
+
+ An example of multiple offline contexts by providing a list or tuple::
+
+ # project/settings.py:
+ COMPRESS_OFFLINE_CONTEXT = [
+ {'THEME': 'plain', 'STATIC_URL': STATIC_URL},
+ {'THEME': 'fancy', 'STATIC_URL': STATIC_URL},
+ # ...
+ ]
+
+ An example of multiple offline contexts generated dynamically::
+
+ # project/settings.py:
+ COMPRESS_OFFLINE_CONTEXT = 'project.module.offline_context'
+
+ # project/module.py:
+ from django.conf import settings
+ def offline_context():
+ from project.models import Company
+ for theme in set(Company.objects.values_list('theme', flat=True)):
+ yield {'THEME': theme, 'STATIC_URL': settings.STATIC_URL}
+
.. attribute:: COMPRESS_OFFLINE_MANIFEST
:Default: ``manifest.json``