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