Skip to content

Commit

Permalink
Merge pull request #158 from ppfeufer/django-sri
Browse files Browse the repository at this point in the history
  • Loading branch information
ppfeufer authored Feb 2, 2025
2 parents 15d8eae + 971284a commit 73f3c64
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 61 deletions.
71 changes: 40 additions & 31 deletions .make/conf.d/django.mk
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
pot:
@echo "Creating or updating .pot file …"
@django-admin makemessages \
-l en \
--locale en \
--keep-pot \
--ignore 'build/*'
--ignore 'build/*' \
--ignore 'node_modules/*' \
--ignore 'testauth/*' \
--ignore 'runtests.py'
@current_app_version=$$(pip show $(appname) | grep 'Version: ' | awk '{print $$NF}'); \
sed -i "/\"Project-Id-Version: /c\\\"Project-Id-Version: $(appname_verbose) $$current_app_version\\\n\"" $(translation_template); \
sed -i "/\"Report-Msgid-Bugs-To: /c\\\"Report-Msgid-Bugs-To: $(git_repository_issues)\\\n\"" $(translation_template);
Expand All @@ -18,9 +21,12 @@ add_translation:
@echo "Adding a new translation"
@read -p "Enter the language code (e.g. 'en_GB'): " language_code; \
django-admin makemessages \
-l $$language_code \
--locale $$language_code \
--keep-pot \
--ignore 'build/*'; \
--ignore 'build/*' \
--ignore 'node_modules/*' \
--ignore 'testauth/*' \
--ignore 'runtests.py'; \
current_app_version=$$(pip show $(appname) | grep 'Version: ' | awk '{print $$NF}'); \
sed -i "/\"Project-Id-Version: /c\\\"Project-Id-Version: $(appname_verbose) $$current_app_version\\\n\"" $(translation_template); \
sed -i "/\"Report-Msgid-Bugs-To: /c\\\"Report-Msgid-Bugs-To: $(git_repository_issues)\\\n\"" $(translation_template); \
Expand All @@ -34,21 +40,24 @@ add_translation:
translations:
@echo "Creating or updating translation files"
@django-admin makemessages \
-l cs_CZ \
-l de \
-l es \
-l fr_FR \
-l it_IT \
-l ja \
-l ko_KR \
-l nl_NL \
-l pl_PL \
-l ru \
-l sk \
-l uk \
-l zh_Hans \
--locale cs_CZ \
--locale de \
--locale es \
--locale fr_FR \
--locale it_IT \
--locale ja \
--locale ko_KR \
--locale nl_NL \
--locale pl_PL \
--locale ru \
--locale sk \
--locale uk \
--locale zh_Hans \
--keep-pot \
--ignore 'build/*'
--ignore 'build/*' \
--ignore 'node_modules/*' \
--ignore 'testauth/*' \
--ignore 'runtests.py'
@current_app_version=$$(pip show $(appname) | grep 'Version: ' | awk '{print $$NF}'); \
sed -i "/\"Project-Id-Version: /c\\\"Project-Id-Version: $(appname_verbose) $$current_app_version\\\n\"" $(translation_template); \
sed -i "/\"Report-Msgid-Bugs-To: /c\\\"Report-Msgid-Bugs-To: $(git_repository_issues)\\\n\"" $(translation_template); \
Expand All @@ -69,19 +78,19 @@ translations:
compile_translations:
@echo "Compiling translation files"
@django-admin compilemessages \
-l cs_CZ \
-l de \
-l es \
-l fr_FR \
-l it_IT \
-l ja \
-l ko_KR \
-l nl_NL \
-l pl_PL \
-l ru \
-l sk \
-l uk \
-l zh_Hans
--locale cs_CZ \
--locale de \
--locale es \
--locale fr_FR \
--locale it_IT \
--locale ja \
--locale ko_KR \
--locale nl_NL \
--locale pl_PL \
--locale ru \
--locale sk \
--locale uk \
--locale zh_Hans

# Migrate all database changes
.PHONY: migrate
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ Section Order:

### Changed

- Use `django-sri` for sri hashes
- Material Icon font updated to v143
- Minimum requirements
- Alliance Auth >= 4.6.0

## [2.2.4] - 2025-01-13

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
[![Automated Checks](https://github.com/ppfeufer/aa-sov-timer/actions/workflows/automated-checks.yml/badge.svg "Automated Checks")](https://github.com/ppfeufer/aa-sov-timer/actions/workflows/automated-checks.yml)
[![codecov](https://codecov.io/gh/ppfeufer/aa-sov-timer/branch/master/graph/badge.svg?token=J9PBF0HM8C "codecov")](https://codecov.io/gh/ppfeufer/aa-sov-timer)
[![Translation status](https://weblate.ppfeufer.de/widget/alliance-auth-apps/aa-sov-timer/svg-badge.svg)](https://weblate.ppfeufer.de/engage/alliance-auth-apps/)
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg "Contributor Covenant")](https://github.com/ppfeufer/aa-forum/blob/master/CODE_OF_CONDUCT.md)
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg "Contributor Covenant")](https://github.com/ppfeufer/aa-sov-timer/blob/master/CODE_OF_CONDUCT.md)
[![Discord](https://img.shields.io/discord/790364535294132234?label=discord "Discord")](https://discord.gg/zmh52wnfvM)

[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/N4N8CL1BY)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ dynamic = [
"version",
]
dependencies = [
"allianceauth>=4.3.1,<5",
"allianceauth>=4.6,<5",
"allianceauth-app-utils>=1.25",
"django-eveuniverse>=1.5.4",
]
Expand Down
8 changes: 8 additions & 0 deletions sovtimer/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
Constants we use throughout the app
"""

# Standard Library
import os

# Alliance Auth
from esi import __version__ as esi_version

Expand All @@ -11,3 +14,8 @@
APP_NAME = "aa-sov-timer"
GITHUB_URL = f"https://github.com/ppfeufer/{APP_NAME}"
USER_AGENT = f"{APP_NAME}/{__version__} +{GITHUB_URL} via django-esi/{esi_version}"

# aa-sov-timer/sovtimer
AA_SOVTIMER_BASE_DIR = os.path.join(os.path.dirname(__file__))
# aa-sov-timer/sovtimer/static/sovtimer
AA_SOVTIMER_STATIC_DIR = os.path.join(AA_SOVTIMER_BASE_DIR, "static", "sovtimer")
42 changes: 42 additions & 0 deletions sovtimer/helper/static_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
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 Sovereignty Timer
# AA Sov Timer
from sovtimer import __title__
from sovtimer.constants import AA_SOVTIMER_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-sov-timer/sovtimer/static/sovtimer` folder
:type relative_file_path: str
:return: The integrity hash
:rtype: str
"""

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

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

<link
rel="stylesheet"
href="{% sovtimer_static 'sovtimer/css/aa-sov-timer.min.css' %}"
integrity="sha512-0evKG/A1AxkfyUwC1rKD8g0trmE4d9fZtnGzCXTOW8duZNOGQQ93gxJxGrDFCJjqS+2sXVbwLOqgvM2K3FsbOQ=="
crossorigin="anonymous"
>
{% sovtimer_static "css/aa-sov-timer.min.css" %}
6 changes: 1 addition & 5 deletions sovtimer/templates/sovtimer/bundles/aa-sov-timer-js.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
{% load sovtimer %}

<script
src="{% sovtimer_static 'sovtimer/js/aa-sov-timer.min.js' %}"
integrity="sha512-qAK/BsND1RartZcIii61vL+0Ez/3KYG6gSDL71uPVrV2s2UAoggIYZfC6wA5nV4ZbWdqgBkYEWp8POHZdBmD8w=="
crossorigin="anonymous"
></script>
{% sovtimer_static "js/aa-sov-timer.min.js" %}
72 changes: 63 additions & 9 deletions sovtimer/templatetags/sovtimer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,80 @@
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 Sovereignty Timer
from sovtimer import __version__
from sovtimer import __title__, __version__
from sovtimer.helper.static_files import calculate_integrity_hash

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


@register.simple_tag
def sovtimer_static(path: str) -> str:
def sovtimer_static(relative_file_path: str, script_type: str = None) -> str | None:
"""
Versioned static URL
:param path:
:type path:
:return:
:rtype:
:param relative_file_path: The file path relative to the `aa-sov-timer/sovtimer/static/sovtimer` folder
:type relative_file_path: str
:param script_type: The script type
:type script_type: str
:return: Versioned static URL
:rtype: str
"""

static_url = static(path=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("sovtimer", 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":
js_type = f' type="{script_type}"' if script_type else ""

return mark_safe(
f'<script{js_type} src="{versioned_url}"{sri_string}></script>'
)

return versioned_url
return None
62 changes: 54 additions & 8 deletions sovtimer/tests/test_templatetags.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,81 @@

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

# AA Sovereignty Timer
from sovtimer import __version__
from sovtimer.helper.static_files import calculate_integrity_hash


class TestForumVersionedStatic(TestCase):
class TestVersionedStatic(TestCase):
"""
Test `sovtimer_static` template tag
"""

def test_sovtimer_static(self):
@override_settings(DEBUG=False)
def test_versioned_static(self):
"""
Test if we get the right version on our static files
Test should return the versioned static
:return:
:rtype:
"""

context = Context(dict_={"version": __version__})
template_to_render = Template(
template_string=(
"{% load sovtimer %}"
"{% sovtimer_static 'sovtimer/css/aa-sov-timer.min.css' %}"
"{% sovtimer_static 'css/aa-sov-timer.min.css' %}"
"{% sovtimer_static 'js/aa-sov-timer.min.js' %}"
)
)

rendered_template = template_to_render.render(context=context)

self.assertInHTML(
needle=f'/static/sovtimer/css/aa-sov-timer.min.css?v={context["version"]}',
haystack=rendered_template,
expected_static_css_src = (
f'/static/sovtimer/css/aa-sov-timer.min.css?v={context["version"]}'
)
expected_static_css_src_integrity = calculate_integrity_hash(
"css/aa-sov-timer.min.css"
)
expected_static_js_src = (
f'/static/sovtimer/js/aa-sov-timer.min.js?v={context["version"]}'
)
expected_static_js_src_integrity = calculate_integrity_hash(
"js/aa-sov-timer.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 sovtimer %}" "{% sovtimer_static 'css/aa-sov-timer.min.css' %}"
)
)

rendered_template = template_to_render.render(context=context)

expected_static_css_src = (
f'/static/sovtimer/css/aa-sov-timer.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 73f3c64

Please sign in to comment.