Skip to content

Commit

Permalink
Merge pull request #162 from ppfeufer/integrity-hash-calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
ppfeufer authored Jan 21, 2025
2 parents 828ad17 + 3c5b789 commit 81fdd17
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 50 deletions.
6 changes: 6 additions & 0 deletions timezones/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
Constants
"""

# Standard Library
import os

AA_TIMEZONES_BASE_DIR = os.path.join(os.path.dirname(__file__))
AA_TIMEZONES_STATIC_DIR = os.path.join(AA_TIMEZONES_BASE_DIR, "static", "timezones")

AA_TIMEZONE_DEFAULT_PANELS: list[dict[str, str | dict[str, str]]] = [
{
"panel_name": "US / Pacific",
Expand Down
Empty file added timezones/helper/__init__.py
Empty file.
40 changes: 40 additions & 0 deletions timezones/helper/static_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""
Helper functions for static integrity calculations
"""

# Standard Library
import os
from pathlib import Path

# Third Party
from sri import Algorithm, calculate_integrity

# Alliance Auth
from allianceauth.services.hooks import get_extension_logger

# Alliance Auth (External Libs)
from app_utils.logging import LoggerAddTag

# AA Time Zones
from timezones import __title__
from timezones.constants import AA_TIMEZONES_STATIC_DIR

logger = LoggerAddTag(my_logger=get_extension_logger(__name__), prefix=__title__)


def calculate_integrity_hash(relative_file_path: str) -> str:
"""
Calculates the integrity hash for a given static file
:param self:
:type self:
:param relative_file_path: The file path relative to the `aa-timezones/timezones/static/timezones` folder
:type relative_file_path: str
:return: The integrity hash
:rtype: str
"""

file_path = os.path.join(AA_TIMEZONES_STATIC_DIR, relative_file_path)
integrity_hash = calculate_integrity(Path(file_path), Algorithm.SHA512)

return integrity_hash
7 changes: 1 addition & 6 deletions timezones/templates/timezones/bundles/aa-timezones-css.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
{% load timezones %}

<link
rel="stylesheet"
href="{% timezones_static 'timezones/css/timezones.min.css' %}"
integrity="sha512-8lYk4QlNTYSaxTlTWH4Z6Qslhf+NrgoxzD78IzMN+//b9WwMdQWzFWWrqX5dl7z/jQHU1hg38fYrm2F0+1KVEg=="
crossorigin="anonymous"
>
{% timezones_static 'css/timezones.min.css' %}
6 changes: 1 addition & 5 deletions timezones/templates/timezones/bundles/aa-timezones-js.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
{% load timezones %}

<script
src="{% timezones_static 'timezones/js/timezones.min.js' %}"
integrity="sha512-p/T1Wmq0lWiYMNBbODA7yXFsFtCzugX8mEsh9Z9S8rlMP3sx6GRvP3q4mhjnnk3Y8h76HaGcKh6kJzV1QW/ZmA=="
crossorigin="anonymous"
></script>
{% timezones_static 'js/timezones.min.js' %}
8 changes: 2 additions & 6 deletions timezones/templates/timezones/bundles/jquery-timeago-js.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
{% load static %}
{% load timezones %}

<script
src="{% static 'timezones/libs/jquery-timeago/1.6.7/jquery.timeago.min.js' %}"
integrity="sha512-Fa5vf0oXzZKvD2EWgLQaebZ7X+7IMDAbdCvOjEivYkFLc830J6x7jYy7N9g+Rz0rVx4UZA7xSUboc14jSrsKVA=="
crossorigin="anonymous"
></script>
{% timezones_static 'libs/jquery-timeago/1.6.7/jquery.timeago.min.js' %}
8 changes: 2 additions & 6 deletions timezones/templates/timezones/bundles/moment-timezone-js.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
{% load static %}
{% load timezones %}

<script
src="{% static 'timezones/libs/moment-timezone/0.5.36/moment-timezone-with-data-1970-2030.min.js' %}"
integrity="sha512-HdWVl8NTC8n0bl0Ypuw1baRuZvO4bUcnGWhLTiI9yiisXzHbgKxlrmf2AMaa3yaXsi8GVzwonsASxsLs7ffAFw=="
crossorigin="anonymous"
></script>
{% timezones_static 'libs/moment-timezone/0.5.36/moment-timezone-with-data-1970-2030.min.js' %}
9 changes: 2 additions & 7 deletions timezones/templates/timezones/bundles/weather-icons-css.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
{% load static %}
{% load timezones %}

<link
rel="stylesheet"
href="{% static 'timezones/libs/weather-icons/2.0.10/css/weather-icons.min.css' %}"
integrity="sha512-DIFdHjUUNaRvJdlgqogTC0kqKpSBBLaa/4cFiwj/WJKaAQCJLBmEuRHddEeJOiTJ1hynZ6/6ZH4ouPhcep+E+g=="
crossorigin="anonymous"
>
{% timezones_static 'libs/weather-icons/2.0.10/css/weather-icons.min.css' %}
67 changes: 58 additions & 9 deletions timezones/templatetags/timezones.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,74 @@
Versioned static URLs to break browser caches when changing the app version
"""

# Standard Library
import os

# Django
from django.conf import settings
from django.template.defaulttags import register
from django.templatetags.static import static
from django.utils.safestring import mark_safe

# Alliance Auth
from allianceauth.services.hooks import get_extension_logger

# Alliance Auth (External Libs)
from app_utils.logging import LoggerAddTag

# AA Time Zones
from timezones import __version__
from timezones import __title__, __version__
from timezones.helper.static_files import calculate_integrity_hash

logger = LoggerAddTag(my_logger=get_extension_logger(__name__), prefix=__title__)


@register.simple_tag
def timezones_static(path: str) -> str:
def timezones_static(relative_file_path: str) -> str | None:
"""
Versioned static URL
:param path:
:type path:
:return:
:rtype:
:param relative_file_path: The file path relative to the `aa-timezones/timezones/static/timezones` folder
:type relative_file_path: str
:return: Versioned static URL
:rtype: str
"""

static_url = static(path)
versioned_url = static_url + "?v=" + __version__
logger.debug(f"Getting versioned static URL for: {relative_file_path}")

file_type = os.path.splitext(relative_file_path)[1][1:]

logger.debug(f"File extension: {file_type}")

# Only support CSS and JS files
if file_type not in ["css", "js"]:
raise ValueError(f"Unsupported file type: {file_type}")

static_file_path = os.path.join("timezones", relative_file_path)
static_url = static(static_file_path)

# Integrity hash calculation only for non-debug mode
sri_string = (
f' integrity="{calculate_integrity_hash(relative_file_path)}" crossorigin="anonymous"'
if not settings.DEBUG
else ""
)

# Versioned URL for CSS and JS files
# Add version query parameter to break browser caches when changing the app version
# Do not add version query parameter for libs as they are already versioned through their file path
versioned_url = (
static_url
if relative_file_path.startswith("libs/")
else static_url + "?v=" + __version__
)

# Return the versioned URL with integrity hash for CSS
if file_type == "css":
return mark_safe(f'<link rel="stylesheet" href="{versioned_url}"{sri_string}>')

# Return the versioned URL with integrity hash for JS files
if file_type == "js":
return mark_safe(f'<script src="{versioned_url}"{sri_string}></script>')

return versioned_url
return None
69 changes: 58 additions & 11 deletions timezones/tests/test_templatetags.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,84 @@
"""
Tests for our template tags
Test the apps' template tags
"""

# Django
from django.template import Context, Template
from django.test import TestCase
from django.test import TestCase, override_settings

# AA Time Zones
from timezones import __version__
from timezones.helper.static_files import calculate_integrity_hash


class TestVersionedStatic(TestCase):
"""
Test versioned static template tag
Test timezones_static template tag
"""

def test_versioned_static(self):
@override_settings(DEBUG=False)
def test_versioned_static_without_debug_enabled(self) -> None:
"""
Test versioned static template tag
Test versioned static template tag without DEBUG enabled
:return:
:rtype:
"""

context = Context({"version": __version__})
template_to_render = Template(
"{% load timezones %}"
"{% timezones_static 'timezones/css/timezones.min.css' %}"
template_string=(
"{% load timezones %}"
"{% timezones_static 'css/timezones.min.css' %}"
"{% timezones_static 'js/timezones.min.js' %}"
)
)

rendered_template = template_to_render.render(context)
rendered_template = template_to_render.render(context=context)

self.assertInHTML(
needle=f'/static/timezones/css/timezones.min.css?v={context["version"]}',
haystack=rendered_template,
expected_static_css_src = (
f'/static/timezones/css/timezones.min.css?v={context["version"]}'
)
expected_static_css_src_integrity = calculate_integrity_hash(
"css/timezones.min.css"
)
expected_static_js_src = (
f'/static/timezones/js/timezones.min.js?v={context["version"]}'
)
expected_static_js_src_integrity = calculate_integrity_hash(
"js/timezones.min.js"
)

self.assertIn(member=expected_static_css_src, container=rendered_template)
self.assertIn(
member=expected_static_css_src_integrity, container=rendered_template
)
self.assertIn(member=expected_static_js_src, container=rendered_template)
self.assertIn(
member=expected_static_js_src_integrity, container=rendered_template
)

@override_settings(DEBUG=True)
def test_versioned_static_with_debug_enabled(self) -> None:
"""
Test versioned static template tag with DEBUG enabled
:return:
:rtype:
"""

context = Context({"version": __version__})
template_to_render = Template(
template_string=(
"{% load timezones %}" "{% timezones_static 'css/timezones.min.css' %}"
)
)

rendered_template = template_to_render.render(context=context)

expected_static_css_src = (
f'/static/timezones/css/timezones.min.css?v={context["version"]}'
)

self.assertIn(member=expected_static_css_src, container=rendered_template)
self.assertNotIn(member="integrity=", container=rendered_template)

0 comments on commit 81fdd17

Please sign in to comment.