diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7e13a5c..dbba860 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,17 +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/)
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)
### Feat
diff --git a/pyproject.toml b/pyproject.toml
index 17e3c94..1cd984b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
[project]
name = "WuttaWeb"
-version = "0.25.1"
+version = "0.25.0"
description = "Web App for Wutta Framework"
readme = "README.md"
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
@@ -44,13 +44,13 @@ dependencies = [
"pyramid_tm",
"waitress",
"WebHelpers2",
- "WuttJamaican[db]>=0.27.0",
+ "WuttJamaican[db]>=0.26.0",
"zope.sqlalchemy>=1.5",
]
[project.optional-dependencies]
-continuum = ["Wutta-Continuum>=0.3.0"]
+continuum = ["Wutta-Continuum>=0.2.2"]
docs = ["Sphinx", "furo", "sphinxcontrib-programoutput"]
tests = ["pylint", "pytest", "pytest-cov", "tox"]
diff --git a/src/wuttaweb/diffs.py b/src/wuttaweb/diffs.py
index cb59ed5..9747fd0 100644
--- a/src/wuttaweb/diffs.py
+++ b/src/wuttaweb/diffs.py
@@ -29,20 +29,60 @@ import sqlalchemy as sa
from pyramid.renderers import render
from webhelpers2.html import HTML
-from wuttjamaican.diffs import Diff
-
-class WebDiff(Diff):
+class Diff:
"""
- Simple diff class for the web app.
+ Represent / display a basic "diff" between two data records.
- This is based on the
- :class:`~wuttjamaican:wuttjamaican.diffs.Diff` class; it just
- tweaks :meth:`render_html()` to use the web template lookup
- engine.
+ You must provide both the "old" and "new" data records, when
+ constructing an instance of this class. Then call
+ :meth:`render_html()` to display the diff table.
+
+ :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.
"""
- cell_padding = None
+ def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments
+ 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):
"""
@@ -58,25 +98,69 @@ class WebDiff(Diff):
"""
context = kwargs
context["diff"] = self
- html = render(template, context)
- return HTML.literal(html)
+ return HTML.literal(render(template, context))
+
+ 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(WebDiff):
+class VersionDiff(Diff):
"""
- Special diff class for use with version history views. While
- based on :class:`WebDiff`, this class uses a different signature
- for the constructor.
+ 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.
- :param config: The app :term:`config object`.
-
- :param version: Reference to a Continuum version record object.
+ :param version: Reference to a Continuum version record (object).
:param \\**kwargs: Remaining kwargs are passed as-is to the
- :class:`WebDiff` constructor.
+ :class:`Diff` constructor.
"""
- def __init__(self, config, version, **kwargs):
+ def __init__(self, version, **kwargs):
import sqlalchemy_continuum as continuum # pylint: disable=import-outside-toplevel
from wutta_continuum.util import ( # pylint: disable=import-outside-toplevel
render_operation_type,
@@ -111,7 +195,7 @@ class VersionDiff(WebDiff):
old_data[field] = getattr(version.previous, field)
new_data[field] = getattr(version, field)
- super().__init__(config, old_data, new_data, **kwargs)
+ super().__init__(old_data, new_data, **kwargs)
def get_default_fields(self): # pylint: disable=missing-function-docstring
fields = sorted(self.version_mapper.columns.keys())
@@ -124,76 +208,17 @@ class VersionDiff(WebDiff):
return [field for field in fields if field not in unwanted]
- 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_version_value(self, value): # pylint: disable=missing-function-docstring
+ return HTML.tag("span", c=[repr(value)], style="font-family: monospace;")
def render_old_value(self, field):
if self.nature == "create":
return ""
value = self.old_value(field)
- return self.render_version_value(self.version.previous, field, value)
+ return self.render_version_value(value)
def render_new_value(self, field):
if self.nature == "delete":
return ""
value = self.new_value(field)
- return self.render_version_value(self.version, field, value)
+ return self.render_version_value(value)
diff --git a/src/wuttaweb/forms/schema.py b/src/wuttaweb/forms/schema.py
index e002c0b..37db3e9 100644
--- a/src/wuttaweb/forms/schema.py
+++ b/src/wuttaweb/forms/schema.py
@@ -31,7 +31,6 @@ import colander
import sqlalchemy as sa
from wuttjamaican.conf import parse_list
-from wuttjamaican.util import localtime
from wuttaweb.db import Session
from wuttaweb.forms import widgets
@@ -39,38 +38,28 @@ from wuttaweb.forms import widgets
class WuttaDateTime(colander.DateTime):
"""
- Custom schema type for :class:`~python:datetime.datetime` fields.
+ Custom schema type for ``datetime`` fields.
This should be used automatically for
- :class:`~sqlalchemy:sqlalchemy.types.DateTime` ORM columns unless
- you register another default.
+ :class:`sqlalchemy:sqlalchemy.types.DateTime` columns unless you
+ register another default.
This schema type exists for sake of convenience, when working with
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):
if not appstruct:
return colander.null
- # nb. request should be present when it matters
- if node.widget and node.widget.request:
- 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)
+ request = node.widget.request
+ config = request.wutta_config
+ app = config.get_app()
+ dt = app.localtime(appstruct)
if self.format:
- return appstruct.strftime(self.format)
- return appstruct.isoformat()
+ return dt.strftime(self.format)
+ return dt.isoformat()
def deserialize( # pylint: disable=inconsistent-return-statements
self, node, cstruct
@@ -83,7 +72,6 @@ class WuttaDateTime(colander.DateTime):
"%Y-%m-%dT%I:%M %p",
]
- # nb. request is always assumed to be present here
request = node.widget.request
config = request.wutta_config
app = config.get_app()
diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py
index f0f45a1..81c92ae 100644
--- a/src/wuttaweb/grids/base.py
+++ b/src/wuttaweb/grids/base.py
@@ -2390,9 +2390,6 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
# convert record to new dict
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
record = make_json_safe(record, warn=False)
diff --git a/src/wuttaweb/templates/master/view_version.mako b/src/wuttaweb/templates/master/view_version.mako
index 8e838e8..361055e 100644
--- a/src/wuttaweb/templates/master/view_version.mako
+++ b/src/wuttaweb/templates/master/view_version.mako
@@ -24,10 +24,6 @@
${transaction.id}
-