Coverage for .tox/coverage/lib/python3.11/site-packages/wuttjamaican/diffs.py: 100%
69 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-12-20 20:09 -0600
« prev ^ index » next coverage.py v7.11.0, created at 2025-12-20 20:09 -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 return sorted(set(self.old_data) | set(self.new_data), key=lambda x: x.lower())
88 def render_html(self, template=None, **kwargs):
89 """
90 Render the diff as HTML table.
92 :param template: Name of template to render, if you need to
93 override the default.
95 :param \\**kwargs: Remaining kwargs are passed as context to
96 the template renderer.
98 :returns: HTML literal string
99 """
100 context = kwargs
101 context["diff"] = self
103 if not isinstance(template, Template):
104 path = self.app.resource_path(
105 template or "wuttjamaican:templates/diff.mako"
106 )
107 template = Template(filename=path)
109 return HTML.literal(template.render(**context))
111 def render_field_row(self, field): # pylint: disable=missing-function-docstring
112 is_diff = self.values_differ(field)
114 kw = {}
115 if self.cell_padding:
116 kw["style"] = f"padding: {self.cell_padding}"
117 td_field = HTML.tag("td", class_="field", c=field, **kw)
119 td_old_value = HTML.tag(
120 "td",
121 c=self.render_old_value(field),
122 **self.get_old_value_attrs(is_diff),
123 )
125 td_new_value = HTML.tag(
126 "td",
127 c=self.render_new_value(field),
128 **self.get_new_value_attrs(is_diff),
129 )
131 return HTML.tag("tr", c=[td_field, td_old_value, td_new_value])
133 def render_cell_value(self, value): # pylint: disable=missing-function-docstring
134 return HTML.tag("span", c=[value], style="font-family: monospace;")
136 def render_old_value(self, field): # pylint: disable=missing-function-docstring
137 value = "" if self.nature == "create" else repr(self.old_value(field))
138 return self.render_cell_value(value)
140 def render_new_value(self, field): # pylint: disable=missing-function-docstring
141 value = "" if self.nature == "delete" else repr(self.new_value(field))
142 return self.render_cell_value(value)
144 def get_cell_attrs( # pylint: disable=missing-function-docstring
145 self, style=None, **attrs
146 ):
147 style = dict(style or {})
149 if self.cell_padding and "padding" not in style:
150 style["padding"] = self.cell_padding
152 if style:
153 attrs["style"] = "; ".join([f"{k}: {v}" for k, v in style.items()])
155 return attrs
157 def get_old_value_attrs( # pylint: disable=missing-function-docstring
158 self, is_diff
159 ):
160 style = {}
161 if self.nature == "update" and is_diff:
162 style["background-color"] = self.old_color
163 elif self.nature == "delete":
164 style["background-color"] = self.old_color
166 return self.get_cell_attrs(style)
168 def get_new_value_attrs( # pylint: disable=missing-function-docstring
169 self, is_diff
170 ):
171 style = {}
172 if self.nature == "create":
173 style["background-color"] = self.new_color
174 elif self.nature == "update" and is_diff:
175 style["background-color"] = self.new_color
177 return self.get_cell_attrs(style)
179 def old_value(self, field): # pylint: disable=missing-function-docstring
180 return self.old_data.get(field)
182 def new_value(self, field): # pylint: disable=missing-function-docstring
183 return self.new_data.get(field)
185 def values_differ(self, field): # pylint: disable=missing-function-docstring
186 return self.new_value(field) != self.old_value(field)