5 from collections import OrderedDict
6 from fnmatch import fnmatch
7 from optparse import make_option
8 from importlib import import_module
11 from django.core.management.base import BaseCommand, CommandError
12 import django.template
13 from django.template import Context
14 from django.utils import six
15 from django.template.loader import get_template # noqa Leave this in to preload template locations
16 from django.template.utils import InvalidTemplateEngineError
17 from django.template import engines
19 from compressor.cache import get_offline_hexdigest, write_offline_manifest
20 from compressor.conf import settings
21 from compressor.exceptions import (OfflineGenerationError, TemplateSyntaxError,
23 from compressor.templatetags.compress import CompressorNode
24 from compressor.utils import get_mod_func
27 # there is an 'io' module in python 2.6+, but io.StringIO does not
28 # accept regular strings, just unicode objects
29 from io import StringIO
32 from cStringIO import StringIO
34 from StringIO import StringIO
37 class Command(BaseCommand):
38 help = "Compress content outside of the request/response cycle"
39 option_list = BaseCommand.option_list + (
40 make_option('--extension', '-e', action='append', dest='extensions',
41 help='The file extension(s) to examine (default: ".html", '
42 'separate multiple extensions with commas, or use -e '
44 make_option('-f', '--force', default=False, action='store_true',
45 help="Force the generation of compressed content even if the "
46 "COMPRESS_ENABLED setting is not True.", dest='force'),
47 make_option('--follow-links', default=False, action='store_true',
48 help="Follow symlinks when traversing the COMPRESS_ROOT "
49 "(which defaults to STATIC_ROOT). Be aware that using this "
50 "can lead to infinite recursion if a link points to a parent "
51 "directory of itself.", dest='follow_links'),
52 make_option('--engine', default="django", action="store",
53 help="Specifies the templating engine. jinja2 or django",
57 def get_loaders(self):
58 template_source_loaders = []
59 for e in engines.all():
60 if hasattr(e, 'engine'):
61 template_source_loaders.extend(
62 e.engine.get_template_loaders(e.engine.loaders))
64 # If template loader is CachedTemplateLoader, return the loaders
65 # that it wraps around. So if we have
66 # TEMPLATE_LOADERS = (
67 # ('django.template.loaders.cached.Loader', (
68 # 'django.template.loaders.filesystem.Loader',
69 # 'django.template.loaders.app_directories.Loader',
72 # The loaders will return django.template.loaders.filesystem.Loader
73 # and django.template.loaders.app_directories.Loader
74 # The cached Loader and similar ones include a 'loaders' attribute
75 # so we look for that.
76 for loader in template_source_loaders:
77 if hasattr(loader, 'loaders'):
78 loaders.extend(loader.loaders)
80 loaders.append(loader)
83 def __get_parser(self, engine):
84 if engine == "jinja2":
85 from compressor.offline.jinja2 import Jinja2Parser
86 env = settings.COMPRESS_JINJA2_GET_ENVIRONMENT()
87 parser = Jinja2Parser(charset=settings.FILE_CHARSET, env=env)
88 elif engine == "django":
89 from compressor.offline.django import DjangoParser
90 parser = DjangoParser(charset=settings.FILE_CHARSET)
92 raise OfflineGenerationError("Invalid templating engine specified.")
96 def compress(self, log=None, **options):
98 Searches templates containing 'compress' nodes and compresses them
99 "offline" -- outside of the request/response cycle.
101 The result is cached with a cache-key derived from the content of the
102 compress nodes (not the content of the possibly linked files!).
104 engine = options.get("engine", "django")
105 extensions = options.get('extensions')
106 extensions = self.handle_extensions(extensions or ['html'])
107 verbosity = int(options.get("verbosity", 0))
110 if not settings.TEMPLATE_LOADERS:
111 raise OfflineGenerationError("No template loaders defined. You "
112 "must set TEMPLATE_LOADERS in your "
115 if engine == 'django':
117 for loader in self.get_loaders():
119 module = import_module(loader.__module__)
120 get_template_sources = getattr(module,
121 'get_template_sources', None)
122 if get_template_sources is None:
123 get_template_sources = loader.get_template_sources
124 paths.update(str(origin) for origin in get_template_sources(''))
125 except (ImportError, AttributeError, TypeError):
126 # Yeah, this didn't work out so well, let's move on
130 raise OfflineGenerationError("No template paths found. None of "
131 "the configured template loaders "
132 "provided template paths. See "
133 "https://docs.djangoproject.com/en/1.8/topics/templates/ "
134 "for more information on template "
137 log.write("Considering paths:\n\t" + "\n\t".join(paths) + "\n")
140 for root, dirs, files in os.walk(path,
141 followlinks=options.get('followlinks', False)):
142 templates.update(os.path.join(root, name)
143 for name in files if not name.startswith('.') and
144 any(fnmatch(name, "*%s" % glob) for glob in extensions))
145 elif engine == 'jinja2' and django.VERSION >= (1, 8):
146 env = settings.COMPRESS_JINJA2_GET_ENVIRONMENT()
147 if env and hasattr(env, 'list_templates'):
148 templates |= set([env.loader.get_source(env, template)[1] for template in
149 env.list_templates(filter_func=lambda _path:
150 os.path.splitext(_path)[-1] in extensions)])
153 raise OfflineGenerationError("No templates found. Make sure your "
154 "TEMPLATE_LOADERS and TEMPLATE_DIRS "
155 "settings are correct.")
157 log.write("Found templates:\n\t" + "\n\t".join(templates) + "\n")
159 parser = self.__get_parser(engine)
160 compressor_nodes = OrderedDict()
161 for template_name in templates:
163 template = parser.parse(template_name)
164 except IOError: # unreadable file -> ignore
166 log.write("Unreadable template at: %s\n" % template_name)
168 except TemplateSyntaxError as e: # broken template -> ignore
170 log.write("Invalid template %s: %s\n" % (template_name, e))
172 except TemplateDoesNotExist: # non existent template -> ignore
174 log.write("Non-existent template at: %s\n" % template_name)
176 except UnicodeDecodeError:
178 log.write("UnicodeDecodeError while trying to read "
179 "template %s\n" % template_name)
181 nodes = list(parser.walk_nodes(template))
182 except (TemplateDoesNotExist, TemplateSyntaxError) as e:
183 # Could be an error in some base template
185 log.write("Error parsing template %s: %s\n" % (template_name, e))
188 template.template_name = template_name
189 compressor_nodes.setdefault(template, []).extend(nodes)
191 if not compressor_nodes:
192 raise OfflineGenerationError(
193 "No 'compress' template tags found in templates."
194 "Try running compress command with --follow-links and/or"
195 "--extension=EXTENSIONS")
198 log.write("Found 'compress' tags in:\n\t" +
199 "\n\t".join((t.template_name
200 for t in compressor_nodes.keys())) + "\n")
202 contexts = settings.COMPRESS_OFFLINE_CONTEXT
203 if isinstance(contexts, six.string_types):
205 module, function = get_mod_func(contexts)
206 contexts = getattr(import_module(module), function)()
207 except (AttributeError, ImportError, TypeError) as e:
208 raise ImportError("Couldn't import offline context function %s: %s" %
209 (settings.COMPRESS_OFFLINE_CONTEXT, e))
210 elif not isinstance(contexts, (list, tuple)):
211 contexts = [contexts]
213 log.write("Compressing... ")
214 block_count = context_count = 0
216 offline_manifest = OrderedDict()
218 for context_dict in contexts:
220 init_context = parser.get_init_context(context_dict)
222 for template, nodes in compressor_nodes.items():
223 context = Context(init_context)
225 template._log_verbosity = verbosity
227 if not parser.process_template(template, context):
232 parser.process_node(template, context, node)
233 rendered = parser.render_nodelist(template, context, node)
234 key = get_offline_hexdigest(rendered)
236 if key in offline_manifest:
240 result = parser.render_node(template, context, node)
241 except Exception as e:
242 raise CommandError("An error occurred during rendering %s: "
243 "%s" % (template.template_name, e))
244 offline_manifest[key] = result
246 results.append(result)
249 write_offline_manifest(offline_manifest)
251 log.write("done\nCompressed %d block(s) from %d template(s) for %d context(s).\n" %
252 (block_count, len(compressor_nodes), context_count))
253 return block_count, results
255 def handle_extensions(self, extensions=('html',)):
257 organizes multiple extensions that are separated with commas or
258 passed by using --extension/-e multiple times.
260 for example: running 'django-admin compress -e js,txt -e xhtml -a'
261 would result in an extension list: ['.js', '.txt', '.xhtml']
263 >>> handle_extensions(['.html', 'html,js,py,py,py,.py', 'py,.py'])
265 >>> handle_extensions(['.html, txt,.tpl'])
266 ['.html', '.tpl', '.txt']
269 for ext in extensions:
270 ext_list.extend(ext.replace(' ', '').split(','))
271 for i, ext in enumerate(ext_list):
272 if not ext.startswith('.'):
273 ext_list[i] = '.%s' % ext_list[i]
276 def handle(self, **options):
277 if not settings.COMPRESS_ENABLED and not options.get("force"):
279 "Compressor is disabled. Set the COMPRESS_ENABLED "
280 "setting or use --force to override.")
281 if not settings.COMPRESS_OFFLINE:
282 if not options.get("force"):
284 "Offline compression is disabled. Set "
285 "COMPRESS_OFFLINE or use the --force to override.")
286 self.compress(sys.stdout, **options)
290 Command.requires_system_checks = False