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`` ...@@ -145,6 +145,18 @@ Defaults to ``True``
this only work when PIPELINE_ENABLED is False. 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`` ``CSS_COMPRESSOR``
.................. ..................
......
...@@ -8,7 +8,7 @@ from django.contrib.staticfiles import finders ...@@ -8,7 +8,7 @@ from django.contrib.staticfiles import finders
from django.contrib.staticfiles.storage import staticfiles_storage from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.utils.encoding import smart_bytes 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.conf import settings
from pipeline.exceptions import CompilerError from pipeline.exceptions import CompilerError
...@@ -125,7 +125,9 @@ class SubProcessCompiler(CompilerBase): ...@@ -125,7 +125,9 @@ class SubProcessCompiler(CompilerBase):
if compiling.returncode != 0: if compiling.returncode != 0:
stdout_captured = None # Don't save erroneous result. stdout_captured = None # Don't save erroneous result.
raise CompilerError( 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. # User wants to see everything that happened.
if self.verbose: if self.verbose:
...@@ -134,7 +136,8 @@ class SubProcessCompiler(CompilerBase): ...@@ -134,7 +136,8 @@ class SubProcessCompiler(CompilerBase):
print(stderr) print(stderr)
except OSError as e: except OSError as e:
stdout_captured = None # Don't save erroneous result. stdout_captured = None # Don't save erroneous result.
raise CompilerError(e) raise CompilerError(e, command=argument_list,
error_output=text_type(e))
finally: finally:
# Decide what to do with captured stdout. # Decide what to do with captured stdout.
if stdout: if stdout:
......
...@@ -23,6 +23,8 @@ DEFAULTS = { ...@@ -23,6 +23,8 @@ DEFAULTS = {
'PIPELINE_ROOT': _settings.STATIC_ROOT, 'PIPELINE_ROOT': _settings.STATIC_ROOT,
'PIPELINE_URL': _settings.STATIC_URL, 'PIPELINE_URL': _settings.STATIC_URL,
'SHOW_ERRORS_INLINE': _settings.DEBUG,
'CSS_COMPRESSOR': 'pipeline.compressors.yuglify.YuglifyCompressor', 'CSS_COMPRESSOR': 'pipeline.compressors.yuglify.YuglifyCompressor',
'JS_COMPRESSOR': 'pipeline.compressors.yuglify.YuglifyCompressor', 'JS_COMPRESSOR': 'pipeline.compressors.yuglify.YuglifyCompressor',
'COMPILERS': [], 'COMPILERS': [],
......
...@@ -10,7 +10,11 @@ class PackageNotFound(PipelineException): ...@@ -10,7 +10,11 @@ class PackageNotFound(PipelineException):
class CompilerError(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): class CompressorError(PipelineException):
......
...@@ -34,7 +34,7 @@ class PipelineExtension(PipelineMixin, Extension): ...@@ -34,7 +34,7 @@ class PipelineExtension(PipelineMixin, Extension):
package = self.package_for(package_name, 'css') package = self.package_for(package_name, 'css')
except PackageNotFound: except PackageNotFound:
return '' # fail silently, do not return anything if an invalid group is specified 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): def render_css(self, package, path):
template_name = package.template_name or "pipeline/css.jinja" template_name = package.template_name or "pipeline/css.jinja"
...@@ -55,7 +55,7 @@ class PipelineExtension(PipelineMixin, Extension): ...@@ -55,7 +55,7 @@ class PipelineExtension(PipelineMixin, Extension):
package = self.package_for(package_name, 'js') package = self.package_for(package_name, 'js')
except PackageNotFound: except PackageNotFound:
return '' # fail silently, do not return anything if an invalid group is specified 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): def render_js(self, package, path):
template_name = package.template_name or "pipeline/js.jinja" 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 from __future__ import unicode_literals
import logging import logging
import subprocess
from django.contrib.staticfiles.storage import staticfiles_storage from django.contrib.staticfiles.storage import staticfiles_storage
from django import template 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.template.loader import render_to_string
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from ..collector import default_collector from ..collector import default_collector
from ..conf import settings from ..conf import settings
from ..exceptions import CompilerError
from ..packager import Packager, PackageNotFound from ..packager import Packager, PackageNotFound
from ..utils import guess_type from ..utils import guess_type
...@@ -51,7 +53,7 @@ class PipelineMixin(object): ...@@ -51,7 +53,7 @@ class PipelineMixin(object):
except VariableDoesNotExist: except VariableDoesNotExist:
pass pass
def render_compressed(self, package, package_type): def render_compressed(self, package, package_name, package_type):
if settings.PIPELINE_ENABLED: if settings.PIPELINE_ENABLED:
method = getattr(self, "render_{0}".format(package_type)) method = getattr(self, "render_{0}".format(package_type))
return method(package, package.output_filename) return method(package, package.output_filename)
...@@ -61,10 +63,29 @@ class PipelineMixin(object): ...@@ -61,10 +63,29 @@ class PipelineMixin(object):
packager = Packager() packager = Packager()
method = getattr(self, "render_individual_{0}".format(package_type)) 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) templates = packager.pack_templates(package)
return method(package, paths, templates=templates) 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): class StylesheetNode(PipelineMixin, template.Node):
def __init__(self, name): def __init__(self, name):
...@@ -79,7 +100,7 @@ class StylesheetNode(PipelineMixin, template.Node): ...@@ -79,7 +100,7 @@ class StylesheetNode(PipelineMixin, template.Node):
except PackageNotFound: except PackageNotFound:
logger.warn("Package %r is unknown. Check PIPELINE_CSS in your settings.", package_name) 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 '' # 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): def render_css(self, package, path):
template_name = package.template_name or "pipeline/css.html" template_name = package.template_name or "pipeline/css.html"
...@@ -94,6 +115,10 @@ class StylesheetNode(PipelineMixin, template.Node): ...@@ -94,6 +115,10 @@ class StylesheetNode(PipelineMixin, template.Node):
tags = [self.render_css(package, path) for path in paths] tags = [self.render_css(package, path) for path in paths]
return '\n'.join(tags) 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): class JavascriptNode(PipelineMixin, template.Node):
def __init__(self, name): def __init__(self, name):
...@@ -108,7 +133,7 @@ class JavascriptNode(PipelineMixin, template.Node): ...@@ -108,7 +133,7 @@ class JavascriptNode(PipelineMixin, template.Node):
except PackageNotFound: except PackageNotFound:
logger.warn("Package %r is unknown. Check PIPELINE_JS in your settings.", package_name) 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 '' # 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): def render_js(self, package, path):
template_name = package.template_name or "pipeline/js.html" template_name = package.template_name or "pipeline/js.html"
...@@ -132,6 +157,10 @@ class JavascriptNode(PipelineMixin, template.Node): ...@@ -132,6 +157,10 @@ class JavascriptNode(PipelineMixin, template.Node):
tags.append(self.render_inline(package, templates)) tags.append(self.render_inline(package, templates))
return '\n'.join(tags) return '\n'.join(tags)
def render_error_js(self, package_name, e):
return super(JavascriptNode, self).render_error(
'JavaScript', package_name, e)
@register.tag @register.tag
def stylesheet(parser, token): def stylesheet(parser, token):
......
...@@ -156,7 +156,16 @@ class InvalidCompilerTest(TestCase): ...@@ -156,7 +156,16 @@ class InvalidCompilerTest(TestCase):
self.compiler = Compiler() self.compiler = Compiler()
def test_compile(self): 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): def tearDown(self):
default_collector.clear() default_collector.clear()
...@@ -170,7 +179,12 @@ class FailingCompilerTest(TestCase): ...@@ -170,7 +179,12 @@ class FailingCompilerTest(TestCase):
self.compiler = Compiler() self.compiler = Compiler()
def test_compile(self): 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): def tearDown(self):
default_collector.clear() 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