Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhanced compress template tag #285

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion compressor/base.py
Expand Up @@ -31,7 +31,7 @@ class Compressor(object):
"""
type = None

def __init__(self, content=None, output_prefix=None, context=None, *args, **kwargs):
def __init__(self, content=None, output_prefix=None, context=None, opts=None, *args, **kwargs):
self.content = content or ""
self.output_prefix = output_prefix or "compressed"
self.output_dir = settings.COMPRESS_OUTPUT_DIR.strip('/')
Expand All @@ -42,6 +42,7 @@ def __init__(self, content=None, output_prefix=None, context=None, *args, **kwar
self.extra_context = {}
self.all_mimetypes = dict(settings.COMPRESS_PRECOMPILERS)
self.finders = staticfiles.finders
self.opts = opts or {}

def split_contents(self):
"""
Expand Down Expand Up @@ -158,6 +159,7 @@ def hunks(self, forced=False):
'kind': kind,
'basename': basename,
}
options.update(self.opts)

if kind == SOURCE_FILE:
options = dict(options, filename=value)
Expand Down
4 changes: 2 additions & 2 deletions compressor/css.py
Expand Up @@ -4,9 +4,9 @@

class CssCompressor(Compressor):

def __init__(self, content=None, output_prefix="css", context=None):
def __init__(self, content=None, output_prefix="css", context=None, opts=None):
super(CssCompressor, self).__init__(content=content,
output_prefix=output_prefix, context=context)
output_prefix=output_prefix, context=context, opts=opts)
self.filters = list(settings.COMPRESS_CSS_FILTERS)
self.type = output_prefix

Expand Down
4 changes: 2 additions & 2 deletions compressor/js.py
Expand Up @@ -4,8 +4,8 @@

class JsCompressor(Compressor):

def __init__(self, content=None, output_prefix="js", context=None):
super(JsCompressor, self).__init__(content, output_prefix, context)
def __init__(self, content=None, output_prefix="js", context=None, opts=None):
super(JsCompressor, self).__init__(content, output_prefix, context, opts)
self.filters = list(settings.COMPRESS_JS_FILTERS)
self.type = output_prefix

Expand Down
62 changes: 40 additions & 22 deletions compressor/templatetags/compress.py
Expand Up @@ -33,9 +33,9 @@ def compressor_cls(self, kind, *args, **kwargs):
return get_class(self.compressors.get(kind),
exception=ImproperlyConfigured)(*args, **kwargs)

def get_compressor(self, context, kind):
def get_compressor(self, context, kind, tag_opts):
return self.compressor_cls(kind,
content=self.get_original_content(context), context=context)
content=self.get_original_content(context), context=context, opts=tag_opts)

def debug_mode(self, context):
if settings.COMPRESS_DEBUG_TOGGLE:
Expand Down Expand Up @@ -82,7 +82,7 @@ def render_cached(self, compressor, kind, mode, forced=False):
return cache_key, cache_content
return None, None

def render_compressed(self, context, kind, mode, forced=False):
def render_compressed(self, context, kind, mode, tag_opts=None, forced=False):

# See if it has been rendered offline
cached_offline = self.render_offline(context, forced=forced)
Expand All @@ -96,7 +96,7 @@ def render_compressed(self, context, kind, mode, forced=False):
return self.get_original_content(context)

context['compressed'] = {'name': getattr(self, 'name', None)}
compressor = self.get_compressor(context, kind)
compressor = self.get_compressor(context, kind, tag_opts)

# Prepare the actual compressor and check cache
cache_key, cache_content = self.render_cached(compressor, kind, mode, forced=forced)
Expand All @@ -122,11 +122,12 @@ def render_output(self, compressor, mode, forced=False):

class CompressorNode(CompressorMixin, template.Node):

def __init__(self, nodelist, kind=None, mode=OUTPUT_FILE, name=None):
def __init__(self, nodelist, kind=None, mode=OUTPUT_FILE, name=None, tag_opts={}):
self.nodelist = nodelist
self.kind = kind
self.mode = mode
self.name = name
self.tag_opts = tag_opts

def get_original_content(self, context):
return self.nodelist.render(context)
Expand All @@ -145,7 +146,16 @@ def render(self, context, forced=False):
if self.debug_mode(context):
return self.get_original_content(context)

return self.render_compressed(context, self.kind, self.mode, forced=forced)
self.resolve_variables(context)
return self.render_compressed(context, self.kind, self.mode, self.tag_opts, forced=forced)

def resolve_variables(self, context):
for option, value in self.tag_opts.items():
try:
value = value.resolve(context)
except template.VariableDoesNotExist:
value = unicode(value)
self.tag_opts[option] = value


@register.tag
Expand All @@ -155,7 +165,7 @@ def compress(parser, token):

Syntax::

{% compress <js/css> %}
{% compress js|css [file|inline] [<option>=<value>[ <option>=<value>...]] [as <variable_name>] %}
<html of inline or linked JS/CSS>
{% endcompress %}

Expand Down Expand Up @@ -192,22 +202,30 @@ def compress(parser, token):

args = token.split_contents()

if not len(args) in (2, 3, 4):
if not len(args) >= 2:
raise template.TemplateSyntaxError(
"%r tag requires either one, two or three arguments." % args[0])
"%r tag requires at least one argument." % args[0])

kind = args[1]

name = None
mode = OUTPUT_FILE
tag_opts = {}
if len(args) >= 3:
mode = args[2]
if not mode in OUTPUT_MODES:
raise template.TemplateSyntaxError(
"%r's second argument must be '%s' or '%s'." %
(args[0], OUTPUT_FILE, OUTPUT_INLINE))
else:
mode = OUTPUT_FILE
if len(args) == 4:
name = args[3]
else:
name = None
return CompressorNode(nodelist, kind, mode, name)
looking_for_name = False
for i in range(2, len(args)):
if looking_for_name:
name = args[i]
looking_for_name = False
elif args[i] == "as":
looking_for_name = True
elif '=' in args[i]:
option, value = args[i].split("=")
tag_opts[option] = template.Variable(value)
elif args[i] in OUTPUT_MODES:
mode = args[i]
else:
raise template.TemplateSyntaxError(
"%r's third argument on must either be (file|input) or <option>=<value> or 'as <name>'" %
args[0])

return CompressorNode(nodelist, kind, mode, name, tag_opts)
25 changes: 23 additions & 2 deletions compressor/tests/test_templatetags.py
Expand Up @@ -3,13 +3,14 @@
import os
import sys

from mock import Mock
from mock import Mock, patch

from django.template import Template, Context, TemplateSyntaxError
from django.test import TestCase

from compressor.conf import settings
from compressor.signals import post_compress
from compressor.templatetags.compress import CompressorNode
from compressor.tests.test_base import css_tag, test_dir


Expand Down Expand Up @@ -116,7 +117,7 @@ class MockDebugRequest(object):
self.assertEqual(out, render(template, context))

def test_named_compress_tag(self):
template = u"""{% load compress %}{% compress js inline foo %}
template = u"""{% load compress %}{% compress js inline as foo %}
<script type="text/javascript">obj.value = "value";</script>
{% endcompress %}
"""
Expand All @@ -130,6 +131,26 @@ def listener(sender, **kwargs):
context = kwargs['context']
self.assertEqual('foo', context['compressed']['name'])

@patch('compressor.templatetags.compress.CompressorNode', wraps=CompressorNode)
def test_tag_opts(self, mock_class):
template = u"""{% load compress %}{% compress js inline group_first=True %}
<script type="text/javascript">obj.value = "value";</script>
{% endcompress %}
"""
render(template)
self.assertEqual(len(mock_class.mock_calls), 1)
self.assertEqual(len(mock_class.mock_calls[0][1]), 5)
self.assertDictEqual(mock_class.mock_calls[0][1][4], {'group_first': 'True'})

@patch('compressor.templatetags.compress.CompressorNode', wraps=CompressorNode)
def test_variable_tag_opts(self, mock_class):
template = u"""{% load compress %}{% compress js inline group_first=True %}
<script type="text/javascript">obj.value = "value";</script>
{% endcompress %}
"""
render(template, {'group_first': True})
self.assertDictEqual(mock_class.mock_calls[0][1][4], {'group_first': 'True'})


class PrecompilerTemplatetagTestCase(TestCase):
def setUp(self):
Expand Down
38 changes: 30 additions & 8 deletions docs/usage.txt
Expand Up @@ -6,7 +6,7 @@ Usage
.. code-block:: django

{% load compress %}
{% compress <js/css> [<file/inline> [block_name]] %}
{% compress js|css [file|inline] [as <block_name>] [<option>=<value>[ <option>=<value>...]] %}
<html of inline or linked JS/CSS>
{% endcompress %}

Expand Down Expand Up @@ -75,10 +75,9 @@ exception. If DEBUG is ``False`` these files will be silently stripped.
:attr:`~django.conf.settings.COMPRESS_CACHE_BACKEND` and
Django's `caching documentation`_).

The compress template tag supports a second argument specifying the output
mode and defaults to saving the result in a file. Alternatively you can
pass '``inline``' to the template tag to return the content directly to the
rendered page, e.g.:
The compress template tag supports specifying the output mode and defaults
to saving the result in a file. Alternatively, you can pass '``inline``' to
the template tag to return the content directly to the rendered page, e.g.:

.. code-block:: django

Expand All @@ -96,9 +95,32 @@ would be rendered something like::
obj.value = "value";
</script>

The compress template tag also supports a third argument for naming the output
of that particular compress tag. This is then added to the context so you can
access it in the `post_compress signal <signals>`.
The compress template tag also supports naming the output of that particular
compress tag, e.g.:

.. code-block:: django

{% load compress %}

{% compress js as main_js %}
<script src="/static/js/one.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript" charset="utf-8">obj.value = "value";</script>
{% endcompress %}

This is then added to the context so you can access it in the `post_compress signal <signals>`.

The compress template also supports passing any number of arbitrary keyword arguments. These are
passed to all the compressors and, when using the default compressor classes, all of the
precompilers and filters, e.g.:

.. code-block:: django

{% load compress %}

{% compress js foo=bar group_first=true %}
<script src="/static/js/one.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript" charset="utf-8">obj.value = "value";</script>
{% endcompress %}

.. _memcached: http://memcached.org/
.. _caching documentation: http://docs.djangoproject.com/en/1.2/topics/cache/#memcached
Expand Down