diff --git a/src/wuttaweb/diffs.py b/src/wuttaweb/diffs.py
index cb59ed5..23eaa7e 100644
--- a/src/wuttaweb/diffs.py
+++ b/src/wuttaweb/diffs.py
@@ -25,6 +25,7 @@ Tools for displaying simple data diffs
"""
import sqlalchemy as sa
+from sqlalchemy import orm
from pyramid.renderers import render
from webhelpers2.html import HTML
@@ -150,6 +151,25 @@ class VersionDiff(WebDiff):
# be embedded within a more complex result.
text = HTML.tag("span", c=[repr(value)], style="font-family: monospace;")
+ # style to apply for bold human-friendly text (if applicable)
+ bold = "margin-left: 2rem; font-style: italic; font-weight: bold;"
+
+ # check for standard datetime field
+ if self.mapper.has_property(field):
+ prop = self.mapper.get_property(field)
+ if isinstance(prop, orm.ColumnProperty):
+ if len(prop.columns) == 1:
+ col = prop.columns[0]
+ if isinstance(col.type, sa.DateTime):
+ if value:
+ # render as local datetime w/ "time since" tooltip
+ display = HTML.tag(
+ "span",
+ c=self.app.render_datetime(value, html=True),
+ style=bold,
+ )
+ return HTML.tag("span", c=[text, display])
+
# loop thru all mapped relationship props
for prop in self.mapper.relationships:
@@ -176,12 +196,9 @@ class VersionDiff(WebDiff):
if ref := getattr(ref, "version_parent", None):
# render text w/ related object as bold string
- style = (
- "margin-left: 2rem; font-style: italic; font-weight: bold;"
- )
return HTML.tag(
"span",
- c=[text, HTML.tag("span", c=[str(ref)], style=style)],
+ c=[text, HTML.tag("span", c=[str(ref)], style=bold)],
)
return text
diff --git a/tests/test_diffs.py b/tests/test_diffs.py
index 00c78a9..6f66893 100644
--- a/tests/test_diffs.py
+++ b/tests/test_diffs.py
@@ -1,5 +1,14 @@
# -*- coding: utf-8; -*-
+import datetime
+import re
+from unittest.mock import patch
+
+import sqlalchemy as sa
+from sqlalchemy import orm
+
+from wuttjamaican.util import get_timezone_by_name
+
from wuttaweb import diffs as mod
from wuttaweb.testing import WebTestCase, VersionWebTestCase
@@ -70,7 +79,7 @@ class TestVersionDiff(VersionWebTestCase):
["active", "person_uuid", "prevent_edit", "username", "uuid"],
)
- def test_render_version_value(self):
+ def test_render_version_value_with_objref(self):
import sqlalchemy_continuum as continuum
model = self.app.model
@@ -130,3 +139,91 @@ class TestVersionDiff(VersionWebTestCase):
self.assertIn(str(person.uuid), html)
self.assertIn("Fred Flintstone", html)
self.assertEqual(diff.render_new_value("person_uuid"), "")
+
+ def test_render_version_value_with_datetime(self):
+ import sqlalchemy_continuum as continuum
+
+ tzlocal = get_timezone_by_name("America/Los_Angeles")
+ with patch.object(self.app, "get_timezone", return_value=tzlocal):
+
+ # make one person
+ model = self.app.model
+ person = model.Person(full_name="Fred Flintstone")
+ self.session.add(person)
+ self.session.commit()
+
+ # get its one version record
+ txncls = continuum.transaction_class(model.Person)
+ vercls = continuum.version_class(model.Person)
+ version = self.session.query(vercls).order_by(vercls.transaction_id).one()
+
+ # make a diff, but we have to mock some things up for the test
+ # coverage, since we don't currently have a versioned datetime
+ # field in the base model.
+ diff = self.make_diff(version)
+
+ mock_column = sa.Column("timestamp", sa.DateTime())
+ mock_property = orm.ColumnProperty(column=mock_column)
+
+ class MockMapper:
+ def __init__(self, mapper):
+ self.mapper = mapper
+
+ def __getattr__(self, name):
+ if hasattr(self.mapper, name):
+ return getattr(self.mapper, name)
+ raise AttributeError(f"attr not found: {name}")
+
+ def has_property(self, field):
+ if field == "timestamp":
+ return True
+ return self.mapper.has_property(field)
+
+ def get_property(self, field):
+ if field == "timestamp":
+ return mock_property
+ return self.mapper.get_property(field)
+
+ mock_mapper = MockMapper(diff.mapper)
+ with patch.object(diff, "mapper", new=mock_mapper):
+
+ # zone-aware local time
+ dt = datetime.datetime(2026, 3, 14, 14, 0, tzinfo=tzlocal)
+ html = diff.render_version_value(version, "timestamp", dt)
+ self.assertTrue(
+ re.search(
+ r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}-\d{4}',
+ html,
+ )
+ )
+ self.assertTrue(
+ re.search(
+ r'2026-03-14 14:00-0700',
+ html,
+ )
+ )
+
+ # zone-naive (presumed UTC)
+ dt = datetime.datetime(2026, 3, 14, 21, 0)
+ html = diff.render_version_value(version, "timestamp", dt)
+ self.assertTrue(
+ re.search(
+ r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}-\d{4}',
+ html,
+ )
+ )
+ self.assertTrue(
+ re.search(
+ r'2026-03-14 14:00-0700',
+ html,
+ )
+ )
+
+ # null
+ html = diff.render_version_value(version, "timestamp", None)
+ self.assertFalse(
+ re.search(
+ r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}-\d{4}',
+ html,
+ )
+ )