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

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 return sorted(set(self.old_data) | set(self.new_data), key=lambda x: x.lower()) 

87 

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

89 """ 

90 Render the diff as HTML table. 

91 

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

93 override the default. 

94 

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

96 the template renderer. 

97 

98 :returns: HTML literal string 

99 """ 

100 context = kwargs 

101 context["diff"] = self 

102 

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) 

108 

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

110 

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

112 is_diff = self.values_differ(field) 

113 

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) 

118 

119 td_old_value = HTML.tag( 

120 "td", 

121 c=self.render_old_value(field), 

122 **self.get_old_value_attrs(is_diff), 

123 ) 

124 

125 td_new_value = HTML.tag( 

126 "td", 

127 c=self.render_new_value(field), 

128 **self.get_new_value_attrs(is_diff), 

129 ) 

130 

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

132 

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

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

135 

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) 

139 

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) 

143 

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

145 self, style=None, **attrs 

146 ): 

147 style = dict(style or {}) 

148 

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

150 style["padding"] = self.cell_padding 

151 

152 if style: 

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

154 

155 return attrs 

156 

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 

165 

166 return self.get_cell_attrs(style) 

167 

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 

176 

177 return self.get_cell_attrs(style) 

178 

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

180 return self.old_data.get(field) 

181 

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

183 return self.new_data.get(field) 

184 

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

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