feat(timeago): add format_time_html returning a <time> element

Wrap humanize_delta in an HTML <time> element with datetime= and
title= attributes carrying the precise UTC value, so hovering surfaces
the exact timestamp regardless of the relative label.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mwiegand 2026-05-16 11:09:23 +02:00
parent 237f26e5cb
commit 1926fe895c
No known key found for this signature in database
2 changed files with 51 additions and 1 deletions

View file

@ -1,5 +1,7 @@
from datetime import UTC, datetime from datetime import UTC, datetime
from markupsafe import Markup, escape
_MONTHS = ( _MONTHS = (
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
@ -51,3 +53,18 @@ def humanize_delta(then: datetime, now: datetime | None = None) -> str:
return _format_date(then, now) return _format_date(then, now)
return _relative_label(abs_seconds, past=(delta_seconds > 0)) return _relative_label(abs_seconds, past=(delta_seconds > 0))
def format_time_html(then: datetime, now: datetime | None = None) -> Markup:
if now is None:
now = datetime.now(UTC)
then_utc = _ensure_utc(then).astimezone(UTC)
now = _ensure_utc(now)
label = humanize_delta(then_utc, now=now)
iso = then_utc.isoformat()
title = then_utc.strftime("%Y-%m-%d %H:%M:%S UTC")
return Markup(
f'<time datetime="{escape(iso)}" title="{escape(title)}">'
f"{escape(label)}</time>"
)

View file

@ -1,8 +1,9 @@
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
import pytest import pytest
from markupsafe import Markup
from l4d2web.services.timeago import humanize_delta from l4d2web.services.timeago import format_time_html, humanize_delta
NOW = datetime(2026, 5, 16, 12, 0, 0, tzinfo=UTC) NOW = datetime(2026, 5, 16, 12, 0, 0, tzinfo=UTC)
@ -82,3 +83,35 @@ def test_humanize_delta_year_boundary_includes_year_when_years_differ():
now = datetime(2026, 1, 15, 12, 0, 0, tzinfo=UTC) now = datetime(2026, 1, 15, 12, 0, 0, tzinfo=UTC)
then = datetime(2025, 11, 15, 12, 0, 0, tzinfo=UTC) then = datetime(2025, 11, 15, 12, 0, 0, tzinfo=UTC)
assert humanize_delta(then, now=now) == "15 Nov 2025" assert humanize_delta(then, now=now) == "15 Nov 2025"
def test_format_time_html_returns_markup():
then = NOW - timedelta(minutes=5)
out = format_time_html(then, now=NOW)
assert isinstance(out, Markup)
def test_format_time_html_contains_time_element_with_attrs():
then = datetime(2026, 5, 16, 14, 32, 11, tzinfo=UTC)
now = then + timedelta(minutes=5)
out = str(format_time_html(then, now=now))
assert out.startswith("<time ")
assert out.endswith("</time>")
assert 'datetime="2026-05-16T14:32:11+00:00"' in out
assert 'title="2026-05-16 14:32:11 UTC"' in out
assert ">5 minutes ago<" in out
def test_format_time_html_label_matches_humanize_delta():
then = NOW - timedelta(hours=2)
label = humanize_delta(then, now=NOW)
out = str(format_time_html(then, now=NOW))
assert f">{label}<" in out
def test_format_time_html_normalises_naive_input_to_utc():
then_naive = datetime(2026, 5, 16, 14, 32, 11)
now = datetime(2026, 5, 16, 14, 37, 11, tzinfo=UTC)
out = str(format_time_html(then_naive, now=now))
assert 'datetime="2026-05-16T14:32:11+00:00"' in out
assert 'title="2026-05-16 14:32:11 UTC"' in out