Merge pull request #171 from alphagov/add-cachebusting-for-assets

Make URLs for assets cache-proof
This commit is contained in:
Rebecca Law
2016-02-12 09:48:38 +00:00
5 changed files with 151 additions and 6 deletions

View File

@@ -18,6 +18,7 @@ from app.notify_client.user_api_client import UserApiClient
from app.notify_client.job_api_client import JobApiClient
from app.notify_client.status_api_client import StatusApiClient
from app.its_dangerous_session import ItsdangerousSessionInterface
from app.asset_fingerprinter import AssetFingerprinter
import app.proxy_fix
from config import configs
from utils import logging
@@ -30,6 +31,7 @@ user_api_client = UserApiClient()
api_key_api_client = ApiKeyApiClient()
job_api_client = JobApiClient()
status_api_client = StatusApiClient()
asset_fingerprinter = AssetFingerprinter()
def create_app(config_name, config_overrides=None):
@@ -106,7 +108,8 @@ def init_app(app, config_overrides):
def inject_global_template_variables():
return {
'asset_path': '/static/',
'header_colour': app.config['HEADER_COLOUR']
'header_colour': app.config['HEADER_COLOUR'],
'asset_url': asset_fingerprinter.get_url
}

View File

@@ -0,0 +1,46 @@
import hashlib
import codecs
class AssetFingerprinter(object):
"""
Get a unique hash for an asset file, so that it doesn't stay cached
when it changes
Usage:
in the application
template_data.asset_fingerprinter = AssetFingerprinter()
where template data is how you pass variables to every template.
in template.html:
{{ asset_fingerprinter.get_url('stylesheets/application.css') }}
* 'app/static' is assumed to be the root for all asset files
"""
def __init__(self, asset_root='/static/', filesystem_path='app/static/'):
self._cache = {}
self._asset_root = asset_root
self._filesystem_path = filesystem_path
def get_url(self, asset_path):
if asset_path not in self._cache:
self._cache[asset_path] = (
self._asset_root +
asset_path +
'?' +
self.get_asset_fingerprint(self._filesystem_path + asset_path)
)
return self._cache[asset_path]
def get_asset_fingerprint(self, asset_file_path):
return hashlib.md5(
self.get_asset_file_contents(asset_file_path).encode('utf-8')
).hexdigest()
def get_asset_file_contents(self, asset_file_path):
with codecs.open(asset_file_path, encoding='utf-8') as asset_file:
contents = asset_file.read()
return contents

View File

@@ -3,19 +3,19 @@
{% block head %}
<!--[if gt IE 8]><!-->
<link rel="stylesheet" media="screen" href="{{ asset_path }}stylesheets/main.css" />
<link rel="stylesheet" media="screen" href="{{ asset_url('stylesheets/main.css') }}" />
<!--<![endif]-->
<style>
#global-header-bar { background-color: {{header_colour}} }
</style>
<!--[if IE 6]>
<link rel="stylesheet" media="screen" href="{{ asset_path }}/stylesheets/main-ie6.css" />
<link rel="stylesheet" media="screen" href="{{ asset_url('stylesheets/main-ie6.css') }}" />
<![endif]-->
<!--[if IE 7]>
<link rel="stylesheet" media="screen" href="{{ asset_path }}/stylesheets/main-ie7.css" />
<link rel="stylesheet" media="screen" href="{{ asset_url('stylesheets/main-ie7.css') }}" />
<![endif]-->
<!--[if IE 8]>
<link rel="stylesheet" media="screen" href="{{ asset_path }}/stylesheets/main-ie8.css" />
<link rel="stylesheet" media="screen" href="{{ asset_url('stylesheets/main-ie8.css') }}" />
<![endif]-->
{% endblock %}
@@ -69,5 +69,5 @@
{% endblock %}
{% block body_end %}
<script type="text/javascript" src="{{ asset_path }}javascripts/all.js" /></script>
<script type="text/javascript" src="{{ asset_url('javascripts/all.js') }}" /></script>
{% endblock %}

View File

@@ -5,6 +5,7 @@ class Config(object):
DEBUG = False
ASSETS_DEBUG = False
cache = False
SEND_FILE_MAX_AGE_DEFAULT = 365 * 24 * 60 * 60 # 1 year
manifest = True
NOTIFY_LOG_LEVEL = 'DEBUG'

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')