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
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,15 @@ jobs:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
django: ["4.2", "5.2"]
django: ["4.2", "5.2", "6.1"]
exclude:
# Django 6.x requires Python >=3.12. Without these, uv cannot
# resolve the combination and the leg fails on dependency
# resolution rather than on anything about this package.
- python-version: "3.10"
django: "6.1"
- python-version: "3.11"
django: "6.1"

steps:
- uses: actions/checkout@v7
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
- python-version: "3.10"
django: "4.2"
- python-version: "3.13"
django: "5.2"
django: "6.1"
steps:
- uses: actions/checkout@v7

Expand Down
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ coverage.xml
*.mo
*.pot

# ...except this package's own catalogs, which are source, not build output.
# This negation is load-bearing twice over: hatchling honours .gitignore when
# it builds, so an ignored catalog is also an UNSHIPPED one -- Django then
# finds no locale directory in the installed package and every string falls
# back to English with nothing in the logs to say why. django.pot hit exactly
# that and was caught by test_packaging.py.
#
# .mo is un-ignored for the same reason, ahead of need: no compiled catalog
# ships today (every translation is still a fuzzy machine draft), and
# test_i18n.py fails if one appears. When a language is reviewed and its
# fuzzy flags come off, its .mo has to be committed to reach users.
!django_mfa/locale/django.pot
!django_mfa/locale/*/LC_MESSAGES/*.mo

# Django stuff:
*.log

Expand Down
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,47 @@ Versions follow [PEP 440](https://peps.python.org/pep-0440/). The version in
`pyproject.toml` is the only place it is written; the git tag and the GitHub
Release are derived from it (see [docs/contributing.md](docs/contributing.md)).

## 4.3.0

### Added

- **Translation catalogs.** `django_mfa/locale/` now ships `django.pot` (75
entries) and machine-drafted `.po` files for `de`, `es`, `fr`, `pt_BR`,
`ja` and `zh_Hans`. Every entry is marked `fuzzy`, so gettext ignores it
and users still see English: **no language is live yet**, and a draft only
starts appearing after a human reviews it and removes the flags. See
[docs/translations.md](docs/translations.md). No compiled `.mo` files
ship, because a fully fuzzy catalog compiles to an empty one.
- The Python side is now translatable, matching the templates (which already
were): the verification error, the passkey sign-in error, each adapter's
`verbose_name`, and `Authenticator.Type`'s labels. Wrapping the `Type`
labels needs **no migration** — a `gettext_lazy` proxy compares equal to
the string it wraps, so the autodetector sees no change to `choices`
(verified on Django 4.2, 5.2 and 6.1).

- **Django 6.1 support**, now claimed in the classifiers and exercised in CI
on Python 3.12 and 3.13. Django 6.x requires Python 3.12+, so the matrix
excludes it on 3.10/3.11; those interpreters keep Django 4.2 and 5.2, both
still LTS. No source change was needed — the suite already passed on 6.x.
(Django 6.0 passes too, but is not claimed or tested.)

### Changed

- The PyPI classifier is now `Development Status :: 5 - Production/Stable`,
up from `4 - Beta`.
- `publish.yml`'s pre-release smoke matrix now tests the newest supported
corner as Python 3.13 + Django 6.1, up from 3.13 + 5.2. The oldest corner
(3.10 + 4.2) is unchanged.

### Fixed

- `.gitignore`'s blanket `*.pot`/`*.mo` rules excluded the package's own
catalogs. Because hatchling honours `.gitignore` at build time, an ignored
catalog is also an unshipped one — Django would find no locale directory
in the installed package and silently fall back to English. Negations now
keep `django_mfa/locale/` tracked, and `test_packaging.py` asserts the
catalogs are in the built wheel.

## 4.2.0

### Added
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<a href="https://pypi.python.org/pypi/django-mfa"><img alt="PyPI" src="https://img.shields.io/pypi/v/django-mfa.svg"></a>
<a href="https://github.com/MicroPyramid/django-mfa/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/MicroPyramid/django-mfa/actions/workflows/ci.yml/badge.svg"></a>
<a href="https://pypi.python.org/pypi/django-mfa"><img alt="Python versions" src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue"></a>
<a href="https://pypi.python.org/pypi/django-mfa"><img alt="Django versions" src="https://img.shields.io/badge/django-4.2%20%7C%205.2-0C4B33"></a>
<a href="https://pypi.python.org/pypi/django-mfa"><img alt="Django versions" src="https://img.shields.io/badge/django-4.2%20%7C%205.2%20%7C%206.1-0C4B33"></a>
<a href="http://django-mfa.readthedocs.io/en/latest/"><img alt="Docs" src="https://readthedocs.org/projects/django-mfa/badge/?version=latest"></a>
<a href="https://github.com/MicroPyramid/django-mfa/blob/master/LICENSE"><img alt="License" src="https://img.shields.io/github/license/micropyramid/django-mfa.svg"></a>
</p>
Expand Down Expand Up @@ -209,13 +209,14 @@ this same API — there's no privileged path.
| | |
|---|---|
| **Python** | 3.10 · 3.11 · 3.12 · 3.13 |
| **Django** | 4.2 LTS · 5.2 LTS |
| **Django** | 4.2 LTS · 5.2 LTS · 6.1 |
| **Database** | Anything Django supports (state is a `JSONField`) |
| **Dependencies** | `fido2`, `qrcode`. TOTP is implemented in-package, not pulled in. |

Every combination in that grid runs the full suite in CI, along with a job that builds
the wheel, installs it into a clean environment, and starts Django against it from
outside the source tree.
Every combination runs the full suite in CI — except Django 6.1 on Python 3.10 or
3.11, which Django itself doesn't support (6.x requires Python 3.12+). Alongside it,
a job builds the wheel, installs it into a clean environment, and starts Django
against it from outside the source tree.

## Documentation

Expand Down
3 changes: 2 additions & 1 deletion django_mfa/adapters/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.utils.crypto import salted_hmac
from django.utils.translation import gettext_lazy as _

from django_mfa import ratelimit
from django_mfa.conf import settings as mfa_settings
Expand Down Expand Up @@ -100,7 +101,7 @@ def _is_fresh(state):

class EmailAdapter(Adapter):
type = Authenticator.Type.EMAIL
verbose_name = "Emailed code"
verbose_name = _("Emailed code")
supports_multiple = False
supports_enroll = True
counts_as_primary_factor = True
Expand Down
3 changes: 2 additions & 1 deletion django_mfa/adapters/recovery_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import string

from django.contrib.auth.hashers import check_password, make_password
from django.utils.translation import gettext_lazy as _

from django_mfa import events
from django_mfa.atomic import update_data
Expand All @@ -17,7 +18,7 @@

class RecoveryCodesAdapter(Adapter):
type = Authenticator.Type.RECOVERY_CODES
verbose_name = "Recovery codes"
verbose_name = _("Recovery codes")
# Recovery codes are exhaustible and must never be a user's sole second
# factor. They still appear in the verification picker (you can verify with
# one), but must not make primary_enabled_for() non-empty.
Expand Down
4 changes: 3 additions & 1 deletion django_mfa/adapters/totp.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import re
import secrets

from django.utils.translation import gettext_lazy as _

from django_mfa import totp as totp_mod
from django_mfa.atomic import update_data
from django_mfa.conf import settings as mfa_settings
Expand Down Expand Up @@ -42,7 +44,7 @@ def generate_secret():

class TOTPAdapter(Adapter):
type = Authenticator.Type.TOTP
verbose_name = "Authenticator app"
verbose_name = _("Authenticator app")

def begin_enroll(self, request):
secret = generate_secret()
Expand Down
3 changes: 2 additions & 1 deletion django_mfa/adapters/webauthn.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# every place this deviates from the brief's hypothesised calls.
import json

from django.utils.translation import gettext_lazy as _
from fido2.server import Fido2Server
from fido2.webauthn import (
AttestedCredentialData,
Expand Down Expand Up @@ -68,7 +69,7 @@ def user_entity(user):

class WebAuthnAdapter(Adapter):
type = Authenticator.Type.WEBAUTHN
verbose_name = "Security key or passkey"
verbose_name = _("Security key or passkey")
supports_multiple = True

def _existing_credentials(self, user):
Expand Down
Loading
Loading