Coverage for .tox / coverage / lib / python3.11 / site-packages / wuttaweb / views / email.py: 100%

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

24Views for email settings 

25""" 

26 

27import colander 

28 

29from wuttaweb.views import MasterView 

30from wuttaweb.forms.schema import EmailRecipients 

31 

32 

33class EmailSettingView(MasterView): # pylint: disable=abstract-method 

34 """ 

35 Master view for :term:`email settings <email setting>`. 

36 """ 

37 

38 model_name = "email_setting" 

39 model_key = "key" 

40 model_title = "Email Setting" 

41 url_prefix = "/email/settings" 

42 filterable = False 

43 sortable = True 

44 sort_on_backend = False 

45 paginated = False 

46 creatable = False 

47 deletable = False 

48 

49 labels = { 

50 "key": "Email Key", 

51 "replyto": "Reply-To", 

52 } 

53 

54 grid_columns = [ 

55 "key", 

56 "subject", 

57 "to", 

58 "enabled", 

59 ] 

60 

61 # TODO: why does this not work? 

62 sort_defaults = "key" 

63 

64 form_fields = [ 

65 "key", 

66 "fallback_key", 

67 "description", 

68 "subject", 

69 "sender", 

70 "replyto", 

71 "to", 

72 "cc", 

73 "bcc", 

74 "notes", 

75 "enabled", 

76 ] 

77 

78 def __init__(self, request, context=None): 

79 super().__init__(request, context=context) 

80 self.email_handler = self.app.get_email_handler() 

81 

82 def get_grid_data(self, columns=None, session=None): 

83 """ 

84 This view calls 

85 :meth:`~wuttjamaican:wuttjamaican.email.EmailHandler.get_email_settings()` 

86 on the :attr:`email_handler` to obtain its grid data. 

87 """ 

88 data = [] 

89 for setting in self.email_handler.get_email_settings().values(): 

90 data.append(self.normalize_setting(setting)) 

91 return data 

92 

93 def normalize_setting(self, setting): # pylint: disable=empty-docstring 

94 """ """ 

95 key = setting.__name__ 

96 setting = setting(self.config) 

97 return { 

98 "key": key, 

99 "fallback_key": setting.fallback_key or "", 

100 "description": setting.get_description() or "", 

101 "subject": self.email_handler.get_auto_subject( 

102 key, rendered=False, setting=setting 

103 ), 

104 "sender": self.email_handler.get_auto_sender(key), 

105 "replyto": self.email_handler.get_auto_replyto(key) or colander.null, 

106 "to": self.email_handler.get_auto_to(key), 

107 "cc": self.email_handler.get_auto_cc(key), 

108 "bcc": self.email_handler.get_auto_bcc(key), 

109 "notes": self.email_handler.get_notes(key) or colander.null, 

110 "enabled": self.email_handler.is_enabled(key), 

111 } 

112 

113 def configure_grid(self, grid): # pylint: disable=empty-docstring 

114 """ """ 

115 g = grid 

116 super().configure_grid(g) 

117 

118 # key 

119 g.set_searchable("key") 

120 g.set_link("key") 

121 

122 # subject 

123 g.set_searchable("subject") 

124 g.set_link("subject") 

125 

126 # to 

127 g.set_renderer("to", self.render_to_short) 

128 

129 def render_to_short( # pylint: disable=empty-docstring,unused-argument 

130 self, setting, field, value 

131 ): 

132 """ """ 

133 recips = value 

134 if not recips: 

135 return None 

136 

137 if len(recips) < 3: 

138 return ", ".join(recips) 

139 

140 recips = ", ".join(recips[:2]) 

141 return f"{recips}, ..." 

142 

143 def get_instance( # pylint: disable=empty-docstring,arguments-differ,unused-argument 

144 self, **kwargs 

145 ): 

146 """ """ 

147 key = self.request.matchdict["key"] 

148 setting = self.email_handler.get_email_setting(key, instance=False) 

149 if setting: 

150 return self.normalize_setting(setting) 

151 

152 raise self.notfound() 

153 

154 def get_instance_title(self, instance): # pylint: disable=empty-docstring 

155 """ """ 

156 setting = instance 

157 return setting["subject"] 

158 

159 def configure_form(self, form): # pylint: disable=empty-docstring 

160 """ """ 

161 f = form 

162 super().configure_form(f) 

163 

164 # fallback_key 

165 f.set_readonly("fallback_key") 

166 

167 # description 

168 f.set_readonly("description") 

169 f.set_widget("description", "notes") 

170 

171 # replyto 

172 f.set_required("replyto", False) 

173 

174 # to 

175 f.set_node("to", EmailRecipients()) 

176 

177 # cc 

178 f.set_node("cc", EmailRecipients()) 

179 

180 # bcc 

181 f.set_node("bcc", EmailRecipients()) 

182 

183 # notes 

184 f.set_widget("notes", "notes") 

185 f.set_required("notes", False) 

186 

187 # enabled 

188 f.set_node("enabled", colander.Boolean()) 

189 

190 def persist( # pylint: disable=too-many-branches,empty-docstring,arguments-differ,unused-argument 

191 self, setting, **kwargs 

192 ): 

193 """ """ 

194 session = self.Session() 

195 key = self.request.matchdict["key"] 

196 

197 def save(name, value): 

198 self.app.save_setting( 

199 session, f"{self.config.appname}.email.{key}.{name}", value 

200 ) 

201 

202 def delete(name): 

203 self.app.delete_setting( 

204 session, f"{self.config.appname}.email.{key}.{name}" 

205 ) 

206 

207 # subject 

208 if setting["subject"]: 

209 save("subject", setting["subject"]) 

210 else: 

211 delete("subject") 

212 

213 # sender 

214 if setting["sender"]: 

215 save("sender", setting["sender"]) 

216 else: 

217 delete("sender") 

218 

219 # replyto 

220 if setting["replyto"]: 

221 save("replyto", setting["replyto"]) 

222 else: 

223 delete("replyto") 

224 

225 # to 

226 if setting["to"]: 

227 save("to", setting["to"]) 

228 else: 

229 delete("to") 

230 

231 # cc 

232 if setting["cc"]: 

233 save("cc", setting["cc"]) 

234 else: 

235 delete("cc") 

236 

237 # bcc 

238 if setting["bcc"]: 

239 save("bcc", setting["bcc"]) 

240 else: 

241 delete("bcc") 

242 

243 # notes 

244 if setting["notes"]: 

245 save("notes", setting["notes"]) 

246 else: 

247 delete("notes") 

248 

249 # enabled 

250 save("enabled", "true" if setting["enabled"] else "false") 

251 

252 def render_to_response(self, template, context): # pylint: disable=empty-docstring 

253 """ """ 

254 if self.viewing: 

255 setting = context["instance"] 

256 context["setting"] = setting 

257 

258 context["has_html_template"] = self.email_handler.get_auto_body_template( 

259 setting["key"], "html", fallback_key=setting["fallback_key"] 

260 ) 

261 context["has_txt_template"] = self.email_handler.get_auto_body_template( 

262 setting["key"], "txt", fallback_key=setting["fallback_key"] 

263 ) 

264 

265 return super().render_to_response(template, context) 

266 

267 def preview(self): 

268 """ 

269 View for showing a rendered preview of a given email template. 

270 

271 This will render the email template according to the "mode" 

272 requested - i.e. HTML or TXT. 

273 """ 

274 key = self.request.matchdict["key"] 

275 setting = self.email_handler.get_email_setting(key) 

276 context = setting.sample_data() 

277 mode = self.request.params.get("mode", "html") 

278 

279 if mode == "txt": 

280 body = self.email_handler.get_auto_txt_body( 

281 key, context, fallback_key=setting.fallback_key 

282 ) 

283 self.request.response.content_type = "text/plain" 

284 

285 else: # html 

286 body = self.email_handler.get_auto_html_body( 

287 key, context, fallback_key=setting.fallback_key 

288 ) 

289 

290 self.request.response.text = body 

291 return self.request.response 

292 

293 @classmethod 

294 def defaults(cls, config): # pylint: disable=empty-docstring 

295 """ """ 

296 cls._email_defaults(config) 

297 cls._defaults(config) 

298 

299 @classmethod 

300 def _email_defaults(cls, config): 

301 """ """ 

302 route_prefix = cls.get_route_prefix() 

303 permission_prefix = cls.get_permission_prefix() 

304 model_title_plural = cls.get_model_title_plural() 

305 instance_url_prefix = cls.get_instance_url_prefix() 

306 

307 # fix permission group 

308 config.add_wutta_permission_group( 

309 permission_prefix, model_title_plural, overwrite=False 

310 ) 

311 

312 # preview 

313 config.add_route(f"{route_prefix}.preview", f"{instance_url_prefix}/preview") 

314 config.add_view( 

315 cls, 

316 attr="preview", 

317 route_name=f"{route_prefix}.preview", 

318 permission=f"{permission_prefix}.view", 

319 ) 

320 

321 

322def defaults(config, **kwargs): # pylint: disable=missing-function-docstring 

323 base = globals() 

324 

325 EmailSettingView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name 

326 "EmailSettingView", base["EmailSettingView"] 

327 ) 

328 EmailSettingView.defaults(config) 

329 

330 

331def includeme(config): # pylint: disable=missing-function-docstring 

332 defaults(config)