allauth sets placeholder="Code" on the field. Grey text sitting inside the otp
boxes reads as an already-typed code, so it goes -- the sr-only label outside the
box still names the field.
Suppressed with placeholder=False rather than by deleting the key: as_widget()
merges the widget's own attrs back in at render time, so popping it from a copy
does nothing. Django's attribute template omits attrs whose value is False.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The MFA pages (manage, TOTP activate/deactivate, recovery codes, security keys,
reauthenticate) are built almost entirely from allauth's `element` primitives, so
they are styled by overriding the elements rather than by rewriting eight page
templates. New allauth pages then inherit the look for free.
- field + img elements were missing entirely, so allauth fell back to bare HTML:
the TOTP secret and recovery-code list rendered as unstyled inputs. The QR now
sits on a white plate -- it is dark modules on a transparent ground, so on the
dark theme it was dark-on-dark and phones could not scan it.
- button now honours the tags allauth sets. They were all flattened to
btn-primary, which made "Deactivate" look exactly as safe as "View".
- the `code` field renders as a daisyUI otp wherever it appears, so the
reauthenticate and activate pages get the same input as the login challenge.
The boxes step aside past six characters: allauth accepts a TOTP code (6) or a
recovery code (8) in that one field.
Fixes a crash: the security-key list does {% load humanize %}, which raised
TemplateSyntaxError because django.contrib.humanize was not installed. That page
500'd on every request; it is now installed and covered by a test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Sign In and "Sign in with a passkey" (btn-accent) now sit side by side with
"Remember Me" on the same row, and the email/password block is given room above
and below.
The passkey button was dead. It submits a *different* form -- the hidden
`mfa_login` that allauth renders from its `extra_body` block -- and our layout
base never defined that block, so neither the form nor the webauthn script was
ever emitted and clicking the button did nothing. _base.html now has the block,
and there is a test asserting the form and script are on the page.
The `fields` element grows an `exclude`, so a page can lay a field out itself
(here: "remember", moved onto the button row). It splits on commas rather than
testing for a substring -- "password" is a substring of "password2", and a page
excluding one would otherwise silently drop the other.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The entrance forms lose their visible field labels and gain an icon inside each
field (mail, lock), and allauth's "Forgot your password?" link -- which is the
password field's help_text -- is spaced away from the input instead of sitting
flush against it.
Three things this depends on:
- allauth already passes `unlabeled=True` on the entrance forms and already sets
a placeholder on every field there, so the visible label was redundant. The
label is still emitted sr-only: a placeholder is not a label, and it vanishes
as soon as you type.
- daisyUI's icon-in-field layout puts the `input` class on the *wrapping label*,
so the input itself must carry only `grow` -- hence the optional css override on
the daisy filter. `input` on both draws a box inside a box, and the error state
belongs on the wrapper for the same reason.
- The help text now carries id="<auto_id>_helptext". Django points the input's
aria-describedby at exactly that id, so without it the reference dangled and a
screen reader never announced the password-reset link.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A club member signing in at ajax-united.rosterchief.app now sees their club's
logo, name and colours; the base domain keeps the RosterChief skin for the
control panel and Django admin.
The mechanism is `{% extends base_template %}` -- Django lets the parent be a
context variable, so the `branding` context processor picks the skin from
request.club and *every* auth screen allauth ships (login, password reset, MFA,
passkeys, and whatever it adds next) follows the tenant without a single one of
them knowing that clubs exist.
Templates split three ways: _base.html is the skeleton with no branding, and
_platform_base.html / _club_base.html dress it. The control panel extends the
platform base *explicitly* rather than through the variable, so a bug in
branding resolution can never dress the panel up as a club.
Club gains an optional logo and primary_color. Notes on both:
- No logo falls back to the club's initials, never the RosterChief mark, which
would pass our branding off as theirs.
- Club colours land in an inline :root. daisyUI declares its theme variables
inside `@layer base`, and unlayered styles beat every layered rule regardless
of specificity, so this needs no !important. --color-primary-content is derived
from WCAG relative luminance, so a club that picks pale yellow gets black text
instead of invisible white.
- primary_color is a text input, not <input type="color">: a colour picker cannot
express "no colour", so every club that never touched it would submit #000000
and silently get a black theme.
"/" now resolves per tenant (club home, or hand off to the control panel), which
is why LOGIN_REDIRECT_URL can stay "/" and allauth needs no redirect adapter.
Also folds in the theme toggle gaining a third "auto" state and the logo
switching from `content:` to background-image (content-replacement on a real
element is not supported in Firefox), both of which lived in the base template
this commit replaces.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds django-browser-reload: runserver already restarts on Python changes, but
the browser had to be refreshed by hand for every template or CSS edit. It also
watches static/, so a Tailwind rebuild now refreshes the page on its own.
Mounted only under DEBUG -- it injects a script into every HTML response and
serves an open event stream, neither of which belongs in production; a test
holds that line. Its endpoint is exempt from RequireMFAMiddleware, otherwise a
not-yet-enrolled staff user has the stream redirected away and live reload dies
on the MFA enrolment page, which is exactly a page we are restyling.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The clubmanager.app domain was taken, so the platform is now RosterChief
(rosterchief.app). Renames the Django project package clubmanager/ ->
rosterchief/ (git tracks it as a move, so history follows), every
`from rosterchief.base import ...`, the settings/wsgi/asgi module paths,
env vars (ROSTERCHIEF_BASE_DOMAIN / ROSTERCHIEF_RP_NAME), the MFA adapter
(RosterChiefMFAAdapter), brand text, and the docs.
Two things were deliberately NOT swept:
- club.models.ClubManager stays: it is the Django manager *for Club*, not the
brand. A blind rename would have silently broken it.
- Migrations are untouched (history is not rewritten). The only reference was a
cosmetic help_text, so a normal AlterField migration carries the new domain.
Note the WebAuthn RP ID is the base domain, so moving to rosterchief.app
cryptographically invalidates any passkey enrolled under the old one; they
cannot be migrated and must be re-enrolled. Nothing is in production, so the
real cost is zero.
Add django-lucide (from bsiebens/lucide) for icons: the theme toggle now swaps
sun/moon against the effective theme, and the control panel gets icons on its
tabs, actions and stat groups. Its classifiers stop at Django 5.0, but that is
stale metadata — verified rendering on Django 6 / Python 3.14.
Also add formbuilder, shop and controlpanel to ruff's known-first-party list,
which had drifted behind the apps that landed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the `controlpanel` app: a platform-wide (not club-scoped) admin panel for
creating clubs, archiving/restoring them, managing club admins, and per-club
statistics (members, teams & staff, events, shop). Statistics are annotated in
one query so the club list cannot fan out into N+1, and are returned as stat
*groups* so growing the domain means adding one entry.
Two access rules, both enforced by PlatformStaffRequiredMixin:
- staff only (is_staff/is_superuser); anonymous are sent to login, signed-in
non-staff get a 403. Staff already need a second factor, so the panel is
2FA-protected for free.
- base domain only: the panel manages *all* clubs, so it 404s if the tenant
middleware resolved a club from the subdomain.
Granting admin to an unknown email creates the account (unusable password —
they set one via password reset) and the Member behind it, since a ClubRole
hangs off a Member. A member who already holds a role is promoted in place,
because there is only one role per member per club.
UI is Tailwind + daisyUI. allauth ships an element system, so overriding
allauth/layouts/base.html plus ~13 element partials restyles *every* auth and
2FA screen at once — login, signup, password reset, the 2FA challenge, TOTP
enrolment, passkeys and recovery codes — rather than templating 20+ pages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>