Skip to content

Commit 0eb77f0

Browse files
authored
fix(settings): resolve static asset URLs through STATIC_URL (#45)
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent 91dc48c commit 0eb77f0

4 files changed

Lines changed: 92 additions & 8 deletions

File tree

django_altcha/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def get_altcha_challenge(max_number=None, expires=None):
8181
class AltchaWidget(HiddenInput):
8282
template_name = "altcha_widget.html"
8383

84-
def __init__(self, options, *args, **kwargs):
84+
def __init__(self, options=None, *args, **kwargs):
8585
"""Initialize the ALTCHA widget with provided options from the field."""
8686
self.options = options or {}
8787
super().__init__(*args, **kwargs)

django_altcha/conf.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,25 @@
1616
"""
1717

1818
from django.conf import settings
19+
from django.templatetags.static import static
1920

2021
_DEFAULTS = {
2122
# Set to `False` to skip Altcha validation altogether.
2223
"ALTCHA_VERIFICATION_ENABLED": True,
2324
# This key is used to HMAC-sign ALTCHA challenges and must be kept secret.
2425
"ALTCHA_HMAC_KEY": None,
2526
# URL of the Altcha JavaScript file.
26-
# Defaults to the bundled django-altcha file.
27-
"ALTCHA_JS_URL": "/static/altcha/altcha.min.js",
27+
# Defaults to the bundled django-altcha file, resolved through STATIC_URL.
28+
# Accepts:
29+
# - a relative static path (e.g. "altcha/altcha.min.js"),
30+
# - an absolute path starting with "/",
31+
# - or a fully-qualified URL (http:// or https://) for CDN usage.
32+
# Relative paths are passed through Django's staticfiles storage,
33+
# so they work with STATIC_URL customization and ManifestStaticFilesStorage.
34+
"ALTCHA_JS_URL": "altcha/altcha.min.js",
2835
# URL of the Altcha translations JavaScript file.
29-
# Defaults to the bundled django-altcha file.
30-
"ALTCHA_JS_TRANSLATIONS_URL": "/static/altcha/dist_i18n/all.min.js",
36+
# Same resolution rules as ALTCHA_JS_URL above.
37+
"ALTCHA_JS_TRANSLATIONS_URL": "altcha/dist_i18n/all.min.js",
3138
# Whether to include Altcha translations.
3239
# https://altcha.org/docs/v2/widget-integration/#internationalization-i18n
3340
"ALTCHA_INCLUDE_TRANSLATIONS": False,
@@ -41,9 +48,27 @@
4148
"ALTCHA_CACHE_ALIAS": "default",
4249
}
4350

51+
# Settings whose value is a static asset path that should be resolved through
52+
# Django's staticfiles machinery when given as a relative path.
53+
_STATIC_ASSET_SETTINGS = {"ALTCHA_JS_URL", "ALTCHA_JS_TRANSLATIONS_URL"}
54+
55+
56+
def _is_absolute(path):
57+
"""Return True if `path` is a full URL or an absolute server path."""
58+
return path.startswith(("http://", "https://", "/"))
59+
4460

4561
def get_setting(name):
4662
"""Look up a django-altcha setting, falling back to the default."""
4763
if name not in _DEFAULTS:
4864
raise ValueError(f"Unknown django-altcha setting: {name}")
49-
return getattr(settings, name, _DEFAULTS[name])
65+
value = getattr(settings, name, _DEFAULTS[name])
66+
67+
# Resolve relative static paths through STATIC_URL so they respect the
68+
# project's staticfiles configuration and storage backend. Absolute paths
69+
# and full URLs are passed through untouched, matching the convention
70+
# used by Django's form Media class.
71+
if name in _STATIC_ASSET_SETTINGS and value and not _is_absolute(value):
72+
return static(value)
73+
74+
return value

tests/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
INSTALLED_APPS = ["django_altcha"]
22
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3"}}
33
ROOT_URLCONF = "tests.urls"
4+
STATIC_URL = "/static/"
45
ALTCHA_HMAC_KEY = "altcha-insecure-hmac-0123456789abcdef"
56
CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}}

tests/test_widget.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
class DjangoAltchaWidgetTest(TestCase):
1717
def test_widget_initialization_with_default_options(self):
18-
widget = AltchaWidget(options=None)
18+
widget = AltchaWidget()
1919
self.assertNotIn("challengeurl", widget.options)
2020
self.assertNotIn("challengejson", widget.options)
2121
self.assertNotIn("auto", widget.options)
@@ -56,7 +56,7 @@ def test_widget_rendering_with_complex_options(self):
5656
self.assertIn(expected, rendered_widget_html)
5757

5858
def test_js_translation_included_if_enabled(self):
59-
widget = AltchaWidget(options=None)
59+
widget = AltchaWidget()
6060
expected_js = "/static/altcha/dist_i18n/all.min.js"
6161

6262
with override_settings(ALTCHA_INCLUDE_TRANSLATIONS=True):
@@ -66,3 +66,61 @@ def test_js_translation_included_if_enabled(self):
6666
with override_settings(ALTCHA_INCLUDE_TRANSLATIONS=False):
6767
rendered_widget_html = widget.render("name", "value")
6868
self.assertNotIn(expected_js, rendered_widget_html)
69+
70+
def test_widget_renders_default_js_url_through_static(self):
71+
widget = AltchaWidget()
72+
rendered_html = widget.render("name", "value")
73+
self.assertIn("/static/altcha/altcha.min.js", rendered_html)
74+
75+
def test_widget_respects_custom_static_url(self):
76+
widget = AltchaWidget()
77+
with override_settings(STATIC_URL="/assets/"):
78+
rendered_html = widget.render("name", "value")
79+
self.assertIn("/assets/altcha/altcha.min.js", rendered_html)
80+
self.assertNotIn("/static/altcha/altcha.min.js", rendered_html)
81+
82+
def test_widget_resolves_relative_js_url_override(self):
83+
widget = AltchaWidget()
84+
with override_settings(ALTCHA_JS_URL="custom/altcha.js"):
85+
rendered_html = widget.render("name", "value")
86+
self.assertIn("/static/custom/altcha.js", rendered_html)
87+
88+
def test_widget_passes_through_absolute_js_url(self):
89+
widget = AltchaWidget()
90+
with override_settings(ALTCHA_JS_URL="/my_static/altcha.js"):
91+
rendered_html = widget.render("name", "value")
92+
self.assertIn('src="/my_static/altcha.js"', rendered_html)
93+
self.assertNotIn("/static/my_static/altcha.js", rendered_html)
94+
95+
def test_widget_passes_through_http_js_url(self):
96+
widget = AltchaWidget()
97+
cdn_url = "http://cdn/altcha.min.js"
98+
with override_settings(ALTCHA_JS_URL=cdn_url):
99+
rendered_html = widget.render("name", "value")
100+
self.assertIn(cdn_url, rendered_html)
101+
102+
def test_widget_passes_through_https_js_url(self):
103+
widget = AltchaWidget()
104+
cdn_url = "https://cdn/altcha.min.js"
105+
with override_settings(ALTCHA_JS_URL=cdn_url):
106+
rendered_html = widget.render("name", "value")
107+
self.assertIn(cdn_url, rendered_html)
108+
109+
def test_widget_resolves_translations_url_through_static(self):
110+
widget = AltchaWidget()
111+
with override_settings(
112+
ALTCHA_INCLUDE_TRANSLATIONS=True,
113+
STATIC_URL="/assets/",
114+
):
115+
rendered_html = widget.render("name", "value")
116+
self.assertIn("/assets/altcha/dist_i18n/all.min.js", rendered_html)
117+
118+
def test_widget_passes_through_absolute_translations_url(self):
119+
widget = AltchaWidget()
120+
cdn_url = "https://cdni18n/all.min.js"
121+
with override_settings(
122+
ALTCHA_INCLUDE_TRANSLATIONS=True,
123+
ALTCHA_JS_TRANSLATIONS_URL=cdn_url,
124+
):
125+
rendered_html = widget.render("name", "value")
126+
self.assertIn(cdn_url, rendered_html)

0 commit comments

Comments
 (0)