3
0
Fork 0

Compare commits

..

7 commits

Author SHA1 Message Date
45909e653b bump: version 0.25.0 → 0.25.1 2025-12-20 20:28:53 -06:00
9a7488b063 fix: add WebDiff class now that Diff lives in wuttjamaican 2025-12-20 19:48:07 -06:00
7e0b16c57d fix: expose fallback key for email settings 2025-12-20 19:48:07 -06:00
7c6bdb404e fix: expose transaction comment for version history 2025-12-20 19:48:07 -06:00
2723965a6a fix: show display text for related objects, in version diff 2025-12-20 19:48:07 -06:00
5b6c686a9d fix: discard non-declared field values for grid vue data 2025-12-20 19:48:04 -06:00
2ccfe29553 fix: prevent error in DateTime schema type if no widget/request set
when we use this intentionally, the widget/request should be set as
expected. but apparently this gets instantiated sometimes (by
ColanderAlchemy?) without a widget.

so this adds sane fallback logic, instead of outright error
2025-12-17 17:46:31 -06:00
12 changed files with 210 additions and 255 deletions

View file

@ -5,6 +5,17 @@ All notable changes to wuttaweb will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## v0.25.1 (2025-12-20)
### Fix
- add `WebDiff` class now that `Diff` lives in wuttjamaican
- expose fallback key for email settings
- expose transaction comment for version history
- show display text for related objects, in version diff
- discard non-declared field values for grid vue data
- prevent error in DateTime schema type if no widget/request set
## v0.25.0 (2025-12-17) ## v0.25.0 (2025-12-17)
### Feat ### Feat

View file

@ -6,7 +6,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "WuttaWeb" name = "WuttaWeb"
version = "0.25.0" version = "0.25.1"
description = "Web App for Wutta Framework" description = "Web App for Wutta Framework"
readme = "README.md" readme = "README.md"
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}] authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
@ -44,13 +44,13 @@ dependencies = [
"pyramid_tm", "pyramid_tm",
"waitress", "waitress",
"WebHelpers2", "WebHelpers2",
"WuttJamaican[db]>=0.26.0", "WuttJamaican[db]>=0.27.0",
"zope.sqlalchemy>=1.5", "zope.sqlalchemy>=1.5",
] ]
[project.optional-dependencies] [project.optional-dependencies]
continuum = ["Wutta-Continuum>=0.2.2"] continuum = ["Wutta-Continuum>=0.3.0"]
docs = ["Sphinx", "furo", "sphinxcontrib-programoutput"] docs = ["Sphinx", "furo", "sphinxcontrib-programoutput"]
tests = ["pylint", "pytest", "pytest-cov", "tox"] tests = ["pylint", "pytest", "pytest-cov", "tox"]

View file

@ -29,60 +29,20 @@ import sqlalchemy as sa
from pyramid.renderers import render from pyramid.renderers import render
from webhelpers2.html import HTML from webhelpers2.html import HTML
from wuttjamaican.diffs import Diff
class Diff:
class WebDiff(Diff):
""" """
Represent / display a basic "diff" between two data records. Simple diff class for the web app.
You must provide both the "old" and "new" data records, when This is based on the
constructing an instance of this class. Then call :class:`~wuttjamaican:wuttjamaican.diffs.Diff` class; it just
:meth:`render_html()` to display the diff table. tweaks :meth:`render_html()` to use the web template lookup
engine.
:param old_data: Dict of "old" data record.
:param new_data: Dict of "new" data record.
:param fields: Optional list of field names. If not specified,
will be derived from the data records.
:param nature: What sort of diff is being represented; must be one
of: ``("create", "update", "delete")``
:param old_color: Background color to display for "old/deleted"
field data, when applicable.
:param new_color: Background color to display for "new/created"
field data, when applicable.
""" """
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments cell_padding = None
self,
old_data: dict,
new_data: dict,
fields: list = None,
nature="update",
old_color="#ffebe9",
new_color="#dafbe1",
):
self.old_data = old_data
self.new_data = new_data
self.columns = ["field name", "old value", "new value"]
self.fields = fields or self.make_fields()
self.nature = nature
self.old_color = old_color
self.new_color = new_color
def make_fields(self): # pylint: disable=missing-function-docstring
return sorted(set(self.old_data) | set(self.new_data), key=lambda x: x.lower())
def old_value(self, field): # pylint: disable=missing-function-docstring
return self.old_data.get(field)
def new_value(self, field): # pylint: disable=missing-function-docstring
return self.new_data.get(field)
def values_differ(self, field): # pylint: disable=missing-function-docstring
return self.new_value(field) != self.old_value(field)
def render_html(self, template="/diff.mako", **kwargs): def render_html(self, template="/diff.mako", **kwargs):
""" """
@ -98,69 +58,25 @@ class Diff:
""" """
context = kwargs context = kwargs
context["diff"] = self context["diff"] = self
return HTML.literal(render(template, context)) html = render(template, context)
return HTML.literal(html)
def render_field_row(self, field): # pylint: disable=missing-function-docstring
is_diff = self.values_differ(field)
td_field = HTML.tag("td", class_="field", c=field)
td_old_value = HTML.tag(
"td",
c=self.render_old_value(field),
**self.get_old_value_attrs(is_diff),
)
td_new_value = HTML.tag(
"td",
c=self.render_new_value(field),
**self.get_new_value_attrs(is_diff),
)
return HTML.tag("tr", c=[td_field, td_old_value, td_new_value])
def render_old_value(self, field): # pylint: disable=missing-function-docstring
value = self.old_value(field)
return repr(value)
def render_new_value(self, field): # pylint: disable=missing-function-docstring
value = self.new_value(field)
return repr(value)
def get_old_value_attrs( # pylint: disable=missing-function-docstring
self, is_diff
):
attrs = {}
if self.nature == "update" and is_diff:
attrs["style"] = f"background-color: {self.old_color};"
elif self.nature == "delete":
attrs["style"] = f"background-color: {self.old_color};"
return attrs
def get_new_value_attrs( # pylint: disable=missing-function-docstring
self, is_diff
):
attrs = {}
if self.nature == "create":
attrs["style"] = f"background-color: {self.new_color};"
elif self.nature == "update" and is_diff:
attrs["style"] = f"background-color: {self.new_color};"
return attrs
class VersionDiff(Diff): class VersionDiff(WebDiff):
""" """
Special diff class, for use with version history views. Note that Special diff class for use with version history views. While
while based on :class:`Diff`, this class uses a different based on :class:`WebDiff`, this class uses a different signature
signature for the constructor. for the constructor.
:param version: Reference to a Continuum version record (object). :param config: The app :term:`config object`.
:param version: Reference to a Continuum version record object.
:param \\**kwargs: Remaining kwargs are passed as-is to the :param \\**kwargs: Remaining kwargs are passed as-is to the
:class:`Diff` constructor. :class:`WebDiff` constructor.
""" """
def __init__(self, version, **kwargs): def __init__(self, config, version, **kwargs):
import sqlalchemy_continuum as continuum # pylint: disable=import-outside-toplevel import sqlalchemy_continuum as continuum # pylint: disable=import-outside-toplevel
from wutta_continuum.util import ( # pylint: disable=import-outside-toplevel from wutta_continuum.util import ( # pylint: disable=import-outside-toplevel
render_operation_type, render_operation_type,
@ -195,7 +111,7 @@ class VersionDiff(Diff):
old_data[field] = getattr(version.previous, field) old_data[field] = getattr(version.previous, field)
new_data[field] = getattr(version, field) new_data[field] = getattr(version, field)
super().__init__(old_data, new_data, **kwargs) super().__init__(config, old_data, new_data, **kwargs)
def get_default_fields(self): # pylint: disable=missing-function-docstring def get_default_fields(self): # pylint: disable=missing-function-docstring
fields = sorted(self.version_mapper.columns.keys()) fields = sorted(self.version_mapper.columns.keys())
@ -208,17 +124,76 @@ class VersionDiff(Diff):
return [field for field in fields if field not in unwanted] return [field for field in fields if field not in unwanted]
def render_version_value(self, value): # pylint: disable=missing-function-docstring def render_version_value(self, version, field, value):
return HTML.tag("span", c=[repr(value)], style="font-family: monospace;") """
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): def render_old_value(self, field):
if self.nature == "create": if self.nature == "create":
return "" return ""
value = self.old_value(field) 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): def render_new_value(self, field):
if self.nature == "delete": if self.nature == "delete":
return "" return ""
value = self.new_value(field) value = self.new_value(field)
return self.render_version_value(value) return self.render_version_value(self.version, field, value)

View file

@ -31,6 +31,7 @@ import colander
import sqlalchemy as sa import sqlalchemy as sa
from wuttjamaican.conf import parse_list from wuttjamaican.conf import parse_list
from wuttjamaican.util import localtime
from wuttaweb.db import Session from wuttaweb.db import Session
from wuttaweb.forms import widgets from wuttaweb.forms import widgets
@ -38,28 +39,38 @@ from wuttaweb.forms import widgets
class WuttaDateTime(colander.DateTime): class WuttaDateTime(colander.DateTime):
""" """
Custom schema type for ``datetime`` fields. Custom schema type for :class:`~python:datetime.datetime` fields.
This should be used automatically for This should be used automatically for
:class:`sqlalchemy:sqlalchemy.types.DateTime` columns unless you :class:`~sqlalchemy:sqlalchemy.types.DateTime` ORM columns unless
register another default. you register another default.
This schema type exists for sake of convenience, when working with This schema type exists for sake of convenience, when working with
the Buefy datepicker + timepicker widgets. the Buefy datepicker + timepicker widgets.
It also follows the datetime handling "rules" as outlined in
:doc:`wuttjamaican:narr/datetime`. On the Python side, values
should be naive/UTC datetime objects. On the HTTP side, values
will be ISO-format strings representing aware/local time.
""" """
def serialize(self, node, appstruct): def serialize(self, node, appstruct):
if not appstruct: if not appstruct:
return colander.null return colander.null
request = node.widget.request # nb. request should be present when it matters
config = request.wutta_config if node.widget and node.widget.request:
app = config.get_app() request = node.widget.request
config = request.wutta_config
app = config.get_app()
appstruct = app.localtime(appstruct)
else:
# but if not, fallback to config-less logic
appstruct = localtime(appstruct)
dt = app.localtime(appstruct)
if self.format: if self.format:
return dt.strftime(self.format) return appstruct.strftime(self.format)
return dt.isoformat() return appstruct.isoformat()
def deserialize( # pylint: disable=inconsistent-return-statements def deserialize( # pylint: disable=inconsistent-return-statements
self, node, cstruct self, node, cstruct
@ -72,6 +83,7 @@ class WuttaDateTime(colander.DateTime):
"%Y-%m-%dT%I:%M %p", "%Y-%m-%dT%I:%M %p",
] ]
# nb. request is always assumed to be present here
request = node.widget.request request = node.widget.request
config = request.wutta_config config = request.wutta_config
app = config.get_app() app = config.get_app()

View file

@ -2390,6 +2390,9 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
# convert record to new dict # convert record to new dict
record = self.object_to_dict(record) record = self.object_to_dict(record)
# discard non-declared fields
record = {field: record[field] for field in record if field in self.columns}
# make all values safe for json # make all values safe for json
record = make_json_safe(record, warn=False) record = make_json_safe(record, warn=False)

View file

@ -24,6 +24,10 @@
<span>${transaction.id}</span> <span>${transaction.id}</span>
</b-field> </b-field>
<b-field label="Comment" horizontal>
<span>${transaction.meta.get("comment", "")}</span>
</b-field>
</div> </div>
<div style="padding: 2rem;"> <div style="padding: 2rem;">

View file

@ -63,6 +63,7 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
form_fields = [ form_fields = [
"key", "key",
"fallback_key",
"description", "description",
"subject", "subject",
"sender", "sender",
@ -92,9 +93,11 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
def normalize_setting(self, setting): # pylint: disable=empty-docstring def normalize_setting(self, setting): # pylint: disable=empty-docstring
""" """ """ """
key = setting.__name__ key = setting.__name__
setting = setting(self.config)
return { return {
"key": key, "key": key,
"description": setting.__doc__, "fallback_key": setting.fallback_key or "",
"description": setting.get_description() or "",
"subject": self.email_handler.get_auto_subject( "subject": self.email_handler.get_auto_subject(
key, rendered=False, setting=setting key, rendered=False, setting=setting
), ),
@ -158,8 +161,12 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
f = form f = form
super().configure_form(f) super().configure_form(f)
# fallback_key
f.set_readonly("fallback_key")
# description # description
f.set_readonly("description") f.set_readonly("description")
f.set_widget("description", "notes")
# replyto # replyto
f.set_required("replyto", False) f.set_required("replyto", False)
@ -247,11 +254,12 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
if self.viewing: if self.viewing:
setting = context["instance"] setting = context["instance"]
context["setting"] = setting context["setting"] = setting
context["has_html_template"] = self.email_handler.get_auto_body_template( context["has_html_template"] = self.email_handler.get_auto_body_template(
setting["key"], "html" setting["key"], "html", fallback_key=setting["fallback_key"]
) )
context["has_txt_template"] = self.email_handler.get_auto_body_template( context["has_txt_template"] = self.email_handler.get_auto_body_template(
setting["key"], "txt" setting["key"], "txt", fallback_key=setting["fallback_key"]
) )
return super().render_to_response(template, context) return super().render_to_response(template, context)
@ -269,11 +277,15 @@ class EmailSettingView(MasterView): # pylint: disable=abstract-method
mode = self.request.params.get("mode", "html") mode = self.request.params.get("mode", "html")
if mode == "txt": if mode == "txt":
body = self.email_handler.get_auto_txt_body(key, context) body = self.email_handler.get_auto_txt_body(
key, context, fallback_key=setting.fallback_key
)
self.request.response.content_type = "text/plain" self.request.response.content_type = "text/plain"
else: # html else: # html
body = self.email_handler.get_auto_html_body(key, context) body = self.email_handler.get_auto_html_body(
key, context, fallback_key=setting.fallback_key
)
self.request.response.text = body self.request.response.text = body
return self.request.response return self.request.response

View file

@ -1145,6 +1145,7 @@ class MasterView(View): # pylint: disable=too-many-public-methods
"issued_at", "issued_at",
"user", "user",
"remote_addr", "remote_addr",
"comment",
] ]
def get_version_grid_data(self, instance): def get_version_grid_data(self, instance):
@ -1197,6 +1198,14 @@ class MasterView(View): # pylint: disable=too-many-public-methods
# remote_addr # remote_addr
g.set_label("remote_addr", "IP Address") g.set_label("remote_addr", "IP Address")
# comment
g.set_renderer("comment", self.render_version_comment)
def render_version_comment( # pylint: disable=missing-function-docstring,unused-argument
self, txn, key, value
):
return txn.meta.get("comment", "")
def view_version(self): # pylint: disable=too-many-locals def view_version(self): # pylint: disable=too-many-locals
""" """
View to show diff details for a particular object version. View to show diff details for a particular object version.
@ -1260,7 +1269,7 @@ class MasterView(View): # pylint: disable=too-many-public-methods
) )
version_diffs = [ version_diffs = [
VersionDiff(version) VersionDiff(self.config, version)
for version in self.get_relevant_versions(txn, instance) for version in self.get_relevant_versions(txn, instance)
] ]

View file

@ -62,6 +62,13 @@ class TestWuttaDateTime(WebTestCase):
) )
self.assertEqual(result, "2024-12-11 02:33 PM") self.assertEqual(result, "2024-12-11 02:33 PM")
# missing widget/request/config
typ = mod.WuttaDateTime()
node = colander.SchemaNode(typ)
result = typ.serialize(node, datetime.datetime(2024, 12, 11, 22, 33))
# nb. not possible to know which timezone is system-local
self.assertTrue(result.startswith("2024-12-"))
def test_deserialize(self): def test_deserialize(self):
tzlocal = get_timezone_by_name("America/Los_Angeles") tzlocal = get_timezone_by_name("America/Los_Angeles")
with patch.object(self.app, "get_timezone", return_value=tzlocal): with patch.object(self.app, "get_timezone", return_value=tzlocal):

View file

@ -1867,7 +1867,19 @@ class TestGrid(WebTestCase):
context = grid.get_vue_context() context = grid.get_vue_context()
self.assertEqual(context, {"data": [{"foo": "bar"}], "row_classes": {}}) self.assertEqual(context, {"data": [{"foo": "bar"}], "row_classes": {}})
# non-declared columns are discarded
mydata = [
{"foo": "a", "bar": "b", "baz": "c"},
]
grid = self.make_grid(columns=["bar"], data=mydata)
context = grid.get_vue_context()
self.assertEqual(context, {"data": [{"bar": "b"}], "row_classes": {}})
# if grid has actions, that list may be supplemented # if grid has actions, that list may be supplemented
mydata = [
{"foo": "bar"},
]
grid = self.make_grid(columns=["foo"], data=mydata)
grid.actions.append(mod.GridAction(self.request, "view", url="/blarg")) grid.actions.append(mod.GridAction(self.request, "view", url="/blarg"))
context = grid.get_vue_context() context = grid.get_vue_context()
self.assertIsNot(context["data"], mydata) self.assertIsNot(context["data"], mydata)

View file

@ -4,114 +4,10 @@ from wuttaweb import diffs as mod
from wuttaweb.testing import WebTestCase, VersionWebTestCase from wuttaweb.testing import WebTestCase, VersionWebTestCase
# nb. using WebTestCase here only for mako support in render_html() class TestWebDiff(WebTestCase):
class TestDiff(WebTestCase):
def make_diff(self, *args, **kwargs): def make_diff(self, *args, **kwargs):
return mod.Diff(*args, **kwargs) return mod.WebDiff(self.config, *args, **kwargs)
def test_constructor(self):
old_data = {"foo": "bar"}
new_data = {"foo": "baz"}
diff = self.make_diff(old_data, new_data, fields=["foo"])
self.assertEqual(diff.fields, ["foo"])
def test_make_fields(self):
old_data = {"foo": "bar"}
new_data = {"foo": "bar", "baz": "zer"}
# nb. this calls make_fields()
diff = self.make_diff(old_data, new_data)
# TODO: should the fields be cumulative? or just use new_data?
self.assertEqual(diff.fields, ["baz", "foo"])
def test_values(self):
old_data = {"foo": "bar"}
new_data = {"foo": "baz"}
diff = self.make_diff(old_data, new_data)
self.assertEqual(diff.old_value("foo"), "bar")
self.assertEqual(diff.new_value("foo"), "baz")
def test_values_differ(self):
old_data = {"foo": "bar"}
new_data = {"foo": "baz"}
diff = self.make_diff(old_data, new_data)
self.assertTrue(diff.values_differ("foo"))
old_data = {"foo": "bar"}
new_data = {"foo": "bar"}
diff = self.make_diff(old_data, new_data)
self.assertFalse(diff.values_differ("foo"))
def test_render_values(self):
old_data = {"foo": "bar"}
new_data = {"foo": "baz"}
diff = self.make_diff(old_data, new_data)
self.assertEqual(diff.render_old_value("foo"), "'bar'")
self.assertEqual(diff.render_new_value("foo"), "'baz'")
def test_get_old_value_attrs(self):
# no change
old_data = {"foo": "bar"}
new_data = {"foo": "bar"}
diff = self.make_diff(old_data, new_data, nature="update")
self.assertEqual(diff.get_old_value_attrs(False), {})
# update
old_data = {"foo": "bar"}
new_data = {"foo": "baz"}
diff = self.make_diff(old_data, new_data, nature="update")
self.assertEqual(
diff.get_old_value_attrs(True),
{"style": f"background-color: {diff.old_color};"},
)
# delete
old_data = {"foo": "bar"}
new_data = {}
diff = self.make_diff(old_data, new_data, nature="delete")
self.assertEqual(
diff.get_old_value_attrs(True),
{"style": f"background-color: {diff.old_color};"},
)
def test_get_new_value_attrs(self):
# no change
old_data = {"foo": "bar"}
new_data = {"foo": "bar"}
diff = self.make_diff(old_data, new_data, nature="update")
self.assertEqual(diff.get_new_value_attrs(False), {})
# update
old_data = {"foo": "bar"}
new_data = {"foo": "baz"}
diff = self.make_diff(old_data, new_data, nature="update")
self.assertEqual(
diff.get_new_value_attrs(True),
{"style": f"background-color: {diff.new_color};"},
)
# create
old_data = {}
new_data = {"foo": "bar"}
diff = self.make_diff(old_data, new_data, nature="create")
self.assertEqual(
diff.get_new_value_attrs(True),
{"style": f"background-color: {diff.new_color};"},
)
def test_render_field_row(self):
old_data = {"foo": "bar"}
new_data = {"foo": "baz"}
diff = self.make_diff(old_data, new_data)
row = diff.render_field_row("foo")
self.assertIn("<tr>", row)
self.assertIn("&#39;bar&#39;", row)
self.assertIn(f'style="background-color: {diff.old_color};"', row)
self.assertIn("&#39;baz&#39;", row)
self.assertIn(f'style="background-color: {diff.new_color};"', row)
self.assertIn("</tr>", row)
def test_render_html(self): def test_render_html(self):
old_data = {"foo": "bar"} old_data = {"foo": "bar"}
@ -121,9 +17,9 @@ class TestDiff(WebTestCase):
self.assertIn("<table", html) self.assertIn("<table", html)
self.assertIn("<tr>", html) self.assertIn("<tr>", html)
self.assertIn("&#39;bar&#39;", html) self.assertIn("&#39;bar&#39;", html)
self.assertIn(f'style="background-color: {diff.old_color};"', html) self.assertIn(f'style="background-color: {diff.old_color}"', html)
self.assertIn("&#39;baz&#39;", html) self.assertIn("&#39;baz&#39;", html)
self.assertIn(f'style="background-color: {diff.new_color};"', html) self.assertIn(f'style="background-color: {diff.new_color}"', html)
self.assertIn("</tr>", html) self.assertIn("</tr>", html)
self.assertIn("</table>", html) self.assertIn("</table>", html)
@ -131,7 +27,7 @@ class TestDiff(WebTestCase):
class TestVersionDiff(VersionWebTestCase): class TestVersionDiff(VersionWebTestCase):
def make_diff(self, *args, **kwargs): def make_diff(self, *args, **kwargs):
return mod.VersionDiff(*args, **kwargs) return mod.VersionDiff(self.config, *args, **kwargs)
def test_constructor(self): def test_constructor(self):
import sqlalchemy_continuum as continuum import sqlalchemy_continuum as continuum
@ -174,11 +70,15 @@ class TestVersionDiff(VersionWebTestCase):
["active", "person_uuid", "prevent_edit", "username", "uuid"], ["active", "person_uuid", "prevent_edit", "username", "uuid"],
) )
def test_render_values(self): def test_render_version_value(self):
import sqlalchemy_continuum as continuum import sqlalchemy_continuum as continuum
model = self.app.model 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.add(user)
self.session.commit() self.session.commit()
user.username = "freddie" user.username = "freddie"
@ -191,32 +91,42 @@ class TestVersionDiff(VersionWebTestCase):
versions = self.session.query(vercls).order_by(vercls.transaction_id).all() versions = self.session.query(vercls).order_by(vercls.transaction_id).all()
self.assertEqual(len(versions), 3) self.assertEqual(len(versions), 3)
# create (1st version)
version = versions[0] version = versions[0]
diff = self.make_diff(version) diff = self.make_diff(version)
self.assertEqual(diff.nature, "create") self.assertEqual(diff.nature, "create")
self.assertEqual(diff.render_old_value("username"), "") self.assertEqual(diff.render_old_value("username"), "")
self.assertEqual( self.assertIn("fred", diff.render_new_value("username"))
diff.render_new_value("username"), self.assertNotIn("freddie", diff.render_new_value("username"))
'<span style="font-family: monospace;">&#39;fred&#39;</span>', 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] version = versions[1]
diff = self.make_diff(version) diff = self.make_diff(version)
self.assertEqual(diff.nature, "update") self.assertEqual(diff.nature, "update")
self.assertEqual( self.assertIn("fred", diff.render_old_value("username"))
diff.render_old_value("username"), self.assertNotIn("freddie", diff.render_old_value("username"))
'<span style="font-family: monospace;">&#39;fred&#39;</span>', self.assertIn("freddie", diff.render_new_value("username"))
) # rendered person_uuid includes display name
self.assertEqual( html = diff.render_old_value("person_uuid")
diff.render_new_value("username"), self.assertIn(str(person.uuid), html)
'<span style="font-family: monospace;">&#39;freddie&#39;</span>', 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] version = versions[2]
diff = self.make_diff(version) diff = self.make_diff(version)
self.assertEqual(diff.nature, "delete") self.assertEqual(diff.nature, "delete")
self.assertEqual( self.assertIn("freddie", diff.render_old_value("username"))
diff.render_old_value("username"),
'<span style="font-family: monospace;">&#39;freddie&#39;</span>',
)
self.assertEqual(diff.render_new_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"), "")

View file

@ -2115,7 +2115,7 @@ class TestVersionedMasterView(VersionWebTestCase):
view = self.make_view() view = self.make_view()
self.assertEqual( self.assertEqual(
view.get_version_grid_columns(), view.get_version_grid_columns(),
["id", "issued_at", "user", "remote_addr"], ["id", "issued_at", "user", "remote_addr", "comment"],
) )
# custom # custom