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/.github/workflows/python-release.yml b/.github/workflows/python-release.yml
index bfb3838..a43dc0d 100644
--- a/.github/workflows/python-release.yml
+++ b/.github/workflows/python-release.yml
@@ -11,7 +11,7 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up Python 3.11
- uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4.9.1
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.11
diff --git a/.github/workflows/python-tox.yml b/.github/workflows/python-tox.yml
index 0130ae7..50c1559 100644
--- a/.github/workflows/python-tox.yml
+++ b/.github/workflows/python-tox.yml
@@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up Python ${{ matrix.python }}
- uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4.9.1
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python }}
diff --git a/CHANGES b/CHANGES
index 7dc216c..a54dfa5 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,20 @@
+Unreleased
+==================
+ - Add support for Wagtail 7.4 (LTS) and Django 6.0
+ - Expand test matrix to cover Python 3.10, 3.11, and 3.12 (previously
+ declared in classifiers but not exercised in tox); CI now tests against
+ Wagtail 7.0, 7.3, and 7.4 (LTS + current + previous current)
+ - Declare upper bounds: ``Django>=5.2,<7`` and ``Wagtail>=7.0,<8``.
+ Users on Wagtail 7.0 with Django 4.2 should remain on ``wagtail-2fa
+ 1.8.x``; this release requires Django 5.2 or newer
+ - Remove vestigial ``six`` install dependency (unused since the Python 2
+ compatibility shim was dropped)
+ - Remove dead ``DJANGO_VERSION`` import guard in ``views.py`` (the
+ ``Django < 4.1`` branch is unreachable now that ``Django>=5.2`` is
+ required)
+ - Update dependencies to latest versions
+
+
1.8.0 (2026-02-05)
==================
- bump required version of django-otp and update middleware
diff --git a/README.rst b/README.rst
index fa22f84..6b65108 100644
--- a/README.rst
+++ b/README.rst
@@ -114,8 +114,8 @@ and 2FA can be enabled or disabled per group.
Sandbox
=======
-First create a new virtualenv with Python 3.8 and activate it. Then run
-the following commands:
+First create a new virtualenv with Python 3.10 or newer and activate it.
+Then run the following commands:
.. code-block:: shell
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..d5fe4c8 100644
--- a/setup.py
+++ b/setup.py
@@ -6,25 +6,24 @@
"Django>=5.2",
"Wagtail>=7.0",
"django-otp>=1.7.0",
- "six>=1.14.0",
"qrcode>=6.1",
]
docs_require = [
- "sphinx>=1.4.1",
- "sphinx_rtd_theme>=0.4.3",
+ "sphinx>=7.0",
+ "sphinx_rtd_theme>=2.0",
]
tests_require = [
- "coverage==5.5",
- "pytest>=7.2.2",
- "pytest-cov>=2.12.1",
- "pytest-django>=4.4.0",
+ "coverage>=7.0",
+ "pytest>=8.0",
+ "pytest-cov>=5.0",
+ "pytest-django>=4.8",
# Linting
- "flake8>=3.9.2", # 3.7.9
- "isort>=5.12.0",
- "flake8-blind-except>=0.2.0",
- "flake8-debugger>=4.0.0",
+ "flake8>=7.0",
+ "isort>=5.13",
+ "flake8-blind-except>=0.2.1",
+ "flake8-debugger>=4.1",
]
with open("README.rst") as fh:
@@ -57,19 +56,17 @@
"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 :: Django :: 6.0",
"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..a044b46 100644
--- a/src/wagtail_2fa/views.py
+++ b/src/wagtail_2fa/views.py
@@ -1,16 +1,8 @@
import qrcode
import qrcode.image.svg
-from django import VERSION as DJANGO_VERSION
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
-
-if DJANGO_VERSION >= (4, 1):
- from django.contrib.auth.views import RedirectURLMixin
-else:
- from django.contrib.auth.views import SuccessURLAllowedHostsMixin as RedirectURLMixin
-
-from wagtail import VERSION as WAGTAIL_VERSION
-
+from django.contrib.auth.views import RedirectURLMixin
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.shortcuts import resolve_url
@@ -30,14 +22,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..9c79832 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,9 +1,14 @@
[tox]
envlist =
- python{3.13,3.14}-django{5.2}-wagtail{7.0}
+ py{310,311,312,313}-dj52-wt{70,73,74}
+ py314-dj52-wt{73,74}
+ py{312,313,314}-dj60-wt{73,74}
[gh-actions]
python =
+ 3.10: python3.10
+ 3.11: python3.11
+ 3.12: python3.12
3.13: python3.13
3.14: python3.14
@@ -11,12 +16,18 @@ python =
commands = coverage run --parallel -m pytest {posargs} -vvv
basepython =
+ python3.10: python3.10
+ python3.11: python3.11
+ python3.12: python3.12
python3.13: python3.13
python3.14: python3.14
deps =
django5.2: Django>=5.2,<6.0
+ django6.0: Django>=6.0,<6.1
wagtail7.0: wagtail>=7.0,<7.1
+ wagtail7.3: wagtail>=7.3,<7.4
+ wagtail7.4: wagtail>=7.4,<7.5
extras = test