Coverage for .tox / coverage / lib / python3.11 / site-packages / wuttaweb / diffs.py: 100%
63 statements
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-28 15:23 -0600
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-28 15:23 -0600
1# -*- coding: utf-8; -*-
2################################################################################
3#
4# wuttaweb -- Web App for Wutta Framework
5# Copyright © 2024-2025 Lance Edgar
6#
7# This file is part of Wutta Framework.
8#
9# Wutta Framework is free software: you can redistribute it and/or modify it
10# under the terms of the GNU General Public License as published by the Free
11# Software Foundation, either version 3 of the License, or (at your option) any
12# later version.
13#
14# Wutta Framework is distributed in the hope that it will be useful, but
15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17# more details.
18#
19# You should have received a copy of the GNU General Public License along with
20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
21#
22################################################################################
23"""
24Tools for displaying simple data diffs
25"""
27import sqlalchemy as sa
29from pyramid.renderers import render
30from webhelpers2.html import HTML
32from wuttjamaican.diffs import Diff
35class WebDiff(Diff):
36 """
37 Simple diff class for the web app.
39 This is based on the
40 :class:`~wuttjamaican:wuttjamaican.diffs.Diff` class; it just
41 tweaks :meth:`render_html()` to use the web template lookup
42 engine.
43 """
45 cell_padding = None
47 def render_html(self, template="/diff.mako", **kwargs):
48 """
49 Render the diff as HTML table.
51 :param template: Name of template to render, if you need to
52 override the default.
54 :param \\**kwargs: Remaining kwargs are passed as context to
55 the template renderer.
57 :returns: HTML literal string
58 """
59 context = kwargs
60 context["diff"] = self
61 html = render(template, context)
62 return HTML.literal(html)
65class VersionDiff(WebDiff):
66 """
67 Special diff class for use with version history views. While
68 based on :class:`WebDiff`, this class uses a different signature
69 for the constructor.
71 :param config: The app :term:`config object`.
73 :param version: Reference to a Continuum version record object.
75 :param \\**kwargs: Remaining kwargs are passed as-is to the
76 :class:`WebDiff` constructor.
77 """
79 def __init__(self, config, version, **kwargs):
80 import sqlalchemy_continuum as continuum # pylint: disable=import-outside-toplevel
81 from wutta_continuum.util import ( # pylint: disable=import-outside-toplevel
82 render_operation_type,
83 )
85 self.version = version
86 self.model_class = continuum.parent_class(type(self.version))
87 self.mapper = sa.inspect(self.model_class)
88 self.version_mapper = sa.inspect(type(self.version))
89 self.title = kwargs.pop("title", self.model_class.__name__)
91 self.operation_title = render_operation_type(self.version.operation_type)
93 if "nature" not in kwargs:
94 if (
95 version.previous
96 and version.operation_type == continuum.Operation.DELETE
97 ):
98 kwargs["nature"] = "delete"
99 elif version.previous:
100 kwargs["nature"] = "update"
101 else:
102 kwargs["nature"] = "create"
104 if "fields" not in kwargs:
105 kwargs["fields"] = self.get_default_fields()
107 old_data = {}
108 new_data = {}
109 for field in kwargs["fields"]:
110 if version.previous:
111 old_data[field] = getattr(version.previous, field)
112 new_data[field] = getattr(version, field)
114 super().__init__(config, old_data, new_data, **kwargs)
116 def get_default_fields(self): # pylint: disable=missing-function-docstring
117 fields = sorted(self.version_mapper.columns.keys())
119 unwanted = [
120 "transaction_id",
121 "end_transaction_id",
122 "operation_type",
123 ]
125 return [field for field in fields if field not in unwanted]
127 def render_version_value(self, version, field, value):
128 """
129 Render the cell value HTML for a given version + field.
131 This method is used to render both sides of the diff (old +
132 new values). It will just render the field value using a
133 monospace font by default. However:
135 If the field is involved in a mapper relationship (i.e. it is
136 the "foreign key" to a related table), the logic here will
137 also (try to) traverse that show display text for the related
138 object (if found).
140 :param version: Reference to the Continuum version object.
142 :param field: Name of the field, as string.
144 :param value: Raw value for the field, as obtained from the
145 version object.
147 :returns: Rendered cell value as HTML literal
148 """
149 # first render normal span; this is our fallback but also may
150 # be embedded within a more complex result.
151 text = HTML.tag("span", c=[repr(value)], style="font-family: monospace;")
153 # loop thru all mapped relationship props
154 for prop in self.mapper.relationships:
156 # we only want singletons
157 if prop.uselist:
158 continue
160 # loop thru columns for prop
161 # nb. there should always be just one colum for a
162 # singleton prop, but technically a list is used, so no
163 # harm in looping i assume..
164 for col in prop.local_columns:
166 # we only want the matching column
167 if col.name != field:
168 continue
170 # grab "related version" reference via prop key. this
171 # would be like a UserVersion for instance.
172 if ref := getattr(version, prop.key):
174 # grab "related object" reference. this would be
175 # like a User for instance.
176 if ref := getattr(ref, "version_parent", None):
178 # render text w/ related object as bold string
179 style = (
180 "margin-left: 2rem; font-style: italic; font-weight: bold;"
181 )
182 return HTML.tag(
183 "span",
184 c=[text, HTML.tag("span", c=[str(ref)], style=style)],
185 )
187 return text
189 def render_old_value(self, field):
190 if self.nature == "create":
191 return ""
192 value = self.old_value(field)
193 return self.render_version_value(self.version.previous, field, value)
195 def render_new_value(self, field):
196 if self.nature == "delete":
197 return ""
198 value = self.new_value(field)
199 return self.render_version_value(self.version, field, value)