If you're reading this you're probably familiar with how, in django-pipeline, you define bundles of static files to be combined and served. If you're not familiar with django-pipeline
it's unlike this'll be of much help.
The Challenge (aka. the pitfall)
So you specify bundles by creating things in your settings.py
something like this:
PIPELINE = {
'STYLESHEETS': {
'colors': {
'source_filenames': (
'css/core.css',
'css/colors/*.css',
'css/layers.css'
),
'output_filename': 'css/colors.css',
'extra_context': {
'media': 'screen,projection',
},
},
},
'JAVASCRIPT': {
'stats': {
'source_filenames': (
'js/jquery.js',
'js/d3.js',
'js/collections/*.js',
'js/aplication.js',
),
'output_filename': 'js/stats.js',
}
}
}
You do a bit more configuration and now, when you run ./manage.py collectstatic --noinput
Django and django-pipeline
will gather up all static files from all Django apps installed, then start post processing then and doing things like concatenating them into one file and doing stuff like minification etc.
The problem is, if you look at the example snippet above, there's a typo. Instead of js/application.js
it's accidentally js/aplication.js
. Oh noes!!
What's sad is it that nobody will notice (running ./manage.py collectstatic
will exit with a 0
). At least not unless you do some careful manual reviewing. Perhaps you will notice later, when you've pushed the site to prod, that the output file js/stats.js
actually doesn't contain the code from js/application.js
.
Or, you can automate it!
A Solution (aka. the hack)
I started this work this morning because the error actually happened to us. Thankfully not in production but our staging server produced a rendered HTML page with <link href="/static/css/report.min.cd784b4a5e2d.css" rel="stylesheet" type="text/css" />
which was an actual file but it was 0 bytes.
It wasn't that hard to figure out what the problem was because of the context of recent changes but it would have been nice to catch this during continuous integration.
So what we did was add an extra class to settings.STATICFILES_FINDERS
called myproject.base.finders.LeftoverPipelineFinder
. So now it looks like this:
# in settings.py
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'pipeline.finders.PipelineFinder',
'myproject.finders.LeftoverPipelineFinder', # the new hotness!
)
And here's the class implementation:
from pipeline.finders import PipelineFinder
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
class LeftoverPipelineFinder(PipelineFinder):
"""This finder is expected to come AFTER
django.contrib.staticfiles.finders.FileSystemFinder and
django.contrib.staticfiles.finders.AppDirectoriesFinder in
settings.STATICFILES_FINDERS.
If a path is looked for here it means it's trying to find a file
that none of the regular staticfiles finders couldn't find.
"""
def find(self, path, all=False):
# Before we raise an error, try to find out where,
# in the bundles, this was defined. This will make it easier to correct
# the mistake.
for config_name in 'STYLESHEETS', 'JAVASCRIPT':
config = settings.PIPELINE[config_name]
for key in config:
if path in config[key]['source_filenames']:
raise ImproperlyConfigured(
'Static file {!r} can not be found anywhere. Defined in '
"PIPELINE[{!r}][{!r}]['source_filenames']".format(
path,
config_name,
key,
)
)
# If the file can't be found AND it's not in bundles, there's
# got to be something else really wrong.
raise NotImplementedError(path)
Now, if you have a typo or something in your bundles, you'll get a nice error about it as soon as you try to run collectstatic
. For example:
▶ ./manage.py collectstatic --noinput Post-processed 'css/search.min.css' as 'css/search.min.css' Post-processed 'css/base.min.css' as 'css/base.min.css' Post-processed 'css/base-dynamic.min.css' as 'css/base-dynamic.min.css' Post-processed 'js/google-analytics.min.js' as 'js/google-analytics.min.js' Traceback (most recent call last): ... django.core.exceptions.ImproperlyConfigured: Static file 'js/aplication.js' can not be found anywhere. Defined in PIPELINE['JAVASCRIPT']['stats']['source_filenames']
Final Thoughts
This was a morning hack. I'm still not entirely sure if this the best approach, but there was none better and the result is pretty good.
We run ./manage.py collectstatic --noinput
in our continous integration just before it runs ./manage.py test
. So if you make a Pull Request that has a typo in bundles.py
it will get caught.
Unfortunately, it won't find missing files if you use foo*.js
or something like that. django-pipeline
uses glob.glob
to convert expressions like that into a list of actual files and that depends on the filesystem and all of that happens before the django.contrib.staticfiles.finders.find
function is called.
If you have any better suggestions to solve this, please let me know.
Comments