diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7da1d9..f4062d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,16 @@ jobs: python-version: "3.12" - django-version: "5.2" python-version: "3.13" + - django-version: "5.2" + python-version: "3.14" + # Django 6.0 + - django-version: "6.0" + python-version: "3.12" + - django-version: "6.0" + python-version: "3.13" + - django-version: "6.0" + python-version: "3.14" steps: - uses: actions/checkout@v4 diff --git a/MANIFEST.in b/MANIFEST.in index 0081f2f..23c26be 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,7 @@ include README.rst CHANGES.rst CONTRIBUTING.rst RELEASES.rst LICENSE Makefile include requirements-dev.txt include tox.ini include .readthedocs.yaml +recursive-include authtools/templates * recursive-include tests *.py recursive-include tests *.json recursive-include docs * diff --git a/authtools/forms.py b/authtools/forms.py index 7ea02a7..372375b 100644 --- a/authtools/forms.py +++ b/authtools/forms.py @@ -1,42 +1,23 @@ from __future__ import unicode_literals from django import forms -from django.forms.utils import flatatt from django.contrib.auth.forms import ( - ReadOnlyPasswordHashField, ReadOnlyPasswordHashWidget, + ReadOnlyPasswordHashWidget, PasswordResetForm as OldPasswordResetForm, UserChangeForm as DjangoUserChangeForm, AuthenticationForm as DjangoAuthenticationForm, ) from django.contrib.auth import get_user_model, password_validation -from django.contrib.auth.hashers import identify_hasher, UNUSABLE_PASSWORD_PREFIX -from django.utils.translation import gettext_lazy as _, gettext -from django.utils.html import format_html +from django.utils.translation import gettext_lazy as _ User = get_user_model() -def is_password_usable(pw): - """Decide whether a password is usable only by the unusable password prefix. - - We can't use django.contrib.auth.hashers.is_password_usable either, because - it not only checks against the unusable password, but checks for a valid - hasher too. We need different error messages in those cases. - """ - - return not pw.startswith(UNUSABLE_PASSWORD_PREFIX) - - class BetterReadOnlyPasswordHashWidget(ReadOnlyPasswordHashWidget): """ A ReadOnlyPasswordHashWidget that has a less intimidating output. """ - - def get_context(self, name, value, attrs): - context = super().get_context(name, value, attrs) - if any(item.get('value') for item in context['summary']): - context['summary'] = [{'label': gettext('*************')}] - return context + template_name = 'authtools/widgets/better_read_only_password_hash.html' class UserChangeForm(DjangoUserChangeForm): diff --git a/authtools/templates/authtools/widgets/better_read_only_password_hash.html b/authtools/templates/authtools/widgets/better_read_only_password_hash.html new file mode 100644 index 0000000..3630e7b --- /dev/null +++ b/authtools/templates/authtools/widgets/better_read_only_password_hash.html @@ -0,0 +1,7 @@ +{% load authtools %} + + {% render_better_read_only_password_hash widget.value %} + {% if button_label %} +

{{ button_label }}

+ {% endif %} + diff --git a/authtools/templatetags/__init__.py b/authtools/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/authtools/templatetags/authtools.py b/authtools/templatetags/authtools.py new file mode 100644 index 0000000..f5fb861 --- /dev/null +++ b/authtools/templatetags/authtools.py @@ -0,0 +1,25 @@ +from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX, identify_hasher +from django.template import Library +from django.utils.html import format_html +from django.utils.translation import gettext + +register = Library() + + +@register.simple_tag +def render_better_read_only_password_hash(value): + if not value or value.startswith(UNUSABLE_PASSWORD_PREFIX): + return format_html("

{}

", gettext("No password set.")) + try: + hasher = identify_hasher(value) + hasher.safe_summary(value) + except ValueError: + return format_html( + "

{}

", + gettext("Invalid password format or unknown hashing algorithm."), + ) + + return format_html( + "

{}

", + gettext("*************"), + ) diff --git a/tests/tests/tests.py b/tests/tests/tests.py index badbc2f..016ac51 100644 --- a/tests/tests/tests.py +++ b/tests/tests/tests.py @@ -257,16 +257,16 @@ def test_better_readonly_password_widget(self): user = User.objects.get(username='testclient') form = UserChangeForm(instance=user) - self.assertIn(_('*************'), form.as_table()) - - version = django.VERSION[0] - - if version < 4: - self.assertIn('', form.as_table()) - elif version < 5: - self.assertIn(''.format(user.id), form.as_table()) + html = form.as_table() + self.assertIn(_('*************'), html) + version = django.VERSION[:2] + + if version < (4, 2): + self.assertIn(''.format(user.id), html) else: - self.assertIn('', form.as_table()) + self.assertIn('=2.2,<2.3 @@ -28,6 +30,7 @@ deps= dj50: Django>=5.0,<5.1 dj51: Django>=5.1,<5.2 dj52: Django>=5.2,<5.3 -whitelist_externals= + dj60: Django>=6.0,<6.1 +allowlist_externals= env make