Coverage for .tox/coverage/lib/python3.11/site-packages/wuttjamaican/diffs.py: 100%
72 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-25 09:03 -0600
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-25 09:03 -0600
1# -*- coding: utf-8; -*-
2################################################################################
3#
4# WuttJamaican -- Base package for Wutta Framework
5# Copyright © 2023-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"""
27from mako.template import Template
28from webhelpers2.html import HTML
31class Diff: # pylint: disable=too-many-instance-attributes
32 """
33 Represent / display a basic "diff" between two data records.
35 You must provide both the "old" and "new" data records, when
36 constructing an instance of this class. Then call
37 :meth:`render_html()` to display the diff table.
39 :param config: The app :term:`config object`.
41 :param old_data: Dict of "old" data record.
43 :param new_data: Dict of "new" data record.
45 :param fields: Optional list of field names. If not specified,
46 will be derived from the data records.
48 :param nature: What sort of diff is being represented; must be one
49 of: ``("create", "update", "delete")``
51 :param old_color: Background color to display for "old/deleted"
52 field data, when applicable.
54 :param new_color: Background color to display for "new/created"
55 field data, when applicable.
57 :param cell_padding: Optional override for cell padding style.
58 """
60 cell_padding = "0.25rem"
62 def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments
63 self,
64 config,
65 old_data: dict,
66 new_data: dict,
67 fields: list = None,
68 nature="update",
69 old_color="#ffebe9",
70 new_color="#dafbe1",
71 cell_padding=None,
72 ):
73 self.config = config
74 self.app = self.config.get_app()
75 self.old_data = old_data
76 self.new_data = new_data
77 self.columns = ["field name", "old value", "new value"]
78 self.fields = fields or self.make_fields()
79 self.nature = nature
80 self.old_color = old_color
81 self.new_color = new_color
82 if cell_padding:
83 self.cell_padding = cell_padding
85 def make_fields(self): # pylint: disable=missing-function-docstring
86 if self.old_data and self.new_data:
87 fields = set(self.old_data) & set(self.new_data)
88 else:
89 fields = set(self.old_data or self.new_data)
90 return sorted(fields, key=lambda f: f.lower())
92 def render_html(self, template=None, **kwargs):
93 """
94 Render the diff as HTML table.
96 :param template: Name of template to render, if you need to
97 override the default.
99 :param \\**kwargs: Remaining kwargs are passed as context to
100 the template renderer.
102 :returns: HTML literal string
103 """
104 context = kwargs
105 context["diff"] = self
107 if not isinstance(template, Template):
108 path = self.app.resource_path(
109 template or "wuttjamaican:templates/diff.mako"
110 )
111 template = Template(filename=path)
113 return HTML.literal(template.render(**context))
115 def render_field_row(self, field): # pylint: disable=missing-function-docstring
116 is_diff = self.values_differ(field)
118 kw = {}
119 if self.cell_padding:
120 kw["style"] = f"padding: {self.cell_padding}"
121 td_field = HTML.tag("td", class_="field", c=field, **kw)
123 td_old_value = HTML.tag(
124 "td",
125 c=self.render_old_value(field),
126 **self.get_old_value_attrs(is_diff),
127 )
129 td_new_value = HTML.tag(
130 "td",
131 c=self.render_new_value(field),
132 **self.get_new_value_attrs(is_diff),
133 )
135 return HTML.tag("tr", c=[td_field, td_old_value, td_new_value])
137 def render_cell_value(self, value): # pylint: disable=missing-function-docstring
138 return HTML.tag("span", c=[value], style="font-family: monospace;")
140 def render_old_value(self, field): # pylint: disable=missing-function-docstring
141 value = "" if self.nature == "create" else repr(self.old_value(field))
142 return self.render_cell_value(value)
144 def render_new_value(self, field): # pylint: disable=missing-function-docstring
145 value = "" if self.nature == "delete" else repr(self.new_value(field))
146 return self.render_cell_value(value)
148 def get_cell_attrs( # pylint: disable=missing-function-docstring
149 self, style=None, **attrs
150 ):
151 style = dict(style or {})
153 if self.cell_padding and "padding" not in style:
154 style["padding"] = self.cell_padding
156 if style:
157 attrs["style"] = "; ".join([f"{k}: {v}" for k, v in style.items()])
159 return attrs
161 def get_old_value_attrs( # pylint: disable=missing-function-docstring
162 self, is_diff
163 ):
164 style = {}
165 if self.nature == "update" and is_diff:
166 style["background-color"] = self.old_color
167 elif self.nature == "delete":
168 style["background-color"] = self.old_color
170 return self.get_cell_attrs(style)
172 def get_new_value_attrs( # pylint: disable=missing-function-docstring
173 self, is_diff
174 ):
175 style = {}
176 if self.nature == "create":
177 style["background-color"] = self.new_color
178 elif self.nature == "update" and is_diff:
179 style["background-color"] = self.new_color
181 return self.get_cell_attrs(style)
183 def old_value(self, field): # pylint: disable=missing-function-docstring
184 return self.old_data.get(field)
186 def new_value(self, field): # pylint: disable=missing-function-docstring
187 return self.new_data.get(field)
189 def values_differ(self, field): # pylint: disable=missing-function-docstring
190 return self.new_value(field) != self.old_value(field)