diff --git a/compressor/management/commands/compress.py b/compressor/management/commands/compress.py index 1c6d82e..f0603cd 100644 --- a/compressor/management/commands/compress.py +++ b/compressor/management/commands/compress.py @@ -179,7 +179,13 @@ class Command(NoArgsCommand): if verbosity > 0: log.write("UnicodeDecodeError while trying to read " "template %s\n" % template_name) - nodes = list(parser.walk_nodes(template)) + try: + nodes = list(parser.walk_nodes(template)) + except (TemplateDoesNotExist, TemplateSyntaxError) as e: + # Could be an error in some base template + if verbosity > 0: + log.write("Error parsing template %s: %s\n" % (template_name, e)) + continue if nodes: template.template_name = template_name compressor_nodes.setdefault(template, []).extend(nodes) diff --git a/compressor/offline/django.py b/compressor/offline/django.py index a62407e..6541471 100644 --- a/compressor/offline/django.py +++ b/compressor/offline/django.py @@ -1,87 +1,91 @@ from __future__ import absolute_import import io -from types import MethodType +from copy import copy from django import template +from django.conf import settings from django.template import Template -from django.template.loader_tags import (ExtendsNode, BlockNode, - BLOCK_CONTEXT_KEY) +from django.template import Context +from django.template.base import Node, VariableNode, TextNode, NodeList +from django.template.defaulttags import IfNode +from django.template.loader_tags import ExtendsNode, BlockNode, BlockContext from compressor.exceptions import TemplateSyntaxError, TemplateDoesNotExist from compressor.templatetags.compress import CompressorNode -def patched_render(self, context): - # 'Fake' _render method that just returns the context instead of - # rendering. It also checks whether the first node is an extend node or - # not, to be able to handle complex inheritance chain. - self._render_firstnode = MethodType(patched_render_firstnode, self) - self._render_firstnode(context) +def handle_extendsnode(extendsnode, block_context=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. + """ + if block_context is None: + block_context = BlockContext() + blocks = dict((n.name, n) for n in + extendsnode.nodelist.get_nodes_by_type(BlockNode)) + block_context.add_blocks(blocks) - # Cleanup, uninstall our _render monkeypatch now that it has been called - self._render = self._old_render - return context + context = Context(settings.COMPRESS_OFFLINE_CONTEXT) + compiled_parent = extendsnode.get_parent(context) + parent_nodelist = compiled_parent.nodelist + # If the parent template has an ExtendsNode it is not the root. + for node in parent_nodelist: + # 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) + break + # Add blocks of the root template to block context. + blocks = dict((n.name, n) for n in + parent_nodelist.get_nodes_by_type(BlockNode)) + block_context.add_blocks(blocks) + + block_stack = [] + new_nodelist = remove_block_nodes(parent_nodelist, block_stack, block_context) + return new_nodelist -def patched_render_firstnode(self, context): - # If this template has a ExtendsNode, we want to find out what - # should be put in render_context to make the {% block ... %} - # tags work. - # - # We can't fully render the base template(s) (we don't have the - # full context vars - only what's necessary to render the compress - # nodes!), therefore we hack the ExtendsNode we found, patching - # its get_parent method so that rendering the ExtendsNode only - # gives us the blocks content without doing any actual rendering. - extra_context = {} - try: - firstnode = self.nodelist[0] - except IndexError: - firstnode = None - if isinstance(firstnode, ExtendsNode): - firstnode._log = self._log - firstnode._log_verbosity = self._log_verbosity - firstnode._old_get_parent = firstnode.get_parent - firstnode.get_parent = MethodType(patched_get_parent, firstnode) - try: - extra_context = firstnode.render(context) - context.render_context = extra_context.render_context - # We aren't rendering {% block %} tags, but we want - # {{ block.super }} inside {% compress %} inside {% block %}s to - # work. Therefore, we need to pop() the last block context for - # each block name, to emulate what would have been done if the - # {% block %} had been fully rendered. - for blockname in firstnode.blocks.keys(): - context.render_context[BLOCK_CONTEXT_KEY].pop(blockname) - except (IOError, template.TemplateSyntaxError, - template.TemplateDoesNotExist): - # That first node we are trying to render might cause more errors - # that we didn't catch when simply creating a Template instance - # above, so we need to catch that (and ignore it, just like above) - # as well. - if self._log_verbosity > 0: - self._log.write("Caught error when rendering extend node from " - "template %s\n" % getattr(self, 'name', self)) - return None - finally: - # Cleanup, uninstall our get_parent monkeypatch now that it has been called - firstnode.get_parent = firstnode._old_get_parent - return extra_context +def remove_block_nodes(nodelist, block_stack, block_context): + new_nodelist = NodeList() + for node in nodelist: + if isinstance(node, VariableNode): + var_name = node.filter_expression.token.strip() + if var_name == 'block.super': + if not block_stack: + continue + node = block_context.get_block(block_stack[-1].name) + if isinstance(node, BlockNode): + expanded_block = expand_blocknode(node, block_stack, block_context) + new_nodelist.extend(expanded_block) + else: + # IfNode has nodelist as a @property so we can not modify it + if isinstance(node, IfNode): + node = copy(node) + for i, (condition, sub_nodelist) in enumerate(node.conditions_nodelists): + sub_nodelist = remove_block_nodes(sub_nodelist, block_stack, block_context) + node.conditions_nodelists[i] = (condition, sub_nodelist) + else: + for attr in node.child_nodelists: + sub_nodelist = getattr(node, attr, None) + if sub_nodelist: + sub_nodelist = remove_block_nodes(sub_nodelist, block_stack, block_context) + node = copy(node) + setattr(node, attr, sub_nodelist) + new_nodelist.append(node) + return new_nodelist -def patched_get_parent(self, context): - # Patch template returned by extendsnode's get_parent to make sure their - # _render method is just returning the context instead of actually - # rendering stuff. - # In addition, this follows the inheritance chain by looking if the first - # node of the template is an extend node itself. - compiled_template = self._old_get_parent(context) - compiled_template._log = self._log - compiled_template._log_verbosity = self._log_verbosity - compiled_template._old_render = compiled_template._render - compiled_template._render = MethodType(patched_render, compiled_template) - return compiled_template +def expand_blocknode(node, block_stack, block_context): + popped_block = block = block_context.pop(node.name) + if block is None: + block = node + block_stack.append(block) + expanded_nodelist = remove_block_nodes(block.nodelist, block_stack, block_context) + block_stack.pop() + if popped_block is not None: + block_context.push(node.name, popped_block) + return expanded_nodelist class DjangoParser(object): @@ -98,26 +102,13 @@ class DjangoParser(object): raise TemplateDoesNotExist(str(e)) def process_template(self, template, context): - template._render_firstnode = MethodType(patched_render_firstnode, template) - template._extra_context = template._render_firstnode(context) - - if template._extra_context is None: - # Something is wrong - ignore this template - return False - return True def get_init_context(self, offline_context): return offline_context def process_node(self, template, context, node): - if template._extra_context and node._block_name: - # Give a block context to the node if it was found inside - # a {% block %}. - context['block'] = context.render_context[BLOCK_CONTEXT_KEY].get_block(node._block_name) - - if context['block']: - context['block'].context = context + pass def render_nodelist(self, template, context, node): return node.nodelist.render(context) @@ -126,18 +117,27 @@ class DjangoParser(object): return node.render(context, forced=True) def get_nodelist(self, node): - # Check if node is an ```if``` switch with true and false branches - if hasattr(node, 'nodelist_true') and hasattr(node, 'nodelist_false'): - return node.nodelist_true + node.nodelist_false - return getattr(node, "nodelist", []) + if isinstance(node, ExtendsNode): + try: + return handle_extendsnode(node) + except template.TemplateSyntaxError as e: + raise TemplateSyntaxError(str(e)) + except template.TemplateDoesNotExist as e: + raise TemplateDoesNotExist(str(e)) - def walk_nodes(self, node, block_name=None): + # Check if node is an ```if``` switch with true and false branches + nodelist = [] + if isinstance(node, Node): + for attr in node.child_nodelists: + nodelist += getattr(node, attr, []) + else: + nodelist = getattr(node, 'nodelist', []) + return nodelist + + def walk_nodes(self, node): for node in self.get_nodelist(node): - if isinstance(node, BlockNode): - block_name = node.name if isinstance(node, CompressorNode) and node.is_offline_compression_enabled(forced=True): - node._block_name = block_name yield node else: - for node in self.walk_nodes(node, block_name=block_name): + for node in self.walk_nodes(node): yield node diff --git a/compressor/tests/test_offline.py b/compressor/tests/test_offline.py index dda1c43..54e8b08 100644 --- a/compressor/tests/test_offline.py +++ b/compressor/tests/test_offline.py @@ -146,7 +146,7 @@ class OfflineGenerationBlockSuperTestCase(OfflineTestCaseMixin, TestCase): class OfflineGenerationBlockSuperMultipleTestCase(OfflineTestCaseMixin, TestCase): templates_dir = "test_block_super_multiple" - expected_hash = "2f6ef61c488e" + expected_hash = "f8891c416981" # Block.super not supported for Jinja2 yet. engines = ("django",) @@ -341,6 +341,43 @@ class OfflineGenerationTestCase(OfflineTestCaseMixin, TestCase): settings.TEMPLATE_LOADERS = old_loaders +class OfflineGenerationBlockSuperBaseCompressed(OfflineTestCaseMixin, TestCase): + template_names = ["base.html", "base2.html", "test_compressor_offline.html"] + templates_dir = 'test_block_super_base_compressed' + expected_hash = ['028c3fc42232', '2e9d3f5545a6', 'f8891c416981'] + # Block.super not supported for Jinja2 yet. + engines = ("django",) + + def setUp(self): + super(OfflineGenerationBlockSuperBaseCompressed, self).setUp() + + self.template_paths = [] + self.templates = [] + for template_name in self.template_names: + template_path = os.path.join(settings.TEMPLATE_DIRS[0], template_name) + self.template_paths.append(template_path) + with io.open(template_path, encoding=settings.FILE_CHARSET) as file: + template = Template(file.read()) + self.templates.append(template) + + def _render_template(self, template, engine): + if engine == "django": + return template.render(Context(settings.COMPRESS_OFFLINE_CONTEXT)) + elif engine == "jinja2": + return template.render(settings.COMPRESS_OFFLINE_CONTEXT) + "\n" + else: + return None + + def _test_offline(self, engine): + count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine) + self.assertEqual(len(self.expected_hash), count) + for expected_hash, template in zip(self.expected_hash, self.templates): + expected_output = '' % (expected_hash, ) + self.assertIn(expected_output, result) + rendered_template = self._render_template(template, engine) + self.assertEqual(rendered_template, expected_output + '\n') + + class OfflineGenerationInlineNonAsciiTestCase(OfflineTestCaseMixin, TestCase): templates_dir = "test_inline_non_ascii" diff --git a/compressor/tests/test_templates/test_block_super_base_compressed/base.html b/compressor/tests/test_templates/test_block_super_base_compressed/base.html new file mode 100644 index 0000000..481ff40 --- /dev/null +++ b/compressor/tests/test_templates/test_block_super_base_compressed/base.html @@ -0,0 +1,10 @@ +{% load compress %}{% spaceless %} + +{% compress js %} +{% block js %} + +{% endblock %} +{% endcompress %} +{% endspaceless %} diff --git a/compressor/tests/test_templates/test_block_super_base_compressed/base2.html b/compressor/tests/test_templates/test_block_super_base_compressed/base2.html new file mode 100644 index 0000000..abd074d --- /dev/null +++ b/compressor/tests/test_templates/test_block_super_base_compressed/base2.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block js %}{% spaceless %} + {{ block.super }} + +{% endspaceless %}{% endblock %} diff --git a/compressor/tests/test_templates/test_block_super_base_compressed/test_compressor_offline.html b/compressor/tests/test_templates/test_block_super_base_compressed/test_compressor_offline.html new file mode 100644 index 0000000..01382ec --- /dev/null +++ b/compressor/tests/test_templates/test_block_super_base_compressed/test_compressor_offline.html @@ -0,0 +1,8 @@ +{% extends "base2.html" %} + +{% block js %}{% spaceless %} + {{ block.super }} + +{% endspaceless %}{% endblock %} diff --git a/compressor/tests/test_templates/test_block_super_multiple/base2.html b/compressor/tests/test_templates/test_block_super_multiple/base2.html index b0b2fef..c781fb5 100644 --- a/compressor/tests/test_templates/test_block_super_multiple/base2.html +++ b/compressor/tests/test_templates/test_block_super_multiple/base2.html @@ -1,3 +1,10 @@ {% extends "base.html" %} +{% block js %}{% spaceless %} + {{ block.super }} + +{% endspaceless %}{% endblock %} + {% block css %}{% endblock %}