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

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

26 

27import sqlalchemy as sa 

28 

29from pyramid.renderers import render 

30from webhelpers2.html import HTML 

31 

32from wuttjamaican.diffs import Diff 

33 

34 

35class WebDiff(Diff): 

36 """ 

37 Simple diff class for the web app. 

38 

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

44 

45 cell_padding = None 

46 

47 def render_html(self, template="/diff.mako", **kwargs): 

48 """ 

49 Render the diff as HTML table. 

50 

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

52 override the default. 

53 

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

55 the template renderer. 

56 

57 :returns: HTML literal string 

58 """ 

59 context = kwargs 

60 context["diff"] = self 

61 html = render(template, context) 

62 return HTML.literal(html) 

63 

64 

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. 

70 

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

72 

73 :param version: Reference to a Continuum version record object. 

74 

75 :param \\**kwargs: Remaining kwargs are passed as-is to the 

76 :class:`WebDiff` constructor. 

77 """ 

78 

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 ) 

84 

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

90 

91 self.operation_title = render_operation_type(self.version.operation_type) 

92 

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" 

103 

104 if "fields" not in kwargs: 

105 kwargs["fields"] = self.get_default_fields() 

106 

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) 

113 

114 super().__init__(config, old_data, new_data, **kwargs) 

115 

116 def get_default_fields(self): # pylint: disable=missing-function-docstring 

117 fields = sorted(self.version_mapper.columns.keys()) 

118 

119 unwanted = [ 

120 "transaction_id", 

121 "end_transaction_id", 

122 "operation_type", 

123 ] 

124 

125 return [field for field in fields if field not in unwanted] 

126 

127 def render_version_value(self, version, field, value): 

128 """ 

129 Render the cell value HTML for a given version + field. 

130 

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: 

134 

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

139 

140 :param version: Reference to the Continuum version object. 

141 

142 :param field: Name of the field, as string. 

143 

144 :param value: Raw value for the field, as obtained from the 

145 version object. 

146 

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

152 

153 # loop thru all mapped relationship props 

154 for prop in self.mapper.relationships: 

155 

156 # we only want singletons 

157 if prop.uselist: 

158 continue 

159 

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: 

165 

166 # we only want the matching column 

167 if col.name != field: 

168 continue 

169 

170 # grab "related version" reference via prop key. this 

171 # would be like a UserVersion for instance. 

172 if ref := getattr(version, prop.key): 

173 

174 # grab "related object" reference. this would be 

175 # like a User for instance. 

176 if ref := getattr(ref, "version_parent", None): 

177 

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 ) 

186 

187 return text 

188 

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) 

194 

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)