Skip to content

Fix "Log in as guest" on the login page - #26

Merged
tanhtm merged 8 commits into
main2.0from
fix/guest-login-button
Oct 17, 2026
Merged

tanhtm merged 8 commits into
main2.0from
fix/guest-login-button

Conversation

@tanhtm

@tanhtm tanhtm commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Problem

  • The guest button used bg-secondary-600, which is not compiled into styles.css, so it had no background and read as plain text.
  • The button showed even without a usable guest account. Clicking it then did nothing: GuestLoginView swallowed every error and bounced back to the login page with no message.

Changes

  • Button: uses Unfold's secondary button, the same style as the other login buttons. The icon is aria-hidden. The button renders only when {% guest_login_available %} (new wn_guest tag) finds an active staff guest. Because the check lives in the template, it works on any admin site that renders admin/login.html.
  • Shared helpers in base/utils.py, used by the view, AutoGuestLoginMiddleware and the tag:
    • get_guest_user(): logs DB errors and returns None, so it never breaks the login page.
    • log_in_guest() keeps the login in only one failure case: a read-only database (demo mode) refuses the last_login write after the session is already set (is_readonly_db_error). That is also how main2.0 behaves.
      • Any other DatabaseError is re-raised.
      • On failure, the previous session is restored in memory and not saved. A logged-in user whose switch fails (DB-backed sessions on a read-only DB) stays logged in, with no 500 from SessionMiddleware.
    • safe_next_url(): same host and same scheme, so there is no HTTPS→HTTP redirect.
  • GuestLoginView:
    • login_not_required, so it works with Django's LoginRequiredMiddleware.
    • On failure it shows "Guest login is not available." (vi translation added) and returns to the login page with the original ?next.
    • Any logged-in user on the login page may switch to guest, staff included (they see the page after a failed password POST). Before, the button just bounced them back.
  • AutoGuestLoginMiddleware:
    • Redirects to ?next as a URL. redirect() treated it as a route name, so ?next=foo raised NoReverseMatch.
    • Logs failures instead of except: pass.
  • CSS:
    • styles.css is rebuilt; the old button's three utilities are gone.
    • Tailwind no longer scans its own output, which kept removed utilities' theme variables alive. Rebuilds were not reproducible, so the "Compiled CSS is up to date" check would have failed.

Testing

  • GuestLoginButtonTests and AutoGuestLoginMiddlewareTests (13 tests). Each one was checked to fail without the change it covers.
  • Full suite: 70 tests pass with SKIP_GUEST_LOGIN=True and with False. django check is clean. bash scripts/tailwind.sh twice in a row gives a byte-identical styles.css.
  • The failed-switch case was also run with SESSION_ENGINE=db and a read-only session store:
    • Before the fix: 500.
    • After: 302 back to login, and the user is still logged in.
  • Ran the app in Chromium:
    • The button is styled in light and dark themes and logs in as guest.
    • With the guest deactivated, the button is hidden, and a stale POST shows the error message.

Not changed

  • AutoGuestLoginMiddleware is enabled when SKIP_GUEST_LOGIN=True. That is the opposite of what the name and docs/GUIDE.md say, but changing it would affect existing projects.
  • The admin: URL namespace is hard-coded, as everywhere else in the package.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YXGgGiATURgjVcvhP18qJ5

bg-secondary-600 is not compiled into styles.css, so the guest button had
no background. Use Unfold's secondary button, and only render it when an
active staff 'guest' user exists (otherwise clicking it did nothing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXGgGiATURgjVcvhP18qJ5
…lures

- get_guest_user() in base/utils replaces three hand-copied queries
  (GuestLoginView, AutoGuestLoginMiddleware, login button)
- {% guest_login_available %} tag decides the button in the template itself, so
  it works on any admin site rendering admin/login.html; hidden for
  authenticated users (GuestLoginView only redirects them) and on lookup
  errors (logged, never breaks the login page)
- GuestLoginView shows an error and returns to the login page instead of
  silently bouncing; exceptions are logged rather than swallowed
- icon is aria-hidden; tests cover inactive/non-staff guest, authenticated
  user, lookup error and the view itself

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXGgGiATURgjVcvhP18qJ5
…, log errors

- GuestLoginView is login_not_required (LoginRequiredMiddleware bounced the
  POST to the login page without logging in)
- on failure, return to the login page with the original ?next
- next must match the request scheme (no HTTPS -> HTTP downgrade), as in
  Django's LoginView and AutoGuestLoginMiddleware
- AutoGuestLoginMiddleware logs failures instead of swallowing them; drop the
  now-unused User import
- guest_login_available reads RequestContext.request, so it works without the
  request context processor and under {% include ... only %}
- tests build the form action with reverse() and cover each of the above

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXGgGiATURgjVcvhP18qJ5
- log_in_guest(): in read-only database (demo) mode saving last_login fails
  after the session is set; keep the login instead of bouncing to the login
  page with a readonly warning and losing next (main2.0 behaviour)
- get_guest_user() handles DB errors itself (logged, None); the template
  tag and view no longer duplicate the try/except
- GuestLoginView only short-circuits staff; a logged-in non-staff account on
  the login page may switch to guest, so the tag needs no request at all
- AutoGuestLoginMiddleware redirects to ?next as a URL (redirect() reversed it
  as a route name: NoReverseMatch, now logged on every hit)
- styles.css rebuilt: the old button's border-transparent, text-gray-900 and
  group-hover:-translate-x-1 are no longer used (CI assets check)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXGgGiATURgjVcvhP18qJ5
… stable CSS build

- GuestLoginView no longer short-circuits authenticated users: the login page
  renders for staff too (after a failed password POST), and login() replaces
  the session, so the button never just bounces back
- a DatabaseError inside login() (DB-backed sessions on a read-only DB) shows
  'Guest login is not available.' instead of a 500
- safe_next_url() in base/utils replaces the two copies of the next check;
  helpers imported at module level
- read-only test swaps update_last_login by its dispatch_uid (disconnect by
  function was a no-op and left a duplicate receiver)
- tailwind no longer scans its own output, which kept removed utilities'
  theme variables alive so rebuilds were not reproducible (CI assets check)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXGgGiATURgjVcvhP18qJ5
…n on failure

- log_in_guest() keeps the login only when the error is a read-only database
  write after the session is set (is_readonly_db_error); any other
  DatabaseError (e.g. IntegrityError from a user_logged_in receiver) is
  re-raised and shown as 'Guest login is not available.' instead of being
  logged as a read-only warning
- on failure the previous session is restored in memory and not saved, so a
  logged-in user switching to guest on a read-only DB with DB-backed sessions
  stays logged in instead of hitting a 500 from SessionMiddleware

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXGgGiATURgjVcvhP18qJ5
…ailed login

- after a non-read-only DatabaseError, request.user is reset to the previous
  user: it stayed the guest, so with AutoGuestLoginMiddleware the admin login
  page redirected to /admin/ and back, forever
- the restored session is saved when flush() completed (old row deleted, no
  key), otherwise a logged-in user with DB-backed sessions was logged out; it
  is still not written when the old key survived (read-only delete)
- tests use the real login() with a failing user_logged_in receiver

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXGgGiATURgjVcvhP18qJ5
- save the restored session whenever login() changed the key: cycle_key()
  (anonymous user) also deletes the old row, not only flush()
- getattr() for request.user in the read-only check, so a request without
  .user re-raises the database error instead of AttributeError

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXGgGiATURgjVcvhP18qJ5
@tanhtm
tanhtm merged commit c61ac63 into main2.0 Oct 17, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants