feat: times in the reader's own zone; the audit log shows exact moments (milestone 490)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m0s
CI / unit (push) Failing after 1m23s
CI / publish (push) Skipped

- users.time_zone (0034). An account without one takes the browser's zone
  on its first page (POST /account/time-zone/adopt, never over a choice made
  by hand); the session follows the account on every request.
- /account: who you are and the zone every page writes its times in,
  reached from your name in the sidebar.
- formatting.when: 'Today 06:58', 'Yesterday 23:41', 'Tue 6 Oct 14:12',
  '6 Oct 2025 14:12', 24-hour, in the reader's zone; |ago's hover is the
  exact local moment instead of UTC; tz_label() for column headers.
- Audit log: When (EDT) with exact times, one detail fact a line, on classes.
- tzdata in the image, and the airgapped smoke checks a zone resolves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 07:56:27 -04:00
co-authored by Claude Opus 5.5
parent 955450d33f
commit 6a81cf4aac
19 changed files with 425 additions and 25 deletions
+45 -3
View File
@@ -44,17 +44,59 @@ def test_duration(secs, text):
(NOW + timedelta(minutes=5), "in 5m"),
(NOW, "just now"),
])
def test_ago_reads_relative_and_titles_the_absolute_utc_time(when, text):
def test_ago_reads_relative_and_titles_the_exact_moment(when, text):
out = str(f.ago(when, now=NOW))
stamp = when.replace(tzinfo=timezone.utc) if when.tzinfo is None else when
assert out == (f'<time datetime="{stamp:%Y-%m-%dT%H:%M:%SZ}" '
f'title="{stamp:%Y-%m-%d %H:%M:%S} UTC">{text}</time>')
f'title="{stamp:%a} {stamp.day} {stamp:%b %Y %H:%M:%S} UTC">{text}</time>')
def test_ago_of_nothing_and_of_another_zone():
assert str(f.ago(None)) == "—"
est = timezone(timedelta(hours=-4))
assert 'title="2026-10-06 18:00:00 UTC"' in str(f.ago(NOW.astimezone(est), now=NOW))
assert 'title="Tue 6 Oct 2026 18:00:00 UTC"' in str(f.ago(NOW.astimezone(est), now=NOW))
# Milestone 490: a moment in the reader's own zone, on a 24-hour clock.
NY = f.zone("America/New_York")
def test_ago_hovers_the_moment_in_the_readers_zone():
assert 'title="Tue 6 Oct 2026 13:57:00 EDT">3m ago<' in str(
f.ago(NOW - timedelta(minutes=3), now=NOW, tz=NY))
@pytest.mark.parametrize("moment, text, title", [
(NOW - timedelta(hours=1), "Today 13:00", "Tue 6 Oct 2026 13:00:00 EDT"),
# 03:30 UTC on the 6th is 23:30 on the 5th in New York: the zone moves the day.
(datetime(2026, 10, 6, 3, 30, tzinfo=timezone.utc), "Yesterday 23:30", "Mon 5 Oct 2026 23:30:00 EDT"),
(datetime(2026, 10, 7, 6, 0, tzinfo=timezone.utc), "Tomorrow 02:00", "Wed 7 Oct 2026 02:00:00 EDT"),
(datetime(2026, 9, 30, 12, 0, tzinfo=timezone.utc), "Wed 30 Sep 08:00", "Wed 30 Sep 2026 08:00:00 EDT"),
# Before this year the year shows; in winter the zone is EST, an hour further back.
(datetime(2025, 12, 25, 12, 0, tzinfo=timezone.utc), "25 Dec 2025 07:00", "Thu 25 Dec 2025 07:00:00 EST"),
(datetime(2026, 10, 6, 21, 5), "Today 17:05", "Tue 6 Oct 2026 17:05:00 EDT"), # naive is UTC
])
def test_when_writes_the_exact_moment_in_the_readers_zone(moment, text, title):
stamp = moment.replace(tzinfo=timezone.utc) if moment.tzinfo is None else moment
assert str(f.when(moment, NY, now=NOW)) == (
f'<time datetime="{stamp:%Y-%m-%dT%H:%M:%SZ}" title="{title}">{text}</time>')
def test_when_is_a_24_hour_clock_and_utc_without_a_zone():
assert ">Today 18:00<" in str(f.when(NOW, now=NOW))
assert ">Today 23:59<" in str(f.when(NOW.replace(hour=23, minute=59), now=NOW))
assert str(f.when(None)) == "—"
def test_zone_falls_back_to_utc_and_names_itself():
assert f.zone(None) is timezone.utc and f.zone("") is timezone.utc
assert f.zone("Not/AZone") is timezone.utc and f.zone("../etc/passwd") is timezone.utc
assert f.tz_label(NY, now=NOW) == "EDT"
assert f.tz_label(NY, now=datetime(2026, 1, 15, tzinfo=timezone.utc)) == "EST"
assert f.tz_label(None) == "UTC"
names = f.zone_names()
assert "America/New_York" in names and "Europe/London" in names and "UTC" in names
assert list(names) == sorted(names)
@pytest.mark.parametrize("value, kind, cls", [