3
0
Fork 0

Compare commits

..

No commits in common. "49f43e53f1f89a2fd35066183d7ada82cf236805" and "787765f98625669e705ca976c76f8e2344382821" have entirely different histories.

8 changed files with 26 additions and 167 deletions

View file

@ -5,16 +5,6 @@ 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.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) ## v0.29.2 (2026-03-10)
### Fix ### Fix

View file

@ -6,7 +6,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "WuttaWeb" name = "WuttaWeb"
version = "0.29.3" version = "0.29.2"
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"}]
@ -52,6 +52,11 @@ dependencies = [
# can address a new bug that showed up in SA 2.1.0b1 # can address a new bug that showed up in SA 2.1.0b1
# cf. https://github.com/kvesteri/sqlalchemy-utils/issues/800 # cf. https://github.com/kvesteri/sqlalchemy-utils/issues/800
"SQLAlchemy<2.1", "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",
] ]

View file

@ -25,7 +25,6 @@ Tools for displaying simple data diffs
""" """
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm
from pyramid.renderers import render from pyramid.renderers import render
from webhelpers2.html import HTML from webhelpers2.html import HTML
@ -151,25 +150,6 @@ class VersionDiff(WebDiff):
# be embedded within a more complex result. # be embedded within a more complex result.
text = HTML.tag("span", c=[repr(value)], style="font-family: monospace;") 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 # loop thru all mapped relationship props
for prop in self.mapper.relationships: for prop in self.mapper.relationships:
@ -196,9 +176,12 @@ class VersionDiff(WebDiff):
if ref := getattr(ref, "version_parent", None): if ref := getattr(ref, "version_parent", None):
# render text w/ related object as bold string # render text w/ related object as bold string
style = (
"margin-left: 2rem; font-style: italic; font-weight: bold;"
)
return HTML.tag( return HTML.tag(
"span", "span",
c=[text, HTML.tag("span", c=[str(ref)], style=bold)], c=[text, HTML.tag("span", c=[str(ref)], style=style)],
) )
return text return text

View file

@ -2501,10 +2501,11 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
# loop thru data # loop thru data
data = [] data = []
row_classes = {} row_classes = {}
for i, original_record in enumerate(original_data, 1): for i, record in enumerate(original_data, 1):
original_record = record
# convert record to new dict # convert record to new dict
record = self.object_to_dict(original_record) record = self.object_to_dict(record)
# discard non-declared fields # discard non-declared fields
record = {field: record[field] for field in record if field in self.columns} record = {field: record[field] for field in record if field in self.columns}
@ -2517,17 +2518,19 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
# nb. no need to render if column not included # nb. no need to render if column not included
if key in self.columns: if key in self.columns:
value = record.get(key, None) value = record.get(key, None)
record[f"_rendered_{key}"] = renderer(original_record, key, value) record[key] = renderer(original_record, key, value)
# add action urls to each record # add action urls to each record
for action in self.actions: for action in self.actions:
key = f"_action_url_{action.key}" key = f"_action_url_{action.key}"
if key not in record: if key not in record:
if url := action.get_url(original_record, i): url = action.get_url(original_record, i)
if url:
record[key] = url record[key] = url
# set row css class if applicable # set row css class if applicable
if css_class := self.get_row_class(original_record, record, i): css_class = self.get_row_class(original_record, record, i)
if css_class:
# nb. use *string* zero-based index, for js compat # nb. use *string* zero-based index, for js compat
row_classes[str(i - 1)] = css_class row_classes[str(i - 1)] = css_class

View file

@ -177,9 +177,9 @@
cell-class="c_${column['field']}"> cell-class="c_${column['field']}">
% if grid.is_linked(column['field']): % if grid.is_linked(column['field']):
<a :href="props.row._action_url_view" <a :href="props.row._action_url_view"
v-html="props.row?._rendered_${column['field']} === undefined ? props.row.${column['field']} : props.row._rendered_${column['field']}" /> v-html="props.row.${column['field']}" />
% else: % else:
<span v-html="props.row?._rendered_${column['field']} === undefined ? props.row.${column['field']} : props.row._rendered_${column['field']}"></span> <span v-html="props.row.${column['field']}"></span>
% endif % endif
</${b}-table-column> </${b}-table-column>
% endif % endif

View file

@ -44,22 +44,18 @@ from webhelpers2.html import tags, HTML
from wuttaweb.views import View, MasterView from wuttaweb.views import View, MasterView
from wuttaweb.forms import widgets from wuttaweb.forms import widgets
from wuttaweb.forms.schema import WuttaDateTime
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def normalize_revision( def normalize_revision(config, rev): # pylint: disable=missing-function-docstring
config, rev, json_safe=False
): # pylint: disable=missing-function-docstring
app = config.get_app() app = config.get_app()
created = None created = None
if match := re.search(r"Create Date: (\d{4}-\d{2}-\d{2}[\d:\. ]+\d)", rev.longdoc): 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 = datetime.datetime.fromisoformat(match.group(1))
created = app.localtime(created, from_utc=False) created = app.localtime(created, from_utc=False)
if json_safe:
created = app.render_datetime(created) created = app.render_datetime(created)
return { return {
@ -96,7 +92,7 @@ class AlembicDashboardView(View):
current_heads = context.get_current_heads() current_heads = context.get_current_heads()
def normalize(rev): def normalize(rev):
normal = normalize_revision(self.config, rev, json_safe=True) normal = normalize_revision(self.config, rev)
normal["is_current"] = rev.revision in current_heads normal["is_current"] = rev.revision in current_heads
normal["revision"] = tags.link_to( normal["revision"] = tags.link_to(
@ -322,9 +318,6 @@ class AlembicMigrationView(MasterView): # pylint: disable=abstract-method
g.set_label("is_head", "Head") g.set_label("is_head", "Head")
g.set_renderer("is_head", self.render_is_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 def render_is_head( # pylint: disable=missing-function-docstring,unused-argument
self, rev, field, value self, rev, field, value
): ):
@ -376,10 +369,6 @@ class AlembicMigrationView(MasterView): # pylint: disable=abstract-method
# path # path
f.set_widget("path", widgets.CopyableTextWidget()) 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 def make_create_form(self): # pylint: disable=empty-docstring
""" """ """ """
alembic = make_alembic_config(self.config) alembic = make_alembic_config(self.config)

View file

@ -2025,12 +2025,7 @@ class TestGrid(WebTestCase):
context, context,
{ {
"data": [ "data": [
{ {"foo": "blah blah", "baz": "zoo", "_action_url_view": "/blarg"}
"foo": "bar",
"_rendered_foo": "blah blah",
"baz": "zoo",
"_action_url_view": "/blarg",
}
], ],
"row_classes": {}, "row_classes": {},
}, },
@ -2085,16 +2080,7 @@ class TestGrid(WebTestCase):
# can override value rendering # can override value rendering
grid.set_renderer("foo", lambda record, key, value: "blah blah") grid.set_renderer("foo", lambda record, key, value: "blah blah")
data = grid.get_vue_data() data = grid.get_vue_data()
self.assertEqual( self.assertEqual(data, [{"foo": "blah blah", "_action_url_view": "/blarg"}])
data,
[
{
"foo": "bar",
"_rendered_foo": "blah blah",
"_action_url_view": "/blarg",
}
],
)
def test_get_row_class(self): def test_get_row_class(self):
model = self.app.model model = self.app.model

View file

@ -1,14 +1,5 @@
# -*- coding: utf-8; -*- # -*- 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 import diffs as mod
from wuttaweb.testing import WebTestCase, VersionWebTestCase from wuttaweb.testing import WebTestCase, VersionWebTestCase
@ -79,7 +70,7 @@ class TestVersionDiff(VersionWebTestCase):
["active", "person_uuid", "prevent_edit", "username", "uuid"], ["active", "person_uuid", "prevent_edit", "username", "uuid"],
) )
def test_render_version_value_with_objref(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
@ -139,91 +130,3 @@ class TestVersionDiff(VersionWebTestCase):
self.assertIn(str(person.uuid), html) self.assertIn(str(person.uuid), html)
self.assertIn("Fred Flintstone", html) self.assertIn("Fred Flintstone", html)
self.assertEqual(diff.render_new_value("person_uuid"), "") 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'<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,
)
)