Add support for multiple offline contexts.
Offline compression now supports generation 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 multiple contexts dynamically for situations where a user might be able to select or be served a different theme for a website, etc.
This commit is contained in:
parent
e6ade966d5
commit
693464a1c2
1
AUTHORS
1
AUTHORS
|
@ -70,6 +70,7 @@ Matthew Tretter
|
|||
Mehmet S. Catalbas
|
||||
Michael van de Waeter
|
||||
Mike Yumatov
|
||||
Nick Pope
|
||||
Nicolas Charlot
|
||||
Niran Babalola
|
||||
Paul McMillan
|
||||
|
|
|
@ -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',)):
|
||||
"""
|
||||
|
|
|
@ -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([
|
||||
'<script type="text/javascript" src="/static/CACHE/js/%s.js"></script>' % (self.expected_hash, ),
|
||||
], result)
|
||||
self.assertEqual(len(hashes), count)
|
||||
self.assertEqual(['<script type="text/javascript" src="/static/CACHE/js/%s.js"></script>' % 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"
|
||||
|
||||
|
|
|
@ -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``
|
||||
|
|
Loading…
Reference in New Issue