Commit 452f97f9 by David Trowbridge

Merge pull request #545 from chipx86/inline-compile-errors

Add error output for compiler errors within the browser.
parents f2366b5e 3b5c5a22
......@@ -145,6 +145,18 @@ Defaults to ``True``
this only work when PIPELINE_ENABLED is False.
``SHOW_ERRORS_INLINE``
......................
``True`` if errors compiling CSS/JavaScript files should be shown inline at
the top of the browser window, or ``False`` if they should trigger exceptions
(the older behavior).
This only applies when compiling through the ``{% stylesheet %}`` or
``{% javascript %}`` template tags. It won't impact ``collectstatic``.
Defaults to ``settings.DEBUG``.
``CSS_COMPRESSOR``
..................
......
......@@ -8,7 +8,7 @@ from django.contrib.staticfiles import finders
from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.files.base import ContentFile
from django.utils.encoding import smart_bytes
from django.utils.six import string_types
from django.utils.six import string_types, text_type
from pipeline.conf import settings
from pipeline.exceptions import CompilerError
......@@ -125,7 +125,9 @@ class SubProcessCompiler(CompilerBase):
if compiling.returncode != 0:
stdout_captured = None # Don't save erroneous result.
raise CompilerError(
"{0!r} exit code {1}\n{2}".format(argument_list, compiling.returncode, stderr))
"{0!r} exit code {1}\n{2}".format(argument_list, compiling.returncode, stderr),
command=argument_list,
error_output=stderr)
# User wants to see everything that happened.
if self.verbose:
......@@ -134,7 +136,8 @@ class SubProcessCompiler(CompilerBase):
print(stderr)
except OSError as e:
stdout_captured = None # Don't save erroneous result.
raise CompilerError(e)
raise CompilerError(e, command=argument_list,
error_output=text_type(e))
finally:
# Decide what to do with captured stdout.
if stdout:
......
......@@ -23,6 +23,8 @@ DEFAULTS = {
'PIPELINE_ROOT': _settings.STATIC_ROOT,
'PIPELINE_URL': _settings.STATIC_URL,
'SHOW_ERRORS_INLINE': _settings.DEBUG,
'CSS_COMPRESSOR': 'pipeline.compressors.yuglify.YuglifyCompressor',
'JS_COMPRESSOR': 'pipeline.compressors.yuglify.YuglifyCompressor',
'COMPILERS': [],
......
......@@ -10,7 +10,11 @@ class PackageNotFound(PipelineException):
class CompilerError(PipelineException):
pass
def __init__(self, msg, command=None, error_output=None):
super(CompilerError, self).__init__(msg)
self.command = command
self.error_output = error_output.strip()
class CompressorError(PipelineException):
......
......@@ -34,7 +34,7 @@ class PipelineExtension(PipelineMixin, Extension):
package = self.package_for(package_name, 'css')
except PackageNotFound:
return '' # fail silently, do not return anything if an invalid group is specified
return self.render_compressed(package, 'css')
return self.render_compressed(package, package_name, 'css')
def render_css(self, package, path):
template_name = package.template_name or "pipeline/css.jinja"
......@@ -55,7 +55,7 @@ class PipelineExtension(PipelineMixin, Extension):
package = self.package_for(package_name, 'js')
except PackageNotFound:
return '' # fail silently, do not return anything if an invalid group is specified
return self.render_compressed(package, 'js')
return self.render_compressed(package, package_name, 'js')
def render_js(self, package, path):
template_name = package.template_name or "pipeline/js.jinja"
......
<div id="django-pipeline-error-{{package_name}}" class="django-pipeline-error"
style="display: none; border: 2px #DD0000 solid; margin: 1em; padding: 1em; background: white;">
<h1>Error compiling {{package_type}} package "{{package_name}}"</h1>
<p><strong>Command:</strong></p>
<pre style="white-space: pre-wrap;">{{command}}</pre>
<p><strong>Errors:</strong></p>
<pre style="white-space: pre-wrap;">{{errors}}</pre>
</div>
<script>
document.addEventListener('readystatechange', function() {
var el,
container;
if (document.readyState !== 'interactive') {
return;
}
el = document.getElementById('django-pipeline-error-{{package_name}}');
container = document.getElementById('django-pipeline-errors');
if (!container) {
container = document.createElement('div');
container.id = 'django-pipeline-errors';
document.body.insertBefore(container, document.body.firstChild);
}
container.appendChild(el);
el.style.display = 'block';
});
</script>
from __future__ import unicode_literals
import logging
import subprocess
from django.contrib.staticfiles.storage import staticfiles_storage
from django import template
from django.template.base import VariableDoesNotExist
from django.template.base import Context, VariableDoesNotExist
from django.template.loader import render_to_string
from django.utils.safestring import mark_safe
from ..collector import default_collector
from ..conf import settings
from ..exceptions import CompilerError
from ..packager import Packager, PackageNotFound
from ..utils import guess_type
......@@ -51,7 +53,7 @@ class PipelineMixin(object):
except VariableDoesNotExist:
pass
def render_compressed(self, package, package_type):
def render_compressed(self, package, package_name, package_type):
if settings.PIPELINE_ENABLED:
method = getattr(self, "render_{0}".format(package_type))
return method(package, package.output_filename)
......@@ -61,10 +63,29 @@ class PipelineMixin(object):
packager = Packager()
method = getattr(self, "render_individual_{0}".format(package_type))
paths = packager.compile(package.paths)
try:
paths = packager.compile(package.paths)
except CompilerError as e:
if settings.SHOW_ERRORS_INLINE:
method = getattr(self, 'render_error_{0}'.format(
package_type))
return method(package_name, e)
else:
raise
templates = packager.pack_templates(package)
return method(package, paths, templates=templates)
def render_error(self, package_type, package_name, e):
return render_to_string('pipeline/compile_error.html', Context({
'package_type': package_type,
'package_name': package_name,
'command': subprocess.list2cmdline(e.command),
'errors': e.error_output,
}))
class StylesheetNode(PipelineMixin, template.Node):
def __init__(self, name):
......@@ -79,7 +100,7 @@ class StylesheetNode(PipelineMixin, template.Node):
except PackageNotFound:
logger.warn("Package %r is unknown. Check PIPELINE_CSS in your settings.", package_name)
return '' # fail silently, do not return anything if an invalid group is specified
return self.render_compressed(package, 'css')
return self.render_compressed(package, package_name, 'css')
def render_css(self, package, path):
template_name = package.template_name or "pipeline/css.html"
......@@ -94,6 +115,10 @@ class StylesheetNode(PipelineMixin, template.Node):
tags = [self.render_css(package, path) for path in paths]
return '\n'.join(tags)
def render_error_css(self, package_name, e):
return super(StylesheetNode, self).render_error(
'CSS', package_name, e)
class JavascriptNode(PipelineMixin, template.Node):
def __init__(self, name):
......@@ -108,7 +133,7 @@ class JavascriptNode(PipelineMixin, template.Node):
except PackageNotFound:
logger.warn("Package %r is unknown. Check PIPELINE_JS in your settings.", package_name)
return '' # fail silently, do not return anything if an invalid group is specified
return self.render_compressed(package, 'js')
return self.render_compressed(package, package_name, 'js')
def render_js(self, package, path):
template_name = package.template_name or "pipeline/js.html"
......@@ -132,6 +157,10 @@ class JavascriptNode(PipelineMixin, template.Node):
tags.append(self.render_inline(package, templates))
return '\n'.join(tags)
def render_error_js(self, package_name, e):
return super(JavascriptNode, self).render_error(
'JavaScript', package_name, e)
@register.tag
def stylesheet(parser, token):
......
......@@ -156,7 +156,16 @@ class InvalidCompilerTest(TestCase):
self.compiler = Compiler()
def test_compile(self):
self.assertRaises(CompilerError, self.compiler.compile, [_('pipeline/js/dummy.coffee')])
with self.assertRaises(CompilerError) as cm:
self.compiler.compile([_('pipeline/js/dummy.coffee')])
e = cm.exception
self.assertEqual(
e.command,
['this-exists-nowhere-as-a-command-and-should-fail',
'pipeline/js/dummy.coffee',
'pipeline/js/dummy.junk'])
self.assertEqual(e.error_output, '')
def tearDown(self):
default_collector.clear()
......@@ -170,7 +179,12 @@ class FailingCompilerTest(TestCase):
self.compiler = Compiler()
def test_compile(self):
self.assertRaises(CompilerError, self.compiler.compile, [_('pipeline/js/dummy.coffee')])
with self.assertRaises(CompilerError) as cm:
self.compiler.compile([_('pipeline/js/dummy.coffee')])
e = cm.exception
self.assertEqual(e.command, ['/usr/bin/env', 'false'])
self.assertEqual(e.error_output, '')
def tearDown(self):
default_collector.clear()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment