##// END OF EJS Templates
reviewers: use common function to obtain reviewers with filter of role.
marcink -
r4514:96651613 stable
parent child
Show More
@@ -1,821 +1,821
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 import datetime
24 import datetime
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28 import collections
28 import collections
29
29
30 from pyramid.threadlocal import get_current_registry, get_current_request
30 from pyramid.threadlocal import get_current_registry, get_current_request
31 from sqlalchemy.sql.expression import null
31 from sqlalchemy.sql.expression import null
32 from sqlalchemy.sql.functions import coalesce
32 from sqlalchemy.sql.functions import coalesce
33
33
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
35 from rhodecode.lib import audit_logger
35 from rhodecode.lib import audit_logger
36 from rhodecode.lib.exceptions import CommentVersionMismatch
36 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
38 from rhodecode.model import BaseModel
38 from rhodecode.model import BaseModel
39 from rhodecode.model.db import (
39 from rhodecode.model.db import (
40 ChangesetComment,
40 ChangesetComment,
41 User,
41 User,
42 Notification,
42 Notification,
43 PullRequest,
43 PullRequest,
44 AttributeDict,
44 AttributeDict,
45 ChangesetCommentHistory,
45 ChangesetCommentHistory,
46 )
46 )
47 from rhodecode.model.notification import NotificationModel
47 from rhodecode.model.notification import NotificationModel
48 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
49 from rhodecode.model.settings import VcsSettingsModel
49 from rhodecode.model.settings import VcsSettingsModel
50 from rhodecode.model.notification import EmailNotificationModel
50 from rhodecode.model.notification import EmailNotificationModel
51 from rhodecode.model.validation_schema.schemas import comment_schema
51 from rhodecode.model.validation_schema.schemas import comment_schema
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class CommentsModel(BaseModel):
57 class CommentsModel(BaseModel):
58
58
59 cls = ChangesetComment
59 cls = ChangesetComment
60
60
61 DIFF_CONTEXT_BEFORE = 3
61 DIFF_CONTEXT_BEFORE = 3
62 DIFF_CONTEXT_AFTER = 3
62 DIFF_CONTEXT_AFTER = 3
63
63
64 def __get_commit_comment(self, changeset_comment):
64 def __get_commit_comment(self, changeset_comment):
65 return self._get_instance(ChangesetComment, changeset_comment)
65 return self._get_instance(ChangesetComment, changeset_comment)
66
66
67 def __get_pull_request(self, pull_request):
67 def __get_pull_request(self, pull_request):
68 return self._get_instance(PullRequest, pull_request)
68 return self._get_instance(PullRequest, pull_request)
69
69
70 def _extract_mentions(self, s):
70 def _extract_mentions(self, s):
71 user_objects = []
71 user_objects = []
72 for username in extract_mentioned_users(s):
72 for username in extract_mentioned_users(s):
73 user_obj = User.get_by_username(username, case_insensitive=True)
73 user_obj = User.get_by_username(username, case_insensitive=True)
74 if user_obj:
74 if user_obj:
75 user_objects.append(user_obj)
75 user_objects.append(user_obj)
76 return user_objects
76 return user_objects
77
77
78 def _get_renderer(self, global_renderer='rst', request=None):
78 def _get_renderer(self, global_renderer='rst', request=None):
79 request = request or get_current_request()
79 request = request or get_current_request()
80
80
81 try:
81 try:
82 global_renderer = request.call_context.visual.default_renderer
82 global_renderer = request.call_context.visual.default_renderer
83 except AttributeError:
83 except AttributeError:
84 log.debug("Renderer not set, falling back "
84 log.debug("Renderer not set, falling back "
85 "to default renderer '%s'", global_renderer)
85 "to default renderer '%s'", global_renderer)
86 except Exception:
86 except Exception:
87 log.error(traceback.format_exc())
87 log.error(traceback.format_exc())
88 return global_renderer
88 return global_renderer
89
89
90 def aggregate_comments(self, comments, versions, show_version, inline=False):
90 def aggregate_comments(self, comments, versions, show_version, inline=False):
91 # group by versions, and count until, and display objects
91 # group by versions, and count until, and display objects
92
92
93 comment_groups = collections.defaultdict(list)
93 comment_groups = collections.defaultdict(list)
94 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
94 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
95
95
96 def yield_comments(pos):
96 def yield_comments(pos):
97 for co in comment_groups[pos]:
97 for co in comment_groups[pos]:
98 yield co
98 yield co
99
99
100 comment_versions = collections.defaultdict(
100 comment_versions = collections.defaultdict(
101 lambda: collections.defaultdict(list))
101 lambda: collections.defaultdict(list))
102 prev_prvid = -1
102 prev_prvid = -1
103 # fake last entry with None, to aggregate on "latest" version which
103 # fake last entry with None, to aggregate on "latest" version which
104 # doesn't have an pull_request_version_id
104 # doesn't have an pull_request_version_id
105 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
105 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
106 prvid = ver.pull_request_version_id
106 prvid = ver.pull_request_version_id
107 if prev_prvid == -1:
107 if prev_prvid == -1:
108 prev_prvid = prvid
108 prev_prvid = prvid
109
109
110 for co in yield_comments(prvid):
110 for co in yield_comments(prvid):
111 comment_versions[prvid]['at'].append(co)
111 comment_versions[prvid]['at'].append(co)
112
112
113 # save until
113 # save until
114 current = comment_versions[prvid]['at']
114 current = comment_versions[prvid]['at']
115 prev_until = comment_versions[prev_prvid]['until']
115 prev_until = comment_versions[prev_prvid]['until']
116 cur_until = prev_until + current
116 cur_until = prev_until + current
117 comment_versions[prvid]['until'].extend(cur_until)
117 comment_versions[prvid]['until'].extend(cur_until)
118
118
119 # save outdated
119 # save outdated
120 if inline:
120 if inline:
121 outdated = [x for x in cur_until
121 outdated = [x for x in cur_until
122 if x.outdated_at_version(show_version)]
122 if x.outdated_at_version(show_version)]
123 else:
123 else:
124 outdated = [x for x in cur_until
124 outdated = [x for x in cur_until
125 if x.older_than_version(show_version)]
125 if x.older_than_version(show_version)]
126 display = [x for x in cur_until if x not in outdated]
126 display = [x for x in cur_until if x not in outdated]
127
127
128 comment_versions[prvid]['outdated'] = outdated
128 comment_versions[prvid]['outdated'] = outdated
129 comment_versions[prvid]['display'] = display
129 comment_versions[prvid]['display'] = display
130
130
131 prev_prvid = prvid
131 prev_prvid = prvid
132
132
133 return comment_versions
133 return comment_versions
134
134
135 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
135 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
136 qry = Session().query(ChangesetComment) \
136 qry = Session().query(ChangesetComment) \
137 .filter(ChangesetComment.repo == repo)
137 .filter(ChangesetComment.repo == repo)
138
138
139 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
139 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
140 qry = qry.filter(ChangesetComment.comment_type == comment_type)
140 qry = qry.filter(ChangesetComment.comment_type == comment_type)
141
141
142 if user:
142 if user:
143 user = self._get_user(user)
143 user = self._get_user(user)
144 if user:
144 if user:
145 qry = qry.filter(ChangesetComment.user_id == user.user_id)
145 qry = qry.filter(ChangesetComment.user_id == user.user_id)
146
146
147 if commit_id:
147 if commit_id:
148 qry = qry.filter(ChangesetComment.revision == commit_id)
148 qry = qry.filter(ChangesetComment.revision == commit_id)
149
149
150 qry = qry.order_by(ChangesetComment.created_on)
150 qry = qry.order_by(ChangesetComment.created_on)
151 return qry.all()
151 return qry.all()
152
152
153 def get_repository_unresolved_todos(self, repo):
153 def get_repository_unresolved_todos(self, repo):
154 todos = Session().query(ChangesetComment) \
154 todos = Session().query(ChangesetComment) \
155 .filter(ChangesetComment.repo == repo) \
155 .filter(ChangesetComment.repo == repo) \
156 .filter(ChangesetComment.resolved_by == None) \
156 .filter(ChangesetComment.resolved_by == None) \
157 .filter(ChangesetComment.comment_type
157 .filter(ChangesetComment.comment_type
158 == ChangesetComment.COMMENT_TYPE_TODO)
158 == ChangesetComment.COMMENT_TYPE_TODO)
159 todos = todos.all()
159 todos = todos.all()
160
160
161 return todos
161 return todos
162
162
163 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
163 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
164
164
165 todos = Session().query(ChangesetComment) \
165 todos = Session().query(ChangesetComment) \
166 .filter(ChangesetComment.pull_request == pull_request) \
166 .filter(ChangesetComment.pull_request == pull_request) \
167 .filter(ChangesetComment.resolved_by == None) \
167 .filter(ChangesetComment.resolved_by == None) \
168 .filter(ChangesetComment.comment_type
168 .filter(ChangesetComment.comment_type
169 == ChangesetComment.COMMENT_TYPE_TODO)
169 == ChangesetComment.COMMENT_TYPE_TODO)
170
170
171 if not show_outdated:
171 if not show_outdated:
172 todos = todos.filter(
172 todos = todos.filter(
173 coalesce(ChangesetComment.display_state, '') !=
173 coalesce(ChangesetComment.display_state, '') !=
174 ChangesetComment.COMMENT_OUTDATED)
174 ChangesetComment.COMMENT_OUTDATED)
175
175
176 todos = todos.all()
176 todos = todos.all()
177
177
178 return todos
178 return todos
179
179
180 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
180 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
181
181
182 todos = Session().query(ChangesetComment) \
182 todos = Session().query(ChangesetComment) \
183 .filter(ChangesetComment.pull_request == pull_request) \
183 .filter(ChangesetComment.pull_request == pull_request) \
184 .filter(ChangesetComment.resolved_by != None) \
184 .filter(ChangesetComment.resolved_by != None) \
185 .filter(ChangesetComment.comment_type
185 .filter(ChangesetComment.comment_type
186 == ChangesetComment.COMMENT_TYPE_TODO)
186 == ChangesetComment.COMMENT_TYPE_TODO)
187
187
188 if not show_outdated:
188 if not show_outdated:
189 todos = todos.filter(
189 todos = todos.filter(
190 coalesce(ChangesetComment.display_state, '') !=
190 coalesce(ChangesetComment.display_state, '') !=
191 ChangesetComment.COMMENT_OUTDATED)
191 ChangesetComment.COMMENT_OUTDATED)
192
192
193 todos = todos.all()
193 todos = todos.all()
194
194
195 return todos
195 return todos
196
196
197 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
197 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
198
198
199 todos = Session().query(ChangesetComment) \
199 todos = Session().query(ChangesetComment) \
200 .filter(ChangesetComment.revision == commit_id) \
200 .filter(ChangesetComment.revision == commit_id) \
201 .filter(ChangesetComment.resolved_by == None) \
201 .filter(ChangesetComment.resolved_by == None) \
202 .filter(ChangesetComment.comment_type
202 .filter(ChangesetComment.comment_type
203 == ChangesetComment.COMMENT_TYPE_TODO)
203 == ChangesetComment.COMMENT_TYPE_TODO)
204
204
205 if not show_outdated:
205 if not show_outdated:
206 todos = todos.filter(
206 todos = todos.filter(
207 coalesce(ChangesetComment.display_state, '') !=
207 coalesce(ChangesetComment.display_state, '') !=
208 ChangesetComment.COMMENT_OUTDATED)
208 ChangesetComment.COMMENT_OUTDATED)
209
209
210 todos = todos.all()
210 todos = todos.all()
211
211
212 return todos
212 return todos
213
213
214 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
214 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
215
215
216 todos = Session().query(ChangesetComment) \
216 todos = Session().query(ChangesetComment) \
217 .filter(ChangesetComment.revision == commit_id) \
217 .filter(ChangesetComment.revision == commit_id) \
218 .filter(ChangesetComment.resolved_by != None) \
218 .filter(ChangesetComment.resolved_by != None) \
219 .filter(ChangesetComment.comment_type
219 .filter(ChangesetComment.comment_type
220 == ChangesetComment.COMMENT_TYPE_TODO)
220 == ChangesetComment.COMMENT_TYPE_TODO)
221
221
222 if not show_outdated:
222 if not show_outdated:
223 todos = todos.filter(
223 todos = todos.filter(
224 coalesce(ChangesetComment.display_state, '') !=
224 coalesce(ChangesetComment.display_state, '') !=
225 ChangesetComment.COMMENT_OUTDATED)
225 ChangesetComment.COMMENT_OUTDATED)
226
226
227 todos = todos.all()
227 todos = todos.all()
228
228
229 return todos
229 return todos
230
230
231 def get_commit_inline_comments(self, commit_id):
231 def get_commit_inline_comments(self, commit_id):
232 inline_comments = Session().query(ChangesetComment) \
232 inline_comments = Session().query(ChangesetComment) \
233 .filter(ChangesetComment.line_no != None) \
233 .filter(ChangesetComment.line_no != None) \
234 .filter(ChangesetComment.f_path != None) \
234 .filter(ChangesetComment.f_path != None) \
235 .filter(ChangesetComment.revision == commit_id)
235 .filter(ChangesetComment.revision == commit_id)
236 inline_comments = inline_comments.all()
236 inline_comments = inline_comments.all()
237 return inline_comments
237 return inline_comments
238
238
239 def _log_audit_action(self, action, action_data, auth_user, comment):
239 def _log_audit_action(self, action, action_data, auth_user, comment):
240 audit_logger.store(
240 audit_logger.store(
241 action=action,
241 action=action,
242 action_data=action_data,
242 action_data=action_data,
243 user=auth_user,
243 user=auth_user,
244 repo=comment.repo)
244 repo=comment.repo)
245
245
246 def create(self, text, repo, user, commit_id=None, pull_request=None,
246 def create(self, text, repo, user, commit_id=None, pull_request=None,
247 f_path=None, line_no=None, status_change=None,
247 f_path=None, line_no=None, status_change=None,
248 status_change_type=None, comment_type=None,
248 status_change_type=None, comment_type=None,
249 resolves_comment_id=None, closing_pr=False, send_email=True,
249 resolves_comment_id=None, closing_pr=False, send_email=True,
250 renderer=None, auth_user=None, extra_recipients=None):
250 renderer=None, auth_user=None, extra_recipients=None):
251 """
251 """
252 Creates new comment for commit or pull request.
252 Creates new comment for commit or pull request.
253 IF status_change is not none this comment is associated with a
253 IF status_change is not none this comment is associated with a
254 status change of commit or commit associated with pull request
254 status change of commit or commit associated with pull request
255
255
256 :param text:
256 :param text:
257 :param repo:
257 :param repo:
258 :param user:
258 :param user:
259 :param commit_id:
259 :param commit_id:
260 :param pull_request:
260 :param pull_request:
261 :param f_path:
261 :param f_path:
262 :param line_no:
262 :param line_no:
263 :param status_change: Label for status change
263 :param status_change: Label for status change
264 :param comment_type: Type of comment
264 :param comment_type: Type of comment
265 :param resolves_comment_id: id of comment which this one will resolve
265 :param resolves_comment_id: id of comment which this one will resolve
266 :param status_change_type: type of status change
266 :param status_change_type: type of status change
267 :param closing_pr:
267 :param closing_pr:
268 :param send_email:
268 :param send_email:
269 :param renderer: pick renderer for this comment
269 :param renderer: pick renderer for this comment
270 :param auth_user: current authenticated user calling this method
270 :param auth_user: current authenticated user calling this method
271 :param extra_recipients: list of extra users to be added to recipients
271 :param extra_recipients: list of extra users to be added to recipients
272 """
272 """
273
273
274 if not text:
274 if not text:
275 log.warning('Missing text for comment, skipping...')
275 log.warning('Missing text for comment, skipping...')
276 return
276 return
277 request = get_current_request()
277 request = get_current_request()
278 _ = request.translate
278 _ = request.translate
279
279
280 if not renderer:
280 if not renderer:
281 renderer = self._get_renderer(request=request)
281 renderer = self._get_renderer(request=request)
282
282
283 repo = self._get_repo(repo)
283 repo = self._get_repo(repo)
284 user = self._get_user(user)
284 user = self._get_user(user)
285 auth_user = auth_user or user
285 auth_user = auth_user or user
286
286
287 schema = comment_schema.CommentSchema()
287 schema = comment_schema.CommentSchema()
288 validated_kwargs = schema.deserialize(dict(
288 validated_kwargs = schema.deserialize(dict(
289 comment_body=text,
289 comment_body=text,
290 comment_type=comment_type,
290 comment_type=comment_type,
291 comment_file=f_path,
291 comment_file=f_path,
292 comment_line=line_no,
292 comment_line=line_no,
293 renderer_type=renderer,
293 renderer_type=renderer,
294 status_change=status_change_type,
294 status_change=status_change_type,
295 resolves_comment_id=resolves_comment_id,
295 resolves_comment_id=resolves_comment_id,
296 repo=repo.repo_id,
296 repo=repo.repo_id,
297 user=user.user_id,
297 user=user.user_id,
298 ))
298 ))
299
299
300 comment = ChangesetComment()
300 comment = ChangesetComment()
301 comment.renderer = validated_kwargs['renderer_type']
301 comment.renderer = validated_kwargs['renderer_type']
302 comment.text = validated_kwargs['comment_body']
302 comment.text = validated_kwargs['comment_body']
303 comment.f_path = validated_kwargs['comment_file']
303 comment.f_path = validated_kwargs['comment_file']
304 comment.line_no = validated_kwargs['comment_line']
304 comment.line_no = validated_kwargs['comment_line']
305 comment.comment_type = validated_kwargs['comment_type']
305 comment.comment_type = validated_kwargs['comment_type']
306
306
307 comment.repo = repo
307 comment.repo = repo
308 comment.author = user
308 comment.author = user
309 resolved_comment = self.__get_commit_comment(
309 resolved_comment = self.__get_commit_comment(
310 validated_kwargs['resolves_comment_id'])
310 validated_kwargs['resolves_comment_id'])
311 # check if the comment actually belongs to this PR
311 # check if the comment actually belongs to this PR
312 if resolved_comment and resolved_comment.pull_request and \
312 if resolved_comment and resolved_comment.pull_request and \
313 resolved_comment.pull_request != pull_request:
313 resolved_comment.pull_request != pull_request:
314 log.warning('Comment tried to resolved unrelated todo comment: %s',
314 log.warning('Comment tried to resolved unrelated todo comment: %s',
315 resolved_comment)
315 resolved_comment)
316 # comment not bound to this pull request, forbid
316 # comment not bound to this pull request, forbid
317 resolved_comment = None
317 resolved_comment = None
318
318
319 elif resolved_comment and resolved_comment.repo and \
319 elif resolved_comment and resolved_comment.repo and \
320 resolved_comment.repo != repo:
320 resolved_comment.repo != repo:
321 log.warning('Comment tried to resolved unrelated todo comment: %s',
321 log.warning('Comment tried to resolved unrelated todo comment: %s',
322 resolved_comment)
322 resolved_comment)
323 # comment not bound to this repo, forbid
323 # comment not bound to this repo, forbid
324 resolved_comment = None
324 resolved_comment = None
325
325
326 comment.resolved_comment = resolved_comment
326 comment.resolved_comment = resolved_comment
327
327
328 pull_request_id = pull_request
328 pull_request_id = pull_request
329
329
330 commit_obj = None
330 commit_obj = None
331 pull_request_obj = None
331 pull_request_obj = None
332
332
333 if commit_id:
333 if commit_id:
334 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
334 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
335 # do a lookup, so we don't pass something bad here
335 # do a lookup, so we don't pass something bad here
336 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
336 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
337 comment.revision = commit_obj.raw_id
337 comment.revision = commit_obj.raw_id
338
338
339 elif pull_request_id:
339 elif pull_request_id:
340 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
340 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
341 pull_request_obj = self.__get_pull_request(pull_request_id)
341 pull_request_obj = self.__get_pull_request(pull_request_id)
342 comment.pull_request = pull_request_obj
342 comment.pull_request = pull_request_obj
343 else:
343 else:
344 raise Exception('Please specify commit or pull_request_id')
344 raise Exception('Please specify commit or pull_request_id')
345
345
346 Session().add(comment)
346 Session().add(comment)
347 Session().flush()
347 Session().flush()
348 kwargs = {
348 kwargs = {
349 'user': user,
349 'user': user,
350 'renderer_type': renderer,
350 'renderer_type': renderer,
351 'repo_name': repo.repo_name,
351 'repo_name': repo.repo_name,
352 'status_change': status_change,
352 'status_change': status_change,
353 'status_change_type': status_change_type,
353 'status_change_type': status_change_type,
354 'comment_body': text,
354 'comment_body': text,
355 'comment_file': f_path,
355 'comment_file': f_path,
356 'comment_line': line_no,
356 'comment_line': line_no,
357 'comment_type': comment_type or 'note',
357 'comment_type': comment_type or 'note',
358 'comment_id': comment.comment_id
358 'comment_id': comment.comment_id
359 }
359 }
360
360
361 if commit_obj:
361 if commit_obj:
362 recipients = ChangesetComment.get_users(
362 recipients = ChangesetComment.get_users(
363 revision=commit_obj.raw_id)
363 revision=commit_obj.raw_id)
364 # add commit author if it's in RhodeCode system
364 # add commit author if it's in RhodeCode system
365 cs_author = User.get_from_cs_author(commit_obj.author)
365 cs_author = User.get_from_cs_author(commit_obj.author)
366 if not cs_author:
366 if not cs_author:
367 # use repo owner if we cannot extract the author correctly
367 # use repo owner if we cannot extract the author correctly
368 cs_author = repo.user
368 cs_author = repo.user
369 recipients += [cs_author]
369 recipients += [cs_author]
370
370
371 commit_comment_url = self.get_url(comment, request=request)
371 commit_comment_url = self.get_url(comment, request=request)
372 commit_comment_reply_url = self.get_url(
372 commit_comment_reply_url = self.get_url(
373 comment, request=request,
373 comment, request=request,
374 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
374 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
375
375
376 target_repo_url = h.link_to(
376 target_repo_url = h.link_to(
377 repo.repo_name,
377 repo.repo_name,
378 h.route_url('repo_summary', repo_name=repo.repo_name))
378 h.route_url('repo_summary', repo_name=repo.repo_name))
379
379
380 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
380 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
381 commit_id=commit_id)
381 commit_id=commit_id)
382
382
383 # commit specifics
383 # commit specifics
384 kwargs.update({
384 kwargs.update({
385 'commit': commit_obj,
385 'commit': commit_obj,
386 'commit_message': commit_obj.message,
386 'commit_message': commit_obj.message,
387 'commit_target_repo_url': target_repo_url,
387 'commit_target_repo_url': target_repo_url,
388 'commit_comment_url': commit_comment_url,
388 'commit_comment_url': commit_comment_url,
389 'commit_comment_reply_url': commit_comment_reply_url,
389 'commit_comment_reply_url': commit_comment_reply_url,
390 'commit_url': commit_url,
390 'commit_url': commit_url,
391 'thread_ids': [commit_url, commit_comment_url],
391 'thread_ids': [commit_url, commit_comment_url],
392 })
392 })
393
393
394 elif pull_request_obj:
394 elif pull_request_obj:
395 # get the current participants of this pull request
395 # get the current participants of this pull request
396 recipients = ChangesetComment.get_users(
396 recipients = ChangesetComment.get_users(
397 pull_request_id=pull_request_obj.pull_request_id)
397 pull_request_id=pull_request_obj.pull_request_id)
398 # add pull request author
398 # add pull request author
399 recipients += [pull_request_obj.author]
399 recipients += [pull_request_obj.author]
400
400
401 # add the reviewers to notification
401 # add the reviewers to notification
402 recipients += [x.user for x in pull_request_obj.reviewers]
402 recipients += [x.user for x in pull_request_obj.get_pull_request_reviewers()]
403
403
404 pr_target_repo = pull_request_obj.target_repo
404 pr_target_repo = pull_request_obj.target_repo
405 pr_source_repo = pull_request_obj.source_repo
405 pr_source_repo = pull_request_obj.source_repo
406
406
407 pr_comment_url = self.get_url(comment, request=request)
407 pr_comment_url = self.get_url(comment, request=request)
408 pr_comment_reply_url = self.get_url(
408 pr_comment_reply_url = self.get_url(
409 comment, request=request,
409 comment, request=request,
410 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
410 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
411
411
412 pr_url = h.route_url(
412 pr_url = h.route_url(
413 'pullrequest_show',
413 'pullrequest_show',
414 repo_name=pr_target_repo.repo_name,
414 repo_name=pr_target_repo.repo_name,
415 pull_request_id=pull_request_obj.pull_request_id, )
415 pull_request_id=pull_request_obj.pull_request_id, )
416
416
417 # set some variables for email notification
417 # set some variables for email notification
418 pr_target_repo_url = h.route_url(
418 pr_target_repo_url = h.route_url(
419 'repo_summary', repo_name=pr_target_repo.repo_name)
419 'repo_summary', repo_name=pr_target_repo.repo_name)
420
420
421 pr_source_repo_url = h.route_url(
421 pr_source_repo_url = h.route_url(
422 'repo_summary', repo_name=pr_source_repo.repo_name)
422 'repo_summary', repo_name=pr_source_repo.repo_name)
423
423
424 # pull request specifics
424 # pull request specifics
425 kwargs.update({
425 kwargs.update({
426 'pull_request': pull_request_obj,
426 'pull_request': pull_request_obj,
427 'pr_id': pull_request_obj.pull_request_id,
427 'pr_id': pull_request_obj.pull_request_id,
428 'pull_request_url': pr_url,
428 'pull_request_url': pr_url,
429 'pull_request_target_repo': pr_target_repo,
429 'pull_request_target_repo': pr_target_repo,
430 'pull_request_target_repo_url': pr_target_repo_url,
430 'pull_request_target_repo_url': pr_target_repo_url,
431 'pull_request_source_repo': pr_source_repo,
431 'pull_request_source_repo': pr_source_repo,
432 'pull_request_source_repo_url': pr_source_repo_url,
432 'pull_request_source_repo_url': pr_source_repo_url,
433 'pr_comment_url': pr_comment_url,
433 'pr_comment_url': pr_comment_url,
434 'pr_comment_reply_url': pr_comment_reply_url,
434 'pr_comment_reply_url': pr_comment_reply_url,
435 'pr_closing': closing_pr,
435 'pr_closing': closing_pr,
436 'thread_ids': [pr_url, pr_comment_url],
436 'thread_ids': [pr_url, pr_comment_url],
437 })
437 })
438
438
439 if send_email:
439 if send_email:
440 recipients += [self._get_user(u) for u in (extra_recipients or [])]
440 recipients += [self._get_user(u) for u in (extra_recipients or [])]
441 # pre-generate the subject for notification itself
441 # pre-generate the subject for notification itself
442 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
442 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
443 notification_type, **kwargs)
443 notification_type, **kwargs)
444
444
445 mention_recipients = set(
445 mention_recipients = set(
446 self._extract_mentions(text)).difference(recipients)
446 self._extract_mentions(text)).difference(recipients)
447
447
448 # create notification objects, and emails
448 # create notification objects, and emails
449 NotificationModel().create(
449 NotificationModel().create(
450 created_by=user,
450 created_by=user,
451 notification_subject=subject,
451 notification_subject=subject,
452 notification_body=body_plaintext,
452 notification_body=body_plaintext,
453 notification_type=notification_type,
453 notification_type=notification_type,
454 recipients=recipients,
454 recipients=recipients,
455 mention_recipients=mention_recipients,
455 mention_recipients=mention_recipients,
456 email_kwargs=kwargs,
456 email_kwargs=kwargs,
457 )
457 )
458
458
459 Session().flush()
459 Session().flush()
460 if comment.pull_request:
460 if comment.pull_request:
461 action = 'repo.pull_request.comment.create'
461 action = 'repo.pull_request.comment.create'
462 else:
462 else:
463 action = 'repo.commit.comment.create'
463 action = 'repo.commit.comment.create'
464
464
465 comment_data = comment.get_api_data()
465 comment_data = comment.get_api_data()
466
466
467 self._log_audit_action(
467 self._log_audit_action(
468 action, {'data': comment_data}, auth_user, comment)
468 action, {'data': comment_data}, auth_user, comment)
469
469
470 return comment
470 return comment
471
471
472 def edit(self, comment_id, text, auth_user, version):
472 def edit(self, comment_id, text, auth_user, version):
473 """
473 """
474 Change existing comment for commit or pull request.
474 Change existing comment for commit or pull request.
475
475
476 :param comment_id:
476 :param comment_id:
477 :param text:
477 :param text:
478 :param auth_user: current authenticated user calling this method
478 :param auth_user: current authenticated user calling this method
479 :param version: last comment version
479 :param version: last comment version
480 """
480 """
481 if not text:
481 if not text:
482 log.warning('Missing text for comment, skipping...')
482 log.warning('Missing text for comment, skipping...')
483 return
483 return
484
484
485 comment = ChangesetComment.get(comment_id)
485 comment = ChangesetComment.get(comment_id)
486 old_comment_text = comment.text
486 old_comment_text = comment.text
487 comment.text = text
487 comment.text = text
488 comment.modified_at = datetime.datetime.now()
488 comment.modified_at = datetime.datetime.now()
489 version = safe_int(version)
489 version = safe_int(version)
490
490
491 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
491 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
492 # would return 3 here
492 # would return 3 here
493 comment_version = ChangesetCommentHistory.get_version(comment_id)
493 comment_version = ChangesetCommentHistory.get_version(comment_id)
494
494
495 if isinstance(version, (int, long)) and (comment_version - version) != 1:
495 if isinstance(version, (int, long)) and (comment_version - version) != 1:
496 log.warning(
496 log.warning(
497 'Version mismatch comment_version {} submitted {}, skipping'.format(
497 'Version mismatch comment_version {} submitted {}, skipping'.format(
498 comment_version-1, # -1 since note above
498 comment_version-1, # -1 since note above
499 version
499 version
500 )
500 )
501 )
501 )
502 raise CommentVersionMismatch()
502 raise CommentVersionMismatch()
503
503
504 comment_history = ChangesetCommentHistory()
504 comment_history = ChangesetCommentHistory()
505 comment_history.comment_id = comment_id
505 comment_history.comment_id = comment_id
506 comment_history.version = comment_version
506 comment_history.version = comment_version
507 comment_history.created_by_user_id = auth_user.user_id
507 comment_history.created_by_user_id = auth_user.user_id
508 comment_history.text = old_comment_text
508 comment_history.text = old_comment_text
509 # TODO add email notification
509 # TODO add email notification
510 Session().add(comment_history)
510 Session().add(comment_history)
511 Session().add(comment)
511 Session().add(comment)
512 Session().flush()
512 Session().flush()
513
513
514 if comment.pull_request:
514 if comment.pull_request:
515 action = 'repo.pull_request.comment.edit'
515 action = 'repo.pull_request.comment.edit'
516 else:
516 else:
517 action = 'repo.commit.comment.edit'
517 action = 'repo.commit.comment.edit'
518
518
519 comment_data = comment.get_api_data()
519 comment_data = comment.get_api_data()
520 comment_data['old_comment_text'] = old_comment_text
520 comment_data['old_comment_text'] = old_comment_text
521 self._log_audit_action(
521 self._log_audit_action(
522 action, {'data': comment_data}, auth_user, comment)
522 action, {'data': comment_data}, auth_user, comment)
523
523
524 return comment_history
524 return comment_history
525
525
526 def delete(self, comment, auth_user):
526 def delete(self, comment, auth_user):
527 """
527 """
528 Deletes given comment
528 Deletes given comment
529 """
529 """
530 comment = self.__get_commit_comment(comment)
530 comment = self.__get_commit_comment(comment)
531 old_data = comment.get_api_data()
531 old_data = comment.get_api_data()
532 Session().delete(comment)
532 Session().delete(comment)
533
533
534 if comment.pull_request:
534 if comment.pull_request:
535 action = 'repo.pull_request.comment.delete'
535 action = 'repo.pull_request.comment.delete'
536 else:
536 else:
537 action = 'repo.commit.comment.delete'
537 action = 'repo.commit.comment.delete'
538
538
539 self._log_audit_action(
539 self._log_audit_action(
540 action, {'old_data': old_data}, auth_user, comment)
540 action, {'old_data': old_data}, auth_user, comment)
541
541
542 return comment
542 return comment
543
543
544 def get_all_comments(self, repo_id, revision=None, pull_request=None, count_only=False):
544 def get_all_comments(self, repo_id, revision=None, pull_request=None, count_only=False):
545 q = ChangesetComment.query()\
545 q = ChangesetComment.query()\
546 .filter(ChangesetComment.repo_id == repo_id)
546 .filter(ChangesetComment.repo_id == repo_id)
547 if revision:
547 if revision:
548 q = q.filter(ChangesetComment.revision == revision)
548 q = q.filter(ChangesetComment.revision == revision)
549 elif pull_request:
549 elif pull_request:
550 pull_request = self.__get_pull_request(pull_request)
550 pull_request = self.__get_pull_request(pull_request)
551 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
551 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
552 else:
552 else:
553 raise Exception('Please specify commit or pull_request')
553 raise Exception('Please specify commit or pull_request')
554 q = q.order_by(ChangesetComment.created_on)
554 q = q.order_by(ChangesetComment.created_on)
555 if count_only:
555 if count_only:
556 return q.count()
556 return q.count()
557
557
558 return q.all()
558 return q.all()
559
559
560 def get_url(self, comment, request=None, permalink=False, anchor=None):
560 def get_url(self, comment, request=None, permalink=False, anchor=None):
561 if not request:
561 if not request:
562 request = get_current_request()
562 request = get_current_request()
563
563
564 comment = self.__get_commit_comment(comment)
564 comment = self.__get_commit_comment(comment)
565 if anchor is None:
565 if anchor is None:
566 anchor = 'comment-{}'.format(comment.comment_id)
566 anchor = 'comment-{}'.format(comment.comment_id)
567
567
568 if comment.pull_request:
568 if comment.pull_request:
569 pull_request = comment.pull_request
569 pull_request = comment.pull_request
570 if permalink:
570 if permalink:
571 return request.route_url(
571 return request.route_url(
572 'pull_requests_global',
572 'pull_requests_global',
573 pull_request_id=pull_request.pull_request_id,
573 pull_request_id=pull_request.pull_request_id,
574 _anchor=anchor)
574 _anchor=anchor)
575 else:
575 else:
576 return request.route_url(
576 return request.route_url(
577 'pullrequest_show',
577 'pullrequest_show',
578 repo_name=safe_str(pull_request.target_repo.repo_name),
578 repo_name=safe_str(pull_request.target_repo.repo_name),
579 pull_request_id=pull_request.pull_request_id,
579 pull_request_id=pull_request.pull_request_id,
580 _anchor=anchor)
580 _anchor=anchor)
581
581
582 else:
582 else:
583 repo = comment.repo
583 repo = comment.repo
584 commit_id = comment.revision
584 commit_id = comment.revision
585
585
586 if permalink:
586 if permalink:
587 return request.route_url(
587 return request.route_url(
588 'repo_commit', repo_name=safe_str(repo.repo_id),
588 'repo_commit', repo_name=safe_str(repo.repo_id),
589 commit_id=commit_id,
589 commit_id=commit_id,
590 _anchor=anchor)
590 _anchor=anchor)
591
591
592 else:
592 else:
593 return request.route_url(
593 return request.route_url(
594 'repo_commit', repo_name=safe_str(repo.repo_name),
594 'repo_commit', repo_name=safe_str(repo.repo_name),
595 commit_id=commit_id,
595 commit_id=commit_id,
596 _anchor=anchor)
596 _anchor=anchor)
597
597
598 def get_comments(self, repo_id, revision=None, pull_request=None):
598 def get_comments(self, repo_id, revision=None, pull_request=None):