2025-10-29 18:32:35 -05:00
|
|
|
# -*- coding: utf-8; -*-
|
|
|
|
|
|
2026-03-14 16:19:17 -05:00
|
|
|
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
|
|
|
|
|
|
2025-10-29 18:32:35 -05:00
|
|
|
from wuttaweb import diffs as mod
|
|
|
|
|
from wuttaweb.testing import WebTestCase, VersionWebTestCase
|
|
|
|
|
|
|
|
|
|
|
2025-12-20 18:38:49 -06:00
|
|
|
class TestWebDiff(WebTestCase):
|
2025-10-29 18:32:35 -05:00
|
|
|
|
|
|
|
|
def make_diff(self, *args, **kwargs):
|
2025-12-20 18:38:49 -06:00
|
|
|
return mod.WebDiff(self.config, *args, **kwargs)
|
2025-10-29 18:32:35 -05:00
|
|
|
|
|
|
|
|
def test_render_html(self):
|
|
|
|
|
old_data = {"foo": "bar"}
|
|
|
|
|
new_data = {"foo": "baz"}
|
|
|
|
|
diff = self.make_diff(old_data, new_data)
|
|
|
|
|
html = diff.render_html()
|
|
|
|
|
self.assertIn("<table", html)
|
|
|
|
|
self.assertIn("<tr>", html)
|
|
|
|
|
self.assertIn("'bar'", html)
|
2025-12-20 18:38:49 -06:00
|
|
|
self.assertIn(f'style="background-color: {diff.old_color}"', html)
|
2025-10-29 18:32:35 -05:00
|
|
|
self.assertIn("'baz'", html)
|
2025-12-20 18:38:49 -06:00
|
|
|
self.assertIn(f'style="background-color: {diff.new_color}"', html)
|
2025-10-29 18:32:35 -05:00
|
|
|
self.assertIn("</tr>", html)
|
|
|
|
|
self.assertIn("</table>", html)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestVersionDiff(VersionWebTestCase):
|
|
|
|
|
|
|
|
|
|
def make_diff(self, *args, **kwargs):
|
2025-12-20 18:38:49 -06:00
|
|
|
return mod.VersionDiff(self.config, *args, **kwargs)
|
2025-10-29 18:32:35 -05:00
|
|
|
|
|
|
|
|
def test_constructor(self):
|
|
|
|
|
import sqlalchemy_continuum as continuum
|
|
|
|
|
|
|
|
|
|
model = self.app.model
|
|
|
|
|
user = model.User(username="fred")
|
|
|
|
|
self.session.add(user)
|
|
|
|
|
self.session.commit()
|
|
|
|
|
user.username = "freddie"
|
|
|
|
|
self.session.commit()
|
|
|
|
|
self.session.delete(user)
|
|
|
|
|
self.session.commit()
|
|
|
|
|
|
|
|
|
|
txncls = continuum.transaction_class(model.User)
|
|
|
|
|
vercls = continuum.version_class(model.User)
|
|
|
|
|
versions = self.session.query(vercls).order_by(vercls.transaction_id).all()
|
|
|
|
|
self.assertEqual(len(versions), 3)
|
|
|
|
|
|
|
|
|
|
version = versions[0]
|
|
|
|
|
diff = self.make_diff(version)
|
|
|
|
|
self.assertEqual(diff.nature, "create")
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
diff.fields,
|
2025-10-29 20:08:30 -05:00
|
|
|
["active", "person_uuid", "prevent_edit", "username", "uuid"],
|
2025-10-29 18:32:35 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
version = versions[1]
|
|
|
|
|
diff = self.make_diff(version)
|
|
|
|
|
self.assertEqual(diff.nature, "update")
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
diff.fields,
|
2025-10-29 20:08:30 -05:00
|
|
|
["active", "person_uuid", "prevent_edit", "username", "uuid"],
|
2025-10-29 18:32:35 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
version = versions[2]
|
|
|
|
|
diff = self.make_diff(version)
|
|
|
|
|
self.assertEqual(diff.nature, "delete")
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
diff.fields,
|
2025-10-29 20:08:30 -05:00
|
|
|
["active", "person_uuid", "prevent_edit", "username", "uuid"],
|
2025-10-29 18:32:35 -05:00
|
|
|
)
|
|
|
|
|
|
2026-03-14 16:19:17 -05:00
|
|
|
def test_render_version_value_with_objref(self):
|
2025-10-29 18:32:35 -05:00
|
|
|
import sqlalchemy_continuum as continuum
|
|
|
|
|
|
|
|
|
|
model = self.app.model
|
2025-12-17 19:37:08 -06:00
|
|
|
person = model.Person(full_name="Fred Flintstone")
|
|
|
|
|
self.session.add(person)
|
|
|
|
|
|
|
|
|
|
# create, update, delete user
|
|
|
|
|
user = model.User(username="fred", person=person)
|
2025-10-29 18:32:35 -05:00
|
|
|
self.session.add(user)
|
|
|
|
|
self.session.commit()
|
|
|
|
|
user.username = "freddie"
|
|
|
|
|
self.session.commit()
|
|
|
|
|
self.session.delete(user)
|
|
|
|
|
self.session.commit()
|
|
|
|
|
|
|
|
|
|
txncls = continuum.transaction_class(model.User)
|
|
|
|
|
vercls = continuum.version_class(model.User)
|
|
|
|
|
versions = self.session.query(vercls).order_by(vercls.transaction_id).all()
|
|
|
|
|
self.assertEqual(len(versions), 3)
|
|
|
|
|
|
2025-12-17 19:37:08 -06:00
|
|
|
# create (1st version)
|
2025-10-29 18:32:35 -05:00
|
|
|
version = versions[0]
|
|
|
|
|
diff = self.make_diff(version)
|
|
|
|
|
self.assertEqual(diff.nature, "create")
|
|
|
|
|
self.assertEqual(diff.render_old_value("username"), "")
|
2025-12-17 19:37:08 -06:00
|
|
|
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)
|
2025-10-29 18:32:35 -05:00
|
|
|
version = versions[1]
|
|
|
|
|
diff = self.make_diff(version)
|
|
|
|
|
self.assertEqual(diff.nature, "update")
|
2025-12-17 19:37:08 -06:00
|
|
|
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)
|
2025-10-29 18:32:35 -05:00
|
|
|
version = versions[2]
|
|
|
|
|
diff = self.make_diff(version)
|
|
|
|
|
self.assertEqual(diff.nature, "delete")
|
2025-12-17 19:37:08 -06:00
|
|
|
self.assertIn("freddie", diff.render_old_value("username"))
|
2025-10-29 18:32:35 -05:00
|
|
|
self.assertEqual(diff.render_new_value("username"), "")
|
2025-12-17 19:37:08 -06:00
|
|
|
# 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"), "")
|
2026-03-14 16:19:17 -05:00
|
|
|
|
|
|
|
|
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'<span style="[^"]+"><span title="[^"]+">\d{4}-\d{2}-\d{2} \d{2}:\d{2}-\d{4}</span></span>',
|
|
|
|
|
html,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
re.search(
|
|
|
|
|
r'<span style="[^"]+"><span title="[^"]+">2026-03-14 14:00-0700</span></span>',
|
|
|
|
|
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'<span style="[^"]+"><span title="[^"]+">\d{4}-\d{2}-\d{2} \d{2}:\d{2}-\d{4}</span></span>',
|
|
|
|
|
html,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
re.search(
|
|
|
|
|
r'<span style="[^"]+"><span title="[^"]+">2026-03-14 14:00-0700</span></span>',
|
|
|
|
|
html,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# null
|
|
|
|
|
html = diff.render_version_value(version, "timestamp", None)
|
|
|
|
|
self.assertFalse(
|
|
|
|
|
re.search(
|
|
|
|
|
r'<span style="[^"]+"><span title="[^"]+">\d{4}-\d{2}-\d{2} \d{2}:\d{2}-\d{4}</span></span>',
|
|
|
|
|
html,
|
|
|
|
|
)
|
|
|
|
|
)
|