Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion django_altcha/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def get_altcha_challenge(max_number=None, expires=None):
class AltchaWidget(HiddenInput):
template_name = "altcha_widget.html"

def __init__(self, options, *args, **kwargs):
def __init__(self, options=None, *args, **kwargs):
"""Initialize the ALTCHA widget with provided options from the field."""
self.options = options or {}
super().__init__(*args, **kwargs)
Expand Down
35 changes: 30 additions & 5 deletions django_altcha/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,25 @@
"""

from django.conf import settings
from django.templatetags.static import static

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

# Settings whose value is a static asset path that should be resolved through
# Django's staticfiles machinery when given as a relative path.
_STATIC_ASSET_SETTINGS = {"ALTCHA_JS_URL", "ALTCHA_JS_TRANSLATIONS_URL"}


def _is_absolute(path):
"""Return True if `path` is a full URL or an absolute server path."""
return path.startswith(("http://", "https://", "/"))


def get_setting(name):
"""Look up a django-altcha setting, falling back to the default."""
if name not in _DEFAULTS:
raise ValueError(f"Unknown django-altcha setting: {name}")
return getattr(settings, name, _DEFAULTS[name])
value = getattr(settings, name, _DEFAULTS[name])

# Resolve relative static paths through STATIC_URL so they respect the
# project's staticfiles configuration and storage backend. Absolute paths
# and full URLs are passed through untouched, matching the convention
# used by Django's form Media class.
if name in _STATIC_ASSET_SETTINGS and value and not _is_absolute(value):
return static(value)

return value
1 change: 1 addition & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
INSTALLED_APPS = ["django_altcha"]
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3"}}
ROOT_URLCONF = "tests.urls"
STATIC_URL = "/static/"
ALTCHA_HMAC_KEY = "altcha-insecure-hmac-0123456789abcdef"
CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}}
62 changes: 60 additions & 2 deletions tests/test_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

class DjangoAltchaWidgetTest(TestCase):
def test_widget_initialization_with_default_options(self):
widget = AltchaWidget(options=None)
widget = AltchaWidget()
self.assertNotIn("challengeurl", widget.options)
self.assertNotIn("challengejson", widget.options)
self.assertNotIn("auto", widget.options)
Expand Down Expand Up @@ -56,7 +56,7 @@ def test_widget_rendering_with_complex_options(self):
self.assertIn(expected, rendered_widget_html)

def test_js_translation_included_if_enabled(self):
widget = AltchaWidget(options=None)
widget = AltchaWidget()
expected_js = "/static/altcha/dist_i18n/all.min.js"

with override_settings(ALTCHA_INCLUDE_TRANSLATIONS=True):
Expand All @@ -66,3 +66,61 @@ def test_js_translation_included_if_enabled(self):
with override_settings(ALTCHA_INCLUDE_TRANSLATIONS=False):
rendered_widget_html = widget.render("name", "value")
self.assertNotIn(expected_js, rendered_widget_html)

def test_widget_renders_default_js_url_through_static(self):
widget = AltchaWidget()
rendered_html = widget.render("name", "value")
self.assertIn("/static/altcha/altcha.min.js", rendered_html)

def test_widget_respects_custom_static_url(self):
widget = AltchaWidget()
with override_settings(STATIC_URL="/assets/"):
rendered_html = widget.render("name", "value")
self.assertIn("/assets/altcha/altcha.min.js", rendered_html)
self.assertNotIn("/static/altcha/altcha.min.js", rendered_html)

def test_widget_resolves_relative_js_url_override(self):
widget = AltchaWidget()
with override_settings(ALTCHA_JS_URL="custom/altcha.js"):
rendered_html = widget.render("name", "value")
self.assertIn("/static/custom/altcha.js", rendered_html)

def test_widget_passes_through_absolute_js_url(self):
widget = AltchaWidget()
with override_settings(ALTCHA_JS_URL="/my_static/altcha.js"):
rendered_html = widget.render("name", "value")
self.assertIn('src="/my_static/altcha.js"', rendered_html)
self.assertNotIn("/static/my_static/altcha.js", rendered_html)

def test_widget_passes_through_http_js_url(self):
widget = AltchaWidget()
cdn_url = "http://cdn/altcha.min.js"
with override_settings(ALTCHA_JS_URL=cdn_url):
rendered_html = widget.render("name", "value")
self.assertIn(cdn_url, rendered_html)

def test_widget_passes_through_https_js_url(self):
widget = AltchaWidget()
cdn_url = "https://cdn/altcha.min.js"
with override_settings(ALTCHA_JS_URL=cdn_url):
rendered_html = widget.render("name", "value")
self.assertIn(cdn_url, rendered_html)

def test_widget_resolves_translations_url_through_static(self):
widget = AltchaWidget()
with override_settings(
ALTCHA_INCLUDE_TRANSLATIONS=True,
STATIC_URL="/assets/",
):
rendered_html = widget.render("name", "value")
self.assertIn("/assets/altcha/dist_i18n/all.min.js", rendered_html)

def test_widget_passes_through_absolute_translations_url(self):
widget = AltchaWidget()
cdn_url = "https://cdni18n/all.min.js"
with override_settings(
ALTCHA_INCLUDE_TRANSLATIONS=True,
ALTCHA_JS_TRANSLATIONS_URL=cdn_url,
):
rendered_html = widget.render("name", "value")
self.assertIn(cdn_url, rendered_html)