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

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""" 

26 

27from mako.template import Template 

28from webhelpers2.html import HTML 

29 

30 

31class Diff: # pylint: disable=too-many-instance-attributes 

32 """ 

33 Represent / display a basic "diff" between two data records. 

34 

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. 

38 

39 :param config: The app :term:`config object`. 

40 

41 :param old_data: Dict of "old" data record. 

42 

43 :param new_data: Dict of "new" data record. 

44 

45 :param fields: Optional list of field names. If not specified, 

46 will be derived from the data records. 

47 

48 :param nature: What sort of diff is being represented; must be one 

49 of: ``("create", "update", "delete")`` 

50 

51 :param old_color: Background color to display for "old/deleted" 

52 field data, when applicable. 

53 

54 :param new_color: Background color to display for "new/created" 

55 field data, when applicable. 

56 

57 :param cell_padding: Optional override for cell padding style. 

58 """ 

59 

60 cell_padding = "0.25rem" 

61 

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 

84 

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()) 

91 

92 def render_html(self, template=None, **kwargs): 

93 """ 

94 Render the diff as HTML table. 

95 

96 :param template: Name of template to render, if you need to 

97 override the default. 

98 

99 :param \\**kwargs: Remaining kwargs are passed as context to 

100 the template renderer. 

101 

102 :returns: HTML literal string 

103 """ 

104 context = kwargs 

105 context["diff"] = self 

106 

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) 

112 

113 return HTML.literal(template.render(**context)) 

114 

115 def render_field_row(self, field): # pylint: disable=missing-function-docstring 

116 is_diff = self.values_differ(field) 

117 

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) 

122 

123 td_old_value = HTML.tag( 

124 "td", 

125 c=self.render_old_value(field), 

126 **self.get_old_value_attrs(is_diff), 

127 ) 

128 

129 td_new_value = HTML.tag( 

130 "td", 

131 c=self.render_new_value(field), 

132 **self.get_new_value_attrs(is_diff), 

133 ) 

134 

135 return HTML.tag("tr", c=[td_field, td_old_value, td_new_value]) 

136 

137 def render_cell_value(self, value): # pylint: disable=missing-function-docstring 

138 return HTML.tag("span", c=[value], style="font-family: monospace;") 

139 

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) 

143 

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) 

147 

148 def get_cell_attrs( # pylint: disable=missing-function-docstring 

149 self, style=None, **attrs 

150 ): 

151 style = dict(style or {}) 

152 

153 if self.cell_padding and "padding" not in style: 

154 style["padding"] = self.cell_padding 

155 

156 if style: 

157 attrs["style"] = "; ".join([f"{k}: {v}" for k, v in style.items()]) 

158 

159 return attrs 

160 

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 

169 

170 return self.get_cell_attrs(style) 

171 

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 

180 

181 return self.get_cell_attrs(style) 

182 

183 def old_value(self, field): # pylint: disable=missing-function-docstring 

184 return self.old_data.get(field) 

185 

186 def new_value(self, field): # pylint: disable=missing-function-docstring 

187 return self.new_data.get(field) 

188 

189 def values_differ(self, field): # pylint: disable=missing-function-docstring 

190 return self.new_value(field) != self.old_value(field)