##// END OF EJS Templates
emails: added note types into emails. Fixes #5221
marcink -
r1453:4b56dcb1 default
parent child Browse files
Show More
@@ -1,634 +1,635 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import collections
27 import collections
28
28
29 from datetime import datetime
29 from datetime import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pyramid.threadlocal import get_current_registry
32 from pyramid.threadlocal import get_current_registry
33 from sqlalchemy.sql.expression import null
33 from sqlalchemy.sql.expression import null
34 from sqlalchemy.sql.functions import coalesce
34 from sqlalchemy.sql.functions import coalesce
35
35
36 from rhodecode.lib import helpers as h, diffs
36 from rhodecode.lib import helpers as h, diffs
37 from rhodecode.lib.channelstream import channelstream_request
37 from rhodecode.lib.channelstream import channelstream_request
38 from rhodecode.lib.utils import action_logger
38 from rhodecode.lib.utils import action_logger
39 from rhodecode.lib.utils2 import extract_mentioned_users
39 from rhodecode.lib.utils2 import extract_mentioned_users
40 from rhodecode.model import BaseModel
40 from rhodecode.model import BaseModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 ChangesetComment, User, Notification, PullRequest, AttributeDict)
42 ChangesetComment, User, Notification, PullRequest, AttributeDict)
43 from rhodecode.model.notification import NotificationModel
43 from rhodecode.model.notification import NotificationModel
44 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
45 from rhodecode.model.settings import VcsSettingsModel
45 from rhodecode.model.settings import VcsSettingsModel
46 from rhodecode.model.notification import EmailNotificationModel
46 from rhodecode.model.notification import EmailNotificationModel
47 from rhodecode.model.validation_schema.schemas import comment_schema
47 from rhodecode.model.validation_schema.schemas import comment_schema
48
48
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class CommentsModel(BaseModel):
53 class CommentsModel(BaseModel):
54
54
55 cls = ChangesetComment
55 cls = ChangesetComment
56
56
57 DIFF_CONTEXT_BEFORE = 3
57 DIFF_CONTEXT_BEFORE = 3
58 DIFF_CONTEXT_AFTER = 3
58 DIFF_CONTEXT_AFTER = 3
59
59
60 def __get_commit_comment(self, changeset_comment):
60 def __get_commit_comment(self, changeset_comment):
61 return self._get_instance(ChangesetComment, changeset_comment)
61 return self._get_instance(ChangesetComment, changeset_comment)
62
62
63 def __get_pull_request(self, pull_request):
63 def __get_pull_request(self, pull_request):
64 return self._get_instance(PullRequest, pull_request)
64 return self._get_instance(PullRequest, pull_request)
65
65
66 def _extract_mentions(self, s):
66 def _extract_mentions(self, s):
67 user_objects = []
67 user_objects = []
68 for username in extract_mentioned_users(s):
68 for username in extract_mentioned_users(s):
69 user_obj = User.get_by_username(username, case_insensitive=True)
69 user_obj = User.get_by_username(username, case_insensitive=True)
70 if user_obj:
70 if user_obj:
71 user_objects.append(user_obj)
71 user_objects.append(user_obj)
72 return user_objects
72 return user_objects
73
73
74 def _get_renderer(self, global_renderer='rst'):
74 def _get_renderer(self, global_renderer='rst'):
75 try:
75 try:
76 # try reading from visual context
76 # try reading from visual context
77 from pylons import tmpl_context
77 from pylons import tmpl_context
78 global_renderer = tmpl_context.visual.default_renderer
78 global_renderer = tmpl_context.visual.default_renderer
79 except AttributeError:
79 except AttributeError:
80 log.debug("Renderer not set, falling back "
80 log.debug("Renderer not set, falling back "
81 "to default renderer '%s'", global_renderer)
81 "to default renderer '%s'", global_renderer)
82 except Exception:
82 except Exception:
83 log.error(traceback.format_exc())
83 log.error(traceback.format_exc())
84 return global_renderer
84 return global_renderer
85
85
86 def aggregate_comments(self, comments, versions, show_version, inline=False):
86 def aggregate_comments(self, comments, versions, show_version, inline=False):
87 # group by versions, and count until, and display objects
87 # group by versions, and count until, and display objects
88
88
89 comment_groups = collections.defaultdict(list)
89 comment_groups = collections.defaultdict(list)
90 [comment_groups[
90 [comment_groups[
91 _co.pull_request_version_id].append(_co) for _co in comments]
91 _co.pull_request_version_id].append(_co) for _co in comments]
92
92
93 def yield_comments(pos):
93 def yield_comments(pos):
94 for co in comment_groups[pos]:
94 for co in comment_groups[pos]:
95 yield co
95 yield co
96
96
97 comment_versions = collections.defaultdict(
97 comment_versions = collections.defaultdict(
98 lambda: collections.defaultdict(list))
98 lambda: collections.defaultdict(list))
99 prev_prvid = -1
99 prev_prvid = -1
100 # fake last entry with None, to aggregate on "latest" version which
100 # fake last entry with None, to aggregate on "latest" version which
101 # doesn't have an pull_request_version_id
101 # doesn't have an pull_request_version_id
102 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
102 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
103 prvid = ver.pull_request_version_id
103 prvid = ver.pull_request_version_id
104 if prev_prvid == -1:
104 if prev_prvid == -1:
105 prev_prvid = prvid
105 prev_prvid = prvid
106
106
107 for co in yield_comments(prvid):
107 for co in yield_comments(prvid):
108 comment_versions[prvid]['at'].append(co)
108 comment_versions[prvid]['at'].append(co)
109
109
110 # save until
110 # save until
111 current = comment_versions[prvid]['at']
111 current = comment_versions[prvid]['at']
112 prev_until = comment_versions[prev_prvid]['until']
112 prev_until = comment_versions[prev_prvid]['until']
113 cur_until = prev_until + current
113 cur_until = prev_until + current
114 comment_versions[prvid]['until'].extend(cur_until)
114 comment_versions[prvid]['until'].extend(cur_until)
115
115
116 # save outdated
116 # save outdated
117 if inline:
117 if inline:
118 outdated = [x for x in cur_until
118 outdated = [x for x in cur_until
119 if x.outdated_at_version(show_version)]
119 if x.outdated_at_version(show_version)]
120 else:
120 else:
121 outdated = [x for x in cur_until
121 outdated = [x for x in cur_until
122 if x.older_than_version(show_version)]
122 if x.older_than_version(show_version)]
123 display = [x for x in cur_until if x not in outdated]
123 display = [x for x in cur_until if x not in outdated]
124
124
125 comment_versions[prvid]['outdated'] = outdated
125 comment_versions[prvid]['outdated'] = outdated
126 comment_versions[prvid]['display'] = display
126 comment_versions[prvid]['display'] = display
127
127
128 prev_prvid = prvid
128 prev_prvid = prvid
129
129
130 return comment_versions
130 return comment_versions
131
131
132 def get_unresolved_todos(self, pull_request, show_outdated=True):
132 def get_unresolved_todos(self, pull_request, show_outdated=True):
133
133
134 todos = Session().query(ChangesetComment) \
134 todos = Session().query(ChangesetComment) \
135 .filter(ChangesetComment.pull_request == pull_request) \
135 .filter(ChangesetComment.pull_request == pull_request) \
136 .filter(ChangesetComment.resolved_by == None) \
136 .filter(ChangesetComment.resolved_by == None) \
137 .filter(ChangesetComment.comment_type
137 .filter(ChangesetComment.comment_type
138 == ChangesetComment.COMMENT_TYPE_TODO)
138 == ChangesetComment.COMMENT_TYPE_TODO)
139
139
140 if not show_outdated:
140 if not show_outdated:
141 todos = todos.filter(
141 todos = todos.filter(
142 coalesce(ChangesetComment.display_state, '') !=
142 coalesce(ChangesetComment.display_state, '') !=
143 ChangesetComment.COMMENT_OUTDATED)
143 ChangesetComment.COMMENT_OUTDATED)
144
144
145 todos = todos.all()
145 todos = todos.all()
146
146
147 return todos
147 return todos
148
148
149 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
149 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
150
150
151 todos = Session().query(ChangesetComment) \
151 todos = Session().query(ChangesetComment) \
152 .filter(ChangesetComment.revision == commit_id) \
152 .filter(ChangesetComment.revision == commit_id) \
153 .filter(ChangesetComment.resolved_by == None) \
153 .filter(ChangesetComment.resolved_by == None) \
154 .filter(ChangesetComment.comment_type
154 .filter(ChangesetComment.comment_type
155 == ChangesetComment.COMMENT_TYPE_TODO)
155 == ChangesetComment.COMMENT_TYPE_TODO)
156
156
157 if not show_outdated:
157 if not show_outdated:
158 todos = todos.filter(
158 todos = todos.filter(
159 coalesce(ChangesetComment.display_state, '') !=
159 coalesce(ChangesetComment.display_state, '') !=
160 ChangesetComment.COMMENT_OUTDATED)
160 ChangesetComment.COMMENT_OUTDATED)
161
161
162 todos = todos.all()
162 todos = todos.all()
163
163
164 return todos
164 return todos
165
165
166 def create(self, text, repo, user, commit_id=None, pull_request=None,
166 def create(self, text, repo, user, commit_id=None, pull_request=None,
167 f_path=None, line_no=None, status_change=None,
167 f_path=None, line_no=None, status_change=None,
168 status_change_type=None, comment_type=None,
168 status_change_type=None, comment_type=None,
169 resolves_comment_id=None, closing_pr=False, send_email=True,
169 resolves_comment_id=None, closing_pr=False, send_email=True,
170 renderer=None):
170 renderer=None):
171 """
171 """
172 Creates new comment for commit or pull request.
172 Creates new comment for commit or pull request.
173 IF status_change is not none this comment is associated with a
173 IF status_change is not none this comment is associated with a
174 status change of commit or commit associated with pull request
174 status change of commit or commit associated with pull request
175
175
176 :param text:
176 :param text:
177 :param repo:
177 :param repo:
178 :param user:
178 :param user:
179 :param commit_id:
179 :param commit_id:
180 :param pull_request:
180 :param pull_request:
181 :param f_path:
181 :param f_path:
182 :param line_no:
182 :param line_no:
183 :param status_change: Label for status change
183 :param status_change: Label for status change
184 :param comment_type: Type of comment
184 :param comment_type: Type of comment
185 :param status_change_type: type of status change
185 :param status_change_type: type of status change
186 :param closing_pr:
186 :param closing_pr:
187 :param send_email:
187 :param send_email:
188 :param renderer: pick renderer for this comment
188 :param renderer: pick renderer for this comment
189 """
189 """
190 if not text:
190 if not text:
191 log.warning('Missing text for comment, skipping...')
191 log.warning('Missing text for comment, skipping...')
192 return
192 return
193
193
194 if not renderer:
194 if not renderer:
195 renderer = self._get_renderer()
195 renderer = self._get_renderer()
196
196
197 repo = self._get_repo(repo)
197 repo = self._get_repo(repo)
198 user = self._get_user(user)
198 user = self._get_user(user)
199
199
200 schema = comment_schema.CommentSchema()
200 schema = comment_schema.CommentSchema()
201 validated_kwargs = schema.deserialize(dict(
201 validated_kwargs = schema.deserialize(dict(
202 comment_body=text,
202 comment_body=text,
203 comment_type=comment_type,
203 comment_type=comment_type,
204 comment_file=f_path,
204 comment_file=f_path,
205 comment_line=line_no,
205 comment_line=line_no,
206 renderer_type=renderer,
206 renderer_type=renderer,
207 status_change=status_change_type,
207 status_change=status_change_type,
208 resolves_comment_id=resolves_comment_id,
208 resolves_comment_id=resolves_comment_id,
209 repo=repo.repo_id,
209 repo=repo.repo_id,
210 user=user.user_id,
210 user=user.user_id,
211 ))
211 ))
212
212
213 comment = ChangesetComment()
213 comment = ChangesetComment()
214 comment.renderer = validated_kwargs['renderer_type']
214 comment.renderer = validated_kwargs['renderer_type']
215 comment.text = validated_kwargs['comment_body']
215 comment.text = validated_kwargs['comment_body']
216 comment.f_path = validated_kwargs['comment_file']
216 comment.f_path = validated_kwargs['comment_file']
217 comment.line_no = validated_kwargs['comment_line']
217 comment.line_no = validated_kwargs['comment_line']
218 comment.comment_type = validated_kwargs['comment_type']
218 comment.comment_type = validated_kwargs['comment_type']
219
219
220 comment.repo = repo
220 comment.repo = repo
221 comment.author = user
221 comment.author = user
222 comment.resolved_comment = self.__get_commit_comment(
222 comment.resolved_comment = self.__get_commit_comment(
223 validated_kwargs['resolves_comment_id'])
223 validated_kwargs['resolves_comment_id'])
224
224
225 pull_request_id = pull_request
225 pull_request_id = pull_request
226
226
227 commit_obj = None
227 commit_obj = None
228 pull_request_obj = None
228 pull_request_obj = None
229
229
230 if commit_id:
230 if commit_id:
231 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
231 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
232 # do a lookup, so we don't pass something bad here
232 # do a lookup, so we don't pass something bad here
233 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
233 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
234 comment.revision = commit_obj.raw_id
234 comment.revision = commit_obj.raw_id
235
235
236 elif pull_request_id:
236 elif pull_request_id:
237 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
237 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
238 pull_request_obj = self.__get_pull_request(pull_request_id)
238 pull_request_obj = self.__get_pull_request(pull_request_id)
239 comment.pull_request = pull_request_obj
239 comment.pull_request = pull_request_obj
240 else:
240 else:
241 raise Exception('Please specify commit or pull_request_id')
241 raise Exception('Please specify commit or pull_request_id')
242
242
243 Session().add(comment)
243 Session().add(comment)
244 Session().flush()
244 Session().flush()
245 kwargs = {
245 kwargs = {
246 'user': user,
246 'user': user,
247 'renderer_type': renderer,
247 'renderer_type': renderer,
248 'repo_name': repo.repo_name,
248 'repo_name': repo.repo_name,
249 'status_change': status_change,
249 'status_change': status_change,
250 'status_change_type': status_change_type,
250 'status_change_type': status_change_type,
251 'comment_body': text,
251 'comment_body': text,
252 'comment_file': f_path,
252 'comment_file': f_path,
253 'comment_line': line_no,
253 'comment_line': line_no,
254 'comment_type': comment_type or 'note'
254 }
255 }
255
256
256 if commit_obj:
257 if commit_obj:
257 recipients = ChangesetComment.get_users(
258 recipients = ChangesetComment.get_users(
258 revision=commit_obj.raw_id)
259 revision=commit_obj.raw_id)
259 # add commit author if it's in RhodeCode system
260 # add commit author if it's in RhodeCode system
260 cs_author = User.get_from_cs_author(commit_obj.author)
261 cs_author = User.get_from_cs_author(commit_obj.author)
261 if not cs_author:
262 if not cs_author:
262 # use repo owner if we cannot extract the author correctly
263 # use repo owner if we cannot extract the author correctly
263 cs_author = repo.user
264 cs_author = repo.user
264 recipients += [cs_author]
265 recipients += [cs_author]
265
266
266 commit_comment_url = self.get_url(comment)
267 commit_comment_url = self.get_url(comment)
267
268
268 target_repo_url = h.link_to(
269 target_repo_url = h.link_to(
269 repo.repo_name,
270 repo.repo_name,
270 h.url('summary_home',
271 h.url('summary_home',
271 repo_name=repo.repo_name, qualified=True))
272 repo_name=repo.repo_name, qualified=True))
272
273
273 # commit specifics
274 # commit specifics
274 kwargs.update({
275 kwargs.update({
275 'commit': commit_obj,
276 'commit': commit_obj,
276 'commit_message': commit_obj.message,
277 'commit_message': commit_obj.message,
277 'commit_target_repo': target_repo_url,
278 'commit_target_repo': target_repo_url,
278 'commit_comment_url': commit_comment_url,
279 'commit_comment_url': commit_comment_url,
279 })
280 })
280
281
281 elif pull_request_obj:
282 elif pull_request_obj:
282 # get the current participants of this pull request
283 # get the current participants of this pull request
283 recipients = ChangesetComment.get_users(
284 recipients = ChangesetComment.get_users(
284 pull_request_id=pull_request_obj.pull_request_id)
285 pull_request_id=pull_request_obj.pull_request_id)
285 # add pull request author
286 # add pull request author
286 recipients += [pull_request_obj.author]
287 recipients += [pull_request_obj.author]
287
288
288 # add the reviewers to notification
289 # add the reviewers to notification
289 recipients += [x.user for x in pull_request_obj.reviewers]
290 recipients += [x.user for x in pull_request_obj.reviewers]
290
291
291 pr_target_repo = pull_request_obj.target_repo
292 pr_target_repo = pull_request_obj.target_repo
292 pr_source_repo = pull_request_obj.source_repo
293 pr_source_repo = pull_request_obj.source_repo
293
294
294 pr_comment_url = h.url(
295 pr_comment_url = h.url(
295 'pullrequest_show',
296 'pullrequest_show',
296 repo_name=pr_target_repo.repo_name,
297 repo_name=pr_target_repo.repo_name,
297 pull_request_id=pull_request_obj.pull_request_id,
298 pull_request_id=pull_request_obj.pull_request_id,
298 anchor='comment-%s' % comment.comment_id,
299 anchor='comment-%s' % comment.comment_id,
299 qualified=True,)
300 qualified=True,)
300
301
301 # set some variables for email notification
302 # set some variables for email notification
302 pr_target_repo_url = h.url(
303 pr_target_repo_url = h.url(
303 'summary_home', repo_name=pr_target_repo.repo_name,
304 'summary_home', repo_name=pr_target_repo.repo_name,
304 qualified=True)
305 qualified=True)
305
306
306 pr_source_repo_url = h.url(
307 pr_source_repo_url = h.url(
307 'summary_home', repo_name=pr_source_repo.repo_name,
308 'summary_home', repo_name=pr_source_repo.repo_name,
308 qualified=True)
309 qualified=True)
309
310
310 # pull request specifics
311 # pull request specifics
311 kwargs.update({
312 kwargs.update({
312 'pull_request': pull_request_obj,
313 'pull_request': pull_request_obj,
313 'pr_id': pull_request_obj.pull_request_id,
314 'pr_id': pull_request_obj.pull_request_id,
314 'pr_target_repo': pr_target_repo,
315 'pr_target_repo': pr_target_repo,
315 'pr_target_repo_url': pr_target_repo_url,
316 'pr_target_repo_url': pr_target_repo_url,
316 'pr_source_repo': pr_source_repo,
317 'pr_source_repo': pr_source_repo,
317 'pr_source_repo_url': pr_source_repo_url,
318 'pr_source_repo_url': pr_source_repo_url,
318 'pr_comment_url': pr_comment_url,
319 'pr_comment_url': pr_comment_url,
319 'pr_closing': closing_pr,
320 'pr_closing': closing_pr,
320 })
321 })
321 if send_email:
322 if send_email:
322 # pre-generate the subject for notification itself
323 # pre-generate the subject for notification itself
323 (subject,
324 (subject,
324 _h, _e, # we don't care about those
325 _h, _e, # we don't care about those
325 body_plaintext) = EmailNotificationModel().render_email(
326 body_plaintext) = EmailNotificationModel().render_email(
326 notification_type, **kwargs)
327 notification_type, **kwargs)
327
328
328 mention_recipients = set(
329 mention_recipients = set(
329 self._extract_mentions(text)).difference(recipients)
330 self._extract_mentions(text)).difference(recipients)
330
331
331 # create notification objects, and emails
332 # create notification objects, and emails
332 NotificationModel().create(
333 NotificationModel().create(
333 created_by=user,
334 created_by=user,
334 notification_subject=subject,
335 notification_subject=subject,
335 notification_body=body_plaintext,
336 notification_body=body_plaintext,
336 notification_type=notification_type,
337 notification_type=notification_type,
337 recipients=recipients,
338 recipients=recipients,
338 mention_recipients=mention_recipients,
339 mention_recipients=mention_recipients,
339 email_kwargs=kwargs,
340 email_kwargs=kwargs,
340 )
341 )
341
342
342 action = (
343 action = (
343 'user_commented_pull_request:{}'.format(
344 'user_commented_pull_request:{}'.format(
344 comment.pull_request.pull_request_id)
345 comment.pull_request.pull_request_id)
345 if comment.pull_request
346 if comment.pull_request
346 else 'user_commented_revision:{}'.format(comment.revision)
347 else 'user_commented_revision:{}'.format(comment.revision)
347 )
348 )
348 action_logger(user, action, comment.repo)
349 action_logger(user, action, comment.repo)
349
350
350 registry = get_current_registry()
351 registry = get_current_registry()
351 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
352 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
352 channelstream_config = rhodecode_plugins.get('channelstream', {})
353 channelstream_config = rhodecode_plugins.get('channelstream', {})
353 msg_url = ''
354 msg_url = ''
354 if commit_obj:
355 if commit_obj:
355 msg_url = commit_comment_url
356 msg_url = commit_comment_url
356 repo_name = repo.repo_name
357 repo_name = repo.repo_name
357 elif pull_request_obj:
358 elif pull_request_obj:
358 msg_url = pr_comment_url
359 msg_url = pr_comment_url
359 repo_name = pr_target_repo.repo_name
360 repo_name = pr_target_repo.repo_name
360
361
361 if channelstream_config.get('enabled'):
362 if channelstream_config.get('enabled'):
362 message = '<strong>{}</strong> {} - ' \
363 message = '<strong>{}</strong> {} - ' \
363 '<a onclick="window.location=\'{}\';' \
364 '<a onclick="window.location=\'{}\';' \
364 'window.location.reload()">' \
365 'window.location.reload()">' \
365 '<strong>{}</strong></a>'
366 '<strong>{}</strong></a>'
366 message = message.format(
367 message = message.format(
367 user.username, _('made a comment'), msg_url,
368 user.username, _('made a comment'), msg_url,
368 _('Show it now'))
369 _('Show it now'))
369 channel = '/repo${}$/pr/{}'.format(
370 channel = '/repo${}$/pr/{}'.format(
370 repo_name,
371 repo_name,
371 pull_request_id
372 pull_request_id
372 )
373 )
373 payload = {
374 payload = {
374 'type': 'message',
375 'type': 'message',
375 'timestamp': datetime.utcnow(),
376 'timestamp': datetime.utcnow(),
376 'user': 'system',
377 'user': 'system',
377 'exclude_users': [user.username],
378 'exclude_users': [user.username],
378 'channel': channel,
379 'channel': channel,
379 'message': {
380 'message': {
380 'message': message,
381 'message': message,
381 'level': 'info',
382 'level': 'info',
382 'topic': '/notifications'
383 'topic': '/notifications'
383 }
384 }
384 }
385 }
385 channelstream_request(channelstream_config, [payload],
386 channelstream_request(channelstream_config, [payload],
386 '/message', raise_exc=False)
387 '/message', raise_exc=False)
387
388
388 return comment
389 return comment
389
390
390 def delete(self, comment):
391 def delete(self, comment):
391 """
392 """
392 Deletes given comment
393 Deletes given comment
393
394
394 :param comment_id:
395 :param comment_id:
395 """
396 """
396 comment = self.__get_commit_comment(comment)
397 comment = self.__get_commit_comment(comment)
397 Session().delete(comment)
398 Session().delete(comment)
398
399
399 return comment
400 return comment
400
401
401 def get_all_comments(self, repo_id, revision=None, pull_request=None):
402 def get_all_comments(self, repo_id, revision=None, pull_request=None):
402 q = ChangesetComment.query()\
403 q = ChangesetComment.query()\
403 .filter(ChangesetComment.repo_id == repo_id)
404 .filter(ChangesetComment.repo_id == repo_id)
404 if revision:
405 if revision:
405 q = q.filter(ChangesetComment.revision == revision)
406 q = q.filter(ChangesetComment.revision == revision)
406 elif pull_request:
407 elif pull_request:
407 pull_request = self.__get_pull_request(pull_request)
408 pull_request = self.__get_pull_request(pull_request)
408 q = q.filter(ChangesetComment.pull_request == pull_request)
409 q = q.filter(ChangesetComment.pull_request == pull_request)
409 else:
410 else:
410 raise Exception('Please specify commit or pull_request')
411 raise Exception('Please specify commit or pull_request')
411 q = q.order_by(ChangesetComment.created_on)
412 q = q.order_by(ChangesetComment.created_on)
412 return q.all()
413 return q.all()
413
414
414 def get_url(self, comment):
415 def get_url(self, comment):
415 comment = self.__get_commit_comment(comment)
416 comment = self.__get_commit_comment(comment)
416 if comment.pull_request:
417 if comment.pull_request:
417 return h.url(
418 return h.url(
418 'pullrequest_show',
419 'pullrequest_show',
419 repo_name=comment.pull_request.target_repo.repo_name,
420 repo_name=comment.pull_request.target_repo.repo_name,
420 pull_request_id=comment.pull_request.pull_request_id,
421 pull_request_id=comment.pull_request.pull_request_id,
421 anchor='comment-%s' % comment.comment_id,
422 anchor='comment-%s' % comment.comment_id,
422 qualified=True,)
423 qualified=True,)
423 else:
424 else:
424 return h.url(
425 return h.url(
425 'changeset_home',
426 'changeset_home',
426 repo_name=comment.repo.repo_name,
427 repo_name=comment.repo.repo_name,
427 revision=comment.revision,
428 revision=comment.revision,
428 anchor='comment-%s' % comment.comment_id,
429 anchor='comment-%s' % comment.comment_id,
429 qualified=True,)
430 qualified=True,)
430
431
431 def get_comments(self, repo_id, revision=None, pull_request=None):
432 def get_comments(self, repo_id, revision=None, pull_request=None):
432 """
433 """
433 Gets main comments based on revision or pull_request_id
434 Gets main comments based on revision or pull_request_id
434
435
435 :param repo_id:
436 :param repo_id:
436 :param revision:
437 :param revision:
437 :param pull_request:
438 :param pull_request:
438 """
439 """
439
440
440 q = ChangesetComment.query()\
441 q = ChangesetComment.query()\
441 .filter(ChangesetComment.repo_id == repo_id)\
442 .filter(ChangesetComment.repo_id == repo_id)\
442 .filter(ChangesetComment.line_no == None)\
443 .filter(ChangesetComment.line_no == None)\
443 .filter(ChangesetComment.f_path == None)
444 .filter(ChangesetComment.f_path == None)
444 if revision:
445 if revision:
445 q = q.filter(ChangesetComment.revision == revision)
446 q = q.filter(ChangesetComment.revision == revision)
446 elif pull_request:
447 elif pull_request:
447 pull_request = self.__get_pull_request(pull_request)
448 pull_request = self.__get_pull_request(pull_request)
448 q = q.filter(ChangesetComment.pull_request == pull_request)
449 q = q.filter(ChangesetComment.pull_request == pull_request)
449 else:
450 else:
450 raise Exception('Please specify commit or pull_request')
451 raise Exception('Please specify commit or pull_request')
451 q = q.order_by(ChangesetComment.created_on)
452 q = q.order_by(ChangesetComment.created_on)
452 return q.all()
453 return q.all()
453
454
454 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
455 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
455 q = self._get_inline_comments_query(repo_id, revision, pull_request)
456 q = self._get_inline_comments_query(repo_id, revision, pull_request)
456 return self._group_comments_by_path_and_line_number(q)
457 return self._group_comments_by_path_and_line_number(q)
457
458
458 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
459 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
459 version=None):
460 version=None):
460 inline_cnt = 0
461 inline_cnt = 0
461 for fname, per_line_comments in inline_comments.iteritems():
462 for fname, per_line_comments in inline_comments.iteritems():
462 for lno, comments in per_line_comments.iteritems():
463 for lno, comments in per_line_comments.iteritems():
463 for comm in comments:
464 for comm in comments:
464 if not comm.outdated_at_version(version) and skip_outdated:
465 if not comm.outdated_at_version(version) and skip_outdated:
465 inline_cnt += 1
466 inline_cnt += 1
466
467
467 return inline_cnt
468 return inline_cnt
468
469
469 def get_outdated_comments(self, repo_id, pull_request):
470 def get_outdated_comments(self, repo_id, pull_request):
470 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
471 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
471 # of a pull request.
472 # of a pull request.
472 q = self._all_inline_comments_of_pull_request(pull_request)
473 q = self._all_inline_comments_of_pull_request(pull_request)
473 q = q.filter(
474 q = q.filter(
474 ChangesetComment.display_state ==
475 ChangesetComment.display_state ==
475 ChangesetComment.COMMENT_OUTDATED
476 ChangesetComment.COMMENT_OUTDATED
476 ).order_by(ChangesetComment.comment_id.asc())
477 ).order_by(ChangesetComment.comment_id.asc())
477
478
478 return self._group_comments_by_path_and_line_number(q)
479 return self._group_comments_by_path_and_line_number(q)
479
480
480 def _get_inline_comments_query(self, repo_id, revision, pull_request):
481 def _get_inline_comments_query(self, repo_id, revision, pull_request):
481 # TODO: johbo: Split this into two methods: One for PR and one for
482 # TODO: johbo: Split this into two methods: One for PR and one for
482 # commit.
483 # commit.
483 if revision:
484 if revision:
484 q = Session().query(ChangesetComment).filter(
485 q = Session().query(ChangesetComment).filter(
485 ChangesetComment.repo_id == repo_id,
486 ChangesetComment.repo_id == repo_id,
486 ChangesetComment.line_no != null(),
487 ChangesetComment.line_no != null(),
487 ChangesetComment.f_path != null(),
488 ChangesetComment.f_path != null(),
488 ChangesetComment.revision == revision)
489 ChangesetComment.revision == revision)
489
490
490 elif pull_request:
491 elif pull_request:
491 pull_request = self.__get_pull_request(pull_request)
492 pull_request = self.__get_pull_request(pull_request)
492 if not CommentsModel.use_outdated_comments(pull_request):
493 if not CommentsModel.use_outdated_comments(pull_request):
493 q = self._visible_inline_comments_of_pull_request(pull_request)
494 q = self._visible_inline_comments_of_pull_request(pull_request)
494 else:
495 else:
495 q = self._all_inline_comments_of_pull_request(pull_request)
496 q = self._all_inline_comments_of_pull_request(pull_request)
496
497
497 else:
498 else:
498 raise Exception('Please specify commit or pull_request_id')
499 raise Exception('Please specify commit or pull_request_id')
499 q = q.order_by(ChangesetComment.comment_id.asc())
500 q = q.order_by(ChangesetComment.comment_id.asc())
500 return q
501 return q
501
502
502 def _group_comments_by_path_and_line_number(self, q):
503 def _group_comments_by_path_and_line_number(self, q):
503 comments = q.all()
504 comments = q.all()
504 paths = collections.defaultdict(lambda: collections.defaultdict(list))
505 paths = collections.defaultdict(lambda: collections.defaultdict(list))
505 for co in comments:
506 for co in comments:
506 paths[co.f_path][co.line_no].append(co)
507 paths[co.f_path][co.line_no].append(co)
507 return paths
508 return paths
508
509
509 @classmethod
510 @classmethod
510 def needed_extra_diff_context(cls):
511 def needed_extra_diff_context(cls):
511 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
512 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
512
513
513 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
514 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
514 if not CommentsModel.use_outdated_comments(pull_request):
515 if not CommentsModel.use_outdated_comments(pull_request):
515 return
516 return
516
517
517 comments = self._visible_inline_comments_of_pull_request(pull_request)
518 comments = self._visible_inline_comments_of_pull_request(pull_request)
518 comments_to_outdate = comments.all()
519 comments_to_outdate = comments.all()
519
520
520 for comment in comments_to_outdate:
521 for comment in comments_to_outdate:
521 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
522 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
522
523
523 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
524 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
524 diff_line = _parse_comment_line_number(comment.line_no)
525 diff_line = _parse_comment_line_number(comment.line_no)
525
526
526 try:
527 try:
527 old_context = old_diff_proc.get_context_of_line(
528 old_context = old_diff_proc.get_context_of_line(
528 path=comment.f_path, diff_line=diff_line)
529 path=comment.f_path, diff_line=diff_line)
529 new_context = new_diff_proc.get_context_of_line(
530 new_context = new_diff_proc.get_context_of_line(
530 path=comment.f_path, diff_line=diff_line)
531 path=comment.f_path, diff_line=diff_line)
531 except (diffs.LineNotInDiffException,
532 except (diffs.LineNotInDiffException,
532 diffs.FileNotInDiffException):
533 diffs.FileNotInDiffException):
533 comment.display_state = ChangesetComment.COMMENT_OUTDATED
534 comment.display_state = ChangesetComment.COMMENT_OUTDATED
534 return
535 return
535
536
536 if old_context == new_context:
537 if old_context == new_context:
537 return
538 return
538
539
539 if self._should_relocate_diff_line(diff_line):
540 if self._should_relocate_diff_line(diff_line):
540 new_diff_lines = new_diff_proc.find_context(
541 new_diff_lines = new_diff_proc.find_context(
541 path=comment.f_path, context=old_context,
542 path=comment.f_path, context=old_context,
542 offset=self.DIFF_CONTEXT_BEFORE)
543 offset=self.DIFF_CONTEXT_BEFORE)
543 if not new_diff_lines:
544 if not new_diff_lines:
544 comment.display_state = ChangesetComment.COMMENT_OUTDATED
545 comment.display_state = ChangesetComment.COMMENT_OUTDATED
545 else:
546 else:
546 new_diff_line = self._choose_closest_diff_line(
547 new_diff_line = self._choose_closest_diff_line(
547 diff_line, new_diff_lines)
548 diff_line, new_diff_lines)
548 comment.line_no = _diff_to_comment_line_number(new_diff_line)
549 comment.line_no = _diff_to_comment_line_number(new_diff_line)
549 else:
550 else:
550 comment.display_state = ChangesetComment.COMMENT_OUTDATED
551 comment.display_state = ChangesetComment.COMMENT_OUTDATED
551
552
552 def _should_relocate_diff_line(self, diff_line):
553 def _should_relocate_diff_line(self, diff_line):
553 """
554 """
554 Checks if relocation shall be tried for the given `diff_line`.
555 Checks if relocation shall be tried for the given `diff_line`.
555
556
556 If a comment points into the first lines, then we can have a situation
557 If a comment points into the first lines, then we can have a situation
557 that after an update another line has been added on top. In this case
558 that after an update another line has been added on top. In this case
558 we would find the context still and move the comment around. This
559 we would find the context still and move the comment around. This
559 would be wrong.
560 would be wrong.
560 """
561 """
561 should_relocate = (
562 should_relocate = (
562 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
563 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
563 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
564 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
564 return should_relocate
565 return should_relocate
565
566
566 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
567 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
567 candidate = new_diff_lines[0]
568 candidate = new_diff_lines[0]
568 best_delta = _diff_line_delta(diff_line, candidate)
569 best_delta = _diff_line_delta(diff_line, candidate)
569 for new_diff_line in new_diff_lines[1:]:
570 for new_diff_line in new_diff_lines[1:]:
570 delta = _diff_line_delta(diff_line, new_diff_line)
571 delta = _diff_line_delta(diff_line, new_diff_line)
571 if delta < best_delta:
572 if delta < best_delta:
572 candidate = new_diff_line
573 candidate = new_diff_line
573 best_delta = delta
574 best_delta = delta
574 return candidate
575 return candidate
575
576
576 def _visible_inline_comments_of_pull_request(self, pull_request):
577 def _visible_inline_comments_of_pull_request(self, pull_request):
577 comments = self._all_inline_comments_of_pull_request(pull_request)
578 comments = self._all_inline_comments_of_pull_request(pull_request)
578 comments = comments.filter(
579 comments = comments.filter(
579 coalesce(ChangesetComment.display_state, '') !=
580 coalesce(ChangesetComment.display_state, '') !=
580 ChangesetComment.COMMENT_OUTDATED)
581 ChangesetComment.COMMENT_OUTDATED)
581 return comments
582 return comments
582
583
583 def _all_inline_comments_of_pull_request(self, pull_request):
584 def _all_inline_comments_of_pull_request(self, pull_request):
584 comments = Session().query(ChangesetComment)\
585 comments = Session().query(ChangesetComment)\
585 .filter(ChangesetComment.line_no != None)\
586 .filter(ChangesetComment.line_no != None)\
586 .filter(ChangesetComment.f_path != None)\
587 .filter(ChangesetComment.f_path != None)\
587 .filter(ChangesetComment.pull_request == pull_request)
588 .filter(ChangesetComment.pull_request == pull_request)
588 return comments
589 return comments
589
590
590 def _all_general_comments_of_pull_request(self, pull_request):
591 def _all_general_comments_of_pull_request(self, pull_request):
591 comments = Session().query(ChangesetComment)\
592 comments = Session().query(ChangesetComment)\
592 .filter(ChangesetComment.line_no == None)\
593 .filter(ChangesetComment.line_no == None)\
593 .filter(ChangesetComment.f_path == None)\
594 .filter(ChangesetComment.f_path == None)\
594 .filter(ChangesetComment.pull_request == pull_request)
595 .filter(ChangesetComment.pull_request == pull_request)
595 return comments
596 return comments
596
597
597 @staticmethod
598 @staticmethod
598 def use_outdated_comments(pull_request):
599 def use_outdated_comments(pull_request):
599 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
600 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
600 settings = settings_model.get_general_settings()
601 settings = settings_model.get_general_settings()
601 return settings.get('rhodecode_use_outdated_comments', False)
602 return settings.get('rhodecode_use_outdated_comments', False)
602
603
603
604
604 def _parse_comment_line_number(line_no):
605 def _parse_comment_line_number(line_no):
605 """
606 """
606 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
607 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
607 """
608 """
608 old_line = None
609 old_line = None
609 new_line = None
610 new_line = None
610 if line_no.startswith('o'):
611 if line_no.startswith('o'):
611 old_line = int(line_no[1:])
612 old_line = int(line_no[1:])
612 elif line_no.startswith('n'):
613 elif line_no.startswith('n'):
613 new_line = int(line_no[1:])
614 new_line = int(line_no[1:])
614 else:
615 else:
615 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
616 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
616 return diffs.DiffLineNumber(old_line, new_line)
617 return diffs.DiffLineNumber(old_line, new_line)
617
618
618
619
619 def _diff_to_comment_line_number(diff_line):
620 def _diff_to_comment_line_number(diff_line):
620 if diff_line.new is not None:
621 if diff_line.new is not None:
621 return u'n{}'.format(diff_line.new)
622 return u'n{}'.format(diff_line.new)
622 elif diff_line.old is not None:
623 elif diff_line.old is not None:
623 return u'o{}'.format(diff_line.old)
624 return u'o{}'.format(diff_line.old)
624 return u''
625 return u''
625
626
626
627
627 def _diff_line_delta(a, b):
628 def _diff_line_delta(a, b):
628 if None not in (a.new, b.new):
629 if None not in (a.new, b.new):
629 return abs(a.new - b.new)
630 return abs(a.new - b.new)
630 elif None not in (a.old, b.old):
631 elif None not in (a.old, b.old):
631 return abs(a.old - b.old)
632 return abs(a.old - b.old)
632 else:
633 else:
633 raise ValueError(
634 raise ValueError(
634 "Cannot compute delta between {} and {}".format(a, b))
635 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,88 +1,105 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
4
4
5 ## EMAIL SUBJECT
5 <%def name="subject()" filter="n,trim">
6 <%def name="subject()" filter="n,trim">
6 <%
7 <%
7 data = {
8 data = {
8 'user': h.person(user),
9 'user': h.person(user),
9 'repo_name': repo_name,
10 'repo_name': repo_name,
10 'commit_id': h.show_id(commit),
11 'commit_id': h.show_id(commit),
11 'status': status_change,
12 'status': status_change,
12 'comment_file': comment_file,
13 'comment_file': comment_file,
13 'comment_line': comment_line,
14 'comment_line': comment_line,
15 'comment_type': comment_type,
14 }
16 }
15 %>
17 %>
16 ${_('[mention]') if mention else ''} \
18 ${_('[mention]') if mention else ''} \
17
19
18 % if comment_file:
20 % if comment_file:
19 ${_('%(user)s commented on commit `%(commit_id)s` (file: `%(comment_file)s`)') % data} ${_('in the %(repo_name)s repository') % data |n}
21 ${_('%(user)s left %(comment_type)s on commit `%(commit_id)s` (file: `%(comment_file)s`)') % data} ${_('in the %(repo_name)s repository') % data |n}
20 % else:
22 % else:
21 % if status_change:
23 % if status_change:
22 ${_('%(user)s commented on commit `%(commit_id)s` (status: %(status)s)') % data |n} ${_('in the %(repo_name)s repository') % data |n}
24 ${_('%(user)s left %(comment_type)s on commit `%(commit_id)s` (status: %(status)s)') % data |n} ${_('in the %(repo_name)s repository') % data |n}
23 % else:
25 % else:
24 ${_('%(user)s commented on commit `%(commit_id)s`') % data |n} ${_('in the %(repo_name)s repository') % data |n}
26 ${_('%(user)s left %(comment_type)s on commit `%(commit_id)s`') % data |n} ${_('in the %(repo_name)s repository') % data |n}
25 % endif
27 % endif
26 % endif
28 % endif
27
29
28 </%def>
30 </%def>
29
31
32 ## PLAINTEXT VERSION OF BODY
30 <%def name="body_plaintext()" filter="n,trim">
33 <%def name="body_plaintext()" filter="n,trim">
31 <%
34 <%
32 data = {
35 data = {
33 'user': h.person(user),
36 'user': h.person(user),
34 'repo_name': repo_name,
37 'repo_name': repo_name,
35 'commit_id': h.show_id(commit),
38 'commit_id': h.show_id(commit),
36 'status': status_change,
39 'status': status_change,
37 'comment_file': comment_file,
40 'comment_file': comment_file,
38 'comment_line': comment_line,
41 'comment_line': comment_line,
42 'comment_type': comment_type,
39 }
43 }
40 %>
44 %>
41 ${self.subject()}
45 ${self.subject()}
42
46
43 * ${_('Comment link')}: ${commit_comment_url}
47 * ${_('Comment link')}: ${commit_comment_url}
44
48
45 * ${_('Commit')}: ${h.show_id(commit)}
49 * ${_('Commit')}: ${h.show_id(commit)}
46
50
47 %if comment_file:
51 %if comment_file:
48 * ${_('File: %(comment_file)s on line %(comment_line)s') % {'comment_file': comment_file, 'comment_line': comment_line}}
52 * ${_('File: %(comment_file)s on line %(comment_line)s') % data}
49 %endif
53 %endif
50
54
51 ---
55 ---
52
56
53 %if status_change:
57 %if status_change:
54 ${_('Commit status was changed to')}: *${status_change}*
58 ${_('Commit status was changed to')}: *${status_change}*
55 %endif
59 %endif
56
60
57 ${comment_body|n}
61 ${comment_body|n}
58
62
59 ${self.plaintext_footer()}
63 ${self.plaintext_footer()}
60 </%def>
64 </%def>
61
65
62
66
63 <%
67 <%
64 data = {
68 data = {
65 'user': h.person(user),
69 'user': h.person(user),
66 'comment_file': comment_file,
67 'comment_line': comment_line,
68 'repo': commit_target_repo,
70 'repo': commit_target_repo,
69 'repo_name': repo_name,
71 'repo_name': repo_name,
70 'commit_id': h.show_id(commit),
72 'commit_id': h.show_id(commit),
73 'comment_file': comment_file,
74 'comment_line': comment_line,
75 'comment_type': comment_type,
71 }
76 }
72 %>
77 %>
73 <table style="text-align:left;vertical-align:middle;">
78 <table style="text-align:left;vertical-align:middle;">
74 <tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;">
79 <tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;">
80
75 % if comment_file:
81 % if comment_file:
76 <h4><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s commented on commit `%(commit_id)s` (file:`%(comment_file)s`)') % data}</a> ${_('in the %(repo)s repository') % data |n}</h4>
82 <h4><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s commented on commit `%(commit_id)s` (file:`%(comment_file)s`)') % data}</a> ${_('in the %(repo)s repository') % data |n}</h4>
77 % else:
83 % else:
78 <h4><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s commented on commit `%(commit_id)s`') % data |n}</a> ${_('in the %(repo)s repository') % data |n}</h4>
84 <h4><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s commented on commit `%(commit_id)s`') % data |n}</a> ${_('in the %(repo)s repository') % data |n}</h4>
79 % endif
85 % endif
80 </td></tr>
86 </td></tr>
87
81 <tr><td style="padding-right:20px;padding-top:15px;">${_('Commit')}</td><td style="padding-top:15px;"><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${h.show_id(commit)}</a></td></tr>
88 <tr><td style="padding-right:20px;padding-top:15px;">${_('Commit')}</td><td style="padding-top:15px;"><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${h.show_id(commit)}</a></td></tr>
82 <tr><td style="padding-right:20px;">${_('Description')}</td><td style="white-space:pre-wrap">${h.urlify_commit_message(commit.message, repo_name)}</td></tr>
89 <tr><td style="padding-right:20px;">${_('Description')}</td><td style="white-space:pre-wrap">${h.urlify_commit_message(commit.message, repo_name)}</td></tr>
83
90
84 % if status_change:
91 % if status_change:
85 <tr><td style="padding-right:20px;">${_('Status')}</td><td>${_('The commit status was changed to')}: ${base.status_text(status_change, tag_type=status_change_type)}</td></tr>
92 <tr><td style="padding-right:20px;">${_('Status')}</td>
93 <td>${_('The commit status was changed to')}: ${base.status_text(status_change, tag_type=status_change_type)}</td>
94 </tr>
86 % endif
95 % endif
87 <tr><td style="padding-right:20px;">${(_('Comment on line: %(comment_line)s') if comment_file else _('Comment')) % data}</td><td style="line-height:1.2em;white-space:pre-wrap">${h.render(comment_body, renderer=renderer_type, mentions=True)}</td></tr>
96 <tr>
97 <td style="padding-right:20px;">
98 % if comment_type == 'todo':
99 ${(_('TODO comment on line: %(comment_line)s') if comment_file else _('TODO comment')) % data}
100 % else:
101 ${(_('Note comment on line: %(comment_line)s') if comment_file else _('Note comment')) % data}
102 % endif
103 </td>
104 <td style="line-height:1.2em;white-space:pre-wrap">${h.render(comment_body, renderer=renderer_type, mentions=True)}</td></tr>
88 </table>
105 </table>
@@ -1,98 +1,114 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
4
4
5
5 ## EMAIL SUBJECT
6 <%def name="subject()" filter="n,trim">
6 <%def name="subject()" filter="n,trim">
7 <%
7 <%
8 data = {
8 data = {
9 'user': h.person(user),
9 'user': h.person(user),
10 'pr_title': pull_request.title,
10 'pr_title': pull_request.title,
11 'pr_id': pull_request.pull_request_id,
11 'pr_id': pull_request.pull_request_id,
12 'status': status_change,
12 'status': status_change,
13 'comment_file': comment_file,
13 'comment_file': comment_file,
14 'comment_line': comment_line,
14 'comment_line': comment_line,
15 'comment_type': comment_type,
15 }
16 }
16 %>
17 %>
17
18
18 ${_('[mention]') if mention else ''} \
19 ${_('[mention]') if mention else ''} \
19
20
20 % if comment_file:
21 % if comment_file:
21 ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s" (file: `%(comment_file)s`)') % data |n}
22 ${_('%(user)s left %(comment_type)s on pull request #%(pr_id)s "%(pr_title)s" (file: `%(comment_file)s`)') % data |n}
22 % else:
23 % else:
23 % if status_change:
24 % if status_change:
24 ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s" (status: %(status)s)') % data |n}
25 ${_('%(user)s left %(comment_type)s on pull request #%(pr_id)s "%(pr_title)s" (status: %(status)s)') % data |n}
25 % else:
26 % else:
26 ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s"') % data |n}
27 ${_('%(user)s left %(comment_type)s on pull request #%(pr_id)s "%(pr_title)s"') % data |n}
27 % endif
28 % endif
28 % endif
29 % endif
29 </%def>
30 </%def>
30
31
32 ## PLAINTEXT VERSION OF BODY
31 <%def name="body_plaintext()" filter="n,trim">
33 <%def name="body_plaintext()" filter="n,trim">
32 <%
34 <%
33 data = {
35 data = {
34 'user': h.person(user),
36 'user': h.person(user),
35 'pr_title': pull_request.title,
37 'pr_title': pull_request.title,
36 'pr_id': pull_request.pull_request_id,
38 'pr_id': pull_request.pull_request_id,
37 'status': status_change,
39 'status': status_change,
38 'comment_file': comment_file,
40 'comment_file': comment_file,
39 'comment_line': comment_line,
41 'comment_line': comment_line,
42 'comment_type': comment_type,
40 }
43 }
41 %>
44 %>
42 ${self.subject()}
45 ${self.subject()}
43
46
44 * ${_('Comment link')}: ${pr_comment_url}
47 * ${_('Comment link')}: ${pr_comment_url}
45
48
46 * ${_('Source repository')}: ${pr_source_repo_url}
49 * ${_('Source repository')}: ${pr_source_repo_url}
47
50
48 %if comment_file:
51 %if comment_file:
49 * ${_('File: %(comment_file)s on line %(comment_line)s') % {'comment_file': comment_file, 'comment_line': comment_line}}
52 * ${_('File: %(comment_file)s on line %(comment_line)s') % {'comment_file': comment_file, 'comment_line': comment_line}}
50 %endif
53 %endif
51
54
52 ---
55 ---
53
56
54 %if status_change and not closing_pr:
57 %if status_change and not closing_pr:
55 ${_('%(user)s submitted pull request #%(pr_id)s status: *%(status)s*') % data}
58 ${_('%(user)s submitted pull request #%(pr_id)s status: *%(status)s*') % data}
56 %elif status_change and closing_pr:
59 %elif status_change and closing_pr:
57 ${_('%(user)s submitted pull request #%(pr_id)s status: *%(status)s and closed*') % data}
60 ${_('%(user)s submitted pull request #%(pr_id)s status: *%(status)s and closed*') % data}
58 %endif
61 %endif
59
62
60 ${comment_body|n}
63 ${comment_body|n}
61
64
62 ${self.plaintext_footer()}
65 ${self.plaintext_footer()}
63 </%def>
66 </%def>
64
67
65
68
66 <%
69 <%
67 data = {
70 data = {
68 'user': h.person(user),
71 'user': h.person(user),
69 'pr_title': pull_request.title,
72 'pr_title': pull_request.title,
70 'pr_id': pull_request.pull_request_id,
73 'pr_id': pull_request.pull_request_id,
71 'status': status_change,
74 'status': status_change,
72 'comment_file': comment_file,
75 'comment_file': comment_file,
73 'comment_line': comment_line,
76 'comment_line': comment_line,
77 'comment_type': comment_type,
74 }
78 }
75 %>
79 %>
76 <table style="text-align:left;vertical-align:middle;">
80 <table style="text-align:left;vertical-align:middle;">
77 <tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;">
81 <tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;">
78 <h4><a href="${pr_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">
79
82
80 % if comment_file:
83 % if comment_file:
81 ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s" (file:`%(comment_file)s`)') % data |n}
84 <h4><a href="${pr_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s" (file:`%(comment_file)s`)') % data |n}</a></h4>
82 % else:
85 % else:
83 ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s"') % data |n}
86 <h4><a href="${pr_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s"') % data |n}</a></h4>
84 % endif
87 % endif
85 </a>
88
86 %if status_change and not closing_pr:
87 , ${_('submitted pull request status: %(status)s') % data}
88 %elif status_change and closing_pr:
89 , ${_('submitted pull request status: %(status)s and closed') % data}
90 %endif
91 </h4>
92 </td></tr>
89 </td></tr>
93 <tr><td style="padding-right:20px;padding-top:15px;">${_('Source')}</td><td style="padding-top:15px;"><a style="color:#427cc9;text-decoration:none;cursor:pointer" href="${pr_source_repo_url}">${pr_source_repo.repo_name}</a></td></tr>
90 <tr><td style="padding-right:20px;padding-top:15px;">${_('Source')}</td><td style="padding-top:15px;"><a style="color:#427cc9;text-decoration:none;cursor:pointer" href="${pr_source_repo_url}">${pr_source_repo.repo_name}</a></td></tr>
91
94 % if status_change:
92 % if status_change:
95 <tr><td style="padding-right:20px;">${_('Submitted status')}</td><td>${base.status_text(status_change, tag_type=status_change_type)}</td></tr>
93 <tr>
94 <td style="padding-right:20px;">${_('Status')}</td>
95 <td>
96 % if closing_pr:
97 ${_('Closed pull request with status')}: ${base.status_text(status_change, tag_type=status_change_type)}
98 % else:
99 ${_('Submitted review status')}: ${base.status_text(status_change, tag_type=status_change_type)}
100 % endif
101 </td>
102 </tr>
96 % endif
103 % endif
97 <tr><td style="padding-right:20px;">${(_('Comment on line: %(comment_line)s') if comment_file else _('Comment')) % data}</td><td style="line-height:1.2em;white-space:pre-wrap">${h.render(comment_body, renderer=renderer_type, mentions=True)}</td></tr>
104 <tr>
105 <td style="padding-right:20px;">
106 % if comment_type == 'todo':
107 ${(_('TODO comment on line: %(comment_line)s') if comment_file else _('TODO comment')) % data}
108 % else:
109 ${(_('Note comment on line: %(comment_line)s') if comment_file else _('Note comment')) % data}
110 % endif
111 </td>
112 <td style="line-height:1.2em;white-space:pre-wrap">${h.render(comment_body, renderer=renderer_type, mentions=True)}</td>
113 </tr>
98 </table>
114 </table>
@@ -1,283 +1,288 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 from pylons.i18n import ungettext
21 from pylons.i18n import ungettext
22 import pytest
22 import pytest
23
23
24 from rhodecode.tests import *
24 from rhodecode.tests import *
25 from rhodecode.model.db import (
25 from rhodecode.model.db import (
26 ChangesetComment, Notification, UserNotification)
26 ChangesetComment, Notification, UserNotification)
27 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
28 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
29
29
30
30
31 @pytest.mark.backends("git", "hg", "svn")
31 @pytest.mark.backends("git", "hg", "svn")
32 class TestCommitCommentsController(TestController):
32 class TestCommitCommentsController(TestController):
33
33
34 @pytest.fixture(autouse=True)
34 @pytest.fixture(autouse=True)
35 def prepare(self, request, pylonsapp):
35 def prepare(self, request, pylonsapp):
36 for x in ChangesetComment.query().all():
36 for x in ChangesetComment.query().all():
37 Session().delete(x)
37 Session().delete(x)
38 Session().commit()
38 Session().commit()
39
39
40 for x in Notification.query().all():
40 for x in Notification.query().all():
41 Session().delete(x)
41 Session().delete(x)
42 Session().commit()
42 Session().commit()
43
43
44 request.addfinalizer(self.cleanup)
44 request.addfinalizer(self.cleanup)
45
45
46 def cleanup(self):
46 def cleanup(self):
47 for x in ChangesetComment.query().all():
47 for x in ChangesetComment.query().all():
48 Session().delete(x)
48 Session().delete(x)
49 Session().commit()
49 Session().commit()
50
50
51 for x in Notification.query().all():
51 for x in Notification.query().all():
52 Session().delete(x)
52 Session().delete(x)
53 Session().commit()
53 Session().commit()
54
54
55 def test_create(self, backend):
55 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
56 def test_create(self, comment_type, backend):
56 self.log_user()
57 self.log_user()
57 commit = backend.repo.get_commit('300')
58 commit = backend.repo.get_commit('300')
58 commit_id = commit.raw_id
59 commit_id = commit.raw_id
59 text = u'CommentOnCommit'
60 text = u'CommentOnCommit'
60
61
61 params = {'text': text, 'csrf_token': self.csrf_token}
62 params = {'text': text, 'csrf_token': self.csrf_token,
63 'comment_type': comment_type}
62 self.app.post(
64 self.app.post(
63 url(controller='changeset', action='comment',
65 url(controller='changeset', action='comment',
64 repo_name=backend.repo_name, revision=commit_id), params=params)
66 repo_name=backend.repo_name, revision=commit_id), params=params)
65
67
66 response = self.app.get(
68 response = self.app.get(
67 url(controller='changeset', action='index',
69 url(controller='changeset', action='index',
68 repo_name=backend.repo_name, revision=commit_id))
70 repo_name=backend.repo_name, revision=commit_id))
69
71
70 # test DB
72 # test DB
71 assert ChangesetComment.query().count() == 1
73 assert ChangesetComment.query().count() == 1
72 assert_comment_links(response, ChangesetComment.query().count(), 0)
74 assert_comment_links(response, ChangesetComment.query().count(), 0)
73
75
74 assert Notification.query().count() == 1
76 assert Notification.query().count() == 1
75 assert ChangesetComment.query().count() == 1
77 assert ChangesetComment.query().count() == 1
76
78
77 notification = Notification.query().all()[0]
79 notification = Notification.query().all()[0]
78
80
79 comment_id = ChangesetComment.query().first().comment_id
81 comment_id = ChangesetComment.query().first().comment_id
80 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
82 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
81
83
82 sbj = 'commented on commit `{0}` in the {1} repository'.format(
84 sbj = 'left {0} on commit `{1}` in the {2} repository'.format(
83 h.show_id(commit), backend.repo_name)
85 comment_type, h.show_id(commit), backend.repo_name)
84 assert sbj in notification.subject
86 assert sbj in notification.subject
85
87
86 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
88 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
87 backend.repo_name, commit_id, comment_id))
89 backend.repo_name, commit_id, comment_id))
88 assert lnk in notification.body
90 assert lnk in notification.body
89
91
90 def test_create_inline(self, backend):
92 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
93 def test_create_inline(self, comment_type, backend):
91 self.log_user()
94 self.log_user()
92 commit = backend.repo.get_commit('300')
95 commit = backend.repo.get_commit('300')
93 commit_id = commit.raw_id
96 commit_id = commit.raw_id
94 text = u'CommentOnCommit'
97 text = u'CommentOnCommit'
95 f_path = 'vcs/web/simplevcs/views/repository.py'
98 f_path = 'vcs/web/simplevcs/views/repository.py'
96 line = 'n1'
99 line = 'n1'
97
100
98 params = {'text': text, 'f_path': f_path, 'line': line,
101 params = {'text': text, 'f_path': f_path, 'line': line,
102 'comment_type': comment_type,
99 'csrf_token': self.csrf_token}
103 'csrf_token': self.csrf_token}
100
104
101 self.app.post(
105 self.app.post(
102 url(controller='changeset', action='comment',
106 url(controller='changeset', action='comment',
103 repo_name=backend.repo_name, revision=commit_id), params=params)
107 repo_name=backend.repo_name, revision=commit_id), params=params)
104
108
105 response = self.app.get(
109 response = self.app.get(
106 url(controller='changeset', action='index',
110 url(controller='changeset', action='index',
107 repo_name=backend.repo_name, revision=commit_id))
111 repo_name=backend.repo_name, revision=commit_id))
108
112
109 # test DB
113 # test DB
110 assert ChangesetComment.query().count() == 1
114 assert ChangesetComment.query().count() == 1
111 assert_comment_links(response, 0, ChangesetComment.query().count())
115 assert_comment_links(response, 0, ChangesetComment.query().count())
112
116
113 if backend.alias == 'svn':
117 if backend.alias == 'svn':
114 response.mustcontain(
118 response.mustcontain(
115 '''data-f-path="vcs/commands/summary.py" '''
119 '''data-f-path="vcs/commands/summary.py" '''
116 '''id="a_c--ad05457a43f8"'''
120 '''id="a_c--ad05457a43f8"'''
117 )
121 )
118 else:
122 else:
119 response.mustcontain(
123 response.mustcontain(
120 '''data-f-path="vcs/backends/hg.py" '''
124 '''data-f-path="vcs/backends/hg.py" '''
121 '''id="a_c--9c390eb52cd6"'''
125 '''id="a_c--9c390eb52cd6"'''
122 )
126 )
123
127
124 assert Notification.query().count() == 1
128 assert Notification.query().count() == 1
125 assert ChangesetComment.query().count() == 1
129 assert ChangesetComment.query().count() == 1
126
130
127 notification = Notification.query().all()[0]
131 notification = Notification.query().all()[0]
128 comment = ChangesetComment.query().first()
132 comment = ChangesetComment.query().first()
129 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
133 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
130
134
131 assert comment.revision == commit_id
135 assert comment.revision == commit_id
132 sbj = 'commented on commit `{commit}` ' \
136 sbj = 'left {comment_type} on commit `{commit}` ' \
133 '(file: `{f_path}`) in the {repo} repository'.format(
137 '(file: `{f_path}`) in the {repo} repository'.format(
134 commit=h.show_id(commit),
138 commit=h.show_id(commit),
135 f_path=f_path, line=line, repo=backend.repo_name)
139 f_path=f_path, line=line, repo=backend.repo_name,
140 comment_type=comment_type)
136 assert sbj in notification.subject
141 assert sbj in notification.subject
137
142
138 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
143 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
139 backend.repo_name, commit_id, comment.comment_id))
144 backend.repo_name, commit_id, comment.comment_id))
140 assert lnk in notification.body
145 assert lnk in notification.body
141 assert 'on line n1' in notification.body
146 assert 'on line n1' in notification.body
142
147
143 def test_create_with_mention(self, backend):
148 def test_create_with_mention(self, backend):
144 self.log_user()
149 self.log_user()
145
150
146 commit_id = backend.repo.get_commit('300').raw_id
151 commit_id = backend.repo.get_commit('300').raw_id
147 text = u'@test_regular check CommentOnCommit'
152 text = u'@test_regular check CommentOnCommit'
148
153
149 params = {'text': text, 'csrf_token': self.csrf_token}
154 params = {'text': text, 'csrf_token': self.csrf_token}
150 self.app.post(
155 self.app.post(
151 url(controller='changeset', action='comment',
156 url(controller='changeset', action='comment',
152 repo_name=backend.repo_name, revision=commit_id), params=params)
157 repo_name=backend.repo_name, revision=commit_id), params=params)
153
158
154 response = self.app.get(
159 response = self.app.get(
155 url(controller='changeset', action='index',
160 url(controller='changeset', action='index',
156 repo_name=backend.repo_name, revision=commit_id))
161 repo_name=backend.repo_name, revision=commit_id))
157 # test DB
162 # test DB
158 assert ChangesetComment.query().count() == 1
163 assert ChangesetComment.query().count() == 1
159 assert_comment_links(response, ChangesetComment.query().count(), 0)
164 assert_comment_links(response, ChangesetComment.query().count(), 0)
160
165
161 notification = Notification.query().one()
166 notification = Notification.query().one()
162
167
163 assert len(notification.recipients) == 2
168 assert len(notification.recipients) == 2
164 users = [x.username for x in notification.recipients]
169 users = [x.username for x in notification.recipients]
165
170
166 # test_regular gets notification by @mention
171 # test_regular gets notification by @mention
167 assert sorted(users) == [u'test_admin', u'test_regular']
172 assert sorted(users) == [u'test_admin', u'test_regular']
168
173
169 def test_create_with_status_change(self, backend):
174 def test_create_with_status_change(self, backend):
170 self.log_user()
175 self.log_user()
171 commit = backend.repo.get_commit('300')
176 commit = backend.repo.get_commit('300')
172 commit_id = commit.raw_id
177 commit_id = commit.raw_id
173 text = u'CommentOnCommit'
178 text = u'CommentOnCommit'
174 f_path = 'vcs/web/simplevcs/views/repository.py'
179 f_path = 'vcs/web/simplevcs/views/repository.py'
175 line = 'n1'
180 line = 'n1'
176
181
177 params = {'text': text, 'changeset_status': 'approved',
182 params = {'text': text, 'changeset_status': 'approved',
178 'csrf_token': self.csrf_token}
183 'csrf_token': self.csrf_token}
179
184
180 self.app.post(
185 self.app.post(
181 url(controller='changeset', action='comment',
186 url(controller='changeset', action='comment',
182 repo_name=backend.repo_name, revision=commit_id), params=params)
187 repo_name=backend.repo_name, revision=commit_id), params=params)
183
188
184 response = self.app.get(
189 response = self.app.get(
185 url(controller='changeset', action='index',
190 url(controller='changeset', action='index',
186 repo_name=backend.repo_name, revision=commit_id))
191 repo_name=backend.repo_name, revision=commit_id))
187
192
188 # test DB
193 # test DB
189 assert ChangesetComment.query().count() == 1
194 assert ChangesetComment.query().count() == 1
190 assert_comment_links(response, ChangesetComment.query().count(), 0)
195 assert_comment_links(response, ChangesetComment.query().count(), 0)
191
196
192 assert Notification.query().count() == 1
197 assert Notification.query().count() == 1
193 assert ChangesetComment.query().count() == 1
198 assert ChangesetComment.query().count() == 1
194
199
195 notification = Notification.query().all()[0]
200 notification = Notification.query().all()[0]
196
201
197 comment_id = ChangesetComment.query().first().comment_id
202 comment_id = ChangesetComment.query().first().comment_id
198 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
203 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
199
204
200 sbj = 'commented on commit `{0}` (status: Approved) ' \
205 sbj = 'left note on commit `{0}` (status: Approved) ' \
201 'in the {1} repository'.format(
206 'in the {1} repository'.format(
202 h.show_id(commit), backend.repo_name)
207 h.show_id(commit), backend.repo_name)
203 assert sbj in notification.subject
208 assert sbj in notification.subject
204
209
205 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
210 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
206 backend.repo_name, commit_id, comment_id))
211 backend.repo_name, commit_id, comment_id))
207 assert lnk in notification.body
212 assert lnk in notification.body
208
213
209 def test_delete(self, backend):
214 def test_delete(self, backend):
210 self.log_user()
215 self.log_user()
211 commit_id = backend.repo.get_commit('300').raw_id
216 commit_id = backend.repo.get_commit('300').raw_id
212 text = u'CommentOnCommit'
217 text = u'CommentOnCommit'
213
218
214 params = {'text': text, 'csrf_token': self.csrf_token}
219 params = {'text': text, 'csrf_token': self.csrf_token}
215 self.app.post(
220 self.app.post(
216 url(
221 url(
217 controller='changeset', action='comment',
222 controller='changeset', action='comment',
218 repo_name=backend.repo_name, revision=commit_id),
223 repo_name=backend.repo_name, revision=commit_id),
219 params=params)
224 params=params)
220
225
221 comments = ChangesetComment.query().all()
226 comments = ChangesetComment.query().all()
222 assert len(comments) == 1
227 assert len(comments) == 1
223 comment_id = comments[0].comment_id
228 comment_id = comments[0].comment_id
224
229
225 self.app.post(
230 self.app.post(
226 url(controller='changeset', action='delete_comment',
231 url(controller='changeset', action='delete_comment',
227 repo_name=backend.repo_name, comment_id=comment_id),
232 repo_name=backend.repo_name, comment_id=comment_id),
228 params={'_method': 'delete', 'csrf_token': self.csrf_token})
233 params={'_method': 'delete', 'csrf_token': self.csrf_token})
229
234
230 comments = ChangesetComment.query().all()
235 comments = ChangesetComment.query().all()
231 assert len(comments) == 0
236 assert len(comments) == 0
232
237
233 response = self.app.get(
238 response = self.app.get(
234 url(controller='changeset', action='index',
239 url(controller='changeset', action='index',
235 repo_name=backend.repo_name, revision=commit_id))
240 repo_name=backend.repo_name, revision=commit_id))
236 assert_comment_links(response, 0, 0)
241 assert_comment_links(response, 0, 0)
237
242
238 @pytest.mark.parametrize('renderer, input, output', [
243 @pytest.mark.parametrize('renderer, input, output', [
239 ('rst', 'plain text', '<p>plain text</p>'),
244 ('rst', 'plain text', '<p>plain text</p>'),
240 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
245 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
241 ('rst', '*italics*', '<em>italics</em>'),
246 ('rst', '*italics*', '<em>italics</em>'),
242 ('rst', '**bold**', '<strong>bold</strong>'),
247 ('rst', '**bold**', '<strong>bold</strong>'),
243 ('markdown', 'plain text', '<p>plain text</p>'),
248 ('markdown', 'plain text', '<p>plain text</p>'),
244 ('markdown', '# header', '<h1>header</h1>'),
249 ('markdown', '# header', '<h1>header</h1>'),
245 ('markdown', '*italics*', '<em>italics</em>'),
250 ('markdown', '*italics*', '<em>italics</em>'),
246 ('markdown', '**bold**', '<strong>bold</strong>'),
251 ('markdown', '**bold**', '<strong>bold</strong>'),
247 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
252 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
248 'md-header', 'md-italics', 'md-bold', ])
253 'md-header', 'md-italics', 'md-bold', ])
249 def test_preview(self, renderer, input, output, backend):
254 def test_preview(self, renderer, input, output, backend):
250 self.log_user()
255 self.log_user()
251 params = {
256 params = {
252 'renderer': renderer,
257 'renderer': renderer,
253 'text': input,
258 'text': input,
254 'csrf_token': self.csrf_token
259 'csrf_token': self.csrf_token
255 }
260 }
256 environ = {
261 environ = {
257 'HTTP_X_PARTIAL_XHR': 'true'
262 'HTTP_X_PARTIAL_XHR': 'true'
258 }
263 }
259 response = self.app.post(
264 response = self.app.post(
260 url(controller='changeset',
265 url(controller='changeset',
261 action='preview_comment',
266 action='preview_comment',
262 repo_name=backend.repo_name),
267 repo_name=backend.repo_name),
263 params=params,
268 params=params,
264 extra_environ=environ)
269 extra_environ=environ)
265
270
266 response.mustcontain(output)
271 response.mustcontain(output)
267
272
268
273
269 def assert_comment_links(response, comments, inline_comments):
274 def assert_comment_links(response, comments, inline_comments):
270 comments_text = ungettext("%d Commit comment",
275 comments_text = ungettext("%d Commit comment",
271 "%d Commit comments", comments) % comments
276 "%d Commit comments", comments) % comments
272 if comments:
277 if comments:
273 response.mustcontain('<a href="#comments">%s</a>,' % comments_text)
278 response.mustcontain('<a href="#comments">%s</a>,' % comments_text)
274 else:
279 else:
275 response.mustcontain(comments_text)
280 response.mustcontain(comments_text)
276
281
277 inline_comments_text = ungettext("%d Inline Comment", "%d Inline Comments",
282 inline_comments_text = ungettext("%d Inline Comment", "%d Inline Comments",
278 inline_comments) % inline_comments
283 inline_comments) % inline_comments
279 if inline_comments:
284 if inline_comments:
280 response.mustcontain(
285 response.mustcontain(
281 'id="inline-comments-counter">%s</' % inline_comments_text)
286 'id="inline-comments-counter">%s</' % inline_comments_text)
282 else:
287 else:
283 response.mustcontain(inline_comments_text)
288 response.mustcontain(inline_comments_text)
General Comments 0
You need to be logged in to leave comments. Login now