From 6f26120640bf48a052409e41d3796486868f45dd Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 14 Mar 2026 15:36:42 -0500 Subject: [PATCH 1/5] fix: remove version pin for setuptools dependency since pyramid 2.1 now specifies the pin cf. https://docs.pylonsproject.org/projects/pyramid/en/2.1-branch/changes.html#a1-2026-02-25 --- pyproject.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2613603..535ddf9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,11 +52,6 @@ dependencies = [ # can address a new bug that showed up in SA 2.1.0b1 # cf. https://github.com/kvesteri/sqlalchemy-utils/issues/800 "SQLAlchemy<2.1", - - # nb. this must be pinned for now, until pyramid can remove - # its dependency on pkg_resources. - # cf. https://github.com/Pylons/pyramid/issues/3731 - "setuptools<81", ] From d08ba5fe51b65d5b2c896a4d0c2ae9d4ad2dcdd0 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 14 Mar 2026 16:19:17 -0500 Subject: [PATCH 2/5] 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, + ) + ) From 16131cd2560f57bb8f19022e46abc636e6f4a38c Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 14 Mar 2026 16:24:58 -0500 Subject: [PATCH 3/5] fix: keep original value along with rendered, in grid vue context this change was made for sake of sorting, when the backend is not responsible for that. in particular datetime values must be "rendered" somehow when passing to frontend, but depending on various factors the rendering may not preserve the "sensible" sort order behavior, e.g. if "weekday name" begins the rendered string. so in all cases now, rendered values will be given a distinct key in the record dict, while the original value stays in its original place. this should let grid sorting work off the original value, and hopefully all is well..fingers crossed --- src/wuttaweb/grids/base.py | 13 +++++-------- src/wuttaweb/templates/grids/vue_template.mako | 4 ++-- tests/grids/test_base.py | 18 ++++++++++++++++-- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py index 1906997..e2a9f83 100644 --- a/src/wuttaweb/grids/base.py +++ b/src/wuttaweb/grids/base.py @@ -2501,11 +2501,10 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth # loop thru data data = [] row_classes = {} - for i, record in enumerate(original_data, 1): - original_record = record + for i, original_record in enumerate(original_data, 1): # convert record to new dict - record = self.object_to_dict(record) + record = self.object_to_dict(original_record) # discard non-declared fields record = {field: record[field] for field in record if field in self.columns} @@ -2518,19 +2517,17 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth # nb. no need to render if column not included if key in self.columns: value = record.get(key, None) - record[key] = renderer(original_record, key, value) + record[f"_rendered_{key}"] = renderer(original_record, key, value) # add action urls to each record for action in self.actions: key = f"_action_url_{action.key}" if key not in record: - url = action.get_url(original_record, i) - if url: + if url := action.get_url(original_record, i): record[key] = url # set row css class if applicable - css_class = self.get_row_class(original_record, record, i) - if css_class: + if css_class := self.get_row_class(original_record, record, i): # nb. use *string* zero-based index, for js compat row_classes[str(i - 1)] = css_class diff --git a/src/wuttaweb/templates/grids/vue_template.mako b/src/wuttaweb/templates/grids/vue_template.mako index 47720fa..34424e9 100644 --- a/src/wuttaweb/templates/grids/vue_template.mako +++ b/src/wuttaweb/templates/grids/vue_template.mako @@ -177,9 +177,9 @@ cell-class="c_${column['field']}"> % if grid.is_linked(column['field']): + v-html="props.row?._rendered_${column['field']} === undefined ? props.row.${column['field']} : props.row._rendered_${column['field']}" /> % else: - + % endif % endif diff --git a/tests/grids/test_base.py b/tests/grids/test_base.py index 15a7a06..2978364 100644 --- a/tests/grids/test_base.py +++ b/tests/grids/test_base.py @@ -2025,7 +2025,12 @@ class TestGrid(WebTestCase): context, { "data": [ - {"foo": "blah blah", "baz": "zoo", "_action_url_view": "/blarg"} + { + "foo": "bar", + "_rendered_foo": "blah blah", + "baz": "zoo", + "_action_url_view": "/blarg", + } ], "row_classes": {}, }, @@ -2080,7 +2085,16 @@ class TestGrid(WebTestCase): # can override value rendering grid.set_renderer("foo", lambda record, key, value: "blah blah") data = grid.get_vue_data() - self.assertEqual(data, [{"foo": "blah blah", "_action_url_view": "/blarg"}]) + self.assertEqual( + data, + [ + { + "foo": "bar", + "_rendered_foo": "blah blah", + "_action_url_view": "/blarg", + } + ], + ) def test_get_row_class(self): model = self.app.model From 6c634303ae9bad18885731fdd58a0136fafdc3a3 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 14 Mar 2026 16:29:06 -0500 Subject: [PATCH 4/5] fix: fix datetime handling for alembic migrations view this should ensure the column sorting works, and adds normal rendering for the form fields --- src/wuttaweb/views/alembic.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/wuttaweb/views/alembic.py b/src/wuttaweb/views/alembic.py index ed9352e..5d73849 100644 --- a/src/wuttaweb/views/alembic.py +++ b/src/wuttaweb/views/alembic.py @@ -44,19 +44,23 @@ from webhelpers2.html import tags, HTML from wuttaweb.views import View, MasterView from wuttaweb.forms import widgets +from wuttaweb.forms.schema import WuttaDateTime log = logging.getLogger(__name__) -def normalize_revision(config, rev): # pylint: disable=missing-function-docstring +def normalize_revision( + config, rev, json_safe=False +): # pylint: disable=missing-function-docstring app = config.get_app() created = None if match := re.search(r"Create Date: (\d{4}-\d{2}-\d{2}[\d:\. ]+\d)", rev.longdoc): created = datetime.datetime.fromisoformat(match.group(1)) created = app.localtime(created, from_utc=False) - created = app.render_datetime(created) + if json_safe: + created = app.render_datetime(created) return { "revision": rev.revision, @@ -92,7 +96,7 @@ class AlembicDashboardView(View): current_heads = context.get_current_heads() def normalize(rev): - normal = normalize_revision(self.config, rev) + normal = normalize_revision(self.config, rev, json_safe=True) normal["is_current"] = rev.revision in current_heads normal["revision"] = tags.link_to( @@ -318,6 +322,9 @@ class AlembicMigrationView(MasterView): # pylint: disable=abstract-method g.set_label("is_head", "Head") g.set_renderer("is_head", self.render_is_head) + # created + g.set_renderer("created", "datetime") + def render_is_head( # pylint: disable=missing-function-docstring,unused-argument self, rev, field, value ): @@ -369,6 +376,10 @@ class AlembicMigrationView(MasterView): # pylint: disable=abstract-method # path f.set_widget("path", widgets.CopyableTextWidget()) + # created + f.set_node("created", WuttaDateTime()) + f.set_widget("created", widgets.WuttaDateTimeWidget(self.request)) + def make_create_form(self): # pylint: disable=empty-docstring """ """ alembic = make_alembic_config(self.config) From 49f43e53f1f89a2fd35066183d7ada82cf236805 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 15 Mar 2026 10:07:17 -0500 Subject: [PATCH 5/5] =?UTF-8?q?bump:=20version=200.29.2=20=E2=86=92=200.29?= =?UTF-8?q?.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 10 ++++++++++ pyproject.toml | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9202116..63eca3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ 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/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.29.3 (2026-03-15) + +### Fix + +- fix datetime handling for alembic migrations view +- keep original value along with rendered, in grid vue context +- render human-friendly datetime for such fields in version diff +- remove version pin for setuptools dependency +- make pylint happy + ## v0.29.2 (2026-03-10) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 535ddf9..25183bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "WuttaWeb" -version = "0.29.2" +version = "0.29.3" description = "Web App for Wutta Framework" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]