Files
FabledSteward/steward/account/routes.py
T
bvandeusenandClaude Opus 5.5 beae8a33f3
CI / lint (push) Successful in 2s
CI / integration (push) Failing after 1m8s
CI / unit (push) Successful in 1m36s
CI / publish (push) Skipped
fix(account): one form and one Save for your account; the same for an account's page in Users
Operator review of the Account page: two sections with two different
save buttons, one stuck to the next card and one beside a squeezed hint.
- Account: time zone and Email are one form; POST /account/ saves both,
  nothing unless all of it is valid. /account/email is gone.
- Users › account: Name and Email are one form; the rename route also
  saves the email switches. /settings/users/<id>/email is gone.
- The Email section is a plain section now (email_section); its hint sits
  under the switches, names the zone as the picker does, and links
  Notifications only for an admin.
- apply_email_form leaves a switch the form doesn't carry as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:05:41 -04:00

97 lines
3.8 KiB
Python

"""The signed-in user's own account (milestone 490): the time zone every
page writes its times in, and their email (milestone 495). Any role reaches
it, for themselves only."""
from __future__ import annotations
from quart import Blueprint, abort, current_app, redirect, render_template, request, session
from steward.auth.middleware import require_role
from steward.core import formatting
from steward.models.users import User, UserRole
account_bp = Blueprint("account", __name__, url_prefix="/account")
_SOURCES = {"local": "a Steward password", "ldap": "LDAP", "oidc": "single sign-on"}
async def _me(db) -> User:
# A share link opens widgets read-only; it has no account.
user_id = session.get("user_id")
if not user_id:
abort(403)
user = await db.get(User, user_id)
if user is None:
abort(403)
return user
@account_bp.get("/")
@require_role(UserRole.viewer)
async def account(error: str | None = None, status: int = 200):
from steward.core.mail import role_defaults
from steward.core.settings import get_all_settings
async with current_app.db_sessionmaker() as db:
user = await _me(db)
defaults = role_defaults(user, await get_all_settings(db))
tz = formatting.zone(user.time_zone)
zone_name = user.time_zone or "UTC"
return await render_template(
"account/index.html", user=user, source=_SOURCES.get(user.auth_source, user.auth_source),
zone_name=zone_name, zone_title=zone_name.split("/", 1)[-1].replace("_", " "),
zone_label=formatting.tz_label(tz), zones=formatting.zones_by_region(),
defaults=defaults, error=error), status
@account_bp.post("/")
@require_role(UserRole.viewer)
async def account_save():
"""The page's one form: your time zone, your address and whether you're
emailed alerts and the weekly report (milestone 495). Nothing is saved
unless all of it is valid."""
from sqlalchemy.exc import IntegrityError
from steward.core.audit import log_audit
from steward.core.mail import apply_email_form
form = await request.form
name = (form.get("time_zone") or "").strip()
if name not in formatting.zone_names():
return await account(f"{name or 'That'} is not a time zone Steward knows.", 400)
try:
async with current_app.db_sessionmaker() as db:
async with db.begin():
user = await _me(db)
changes, error = apply_email_form(user, form)
if error:
raise ValueError(error)
before = user.time_zone
user.time_zone = name
except ValueError as exc:
return await account(str(exc), 400)
except IntegrityError:
return await account("Another account already uses that address.", 400)
session["tz"] = name
if before != name:
await log_audit(current_app, user.id, user.username, "account.time_zone",
detail={"time_zone": name, "was": before or "UTC"})
if changes:
await log_audit(current_app, user.id, user.username, "account.email", detail=changes)
return redirect("/account/")
@account_bp.post("/time-zone/adopt")
@require_role(UserRole.viewer)
async def adopt_time_zone():
"""The browser's zone, saved for an account that has none yet: base.html
sends it once, on the first page after a sign-in without one. A zone set
on the Account page is never replaced. Answers whether it was saved."""
name = ((await request.get_json(silent=True)) or {}).get("time_zone") or ""
if name not in formatting.zone_names():
return {"adopted": False}, 400
async with current_app.db_sessionmaker() as db:
async with db.begin():
user = await _me(db)
if user.time_zone is not None:
return {"adopted": False}
user.time_zone = name
session["tz"] = name
return {"adopted": True}