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
« 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"""
27import colander
29from wuttjamaican.problems import ProblemEmailSetting
31from wuttaweb.views import MasterView
32from wuttaweb.forms.schema import EmailRecipients
35class EmailSettingView(MasterView): # pylint: disable=abstract-method
36 """
37 Master view for :term:`email settings <email setting>`.
38 """
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
54 labels = {
55 "key": "Email Key",
56 "replyto": "Reply-To",
57 }
59 grid_columns = [
60 "key",
61 "subject",
62 "to",
63 "enabled",
64 ]
66 # TODO: why does this not work?
67 sort_defaults = "key"
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 ]
83 def __init__(self, request, context=None):
84 super().__init__(request, context=context)
85 self.email_handler = self.app.get_email_handler()
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
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 }
119 def configure_grid(self, grid): # pylint: disable=empty-docstring
120 """ """
121 g = grid
122 super().configure_grid(g)
124 # key
125 g.set_searchable("key")
126 g.set_link("key")
128 # subject
129 g.set_searchable("subject")
130 g.set_link("subject")
132 # to
133 g.set_renderer("to", self.render_to_short)
135 # enabled
136 g.set_renderer("enabled", "boolean")
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
146 if len(recips) < 3:
147 return ", ".join(recips)
149 recips = ", ".join(recips[:2])
150 return f"{recips}, ..."
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)
161 raise self.notfound()
163 def get_instance_title(self, instance): # pylint: disable=empty-docstring
164 """ """
165 setting = instance
166 return setting["subject"]
168 def configure_form(self, form): # pylint: disable=empty-docstring
169 """ """
170 f = form
171 super().configure_form(f)
172 setting = f.model_instance
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")
184 # description
185 f.set_readonly("description")
186 f.set_widget("description", "notes")
188 # replyto
189 f.set_required("replyto", False)
191 # to
192 f.set_node("to", EmailRecipients())
194 # cc
195 f.set_node("cc", EmailRecipients())
197 # bcc
198 f.set_node("bcc", EmailRecipients())
200 # notes
201 f.set_widget("notes", "notes")
202 f.set_required("notes", False)
204 # enabled
205 f.set_node("enabled", colander.Boolean())
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"]
214 def save(name, value):
215 self.app.save_setting(
216 session, f"{self.config.appname}.email.{key}.{name}", value
217 )
219 def delete(name):
220 self.app.delete_setting(
221 session, f"{self.config.appname}.email.{key}.{name}"
222 )
224 # subject
225 if setting["subject"]:
226 save("subject", setting["subject"])
227 else:
228 delete("subject")
230 # sender
231 if setting["sender"]:
232 save("sender", setting["sender"])
233 else:
234 delete("sender")
236 # replyto
237 if setting["replyto"]:
238 save("replyto", setting["replyto"])
239 else:
240 delete("replyto")
242 # to
243 if setting["to"]:
244 save("to", setting["to"])
245 else:
246 delete("to")
248 # cc
249 if setting["cc"]:
250 save("cc", setting["cc"])
251 else:
252 delete("cc")
254 # bcc
255 if setting["bcc"]:
256 save("bcc", setting["bcc"])
257 else:
258 delete("bcc")
260 # notes
261 if setting["notes"]:
262 save("notes", setting["notes"])
263 else:
264 delete("notes")
266 # enabled
267 save("enabled", "true" if setting["enabled"] else "false")
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 = []
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"]
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
302 return buttons
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
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 )
317 return super().render_to_response(template, context)
319 def preview(self):
320 """
321 View for showing a rendered preview of a given email template.
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")
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"
337 else: # html
338 body = self.email_handler.get_auto_html_body(
339 key, context, fallback_key=setting.fallback_key
340 )
342 self.request.response.text = body
343 return self.request.response
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 ]
360 return simple_settings
362 @classmethod
363 def defaults(cls, config): # pylint: disable=empty-docstring
364 """ """
365 cls._email_defaults(config)
366 cls._defaults(config)
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()
376 # fix permission group
377 config.add_wutta_permission_group(
378 permission_prefix, model_title_plural, overwrite=False
379 )
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 )
391def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
392 base = globals()
394 EmailSettingView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
395 "EmailSettingView", base["EmailSettingView"]
396 )
397 EmailSettingView.defaults(config)
400def includeme(config): # pylint: disable=missing-function-docstring
401 defaults(config)