Total Complexity | 45 |
Total Lines | 727 |
Duplicated Lines | 18.57 % |
Changes | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like notifier often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | # -*- coding: utf-8 -*- |
||
2 | import datetime |
||
3 | import typing |
||
4 | from email.mime.multipart import MIMEMultipart |
||
5 | from email.mime.text import MIMEText |
||
6 | from email.utils import formataddr |
||
7 | from smtplib import SMTPRecipientsRefused |
||
8 | |||
9 | from lxml.html.diff import htmldiff |
||
10 | from mako.template import Template |
||
11 | from sqlalchemy.orm import Session |
||
12 | |||
13 | from tracim_backend.app_models.contents import content_type_list |
||
14 | from tracim_backend.config import CFG |
||
15 | from tracim_backend.exceptions import EmptyNotificationError |
||
16 | from tracim_backend.lib.core.notifications import INotifier |
||
17 | from tracim_backend.lib.core.workspace import WorkspaceApi |
||
18 | from tracim_backend.lib.mail_notifier.sender import EmailSender |
||
19 | from tracim_backend.lib.mail_notifier.sender import send_email_through |
||
20 | from tracim_backend.lib.mail_notifier.utils import EST |
||
21 | from tracim_backend.lib.mail_notifier.utils import SmtpConfiguration |
||
22 | from tracim_backend.lib.utils.logger import logger |
||
23 | from tracim_backend.lib.utils.translation import Translator |
||
24 | from tracim_backend.lib.utils.utils import get_email_logo_frontend_url |
||
25 | from tracim_backend.lib.utils.utils import get_login_frontend_url |
||
26 | from tracim_backend.lib.utils.utils import get_reset_password_frontend_url |
||
27 | from tracim_backend.models.auth import User |
||
28 | from tracim_backend.models.context_models import ContentInContext |
||
29 | from tracim_backend.models.context_models import WorkspaceInContext |
||
30 | from tracim_backend.models.data import ActionDescription |
||
31 | from tracim_backend.models.data import Content |
||
32 | from tracim_backend.models.data import UserRoleInWorkspace |
||
33 | |||
34 | |||
35 | class EmailNotifier(INotifier): |
||
36 | """ |
||
37 | EmailNotifier, this class will decide how to notify by mail |
||
38 | in order to let a EmailManager create email |
||
39 | """ |
||
40 | |||
41 | def __init__( |
||
42 | self, |
||
43 | config: CFG, |
||
44 | session: Session, |
||
45 | current_user: User=None |
||
46 | ): |
||
47 | """ |
||
48 | :param current_user: the user that has triggered the notification |
||
49 | :return: |
||
50 | """ |
||
51 | INotifier.__init__(self, config, session, current_user) |
||
52 | logger.info(self, 'Instantiating Email Notifier') |
||
53 | |||
54 | self._user = current_user |
||
55 | self.session = session |
||
56 | self.config = config |
||
57 | self._smtp_config = SmtpConfiguration( |
||
58 | self.config.EMAIL_NOTIFICATION_SMTP_SERVER, |
||
59 | self.config.EMAIL_NOTIFICATION_SMTP_PORT, |
||
60 | self.config.EMAIL_NOTIFICATION_SMTP_USER, |
||
61 | self.config.EMAIL_NOTIFICATION_SMTP_PASSWORD |
||
62 | ) |
||
63 | |||
64 | def notify_content_update(self, content: Content): |
||
65 | |||
66 | if content.get_last_action().id not \ |
||
67 | in self.config.EMAIL_NOTIFICATION_NOTIFIED_EVENTS: |
||
68 | logger.info( |
||
69 | self, |
||
70 | 'Skip email notification for update of content {}' |
||
71 | 'by user {} (the action is {})'.format( |
||
72 | content.content_id, |
||
73 | # below: 0 means "no user" |
||
74 | self._user.user_id if self._user else 0, |
||
75 | content.get_last_action().id |
||
76 | ) |
||
77 | ) |
||
78 | return |
||
79 | |||
80 | logger.info(self, |
||
81 | 'About to email-notify update' |
||
82 | 'of content {} by user {}'.format( |
||
83 | content.content_id, |
||
84 | # Below: 0 means "no user" |
||
85 | self._user.user_id if self._user else 0 |
||
86 | ) |
||
87 | ) |
||
88 | |||
89 | if content.type not \ |
||
90 | in self.config.EMAIL_NOTIFICATION_NOTIFIED_CONTENTS: |
||
91 | logger.info( |
||
92 | self, |
||
93 | 'Skip email notification for update of content {}' |
||
94 | 'by user {} (the content type is {})'.format( |
||
95 | content.type, |
||
96 | # below: 0 means "no user" |
||
97 | self._user.user_id if self._user else 0, |
||
98 | content.get_last_action().id |
||
99 | ) |
||
100 | ) |
||
101 | return |
||
102 | |||
103 | logger.info(self, |
||
104 | 'About to email-notify update' |
||
105 | 'of content {} by user {}'.format( |
||
106 | content.content_id, |
||
107 | # Below: 0 means "no user" |
||
108 | self._user.user_id if self._user else 0 |
||
109 | ) |
||
110 | ) |
||
111 | |||
112 | #### |
||
113 | # |
||
114 | # INFO - D.A. - 2014-11-05 - Emails are sent through asynchronous jobs. |
||
115 | # For that reason, we do not give SQLAlchemy objects but ids only |
||
116 | # (SQLA objects are related to a given thread/session) |
||
117 | # |
||
118 | try: |
||
119 | if self.config.EMAIL_NOTIFICATION_PROCESSING_MODE.lower() == self.config.CST.ASYNC.lower(): |
||
120 | logger.info(self, 'Sending email in ASYNC mode') |
||
121 | # TODO - D.A - 2014-11-06 |
||
122 | # This feature must be implemented in order to be able to scale to large communities |
||
123 | raise NotImplementedError('Sending emails through ASYNC mode is not working yet') |
||
124 | else: |
||
125 | logger.info(self, 'Sending email in SYNC mode') |
||
126 | EmailManager( |
||
127 | self._smtp_config, |
||
128 | self.config, |
||
129 | self.session, |
||
130 | ).notify_content_update(self._user.user_id, content.content_id) |
||
131 | except Exception as e: |
||
132 | # TODO - G.M - 2018-08-27 - Do Better catching for exception here |
||
133 | logger.error(self, 'Exception catched during email notification: {}'.format(e.__str__())) |
||
134 | logger.exception(self, e) |
||
135 | |||
136 | |||
137 | class EmailManager(object): |
||
138 | """ |
||
139 | Compared to Notifier, this class is independant from the HTTP request thread |
||
140 | This class will build Email and send it for both created account and content |
||
141 | update |
||
142 | """ |
||
143 | |||
144 | def __init__( |
||
145 | self, |
||
146 | smtp_config: SmtpConfiguration, |
||
147 | config: CFG, |
||
148 | session: Session |
||
149 | ) -> None: |
||
150 | self._smtp_config = smtp_config |
||
151 | self.config = config |
||
152 | self.session = session |
||
153 | # FIXME - G.M - We need to have a session for the emailNotifier |
||
154 | |||
155 | # if not self.session: |
||
156 | # engine = get_engine(settings) |
||
157 | # session_factory = get_session_factory(engine) |
||
158 | # app_config = CFG(settings) |
||
159 | |||
160 | def _get_sender(self, user: User=None) -> str: |
||
161 | """ |
||
162 | Return sender string like "Bob Dylan |
||
163 | (via Tracim) <[email protected]>" |
||
164 | :param user: user to extract display name |
||
165 | :return: sender string |
||
166 | """ |
||
167 | |||
168 | email_template = self.config.EMAIL_NOTIFICATION_FROM_EMAIL |
||
169 | mail_sender_name = self.config.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL # nopep8 |
||
170 | if user: |
||
171 | mail_sender_name = '{name} via Tracim'.format(name=user.display_name) |
||
172 | email_address = email_template.replace('{user_id}', str(user.user_id)) |
||
173 | # INFO - D.A. - 2017-08-04 |
||
174 | # We use email_template.replace() instead of .format() because this |
||
175 | # method is more robust to errors in config file. |
||
176 | # |
||
177 | # For example, if the email is info+{userid}@tracim.fr |
||
178 | # email.format(user_id='bob') will raise an exception |
||
179 | # email.replace('{user_id}', 'bob') will just ignore {userid} |
||
180 | else: |
||
181 | email_address = email_template.replace('{user_id}', '0') |
||
182 | |||
183 | return formataddr((mail_sender_name, email_address)) |
||
184 | |||
185 | # Content Notification |
||
186 | |||
187 | @staticmethod |
||
188 | def log_notification( |
||
189 | config: CFG, |
||
190 | action: str, |
||
191 | recipient: typing.Optional[str], |
||
192 | subject: typing.Optional[str], |
||
193 | ) -> None: |
||
194 | """Log notification metadata.""" |
||
195 | log_path = config.EMAIL_NOTIFICATION_LOG_FILE_PATH |
||
196 | if log_path: |
||
197 | # TODO - A.P - 2017-09-06 - file logging inefficiency |
||
198 | # Updating a document with 100 users to notify will leads to open |
||
199 | # and close the file 100 times. |
||
200 | with open(log_path, 'a') as log_file: |
||
201 | print( |
||
202 | datetime.datetime.now(), |
||
203 | action, |
||
204 | recipient, |
||
205 | subject, |
||
206 | sep='|', |
||
207 | file=log_file, |
||
208 | ) |
||
209 | |||
210 | def notify_content_update( |
||
211 | self, |
||
212 | event_actor_id: int, |
||
213 | event_content_id: int |
||
214 | ) -> None: |
||
215 | """ |
||
216 | Look for all users to be notified about the new content and send them an |
||
217 | individual email |
||
218 | :param event_actor_id: id of the user that has triggered the event |
||
219 | :param event_content_id: related content_id |
||
220 | :return: |
||
221 | """ |
||
222 | # FIXME - D.A. - 2014-11-05 |
||
223 | # Dirty import. It's here in order to avoid circular import |
||
224 | from tracim_backend.lib.core.content import ContentApi |
||
225 | from tracim_backend.lib.core.user import UserApi |
||
226 | user = UserApi( |
||
227 | None, |
||
228 | config=self.config, |
||
229 | session=self.session, |
||
230 | ).get_one(event_actor_id) |
||
231 | logger.debug(self, 'Content: {}'.format(event_content_id)) |
||
232 | content_api = ContentApi( |
||
233 | current_user=user, |
||
234 | session=self.session, |
||
235 | config=self.config, |
||
236 | ) |
||
237 | content = ContentApi( |
||
238 | session=self.session, |
||
239 | current_user=user, # nopep8 TODO - use a system user instead of the user that has triggered the event |
||
240 | config=self.config, |
||
241 | show_archived=True, |
||
242 | show_deleted=True, |
||
243 | ).get_one(event_content_id, content_type_list.Any_SLUG) |
||
244 | workspace_api = WorkspaceApi( |
||
245 | session=self.session, |
||
246 | current_user=user, |
||
247 | config=self.config, |
||
248 | ) |
||
249 | workpace_in_context = workspace_api.get_workspace_with_context(workspace_api.get_one(content.workspace_id)) # nopep8 |
||
250 | main_content = content.parent if content.type == content_type_list.Comment.slug else content # nopep8 |
||
251 | notifiable_roles = WorkspaceApi( |
||
252 | current_user=user, |
||
253 | session=self.session, |
||
254 | config=self.config, |
||
255 | ).get_notifiable_roles(content.workspace) |
||
256 | |||
257 | if len(notifiable_roles) <= 0: |
||
258 | logger.info(self, 'Skipping notification as nobody subscribed to in workspace {}'.format(content.workspace.label)) |
||
259 | return |
||
260 | |||
261 | |||
262 | logger.info(self, 'Sending asynchronous emails to {} user(s)'.format(len(notifiable_roles))) |
||
263 | # INFO - D.A. - 2014-11-06 |
||
264 | # The following email sender will send emails in the async task queue |
||
265 | # This allow to build all mails through current thread but really send them (including SMTP connection) |
||
266 | # In the other thread. |
||
267 | # |
||
268 | # This way, the webserver will return sooner (actually before notification emails are sent |
||
269 | async_email_sender = EmailSender( |
||
270 | self.config, |
||
271 | self._smtp_config, |
||
272 | self.config.EMAIL_NOTIFICATION_ACTIVATED |
||
273 | ) |
||
274 | for role in notifiable_roles: |
||
275 | logger.info(self, 'Sending email to {}'.format(role.user.email)) |
||
276 | translator = Translator(app_config=self.config, default_lang=role.user.lang) # nopep8 |
||
277 | _ = translator.get_translation |
||
278 | to_addr = formataddr((role.user.display_name, role.user.email)) |
||
279 | # INFO - G.M - 2017-11-15 - set content_id in header to permit reply |
||
280 | # references can have multiple values, but only one in this case. |
||
281 | replyto_addr = self.config.EMAIL_NOTIFICATION_REPLY_TO_EMAIL.replace( # nopep8 |
||
282 | '{content_id}', str(main_content.content_id) |
||
283 | ) |
||
284 | |||
285 | reference_addr = self.config.EMAIL_NOTIFICATION_REFERENCES_EMAIL.replace( #nopep8 |
||
286 | '{content_id}',str(main_content.content_id) |
||
287 | ) |
||
288 | # |
||
289 | # INFO - D.A. - 2014-11-06 |
||
290 | # We do not use .format() here because the subject defined in the .ini file |
||
291 | # may not include all required labels. In order to avoid partial format() (which result in an exception) |
||
292 | # we do use replace and force the use of .__str__() in order to process LazyString objects |
||
293 | # |
||
294 | subject = self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_SUBJECT |
||
295 | subject = subject.replace(EST.WEBSITE_TITLE, self.config.WEBSITE_TITLE.__str__()) |
||
296 | subject = subject.replace(EST.WORKSPACE_LABEL, main_content.workspace.label.__str__()) |
||
297 | subject = subject.replace(EST.CONTENT_LABEL, main_content.label.__str__()) |
||
298 | subject = subject.replace(EST.CONTENT_STATUS_LABEL, main_content.get_status().label.__str__()) |
||
299 | reply_to_label = _('{username} & all members of {workspace}').format( # nopep8 |
||
300 | username=user.display_name, |
||
301 | workspace=main_content.workspace.label) |
||
302 | |||
303 | message = MIMEMultipart('alternative') |
||
304 | message['Subject'] = subject |
||
305 | message['From'] = self._get_sender(user) |
||
306 | message['To'] = to_addr |
||
307 | message['Reply-to'] = formataddr((reply_to_label, replyto_addr)) |
||
308 | # INFO - G.M - 2017-11-15 |
||
309 | # References can theorically have label, but in pratice, references |
||
310 | # contains only message_id from parents post in thread. |
||
311 | # To link this email to a content we create a virtual parent |
||
312 | # in reference who contain the content_id. |
||
313 | message['References'] = formataddr(('', reference_addr)) |
||
314 | content_in_context = content_api.get_content_in_context(content) |
||
315 | parent_in_context = None |
||
316 | if content.parent_id: |
||
317 | parent_in_context = content_api.get_content_in_context(content.parent) # nopep8 |
||
318 | body_text = self._build_email_body_for_content( |
||
319 | self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT, |
||
320 | role, |
||
321 | content_in_context, |
||
322 | parent_in_context, |
||
323 | workpace_in_context, |
||
324 | user, |
||
325 | translator, |
||
326 | ) |
||
327 | body_html = self._build_email_body_for_content( |
||
328 | self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML, |
||
329 | role, |
||
330 | content_in_context, |
||
331 | parent_in_context, |
||
332 | workpace_in_context, |
||
333 | user, |
||
334 | translator, |
||
335 | ) |
||
336 | |||
337 | part1 = MIMEText(body_text, 'plain', 'utf-8') |
||
338 | part2 = MIMEText(body_html, 'html', 'utf-8') |
||
339 | # Attach parts into message container. |
||
340 | # According to RFC 2046, the last part of a multipart message, in this case |
||
341 | # the HTML message, is best and preferred. |
||
342 | message.attach(part1) |
||
343 | message.attach(part2) |
||
344 | |||
345 | self.log_notification( |
||
346 | action='CREATED', |
||
347 | recipient=message['To'], |
||
348 | subject=message['Subject'], |
||
349 | config=self.config, |
||
350 | ) |
||
351 | |||
352 | send_email_through( |
||
353 | self.config, |
||
354 | async_email_sender.send_mail, |
||
355 | message |
||
356 | ) |
||
357 | |||
358 | View Code Duplication | def notify_created_account( |
|
|
|||
359 | self, |
||
360 | user: User, |
||
361 | password: str, |
||
362 | ) -> None: |
||
363 | """ |
||
364 | Send created account email to given user. |
||
365 | |||
366 | :param password: choosed password |
||
367 | :param user: user to notify |
||
368 | """ |
||
369 | # TODO BS 20160712: Cyclic import |
||
370 | logger.debug(self, 'user: {}'.format(user.user_id)) |
||
371 | logger.info(self, 'Sending asynchronous email to 1 user ({0})'.format( |
||
372 | user.email, |
||
373 | )) |
||
374 | |||
375 | async_email_sender = EmailSender( |
||
376 | self.config, |
||
377 | self._smtp_config, |
||
378 | self.config.EMAIL_NOTIFICATION_ACTIVATED |
||
379 | ) |
||
380 | |||
381 | subject = \ |
||
382 | self.config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_SUBJECT \ |
||
383 | .replace( |
||
384 | EST.WEBSITE_TITLE, |
||
385 | str(self.config.WEBSITE_TITLE) |
||
386 | ) |
||
387 | message = MIMEMultipart('alternative') |
||
388 | message['Subject'] = subject |
||
389 | message['From'] = self._get_sender() |
||
390 | message['To'] = formataddr((user.get_display_name(), user.email)) |
||
391 | |||
392 | text_template_file_path = self.config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_TEXT # nopep8 |
||
393 | html_template_file_path = self.config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_HTML # nopep8 |
||
394 | |||
395 | context = { |
||
396 | 'user': user, |
||
397 | 'password': password, |
||
398 | 'logo_url': get_email_logo_frontend_url(self.config), |
||
399 | 'login_url': get_login_frontend_url(self.config), |
||
400 | } |
||
401 | translator = Translator(self.config, default_lang=user.lang) |
||
402 | body_text = self._render_template( |
||
403 | mako_template_filepath=text_template_file_path, |
||
404 | context=context, |
||
405 | translator=translator |
||
406 | ) |
||
407 | |||
408 | body_html = self._render_template( |
||
409 | mako_template_filepath=html_template_file_path, |
||
410 | context=context, |
||
411 | translator=translator |
||
412 | ) |
||
413 | |||
414 | part1 = MIMEText(body_text, 'plain', 'utf-8') |
||
415 | part2 = MIMEText(body_html, 'html', 'utf-8') |
||
416 | |||
417 | # Attach parts into message container. |
||
418 | # According to RFC 2046, the last part of a multipart message, |
||
419 | # in this case the HTML message, is best and preferred. |
||
420 | message.attach(part1) |
||
421 | message.attach(part2) |
||
422 | |||
423 | send_email_through( |
||
424 | config=self.config, |
||
425 | sendmail_callable=async_email_sender.send_mail, |
||
426 | message=message |
||
427 | ) |
||
428 | |||
429 | View Code Duplication | def notify_reset_password( |
|
430 | self, |
||
431 | user: User, |
||
432 | reset_password_token: str, |
||
433 | ) -> None: |
||
434 | """ |
||
435 | Reset password link for user |
||
436 | :param user: user to notify |
||
437 | :param reset_password_token: token for resetting password |
||
438 | """ |
||
439 | logger.debug(self, 'user: {}'.format(user.user_id)) |
||
440 | logger.info(self, 'Sending asynchronous email to 1 user ({0})'.format( |
||
441 | user.email, |
||
442 | )) |
||
443 | translator = Translator(self.config, default_lang=user.lang) |
||
444 | async_email_sender = EmailSender( |
||
445 | self.config, |
||
446 | self._smtp_config, |
||
447 | self.config.EMAIL_NOTIFICATION_ACTIVATED |
||
448 | ) |
||
449 | subject = self.config.EMAIL_NOTIFICATION_RESET_PASSWORD_SUBJECT.replace( |
||
450 | EST.WEBSITE_TITLE, |
||
451 | str(self.config.WEBSITE_TITLE) |
||
452 | ) |
||
453 | message = MIMEMultipart('alternative') |
||
454 | message['Subject'] = subject |
||
455 | message['From'] = self._get_sender() |
||
456 | message['To'] = formataddr((user.get_display_name(), user.email)) |
||
457 | |||
458 | text_template_file_path = self.config.EMAIL_NOTIFICATION_RESET_PASSWORD_TEMPLATE_TEXT # nopep8 |
||
459 | html_template_file_path = self.config.EMAIL_NOTIFICATION_RESET_PASSWORD_TEMPLATE_HTML # nopep8 |
||
460 | # TODO - G.M - 2018-08-17 - Generate token |
||
461 | context = { |
||
462 | 'user': user, |
||
463 | 'logo_url': get_email_logo_frontend_url(self.config), |
||
464 | 'reset_password_url': get_reset_password_frontend_url( |
||
465 | self.config, |
||
466 | token=reset_password_token, |
||
467 | email=user.email, |
||
468 | ), |
||
469 | } |
||
470 | body_text = self._render_template( |
||
471 | mako_template_filepath=text_template_file_path, |
||
472 | context=context, |
||
473 | translator=translator, |
||
474 | ) |
||
475 | |||
476 | body_html = self._render_template( |
||
477 | mako_template_filepath=html_template_file_path, |
||
478 | context=context, |
||
479 | translator=translator, |
||
480 | ) |
||
481 | |||
482 | part1 = MIMEText(body_text, 'plain', 'utf-8') |
||
483 | part2 = MIMEText(body_html, 'html', 'utf-8') |
||
484 | |||
485 | # Attach parts into message container. |
||
486 | # According to RFC 2046, the last part of a multipart message, |
||
487 | # in this case the HTML message, is best and preferred. |
||
488 | message.attach(part1) |
||
489 | message.attach(part2) |
||
490 | |||
491 | send_email_through( |
||
492 | config=self.config, |
||
493 | sendmail_callable=async_email_sender.send_mail, |
||
494 | message=message |
||
495 | ) |
||
496 | |||
497 | def _render_template( |
||
498 | self, |
||
499 | mako_template_filepath: str, |
||
500 | context: dict, |
||
501 | translator: Translator |
||
502 | ) -> str: |
||
503 | """ |
||
504 | Render mako template with all needed current variables. |
||
505 | |||
506 | :param mako_template_filepath: file path of mako template |
||
507 | :param context: dict with template context |
||
508 | :return: template rendered string |
||
509 | """ |
||
510 | |||
511 | template = Template(filename=mako_template_filepath) |
||
512 | return template.render( |
||
513 | _=translator.get_translation, |
||
514 | config=self.config, |
||
515 | **context |
||
516 | ) |
||
517 | |||
518 | def _build_context_for_content_update( |
||
519 | self, |
||
520 | role: UserRoleInWorkspace, |
||
521 | content_in_context: ContentInContext, |
||
522 | parent_in_context: typing.Optional[ContentInContext], |
||
523 | workspace_in_context: WorkspaceInContext, |
||
524 | actor: User, |
||
525 | translator: Translator |
||
526 | ): |
||
527 | |||
528 | _ = translator.get_translation |
||
529 | content = content_in_context.content |
||
530 | action = content.get_last_action().id |
||
531 | |||
532 | # default values |
||
533 | user = role.user |
||
534 | workspace = role.workspace |
||
535 | workspace_url = workspace_in_context.frontend_url |
||
536 | main_title = content.label |
||
537 | status_label = content.get_status().label |
||
538 | # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for status_icon_url # nopep8 |
||
539 | status_icon_url = '' |
||
540 | role_label = role.role_as_label() |
||
541 | content_intro = '<span id="content-intro-username">{}</span> did something.'.format(actor.display_name) # nopep8 |
||
542 | content_text = content.description |
||
543 | call_to_action_text = 'See more' |
||
544 | call_to_action_url = content_in_context.frontend_url |
||
545 | logo_url = get_email_logo_frontend_url(self.config) |
||
546 | |||
547 | if ActionDescription.CREATION == action: |
||
548 | call_to_action_text = _('View online') |
||
549 | content_intro = _('<span id="content-intro-username">{}</span> create a content:').format(actor.display_name) # nopep8 |
||
550 | |||
551 | if content_type_list.Thread.slug == content.type: |
||
552 | if content.get_last_comment_from(actor): |
||
553 | content_text = content.get_last_comment_from(actor).description # nopep8 |
||
554 | |||
555 | call_to_action_text = _('Answer') |
||
556 | content_intro = _('<span id="content-intro-username">{}</span> started a thread entitled:').format(actor.display_name) |
||
557 | content_text = '<p id="content-body-intro">{}</p>'.format(content.label) + content_text # nopep8 |
||
558 | |||
559 | elif content_type_list.File.slug == content.type: |
||
560 | content_intro = _('<span id="content-intro-username">{}</span> added a file entitled:').format(actor.display_name) |
||
561 | if content.description: |
||
562 | content_text = content.description |
||
563 | else: |
||
564 | content_text = '<span id="content-body-only-title">{}</span>'.format(content.label) |
||
565 | |||
566 | elif content_type_list.Page.slug == content.type: |
||
567 | content_intro = _('<span id="content-intro-username">{}</span> added a page entitled:').format(actor.display_name) |
||
568 | content_text = '<span id="content-body-only-title">{}</span>'.format(content.label) |
||
569 | |||
570 | elif ActionDescription.REVISION == action: |
||
571 | content_text = content.description |
||
572 | call_to_action_text = _('View online') |
||
573 | |||
574 | if content_type_list.File.slug == content.type: |
||
575 | content_intro = _('<span id="content-intro-username">{}</span> uploaded a new revision.').format(actor.display_name) |
||
576 | content_text = content.description |
||
577 | |||
578 | elif ActionDescription.EDITION == action: |
||
579 | call_to_action_text = _('View online') |
||
580 | |||
581 | if content_type_list.File.slug == content.type: |
||
582 | content_intro = _('<span id="content-intro-username">{}</span> updated the file description.').format(actor.display_name) |
||
583 | content_text = '<p id="content-body-intro">{}</p>'.format(content.get_label()) + content.description # nopep8 |
||
584 | |||
585 | elif content_type_list.Thread.slug == content.type: |
||
586 | content_intro = _('<span id="content-intro-username">{}</span> updated the thread description.').format(actor.display_name) |
||
587 | previous_revision = content.get_previous_revision() |
||
588 | title_diff = '' |
||
589 | if previous_revision.label != content.label: |
||
590 | title_diff = htmldiff(previous_revision.label, content.label) |
||
591 | content_text = str('<p id="content-body-intro">{}</p> {text} {title_diff} {content_diff}').format( |
||
592 | text=_('Here is an overview of the changes:'), |
||
593 | title_diff=title_diff, |
||
594 | content_diff=htmldiff(previous_revision.description, content.description) |
||
595 | ) |
||
596 | elif content_type_list.Page.slug == content.type: |
||
597 | content_intro = _('<span id="content-intro-username">{}</span> updated this page.').format(actor.display_name) |
||
598 | previous_revision = content.get_previous_revision() |
||
599 | title_diff = '' |
||
600 | if previous_revision.label != content.label: |
||
601 | title_diff = htmldiff(previous_revision.label, content.label) # nopep8 |
||
602 | content_text = str('<p id="content-body-intro">{}</p> {text}</p> {title_diff} {content_diff}').format( # nopep8 |
||
603 | actor.display_name, |
||
604 | text=_('Here is an overview of the changes:'), |
||
605 | title_diff=title_diff, |
||
606 | content_diff=htmldiff(previous_revision.description, content.description) |
||
607 | ) |
||
608 | |||
609 | elif ActionDescription.STATUS_UPDATE == action: |
||
610 | intro_user_msg = _( |
||
611 | '<span id="content-intro-username">{}</span> ' |
||
612 | 'updated the following status:' |
||
613 | ) |
||
614 | intro_body_msg = '<p id="content-body-intro">{}: {}</p>' |
||
615 | |||
616 | call_to_action_text = _('View online') |
||
617 | content_intro = intro_user_msg.format(actor.display_name) |
||
618 | content_text = intro_body_msg.format( |
||
619 | content.get_label(), |
||
620 | content.get_status().label, |
||
621 | ) |
||
622 | |||
623 | elif ActionDescription.COMMENT == action: |
||
624 | call_to_action_text = _('Answer') |
||
625 | main_title = parent_in_context.label |
||
626 | content_intro = _('<span id="content-intro-username">{}</span> added a comment:').format(actor.display_name) # nopep8 |
||
627 | call_to_action_url = parent_in_context.frontend_url |
||
628 | |||
629 | if not content_intro and not content_text: |
||
630 | # Skip notification, but it's not normal |
||
631 | logger.error( |
||
632 | self, |
||
633 | 'A notification is being sent but no content. ' |
||
634 | 'Here are some debug informations: [content_id: {cid}]' |
||
635 | '[action: {act}][author: {actor}]'.format( |
||
636 | cid=content.content_id, |
||
637 | act=action, |
||
638 | actor=actor |
||
639 | ) |
||
640 | ) |
||
641 | raise EmptyNotificationError('Unexpected empty notification') |
||
642 | |||
643 | # FIXME: remove/readapt assert to debug easily broken case |
||
644 | assert user |
||
645 | assert workspace |
||
646 | assert main_title |
||
647 | assert status_label |
||
648 | # assert status_icon_url |
||
649 | assert role_label |
||
650 | assert content_intro |
||
651 | assert content_text or content_text == content.description |
||
652 | assert call_to_action_text |
||
653 | assert call_to_action_url |
||
654 | assert logo_url |
||
655 | |||
656 | return { |
||
657 | 'user': role.user, |
||
658 | 'workspace': role.workspace, |
||
659 | 'workspace_url': workspace_url, |
||
660 | 'main_title': main_title, |
||
661 | 'status_label': status_label, |
||
662 | 'status_icon_url': status_icon_url, |
||
663 | 'role_label': role_label, |
||
664 | 'content_intro': content_intro, |
||
665 | 'content_text': content_text, |
||
666 | 'call_to_action_text': call_to_action_text, |
||
667 | 'call_to_action_url': call_to_action_url, |
||
668 | 'logo_url': logo_url, |
||
669 | } |
||
670 | |||
671 | def _build_email_body_for_content( |
||
672 | self, |
||
673 | mako_template_filepath: str, |
||
674 | role: UserRoleInWorkspace, |
||
675 | content_in_context: ContentInContext, |
||
676 | parent_in_context: typing.Optional[ContentInContext], |
||
677 | workspace_in_context: WorkspaceInContext, |
||
678 | actor: User, |
||
679 | translator: Translator |
||
680 | ) -> str: |
||
681 | """ |
||
682 | Build an email body and return it as a string |
||
683 | :param mako_template_filepath: the absolute path to the mako template |
||
684 | to be used for email body building |
||
685 | :param role: the role related to user to whom the email must be sent. |
||
686 | The role is required (and not the user only) in order to show in the |
||
687 | mail why the user receive the notification |
||
688 | :param content_in_context: the content item related to the notification |
||
689 | :param parent_in_context: parent of the content item related to the |
||
690 | notification |
||
691 | :param actor: the user at the origin of the action / notification |
||
692 | (for example the one who wrote a comment |
||
693 | :return: the built email body as string. In case of multipart email, |
||
694 | this method must be called one time for text and one time for html |
||
695 | """ |
||
696 | logger.debug(self, 'Building email content from MAKO template {}'.format(mako_template_filepath)) # nopep8 |
||
697 | context = self._build_context_for_content_update( |
||
698 | role=role, |
||
699 | content_in_context=content_in_context, |
||
700 | parent_in_context=parent_in_context, |
||
701 | workspace_in_context=workspace_in_context, |
||
702 | actor=actor, |
||
703 | translator=translator |
||
704 | ) |
||
705 | body_content = self._render_template( |
||
706 | mako_template_filepath=mako_template_filepath, |
||
707 | context=context, |
||
708 | translator=translator, |
||
709 | ) |
||
710 | return body_content |
||
711 | |||
712 | |||
713 | def get_email_manager(config: CFG, session: Session): |
||
714 | """ |
||
715 | :return: EmailManager instance |
||
716 | """ |
||
717 | # TODO: Find a way to import properly without cyclic import |
||
718 | |||
719 | smtp_config = SmtpConfiguration( |
||
720 | config.EMAIL_NOTIFICATION_SMTP_SERVER, |
||
721 | config.EMAIL_NOTIFICATION_SMTP_PORT, |
||
722 | config.EMAIL_NOTIFICATION_SMTP_USER, |
||
723 | config.EMAIL_NOTIFICATION_SMTP_PASSWORD |
||
724 | ) |
||
725 | |||
726 | return EmailManager(config=config, smtp_config=smtp_config, session=session) |
||
727 |