Passed
Pull Request — develop (#29)
by inkhey
03:53
created

notifier   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 669
Duplicated Lines 20.18 %

Importance

Changes 0
Metric Value
wmc 42
eloc 421
dl 135
loc 669
rs 9.0399
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A EmailManager.__init__() 0 9 1
A EmailNotifier.__init__() 0 21 1
C EmailNotifier.notify_content_update() 0 69 9
A EmailManager._get_sender() 0 24 2
A EmailManager.log_notification() 0 21 3
F EmailManager._build_email_body_for_content() 0 144 18
B EmailManager.notify_content_update() 0 141 4
B EmailManager.notify_created_account() 71 71 1
B EmailManager.notify_reset_password() 64 64 1
A EmailManager._render_template() 0 19 1

1 Function

Rating   Name   Duplication   Size   Complexity  
A get_email_manager() 0 14 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

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:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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