Commit d5a6810d by Alvin Mites

Merge branch 'master' of github.com:jazzband/django-pipeline

parents 21efb5c0 930b12c9
...@@ -76,6 +76,7 @@ or just made Pipeline more awesome. ...@@ -76,6 +76,7 @@ or just made Pipeline more awesome.
* Mike Gilbert <floppym@gentoo.org> * Mike Gilbert <floppym@gentoo.org>
* Miroslav Shubernetskiy <miroslav@miki725.com> * Miroslav Shubernetskiy <miroslav@miki725.com>
* Natal Ngetal <natal.ngetal@novapost.fr> * Natal Ngetal <natal.ngetal@novapost.fr>
* Nathan Cox <akujin@akujin.com>
* Nathan Shafer <nate@torzo.com> * Nathan Shafer <nate@torzo.com>
* Patrick Altman <paltman@gmail.com> * Patrick Altman <paltman@gmail.com>
* Peter Baumgartner <pete@lincolnloop.com> * Peter Baumgartner <pete@lincolnloop.com>
......
...@@ -3,6 +3,20 @@ ...@@ -3,6 +3,20 @@
History History
======= =======
1.6.7
=====
* Add a view for collecting static files before serving them. This behaves like
django's built-in ``static`` view and allows running the collector for
images, fonts, and other static files that do not need to be compiled. Thanks
to Christian Hammond.
* Update documentation for the ES6Compiler to clarify filename requirements.
Thanks to Nathan Cox.
* Add error output for compiler errors within the browser. This provides for a
much better experience when compiling files from the devserver. Thanks to
Christian Hammond.
* Make unit tests run against Django 1.6 and 1.7. Thanks to Sławek Ehlert.
1.6.6 1.6.6
===== =====
......
...@@ -26,4 +26,4 @@ Documentation ...@@ -26,4 +26,4 @@ Documentation
------------- -------------
For documentation, usage, and examples, see : For documentation, usage, and examples, see :
http://django-pipeline.readthedocs.org https://django-pipeline.readthedocs.org
...@@ -150,12 +150,14 @@ ES6 compiler ...@@ -150,12 +150,14 @@ ES6 compiler
The ES6 compiler uses `Babel <https://babeljs.io>`_ The ES6 compiler uses `Babel <https://babeljs.io>`_
to convert ES6+ code into vanilla ES5. to convert ES6+ code into vanilla ES5.
Note that for files to be transpiled properly they must have the file extension **.es6**
To use it add this to your ``PIPELINE['COMPILERS']`` :: To use it add this to your ``PIPELINE['COMPILERS']`` ::
PIPELINE['COMPILERS'] = ( PIPELINE['COMPILERS'] = (
'pipeline.compilers.es6.ES6Compiler', 'pipeline.compilers.es6.ES6Compiler',
) )
``BABEL_BINARY`` ``BABEL_BINARY``
-------------------------- --------------------------
......
...@@ -51,7 +51,7 @@ copyright = u'2011-2014, Timothée Peignier' ...@@ -51,7 +51,7 @@ copyright = u'2011-2014, Timothée Peignier'
# The short X.Y version. # The short X.Y version.
version = '1.6' version = '1.6'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '1.6.6' release = '1.6.7'
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
......
...@@ -6,6 +6,7 @@ from collections import OrderedDict ...@@ -6,6 +6,7 @@ from collections import OrderedDict
from django.contrib.staticfiles import finders from django.contrib.staticfiles import finders
from django.contrib.staticfiles.storage import staticfiles_storage from django.contrib.staticfiles.storage import staticfiles_storage
from django.utils import six
from pipeline.finders import PipelineFinder from pipeline.finders import PipelineFinder
...@@ -26,7 +27,7 @@ class Collector(object): ...@@ -26,7 +27,7 @@ class Collector(object):
for d in dirs: for d in dirs:
self.clear(os.path.join(path, d)) self.clear(os.path.join(path, d))
def collect(self, request=None): def collect(self, request=None, files=[]):
if self.request and self.request is request: if self.request and self.request is request:
return return
self.request = request self.request = request
...@@ -41,10 +42,17 @@ class Collector(object): ...@@ -41,10 +42,17 @@ class Collector(object):
prefixed_path = os.path.join(storage.prefix, path) prefixed_path = os.path.join(storage.prefix, path)
else: else:
prefixed_path = path prefixed_path = path
if prefixed_path not in found_files:
if (prefixed_path not in found_files and
(not files or prefixed_path in files)):
found_files[prefixed_path] = (storage, path) found_files[prefixed_path] = (storage, path)
self.copy_file(path, prefixed_path, storage) self.copy_file(path, prefixed_path, storage)
if files and len(files) == len(found_files):
break
return six.iterkeys(found_files)
def copy_file(self, path, prefixed_path, source_storage): def copy_file(self, path, prefixed_path, source_storage):
# Delete the target file if needed or break # Delete the target file if needed or break
if not self.delete_file(path, prefixed_path, source_storage): if not self.delete_file(path, prefixed_path, source_storage):
......
...@@ -79,11 +79,12 @@ class CompilerBase(object): ...@@ -79,11 +79,12 @@ class CompilerBase(object):
return '.'.join((path[0], extension)) return '.'.join((path[0], extension))
def is_outdated(self, infile, outfile): def is_outdated(self, infile, outfile):
if not self.storage.exists(outfile): if not os.path.exists(outfile):
return True return True
try: try:
return self.storage.modified_time(infile) > self.storage.modified_time(outfile) return os.path.getmtime(infile) > os.path.getmtime(outfile)
except (OSError, NotImplementedError): except OSError:
return True return True
......
...@@ -54,29 +54,66 @@ class PipelineMixin(object): ...@@ -54,29 +54,66 @@ class PipelineMixin(object):
pass pass
def render_compressed(self, package, package_name, package_type): def render_compressed(self, package, package_name, package_type):
"""Render HTML for the package.
If ``PIPELINE_ENABLED`` is ``True``, this will render the package's
output file (using :py:meth:`render_compressed_output`). Otherwise,
this will render the package's source files (using
:py:meth:`render_compressed_sources`).
Subclasses can override this method to provide custom behavior for
determining what to render.
"""
if settings.PIPELINE_ENABLED: if settings.PIPELINE_ENABLED:
method = getattr(self, "render_{0}".format(package_type)) return self.render_compressed_output(package, package_name,
return method(package, package.output_filename) package_type)
else: else:
if settings.PIPELINE_COLLECTOR_ENABLED: return self.render_compressed_sources(package, package_name,
default_collector.collect(self.request) package_type)
def render_compressed_output(self, package, package_name, package_type):
"""Render HTML for using the package's output file.
Subclasses can override this method to provide custom behavior for
rendering the output file.
"""
method = getattr(self, 'render_{0}'.format(package_type))
return method(package, package.output_filename)
def render_compressed_sources(self, package, package_name, package_type):
"""Render HTML for using the package's list of source files.
packager = Packager() Each source file will first be collected, if
method = getattr(self, "render_individual_{0}".format(package_type)) ``PIPELINE_COLLECTOR_ENABLED`` is ``True``.
If there are any errors compiling any of the source files, an
``SHOW_ERRORS_INLINE`` is ``True``, those errors will be shown at
the top of the page.
Subclasses can override this method to provide custom behavior for
rendering the source files.
"""
if settings.PIPELINE_COLLECTOR_ENABLED:
default_collector.collect(self.request)
packager = Packager()
method = getattr(self, 'render_individual_{0}'.format(package_type))
try:
paths = packager.compile(package.paths)
except CompilerError as e:
if settings.SHOW_ERRORS_INLINE:
method = getattr(self, 'render_error_{0}'.format(
package_type))
try: return method(package_name, e)
paths = packager.compile(package.paths) else:
except CompilerError as e: raise
if settings.SHOW_ERRORS_INLINE:
method = getattr(self, 'render_error_{0}'.format(
package_type))
return method(package_name, e) templates = packager.pack_templates(package)
else:
raise
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): def render_error(self, package_type, package_name, e):
return render_to_string('pipeline/compile_error.html', Context({ return render_to_string('pipeline/compile_error.html', Context({
......
from __future__ import unicode_literals
from django.conf import settings as django_settings
from django.core.exceptions import ImproperlyConfigured
from django.views.static import serve
from .collector import default_collector
from .conf import settings
def serve_static(request, path, insecure=False, **kwargs):
"""Collect and serve static files.
This view serves up static files, much like Django's
:py:func:`~django.views.static.serve` view, with the addition that it
collects static files first (if enabled). This allows images, fonts, and
other assets to be served up without first loading a page using the
``{% javascript %}`` or ``{% stylesheet %}`` template tags.
You can use this view by adding the following to any :file:`urls.py`::
urlpatterns += static('static/', view='pipeline.views.serve_static')
"""
# Follow the same logic Django uses for determining access to the
# static-serving view.
if not django_settings.DEBUG and not insecure:
raise ImproperlyConfigured("The staticfiles view can only be used in "
"debug mode or if the --insecure "
"option of 'runserver' is used")
if not settings.PIPELINE_ENABLED and settings.PIPELINE_COLLECTOR_ENABLED:
# Collect only the requested file, in order to serve the result as
# fast as possible. This won't interfere with the template tags in any
# way, as those will still cause Django to collect all media.
default_collector.collect(request, files=[path])
return serve(request, path, document_root=django_settings.STATIC_ROOT,
**kwargs)
...@@ -10,7 +10,7 @@ if (sys.version_info[0], sys.version_info[1]) < (3, 2): ...@@ -10,7 +10,7 @@ if (sys.version_info[0], sys.version_info[1]) < (3, 2):
setup( setup(
name='django-pipeline', name='django-pipeline',
version='1.6.6', version='1.6.7',
description='Pipeline is an asset packaging library for Django.', description='Pipeline is an asset packaging library for Django.',
long_description=io.open('README.rst', encoding='utf-8').read() + '\n\n' + long_description=io.open('README.rst', encoding='utf-8').read() + '\n\n' +
io.open('HISTORY.rst', encoding='utf-8').read(), io.open('HISTORY.rst', encoding='utf-8').read(),
......
...@@ -7,6 +7,7 @@ if sys.platform.startswith('win'): ...@@ -7,6 +7,7 @@ if sys.platform.startswith('win'):
os.environ.setdefault('NUMBER_OF_PROCESSORS', '1') os.environ.setdefault('NUMBER_OF_PROCESSORS', '1')
from .test_collector import *
from .test_compiler import * from .test_compiler import *
from .test_compressor import * from .test_compressor import *
from .test_template import * from .test_template import *
...@@ -15,3 +16,4 @@ from .test_middleware import * ...@@ -15,3 +16,4 @@ from .test_middleware import *
from .test_packager import * from .test_packager import *
from .test_storage import * from .test_storage import *
from .test_utils import * from .test_utils import *
from .test_views import *
from __future__ import unicode_literals
import os
from django.contrib.staticfiles import finders
from django.test import TestCase
from pipeline.collector import default_collector
from pipeline.finders import PipelineFinder
class CollectorTest(TestCase):
def tearDown(self):
super(CollectorTest, self).tearDown()
default_collector.clear()
def test_collect(self):
self.assertEqual(
set(default_collector.collect()),
set(self._get_collectable_files()))
def test_collect_with_files(self):
self.assertEqual(
set(default_collector.collect(files=[
'pipeline/js/first.js',
'pipeline/js/second.js',
])),
set([
'pipeline/js/first.js',
'pipeline/js/second.js',
]))
def _get_collectable_files(self):
for finder in finders.get_finders():
if not isinstance(finder, PipelineFinder):
for path, storage in finder.list(['CVS', '.*', '*~']):
if getattr(storage, 'prefix', None):
yield os.path.join(storage.prefix, path)
else:
yield path
from __future__ import unicode_literals
from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.exceptions import ImproperlyConfigured
from django.http import Http404
from django.test import RequestFactory, TestCase
from django.test.utils import override_settings
from pipeline.collector import default_collector
from pipeline.views import serve_static
from tests.utils import pipeline_settings
@override_settings(DEBUG=True)
@pipeline_settings(PIPELINE_COLLECTOR_ENABLED=True, PIPELINE_ENABLED=False)
class ServeStaticViewsTest(TestCase):
def setUp(self):
super(ServeStaticViewsTest, self).setUp()
self.filename = 'pipeline/js/first.js'
self.storage = staticfiles_storage
self.request = RequestFactory().get('/static/%s' % self.filename)
default_collector.clear()
def tearDown(self):
super(ServeStaticViewsTest, self).tearDown()
default_collector.clear()
staticfiles_storage._setup()
def test_found(self):
self._test_found()
def test_not_found(self):
self._test_not_found('missing-file')
@override_settings(DEBUG=False)
def test_debug_false(self):
with self.assertRaises(ImproperlyConfigured):
serve_static(self.request, self.filename)
self.assertFalse(self.storage.exists(self.filename))
@override_settings(DEBUG=False)
def test_debug_false_and_insecure(self):
self._test_found(insecure=True)
@pipeline_settings(PIPELINE_ENABLED=True)
def test_pipeline_enabled_and_found(self):
self._write_content()
self._test_found()
@pipeline_settings(PIPELINE_ENABLED=True)
def test_pipeline_enabled_and_not_found(self):
self._test_not_found(self.filename)
@pipeline_settings(PIPELINE_COLLECTOR_ENABLED=False)
def test_collector_disabled_and_found(self):
self._write_content()
self._test_found()
@pipeline_settings(PIPELINE_COLLECTOR_ENABLED=False)
def test_collector_disabled_and_not_found(self):
self._test_not_found(self.filename)
def _write_content(self, content='abc123'):
"""Write sample content to the test static file."""
with self.storage.open(self.filename, 'w') as f:
f.write(content)
def _test_found(self, **kwargs):
"""Test that a file can be found and contains the correct content."""
response = serve_static(self.request, self.filename, **kwargs)
self.assertEqual(response.status_code, 200)
self.assertTrue(self.storage.exists(self.filename))
if hasattr(response, 'streaming_content'):
content = b''.join(response.streaming_content)
else:
content = response.content
with self.storage.open(self.filename) as f:
self.assertEqual(f.read(), content)
def _test_not_found(self, filename):
"""Test that a file could not be found."""
self.assertFalse(self.storage.exists(filename))
with self.assertRaises(Http404):
serve_static(self.request, filename)
self.assertFalse(self.storage.exists(filename))
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