##// END OF EJS Templates
sidebar: few fixes for panel rendering of reviewers/observers for both commits and PRS.
marcink -
r4503:3bcf2943 stable
parent child Browse files
Show More
@@ -1,781 +1,784 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 import logging
22 22 import collections
23 23
24 24 from pyramid.httpexceptions import (
25 25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
26 26 from pyramid.view import view_config
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode.apps._base import RepoAppView
31 31 from rhodecode.apps.file_store import utils as store_utils
32 32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
33 33
34 34 from rhodecode.lib import diffs, codeblocks
35 35 from rhodecode.lib.auth import (
36 36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.compat import OrderedDict
39 39 from rhodecode.lib.diffs import (
40 40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
41 41 get_diff_whitespace_flag)
42 42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
43 43 import rhodecode.lib.helpers as h
44 44 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict
45 45 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 46 from rhodecode.lib.vcs.exceptions import (
47 47 RepositoryError, CommitDoesNotExistError)
48 48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
49 49 ChangesetCommentHistory
50 50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 51 from rhodecode.model.comment import CommentsModel
52 52 from rhodecode.model.meta import Session
53 53 from rhodecode.model.settings import VcsSettingsModel
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 def _update_with_GET(params, request):
59 59 for k in ['diff1', 'diff2', 'diff']:
60 60 params[k] += request.GET.getall(k)
61 61
62 62
63 63 class RepoCommitsView(RepoAppView):
64 64 def load_default_context(self):
65 65 c = self._get_local_tmpl_context(include_app_defaults=True)
66 66 c.rhodecode_repo = self.rhodecode_vcs_repo
67 67
68 68 return c
69 69
70 70 def _is_diff_cache_enabled(self, target_repo):
71 71 caching_enabled = self._get_general_setting(
72 72 target_repo, 'rhodecode_diff_cache')
73 73 log.debug('Diff caching enabled: %s', caching_enabled)
74 74 return caching_enabled
75 75
76 76 def _commit(self, commit_id_range, method):
77 77 _ = self.request.translate
78 78 c = self.load_default_context()
79 79 c.fulldiff = self.request.GET.get('fulldiff')
80 80
81 81 # fetch global flags of ignore ws or context lines
82 82 diff_context = get_diff_context(self.request)
83 83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
84 84
85 85 # diff_limit will cut off the whole diff if the limit is applied
86 86 # otherwise it will just hide the big files from the front-end
87 87 diff_limit = c.visual.cut_off_limit_diff
88 88 file_limit = c.visual.cut_off_limit_file
89 89
90 90 # get ranges of commit ids if preset
91 91 commit_range = commit_id_range.split('...')[:2]
92 92
93 93 try:
94 94 pre_load = ['affected_files', 'author', 'branch', 'date',
95 95 'message', 'parents']
96 96 if self.rhodecode_vcs_repo.alias == 'hg':
97 97 pre_load += ['hidden', 'obsolete', 'phase']
98 98
99 99 if len(commit_range) == 2:
100 100 commits = self.rhodecode_vcs_repo.get_commits(
101 101 start_id=commit_range[0], end_id=commit_range[1],
102 102 pre_load=pre_load, translate_tags=False)
103 103 commits = list(commits)
104 104 else:
105 105 commits = [self.rhodecode_vcs_repo.get_commit(
106 106 commit_id=commit_id_range, pre_load=pre_load)]
107 107
108 108 c.commit_ranges = commits
109 109 if not c.commit_ranges:
110 110 raise RepositoryError('The commit range returned an empty result')
111 111 except CommitDoesNotExistError as e:
112 112 msg = _('No such commit exists. Org exception: `{}`').format(e)
113 113 h.flash(msg, category='error')
114 114 raise HTTPNotFound()
115 115 except Exception:
116 116 log.exception("General failure")
117 117 raise HTTPNotFound()
118 118 single_commit = len(c.commit_ranges) == 1
119 119
120 120 c.changes = OrderedDict()
121 121 c.lines_added = 0
122 122 c.lines_deleted = 0
123 123
124 124 # auto collapse if we have more than limit
125 125 collapse_limit = diffs.DiffProcessor._collapse_commits_over
126 126 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
127 127
128 128 c.commit_statuses = ChangesetStatus.STATUSES
129 129 c.inline_comments = []
130 130 c.files = []
131 131
132 132 c.comments = []
133 133 c.unresolved_comments = []
134 134 c.resolved_comments = []
135 135
136 136 # Single commit
137 137 if single_commit:
138 138 commit = c.commit_ranges[0]
139 139 c.comments = CommentsModel().get_comments(
140 140 self.db_repo.repo_id,
141 141 revision=commit.raw_id)
142 142
143 143 # comments from PR
144 144 statuses = ChangesetStatusModel().get_statuses(
145 145 self.db_repo.repo_id, commit.raw_id,
146 146 with_revisions=True)
147 147
148 148 prs = set()
149 149 reviewers = list()
150 150 reviewers_duplicates = set() # to not have duplicates from multiple votes
151 151 for c_status in statuses:
152 152
153 153 # extract associated pull-requests from votes
154 154 if c_status.pull_request:
155 155 prs.add(c_status.pull_request)
156 156
157 157 # extract reviewers
158 158 _user_id = c_status.author.user_id
159 159 if _user_id not in reviewers_duplicates:
160 160 reviewers.append(
161 161 StrictAttributeDict({
162 162 'user': c_status.author,
163 163
164 164 # fake attributed for commit, page that we don't have
165 165 # but we share the display with PR page
166 166 'mandatory': False,
167 167 'reasons': [],
168 168 'rule_user_group_data': lambda: None
169 169 })
170 170 )
171 171 reviewers_duplicates.add(_user_id)
172 172
173 173 c.allowed_reviewers = reviewers
174 c.reviewers_count = len(reviewers)
175 c.observers_count = 0
176
174 177 # from associated statuses, check the pull requests, and
175 178 # show comments from them
176 179 for pr in prs:
177 180 c.comments.extend(pr.comments)
178 181
179 182 c.unresolved_comments = CommentsModel()\
180 183 .get_commit_unresolved_todos(commit.raw_id)
181 184 c.resolved_comments = CommentsModel()\
182 185 .get_commit_resolved_todos(commit.raw_id)
183 186
184 187 c.inline_comments_flat = CommentsModel()\
185 188 .get_commit_inline_comments(commit.raw_id)
186 189
187 190 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
188 191 statuses, reviewers)
189 192
190 193 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
191 194
192 195 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
193 196
194 197 for review_obj, member, reasons, mandatory, status in review_statuses:
195 198 member_reviewer = h.reviewer_as_json(
196 199 member, reasons=reasons, mandatory=mandatory, role=None,
197 200 user_group=None
198 201 )
199 202
200 203 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
201 204 member_reviewer['review_status'] = current_review_status
202 205 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
203 206 member_reviewer['allowed_to_update'] = False
204 207 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
205 208
206 209 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
207 210
208 211 # NOTE(marcink): this uses the same voting logic as in pull-requests
209 212 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
210 213 c.commit_broadcast_channel = u'/repo${}$/commit/{}'.format(
211 214 c.repo_name,
212 215 commit.raw_id
213 216 )
214 217
215 218 diff = None
216 219 # Iterate over ranges (default commit view is always one commit)
217 220 for commit in c.commit_ranges:
218 221 c.changes[commit.raw_id] = []
219 222
220 223 commit2 = commit
221 224 commit1 = commit.first_parent
222 225
223 226 if method == 'show':
224 227 inline_comments = CommentsModel().get_inline_comments(
225 228 self.db_repo.repo_id, revision=commit.raw_id)
226 229 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
227 230 inline_comments))
228 231 c.inline_comments = inline_comments
229 232
230 233 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
231 234 self.db_repo)
232 235 cache_file_path = diff_cache_exist(
233 236 cache_path, 'diff', commit.raw_id,
234 237 hide_whitespace_changes, diff_context, c.fulldiff)
235 238
236 239 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
237 240 force_recache = str2bool(self.request.GET.get('force_recache'))
238 241
239 242 cached_diff = None
240 243 if caching_enabled:
241 244 cached_diff = load_cached_diff(cache_file_path)
242 245
243 246 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
244 247 if not force_recache and has_proper_diff_cache:
245 248 diffset = cached_diff['diff']
246 249 else:
247 250 vcs_diff = self.rhodecode_vcs_repo.get_diff(
248 251 commit1, commit2,
249 252 ignore_whitespace=hide_whitespace_changes,
250 253 context=diff_context)
251 254
252 255 diff_processor = diffs.DiffProcessor(
253 256 vcs_diff, format='newdiff', diff_limit=diff_limit,
254 257 file_limit=file_limit, show_full_diff=c.fulldiff)
255 258
256 259 _parsed = diff_processor.prepare()
257 260
258 261 diffset = codeblocks.DiffSet(
259 262 repo_name=self.db_repo_name,
260 263 source_node_getter=codeblocks.diffset_node_getter(commit1),
261 264 target_node_getter=codeblocks.diffset_node_getter(commit2))
262 265
263 266 diffset = self.path_filter.render_patchset_filtered(
264 267 diffset, _parsed, commit1.raw_id, commit2.raw_id)
265 268
266 269 # save cached diff
267 270 if caching_enabled:
268 271 cache_diff(cache_file_path, diffset, None)
269 272
270 273 c.limited_diff = diffset.limited_diff
271 274 c.changes[commit.raw_id] = diffset
272 275 else:
273 276 # TODO(marcink): no cache usage here...
274 277 _diff = self.rhodecode_vcs_repo.get_diff(
275 278 commit1, commit2,
276 279 ignore_whitespace=hide_whitespace_changes, context=diff_context)
277 280 diff_processor = diffs.DiffProcessor(
278 281 _diff, format='newdiff', diff_limit=diff_limit,
279 282 file_limit=file_limit, show_full_diff=c.fulldiff)
280 283 # downloads/raw we only need RAW diff nothing else
281 284 diff = self.path_filter.get_raw_patch(diff_processor)
282 285 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
283 286
284 287 # sort comments by how they were generated
285 288 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
286 289 c.at_version_num = None
287 290
288 291 if len(c.commit_ranges) == 1:
289 292 c.commit = c.commit_ranges[0]
290 293 c.parent_tmpl = ''.join(
291 294 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
292 295
293 296 if method == 'download':
294 297 response = Response(diff)
295 298 response.content_type = 'text/plain'
296 299 response.content_disposition = (
297 300 'attachment; filename=%s.diff' % commit_id_range[:12])
298 301 return response
299 302 elif method == 'patch':
300 303 c.diff = safe_unicode(diff)
301 304 patch = render(
302 305 'rhodecode:templates/changeset/patch_changeset.mako',
303 306 self._get_template_context(c), self.request)
304 307 response = Response(patch)
305 308 response.content_type = 'text/plain'
306 309 return response
307 310 elif method == 'raw':
308 311 response = Response(diff)
309 312 response.content_type = 'text/plain'
310 313 return response
311 314 elif method == 'show':
312 315 if len(c.commit_ranges) == 1:
313 316 html = render(
314 317 'rhodecode:templates/changeset/changeset.mako',
315 318 self._get_template_context(c), self.request)
316 319 return Response(html)
317 320 else:
318 321 c.ancestor = None
319 322 c.target_repo = self.db_repo
320 323 html = render(
321 324 'rhodecode:templates/changeset/changeset_range.mako',
322 325 self._get_template_context(c), self.request)
323 326 return Response(html)
324 327
325 328 raise HTTPBadRequest()
326 329
327 330 @LoginRequired()
328 331 @HasRepoPermissionAnyDecorator(
329 332 'repository.read', 'repository.write', 'repository.admin')
330 333 @view_config(
331 334 route_name='repo_commit', request_method='GET',
332 335 renderer=None)
333 336 def repo_commit_show(self):
334 337 commit_id = self.request.matchdict['commit_id']
335 338 return self._commit(commit_id, method='show')
336 339
337 340 @LoginRequired()
338 341 @HasRepoPermissionAnyDecorator(
339 342 'repository.read', 'repository.write', 'repository.admin')
340 343 @view_config(
341 344 route_name='repo_commit_raw', request_method='GET',
342 345 renderer=None)
343 346 @view_config(
344 347 route_name='repo_commit_raw_deprecated', request_method='GET',
345 348 renderer=None)
346 349 def repo_commit_raw(self):
347 350 commit_id = self.request.matchdict['commit_id']
348 351 return self._commit(commit_id, method='raw')
349 352
350 353 @LoginRequired()
351 354 @HasRepoPermissionAnyDecorator(
352 355 'repository.read', 'repository.write', 'repository.admin')
353 356 @view_config(
354 357 route_name='repo_commit_patch', request_method='GET',
355 358 renderer=None)
356 359 def repo_commit_patch(self):
357 360 commit_id = self.request.matchdict['commit_id']
358 361 return self._commit(commit_id, method='patch')
359 362
360 363 @LoginRequired()
361 364 @HasRepoPermissionAnyDecorator(
362 365 'repository.read', 'repository.write', 'repository.admin')
363 366 @view_config(
364 367 route_name='repo_commit_download', request_method='GET',
365 368 renderer=None)
366 369 def repo_commit_download(self):
367 370 commit_id = self.request.matchdict['commit_id']
368 371 return self._commit(commit_id, method='download')
369 372
370 373 @LoginRequired()
371 374 @NotAnonymous()
372 375 @HasRepoPermissionAnyDecorator(
373 376 'repository.read', 'repository.write', 'repository.admin')
374 377 @CSRFRequired()
375 378 @view_config(
376 379 route_name='repo_commit_comment_create', request_method='POST',
377 380 renderer='json_ext')
378 381 def repo_commit_comment_create(self):
379 382 _ = self.request.translate
380 383 commit_id = self.request.matchdict['commit_id']
381 384
382 385 c = self.load_default_context()
383 386 status = self.request.POST.get('changeset_status', None)
384 387 text = self.request.POST.get('text')
385 388 comment_type = self.request.POST.get('comment_type')
386 389 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
387 390
388 391 if status:
389 392 text = text or (_('Status change %(transition_icon)s %(status)s')
390 393 % {'transition_icon': '>',
391 394 'status': ChangesetStatus.get_status_lbl(status)})
392 395
393 396 multi_commit_ids = []
394 397 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
395 398 if _commit_id not in ['', None, EmptyCommit.raw_id]:
396 399 if _commit_id not in multi_commit_ids:
397 400 multi_commit_ids.append(_commit_id)
398 401
399 402 commit_ids = multi_commit_ids or [commit_id]
400 403
401 404 comment = None
402 405 for current_id in filter(None, commit_ids):
403 406 comment = CommentsModel().create(
404 407 text=text,
405 408 repo=self.db_repo.repo_id,
406 409 user=self._rhodecode_db_user.user_id,
407 410 commit_id=current_id,
408 411 f_path=self.request.POST.get('f_path'),
409 412 line_no=self.request.POST.get('line'),
410 413 status_change=(ChangesetStatus.get_status_lbl(status)
411 414 if status else None),
412 415 status_change_type=status,
413 416 comment_type=comment_type,
414 417 resolves_comment_id=resolves_comment_id,
415 418 auth_user=self._rhodecode_user
416 419 )
417 420
418 421 # get status if set !
419 422 if status:
420 423 # if latest status was from pull request and it's closed
421 424 # disallow changing status !
422 425 # dont_allow_on_closed_pull_request = True !
423 426
424 427 try:
425 428 ChangesetStatusModel().set_status(
426 429 self.db_repo.repo_id,
427 430 status,
428 431 self._rhodecode_db_user.user_id,
429 432 comment,
430 433 revision=current_id,
431 434 dont_allow_on_closed_pull_request=True
432 435 )
433 436 except StatusChangeOnClosedPullRequestError:
434 437 msg = _('Changing the status of a commit associated with '
435 438 'a closed pull request is not allowed')
436 439 log.exception(msg)
437 440 h.flash(msg, category='warning')
438 441 raise HTTPFound(h.route_path(
439 442 'repo_commit', repo_name=self.db_repo_name,
440 443 commit_id=current_id))
441 444
442 445 commit = self.db_repo.get_commit(current_id)
443 446 CommentsModel().trigger_commit_comment_hook(
444 447 self.db_repo, self._rhodecode_user, 'create',
445 448 data={'comment': comment, 'commit': commit})
446 449
447 450 # finalize, commit and redirect
448 451 Session().commit()
449 452
450 453 data = {
451 454 'target_id': h.safeid(h.safe_unicode(
452 455 self.request.POST.get('f_path'))),
453 456 }
454 457 if comment:
455 458 c.co = comment
456 459 c.at_version_num = 0
457 460 rendered_comment = render(
458 461 'rhodecode:templates/changeset/changeset_comment_block.mako',
459 462 self._get_template_context(c), self.request)
460 463
461 464 data.update(comment.get_dict())
462 465 data.update({'rendered_text': rendered_comment})
463 466
464 467 return data
465 468
466 469 @LoginRequired()
467 470 @NotAnonymous()
468 471 @HasRepoPermissionAnyDecorator(
469 472 'repository.read', 'repository.write', 'repository.admin')
470 473 @CSRFRequired()
471 474 @view_config(
472 475 route_name='repo_commit_comment_preview', request_method='POST',
473 476 renderer='string', xhr=True)
474 477 def repo_commit_comment_preview(self):
475 478 # Technically a CSRF token is not needed as no state changes with this
476 479 # call. However, as this is a POST is better to have it, so automated
477 480 # tools don't flag it as potential CSRF.
478 481 # Post is required because the payload could be bigger than the maximum
479 482 # allowed by GET.
480 483
481 484 text = self.request.POST.get('text')
482 485 renderer = self.request.POST.get('renderer') or 'rst'
483 486 if text:
484 487 return h.render(text, renderer=renderer, mentions=True,
485 488 repo_name=self.db_repo_name)
486 489 return ''
487 490
488 491 @LoginRequired()
489 492 @HasRepoPermissionAnyDecorator(
490 493 'repository.read', 'repository.write', 'repository.admin')
491 494 @CSRFRequired()
492 495 @view_config(
493 496 route_name='repo_commit_comment_history_view', request_method='POST',
494 497 renderer='string', xhr=True)
495 498 def repo_commit_comment_history_view(self):
496 499 c = self.load_default_context()
497 500
498 501 comment_history_id = self.request.matchdict['comment_history_id']
499 502 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
500 503 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
501 504
502 505 if is_repo_comment:
503 506 c.comment_history = comment_history
504 507
505 508 rendered_comment = render(
506 509 'rhodecode:templates/changeset/comment_history.mako',
507 510 self._get_template_context(c)
508 511 , self.request)
509 512 return rendered_comment
510 513 else:
511 514 log.warning('No permissions for user %s to show comment_history_id: %s',
512 515 self._rhodecode_db_user, comment_history_id)
513 516 raise HTTPNotFound()
514 517
515 518 @LoginRequired()
516 519 @NotAnonymous()
517 520 @HasRepoPermissionAnyDecorator(
518 521 'repository.read', 'repository.write', 'repository.admin')
519 522 @CSRFRequired()
520 523 @view_config(
521 524 route_name='repo_commit_comment_attachment_upload', request_method='POST',
522 525 renderer='json_ext', xhr=True)
523 526 def repo_commit_comment_attachment_upload(self):
524 527 c = self.load_default_context()
525 528 upload_key = 'attachment'
526 529
527 530 file_obj = self.request.POST.get(upload_key)
528 531
529 532 if file_obj is None:
530 533 self.request.response.status = 400
531 534 return {'store_fid': None,
532 535 'access_path': None,
533 536 'error': '{} data field is missing'.format(upload_key)}
534 537
535 538 if not hasattr(file_obj, 'filename'):
536 539 self.request.response.status = 400
537 540 return {'store_fid': None,
538 541 'access_path': None,
539 542 'error': 'filename cannot be read from the data field'}
540 543
541 544 filename = file_obj.filename
542 545 file_display_name = filename
543 546
544 547 metadata = {
545 548 'user_uploaded': {'username': self._rhodecode_user.username,
546 549 'user_id': self._rhodecode_user.user_id,
547 550 'ip': self._rhodecode_user.ip_addr}}
548 551
549 552 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
550 553 allowed_extensions = [
551 554 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
552 555 '.pptx', '.txt', '.xlsx', '.zip']
553 556 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
554 557
555 558 try:
556 559 storage = store_utils.get_file_storage(self.request.registry.settings)
557 560 store_uid, metadata = storage.save_file(
558 561 file_obj.file, filename, extra_metadata=metadata,
559 562 extensions=allowed_extensions, max_filesize=max_file_size)
560 563 except FileNotAllowedException:
561 564 self.request.response.status = 400
562 565 permitted_extensions = ', '.join(allowed_extensions)
563 566 error_msg = 'File `{}` is not allowed. ' \
564 567 'Only following extensions are permitted: {}'.format(
565 568 filename, permitted_extensions)
566 569 return {'store_fid': None,
567 570 'access_path': None,
568 571 'error': error_msg}
569 572 except FileOverSizeException:
570 573 self.request.response.status = 400
571 574 limit_mb = h.format_byte_size_binary(max_file_size)
572 575 return {'store_fid': None,
573 576 'access_path': None,
574 577 'error': 'File {} is exceeding allowed limit of {}.'.format(
575 578 filename, limit_mb)}
576 579
577 580 try:
578 581 entry = FileStore.create(
579 582 file_uid=store_uid, filename=metadata["filename"],
580 583 file_hash=metadata["sha256"], file_size=metadata["size"],
581 584 file_display_name=file_display_name,
582 585 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
583 586 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
584 587 scope_repo_id=self.db_repo.repo_id
585 588 )
586 589 Session().add(entry)
587 590 Session().commit()
588 591 log.debug('Stored upload in DB as %s', entry)
589 592 except Exception:
590 593 log.exception('Failed to store file %s', filename)
591 594 self.request.response.status = 400
592 595 return {'store_fid': None,
593 596 'access_path': None,
594 597 'error': 'File {} failed to store in DB.'.format(filename)}
595 598
596 599 Session().commit()
597 600
598 601 return {
599 602 'store_fid': store_uid,
600 603 'access_path': h.route_path(
601 604 'download_file', fid=store_uid),
602 605 'fqn_access_path': h.route_url(
603 606 'download_file', fid=store_uid),
604 607 'repo_access_path': h.route_path(
605 608 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
606 609 'repo_fqn_access_path': h.route_url(
607 610 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
608 611 }
609 612
610 613 @LoginRequired()
611 614 @NotAnonymous()
612 615 @HasRepoPermissionAnyDecorator(
613 616 'repository.read', 'repository.write', 'repository.admin')
614 617 @CSRFRequired()
615 618 @view_config(
616 619 route_name='repo_commit_comment_delete', request_method='POST',
617 620 renderer='json_ext')
618 621 def repo_commit_comment_delete(self):
619 622 commit_id = self.request.matchdict['commit_id']
620 623 comment_id = self.request.matchdict['comment_id']
621 624
622 625 comment = ChangesetComment.get_or_404(comment_id)
623 626 if not comment:
624 627 log.debug('Comment with id:%s not found, skipping', comment_id)
625 628 # comment already deleted in another call probably
626 629 return True
627 630
628 631 if comment.immutable:
629 632 # don't allow deleting comments that are immutable
630 633 raise HTTPForbidden()
631 634
632 635 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
633 636 super_admin = h.HasPermissionAny('hg.admin')()
634 637 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
635 638 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
636 639 comment_repo_admin = is_repo_admin and is_repo_comment
637 640
638 641 if super_admin or comment_owner or comment_repo_admin:
639 642 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
640 643 Session().commit()
641 644 return True
642 645 else:
643 646 log.warning('No permissions for user %s to delete comment_id: %s',
644 647 self._rhodecode_db_user, comment_id)
645 648 raise HTTPNotFound()
646 649
647 650 @LoginRequired()
648 651 @NotAnonymous()
649 652 @HasRepoPermissionAnyDecorator(
650 653 'repository.read', 'repository.write', 'repository.admin')
651 654 @CSRFRequired()
652 655 @view_config(
653 656 route_name='repo_commit_comment_edit', request_method='POST',
654 657 renderer='json_ext')
655 658 def repo_commit_comment_edit(self):
656 659 self.load_default_context()
657 660
658 661 comment_id = self.request.matchdict['comment_id']
659 662 comment = ChangesetComment.get_or_404(comment_id)
660 663
661 664 if comment.immutable:
662 665 # don't allow deleting comments that are immutable
663 666 raise HTTPForbidden()
664 667
665 668 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
666 669 super_admin = h.HasPermissionAny('hg.admin')()
667 670 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
668 671 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
669 672 comment_repo_admin = is_repo_admin and is_repo_comment
670 673
671 674 if super_admin or comment_owner or comment_repo_admin:
672 675 text = self.request.POST.get('text')
673 676 version = self.request.POST.get('version')
674 677 if text == comment.text:
675 678 log.warning(
676 679 'Comment(repo): '
677 680 'Trying to create new version '
678 681 'with the same comment body {}'.format(
679 682 comment_id,
680 683 )
681 684 )
682 685 raise HTTPNotFound()
683 686
684 687 if version.isdigit():
685 688 version = int(version)
686 689 else:
687 690 log.warning(
688 691 'Comment(repo): Wrong version type {} {} '
689 692 'for comment {}'.format(
690 693 version,
691 694 type(version),
692 695 comment_id,
693 696 )
694 697 )
695 698 raise HTTPNotFound()
696 699
697 700 try:
698 701 comment_history = CommentsModel().edit(
699 702 comment_id=comment_id,
700 703 text=text,
701 704 auth_user=self._rhodecode_user,
702 705 version=version,
703 706 )
704 707 except CommentVersionMismatch:
705 708 raise HTTPConflict()
706 709
707 710 if not comment_history:
708 711 raise HTTPNotFound()
709 712
710 713 commit_id = self.request.matchdict['commit_id']
711 714 commit = self.db_repo.get_commit(commit_id)
712 715 CommentsModel().trigger_commit_comment_hook(
713 716 self.db_repo, self._rhodecode_user, 'edit',
714 717 data={'comment': comment, 'commit': commit})
715 718
716 719 Session().commit()
717 720 return {
718 721 'comment_history_id': comment_history.comment_history_id,
719 722 'comment_id': comment.comment_id,
720 723 'comment_version': comment_history.version,
721 724 'comment_author_username': comment_history.author.username,
722 725 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
723 726 'comment_created_on': h.age_component(comment_history.created_on,
724 727 time_is_local=True),
725 728 }
726 729 else:
727 730 log.warning('No permissions for user %s to edit comment_id: %s',
728 731 self._rhodecode_db_user, comment_id)
729 732 raise HTTPNotFound()
730 733
731 734 @LoginRequired()
732 735 @HasRepoPermissionAnyDecorator(
733 736 'repository.read', 'repository.write', 'repository.admin')
734 737 @view_config(
735 738 route_name='repo_commit_data', request_method='GET',
736 739 renderer='json_ext', xhr=True)
737 740 def repo_commit_data(self):
738 741 commit_id = self.request.matchdict['commit_id']
739 742 self.load_default_context()
740 743
741 744 try:
742 745 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
743 746 except CommitDoesNotExistError as e:
744 747 return EmptyCommit(message=str(e))
745 748
746 749 @LoginRequired()
747 750 @HasRepoPermissionAnyDecorator(
748 751 'repository.read', 'repository.write', 'repository.admin')
749 752 @view_config(
750 753 route_name='repo_commit_children', request_method='GET',
751 754 renderer='json_ext', xhr=True)
752 755 def repo_commit_children(self):
753 756 commit_id = self.request.matchdict['commit_id']
754 757 self.load_default_context()
755 758
756 759 try:
757 760 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
758 761 children = commit.children
759 762 except CommitDoesNotExistError:
760 763 children = []
761 764
762 765 result = {"results": children}
763 766 return result
764 767
765 768 @LoginRequired()
766 769 @HasRepoPermissionAnyDecorator(
767 770 'repository.read', 'repository.write', 'repository.admin')
768 771 @view_config(
769 772 route_name='repo_commit_parents', request_method='GET',
770 773 renderer='json_ext')
771 774 def repo_commit_parents(self):
772 775 commit_id = self.request.matchdict['commit_id']
773 776 self.load_default_context()
774 777
775 778 try:
776 779 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
777 780 parents = commit.parents
778 781 except CommitDoesNotExistError:
779 782 parents = []
780 783 result = {"results": parents}
781 784 return result
@@ -1,1175 +1,1184 b''
1 1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 var prButtonLockChecks = {
21 21 'compare': false,
22 22 'reviewers': false
23 23 };
24 24
25 25 /**
26 26 * lock button until all checks and loads are made. E.g reviewer calculation
27 27 * should prevent from submitting a PR
28 28 * @param lockEnabled
29 29 * @param msg
30 30 * @param scope
31 31 */
32 32 var prButtonLock = function(lockEnabled, msg, scope) {
33 33 scope = scope || 'all';
34 34 if (scope == 'all'){
35 35 prButtonLockChecks['compare'] = !lockEnabled;
36 36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 37 } else if (scope == 'compare') {
38 38 prButtonLockChecks['compare'] = !lockEnabled;
39 39 } else if (scope == 'reviewers'){
40 40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 41 }
42 42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 43 if (lockEnabled) {
44 44 $('#pr_submit').attr('disabled', 'disabled');
45 45 }
46 46 else if (checksMeet) {
47 47 $('#pr_submit').removeAttr('disabled');
48 48 }
49 49
50 50 if (msg) {
51 51 $('#pr_open_message').html(msg);
52 52 }
53 53 };
54 54
55 55
56 56 /**
57 57 Generate Title and Description for a PullRequest.
58 58 In case of 1 commits, the title and description is that one commit
59 59 in case of multiple commits, we iterate on them with max N number of commits,
60 60 and build description in a form
61 61 - commitN
62 62 - commitN+1
63 63 ...
64 64
65 65 Title is then constructed from branch names, or other references,
66 66 replacing '-' and '_' into spaces
67 67
68 68 * @param sourceRef
69 69 * @param elements
70 70 * @param limit
71 71 * @returns {*[]}
72 72 */
73 73 var getTitleAndDescription = function(sourceRefType, sourceRef, elements, limit) {
74 74 var title = '';
75 75 var desc = '';
76 76
77 77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 78 var rawMessage = value['message'];
79 79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 80 });
81 81 // only 1 commit, use commit message as title
82 82 if (elements.length === 1) {
83 83 var rawMessage = elements[0]['message'];
84 84 title = rawMessage.split('\n')[0];
85 85 }
86 86 else {
87 87 // use reference name
88 88 var normalizedRef = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter()
89 89 var refType = sourceRefType;
90 90 title = 'Changes from {0}: {1}'.format(refType, normalizedRef);
91 91 }
92 92
93 93 return [title, desc]
94 94 };
95 95
96 96
97 97 window.ReviewersController = function () {
98 98 var self = this;
99 99 this.$loadingIndicator = $('.calculate-reviewers');
100 100 this.$reviewRulesContainer = $('#review_rules');
101 101 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
102 102 this.$userRule = $('.pr-user-rule-container');
103 103 this.$reviewMembers = $('#review_members');
104 104 this.$observerMembers = $('#observer_members');
105 105
106 106 this.currentRequest = null;
107 107 this.diffData = null;
108 108 this.enabledRules = [];
109 109 // sync with db.py entries
110 110 this.ROLE_REVIEWER = 'reviewer';
111 111 this.ROLE_OBSERVER = 'observer'
112 112
113 113 //dummy handler, we might register our own later
114 114 this.diffDataHandler = function (data) {};
115 115
116 116 this.defaultForbidUsers = function () {
117 117 return [
118 118 {
119 119 'username': 'default',
120 120 'user_id': templateContext.default_user.user_id
121 121 }
122 122 ];
123 123 };
124 124
125 125 // init default forbidden users
126 126 this.forbidUsers = this.defaultForbidUsers();
127 127
128 128 this.hideReviewRules = function () {
129 129 self.$reviewRulesContainer.hide();
130 130 $(self.$userRule.selector).hide();
131 131 };
132 132
133 133 this.showReviewRules = function () {
134 134 self.$reviewRulesContainer.show();
135 135 $(self.$userRule.selector).show();
136 136 };
137 137
138 138 this.addRule = function (ruleText) {
139 139 self.showReviewRules();
140 140 self.enabledRules.push(ruleText);
141 141 return '<div>- {0}</div>'.format(ruleText)
142 142 };
143 143
144 144 this.increaseCounter = function(role) {
145 145 if (role === self.ROLE_REVIEWER) {
146 146 var $elem = $('#reviewers-cnt')
147 147 var cnt = parseInt($elem.data('count') || 0)
148 148 cnt +=1
149 149 $elem.html(cnt);
150 150 $elem.data('count', cnt);
151 151 }
152 152 else if (role === self.ROLE_OBSERVER) {
153 153 var $elem = $('#observers-cnt');
154 154 var cnt = parseInt($elem.data('count') || 0)
155 155 cnt +=1
156 156 $elem.html(cnt);
157 157 $elem.data('count', cnt);
158 158 }
159 159 }
160 160
161 161 this.resetCounter = function () {
162 162 var $elem = $('#reviewers-cnt');
163 163
164 164 $elem.data('count', 0);
165 165 $elem.html(0);
166 166
167 167 var $elem = $('#observers-cnt');
168 168
169 169 $elem.data('count', 0);
170 170 $elem.html(0);
171 171 }
172 172
173 173 this.loadReviewRules = function (data) {
174 174 self.diffData = data;
175 175
176 176 // reset forbidden Users
177 177 this.forbidUsers = self.defaultForbidUsers();
178 178
179 179 // reset state of review rules
180 180 self.$rulesList.html('');
181 181
182 182 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
183 183 // default rule, case for older repo that don't have any rules stored
184 184 self.$rulesList.append(
185 185 self.addRule(
186 186 _gettext('All reviewers must vote.'))
187 187 );
188 188 return self.forbidUsers
189 189 }
190 190
191 191 if (data.rules.voting !== undefined) {
192 192 if (data.rules.voting < 0) {
193 193 self.$rulesList.append(
194 194 self.addRule(
195 195 _gettext('All individual reviewers must vote.'))
196 196 )
197 197 } else if (data.rules.voting === 1) {
198 198 self.$rulesList.append(
199 199 self.addRule(
200 200 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
201 201 )
202 202
203 203 } else {
204 204 self.$rulesList.append(
205 205 self.addRule(
206 206 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
207 207 )
208 208 }
209 209 }
210 210
211 211 if (data.rules.voting_groups !== undefined) {
212 212 $.each(data.rules.voting_groups, function (index, rule_data) {
213 213 self.$rulesList.append(
214 214 self.addRule(rule_data.text)
215 215 )
216 216 });
217 217 }
218 218
219 219 if (data.rules.use_code_authors_for_review) {
220 220 self.$rulesList.append(
221 221 self.addRule(
222 222 _gettext('Reviewers picked from source code changes.'))
223 223 )
224 224 }
225 225
226 226 if (data.rules.forbid_adding_reviewers) {
227 227 $('#add_reviewer_input').remove();
228 228 self.$rulesList.append(
229 229 self.addRule(
230 230 _gettext('Adding new reviewers is forbidden.'))
231 231 )
232 232 }
233 233
234 234 if (data.rules.forbid_author_to_review) {
235 235 self.forbidUsers.push(data.rules_data.pr_author);
236 236 self.$rulesList.append(
237 237 self.addRule(
238 238 _gettext('Author is not allowed to be a reviewer.'))
239 239 )
240 240 }
241 241
242 242 if (data.rules.forbid_commit_author_to_review) {
243 243
244 244 if (data.rules_data.forbidden_users) {
245 245 $.each(data.rules_data.forbidden_users, function (index, member_data) {
246 246 self.forbidUsers.push(member_data)
247 247 });
248 248 }
249 249
250 250 self.$rulesList.append(
251 251 self.addRule(
252 252 _gettext('Commit Authors are not allowed to be a reviewer.'))
253 253 )
254 254 }
255 255
256 256 // we don't have any rules set, so we inform users about it
257 257 if (self.enabledRules.length === 0) {
258 258 self.addRule(
259 259 _gettext('No review rules set.'))
260 260 }
261 261
262 262 return self.forbidUsers
263 263 };
264 264
265 265 this.emptyTables = function () {
266 266 self.emptyReviewersTable();
267 267 self.emptyObserversTable();
268 268
269 269 // Also reset counters.
270 270 self.resetCounter();
271 271 }
272 272
273 273 this.emptyReviewersTable = function (withText) {
274 274 self.$reviewMembers.empty();
275 275 if (withText !== undefined) {
276 276 self.$reviewMembers.html(withText)
277 277 }
278 278 };
279 279
280 280 this.emptyObserversTable = function (withText) {
281 281 self.$observerMembers.empty();
282 282 if (withText !== undefined) {
283 283 self.$observerMembers.html(withText)
284 284 }
285 285 }
286 286
287 287 this.loadDefaultReviewers = function (sourceRepo, sourceRef, targetRepo, targetRef) {
288 288
289 289 if (self.currentRequest) {
290 290 // make sure we cleanup old running requests before triggering this again
291 291 self.currentRequest.abort();
292 292 }
293 293
294 294 self.$loadingIndicator.show();
295 295
296 296 // reset reviewer/observe members
297 297 self.emptyTables();
298 298
299 299 prButtonLock(true, null, 'reviewers');
300 300 $('#user').hide(); // hide user autocomplete before load
301 301 $('#observer').hide(); //hide observer autocomplete before load
302 302
303 303 // lock PR button, so we cannot send PR before it's calculated
304 304 prButtonLock(true, _gettext('Loading diff ...'), 'compare');
305 305
306 306 if (sourceRef.length !== 3 || targetRef.length !== 3) {
307 307 // don't load defaults in case we're missing some refs...
308 308 self.$loadingIndicator.hide();
309 309 return
310 310 }
311 311
312 312 var url = pyroutes.url('repo_default_reviewers_data',
313 313 {
314 314 'repo_name': templateContext.repo_name,
315 315 'source_repo': sourceRepo,
316 316 'source_ref': sourceRef[2],
317 317 'target_repo': targetRepo,
318 318 'target_ref': targetRef[2]
319 319 });
320 320
321 321 self.currentRequest = $.ajax({
322 322 url: url,
323 323 headers: {'X-PARTIAL-XHR': true},
324 324 type: 'GET',
325 325 success: function (data) {
326 326
327 327 self.currentRequest = null;
328 328
329 329 // review rules
330 330 self.loadReviewRules(data);
331 331 self.handleDiffData(data["diff_info"]);
332 332
333 333 for (var i = 0; i < data.reviewers.length; i++) {
334 334 var reviewer = data.reviewers[i];
335 335 // load reviewer rules from the repo data
336 336 self.addMember(reviewer, reviewer.reasons, reviewer.mandatory, reviewer.role);
337 337 }
338 338
339 339
340 340 self.$loadingIndicator.hide();
341 341 prButtonLock(false, null, 'reviewers');
342 342
343 343 $('#user').show(); // show user autocomplete before load
344 344 $('#observer').show(); // show observer autocomplete before load
345 345
346 346 var commitElements = data["diff_info"]['commits'];
347 347
348 348 if (commitElements.length === 0) {
349 349 var noCommitsMsg = '<span class="alert-text-warning">{0}</span>'.format(
350 350 _gettext('There are no commits to merge.'));
351 351 prButtonLock(true, noCommitsMsg, 'all');
352 352
353 353 } else {
354 354 // un-lock PR button, so we cannot send PR before it's calculated
355 355 prButtonLock(false, null, 'compare');
356 356 }
357 357
358 358 },
359 359 error: function (jqXHR, textStatus, errorThrown) {
360 360 var prefix = "Loading diff and reviewers/observers failed\n"
361 361 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
362 362 ajaxErrorSwal(message);
363 363 }
364 364 });
365 365
366 366 };
367 367
368 368 // check those, refactor
369 369 this.removeMember = function (reviewer_id, mark_delete) {
370 370 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
371 371
372 372 if (typeof (mark_delete) === undefined) {
373 373 mark_delete = false;
374 374 }
375 375
376 376 if (mark_delete === true) {
377 377 if (reviewer) {
378 378 // now delete the input
379 379 $('#reviewer_{0} input'.format(reviewer_id)).remove();
380 380 $('#reviewer_{0}_rules input'.format(reviewer_id)).remove();
381 381 // mark as to-delete
382 382 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
383 383 obj.addClass('to-delete');
384 384 obj.css({"text-decoration": "line-through", "opacity": 0.5});
385 385 }
386 386 } else {
387 387 $('#reviewer_{0}'.format(reviewer_id)).remove();
388 388 }
389 389 };
390 390
391 391 this.addMember = function (reviewer_obj, reasons, mandatory, role) {
392 392
393 393 var id = reviewer_obj.user_id;
394 394 var username = reviewer_obj.username;
395 395
396 396 reasons = reasons || [];
397 397 mandatory = mandatory || false;
398 398 role = role || self.ROLE_REVIEWER
399 399
400 400 // register current set IDS to check if we don't have this ID already in
401 401 // and prevent duplicates
402 402 var currentIds = [];
403 403
404 404 $.each($('.reviewer_entry'), function (index, value) {
405 405 currentIds.push($(value).data('reviewerUserId'))
406 406 })
407 407
408 408 var userAllowedReview = function (userId) {
409 409 var allowed = true;
410 410 $.each(self.forbidUsers, function (index, member_data) {
411 411 if (parseInt(userId) === member_data['user_id']) {
412 412 allowed = false;
413 413 return false // breaks the loop
414 414 }
415 415 });
416 416 return allowed
417 417 };
418 418
419 419 var userAllowed = userAllowedReview(id);
420 420
421 421 if (!userAllowed) {
422 422 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
423 423 } else {
424 424 // only add if it's not there
425 425 var alreadyReviewer = currentIds.indexOf(id) != -1;
426 426
427 427 if (alreadyReviewer) {
428 428 alert(_gettext('User `{0}` already in reviewers/observers').format(username));
429 429 } else {
430 430
431 431 var reviewerEntry = renderTemplate('reviewMemberEntry', {
432 432 'member': reviewer_obj,
433 433 'mandatory': mandatory,
434 434 'role': role,
435 435 'reasons': reasons,
436 436 'allowed_to_update': true,
437 437 'review_status': 'not_reviewed',
438 438 'review_status_label': _gettext('Not Reviewed'),
439 439 'user_group': reviewer_obj.user_group,
440 440 'create': true,
441 441 'rule_show': true,
442 442 })
443 443
444 444 if (role === self.ROLE_REVIEWER) {
445 445 $(self.$reviewMembers.selector).append(reviewerEntry);
446 446 self.increaseCounter(self.ROLE_REVIEWER);
447 447 $('#reviewer-empty-msg').remove()
448 448 }
449 449 else if (role === self.ROLE_OBSERVER) {
450 450 $(self.$observerMembers.selector).append(reviewerEntry);
451 451 self.increaseCounter(self.ROLE_OBSERVER);
452 452 $('#observer-empty-msg').remove();
453 453 }
454 454
455 455 tooltipActivate();
456 456 }
457 457 }
458 458
459 459 };
460 460
461 461 this.updateReviewers = function (repo_name, pull_request_id, role) {
462 462 if (role === 'reviewer') {
463 463 var postData = $('#reviewers input').serialize();
464 464 _updatePullRequest(repo_name, pull_request_id, postData);
465 465 } else if (role === 'observer') {
466 466 var postData = $('#observers input').serialize();
467 467 _updatePullRequest(repo_name, pull_request_id, postData);
468 468 }
469 469 };
470 470
471 471 this.handleDiffData = function (data) {
472 472 self.diffDataHandler(data)
473 473 }
474 474 };
475 475
476 476
477 477 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
478 478 var url = pyroutes.url(
479 479 'pullrequest_update',
480 480 {"repo_name": repo_name, "pull_request_id": pull_request_id});
481 481 if (typeof postData === 'string' ) {
482 482 postData += '&csrf_token=' + CSRF_TOKEN;
483 483 } else {
484 484 postData.csrf_token = CSRF_TOKEN;
485 485 }
486 486
487 487 var success = function(o) {
488 488 var redirectUrl = o['redirect_url'];
489 489 if (redirectUrl !== undefined && redirectUrl !== null && redirectUrl !== '') {
490 490 window.location = redirectUrl;
491 491 } else {
492 492 window.location.reload();
493 493 }
494 494 };
495 495
496 496 ajaxPOST(url, postData, success);
497 497 };
498 498
499 499 /**
500 500 * PULL REQUEST update commits
501 501 */
502 502 var updateCommits = function(repo_name, pull_request_id, force) {
503 503 var postData = {
504 504 'update_commits': true
505 505 };
506 506 if (force !== undefined && force === true) {
507 507 postData['force_refresh'] = true
508 508 }
509 509 _updatePullRequest(repo_name, pull_request_id, postData);
510 510 };
511 511
512 512
513 513 /**
514 514 * PULL REQUEST edit info
515 515 */
516 516 var editPullRequest = function(repo_name, pull_request_id, title, description, renderer) {
517 517 var url = pyroutes.url(
518 518 'pullrequest_update',
519 519 {"repo_name": repo_name, "pull_request_id": pull_request_id});
520 520
521 521 var postData = {
522 522 'title': title,
523 523 'description': description,
524 524 'description_renderer': renderer,
525 525 'edit_pull_request': true,
526 526 'csrf_token': CSRF_TOKEN
527 527 };
528 528 var success = function(o) {
529 529 window.location.reload();
530 530 };
531 531 ajaxPOST(url, postData, success);
532 532 };
533 533
534 534
535 535 /**
536 536 * autocomplete handler for reviewers/observers
537 537 */
538 538 var autoCompleteHandler = function (inputId, controller, role) {
539 539
540 540 return function (element, data) {
541 541 var mandatory = false;
542 542 var reasons = [_gettext('added manually by "{0}"').format(
543 543 templateContext.rhodecode_user.username)];
544 544
545 545 // add whole user groups
546 546 if (data.value_type == 'user_group') {
547 547 reasons.push(_gettext('member of "{0}"').format(data.value_display));
548 548
549 549 $.each(data.members, function (index, member_data) {
550 550 var reviewer = member_data;
551 551 reviewer['user_id'] = member_data['id'];
552 552 reviewer['gravatar_link'] = member_data['icon_link'];
553 553 reviewer['user_link'] = member_data['profile_link'];
554 554 reviewer['rules'] = [];
555 555 controller.addMember(reviewer, reasons, mandatory, role);
556 556 })
557 557 }
558 558 // add single user
559 559 else {
560 560 var reviewer = data;
561 561 reviewer['user_id'] = data['id'];
562 562 reviewer['gravatar_link'] = data['icon_link'];
563 563 reviewer['user_link'] = data['profile_link'];
564 564 reviewer['rules'] = [];
565 565 controller.addMember(reviewer, reasons, mandatory, role);
566 566 }
567 567
568 568 $(inputId).val('');
569 569 }
570 570 }
571 571
572 572 /**
573 573 * Reviewer autocomplete
574 574 */
575 575 var ReviewerAutoComplete = function (inputId, controller) {
576 576 var self = this;
577 577 self.controller = controller;
578 578 self.inputId = inputId;
579 579 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_REVIEWER);
580 580
581 581 $(inputId).autocomplete({
582 582 serviceUrl: pyroutes.url('user_autocomplete_data'),
583 583 minChars: 2,
584 584 maxHeight: 400,
585 585 deferRequestBy: 300, //miliseconds
586 586 showNoSuggestionNotice: true,
587 587 tabDisabled: true,
588 588 autoSelectFirst: true,
589 589 params: {
590 590 user_id: templateContext.rhodecode_user.user_id,
591 591 user_groups: true,
592 592 user_groups_expand: true,
593 593 skip_default_user: true
594 594 },
595 595 formatResult: autocompleteFormatResult,
596 596 lookupFilter: autocompleteFilterResult,
597 597 onSelect: handler
598 598 });
599 599 };
600 600
601 601 /**
602 602 * Observers autocomplete
603 603 */
604 604 var ObserverAutoComplete = function(inputId, controller) {
605 605 var self = this;
606 606 self.controller = controller;
607 607 self.inputId = inputId;
608 608 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_OBSERVER);
609 609
610 610 $(inputId).autocomplete({
611 611 serviceUrl: pyroutes.url('user_autocomplete_data'),
612 612 minChars: 2,
613 613 maxHeight: 400,
614 614 deferRequestBy: 300, //miliseconds
615 615 showNoSuggestionNotice: true,
616 616 tabDisabled: true,
617 617 autoSelectFirst: true,
618 618 params: {
619 619 user_id: templateContext.rhodecode_user.user_id,
620 620 user_groups: true,
621 621 user_groups_expand: true,
622 622 skip_default_user: true
623 623 },
624 624 formatResult: autocompleteFormatResult,
625 625 lookupFilter: autocompleteFilterResult,
626 626 onSelect: handler
627 627 });
628 628 }
629 629
630 630
631 631 window.VersionController = function () {
632 632 var self = this;
633 633 this.$verSource = $('input[name=ver_source]');
634 634 this.$verTarget = $('input[name=ver_target]');
635 635 this.$showVersionDiff = $('#show-version-diff');
636 636
637 637 this.adjustRadioSelectors = function (curNode) {
638 638 var getVal = function (item) {
639 639 if (item === 'latest') {
640 640 return Number.MAX_SAFE_INTEGER
641 641 }
642 642 else {
643 643 return parseInt(item)
644 644 }
645 645 };
646 646
647 647 var curVal = getVal($(curNode).val());
648 648 var cleared = false;
649 649
650 650 $.each(self.$verSource, function (index, value) {
651 651 var elVal = getVal($(value).val());
652 652
653 653 if (elVal > curVal) {
654 654 if ($(value).is(':checked')) {
655 655 cleared = true;
656 656 }
657 657 $(value).attr('disabled', 'disabled');
658 658 $(value).removeAttr('checked');
659 659 $(value).css({'opacity': 0.1});
660 660 }
661 661 else {
662 662 $(value).css({'opacity': 1});
663 663 $(value).removeAttr('disabled');
664 664 }
665 665 });
666 666
667 667 if (cleared) {
668 668 // if we unchecked an active, set the next one to same loc.
669 669 $(this.$verSource).filter('[value={0}]'.format(
670 670 curVal)).attr('checked', 'checked');
671 671 }
672 672
673 673 self.setLockAction(false,
674 674 $(curNode).data('verPos'),
675 675 $(this.$verSource).filter(':checked').data('verPos')
676 676 );
677 677 };
678 678
679 679
680 680 this.attachVersionListener = function () {
681 681 self.$verTarget.change(function (e) {
682 682 self.adjustRadioSelectors(this)
683 683 });
684 684 self.$verSource.change(function (e) {
685 685 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
686 686 });
687 687 };
688 688
689 689 this.init = function () {
690 690
691 691 var curNode = self.$verTarget.filter(':checked');
692 692 self.adjustRadioSelectors(curNode);
693 693 self.setLockAction(true);
694 694 self.attachVersionListener();
695 695
696 696 };
697 697
698 698 this.setLockAction = function (state, selectedVersion, otherVersion) {
699 699 var $showVersionDiff = this.$showVersionDiff;
700 700
701 701 if (state) {
702 702 $showVersionDiff.attr('disabled', 'disabled');
703 703 $showVersionDiff.addClass('disabled');
704 704 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
705 705 }
706 706 else {
707 707 $showVersionDiff.removeAttr('disabled');
708 708 $showVersionDiff.removeClass('disabled');
709 709
710 710 if (selectedVersion == otherVersion) {
711 711 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
712 712 } else {
713 713 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
714 714 }
715 715 }
716 716
717 717 };
718 718
719 719 this.showVersionDiff = function () {
720 720 var target = self.$verTarget.filter(':checked');
721 721 var source = self.$verSource.filter(':checked');
722 722
723 723 if (target.val() && source.val()) {
724 724 var params = {
725 725 'pull_request_id': templateContext.pull_request_data.pull_request_id,
726 726 'repo_name': templateContext.repo_name,
727 727 'version': target.val(),
728 728 'from_version': source.val()
729 729 };
730 730 window.location = pyroutes.url('pullrequest_show', params)
731 731 }
732 732
733 733 return false;
734 734 };
735 735
736 736 this.toggleVersionView = function (elem) {
737 737
738 738 if (this.$showVersionDiff.is(':visible')) {
739 739 $('.version-pr').hide();
740 740 this.$showVersionDiff.hide();
741 741 $(elem).html($(elem).data('toggleOn'))
742 742 } else {
743 743 $('.version-pr').show();
744 744 this.$showVersionDiff.show();
745 745 $(elem).html($(elem).data('toggleOff'))
746 746 }
747 747
748 748 return false
749 749 };
750 750
751 751 };
752 752
753 753
754 754 window.UpdatePrController = function () {
755 755 var self = this;
756 756 this.$updateCommits = $('#update_commits');
757 757 this.$updateCommitsSwitcher = $('#update_commits_switcher');
758 758
759 759 this.lockUpdateButton = function (label) {
760 760 self.$updateCommits.attr('disabled', 'disabled');
761 761 self.$updateCommitsSwitcher.attr('disabled', 'disabled');
762 762
763 763 self.$updateCommits.addClass('disabled');
764 764 self.$updateCommitsSwitcher.addClass('disabled');
765 765
766 766 self.$updateCommits.removeClass('btn-primary');
767 767 self.$updateCommitsSwitcher.removeClass('btn-primary');
768 768
769 769 self.$updateCommits.text(_gettext(label));
770 770 };
771 771
772 772 this.isUpdateLocked = function () {
773 773 return self.$updateCommits.attr('disabled') !== undefined;
774 774 };
775 775
776 776 this.updateCommits = function (curNode) {
777 777 if (self.isUpdateLocked()) {
778 778 return
779 779 }
780 780 self.lockUpdateButton(_gettext('Updating...'));
781 781 updateCommits(
782 782 templateContext.repo_name,
783 783 templateContext.pull_request_data.pull_request_id);
784 784 };
785 785
786 786 this.forceUpdateCommits = function () {
787 787 if (self.isUpdateLocked()) {
788 788 return
789 789 }
790 790 self.lockUpdateButton(_gettext('Force updating...'));
791 791 var force = true;
792 792 updateCommits(
793 793 templateContext.repo_name,
794 794 templateContext.pull_request_data.pull_request_id, force);
795 795 };
796 796 };
797 797
798 798
799 799 /**
800 800 * Reviewer display panel
801 801 */
802 802 window.ReviewersPanel = {
803 803 editButton: null,
804 804 closeButton: null,
805 805 addButton: null,
806 806 removeButtons: null,
807 807 reviewRules: null,
808 808 setReviewers: null,
809 controller: null,
809 810
810 811 setSelectors: function () {
811 812 var self = this;
812 813 self.editButton = $('#open_edit_reviewers');
813 814 self.closeButton =$('#close_edit_reviewers');
814 815 self.addButton = $('#add_reviewer');
815 816 self.removeButtons = $('.reviewer_member_remove,.reviewer_member_mandatory_remove');
816 817 },
817 818
818 init: function (reviewRules, setReviewers) {
819 init: function (controller, reviewRules, setReviewers) {
819 820 var self = this;
820 821 self.setSelectors();
821 822
822 this.reviewRules = reviewRules;
823 this.setReviewers = setReviewers;
823 self.controller = controller;
824 self.reviewRules = reviewRules;
825 self.setReviewers = setReviewers;
824 826
825 this.editButton.on('click', function (e) {
827 self.editButton.on('click', function (e) {
826 828 self.edit();
827 829 });
828 this.closeButton.on('click', function (e) {
830 self.closeButton.on('click', function (e) {
829 831 self.close();
830 832 self.renderReviewers();
831 833 });
832 834
833 835 self.renderReviewers();
834 836
835 837 },
836 838
837 839 renderReviewers: function () {
838 if (this.setReviewers.reviewers === undefined) {
840 var self = this;
841
842 if (self.setReviewers.reviewers === undefined) {
839 843 return
840 844 }
841 if (this.setReviewers.reviewers.length === 0) {
842 reviewersController.emptyReviewersTable('<tr id="reviewer-empty-msg"><td colspan="6">No reviewers</td></tr>');
845 if (self.setReviewers.reviewers.length === 0) {
846 self.controller.emptyReviewersTable('<tr id="reviewer-empty-msg"><td colspan="6">No reviewers</td></tr>');
843 847 return
844 848 }
845 849
846 reviewersController.emptyReviewersTable();
850 self.controller.emptyReviewersTable();
847 851
848 $.each(this.setReviewers.reviewers, function (key, val) {
852 $.each(self.setReviewers.reviewers, function (key, val) {
849 853
850 854 var member = val;
851 if (member.role === reviewersController.ROLE_REVIEWER) {
855 if (member.role === self.controller.ROLE_REVIEWER) {
852 856 var entry = renderTemplate('reviewMemberEntry', {
853 857 'member': member,
854 858 'mandatory': member.mandatory,
855 859 'role': member.role,
856 860 'reasons': member.reasons,
857 861 'allowed_to_update': member.allowed_to_update,
858 862 'review_status': member.review_status,
859 863 'review_status_label': member.review_status_label,
860 864 'user_group': member.user_group,
861 865 'create': false
862 866 });
863 867
864 $(reviewersController.$reviewMembers.selector).append(entry)
868 $(self.controller.$reviewMembers.selector).append(entry)
865 869 }
866 870 });
867 871
868 872 tooltipActivate();
869 873 },
870 874
871 875 edit: function (event) {
872 this.editButton.hide();
873 this.closeButton.show();
874 this.addButton.show();
875 $(this.removeButtons.selector).css('visibility', 'visible');
876 var self = this;
877 self.editButton.hide();
878 self.closeButton.show();
879 self.addButton.show();
880 $(self.removeButtons.selector).css('visibility', 'visible');
876 881 // review rules
877 reviewersController.loadReviewRules(this.reviewRules);
882 self.controller.loadReviewRules(this.reviewRules);
878 883 },
879 884
880 885 close: function (event) {
886 var self = this;
881 887 this.editButton.show();
882 888 this.closeButton.hide();
883 889 this.addButton.hide();
884 890 $(this.removeButtons.selector).css('visibility', 'hidden');
885 891 // hide review rules
886 reviewersController.hideReviewRules();
892 self.controller.hideReviewRules();
887 893 }
888 894 };
889 895
890 896 /**
891 897 * Reviewer display panel
892 898 */
893 899 window.ObserversPanel = {
894 900 editButton: null,
895 901 closeButton: null,
896 902 addButton: null,
897 903 removeButtons: null,
898 904 reviewRules: null,
899 905 setReviewers: null,
906 controller: null,
900 907
901 908 setSelectors: function () {
902 909 var self = this;
903 910 self.editButton = $('#open_edit_observers');
904 911 self.closeButton =$('#close_edit_observers');
905 912 self.addButton = $('#add_observer');
906 913 self.removeButtons = $('.observer_member_remove,.observer_member_mandatory_remove');
907 914 },
908 915
909 init: function (reviewRules, setReviewers) {
916 init: function (controller, reviewRules, setReviewers) {
910 917 var self = this;
911 918 self.setSelectors();
912 919
913 this.reviewRules = reviewRules;
914 this.setReviewers = setReviewers;
920 self.controller = controller;
921 self.reviewRules = reviewRules;
922 self.setReviewers = setReviewers;
915 923
916 this.editButton.on('click', function (e) {
924 self.editButton.on('click', function (e) {
917 925 self.edit();
918 926 });
919 this.closeButton.on('click', function (e) {
927 self.closeButton.on('click', function (e) {
920 928 self.close();
921 929 self.renderObservers();
922 930 });
923 931
924 932 self.renderObservers();
925 933
926 934 },
927 935
928 936 renderObservers: function () {
929 if (this.setReviewers.observers === undefined) {
937 var self = this;
938 if (self.setReviewers.observers === undefined) {
930 939 return
931 940 }
932 if (this.setReviewers.observers.length === 0) {
933 reviewersController.emptyObserversTable('<tr id="observer-empty-msg"><td colspan="6">No observers</td></tr>');
941 if (self.setReviewers.observers.length === 0) {
942 self.controller.emptyObserversTable('<tr id="observer-empty-msg"><td colspan="6">No observers</td></tr>');
934 943 return
935 944 }
936 945
937 reviewersController.emptyObserversTable();
946 self.controller.emptyObserversTable();
938 947
939 $.each(this.setReviewers.observers, function (key, val) {
948 $.each(self.setReviewers.observers, function (key, val) {
940 949 var member = val;
941 if (member.role === reviewersController.ROLE_OBSERVER) {
950 if (member.role === self.controller.ROLE_OBSERVER) {
942 951 var entry = renderTemplate('reviewMemberEntry', {
943 952 'member': member,
944 953 'mandatory': member.mandatory,
945 954 'role': member.role,
946 955 'reasons': member.reasons,
947 956 'allowed_to_update': member.allowed_to_update,
948 957 'review_status': member.review_status,
949 958 'review_status_label': member.review_status_label,
950 959 'user_group': member.user_group,
951 960 'create': false
952 961 });
953 962
954 $(reviewersController.$observerMembers.selector).append(entry)
963 $(self.controller.$observerMembers.selector).append(entry)
955 964 }
956 965 });
957 966
958 967 tooltipActivate();
959 968 },
960 969
961 970 edit: function (event) {
962 971 this.editButton.hide();
963 972 this.closeButton.show();
964 973 this.addButton.show();
965 974 $(this.removeButtons.selector).css('visibility', 'visible');
966 975 },
967 976
968 977 close: function (event) {
969 978 this.editButton.show();
970 979 this.closeButton.hide();
971 980 this.addButton.hide();
972 981 $(this.removeButtons.selector).css('visibility', 'hidden');
973 982 }
974 983
975 984 };
976 985
977 986 window.PRDetails = {
978 987 editButton: null,
979 988 closeButton: null,
980 989 deleteButton: null,
981 990 viewFields: null,
982 991 editFields: null,
983 992
984 993 setSelectors: function () {
985 994 var self = this;
986 995 self.editButton = $('#open_edit_pullrequest')
987 996 self.closeButton = $('#close_edit_pullrequest')
988 997 self.deleteButton = $('#delete_pullrequest')
989 998 self.viewFields = $('#pr-desc, #pr-title')
990 999 self.editFields = $('#pr-desc-edit, #pr-title-edit, .pr-save')
991 1000 },
992 1001
993 1002 init: function () {
994 1003 var self = this;
995 1004 self.setSelectors();
996 1005 self.editButton.on('click', function (e) {
997 1006 self.edit();
998 1007 });
999 1008 self.closeButton.on('click', function (e) {
1000 1009 self.view();
1001 1010 });
1002 1011 },
1003 1012
1004 1013 edit: function (event) {
1005 1014 var cmInstance = $('#pr-description-input').get(0).MarkupForm.cm;
1006 1015 this.viewFields.hide();
1007 1016 this.editButton.hide();
1008 1017 this.deleteButton.hide();
1009 1018 this.closeButton.show();
1010 1019 this.editFields.show();
1011 1020 cmInstance.refresh();
1012 1021 },
1013 1022
1014 1023 view: function (event) {
1015 1024 this.editButton.show();
1016 1025 this.deleteButton.show();
1017 1026 this.editFields.hide();
1018 1027 this.closeButton.hide();
1019 1028 this.viewFields.show();
1020 1029 }
1021 1030 };
1022 1031
1023 1032 /**
1024 1033 * OnLine presence using channelstream
1025 1034 */
1026 1035 window.ReviewerPresenceController = function (channel) {
1027 1036 var self = this;
1028 1037 this.channel = channel;
1029 1038 this.users = {};
1030 1039
1031 1040 this.storeUsers = function (users) {
1032 1041 self.users = {}
1033 1042 $.each(users, function (index, value) {
1034 1043 var userId = value.state.id;
1035 1044 self.users[userId] = value.state;
1036 1045 })
1037 1046 }
1038 1047
1039 1048 this.render = function () {
1040 1049 $.each($('.reviewer_entry'), function (index, value) {
1041 1050 var userData = $(value).data();
1042 1051 if (self.users[userData.reviewerUserId] !== undefined) {
1043 1052 $(value).find('.presence-state').show();
1044 1053 } else {
1045 1054 $(value).find('.presence-state').hide();
1046 1055 }
1047 1056 })
1048 1057 };
1049 1058
1050 1059 this.handlePresence = function (data) {
1051 1060 if (data.type == 'presence' && data.channel === self.channel) {
1052 1061 this.storeUsers(data.users);
1053 1062 this.render()
1054 1063 }
1055 1064 };
1056 1065
1057 1066 this.handleChannelUpdate = function (data) {
1058 1067 if (data.channel === this.channel) {
1059 1068 this.storeUsers(data.state.users);
1060 1069 this.render()
1061 1070 }
1062 1071
1063 1072 };
1064 1073
1065 1074 /* subscribe to the current presence */
1066 1075 $.Topic('/connection_controller/presence').subscribe(this.handlePresence.bind(this));
1067 1076 /* subscribe to updates e.g connect/disconnect */
1068 1077 $.Topic('/connection_controller/channel_update').subscribe(this.handleChannelUpdate.bind(this));
1069 1078
1070 1079 };
1071 1080
1072 1081 window.refreshComments = function (version) {
1073 1082 version = version || templateContext.pull_request_data.pull_request_version || '';
1074 1083
1075 1084 // Pull request case
1076 1085 if (templateContext.pull_request_data.pull_request_id !== null) {
1077 1086 var params = {
1078 1087 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1079 1088 'repo_name': templateContext.repo_name,
1080 1089 'version': version,
1081 1090 };
1082 1091 var loadUrl = pyroutes.url('pullrequest_comments', params);
1083 1092 } // commit case
1084 1093 else {
1085 1094 return
1086 1095 }
1087 1096
1088 1097 var currentIDs = []
1089 1098 $.each($('.comment'), function (idx, element) {
1090 1099 currentIDs.push($(element).data('commentId'));
1091 1100 });
1092 1101 var data = {"comments": currentIDs};
1093 1102
1094 1103 var $targetElem = $('.comments-content-table');
1095 1104 $targetElem.css('opacity', 0.3);
1096 1105
1097 1106 var success = function (data) {
1098 1107 var $counterElem = $('#comments-count');
1099 1108 var newCount = $(data).data('counter');
1100 1109 if (newCount !== undefined) {
1101 1110 var callback = function () {
1102 1111 $counterElem.animate({'opacity': 1.00}, 200)
1103 1112 $counterElem.html(newCount);
1104 1113 };
1105 1114 $counterElem.animate({'opacity': 0.15}, 200, callback);
1106 1115 }
1107 1116
1108 1117 $targetElem.css('opacity', 1);
1109 1118 $targetElem.html(data);
1110 1119 tooltipActivate();
1111 1120 }
1112 1121
1113 1122 ajaxPOST(loadUrl, data, success, null, {})
1114 1123
1115 1124 }
1116 1125
1117 1126 window.refreshTODOs = function (version) {
1118 1127 version = version || templateContext.pull_request_data.pull_request_version || '';
1119 1128 // Pull request case
1120 1129 if (templateContext.pull_request_data.pull_request_id !== null) {
1121 1130 var params = {
1122 1131 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1123 1132 'repo_name': templateContext.repo_name,
1124 1133 'version': version,
1125 1134 };
1126 1135 var loadUrl = pyroutes.url('pullrequest_comments', params);
1127 1136 } // commit case
1128 1137 else {
1129 1138 return
1130 1139 }
1131 1140
1132 1141 var currentIDs = []
1133 1142 $.each($('.comment'), function (idx, element) {
1134 1143 currentIDs.push($(element).data('commentId'));
1135 1144 });
1136 1145
1137 1146 var data = {"comments": currentIDs};
1138 1147 var $targetElem = $('.todos-content-table');
1139 1148 $targetElem.css('opacity', 0.3);
1140 1149
1141 1150 var success = function (data) {
1142 1151 var $counterElem = $('#todos-count')
1143 1152 var newCount = $(data).data('counter');
1144 1153 if (newCount !== undefined) {
1145 1154 var callback = function () {
1146 1155 $counterElem.animate({'opacity': 1.00}, 200)
1147 1156 $counterElem.html(newCount);
1148 1157 };
1149 1158 $counterElem.animate({'opacity': 0.15}, 200, callback);
1150 1159 }
1151 1160
1152 1161 $targetElem.css('opacity', 1);
1153 1162 $targetElem.html(data);
1154 1163 tooltipActivate();
1155 1164 }
1156 1165
1157 1166 ajaxPOST(loadUrl, data, success, null, {})
1158 1167
1159 1168 }
1160 1169
1161 1170 window.refreshAllComments = function (version) {
1162 1171 version = version || templateContext.pull_request_data.pull_request_version || '';
1163 1172
1164 1173 refreshComments(version);
1165 1174 refreshTODOs(version);
1166 1175 };
1167 1176
1168 1177 window.sidebarComment = function (commentId) {
1169 1178 var jsonData = $('#commentHovercard{0}'.format(commentId)).data('commentJsonB64');
1170 1179 if (!jsonData) {
1171 1180 return 'Failed to load comment {0}'.format(commentId)
1172 1181 }
1173 1182 var funcData = JSON.parse(atob(jsonData));
1174 1183 return renderTemplate('sideBarCommentHovercard', funcData)
1175 1184 };
@@ -1,431 +1,432 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.mako"/>
4 4 <%namespace name="base" file="/base/base.mako"/>
5 5 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
6 6 <%namespace name="file_base" file="/files/base.mako"/>
7 7 <%namespace name="sidebar" file="/base/sidebar.mako"/>
8 8
9 9
10 10 <%def name="title()">
11 11 ${_('{} Commit').format(c.repo_name)} - ${h.show_id(c.commit)}
12 12 %if c.rhodecode_name:
13 13 &middot; ${h.branding(c.rhodecode_name)}
14 14 %endif
15 15 </%def>
16 16
17 17 <%def name="menu_bar_nav()">
18 18 ${self.menu_items(active='repositories')}
19 19 </%def>
20 20
21 21 <%def name="menu_bar_subnav()">
22 22 ${self.repo_menu(active='commits')}
23 23 </%def>
24 24
25 25 <%def name="main()">
26 26 <script type="text/javascript">
27 27 // TODO: marcink switch this to pyroutes
28 28 AJAX_COMMENT_DELETE_URL = "${h.route_path('repo_commit_comment_delete',repo_name=c.repo_name,commit_id=c.commit.raw_id,comment_id='__COMMENT_ID__')}";
29 29 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
30 30 </script>
31 31
32 32 <div class="box">
33 33
34 34 <div class="summary">
35 35
36 36 <div class="fieldset">
37 37 <div class="left-content">
38 38 <%
39 39 rc_user = h.discover_user(c.commit.author_email)
40 40 %>
41 41 <div class="left-content-avatar">
42 42 ${base.gravatar(c.commit.author_email, 30, tooltip=(True if rc_user else False), user=rc_user)}
43 43 </div>
44 44
45 45 <div class="left-content-message">
46 46 <div class="fieldset collapsable-content no-hide" data-toggle="summary-details">
47 47 <div class="commit truncate-wrap">${h.urlify_commit_message(h.chop_at_smart(c.commit.message, '\n', suffix_if_chopped='...'), c.repo_name)}</div>
48 48 </div>
49 49
50 50 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none">
51 51 <div class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
52 52 </div>
53 53
54 54 <div class="fieldset" data-toggle="summary-details">
55 55 <div class="">
56 56 <table>
57 57 <tr class="file_author">
58 58
59 59 <td>
60 60 <span class="user commit-author">${h.link_to_user(rc_user or c.commit.author)}</span>
61 61 <span class="commit-date">- ${h.age_component(c.commit.date)}</span>
62 62 </td>
63 63
64 64 <td>
65 65 ## second cell for consistency with files
66 66 </td>
67 67 </tr>
68 68 </table>
69 69 </div>
70 70 </div>
71 71
72 72 </div>
73 73 </div>
74 74
75 75 <div class="right-content">
76 76
77 77 <div data-toggle="summary-details">
78 78 <div class="tags tags-main">
79 79 <code><a href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">${h.show_id(c.commit)}</a></code>
80 80 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.commit.raw_id}" title="${_('Copy the full commit id')}"></i>
81 81 ${file_base.refs(c.commit)}
82 82
83 83 ## phase
84 84 % if hasattr(c.commit, 'phase') and getattr(c.commit, 'phase') != 'public':
85 85 <span class="tag phase-${c.commit.phase} tooltip" title="${_('Commit phase')}">
86 86 <i class="icon-info"></i>${c.commit.phase}
87 87 </span>
88 88 % endif
89 89
90 90 ## obsolete commits
91 91 % if getattr(c.commit, 'obsolete', False):
92 92 <span class="tag obsolete-${c.commit.obsolete} tooltip" title="${_('Evolve State')}">
93 93 ${_('obsolete')}
94 94 </span>
95 95 % endif
96 96
97 97 ## hidden commits
98 98 % if getattr(c.commit, 'hidden', False):
99 99 <span class="tag hidden-${c.commit.hidden} tooltip" title="${_('Evolve State')}">
100 100 ${_('hidden')}
101 101 </span>
102 102 % endif
103 103 </div>
104 104
105 105 <span id="parent_link" class="tag tagtag">
106 106 <a href="#parentCommit" title="${_('Parent Commit')}"><i class="icon-left icon-no-margin"></i>${_('parent')}</a>
107 107 </span>
108 108
109 109 <span id="child_link" class="tag tagtag">
110 110 <a href="#childCommit" title="${_('Child Commit')}">${_('child')}<i class="icon-right icon-no-margin"></i></a>
111 111 </span>
112 112
113 113 </div>
114 114
115 115 </div>
116 116 </div>
117 117
118 118 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
119 119 <div class="left-label-summary">
120 120 <p>${_('Diff options')}:</p>
121 121 <div class="right-label-summary">
122 122 <div class="diff-actions">
123 123 <a href="${h.route_path('repo_commit_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">
124 124 ${_('Raw Diff')}
125 125 </a>
126 126 |
127 127 <a href="${h.route_path('repo_commit_patch',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">
128 128 ${_('Patch Diff')}
129 129 </a>
130 130 |
131 131 <a href="${h.route_path('repo_commit_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(diff='download'))}">
132 132 ${_('Download Diff')}
133 133 </a>
134 134 </div>
135 135 </div>
136 136 </div>
137 137 </div>
138 138
139 139 <div class="clear-fix"></div>
140 140
141 141 <div class="btn-collapse" data-toggle="summary-details">
142 142 ${_('Show More')}
143 143 </div>
144 144
145 145 </div>
146 146
147 147 <div class="cs_files">
148 148 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
149 149 ${cbdiffs.render_diffset_menu(c.changes[c.commit.raw_id], commit=c.commit)}
150 150 ${cbdiffs.render_diffset(
151 151 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True,
152 152 inline_comments=c.inline_comments,
153 153 show_todos=False)}
154 154 </div>
155 155
156 156 ## template for inline comment form
157 157 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
158 158
159 159 ## comments heading with count
160 160 <div class="comments-heading">
161 161 <i class="icon-comment"></i>
162 162 ${_('General Comments')} ${len(c.comments)}
163 163 </div>
164 164
165 165 ## render comments
166 166 ${comment.generate_comments(c.comments)}
167 167
168 168 ## main comment form and it status
169 169 ${comment.comments(h.route_path('repo_commit_comment_create', repo_name=c.repo_name, commit_id=c.commit.raw_id),
170 170 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
171 171 </div>
172 172
173 173 ### NAV SIDEBAR
174 174 <aside class="right-sidebar right-sidebar-expanded" id="commit-nav-sticky" style="display: none">
175 175 <div class="sidenav navbar__inner" >
176 176 ## TOGGLE
177 177 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
178 178 <a href="#toggleSidebar" class="grey-link-action">
179 179
180 180 </a>
181 181 </div>
182 182
183 183 ## CONTENT
184 184 <div class="sidebar-content">
185 185
186 186 ## RULES SUMMARY/RULES
187 187 <div class="sidebar-element clear-both">
188 188 <% vote_title = _ungettext(
189 189 'Status calculated based on votes from {} reviewer',
190 190 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
191 191 %>
192 192
193 193 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
194 194 <i class="icon-circle review-status-${c.commit_review_status}"></i>
195 195 ${c.reviewers_count}
196 196 </div>
197 197 </div>
198 198
199 199 ## REVIEWERS
200 200 <div class="right-sidebar-expanded-state pr-details-title">
201 201 <span class="tooltip sidebar-heading" title="${vote_title}">
202 202 <i class="icon-circle review-status-${c.commit_review_status}"></i>
203 203 ${_('Reviewers')}
204 204 </span>
205 205 </div>
206 206
207 207 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
208 208
209 209 <table id="review_members" class="group_members">
210 210 ## This content is loaded via JS and ReviewersPanel
211 211 </table>
212 212
213 213 </div>
214 214
215 215 ## TODOs
216 216 <div class="sidebar-element clear-both">
217 217 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
218 218 <i class="icon-flag-filled"></i>
219 219 <span id="todos-count">${len(c.unresolved_comments)}</span>
220 220 </div>
221 221
222 222 <div class="right-sidebar-expanded-state pr-details-title">
223 223 ## Only show unresolved, that is only what matters
224 224 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
225 225 <i class="icon-flag-filled"></i>
226 226 TODOs
227 227 </span>
228 228
229 229 % if c.resolved_comments:
230 230 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return toggleElement(this, '.resolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
231 231 % else:
232 232 <span class="block-right last-item noselect">Show resolved</span>
233 233 % endif
234 234
235 235 </div>
236 236
237 237 <div class="right-sidebar-expanded-state pr-details-content">
238 238 % if c.unresolved_comments + c.resolved_comments:
239 239 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True, is_pr=False)}
240 240 % else:
241 241 <table>
242 242 <tr>
243 243 <td>
244 244 ${_('No TODOs yet')}
245 245 </td>
246 246 </tr>
247 247 </table>
248 248 % endif
249 249 </div>
250 250 </div>
251 251
252 252 ## COMMENTS
253 253 <div class="sidebar-element clear-both">
254 254 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
255 255 <i class="icon-comment" style="color: #949494"></i>
256 256 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
257 257 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
258 258 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
259 259 </div>
260 260
261 261 <div class="right-sidebar-expanded-state pr-details-title">
262 262 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
263 263 <i class="icon-comment" style="color: #949494"></i>
264 264 ${_('Comments')}
265 265 </span>
266 266
267 267 </div>
268 268
269 269 <div class="right-sidebar-expanded-state pr-details-content">
270 270 % if c.inline_comments_flat + c.comments:
271 271 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments), is_pr=False)}
272 272 % else:
273 273 <table>
274 274 <tr>
275 275 <td>
276 276 ${_('No Comments yet')}
277 277 </td>
278 278 </tr>
279 279 </table>
280 280 % endif
281 281 </div>
282 282
283 283 </div>
284 284
285 285 </div>
286 286
287 287 </div>
288 288 </aside>
289 289
290 290 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
291 291 <script type="text/javascript">
292 292 window.setReviewersData = ${c.commit_set_reviewers_data_json | n};
293 293
294 294 $(document).ready(function () {
295 295 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
296 296
297 297 if ($('#trimmed_message_box').height() === boxmax) {
298 298 $('#message_expand').show();
299 299 }
300 300
301 301 $('#message_expand').on('click', function (e) {
302 302 $('#trimmed_message_box').css('max-height', 'none');
303 303 $(this).hide();
304 304 });
305 305
306 306 $('.show-inline-comments').on('click', function (e) {
307 307 var boxid = $(this).attr('data-comment-id');
308 308 var button = $(this);
309 309
310 310 if (button.hasClass("comments-visible")) {
311 311 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
312 312 $(this).hide();
313 313 });
314 314 button.removeClass("comments-visible");
315 315 } else {
316 316 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
317 317 $(this).show();
318 318 });
319 319 button.addClass("comments-visible");
320 320 }
321 321 });
322 322
323 323 // next links
324 324 $('#child_link').on('click', function (e) {
325 325 // fetch via ajax what is going to be the next link, if we have
326 326 // >1 links show them to user to choose
327 327 if (!$('#child_link').hasClass('disabled')) {
328 328 $.ajax({
329 329 url: '${h.route_path('repo_commit_children',repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
330 330 success: function (data) {
331 331 if (data.results.length === 0) {
332 332 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
333 333 }
334 334 if (data.results.length === 1) {
335 335 var commit = data.results[0];
336 336 window.location = pyroutes.url('repo_commit', {
337 337 'repo_name': '${c.repo_name}',
338 338 'commit_id': commit.raw_id
339 339 });
340 340 } else if (data.results.length === 2) {
341 341 $('#child_link').addClass('disabled');
342 342 $('#child_link').addClass('double');
343 343
344 344 var _html = '';
345 345 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
346 346 .replace('__branch__', data.results[0].branch)
347 347 .replace('__rev__', 'r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0, 6)))
348 348 .replace('__title__', data.results[0].message)
349 349 .replace('__url__', pyroutes.url('repo_commit', {
350 350 'repo_name': '${c.repo_name}',
351 351 'commit_id': data.results[0].raw_id
352 352 }));
353 353 _html += ' | ';
354 354 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
355 355 .replace('__branch__', data.results[1].branch)
356 356 .replace('__rev__', 'r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0, 6)))
357 357 .replace('__title__', data.results[1].message)
358 358 .replace('__url__', pyroutes.url('repo_commit', {
359 359 'repo_name': '${c.repo_name}',
360 360 'commit_id': data.results[1].raw_id
361 361 }));
362 362 $('#child_link').html(_html);
363 363 }
364 364 }
365 365 });
366 366 e.preventDefault();
367 367 }
368 368 });
369 369
370 370 // prev links
371 371 $('#parent_link').on('click', function (e) {
372 372 // fetch via ajax what is going to be the next link, if we have
373 373 // >1 links show them to user to choose
374 374 if (!$('#parent_link').hasClass('disabled')) {
375 375 $.ajax({
376 376 url: '${h.route_path("repo_commit_parents",repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
377 377 success: function (data) {
378 378 if (data.results.length === 0) {
379 379 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
380 380 }
381 381 if (data.results.length === 1) {
382 382 var commit = data.results[0];
383 383 window.location = pyroutes.url('repo_commit', {
384 384 'repo_name': '${c.repo_name}',
385 385 'commit_id': commit.raw_id
386 386 });
387 387 } else if (data.results.length === 2) {
388 388 $('#parent_link').addClass('disabled');
389 389 $('#parent_link').addClass('double');
390 390
391 391 var _html = '';
392 392 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
393 393 .replace('__branch__', data.results[0].branch)
394 394 .replace('__rev__', 'r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0, 6)))
395 395 .replace('__title__', data.results[0].message)
396 396 .replace('__url__', pyroutes.url('repo_commit', {
397 397 'repo_name': '${c.repo_name}',
398 398 'commit_id': data.results[0].raw_id
399 399 }));
400 400 _html += ' | ';
401 401 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
402 402 .replace('__branch__', data.results[1].branch)
403 403 .replace('__rev__', 'r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0, 6)))
404 404 .replace('__title__', data.results[1].message)
405 405 .replace('__url__', pyroutes.url('repo_commit', {
406 406 'repo_name': '${c.repo_name}',
407 407 'commit_id': data.results[1].raw_id
408 408 }));
409 409 $('#parent_link').html(_html);
410 410 }
411 411 }
412 412 });
413 413 e.preventDefault();
414 414 }
415 415 });
416 416
417 417 // browse tree @ revision
418 418 $('#files_link').on('click', function (e) {
419 419 window.location = '${h.route_path('repo_files:default_path',repo_name=c.repo_name, commit_id=c.commit.raw_id)}';
420 420 e.preventDefault();
421 421 });
422 422
423 ReviewersPanel.init(null, setReviewersData);
423 reviewersController = new ReviewersController();
424 ReviewersPanel.init(reviewersController, null, setReviewersData);
424 425
425 426 var channel = '${c.commit_broadcast_channel}';
426 427 new ReviewerPresenceController(channel)
427 428
428 429 })
429 430 </script>
430 431
431 432 </%def>
@@ -1,994 +1,994 b''
1 1 <%inherit file="/base/base.mako"/>
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 4 <%namespace name="sidebar" file="/base/sidebar.mako"/>
5 5
6 6
7 7 <%def name="title()">
8 8 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
9 9 %if c.rhodecode_name:
10 10 &middot; ${h.branding(c.rhodecode_name)}
11 11 %endif
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()">
15 15
16 16 </%def>
17 17
18 18 <%def name="menu_bar_nav()">
19 19 ${self.menu_items(active='repositories')}
20 20 </%def>
21 21
22 22 <%def name="menu_bar_subnav()">
23 23 ${self.repo_menu(active='showpullrequest')}
24 24 </%def>
25 25
26 26
27 27 <%def name="main()">
28 28 ## Container to gather extracted Tickets
29 29 <%
30 30 c.referenced_commit_issues = []
31 31 c.referenced_desc_issues = []
32 32 %>
33 33
34 34 <script type="text/javascript">
35 35 // TODO: marcink switch this to pyroutes
36 36 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
37 37 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
38 38 templateContext.pull_request_data.pull_request_version = '${request.GET.get('version', '')}';
39 39 </script>
40 40
41 41 <div class="box">
42 42
43 43 <div class="box pr-summary">
44 44
45 45 <div class="summary-details block-left">
46 46 <div id="pr-title">
47 47 % if c.pull_request.is_closed():
48 48 <span class="pr-title-closed-tag tag">${_('Closed')}</span>
49 49 % endif
50 50 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${c.pull_request.title}">
51 51 </div>
52 52 <div id="pr-title-edit" class="input" style="display: none;">
53 53 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
54 54 </div>
55 55
56 56 <% summary = lambda n:{False:'summary-short'}.get(n) %>
57 57 <div class="pr-details-title">
58 58 <div class="pull-left">
59 59 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request !{}').format(c.pull_request.pull_request_id)}</a>
60 60 ${_('Created on')}
61 61 <span class="tooltip" title="${_('Last updated on')} ${h.format_date(c.pull_request.updated_on)}">${h.format_date(c.pull_request.created_on)},</span>
62 62 <span class="pr-details-title-author-pref">${_('by')}</span>
63 63 </div>
64 64
65 65 <div class="pull-left">
66 66 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
67 67 </div>
68 68
69 69 %if c.allowed_to_update:
70 70 <div class="pull-right">
71 71 <div id="edit_pull_request" class="action_button pr-save" style="display: none;">${_('Update title & description')}</div>
72 72 <div id="delete_pullrequest" class="action_button pr-save ${('' if c.allowed_to_delete else 'disabled' )}" style="display: none;">
73 73 % if c.allowed_to_delete:
74 74 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
75 75 <input class="btn btn-link btn-danger no-margin" id="remove_${c.pull_request.pull_request_id}" name="remove_${c.pull_request.pull_request_id}"
76 76 onclick="submitConfirm(event, this, _gettext('Confirm to delete this pull request'), _gettext('Delete'), '${'!{}'.format(c.pull_request.pull_request_id)}')"
77 77 type="submit" value="${_('Delete pull request')}">
78 78 ${h.end_form()}
79 79 % else:
80 80 <span class="tooltip" title="${_('Not allowed to delete this pull request')}">${_('Delete pull request')}</span>
81 81 % endif
82 82 </div>
83 83 <div id="open_edit_pullrequest" class="action_button">${_('Edit')}</div>
84 84 <div id="close_edit_pullrequest" class="action_button" style="display: none;">${_('Cancel')}</div>
85 85 </div>
86 86
87 87 %endif
88 88 </div>
89 89
90 90 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
91 91 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name, issues_container=c.referenced_desc_issues)}
92 92 </div>
93 93
94 94 <div id="pr-desc-edit" class="input textarea" style="display: none;">
95 95 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
96 96 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
97 97 </div>
98 98
99 99 <div id="summary" class="fields pr-details-content">
100 100
101 101 ## source
102 102 <div class="field">
103 103 <div class="label-pr-detail">
104 104 <label>${_('Commit flow')}:</label>
105 105 </div>
106 106 <div class="input">
107 107 <div class="pr-commit-flow">
108 108 ## Source
109 109 %if c.pull_request.source_ref_parts.type == 'branch':
110 110 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}"><code class="pr-source-info">${c.pull_request.source_ref_parts.type}:${c.pull_request.source_ref_parts.name}</code></a>
111 111 %else:
112 112 <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code>
113 113 %endif
114 114 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.repo_name}</a>
115 115 &rarr;
116 116 ## Target
117 117 %if c.pull_request.target_ref_parts.type == 'branch':
118 118 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}"><code class="pr-target-info">${c.pull_request.target_ref_parts.type}:${c.pull_request.target_ref_parts.name}</code></a>
119 119 %else:
120 120 <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
121 121 %endif
122 122
123 123 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.repo_name}</a>
124 124
125 125 <a class="source-details-action" href="#expand-source-details" onclick="return toggleElement(this, '.source-details')" data-toggle-on='<i class="icon-angle-down">more details</i>' data-toggle-off='<i class="icon-angle-up">less details</i>'>
126 126 <i class="icon-angle-down">more details</i>
127 127 </a>
128 128
129 129 </div>
130 130
131 131 <div class="source-details" style="display: none">
132 132
133 133 <ul>
134 134
135 135 ## common ancestor
136 136 <li>
137 137 ${_('Common ancestor')}:
138 138 % if c.ancestor_commit:
139 139 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a>
140 140 % else:
141 141 ${_('not available')}
142 142 % endif
143 143 </li>
144 144
145 145 ## pull url
146 146 <li>
147 147 %if h.is_hg(c.pull_request.source_repo):
148 148 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
149 149 %elif h.is_git(c.pull_request.source_repo):
150 150 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
151 151 %endif
152 152
153 153 <span>${_('Pull changes from source')}</span>: <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
154 154 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
155 155 </li>
156 156
157 157 ## Shadow repo
158 158 <li>
159 159 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
160 160 %if h.is_hg(c.pull_request.target_repo):
161 161 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
162 162 %elif h.is_git(c.pull_request.target_repo):
163 163 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
164 164 %endif
165 165
166 166 <span class="tooltip" title="${_('Clone repository in its merged state using shadow repository')}">${_('Clone from shadow repository')}</span>: <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
167 167 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
168 168
169 169 % else:
170 170 <div class="">
171 171 ${_('Shadow repository data not available')}.
172 172 </div>
173 173 % endif
174 174 </li>
175 175
176 176 </ul>
177 177
178 178 </div>
179 179
180 180 </div>
181 181
182 182 </div>
183 183
184 184 ## versions
185 185 <div class="field">
186 186 <div class="label-pr-detail">
187 187 <label>${_('Versions')}:</label>
188 188 </div>
189 189
190 190 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
191 191 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
192 192
193 193 <div class="pr-versions">
194 194 % if c.show_version_changes:
195 195 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
196 196 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
197 197 ${_ungettext('{} version available for this pull request, ', '{} versions available for this pull request, ', len(c.versions)).format(len(c.versions))}
198 198 <a id="show-pr-versions" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
199 199 data-toggle-on="${_('show versions')}."
200 200 data-toggle-off="${_('hide versions')}.">
201 201 ${_('show versions')}.
202 202 </a>
203 203 <table>
204 204 ## SHOW ALL VERSIONS OF PR
205 205 <% ver_pr = None %>
206 206
207 207 % for data in reversed(list(enumerate(c.versions, 1))):
208 208 <% ver_pos = data[0] %>
209 209 <% ver = data[1] %>
210 210 <% ver_pr = ver.pull_request_version_id %>
211 211 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
212 212
213 213 <tr class="version-pr" style="display: ${display_row}">
214 214 <td>
215 215 <code>
216 216 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
217 217 </code>
218 218 </td>
219 219 <td>
220 220 <input ${('checked="checked"' if c.from_version_index == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
221 221 <input ${('checked="checked"' if c.at_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
222 222 </td>
223 223 <td>
224 224 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
225 225 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
226 226
227 227 </td>
228 228 <td>
229 229 % if c.at_version_num != ver_pr:
230 230 <i class="tooltip icon-comment" title="${_('Comments from pull request version v{0}').format(ver_pos)}"></i>
231 231 <code>
232 232 General:${len(c.comment_versions[ver_pr]['at'])} / Inline:${len(c.inline_versions[ver_pr]['at'])}
233 233 </code>
234 234 % endif
235 235 </td>
236 236 <td>
237 237 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
238 238 </td>
239 239 <td>
240 240 <code>${h.age_component(ver.updated_on, time_is_local=True, tooltip=False)}</code>
241 241 </td>
242 242 </tr>
243 243 % endfor
244 244
245 245 <tr>
246 246 <td colspan="6">
247 247 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
248 248 data-label-text-locked="${_('select versions to show changes')}"
249 249 data-label-text-diff="${_('show changes between versions')}"
250 250 data-label-text-show="${_('show pull request for this version')}"
251 251 >
252 252 ${_('select versions to show changes')}
253 253 </button>
254 254 </td>
255 255 </tr>
256 256 </table>
257 257 % else:
258 258 <div>
259 259 ${_('Pull request versions not available')}.
260 260 </div>
261 261 % endif
262 262 </div>
263 263 </div>
264 264
265 265 </div>
266 266
267 267 </div>
268 268
269 269
270 270 </div>
271 271
272 272 </div>
273 273
274 274 <div class="box">
275 275
276 276 % if c.state_progressing:
277 277
278 278 <h2 style="text-align: center">
279 279 ${_('Cannot show diff when pull request state is changing. Current progress state')}: <span class="tag tag-merge-state-${c.pull_request.state}">${c.pull_request.state}</span>
280 280
281 281 % if c.is_super_admin:
282 282 <br/>
283 283 If you think this is an error try <a href="${h.current_route_path(request, force_state='created')}">forced state reset</a> to <span class="tag tag-merge-state-created">created</span> state.
284 284 % endif
285 285 </h2>
286 286
287 287 % else:
288 288
289 289 ## Diffs rendered here
290 290 <div class="table" >
291 291 <div id="changeset_compare_view_content">
292 292 ##CS
293 293 % if c.missing_requirements:
294 294 <div class="box">
295 295 <div class="alert alert-warning">
296 296 <div>
297 297 <strong>${_('Missing requirements:')}</strong>
298 298 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
299 299 </div>
300 300 </div>
301 301 </div>
302 302 % elif c.missing_commits:
303 303 <div class="box">
304 304 <div class="alert alert-warning">
305 305 <div>
306 306 <strong>${_('Missing commits')}:</strong>
307 307 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}<br/>
308 308 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}<br/>
309 309 ${_('Consider doing a `force update commits` in case you think this is an error.')}
310 310 </div>
311 311 </div>
312 312 </div>
313 313 % elif c.pr_merge_source_commit.changed and not c.pull_request.is_closed():
314 314 <div class="box">
315 315 <div class="alert alert-info">
316 316 <div>
317 317 <strong>${_('There are new changes for `{}:{}` in source repository, please consider updating this pull request.').format(c.pr_merge_source_commit.ref_spec.type, c.pr_merge_source_commit.ref_spec.name)}</strong>
318 318 </div>
319 319 </div>
320 320 </div>
321 321 % endif
322 322
323 323 <div class="compare_view_commits_title">
324 324 % if not c.compare_mode:
325 325
326 326 % if c.at_version_index:
327 327 <h4>
328 328 ${_('Showing changes at v{}, commenting is disabled.').format(c.at_version_index)}
329 329 </h4>
330 330 % endif
331 331
332 332 <div class="pull-left">
333 333 <div class="btn-group">
334 334 <a class="${('collapsed' if c.collapse_all_commits else '')}" href="#expand-commits" onclick="toggleCommitExpand(this); return false" data-toggle-commits-cnt=${len(c.commit_ranges)} >
335 335 % if c.collapse_all_commits:
336 336 <i class="icon-plus-squared-alt icon-no-margin"></i>
337 337 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
338 338 % else:
339 339 <i class="icon-minus-squared-alt icon-no-margin"></i>
340 340 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
341 341 % endif
342 342 </a>
343 343 </div>
344 344 </div>
345 345
346 346 <div class="pull-right">
347 347 % if c.allowed_to_update and not c.pull_request.is_closed():
348 348
349 349 <div class="btn-group btn-group-actions">
350 350 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
351 351 ${_('Update commits')}
352 352 </a>
353 353
354 354 <a id="update_commits_switcher" class="tooltip btn btn-primary btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more update options')}">
355 355 <i class="icon-down"></i>
356 356 </a>
357 357
358 358 <div class="btn-action-switcher-container right-align" id="update-commits-switcher">
359 359 <ul class="btn-action-switcher" role="menu" style="min-width: 300px;">
360 360 <li>
361 361 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
362 362 ${_('Force update commits')}
363 363 </a>
364 364 <div class="action-help-block">
365 365 ${_('Update commits and force refresh this pull request.')}
366 366 </div>
367 367 </li>
368 368 </ul>
369 369 </div>
370 370 </div>
371 371
372 372 % else:
373 373 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
374 374 % endif
375 375
376 376 </div>
377 377 % endif
378 378 </div>
379 379
380 380 % if not c.missing_commits:
381 381 ## COMPARE RANGE DIFF MODE
382 382 % if c.compare_mode:
383 383 % if c.at_version:
384 384 <h4>
385 385 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_index, ver_to=c.at_version_index if c.at_version_index else 'latest')}:
386 386 </h4>
387 387
388 388 <div class="subtitle-compare">
389 389 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
390 390 </div>
391 391
392 392 <div class="container">
393 393 <table class="rctable compare_view_commits">
394 394 <tr>
395 395 <th></th>
396 396 <th>${_('Time')}</th>
397 397 <th>${_('Author')}</th>
398 398 <th>${_('Commit')}</th>
399 399 <th></th>
400 400 <th>${_('Description')}</th>
401 401 </tr>
402 402
403 403 % for c_type, commit in c.commit_changes:
404 404 % if c_type in ['a', 'r']:
405 405 <%
406 406 if c_type == 'a':
407 407 cc_title = _('Commit added in displayed changes')
408 408 elif c_type == 'r':
409 409 cc_title = _('Commit removed in displayed changes')
410 410 else:
411 411 cc_title = ''
412 412 %>
413 413 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
414 414 <td>
415 415 <div class="commit-change-indicator color-${c_type}-border">
416 416 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
417 417 ${c_type.upper()}
418 418 </div>
419 419 </div>
420 420 </td>
421 421 <td class="td-time">
422 422 ${h.age_component(commit.date)}
423 423 </td>
424 424 <td class="td-user">
425 425 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
426 426 </td>
427 427 <td class="td-hash">
428 428 <code>
429 429 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
430 430 r${commit.idx}:${h.short_id(commit.raw_id)}
431 431 </a>
432 432 ${h.hidden('revisions', commit.raw_id)}
433 433 </code>
434 434 </td>
435 435 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
436 436 <i class="icon-expand-linked"></i>
437 437 </td>
438 438 <td class="mid td-description">
439 439 <div class="log-container truncate-wrap">
440 440 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name, issues_container=c.referenced_commit_issues)}</div>
441 441 </div>
442 442 </td>
443 443 </tr>
444 444 % endif
445 445 % endfor
446 446 </table>
447 447 </div>
448 448
449 449 % endif
450 450
451 451 ## Regular DIFF
452 452 % else:
453 453 <%include file="/compare/compare_commits.mako" />
454 454 % endif
455 455
456 456 <div class="cs_files">
457 457 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
458 458
459 459 <%
460 460 pr_menu_data = {
461 461 'outdated_comm_count_ver': outdated_comm_count_ver,
462 462 'pull_request': c.pull_request
463 463 }
464 464 %>
465 465
466 466 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on, pull_request_menu=pr_menu_data)}
467 467
468 468 % if c.range_diff_on:
469 469 % for commit in c.commit_ranges:
470 470 ${cbdiffs.render_diffset(
471 471 c.changes[commit.raw_id],
472 472 commit=commit, use_comments=True,
473 473 collapse_when_files_over=5,
474 474 disable_new_comments=True,
475 475 deleted_files_comments=c.deleted_files_comments,
476 476 inline_comments=c.inline_comments,
477 477 pull_request_menu=pr_menu_data, show_todos=False)}
478 478 % endfor
479 479 % else:
480 480 ${cbdiffs.render_diffset(
481 481 c.diffset, use_comments=True,
482 482 collapse_when_files_over=30,
483 483 disable_new_comments=not c.allowed_to_comment,
484 484 deleted_files_comments=c.deleted_files_comments,
485 485 inline_comments=c.inline_comments,
486 486 pull_request_menu=pr_menu_data, show_todos=False)}
487 487 % endif
488 488
489 489 </div>
490 490 % else:
491 491 ## skipping commits we need to clear the view for missing commits
492 492 <div style="clear:both;"></div>
493 493 % endif
494 494
495 495 </div>
496 496 </div>
497 497
498 498 ## template for inline comment form
499 499 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
500 500
501 501 ## comments heading with count
502 502 <div class="comments-heading">
503 503 <i class="icon-comment"></i>
504 504 ${_('General Comments')} ${len(c.comments)}
505 505 </div>
506 506
507 507 ## render general comments
508 508 <div id="comment-tr-show">
509 509 % if general_outdated_comm_count_ver:
510 510 <div class="info-box">
511 511 % if general_outdated_comm_count_ver == 1:
512 512 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
513 513 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
514 514 % else:
515 515 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
516 516 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
517 517 % endif
518 518 </div>
519 519 % endif
520 520 </div>
521 521
522 522 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
523 523
524 524 % if not c.pull_request.is_closed():
525 525 ## main comment form and it status
526 526 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
527 527 pull_request_id=c.pull_request.pull_request_id),
528 528 c.pull_request_review_status,
529 529 is_pull_request=True, change_status=c.allowed_to_change_status)}
530 530
531 531 ## merge status, and merge action
532 532 <div class="pull-request-merge">
533 533 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
534 534 </div>
535 535
536 536 %endif
537 537
538 538 % endif
539 539 </div>
540 540
541 541
542 542 ### NAV SIDEBAR
543 543 <aside class="right-sidebar right-sidebar-expanded" id="pr-nav-sticky" style="display: none">
544 544 <div class="sidenav navbar__inner" >
545 545 ## TOGGLE
546 546 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
547 547 <a href="#toggleSidebar" class="grey-link-action">
548 548
549 549 </a>
550 550 </div>
551 551
552 552 ## CONTENT
553 553 <div class="sidebar-content">
554 554
555 555 ## RULES SUMMARY/RULES
556 556 <div class="sidebar-element clear-both">
557 557 <% vote_title = _ungettext(
558 558 'Status calculated based on votes from {} reviewer',
559 559 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
560 560 %>
561 561
562 562 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
563 563 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
564 564 ${c.reviewers_count}
565 565 </div>
566 566
567 567 ## REVIEW RULES
568 568 <div id="review_rules" style="display: none" class="">
569 569 <div class="right-sidebar-expanded-state pr-details-title">
570 570 <span class="sidebar-heading">
571 571 ${_('Reviewer rules')}
572 572 </span>
573 573
574 574 </div>
575 575 <div class="pr-reviewer-rules">
576 576 ## review rules will be appended here, by default reviewers logic
577 577 </div>
578 578 <input id="review_data" type="hidden" name="review_data" value="">
579 579 </div>
580 580
581 581 ## REVIEWERS
582 582 <div class="right-sidebar-expanded-state pr-details-title">
583 583 <span class="tooltip sidebar-heading" title="${vote_title}">
584 584 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
585 585 ${_('Reviewers')}
586 586 </span>
587 587 %if c.allowed_to_update:
588 588 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
589 589 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
590 590 %else:
591 591 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Show rules')}</span>
592 592 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
593 593 %endif
594 594 </div>
595 595
596 596 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
597 597
598 598 ## members redering block
599 599 <input type="hidden" name="__start__" value="review_members:sequence">
600 600
601 601 <table id="review_members" class="group_members">
602 602 ## This content is loaded via JS and ReviewersPanel
603 603 </table>
604 604
605 605 <input type="hidden" name="__end__" value="review_members:sequence">
606 606 ## end members redering block
607 607
608 608 %if not c.pull_request.is_closed():
609 609 <div id="add_reviewer" class="ac" style="display: none;">
610 610 %if c.allowed_to_update:
611 611 % if not c.forbid_adding_reviewers:
612 612 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px">
613 613 <input class="ac-input" id="user" name="user" placeholder="${_('Add reviewer or reviewer group')}" type="text" autocomplete="off">
614 614 <div id="reviewers_container"></div>
615 615 </div>
616 616 % endif
617 617 <div class="pull-right" style="margin-bottom: 15px">
618 618 <button data-role="reviewer" id="update_reviewers" class="btn btn-small no-margin">${_('Save Changes')}</button>
619 619 </div>
620 620 %endif
621 621 </div>
622 622 %endif
623 623 </div>
624 624 </div>
625 625
626 626 ## OBSERVERS
627 627 <div class="sidebar-element clear-both">
628 628 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Observers')}">
629 629 <i class="icon-circle-thin"></i>
630 630 ${c.observers_count}
631 631 </div>
632 632
633 633 <div class="right-sidebar-expanded-state pr-details-title">
634 634 <span class="sidebar-heading">
635 635 <i class="icon-circle-thin"></i>
636 636 ${_('Observers')}
637 637 </span>
638 638 %if c.allowed_to_update:
639 639 <span id="open_edit_observers" class="block-right action_button last-item">${_('Edit')}</span>
640 640 <span id="close_edit_observers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
641 641 %endif
642 642 </div>
643 643
644 644 <div id="observers" class="right-sidebar-expanded-state pr-details-content reviewers">
645 645 ## members redering block
646 646 <input type="hidden" name="__start__" value="observer_members:sequence">
647 647
648 648 <table id="observer_members" class="group_members">
649 649 ## This content is loaded via JS and ReviewersPanel
650 650 </table>
651 651
652 652 <input type="hidden" name="__end__" value="observer_members:sequence">
653 653 ## end members redering block
654 654
655 655 %if not c.pull_request.is_closed():
656 656 <div id="add_observer" class="ac" style="display: none;">
657 657 %if c.allowed_to_update:
658 658 % if not c.forbid_adding_reviewers or 1:
659 659 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px" >
660 660 <input class="ac-input" id="observer" name="observer" placeholder="${_('Add observer or observer group')}" type="text" autocomplete="off">
661 661 <div id="observers_container"></div>
662 662 </div>
663 663 % endif
664 664 <div class="pull-right" style="margin-bottom: 15px">
665 665 <button data-role="observer" id="update_observers" class="btn btn-small no-margin">${_('Save Changes')}</button>
666 666 </div>
667 667 %endif
668 668 </div>
669 669 %endif
670 670 </div>
671 671 </div>
672 672
673 673 ## TODOs
674 674 <div class="sidebar-element clear-both">
675 675 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
676 676 <i class="icon-flag-filled"></i>
677 677 <span id="todos-count">${len(c.unresolved_comments)}</span>
678 678 </div>
679 679
680 680 <div class="right-sidebar-expanded-state pr-details-title">
681 681 ## Only show unresolved, that is only what matters
682 682 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
683 683 <i class="icon-flag-filled"></i>
684 684 TODOs
685 685 </span>
686 686
687 687 % if not c.at_version:
688 688 % if c.resolved_comments:
689 689 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return toggleElement(this, '.resolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
690 690 % else:
691 691 <span class="block-right last-item noselect">Show resolved</span>
692 692 % endif
693 693 % endif
694 694 </div>
695 695
696 696 <div class="right-sidebar-expanded-state pr-details-content">
697 697
698 698 % if c.at_version:
699 699 <table>
700 700 <tr>
701 701 <td class="unresolved-todo-text">${_('TODOs unavailable when browsing versions')}.</td>
702 702 </tr>
703 703 </table>
704 704 % else:
705 705 % if c.unresolved_comments + c.resolved_comments:
706 706 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True)}
707 707 % else:
708 708 <table>
709 709 <tr>
710 710 <td>
711 711 ${_('No TODOs yet')}
712 712 </td>
713 713 </tr>
714 714 </table>
715 715 % endif
716 716 % endif
717 717 </div>
718 718 </div>
719 719
720 720 ## COMMENTS
721 721 <div class="sidebar-element clear-both">
722 722 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
723 723 <i class="icon-comment" style="color: #949494"></i>
724 724 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
725 725 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
726 726 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
727 727 </div>
728 728
729 729 <div class="right-sidebar-expanded-state pr-details-title">
730 730 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
731 731 <i class="icon-comment" style="color: #949494"></i>
732 732 ${_('Comments')}
733 733
734 734 ## % if outdated_comm_count_ver:
735 735 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
736 736 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
737 737 ## </a>
738 738 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
739 739 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
740 740
741 741 ## % else:
742 742 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
743 743 ## % endif
744 744
745 745 </span>
746 746
747 747 % if outdated_comm_count_ver:
748 748 <span class="block-right action_button last-item noselect" onclick="return toggleElement(this, '.hidden-comment');" data-toggle-on="Show outdated" data-toggle-off="Hide outdated">Show outdated</span>
749 749 % else:
750 750 <span class="block-right last-item noselect">Show hidden</span>
751 751 % endif
752 752
753 753 </div>
754 754
755 755 <div class="right-sidebar-expanded-state pr-details-content">
756 756 % if c.inline_comments_flat + c.comments:
757 757 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
758 758 % else:
759 759 <table>
760 760 <tr>
761 761 <td>
762 762 ${_('No Comments yet')}
763 763 </td>
764 764 </tr>
765 765 </table>
766 766 % endif
767 767 </div>
768 768
769 769 </div>
770 770
771 771 ## Referenced Tickets
772 772 <div class="sidebar-element clear-both">
773 773 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Referenced Tickets')}">
774 774 <i class="icon-info-circled"></i>
775 775 ${(len(c.referenced_desc_issues) + len(c.referenced_commit_issues))}
776 776 </div>
777 777
778 778 <div class="right-sidebar-expanded-state pr-details-title">
779 779 <span class="sidebar-heading">
780 780 <i class="icon-info-circled"></i>
781 781 ${_('Referenced Tickets')}
782 782 </span>
783 783 </div>
784 784 <div class="right-sidebar-expanded-state pr-details-content">
785 785 <table>
786 786
787 787 <tr><td><code>${_('In pull request description')}:</code></td></tr>
788 788 % if c.referenced_desc_issues:
789 789 % for ticket_dict in c.referenced_desc_issues:
790 790 <tr>
791 791 <td>
792 792 <a href="${ticket_dict.get('url')}">
793 793 ${ticket_dict.get('id')}
794 794 </a>
795 795 </td>
796 796 </tr>
797 797 % endfor
798 798 % else:
799 799 <tr>
800 800 <td>
801 801 ${_('No Ticket data found.')}
802 802 </td>
803 803 </tr>
804 804 % endif
805 805
806 806 <tr><td style="padding-top: 10px"><code>${_('In commit messages')}:</code></td></tr>
807 807 % if c.referenced_commit_issues:
808 808 % for ticket_dict in c.referenced_commit_issues:
809 809 <tr>
810 810 <td>
811 811 <a href="${ticket_dict.get('url')}">
812 812 ${ticket_dict.get('id')}
813 813 </a>
814 814 </td>
815 815 </tr>
816 816 % endfor
817 817 % else:
818 818 <tr>
819 819 <td>
820 820 ${_('No Ticket data found.')}
821 821 </td>
822 822 </tr>
823 823 % endif
824 824 </table>
825 825
826 826 </div>
827 827 </div>
828 828
829 829 </div>
830 830
831 831 </div>
832 832 </aside>
833 833
834 834 ## This JS needs to be at the end
835 835 <script type="text/javascript">
836 836
837 837 versionController = new VersionController();
838 838 versionController.init();
839 839
840 840 reviewersController = new ReviewersController();
841 841 commitsController = new CommitsController();
842 842
843 843 updateController = new UpdatePrController();
844 844
845 845 window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
846 846 window.setReviewersData = ${c.pull_request_set_reviewers_data_json | n};
847 847 window.setObserversData = ${c.pull_request_set_observers_data_json | n};
848 848
849 849 (function () {
850 850 "use strict";
851 851
852 852 // custom code mirror
853 853 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
854 854
855 855 PRDetails.init();
856 ReviewersPanel.init(reviewerRulesData, setReviewersData);
857 ObserversPanel.init(reviewerRulesData, setObserversData);
856 ReviewersPanel.init(reviewersController, reviewerRulesData, setReviewersData);
857 ObserversPanel.init(reviewersController, reviewerRulesData, setObserversData);
858 858
859 859 window.showOutdated = function (self) {
860 860 $('.comment-inline.comment-outdated').show();
861 861 $('.filediff-outdated').show();
862 862 $('.showOutdatedComments').hide();
863 863 $('.hideOutdatedComments').show();
864 864 };
865 865
866 866 window.hideOutdated = function (self) {
867 867 $('.comment-inline.comment-outdated').hide();
868 868 $('.filediff-outdated').hide();
869 869 $('.hideOutdatedComments').hide();
870 870 $('.showOutdatedComments').show();
871 871 };
872 872
873 873 window.refreshMergeChecks = function () {
874 874 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
875 875 $('.pull-request-merge').css('opacity', 0.3);
876 876 $('.action-buttons-extra').css('opacity', 0.3);
877 877
878 878 $('.pull-request-merge').load(
879 879 loadUrl, function () {
880 880 $('.pull-request-merge').css('opacity', 1);
881 881
882 882 $('.action-buttons-extra').css('opacity', 1);
883 883 }
884 884 );
885 885 };
886 886
887 887 window.closePullRequest = function (status) {
888 888 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
889 889 return false;
890 890 }
891 891 // inject closing flag
892 892 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
893 893 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
894 894 $(generalCommentForm.submitForm).submit();
895 895 };
896 896
897 897 //TODO this functionality is now missing
898 898 $('#show-outdated-comments').on('click', function (e) {
899 899 var button = $(this);
900 900 var outdated = $('.comment-outdated');
901 901
902 902 if (button.html() === "(Show)") {
903 903 button.html("(Hide)");
904 904 outdated.show();
905 905 } else {
906 906 button.html("(Show)");
907 907 outdated.hide();
908 908 }
909 909 });
910 910
911 911 $('#merge_pull_request_form').submit(function () {
912 912 if (!$('#merge_pull_request').attr('disabled')) {
913 913 $('#merge_pull_request').attr('disabled', 'disabled');
914 914 }
915 915 return true;
916 916 });
917 917
918 918 $('#edit_pull_request').on('click', function (e) {
919 919 var title = $('#pr-title-input').val();
920 920 var description = codeMirrorInstance.getValue();
921 921 var renderer = $('#pr-renderer-input').val();
922 922 editPullRequest(
923 923 "${c.repo_name}", "${c.pull_request.pull_request_id}",
924 924 title, description, renderer);
925 925 });
926 926
927 927 var $updateButtons = $('#update_reviewers,#update_observers');
928 928 $updateButtons.on('click', function (e) {
929 929 var role = $(this).data('role');
930 930 $updateButtons.attr('disabled', 'disabled');
931 931 $updateButtons.addClass('disabled');
932 932 $updateButtons.html(_gettext('Saving...'));
933 933 reviewersController.updateReviewers(
934 934 templateContext.repo_name,
935 935 templateContext.pull_request_data.pull_request_id,
936 936 role
937 937 );
938 938 });
939 939
940 940 // fixing issue with caches on firefox
941 941 $('#update_commits').removeAttr("disabled");
942 942
943 943 $('.show-inline-comments').on('click', function (e) {
944 944 var boxid = $(this).attr('data-comment-id');
945 945 var button = $(this);
946 946
947 947 if (button.hasClass("comments-visible")) {
948 948 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
949 949 $(this).hide();
950 950 });
951 951 button.removeClass("comments-visible");
952 952 } else {
953 953 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
954 954 $(this).show();
955 955 });
956 956 button.addClass("comments-visible");
957 957 }
958 958 });
959 959
960 960 $('.show-inline-comments').on('change', function (e) {
961 961 var show = 'none';
962 962 var target = e.currentTarget;
963 963 if (target.checked) {
964 964 show = ''
965 965 }
966 966 var boxid = $(target).attr('id_for');
967 967 var comments = $('#{0} .inline-comments'.format(boxid));
968 968 var fn_display = function (idx) {
969 969 $(this).css('display', show);
970 970 };
971 971 $(comments).each(fn_display);
972 972 var btns = $('#{0} .inline-comments-button'.format(boxid));
973 973 $(btns).each(fn_display);
974 974 });
975 975
976 976 // register submit callback on commentForm form to track TODOs
977 977 window.commentFormGlobalSubmitSuccessCallback = function () {
978 978 refreshMergeChecks();
979 979 };
980 980
981 981 ReviewerAutoComplete('#user', reviewersController);
982 982 ObserverAutoComplete('#observer', reviewersController);
983 983
984 984 })();
985 985
986 986 $(document).ready(function () {
987 987
988 988 var channel = '${c.pr_broadcast_channel}';
989 989 new ReviewerPresenceController(channel)
990 990
991 991 })
992 992 </script>
993 993
994 994 </%def>
General Comments 0
You need to be logged in to leave comments. Login now