From d08ba5fe51b65d5b2c896a4d0c2ae9d4ad2dcdd0 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 14 Mar 2026 16:19:17 -0500 Subject: [PATCH] fix: render human-friendly datetime for such fields in version diff still render the "raw" value but include human-friendly for convenience --- src/wuttaweb/diffs.py | 25 +++++++++-- tests/test_diffs.py | 99 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 5 deletions(-) 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, + ) + )