diff --git a/src/wuttaweb/diffs.py b/src/wuttaweb/diffs.py
index 9747fd0..ed3b237 100644
--- a/src/wuttaweb/diffs.py
+++ b/src/wuttaweb/diffs.py
@@ -150,9 +150,9 @@ class Diff:
class VersionDiff(Diff):
"""
- Special diff class, for use with version history views. Note that
- while based on :class:`Diff`, this class uses a different
- signature for the constructor.
+ Special diff class for use with version history views. While
+ based on :class:`Diff`, this class uses a different signature for
+ the constructor.
:param version: Reference to a Continuum version record (object).
@@ -208,17 +208,76 @@ class VersionDiff(Diff):
return [field for field in fields if field not in unwanted]
- def render_version_value(self, value): # pylint: disable=missing-function-docstring
- return HTML.tag("span", c=[repr(value)], style="font-family: monospace;")
+ def render_version_value(self, version, field, value):
+ """
+ Render the cell value HTML for a given version + field.
+
+ This method is used to render both sides of the diff (old +
+ new values). It will just render the field value using a
+ monospace font by default. However:
+
+ If the field is involved in a mapper relationship (i.e. it is
+ the "foreign key" to a related table), the logic here will
+ also (try to) traverse that show display text for the related
+ object (if found).
+
+ :param version: Reference to the Continuum version object.
+
+ :param field: Name of the field, as string.
+
+ :param value: Raw value for the field, as obtained from the
+ version object.
+
+ :returns: Rendered cell value as HTML literal
+ """
+ # first render normal span; this is our fallback but also may
+ # be embedded within a more complex result.
+ text = HTML.tag("span", c=[repr(value)], style="font-family: monospace;")
+
+ # loop thru all mapped relationship props
+ for prop in self.mapper.relationships:
+
+ # we only want singletons
+ if prop.uselist:
+ continue
+
+ # loop thru columns for prop
+ # nb. there should always be just one colum for a
+ # singleton prop, but technically a list is used, so no
+ # harm in looping i assume..
+ for col in prop.local_columns:
+
+ # we only want the matching column
+ if col.name != field:
+ continue
+
+ # grab "related version" reference via prop key. this
+ # would be like a UserVersion for instance.
+ if ref := getattr(version, prop.key):
+
+ # grab "related object" reference. this would be
+ # like a User for instance.
+ 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)],
+ )
+
+ return text
def render_old_value(self, field):
if self.nature == "create":
return ""
value = self.old_value(field)
- return self.render_version_value(value)
+ return self.render_version_value(self.version.previous, field, value)
def render_new_value(self, field):
if self.nature == "delete":
return ""
value = self.new_value(field)
- return self.render_version_value(value)
+ return self.render_version_value(self.version, field, value)
diff --git a/tests/test_diffs.py b/tests/test_diffs.py
index ee76314..aaf17af 100644
--- a/tests/test_diffs.py
+++ b/tests/test_diffs.py
@@ -174,11 +174,15 @@ class TestVersionDiff(VersionWebTestCase):
["active", "person_uuid", "prevent_edit", "username", "uuid"],
)
- def test_render_values(self):
+ def test_render_version_value(self):
import sqlalchemy_continuum as continuum
model = self.app.model
- user = model.User(username="fred")
+ person = model.Person(full_name="Fred Flintstone")
+ self.session.add(person)
+
+ # create, update, delete user
+ user = model.User(username="fred", person=person)
self.session.add(user)
self.session.commit()
user.username = "freddie"
@@ -191,32 +195,42 @@ class TestVersionDiff(VersionWebTestCase):
versions = self.session.query(vercls).order_by(vercls.transaction_id).all()
self.assertEqual(len(versions), 3)
+ # create (1st version)
version = versions[0]
diff = self.make_diff(version)
self.assertEqual(diff.nature, "create")
self.assertEqual(diff.render_old_value("username"), "")
- self.assertEqual(
- diff.render_new_value("username"),
- ''fred'',
- )
+ self.assertIn("fred", diff.render_new_value("username"))
+ self.assertNotIn("freddie", diff.render_new_value("username"))
+ self.assertEqual(diff.render_old_value("person_uuid"), "")
+ # rendered person_uuid includes display name
+ html = diff.render_new_value("person_uuid")
+ self.assertIn(str(person.uuid), html)
+ self.assertIn("Fred Flintstone", html)
+ # update (2nd version)
version = versions[1]
diff = self.make_diff(version)
self.assertEqual(diff.nature, "update")
- self.assertEqual(
- diff.render_old_value("username"),
- ''fred'',
- )
- self.assertEqual(
- diff.render_new_value("username"),
- ''freddie'',
- )
+ self.assertIn("fred", diff.render_old_value("username"))
+ self.assertNotIn("freddie", diff.render_old_value("username"))
+ self.assertIn("freddie", diff.render_new_value("username"))
+ # rendered person_uuid includes display name
+ html = diff.render_old_value("person_uuid")
+ self.assertIn(str(person.uuid), html)
+ self.assertIn("Fred Flintstone", html)
+ html = diff.render_new_value("person_uuid")
+ self.assertIn(str(person.uuid), html)
+ self.assertIn("Fred Flintstone", html)
+ # delete (3rd version)
version = versions[2]
diff = self.make_diff(version)
self.assertEqual(diff.nature, "delete")
- self.assertEqual(
- diff.render_old_value("username"),
- ''freddie'',
- )
+ self.assertIn("freddie", diff.render_old_value("username"))
self.assertEqual(diff.render_new_value("username"), "")
+ # rendered person_uuid includes display name
+ html = diff.render_old_value("person_uuid")
+ self.assertIn(str(person.uuid), html)
+ self.assertIn("Fred Flintstone", html)
+ self.assertEqual(diff.render_new_value("person_uuid"), "")