Changeset - 5fe492c6d7d7
[Not reviewed]
default
0 4 0
Thomas De Schampheleire - 6 years ago 2020-03-22 00:00:44
thomas.de_schampheleire@nokia.com
Grafted from: 78778bb6b969
comments: add changeset author in mail notification subject

In analogy to the handling of comments on pull requests, as changed in
commit d4061c6cc0e2.
4 files changed with 7 insertions and 5 deletions:
0 comments (0 inline, 0 general)
kallithea/model/comment.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
"""
 
kallithea.model.comment
 
~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
comments model for Kallithea
 

	
 
This file was forked by the Kallithea project in July 2014.
 
Original author and date, and relevant copyright and licensing information is below:
 
:created_on: Nov 11, 2011
 
:author: marcink
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 
"""
 

	
 
import logging
 
from collections import defaultdict
 

	
 
from tg.i18n import ugettext as _
 

	
 
from kallithea.lib import helpers as h
 
from kallithea.lib.utils2 import extract_mentioned_users
 
from kallithea.model.db import ChangesetComment, PullRequest, Repository, User
 
from kallithea.model.meta import Session
 
from kallithea.model.notification import NotificationModel
 

	
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
def _list_changeset_commenters(revision):
 
    return (Session().query(User)
 
        .join(ChangesetComment.author)
 
        .filter(ChangesetComment.revision == revision)
 
        .all())
 

	
 
def _list_pull_request_commenters(pull_request):
 
    return (Session().query(User)
 
        .join(ChangesetComment.author)
 
        .filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
 
        .all())
 

	
 

	
 
class ChangesetCommentsModel(object):
 

	
 
    def _get_notification_data(self, repo, comment, author, comment_text,
 
                               line_no=None, revision=None, pull_request=None,
 
                               status_change=None, closing_pr=False):
 
        """
 
        :returns: tuple (subj,body,recipients,notification_type,email_kwargs)
 
        """
 
        # make notification
 
        body = comment_text  # text of the comment
 
        line = ''
 
        if line_no:
 
            line = _('on line %s') % line_no
 

	
 
        # changeset
 
        if revision:
 
            notification_type = NotificationModel.TYPE_CHANGESET_COMMENT
 
            cs = repo.scm_instance.get_changeset(revision)
 
            desc = cs.short_id
 

	
 
            threading = ['%s-rev-%s@%s' % (repo.repo_name, revision, h.canonical_hostname())]
 
            if line_no: # TODO: url to file _and_ line number
 
                threading.append('%s-rev-%s-line-%s@%s' % (repo.repo_name, revision, line_no,
 
                                                           h.canonical_hostname()))
 
            comment_url = h.canonical_url('changeset_home',
 
                repo_name=repo.repo_name,
 
                revision=revision,
 
                anchor='comment-%s' % comment.comment_id)
 
            subj = h.link_to(
 
                'Re changeset: %(desc)s %(line)s' %
 
                          {'desc': desc, 'line': line},
 
                 comment_url)
 
            # get the current participants of this changeset
 
            recipients = _list_changeset_commenters(revision)
 
            # add changeset author if it's known locally
 
            cs_author = User.get_from_cs_author(cs.author)
 
            if not cs_author:
 
                # use repo owner if we cannot extract the author correctly
 
                # FIXME: just use committer name even if not a user
 
                cs_author = repo.owner
 
            recipients.append(cs_author)
 

	
 
            email_kwargs = {
 
                'status_change': status_change,
 
                'cs_comment_user': author.full_name_and_username,
 
                'cs_target_repo': h.canonical_url('summary_home', repo_name=repo.repo_name),
 
                'cs_comment_url': comment_url,
 
                'cs_url': h.canonical_url('changeset_home', repo_name=repo.repo_name, revision=revision),
 
                'raw_id': revision,
 
                'message': cs.message,
 
                'message_short': h.shorter(cs.message, 50, firstline=True),
 
                'cs_author': cs_author,
 
                'cs_author_username': cs_author.username,
 
                'repo_name': repo.repo_name,
 
                'short_id': h.short_id(revision),
 
                'branch': cs.branch,
 
                'comment_username': author.username,
 
                'threading': threading,
 
            }
 
        # pull request
 
        elif pull_request:
 
            notification_type = NotificationModel.TYPE_PULL_REQUEST_COMMENT
 
            desc = comment.pull_request.title
 
            _org_ref_type, org_ref_name, _org_rev = comment.pull_request.org_ref.split(':')
 
            _other_ref_type, other_ref_name, _other_rev = comment.pull_request.other_ref.split(':')
 
            threading = ['%s-pr-%s@%s' % (pull_request.other_repo.repo_name,
 
                                          pull_request.pull_request_id,
 
                                          h.canonical_hostname())]
 
            if line_no: # TODO: url to file _and_ line number
 
                threading.append('%s-pr-%s-line-%s@%s' % (pull_request.other_repo.repo_name,
 
                                                          pull_request.pull_request_id, line_no,
 
                                                          h.canonical_hostname()))
 
            comment_url = pull_request.url(canonical=True,
 
                anchor='comment-%s' % comment.comment_id)
 
            subj = h.link_to(
 
                'Re pull request %(pr_nice_id)s: %(desc)s %(line)s' %
 
                          {'desc': desc,
 
                           'pr_nice_id': comment.pull_request.nice_id(),
 
                           'line': line},
 
                comment_url)
 
            # get the current participants of this pull request
 
            recipients = _list_pull_request_commenters(pull_request)
 
            recipients.append(pull_request.owner)
 
            recipients += pull_request.get_reviewer_users()
 

	
 
            # set some variables for email notification
 
            email_kwargs = {
 
                'pr_title': pull_request.title,
 
                'pr_title_short': h.shorter(pull_request.title, 50),
 
                'pr_nice_id': pull_request.nice_id(),
 
                'status_change': status_change,
 
                'closing_pr': closing_pr,
 
                'pr_comment_url': comment_url,
 
                'pr_url': pull_request.url(canonical=True),
 
                'pr_comment_user': author.full_name_and_username,
 
                'pr_target_repo': h.canonical_url('summary_home',
 
                                   repo_name=pull_request.other_repo.repo_name),
 
                'pr_target_branch': other_ref_name,
 
                'pr_source_repo': h.canonical_url('summary_home',
 
                                   repo_name=pull_request.org_repo.repo_name),
 
                'pr_source_branch': org_ref_name,
 
                'pr_owner': pull_request.owner,
 
                'pr_owner_username': pull_request.owner.username,
 
                'repo_name': pull_request.other_repo.repo_name,
 
                'comment_username': author.username,
 
                'threading': threading,
 
            }
 

	
 
        return subj, body, recipients, notification_type, email_kwargs
 

	
 
    def create(self, text, repo, author, revision=None, pull_request=None,
 
               f_path=None, line_no=None, status_change=None, closing_pr=False,
 
               send_email=True):
 
        """
 
        Creates a new comment for either a changeset or a pull request.
 
        status_change and closing_pr is only for the optional email.
 

	
 
        Returns the created comment.
 
        """
 
        if not status_change and not text:
 
            log.warning('Missing text for comment, skipping...')
 
            return None
 

	
 
        repo = Repository.guess_instance(repo)
 
        author = User.guess_instance(author)
 
        comment = ChangesetComment()
 
        comment.repo = repo
 
        comment.author = author
 
        comment.text = text
 
        comment.f_path = f_path
 
        comment.line_no = line_no
 

	
 
        if revision is not None:
 
            comment.revision = revision
 
        elif pull_request is not None:
 
            pull_request = PullRequest.guess_instance(pull_request)
 
            comment.pull_request = pull_request
 
        else:
 
            raise Exception('Please specify revision or pull_request_id')
 

	
 
        Session().add(comment)
 
        Session().flush()
 

	
 
        if send_email:
 
            (subj, body, recipients, notification_type,
 
             email_kwargs) = self._get_notification_data(
 
                                repo, comment, author,
 
                                comment_text=text,
 
                                line_no=line_no,
 
                                revision=revision,
 
                                pull_request=pull_request,
 
                                status_change=status_change,
 
                                closing_pr=closing_pr)
 
            email_kwargs['is_mention'] = False
 
            # create notification objects, and emails
 
            NotificationModel().create(
 
                created_by=author, subject=subj, body=body,
 
                recipients=recipients, type_=notification_type,
 
                email_kwargs=email_kwargs,
 
            )
 

	
 
            mention_recipients = extract_mentioned_users(body).difference(recipients)
 
            if mention_recipients:
 
                email_kwargs['is_mention'] = True
 
                subj = _('[Mention]') + ' ' + subj
 
                # FIXME: this subject is wrong and unused!
 
                NotificationModel().create(
 
                    created_by=author, subject=subj, body=body,
 
                    recipients=mention_recipients,
 
                    type_=notification_type,
 
                    email_kwargs=email_kwargs
 
                )
 

	
 
        return comment
 

	
 
    def delete(self, comment):
 
        comment = ChangesetComment.guess_instance(comment)
 
        Session().delete(comment)
 

	
 
        return comment
 

	
 
    def get_comments(self, repo_id, revision=None, pull_request=None):
 
        """
 
        Gets general comments for either revision or pull_request.
 

	
 
        Returns a list, ordered by creation date.
 
        """
 
        return self._get_comments(repo_id, revision=revision, pull_request=pull_request,
 
                                  inline=False)
 

	
 
    def get_inline_comments(self, repo_id, revision=None, pull_request=None,
 
                f_path=None, line_no=None):
 
        """
 
        Gets inline comments for either revision or pull_request.
 

	
 
        Returns a list of tuples with file path and list of comments per line number.
 
        """
 
        comments = self._get_comments(repo_id, revision=revision, pull_request=pull_request,
 
                                      inline=True, f_path=f_path, line_no=line_no)
 

	
 
        paths = defaultdict(lambda: defaultdict(list))
 
        for co in comments:
 
            paths[co.f_path][co.line_no].append(co)
 
        return sorted(paths.items())
 

	
 
    def _get_comments(self, repo_id, revision=None, pull_request=None,
 
                inline=False, f_path=None, line_no=None):
 
        """
 
        Gets comments for either revision or pull_request_id, either inline or general.
 
        If a file path and optionally line number are given, return only the matching inline comments.
 
        """
 
        if f_path is None and line_no is not None:
 
            raise Exception("line_no only makes sense if f_path is given.")
 

	
 
        if inline is None and f_path is not None:
 
            raise Exception("f_path only makes sense for inline comments.")
 

	
 
        q = Session().query(ChangesetComment)
 

	
 
        if inline:
 
            if f_path is not None:
 
                # inline comments for a given file...
 
                q = q.filter(ChangesetComment.f_path == f_path)
 
                if line_no is None:
 
                    # ... on any line
 
                    q = q.filter(ChangesetComment.line_no != None)
 
                else:
 
                    # ... on specific line
 
                    q = q.filter(ChangesetComment.line_no == line_no)
 
            else:
 
                # all inline comments
 
                q = q.filter(ChangesetComment.line_no != None) \
 
                    .filter(ChangesetComment.f_path != None)
 
        else:
 
            # all general comments
 
            q = q.filter(ChangesetComment.line_no == None) \
 
                .filter(ChangesetComment.f_path == None)
 

	
 
        if revision is not None:
 
            q = q.filter(ChangesetComment.revision == revision) \
 
                .filter(ChangesetComment.repo_id == repo_id)
 
        elif pull_request is not None:
 
            pull_request = PullRequest.guess_instance(pull_request)
 
            q = q.filter(ChangesetComment.pull_request == pull_request)
 
        else:
kallithea/model/notification.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
"""
 
kallithea.model.notification
 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
Model for notifications
 

	
 

	
 
This file was forked by the Kallithea project in July 2014.
 
Original author and date, and relevant copyright and licensing information is below:
 
:created_on: Nov 20, 2011
 
:author: marcink
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 
"""
 

	
 
import datetime
 
import logging
 

	
 
from tg import app_globals
 
from tg import tmpl_context as c
 
from tg.i18n import ugettext as _
 

	
 
from kallithea.lib import helpers as h
 
from kallithea.model.db import User
 

	
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class NotificationModel(object):
 

	
 
    TYPE_CHANGESET_COMMENT = 'cs_comment'
 
    TYPE_MESSAGE = 'message'
 
    TYPE_MENTION = 'mention' # not used
 
    TYPE_REGISTRATION = 'registration'
 
    TYPE_PULL_REQUEST = 'pull_request'
 
    TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
 

	
 
    def create(self, created_by, subject, body, recipients=None,
 
               type_=TYPE_MESSAGE, with_email=True,
 
               email_kwargs=None, repo_name=None):
 
        """
 

	
 
        Creates notification of given type
 

	
 
        :param created_by: int, str or User instance. User who created this
 
            notification
 
        :param subject:
 
        :param body:
 
        :param recipients: list of int, str or User objects, when None
 
            is given send to all admins
 
        :param type_: type of notification
 
        :param with_email: send email with this notification
 
        :param email_kwargs: additional dict to pass as args to email template
 
        """
 
        from kallithea.lib.celerylib import tasks
 
        email_kwargs = email_kwargs or {}
 
        if recipients and not getattr(recipients, '__iter__', False):
 
            raise Exception('recipients must be a list or iterable')
 

	
 
        created_by_obj = User.guess_instance(created_by)
 

	
 
        recipients_objs = set()
 
        if recipients:
 
            for u in recipients:
 
                obj = User.guess_instance(u)
 
                if obj is not None:
 
                    recipients_objs.add(obj)
 
                else:
 
                    # TODO: inform user that requested operation couldn't be completed
 
                    log.error('cannot email unknown user %r', u)
 
            log.debug('sending notifications %s to %s',
 
                type_, recipients_objs
 
            )
 
        elif recipients is None:
 
            # empty recipients means to all admins
 
            recipients_objs = User.query().filter(User.admin == True).all()
 
            log.debug('sending notifications %s to admins: %s',
 
                type_, recipients_objs
 
            )
 
        #else: silently skip notification mails?
 

	
 
        if not with_email:
 
            return
 

	
 
        headers = {}
 
        headers['X-Kallithea-Notification-Type'] = type_
 
        if 'threading' in email_kwargs:
 
            headers['References'] = ' '.join('<%s>' % x for x in email_kwargs['threading'])
 

	
 
        # this is passed into template
 
        created_on = h.fmt_date(datetime.datetime.now())
 
        html_kwargs = {
 
                  'subject': subject,
 
                  'body': h.render_w_mentions(body, repo_name),
 
                  'when': created_on,
 
                  'user': created_by_obj.username,
 
                  }
 

	
 
        txt_kwargs = {
 
                  'subject': subject,
 
                  'body': body,
 
                  'when': created_on,
 
                  'user': created_by_obj.username,
 
                  }
 

	
 
        html_kwargs.update(email_kwargs)
 
        txt_kwargs.update(email_kwargs)
 
        email_subject = EmailNotificationModel() \
 
                            .get_email_description(type_, **txt_kwargs)
 
        email_txt_body = EmailNotificationModel() \
 
                            .get_email_tmpl(type_, 'txt', **txt_kwargs)
 
        email_html_body = EmailNotificationModel() \
 
                            .get_email_tmpl(type_, 'html', **html_kwargs)
 

	
 
        # don't send email to the person who caused the notification, except for
 
        # notifications about new pull requests where the author is explicitly
 
        # added.
 
        rec_mails = set(obj.email for obj in recipients_objs)
 
        if type_ == NotificationModel.TYPE_PULL_REQUEST:
 
            rec_mails.add(created_by_obj.email)
 
        else:
 
            rec_mails.discard(created_by_obj.email)
 

	
 
        # send email with notification to participants
 
        for rec_mail in sorted(rec_mails):
 
            tasks.send_email([rec_mail], email_subject, email_txt_body,
 
                     email_html_body, headers,
 
                     from_name=created_by_obj.full_name_or_username)
 

	
 

	
 
class EmailNotificationModel(object):
 

	
 
    TYPE_CHANGESET_COMMENT = NotificationModel.TYPE_CHANGESET_COMMENT
 
    TYPE_MESSAGE = NotificationModel.TYPE_MESSAGE # only used for testing
 
    # NotificationModel.TYPE_MENTION is not used
 
    TYPE_PASSWORD_RESET = 'password_link'
 
    TYPE_REGISTRATION = NotificationModel.TYPE_REGISTRATION
 
    TYPE_PULL_REQUEST = NotificationModel.TYPE_PULL_REQUEST
 
    TYPE_PULL_REQUEST_COMMENT = NotificationModel.TYPE_PULL_REQUEST_COMMENT
 
    TYPE_DEFAULT = 'default'
 

	
 
    def __init__(self):
 
        super(EmailNotificationModel, self).__init__()
 
        self._tmpl_lookup = app_globals.mako_lookup
 
        self.email_types = {
 
            self.TYPE_CHANGESET_COMMENT: 'changeset_comment',
 
            self.TYPE_PASSWORD_RESET: 'password_reset',
 
            self.TYPE_REGISTRATION: 'registration',
 
            self.TYPE_DEFAULT: 'default',
 
            self.TYPE_PULL_REQUEST: 'pull_request',
 
            self.TYPE_PULL_REQUEST_COMMENT: 'pull_request_comment',
 
        }
 
        self._subj_map = {
 
            self.TYPE_CHANGESET_COMMENT: _('[Comment] %(repo_name)s changeset %(short_id)s "%(message_short)s" on %(branch)s'),
 
            self.TYPE_CHANGESET_COMMENT: _('[Comment] %(repo_name)s changeset %(short_id)s "%(message_short)s" on %(branch)s by %(cs_author_username)s'),
 
            self.TYPE_MESSAGE: 'Test Message',
 
            # self.TYPE_PASSWORD_RESET
 
            self.TYPE_REGISTRATION: _('New user %(new_username)s registered'),
 
            # self.TYPE_DEFAULT
 
            self.TYPE_PULL_REQUEST: _('[Review] %(repo_name)s PR %(pr_nice_id)s "%(pr_title_short)s" from %(pr_source_branch)s by %(pr_owner_username)s'),
 
            self.TYPE_PULL_REQUEST_COMMENT: _('[Comment] %(repo_name)s PR %(pr_nice_id)s "%(pr_title_short)s" from %(pr_source_branch)s by %(pr_owner_username)s'),
 
        }
 

	
 
    def get_email_description(self, type_, **kwargs):
 
        """
 
        return subject for email based on given type
 
        """
 
        tmpl = self._subj_map[type_]
 
        try:
 
            subj = tmpl % kwargs
 
        except KeyError as e:
 
            log.error('error generating email subject for %r from %s: %s', type_, ', '.join(self._subj_map), e)
 
            raise
 
        # gmail doesn't do proper threading but will ignore leading square
 
        # bracket content ... so that is where we put status info
 
        bracket_tags = []
 
        status_change = kwargs.get('status_change')
 
        if status_change:
 
            bracket_tags.append(str(status_change))  # apply str to evaluate LazyString before .join
 
        if kwargs.get('closing_pr'):
 
            bracket_tags.append(_('Closing'))
 
        if bracket_tags:
 
            if subj.startswith('['):
 
                subj = '[' + ', '.join(bracket_tags) + ': ' + subj[1:]
 
            else:
 
                subj = '[' + ', '.join(bracket_tags) + '] ' + subj
 
        return subj
 

	
 
    def get_email_tmpl(self, type_, content_type, **kwargs):
 
        """
 
        return generated template for email based on given type
 
        """
 

	
 
        base = 'email_templates/' + self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT]) + '.' + content_type
 
        email_template = self._tmpl_lookup.get_template(base)
 
        # translator and helpers inject
 
        _kwargs = {'_': _,
 
                   'h': h,
 
                   'c': c}
 
        _kwargs.update(kwargs)
 
        if content_type == 'html':
 
            _kwargs.update({
 
                "color_text": "#202020",
 
                "color_emph": "#395fa0",
 
                "color_link": "#395fa0",
 
                "color_border": "#ddd",
 
                "color_background_grey": "#f9f9f9",
 
                "color_button": "#395fa0",
 
                "monospace_style": "font-family:Lucida Console,Consolas,Monaco,Inconsolata,Liberation Mono,monospace",
 
                "sans_style": "font-family:Helvetica,Arial,sans-serif",
 
                })
 
            _kwargs.update({
 
                "default_style": "%(sans_style)s;font-weight:200;font-size:14px;line-height:17px;color:%(color_text)s" % _kwargs,
 
                "comment_style": "%(monospace_style)s;white-space:pre-wrap" % _kwargs,
 
                "data_style": "border:%(color_border)s 1px solid;background:%(color_background_grey)s" % _kwargs,
 
                "emph_style": "font-weight:600;color:%(color_emph)s" % _kwargs,
 
                "link_style": "color:%(color_link)s;text-decoration:none" % _kwargs,
 
                "link_text_style": "color:%(color_text)s;text-decoration:none;border:%(color_border)s 1px solid;background:%(color_background_grey)s" % _kwargs,
 
                })
 

	
 
        log.debug('rendering tmpl %s with kwargs %s', base, _kwargs)
 
        return email_template.render_unicode(**_kwargs)
kallithea/tests/models/test_dump_html_mails.ref.html
Show inline comments
 
<!doctype html>
 
<html lang="en">
 
<head><title>Notifications</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>
 
<body>
 
<hr/>
 
<h1>cs_comment, is_mention=False, status_change=None</h1>
 
<pre>
 
From: u1 u1 <name@example.com>
 
To: u2@example.com
 
Subject: [Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch
 
Subject: [Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch by u2
 
</pre>
 
<hr/>
 
<pre>http://comment.org
 

	
 
Comment on Changeset "This changeset did something clever which is hard to explain"
 

	
 

	
 
Opinionated User (jsmith):
 

	
 
This is the new 'comment'.
 

	
 
 - and here it ends indented.
 

	
 

	
 
Changeset on http://example.com/repo_target branch brunch:
 
"This changeset did something clever which is hard to explain" by u2 u3 (u2).
 

	
 

	
 
View Comment: http://comment.org
 
</pre>
 
<hr/>
 
<!--!doctype html-->
 
<!--html lang="en"-->
 
<!--head-->
 
    <!--title--><!--/title-->
 
    <!--meta name="viewport" content="width=device-width"-->
 
    <!--meta http-equiv="Content-Type" content="text/html; charset=UTF-8"-->
 
<!--/head-->
 
<!--body-->
 
<table align="center" cellpadding="0" cellspacing="0" border="0" style="min-width:348px;max-width:800px;font-family:Helvetica,Arial,sans-serif;font-weight:200;font-size:14px;line-height:17px;color:#202020">
 
    <tr>
 
        <td width="30px" style="width:30px"></td>
 
        <td>
 
            <table width="100%" cellpadding="0" cellspacing="0" border="0"
 
                   style="table-layout:fixed;font-family:Helvetica,Arial,sans-serif;border:1px solid #ddd">
 
                <tr><td width="30px" style="width:30px"></td><td></td><td width="30px" style="width:30px"></td></tr>
 
                <tr>
 
                    <td colspan="3">
 
<table bgcolor="#f9f9f9" width="100%" cellpadding="0" cellspacing="0"
 
       style="border-bottom:1px solid #ddd">
 
    <tr>
 
        <td height="20px" style="height:20px" colspan="3"></td>
 
    </tr>
 
    <tr>
 
        <td width="30px" style="width:30px"></td>
 
        <td style="font-family:Helvetica,Arial,sans-serif;font-size:19px;line-height:24px">
 
            <a style="text-decoration:none;font-weight:600;color:#395fa0" href="http://comment.org"
 
               target="_blank">Comment on Changeset &#34;This changeset did something clever which is hard to explain&#34;</a>
 
        </td>
 
        <td width="30px" style="width:30px"></td>
 
    </tr>
 
    <tr>
 
        <td height="20px" style="height:20px" colspan="3"></td>
 
    </tr>
 
</table>
 
                    </td>
 
                </tr>
 
                <tr>
 
                    <td height="30px" style="height:30px" colspan="3"></td>
 
                </tr>
 
                <tr>
 
                    <td></td>
 
                    <td>
 
<table cellpadding="0" cellspacing="0" border="0" width="100%">
 
    <tr>
 
        <td>
 
<table cellpadding="0" cellspacing="0" width="100%" border="0" bgcolor="#f9f9f9" style="border:1px solid #ddd;border-radius:4px">
 
    <tr>
 
        <td height="10px" style="height:10px" colspan="3"></td>
 
    </tr>
 
    <tr>
 
        <td width="20px" style="width:20px"></td>
 
        <td>
 
            <div style="font-weight:600;color:#395fa0">Opinionated User (jsmith)</div>
 
        </td>
 
        <td width="20px" style="width:20px"></td>
 
    </tr>
 
    <tr>
 
        <td height="10px" style="height:10px" colspan="3" style="border-bottom:1px solid #ddd"></td>
 
    </tr>
 
    <tr>
 
        <td height="10px" style="height:10px" colspan="3"></td>
 
    </tr>
 
    <tr>
 
        <td width="20px" style="width:20px"></td>
 
        <td>
 
            <div style="font-family:Lucida Console,Consolas,Monaco,Inconsolata,Liberation Mono,monospace;white-space:pre-wrap"><div class="formatted-fixed">This is the new &#39;comment&#39;.<br/><br/> - and here it ends indented.</div></div>
 
        </td>
 
        <td width="20px" style="width:20px"></td>
 
    </tr>
 
    <tr>
 
        <td height="10px" style="height:10px" colspan="3"></td>
 
    </tr>
 
</table>
 
        </td>
 
    </tr>
 
    <tr>
 
        <td height="30px" style="height:30px"></td>
 
    </tr>
 
    <tr>
 
        <td>
 
            <div>
 
                Changeset on
 
                <a style="color:#202020;text-decoration:none;border:#ddd 1px solid;background:#f9f9f9"
 
                   href="http://example.com/repo_target">http://example.com/repo_target</a>
 
                branch
 
                <span style="border:#ddd 1px solid;background:#f9f9f9">brunch</span>:
 
            </div>
 
            <div>
 
                "<a style="color:#395fa0;text-decoration:none"
 
                   href="http://changeset.com">This changeset did something clever which is hard to explain</a>"
 
                by
 
                <span style="border:#ddd 1px solid;background:#f9f9f9">u2 u3 (u2)</span>.
 
            </div>
 
        </td>
 
    </tr>
 
    <tr>
 
        <td>
 
<center>
 
    <table cellspacing="0" cellpadding="0" style="margin-left:auto;margin-right:auto">
 
        <tr>
 
            <td height="25px" style="height:25px"></td>
 
        </tr>
 
        <tr>
 
            <td style="border-collapse:collapse;border-radius:2px;text-align:center;display:block;border:solid 1px #395fa0;padding:11px 20px 11px 20px">
 
                <a href="http://comment.org" style="text-decoration:none;display:block" target="_blank">
 
                    <center>
 
                        <font size="3">
 
                            <span style="font-family:Helvetica,Arial,sans-serif;font-weight:700;font-size:15px;line-height:14px;color:#395fa0;white-space:nowrap;vertical-align:middle">View Comment</span>
 
                        </font>
 
                    </center>
 
                </a>
 
            </td>
 
        </tr>
 
    </table>
 
</center>
 
        </td>
 
    </tr>
 
</table>
 
                    </td>
 
                    <td></td>
 
                </tr>
 
                <tr>
 
                    <td height="30px" style="height:30px" colspan="3"></td>
 
                </tr>
 
            </table>
 
        </td>
 
        <td width="30px" style="width:30px"></td>
 
    </tr>
 
</table>
 
<!--/body-->
 
<!--/html-->
 
<hr/>
 
<hr/>
 
<h1>cs_comment, is_mention=True, status_change=None</h1>
 
<pre>
 
From: u1 u1 <name@example.com>
 
To: u2@example.com
 
Subject: [Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch
 
Subject: [Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch by u2
 
</pre>
 
<hr/>
 
<pre>http://comment.org
 

	
 
Mention in Comment on Changeset "This changeset did something clever which is hard to explain"
 

	
 

	
 
Opinionated User (jsmith):
 

	
 
This is the new 'comment'.
 

	
 
 - and here it ends indented.
 

	
 

	
 
Changeset on http://example.com/repo_target branch brunch:
 
"This changeset did something clever which is hard to explain" by u2 u3 (u2).
 

	
 

	
 
View Comment: http://comment.org
 
</pre>
 
<hr/>
 
<!--!doctype html-->
 
<!--html lang="en"-->
 
<!--head-->
 
    <!--title--><!--/title-->
 
    <!--meta name="viewport" content="width=device-width"-->
 
    <!--meta http-equiv="Content-Type" content="text/html; charset=UTF-8"-->
 
<!--/head-->
 
<!--body-->
 
<table align="center" cellpadding="0" cellspacing="0" border="0" style="min-width:348px;max-width:800px;font-family:Helvetica,Arial,sans-serif;font-weight:200;font-size:14px;line-height:17px;color:#202020">
 
    <tr>
 
        <td width="30px" style="width:30px"></td>
 
        <td>
 
            <table width="100%" cellpadding="0" cellspacing="0" border="0"
 
                   style="table-layout:fixed;font-family:Helvetica,Arial,sans-serif;border:1px solid #ddd">
 
                <tr><td width="30px" style="width:30px"></td><td></td><td width="30px" style="width:30px"></td></tr>
 
                <tr>
 
                    <td colspan="3">
 
<table bgcolor="#f9f9f9" width="100%" cellpadding="0" cellspacing="0"
 
       style="border-bottom:1px solid #ddd">
 
    <tr>
 
        <td height="20px" style="height:20px" colspan="3"></td>
 
    </tr>
 
    <tr>
 
        <td width="30px" style="width:30px"></td>
 
        <td style="font-family:Helvetica,Arial,sans-serif;font-size:19px;line-height:24px">
 
            <a style="text-decoration:none;font-weight:600;color:#395fa0" href="http://comment.org"
 
               target="_blank">Mention in Comment on Changeset &#34;This changeset did something clever which is hard to explain&#34;</a>
 
        </td>
 
        <td width="30px" style="width:30px"></td>
 
    </tr>
 
    <tr>
 
        <td height="20px" style="height:20px" colspan="3"></td>
 
    </tr>
 
</table>
 
                    </td>
 
                </tr>
 
                <tr>
 
                    <td height="30px" style="height:30px" colspan="3"></td>
 
                </tr>
 
                <tr>
 
                    <td></td>
 
                    <td>
 
<table cellpadding="0" cellspacing="0" border="0" width="100%">
 
    <tr>
 
        <td>
 
<table cellpadding="0" cellspacing="0" width="100%" border="0" bgcolor="#f9f9f9" style="border:1px solid #ddd;border-radius:4px">
 
    <tr>
 
        <td height="10px" style="height:10px" colspan="3"></td>
 
    </tr>
 
    <tr>
 
        <td width="20px" style="width:20px"></td>
 
        <td>
 
            <div style="font-weight:600;color:#395fa0">Opinionated User (jsmith)</div>
 
        </td>
 
        <td width="20px" style="width:20px"></td>
 
    </tr>
 
    <tr>
 
        <td height="10px" style="height:10px" colspan="3" style="border-bottom:1px solid #ddd"></td>
 
    </tr>
 
    <tr>
 
        <td height="10px" style="height:10px" colspan="3"></td>
 
    </tr>
 
    <tr>
 
        <td width="20px" style="width:20px"></td>
 
        <td>
 
            <div style="font-family:Lucida Console,Consolas,Monaco,Inconsolata,Liberation Mono,monospace;white-space:pre-wrap"><div class="formatted-fixed">This is the new &#39;comment&#39;.<br/><br/> - and here it ends indented.</div></div>
 
        </td>
 
        <td width="20px" style="width:20px"></td>
 
    </tr>
 
    <tr>
 
        <td height="10px" style="height:10px" colspan="3"></td>
 
    </tr>
 
</table>
 
        </td>
 
    </tr>
 
    <tr>
 
        <td height="30px" style="height:30px"></td>
 
    </tr>
 
    <tr>
 
        <td>
 
            <div>
 
                Changeset on
 
                <a style="color:#202020;text-decoration:none;border:#ddd 1px solid;background:#f9f9f9"
 
                   href="http://example.com/repo_target">http://example.com/repo_target</a>
 
                branch
 
                <span style="border:#ddd 1px solid;background:#f9f9f9">brunch</span>:
 
            </div>
 
            <div>
 
                "<a style="color:#395fa0;text-decoration:none"
 
                   href="http://changeset.com">This changeset did something clever which is hard to explain</a>"
 
                by
 
                <span style="border:#ddd 1px solid;background:#f9f9f9">u2 u3 (u2)</span>.
 
            </div>
 
        </td>
 
    </tr>
 
    <tr>
 
        <td>
 
<center>
 
    <table cellspacing="0" cellpadding="0" style="margin-left:auto;margin-right:auto">
 
        <tr>
 
            <td height="25px" style="height:25px"></td>
 
        </tr>
 
        <tr>
 
            <td style="border-collapse:collapse;border-radius:2px;text-align:center;display:block;border:solid 1px #395fa0;padding:11px 20px 11px 20px">
 
                <a href="http://comment.org" style="text-decoration:none;display:block" target="_blank">
 
                    <center>
 
                        <font size="3">
 
                            <span style="font-family:Helvetica,Arial,sans-serif;font-weight:700;font-size:15px;line-height:14px;color:#395fa0;white-space:nowrap;vertical-align:middle">View Comment</span>
 
                        </font>
 
                    </center>
 
                </a>
 
            </td>
 
        </tr>
 
    </table>
 
</center>
 
        </td>
 
    </tr>
 
</table>
 
                    </td>
 
                    <td></td>
 
                </tr>
 
                <tr>
 
                    <td height="30px" style="height:30px" colspan="3"></td>
 
                </tr>
 
            </table>
 
        </td>
 
        <td width="30px" style="width:30px"></td>
 
    </tr>
 
</table>
 
<!--/body-->
 
<!--/html-->
 
<hr/>
 
<hr/>
 
<h1>cs_comment, is_mention=False, status_change='Approved'</h1>
 
<pre>
 
From: u1 u1 <name@example.com>
 
To: u2@example.com
 
Subject: [Approved: Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch
 
Subject: [Approved: Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch by u2
 
</pre>
 
<hr/>
 
<pre>http://comment.org
 

	
 
Comment on Changeset "This changeset did something clever which is hard to explain"
 

	
 

	
 
Opinionated User (jsmith):
 

	
 
Status change: Approved
 

	
 
This is the new 'comment'.
 

	
 
 - and here it ends indented.
 

	
 

	
 
Changeset on http://example.com/repo_target branch brunch:
 
"This changeset did something clever which is hard to explain" by u2 u3 (u2).
 

	
 

	
 
View Comment: http://comment.org
 
</pre>
 
<hr/>
 
<!--!doctype html-->
 
<!--html lang="en"-->
 
<!--head-->
 
    <!--title--><!--/title-->
 
    <!--meta name="viewport" content="width=device-width"-->
 
    <!--meta http-equiv="Content-Type" content="text/html; charset=UTF-8"-->
 
<!--/head-->
 
<!--body-->
 
<table align="center" cellpadding="0" cellspacing="0" border="0" style="min-width:348px;max-width:800px;font-family:Helvetica,Arial,sans-serif;font-weight:200;font-size:14px;line-height:17px;color:#202020">
 
    <tr>
 
        <td width="30px" style="width:30px"></td>
 
        <td>
 
            <table width="100%" cellpadding="0" cellspacing="0" border="0"
 
                   style="table-layout:fixed;font-family:Helvetica,Arial,sans-serif;border:1px solid #ddd">
 
                <tr><td width="30px" style="width:30px"></td><td></td><td width="30px" style="width:30px"></td></tr>
 
                <tr>
 
                    <td colspan="3">
 
<table bgcolor="#f9f9f9" width="100%" cellpadding="0" cellspacing="0"
 
       style="border-bottom:1px solid #ddd">
 
    <tr>
 
        <td height="20px" style="height:20px" colspan="3"></td>
 
    </tr>
 
    <tr>
 
        <td width="30px" style="width:30px"></td>
 
        <td style="font-family:Helvetica,Arial,sans-serif;font-size:19px;line-height:24px">
 
            <a style="text-decoration:none;font-weight:600;color:#395fa0" href="http://comment.org"
 
               target="_blank">Comment on Changeset &#34;This changeset did something clever which is hard to explain&#34;</a>
 
        </td>
 
        <td width="30px" style="width:30px"></td>
 
    </tr>
 
    <tr>
 
        <td height="20px" style="height:20px" colspan="3"></td>
 
    </tr>
 
</table>
 
                    </td>
 
                </tr>
 
                <tr>
 
                    <td height="30px" style="height:30px" colspan="3"></td>
 
                </tr>
 
                <tr>
 
                    <td></td>
 
                    <td>
 
<table cellpadding="0" cellspacing="0" border="0" width="100%">
 
    <tr>
 
        <td>
 
<table cellpadding="0" cellspacing="0" width="100%" border="0" bgcolor="#f9f9f9" style="border:1px solid #ddd;border-radius:4px">
 
    <tr>
 
        <td height="10px" style="height:10px" colspan="3"></td>
 
    </tr>
 
    <tr>
 
        <td width="20px" style="width:20px"></td>
 
        <td>
 
            <div style="font-weight:600;color:#395fa0">Opinionated User (jsmith)</div>
 
        </td>
 
        <td width="20px" style="width:20px"></td>
 
    </tr>
 
    <tr>
 
        <td height="10px" style="height:10px" colspan="3" style="border-bottom:1px solid #ddd"></td>
 
    </tr>
 
        <tr>
 
            <td height="10px" style="height:10px" colspan="3"></td>
 
        </tr>
 
        <tr>
 
            <td width="20px" style="width:20px"></td>
 
            <td>
 
                    <div style="font-weight:600">
 
                        Status change:
 
                        Approved
 
                    </div>
 
            </td>
 
            <td width="20px" style="width:20px"></td>
 
        </tr>
 
        <tr>
 
            <td height="10px" style="height:10px" colspan="3" style="border-bottom:1px solid #ddd"></td>
 
        </tr>
 
    <tr>
 
        <td height="10px" style="height:10px" colspan="3"></td>
 
    </tr>
 
    <tr>
 
        <td width="20px" style="width:20px"></td>
 
        <td>
 
            <div style="font-family:Lucida Console,Consolas,Monaco,Inconsolata,Liberation Mono,monospace;white-space:pre-wrap"><div class="formatted-fixed">This is the new &#39;comment&#39;.<br/><br/> - and here it ends indented.</div></div>
 
        </td>
 
        <td width="20px" style="width:20px"></td>
 
    </tr>
 
    <tr>
 
        <td height="10px" style="height:10px" colspan="3"></td>
 
    </tr>
 
</table>
 
        </td>
 
    </tr>
 
    <tr>
 
        <td height="30px" style="height:30px"></td>
 
    </tr>
 
    <tr>
 
        <td>
 
            <div>
 
                Changeset on
 
                <a style="color:#202020;text-decoration:none;border:#ddd 1px solid;background:#f9f9f9"
 
                   href="http://example.com/repo_target">http://example.com/repo_target</a>
 
                branch
 
                <span style="border:#ddd 1px solid;background:#f9f9f9">brunch</span>:
 
            </div>
 
            <div>
 
                "<a style="color:#395fa0;text-decoration:none"
 
                   href="http://changeset.com">This changeset did something clever which is hard to explain</a>"
 
                by
 
                <span style="border:#ddd 1px solid;background:#f9f9f9">u2 u3 (u2)</span>.
 
            </div>
 
        </td>
 
    </tr>
 
    <tr>
 
        <td>
 
<center>
 
    <table cellspacing="0" cellpadding="0" style="margin-left:auto;margin-right:auto">
 
        <tr>
 
            <td height="25px" style="height:25px"></td>
 
        </tr>
 
        <tr>
 
            <td style="border-collapse:collapse;border-radius:2px;text-align:center;display:block;border:solid 1px #395fa0;padding:11px 20px 11px 20px">
 
                <a href="http://comment.org" style="text-decoration:none;display:block" target="_blank">
 
                    <center>
 
                        <font size="3">
 
                            <span style="font-family:Helvetica,Arial,sans-serif;font-weight:700;font-size:15px;line-height:14px;color:#395fa0;white-space:nowrap;vertical-align:middle">View Comment</span>
 
                        </font>
 
                    </center>
 
                </a>
 
            </td>
 
        </tr>
 
    </table>
 
</center>
 
        </td>
 
    </tr>
 
</table>
 
                    </td>
 
                    <td></td>
 
                </tr>
 
                <tr>
 
                    <td height="30px" style="height:30px" colspan="3"></td>
 
                </tr>
 
            </table>
 
        </td>
 
        <td width="30px" style="width:30px"></td>
 
    </tr>
 
</table>
 
<!--/body-->
 
<!--/html-->
 
<hr/>
 
<hr/>
 
<h1>cs_comment, is_mention=True, status_change='Approved'</h1>
 
<pre>
 
From: u1 u1 <name@example.com>
 
To: u2@example.com
 
Subject: [Approved: Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch
 
Subject: [Approved: Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch by u2
 
</pre>
 
<hr/>
 
<pre>http://comment.org
 

	
 
Mention in Comment on Changeset "This changeset did something clever which is hard to explain"
 

	
 

	
 
Opinionated User (jsmith):
 

	
 
Status change: Approved
 

	
 
This is the new 'comment'.
 

	
 
 - and here it ends indented.
 

	
 

	
 
Changeset on http://example.com/repo_target branch brunch:
 
"This changeset did something clever which is hard to explain" by u2 u3 (u2).
 

	
 

	
 
View Comment: http://comment.org
 
</pre>
 
<hr/>
 
<!--!doctype html-->
 
<!--html lang="en"-->
 
<!--head-->
 
    <!--title--><!--/title-->
 
    <!--meta name="viewport" content="width=device-width"-->
 
    <!--meta http-equiv="Content-Type" content="text/html; charset=UTF-8"-->
 
<!--/head-->
 
<!--body-->
 
<table align="center" cellpadding="0" cellspacing="0" border="0" style="min-width:348px;max-width:800px;font-family:Helvetica,Arial,sans-serif;font-weight:200;font-size:14px;line-height:17px;color:#202020">
 
    <tr>
 
        <td width="30px" style="width:30px"></td>
 
        <td>
 
            <table width="100%" cellpadding="0" cellspacing="0" border="0"
 
                   style="table-layout:fixed;font-family:Helvetica,Arial,sans-serif;border:1px solid #ddd">
 
                <tr><td width="30px" style="width:30px"></td><td></td><td width="30px" style="width:30px"></td></tr>
 
                <tr>
 
                    <td colspan="3">
 
<table bgcolor="#f9f9f9" width="100%" cellpadding="0" cellspacing="0"
 
       style="border-bottom:1px solid #ddd">
 
    <tr>
 
        <td height="20px" style="height:20px" colspan="3"></td>
 
    </tr>
 
    <tr>
 
        <td width="30px" style="width:30px"></td>
 
        <td style="font-family:Helvetica,Arial,sans-serif;font-size:19px;line-height:24px">
 
            <a style="text-decoration:none;font-weight:600;color:#395fa0" href="http://comment.org"
 
               target="_blank">Mention in Comment on Changeset &#34;This changeset did something clever which is hard to explain&#34;</a>
 
        </td>
 
        <td width="30px" style="width:30px"></td>
 
    </tr>
 
    <tr>
 
        <td height="20px" style="height:20px" colspan="3"></td>
 
    </tr>
 
</table>
 
                    </td>
 
                </tr>
 
                <tr>
 
                    <td height="30px" style="height:30px" colspan="3"></td>
 
                </tr>
 
                <tr>
 
                    <td></td>
 
                    <td>
 
<table cellpadding="0" cellspacing="0" border="0" width="100%">
 
    <tr>
 
        <td>
 
<table cellpadding="0" cellspacing="0" width="100%" border="0" bgcolor="#f9f9f9" style="border:1px solid #ddd;border-radius:4px">
 
    <tr>
 
        <td height="10px" style="height:10px" colspan="3"></td>
 
    </tr>
 
    <tr>
 
        <td width="20px" style="width:20px"></td>
 
        <td>
 
            <div style="font-weight:600;color:#395fa0">Opinionated User (jsmith)</div>
 
        </td>
 
        <td width="20px" style="width:20px"></td>
 
    </tr>
 
    <tr>
 
        <td height="10px" style="height:10px" colspan="3" style="border-bottom:1px solid #ddd"></td>
 
    </tr>
 
        <tr>
 
            <td height="10px" style="height:10px" colspan="3"></td>
 
        </tr>
 
        <tr>
 
            <td width="20px" style="width:20px"></td>
 
            <td>
 
                    <div style="font-weight:600">
 
                        Status change:
 
                        Approved
 
                    </div>
 
            </td>
 
            <td width="20px" style="width:20px"></td>
 
        </tr>
 
        <tr>
 
            <td height="10px" style="height:10px" colspan="3" style="border-bottom:1px solid #ddd"></td>
 
        </tr>
 
    <tr>
 
        <td height="10px" style="height:10px" colspan="3"></td>
 
    </tr>
 
    <tr>
 
        <td width="20px" style="width:20px"></td>
 
        <td>
 
            <div style="font-family:Lucida Console,Consolas,Monaco,Inconsolata,Liberation Mono,monospace;white-space:pre-wrap"><div class="formatted-fixed">This is the new &#39;comment&#39;.<br/><br/> - and here it ends indented.</div></div>
 
        </td>
 
        <td width="20px" style="width:20px"></td>
 
    </tr>
 
    <tr>
 
        <td height="10px" style="height:10px" colspan="3"></td>
 
    </tr>
 
</table>
 
        </td>
 
    </tr>
 
    <tr>
 
        <td height="30px" style="height:30px"></td>
 
    </tr>
 
    <tr>
 
        <td>
 
            <div>
 
                Changeset on
 
                <a style="color:#202020;text-decoration:none;border:#ddd 1px solid;background:#f9f9f9"
 
                   href="http://example.com/repo_target">http://example.com/repo_target</a>
 
                branch
 
                <span style="border:#ddd 1px solid;background:#f9f9f9">brunch</span>:
 
            </div>
 
            <div>
 
                "<a style="color:#395fa0;text-decoration:none"
 
                   href="http://changeset.com">This changeset did something clever which is hard to explain</a>"
 
                by
 
                <span style="border:#ddd 1px solid;background:#f9f9f9">u2 u3 (u2)</span>.
 
            </div>
 
        </td>
 
    </tr>
 
    <tr>
 
        <td>
 
<center>
 
    <table cellspacing="0" cellpadding="0" style="margin-left:auto;margin-right:auto">
 
        <tr>
 
            <td height="25px" style="height:25px"></td>
 
        </tr>
 
        <tr>
 
            <td style="border-collapse:collapse;border-radius:2px;text-align:center;display:block;border:solid 1px #395fa0;padding:11px 20px 11px 20px">
 
                <a href="http://comment.org" style="text-decoration:none;display:block" target="_blank">
 
                    <center>
 
                        <font size="3">
 
                            <span style="font-family:Helvetica,Arial,sans-serif;font-weight:700;font-size:15px;line-height:14px;color:#395fa0;white-space:nowrap;vertical-align:middle">View Comment</span>
 
                        </font>
 
                    </center>
 
                </a>
 
            </td>
 
        </tr>
 
    </table>
 
</center>
 
        </td>
 
    </tr>
 
</table>
 
                    </td>
 
                    <td></td>
 
                </tr>
 
                <tr>
 
                    <td height="30px" style="height:30px" colspan="3"></td>
 
                </tr>
 
            </table>
 
        </td>
 
        <td width="30px" style="width:30px"></td>
 
    </tr>
 
</table>
 
<!--/body-->
 
<!--/html-->
 
<hr/>
 
<hr/>
 
<h1>message</h1>
 
<pre>
 
From: u1 u1 <name@example.com>
 
To: u2@example.com
 
Subject: Test Message
 
</pre>
 
<hr/>
 
<pre>This is the 'body' of the "test" message
 
 - nothing interesting here except indentation.</pre>
 
<hr/>
 
<!--!doctype html-->
 
<!--html lang="en"-->
 
<!--head-->
 
    <!--title--><!--/title-->
 
    <!--meta name="viewport" content="width=device-width"-->
 
    <!--meta http-equiv="Content-Type" content="text/html; charset=UTF-8"-->
 
<!--/head-->
 
<!--body-->
 
<table align="center" cellpadding="0" cellspacing="0" border="0" style="min-width:348px;max-width:800px;font-family:Helvetica,Arial,sans-serif;font-weight:200;font-size:14px;line-height:17px;color:#202020">
 
    <tr>
kallithea/tests/models/test_notifications.py
Show inline comments
 
import os
 
import re
 

	
 
import mock
 
from tg.util.webtest import test_context
 

	
 
import kallithea.lib.celerylib
 
import kallithea.lib.celerylib.tasks
 
from kallithea.lib import helpers as h
 
from kallithea.model.db import User
 
from kallithea.model.meta import Session
 
from kallithea.model.notification import EmailNotificationModel, NotificationModel
 
from kallithea.model.user import UserModel
 
from kallithea.tests import base
 

	
 

	
 
class TestNotifications(base.TestController):
 

	
 
    def setup_method(self, method):
 
        Session.remove()
 
        u1 = UserModel().create_or_update(username='u1',
 
                                        password='qweqwe',
 
                                        email='u1@example.com',
 
                                        firstname='u1', lastname='u1')
 
        Session().commit()
 
        self.u1 = u1.user_id
 

	
 
        u2 = UserModel().create_or_update(username='u2',
 
                                        password='qweqwe',
 
                                        email='u2@example.com',
 
                                        firstname='u2', lastname='u3')
 
        Session().commit()
 
        self.u2 = u2.user_id
 

	
 
        u3 = UserModel().create_or_update(username='u3',
 
                                        password='qweqwe',
 
                                        email='u3@example.com',
 
                                        firstname='u3', lastname='u3')
 
        Session().commit()
 
        self.u3 = u3.user_id
 

	
 
    def test_create_notification(self):
 
        with test_context(self.app):
 
            usrs = [self.u1, self.u2]
 

	
 
            def send_email(recipients, subject, body='', html_body='', headers=None, from_name=None):
 
                assert recipients == ['u2@example.com']
 
                assert subject == 'Test Message'
 
                assert body == "hi there"
 
                assert '>hi there<' in html_body
 
                assert from_name == 'u1 u1'
 
            with mock.patch.object(kallithea.lib.celerylib.tasks, 'send_email', send_email):
 
                NotificationModel().create(created_by=self.u1,
 
                                                   subject='subj', body='hi there',
 
                                                   recipients=usrs)
 

	
 
    @mock.patch.object(h, 'canonical_url', (lambda arg, **kwargs: 'http://%s/?%s' % (arg, '&'.join('%s=%s' % (k, v) for (k, v) in sorted(kwargs.items())))))
 
    def test_dump_html_mails(self):
 
        # Exercise all notification types and dump them to one big html file
 
        l = []
 

	
 
        def send_email(recipients, subject, body='', html_body='', headers=None, from_name=None):
 
            l.append('<hr/>\n')
 
            l.append('<h1>%s</h1>\n' % desc) # desc is from outer scope
 
            l.append('<pre>\n')
 
            l.append('From: %s <name@example.com>\n' % from_name)
 
            l.append('To: %s\n' % ' '.join(recipients))
 
            l.append('Subject: %s\n' % subject)
 
            l.append('</pre>\n')
 
            l.append('<hr/>\n')
 
            l.append('<pre>%s</pre>\n' % body)
 
            l.append('<hr/>\n')
 
            l.append(html_body)
 
            l.append('<hr/>\n')
 

	
 
        with test_context(self.app):
 
            with mock.patch.object(kallithea.lib.celerylib.tasks, 'send_email', send_email):
 
                pr_kwargs = dict(
 
                    pr_nice_id='#7',
 
                    pr_title='The Title',
 
                    pr_title_short='The Title',
 
                    pr_url='http://pr.org/7',
 
                    pr_target_repo='http://mainline.com/repo',
 
                    pr_target_branch='trunk',
 
                    pr_source_repo='https://dev.org/repo',
 
                    pr_source_branch='devbranch',
 
                    pr_owner=User.get(self.u2),
 
                    pr_owner_username='u2'
 
                    )
 

	
 
                for type_, body, kwargs in [
 
                        (NotificationModel.TYPE_CHANGESET_COMMENT,
 
                         'This is the new \'comment\'.\n\n - and here it ends indented.',
 
                         dict(
 
                            short_id='cafe1234',
 
                            raw_id='cafe1234c0ffeecafe',
 
                            branch='brunch',
 
                            cs_comment_user='Opinionated User (jsmith)',
 
                            cs_comment_url='http://comment.org',
 
                            is_mention=[False, True],
 
                            message='This changeset did something clever which is hard to explain',
 
                            message_short='This changeset did something cl...',
 
                            status_change=[None, 'Approved'],
 
                            cs_target_repo='http://example.com/repo_target',
 
                            cs_url='http://changeset.com',
 
                            cs_author_username=User.get(self.u2).username,
 
                            cs_author=User.get(self.u2))),
 
                        (NotificationModel.TYPE_MESSAGE,
 
                         'This is the \'body\' of the "test" message\n - nothing interesting here except indentation.',
 
                         dict()),
 
                        #(NotificationModel.TYPE_MENTION, '$body', None), # not used
 
                        (NotificationModel.TYPE_REGISTRATION,
 
                         'Registration body',
 
                         dict(
 
                            new_username='newbie',
 
                            registered_user_url='http://newbie.org',
 
                            new_email='new@email.com',
 
                            new_full_name='New Full Name')),
 
                        (NotificationModel.TYPE_PULL_REQUEST,
 
                         'This PR is \'awesome\' because it does <stuff>\n - please approve indented!',
 
                         dict(
 
                            pr_user_created='Requesting User (root)', # pr_owner should perhaps be used for @mention in description ...
 
                            is_mention=[False, True],
 
                            pr_revisions=[('123abc'*7, "Introduce one and two\n\nand that's it"), ('567fed'*7, 'Make one plus two equal tree')],
 
                            org_repo_name='repo_org',
 
                            **pr_kwargs)),
 
                        (NotificationModel.TYPE_PULL_REQUEST_COMMENT,
 
                         'Me too!\n\n - and indented on second line',
 
                         dict(
 
                            closing_pr=[False, True],
 
                            is_mention=[False, True],
 
                            pr_comment_user='Opinionated User (jsmith)',
 
                            pr_comment_url='http://pr.org/comment',
 
                            status_change=[None, 'Under Review'],
 
                            **pr_kwargs)),
 
                        ]:
 
                    kwargs['repo_name'] = 'repo/name'
 
                    params = [(type_, type_, body, kwargs)]
 
                    for param_name in ['is_mention', 'status_change', 'closing_pr']: # TODO: inline/general
 
                        if not isinstance(kwargs.get(param_name), list):
 
                            continue
 
                        new_params = []
 
                        for v in kwargs[param_name]:
 
                            for desc, type_, body, kwargs in params:
 
                                kwargs = dict(kwargs)
 
                                kwargs[param_name] = v
 
                                new_params.append(('%s, %s=%r' % (desc, param_name, v), type_, body, kwargs))
 
                        params = new_params
 

	
 
                    for desc, type_, body, kwargs in params:
 
                        # desc is used as "global" variable
 
                        NotificationModel().create(created_by=self.u1,
 
                                                           subject='unused', body=body, email_kwargs=kwargs,
 
                                                           recipients=[self.u2], type_=type_)
 

	
 
                # Email type TYPE_PASSWORD_RESET has no corresponding notification type - test it directly:
 
                desc = 'TYPE_PASSWORD_RESET'
 
                kwargs = dict(user='John Doe', reset_token='decbf64715098db5b0bd23eab44bd792670ab746', reset_url='http://reset.com/decbf64715098db5b0bd23eab44bd792670ab746')
 
                kallithea.lib.celerylib.tasks.send_email(['john@doe.com'],
 
                    "Password reset link",
 
                    EmailNotificationModel().get_email_tmpl(EmailNotificationModel.TYPE_PASSWORD_RESET, 'txt', **kwargs),
 
                    EmailNotificationModel().get_email_tmpl(EmailNotificationModel.TYPE_PASSWORD_RESET, 'html', **kwargs),
 
                    from_name=User.get(self.u1).full_name_or_username)
 

	
 
        out = '<!doctype html>\n<html lang="en">\n<head><title>Notifications</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>\n<body>\n%s\n</body>\n</html>\n' % \
 
            re.sub(r'<(/?(?:!doctype|html|head|title|meta|body)\b[^>]*)>', r'<!--\1-->', ''.join(l))
 

	
 
        outfn = os.path.join(os.path.dirname(__file__), 'test_dump_html_mails.out.html')
 
        reffn = os.path.join(os.path.dirname(__file__), 'test_dump_html_mails.ref.html')
 
        with open(outfn, 'w') as f:
 
            f.write(out)
 
        with open(reffn) as f:
 
            ref = f.read()
 
        assert ref == out # copy test_dump_html_mails.out.html to test_dump_html_mails.ref.html to update expectations
 
        os.unlink(outfn)
0 comments (0 inline, 0 general)