Skip to content
This repository has been archived by the owner on Oct 23, 2019. It is now read-only.

Commit

Permalink
Add templatetag and staticfiles storage for Django
Browse files Browse the repository at this point in the history
  • Loading branch information
Rocky Meza committed May 11, 2015
1 parent 7130408 commit 9c1ec07
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 10 deletions.
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Documentation
- [Installation](#installation)
- [Settings](#settings)
- [Settings for Django projects](#settings-for-django-projects)
- [Extras for Django](#extras-for-django)
- [Usage](#usage)
- [Running the tests](#running-the-tests)

Expand Down Expand Up @@ -203,6 +204,45 @@ For example, `webpack('my_app/webpack.config.js')` could match a file within an
such as `my_app/static/my_app/webpack.config.js`.


Extras for Django
-----------------

python-webpack also provides a template tag and storage backend for compiling
during collectstatic.

You can use the template tag like this:

```html+django
{% load webpack %}
{% webpack 'path/to/webpack.config.js' %}
```

If you wish to pre-compile your webpack bundles during `collecstatic`, you can
use the special storage backend.

```python
# settings.py

WEBPACK = {
# ...

# defines whether or not we should compile during collectstatic
'COMPILE_OFFLINE': True

# a list of all webpack configs to compile during collectstatic
'OFFLINE_BUNDLES': [
'path/to/webpack.config.js',
]
}

STATICFILES_STORAGE = 'webpack.django_integration.WebpackOfflineStaticFilesStorage'
```

If `COMPILE_OFFLINE` is set to `True`, the template tag will check the
pre-compiled bundles. Otherwise, it will compile the files during the request.


Usage
-----

Expand Down
7 changes: 6 additions & 1 deletion tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
'webpack.django_integration.WebpackFinder',
)

STATICFILES_DIRS = (
# Give Django access the the bundles directory.
os.path.join(BASE_DIR, 'bundles'),
)

WEBPACK = {
'BUNDLE_ROOT': os.path.join(BASE_DIR, '__BUNDLE_ROOT__'),
'BUNDLE_URL': '/static/',
Expand All @@ -32,4 +37,4 @@
# Let the manager spin up an instance
'USE_MANAGER': True,
'VERBOSITY': verbosity.SILENT,
}
}
2 changes: 1 addition & 1 deletion tests/test_bundles.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,4 @@ def test_bundle_can_expose_its_library_config(self):
self.assertEqual(bundle.get_library(), 'LIBRARY_TEST')
self.assertEqual(bundle.get_var(), 'LIBRARY_TEST')
bundle = webpack(PATH_TO_MULTIPLE_BUNDLES_CONFIG)
self.assertIsNone(bundle.get_library())
self.assertIsNone(bundle.get_library())
27 changes: 22 additions & 5 deletions tests/test_django_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
TEST_ROOT = os.path.dirname(__file__)
BUNDLES = os.path.join(TEST_ROOT, 'bundles',)

PATH_TO_BASIC_CONFIG = os.path.join(BUNDLES, 'basic', 'webpack.config.js')
PATH_TO_LIBRARY_CONFIG = os.path.join(BUNDLES, 'library', 'webpack.config.js')
PATH_TO_MULTIPLE_BUNDLES_CONFIG = os.path.join(BUNDLES, 'multiple_bundles', 'webpack.config.js')
PATH_TO_MULTIPLE_ENTRY_CONFIG = os.path.join(BUNDLES, 'multiple_entry', 'webpack.config.js')
PATH_TO_BASIC_CONFIG = 'basic/webpack.config.js'
PATH_TO_LIBRARY_CONFIG = 'library/webpack.config.js'
PATH_TO_MULTIPLE_BUNDLES_CONFIG = 'multiple_bundles/webpack.config.js'
PATH_TO_MULTIPLE_ENTRY_CONFIG = 'multiple_entry/webpack.config.js'


class TestDjangoIntegration(unittest.TestCase):
Expand All @@ -26,6 +26,10 @@ def setUpClass(cls):
def tearDownClass(cls):
clean_bundle_root()

def render_template(self, template, **context):
from django.template import Template, Context
return Template(template).render(Context(context))

def test_bundle_can_resolve_files_via_the_django_static_file_finder(self):
bundle = webpack('django_test_app/webpack.config.js')
assets = bundle.get_assets()
Expand All @@ -39,4 +43,17 @@ def test_bundle_urls_can_be_resolved_via_the_static_file_finder_used_by_the_dev_
assets = bundle.get_assets()
self.assertTrue(len(assets), 1)
relative_url = assets[0]['url'].split('/static/')[-1]
self.assertEqual(staticfiles.find(relative_url), assets[0]['path'])
self.assertEqual(staticfiles.find(relative_url), assets[0]['path'])

def test_templatetag_basic(self):
template = "{% load webpack %}{% webpack path_to_config %}"
rendered = self.render_template(template,
path_to_config=PATH_TO_BASIC_CONFIG)
self.assertIn('710e9657b7951fbc79b6.js', rendered)

def test_templatetag_multiple(self):
template = "{% load webpack %}{% webpack path_to_config %}"
rendered = self.render_template(template,
path_to_config=PATH_TO_MULTIPLE_BUNDLES_CONFIG)
self.assertIn('bundle_1.js', rendered)
self.assertIn('bundle_2.js', rendered)
6 changes: 5 additions & 1 deletion webpack/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ class Conf(conf.Conf):

JS_HOST_FUNCTION = 'webpack'

COMPILE_OFFLINE = False

OFFLINE_BUNDLES = []

def configure(self, **kwargs):
if kwargs.get('BUNDLE_URL', None) and not kwargs['BUNDLE_URL'].endswith('/'):
raise ImproperlyConfigured('`BUNDLE_URL` must have a trailing slash')
Expand All @@ -44,4 +48,4 @@ def get_path_to_bundle_dir(self):
def get_path_to_config_dir(self):
return os.path.join(self.get_path_to_output_dir(), self.CONFIG_DIR)

settings = Conf()
settings = Conf()
100 changes: 99 additions & 1 deletion webpack/django_integration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import json
from collections import OrderedDict

from django.core.files.base import ContentFile
from django.core.files.storage import FileSystemStorage
from django.contrib.staticfiles.finders import BaseStorageFinder
from django.contrib.staticfiles.storage import StaticFilesStorage

from .conf import settings
from .compiler import webpack

# XXX: We need to ensure that the configuration is triggered, if we in a
# management command (like collectstatic), it is possible that we don't get
# configured.
from js_host import models # NOQA
from . import models # NOQA


class WebpackFileStorage(FileSystemStorage):
Expand All @@ -25,4 +38,89 @@ class WebpackFinder(BaseStorageFinder):
storage = WebpackFileStorage

def list(self, *args, **kwargs):
return []
return []


class WebpackOfflineStorageMixin(object):
"""
A StaticFilesStorage mixin similar inspired by ManifestFilesMixin. It will
compile all of the webpack bundles specified by the WEBPACK_OFFLINE_BUNDLES
setting and then stores them in a manifest file to be read by the webpack
templatetag.
Usage:
# myproject/storage.py
from django.contrib.staticfiles.storage import StaticFilesStorage
class MyStaticFilesStorage(WebpackOfflineStorageMixin, StaticFilesStorage):
pass
# settings.py
WEBPACK = {
# ...
'COMPILE_OFFLINE': True,
'OFFLINE_BUNDLES': [
'path/to/webpack.config.js'
]
}
"""
webpack_bundle_stats_path = 'webpack-bundles.json'
webpack_bundle_stats_version = '1.0'

def load_webpack_bundle_stats(self):
content = self._read_webpack_bundle_stats()
if content is None:
return OrderedDict()
try:
stored = json.loads(content, object_pairs_hook=OrderedDict)
except ValueError:
pass
else:
version = stored.get('version', None)
if version == self.webpack_bundle_stats_version:
return stored.get('stats', OrderedDict())
raise ValueError("Couldn't load webpack bundle stats '%s' (version %s)" %
(self.webpack_bundle_stats_path, self.webpack_bundle_stats_version))

def post_process(self, paths, dry_run=False, **kwargs):
if dry_run or not settings.COMPILE_OFFLINE:
return

bundle_stats = OrderedDict()

for config_path in settings.OFFLINE_BUNDLES:
bundle = webpack(config_path)
bundle_stats[config_path] = bundle.stats

output_paths = ','.join(
asset['path'] for asset in bundle.get_assets()
)
yield config_path, output_paths, True

self._save_webpack_bundle_stats(bundle_stats)

# Call super if it exists.
sup = super(WebpackOfflineStorageMixin, self)
if hasattr(sup, 'post_process'):
processed_files = sup.post_process(paths, dry_run, **kwargs)
for name, hashed_name, processed in processed_files:
yield name, hashed_name, processed

def _read_webpack_bundle_stats(self):
try:
with self.open(self.webpack_bundle_stats_path) as bundle_stats:
return bundle_stats.read().decode('utf-8')
except IOError:
return None

def _save_webpack_bundle_stats(self, bundle_stats):
payload = {'stats': bundle_stats, 'version': self.webpack_bundle_stats_version}
if self.exists(self.webpack_bundle_stats_path):
self.delete(self.webpack_bundle_stats_path)
contents = json.dumps(payload).encode('utf-8')
self._save(self.webpack_bundle_stats_path, ContentFile(contents))


class WebpackOfflineStaticFilesStorage(WebpackOfflineStorageMixin, StaticFilesStorage):
pass
2 changes: 1 addition & 1 deletion webpack/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@

webpack.conf.settings.configure(
**getattr(settings, 'WEBPACK', {})
)
)
Empty file.
58 changes: 58 additions & 0 deletions webpack/templatetags/webpack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from __future__ import absolute_import

import logging

from django.template import Library
from django.core.exceptions import ImproperlyConfigured
from django.contrib.staticfiles.storage import staticfiles_storage

from ..conf import settings
from ..compiler import webpack, WebpackBundle
from ..exceptions import BundlingError

register = Library()

logger = logging.getLogger(__name__)


@register.simple_tag(name='webpack')
def do_webpack(path_to_config):
"""
A template tag that will output a webpack bundle. To be used in combination
with the OfflineWebpackStorageMixin.
Usage:
{% load webpack %}
{% webpack 'path/to/webpack.config.js' %}
"""
if settings.COMPILE_OFFLINE:
if not hasattr(staticfiles_storage, 'load_webpack_bundle_stats'):
raise ImproperlyConfigured(
"Your STATICFILES_STORAGE is not a subclass of"
" OfflineWebpackStorageMixin. Please make sure to use it or"
" implement the load_bundle_stats method on your class."
)
elif path_to_config not in settings.OFFLINE_BUNDLES:
raise ImproperlyConfigured(
"'{}' was not found in the webpack bundle stats manifest, did"
" you include it in WEBPACK_OFFLINE_BUNDLES?".format(path_to_config)
)
stats = staticfiles_storage.load_webpack_bundle_stats()
if stats is None:
raise ImproperlyConfigured(
"Could not find the webpack bundle stats manifest, did you"
" run collectstatic?"
)
elif path_to_config not in stats:
raise BundlingError(
"Could not find the webpack bundle stats manifest, I'm not"
" sure what the error was. Please run collectstatic again,"
" if the problem persists, report a bug."
)
# Re-instantiate from the cache.
bundle = WebpackBundle(stats[path_to_config])
else:
bundle = webpack(path_to_config)

return bundle.render()

0 comments on commit 9c1ec07

Please sign in to comment.