Make URLs for assets cache-proof

https://www.pivotaltracker.com/story/show/113448149

This commit adds a query string to assets URLs which is generated from a hash
of the file contents. When asset files are changed they will now be served from
a different URL, which means they wont be loaded from browser cache.

This is similar to how GOV.UK template adds its version number as a querystring
parameter for its assets.

This is mostly copied from Digital Marketplace utils:
https://github.com/alphagov/digitalmarketplace-utils/pull/102

They have it in a shared codebase, we only have one frontend app so don’t need
to do that.

Usage in a template:
``` jinja
{{ asset_fingerprinter.get_url('stylesheets/application.css') }}
```

Output:
```
static/stylesheets/application.css?418e6f4a6cdf1142e45c072ed3e1c90a
```
This commit is contained in:
Chris Hill-Scott
2016-02-10 15:47:00 +00:00
parent 5fd8ca492d
commit 2f0cc99610
4 changed files with 150 additions and 6 deletions

View File

@@ -0,0 +1,95 @@
# coding=utf-8
import os
from unittest import mock
from app.asset_fingerprinter import AssetFingerprinter
@mock.patch.object(AssetFingerprinter, 'get_asset_file_contents')
class TestAssetFingerprint(object):
def test_url_format(self, get_file_content_mock):
get_file_content_mock.return_value = """
body {
font-family: nta;
}
"""
asset_fingerprinter = AssetFingerprinter(
asset_root='/suppliers/static/'
)
assert (
asset_fingerprinter.get_url('application.css') ==
'/suppliers/static/application.css?418e6f4a6cdf1142e45c072ed3e1c90a' # noqa
)
assert (
asset_fingerprinter.get_url('application-ie6.css') ==
'/suppliers/static/application-ie6.css?418e6f4a6cdf1142e45c072ed3e1c90a' # noqa
)
def test_building_file_path(self, get_file_content_mock):
get_file_content_mock.return_value = """
document.write('Hello world!');
"""
fingerprinter = AssetFingerprinter()
fingerprinter.get_url('javascripts/application.js')
fingerprinter.get_asset_file_contents.assert_called_with(
'app/static/javascripts/application.js'
)
def test_hashes_are_consistent(self, get_file_content_mock):
get_file_content_mock.return_value = """
body {
font-family: nta;
}
"""
asset_fingerprinter = AssetFingerprinter()
assert (
asset_fingerprinter.get_asset_fingerprint('application.css') ==
asset_fingerprinter.get_asset_fingerprint('same_contents.css')
)
def test_hashes_are_different_for_different_files(
self, get_file_content_mock
):
asset_fingerprinter = AssetFingerprinter()
get_file_content_mock.return_value = """
body {
font-family: nta;
}
"""
css_hash = asset_fingerprinter.get_asset_fingerprint('application.css')
get_file_content_mock.return_value = """
document.write('Hello world!');
"""
js_hash = asset_fingerprinter.get_asset_fingerprint('application.js')
assert (
js_hash != css_hash
)
def test_hash_gets_cached(self, get_file_content_mock):
get_file_content_mock.return_value = """
body {
font-family: nta;
}
"""
fingerprinter = AssetFingerprinter()
assert (
fingerprinter.get_url('application.css') ==
'/static/application.css?418e6f4a6cdf1142e45c072ed3e1c90a'
)
fingerprinter._cache[
'application.css'
] = 'a1a1a1'
assert (
fingerprinter.get_url('application.css') ==
'a1a1a1'
)
fingerprinter.get_asset_file_contents.assert_called_once_with(
'app/static/application.css'
)
class TestAssetFingerprintWithUnicode(object):
def test_can_read_self(self):
string_with_unicode_character = 'Ralphs apostrophe'
AssetFingerprinter(filesystem_path='tests/app/main/').get_url('test_asset_fingerprinter.py')