fix(account): one form and one Save for your account; the same for an account's page in Users
CI / lint (push) Successful in 2s
CI / integration (push) Failing after 1m8s
CI / unit (push) Successful in 1m36s
CI / publish (push) Skipped

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>
This commit is contained in:
2026-10-08 16:05:41 -04:00
co-authored by Claude Opus 5.5
parent 31e32c173c
commit beae8a33f3
8 changed files with 102 additions and 106 deletions
+33 -31
View File
@@ -1,34 +1,36 @@
{# The email form for one account (milestone 495): its address, and whether
it gets alert emails and the weekly report. Used by the Account page (your
own) and an account's page in Settings › Users (an admin's view).
`defaults` is {kind: bool}, the account's role defaults. #}
{% macro email_choices(user, defaults, action, error=none, with_address=true) -%}
<form method="post" action="{{ action }}" class="form-page" id="email">{{ csrf_field() }}
<section class="card form-section" aria-labelledby="email-title">
<h2 id="email-title" class="section-title">Email</h2>
{% if error %}<p class="field-error" role="alert">{{ error }}</p>{% endif %}
{% if with_address %}
{# The Email section for one account (milestone 495): its address, and whether
it gets alert emails and the weekly report. A section of the page's one
form, saved by that form's Save: the Account page (your own) and an
account's page in Settings › Users (an admin's view, whose address sits
with its name). `defaults` is {kind: bool}, the account's role defaults;
`zone_note` says whose zone the emails' times are in. #}
{% macro email_section(user, defaults, zone_note, with_address=true, unreachable=false) -%}
<section class="card form-section" aria-labelledby="email-title" id="email">
<h2 id="email-title" class="section-title">Email</h2>
{% if with_address %}
<div class="field">
<label class="field-label" for="email-address">Address</label>
<input class="input" id="email-address" type="email" name="email" maxlength="255" value="{{ user.email }}" required>
{% if user.auth_source != "local" %}<span class="field-hint">Your sign-in provider set this first; change it if mail doesn't reach it.</span>{% endif %}
</div>
{% elif unreachable %}
<p class="field-hint tone-warn">{{ user.email }} isn't an address mail can reach, so this account gets no email. Set a real one above.</p>
{% endif %}
<div class="form-pair">
{% for kind, label in (("alerts", "Alert emails"), ("report", "Weekly report")) %}
{% set choice = user["notify_" ~ kind] %}
<div class="field">
<label class="field-label" for="email-address">Address</label>
<input class="input" id="email-address" type="email" name="email" maxlength="255" value="{{ user.email }}" required>
{% if user.auth_source != "local" %}<span class="field-hint">Your sign-in provider set this first; change it if mail doesn't reach it.</span>{% endif %}
<label class="field-label" for="email-{{ kind }}">{{ label }}</label>
<select class="input" id="email-{{ kind }}" name="notify_{{ kind }}">
<option value=""{% if choice is none %} selected{% endif %}>As for {{ user.role.value }}s ({{ "on" if defaults[kind] else "off" }})</option>
<option value="on"{% if choice is sameas true %} selected{% endif %}>On</option>
<option value="off"{% if choice is sameas false %} selected{% endif %}>Off</option>
</select>
</div>
{% endif %}
<div class="form-pair">
{% for kind, label in (("alerts", "Alert emails"), ("report", "Weekly report")) %}
{% set choice = user["notify_" ~ kind] %}
<div class="field">
<label class="field-label" for="email-{{ kind }}">{{ label }}</label>
<select class="input" id="email-{{ kind }}" name="notify_{{ kind }}">
<option value=""{% if choice is none %} selected{% endif %}>As for {{ user.role.value }}s ({{ "on" if defaults[kind] else "off" }})</option>
<option value="on"{% if choice is sameas true %} selected{% endif %}>On</option>
<option value="off"{% if choice is sameas false %} selected{% endif %}>Off</option>
</select>
</div>
{% endfor %}
</div>
<span class="field-hint">Times in these emails are in {{ user.time_zone or "UTC" }}. A role's default is set in Settings › Notifications.</span>
<button type="submit" class="btn btn-ghost">Save email settings</button>
</section>
</form>
{% endfor %}
</div>
<p class="field-hint">Times in these emails are in {{ zone_note }}.
{% if session.user_role == "admin" %}Each role's default is set in Settings › <a href="/settings/notifications/">Notifications</a>.
{%- else %}An admin sets each role's default.{% endif %}</p>
</section>
{%- endmacro %}
+10 -7
View File
@@ -1,29 +1,32 @@
{# account/index.html — the signed-in user's own account (milestone 490): who
they are, and the time zone every page writes its times in. #}
they are, the time zone every page writes its times in, and their email
(milestone 495). One form, one Save. #}
{% extends "base.html" %}
{% from "_macros.html" import page_header, zone_select %}
{% from "account/_email_choices.html" import email_choices with context %}
{% from "account/_email_choices.html" import email_section with context %}
{% block title %}Your account — Steward{% endblock %}
{% block content %}
{{ page_header("Your account") }}
<p class="page-verdict">
Signed in as <strong>{{ user.username }}</strong> ({{ user.role.value }}) with {{ source }}
· times show in <strong>{{ zone_name }}</strong> ({{ zone_label }})
· times show in <strong>{{ zone_title }}</strong> ({{ zone_label }})
</p>
{% if error %}<div class="alert alert-error" role="alert">{{ error }}</div>{% endif %}
<form method="post" action="/account/" class="form-page">{{ csrf_field() }}
<section class="card form-section" aria-labelledby="acct-time">
<h2 id="acct-time" class="section-title">Time zone</h2>
{% if error %}<p class="field-error" role="alert">{{ error }}</p>{% endif %}
<div class="field">
<label class="field-label" for="acct-tz">Show times in</label>
{{ zone_select("time_zone", "acct-tz", zone_name, zones) }}
<span class="field-hint">Every page writes its times in this zone, on a 24-hour clock.</span>
</div>
</section>
<button type="submit" class="btn">Save</button>
</form>
{{ email_choices(user, defaults, "/account/email", email_error) }}
{{ email_section(user, defaults, "your time zone") }}
<div class="form-actions"><button type="submit" class="btn">Save</button></div>
</form>
{% endblock %}
+6 -5
View File
@@ -5,7 +5,7 @@
{% extends "base.html" %}
{% from "_macros.html" import crumbs, page_header, status_badge %}
{% from "settings/_tabs.html" import settings_tabs %}
{% from "account/_email_choices.html" import email_choices with context %}
{% from "account/_email_choices.html" import email_section with context %}
{% block title %}{{ user.username }} — Users — Steward{% endblock %}
{% block section_tabs %}{{ settings_tabs("users") }}{% endblock %}
{% block breadcrumb %}{{ crumbs([("Settings", "/settings/"), ("Users", "/settings/users/"), (user.username, "")]) }}{% endblock %}
@@ -38,12 +38,13 @@
</div>
</div>
{% if not local %}<p class="field-hint">{{ source }} finds this account by its own id, not by name, so renaming it here does not stop it signing in.</p>{% endif %}
<button type="submit" class="btn">Save</button>
</section>
</form>
{% if not reachable %}<p class="field-hint tone-warn">{{ user.email }} isn't an address mail can reach, so this account gets no email. Set a real one above.</p>{% endif %}
{{ email_choices(user, defaults, "/settings/users/" ~ user.id ~ "/email", with_address=false) }}
{% set zone = (user.time_zone or "UTC").split("/", 1)[-1]|replace("_", " ") %}
{{ email_section(user, defaults, "this account's own zone (" ~ zone ~ ")", with_address=false, unreachable=not reachable) }}
<div class="form-actions"><button type="submit" class="btn">Save</button></div>
</form>
<section class="card form-section" aria-labelledby="u-role">
<h2 id="u-role" class="section-title">Role</h2>