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

157 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-24 16:36 -0500

1# -*- coding: utf-8; -*- 

2################################################################################ 

3# 

4# wuttaweb -- Web App for Wutta Framework 

5# Copyright © 2024-2026 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 wuttjamaican.problems import ProblemEmailSetting 

30 

31from wuttaweb.views import MasterView 

32from wuttaweb.forms.schema import EmailRecipients 

33 

34 

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

36 """ 

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

38 """ 

39 

40 model_name = "email_setting" 

41 model_key = "key" 

42 model_title = "Email Setting" 

43 url_prefix = "/email/settings" 

44 # pylint: disable=duplicate-code 

45 filterable = False 

46 sortable = True 

47 sort_on_backend = False 

48 paginated = False 

49 creatable = False 

50 deletable = False 

51 configurable = True 

52 # pylint: enable=duplicate-code 

53 

54 labels = { 

55 "key": "Email Key", 

56 "replyto": "Reply-To", 

57 } 

58 

59 grid_columns = [ 

60 "key", 

61 "subject", 

62 "to", 

63 "enabled", 

64 ] 

65 

66 # TODO: why does this not work? 

67 sort_defaults = "key" 

68 

69 form_fields = [ 

70 "key", 

71 "fallback_key", 

72 "description", 

73 "subject", 

74 "sender", 

75 "replyto", 

76 "to", 

77 "cc", 

78 "bcc", 

79 "notes", 

80 "enabled", 

81 ] 

82 

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

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

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

86 

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

88 """ 

89 This view calls 

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

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

92 """ 

93 data = [] 

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

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

96 return data 

97 

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

99 """ """ 

100 key = setting.__name__ 

101 setting = setting(self.config) 

102 return { 

103 "_setting": setting, 

104 "key": key, 

105 "fallback_key": setting.fallback_key or "", 

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

107 "subject": self.email_handler.get_auto_subject( 

108 key, rendered=False, setting=setting 

109 ), 

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

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

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

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

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

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

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

117 } 

118 

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

120 """ """ 

121 g = grid 

122 super().configure_grid(g) 

123 

124 # key 

125 g.set_searchable("key") 

126 g.set_link("key") 

127 

128 # subject 

129 g.set_searchable("subject") 

130 g.set_link("subject") 

131 

132 # to 

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

134 

135 # enabled 

136 g.set_renderer("enabled", "boolean") 

137 

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

139 self, setting, field, value 

140 ): 

141 """ """ 

142 recips = value 

143 if not recips: 

144 return None 

145 

146 if len(recips) < 3: 

147 return ", ".join(recips) 

148 

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

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

151 

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

153 self, **kwargs 

154 ): 

155 """ """ 

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

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

158 if setting: 

159 return self.normalize_setting(setting) 

160 

161 raise self.notfound() 

162 

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

164 """ """ 

165 setting = instance 

166 return setting["subject"] 

167 

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

169 """ """ 

170 f = form 

171 super().configure_form(f) 

172 setting = f.model_instance 

173 

174 # fallback_key 

175 if ( 

176 not self.creating 

177 and setting["fallback_key"] 

178 and setting["fallback_key"] != setting["key"] 

179 ): 

180 f.set_readonly("fallback_key") 

181 else: 

182 f.remove("fallback_key") 

183 

184 # description 

185 f.set_readonly("description") 

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

187 

188 # replyto 

189 f.set_required("replyto", False) 

190 

191 # to 

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

193 

194 # cc 

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

196 

197 # bcc 

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

199 

200 # notes 

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

202 f.set_required("notes", False) 

203 

204 # enabled 

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

206 

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

208 self, setting, **kwargs 

209 ): 

210 """ """ 

211 session = self.Session() 

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

213 

214 def save(name, value): 

215 self.app.save_setting( 

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

217 ) 

218 

219 def delete(name): 

220 self.app.delete_setting( 

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

222 ) 

223 

224 # subject 

225 if setting["subject"]: 

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

227 else: 

228 delete("subject") 

229 

230 # sender 

231 if setting["sender"]: 

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

233 else: 

234 delete("sender") 

235 

236 # replyto 

237 if setting["replyto"]: 

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

239 else: 

240 delete("replyto") 

241 

242 # to 

243 if setting["to"]: 

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

245 else: 

246 delete("to") 

247 

248 # cc 

249 if setting["cc"]: 

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

251 else: 

252 delete("cc") 

253 

254 # bcc 

255 if setting["bcc"]: 

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

257 else: 

258 delete("bcc") 

259 

260 # notes 

261 if setting["notes"]: 

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

263 else: 

264 delete("notes") 

265 

266 # enabled 

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

268 

269 def get_xref_buttons(self, obj): 

270 """ 

271 By default this returns an empty list, unless the email 

272 setting is for a :term:`problem check`, in which case a button 

273 to view the latter is included. 

274 """ 

275 setting = obj 

276 buttons = [] 

277 

278 setting_class = type(setting["_setting"]) 

279 if issubclass(setting_class, ProblemEmailSetting): 

280 prob_handler = self.app.get_problem_handler() 

281 email_key = setting["key"] 

282 

283 # ugh this seems heavy-handed, but good enough i guess.. 

284 for check in prob_handler.get_all_problem_checks(): 

285 key = prob_handler.get_email_key(check) 

286 if key == email_key: 

287 url = self.request.route_url( 

288 "problem_checks.view", 

289 system_key=check.system_key, 

290 problem_key=check.problem_key, 

291 ) 

292 buttons.append( 

293 self.make_button( 

294 "Problem Check", 

295 primary=True, 

296 url=url, 

297 icon_left="exclamation", 

298 ) 

299 ) 

300 break 

301 

302 return buttons 

303 

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

305 """ """ 

306 if self.viewing: 

307 setting = context["instance"] 

308 context["setting"] = setting 

309 

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

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

312 ) 

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

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

315 ) 

316 

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

318 

319 def preview(self): 

320 """ 

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

322 

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

324 requested - i.e. HTML or TXT. 

325 """ 

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

327 setting = self.email_handler.get_email_setting(key) 

328 context = setting.get_sample_data() 

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

330 

331 if mode == "txt": 

332 body = self.email_handler.get_auto_txt_body( 

333 key, context, fallback_key=setting.fallback_key 

334 ) 

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

336 

337 else: # html 

338 body = self.email_handler.get_auto_html_body( 

339 key, context, fallback_key=setting.fallback_key 

340 ) 

341 

342 self.request.response.text = body 

343 return self.request.response 

344 

345 def configure_get_simple_settings(self): # pylint: disable=empty-docstring 

346 """ """ 

347 simple_settings = [ 

348 { 

349 "name": f"{self.config.appname}.mail.send_emails", 

350 "type": bool, 

351 "default": False, 

352 }, 

353 {"name": f"{self.config.appname}.email.default.sender"}, 

354 {"name": f"{self.config.appname}.email.default.subject"}, 

355 {"name": f"{self.config.appname}.email.default.to"}, 

356 {"name": f"{self.config.appname}.email.feedback.subject"}, 

357 {"name": f"{self.config.appname}.email.feedback.to"}, 

358 ] 

359 

360 return simple_settings 

361 

362 @classmethod 

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

364 """ """ 

365 cls._email_defaults(config) 

366 cls._defaults(config) 

367 

368 @classmethod 

369 def _email_defaults(cls, config): 

370 """ """ 

371 route_prefix = cls.get_route_prefix() 

372 permission_prefix = cls.get_permission_prefix() 

373 model_title_plural = cls.get_model_title_plural() 

374 instance_url_prefix = cls.get_instance_url_prefix() 

375 

376 # fix permission group 

377 config.add_wutta_permission_group( 

378 permission_prefix, model_title_plural, overwrite=False 

379 ) 

380 

381 # preview 

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

383 config.add_view( 

384 cls, 

385 attr="preview", 

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

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

388 ) 

389 

390 

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

392 base = globals() 

393 

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

395 "EmailSettingView", base["EmailSettingView"] 

396 ) 

397 EmailSettingView.defaults(config) 

398 

399 

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

401 defaults(config)