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
|
Mehmet S. Catalbas
|
||||||
Michael van de Waeter
|
Michael van de Waeter
|
||||||
Mike Yumatov
|
Mike Yumatov
|
||||||
|
Nick Pope
|
||||||
Nicolas Charlot
|
Nicolas Charlot
|
||||||
Niran Babalola
|
Niran Babalola
|
||||||
Paul McMillan
|
Paul McMillan
|
||||||
|
|
|
@ -22,6 +22,7 @@ from compressor.conf import settings
|
||||||
from compressor.exceptions import (OfflineGenerationError, TemplateSyntaxError,
|
from compressor.exceptions import (OfflineGenerationError, TemplateSyntaxError,
|
||||||
TemplateDoesNotExist)
|
TemplateDoesNotExist)
|
||||||
from compressor.templatetags.compress import CompressorNode
|
from compressor.templatetags.compress import CompressorNode
|
||||||
|
from compressor.utils import get_mod_func
|
||||||
|
|
||||||
if six.PY3:
|
if six.PY3:
|
||||||
# there is an 'io' module in python 2.6+, but io.StringIO does not
|
# there is an 'io' module in python 2.6+, but io.StringIO does not
|
||||||
|
@ -211,11 +212,25 @@ class Command(NoArgsCommand):
|
||||||
"\n\t".join((t.template_name
|
"\n\t".join((t.template_name
|
||||||
for t in compressor_nodes.keys())) + "\n")
|
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... ")
|
log.write("Compressing... ")
|
||||||
count = 0
|
block_count = context_count = 0
|
||||||
results = []
|
results = []
|
||||||
offline_manifest = SortedDict()
|
offline_manifest = SortedDict()
|
||||||
init_context = parser.get_init_context(settings.COMPRESS_OFFLINE_CONTEXT)
|
|
||||||
|
for context_dict in contexts:
|
||||||
|
context_count += 1
|
||||||
|
init_context = parser.get_init_context(context_dict)
|
||||||
|
|
||||||
for template, nodes in compressor_nodes.items():
|
for template, nodes in compressor_nodes.items():
|
||||||
context = Context(init_context)
|
context = Context(init_context)
|
||||||
|
@ -242,13 +257,13 @@ class Command(NoArgsCommand):
|
||||||
offline_manifest[key] = result
|
offline_manifest[key] = result
|
||||||
context.pop()
|
context.pop()
|
||||||
results.append(result)
|
results.append(result)
|
||||||
count += 1
|
block_count += 1
|
||||||
|
|
||||||
write_offline_manifest(offline_manifest)
|
write_offline_manifest(offline_manifest)
|
||||||
|
|
||||||
log.write("done\nCompressed %d block(s) from %d template(s).\n" %
|
log.write("done\nCompressed %d block(s) from %d template(s) for %d context(s).\n" %
|
||||||
(count, len(compressor_nodes)))
|
(block_count, len(compressor_nodes), context_count))
|
||||||
return count, results
|
return block_count, results
|
||||||
|
|
||||||
def handle_extensions(self, extensions=('html',)):
|
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.template import Template, Context
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import six, unittest
|
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.cache import flush_offline_manifest, get_offline_manifest
|
||||||
from compressor.conf import settings
|
from compressor.conf import settings
|
||||||
from compressor.exceptions import OfflineGenerationError
|
from compressor.exceptions import OfflineGenerationError
|
||||||
from compressor.management.commands.compress import Command as CompressCommand
|
from compressor.management.commands.compress import Command as CompressCommand
|
||||||
from compressor.storage import default_storage
|
from compressor.storage import default_storage
|
||||||
|
from compressor.utils import get_mod_func
|
||||||
|
|
||||||
if six.PY3:
|
if six.PY3:
|
||||||
# there is an 'io' module in python 2.6+, but io.StringIO does not
|
# 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)
|
_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):
|
class OfflineTestCaseMixin(object):
|
||||||
template_name = "test_compressor_offline.html"
|
template_name = "test_compressor_offline.html"
|
||||||
verbosity = 0
|
verbosity = 0
|
||||||
|
@ -88,22 +95,30 @@ class OfflineTestCaseMixin(object):
|
||||||
if default_storage.exists(manifest_path):
|
if default_storage.exists(manifest_path):
|
||||||
default_storage.delete(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):
|
def _render_template(self, engine):
|
||||||
if engine == "django":
|
contexts = self._prepare_contexts(engine)
|
||||||
return self.template.render(Context(settings.COMPRESS_OFFLINE_CONTEXT))
|
if engine == 'django':
|
||||||
elif engine == "jinja2":
|
return ''.join(self.template.render(c) for c in contexts)
|
||||||
return self.template_jinja2.render(settings.COMPRESS_OFFLINE_CONTEXT) + "\n"
|
if engine == 'jinja2':
|
||||||
else:
|
return '\n'.join(self.template_jinja2.render(c) for c in contexts) + "\n"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _test_offline(self, engine):
|
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)
|
count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
|
||||||
self.assertEqual(1, count)
|
self.assertEqual(len(hashes), count)
|
||||||
self.assertEqual([
|
self.assertEqual(['<script type="text/javascript" src="/static/CACHE/js/%s.js"></script>' % h for h in hashes], result)
|
||||||
'<script type="text/javascript" src="/static/CACHE/js/%s.js"></script>' % (self.expected_hash, ),
|
|
||||||
], result)
|
|
||||||
rendered_template = self._render_template(engine)
|
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):
|
def test_offline(self):
|
||||||
for engine in self.engines:
|
for engine in self.engines:
|
||||||
|
@ -247,6 +262,84 @@ class OfflineGenerationTestCaseWithContext(OfflineTestCaseMixin, TestCase):
|
||||||
super(OfflineGenerationTestCaseWithContext, self).tearDown()
|
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):
|
class OfflineGenerationTestCaseErrors(OfflineTestCaseMixin, TestCase):
|
||||||
templates_dir = "test_error_handling"
|
templates_dir = "test_error_handling"
|
||||||
|
|
||||||
|
|
|
@ -492,6 +492,37 @@ Offline settings
|
||||||
|
|
||||||
If available, the ``STATIC_URL`` setting is also added to the context.
|
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
|
.. attribute:: COMPRESS_OFFLINE_MANIFEST
|
||||||
|
|
||||||
:Default: ``manifest.json``
|
:Default: ``manifest.json``
|
||||||
|
|
Loading…
Reference in New Issue