mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-02-05 10:53:28 -05:00
Merge pull request #171 from alphagov/add-cachebusting-for-assets
Make URLs for assets cache-proof
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
46
app/asset_fingerprinter.py
Normal file
46
app/asset_fingerprinter.py
Normal 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
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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'
|
||||
|
||||
95
tests/app/main/test_asset_fingerprinter.py
Normal file
95
tests/app/main/test_asset_fingerprinter.py
Normal 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 = 'Ralph’s apostrophe'
|
||||
AssetFingerprinter(filesystem_path='tests/app/main/').get_url('test_asset_fingerprinter.py')
|
||||
Reference in New Issue
Block a user