Fix empty allauth forms, and lay out the 2FA page

The 2FA code input was not invisible -- it was absent, along with the fields of
every other allauth form except login.

Cause: the `fields` element passed `attrs.exclude` straight into a filter. On a
page that never sets it, resolving a filter *argument* raises
VariableDoesNotExist; Django rescues that for the main variable of an expression
but not for a filter argument, and {% if %} then swallows it and reads the
condition as false. So every field was skipped. Login was the one page that
passes `exclude`, which is exactly why it kept working and hid the damage.
`exclude` is now pinned to a real variable first, with tests that render the
login, signup and password-reset forms and assert their inputs exist.

Two dangling buttons fixed while in here: `elements/form.html` dropped the `id`
attribute, so the out-of-band forms allauth generates (webauthn_form,
logout-from-stage) had no id for a button's `form` attribute to point at. "Use a
security key" submitted nothing.

Layout: the code is a daisyUI otp field, Cancel sits beside Sign In as a plain
button, both gain icons, and "Use a security key" becomes an accent button.

The otp boxes yield once more than six characters are typed. allauth accepts a
TOTP code (6) *or* a recovery code (8) in this one field, so hard-boxing it to
six would have locked out every recovery code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 22:59:00 +02:00
parent b3f153a2dc
commit 2b7b2b64db
6 changed files with 366 additions and 6 deletions

View File

@@ -17,9 +17,17 @@
<span>{{ error }}</span>
</div>
{% endfor %}
{% comment %}
`exclude` lets a page lay a field out itself — the login page puts "remember" on the
button row. It is pinned to a real variable first, and that is not cosmetic: passing
`attrs.exclude` straight into a filter on a page that never set it raises
VariableDoesNotExist, which Django catches for the *main* variable of an expression
but not for a filter *argument*. {% if %} then swallows it and reads the condition as
false — silently dropping every field on every form that doesn't pass `exclude`.
{% endcomment %}
{% with exclude=attrs.exclude|default:"" %}
{% for field in attrs.form.visible_fields %}
{# `exclude` lets a page lay a field out itself — the login page puts "remember" on the button row. #}
{% if not field|excluded:attrs.exclude %}
{% if not field|excluded:exclude %}
<div class="form-control my-3 w-full">
{% if field.field.widget.input_type == "checkbox" %}
<label class="label cursor-pointer justify-start gap-3" for="{{ field.id_for_label }}">
@@ -55,3 +63,4 @@
</div>
{% endif %}
{% endfor %}
{% endwith %}

View File

@@ -1,9 +1,18 @@
{% load allauth %}
{% comment %}
`id` is not decorative: allauth renders out-of-band forms (webauthn, logout-from-stage)
and points buttons at them with the HTML `form` attribute. Drop the id and those
buttons submit nothing.
{% endcomment %}
<form method="{{ attrs.method }}"
{% if attrs.id %}id="{{ attrs.id }}"{% endif %}
{% if attrs.action %}action="{{ attrs.action }}"{% endif %}
class="space-y-2">
{% slot body %}{% endslot %}
<div class="card-actions justify-end pt-3">
{% slot actions %}{% endslot %}
</div>
{% if not attrs.no_visible_fields %}
<div class="card-actions justify-end pt-3">
{% slot actions %}{% endslot %}
</div>
{% endif %}
</form>

View File

@@ -1 +1 @@
{% comment %} djlint:off {% endcomment %}{% load allauth %}<h1 class="card-title text-2xl">{% slot %}{% endslot %}</h1>
{% comment %} djlint:off {% endcomment %}{% load allauth %}<h1 class="card-title text-2xl border-b border-base-content pb-2 mb-2">{% slot %}{% endslot %}</h1>