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"), "")