##// END OF EJS Templates
comments: use unified aggregation of comments counters....
marcink -
r1332:f4e615fc default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,1029 +1,1018 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 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 pull requests controller for rhodecode for initializing pull requests
23 23 """
24 24 import types
25 25
26 26 import peppercorn
27 27 import formencode
28 28 import logging
29 29 import collections
30 30
31 31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 32 from pylons import request, tmpl_context as c, url
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35 from pyramid.threadlocal import get_current_registry
36 36 from sqlalchemy.sql import func
37 37 from sqlalchemy.sql.expression import or_
38 38
39 39 from rhodecode import events
40 40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 41 from rhodecode.lib.ext_json import json
42 42 from rhodecode.lib.base import (
43 43 BaseRepoController, render, vcs_operation_context)
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 46 HasAcceptedRepoType, XHRRequired)
47 47 from rhodecode.lib.channelstream import channelstream_request
48 48 from rhodecode.lib.utils import jsonify
49 49 from rhodecode.lib.utils2 import (
50 50 safe_int, safe_str, str2bool, safe_unicode)
51 51 from rhodecode.lib.vcs.backends.base import (
52 52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 55 NodeDoesNotExistError)
56 56
57 57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 58 from rhodecode.model.comment import CommentsModel
59 59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 60 Repository, PullRequestVersion)
61 61 from rhodecode.model.forms import PullRequestForm
62 62 from rhodecode.model.meta import Session
63 63 from rhodecode.model.pull_request import PullRequestModel
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 class PullrequestsController(BaseRepoController):
69 69 def __before__(self):
70 70 super(PullrequestsController, self).__before__()
71 71
72 72 def _load_compare_data(self, pull_request, inline_comments):
73 73 """
74 74 Load context data needed for generating compare diff
75 75
76 76 :param pull_request: object related to the request
77 77 :param enable_comments: flag to determine if comments are included
78 78 """
79 79 source_repo = pull_request.source_repo
80 80 source_ref_id = pull_request.source_ref_parts.commit_id
81 81
82 82 target_repo = pull_request.target_repo
83 83 target_ref_id = pull_request.target_ref_parts.commit_id
84 84
85 85 # despite opening commits for bookmarks/branches/tags, we always
86 86 # convert this to rev to prevent changes after bookmark or branch change
87 87 c.source_ref_type = 'rev'
88 88 c.source_ref = source_ref_id
89 89
90 90 c.target_ref_type = 'rev'
91 91 c.target_ref = target_ref_id
92 92
93 93 c.source_repo = source_repo
94 94 c.target_repo = target_repo
95 95
96 96 c.fulldiff = bool(request.GET.get('fulldiff'))
97 97
98 98 # diff_limit is the old behavior, will cut off the whole diff
99 99 # if the limit is applied otherwise will just hide the
100 100 # big files from the front-end
101 101 diff_limit = self.cut_off_limit_diff
102 102 file_limit = self.cut_off_limit_file
103 103
104 104 pre_load = ["author", "branch", "date", "message"]
105 105
106 106 c.commit_ranges = []
107 107 source_commit = EmptyCommit()
108 108 target_commit = EmptyCommit()
109 109 c.missing_requirements = False
110 110 try:
111 111 c.commit_ranges = [
112 112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
113 113 for rev in pull_request.revisions]
114 114
115 115 c.statuses = source_repo.statuses(
116 116 [x.raw_id for x in c.commit_ranges])
117 117
118 118 target_commit = source_repo.get_commit(
119 119 commit_id=safe_str(target_ref_id))
120 120 source_commit = source_repo.get_commit(
121 121 commit_id=safe_str(source_ref_id))
122 122 except RepositoryRequirementError:
123 123 c.missing_requirements = True
124 124
125 125 # auto collapse if we have more than limit
126 126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
127 127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
128 128
129 129 c.changes = {}
130 130 c.missing_commits = False
131 131 if (c.missing_requirements or
132 132 isinstance(source_commit, EmptyCommit) or
133 133 source_commit == target_commit):
134 134 _parsed = []
135 135 c.missing_commits = True
136 136 else:
137 137 vcs_diff = PullRequestModel().get_diff(pull_request)
138 138 diff_processor = diffs.DiffProcessor(
139 139 vcs_diff, format='newdiff', diff_limit=diff_limit,
140 140 file_limit=file_limit, show_full_diff=c.fulldiff)
141 141
142 142 _parsed = diff_processor.prepare()
143 143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
144 144
145 145 included_files = {}
146 146 for f in _parsed:
147 147 included_files[f['filename']] = f['stats']
148 148
149 149 c.deleted_files = [fname for fname in inline_comments if
150 150 fname not in included_files]
151 151
152 152 c.deleted_files_comments = collections.defaultdict(dict)
153 153 for fname, per_line_comments in inline_comments.items():
154 154 if fname in c.deleted_files:
155 155 c.deleted_files_comments[fname]['stats'] = 0
156 156 c.deleted_files_comments[fname]['comments'] = list()
157 157 for lno, comments in per_line_comments.items():
158 158 c.deleted_files_comments[fname]['comments'].extend(comments)
159 159
160 160 def _node_getter(commit):
161 161 def get_node(fname):
162 162 try:
163 163 return commit.get_node(fname)
164 164 except NodeDoesNotExistError:
165 165 return None
166 166 return get_node
167 167
168 168 c.diffset = codeblocks.DiffSet(
169 169 repo_name=c.repo_name,
170 170 source_repo_name=c.source_repo.repo_name,
171 171 source_node_getter=_node_getter(target_commit),
172 172 target_node_getter=_node_getter(source_commit),
173 173 comments=inline_comments
174 174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
175 175
176 176 def _extract_ordering(self, request):
177 177 column_index = safe_int(request.GET.get('order[0][column]'))
178 178 order_dir = request.GET.get('order[0][dir]', 'desc')
179 179 order_by = request.GET.get(
180 180 'columns[%s][data][sort]' % column_index, 'name_raw')
181 181 return order_by, order_dir
182 182
183 183 @LoginRequired()
184 184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
185 185 'repository.admin')
186 186 @HasAcceptedRepoType('git', 'hg')
187 187 def show_all(self, repo_name):
188 188 # filter types
189 189 c.active = 'open'
190 190 c.source = str2bool(request.GET.get('source'))
191 191 c.closed = str2bool(request.GET.get('closed'))
192 192 c.my = str2bool(request.GET.get('my'))
193 193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
194 194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
195 195 c.repo_name = repo_name
196 196
197 197 opened_by = None
198 198 if c.my:
199 199 c.active = 'my'
200 200 opened_by = [c.rhodecode_user.user_id]
201 201
202 202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 203 if c.closed:
204 204 c.active = 'closed'
205 205 statuses = [PullRequest.STATUS_CLOSED]
206 206
207 207 if c.awaiting_review and not c.source:
208 208 c.active = 'awaiting'
209 209 if c.source and not c.awaiting_review:
210 210 c.active = 'source'
211 211 if c.awaiting_my_review:
212 212 c.active = 'awaiting_my'
213 213
214 214 data = self._get_pull_requests_list(
215 215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
216 216 if not request.is_xhr:
217 217 c.data = json.dumps(data['data'])
218 218 c.records_total = data['recordsTotal']
219 219 return render('/pullrequests/pullrequests.mako')
220 220 else:
221 221 return json.dumps(data)
222 222
223 223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
224 224 # pagination
225 225 start = safe_int(request.GET.get('start'), 0)
226 226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
227 227 order_by, order_dir = self._extract_ordering(request)
228 228
229 229 if c.awaiting_review:
230 230 pull_requests = PullRequestModel().get_awaiting_review(
231 231 repo_name, source=c.source, opened_by=opened_by,
232 232 statuses=statuses, offset=start, length=length,
233 233 order_by=order_by, order_dir=order_dir)
234 234 pull_requests_total_count = PullRequestModel(
235 235 ).count_awaiting_review(
236 236 repo_name, source=c.source, statuses=statuses,
237 237 opened_by=opened_by)
238 238 elif c.awaiting_my_review:
239 239 pull_requests = PullRequestModel().get_awaiting_my_review(
240 240 repo_name, source=c.source, opened_by=opened_by,
241 241 user_id=c.rhodecode_user.user_id, statuses=statuses,
242 242 offset=start, length=length, order_by=order_by,
243 243 order_dir=order_dir)
244 244 pull_requests_total_count = PullRequestModel(
245 245 ).count_awaiting_my_review(
246 246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
247 247 statuses=statuses, opened_by=opened_by)
248 248 else:
249 249 pull_requests = PullRequestModel().get_all(
250 250 repo_name, source=c.source, opened_by=opened_by,
251 251 statuses=statuses, offset=start, length=length,
252 252 order_by=order_by, order_dir=order_dir)
253 253 pull_requests_total_count = PullRequestModel().count_all(
254 254 repo_name, source=c.source, statuses=statuses,
255 255 opened_by=opened_by)
256 256
257 257 from rhodecode.lib.utils import PartialRenderer
258 258 _render = PartialRenderer('data_table/_dt_elements.mako')
259 259 data = []
260 260 for pr in pull_requests:
261 261 comments = CommentsModel().get_all_comments(
262 262 c.rhodecode_db_repo.repo_id, pull_request=pr)
263 263
264 264 data.append({
265 265 'name': _render('pullrequest_name',
266 266 pr.pull_request_id, pr.target_repo.repo_name),
267 267 'name_raw': pr.pull_request_id,
268 268 'status': _render('pullrequest_status',
269 269 pr.calculated_review_status()),
270 270 'title': _render(
271 271 'pullrequest_title', pr.title, pr.description),
272 272 'description': h.escape(pr.description),
273 273 'updated_on': _render('pullrequest_updated_on',
274 274 h.datetime_to_time(pr.updated_on)),
275 275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
276 276 'created_on': _render('pullrequest_updated_on',
277 277 h.datetime_to_time(pr.created_on)),
278 278 'created_on_raw': h.datetime_to_time(pr.created_on),
279 279 'author': _render('pullrequest_author',
280 280 pr.author.full_contact, ),
281 281 'author_raw': pr.author.full_name,
282 282 'comments': _render('pullrequest_comments', len(comments)),
283 283 'comments_raw': len(comments),
284 284 'closed': pr.is_closed(),
285 285 })
286 286 # json used to render the grid
287 287 data = ({
288 288 'data': data,
289 289 'recordsTotal': pull_requests_total_count,
290 290 'recordsFiltered': pull_requests_total_count,
291 291 })
292 292 return data
293 293
294 294 @LoginRequired()
295 295 @NotAnonymous()
296 296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
297 297 'repository.admin')
298 298 @HasAcceptedRepoType('git', 'hg')
299 299 def index(self):
300 300 source_repo = c.rhodecode_db_repo
301 301
302 302 try:
303 303 source_repo.scm_instance().get_commit()
304 304 except EmptyRepositoryError:
305 305 h.flash(h.literal(_('There are no commits yet')),
306 306 category='warning')
307 307 redirect(url('summary_home', repo_name=source_repo.repo_name))
308 308
309 309 commit_id = request.GET.get('commit')
310 310 branch_ref = request.GET.get('branch')
311 311 bookmark_ref = request.GET.get('bookmark')
312 312
313 313 try:
314 314 source_repo_data = PullRequestModel().generate_repo_data(
315 315 source_repo, commit_id=commit_id,
316 316 branch=branch_ref, bookmark=bookmark_ref)
317 317 except CommitDoesNotExistError as e:
318 318 log.exception(e)
319 319 h.flash(_('Commit does not exist'), 'error')
320 320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
321 321
322 322 default_target_repo = source_repo
323 323
324 324 if source_repo.parent:
325 325 parent_vcs_obj = source_repo.parent.scm_instance()
326 326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
327 327 # change default if we have a parent repo
328 328 default_target_repo = source_repo.parent
329 329
330 330 target_repo_data = PullRequestModel().generate_repo_data(
331 331 default_target_repo)
332 332
333 333 selected_source_ref = source_repo_data['refs']['selected_ref']
334 334
335 335 title_source_ref = selected_source_ref.split(':', 2)[1]
336 336 c.default_title = PullRequestModel().generate_pullrequest_title(
337 337 source=source_repo.repo_name,
338 338 source_ref=title_source_ref,
339 339 target=default_target_repo.repo_name
340 340 )
341 341
342 342 c.default_repo_data = {
343 343 'source_repo_name': source_repo.repo_name,
344 344 'source_refs_json': json.dumps(source_repo_data),
345 345 'target_repo_name': default_target_repo.repo_name,
346 346 'target_refs_json': json.dumps(target_repo_data),
347 347 }
348 348 c.default_source_ref = selected_source_ref
349 349
350 350 return render('/pullrequests/pullrequest.mako')
351 351
352 352 @LoginRequired()
353 353 @NotAnonymous()
354 354 @XHRRequired()
355 355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
356 356 'repository.admin')
357 357 @jsonify
358 358 def get_repo_refs(self, repo_name, target_repo_name):
359 359 repo = Repository.get_by_repo_name(target_repo_name)
360 360 if not repo:
361 361 raise HTTPNotFound
362 362 return PullRequestModel().generate_repo_data(repo)
363 363
364 364 @LoginRequired()
365 365 @NotAnonymous()
366 366 @XHRRequired()
367 367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
368 368 'repository.admin')
369 369 @jsonify
370 370 def get_repo_destinations(self, repo_name):
371 371 repo = Repository.get_by_repo_name(repo_name)
372 372 if not repo:
373 373 raise HTTPNotFound
374 374 filter_query = request.GET.get('query')
375 375
376 376 query = Repository.query() \
377 377 .order_by(func.length(Repository.repo_name)) \
378 378 .filter(or_(
379 379 Repository.repo_name == repo.repo_name,
380 380 Repository.fork_id == repo.repo_id))
381 381
382 382 if filter_query:
383 383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
384 384 query = query.filter(
385 385 Repository.repo_name.ilike(ilike_expression))
386 386
387 387 add_parent = False
388 388 if repo.parent:
389 389 if filter_query in repo.parent.repo_name:
390 390 parent_vcs_obj = repo.parent.scm_instance()
391 391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
392 392 add_parent = True
393 393
394 394 limit = 20 - 1 if add_parent else 20
395 395 all_repos = query.limit(limit).all()
396 396 if add_parent:
397 397 all_repos += [repo.parent]
398 398
399 399 repos = []
400 400 for obj in self.scm_model.get_repos(all_repos):
401 401 repos.append({
402 402 'id': obj['name'],
403 403 'text': obj['name'],
404 404 'type': 'repo',
405 405 'obj': obj['dbrepo']
406 406 })
407 407
408 408 data = {
409 409 'more': False,
410 410 'results': [{
411 411 'text': _('Repositories'),
412 412 'children': repos
413 413 }] if repos else []
414 414 }
415 415 return data
416 416
417 417 @LoginRequired()
418 418 @NotAnonymous()
419 419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
420 420 'repository.admin')
421 421 @HasAcceptedRepoType('git', 'hg')
422 422 @auth.CSRFRequired()
423 423 def create(self, repo_name):
424 424 repo = Repository.get_by_repo_name(repo_name)
425 425 if not repo:
426 426 raise HTTPNotFound
427 427
428 428 controls = peppercorn.parse(request.POST.items())
429 429
430 430 try:
431 431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
432 432 except formencode.Invalid as errors:
433 433 if errors.error_dict.get('revisions'):
434 434 msg = 'Revisions: %s' % errors.error_dict['revisions']
435 435 elif errors.error_dict.get('pullrequest_title'):
436 436 msg = _('Pull request requires a title with min. 3 chars')
437 437 else:
438 438 msg = _('Error creating pull request: {}').format(errors)
439 439 log.exception(msg)
440 440 h.flash(msg, 'error')
441 441
442 442 # would rather just go back to form ...
443 443 return redirect(url('pullrequest_home', repo_name=repo_name))
444 444
445 445 source_repo = _form['source_repo']
446 446 source_ref = _form['source_ref']
447 447 target_repo = _form['target_repo']
448 448 target_ref = _form['target_ref']
449 449 commit_ids = _form['revisions'][::-1]
450 450 reviewers = [
451 451 (r['user_id'], r['reasons']) for r in _form['review_members']]
452 452
453 453 # find the ancestor for this pr
454 454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
455 455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
456 456
457 457 source_scm = source_db_repo.scm_instance()
458 458 target_scm = target_db_repo.scm_instance()
459 459
460 460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
461 461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
462 462
463 463 ancestor = source_scm.get_common_ancestor(
464 464 source_commit.raw_id, target_commit.raw_id, target_scm)
465 465
466 466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
467 467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
468 468
469 469 pullrequest_title = _form['pullrequest_title']
470 470 title_source_ref = source_ref.split(':', 2)[1]
471 471 if not pullrequest_title:
472 472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
473 473 source=source_repo,
474 474 source_ref=title_source_ref,
475 475 target=target_repo
476 476 )
477 477
478 478 description = _form['pullrequest_desc']
479 479 try:
480 480 pull_request = PullRequestModel().create(
481 481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
482 482 target_ref, commit_ids, reviewers, pullrequest_title,
483 483 description
484 484 )
485 485 Session().commit()
486 486 h.flash(_('Successfully opened new pull request'),
487 487 category='success')
488 488 except Exception as e:
489 489 msg = _('Error occurred during sending pull request')
490 490 log.exception(msg)
491 491 h.flash(msg, category='error')
492 492 return redirect(url('pullrequest_home', repo_name=repo_name))
493 493
494 494 return redirect(url('pullrequest_show', repo_name=target_repo,
495 495 pull_request_id=pull_request.pull_request_id))
496 496
497 497 @LoginRequired()
498 498 @NotAnonymous()
499 499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
500 500 'repository.admin')
501 501 @auth.CSRFRequired()
502 502 @jsonify
503 503 def update(self, repo_name, pull_request_id):
504 504 pull_request_id = safe_int(pull_request_id)
505 505 pull_request = PullRequest.get_or_404(pull_request_id)
506 506 # only owner or admin can update it
507 507 allowed_to_update = PullRequestModel().check_user_update(
508 508 pull_request, c.rhodecode_user)
509 509 if allowed_to_update:
510 510 controls = peppercorn.parse(request.POST.items())
511 511
512 512 if 'review_members' in controls:
513 513 self._update_reviewers(
514 514 pull_request_id, controls['review_members'])
515 515 elif str2bool(request.POST.get('update_commits', 'false')):
516 516 self._update_commits(pull_request)
517 517 elif str2bool(request.POST.get('close_pull_request', 'false')):
518 518 self._reject_close(pull_request)
519 519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
520 520 self._edit_pull_request(pull_request)
521 521 else:
522 522 raise HTTPBadRequest()
523 523 return True
524 524 raise HTTPForbidden()
525 525
526 526 def _edit_pull_request(self, pull_request):
527 527 try:
528 528 PullRequestModel().edit(
529 529 pull_request, request.POST.get('title'),
530 530 request.POST.get('description'))
531 531 except ValueError:
532 532 msg = _(u'Cannot update closed pull requests.')
533 533 h.flash(msg, category='error')
534 534 return
535 535 else:
536 536 Session().commit()
537 537
538 538 msg = _(u'Pull request title & description updated.')
539 539 h.flash(msg, category='success')
540 540 return
541 541
542 542 def _update_commits(self, pull_request):
543 543 resp = PullRequestModel().update_commits(pull_request)
544 544
545 545 if resp.executed:
546 546 msg = _(
547 547 u'Pull request updated to "{source_commit_id}" with '
548 548 u'{count_added} added, {count_removed} removed commits.')
549 549 msg = msg.format(
550 550 source_commit_id=pull_request.source_ref_parts.commit_id,
551 551 count_added=len(resp.changes.added),
552 552 count_removed=len(resp.changes.removed))
553 553 h.flash(msg, category='success')
554 554
555 555 registry = get_current_registry()
556 556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
557 557 channelstream_config = rhodecode_plugins.get('channelstream', {})
558 558 if channelstream_config.get('enabled'):
559 559 message = msg + (
560 560 ' - <a onclick="window.location.reload()">'
561 561 '<strong>{}</strong></a>'.format(_('Reload page')))
562 562 channel = '/repo${}$/pr/{}'.format(
563 563 pull_request.target_repo.repo_name,
564 564 pull_request.pull_request_id
565 565 )
566 566 payload = {
567 567 'type': 'message',
568 568 'user': 'system',
569 569 'exclude_users': [request.user.username],
570 570 'channel': channel,
571 571 'message': {
572 572 'message': message,
573 573 'level': 'success',
574 574 'topic': '/notifications'
575 575 }
576 576 }
577 577 channelstream_request(
578 578 channelstream_config, [payload], '/message',
579 579 raise_exc=False)
580 580 else:
581 581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
582 582 warning_reasons = [
583 583 UpdateFailureReason.NO_CHANGE,
584 584 UpdateFailureReason.WRONG_REF_TPYE,
585 585 ]
586 586 category = 'warning' if resp.reason in warning_reasons else 'error'
587 587 h.flash(msg, category=category)
588 588
589 589 @auth.CSRFRequired()
590 590 @LoginRequired()
591 591 @NotAnonymous()
592 592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
593 593 'repository.admin')
594 594 def merge(self, repo_name, pull_request_id):
595 595 """
596 596 POST /{repo_name}/pull-request/{pull_request_id}
597 597
598 598 Merge will perform a server-side merge of the specified
599 599 pull request, if the pull request is approved and mergeable.
600 600 After succesfull merging, the pull request is automatically
601 601 closed, with a relevant comment.
602 602 """
603 603 pull_request_id = safe_int(pull_request_id)
604 604 pull_request = PullRequest.get_or_404(pull_request_id)
605 605 user = c.rhodecode_user
606 606
607 607 if self._meets_merge_pre_conditions(pull_request, user):
608 608 log.debug("Pre-conditions checked, trying to merge.")
609 609 extras = vcs_operation_context(
610 610 request.environ, repo_name=pull_request.target_repo.repo_name,
611 611 username=user.username, action='push',
612 612 scm=pull_request.target_repo.repo_type)
613 613 self._merge_pull_request(pull_request, user, extras)
614 614
615 615 return redirect(url(
616 616 'pullrequest_show',
617 617 repo_name=pull_request.target_repo.repo_name,
618 618 pull_request_id=pull_request.pull_request_id))
619 619
620 620 def _meets_merge_pre_conditions(self, pull_request, user):
621 621 if not PullRequestModel().check_user_merge(pull_request, user):
622 622 raise HTTPForbidden()
623 623
624 624 merge_status, msg = PullRequestModel().merge_status(pull_request)
625 625 if not merge_status:
626 626 log.debug("Cannot merge, not mergeable.")
627 627 h.flash(msg, category='error')
628 628 return False
629 629
630 630 if (pull_request.calculated_review_status()
631 631 is not ChangesetStatus.STATUS_APPROVED):
632 632 log.debug("Cannot merge, approval is pending.")
633 633 msg = _('Pull request reviewer approval is pending.')
634 634 h.flash(msg, category='error')
635 635 return False
636 636 return True
637 637
638 638 def _merge_pull_request(self, pull_request, user, extras):
639 639 merge_resp = PullRequestModel().merge(
640 640 pull_request, user, extras=extras)
641 641
642 642 if merge_resp.executed:
643 643 log.debug("The merge was successful, closing the pull request.")
644 644 PullRequestModel().close_pull_request(
645 645 pull_request.pull_request_id, user)
646 646 Session().commit()
647 647 msg = _('Pull request was successfully merged and closed.')
648 648 h.flash(msg, category='success')
649 649 else:
650 650 log.debug(
651 651 "The merge was not successful. Merge response: %s",
652 652 merge_resp)
653 653 msg = PullRequestModel().merge_status_message(
654 654 merge_resp.failure_reason)
655 655 h.flash(msg, category='error')
656 656
657 657 def _update_reviewers(self, pull_request_id, review_members):
658 658 reviewers = [
659 659 (int(r['user_id']), r['reasons']) for r in review_members]
660 660 PullRequestModel().update_reviewers(pull_request_id, reviewers)
661 661 Session().commit()
662 662
663 663 def _reject_close(self, pull_request):
664 664 if pull_request.is_closed():
665 665 raise HTTPForbidden()
666 666
667 667 PullRequestModel().close_pull_request_with_comment(
668 668 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
669 669 Session().commit()
670 670
671 671 @LoginRequired()
672 672 @NotAnonymous()
673 673 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
674 674 'repository.admin')
675 675 @auth.CSRFRequired()
676 676 @jsonify
677 677 def delete(self, repo_name, pull_request_id):
678 678 pull_request_id = safe_int(pull_request_id)
679 679 pull_request = PullRequest.get_or_404(pull_request_id)
680 680 # only owner can delete it !
681 681 if pull_request.author.user_id == c.rhodecode_user.user_id:
682 682 PullRequestModel().delete(pull_request)
683 683 Session().commit()
684 684 h.flash(_('Successfully deleted pull request'),
685 685 category='success')
686 686 return redirect(url('my_account_pullrequests'))
687 687 raise HTTPForbidden()
688 688
689 689 def _get_pr_version(self, pull_request_id, version=None):
690 690 pull_request_id = safe_int(pull_request_id)
691 691 at_version = None
692 692
693 693 if version and version == 'latest':
694 694 pull_request_ver = PullRequest.get(pull_request_id)
695 695 pull_request_obj = pull_request_ver
696 696 _org_pull_request_obj = pull_request_obj
697 697 at_version = 'latest'
698 698 elif version:
699 699 pull_request_ver = PullRequestVersion.get_or_404(version)
700 700 pull_request_obj = pull_request_ver
701 701 _org_pull_request_obj = pull_request_ver.pull_request
702 702 at_version = pull_request_ver.pull_request_version_id
703 703 else:
704 704 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
705 705
706 706 pull_request_display_obj = PullRequest.get_pr_display_object(
707 707 pull_request_obj, _org_pull_request_obj)
708 708 return _org_pull_request_obj, pull_request_obj, \
709 709 pull_request_display_obj, at_version
710 710
711 711 def _get_pr_version_changes(self, version, pull_request_latest):
712 712 """
713 713 Generate changes commits, and diff data based on the current pr version
714 714 """
715 715
716 716 #TODO(marcink): save those changes as JSON metadata for chaching later.
717 717
718 718 # fake the version to add the "initial" state object
719 719 pull_request_initial = PullRequest.get_pr_display_object(
720 720 pull_request_latest, pull_request_latest,
721 721 internal_methods=['get_commit', 'versions'])
722 722 pull_request_initial.revisions = []
723 723 pull_request_initial.source_repo.get_commit = types.MethodType(
724 724 lambda *a, **k: EmptyCommit(), pull_request_initial)
725 725 pull_request_initial.source_repo.scm_instance = types.MethodType(
726 726 lambda *a, **k: EmptyRepository(), pull_request_initial)
727 727
728 728 _changes_versions = [pull_request_latest] + \
729 729 list(reversed(c.versions)) + \
730 730 [pull_request_initial]
731 731
732 732 if version == 'latest':
733 733 index = 0
734 734 else:
735 735 for pos, prver in enumerate(_changes_versions):
736 736 ver = getattr(prver, 'pull_request_version_id', -1)
737 737 if ver == safe_int(version):
738 738 index = pos
739 739 break
740 740 else:
741 741 index = 0
742 742
743 743 cur_obj = _changes_versions[index]
744 744 prev_obj = _changes_versions[index + 1]
745 745
746 746 old_commit_ids = set(prev_obj.revisions)
747 747 new_commit_ids = set(cur_obj.revisions)
748 748
749 749 changes = PullRequestModel()._calculate_commit_id_changes(
750 750 old_commit_ids, new_commit_ids)
751 751
752 752 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
753 753 cur_obj, prev_obj)
754 754 file_changes = PullRequestModel()._calculate_file_changes(
755 755 old_diff_data, new_diff_data)
756 756 return changes, file_changes
757 757
758 758 @LoginRequired()
759 759 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
760 760 'repository.admin')
761 761 def show(self, repo_name, pull_request_id):
762 762 pull_request_id = safe_int(pull_request_id)
763 763 version = request.GET.get('version')
764 764
765 765 (pull_request_latest,
766 766 pull_request_at_ver,
767 767 pull_request_display_obj,
768 768 at_version) = self._get_pr_version(pull_request_id, version=version)
769 769
770 770 c.template_context['pull_request_data']['pull_request_id'] = \
771 771 pull_request_id
772 772
773 773 # pull_requests repo_name we opened it against
774 774 # ie. target_repo must match
775 775 if repo_name != pull_request_at_ver.target_repo.repo_name:
776 776 raise HTTPNotFound
777 777
778 778 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
779 779 pull_request_at_ver)
780 780
781 781 pr_closed = pull_request_latest.is_closed()
782 782 if at_version and not at_version == 'latest':
783 783 c.allowed_to_change_status = False
784 784 c.allowed_to_update = False
785 785 c.allowed_to_merge = False
786 786 c.allowed_to_delete = False
787 787 c.allowed_to_comment = False
788 788 else:
789 789 c.allowed_to_change_status = PullRequestModel(). \
790 790 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
791 791 c.allowed_to_update = PullRequestModel().check_user_update(
792 792 pull_request_latest, c.rhodecode_user) and not pr_closed
793 793 c.allowed_to_merge = PullRequestModel().check_user_merge(
794 794 pull_request_latest, c.rhodecode_user) and not pr_closed
795 795 c.allowed_to_delete = PullRequestModel().check_user_delete(
796 796 pull_request_latest, c.rhodecode_user) and not pr_closed
797 797 c.allowed_to_comment = not pr_closed
798 798
799 799 cc_model = CommentsModel()
800 800
801 801 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
802 802 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
803 803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
804 804 pull_request_at_ver)
805 805 c.approval_msg = None
806 806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
807 807 c.approval_msg = _('Reviewer approval is pending.')
808 808 c.pr_merge_status = False
809 809
810 # inline comments
811 inline_comments = cc_model.get_inline_comments(
812 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
813
814 _inline_cnt, c.inline_versions = cc_model.get_inline_comments_count(
815 inline_comments, version=at_version, include_aggregates=True)
816
817 810 c.versions = pull_request_display_obj.versions()
811 c.at_version = at_version
818 812 c.at_version_num = at_version if at_version and at_version != 'latest' else None
819 813 c.at_version_pos = ChangesetComment.get_index_from_version(
820 814 c.at_version_num, c.versions)
821 815
822 is_outdated = lambda co: \
823 not c.at_version_num \
824 or co.pull_request_version_id <= c.at_version_num
816 # GENERAL COMMENTS with versions #
817 q = cc_model._all_general_comments_of_pull_request(pull_request_latest)
818 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
825 819
826 # inline_comments_until_version
827 if c.at_version_num:
828 # if we use version, then do not show later comments
829 # than current version
830 paths = collections.defaultdict(lambda: collections.defaultdict(list))
831 for fname, per_line_comments in inline_comments.iteritems():
832 for lno, comments in per_line_comments.iteritems():
833 for co in comments:
834 if co.pull_request_version_id and is_outdated(co):
835 paths[co.f_path][co.line_no].append(co)
836 inline_comments = paths
820 # pick comments we want to render at current version
821 c.comment_versions = cc_model.aggregate_comments(
822 general_comments, c.versions, c.at_version_num)
823 c.comments = c.comment_versions[c.at_version_num]['until']
824
825 # INLINE COMMENTS with versions #
826 q = cc_model._all_inline_comments_of_pull_request(pull_request_latest)
827 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
828 c.inline_versions = cc_model.aggregate_comments(
829 inline_comments, c.versions, c.at_version_num, inline=True)
837 830
838 # outdated comments
839 c.outdated_cnt = 0
840 if CommentsModel.use_outdated_comments(pull_request_latest):
841 outdated_comments = cc_model.get_outdated_comments(
842 c.rhodecode_db_repo.repo_id,
843 pull_request=pull_request_at_ver)
831 # if we use version, then do not show later comments
832 # than current version
833 paths = collections.defaultdict(lambda: collections.defaultdict(list))
834 for co in inline_comments:
835 if c.at_version_num:
836 # pick comments that are at least UPTO given version, so we
837 # don't render comments for higher version
838 should_render = co.pull_request_version_id and \
839 co.pull_request_version_id <= c.at_version_num
840 else:
841 # showing all, for 'latest'
842 should_render = True
844 843
845 # Count outdated comments and check for deleted files
846 is_outdated = lambda co: \
847 not c.at_version_num \
848 or co.pull_request_version_id < c.at_version_num
849 for file_name, lines in outdated_comments.iteritems():
850 for comments in lines.values():
851 comments = [comm for comm in comments if is_outdated(comm)]
852 c.outdated_cnt += len(comments)
844 if should_render:
845 paths[co.f_path][co.line_no].append(co)
846 inline_comments = paths
853 847
854 848 # load compare data into template context
855 849 self._load_compare_data(pull_request_at_ver, inline_comments)
856 850
857 851 # this is a hack to properly display links, when creating PR, the
858 852 # compare view and others uses different notation, and
859 853 # compare_commits.mako renders links based on the target_repo.
860 854 # We need to swap that here to generate it properly on the html side
861 855 c.target_repo = c.source_repo
862 856
863 # general comments
864 c.comments = cc_model.get_comments(
865 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
866
867 857 if c.allowed_to_update:
868 858 force_close = ('forced_closed', _('Close Pull Request'))
869 859 statuses = ChangesetStatus.STATUSES + [force_close]
870 860 else:
871 861 statuses = ChangesetStatus.STATUSES
872 862 c.commit_statuses = statuses
873 863
874 864 c.ancestor = None # TODO: add ancestor here
875 865 c.pull_request = pull_request_display_obj
876 866 c.pull_request_latest = pull_request_latest
877 c.at_version = at_version
878 867
879 868 c.changes = None
880 869 c.file_changes = None
881 870
882 871 c.show_version_changes = 1 # control flag, not used yet
883 872
884 873 if at_version and c.show_version_changes:
885 874 c.changes, c.file_changes = self._get_pr_version_changes(
886 875 version, pull_request_latest)
887 876
888 877 return render('/pullrequests/pullrequest_show.mako')
889 878
890 879 @LoginRequired()
891 880 @NotAnonymous()
892 881 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
893 882 'repository.admin')
894 883 @auth.CSRFRequired()
895 884 @jsonify
896 885 def comment(self, repo_name, pull_request_id):
897 886 pull_request_id = safe_int(pull_request_id)
898 887 pull_request = PullRequest.get_or_404(pull_request_id)
899 888 if pull_request.is_closed():
900 889 raise HTTPForbidden()
901 890
902 891 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
903 892 # as a changeset status, still we want to send it in one value.
904 893 status = request.POST.get('changeset_status', None)
905 894 text = request.POST.get('text')
906 895 comment_type = request.POST.get('comment_type')
907 896 resolves_comment_id = request.POST.get('resolves_comment_id', None)
908 897
909 898 if status and '_closed' in status:
910 899 close_pr = True
911 900 status = status.replace('_closed', '')
912 901 else:
913 902 close_pr = False
914 903
915 904 forced = (status == 'forced')
916 905 if forced:
917 906 status = 'rejected'
918 907
919 908 allowed_to_change_status = PullRequestModel().check_user_change_status(
920 909 pull_request, c.rhodecode_user)
921 910
922 911 if status and allowed_to_change_status:
923 912 message = (_('Status change %(transition_icon)s %(status)s')
924 913 % {'transition_icon': '>',
925 914 'status': ChangesetStatus.get_status_lbl(status)})
926 915 if close_pr:
927 916 message = _('Closing with') + ' ' + message
928 917 text = text or message
929 918 comm = CommentsModel().create(
930 919 text=text,
931 920 repo=c.rhodecode_db_repo.repo_id,
932 921 user=c.rhodecode_user.user_id,
933 922 pull_request=pull_request_id,
934 923 f_path=request.POST.get('f_path'),
935 924 line_no=request.POST.get('line'),
936 925 status_change=(ChangesetStatus.get_status_lbl(status)
937 926 if status and allowed_to_change_status else None),
938 927 status_change_type=(status
939 928 if status and allowed_to_change_status else None),
940 929 closing_pr=close_pr,
941 930 comment_type=comment_type,
942 931 resolves_comment_id=resolves_comment_id
943 932 )
944 933
945 934 if allowed_to_change_status:
946 935 old_calculated_status = pull_request.calculated_review_status()
947 936 # get status if set !
948 937 if status:
949 938 ChangesetStatusModel().set_status(
950 939 c.rhodecode_db_repo.repo_id,
951 940 status,
952 941 c.rhodecode_user.user_id,
953 942 comm,
954 943 pull_request=pull_request_id
955 944 )
956 945
957 946 Session().flush()
958 947 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
959 948 # we now calculate the status of pull request, and based on that
960 949 # calculation we set the commits status
961 950 calculated_status = pull_request.calculated_review_status()
962 951 if old_calculated_status != calculated_status:
963 952 PullRequestModel()._trigger_pull_request_hook(
964 953 pull_request, c.rhodecode_user, 'review_status_change')
965 954
966 955 calculated_status_lbl = ChangesetStatus.get_status_lbl(
967 956 calculated_status)
968 957
969 958 if close_pr:
970 959 status_completed = (
971 960 calculated_status in [ChangesetStatus.STATUS_APPROVED,
972 961 ChangesetStatus.STATUS_REJECTED])
973 962 if forced or status_completed:
974 963 PullRequestModel().close_pull_request(
975 964 pull_request_id, c.rhodecode_user)
976 965 else:
977 966 h.flash(_('Closing pull request on other statuses than '
978 967 'rejected or approved is forbidden. '
979 968 'Calculated status from all reviewers '
980 969 'is currently: %s') % calculated_status_lbl,
981 970 category='warning')
982 971
983 972 Session().commit()
984 973
985 974 if not request.is_xhr:
986 975 return redirect(h.url('pullrequest_show', repo_name=repo_name,
987 976 pull_request_id=pull_request_id))
988 977
989 978 data = {
990 979 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
991 980 }
992 981 if comm:
993 982 c.co = comm
994 983 c.inline_comment = True if comm.line_no else False
995 984 data.update(comm.get_dict())
996 985 data.update({'rendered_text':
997 986 render('changeset/changeset_comment_block.mako')})
998 987
999 988 return data
1000 989
1001 990 @LoginRequired()
1002 991 @NotAnonymous()
1003 992 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1004 993 'repository.admin')
1005 994 @auth.CSRFRequired()
1006 995 @jsonify
1007 996 def delete_comment(self, repo_name, comment_id):
1008 997 return self._delete_comment(comment_id)
1009 998
1010 999 def _delete_comment(self, comment_id):
1011 1000 comment_id = safe_int(comment_id)
1012 1001 co = ChangesetComment.get_or_404(comment_id)
1013 1002 if co.pull_request.is_closed():
1014 1003 # don't allow deleting comments on closed pull request
1015 1004 raise HTTPForbidden()
1016 1005
1017 1006 is_owner = co.author.user_id == c.rhodecode_user.user_id
1018 1007 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1019 1008 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1020 1009 old_calculated_status = co.pull_request.calculated_review_status()
1021 1010 CommentsModel().delete(comment=co)
1022 1011 Session().commit()
1023 1012 calculated_status = co.pull_request.calculated_review_status()
1024 1013 if old_calculated_status != calculated_status:
1025 1014 PullRequestModel()._trigger_pull_request_hook(
1026 1015 co.pull_request, c.rhodecode_user, 'review_status_change')
1027 1016 return True
1028 1017 else:
1029 1018 raise HTTPForbidden()
@@ -1,551 +1,600 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 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 ChangesetComment, User, Notification, PullRequest)
42 ChangesetComment, User, Notification, PullRequest, AttributeDict)
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 from rhodecode.model.validation_schema.schemas import comment_schema
48 48
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class CommentsModel(BaseModel):
54 54
55 55 cls = ChangesetComment
56 56
57 57 DIFF_CONTEXT_BEFORE = 3
58 58 DIFF_CONTEXT_AFTER = 3
59 59
60 60 def __get_commit_comment(self, changeset_comment):
61 61 return self._get_instance(ChangesetComment, changeset_comment)
62 62
63 63 def __get_pull_request(self, pull_request):
64 64 return self._get_instance(PullRequest, pull_request)
65 65
66 66 def _extract_mentions(self, s):
67 67 user_objects = []
68 68 for username in extract_mentioned_users(s):
69 69 user_obj = User.get_by_username(username, case_insensitive=True)
70 70 if user_obj:
71 71 user_objects.append(user_obj)
72 72 return user_objects
73 73
74 74 def _get_renderer(self, global_renderer='rst'):
75 75 try:
76 76 # try reading from visual context
77 77 from pylons import tmpl_context
78 78 global_renderer = tmpl_context.visual.default_renderer
79 79 except AttributeError:
80 80 log.debug("Renderer not set, falling back "
81 81 "to default renderer '%s'", global_renderer)
82 82 except Exception:
83 83 log.error(traceback.format_exc())
84 84 return global_renderer
85 85
86 def aggregate_comments(self, comments, versions, show_version, inline=False):
87 # group by versions, and count until, and display objects
88
89 comment_groups = collections.defaultdict(list)
90 [comment_groups[
91 _co.pull_request_version_id].append(_co) for _co in comments]
92
93 def yield_comments(pos):
94 for co in comment_groups[pos]:
95 yield co
96
97 comment_versions = collections.defaultdict(
98 lambda: collections.defaultdict(list))
99 prev_prvid = -1
100 # fake last entry with None, to aggregate on "latest" version which
101 # doesn't have an pull_request_version_id
102 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
103 prvid = ver.pull_request_version_id
104 if prev_prvid == -1:
105 prev_prvid = prvid
106
107 for co in yield_comments(prvid):
108 comment_versions[prvid]['at'].append(co)
109
110 # save until
111 current = comment_versions[prvid]['at']
112 prev_until = comment_versions[prev_prvid]['until']
113 cur_until = prev_until + current
114 comment_versions[prvid]['until'].extend(cur_until)
115
116 # save outdated
117 if inline:
118 outdated = [x for x in cur_until
119 if x.outdated_at_version(show_version)]
120 else:
121 outdated = [x for x in cur_until
122 if x.older_than_version(show_version)]
123 display = [x for x in cur_until if x not in outdated]
124
125 comment_versions[prvid]['outdated'] = outdated
126 comment_versions[prvid]['display'] = display
127
128 prev_prvid = prvid
129
130 return comment_versions
131
86 132 def create(self, text, repo, user, commit_id=None, pull_request=None,
87 133 f_path=None, line_no=None, status_change=None,
88 134 status_change_type=None, comment_type=None,
89 135 resolves_comment_id=None, closing_pr=False, send_email=True,
90 136 renderer=None):
91 137 """
92 138 Creates new comment for commit or pull request.
93 139 IF status_change is not none this comment is associated with a
94 140 status change of commit or commit associated with pull request
95 141
96 142 :param text:
97 143 :param repo:
98 144 :param user:
99 145 :param commit_id:
100 146 :param pull_request:
101 147 :param f_path:
102 148 :param line_no:
103 149 :param status_change: Label for status change
104 150 :param comment_type: Type of comment
105 151 :param status_change_type: type of status change
106 152 :param closing_pr:
107 153 :param send_email:
108 154 :param renderer: pick renderer for this comment
109 155 """
110 156 if not text:
111 157 log.warning('Missing text for comment, skipping...')
112 158 return
113 159
114 160 if not renderer:
115 161 renderer = self._get_renderer()
116 162
117 163 repo = self._get_repo(repo)
118 164 user = self._get_user(user)
119 165
120 166 schema = comment_schema.CommentSchema()
121 167 validated_kwargs = schema.deserialize(dict(
122 168 comment_body=text,
123 169 comment_type=comment_type,
124 170 comment_file=f_path,
125 171 comment_line=line_no,
126 172 renderer_type=renderer,
127 173 status_change=status_change_type,
128 174 resolves_comment_id=resolves_comment_id,
129 175 repo=repo.repo_id,
130 176 user=user.user_id,
131 177 ))
132 178
133 179 comment = ChangesetComment()
134 180 comment.renderer = validated_kwargs['renderer_type']
135 181 comment.text = validated_kwargs['comment_body']
136 182 comment.f_path = validated_kwargs['comment_file']
137 183 comment.line_no = validated_kwargs['comment_line']
138 184 comment.comment_type = validated_kwargs['comment_type']
139 185
140 186 comment.repo = repo
141 187 comment.author = user
142 188 comment.resolved_comment = self.__get_commit_comment(
143 189 validated_kwargs['resolves_comment_id'])
144 190
145 191 pull_request_id = pull_request
146 192
147 193 commit_obj = None
148 194 pull_request_obj = None
149 195
150 196 if commit_id:
151 197 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
152 198 # do a lookup, so we don't pass something bad here
153 199 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
154 200 comment.revision = commit_obj.raw_id
155 201
156 202 elif pull_request_id:
157 203 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
158 204 pull_request_obj = self.__get_pull_request(pull_request_id)
159 205 comment.pull_request = pull_request_obj
160 206 else:
161 207 raise Exception('Please specify commit or pull_request_id')
162 208
163 209 Session().add(comment)
164 210 Session().flush()
165 211 kwargs = {
166 212 'user': user,
167 213 'renderer_type': renderer,
168 214 'repo_name': repo.repo_name,
169 215 'status_change': status_change,
170 216 'status_change_type': status_change_type,
171 217 'comment_body': text,
172 218 'comment_file': f_path,
173 219 'comment_line': line_no,
174 220 }
175 221
176 222 if commit_obj:
177 223 recipients = ChangesetComment.get_users(
178 224 revision=commit_obj.raw_id)
179 225 # add commit author if it's in RhodeCode system
180 226 cs_author = User.get_from_cs_author(commit_obj.author)
181 227 if not cs_author:
182 228 # use repo owner if we cannot extract the author correctly
183 229 cs_author = repo.user
184 230 recipients += [cs_author]
185 231
186 232 commit_comment_url = self.get_url(comment)
187 233
188 234 target_repo_url = h.link_to(
189 235 repo.repo_name,
190 236 h.url('summary_home',
191 237 repo_name=repo.repo_name, qualified=True))
192 238
193 239 # commit specifics
194 240 kwargs.update({
195 241 'commit': commit_obj,
196 242 'commit_message': commit_obj.message,
197 243 'commit_target_repo': target_repo_url,
198 244 'commit_comment_url': commit_comment_url,
199 245 })
200 246
201 247 elif pull_request_obj:
202 248 # get the current participants of this pull request
203 249 recipients = ChangesetComment.get_users(
204 250 pull_request_id=pull_request_obj.pull_request_id)
205 251 # add pull request author
206 252 recipients += [pull_request_obj.author]
207 253
208 254 # add the reviewers to notification
209 255 recipients += [x.user for x in pull_request_obj.reviewers]
210 256
211 257 pr_target_repo = pull_request_obj.target_repo
212 258 pr_source_repo = pull_request_obj.source_repo
213 259
214 260 pr_comment_url = h.url(
215 261 'pullrequest_show',
216 262 repo_name=pr_target_repo.repo_name,
217 263 pull_request_id=pull_request_obj.pull_request_id,
218 264 anchor='comment-%s' % comment.comment_id,
219 265 qualified=True,)
220 266
221 267 # set some variables for email notification
222 268 pr_target_repo_url = h.url(
223 269 'summary_home', repo_name=pr_target_repo.repo_name,
224 270 qualified=True)
225 271
226 272 pr_source_repo_url = h.url(
227 273 'summary_home', repo_name=pr_source_repo.repo_name,
228 274 qualified=True)
229 275
230 276 # pull request specifics
231 277 kwargs.update({
232 278 'pull_request': pull_request_obj,
233 279 'pr_id': pull_request_obj.pull_request_id,
234 280 'pr_target_repo': pr_target_repo,
235 281 'pr_target_repo_url': pr_target_repo_url,
236 282 'pr_source_repo': pr_source_repo,
237 283 'pr_source_repo_url': pr_source_repo_url,
238 284 'pr_comment_url': pr_comment_url,
239 285 'pr_closing': closing_pr,
240 286 })
241 287 if send_email:
242 288 # pre-generate the subject for notification itself
243 289 (subject,
244 290 _h, _e, # we don't care about those
245 291 body_plaintext) = EmailNotificationModel().render_email(
246 292 notification_type, **kwargs)
247 293
248 294 mention_recipients = set(
249 295 self._extract_mentions(text)).difference(recipients)
250 296
251 297 # create notification objects, and emails
252 298 NotificationModel().create(
253 299 created_by=user,
254 300 notification_subject=subject,
255 301 notification_body=body_plaintext,
256 302 notification_type=notification_type,
257 303 recipients=recipients,
258 304 mention_recipients=mention_recipients,
259 305 email_kwargs=kwargs,
260 306 )
261 307
262 308 action = (
263 309 'user_commented_pull_request:{}'.format(
264 310 comment.pull_request.pull_request_id)
265 311 if comment.pull_request
266 312 else 'user_commented_revision:{}'.format(comment.revision)
267 313 )
268 314 action_logger(user, action, comment.repo)
269 315
270 316 registry = get_current_registry()
271 317 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
272 318 channelstream_config = rhodecode_plugins.get('channelstream', {})
273 319 msg_url = ''
274 320 if commit_obj:
275 321 msg_url = commit_comment_url
276 322 repo_name = repo.repo_name
277 323 elif pull_request_obj:
278 324 msg_url = pr_comment_url
279 325 repo_name = pr_target_repo.repo_name
280 326
281 327 if channelstream_config.get('enabled'):
282 328 message = '<strong>{}</strong> {} - ' \
283 329 '<a onclick="window.location=\'{}\';' \
284 330 'window.location.reload()">' \
285 331 '<strong>{}</strong></a>'
286 332 message = message.format(
287 333 user.username, _('made a comment'), msg_url,
288 334 _('Show it now'))
289 335 channel = '/repo${}$/pr/{}'.format(
290 336 repo_name,
291 337 pull_request_id
292 338 )
293 339 payload = {
294 340 'type': 'message',
295 341 'timestamp': datetime.utcnow(),
296 342 'user': 'system',
297 343 'exclude_users': [user.username],
298 344 'channel': channel,
299 345 'message': {
300 346 'message': message,
301 347 'level': 'info',
302 348 'topic': '/notifications'
303 349 }
304 350 }
305 351 channelstream_request(channelstream_config, [payload],
306 352 '/message', raise_exc=False)
307 353
308 354 return comment
309 355
310 356 def delete(self, comment):
311 357 """
312 358 Deletes given comment
313 359
314 360 :param comment_id:
315 361 """
316 362 comment = self.__get_commit_comment(comment)
317 363 Session().delete(comment)
318 364
319 365 return comment
320 366
321 367 def get_all_comments(self, repo_id, revision=None, pull_request=None):
322 368 q = ChangesetComment.query()\
323 369 .filter(ChangesetComment.repo_id == repo_id)
324 370 if revision:
325 371 q = q.filter(ChangesetComment.revision == revision)
326 372 elif pull_request:
327 373 pull_request = self.__get_pull_request(pull_request)
328 374 q = q.filter(ChangesetComment.pull_request == pull_request)
329 375 else:
330 376 raise Exception('Please specify commit or pull_request')
331 377 q = q.order_by(ChangesetComment.created_on)
332 378 return q.all()
333 379
334 380 def get_url(self, comment):
335 381 comment = self.__get_commit_comment(comment)
336 382 if comment.pull_request:
337 383 return h.url(
338 384 'pullrequest_show',
339 385 repo_name=comment.pull_request.target_repo.repo_name,
340 386 pull_request_id=comment.pull_request.pull_request_id,
341 387 anchor='comment-%s' % comment.comment_id,
342 388 qualified=True,)
343 389 else:
344 390 return h.url(
345 391 'changeset_home',
346 392 repo_name=comment.repo.repo_name,
347 393 revision=comment.revision,
348 394 anchor='comment-%s' % comment.comment_id,
349 395 qualified=True,)
350 396
351 397 def get_comments(self, repo_id, revision=None, pull_request=None):
352 398 """
353 399 Gets main comments based on revision or pull_request_id
354 400
355 401 :param repo_id:
356 402 :param revision:
357 403 :param pull_request:
358 404 """
359 405
360 406 q = ChangesetComment.query()\
361 407 .filter(ChangesetComment.repo_id == repo_id)\
362 408 .filter(ChangesetComment.line_no == None)\
363 409 .filter(ChangesetComment.f_path == None)
364 410 if revision:
365 411 q = q.filter(ChangesetComment.revision == revision)
366 412 elif pull_request:
367 413 pull_request = self.__get_pull_request(pull_request)
368 414 q = q.filter(ChangesetComment.pull_request == pull_request)
369 415 else:
370 416 raise Exception('Please specify commit or pull_request')
371 417 q = q.order_by(ChangesetComment.created_on)
372 418 return q.all()
373 419
374 420 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
375 421 q = self._get_inline_comments_query(repo_id, revision, pull_request)
376 422 return self._group_comments_by_path_and_line_number(q)
377 423
378 424 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
379 version=None, include_aggregates=False):
380 version_aggregates = collections.defaultdict(list)
425 version=None):
381 426 inline_cnt = 0
382 427 for fname, per_line_comments in inline_comments.iteritems():
383 428 for lno, comments in per_line_comments.iteritems():
384 429 for comm in comments:
385 version_aggregates[comm.pull_request_version_id].append(comm)
386 430 if not comm.outdated_at_version(version) and skip_outdated:
387 431 inline_cnt += 1
388 432
389 if include_aggregates:
390 return inline_cnt, version_aggregates
391 433 return inline_cnt
392 434
393 435 def get_outdated_comments(self, repo_id, pull_request):
394 436 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
395 437 # of a pull request.
396 438 q = self._all_inline_comments_of_pull_request(pull_request)
397 439 q = q.filter(
398 440 ChangesetComment.display_state ==
399 441 ChangesetComment.COMMENT_OUTDATED
400 442 ).order_by(ChangesetComment.comment_id.asc())
401 443
402 444 return self._group_comments_by_path_and_line_number(q)
403 445
404 446 def _get_inline_comments_query(self, repo_id, revision, pull_request):
405 447 # TODO: johbo: Split this into two methods: One for PR and one for
406 448 # commit.
407 449 if revision:
408 450 q = Session().query(ChangesetComment).filter(
409 451 ChangesetComment.repo_id == repo_id,
410 452 ChangesetComment.line_no != null(),
411 453 ChangesetComment.f_path != null(),
412 454 ChangesetComment.revision == revision)
413 455
414 456 elif pull_request:
415 457 pull_request = self.__get_pull_request(pull_request)
416 458 if not CommentsModel.use_outdated_comments(pull_request):
417 459 q = self._visible_inline_comments_of_pull_request(pull_request)
418 460 else:
419 461 q = self._all_inline_comments_of_pull_request(pull_request)
420 462
421 463 else:
422 464 raise Exception('Please specify commit or pull_request_id')
423 465 q = q.order_by(ChangesetComment.comment_id.asc())
424 466 return q
425 467
426 468 def _group_comments_by_path_and_line_number(self, q):
427 469 comments = q.all()
428 470 paths = collections.defaultdict(lambda: collections.defaultdict(list))
429 471 for co in comments:
430 472 paths[co.f_path][co.line_no].append(co)
431 473 return paths
432 474
433 475 @classmethod
434 476 def needed_extra_diff_context(cls):
435 477 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
436 478
437 479 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
438 480 if not CommentsModel.use_outdated_comments(pull_request):
439 481 return
440 482
441 483 comments = self._visible_inline_comments_of_pull_request(pull_request)
442 484 comments_to_outdate = comments.all()
443 485
444 486 for comment in comments_to_outdate:
445 487 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
446 488
447 489 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
448 490 diff_line = _parse_comment_line_number(comment.line_no)
449 491
450 492 try:
451 493 old_context = old_diff_proc.get_context_of_line(
452 494 path=comment.f_path, diff_line=diff_line)
453 495 new_context = new_diff_proc.get_context_of_line(
454 496 path=comment.f_path, diff_line=diff_line)
455 497 except (diffs.LineNotInDiffException,
456 498 diffs.FileNotInDiffException):
457 499 comment.display_state = ChangesetComment.COMMENT_OUTDATED
458 500 return
459 501
460 502 if old_context == new_context:
461 503 return
462 504
463 505 if self._should_relocate_diff_line(diff_line):
464 506 new_diff_lines = new_diff_proc.find_context(
465 507 path=comment.f_path, context=old_context,
466 508 offset=self.DIFF_CONTEXT_BEFORE)
467 509 if not new_diff_lines:
468 510 comment.display_state = ChangesetComment.COMMENT_OUTDATED
469 511 else:
470 512 new_diff_line = self._choose_closest_diff_line(
471 513 diff_line, new_diff_lines)
472 514 comment.line_no = _diff_to_comment_line_number(new_diff_line)
473 515 else:
474 516 comment.display_state = ChangesetComment.COMMENT_OUTDATED
475 517
476 518 def _should_relocate_diff_line(self, diff_line):
477 519 """
478 520 Checks if relocation shall be tried for the given `diff_line`.
479 521
480 522 If a comment points into the first lines, then we can have a situation
481 523 that after an update another line has been added on top. In this case
482 524 we would find the context still and move the comment around. This
483 525 would be wrong.
484 526 """
485 527 should_relocate = (
486 528 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
487 529 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
488 530 return should_relocate
489 531
490 532 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
491 533 candidate = new_diff_lines[0]
492 534 best_delta = _diff_line_delta(diff_line, candidate)
493 535 for new_diff_line in new_diff_lines[1:]:
494 536 delta = _diff_line_delta(diff_line, new_diff_line)
495 537 if delta < best_delta:
496 538 candidate = new_diff_line
497 539 best_delta = delta
498 540 return candidate
499 541
500 542 def _visible_inline_comments_of_pull_request(self, pull_request):
501 543 comments = self._all_inline_comments_of_pull_request(pull_request)
502 544 comments = comments.filter(
503 545 coalesce(ChangesetComment.display_state, '') !=
504 546 ChangesetComment.COMMENT_OUTDATED)
505 547 return comments
506 548
507 549 def _all_inline_comments_of_pull_request(self, pull_request):
508 550 comments = Session().query(ChangesetComment)\
509 551 .filter(ChangesetComment.line_no != None)\
510 552 .filter(ChangesetComment.f_path != None)\
511 553 .filter(ChangesetComment.pull_request == pull_request)
512 554 return comments
513 555
556 def _all_general_comments_of_pull_request(self, pull_request):
557 comments = Session().query(ChangesetComment)\
558 .filter(ChangesetComment.line_no == None)\
559 .filter(ChangesetComment.f_path == None)\
560 .filter(ChangesetComment.pull_request == pull_request)
561 return comments
562
514 563 @staticmethod
515 564 def use_outdated_comments(pull_request):
516 565 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
517 566 settings = settings_model.get_general_settings()
518 567 return settings.get('rhodecode_use_outdated_comments', False)
519 568
520 569
521 570 def _parse_comment_line_number(line_no):
522 571 """
523 572 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
524 573 """
525 574 old_line = None
526 575 new_line = None
527 576 if line_no.startswith('o'):
528 577 old_line = int(line_no[1:])
529 578 elif line_no.startswith('n'):
530 579 new_line = int(line_no[1:])
531 580 else:
532 581 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
533 582 return diffs.DiffLineNumber(old_line, new_line)
534 583
535 584
536 585 def _diff_to_comment_line_number(diff_line):
537 586 if diff_line.new is not None:
538 587 return u'n{}'.format(diff_line.new)
539 588 elif diff_line.old is not None:
540 589 return u'o{}'.format(diff_line.old)
541 590 return u''
542 591
543 592
544 593 def _diff_line_delta(a, b):
545 594 if None not in (a.new, b.new):
546 595 return abs(a.new - b.new)
547 596 elif None not in (a.old, b.old):
548 597 return abs(a.old - b.old)
549 598 else:
550 599 raise ValueError(
551 600 "Cannot compute delta between {} and {}".format(a, b))
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,547 +1,547 b''
1 1 // comments.less
2 2 // For use in RhodeCode applications;
3 3 // see style guide documentation for guidelines.
4 4
5 5
6 6 // Comments
7 7 .comments {
8 8 width: 100%;
9 9 }
10 10
11 11 tr.inline-comments div {
12 12 max-width: 100%;
13 13
14 14 p {
15 15 white-space: normal;
16 16 }
17 17
18 18 code, pre, .code, dd {
19 19 overflow-x: auto;
20 20 width: 1062px;
21 21 }
22 22
23 23 dd {
24 24 width: auto;
25 25 }
26 26 }
27 27
28 28 #injected_page_comments {
29 29 .comment-previous-link,
30 30 .comment-next-link,
31 31 .comment-links-divider {
32 32 display: none;
33 33 }
34 34 }
35 35
36 36 .add-comment {
37 37 margin-bottom: 10px;
38 38 }
39 39 .hide-comment-button .add-comment {
40 40 display: none;
41 41 }
42 42
43 43 .comment-bubble {
44 44 color: @grey4;
45 45 margin-top: 4px;
46 46 margin-right: 30px;
47 47 visibility: hidden;
48 48 }
49 49
50 50 .comment-label {
51 51 float: left;
52 52
53 53 padding: 0.4em 0.4em;
54 margin: 2px 5px 0px -10px;
54 margin: 3px 5px 0px -10px;
55 55 display: inline-block;
56 56 min-height: 0;
57 57
58 58 text-align: center;
59 59 font-size: 10px;
60 60 line-height: .8em;
61 61
62 62 font-family: @text-italic;
63 63 background: #fff none;
64 64 color: @grey4;
65 65 border: 1px solid @grey4;
66 66 white-space: nowrap;
67 67
68 68 text-transform: uppercase;
69 69 min-width: 40px;
70 70
71 71 &.todo {
72 72 color: @color5;
73 73 font-family: @text-bold-italic;
74 74 }
75 75
76 76 .resolve {
77 77 cursor: pointer;
78 78 text-decoration: underline;
79 79 }
80 80
81 81 .resolved {
82 82 text-decoration: line-through;
83 83 color: @color1;
84 84 }
85 85 .resolved a {
86 86 text-decoration: line-through;
87 87 color: @color1;
88 88 }
89 89 .resolve-text {
90 90 color: @color1;
91 91 margin: 2px 8px;
92 92 font-family: @text-italic;
93 93 }
94 94
95 95 }
96 96
97 97
98 98 .comment {
99 99
100 100 &.comment-general {
101 101 border: 1px solid @grey5;
102 102 padding: 5px 5px 5px 5px;
103 103 }
104 104
105 105 margin: @padding 0;
106 106 padding: 4px 0 0 0;
107 107 line-height: 1em;
108 108
109 109 .rc-user {
110 110 min-width: 0;
111 111 margin: 0px .5em 0 0;
112 112
113 113 .user {
114 114 display: inline;
115 115 }
116 116 }
117 117
118 118 .meta {
119 119 position: relative;
120 120 width: 100%;
121 121 border-bottom: 1px solid @grey5;
122 122 margin: -5px 0px;
123 123 line-height: 24px;
124 124
125 125 &:hover .permalink {
126 126 visibility: visible;
127 127 color: @rcblue;
128 128 }
129 129 }
130 130
131 131 .author,
132 132 .date {
133 133 display: inline;
134 134
135 135 &:after {
136 136 content: ' | ';
137 137 color: @grey5;
138 138 }
139 139 }
140 140
141 141 .author-general img {
142 142 top: 3px;
143 143 }
144 144 .author-inline img {
145 145 top: 3px;
146 146 }
147 147
148 148 .status-change,
149 149 .permalink,
150 150 .changeset-status-lbl {
151 151 display: inline;
152 152 }
153 153
154 154 .permalink {
155 155 visibility: hidden;
156 156 }
157 157
158 158 .comment-links-divider {
159 159 display: inline;
160 160 }
161 161
162 162 .comment-links-block {
163 163 float:right;
164 164 text-align: right;
165 165 min-width: 85px;
166 166
167 167 [class^="icon-"]:before,
168 168 [class*=" icon-"]:before {
169 169 margin-left: 0;
170 170 margin-right: 0;
171 171 }
172 172 }
173 173
174 174 .comment-previous-link {
175 175 display: inline-block;
176 176
177 177 .arrow_comment_link{
178 178 cursor: pointer;
179 179 i {
180 180 font-size:10px;
181 181 }
182 182 }
183 183 .arrow_comment_link.disabled {
184 184 cursor: default;
185 185 color: @grey5;
186 186 }
187 187 }
188 188
189 189 .comment-next-link {
190 190 display: inline-block;
191 191
192 192 .arrow_comment_link{
193 193 cursor: pointer;
194 194 i {
195 195 font-size:10px;
196 196 }
197 197 }
198 198 .arrow_comment_link.disabled {
199 199 cursor: default;
200 200 color: @grey5;
201 201 }
202 202 }
203 203
204 204 .flag_status {
205 205 display: inline-block;
206 206 margin: -2px .5em 0 .25em
207 207 }
208 208
209 209 .delete-comment {
210 210 display: inline-block;
211 211 color: @rcblue;
212 212
213 213 &:hover {
214 214 cursor: pointer;
215 215 }
216 216 }
217 217
218 218 .text {
219 219 clear: both;
220 220 .border-radius(@border-radius);
221 221 .box-sizing(border-box);
222 222
223 223 .markdown-block p,
224 224 .rst-block p {
225 225 margin: .5em 0 !important;
226 226 // TODO: lisa: This is needed because of other rst !important rules :[
227 227 }
228 228 }
229 229
230 230 .pr-version {
231 231 float: left;
232 232 margin: 0px 4px;
233 233 }
234 234 .pr-version-inline {
235 235 float: left;
236 236 margin: 0px 4px;
237 237 }
238 238 .pr-version-num {
239 239 font-size: 10px;
240 240 }
241 241
242 242 }
243 243
244 244 @comment-padding: 5px;
245 245
246 246 .inline-comments {
247 247 border-radius: @border-radius;
248 248 .comment {
249 249 margin: 0;
250 250 border-radius: @border-radius;
251 251 }
252 252 .comment-outdated {
253 253 opacity: 0.5;
254 254 }
255 255
256 256 .comment-inline {
257 257 background: white;
258 258 padding: @comment-padding @comment-padding;
259 259 border: @comment-padding solid @grey6;
260 260
261 261 .text {
262 262 border: none;
263 263 }
264 264 .meta {
265 265 border-bottom: 1px solid @grey6;
266 266 margin: -5px 0px;
267 267 line-height: 24px;
268 268 }
269 269 }
270 270 .comment-selected {
271 271 border-left: 6px solid @comment-highlight-color;
272 272 }
273 273 .comment-inline-form {
274 274 padding: @comment-padding;
275 275 display: none;
276 276 }
277 277 .cb-comment-add-button {
278 278 margin: @comment-padding;
279 279 }
280 280 /* hide add comment button when form is open */
281 281 .comment-inline-form-open ~ .cb-comment-add-button {
282 282 display: none;
283 283 }
284 284 .comment-inline-form-open {
285 285 display: block;
286 286 }
287 287 /* hide add comment button when form but no comments */
288 288 .comment-inline-form:first-child + .cb-comment-add-button {
289 289 display: none;
290 290 }
291 291 /* hide add comment button when no comments or form */
292 292 .cb-comment-add-button:first-child {
293 293 display: none;
294 294 }
295 295 /* hide add comment button when only comment is being deleted */
296 296 .comment-deleting:first-child + .cb-comment-add-button {
297 297 display: none;
298 298 }
299 299 }
300 300
301 301
302 302 .show-outdated-comments {
303 303 display: inline;
304 304 color: @rcblue;
305 305 }
306 306
307 307 // Comment Form
308 308 div.comment-form {
309 309 margin-top: 20px;
310 310 }
311 311
312 312 .comment-form strong {
313 313 display: block;
314 314 margin-bottom: 15px;
315 315 }
316 316
317 317 .comment-form textarea {
318 318 width: 100%;
319 319 height: 100px;
320 320 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
321 321 }
322 322
323 323 form.comment-form {
324 324 margin-top: 10px;
325 325 margin-left: 10px;
326 326 }
327 327
328 328 .comment-inline-form .comment-block-ta,
329 329 .comment-form .comment-block-ta,
330 330 .comment-form .preview-box {
331 331 .border-radius(@border-radius);
332 332 .box-sizing(border-box);
333 333 background-color: white;
334 334 }
335 335
336 336 .comment-form-submit {
337 337 margin-top: 5px;
338 338 margin-left: 525px;
339 339 }
340 340
341 341 .file-comments {
342 342 display: none;
343 343 }
344 344
345 345 .comment-form .preview-box.unloaded,
346 346 .comment-inline-form .preview-box.unloaded {
347 347 height: 50px;
348 348 text-align: center;
349 349 padding: 20px;
350 350 background-color: white;
351 351 }
352 352
353 353 .comment-footer {
354 354 position: relative;
355 355 width: 100%;
356 356 min-height: 42px;
357 357
358 358 .status_box,
359 359 .cancel-button {
360 360 float: left;
361 361 display: inline-block;
362 362 }
363 363
364 364 .action-buttons {
365 365 float: right;
366 366 display: inline-block;
367 367 }
368 368 }
369 369
370 370 .comment-form {
371 371
372 372 .comment {
373 373 margin-left: 10px;
374 374 }
375 375
376 376 .comment-help {
377 377 color: @grey4;
378 378 padding: 5px 0 5px 0;
379 379 }
380 380
381 381 .comment-title {
382 382 padding: 5px 0 5px 0;
383 383 }
384 384
385 385 .comment-button {
386 386 display: inline-block;
387 387 }
388 388
389 389 .comment-button-input {
390 390 margin-right: 0;
391 391 }
392 392
393 393 .comment-footer {
394 394 margin-bottom: 110px;
395 395 margin-top: 10px;
396 396 }
397 397 }
398 398
399 399
400 400 .comment-form-login {
401 401 .comment-help {
402 402 padding: 0.9em; //same as the button
403 403 }
404 404
405 405 div.clearfix {
406 406 clear: both;
407 407 width: 100%;
408 408 display: block;
409 409 }
410 410 }
411 411
412 412 .comment-type {
413 413 margin: 0px;
414 414 border-radius: inherit;
415 415 border-color: @grey6;
416 416 }
417 417
418 418 .preview-box {
419 419 min-height: 105px;
420 420 margin-bottom: 15px;
421 421 background-color: white;
422 422 .border-radius(@border-radius);
423 423 .box-sizing(border-box);
424 424 }
425 425
426 426 .add-another-button {
427 427 margin-left: 10px;
428 428 margin-top: 10px;
429 429 margin-bottom: 10px;
430 430 }
431 431
432 432 .comment .buttons {
433 433 float: right;
434 434 margin: -1px 0px 0px 0px;
435 435 }
436 436
437 437 // Inline Comment Form
438 438 .injected_diff .comment-inline-form,
439 439 .comment-inline-form {
440 440 background-color: white;
441 441 margin-top: 10px;
442 442 margin-bottom: 20px;
443 443 }
444 444
445 445 .inline-form {
446 446 padding: 10px 7px;
447 447 }
448 448
449 449 .inline-form div {
450 450 max-width: 100%;
451 451 }
452 452
453 453 .overlay {
454 454 display: none;
455 455 position: absolute;
456 456 width: 100%;
457 457 text-align: center;
458 458 vertical-align: middle;
459 459 font-size: 16px;
460 460 background: none repeat scroll 0 0 white;
461 461
462 462 &.submitting {
463 463 display: block;
464 464 opacity: 0.5;
465 465 z-index: 100;
466 466 }
467 467 }
468 468 .comment-inline-form .overlay.submitting .overlay-text {
469 469 margin-top: 5%;
470 470 }
471 471
472 472 .comment-inline-form .clearfix,
473 473 .comment-form .clearfix {
474 474 .border-radius(@border-radius);
475 475 margin: 0px;
476 476 }
477 477
478 478 .comment-inline-form .comment-footer {
479 479 margin: 10px 0px 0px 0px;
480 480 }
481 481
482 482 .hide-inline-form-button {
483 483 margin-left: 5px;
484 484 }
485 485 .comment-button .hide-inline-form {
486 486 background: white;
487 487 }
488 488
489 489 .comment-area {
490 490 padding: 8px 12px;
491 491 border: 1px solid @grey5;
492 492 .border-radius(@border-radius);
493 493 }
494 494
495 495 .comment-area-header .nav-links {
496 496 display: flex;
497 497 flex-flow: row wrap;
498 498 -webkit-flex-flow: row wrap;
499 499 width: 100%;
500 500 }
501 501
502 502 .comment-area-footer {
503 503 display: flex;
504 504 }
505 505
506 506 .comment-footer .toolbar {
507 507
508 508 }
509 509
510 510 .nav-links {
511 511 padding: 0;
512 512 margin: 0;
513 513 list-style: none;
514 514 height: auto;
515 515 border-bottom: 1px solid @grey5;
516 516 }
517 517 .nav-links li {
518 518 display: inline-block;
519 519 }
520 520 .nav-links li:before {
521 521 content: "";
522 522 }
523 523 .nav-links li a.disabled {
524 524 cursor: not-allowed;
525 525 }
526 526
527 527 .nav-links li.active a {
528 528 border-bottom: 2px solid @rcblue;
529 529 color: #000;
530 530 font-weight: 600;
531 531 }
532 532 .nav-links li a {
533 533 display: inline-block;
534 534 padding: 0px 10px 5px 10px;
535 535 margin-bottom: -1px;
536 536 font-size: 14px;
537 537 line-height: 28px;
538 538 color: #8f8f8f;
539 539 border-bottom: 2px solid transparent;
540 540 }
541 541
542 542 .toolbar-text {
543 543 float: left;
544 544 margin: -5px 0px 0px 0px;
545 545 font-size: 12px;
546 546 }
547 547
@@ -1,314 +1,314 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.mako"/>
4 4 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
5 5
6 6 <%def name="title()">
7 7 ${_('%s Commit') % c.repo_name} - ${h.show_id(c.commit)}
8 8 %if c.rhodecode_name:
9 9 &middot; ${h.branding(c.rhodecode_name)}
10 10 %endif
11 11 </%def>
12 12
13 13 <%def name="menu_bar_nav()">
14 14 ${self.menu_items(active='repositories')}
15 15 </%def>
16 16
17 17 <%def name="menu_bar_subnav()">
18 18 ${self.repo_menu(active='changelog')}
19 19 </%def>
20 20
21 21 <%def name="main()">
22 22 <script>
23 23 // TODO: marcink switch this to pyroutes
24 24 AJAX_COMMENT_DELETE_URL = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
25 25 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
26 26 </script>
27 27 <div class="box">
28 28 <div class="title">
29 29 ${self.repo_page_title(c.rhodecode_db_repo)}
30 30 </div>
31 31
32 32 <div id="changeset_compare_view_content" class="summary changeset">
33 33 <div class="summary-detail">
34 34 <div class="summary-detail-header">
35 35 <span class="breadcrumbs files_location">
36 36 <h4>${_('Commit')}
37 37 <code>
38 38 ${h.show_id(c.commit)}
39 39 </code>
40 40 </h4>
41 41 </span>
42 42 <span id="parent_link">
43 43 <a href="#" title="${_('Parent Commit')}">${_('Parent')}</a>
44 44 </span>
45 45 |
46 46 <span id="child_link">
47 47 <a href="#" title="${_('Child Commit')}">${_('Child')}</a>
48 48 </span>
49 49 </div>
50 50
51 51 <div class="fieldset">
52 52 <div class="left-label">
53 53 ${_('Description')}:
54 54 </div>
55 55 <div class="right-content">
56 56 <div id="trimmed_message_box" class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
57 57 <div id="message_expand" style="display:none;">
58 58 ${_('Expand')}
59 59 </div>
60 60 </div>
61 61 </div>
62 62
63 63 %if c.statuses:
64 64 <div class="fieldset">
65 65 <div class="left-label">
66 66 ${_('Commit status')}:
67 67 </div>
68 68 <div class="right-content">
69 69 <div class="changeset-status-ico">
70 70 <div class="${'flag_status %s' % c.statuses[0]} pull-left"></div>
71 71 </div>
72 72 <div title="${_('Commit status')}" class="changeset-status-lbl">[${h.commit_status_lbl(c.statuses[0])}]</div>
73 73 </div>
74 74 </div>
75 75 %endif
76 76
77 77 <div class="fieldset">
78 78 <div class="left-label">
79 79 ${_('References')}:
80 80 </div>
81 81 <div class="right-content">
82 82 <div class="tags">
83 83
84 84 %if c.commit.merge:
85 85 <span class="mergetag tag">
86 86 <i class="icon-merge"></i>${_('merge')}
87 87 </span>
88 88 %endif
89 89
90 90 %if h.is_hg(c.rhodecode_repo):
91 91 %for book in c.commit.bookmarks:
92 92 <span class="booktag tag" title="${_('Bookmark %s') % book}">
93 93 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
94 94 </span>
95 95 %endfor
96 96 %endif
97 97
98 98 %for tag in c.commit.tags:
99 99 <span class="tagtag tag" title="${_('Tag %s') % tag}">
100 100 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-tag"></i>${tag}</a>
101 101 </span>
102 102 %endfor
103 103
104 104 %if c.commit.branch:
105 105 <span class="branchtag tag" title="${_('Branch %s') % c.commit.branch}">
106 106 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-code-fork"></i>${h.shorter(c.commit.branch)}</a>
107 107 </span>
108 108 %endif
109 109 </div>
110 110 </div>
111 111 </div>
112 112
113 113 <div class="fieldset">
114 114 <div class="left-label">
115 115 ${_('Diff options')}:
116 116 </div>
117 117 <div class="right-content">
118 118 <div class="diff-actions">
119 119 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
120 120 ${_('Raw Diff')}
121 121 </a>
122 122 |
123 123 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
124 124 ${_('Patch Diff')}
125 125 </a>
126 126 |
127 127 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision=c.commit.raw_id,diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
128 128 ${_('Download Diff')}
129 129 </a>
130 130 |
131 131 ${c.ignorews_url(request.GET)}
132 132 |
133 133 ${c.context_url(request.GET)}
134 134 </div>
135 135 </div>
136 136 </div>
137 137
138 138 <div class="fieldset">
139 139 <div class="left-label">
140 140 ${_('Comments')}:
141 141 </div>
142 142 <div class="right-content">
143 143 <div class="comments-number">
144 144 %if c.comments:
145 145 <a href="#comments">${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}</a>,
146 146 %else:
147 147 ${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}
148 148 %endif
149 149 %if c.inline_cnt:
150 150 <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
151 151 %else:
152 152 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
153 153 %endif
154 154 </div>
155 155 </div>
156 156 </div>
157 157
158 158 </div> <!-- end summary-detail -->
159 159
160 160 <div id="commit-stats" class="sidebar-right">
161 161 <div class="summary-detail-header">
162 162 <h4 class="item">
163 163 ${_('Author')}
164 164 </h4>
165 165 </div>
166 166 <div class="sidebar-right-content">
167 167 ${self.gravatar_with_user(c.commit.author)}
168 168 <div class="user-inline-data">- ${h.age_component(c.commit.date)}</div>
169 169 </div>
170 170 </div><!-- end sidebar -->
171 171 </div> <!-- end summary -->
172 172 <div class="cs_files">
173 173 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
174 174 ${cbdiffs.render_diffset_menu()}
175 175 ${cbdiffs.render_diffset(
176 176 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True)}
177 177 </div>
178 178
179 179 ## template for inline comment form
180 180 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
181 181
182 182 ## render comments
183 ${comment.generate_comments()}
183 ${comment.generate_comments(c.comments)}
184 184
185 185 ## main comment form and it status
186 186 ${comment.comments(h.url('changeset_comment', repo_name=c.repo_name, revision=c.commit.raw_id),
187 187 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
188 188 </div>
189 189
190 190 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
191 191 <script type="text/javascript">
192 192
193 193 $(document).ready(function() {
194 194
195 195 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
196 196 if($('#trimmed_message_box').height() === boxmax){
197 197 $('#message_expand').show();
198 198 }
199 199
200 200 $('#message_expand').on('click', function(e){
201 201 $('#trimmed_message_box').css('max-height', 'none');
202 202 $(this).hide();
203 203 });
204 204
205 205 $('.show-inline-comments').on('click', function(e){
206 206 var boxid = $(this).attr('data-comment-id');
207 207 var button = $(this);
208 208
209 209 if(button.hasClass("comments-visible")) {
210 210 $('#{0} .inline-comments'.format(boxid)).each(function(index){
211 211 $(this).hide();
212 212 });
213 213 button.removeClass("comments-visible");
214 214 } else {
215 215 $('#{0} .inline-comments'.format(boxid)).each(function(index){
216 216 $(this).show();
217 217 });
218 218 button.addClass("comments-visible");
219 219 }
220 220 });
221 221
222 222
223 223 // next links
224 224 $('#child_link').on('click', function(e){
225 225 // fetch via ajax what is going to be the next link, if we have
226 226 // >1 links show them to user to choose
227 227 if(!$('#child_link').hasClass('disabled')){
228 228 $.ajax({
229 229 url: '${h.url('changeset_children',repo_name=c.repo_name, revision=c.commit.raw_id)}',
230 230 success: function(data) {
231 231 if(data.results.length === 0){
232 232 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
233 233 }
234 234 if(data.results.length === 1){
235 235 var commit = data.results[0];
236 236 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
237 237 }
238 238 else if(data.results.length === 2){
239 239 $('#child_link').addClass('disabled');
240 240 $('#child_link').addClass('double');
241 241 var _html = '';
242 242 _html +='<a title="__title__" href="__url__">__rev__</a> '
243 243 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
244 244 .replace('__title__', data.results[0].message)
245 245 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
246 246 _html +=' | ';
247 247 _html +='<a title="__title__" href="__url__">__rev__</a> '
248 248 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
249 249 .replace('__title__', data.results[1].message)
250 250 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
251 251 $('#child_link').html(_html);
252 252 }
253 253 }
254 254 });
255 255 e.preventDefault();
256 256 }
257 257 });
258 258
259 259 // prev links
260 260 $('#parent_link').on('click', function(e){
261 261 // fetch via ajax what is going to be the next link, if we have
262 262 // >1 links show them to user to choose
263 263 if(!$('#parent_link').hasClass('disabled')){
264 264 $.ajax({
265 265 url: '${h.url("changeset_parents",repo_name=c.repo_name, revision=c.commit.raw_id)}',
266 266 success: function(data) {
267 267 if(data.results.length === 0){
268 268 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
269 269 }
270 270 if(data.results.length === 1){
271 271 var commit = data.results[0];
272 272 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
273 273 }
274 274 else if(data.results.length === 2){
275 275 $('#parent_link').addClass('disabled');
276 276 $('#parent_link').addClass('double');
277 277 var _html = '';
278 278 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
279 279 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
280 280 .replace('__title__', data.results[0].message)
281 281 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
282 282 _html +=' | ';
283 283 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
284 284 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
285 285 .replace('__title__', data.results[1].message)
286 286 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
287 287 $('#parent_link').html(_html);
288 288 }
289 289 }
290 290 });
291 291 e.preventDefault();
292 292 }
293 293 });
294 294
295 295 if (location.hash) {
296 296 var result = splitDelimitedHash(location.hash);
297 297 var line = $('html').find(result.loc);
298 298 if (line.length > 0){
299 299 offsetScroll(line, 70);
300 300 }
301 301 }
302 302
303 303 // browse tree @ revision
304 304 $('#files_link').on('click', function(e){
305 305 window.location = '${h.url('files_home',repo_name=c.repo_name, revision=c.commit.raw_id, f_path='')}';
306 306 e.preventDefault();
307 307 });
308 308
309 309 // inject comments into their proper positions
310 310 var file_comments = $('.inline-comment-placeholder');
311 311 })
312 312 </script>
313 313
314 314 </%def>
@@ -1,400 +1,408 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6 6 <%namespace name="base" file="/base/base.mako"/>
7 7
8 8 <%def name="comment_block(comment, inline=False)">
9 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version', None)) %>
10 9 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10 % if inline:
11 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
12 % else:
13 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
14 % endif
15
11 16
12 17 <div class="comment
13 18 ${'comment-inline' if inline else 'comment-general'}
14 19 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
15 20 id="comment-${comment.comment_id}"
16 21 line="${comment.line_no}"
17 22 data-comment-id="${comment.comment_id}"
18 23 data-comment-type="${comment.comment_type}"
19 24 data-comment-inline=${h.json.dumps(inline)}
20 25 style="${'display: none;' if outdated_at_ver else ''}">
21 26
22 27 <div class="meta">
23 28 <div class="comment-type-label">
24 29 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
25 30 % if comment.comment_type == 'todo':
26 31 % if comment.resolved:
27 32 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
28 33 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
29 34 </div>
30 35 % else:
31 36 <div class="resolved tooltip" style="display: none">
32 37 <span>${comment.comment_type}</span>
33 38 </div>
34 39 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
35 40 ${comment.comment_type}
36 41 </div>
37 42 % endif
38 43 % else:
39 44 % if comment.resolved_comment:
40 45 fix
41 46 % else:
42 47 ${comment.comment_type or 'note'}
43 48 % endif
44 49 % endif
45 50 </div>
46 51 </div>
47 52
48 53 <div class="author ${'author-inline' if inline else 'author-general'}">
49 54 ${base.gravatar_with_user(comment.author.email, 16)}
50 55 </div>
51 56 <div class="date">
52 57 ${h.age_component(comment.modified_at, time_is_local=True)}
53 58 </div>
54 59 % if inline:
55 60 <span></span>
56 61 % else:
57 62 <div class="status-change">
58 63 % if comment.pull_request:
59 64 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
60 65 % if comment.status_change:
61 66 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
62 67 % else:
63 68 ${_('pull request #%s') % comment.pull_request.pull_request_id}
64 69 % endif
65 70 </a>
66 71 % else:
67 72 % if comment.status_change:
68 73 ${_('Status change on commit')}:
69 74 % endif
70 75 % endif
71 76 </div>
72 77 % endif
73 78
74 79 % if comment.status_change:
75 80 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
76 81 <div title="${_('Commit status')}" class="changeset-status-lbl">
77 82 ${comment.status_change[0].status_lbl}
78 83 </div>
79 84 % endif
80 85
81 86 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
82 87
83 88 <div class="comment-links-block">
84 89
85 90 % if inline:
86 % if outdated_at_ver:
87 91 <div class="pr-version-inline">
88 92 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
89 <code class="pr-version-num">
90 outdated ${'v{}'.format(pr_index_ver)}
91 </code>
93 % if outdated_at_ver:
94 <code class="pr-version-num" title="${_('Outdated comment from pull request version {0}').format(pr_index_ver)}">
95 outdated ${'v{}'.format(pr_index_ver)} |
96 </code>
97 % elif pr_index_ver:
98 <code class="pr-version-num" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
99 ${'v{}'.format(pr_index_ver)} |
100 </code>
101 % endif
92 102 </a>
93 103 </div>
94 |
95 % endif
96 104 % else:
97 105 % if comment.pull_request_version_id and pr_index_ver:
98 106 |
99 107 <div class="pr-version">
100 108 % if comment.outdated:
101 109 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
102 110 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
103 111 </a>
104 112 % else:
105 <div class="tooltip" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
113 <div title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
106 114 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
107 115 <code class="pr-version-num">
108 116 ${'v{}'.format(pr_index_ver)}
109 117 </code>
110 118 </a>
111 119 </div>
112 120 % endif
113 121 </div>
114 122 % endif
115 123 % endif
116 124
117 125 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
118 126 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
119 127 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
120 128 ## permissions to delete
121 129 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
122 130 ## TODO: dan: add edit comment here
123 131 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
124 132 %else:
125 133 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
126 134 %endif
127 135 %else:
128 136 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
129 137 %endif
130 138
131 139 %if not outdated_at_ver:
132 140 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
133 141 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
134 142 %endif
135 143
136 144 </div>
137 145 </div>
138 146 <div class="text">
139 147 ${comment.render(mentions=True)|n}
140 148 </div>
141 149
142 150 </div>
143 151 </%def>
144 152
145 153 ## generate main comments
146 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
154 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
147 155 <div id="comments">
148 %for comment in c.comments:
156 %for comment in comments:
149 157 <div id="comment-tr-${comment.comment_id}">
150 158 ## only render comments that are not from pull request, or from
151 159 ## pull request and a status change
152 160 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
153 161 ${comment_block(comment)}
154 162 %endif
155 163 </div>
156 164 %endfor
157 165 ## to anchor ajax comments
158 166 <div id="injected_page_comments"></div>
159 167 </div>
160 168 </%def>
161 169
162 170
163 171 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
164 172
165 173 ## merge status, and merge action
166 174 %if is_pull_request:
167 175 <div class="pull-request-merge">
168 176 %if c.allowed_to_merge:
169 177 <div class="pull-request-wrap">
170 178 <div class="pull-right">
171 179 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
172 180 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
173 181 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
174 182 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
175 183 ${h.end_form()}
176 184 </div>
177 185 </div>
178 186 %else:
179 187 <div class="pull-request-wrap">
180 188 <div class="pull-right">
181 189 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
182 190 </div>
183 191 </div>
184 192 %endif
185 193 </div>
186 194 %endif
187 195
188 196 <div class="comments">
189 197 <%
190 198 if is_pull_request:
191 199 placeholder = _('Leave a comment on this Pull Request.')
192 200 elif is_compare:
193 201 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
194 202 else:
195 203 placeholder = _('Leave a comment on this Commit.')
196 204 %>
197 205
198 206 % if c.rhodecode_user.username != h.DEFAULT_USER:
199 207 <div class="js-template" id="cb-comment-general-form-template">
200 208 ## template generated for injection
201 209 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
202 210 </div>
203 211
204 212 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
205 213 ## inject form here
206 214 </div>
207 215 <script type="text/javascript">
208 216 var lineNo = 'general';
209 217 var resolvesCommentId = null;
210 218 var generalCommentForm = Rhodecode.comments.createGeneralComment(
211 219 lineNo, "${placeholder}", resolvesCommentId);
212 220
213 221 // set custom success callback on rangeCommit
214 222 % if is_compare:
215 223 generalCommentForm.setHandleFormSubmit(function(o) {
216 224 var self = generalCommentForm;
217 225
218 226 var text = self.cm.getValue();
219 227 var status = self.getCommentStatus();
220 228 var commentType = self.getCommentType();
221 229
222 230 if (text === "" && !status) {
223 231 return;
224 232 }
225 233
226 234 // we can pick which commits we want to make the comment by
227 235 // selecting them via click on preview pane, this will alter the hidden inputs
228 236 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
229 237
230 238 var commitIds = [];
231 239 $('#changeset_compare_view_content .compare_select').each(function(el) {
232 240 var commitId = this.id.replace('row-', '');
233 241 if ($(this).hasClass('hl') || !cherryPicked) {
234 242 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
235 243 commitIds.push(commitId);
236 244 } else {
237 245 $("input[data-commit-id='{0}']".format(commitId)).val('')
238 246 }
239 247 });
240 248
241 249 self.setActionButtonsDisabled(true);
242 250 self.cm.setOption("readOnly", true);
243 251 var postData = {
244 252 'text': text,
245 253 'changeset_status': status,
246 254 'comment_type': commentType,
247 255 'commit_ids': commitIds,
248 256 'csrf_token': CSRF_TOKEN
249 257 };
250 258
251 259 var submitSuccessCallback = function(o) {
252 260 location.reload(true);
253 261 };
254 262 var submitFailCallback = function(){
255 263 self.resetCommentFormState(text)
256 264 };
257 265 self.submitAjaxPOST(
258 266 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
259 267 });
260 268 % endif
261 269
262 270
263 271 </script>
264 272 % else:
265 273 ## form state when not logged in
266 274 <div class="comment-form ac">
267 275
268 276 <div class="comment-area">
269 277 <div class="comment-area-header">
270 278 <ul class="nav-links clearfix">
271 279 <li class="active">
272 280 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
273 281 </li>
274 282 <li class="">
275 283 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
276 284 </li>
277 285 </ul>
278 286 </div>
279 287
280 288 <div class="comment-area-write" style="display: block;">
281 289 <div id="edit-container">
282 290 <div style="padding: 40px 0">
283 291 ${_('You need to be logged in to leave comments.')}
284 292 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
285 293 </div>
286 294 </div>
287 295 <div id="preview-container" class="clearfix" style="display: none;">
288 296 <div id="preview-box" class="preview-box"></div>
289 297 </div>
290 298 </div>
291 299
292 300 <div class="comment-area-footer">
293 301 <div class="toolbar">
294 302 <div class="toolbar-text">
295 303 </div>
296 304 </div>
297 305 </div>
298 306 </div>
299 307
300 308 <div class="comment-footer">
301 309 </div>
302 310
303 311 </div>
304 312 % endif
305 313
306 314 <script type="text/javascript">
307 315 bindToggleButtons();
308 316 </script>
309 317 </div>
310 318 </%def>
311 319
312 320
313 321 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
314 322 ## comment injected based on assumption that user is logged in
315 323
316 324 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
317 325
318 326 <div class="comment-area">
319 327 <div class="comment-area-header">
320 328 <ul class="nav-links clearfix">
321 329 <li class="active">
322 330 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
323 331 </li>
324 332 <li class="">
325 333 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
326 334 </li>
327 335 <li class="pull-right">
328 336 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
329 337 % for val in c.visual.comment_types:
330 338 <option value="${val}">${val.upper()}</option>
331 339 % endfor
332 340 </select>
333 341 </li>
334 342 </ul>
335 343 </div>
336 344
337 345 <div class="comment-area-write" style="display: block;">
338 346 <div id="edit-container_${lineno_id}">
339 347 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
340 348 </div>
341 349 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
342 350 <div id="preview-box_${lineno_id}" class="preview-box"></div>
343 351 </div>
344 352 </div>
345 353
346 354 <div class="comment-area-footer">
347 355 <div class="toolbar">
348 356 <div class="toolbar-text">
349 357 ${(_('Comments parsed using %s syntax with %s support.') % (
350 358 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
351 359 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
352 360 )
353 361 )|n}
354 362 </div>
355 363 </div>
356 364 </div>
357 365 </div>
358 366
359 367 <div class="comment-footer">
360 368
361 369 % if review_statuses:
362 370 <div class="status_box">
363 371 <select id="change_status_${lineno_id}" name="changeset_status">
364 372 <option></option> ## Placeholder
365 373 % for status, lbl in review_statuses:
366 374 <option value="${status}" data-status="${status}">${lbl}</option>
367 375 %if is_pull_request and change_status and status in ('approved', 'rejected'):
368 376 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
369 377 %endif
370 378 % endfor
371 379 </select>
372 380 </div>
373 381 % endif
374 382
375 383 ## inject extra inputs into the form
376 384 % if form_extras and isinstance(form_extras, (list, tuple)):
377 385 <div id="comment_form_extras">
378 386 % for form_ex_el in form_extras:
379 387 ${form_ex_el|n}
380 388 % endfor
381 389 </div>
382 390 % endif
383 391
384 392 <div class="action-buttons">
385 393 ## inline for has a file, and line-number together with cancel hide button.
386 394 % if form_type == 'inline':
387 395 <input type="hidden" name="f_path" value="{0}">
388 396 <input type="hidden" name="line" value="${lineno_id}">
389 397 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
390 398 ${_('Cancel')}
391 399 </button>
392 400 % endif
393 401 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
394 402
395 403 </div>
396 404 </div>
397 405
398 406 </form>
399 407
400 408 </%def> No newline at end of file
@@ -1,647 +1,692 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="breadcrumbs_links()">
11 11 <span id="pr-title">
12 12 ${c.pull_request.title}
13 13 %if c.pull_request.is_closed():
14 14 (${_('Closed')})
15 15 %endif
16 16 </span>
17 17 <div id="pr-title-edit" class="input" style="display: none;">
18 18 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
19 19 </div>
20 20 </%def>
21 21
22 22 <%def name="menu_bar_nav()">
23 23 ${self.menu_items(active='repositories')}
24 24 </%def>
25 25
26 26 <%def name="menu_bar_subnav()">
27 27 ${self.repo_menu(active='showpullrequest')}
28 28 </%def>
29 29
30 30 <%def name="main()">
31 31
32 32 <script type="text/javascript">
33 33 // TODO: marcink switch this to pyroutes
34 34 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
35 35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
36 36 </script>
37 37 <div class="box">
38
38 39 <div class="title">
39 40 ${self.repo_page_title(c.rhodecode_db_repo)}
40 41 </div>
41 42
42 43 ${self.breadcrumbs()}
43 44
44 45 <div class="box pr-summary">
46
45 47 <div class="summary-details block-left">
46 48 <% summary = lambda n:{False:'summary-short'}.get(n) %>
47 49 <div class="pr-details-title">
48 50 <a href="${h.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
49 51 %if c.allowed_to_update:
50 52 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
51 53 % if c.allowed_to_delete:
52 54 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
53 55 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
54 56 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
55 57 ${h.end_form()}
56 58 % else:
57 59 ${_('Delete')}
58 60 % endif
59 61 </div>
60 62 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
61 63 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
62 64 %endif
63 65 </div>
64 66
65 67 <div id="summary" class="fields pr-details-content">
66 68 <div class="field">
67 69 <div class="label-summary">
68 70 <label>${_('Origin')}:</label>
69 71 </div>
70 72 <div class="input">
71 73 <div class="pr-origininfo">
72 74 ## branch link is only valid if it is a branch
73 75 <span class="tag">
74 76 %if c.pull_request.source_ref_parts.type == 'branch':
75 77 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
76 78 %else:
77 79 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
78 80 %endif
79 81 </span>
80 82 <span class="clone-url">
81 83 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
82 84 </span>
83 85 </div>
84 86 <div class="pr-pullinfo">
85 87 %if h.is_hg(c.pull_request.source_repo):
86 88 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
87 89 %elif h.is_git(c.pull_request.source_repo):
88 90 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
89 91 %endif
90 92 </div>
91 93 </div>
92 94 </div>
93 95 <div class="field">
94 96 <div class="label-summary">
95 97 <label>${_('Target')}:</label>
96 98 </div>
97 99 <div class="input">
98 100 <div class="pr-targetinfo">
99 101 ## branch link is only valid if it is a branch
100 102 <span class="tag">
101 103 %if c.pull_request.target_ref_parts.type == 'branch':
102 104 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
103 105 %else:
104 106 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
105 107 %endif
106 108 </span>
107 109 <span class="clone-url">
108 110 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
109 111 </span>
110 112 </div>
111 113 </div>
112 114 </div>
113 115
114 116 ## Link to the shadow repository.
115 117 <div class="field">
116 118 <div class="label-summary">
117 119 <label>${_('Merge')}:</label>
118 120 </div>
119 121 <div class="input">
120 122 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
121 123 <div class="pr-mergeinfo">
122 124 %if h.is_hg(c.pull_request.target_repo):
123 125 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
124 126 %elif h.is_git(c.pull_request.target_repo):
125 127 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
126 128 %endif
127 129 </div>
128 130 % else:
129 131 <div class="">
130 132 ${_('Shadow repository data not available')}.
131 133 </div>
132 134 % endif
133 135 </div>
134 136 </div>
135 137
136 138 <div class="field">
137 139 <div class="label-summary">
138 140 <label>${_('Review')}:</label>
139 141 </div>
140 142 <div class="input">
141 143 %if c.pull_request_review_status:
142 144 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
143 145 <span class="changeset-status-lbl tooltip">
144 146 %if c.pull_request.is_closed():
145 147 ${_('Closed')},
146 148 %endif
147 149 ${h.commit_status_lbl(c.pull_request_review_status)}
148 150 </span>
149 151 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
150 152 %endif
151 153 </div>
152 154 </div>
153 155 <div class="field">
154 156 <div class="pr-description-label label-summary">
155 157 <label>${_('Description')}:</label>
156 158 </div>
157 159 <div id="pr-desc" class="input">
158 160 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
159 161 </div>
160 162 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
161 163 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
162 164 </div>
163 165 </div>
164 166
165 167 <div class="field">
166 168 <div class="label-summary">
167 169 <label>${_('Versions')} (${len(c.versions)+1}):</label>
168 170 </div>
169 171
170 172 <div class="pr-versions">
171 173 % if c.show_version_changes:
172 174 <table>
173 175 ## CURRENTLY SELECT PR VERSION
174 176 <tr class="version-pr" style="display: ${'' if c.at_version_num is None else 'none'}">
175 177 <td>
176 % if c.at_version in [None, 'latest']:
178 % if c.at_version_num is None:
177 179 <i class="icon-ok link"></i>
178 180 % else:
179 <i class="icon-comment"></i> <code>${len(c.inline_versions[None])}</code>
181 <i class="icon-comment"></i>
182 <code>
183 ${len(c.comment_versions[None]['at'])}/${len(c.inline_versions[None]['at'])}
184 </code>
180 185 % endif
181 186 </td>
182 187 <td>
183 188 <code>
184 189 % if c.versions:
185 190 <a href="${h.url.current(version='latest')}">${_('latest')}</a>
186 191 % else:
187 192 ${_('initial')}
188 193 % endif
189 194 </code>
190 195 </td>
191 196 <td>
192 197 <code>${c.pull_request_latest.source_ref_parts.commit_id[:6]}</code>
193 198 </td>
194 199 <td>
195 200 ${_('created')} ${h.age_component(c.pull_request_latest.updated_on)}
196 201 </td>
197 202 <td align="right">
198 203 % if c.versions and c.at_version_num in [None, 'latest']:
199 204 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
200 205 % endif
201 206 </td>
202 207 </tr>
203 208
204 209 ## SHOW ALL VERSIONS OF PR
205 210 <% ver_pr = None %>
211
206 212 % for data in reversed(list(enumerate(c.versions, 1))):
207 <% ver_pos = data[0] %>
208 <% ver = data[1] %>
209 <% ver_pr = ver.pull_request_version_id %>
213 <% ver_pos = data[0] %>
214 <% ver = data[1] %>
215 <% ver_pr = ver.pull_request_version_id %>
210 216
211 <tr class="version-pr" style="display: ${'' if c.at_version == ver_pr else 'none'}">
212 <td>
213 % if c.at_version == ver_pr:
214 <i class="icon-ok link"></i>
215 % else:
216 <i class="icon-comment"></i> <code>${len(c.inline_versions[ver_pr])}</code>
217 % endif
218 </td>
219 <td>
220 <code class="tooltip" title="${_('Comment from pull request version {0}').format(ver_pos)}">
221 <a href="${h.url.current(version=ver_pr)}">v${ver_pos}</a>
222 </code>
223 </td>
224 <td>
225 <code>${ver.source_ref_parts.commit_id[:6]}</code>
226 </td>
227 <td>
228 ${_('created')} ${h.age_component(ver.updated_on)}
229 </td>
230 <td align="right">
231 % if c.at_version == ver_pr:
232 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
233 % endif
234 </td>
235 </tr>
217 <tr class="version-pr" style="display: ${'' if c.at_version_num == ver_pr else 'none'}">
218 <td>
219 % if c.at_version_num == ver_pr:
220 <i class="icon-ok link"></i>
221 % else:
222 <i class="icon-comment"></i>
223 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
224 ${len(c.comment_versions[ver_pr]['at'])}/${len(c.inline_versions[ver_pr]['at'])}
225 </code>
226 % endif
227 </td>
228 <td>
229 <code>
230 <a href="${h.url.current(version=ver_pr)}">v${ver_pos}</a>
231 </code>
232 </td>
233 <td>
234 <code>${ver.source_ref_parts.commit_id[:6]}</code>
235 </td>
236 <td>
237 ${_('created')} ${h.age_component(ver.updated_on)}
238 </td>
239 <td align="right">
240 % if c.at_version_num == ver_pr:
241 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
242 % endif
243 </td>
244 </tr>
236 245 % endfor
237 246
238 247 ## show comment/inline comments summary
239 248 <tr>
240 249 <td>
241 250 </td>
242 251
243 <% inline_comm_count_ver = len(c.inline_versions[ver_pr])%>
244 252 <td colspan="4" style="border-top: 1px dashed #dbd9da">
245 ${_('Comments for this version')}:
246 %if c.comments:
247 <a href="#comments">${_("%d General ") % len(c.comments)}</a>
253 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
254 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
255
256
257 % if c.at_version:
258 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
259 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
260 ${_('Comments at this version')}:
261 % else:
262 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
263 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
264 ${_('Comments for this pull request')}:
265 % endif
266
267 %if general_comm_count_ver:
268 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
248 269 %else:
249 ${_("%d General ") % len(c.comments)}
270 ${_("%d General ") % general_comm_count_ver}
250 271 %endif
251 272
252 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num])%>
253 273 %if inline_comm_count_ver:
254 274 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
255 275 %else:
256 276 , ${_("%d Inline") % inline_comm_count_ver}
257 277 %endif
258 278
259 %if c.outdated_cnt:
260 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % c.outdated_cnt}</a>
279 %if outdated_comm_count_ver:
280 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
261 281 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
262 282 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
263 283 %else:
264 , ${_("%d Outdated") % c.outdated_cnt}
284 , ${_("%d Outdated") % outdated_comm_count_ver}
265 285 %endif
266 286 </td>
267 287 </tr>
268 288
269 289 <tr>
270 290 <td></td>
271 291 <td colspan="4">
272 292 % if c.at_version:
273 293 <pre>
274 294 Changed commits:
275 295 * added: ${len(c.changes.added)}
276 296 * removed: ${len(c.changes.removed)}
277 297
278 298 % if not (c.file_changes.added+c.file_changes.modified+c.file_changes.removed):
279 299 No file changes found
280 300 % else:
281 301 Changed files:
282 302 %for file_name in c.file_changes.added:
283 303 * A <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
284 304 %endfor
285 305 %for file_name in c.file_changes.modified:
286 306 * M <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
287 307 %endfor
288 308 %for file_name in c.file_changes.removed:
289 309 * R ${file_name}
290 310 %endfor
291 311 % endif
292 312 </pre>
293 313 % endif
294 314 </td>
295 315 </tr>
296 316 </table>
297 317 % else:
298 318 ${_('Pull request versions not available')}.
299 319 % endif
300 320 </div>
301 321 </div>
302 322
303 323 <div id="pr-save" class="field" style="display: none;">
304 324 <div class="label-summary"></div>
305 325 <div class="input">
306 326 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
307 327 </div>
308 328 </div>
309 329 </div>
310 330 </div>
311 331 <div>
312 332 ## AUTHOR
313 333 <div class="reviewers-title block-right">
314 334 <div class="pr-details-title">
315 335 ${_('Author')}
316 336 </div>
317 337 </div>
318 338 <div class="block-right pr-details-content reviewers">
319 339 <ul class="group_members">
320 340 <li>
321 341 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
322 342 </li>
323 343 </ul>
324 344 </div>
325 345 ## REVIEWERS
326 346 <div class="reviewers-title block-right">
327 347 <div class="pr-details-title">
328 348 ${_('Pull request reviewers')}
329 349 %if c.allowed_to_update:
330 350 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
331 351 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
332 352 %endif
333 353 </div>
334 354 </div>
335 355 <div id="reviewers" class="block-right pr-details-content reviewers">
336 356 ## members goes here !
337 357 <input type="hidden" name="__start__" value="review_members:sequence">
338 358 <ul id="review_members" class="group_members">
339 359 %for member,reasons,status in c.pull_request_reviewers:
340 360 <li id="reviewer_${member.user_id}">
341 361 <div class="reviewers_member">
342 362 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
343 363 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
344 364 </div>
345 365 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
346 366 ${self.gravatar_with_user(member.email, 16)}
347 367 </div>
348 368 <input type="hidden" name="__start__" value="reviewer:mapping">
349 369 <input type="hidden" name="__start__" value="reasons:sequence">
350 370 %for reason in reasons:
351 371 <div class="reviewer_reason">- ${reason}</div>
352 372 <input type="hidden" name="reason" value="${reason}">
353 373
354 374 %endfor
355 375 <input type="hidden" name="__end__" value="reasons:sequence">
356 376 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
357 377 <input type="hidden" name="__end__" value="reviewer:mapping">
358 378 %if c.allowed_to_update:
359 379 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
360 380 <i class="icon-remove-sign" ></i>
361 381 </div>
362 382 %endif
363 383 </div>
364 384 </li>
365 385 %endfor
366 386 </ul>
367 387 <input type="hidden" name="__end__" value="review_members:sequence">
368 388 %if not c.pull_request.is_closed():
369 389 <div id="add_reviewer_input" class='ac' style="display: none;">
370 390 %if c.allowed_to_update:
371 391 <div class="reviewer_ac">
372 392 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
373 393 <div id="reviewers_container"></div>
374 394 </div>
375 395 <div>
376 396 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
377 397 </div>
378 398 %endif
379 399 </div>
380 400 %endif
381 401 </div>
382 402 </div>
383 403 </div>
384 404 <div class="box">
385 405 ##DIFF
386 406 <div class="table" >
387 407 <div id="changeset_compare_view_content">
388 408 ##CS
389 409 % if c.missing_requirements:
390 410 <div class="box">
391 411 <div class="alert alert-warning">
392 412 <div>
393 413 <strong>${_('Missing requirements:')}</strong>
394 414 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
395 415 </div>
396 416 </div>
397 417 </div>
398 418 % elif c.missing_commits:
399 419 <div class="box">
400 420 <div class="alert alert-warning">
401 421 <div>
402 422 <strong>${_('Missing commits')}:</strong>
403 423 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
404 424 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
405 425 </div>
406 426 </div>
407 427 </div>
408 428 % endif
409 429 <div class="compare_view_commits_title">
410 430
411 431 <div class="pull-left">
412 432 <div class="btn-group">
413 433 <a
414 434 class="btn"
415 435 href="#"
416 436 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
417 437 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
418 438 </a>
419 439 <a
420 440 class="btn"
421 441 href="#"
422 442 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
423 443 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
424 444 </a>
425 445 </div>
426 446 </div>
427 447
428 448 <div class="pull-right">
429 449 % if c.allowed_to_update and not c.pull_request.is_closed():
430 450 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
431 451 % else:
432 452 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
433 453 % endif
434 454
435 455 </div>
436 456
437 457 </div>
438 458
439 459 % if not c.missing_commits:
440 460 <%include file="/compare/compare_commits.mako" />
441 461 <div class="cs_files">
442 462 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
443 463 ${cbdiffs.render_diffset_menu()}
444 464 ${cbdiffs.render_diffset(
445 465 c.diffset, use_comments=True,
446 466 collapse_when_files_over=30,
447 467 disable_new_comments=not c.allowed_to_comment,
448 468 deleted_files_comments=c.deleted_files_comments)}
449 469 </div>
450 470 % else:
451 471 ## skipping commits we need to clear the view for missing commits
452 472 <div style="clear:both;"></div>
453 473 % endif
454 474
455 475 </div>
456 476 </div>
457 477
458 478 ## template for inline comment form
459 479 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
460 480
461 481 ## render general comments
462 ${comment.generate_comments(include_pull_request=True, is_pull_request=True)}
482
483 <div id="comment-tr-show">
484 <div class="comment">
485 <div class="meta">
486 % if general_outdated_comm_count_ver:
487 % if general_outdated_comm_count_ver == 1:
488 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
489 <a href="#" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
490 % else:
491 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
492 <a href="#" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
493 % endif
494 % endif
495 </div>
496 </div>
497 </div>
498
499 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
463 500
464 501 % if not c.pull_request.is_closed():
465 502 ## main comment form and it status
466 503 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
467 504 pull_request_id=c.pull_request.pull_request_id),
468 505 c.pull_request_review_status,
469 506 is_pull_request=True, change_status=c.allowed_to_change_status)}
470 507 %endif
471 508
472 509 <script type="text/javascript">
473 510 if (location.hash) {
474 511 var result = splitDelimitedHash(location.hash);
475 var line = $('html').find(result.loc);
512 var line = $('html').find(result.loc);
513 // show hidden comments if we use location.hash
514 if (line.hasClass('comment-general')) {
515 $(line).show();
516 } else if (line.hasClass('comment-inline')) {
517 $(line).show();
518 var $cb = $(line).closest('.cb');
519 $cb.removeClass('cb-collapsed')
520 }
476 521 if (line.length > 0){
477 522 offsetScroll(line, 70);
478 523 }
479 524 }
525
480 526 $(function(){
481 527 ReviewerAutoComplete('user');
482 528 // custom code mirror
483 529 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
484 530
485 531 var PRDetails = {
486 532 editButton: $('#open_edit_pullrequest'),
487 533 closeButton: $('#close_edit_pullrequest'),
488 534 deleteButton: $('#delete_pullrequest'),
489 535 viewFields: $('#pr-desc, #pr-title'),
490 536 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
491 537
492 538 init: function() {
493 539 var that = this;
494 540 this.editButton.on('click', function(e) { that.edit(); });
495 541 this.closeButton.on('click', function(e) { that.view(); });
496 542 },
497 543
498 544 edit: function(event) {
499 545 this.viewFields.hide();
500 546 this.editButton.hide();
501 547 this.deleteButton.hide();
502 548 this.closeButton.show();
503 549 this.editFields.show();
504 550 codeMirrorInstance.refresh();
505 551 },
506 552
507 553 view: function(event) {
508 554 this.editButton.show();
509 555 this.deleteButton.show();
510 556 this.editFields.hide();
511 557 this.closeButton.hide();
512 558 this.viewFields.show();
513 559 }
514 560 };
515 561
516 562 var ReviewersPanel = {
517 563 editButton: $('#open_edit_reviewers'),
518 564 closeButton: $('#close_edit_reviewers'),
519 565 addButton: $('#add_reviewer_input'),
520 566 removeButtons: $('.reviewer_member_remove'),
521 567
522 568 init: function() {
523 569 var that = this;
524 570 this.editButton.on('click', function(e) { that.edit(); });
525 571 this.closeButton.on('click', function(e) { that.close(); });
526 572 },
527 573
528 574 edit: function(event) {
529 575 this.editButton.hide();
530 576 this.closeButton.show();
531 577 this.addButton.show();
532 578 this.removeButtons.css('visibility', 'visible');
533 579 },
534 580
535 581 close: function(event) {
536 582 this.editButton.show();
537 583 this.closeButton.hide();
538 584 this.addButton.hide();
539 585 this.removeButtons.css('visibility', 'hidden');
540 586 }
541 587 };
542 588
543 589 PRDetails.init();
544 590 ReviewersPanel.init();
545 591
546 592 showOutdated = function(self){
547 $('.comment-outdated').show();
593 $('.comment-inline.comment-outdated').show();
548 594 $('.filediff-outdated').show();
549 595 $('.showOutdatedComments').hide();
550 596 $('.hideOutdatedComments').show();
551
552 597 };
553 598
554 599 hideOutdated = function(self){
555 $('.comment-outdated').hide();
600 $('.comment-inline.comment-outdated').hide();
556 601 $('.filediff-outdated').hide();
557 602 $('.hideOutdatedComments').hide();
558 603 $('.showOutdatedComments').show();
559 604 };
560 605
561 606 $('#show-outdated-comments').on('click', function(e){
562 607 var button = $(this);
563 608 var outdated = $('.comment-outdated');
564 609
565 610 if (button.html() === "(Show)") {
566 611 button.html("(Hide)");
567 612 outdated.show();
568 613 } else {
569 614 button.html("(Show)");
570 615 outdated.hide();
571 616 }
572 617 });
573 618
574 619 $('.show-inline-comments').on('change', function(e){
575 620 var show = 'none';
576 621 var target = e.currentTarget;
577 622 if(target.checked){
578 623 show = ''
579 624 }
580 625 var boxid = $(target).attr('id_for');
581 626 var comments = $('#{0} .inline-comments'.format(boxid));
582 627 var fn_display = function(idx){
583 628 $(this).css('display', show);
584 629 };
585 630 $(comments).each(fn_display);
586 631 var btns = $('#{0} .inline-comments-button'.format(boxid));
587 632 $(btns).each(fn_display);
588 633 });
589 634
590 635 $('#merge_pull_request_form').submit(function() {
591 636 if (!$('#merge_pull_request').attr('disabled')) {
592 637 $('#merge_pull_request').attr('disabled', 'disabled');
593 638 }
594 639 return true;
595 640 });
596 641
597 642 $('#edit_pull_request').on('click', function(e){
598 643 var title = $('#pr-title-input').val();
599 644 var description = codeMirrorInstance.getValue();
600 645 editPullRequest(
601 646 "${c.repo_name}", "${c.pull_request.pull_request_id}",
602 647 title, description);
603 648 });
604 649
605 650 $('#update_pull_request').on('click', function(e){
606 651 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
607 652 });
608 653
609 654 $('#update_commits').on('click', function(e){
610 655 var isDisabled = !$(e.currentTarget).attr('disabled');
611 656 $(e.currentTarget).text(_gettext('Updating...'));
612 657 $(e.currentTarget).attr('disabled', 'disabled');
613 658 if(isDisabled){
614 659 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
615 660 }
616 661
617 662 });
618 663 // fixing issue with caches on firefox
619 664 $('#update_commits').removeAttr("disabled");
620 665
621 666 $('#close_pull_request').on('click', function(e){
622 667 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
623 668 });
624 669
625 670 $('.show-inline-comments').on('click', function(e){
626 671 var boxid = $(this).attr('data-comment-id');
627 672 var button = $(this);
628 673
629 674 if(button.hasClass("comments-visible")) {
630 675 $('#{0} .inline-comments'.format(boxid)).each(function(index){
631 676 $(this).hide();
632 677 });
633 678 button.removeClass("comments-visible");
634 679 } else {
635 680 $('#{0} .inline-comments'.format(boxid)).each(function(index){
636 681 $(this).show();
637 682 });
638 683 button.addClass("comments-visible");
639 684 }
640 685 });
641 686 })
642 687 </script>
643 688
644 689 </div>
645 690 </div>
646 691
647 692 </%def>
General Comments 0
You need to be logged in to leave comments. Login now