##// END OF EJS Templates
comments: fix extracing auth_user from the passed in objects. Before if auth_user is empty we could relly on INT or STR passed in
marcink -
r3026:71b96937 stable
parent child Browse files
Show More
@@ -1,662 +1,662 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 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 pyramid.threadlocal import get_current_registry, get_current_request
29 from pyramid.threadlocal import get_current_registry, get_current_request
30 from sqlalchemy.sql.expression import null
30 from sqlalchemy.sql.expression import null
31 from sqlalchemy.sql.functions import coalesce
31 from sqlalchemy.sql.functions import coalesce
32
32
33 from rhodecode.lib import helpers as h, diffs, channelstream
33 from rhodecode.lib import helpers as h, diffs, channelstream
34 from rhodecode.lib import audit_logger
34 from rhodecode.lib import audit_logger
35 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
35 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
36 from rhodecode.model import BaseModel
36 from rhodecode.model import BaseModel
37 from rhodecode.model.db import (
37 from rhodecode.model.db import (
38 ChangesetComment, User, Notification, PullRequest, AttributeDict)
38 ChangesetComment, User, Notification, PullRequest, AttributeDict)
39 from rhodecode.model.notification import NotificationModel
39 from rhodecode.model.notification import NotificationModel
40 from rhodecode.model.meta import Session
40 from rhodecode.model.meta import Session
41 from rhodecode.model.settings import VcsSettingsModel
41 from rhodecode.model.settings import VcsSettingsModel
42 from rhodecode.model.notification import EmailNotificationModel
42 from rhodecode.model.notification import EmailNotificationModel
43 from rhodecode.model.validation_schema.schemas import comment_schema
43 from rhodecode.model.validation_schema.schemas import comment_schema
44
44
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 class CommentsModel(BaseModel):
49 class CommentsModel(BaseModel):
50
50
51 cls = ChangesetComment
51 cls = ChangesetComment
52
52
53 DIFF_CONTEXT_BEFORE = 3
53 DIFF_CONTEXT_BEFORE = 3
54 DIFF_CONTEXT_AFTER = 3
54 DIFF_CONTEXT_AFTER = 3
55
55
56 def __get_commit_comment(self, changeset_comment):
56 def __get_commit_comment(self, changeset_comment):
57 return self._get_instance(ChangesetComment, changeset_comment)
57 return self._get_instance(ChangesetComment, changeset_comment)
58
58
59 def __get_pull_request(self, pull_request):
59 def __get_pull_request(self, pull_request):
60 return self._get_instance(PullRequest, pull_request)
60 return self._get_instance(PullRequest, pull_request)
61
61
62 def _extract_mentions(self, s):
62 def _extract_mentions(self, s):
63 user_objects = []
63 user_objects = []
64 for username in extract_mentioned_users(s):
64 for username in extract_mentioned_users(s):
65 user_obj = User.get_by_username(username, case_insensitive=True)
65 user_obj = User.get_by_username(username, case_insensitive=True)
66 if user_obj:
66 if user_obj:
67 user_objects.append(user_obj)
67 user_objects.append(user_obj)
68 return user_objects
68 return user_objects
69
69
70 def _get_renderer(self, global_renderer='rst', request=None):
70 def _get_renderer(self, global_renderer='rst', request=None):
71 request = request or get_current_request()
71 request = request or get_current_request()
72
72
73 try:
73 try:
74 global_renderer = request.call_context.visual.default_renderer
74 global_renderer = request.call_context.visual.default_renderer
75 except AttributeError:
75 except AttributeError:
76 log.debug("Renderer not set, falling back "
76 log.debug("Renderer not set, falling back "
77 "to default renderer '%s'", global_renderer)
77 "to default renderer '%s'", global_renderer)
78 except Exception:
78 except Exception:
79 log.error(traceback.format_exc())
79 log.error(traceback.format_exc())
80 return global_renderer
80 return global_renderer
81
81
82 def aggregate_comments(self, comments, versions, show_version, inline=False):
82 def aggregate_comments(self, comments, versions, show_version, inline=False):
83 # group by versions, and count until, and display objects
83 # group by versions, and count until, and display objects
84
84
85 comment_groups = collections.defaultdict(list)
85 comment_groups = collections.defaultdict(list)
86 [comment_groups[
86 [comment_groups[
87 _co.pull_request_version_id].append(_co) for _co in comments]
87 _co.pull_request_version_id].append(_co) for _co in comments]
88
88
89 def yield_comments(pos):
89 def yield_comments(pos):
90 for co in comment_groups[pos]:
90 for co in comment_groups[pos]:
91 yield co
91 yield co
92
92
93 comment_versions = collections.defaultdict(
93 comment_versions = collections.defaultdict(
94 lambda: collections.defaultdict(list))
94 lambda: collections.defaultdict(list))
95 prev_prvid = -1
95 prev_prvid = -1
96 # fake last entry with None, to aggregate on "latest" version which
96 # fake last entry with None, to aggregate on "latest" version which
97 # doesn't have an pull_request_version_id
97 # doesn't have an pull_request_version_id
98 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
98 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
99 prvid = ver.pull_request_version_id
99 prvid = ver.pull_request_version_id
100 if prev_prvid == -1:
100 if prev_prvid == -1:
101 prev_prvid = prvid
101 prev_prvid = prvid
102
102
103 for co in yield_comments(prvid):
103 for co in yield_comments(prvid):
104 comment_versions[prvid]['at'].append(co)
104 comment_versions[prvid]['at'].append(co)
105
105
106 # save until
106 # save until
107 current = comment_versions[prvid]['at']
107 current = comment_versions[prvid]['at']
108 prev_until = comment_versions[prev_prvid]['until']
108 prev_until = comment_versions[prev_prvid]['until']
109 cur_until = prev_until + current
109 cur_until = prev_until + current
110 comment_versions[prvid]['until'].extend(cur_until)
110 comment_versions[prvid]['until'].extend(cur_until)
111
111
112 # save outdated
112 # save outdated
113 if inline:
113 if inline:
114 outdated = [x for x in cur_until
114 outdated = [x for x in cur_until
115 if x.outdated_at_version(show_version)]
115 if x.outdated_at_version(show_version)]
116 else:
116 else:
117 outdated = [x for x in cur_until
117 outdated = [x for x in cur_until
118 if x.older_than_version(show_version)]
118 if x.older_than_version(show_version)]
119 display = [x for x in cur_until if x not in outdated]
119 display = [x for x in cur_until if x not in outdated]
120
120
121 comment_versions[prvid]['outdated'] = outdated
121 comment_versions[prvid]['outdated'] = outdated
122 comment_versions[prvid]['display'] = display
122 comment_versions[prvid]['display'] = display
123
123
124 prev_prvid = prvid
124 prev_prvid = prvid
125
125
126 return comment_versions
126 return comment_versions
127
127
128 def get_unresolved_todos(self, pull_request, show_outdated=True):
128 def get_unresolved_todos(self, pull_request, show_outdated=True):
129
129
130 todos = Session().query(ChangesetComment) \
130 todos = Session().query(ChangesetComment) \
131 .filter(ChangesetComment.pull_request == pull_request) \
131 .filter(ChangesetComment.pull_request == pull_request) \
132 .filter(ChangesetComment.resolved_by == None) \
132 .filter(ChangesetComment.resolved_by == None) \
133 .filter(ChangesetComment.comment_type
133 .filter(ChangesetComment.comment_type
134 == ChangesetComment.COMMENT_TYPE_TODO)
134 == ChangesetComment.COMMENT_TYPE_TODO)
135
135
136 if not show_outdated:
136 if not show_outdated:
137 todos = todos.filter(
137 todos = todos.filter(
138 coalesce(ChangesetComment.display_state, '') !=
138 coalesce(ChangesetComment.display_state, '') !=
139 ChangesetComment.COMMENT_OUTDATED)
139 ChangesetComment.COMMENT_OUTDATED)
140
140
141 todos = todos.all()
141 todos = todos.all()
142
142
143 return todos
143 return todos
144
144
145 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
145 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
146
146
147 todos = Session().query(ChangesetComment) \
147 todos = Session().query(ChangesetComment) \
148 .filter(ChangesetComment.revision == commit_id) \
148 .filter(ChangesetComment.revision == commit_id) \
149 .filter(ChangesetComment.resolved_by == None) \
149 .filter(ChangesetComment.resolved_by == None) \
150 .filter(ChangesetComment.comment_type
150 .filter(ChangesetComment.comment_type
151 == ChangesetComment.COMMENT_TYPE_TODO)
151 == ChangesetComment.COMMENT_TYPE_TODO)
152
152
153 if not show_outdated:
153 if not show_outdated:
154 todos = todos.filter(
154 todos = todos.filter(
155 coalesce(ChangesetComment.display_state, '') !=
155 coalesce(ChangesetComment.display_state, '') !=
156 ChangesetComment.COMMENT_OUTDATED)
156 ChangesetComment.COMMENT_OUTDATED)
157
157
158 todos = todos.all()
158 todos = todos.all()
159
159
160 return todos
160 return todos
161
161
162 def _log_audit_action(self, action, action_data, auth_user, comment):
162 def _log_audit_action(self, action, action_data, auth_user, comment):
163 audit_logger.store(
163 audit_logger.store(
164 action=action,
164 action=action,
165 action_data=action_data,
165 action_data=action_data,
166 user=auth_user,
166 user=auth_user,
167 repo=comment.repo)
167 repo=comment.repo)
168
168
169 def create(self, text, repo, user, commit_id=None, pull_request=None,
169 def create(self, text, repo, user, commit_id=None, pull_request=None,
170 f_path=None, line_no=None, status_change=None,
170 f_path=None, line_no=None, status_change=None,
171 status_change_type=None, comment_type=None,
171 status_change_type=None, comment_type=None,
172 resolves_comment_id=None, closing_pr=False, send_email=True,
172 resolves_comment_id=None, closing_pr=False, send_email=True,
173 renderer=None, auth_user=None):
173 renderer=None, auth_user=None):
174 """
174 """
175 Creates new comment for commit or pull request.
175 Creates new comment for commit or pull request.
176 IF status_change is not none this comment is associated with a
176 IF status_change is not none this comment is associated with a
177 status change of commit or commit associated with pull request
177 status change of commit or commit associated with pull request
178
178
179 :param text:
179 :param text:
180 :param repo:
180 :param repo:
181 :param user:
181 :param user:
182 :param commit_id:
182 :param commit_id:
183 :param pull_request:
183 :param pull_request:
184 :param f_path:
184 :param f_path:
185 :param line_no:
185 :param line_no:
186 :param status_change: Label for status change
186 :param status_change: Label for status change
187 :param comment_type: Type of comment
187 :param comment_type: Type of comment
188 :param status_change_type: type of status change
188 :param status_change_type: type of status change
189 :param closing_pr:
189 :param closing_pr:
190 :param send_email:
190 :param send_email:
191 :param renderer: pick renderer for this comment
191 :param renderer: pick renderer for this comment
192 """
192 """
193
193
194 auth_user = auth_user or user
195 if not text:
194 if not text:
196 log.warning('Missing text for comment, skipping...')
195 log.warning('Missing text for comment, skipping...')
197 return
196 return
198 request = get_current_request()
197 request = get_current_request()
199 _ = request.translate
198 _ = request.translate
200
199
201 if not renderer:
200 if not renderer:
202 renderer = self._get_renderer(request=request)
201 renderer = self._get_renderer(request=request)
203
202
204 repo = self._get_repo(repo)
203 repo = self._get_repo(repo)
205 user = self._get_user(user)
204 user = self._get_user(user)
205 auth_user = auth_user or user
206
206
207 schema = comment_schema.CommentSchema()
207 schema = comment_schema.CommentSchema()
208 validated_kwargs = schema.deserialize(dict(
208 validated_kwargs = schema.deserialize(dict(
209 comment_body=text,
209 comment_body=text,
210 comment_type=comment_type,
210 comment_type=comment_type,
211 comment_file=f_path,
211 comment_file=f_path,
212 comment_line=line_no,
212 comment_line=line_no,
213 renderer_type=renderer,
213 renderer_type=renderer,
214 status_change=status_change_type,
214 status_change=status_change_type,
215 resolves_comment_id=resolves_comment_id,
215 resolves_comment_id=resolves_comment_id,
216 repo=repo.repo_id,
216 repo=repo.repo_id,
217 user=user.user_id,
217 user=user.user_id,
218 ))
218 ))
219
219
220 comment = ChangesetComment()
220 comment = ChangesetComment()
221 comment.renderer = validated_kwargs['renderer_type']
221 comment.renderer = validated_kwargs['renderer_type']
222 comment.text = validated_kwargs['comment_body']
222 comment.text = validated_kwargs['comment_body']
223 comment.f_path = validated_kwargs['comment_file']
223 comment.f_path = validated_kwargs['comment_file']
224 comment.line_no = validated_kwargs['comment_line']
224 comment.line_no = validated_kwargs['comment_line']
225 comment.comment_type = validated_kwargs['comment_type']
225 comment.comment_type = validated_kwargs['comment_type']
226
226
227 comment.repo = repo
227 comment.repo = repo
228 comment.author = user
228 comment.author = user
229 resolved_comment = self.__get_commit_comment(
229 resolved_comment = self.__get_commit_comment(
230 validated_kwargs['resolves_comment_id'])
230 validated_kwargs['resolves_comment_id'])
231 # check if the comment actually belongs to this PR
231 # check if the comment actually belongs to this PR
232 if resolved_comment and resolved_comment.pull_request and \
232 if resolved_comment and resolved_comment.pull_request and \
233 resolved_comment.pull_request != pull_request:
233 resolved_comment.pull_request != pull_request:
234 # comment not bound to this pull request, forbid
234 # comment not bound to this pull request, forbid
235 resolved_comment = None
235 resolved_comment = None
236 comment.resolved_comment = resolved_comment
236 comment.resolved_comment = resolved_comment
237
237
238 pull_request_id = pull_request
238 pull_request_id = pull_request
239
239
240 commit_obj = None
240 commit_obj = None
241 pull_request_obj = None
241 pull_request_obj = None
242
242
243 if commit_id:
243 if commit_id:
244 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
244 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
245 # do a lookup, so we don't pass something bad here
245 # do a lookup, so we don't pass something bad here
246 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
246 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
247 comment.revision = commit_obj.raw_id
247 comment.revision = commit_obj.raw_id
248
248
249 elif pull_request_id:
249 elif pull_request_id:
250 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
250 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
251 pull_request_obj = self.__get_pull_request(pull_request_id)
251 pull_request_obj = self.__get_pull_request(pull_request_id)
252 comment.pull_request = pull_request_obj
252 comment.pull_request = pull_request_obj
253 else:
253 else:
254 raise Exception('Please specify commit or pull_request_id')
254 raise Exception('Please specify commit or pull_request_id')
255
255
256 Session().add(comment)
256 Session().add(comment)
257 Session().flush()
257 Session().flush()
258 kwargs = {
258 kwargs = {
259 'user': user,
259 'user': user,
260 'renderer_type': renderer,
260 'renderer_type': renderer,
261 'repo_name': repo.repo_name,
261 'repo_name': repo.repo_name,
262 'status_change': status_change,
262 'status_change': status_change,
263 'status_change_type': status_change_type,
263 'status_change_type': status_change_type,
264 'comment_body': text,
264 'comment_body': text,
265 'comment_file': f_path,
265 'comment_file': f_path,
266 'comment_line': line_no,
266 'comment_line': line_no,
267 'comment_type': comment_type or 'note'
267 'comment_type': comment_type or 'note'
268 }
268 }
269
269
270 if commit_obj:
270 if commit_obj:
271 recipients = ChangesetComment.get_users(
271 recipients = ChangesetComment.get_users(
272 revision=commit_obj.raw_id)
272 revision=commit_obj.raw_id)
273 # add commit author if it's in RhodeCode system
273 # add commit author if it's in RhodeCode system
274 cs_author = User.get_from_cs_author(commit_obj.author)
274 cs_author = User.get_from_cs_author(commit_obj.author)
275 if not cs_author:
275 if not cs_author:
276 # use repo owner if we cannot extract the author correctly
276 # use repo owner if we cannot extract the author correctly
277 cs_author = repo.user
277 cs_author = repo.user
278 recipients += [cs_author]
278 recipients += [cs_author]
279
279
280 commit_comment_url = self.get_url(comment, request=request)
280 commit_comment_url = self.get_url(comment, request=request)
281
281
282 target_repo_url = h.link_to(
282 target_repo_url = h.link_to(
283 repo.repo_name,
283 repo.repo_name,
284 h.route_url('repo_summary', repo_name=repo.repo_name))
284 h.route_url('repo_summary', repo_name=repo.repo_name))
285
285
286 # commit specifics
286 # commit specifics
287 kwargs.update({
287 kwargs.update({
288 'commit': commit_obj,
288 'commit': commit_obj,
289 'commit_message': commit_obj.message,
289 'commit_message': commit_obj.message,
290 'commit_target_repo': target_repo_url,
290 'commit_target_repo': target_repo_url,
291 'commit_comment_url': commit_comment_url,
291 'commit_comment_url': commit_comment_url,
292 })
292 })
293
293
294 elif pull_request_obj:
294 elif pull_request_obj:
295 # get the current participants of this pull request
295 # get the current participants of this pull request
296 recipients = ChangesetComment.get_users(
296 recipients = ChangesetComment.get_users(
297 pull_request_id=pull_request_obj.pull_request_id)
297 pull_request_id=pull_request_obj.pull_request_id)
298 # add pull request author
298 # add pull request author
299 recipients += [pull_request_obj.author]
299 recipients += [pull_request_obj.author]
300
300
301 # add the reviewers to notification
301 # add the reviewers to notification
302 recipients += [x.user for x in pull_request_obj.reviewers]
302 recipients += [x.user for x in pull_request_obj.reviewers]
303
303
304 pr_target_repo = pull_request_obj.target_repo
304 pr_target_repo = pull_request_obj.target_repo
305 pr_source_repo = pull_request_obj.source_repo
305 pr_source_repo = pull_request_obj.source_repo
306
306
307 pr_comment_url = h.route_url(
307 pr_comment_url = h.route_url(
308 'pullrequest_show',
308 'pullrequest_show',
309 repo_name=pr_target_repo.repo_name,
309 repo_name=pr_target_repo.repo_name,
310 pull_request_id=pull_request_obj.pull_request_id,
310 pull_request_id=pull_request_obj.pull_request_id,
311 _anchor='comment-%s' % comment.comment_id)
311 _anchor='comment-%s' % comment.comment_id)
312
312
313 # set some variables for email notification
313 # set some variables for email notification
314 pr_target_repo_url = h.route_url(
314 pr_target_repo_url = h.route_url(
315 'repo_summary', repo_name=pr_target_repo.repo_name)
315 'repo_summary', repo_name=pr_target_repo.repo_name)
316
316
317 pr_source_repo_url = h.route_url(
317 pr_source_repo_url = h.route_url(
318 'repo_summary', repo_name=pr_source_repo.repo_name)
318 'repo_summary', repo_name=pr_source_repo.repo_name)
319
319
320 # pull request specifics
320 # pull request specifics
321 kwargs.update({
321 kwargs.update({
322 'pull_request': pull_request_obj,
322 'pull_request': pull_request_obj,
323 'pr_id': pull_request_obj.pull_request_id,
323 'pr_id': pull_request_obj.pull_request_id,
324 'pr_target_repo': pr_target_repo,
324 'pr_target_repo': pr_target_repo,
325 'pr_target_repo_url': pr_target_repo_url,
325 'pr_target_repo_url': pr_target_repo_url,
326 'pr_source_repo': pr_source_repo,
326 'pr_source_repo': pr_source_repo,
327 'pr_source_repo_url': pr_source_repo_url,
327 'pr_source_repo_url': pr_source_repo_url,
328 'pr_comment_url': pr_comment_url,
328 'pr_comment_url': pr_comment_url,
329 'pr_closing': closing_pr,
329 'pr_closing': closing_pr,
330 })
330 })
331 if send_email:
331 if send_email:
332 # pre-generate the subject for notification itself
332 # pre-generate the subject for notification itself
333 (subject,
333 (subject,
334 _h, _e, # we don't care about those
334 _h, _e, # we don't care about those
335 body_plaintext) = EmailNotificationModel().render_email(
335 body_plaintext) = EmailNotificationModel().render_email(
336 notification_type, **kwargs)
336 notification_type, **kwargs)
337
337
338 mention_recipients = set(
338 mention_recipients = set(
339 self._extract_mentions(text)).difference(recipients)
339 self._extract_mentions(text)).difference(recipients)
340
340
341 # create notification objects, and emails
341 # create notification objects, and emails
342 NotificationModel().create(
342 NotificationModel().create(
343 created_by=user,
343 created_by=user,
344 notification_subject=subject,
344 notification_subject=subject,
345 notification_body=body_plaintext,
345 notification_body=body_plaintext,
346 notification_type=notification_type,
346 notification_type=notification_type,
347 recipients=recipients,
347 recipients=recipients,
348 mention_recipients=mention_recipients,
348 mention_recipients=mention_recipients,
349 email_kwargs=kwargs,
349 email_kwargs=kwargs,
350 )
350 )
351
351
352 Session().flush()
352 Session().flush()
353 if comment.pull_request:
353 if comment.pull_request:
354 action = 'repo.pull_request.comment.create'
354 action = 'repo.pull_request.comment.create'
355 else:
355 else:
356 action = 'repo.commit.comment.create'
356 action = 'repo.commit.comment.create'
357
357
358 comment_data = comment.get_api_data()
358 comment_data = comment.get_api_data()
359 self._log_audit_action(
359 self._log_audit_action(
360 action, {'data': comment_data}, auth_user, comment)
360 action, {'data': comment_data}, auth_user, comment)
361
361
362 msg_url = ''
362 msg_url = ''
363 channel = None
363 channel = None
364 if commit_obj:
364 if commit_obj:
365 msg_url = commit_comment_url
365 msg_url = commit_comment_url
366 repo_name = repo.repo_name
366 repo_name = repo.repo_name
367 channel = u'/repo${}$/commit/{}'.format(
367 channel = u'/repo${}$/commit/{}'.format(
368 repo_name,
368 repo_name,
369 commit_obj.raw_id
369 commit_obj.raw_id
370 )
370 )
371 elif pull_request_obj:
371 elif pull_request_obj:
372 msg_url = pr_comment_url
372 msg_url = pr_comment_url
373 repo_name = pr_target_repo.repo_name
373 repo_name = pr_target_repo.repo_name
374 channel = u'/repo${}$/pr/{}'.format(
374 channel = u'/repo${}$/pr/{}'.format(
375 repo_name,
375 repo_name,
376 pull_request_id
376 pull_request_id
377 )
377 )
378
378
379 message = '<strong>{}</strong> {} - ' \
379 message = '<strong>{}</strong> {} - ' \
380 '<a onclick="window.location=\'{}\';' \
380 '<a onclick="window.location=\'{}\';' \
381 'window.location.reload()">' \
381 'window.location.reload()">' \
382 '<strong>{}</strong></a>'
382 '<strong>{}</strong></a>'
383 message = message.format(
383 message = message.format(
384 user.username, _('made a comment'), msg_url,
384 user.username, _('made a comment'), msg_url,
385 _('Show it now'))
385 _('Show it now'))
386
386
387 channelstream.post_message(
387 channelstream.post_message(
388 channel, message, user.username,
388 channel, message, user.username,
389 registry=get_current_registry())
389 registry=get_current_registry())
390
390
391 return comment
391 return comment
392
392
393 def delete(self, comment, auth_user):
393 def delete(self, comment, auth_user):
394 """
394 """
395 Deletes given comment
395 Deletes given comment
396 """
396 """
397 comment = self.__get_commit_comment(comment)
397 comment = self.__get_commit_comment(comment)
398 old_data = comment.get_api_data()
398 old_data = comment.get_api_data()
399 Session().delete(comment)
399 Session().delete(comment)
400
400
401 if comment.pull_request:
401 if comment.pull_request:
402 action = 'repo.pull_request.comment.delete'
402 action = 'repo.pull_request.comment.delete'
403 else:
403 else:
404 action = 'repo.commit.comment.delete'
404 action = 'repo.commit.comment.delete'
405
405
406 self._log_audit_action(
406 self._log_audit_action(
407 action, {'old_data': old_data}, auth_user, comment)
407 action, {'old_data': old_data}, auth_user, comment)
408
408
409 return comment
409 return comment
410
410
411 def get_all_comments(self, repo_id, revision=None, pull_request=None):
411 def get_all_comments(self, repo_id, revision=None, pull_request=None):
412 q = ChangesetComment.query()\
412 q = ChangesetComment.query()\
413 .filter(ChangesetComment.repo_id == repo_id)
413 .filter(ChangesetComment.repo_id == repo_id)
414 if revision:
414 if revision:
415 q = q.filter(ChangesetComment.revision == revision)
415 q = q.filter(ChangesetComment.revision == revision)
416 elif pull_request:
416 elif pull_request:
417 pull_request = self.__get_pull_request(pull_request)
417 pull_request = self.__get_pull_request(pull_request)
418 q = q.filter(ChangesetComment.pull_request == pull_request)
418 q = q.filter(ChangesetComment.pull_request == pull_request)
419 else:
419 else:
420 raise Exception('Please specify commit or pull_request')
420 raise Exception('Please specify commit or pull_request')
421 q = q.order_by(ChangesetComment.created_on)
421 q = q.order_by(ChangesetComment.created_on)
422 return q.all()
422 return q.all()
423
423
424 def get_url(self, comment, request=None, permalink=False):
424 def get_url(self, comment, request=None, permalink=False):
425 if not request:
425 if not request:
426 request = get_current_request()
426 request = get_current_request()
427
427
428 comment = self.__get_commit_comment(comment)
428 comment = self.__get_commit_comment(comment)
429 if comment.pull_request:
429 if comment.pull_request:
430 pull_request = comment.pull_request
430 pull_request = comment.pull_request
431 if permalink:
431 if permalink:
432 return request.route_url(
432 return request.route_url(
433 'pull_requests_global',
433 'pull_requests_global',
434 pull_request_id=pull_request.pull_request_id,
434 pull_request_id=pull_request.pull_request_id,
435 _anchor='comment-%s' % comment.comment_id)
435 _anchor='comment-%s' % comment.comment_id)
436 else:
436 else:
437 return request.route_url(
437 return request.route_url(
438 'pullrequest_show',
438 'pullrequest_show',
439 repo_name=safe_str(pull_request.target_repo.repo_name),
439 repo_name=safe_str(pull_request.target_repo.repo_name),
440 pull_request_id=pull_request.pull_request_id,
440 pull_request_id=pull_request.pull_request_id,
441 _anchor='comment-%s' % comment.comment_id)
441 _anchor='comment-%s' % comment.comment_id)
442
442
443 else:
443 else:
444 repo = comment.repo
444 repo = comment.repo
445 commit_id = comment.revision
445 commit_id = comment.revision
446
446
447 if permalink:
447 if permalink:
448 return request.route_url(
448 return request.route_url(
449 'repo_commit', repo_name=safe_str(repo.repo_id),
449 'repo_commit', repo_name=safe_str(repo.repo_id),
450 commit_id=commit_id,
450 commit_id=commit_id,
451 _anchor='comment-%s' % comment.comment_id)
451 _anchor='comment-%s' % comment.comment_id)
452
452
453 else:
453 else:
454 return request.route_url(
454 return request.route_url(
455 'repo_commit', repo_name=safe_str(repo.repo_name),
455 'repo_commit', repo_name=safe_str(repo.repo_name),
456 commit_id=commit_id,
456 commit_id=commit_id,
457 _anchor='comment-%s' % comment.comment_id)
457 _anchor='comment-%s' % comment.comment_id)
458
458
459 def get_comments(self, repo_id, revision=None, pull_request=None):
459 def get_comments(self, repo_id, revision=None, pull_request=None):
460 """
460 """
461 Gets main comments based on revision or pull_request_id
461 Gets main comments based on revision or pull_request_id
462
462
463 :param repo_id:
463 :param repo_id:
464 :param revision:
464 :param revision:
465 :param pull_request:
465 :param pull_request:
466 """
466 """
467
467
468 q = ChangesetComment.query()\
468 q = ChangesetComment.query()\
469 .filter(ChangesetComment.repo_id == repo_id)\
469 .filter(ChangesetComment.repo_id == repo_id)\
470 .filter(ChangesetComment.line_no == None)\
470 .filter(ChangesetComment.line_no == None)\
471 .filter(ChangesetComment.f_path == None)
471 .filter(ChangesetComment.f_path == None)
472 if revision:
472 if revision:
473 q = q.filter(ChangesetComment.revision == revision)
473 q = q.filter(ChangesetComment.revision == revision)
474 elif pull_request:
474 elif pull_request:
475 pull_request = self.__get_pull_request(pull_request)
475 pull_request = self.__get_pull_request(pull_request)
476 q = q.filter(ChangesetComment.pull_request == pull_request)
476 q = q.filter(ChangesetComment.pull_request == pull_request)
477 else:
477 else:
478 raise Exception('Please specify commit or pull_request')
478 raise Exception('Please specify commit or pull_request')
479 q = q.order_by(ChangesetComment.created_on)
479 q = q.order_by(ChangesetComment.created_on)
480 return q.all()
480 return q.all()
481
481
482 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
482 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
483 q = self._get_inline_comments_query(repo_id, revision, pull_request)
483 q = self._get_inline_comments_query(repo_id, revision, pull_request)
484 return self._group_comments_by_path_and_line_number(q)
484 return self._group_comments_by_path_and_line_number(q)
485
485
486 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
486 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
487 version=None):
487 version=None):
488 inline_cnt = 0
488 inline_cnt = 0
489 for fname, per_line_comments in inline_comments.iteritems():
489 for fname, per_line_comments in inline_comments.iteritems():
490 for lno, comments in per_line_comments.iteritems():
490 for lno, comments in per_line_comments.iteritems():
491 for comm in comments:
491 for comm in comments:
492 if not comm.outdated_at_version(version) and skip_outdated:
492 if not comm.outdated_at_version(version) and skip_outdated:
493 inline_cnt += 1
493 inline_cnt += 1
494
494
495 return inline_cnt
495 return inline_cnt
496
496
497 def get_outdated_comments(self, repo_id, pull_request):
497 def get_outdated_comments(self, repo_id, pull_request):
498 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
498 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
499 # of a pull request.
499 # of a pull request.
500 q = self._all_inline_comments_of_pull_request(pull_request)
500 q = self._all_inline_comments_of_pull_request(pull_request)
501 q = q.filter(
501 q = q.filter(
502 ChangesetComment.display_state ==
502 ChangesetComment.display_state ==
503 ChangesetComment.COMMENT_OUTDATED
503 ChangesetComment.COMMENT_OUTDATED
504 ).order_by(ChangesetComment.comment_id.asc())
504 ).order_by(ChangesetComment.comment_id.asc())
505
505
506 return self._group_comments_by_path_and_line_number(q)
506 return self._group_comments_by_path_and_line_number(q)
507
507
508 def _get_inline_comments_query(self, repo_id, revision, pull_request):
508 def _get_inline_comments_query(self, repo_id, revision, pull_request):
509 # TODO: johbo: Split this into two methods: One for PR and one for
509 # TODO: johbo: Split this into two methods: One for PR and one for
510 # commit.
510 # commit.
511 if revision:
511 if revision:
512 q = Session().query(ChangesetComment).filter(
512 q = Session().query(ChangesetComment).filter(
513 ChangesetComment.repo_id == repo_id,
513 ChangesetComment.repo_id == repo_id,
514 ChangesetComment.line_no != null(),
514 ChangesetComment.line_no != null(),
515 ChangesetComment.f_path != null(),
515 ChangesetComment.f_path != null(),
516 ChangesetComment.revision == revision)
516 ChangesetComment.revision == revision)
517
517
518 elif pull_request:
518 elif pull_request:
519 pull_request = self.__get_pull_request(pull_request)
519 pull_request = self.__get_pull_request(pull_request)
520 if not CommentsModel.use_outdated_comments(pull_request):
520 if not CommentsModel.use_outdated_comments(pull_request):
521 q = self._visible_inline_comments_of_pull_request(pull_request)
521 q = self._visible_inline_comments_of_pull_request(pull_request)
522 else:
522 else:
523 q = self._all_inline_comments_of_pull_request(pull_request)
523 q = self._all_inline_comments_of_pull_request(pull_request)
524
524
525 else:
525 else:
526 raise Exception('Please specify commit or pull_request_id')
526 raise Exception('Please specify commit or pull_request_id')
527 q = q.order_by(ChangesetComment.comment_id.asc())
527 q = q.order_by(ChangesetComment.comment_id.asc())
528 return q
528 return q
529
529
530 def _group_comments_by_path_and_line_number(self, q):
530 def _group_comments_by_path_and_line_number(self, q):
531 comments = q.all()
531 comments = q.all()
532 paths = collections.defaultdict(lambda: collections.defaultdict(list))
532 paths = collections.defaultdict(lambda: collections.defaultdict(list))
533 for co in comments:
533 for co in comments:
534 paths[co.f_path][co.line_no].append(co)
534 paths[co.f_path][co.line_no].append(co)
535 return paths
535 return paths
536
536
537 @classmethod
537 @classmethod
538 def needed_extra_diff_context(cls):
538 def needed_extra_diff_context(cls):
539 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
539 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
540
540
541 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
541 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
542 if not CommentsModel.use_outdated_comments(pull_request):
542 if not CommentsModel.use_outdated_comments(pull_request):
543 return
543 return
544
544
545 comments = self._visible_inline_comments_of_pull_request(pull_request)
545 comments = self._visible_inline_comments_of_pull_request(pull_request)
546 comments_to_outdate = comments.all()
546 comments_to_outdate = comments.all()
547
547
548 for comment in comments_to_outdate:
548 for comment in comments_to_outdate:
549 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
549 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
550
550
551 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
551 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
552 diff_line = _parse_comment_line_number(comment.line_no)
552 diff_line = _parse_comment_line_number(comment.line_no)
553
553
554 try:
554 try:
555 old_context = old_diff_proc.get_context_of_line(
555 old_context = old_diff_proc.get_context_of_line(
556 path=comment.f_path, diff_line=diff_line)
556 path=comment.f_path, diff_line=diff_line)
557 new_context = new_diff_proc.get_context_of_line(
557 new_context = new_diff_proc.get_context_of_line(
558 path=comment.f_path, diff_line=diff_line)
558 path=comment.f_path, diff_line=diff_line)
559 except (diffs.LineNotInDiffException,
559 except (diffs.LineNotInDiffException,
560 diffs.FileNotInDiffException):
560 diffs.FileNotInDiffException):
561 comment.display_state = ChangesetComment.COMMENT_OUTDATED
561 comment.display_state = ChangesetComment.COMMENT_OUTDATED
562 return
562 return
563
563
564 if old_context == new_context:
564 if old_context == new_context:
565 return
565 return
566
566
567 if self._should_relocate_diff_line(diff_line):
567 if self._should_relocate_diff_line(diff_line):
568 new_diff_lines = new_diff_proc.find_context(
568 new_diff_lines = new_diff_proc.find_context(
569 path=comment.f_path, context=old_context,
569 path=comment.f_path, context=old_context,
570 offset=self.DIFF_CONTEXT_BEFORE)
570 offset=self.DIFF_CONTEXT_BEFORE)
571 if not new_diff_lines:
571 if not new_diff_lines:
572 comment.display_state = ChangesetComment.COMMENT_OUTDATED
572 comment.display_state = ChangesetComment.COMMENT_OUTDATED
573 else:
573 else:
574 new_diff_line = self._choose_closest_diff_line(
574 new_diff_line = self._choose_closest_diff_line(
575 diff_line, new_diff_lines)
575 diff_line, new_diff_lines)
576 comment.line_no = _diff_to_comment_line_number(new_diff_line)
576 comment.line_no = _diff_to_comment_line_number(new_diff_line)
577 else:
577 else:
578 comment.display_state = ChangesetComment.COMMENT_OUTDATED
578 comment.display_state = ChangesetComment.COMMENT_OUTDATED
579
579
580 def _should_relocate_diff_line(self, diff_line):
580 def _should_relocate_diff_line(self, diff_line):
581 """
581 """
582 Checks if relocation shall be tried for the given `diff_line`.
582 Checks if relocation shall be tried for the given `diff_line`.
583
583
584 If a comment points into the first lines, then we can have a situation
584 If a comment points into the first lines, then we can have a situation
585 that after an update another line has been added on top. In this case
585 that after an update another line has been added on top. In this case
586 we would find the context still and move the comment around. This
586 we would find the context still and move the comment around. This
587 would be wrong.
587 would be wrong.
588 """
588 """
589 should_relocate = (
589 should_relocate = (
590 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
590 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
591 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
591 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
592 return should_relocate
592 return should_relocate
593
593
594 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
594 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
595 candidate = new_diff_lines[0]
595 candidate = new_diff_lines[0]
596 best_delta = _diff_line_delta(diff_line, candidate)
596 best_delta = _diff_line_delta(diff_line, candidate)
597 for new_diff_line in new_diff_lines[1:]:
597 for new_diff_line in new_diff_lines[1:]:
598 delta = _diff_line_delta(diff_line, new_diff_line)
598 delta = _diff_line_delta(diff_line, new_diff_line)
599 if delta < best_delta:
599 if delta < best_delta:
600 candidate = new_diff_line
600 candidate = new_diff_line
601 best_delta = delta
601 best_delta = delta
602 return candidate
602 return candidate
603
603
604 def _visible_inline_comments_of_pull_request(self, pull_request):
604 def _visible_inline_comments_of_pull_request(self, pull_request):
605 comments = self._all_inline_comments_of_pull_request(pull_request)
605 comments = self._all_inline_comments_of_pull_request(pull_request)
606 comments = comments.filter(
606 comments = comments.filter(
607 coalesce(ChangesetComment.display_state, '') !=
607 coalesce(ChangesetComment.display_state, '') !=
608 ChangesetComment.COMMENT_OUTDATED)
608 ChangesetComment.COMMENT_OUTDATED)
609 return comments
609 return comments
610
610
611 def _all_inline_comments_of_pull_request(self, pull_request):
611 def _all_inline_comments_of_pull_request(self, pull_request):
612 comments = Session().query(ChangesetComment)\
612 comments = Session().query(ChangesetComment)\
613 .filter(ChangesetComment.line_no != None)\
613 .filter(ChangesetComment.line_no != None)\
614 .filter(ChangesetComment.f_path != None)\
614 .filter(ChangesetComment.f_path != None)\
615 .filter(ChangesetComment.pull_request == pull_request)
615 .filter(ChangesetComment.pull_request == pull_request)
616 return comments
616 return comments
617
617
618 def _all_general_comments_of_pull_request(self, pull_request):
618 def _all_general_comments_of_pull_request(self, pull_request):
619 comments = Session().query(ChangesetComment)\
619 comments = Session().query(ChangesetComment)\
620 .filter(ChangesetComment.line_no == None)\
620 .filter(ChangesetComment.line_no == None)\
621 .filter(ChangesetComment.f_path == None)\
621 .filter(ChangesetComment.f_path == None)\
622 .filter(ChangesetComment.pull_request == pull_request)
622 .filter(ChangesetComment.pull_request == pull_request)
623 return comments
623 return comments
624
624
625 @staticmethod
625 @staticmethod
626 def use_outdated_comments(pull_request):
626 def use_outdated_comments(pull_request):
627 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
627 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
628 settings = settings_model.get_general_settings()
628 settings = settings_model.get_general_settings()
629 return settings.get('rhodecode_use_outdated_comments', False)
629 return settings.get('rhodecode_use_outdated_comments', False)
630
630
631
631
632 def _parse_comment_line_number(line_no):
632 def _parse_comment_line_number(line_no):
633 """
633 """
634 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
634 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
635 """
635 """
636 old_line = None
636 old_line = None
637 new_line = None
637 new_line = None
638 if line_no.startswith('o'):
638 if line_no.startswith('o'):
639 old_line = int(line_no[1:])
639 old_line = int(line_no[1:])
640 elif line_no.startswith('n'):
640 elif line_no.startswith('n'):
641 new_line = int(line_no[1:])
641 new_line = int(line_no[1:])
642 else:
642 else:
643 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
643 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
644 return diffs.DiffLineNumber(old_line, new_line)
644 return diffs.DiffLineNumber(old_line, new_line)
645
645
646
646
647 def _diff_to_comment_line_number(diff_line):
647 def _diff_to_comment_line_number(diff_line):
648 if diff_line.new is not None:
648 if diff_line.new is not None:
649 return u'n{}'.format(diff_line.new)
649 return u'n{}'.format(diff_line.new)
650 elif diff_line.old is not None:
650 elif diff_line.old is not None:
651 return u'o{}'.format(diff_line.old)
651 return u'o{}'.format(diff_line.old)
652 return u''
652 return u''
653
653
654
654
655 def _diff_line_delta(a, b):
655 def _diff_line_delta(a, b):
656 if None not in (a.new, b.new):
656 if None not in (a.new, b.new):
657 return abs(a.new - b.new)
657 return abs(a.new - b.new)
658 elif None not in (a.old, b.old):
658 elif None not in (a.old, b.old):
659 return abs(a.old - b.old)
659 return abs(a.old - b.old)
660 else:
660 else:
661 raise ValueError(
661 raise ValueError(
662 "Cannot compute delta between {} and {}".format(a, b))
662 "Cannot compute delta between {} and {}".format(a, b))
General Comments 0
You need to be logged in to leave comments. Login now