##// END OF EJS Templates
tests: add test coverage of PR comment @mention
Mads Kiilerich -
r6020:55280080 default
parent child Browse files
Show More
@@ -1,276 +1,277 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.model.comment
15 kallithea.model.comment
16 ~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 comments model for Kallithea
18 comments model for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Nov 11, 2011
22 :created_on: Nov 11, 2011
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29
29
30 from pylons.i18n.translation import _
30 from pylons.i18n.translation import _
31 from collections import defaultdict
31 from collections import defaultdict
32
32
33 from kallithea.lib.utils2 import extract_mentioned_users, safe_unicode
33 from kallithea.lib.utils2 import extract_mentioned_users, safe_unicode
34 from kallithea.lib import helpers as h
34 from kallithea.lib import helpers as h
35 from kallithea.model import BaseModel
35 from kallithea.model import BaseModel
36 from kallithea.model.db import ChangesetComment, User, \
36 from kallithea.model.db import ChangesetComment, User, \
37 Notification, PullRequest
37 Notification, PullRequest
38 from kallithea.model.notification import NotificationModel
38 from kallithea.model.notification import NotificationModel
39 from kallithea.model.meta import Session
39 from kallithea.model.meta import Session
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43
43
44 class ChangesetCommentsModel(BaseModel):
44 class ChangesetCommentsModel(BaseModel):
45
45
46 cls = ChangesetComment
46 cls = ChangesetComment
47
47
48 def __get_changeset_comment(self, changeset_comment):
48 def __get_changeset_comment(self, changeset_comment):
49 return self._get_instance(ChangesetComment, changeset_comment)
49 return self._get_instance(ChangesetComment, changeset_comment)
50
50
51 def __get_pull_request(self, pull_request):
51 def __get_pull_request(self, pull_request):
52 return self._get_instance(PullRequest, pull_request)
52 return self._get_instance(PullRequest, pull_request)
53
53
54 def _get_notification_data(self, repo, comment, user, comment_text,
54 def _get_notification_data(self, repo, comment, user, comment_text,
55 line_no=None, revision=None, pull_request=None,
55 line_no=None, revision=None, pull_request=None,
56 status_change=None, closing_pr=False):
56 status_change=None, closing_pr=False):
57 """
57 """
58 :returns: tuple (subj,body,recipients,notification_type,email_kwargs)
58 :returns: tuple (subj,body,recipients,notification_type,email_kwargs)
59 """
59 """
60 # make notification
60 # make notification
61 body = comment_text # text of the comment
61 body = comment_text # text of the comment
62 line = ''
62 line = ''
63 if line_no:
63 if line_no:
64 line = _('on line %s') % line_no
64 line = _('on line %s') % line_no
65
65
66 #changeset
66 #changeset
67 if revision:
67 if revision:
68 notification_type = Notification.TYPE_CHANGESET_COMMENT
68 notification_type = Notification.TYPE_CHANGESET_COMMENT
69 cs = repo.scm_instance.get_changeset(revision)
69 cs = repo.scm_instance.get_changeset(revision)
70 desc = cs.short_id
70 desc = cs.short_id
71
71
72 threading = ['%s-rev-%s@%s' % (repo.repo_name, revision, h.canonical_hostname())]
72 threading = ['%s-rev-%s@%s' % (repo.repo_name, revision, h.canonical_hostname())]
73 if line_no: # TODO: url to file _and_ line number
73 if line_no: # TODO: url to file _and_ line number
74 threading.append('%s-rev-%s-line-%s@%s' % (repo.repo_name, revision, line_no,
74 threading.append('%s-rev-%s-line-%s@%s' % (repo.repo_name, revision, line_no,
75 h.canonical_hostname()))
75 h.canonical_hostname()))
76 comment_url = h.canonical_url('changeset_home',
76 comment_url = h.canonical_url('changeset_home',
77 repo_name=repo.repo_name,
77 repo_name=repo.repo_name,
78 revision=revision,
78 revision=revision,
79 anchor='comment-%s' % comment.comment_id)
79 anchor='comment-%s' % comment.comment_id)
80 subj = safe_unicode(
80 subj = safe_unicode(
81 h.link_to('Re changeset: %(desc)s %(line)s' % \
81 h.link_to('Re changeset: %(desc)s %(line)s' % \
82 {'desc': desc, 'line': line},
82 {'desc': desc, 'line': line},
83 comment_url)
83 comment_url)
84 )
84 )
85 # get the current participants of this changeset
85 # get the current participants of this changeset
86 recipients = ChangesetComment.get_users(revision=revision)
86 recipients = ChangesetComment.get_users(revision=revision)
87 # add changeset author if it's known locally
87 # add changeset author if it's known locally
88 cs_author = User.get_from_cs_author(cs.author)
88 cs_author = User.get_from_cs_author(cs.author)
89 if not cs_author:
89 if not cs_author:
90 #use repo owner if we cannot extract the author correctly
90 #use repo owner if we cannot extract the author correctly
91 # FIXME: just use committer name even if not a user
91 # FIXME: just use committer name even if not a user
92 cs_author = repo.user
92 cs_author = repo.user
93 recipients += [cs_author]
93 recipients += [cs_author]
94 email_kwargs = {
94 email_kwargs = {
95 'status_change': status_change,
95 'status_change': status_change,
96 'cs_comment_user': user.full_name_and_username,
96 'cs_comment_user': user.full_name_and_username,
97 'cs_target_repo': h.canonical_url('summary_home', repo_name=repo.repo_name),
97 'cs_target_repo': h.canonical_url('summary_home', repo_name=repo.repo_name),
98 'cs_comment_url': comment_url,
98 'cs_comment_url': comment_url,
99 'raw_id': revision,
99 'raw_id': revision,
100 'message': cs.message,
100 'message': cs.message,
101 'cs_author': cs_author,
101 'cs_author': cs_author,
102 'repo_name': repo.repo_name,
102 'repo_name': repo.repo_name,
103 'short_id': h.short_id(revision),
103 'short_id': h.short_id(revision),
104 'branch': cs.branch,
104 'branch': cs.branch,
105 'comment_username': user.username,
105 'comment_username': user.username,
106 'threading': threading,
106 'threading': threading,
107 }
107 }
108 #pull request
108 #pull request
109 elif pull_request:
109 elif pull_request:
110 notification_type = Notification.TYPE_PULL_REQUEST_COMMENT
110 notification_type = Notification.TYPE_PULL_REQUEST_COMMENT
111 desc = comment.pull_request.title
111 desc = comment.pull_request.title
112 _org_ref_type, org_ref_name, _org_rev = comment.pull_request.org_ref.split(':')
112 _org_ref_type, org_ref_name, _org_rev = comment.pull_request.org_ref.split(':')
113 _other_ref_type, other_ref_name, _other_rev = comment.pull_request.other_ref.split(':')
113 _other_ref_type, other_ref_name, _other_rev = comment.pull_request.other_ref.split(':')
114 threading = ['%s-pr-%s@%s' % (pull_request.other_repo.repo_name,
114 threading = ['%s-pr-%s@%s' % (pull_request.other_repo.repo_name,
115 pull_request.pull_request_id,
115 pull_request.pull_request_id,
116 h.canonical_hostname())]
116 h.canonical_hostname())]
117 if line_no: # TODO: url to file _and_ line number
117 if line_no: # TODO: url to file _and_ line number
118 threading.append('%s-pr-%s-line-%s@%s' % (pull_request.other_repo.repo_name,
118 threading.append('%s-pr-%s-line-%s@%s' % (pull_request.other_repo.repo_name,
119 pull_request.pull_request_id, line_no,
119 pull_request.pull_request_id, line_no,
120 h.canonical_hostname()))
120 h.canonical_hostname()))
121 comment_url = pull_request.url(canonical=True,
121 comment_url = pull_request.url(canonical=True,
122 anchor='comment-%s' % comment.comment_id)
122 anchor='comment-%s' % comment.comment_id)
123 subj = safe_unicode(
123 subj = safe_unicode(
124 h.link_to('Re pull request %(pr_nice_id)s: %(desc)s %(line)s' % \
124 h.link_to('Re pull request %(pr_nice_id)s: %(desc)s %(line)s' % \
125 {'desc': desc,
125 {'desc': desc,
126 'pr_nice_id': comment.pull_request.nice_id(),
126 'pr_nice_id': comment.pull_request.nice_id(),
127 'line': line},
127 'line': line},
128 comment_url)
128 comment_url)
129 )
129 )
130 # get the current participants of this pull request
130 # get the current participants of this pull request
131 recipients = ChangesetComment.get_users(pull_request_id=
131 recipients = ChangesetComment.get_users(pull_request_id=
132 pull_request.pull_request_id)
132 pull_request.pull_request_id)
133 # add pull request author
133 # add pull request author
134 recipients += [pull_request.owner]
134 recipients += [pull_request.owner]
135
135
136 # add the reviewers to notification
136 # add the reviewers to notification
137 recipients += pull_request.get_reviewer_users()
137 recipients += pull_request.get_reviewer_users()
138
138
139 #set some variables for email notification
139 #set some variables for email notification
140 email_kwargs = {
140 email_kwargs = {
141 'pr_title': pull_request.title,
141 'pr_title': pull_request.title,
142 'pr_nice_id': pull_request.nice_id(),
142 'pr_nice_id': pull_request.nice_id(),
143 'status_change': status_change,
143 'status_change': status_change,
144 'closing_pr': closing_pr,
144 'closing_pr': closing_pr,
145 'pr_comment_url': comment_url,
145 'pr_comment_url': comment_url,
146 'pr_comment_user': user.full_name_and_username,
146 'pr_comment_user': user.full_name_and_username,
147 'pr_target_repo': h.canonical_url('summary_home',
147 'pr_target_repo': h.canonical_url('summary_home',
148 repo_name=pull_request.other_repo.repo_name),
148 repo_name=pull_request.other_repo.repo_name),
149 'pr_target_branch': other_ref_name,
149 'pr_target_branch': other_ref_name,
150 'pr_source_repo': h.canonical_url('summary_home',
150 'pr_source_repo': h.canonical_url('summary_home',
151 repo_name=pull_request.org_repo.repo_name),
151 repo_name=pull_request.org_repo.repo_name),
152 'pr_source_branch': org_ref_name,
152 'pr_source_branch': org_ref_name,
153 'pr_owner': pull_request.owner,
153 'pr_owner': pull_request.owner,
154 'repo_name': pull_request.other_repo.repo_name,
154 'repo_name': pull_request.other_repo.repo_name,
155 'comment_username': user.username,
155 'comment_username': user.username,
156 'threading': threading,
156 'threading': threading,
157 }
157 }
158
158
159 return subj, body, recipients, notification_type, email_kwargs
159 return subj, body, recipients, notification_type, email_kwargs
160
160
161 def create(self, text, repo, user, revision=None, pull_request=None,
161 def create(self, text, repo, user, revision=None, pull_request=None,
162 f_path=None, line_no=None, status_change=None, closing_pr=False,
162 f_path=None, line_no=None, status_change=None, closing_pr=False,
163 send_email=True):
163 send_email=True):
164 """
164 """
165 Creates a new comment for either a changeset or a pull request.
165 Creates a new comment for either a changeset or a pull request.
166 status_change and closing_pr is only for the optional email.
166 status_change and closing_pr is only for the optional email.
167
167
168 Returns the created comment.
168 Returns the created comment.
169 """
169 """
170 if not status_change and not text:
170 if not status_change and not text:
171 log.warning('Missing text for comment, skipping...')
171 log.warning('Missing text for comment, skipping...')
172 return None
172 return None
173
173
174 repo = self._get_repo(repo)
174 repo = self._get_repo(repo)
175 user = self._get_user(user)
175 user = self._get_user(user)
176 comment = ChangesetComment()
176 comment = ChangesetComment()
177 comment.repo = repo
177 comment.repo = repo
178 comment.author = user
178 comment.author = user
179 comment.text = text
179 comment.text = text
180 comment.f_path = f_path
180 comment.f_path = f_path
181 comment.line_no = line_no
181 comment.line_no = line_no
182
182
183 if revision is not None:
183 if revision is not None:
184 comment.revision = revision
184 comment.revision = revision
185 elif pull_request is not None:
185 elif pull_request is not None:
186 pull_request = self.__get_pull_request(pull_request)
186 pull_request = self.__get_pull_request(pull_request)
187 comment.pull_request = pull_request
187 comment.pull_request = pull_request
188 else:
188 else:
189 raise Exception('Please specify revision or pull_request_id')
189 raise Exception('Please specify revision or pull_request_id')
190
190
191 Session().add(comment)
191 Session().add(comment)
192 Session().flush()
192 Session().flush()
193
193
194 if send_email:
194 if send_email:
195 (subj, body, recipients, notification_type,
195 (subj, body, recipients, notification_type,
196 email_kwargs) = self._get_notification_data(
196 email_kwargs) = self._get_notification_data(
197 repo, comment, user,
197 repo, comment, user,
198 comment_text=text,
198 comment_text=text,
199 line_no=line_no,
199 line_no=line_no,
200 revision=revision,
200 revision=revision,
201 pull_request=pull_request,
201 pull_request=pull_request,
202 status_change=status_change,
202 status_change=status_change,
203 closing_pr=closing_pr)
203 closing_pr=closing_pr)
204 email_kwargs['is_mention'] = False
204 email_kwargs['is_mention'] = False
205 # create notification objects, and emails
205 # create notification objects, and emails
206 NotificationModel().create(
206 NotificationModel().create(
207 created_by=user, subject=subj, body=body,
207 created_by=user, subject=subj, body=body,
208 recipients=recipients, type_=notification_type,
208 recipients=recipients, type_=notification_type,
209 email_kwargs=email_kwargs,
209 email_kwargs=email_kwargs,
210 )
210 )
211
211
212 mention_recipients = extract_mentioned_users(body).difference(recipients)
212 mention_recipients = extract_mentioned_users(body).difference(recipients)
213 if mention_recipients:
213 if mention_recipients:
214 email_kwargs['is_mention'] = True
214 email_kwargs['is_mention'] = True
215 subj = _('[Mention]') + ' ' + subj
215 subj = _('[Mention]') + ' ' + subj
216 # FIXME: this subject is wrong and unused!
216 NotificationModel().create(
217 NotificationModel().create(
217 created_by=user, subject=subj, body=body,
218 created_by=user, subject=subj, body=body,
218 recipients=mention_recipients,
219 recipients=mention_recipients,
219 type_=notification_type,
220 type_=notification_type,
220 email_kwargs=email_kwargs
221 email_kwargs=email_kwargs
221 )
222 )
222
223
223 return comment
224 return comment
224
225
225 def delete(self, comment):
226 def delete(self, comment):
226 comment = self.__get_changeset_comment(comment)
227 comment = self.__get_changeset_comment(comment)
227 Session().delete(comment)
228 Session().delete(comment)
228
229
229 return comment
230 return comment
230
231
231 def get_comments(self, repo_id, revision=None, pull_request=None):
232 def get_comments(self, repo_id, revision=None, pull_request=None):
232 """
233 """
233 Gets general comments for either revision or pull_request.
234 Gets general comments for either revision or pull_request.
234
235
235 Returns a list, ordered by creation date.
236 Returns a list, ordered by creation date.
236 """
237 """
237 return self._get_comments(repo_id, revision=revision, pull_request=pull_request,
238 return self._get_comments(repo_id, revision=revision, pull_request=pull_request,
238 inline=False)
239 inline=False)
239
240
240 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
241 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
241 """
242 """
242 Gets inline comments for either revision or pull_request.
243 Gets inline comments for either revision or pull_request.
243
244
244 Returns a list of tuples with file path and list of comments per line number.
245 Returns a list of tuples with file path and list of comments per line number.
245 """
246 """
246 comments = self._get_comments(repo_id, revision=revision, pull_request=pull_request,
247 comments = self._get_comments(repo_id, revision=revision, pull_request=pull_request,
247 inline=True)
248 inline=True)
248
249
249 paths = defaultdict(lambda: defaultdict(list))
250 paths = defaultdict(lambda: defaultdict(list))
250 for co in comments:
251 for co in comments:
251 paths[co.f_path][co.line_no].append(co)
252 paths[co.f_path][co.line_no].append(co)
252 return paths.items()
253 return paths.items()
253
254
254 def _get_comments(self, repo_id, revision=None, pull_request=None, inline=False):
255 def _get_comments(self, repo_id, revision=None, pull_request=None, inline=False):
255 """
256 """
256 Gets comments for either revision or pull_request_id, either inline or general.
257 Gets comments for either revision or pull_request_id, either inline or general.
257 """
258 """
258 q = Session().query(ChangesetComment)
259 q = Session().query(ChangesetComment)
259
260
260 if inline:
261 if inline:
261 q = q.filter(ChangesetComment.line_no != None) \
262 q = q.filter(ChangesetComment.line_no != None) \
262 .filter(ChangesetComment.f_path != None)
263 .filter(ChangesetComment.f_path != None)
263 else:
264 else:
264 q = q.filter(ChangesetComment.line_no == None) \
265 q = q.filter(ChangesetComment.line_no == None) \
265 .filter(ChangesetComment.f_path == None)
266 .filter(ChangesetComment.f_path == None)
266
267
267 if revision is not None:
268 if revision is not None:
268 q = q.filter(ChangesetComment.revision == revision) \
269 q = q.filter(ChangesetComment.revision == revision) \
269 .filter(ChangesetComment.repo_id == repo_id)
270 .filter(ChangesetComment.repo_id == repo_id)
270 elif pull_request is not None:
271 elif pull_request is not None:
271 pull_request = self.__get_pull_request(pull_request)
272 pull_request = self.__get_pull_request(pull_request)
272 q = q.filter(ChangesetComment.pull_request == pull_request)
273 q = q.filter(ChangesetComment.pull_request == pull_request)
273 else:
274 else:
274 raise Exception('Please specify either revision or pull_request')
275 raise Exception('Please specify either revision or pull_request')
275
276
276 return q.order_by(ChangesetComment.created_on).all()
277 return q.order_by(ChangesetComment.created_on).all()
@@ -1,232 +1,233 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.model.pull_request
15 kallithea.model.pull_request
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 pull request model for Kallithea
18 pull request model for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Jun 6, 2012
22 :created_on: Jun 6, 2012
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import datetime
29 import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32
32
33 from sqlalchemy.orm import joinedload
33 from sqlalchemy.orm import joinedload
34
34
35 from kallithea.model.meta import Session
35 from kallithea.model.meta import Session
36 from kallithea.lib import helpers as h
36 from kallithea.lib import helpers as h
37 from kallithea.lib.exceptions import UserInvalidException
37 from kallithea.lib.exceptions import UserInvalidException
38 from kallithea.model import BaseModel
38 from kallithea.model import BaseModel
39 from kallithea.model.db import PullRequest, PullRequestReviewers, Notification, \
39 from kallithea.model.db import PullRequest, PullRequestReviewers, Notification, \
40 ChangesetStatus, User
40 ChangesetStatus, User
41 from kallithea.model.notification import NotificationModel
41 from kallithea.model.notification import NotificationModel
42 from kallithea.lib.utils2 import extract_mentioned_users, safe_unicode
42 from kallithea.lib.utils2 import extract_mentioned_users, safe_unicode
43
43
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47
47
48 class PullRequestModel(BaseModel):
48 class PullRequestModel(BaseModel):
49
49
50 cls = PullRequest
50 cls = PullRequest
51
51
52 def __get_pull_request(self, pull_request):
52 def __get_pull_request(self, pull_request):
53 return self._get_instance(PullRequest, pull_request)
53 return self._get_instance(PullRequest, pull_request)
54
54
55 def get_pullrequest_cnt_for_user(self, user):
55 def get_pullrequest_cnt_for_user(self, user):
56 return PullRequest.query() \
56 return PullRequest.query() \
57 .join(PullRequestReviewers) \
57 .join(PullRequestReviewers) \
58 .filter(PullRequestReviewers.user_id == user) \
58 .filter(PullRequestReviewers.user_id == user) \
59 .filter(PullRequest.status != PullRequest.STATUS_CLOSED) \
59 .filter(PullRequest.status != PullRequest.STATUS_CLOSED) \
60 .count()
60 .count()
61
61
62 def get_all(self, repo_name, from_=False, closed=False):
62 def get_all(self, repo_name, from_=False, closed=False):
63 """Get all PRs for repo.
63 """Get all PRs for repo.
64 Default is all PRs to the repo, PRs from the repo if from_.
64 Default is all PRs to the repo, PRs from the repo if from_.
65 Closed PRs are only included if closed is true."""
65 Closed PRs are only included if closed is true."""
66 repo = self._get_repo(repo_name)
66 repo = self._get_repo(repo_name)
67 q = PullRequest.query()
67 q = PullRequest.query()
68 if from_:
68 if from_:
69 q = q.filter(PullRequest.org_repo == repo)
69 q = q.filter(PullRequest.org_repo == repo)
70 else:
70 else:
71 q = q.filter(PullRequest.other_repo == repo)
71 q = q.filter(PullRequest.other_repo == repo)
72 if not closed:
72 if not closed:
73 q = q.filter(PullRequest.status != PullRequest.STATUS_CLOSED)
73 q = q.filter(PullRequest.status != PullRequest.STATUS_CLOSED)
74 return q.order_by(PullRequest.created_on.desc()).all()
74 return q.order_by(PullRequest.created_on.desc()).all()
75
75
76 def _get_valid_reviewers(self, seq):
76 def _get_valid_reviewers(self, seq):
77 """ Generate User objects from a sequence of user IDs, usernames or
77 """ Generate User objects from a sequence of user IDs, usernames or
78 User objects. Raises UserInvalidException if the DEFAULT user is
78 User objects. Raises UserInvalidException if the DEFAULT user is
79 specified, or if a given ID or username does not match any user.
79 specified, or if a given ID or username does not match any user.
80 """
80 """
81 for user_spec in seq:
81 for user_spec in seq:
82 user = self._get_user(user_spec)
82 user = self._get_user(user_spec)
83 if user is None or user.username == User.DEFAULT_USER:
83 if user is None or user.username == User.DEFAULT_USER:
84 raise UserInvalidException(user_spec)
84 raise UserInvalidException(user_spec)
85 yield user
85 yield user
86
86
87 def create(self, created_by, org_repo, org_ref, other_repo, other_ref,
87 def create(self, created_by, org_repo, org_ref, other_repo, other_ref,
88 revisions, reviewers, title, description=None):
88 revisions, reviewers, title, description=None):
89 from kallithea.model.changeset_status import ChangesetStatusModel
89 from kallithea.model.changeset_status import ChangesetStatusModel
90
90
91 created_by_user = self._get_user(created_by)
91 created_by_user = self._get_user(created_by)
92 org_repo = self._get_repo(org_repo)
92 org_repo = self._get_repo(org_repo)
93 other_repo = self._get_repo(other_repo)
93 other_repo = self._get_repo(other_repo)
94
94
95 new = PullRequest()
95 new = PullRequest()
96 new.org_repo = org_repo
96 new.org_repo = org_repo
97 new.org_ref = org_ref
97 new.org_ref = org_ref
98 new.other_repo = other_repo
98 new.other_repo = other_repo
99 new.other_ref = other_ref
99 new.other_ref = other_ref
100 new.revisions = revisions
100 new.revisions = revisions
101 new.title = title
101 new.title = title
102 new.description = description
102 new.description = description
103 new.owner = created_by_user
103 new.owner = created_by_user
104 Session().add(new)
104 Session().add(new)
105 Session().flush()
105 Session().flush()
106
106
107 #reset state to under-review
107 #reset state to under-review
108 from kallithea.model.comment import ChangesetCommentsModel
108 from kallithea.model.comment import ChangesetCommentsModel
109 comment = ChangesetCommentsModel().create(
109 comment = ChangesetCommentsModel().create(
110 text=u'',
110 text=u'',
111 repo=org_repo,
111 repo=org_repo,
112 user=new.owner,
112 user=new.owner,
113 pull_request=new,
113 pull_request=new,
114 send_email=False,
114 send_email=False,
115 status_change=ChangesetStatus.STATUS_UNDER_REVIEW,
115 status_change=ChangesetStatus.STATUS_UNDER_REVIEW,
116 )
116 )
117 ChangesetStatusModel().set_status(
117 ChangesetStatusModel().set_status(
118 org_repo,
118 org_repo,
119 ChangesetStatus.STATUS_UNDER_REVIEW,
119 ChangesetStatus.STATUS_UNDER_REVIEW,
120 new.owner,
120 new.owner,
121 comment,
121 comment,
122 pull_request=new
122 pull_request=new
123 )
123 )
124
124
125 reviewers = set(self._get_valid_reviewers(reviewers))
125 reviewers = set(self._get_valid_reviewers(reviewers))
126 mention_recipients = extract_mentioned_users(new.description)
126 mention_recipients = extract_mentioned_users(new.description)
127 self.__add_reviewers(created_by_user, new, reviewers, mention_recipients)
127 self.__add_reviewers(created_by_user, new, reviewers, mention_recipients)
128
128
129 return new
129 return new
130
130
131 def __add_reviewers(self, user, pr, reviewers, mention_recipients):
131 def __add_reviewers(self, user, pr, reviewers, mention_recipients):
132 # reviewers and mention_recipients should be sets of User objects.
132 # reviewers and mention_recipients should be sets of User objects.
133 #members
133 #members
134 for reviewer in reviewers:
134 for reviewer in reviewers:
135 reviewer = PullRequestReviewers(reviewer, pr)
135 reviewer = PullRequestReviewers(reviewer, pr)
136 Session().add(reviewer)
136 Session().add(reviewer)
137
137
138 revision_data = [(x.raw_id, x.message)
138 revision_data = [(x.raw_id, x.message)
139 for x in map(pr.org_repo.get_changeset, pr.revisions)]
139 for x in map(pr.org_repo.get_changeset, pr.revisions)]
140
140
141 #notification to reviewers
141 #notification to reviewers
142 pr_url = pr.url(canonical=True)
142 pr_url = pr.url(canonical=True)
143 threading = ['%s-pr-%s@%s' % (pr.other_repo.repo_name,
143 threading = ['%s-pr-%s@%s' % (pr.other_repo.repo_name,
144 pr.pull_request_id,
144 pr.pull_request_id,
145 h.canonical_hostname())]
145 h.canonical_hostname())]
146 subject = safe_unicode(
146 subject = safe_unicode(
147 h.link_to(
147 h.link_to(
148 _('%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s') % \
148 _('%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s') % \
149 {'user': user.username,
149 {'user': user.username,
150 'pr_title': pr.title,
150 'pr_title': pr.title,
151 'pr_nice_id': pr.nice_id()},
151 'pr_nice_id': pr.nice_id()},
152 pr_url)
152 pr_url)
153 )
153 )
154 body = pr.description
154 body = pr.description
155 _org_ref_type, org_ref_name, _org_rev = pr.org_ref.split(':')
155 _org_ref_type, org_ref_name, _org_rev = pr.org_ref.split(':')
156 _other_ref_type, other_ref_name, _other_rev = pr.other_ref.split(':')
156 _other_ref_type, other_ref_name, _other_rev = pr.other_ref.split(':')
157 email_kwargs = {
157 email_kwargs = {
158 'pr_title': pr.title,
158 'pr_title': pr.title,
159 'pr_user_created': user.full_name_and_username,
159 'pr_user_created': user.full_name_and_username,
160 'pr_repo_url': h.canonical_url('summary_home', repo_name=pr.other_repo.repo_name),
160 'pr_repo_url': h.canonical_url('summary_home', repo_name=pr.other_repo.repo_name),
161 'pr_url': pr_url,
161 'pr_url': pr_url,
162 'pr_revisions': revision_data,
162 'pr_revisions': revision_data,
163 'repo_name': pr.other_repo.repo_name,
163 'repo_name': pr.other_repo.repo_name,
164 'org_repo_name': pr.org_repo.repo_name,
164 'org_repo_name': pr.org_repo.repo_name,
165 'pr_nice_id': pr.nice_id(),
165 'pr_nice_id': pr.nice_id(),
166 'pr_target_repo': h.canonical_url('summary_home',
166 'pr_target_repo': h.canonical_url('summary_home',
167 repo_name=pr.other_repo.repo_name),
167 repo_name=pr.other_repo.repo_name),
168 'pr_target_branch': other_ref_name,
168 'pr_target_branch': other_ref_name,
169 'pr_source_repo': h.canonical_url('summary_home',
169 'pr_source_repo': h.canonical_url('summary_home',
170 repo_name=pr.org_repo.repo_name),
170 repo_name=pr.org_repo.repo_name),
171 'pr_source_branch': org_ref_name,
171 'pr_source_branch': org_ref_name,
172 'pr_owner': pr.owner,
172 'pr_owner': pr.owner,
173 'pr_username': user.username,
173 'pr_username': user.username,
174 'threading': threading,
174 'threading': threading,
175 'is_mention': False,
175 'is_mention': False,
176 }
176 }
177 if reviewers:
177 if reviewers:
178 NotificationModel().create(created_by=user, subject=subject, body=body,
178 NotificationModel().create(created_by=user, subject=subject, body=body,
179 recipients=reviewers,
179 recipients=reviewers,
180 type_=Notification.TYPE_PULL_REQUEST,
180 type_=Notification.TYPE_PULL_REQUEST,
181 email_kwargs=email_kwargs)
181 email_kwargs=email_kwargs)
182
182
183 if mention_recipients:
183 if mention_recipients:
184 mention_recipients.difference_update(reviewers)
184 mention_recipients.difference_update(reviewers)
185 if mention_recipients:
185 if mention_recipients:
186 email_kwargs['is_mention'] = True
186 email_kwargs['is_mention'] = True
187 subject = _('[Mention]') + ' ' + subject
187 subject = _('[Mention]') + ' ' + subject
188 # FIXME: this subject is wrong and unused!
188 NotificationModel().create(created_by=user, subject=subject, body=body,
189 NotificationModel().create(created_by=user, subject=subject, body=body,
189 recipients=mention_recipients,
190 recipients=mention_recipients,
190 type_=Notification.TYPE_PULL_REQUEST,
191 type_=Notification.TYPE_PULL_REQUEST,
191 email_kwargs=email_kwargs)
192 email_kwargs=email_kwargs)
192
193
193 def mention_from_description(self, user, pr, old_description=''):
194 def mention_from_description(self, user, pr, old_description=''):
194 mention_recipients = (extract_mentioned_users(pr.description) -
195 mention_recipients = (extract_mentioned_users(pr.description) -
195 extract_mentioned_users(old_description))
196 extract_mentioned_users(old_description))
196
197
197 log.debug("Mentioning %s", mention_recipients)
198 log.debug("Mentioning %s", mention_recipients)
198 self.__add_reviewers(user, pr, set(), mention_recipients)
199 self.__add_reviewers(user, pr, set(), mention_recipients)
199
200
200 def update_reviewers(self, user, pull_request, reviewers_ids):
201 def update_reviewers(self, user, pull_request, reviewers_ids):
201 reviewers_ids = set(reviewers_ids)
202 reviewers_ids = set(reviewers_ids)
202 pull_request = self.__get_pull_request(pull_request)
203 pull_request = self.__get_pull_request(pull_request)
203 current_reviewers = PullRequestReviewers.query() \
204 current_reviewers = PullRequestReviewers.query() \
204 .options(joinedload('user')) \
205 .options(joinedload('user')) \
205 .filter_by(pull_request=pull_request) \
206 .filter_by(pull_request=pull_request) \
206 .all()
207 .all()
207 current_reviewer_users = set(x.user for x in current_reviewers)
208 current_reviewer_users = set(x.user for x in current_reviewers)
208 new_reviewer_users = set(self._get_valid_reviewers(reviewers_ids))
209 new_reviewer_users = set(self._get_valid_reviewers(reviewers_ids))
209
210
210 to_add = new_reviewer_users - current_reviewer_users
211 to_add = new_reviewer_users - current_reviewer_users
211 to_remove = current_reviewer_users - new_reviewer_users
212 to_remove = current_reviewer_users - new_reviewer_users
212
213
213 if not to_add and not to_remove:
214 if not to_add and not to_remove:
214 return # all done
215 return # all done
215
216
216 log.debug("Adding %s reviewers", to_add)
217 log.debug("Adding %s reviewers", to_add)
217 self.__add_reviewers(user, pull_request, to_add, set())
218 self.__add_reviewers(user, pull_request, to_add, set())
218
219
219 log.debug("Removing %s reviewers", to_remove)
220 log.debug("Removing %s reviewers", to_remove)
220 for prr in current_reviewers:
221 for prr in current_reviewers:
221 if prr.user in to_remove:
222 if prr.user in to_remove:
222 Session().delete(prr)
223 Session().delete(prr)
223
224
224 def delete(self, pull_request):
225 def delete(self, pull_request):
225 pull_request = self.__get_pull_request(pull_request)
226 pull_request = self.__get_pull_request(pull_request)
226 Session().delete(pull_request)
227 Session().delete(pull_request)
227
228
228 def close_pull_request(self, pull_request):
229 def close_pull_request(self, pull_request):
229 pull_request = self.__get_pull_request(pull_request)
230 pull_request = self.__get_pull_request(pull_request)
230 pull_request.status = PullRequest.STATUS_CLOSED
231 pull_request.status = PullRequest.STATUS_CLOSED
231 pull_request.updated_on = datetime.datetime.now()
232 pull_request.updated_on = datetime.datetime.now()
232 Session().add(pull_request)
233 Session().add(pull_request)
@@ -1,637 +1,813 b''
1 <html><body>
1 <html><body>
2
2
3
3
4 <h1>cs_comment, is_mention=False, status_change=None</h1>
4 <h1>cs_comment, is_mention=False, status_change=None</h1>
5 <pre>
5 <pre>
6
6
7 From: u1
7 From: u1
8 To: u2@example.com
8 To: u2@example.com
9 Subject: [Comment] repo/name changeset cafe1234 on brunch
9 Subject: [Comment] repo/name changeset cafe1234 on brunch
10
10
11 --------------------
11 --------------------
12
12
13
13
14 Comment from Opinionated User (jsmith) on repo_target changeset cafe1234c0ff:
14 Comment from Opinionated User (jsmith) on repo_target changeset cafe1234c0ff:
15 This is the new comment.
15 This is the new comment.
16
16
17 - and here it ends indented.
17 - and here it ends indented.
18
18
19
19
20 URL: http://comment.org
20 URL: http://comment.org
21
21
22 Changeset: cafe1234c0ff
22 Changeset: cafe1234c0ff
23 Description:
23 Description:
24 This changeset did something clever which is hard to explain
24 This changeset did something clever which is hard to explain
25
25
26
26
27 --
27 --
28 This is an automatic notification. Don't reply to this mail.
28 This is an automatic notification. Don't reply to this mail.
29
29
30 --------------------</pre>
30 --------------------</pre>
31
31
32
32
33
33
34 <p>Comment from Opinionated User (jsmith) on repo_target changeset cafe1234c0ff:</p>
34 <p>Comment from Opinionated User (jsmith) on repo_target changeset cafe1234c0ff:</p>
35 <p><div class="formatted-fixed">This is the new comment.
35 <p><div class="formatted-fixed">This is the new comment.
36
36
37 - and here it ends indented.</div></p>
37 - and here it ends indented.</div></p>
38
38
39
39
40 <p>URL: <a href="http://comment.org">http://comment.org</a></p>
40 <p>URL: <a href="http://comment.org">http://comment.org</a></p>
41
41
42 <p>Changeset: cafe1234c0ff</p>
42 <p>Changeset: cafe1234c0ff</p>
43 <p>Description:<br/>
43 <p>Description:<br/>
44 This changeset did something clever which is hard to explain
44 This changeset did something clever which is hard to explain
45 </p>
45 </p>
46
46
47
47
48 <br/>
48 <br/>
49 <br/>
49 <br/>
50 -- <br/>
50 -- <br/>
51 This is an automatic notification. Don&#39;t reply to this mail.
51 This is an automatic notification. Don&#39;t reply to this mail.
52
52
53 <pre>--------------------</pre>
53 <pre>--------------------</pre>
54
54
55
55
56 <h1>cs_comment, is_mention=True, status_change=None</h1>
56 <h1>cs_comment, is_mention=True, status_change=None</h1>
57 <pre>
57 <pre>
58
58
59 From: u1
59 From: u1
60 To: u2@example.com
60 To: u2@example.com
61 Subject: [Comment] repo/name changeset cafe1234 on brunch
61 Subject: [Comment] repo/name changeset cafe1234 on brunch
62
62
63 --------------------
63 --------------------
64
64
65
65
66 Comment from Opinionated User (jsmith) on repo_target changeset cafe1234c0ff mentioned you:
66 Comment from Opinionated User (jsmith) on repo_target changeset cafe1234c0ff mentioned you:
67 This is the new comment.
67 This is the new comment.
68
68
69 - and here it ends indented.
69 - and here it ends indented.
70
70
71
71
72 URL: http://comment.org
72 URL: http://comment.org
73
73
74 Changeset: cafe1234c0ff
74 Changeset: cafe1234c0ff
75 Description:
75 Description:
76 This changeset did something clever which is hard to explain
76 This changeset did something clever which is hard to explain
77
77
78
78
79 --
79 --
80 This is an automatic notification. Don't reply to this mail.
80 This is an automatic notification. Don't reply to this mail.
81
81
82 --------------------</pre>
82 --------------------</pre>
83
83
84
84
85
85
86 <p>Comment from Opinionated User (jsmith) on repo_target changeset cafe1234c0ff mentioned you:</p>
86 <p>Comment from Opinionated User (jsmith) on repo_target changeset cafe1234c0ff mentioned you:</p>
87 <p><div class="formatted-fixed">This is the new comment.
87 <p><div class="formatted-fixed">This is the new comment.
88
88
89 - and here it ends indented.</div></p>
89 - and here it ends indented.</div></p>
90
90
91
91
92 <p>URL: <a href="http://comment.org">http://comment.org</a></p>
92 <p>URL: <a href="http://comment.org">http://comment.org</a></p>
93
93
94 <p>Changeset: cafe1234c0ff</p>
94 <p>Changeset: cafe1234c0ff</p>
95 <p>Description:<br/>
95 <p>Description:<br/>
96 This changeset did something clever which is hard to explain
96 This changeset did something clever which is hard to explain
97 </p>
97 </p>
98
98
99
99
100 <br/>
100 <br/>
101 <br/>
101 <br/>
102 -- <br/>
102 -- <br/>
103 This is an automatic notification. Don&#39;t reply to this mail.
103 This is an automatic notification. Don&#39;t reply to this mail.
104
104
105 <pre>--------------------</pre>
105 <pre>--------------------</pre>
106
106
107
107
108 <h1>cs_comment, is_mention=False, status_change='Approved'</h1>
108 <h1>cs_comment, is_mention=False, status_change='Approved'</h1>
109 <pre>
109 <pre>
110
110
111 From: u1
111 From: u1
112 To: u2@example.com
112 To: u2@example.com
113 Subject: [Approved: Comment] repo/name changeset cafe1234 on brunch
113 Subject: [Approved: Comment] repo/name changeset cafe1234 on brunch
114
114
115 --------------------
115 --------------------
116
116
117
117
118 Comment from Opinionated User (jsmith) on repo_target changeset cafe1234c0ff:
118 Comment from Opinionated User (jsmith) on repo_target changeset cafe1234c0ff:
119 This is the new comment.
119 This is the new comment.
120
120
121 - and here it ends indented.
121 - and here it ends indented.
122
122
123 The changeset status was changed to: Approved
123 The changeset status was changed to: Approved
124
124
125 URL: http://comment.org
125 URL: http://comment.org
126
126
127 Changeset: cafe1234c0ff
127 Changeset: cafe1234c0ff
128 Description:
128 Description:
129 This changeset did something clever which is hard to explain
129 This changeset did something clever which is hard to explain
130
130
131
131
132 --
132 --
133 This is an automatic notification. Don't reply to this mail.
133 This is an automatic notification. Don't reply to this mail.
134
134
135 --------------------</pre>
135 --------------------</pre>
136
136
137
137
138
138
139 <p>Comment from Opinionated User (jsmith) on repo_target changeset cafe1234c0ff:</p>
139 <p>Comment from Opinionated User (jsmith) on repo_target changeset cafe1234c0ff:</p>
140 <p><div class="formatted-fixed">This is the new comment.
140 <p><div class="formatted-fixed">This is the new comment.
141
141
142 - and here it ends indented.</div></p>
142 - and here it ends indented.</div></p>
143
143
144 <p>The changeset status was changed to: <b>Approved</b></p>
144 <p>The changeset status was changed to: <b>Approved</b></p>
145
145
146 <p>URL: <a href="http://comment.org">http://comment.org</a></p>
146 <p>URL: <a href="http://comment.org">http://comment.org</a></p>
147
147
148 <p>Changeset: cafe1234c0ff</p>
148 <p>Changeset: cafe1234c0ff</p>
149 <p>Description:<br/>
149 <p>Description:<br/>
150 This changeset did something clever which is hard to explain
150 This changeset did something clever which is hard to explain
151 </p>
151 </p>
152
152
153
153
154 <br/>
154 <br/>
155 <br/>
155 <br/>
156 -- <br/>
156 -- <br/>
157 This is an automatic notification. Don&#39;t reply to this mail.
157 This is an automatic notification. Don&#39;t reply to this mail.
158
158
159 <pre>--------------------</pre>
159 <pre>--------------------</pre>
160
160
161
161
162 <h1>cs_comment, is_mention=True, status_change='Approved'</h1>
162 <h1>cs_comment, is_mention=True, status_change='Approved'</h1>
163 <pre>
163 <pre>
164
164
165 From: u1
165 From: u1
166 To: u2@example.com
166 To: u2@example.com
167 Subject: [Approved: Comment] repo/name changeset cafe1234 on brunch
167 Subject: [Approved: Comment] repo/name changeset cafe1234 on brunch
168
168
169 --------------------
169 --------------------
170
170
171
171
172 Comment from Opinionated User (jsmith) on repo_target changeset cafe1234c0ff mentioned you:
172 Comment from Opinionated User (jsmith) on repo_target changeset cafe1234c0ff mentioned you:
173 This is the new comment.
173 This is the new comment.
174
174
175 - and here it ends indented.
175 - and here it ends indented.
176
176
177 The changeset status was changed to: Approved
177 The changeset status was changed to: Approved
178
178
179 URL: http://comment.org
179 URL: http://comment.org
180
180
181 Changeset: cafe1234c0ff
181 Changeset: cafe1234c0ff
182 Description:
182 Description:
183 This changeset did something clever which is hard to explain
183 This changeset did something clever which is hard to explain
184
184
185
185
186 --
186 --
187 This is an automatic notification. Don't reply to this mail.
187 This is an automatic notification. Don't reply to this mail.
188
188
189 --------------------</pre>
189 --------------------</pre>
190
190
191
191
192
192
193 <p>Comment from Opinionated User (jsmith) on repo_target changeset cafe1234c0ff mentioned you:</p>
193 <p>Comment from Opinionated User (jsmith) on repo_target changeset cafe1234c0ff mentioned you:</p>
194 <p><div class="formatted-fixed">This is the new comment.
194 <p><div class="formatted-fixed">This is the new comment.
195
195
196 - and here it ends indented.</div></p>
196 - and here it ends indented.</div></p>
197
197
198 <p>The changeset status was changed to: <b>Approved</b></p>
198 <p>The changeset status was changed to: <b>Approved</b></p>
199
199
200 <p>URL: <a href="http://comment.org">http://comment.org</a></p>
200 <p>URL: <a href="http://comment.org">http://comment.org</a></p>
201
201
202 <p>Changeset: cafe1234c0ff</p>
202 <p>Changeset: cafe1234c0ff</p>
203 <p>Description:<br/>
203 <p>Description:<br/>
204 This changeset did something clever which is hard to explain
204 This changeset did something clever which is hard to explain
205 </p>
205 </p>
206
206
207
207
208 <br/>
208 <br/>
209 <br/>
209 <br/>
210 -- <br/>
210 -- <br/>
211 This is an automatic notification. Don&#39;t reply to this mail.
211 This is an automatic notification. Don&#39;t reply to this mail.
212
212
213 <pre>--------------------</pre>
213 <pre>--------------------</pre>
214
214
215
215
216 <h1>message</h1>
216 <h1>message</h1>
217 <pre>
217 <pre>
218
218
219 From: u1
219 From: u1
220 To: u2@example.com
220 To: u2@example.com
221 Subject: Test Message
221 Subject: Test Message
222
222
223 --------------------
223 --------------------
224
224
225
225
226 This is the body of the test message
226 This is the body of the test message
227 - nothing interesting here except indentation.
227 - nothing interesting here except indentation.
228
228
229
229
230 --
230 --
231 This is an automatic notification. Don't reply to this mail.
231 This is an automatic notification. Don't reply to this mail.
232
232
233 --------------------</pre>
233 --------------------</pre>
234
234
235
235
236
236
237 <div class="formatted-fixed">This is the body of the test message
237 <div class="formatted-fixed">This is the body of the test message
238 - nothing interesting here except indentation.</div>
238 - nothing interesting here except indentation.</div>
239
239
240
240
241 <br/>
241 <br/>
242 <br/>
242 <br/>
243 -- <br/>
243 -- <br/>
244 This is an automatic notification. Don&#39;t reply to this mail.
244 This is an automatic notification. Don&#39;t reply to this mail.
245
245
246 <pre>--------------------</pre>
246 <pre>--------------------</pre>
247
247
248
248
249 <h1>registration</h1>
249 <h1>registration</h1>
250 <pre>
250 <pre>
251
251
252 From: u1
252 From: u1
253 To: u2@example.com
253 To: u2@example.com
254 Subject: New user newbie registered
254 Subject: New user newbie registered
255
255
256 --------------------
256 --------------------
257
257
258
258
259 Registration body
259 Registration body
260
260
261 View this user here: http://newbie.org
261 View this user here: http://newbie.org
262
262
263
263
264 --
264 --
265 This is an automatic notification. Don't reply to this mail.
265 This is an automatic notification. Don't reply to this mail.
266
266
267 --------------------</pre>
267 --------------------</pre>
268
268
269
269
270
270
271 <div class="formatted-fixed">Registration body</div>
271 <div class="formatted-fixed">Registration body</div>
272
272
273 View this user here: <a href="http://newbie.org">http://newbie.org</a>
273 View this user here: <a href="http://newbie.org">http://newbie.org</a>
274
274
275
275
276 <br/>
276 <br/>
277 <br/>
277 <br/>
278 -- <br/>
278 -- <br/>
279 This is an automatic notification. Don&#39;t reply to this mail.
279 This is an automatic notification. Don&#39;t reply to this mail.
280
280
281 <pre>--------------------</pre>
281 <pre>--------------------</pre>
282
282
283
283
284 <h1>pull_request, is_mention=False</h1>
284 <h1>pull_request, is_mention=False</h1>
285 <pre>
285 <pre>
286
286
287 From: u1
287 From: u1
288 To: u2@example.com
288 To: u2@example.com
289 Subject: [Added] repo/name pull request #7 from devbranch
289 Subject: [Added] repo/name pull request #7 from devbranch
290
290
291 --------------------
291 --------------------
292
292
293
293
294 Requesting User (root) requested your review of repo/name pull request "The Title"
294 Requesting User (root) requested your review of repo/name pull request "The Title"
295
295
296 URL: http://pr.org/7
296 URL: http://pr.org/7
297
297
298 Description:
298 Description:
299 This PR is awesome because it does stuff
299 This PR is awesome because it does stuff
300 - please approve indented!
300 - please approve indented!
301
301
302 Changesets:
302 Changesets:
303 123abc123abc: http://changeset_home/?repo_name=repo_org&amp;revision=123abc123abc123abc123abc123abc123abc123abc
303 123abc123abc: http://changeset_home/?repo_name=repo_org&amp;revision=123abc123abc123abc123abc123abc123abc123abc
304 Introduce one and two
304 Introduce one and two
305
305
306 and that's it
306 and that's it
307
307
308 567fed567fed: http://changeset_home/?repo_name=repo_org&amp;revision=567fed567fed567fed567fed567fed567fed567fed
308 567fed567fed: http://changeset_home/?repo_name=repo_org&amp;revision=567fed567fed567fed567fed567fed567fed567fed
309 Make one plus two equal tree
309 Make one plus two equal tree
310
310
311
311
312
312
313 --
313 --
314 This is an automatic notification. Don't reply to this mail.
314 This is an automatic notification. Don't reply to this mail.
315
315
316 --------------------</pre>
316 --------------------</pre>
317
317
318
318
319
319
320 <p>Requesting User (root) requested your review of repo/name pull request &#34;The Title&#34;</p>
320 <p>Requesting User (root) requested your review of repo/name pull request &#34;The Title&#34;</p>
321
321
322 <p>URL: <a href="http://pr.org/7">http://pr.org/7</a></p>
322 <p>URL: <a href="http://pr.org/7">http://pr.org/7</a></p>
323
323
324 <p>Description:</p>
324 <p>Description:</p>
325 <p style="white-space: pre-wrap; font-family: monospace"><div class="formatted-fixed">This PR is awesome because it does stuff
325 <p style="white-space: pre-wrap; font-family: monospace"><div class="formatted-fixed">This PR is awesome because it does stuff
326 - please approve indented!</div></p>
326 - please approve indented!</div></p>
327
327
328 <p>Changesets:</p>
328 <p>Changesets:</p>
329 <p style="white-space: pre-wrap">
329 <p style="white-space: pre-wrap">
330 <i><a href="http://changeset_home/?repo_name=repo_org&amp;revision=123abc123abc123abc123abc123abc123abc123abc">123abc123abc</a></i>:
330 <i><a href="http://changeset_home/?repo_name=repo_org&amp;revision=123abc123abc123abc123abc123abc123abc123abc">123abc123abc</a></i>:
331 Introduce one and two
331 Introduce one and two
332
332
333 and that&#39;s it
333 and that&#39;s it
334
334
335 <i><a href="http://changeset_home/?repo_name=repo_org&amp;revision=567fed567fed567fed567fed567fed567fed567fed">567fed567fed</a></i>:
335 <i><a href="http://changeset_home/?repo_name=repo_org&amp;revision=567fed567fed567fed567fed567fed567fed567fed">567fed567fed</a></i>:
336 Make one plus two equal tree
336 Make one plus two equal tree
337
337
338 </p>
338 </p>
339
339
340
340
341 <br/>
341 <br/>
342 <br/>
342 <br/>
343 -- <br/>
343 -- <br/>
344 This is an automatic notification. Don&#39;t reply to this mail.
344 This is an automatic notification. Don&#39;t reply to this mail.
345
345
346 <pre>--------------------</pre>
346 <pre>--------------------</pre>
347
347
348
348
349 <h1>pull_request, is_mention=True</h1>
349 <h1>pull_request, is_mention=True</h1>
350 <pre>
350 <pre>
351
351
352 From: u1
352 From: u1
353 To: u2@example.com
353 To: u2@example.com
354 Subject: [Added] repo/name pull request #7 from devbranch
354 Subject: [Added] repo/name pull request #7 from devbranch
355
355
356 --------------------
356 --------------------
357
357
358
358
359 Requesting User (root) mentioned you on repo/name pull request "The Title"
359 Requesting User (root) mentioned you on repo/name pull request "The Title"
360
360
361 URL: http://pr.org/7
361 URL: http://pr.org/7
362
362
363 Description:
363 Description:
364 This PR is awesome because it does stuff
364 This PR is awesome because it does stuff
365 - please approve indented!
365 - please approve indented!
366
366
367 Changesets:
367 Changesets:
368 123abc123abc: http://changeset_home/?repo_name=repo_org&amp;revision=123abc123abc123abc123abc123abc123abc123abc
368 123abc123abc: http://changeset_home/?repo_name=repo_org&amp;revision=123abc123abc123abc123abc123abc123abc123abc
369 Introduce one and two
369 Introduce one and two
370
370
371 and that's it
371 and that's it
372
372
373 567fed567fed: http://changeset_home/?repo_name=repo_org&amp;revision=567fed567fed567fed567fed567fed567fed567fed
373 567fed567fed: http://changeset_home/?repo_name=repo_org&amp;revision=567fed567fed567fed567fed567fed567fed567fed
374 Make one plus two equal tree
374 Make one plus two equal tree
375
375
376
376
377
377
378 --
378 --
379 This is an automatic notification. Don't reply to this mail.
379 This is an automatic notification. Don't reply to this mail.
380
380
381 --------------------</pre>
381 --------------------</pre>
382
382
383
383
384
384
385 <p>Requesting User (root) mentioned you on repo/name pull request &#34;The Title&#34;</p>
385 <p>Requesting User (root) mentioned you on repo/name pull request &#34;The Title&#34;</p>
386
386
387 <p>URL: <a href="http://pr.org/7">http://pr.org/7</a></p>
387 <p>URL: <a href="http://pr.org/7">http://pr.org/7</a></p>
388
388
389 <p>Description:</p>
389 <p>Description:</p>
390 <p style="white-space: pre-wrap; font-family: monospace"><div class="formatted-fixed">This PR is awesome because it does stuff
390 <p style="white-space: pre-wrap; font-family: monospace"><div class="formatted-fixed">This PR is awesome because it does stuff
391 - please approve indented!</div></p>
391 - please approve indented!</div></p>
392
392
393 <p>Changesets:</p>
393 <p>Changesets:</p>
394 <p style="white-space: pre-wrap">
394 <p style="white-space: pre-wrap">
395 <i><a href="http://changeset_home/?repo_name=repo_org&amp;revision=123abc123abc123abc123abc123abc123abc123abc">123abc123abc</a></i>:
395 <i><a href="http://changeset_home/?repo_name=repo_org&amp;revision=123abc123abc123abc123abc123abc123abc123abc">123abc123abc</a></i>:
396 Introduce one and two
396 Introduce one and two
397
397
398 and that&#39;s it
398 and that&#39;s it
399
399
400 <i><a href="http://changeset_home/?repo_name=repo_org&amp;revision=567fed567fed567fed567fed567fed567fed567fed">567fed567fed</a></i>:
400 <i><a href="http://changeset_home/?repo_name=repo_org&amp;revision=567fed567fed567fed567fed567fed567fed567fed">567fed567fed</a></i>:
401 Make one plus two equal tree
401 Make one plus two equal tree
402
402
403 </p>
403 </p>
404
404
405
405
406 <br/>
406 <br/>
407 <br/>
407 <br/>
408 -- <br/>
408 -- <br/>
409 This is an automatic notification. Don&#39;t reply to this mail.
409 This is an automatic notification. Don&#39;t reply to this mail.
410
410
411 <pre>--------------------</pre>
411 <pre>--------------------</pre>
412
412
413
413
414 <h1>pull_request_comment, status_change=None, closing_pr=False</h1>
414 <h1>pull_request_comment, is_mention=False, status_change=None, closing_pr=False</h1>
415 <pre>
415 <pre>
416
416
417 From: u1
417 From: u1
418 To: u2@example.com
418 To: u2@example.com
419 Subject: [Comment] repo/name pull request #7 from devbranch
419 Subject: [Comment] repo/name pull request #7 from devbranch
420
420
421 --------------------
421 --------------------
422
422
423
423
424 Comment from Opinionated User (jsmith) on repo/name pull request "The Title":
424 Comment from Opinionated User (jsmith) on repo/name pull request "The Title":
425 Me too!
425 Me too!
426
426
427 - and indented on second line
427 - and indented on second line
428
428
429
429
430 URL: http://pr.org/comment
430 URL: http://pr.org/comment
431
431
432
432
433 --
433 --
434 This is an automatic notification. Don't reply to this mail.
434 This is an automatic notification. Don't reply to this mail.
435
435
436 --------------------</pre>
436 --------------------</pre>
437
437
438
438
439
439
440 <p>Comment from Opinionated User (jsmith) on repo/name pull request &#34;The Title&#34;:</p>
440 <p>Comment from Opinionated User (jsmith) on repo/name pull request &#34;The Title&#34;:</p>
441 <p><div class="formatted-fixed">Me too!
441 <p><div class="formatted-fixed">Me too!
442
442
443 - and indented on second line</div></p>
443 - and indented on second line</div></p>
444
444
445
445
446 <p>URL: <a href="http://pr.org/comment">http://pr.org/comment</a></p>
446 <p>URL: <a href="http://pr.org/comment">http://pr.org/comment</a></p>
447
447
448
448
449 <br/>
449 <br/>
450 <br/>
450 <br/>
451 -- <br/>
451 -- <br/>
452 This is an automatic notification. Don&#39;t reply to this mail.
452 This is an automatic notification. Don&#39;t reply to this mail.
453
453
454 <pre>--------------------</pre>
454 <pre>--------------------</pre>
455
455
456
456
457 <h1>pull_request_comment, status_change='Under Review', closing_pr=False</h1>
457 <h1>pull_request_comment, is_mention=True, status_change=None, closing_pr=False</h1>
458 <pre>
459
460 From: u1
461 To: u2@example.com
462 Subject: [Comment] repo/name pull request #7 from devbranch
463
464 --------------------
465
466
467 Comment from Opinionated User (jsmith) on repo/name pull request "The Title":
468 Me too!
469
470 - and indented on second line
471
472
473 URL: http://pr.org/comment
474
475
476 --
477 This is an automatic notification. Don't reply to this mail.
478
479 --------------------</pre>
480
481
482
483 <p>Comment from Opinionated User (jsmith) on repo/name pull request &#34;The Title&#34;:</p>
484 <p><div class="formatted-fixed">Me too!
485
486 - and indented on second line</div></p>
487
488
489 <p>URL: <a href="http://pr.org/comment">http://pr.org/comment</a></p>
490
491
492 <br/>
493 <br/>
494 -- <br/>
495 This is an automatic notification. Don&#39;t reply to this mail.
496
497 <pre>--------------------</pre>
498
499
500 <h1>pull_request_comment, is_mention=False, status_change='Under Review', closing_pr=False</h1>
458 <pre>
501 <pre>
459
502
460 From: u1
503 From: u1
461 To: u2@example.com
504 To: u2@example.com
462 Subject: [Under Review: Comment] repo/name pull request #7 from devbranch
505 Subject: [Under Review: Comment] repo/name pull request #7 from devbranch
463
506
464 --------------------
507 --------------------
465
508
466
509
467 Comment from Opinionated User (jsmith) on repo/name pull request "The Title":
510 Comment from Opinionated User (jsmith) on repo/name pull request "The Title":
468 Me too!
511 Me too!
469
512
470 - and indented on second line
513 - and indented on second line
471
514
472 The comment was made with status: Under Review
515 The comment was made with status: Under Review
473
516
474 URL: http://pr.org/comment
517 URL: http://pr.org/comment
475
518
476
519
477 --
520 --
478 This is an automatic notification. Don't reply to this mail.
521 This is an automatic notification. Don't reply to this mail.
479
522
480 --------------------</pre>
523 --------------------</pre>
481
524
482
525
483
526
484 <p>Comment from Opinionated User (jsmith) on repo/name pull request &#34;The Title&#34;:</p>
527 <p>Comment from Opinionated User (jsmith) on repo/name pull request &#34;The Title&#34;:</p>
485 <p><div class="formatted-fixed">Me too!
528 <p><div class="formatted-fixed">Me too!
486
529
487 - and indented on second line</div></p>
530 - and indented on second line</div></p>
488
531
489 <p>The comment was made with status: <b>Under Review</b></p>
532 <p>The comment was made with status: <b>Under Review</b></p>
490
533
491 <p>URL: <a href="http://pr.org/comment">http://pr.org/comment</a></p>
534 <p>URL: <a href="http://pr.org/comment">http://pr.org/comment</a></p>
492
535
493
536
494 <br/>
537 <br/>
495 <br/>
538 <br/>
496 -- <br/>
539 -- <br/>
497 This is an automatic notification. Don&#39;t reply to this mail.
540 This is an automatic notification. Don&#39;t reply to this mail.
498
541
499 <pre>--------------------</pre>
542 <pre>--------------------</pre>
500
543
501
544
502 <h1>pull_request_comment, status_change=None, closing_pr=True</h1>
545 <h1>pull_request_comment, is_mention=True, status_change='Under Review', closing_pr=False</h1>
546 <pre>
547
548 From: u1
549 To: u2@example.com
550 Subject: [Under Review: Comment] repo/name pull request #7 from devbranch
551
552 --------------------
553
554
555 Comment from Opinionated User (jsmith) on repo/name pull request "The Title":
556 Me too!
557
558 - and indented on second line
559
560 The comment was made with status: Under Review
561
562 URL: http://pr.org/comment
563
564
565 --
566 This is an automatic notification. Don't reply to this mail.
567
568 --------------------</pre>
569
570
571
572 <p>Comment from Opinionated User (jsmith) on repo/name pull request &#34;The Title&#34;:</p>
573 <p><div class="formatted-fixed">Me too!
574
575 - and indented on second line</div></p>
576
577 <p>The comment was made with status: <b>Under Review</b></p>
578
579 <p>URL: <a href="http://pr.org/comment">http://pr.org/comment</a></p>
580
581
582 <br/>
583 <br/>
584 -- <br/>
585 This is an automatic notification. Don&#39;t reply to this mail.
586
587 <pre>--------------------</pre>
588
589
590 <h1>pull_request_comment, is_mention=False, status_change=None, closing_pr=True</h1>
503 <pre>
591 <pre>
504
592
505 From: u1
593 From: u1
506 To: u2@example.com
594 To: u2@example.com
507 Subject: [Closing: Comment] repo/name pull request #7 from devbranch
595 Subject: [Closing: Comment] repo/name pull request #7 from devbranch
508
596
509 --------------------
597 --------------------
510
598
511
599
512 Comment from Opinionated User (jsmith) on repo/name pull request "The Title":
600 Comment from Opinionated User (jsmith) on repo/name pull request "The Title":
513 Me too!
601 Me too!
514
602
515 - and indented on second line
603 - and indented on second line
516
604
517
605
518 URL: http://pr.org/comment
606 URL: http://pr.org/comment
519
607
520
608
521 --
609 --
522 This is an automatic notification. Don't reply to this mail.
610 This is an automatic notification. Don't reply to this mail.
523
611
524 --------------------</pre>
612 --------------------</pre>
525
613
526
614
527
615
528 <p>Comment from Opinionated User (jsmith) on repo/name pull request &#34;The Title&#34;:</p>
616 <p>Comment from Opinionated User (jsmith) on repo/name pull request &#34;The Title&#34;:</p>
529 <p><div class="formatted-fixed">Me too!
617 <p><div class="formatted-fixed">Me too!
530
618
531 - and indented on second line</div></p>
619 - and indented on second line</div></p>
532
620
533
621
534 <p>URL: <a href="http://pr.org/comment">http://pr.org/comment</a></p>
622 <p>URL: <a href="http://pr.org/comment">http://pr.org/comment</a></p>
535
623
536
624
537 <br/>
625 <br/>
538 <br/>
626 <br/>
539 -- <br/>
627 -- <br/>
540 This is an automatic notification. Don&#39;t reply to this mail.
628 This is an automatic notification. Don&#39;t reply to this mail.
541
629
542 <pre>--------------------</pre>
630 <pre>--------------------</pre>
543
631
544
632
545 <h1>pull_request_comment, status_change='Under Review', closing_pr=True</h1>
633 <h1>pull_request_comment, is_mention=True, status_change=None, closing_pr=True</h1>
634 <pre>
635
636 From: u1
637 To: u2@example.com
638 Subject: [Closing: Comment] repo/name pull request #7 from devbranch
639
640 --------------------
641
642
643 Comment from Opinionated User (jsmith) on repo/name pull request "The Title":
644 Me too!
645
646 - and indented on second line
647
648
649 URL: http://pr.org/comment
650
651
652 --
653 This is an automatic notification. Don't reply to this mail.
654
655 --------------------</pre>
656
657
658
659 <p>Comment from Opinionated User (jsmith) on repo/name pull request &#34;The Title&#34;:</p>
660 <p><div class="formatted-fixed">Me too!
661
662 - and indented on second line</div></p>
663
664
665 <p>URL: <a href="http://pr.org/comment">http://pr.org/comment</a></p>
666
667
668 <br/>
669 <br/>
670 -- <br/>
671 This is an automatic notification. Don&#39;t reply to this mail.
672
673 <pre>--------------------</pre>
674
675
676 <h1>pull_request_comment, is_mention=False, status_change='Under Review', closing_pr=True</h1>
677 <pre>
678
679 From: u1
680 To: u2@example.com
681 Subject: [Under Review, Closing: Comment] repo/name pull request #7 from devbranch
682
683 --------------------
684
685
686 Comment from Opinionated User (jsmith) on repo/name pull request "The Title":
687 Me too!
688
689 - and indented on second line
690
691 The comment closed the pull request with status: Under Review
692
693 URL: http://pr.org/comment
694
695
696 --
697 This is an automatic notification. Don't reply to this mail.
698
699 --------------------</pre>
700
701
702
703 <p>Comment from Opinionated User (jsmith) on repo/name pull request &#34;The Title&#34;:</p>
704 <p><div class="formatted-fixed">Me too!
705
706 - and indented on second line</div></p>
707
708 <p>The comment closed the pull request with status: <b>Under Review</b></p>
709
710 <p>URL: <a href="http://pr.org/comment">http://pr.org/comment</a></p>
711
712
713 <br/>
714 <br/>
715 -- <br/>
716 This is an automatic notification. Don&#39;t reply to this mail.
717
718 <pre>--------------------</pre>
719
720
721 <h1>pull_request_comment, is_mention=True, status_change='Under Review', closing_pr=True</h1>
546 <pre>
722 <pre>
547
723
548 From: u1
724 From: u1
549 To: u2@example.com
725 To: u2@example.com
550 Subject: [Under Review, Closing: Comment] repo/name pull request #7 from devbranch
726 Subject: [Under Review, Closing: Comment] repo/name pull request #7 from devbranch
551
727
552 --------------------
728 --------------------
553
729
554
730
555 Comment from Opinionated User (jsmith) on repo/name pull request "The Title":
731 Comment from Opinionated User (jsmith) on repo/name pull request "The Title":
556 Me too!
732 Me too!
557
733
558 - and indented on second line
734 - and indented on second line
559
735
560 The comment closed the pull request with status: Under Review
736 The comment closed the pull request with status: Under Review
561
737
562 URL: http://pr.org/comment
738 URL: http://pr.org/comment
563
739
564
740
565 --
741 --
566 This is an automatic notification. Don't reply to this mail.
742 This is an automatic notification. Don't reply to this mail.
567
743
568 --------------------</pre>
744 --------------------</pre>
569
745
570
746
571
747
572 <p>Comment from Opinionated User (jsmith) on repo/name pull request &#34;The Title&#34;:</p>
748 <p>Comment from Opinionated User (jsmith) on repo/name pull request &#34;The Title&#34;:</p>
573 <p><div class="formatted-fixed">Me too!
749 <p><div class="formatted-fixed">Me too!
574
750
575 - and indented on second line</div></p>
751 - and indented on second line</div></p>
576
752
577 <p>The comment closed the pull request with status: <b>Under Review</b></p>
753 <p>The comment closed the pull request with status: <b>Under Review</b></p>
578
754
579 <p>URL: <a href="http://pr.org/comment">http://pr.org/comment</a></p>
755 <p>URL: <a href="http://pr.org/comment">http://pr.org/comment</a></p>
580
756
581
757
582 <br/>
758 <br/>
583 <br/>
759 <br/>
584 -- <br/>
760 -- <br/>
585 This is an automatic notification. Don&#39;t reply to this mail.
761 This is an automatic notification. Don&#39;t reply to this mail.
586
762
587 <pre>--------------------</pre>
763 <pre>--------------------</pre>
588
764
589
765
590 <h1>TYPE_PASSWORD_RESET</h1>
766 <h1>TYPE_PASSWORD_RESET</h1>
591 <pre>
767 <pre>
592
768
593 From: u1
769 From: u1
594 To: john@doe.com
770 To: john@doe.com
595 Subject: Password reset link
771 Subject: Password reset link
596
772
597 --------------------
773 --------------------
598
774
599
775
600 Hello John Doe
776 Hello John Doe
601
777
602 We have received a request to reset the password for your account.
778 We have received a request to reset the password for your account.
603 To set a new password, click the following link:
779 To set a new password, click the following link:
604
780
605 http://reset.com/decbf64715098db5b0bd23eab44bd792670ab746
781 http://reset.com/decbf64715098db5b0bd23eab44bd792670ab746
606
782
607 Should you not be able to use the link above, please type the following code into the password reset form: decbf64715098db5b0bd23eab44bd792670ab746
783 Should you not be able to use the link above, please type the following code into the password reset form: decbf64715098db5b0bd23eab44bd792670ab746
608
784
609 If it weren't you who requested the password reset, just disregard this message.
785 If it weren't you who requested the password reset, just disregard this message.
610
786
611
787
612 --
788 --
613 This is an automatic notification. Don't reply to this mail.
789 This is an automatic notification. Don't reply to this mail.
614
790
615 --------------------</pre>
791 --------------------</pre>
616
792
617
793
618
794
619 <h4>Hello John Doe</h4>
795 <h4>Hello John Doe</h4>
620
796
621 <p>We have received a request to reset the password for your account.</p>
797 <p>We have received a request to reset the password for your account.</p>
622 <p>To set a new password, click the following link:</p>
798 <p>To set a new password, click the following link:</p>
623 <p><a href="http://reset.com/decbf64715098db5b0bd23eab44bd792670ab746">http://reset.com/decbf64715098db5b0bd23eab44bd792670ab746</a></p>
799 <p><a href="http://reset.com/decbf64715098db5b0bd23eab44bd792670ab746">http://reset.com/decbf64715098db5b0bd23eab44bd792670ab746</a></p>
624
800
625 <p>Should you not be able to use the link above, please type the following code into the password reset form: <code>decbf64715098db5b0bd23eab44bd792670ab746</code></p>
801 <p>Should you not be able to use the link above, please type the following code into the password reset form: <code>decbf64715098db5b0bd23eab44bd792670ab746</code></p>
626
802
627 <p>If it weren&#39;t you who requested the password reset, just disregard this message.</p>
803 <p>If it weren&#39;t you who requested the password reset, just disregard this message.</p>
628
804
629
805
630 <br/>
806 <br/>
631 <br/>
807 <br/>
632 -- <br/>
808 -- <br/>
633 This is an automatic notification. Don&#39;t reply to this mail.
809 This is an automatic notification. Don&#39;t reply to this mail.
634
810
635 <pre>--------------------</pre>
811 <pre>--------------------</pre>
636
812
637 </body></html>
813 </body></html>
@@ -1,277 +1,278 b''
1 import os
1 import os
2
2
3 import mock
3 import mock
4 import routes.util
4 import routes.util
5
5
6 from kallithea.tests import *
6 from kallithea.tests import *
7 from kallithea.lib import helpers as h
7 from kallithea.lib import helpers as h
8 from kallithea.model.db import User, Notification, UserNotification
8 from kallithea.model.db import User, Notification, UserNotification
9 from kallithea.model.user import UserModel
9 from kallithea.model.user import UserModel
10 from kallithea.model.meta import Session
10 from kallithea.model.meta import Session
11 from kallithea.model.notification import NotificationModel, EmailNotificationModel
11 from kallithea.model.notification import NotificationModel, EmailNotificationModel
12
12
13 import kallithea.lib.celerylib
13 import kallithea.lib.celerylib
14 import kallithea.lib.celerylib.tasks
14 import kallithea.lib.celerylib.tasks
15
15
16
16
17 class TestNotifications(TestController):
17 class TestNotifications(TestController):
18
18
19 def setup_method(self, method):
19 def setup_method(self, method):
20 Session.remove()
20 Session.remove()
21 u1 = UserModel().create_or_update(username=u'u1',
21 u1 = UserModel().create_or_update(username=u'u1',
22 password=u'qweqwe',
22 password=u'qweqwe',
23 email=u'u1@example.com',
23 email=u'u1@example.com',
24 firstname=u'u1', lastname=u'u1')
24 firstname=u'u1', lastname=u'u1')
25 Session().commit()
25 Session().commit()
26 self.u1 = u1.user_id
26 self.u1 = u1.user_id
27
27
28 u2 = UserModel().create_or_update(username=u'u2',
28 u2 = UserModel().create_or_update(username=u'u2',
29 password=u'qweqwe',
29 password=u'qweqwe',
30 email=u'u2@example.com',
30 email=u'u2@example.com',
31 firstname=u'u2', lastname=u'u3')
31 firstname=u'u2', lastname=u'u3')
32 Session().commit()
32 Session().commit()
33 self.u2 = u2.user_id
33 self.u2 = u2.user_id
34
34
35 u3 = UserModel().create_or_update(username=u'u3',
35 u3 = UserModel().create_or_update(username=u'u3',
36 password=u'qweqwe',
36 password=u'qweqwe',
37 email=u'u3@example.com',
37 email=u'u3@example.com',
38 firstname=u'u3', lastname=u'u3')
38 firstname=u'u3', lastname=u'u3')
39 Session().commit()
39 Session().commit()
40 self.u3 = u3.user_id
40 self.u3 = u3.user_id
41
41
42 self.remove_all_notifications()
42 self.remove_all_notifications()
43 assert [] == Notification.query().all()
43 assert [] == Notification.query().all()
44 assert [] == UserNotification.query().all()
44 assert [] == UserNotification.query().all()
45
45
46 def test_create_notification(self):
46 def test_create_notification(self):
47 usrs = [self.u1, self.u2]
47 usrs = [self.u1, self.u2]
48 def send_email(recipients, subject, body='', html_body='', headers=None, author=None):
48 def send_email(recipients, subject, body='', html_body='', headers=None, author=None):
49 assert recipients == ['u2@example.com']
49 assert recipients == ['u2@example.com']
50 assert subject == 'Test Message'
50 assert subject == 'Test Message'
51 assert body == "\n\nhi there\n\n\n-- \nThis is an automatic notification. Don't reply to this mail.\n"
51 assert body == "\n\nhi there\n\n\n-- \nThis is an automatic notification. Don't reply to this mail.\n"
52 assert '>hi there<' in html_body
52 assert '>hi there<' in html_body
53 assert author.username == 'u1'
53 assert author.username == 'u1'
54 with mock.patch.object(kallithea.lib.celerylib.tasks, 'send_email', send_email):
54 with mock.patch.object(kallithea.lib.celerylib.tasks, 'send_email', send_email):
55 notification = NotificationModel().create(created_by=self.u1,
55 notification = NotificationModel().create(created_by=self.u1,
56 subject=u'subj', body=u'hi there',
56 subject=u'subj', body=u'hi there',
57 recipients=usrs)
57 recipients=usrs)
58 Session().commit()
58 Session().commit()
59 u1 = User.get(self.u1)
59 u1 = User.get(self.u1)
60 u2 = User.get(self.u2)
60 u2 = User.get(self.u2)
61 u3 = User.get(self.u3)
61 u3 = User.get(self.u3)
62 notifications = Notification.query().all()
62 notifications = Notification.query().all()
63 assert len(notifications) == 1
63 assert len(notifications) == 1
64
64
65 assert notifications[0].recipients == [u1, u2]
65 assert notifications[0].recipients == [u1, u2]
66 assert notification.notification_id == notifications[0].notification_id
66 assert notification.notification_id == notifications[0].notification_id
67
67
68 unotification = UserNotification.query() \
68 unotification = UserNotification.query() \
69 .filter(UserNotification.notification == notification).all()
69 .filter(UserNotification.notification == notification).all()
70
70
71 assert len(unotification) == len(usrs)
71 assert len(unotification) == len(usrs)
72 assert set([x.user.user_id for x in unotification]) == set(usrs)
72 assert set([x.user.user_id for x in unotification]) == set(usrs)
73
73
74 def test_user_notifications(self):
74 def test_user_notifications(self):
75 notification1 = NotificationModel().create(created_by=self.u1,
75 notification1 = NotificationModel().create(created_by=self.u1,
76 subject=u'subj', body=u'hi there1',
76 subject=u'subj', body=u'hi there1',
77 recipients=[self.u3])
77 recipients=[self.u3])
78 Session().commit()
78 Session().commit()
79 notification2 = NotificationModel().create(created_by=self.u1,
79 notification2 = NotificationModel().create(created_by=self.u1,
80 subject=u'subj', body=u'hi there2',
80 subject=u'subj', body=u'hi there2',
81 recipients=[self.u3])
81 recipients=[self.u3])
82 Session().commit()
82 Session().commit()
83 u3 = Session().query(User).get(self.u3)
83 u3 = Session().query(User).get(self.u3)
84
84
85 assert sorted([x.notification for x in u3.notifications]) == sorted([notification2, notification1])
85 assert sorted([x.notification for x in u3.notifications]) == sorted([notification2, notification1])
86
86
87 def test_delete_notifications(self):
87 def test_delete_notifications(self):
88 notification = NotificationModel().create(created_by=self.u1,
88 notification = NotificationModel().create(created_by=self.u1,
89 subject=u'title', body=u'hi there3',
89 subject=u'title', body=u'hi there3',
90 recipients=[self.u3, self.u1, self.u2])
90 recipients=[self.u3, self.u1, self.u2])
91 Session().commit()
91 Session().commit()
92 notifications = Notification.query().all()
92 notifications = Notification.query().all()
93 assert notification in notifications
93 assert notification in notifications
94
94
95 Notification.delete(notification.notification_id)
95 Notification.delete(notification.notification_id)
96 Session().commit()
96 Session().commit()
97
97
98 notifications = Notification.query().all()
98 notifications = Notification.query().all()
99 assert not notification in notifications
99 assert not notification in notifications
100
100
101 un = UserNotification.query().filter(UserNotification.notification
101 un = UserNotification.query().filter(UserNotification.notification
102 == notification).all()
102 == notification).all()
103 assert un == []
103 assert un == []
104
104
105 def test_delete_association(self):
105 def test_delete_association(self):
106 notification = NotificationModel().create(created_by=self.u1,
106 notification = NotificationModel().create(created_by=self.u1,
107 subject=u'title', body=u'hi there3',
107 subject=u'title', body=u'hi there3',
108 recipients=[self.u3, self.u1, self.u2])
108 recipients=[self.u3, self.u1, self.u2])
109 Session().commit()
109 Session().commit()
110
110
111 unotification = UserNotification.query() \
111 unotification = UserNotification.query() \
112 .filter(UserNotification.notification ==
112 .filter(UserNotification.notification ==
113 notification) \
113 notification) \
114 .filter(UserNotification.user_id == self.u3) \
114 .filter(UserNotification.user_id == self.u3) \
115 .scalar()
115 .scalar()
116
116
117 assert unotification.user_id == self.u3
117 assert unotification.user_id == self.u3
118
118
119 NotificationModel().delete(self.u3,
119 NotificationModel().delete(self.u3,
120 notification.notification_id)
120 notification.notification_id)
121 Session().commit()
121 Session().commit()
122
122
123 u3notification = UserNotification.query() \
123 u3notification = UserNotification.query() \
124 .filter(UserNotification.notification ==
124 .filter(UserNotification.notification ==
125 notification) \
125 notification) \
126 .filter(UserNotification.user_id == self.u3) \
126 .filter(UserNotification.user_id == self.u3) \
127 .scalar()
127 .scalar()
128
128
129 assert u3notification == None
129 assert u3notification == None
130
130
131 # notification object is still there
131 # notification object is still there
132 assert Notification.query().all() == [notification]
132 assert Notification.query().all() == [notification]
133
133
134 #u1 and u2 still have assignments
134 #u1 and u2 still have assignments
135 u1notification = UserNotification.query() \
135 u1notification = UserNotification.query() \
136 .filter(UserNotification.notification ==
136 .filter(UserNotification.notification ==
137 notification) \
137 notification) \
138 .filter(UserNotification.user_id == self.u1) \
138 .filter(UserNotification.user_id == self.u1) \
139 .scalar()
139 .scalar()
140 assert u1notification != None
140 assert u1notification != None
141 u2notification = UserNotification.query() \
141 u2notification = UserNotification.query() \
142 .filter(UserNotification.notification ==
142 .filter(UserNotification.notification ==
143 notification) \
143 notification) \
144 .filter(UserNotification.user_id == self.u2) \
144 .filter(UserNotification.user_id == self.u2) \
145 .scalar()
145 .scalar()
146 assert u2notification != None
146 assert u2notification != None
147
147
148 def test_notification_counter(self):
148 def test_notification_counter(self):
149 NotificationModel().create(created_by=self.u1,
149 NotificationModel().create(created_by=self.u1,
150 subject=u'title', body=u'hi there_delete',
150 subject=u'title', body=u'hi there_delete',
151 recipients=[self.u3, self.u1])
151 recipients=[self.u3, self.u1])
152 Session().commit()
152 Session().commit()
153
153
154 assert NotificationModel().get_unread_cnt_for_user(self.u1) == 0
154 assert NotificationModel().get_unread_cnt_for_user(self.u1) == 0
155 assert NotificationModel().get_unread_cnt_for_user(self.u2) == 0
155 assert NotificationModel().get_unread_cnt_for_user(self.u2) == 0
156 assert NotificationModel().get_unread_cnt_for_user(self.u3) == 1
156 assert NotificationModel().get_unread_cnt_for_user(self.u3) == 1
157
157
158 notification = NotificationModel().create(created_by=self.u1,
158 notification = NotificationModel().create(created_by=self.u1,
159 subject=u'title', body=u'hi there3',
159 subject=u'title', body=u'hi there3',
160 recipients=[self.u3, self.u1, self.u2])
160 recipients=[self.u3, self.u1, self.u2])
161 Session().commit()
161 Session().commit()
162
162
163 assert NotificationModel().get_unread_cnt_for_user(self.u1) == 0
163 assert NotificationModel().get_unread_cnt_for_user(self.u1) == 0
164 assert NotificationModel().get_unread_cnt_for_user(self.u2) == 1
164 assert NotificationModel().get_unread_cnt_for_user(self.u2) == 1
165 assert NotificationModel().get_unread_cnt_for_user(self.u3) == 2
165 assert NotificationModel().get_unread_cnt_for_user(self.u3) == 2
166
166
167 @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())))))
167 @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())))))
168 def test_dump_html_mails(self):
168 def test_dump_html_mails(self):
169 # Exercise all notification types and dump them to one big html file
169 # Exercise all notification types and dump them to one big html file
170 l = []
170 l = []
171
171
172 def send_email(recipients, subject, body='', html_body='', headers=None, author=None):
172 def send_email(recipients, subject, body='', html_body='', headers=None, author=None):
173 l.append('\n\n<h1>%s</h1>\n' % desc) # desc is from outer scope
173 l.append('\n\n<h1>%s</h1>\n' % desc) # desc is from outer scope
174 l.append('<pre>\n\n')
174 l.append('<pre>\n\n')
175 l.append('From: %s\n' % author.username)
175 l.append('From: %s\n' % author.username)
176 l.append('To: %s\n' % ' '.join(recipients))
176 l.append('To: %s\n' % ' '.join(recipients))
177 l.append('Subject: %s\n' % subject)
177 l.append('Subject: %s\n' % subject)
178 l.append('\n--------------------\n%s\n--------------------' % body)
178 l.append('\n--------------------\n%s\n--------------------' % body)
179 l.append('</pre>\n')
179 l.append('</pre>\n')
180 l.append('\n%s\n' % html_body)
180 l.append('\n%s\n' % html_body)
181 l.append('<pre>--------------------</pre>\n')
181 l.append('<pre>--------------------</pre>\n')
182
182
183 l.append('<html><body>\n')
183 l.append('<html><body>\n')
184 with mock.patch.object(kallithea.lib.celerylib.tasks, 'send_email', send_email):
184 with mock.patch.object(kallithea.lib.celerylib.tasks, 'send_email', send_email):
185 pr_kwargs = dict(
185 pr_kwargs = dict(
186 pr_nice_id='#7',
186 pr_nice_id='#7',
187 pr_title='The Title',
187 pr_title='The Title',
188 pr_url='http://pr.org/7',
188 pr_url='http://pr.org/7',
189 pr_target_repo='http://mainline.com/repo',
189 pr_target_repo='http://mainline.com/repo',
190 pr_target_branch='trunk',
190 pr_target_branch='trunk',
191 pr_source_repo='https://dev.org/repo',
191 pr_source_repo='https://dev.org/repo',
192 pr_source_branch='devbranch',
192 pr_source_branch='devbranch',
193 pr_owner=User.get(self.u2),
193 pr_owner=User.get(self.u2),
194 )
194 )
195
195
196 for type_, body, kwargs in [
196 for type_, body, kwargs in [
197 (Notification.TYPE_CHANGESET_COMMENT,
197 (Notification.TYPE_CHANGESET_COMMENT,
198 u'This is the new comment.\n\n - and here it ends indented.',
198 u'This is the new comment.\n\n - and here it ends indented.',
199 dict(
199 dict(
200 short_id='cafe1234',
200 short_id='cafe1234',
201 raw_id='cafe1234c0ffeecafe',
201 raw_id='cafe1234c0ffeecafe',
202 branch='brunch',
202 branch='brunch',
203 cs_comment_user='Opinionated User (jsmith)',
203 cs_comment_user='Opinionated User (jsmith)',
204 cs_comment_url='http://comment.org',
204 cs_comment_url='http://comment.org',
205 is_mention=[False, True],
205 is_mention=[False, True],
206 message='This changeset did something clever which is hard to explain',
206 message='This changeset did something clever which is hard to explain',
207 status_change=[None, 'Approved'],
207 status_change=[None, 'Approved'],
208 cs_target_repo='repo_target',
208 cs_target_repo='repo_target',
209 cs_url='http://changeset.com',
209 cs_url='http://changeset.com',
210 cs_author=User.get(self.u2))),
210 cs_author=User.get(self.u2))),
211 (Notification.TYPE_MESSAGE,
211 (Notification.TYPE_MESSAGE,
212 u'This is the body of the test message\n - nothing interesting here except indentation.',
212 u'This is the body of the test message\n - nothing interesting here except indentation.',
213 dict()),
213 dict()),
214 #(Notification.TYPE_MENTION, '$body', None), # not used
214 #(Notification.TYPE_MENTION, '$body', None), # not used
215 (Notification.TYPE_REGISTRATION,
215 (Notification.TYPE_REGISTRATION,
216 u'Registration body',
216 u'Registration body',
217 dict(
217 dict(
218 new_username='newbie',
218 new_username='newbie',
219 registered_user_url='http://newbie.org',
219 registered_user_url='http://newbie.org',
220 new_email='new@email.com',
220 new_email='new@email.com',
221 new_full_name='New Full Name')),
221 new_full_name='New Full Name')),
222 (Notification.TYPE_PULL_REQUEST,
222 (Notification.TYPE_PULL_REQUEST,
223 u'This PR is awesome because it does stuff\n - please approve indented!',
223 u'This PR is awesome because it does stuff\n - please approve indented!',
224 dict(
224 dict(
225 pr_user_created='Requesting User (root)', # pr_owner should perhaps be used for @mention in description ...
225 pr_user_created='Requesting User (root)', # pr_owner should perhaps be used for @mention in description ...
226 is_mention=[False, True],
226 is_mention=[False, True],
227 pr_revisions=[('123abc'*7, "Introduce one and two\n\nand that's it"), ('567fed'*7, 'Make one plus two equal tree')],
227 pr_revisions=[('123abc'*7, "Introduce one and two\n\nand that's it"), ('567fed'*7, 'Make one plus two equal tree')],
228 org_repo_name='repo_org',
228 org_repo_name='repo_org',
229 **pr_kwargs)),
229 **pr_kwargs)),
230 (Notification.TYPE_PULL_REQUEST_COMMENT,
230 (Notification.TYPE_PULL_REQUEST_COMMENT,
231 u'Me too!\n\n - and indented on second line',
231 u'Me too!\n\n - and indented on second line',
232 dict(
232 dict(
233 closing_pr=[False, True],
233 closing_pr=[False, True],
234 is_mention=[False, True],
234 pr_comment_user='Opinionated User (jsmith)',
235 pr_comment_user='Opinionated User (jsmith)',
235 pr_comment_url='http://pr.org/comment',
236 pr_comment_url='http://pr.org/comment',
236 status_change=[None, 'Under Review'],
237 status_change=[None, 'Under Review'],
237 **pr_kwargs)),
238 **pr_kwargs)),
238 ]:
239 ]:
239 kwargs['repo_name'] = u'repo/name'
240 kwargs['repo_name'] = u'repo/name'
240 params = [(type_, type_, body, kwargs)]
241 params = [(type_, type_, body, kwargs)]
241 for param_name in ['is_mention', 'status_change', 'closing_pr']: # TODO: inline/general
242 for param_name in ['is_mention', 'status_change', 'closing_pr']: # TODO: inline/general
242 if not isinstance(kwargs.get(param_name), list):
243 if not isinstance(kwargs.get(param_name), list):
243 continue
244 continue
244 new_params = []
245 new_params = []
245 for v in kwargs[param_name]:
246 for v in kwargs[param_name]:
246 for desc, type_, body, kwargs in params:
247 for desc, type_, body, kwargs in params:
247 kwargs = dict(kwargs)
248 kwargs = dict(kwargs)
248 kwargs[param_name] = v
249 kwargs[param_name] = v
249 new_params.append(('%s, %s=%r' % (desc, param_name, v), type_, body, kwargs))
250 new_params.append(('%s, %s=%r' % (desc, param_name, v), type_, body, kwargs))
250 params = new_params
251 params = new_params
251
252
252 for desc, type_, body, kwargs in params:
253 for desc, type_, body, kwargs in params:
253 # desc is used as "global" variable
254 # desc is used as "global" variable
254 notification = NotificationModel().create(created_by=self.u1,
255 notification = NotificationModel().create(created_by=self.u1,
255 subject=u'unused', body=body, email_kwargs=kwargs,
256 subject=u'unused', body=body, email_kwargs=kwargs,
256 recipients=[self.u2], type_=type_)
257 recipients=[self.u2], type_=type_)
257
258
258 # Email type TYPE_PASSWORD_RESET has no corresponding notification type - test it directly:
259 # Email type TYPE_PASSWORD_RESET has no corresponding notification type - test it directly:
259 desc = 'TYPE_PASSWORD_RESET'
260 desc = 'TYPE_PASSWORD_RESET'
260 kwargs = dict(user='John Doe', reset_token='decbf64715098db5b0bd23eab44bd792670ab746', reset_url='http://reset.com/decbf64715098db5b0bd23eab44bd792670ab746')
261 kwargs = dict(user='John Doe', reset_token='decbf64715098db5b0bd23eab44bd792670ab746', reset_url='http://reset.com/decbf64715098db5b0bd23eab44bd792670ab746')
261 kallithea.lib.celerylib.run_task(kallithea.lib.celerylib.tasks.send_email, ['john@doe.com'],
262 kallithea.lib.celerylib.run_task(kallithea.lib.celerylib.tasks.send_email, ['john@doe.com'],
262 "Password reset link",
263 "Password reset link",
263 EmailNotificationModel().get_email_tmpl(EmailNotificationModel.TYPE_PASSWORD_RESET, 'txt', **kwargs),
264 EmailNotificationModel().get_email_tmpl(EmailNotificationModel.TYPE_PASSWORD_RESET, 'txt', **kwargs),
264 EmailNotificationModel().get_email_tmpl(EmailNotificationModel.TYPE_PASSWORD_RESET, 'html', **kwargs),
265 EmailNotificationModel().get_email_tmpl(EmailNotificationModel.TYPE_PASSWORD_RESET, 'html', **kwargs),
265 author=User.get(self.u1))
266 author=User.get(self.u1))
266
267
267 l.append('\n</body></html>\n')
268 l.append('\n</body></html>\n')
268 out = ''.join(l)
269 out = ''.join(l)
269
270
270 outfn = os.path.join(os.path.dirname(__file__), 'test_dump_html_mails.out.html')
271 outfn = os.path.join(os.path.dirname(__file__), 'test_dump_html_mails.out.html')
271 reffn = os.path.join(os.path.dirname(__file__), 'test_dump_html_mails.ref.html')
272 reffn = os.path.join(os.path.dirname(__file__), 'test_dump_html_mails.ref.html')
272 with file(outfn, 'w') as f:
273 with file(outfn, 'w') as f:
273 f.write(out)
274 f.write(out)
274 with file(reffn) as f:
275 with file(reffn) as f:
275 ref = f.read()
276 ref = f.read()
276 assert ref == out # copy test_dump_html_mails.out.html to test_dump_html_mails.ref.html to update expectations
277 assert ref == out # copy test_dump_html_mails.out.html to test_dump_html_mails.ref.html to update expectations
277 os.unlink(outfn)
278 os.unlink(outfn)
General Comments 0
You need to be logged in to leave comments. Login now