"""The signed-in user's own account (milestone 490): the time zone every page writes its times in. Any role reaches it, for themselves only.""" from __future__ import annotations from itertools import groupby 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"} def _zones_by_region() -> list[tuple[str, list[str]]]: """The Account page's choices: UTC alone, then every zone under its region ('America', 'Europe', …).""" named = [z for z in formatting.zone_names() if "/" in z] return [("", ["UTC"])] + [(region, list(zones)) for region, zones in groupby(named, key=lambda z: z.split("/", 1)[0])] 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): async with current_app.db_sessionmaker() as db: user = await _me(db) tz = formatting.zone(user.time_zone) return await render_template( "account/index.html", user=user, source=_SOURCES.get(user.auth_source, user.auth_source), zone_name=user.time_zone or "UTC", zone_label=formatting.tz_label(tz), zones=_zones_by_region(), error=error), status @account_bp.post("/") @require_role(UserRole.viewer) async def account_save(): from steward.core.audit import log_audit name = ((await request.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) async with current_app.db_sessionmaker() as db: async with db.begin(): user = await _me(db) before = user.time_zone user.time_zone = name 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"}) 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}