diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 5f4d7c7..f8433e0 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -9,20 +9,19 @@ updates:
ignore:
- dependency-name: flake8
versions:
- - 3.8.4
- - 3.9.0
+ - 7.3.0
- dependency-name: coverage
versions:
- - "5.4"
+ - "7.13.4"
- dependency-name: pytest
versions:
- - 6.2.2
+ - 9.0.2
- dependency-name: isort
versions:
- - 5.7.0
+ - 8.0.1
- dependency-name: django-debug-toolbar
versions:
- - "3.2"
+ - "6.2.0"
- dependency-name: pytest-django
versions:
- - 4.1.0
+ - 4.12.0
diff --git a/CHANGES b/CHANGES
index 7dc216c..fe48d46 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,9 @@
+Unreleased
+==================
+ - Add testing for Python 3.14, Wagtail 7.2, and Wagtail 7.3
+ - Update dependencies to latest versions
+
+
1.8.0 (2026-02-05)
==================
- bump required version of django-otp and update middleware
diff --git a/sandbox/requirements.txt b/sandbox/requirements.txt
index aebb00e..010059c 100644
--- a/sandbox/requirements.txt
+++ b/sandbox/requirements.txt
@@ -1,4 +1,4 @@
-Django>=3.2
-wagtail>=4.1
-django-debug-toolbar==3.2.2
+Django>=5.2
+Wagtail>=7.0
+django-debug-toolbar==6.2.0
-e .[docs,test]
diff --git a/sandbox/sandbox/settings.py b/sandbox/sandbox/settings.py
index 5ce9f87..8289b02 100644
--- a/sandbox/sandbox/settings.py
+++ b/sandbox/sandbox/settings.py
@@ -119,7 +119,6 @@
USE_I18N = True
-USE_L10N = True
USE_TZ = True
diff --git a/sandbox/sandbox/urls.py b/sandbox/sandbox/urls.py
index 7439f5a..8553305 100644
--- a/sandbox/sandbox/urls.py
+++ b/sandbox/sandbox/urls.py
@@ -1,6 +1,7 @@
import debug_toolbar
from django.conf import settings
from django.contrib import admin
+from django.urls import path
from django.urls import include, re_path
from wagtail import urls as wagtail_urls
@@ -9,9 +10,9 @@
urlpatterns = [
re_path(r'^admin/', admin.site.urls),
- re_path(r'^cms/', include(wagtailadmin_urls)),
- re_path(r'^documents/', include(wagtaildocs_urls)),
- re_path(r'', include(wagtail_urls)),
+ path('cms/', include(wagtailadmin_urls)),
+ path('documents/', include(wagtaildocs_urls)),
+ path('', include(wagtail_urls)),
]
@@ -24,5 +25,5 @@
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns = [
- re_path(r'^__debug__/', include(debug_toolbar.urls)),
+ path('__debug__/', include(debug_toolbar.urls)),
] + urlpatterns
diff --git a/setup.py b/setup.py
index 37b8836..292bb94 100644
--- a/setup.py
+++ b/setup.py
@@ -57,19 +57,16 @@
"Development Status :: 4 - Beta",
"Environment :: Web Environment",
"Framework :: Django",
- "Framework :: Django :: 3.2",
- "Framework :: Django :: 4.1",
- "Framework :: Django :: 4.2",
+ "Framework :: Django :: 5.2",
"Framework :: Wagtail",
- "Framework :: Wagtail :: 2",
- "Framework :: Wagtail :: 3",
- "Framework :: Wagtail :: 4",
+ "Framework :: Wagtail :: 7",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
],
zip_safe=False,
)
diff --git a/src/wagtail_2fa/middleware.py b/src/wagtail_2fa/middleware.py
index 9773a78..f506190 100644
--- a/src/wagtail_2fa/middleware.py
+++ b/src/wagtail_2fa/middleware.py
@@ -17,6 +17,10 @@ class VerifyUserMiddleware(_OTPMiddleware):
"wagtailadmin_sprite",
]
+ def get_allowed_url_names(self):
+ extra = getattr(settings, "WAGTAIL_2FA_ALLOWED_URL_NAMES", [])
+ return self._allowed_url_names + list(extra)
+
# These URLs do not require verification if the user has no devices
_allowed_url_names_no_device = [
"wagtail_2fa_device_list",
@@ -75,7 +79,7 @@ def _require_verified_user(self, request):
# Don't require verification for specified URL names
request_url_name = resolve(request.path_info).url_name
- if request_url_name in self._allowed_url_names:
+ if request_url_name in self.get_allowed_url_names():
return False
# If the user does not have a device, don't require verification
diff --git a/src/wagtail_2fa/templates/wagtail_2fa/device_list.html b/src/wagtail_2fa/templates/wagtail_2fa/device_list.html
index 918812e..ada83a0 100644
--- a/src/wagtail_2fa/templates/wagtail_2fa/device_list.html
+++ b/src/wagtail_2fa/templates/wagtail_2fa/device_list.html
@@ -46,7 +46,10 @@
{# Users can only add devices to their own account #}
{% if user_id == request.user.id %}
- {% trans 'New device' %}
+
+
+ {% trans 'New device' %}
+
{% endif %}
{% endblock %}
diff --git a/src/wagtail_2fa/templates/wagtail_2fa/legacy/otp_form.html b/src/wagtail_2fa/templates/wagtail_2fa/legacy/otp_form.html
deleted file mode 100644
index 05b3dd1..0000000
--- a/src/wagtail_2fa/templates/wagtail_2fa/legacy/otp_form.html
+++ /dev/null
@@ -1,62 +0,0 @@
-{% extends "wagtailadmin/admin_base.html" %}
-{% load i18n wagtailadmin_tags %}
-{% block titletag %}{% trans "Sign in" %}{% endblock %}
-{% block bodyclass %}login{% endblock %}
-
-{% block furniture %}
-
- {% block branding_login %}{% trans "Enter your two-factor authentication code" %}{% endblock %}
-
-
- {# Always show messages div so it can be appended to by JS #}
- {% if messages or form.errors %}
-
- {% if form.errors %}
- - {% blocktrans %}Invalid code{% endblocktrans %}
- {% endif %}
- {% for message in messages %}
- - {{ message }}
- {% endfor %}
-
- {% endif %}
-
-
- {% block above_login %}{% endblock %}
-
-
-
- {% block below_login %}{% endblock %}
-
- {% block branding_logo %}
-
-

-
- {% endblock %}
-
-{% endblock %}
diff --git a/src/wagtail_2fa/templates/wagtail_2fa/otp_form.html b/src/wagtail_2fa/templates/wagtail_2fa/otp_form.html
index 4186232..1cd69da 100644
--- a/src/wagtail_2fa/templates/wagtail_2fa/otp_form.html
+++ b/src/wagtail_2fa/templates/wagtail_2fa/otp_form.html
@@ -1,4 +1,4 @@
-{% extends "wagtailadmin/admin_base.html" %}
+{% extends "wagtailadmin/base.html" %}
{% load i18n wagtailadmin_tags %}
{% block titletag %}{% trans "Sign in" %}{% endblock %}
{% block bodyclass %}login{% endblock %}
@@ -31,11 +31,11 @@ {% block branding_login %}{% trans "Enter your two-factor authentication cod
{% block fields %}
- {% field field=form.otp_token %}{% endfield %}
+ {% formattedfield form.otp_token %}
{% block extra_fields %}
{% for field_name, field in form.extra_fields %}
- {% field field=field %}{% endfield %}
+ {% formattedfield field %}
{% endfor %}
{% endblock extra_fields %}
@@ -45,7 +45,9 @@ {% block branding_login %}{% trans "Enter your two-factor authentication cod
{% block submit_buttons %}
- {% trans 'Sign out' %}
+
{% endblock %}
diff --git a/src/wagtail_2fa/templates/wagtail_2fa/otp_form_v6.html b/src/wagtail_2fa/templates/wagtail_2fa/otp_form_v6.html
deleted file mode 100644
index d114d95..0000000
--- a/src/wagtail_2fa/templates/wagtail_2fa/otp_form_v6.html
+++ /dev/null
@@ -1,66 +0,0 @@
-{% extends "wagtailadmin/base.html" %}
-{% load i18n wagtailadmin_tags %}
-{% block titletag %}{% trans "Sign in" %}{% endblock %}
-{% block bodyclass %}login{% endblock %}
-
-{% block furniture %}
-
- {% block branding_login %}{% trans "Enter your two-factor authentication code" %}{% endblock %}
-
-
- {# Always show messages div so it can be appended to by JS #}
- {% if messages or form.errors %}
-
- {% if form.errors %}
- - {% blocktrans %}Invalid code{% endblocktrans %}
- {% endif %}
- {% for message in messages %}
- - {{ message }}
- {% endfor %}
-
- {% endif %}
-
-
- {% block above_login %}{% endblock %}
-
-
-
- {% block below_login %}{% endblock %}
-
- {% block branding_logo %}
-
- {% include "wagtailadmin/icons/wagtail.svg" %}
-
- {% endblock %}
-
-
-
-{% endblock %}
\ No newline at end of file
diff --git a/src/wagtail_2fa/views.py b/src/wagtail_2fa/views.py
index ef0b348..e59db1b 100644
--- a/src/wagtail_2fa/views.py
+++ b/src/wagtail_2fa/views.py
@@ -30,14 +30,7 @@
class LoginView(RedirectURLMixin, FormView):
-
- if WAGTAIL_VERSION >= (6, 0):
- template_name = "wagtail_2fa/otp_form_v6.html"
- elif WAGTAIL_VERSION < (6, 0) and WAGTAIL_VERSION >= (5, 0):
- template_name = "wagtail_2fa/otp_form.html"
- else:
- template_name = "wagtail_2fa/legacy/otp_form.html"
-
+ template_name = "wagtail_2fa/otp_form.html"
form_class = forms.TokenForm
redirect_field_name = REDIRECT_FIELD_NAME
diff --git a/src/wagtail_2fa/wagtail_hooks.py b/src/wagtail_2fa/wagtail_hooks.py
index 17284cf..9401ccc 100644
--- a/src/wagtail_2fa/wagtail_hooks.py
+++ b/src/wagtail_2fa/wagtail_hooks.py
@@ -1,23 +1,26 @@
from django.conf import settings
from django.contrib.auth.models import Permission
-from django.urls import path, re_path, reverse
+from django.urls import path, reverse
from django.utils.translation import gettext_lazy as _
-from wagtail import hooks
+from wagtail import hooks, VERSION as WAGTAIL_VERSION
from wagtail.admin.menu import MenuItem
-from wagtail.users.widgets import UserListingButton
-from wagtail_2fa import views
+if WAGTAIL_VERSION >= (7, 1):
+ from wagtail.admin.widgets import Button
+else:
+ from wagtail.users.widgets import UserListingButton as Button
+
-from wagtail import VERSION as WAGTAIL_VERSION
+from wagtail_2fa import views
@hooks.register("register_admin_urls")
def urlpatterns():
return [
path("2fa/auth", views.LoginView.as_view(), name="wagtail_2fa_auth"),
- re_path(
- r"^2fa/devices/(?P\d+)$",
+ path(
+ "2fa/devices/",
views.DeviceListView.as_view(),
name="wagtail_2fa_device_list",
),
@@ -26,18 +29,18 @@ def urlpatterns():
views.DeviceCreateView.as_view(),
name="wagtail_2fa_device_new",
),
- re_path(
- r"^2fa/devices/(?P\d+)/update$",
+ path(
+ "2fa/devices//update",
views.DeviceUpdateView.as_view(),
name="wagtail_2fa_device_update",
),
- re_path(
- r"^2fa/devices/(?P\d+)/remove$",
+ path(
+ "2fa/devices//remove",
views.DeviceDeleteView.as_view(),
name="wagtail_2fa_device_remove",
),
- re_path(
- r"^2fa/devices/qr-code$",
+ path(
+ "2fa/devices/qr-code",
views.DeviceQRCodeView.as_view(),
name="wagtail_2fa_device_qrcode",
),
@@ -70,25 +73,14 @@ def register(request):
}
-if WAGTAIL_VERSION >= (6, 0):
- @hooks.register("register_user_listing_buttons")
- def register_user_listing_buttons(user, request_user):
- yield UserListingButton(
- _("Manage 2FA"),
- reverse("wagtail_2fa_device_list", kwargs={"user_id": user.id}),
- attrs={"title": _("Edit this user")},
- priority=100,
- )
-else:
- @hooks.register("register_user_listing_buttons")
- def register_user_listing_buttons(context, user):
- yield UserListingButton(
- _("Manage 2FA"),
- reverse("wagtail_2fa_device_list", kwargs={"user_id": user.id}),
- attrs={"title": _("Edit this user")},
- priority=100,
- )
-
+@hooks.register("register_user_listing_buttons")
+def register_user_listing_buttons(user, request_user):
+ yield Button(
+ _("Manage 2FA"),
+ reverse("wagtail_2fa_device_list", kwargs={"user_id": user.id}),
+ attrs={"title": _("Edit this user")},
+ priority=100,
+ )
@hooks.register("register_permissions")
diff --git a/tox.ini b/tox.ini
index 53f6768..94ae607 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,7 @@
[tox]
envlist =
- python{3.13,3.14}-django{5.2}-wagtail{7.0}
+ python{3.13}-django{5.2}-wagtail{7.0,7.1,7.2,7.3},
+ python{3.14}-django{5.2}-wagtail{7.2,7.3},
[gh-actions]
python =
@@ -17,6 +18,9 @@ basepython =
deps =
django5.2: Django>=5.2,<6.0
wagtail7.0: wagtail>=7.0,<7.1
+ wagtail7.1: wagtail>=7.1,<7.2
+ wagtail7.2: wagtail>=7.2,<7.3
+ wagtail7.3: wagtail>=7.3,<7.4
extras = test