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

135 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-17 14:42 -0600

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 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 # enabled 

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

131 

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

133 self, setting, field, value 

134 ): 

135 """ """ 

136 recips = value 

137 if not recips: 

138 return None 

139 

140 if len(recips) < 3: 

141 return ", ".join(recips) 

142 

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

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

145 

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

147 self, **kwargs 

148 ): 

149 """ """ 

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

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

152 if setting: 

153 return self.normalize_setting(setting) 

154 

155 raise self.notfound() 

156 

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

158 """ """ 

159 setting = instance 

160 return setting["subject"] 

161 

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

163 """ """ 

164 f = form 

165 super().configure_form(f) 

166 

167 # fallback_key 

168 f.set_readonly("fallback_key") 

169 

170 # description 

171 f.set_readonly("description") 

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

173 

174 # replyto 

175 f.set_required("replyto", False) 

176 

177 # to 

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

179 

180 # cc 

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

182 

183 # bcc 

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

185 

186 # notes 

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

188 f.set_required("notes", False) 

189 

190 # enabled 

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

192 

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

194 self, setting, **kwargs 

195 ): 

196 """ """ 

197 session = self.Session() 

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

199 

200 def save(name, value): 

201 self.app.save_setting( 

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

203 ) 

204 

205 def delete(name): 

206 self.app.delete_setting( 

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

208 ) 

209 

210 # subject 

211 if setting["subject"]: 

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

213 else: 

214 delete("subject") 

215 

216 # sender 

217 if setting["sender"]: 

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

219 else: 

220 delete("sender") 

221 

222 # replyto 

223 if setting["replyto"]: 

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

225 else: 

226 delete("replyto") 

227 

228 # to 

229 if setting["to"]: 

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

231 else: 

232 delete("to") 

233 

234 # cc 

235 if setting["cc"]: 

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

237 else: 

238 delete("cc") 

239 

240 # bcc 

241 if setting["bcc"]: 

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

243 else: 

244 delete("bcc") 

245 

246 # notes 

247 if setting["notes"]: 

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

249 else: 

250 delete("notes") 

251 

252 # enabled 

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

254 

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

256 """ """ 

257 if self.viewing: 

258 setting = context["instance"] 

259 context["setting"] = setting 

260 

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

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

263 ) 

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

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

266 ) 

267 

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

269 

270 def preview(self): 

271 """ 

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

273 

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

275 requested - i.e. HTML or TXT. 

276 """ 

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

278 setting = self.email_handler.get_email_setting(key) 

279 context = setting.sample_data() 

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

281 

282 if mode == "txt": 

283 body = self.email_handler.get_auto_txt_body( 

284 key, context, fallback_key=setting.fallback_key 

285 ) 

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

287 

288 else: # html 

289 body = self.email_handler.get_auto_html_body( 

290 key, context, fallback_key=setting.fallback_key 

291 ) 

292 

293 self.request.response.text = body 

294 return self.request.response 

295 

296 @classmethod 

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

298 """ """ 

299 cls._email_defaults(config) 

300 cls._defaults(config) 

301 

302 @classmethod 

303 def _email_defaults(cls, config): 

304 """ """ 

305 route_prefix = cls.get_route_prefix() 

306 permission_prefix = cls.get_permission_prefix() 

307 model_title_plural = cls.get_model_title_plural() 

308 instance_url_prefix = cls.get_instance_url_prefix() 

309 

310 # fix permission group 

311 config.add_wutta_permission_group( 

312 permission_prefix, model_title_plural, overwrite=False 

313 ) 

314 

315 # preview 

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

317 config.add_view( 

318 cls, 

319 attr="preview", 

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

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

322 ) 

323 

324 

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

326 base = globals() 

327 

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

329 "EmailSettingView", base["EmailSettingView"] 

330 ) 

331 EmailSettingView.defaults(config) 

332 

333 

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

335 defaults(config)