##// END OF EJS Templates
channelstream: don't try to communicate with ws server if not explictly enabled
ergo -
r546:c224f1f2 default
parent child Browse files
Show More
@@ -1,513 +1,512 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 comments model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import collections
28 28
29 29 from datetime import datetime
30 30
31 31 from pylons.i18n.translation import _
32 32 from pyramid.threadlocal import get_current_registry
33 33 from sqlalchemy.sql.expression import null
34 34 from sqlalchemy.sql.functions import coalesce
35 35
36 36 from rhodecode.lib import helpers as h, diffs
37 37 from rhodecode.lib.channelstream import channelstream_request
38 38 from rhodecode.lib.utils import action_logger
39 39 from rhodecode.lib.utils2 import extract_mentioned_users
40 40 from rhodecode.model import BaseModel
41 41 from rhodecode.model.db import (
42 42 ChangesetComment, User, Notification, PullRequest)
43 43 from rhodecode.model.notification import NotificationModel
44 44 from rhodecode.model.meta import Session
45 45 from rhodecode.model.settings import VcsSettingsModel
46 46 from rhodecode.model.notification import EmailNotificationModel
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50
51 51 class ChangesetCommentsModel(BaseModel):
52 52
53 53 cls = ChangesetComment
54 54
55 55 DIFF_CONTEXT_BEFORE = 3
56 56 DIFF_CONTEXT_AFTER = 3
57 57
58 58 def __get_commit_comment(self, changeset_comment):
59 59 return self._get_instance(ChangesetComment, changeset_comment)
60 60
61 61 def __get_pull_request(self, pull_request):
62 62 return self._get_instance(PullRequest, pull_request)
63 63
64 64 def _extract_mentions(self, s):
65 65 user_objects = []
66 66 for username in extract_mentioned_users(s):
67 67 user_obj = User.get_by_username(username, case_insensitive=True)
68 68 if user_obj:
69 69 user_objects.append(user_obj)
70 70 return user_objects
71 71
72 72 def _get_renderer(self, global_renderer='rst'):
73 73 try:
74 74 # try reading from visual context
75 75 from pylons import tmpl_context
76 76 global_renderer = tmpl_context.visual.default_renderer
77 77 except AttributeError:
78 78 log.debug("Renderer not set, falling back "
79 79 "to default renderer '%s'", global_renderer)
80 80 except Exception:
81 81 log.error(traceback.format_exc())
82 82 return global_renderer
83 83
84 84 def create(self, text, repo, user, revision=None, pull_request=None,
85 85 f_path=None, line_no=None, status_change=None, closing_pr=False,
86 86 send_email=True, renderer=None):
87 87 """
88 88 Creates new comment for commit or pull request.
89 89 IF status_change is not none this comment is associated with a
90 90 status change of commit or commit associated with pull request
91 91
92 92 :param text:
93 93 :param repo:
94 94 :param user:
95 95 :param revision:
96 96 :param pull_request:
97 97 :param f_path:
98 98 :param line_no:
99 99 :param status_change:
100 100 :param closing_pr:
101 101 :param send_email:
102 102 """
103 103 if not text:
104 104 log.warning('Missing text for comment, skipping...')
105 105 return
106 106
107 107 if not renderer:
108 108 renderer = self._get_renderer()
109 109
110 110 repo = self._get_repo(repo)
111 111 user = self._get_user(user)
112 112 comment = ChangesetComment()
113 113 comment.renderer = renderer
114 114 comment.repo = repo
115 115 comment.author = user
116 116 comment.text = text
117 117 comment.f_path = f_path
118 118 comment.line_no = line_no
119 119
120 120 #TODO (marcink): fix this and remove revision as param
121 121 commit_id = revision
122 122 pull_request_id = pull_request
123 123
124 124 commit_obj = None
125 125 pull_request_obj = None
126 126
127 127 if commit_id:
128 128 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
129 129 # do a lookup, so we don't pass something bad here
130 130 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
131 131 comment.revision = commit_obj.raw_id
132 132
133 133 elif pull_request_id:
134 134 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
135 135 pull_request_obj = self.__get_pull_request(pull_request_id)
136 136 comment.pull_request = pull_request_obj
137 137 else:
138 138 raise Exception('Please specify commit or pull_request_id')
139 139
140 140 Session().add(comment)
141 141 Session().flush()
142 142 kwargs = {
143 143 'user': user,
144 144 'renderer_type': renderer,
145 145 'repo_name': repo.repo_name,
146 146 'status_change': status_change,
147 147 'comment_body': text,
148 148 'comment_file': f_path,
149 149 'comment_line': line_no,
150 150 }
151 151
152 152 if commit_obj:
153 153 recipients = ChangesetComment.get_users(
154 154 revision=commit_obj.raw_id)
155 155 # add commit author if it's in RhodeCode system
156 156 cs_author = User.get_from_cs_author(commit_obj.author)
157 157 if not cs_author:
158 158 # use repo owner if we cannot extract the author correctly
159 159 cs_author = repo.user
160 160 recipients += [cs_author]
161 161
162 162 commit_comment_url = self.get_url(comment)
163 163
164 164 target_repo_url = h.link_to(
165 165 repo.repo_name,
166 166 h.url('summary_home',
167 167 repo_name=repo.repo_name, qualified=True))
168 168
169 169 # commit specifics
170 170 kwargs.update({
171 171 'commit': commit_obj,
172 172 'commit_message': commit_obj.message,
173 173 'commit_target_repo': target_repo_url,
174 174 'commit_comment_url': commit_comment_url,
175 175 })
176 176
177 177 elif pull_request_obj:
178 178 # get the current participants of this pull request
179 179 recipients = ChangesetComment.get_users(
180 180 pull_request_id=pull_request_obj.pull_request_id)
181 181 # add pull request author
182 182 recipients += [pull_request_obj.author]
183 183
184 184 # add the reviewers to notification
185 185 recipients += [x.user for x in pull_request_obj.reviewers]
186 186
187 187 pr_target_repo = pull_request_obj.target_repo
188 188 pr_source_repo = pull_request_obj.source_repo
189 189
190 190 pr_comment_url = h.url(
191 191 'pullrequest_show',
192 192 repo_name=pr_target_repo.repo_name,
193 193 pull_request_id=pull_request_obj.pull_request_id,
194 194 anchor='comment-%s' % comment.comment_id,
195 195 qualified=True,)
196 196
197 197 # set some variables for email notification
198 198 pr_target_repo_url = h.url(
199 199 'summary_home', repo_name=pr_target_repo.repo_name,
200 200 qualified=True)
201 201
202 202 pr_source_repo_url = h.url(
203 203 'summary_home', repo_name=pr_source_repo.repo_name,
204 204 qualified=True)
205 205
206 206 # pull request specifics
207 207 kwargs.update({
208 208 'pull_request': pull_request_obj,
209 209 'pr_id': pull_request_obj.pull_request_id,
210 210 'pr_target_repo': pr_target_repo,
211 211 'pr_target_repo_url': pr_target_repo_url,
212 212 'pr_source_repo': pr_source_repo,
213 213 'pr_source_repo_url': pr_source_repo_url,
214 214 'pr_comment_url': pr_comment_url,
215 215 'pr_closing': closing_pr,
216 216 })
217 217 if send_email:
218 218 # pre-generate the subject for notification itself
219 219 (subject,
220 220 _h, _e, # we don't care about those
221 221 body_plaintext) = EmailNotificationModel().render_email(
222 222 notification_type, **kwargs)
223 223
224 224 mention_recipients = set(
225 225 self._extract_mentions(text)).difference(recipients)
226 226
227 227 # create notification objects, and emails
228 228 NotificationModel().create(
229 229 created_by=user,
230 230 notification_subject=subject,
231 231 notification_body=body_plaintext,
232 232 notification_type=notification_type,
233 233 recipients=recipients,
234 234 mention_recipients=mention_recipients,
235 235 email_kwargs=kwargs,
236 236 )
237 237
238 238 action = (
239 239 'user_commented_pull_request:{}'.format(
240 240 comment.pull_request.pull_request_id)
241 241 if comment.pull_request
242 242 else 'user_commented_revision:{}'.format(comment.revision)
243 243 )
244 244 action_logger(user, action, comment.repo)
245 245
246 246 registry = get_current_registry()
247 247 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
248 channelstream_config = rhodecode_plugins.get('channelstream')
248 channelstream_config = rhodecode_plugins.get('channelstream', {})
249 249 msg_url = ''
250 250 if commit_obj:
251 251 msg_url = commit_comment_url
252 252 repo_name = repo.repo_name
253 253 elif pull_request_obj:
254 254 msg_url = pr_comment_url
255 255 repo_name = pr_target_repo.repo_name
256 256
257 if channelstream_config:
257 if channelstream_config.get('enabled'):
258 258 message = '<strong>{}</strong> {} - ' \
259 259 '<a onclick="window.location=\'{}\';' \
260 260 'window.location.reload()">' \
261 261 '<strong>{}</strong></a>'
262 262 message = message.format(
263 263 user.username, _('made a comment'), msg_url,
264 264 _('Refresh page'))
265 if channelstream_config:
266 channel = '/repo${}$/pr/{}'.format(
267 repo_name,
268 pull_request_id
269 )
270 payload = {
271 'type': 'message',
272 'timestamp': datetime.utcnow(),
273 'user': 'system',
274 'exclude_users': [user.username],
275 'channel': channel,
276 'message': {
277 'message': message,
278 'level': 'info',
279 'topic': '/notifications'
280 }
265 channel = '/repo${}$/pr/{}'.format(
266 repo_name,
267 pull_request_id
268 )
269 payload = {
270 'type': 'message',
271 'timestamp': datetime.utcnow(),
272 'user': 'system',
273 'exclude_users': [user.username],
274 'channel': channel,
275 'message': {
276 'message': message,
277 'level': 'info',
278 'topic': '/notifications'
281 279 }
282 channelstream_request(channelstream_config, [payload],
283 '/message', raise_exc=False)
280 }
281 channelstream_request(channelstream_config, [payload],
282 '/message', raise_exc=False)
284 283
285 284 return comment
286 285
287 286 def delete(self, comment):
288 287 """
289 288 Deletes given comment
290 289
291 290 :param comment_id:
292 291 """
293 292 comment = self.__get_commit_comment(comment)
294 293 Session().delete(comment)
295 294
296 295 return comment
297 296
298 297 def get_all_comments(self, repo_id, revision=None, pull_request=None):
299 298 q = ChangesetComment.query()\
300 299 .filter(ChangesetComment.repo_id == repo_id)
301 300 if revision:
302 301 q = q.filter(ChangesetComment.revision == revision)
303 302 elif pull_request:
304 303 pull_request = self.__get_pull_request(pull_request)
305 304 q = q.filter(ChangesetComment.pull_request == pull_request)
306 305 else:
307 306 raise Exception('Please specify commit or pull_request')
308 307 q = q.order_by(ChangesetComment.created_on)
309 308 return q.all()
310 309
311 310 def get_url(self, comment):
312 311 comment = self.__get_commit_comment(comment)
313 312 if comment.pull_request:
314 313 return h.url(
315 314 'pullrequest_show',
316 315 repo_name=comment.pull_request.target_repo.repo_name,
317 316 pull_request_id=comment.pull_request.pull_request_id,
318 317 anchor='comment-%s' % comment.comment_id,
319 318 qualified=True,)
320 319 else:
321 320 return h.url(
322 321 'changeset_home',
323 322 repo_name=comment.repo.repo_name,
324 323 revision=comment.revision,
325 324 anchor='comment-%s' % comment.comment_id,
326 325 qualified=True,)
327 326
328 327 def get_comments(self, repo_id, revision=None, pull_request=None):
329 328 """
330 329 Gets main comments based on revision or pull_request_id
331 330
332 331 :param repo_id:
333 332 :param revision:
334 333 :param pull_request:
335 334 """
336 335
337 336 q = ChangesetComment.query()\
338 337 .filter(ChangesetComment.repo_id == repo_id)\
339 338 .filter(ChangesetComment.line_no == None)\
340 339 .filter(ChangesetComment.f_path == None)
341 340 if revision:
342 341 q = q.filter(ChangesetComment.revision == revision)
343 342 elif pull_request:
344 343 pull_request = self.__get_pull_request(pull_request)
345 344 q = q.filter(ChangesetComment.pull_request == pull_request)
346 345 else:
347 346 raise Exception('Please specify commit or pull_request')
348 347 q = q.order_by(ChangesetComment.created_on)
349 348 return q.all()
350 349
351 350 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
352 351 q = self._get_inline_comments_query(repo_id, revision, pull_request)
353 352 return self._group_comments_by_path_and_line_number(q)
354 353
355 354 def get_outdated_comments(self, repo_id, pull_request):
356 355 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
357 356 # of a pull request.
358 357 q = self._all_inline_comments_of_pull_request(pull_request)
359 358 q = q.filter(
360 359 ChangesetComment.display_state ==
361 360 ChangesetComment.COMMENT_OUTDATED
362 361 ).order_by(ChangesetComment.comment_id.asc())
363 362
364 363 return self._group_comments_by_path_and_line_number(q)
365 364
366 365 def _get_inline_comments_query(self, repo_id, revision, pull_request):
367 366 # TODO: johbo: Split this into two methods: One for PR and one for
368 367 # commit.
369 368 if revision:
370 369 q = Session().query(ChangesetComment).filter(
371 370 ChangesetComment.repo_id == repo_id,
372 371 ChangesetComment.line_no != null(),
373 372 ChangesetComment.f_path != null(),
374 373 ChangesetComment.revision == revision)
375 374
376 375 elif pull_request:
377 376 pull_request = self.__get_pull_request(pull_request)
378 377 if ChangesetCommentsModel.use_outdated_comments(pull_request):
379 378 q = self._visible_inline_comments_of_pull_request(pull_request)
380 379 else:
381 380 q = self._all_inline_comments_of_pull_request(pull_request)
382 381
383 382 else:
384 383 raise Exception('Please specify commit or pull_request_id')
385 384 q = q.order_by(ChangesetComment.comment_id.asc())
386 385 return q
387 386
388 387 def _group_comments_by_path_and_line_number(self, q):
389 388 comments = q.all()
390 389 paths = collections.defaultdict(lambda: collections.defaultdict(list))
391 390 for co in comments:
392 391 paths[co.f_path][co.line_no].append(co)
393 392 return paths
394 393
395 394 @classmethod
396 395 def needed_extra_diff_context(cls):
397 396 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
398 397
399 398 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
400 399 if not ChangesetCommentsModel.use_outdated_comments(pull_request):
401 400 return
402 401
403 402 comments = self._visible_inline_comments_of_pull_request(pull_request)
404 403 comments_to_outdate = comments.all()
405 404
406 405 for comment in comments_to_outdate:
407 406 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
408 407
409 408 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
410 409 diff_line = _parse_comment_line_number(comment.line_no)
411 410
412 411 try:
413 412 old_context = old_diff_proc.get_context_of_line(
414 413 path=comment.f_path, diff_line=diff_line)
415 414 new_context = new_diff_proc.get_context_of_line(
416 415 path=comment.f_path, diff_line=diff_line)
417 416 except (diffs.LineNotInDiffException,
418 417 diffs.FileNotInDiffException):
419 418 comment.display_state = ChangesetComment.COMMENT_OUTDATED
420 419 return
421 420
422 421 if old_context == new_context:
423 422 return
424 423
425 424 if self._should_relocate_diff_line(diff_line):
426 425 new_diff_lines = new_diff_proc.find_context(
427 426 path=comment.f_path, context=old_context,
428 427 offset=self.DIFF_CONTEXT_BEFORE)
429 428 if not new_diff_lines:
430 429 comment.display_state = ChangesetComment.COMMENT_OUTDATED
431 430 else:
432 431 new_diff_line = self._choose_closest_diff_line(
433 432 diff_line, new_diff_lines)
434 433 comment.line_no = _diff_to_comment_line_number(new_diff_line)
435 434 else:
436 435 comment.display_state = ChangesetComment.COMMENT_OUTDATED
437 436
438 437 def _should_relocate_diff_line(self, diff_line):
439 438 """
440 439 Checks if relocation shall be tried for the given `diff_line`.
441 440
442 441 If a comment points into the first lines, then we can have a situation
443 442 that after an update another line has been added on top. In this case
444 443 we would find the context still and move the comment around. This
445 444 would be wrong.
446 445 """
447 446 should_relocate = (
448 447 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
449 448 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
450 449 return should_relocate
451 450
452 451 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
453 452 candidate = new_diff_lines[0]
454 453 best_delta = _diff_line_delta(diff_line, candidate)
455 454 for new_diff_line in new_diff_lines[1:]:
456 455 delta = _diff_line_delta(diff_line, new_diff_line)
457 456 if delta < best_delta:
458 457 candidate = new_diff_line
459 458 best_delta = delta
460 459 return candidate
461 460
462 461 def _visible_inline_comments_of_pull_request(self, pull_request):
463 462 comments = self._all_inline_comments_of_pull_request(pull_request)
464 463 comments = comments.filter(
465 464 coalesce(ChangesetComment.display_state, '') !=
466 465 ChangesetComment.COMMENT_OUTDATED)
467 466 return comments
468 467
469 468 def _all_inline_comments_of_pull_request(self, pull_request):
470 469 comments = Session().query(ChangesetComment)\
471 470 .filter(ChangesetComment.line_no != None)\
472 471 .filter(ChangesetComment.f_path != None)\
473 472 .filter(ChangesetComment.pull_request == pull_request)
474 473 return comments
475 474
476 475 @staticmethod
477 476 def use_outdated_comments(pull_request):
478 477 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
479 478 settings = settings_model.get_general_settings()
480 479 return settings.get('rhodecode_use_outdated_comments', False)
481 480
482 481
483 482 def _parse_comment_line_number(line_no):
484 483 """
485 484 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
486 485 """
487 486 old_line = None
488 487 new_line = None
489 488 if line_no.startswith('o'):
490 489 old_line = int(line_no[1:])
491 490 elif line_no.startswith('n'):
492 491 new_line = int(line_no[1:])
493 492 else:
494 493 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
495 494 return diffs.DiffLineNumber(old_line, new_line)
496 495
497 496
498 497 def _diff_to_comment_line_number(diff_line):
499 498 if diff_line.new is not None:
500 499 return u'n{}'.format(diff_line.new)
501 500 elif diff_line.old is not None:
502 501 return u'o{}'.format(diff_line.old)
503 502 return u''
504 503
505 504
506 505 def _diff_line_delta(a, b):
507 506 if None not in (a.new, b.new):
508 507 return abs(a.new - b.new)
509 508 elif None not in (a.old, b.old):
510 509 return abs(a.old - b.old)
511 510 else:
512 511 raise ValueError(
513 512 "Cannot compute delta between {} and {}".format(a, b))
General Comments 0
You need to be logged in to leave comments. Login now