Commit 3b5c5a22 by Christian Hammond

Add error output for compiler errors within the browser.

Debugging CSS/JavaScript compilation errors can be difficult when viewed
within the context of a Django error page, and if there's a wider
problem with other CSS/JavaScript packages, they won't show up until the
first compilation error is fixed. This is worse if running in a mode
where Pipeline is running but Django error pages aren't being shown.

This change introduces an option, PIPELINE.SHOW_ERRORS_INLINE (now
enabled by default) that outputs any compilation errors at the top of
the page, visually distinguished from the rest of the page, and
containing information on the failing package, command line, and output.
This makes it much easier to debug what went wrong.

The errors are outputted into a hidden div, and then once the page has
fully loaded, they're placed at the top. This allows Pipeline usage
anywhere on the page to promote errors to the top, in the order in which
they're loaded. These also have a default style, but that can be
overridden (and in fact the whole error template overridden) by the
consuming application.
parent f2366b5e
......@@ -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