##// END OF EJS Templates
drafts: support draft in commits view...
milka -
r4555:57bb7bdc default
parent child Browse files
Show More
@@ -1,816 +1,852 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, channelstream
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 redirect_to_combined = str2bool(self.request.GET.get('redirect_combined'))
81 81
82 82 # fetch global flags of ignore ws or context lines
83 83 diff_context = get_diff_context(self.request)
84 84 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
85 85
86 86 # diff_limit will cut off the whole diff if the limit is applied
87 87 # otherwise it will just hide the big files from the front-end
88 88 diff_limit = c.visual.cut_off_limit_diff
89 89 file_limit = c.visual.cut_off_limit_file
90 90
91 91 # get ranges of commit ids if preset
92 92 commit_range = commit_id_range.split('...')[:2]
93 93
94 94 try:
95 95 pre_load = ['affected_files', 'author', 'branch', 'date',
96 96 'message', 'parents']
97 97 if self.rhodecode_vcs_repo.alias == 'hg':
98 98 pre_load += ['hidden', 'obsolete', 'phase']
99 99
100 100 if len(commit_range) == 2:
101 101 commits = self.rhodecode_vcs_repo.get_commits(
102 102 start_id=commit_range[0], end_id=commit_range[1],
103 103 pre_load=pre_load, translate_tags=False)
104 104 commits = list(commits)
105 105 else:
106 106 commits = [self.rhodecode_vcs_repo.get_commit(
107 107 commit_id=commit_id_range, pre_load=pre_load)]
108 108
109 109 c.commit_ranges = commits
110 110 if not c.commit_ranges:
111 111 raise RepositoryError('The commit range returned an empty result')
112 112 except CommitDoesNotExistError as e:
113 113 msg = _('No such commit exists. Org exception: `{}`').format(e)
114 114 h.flash(msg, category='error')
115 115 raise HTTPNotFound()
116 116 except Exception:
117 117 log.exception("General failure")
118 118 raise HTTPNotFound()
119 119 single_commit = len(c.commit_ranges) == 1
120 120
121 121 if redirect_to_combined and not single_commit:
122 122 source_ref = getattr(c.commit_ranges[0].parents[0]
123 123 if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id')
124 124 target_ref = c.commit_ranges[-1].raw_id
125 125 next_url = h.route_path(
126 126 'repo_compare',
127 127 repo_name=c.repo_name,
128 128 source_ref_type='rev',
129 129 source_ref=source_ref,
130 130 target_ref_type='rev',
131 131 target_ref=target_ref)
132 132 raise HTTPFound(next_url)
133 133
134 134 c.changes = OrderedDict()
135 135 c.lines_added = 0
136 136 c.lines_deleted = 0
137 137
138 138 # auto collapse if we have more than limit
139 139 collapse_limit = diffs.DiffProcessor._collapse_commits_over
140 140 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
141 141
142 142 c.commit_statuses = ChangesetStatus.STATUSES
143 143 c.inline_comments = []
144 144 c.files = []
145 145
146 146 c.comments = []
147 147 c.unresolved_comments = []
148 148 c.resolved_comments = []
149 149
150 150 # Single commit
151 151 if single_commit:
152 152 commit = c.commit_ranges[0]
153 153 c.comments = CommentsModel().get_comments(
154 154 self.db_repo.repo_id,
155 155 revision=commit.raw_id)
156 156
157 157 # comments from PR
158 158 statuses = ChangesetStatusModel().get_statuses(
159 159 self.db_repo.repo_id, commit.raw_id,
160 160 with_revisions=True)
161 161
162 162 prs = set()
163 163 reviewers = list()
164 164 reviewers_duplicates = set() # to not have duplicates from multiple votes
165 165 for c_status in statuses:
166 166
167 167 # extract associated pull-requests from votes
168 168 if c_status.pull_request:
169 169 prs.add(c_status.pull_request)
170 170
171 171 # extract reviewers
172 172 _user_id = c_status.author.user_id
173 173 if _user_id not in reviewers_duplicates:
174 174 reviewers.append(
175 175 StrictAttributeDict({
176 176 'user': c_status.author,
177 177
178 178 # fake attributed for commit, page that we don't have
179 179 # but we share the display with PR page
180 180 'mandatory': False,
181 181 'reasons': [],
182 182 'rule_user_group_data': lambda: None
183 183 })
184 184 )
185 185 reviewers_duplicates.add(_user_id)
186 186
187 187 c.reviewers_count = len(reviewers)
188 188 c.observers_count = 0
189 189
190 190 # from associated statuses, check the pull requests, and
191 191 # show comments from them
192 192 for pr in prs:
193 193 c.comments.extend(pr.comments)
194 194
195 195 c.unresolved_comments = CommentsModel()\
196 196 .get_commit_unresolved_todos(commit.raw_id)
197 197 c.resolved_comments = CommentsModel()\
198 198 .get_commit_resolved_todos(commit.raw_id)
199 199
200 200 c.inline_comments_flat = CommentsModel()\
201 201 .get_commit_inline_comments(commit.raw_id)
202 202
203 203 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
204 204 statuses, reviewers)
205 205
206 206 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
207 207
208 208 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
209 209
210 210 for review_obj, member, reasons, mandatory, status in review_statuses:
211 211 member_reviewer = h.reviewer_as_json(
212 212 member, reasons=reasons, mandatory=mandatory, role=None,
213 213 user_group=None
214 214 )
215 215
216 216 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
217 217 member_reviewer['review_status'] = current_review_status
218 218 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
219 219 member_reviewer['allowed_to_update'] = False
220 220 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
221 221
222 222 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
223 223
224 224 # NOTE(marcink): this uses the same voting logic as in pull-requests
225 225 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
226 226 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
227 227
228 228 diff = None
229 229 # Iterate over ranges (default commit view is always one commit)
230 230 for commit in c.commit_ranges:
231 231 c.changes[commit.raw_id] = []
232 232
233 233 commit2 = commit
234 234 commit1 = commit.first_parent
235 235
236 236 if method == 'show':
237 237 inline_comments = CommentsModel().get_inline_comments(
238 238 self.db_repo.repo_id, revision=commit.raw_id)
239 239 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
240 240 inline_comments))
241 241 c.inline_comments = inline_comments
242 242
243 243 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
244 244 self.db_repo)
245 245 cache_file_path = diff_cache_exist(
246 246 cache_path, 'diff', commit.raw_id,
247 247 hide_whitespace_changes, diff_context, c.fulldiff)
248 248
249 249 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
250 250 force_recache = str2bool(self.request.GET.get('force_recache'))
251 251
252 252 cached_diff = None
253 253 if caching_enabled:
254 254 cached_diff = load_cached_diff(cache_file_path)
255 255
256 256 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
257 257 if not force_recache and has_proper_diff_cache:
258 258 diffset = cached_diff['diff']
259 259 else:
260 260 vcs_diff = self.rhodecode_vcs_repo.get_diff(
261 261 commit1, commit2,
262 262 ignore_whitespace=hide_whitespace_changes,
263 263 context=diff_context)
264 264
265 265 diff_processor = diffs.DiffProcessor(
266 266 vcs_diff, format='newdiff', diff_limit=diff_limit,
267 267 file_limit=file_limit, show_full_diff=c.fulldiff)
268 268
269 269 _parsed = diff_processor.prepare()
270 270
271 271 diffset = codeblocks.DiffSet(
272 272 repo_name=self.db_repo_name,
273 273 source_node_getter=codeblocks.diffset_node_getter(commit1),
274 274 target_node_getter=codeblocks.diffset_node_getter(commit2))
275 275
276 276 diffset = self.path_filter.render_patchset_filtered(
277 277 diffset, _parsed, commit1.raw_id, commit2.raw_id)
278 278
279 279 # save cached diff
280 280 if caching_enabled:
281 281 cache_diff(cache_file_path, diffset, None)
282 282
283 283 c.limited_diff = diffset.limited_diff
284 284 c.changes[commit.raw_id] = diffset
285 285 else:
286 286 # TODO(marcink): no cache usage here...
287 287 _diff = self.rhodecode_vcs_repo.get_diff(
288 288 commit1, commit2,
289 289 ignore_whitespace=hide_whitespace_changes, context=diff_context)
290 290 diff_processor = diffs.DiffProcessor(
291 291 _diff, format='newdiff', diff_limit=diff_limit,
292 292 file_limit=file_limit, show_full_diff=c.fulldiff)
293 293 # downloads/raw we only need RAW diff nothing else
294 294 diff = self.path_filter.get_raw_patch(diff_processor)
295 295 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
296 296
297 297 # sort comments by how they were generated
298 298 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
299 299 c.at_version_num = None
300 300
301 301 if len(c.commit_ranges) == 1:
302 302 c.commit = c.commit_ranges[0]
303 303 c.parent_tmpl = ''.join(
304 304 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
305 305
306 306 if method == 'download':
307 307 response = Response(diff)
308 308 response.content_type = 'text/plain'
309 309 response.content_disposition = (
310 310 'attachment; filename=%s.diff' % commit_id_range[:12])
311 311 return response
312 312 elif method == 'patch':
313 313 c.diff = safe_unicode(diff)
314 314 patch = render(
315 315 'rhodecode:templates/changeset/patch_changeset.mako',
316 316 self._get_template_context(c), self.request)
317 317 response = Response(patch)
318 318 response.content_type = 'text/plain'
319 319 return response
320 320 elif method == 'raw':
321 321 response = Response(diff)
322 322 response.content_type = 'text/plain'
323 323 return response
324 324 elif method == 'show':
325 325 if len(c.commit_ranges) == 1:
326 326 html = render(
327 327 'rhodecode:templates/changeset/changeset.mako',
328 328 self._get_template_context(c), self.request)
329 329 return Response(html)
330 330 else:
331 331 c.ancestor = None
332 332 c.target_repo = self.db_repo
333 333 html = render(
334 334 'rhodecode:templates/changeset/changeset_range.mako',
335 335 self._get_template_context(c), self.request)
336 336 return Response(html)
337 337
338 338 raise HTTPBadRequest()
339 339
340 340 @LoginRequired()
341 341 @HasRepoPermissionAnyDecorator(
342 342 'repository.read', 'repository.write', 'repository.admin')
343 343 @view_config(
344 344 route_name='repo_commit', request_method='GET',
345 345 renderer=None)
346 346 def repo_commit_show(self):
347 347 commit_id = self.request.matchdict['commit_id']
348 348 return self._commit(commit_id, method='show')
349 349
350 350 @LoginRequired()
351 351 @HasRepoPermissionAnyDecorator(
352 352 'repository.read', 'repository.write', 'repository.admin')
353 353 @view_config(
354 354 route_name='repo_commit_raw', request_method='GET',
355 355 renderer=None)
356 356 @view_config(
357 357 route_name='repo_commit_raw_deprecated', request_method='GET',
358 358 renderer=None)
359 359 def repo_commit_raw(self):
360 360 commit_id = self.request.matchdict['commit_id']
361 361 return self._commit(commit_id, method='raw')
362 362
363 363 @LoginRequired()
364 364 @HasRepoPermissionAnyDecorator(
365 365 'repository.read', 'repository.write', 'repository.admin')
366 366 @view_config(
367 367 route_name='repo_commit_patch', request_method='GET',
368 368 renderer=None)
369 369 def repo_commit_patch(self):
370 370 commit_id = self.request.matchdict['commit_id']
371 371 return self._commit(commit_id, method='patch')
372 372
373 373 @LoginRequired()
374 374 @HasRepoPermissionAnyDecorator(
375 375 'repository.read', 'repository.write', 'repository.admin')
376 376 @view_config(
377 377 route_name='repo_commit_download', request_method='GET',
378 378 renderer=None)
379 379 def repo_commit_download(self):
380 380 commit_id = self.request.matchdict['commit_id']
381 381 return self._commit(commit_id, method='download')
382 382
383 @LoginRequired()
384 @NotAnonymous()
385 @HasRepoPermissionAnyDecorator(
386 'repository.read', 'repository.write', 'repository.admin')
387 @CSRFRequired()
388 @view_config(
389 route_name='repo_commit_comment_create', request_method='POST',
390 renderer='json_ext')
391 def repo_commit_comment_create(self):
383 def _commit_comments_create(self, commit_id, comments):
392 384 _ = self.request.translate
393 commit_id = self.request.matchdict['commit_id']
385 data = {}
386 if not comments:
387 return
388
389 commit = self.db_repo.get_commit(commit_id)
394 390
395 c = self.load_default_context()
396 status = self.request.POST.get('changeset_status', None)
397 is_draft = str2bool(self.request.POST.get('draft'))
398 text = self.request.POST.get('text')
399 comment_type = self.request.POST.get('comment_type')
400 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
401 f_path = self.request.POST.get('f_path')
402 line_no = self.request.POST.get('line')
403 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
391 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
392 for entry in comments:
393 c = self.load_default_context()
394 comment_type = entry['comment_type']
395 text = entry['text']
396 status = entry['status']
397 is_draft = str2bool(entry['is_draft'])
398 resolves_comment_id = entry['resolves_comment_id']
399 f_path = entry['f_path']
400 line_no = entry['line']
401 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
404 402
405 if status:
406 text = text or (_('Status change %(transition_icon)s %(status)s')
407 % {'transition_icon': '>',
408 'status': ChangesetStatus.get_status_lbl(status)})
403 if status:
404 text = text or (_('Status change %(transition_icon)s %(status)s')
405 % {'transition_icon': '>',
406 'status': ChangesetStatus.get_status_lbl(status)})
409 407
410 multi_commit_ids = []
411 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
412 if _commit_id not in ['', None, EmptyCommit.raw_id]:
413 if _commit_id not in multi_commit_ids:
414 multi_commit_ids.append(_commit_id)
415
416 commit_ids = multi_commit_ids or [commit_id]
417
418 data = {}
419 # Multiple comments for each passed commit id
420 for current_id in filter(None, commit_ids):
421 408 comment = CommentsModel().create(
422 409 text=text,
423 410 repo=self.db_repo.repo_id,
424 411 user=self._rhodecode_db_user.user_id,
425 commit_id=current_id,
412 commit_id=commit_id,
426 413 f_path=f_path,
427 414 line_no=line_no,
428 415 status_change=(ChangesetStatus.get_status_lbl(status)
429 416 if status else None),
430 417 status_change_type=status,
431 418 comment_type=comment_type,
432 419 is_draft=is_draft,
433 420 resolves_comment_id=resolves_comment_id,
434 421 auth_user=self._rhodecode_user,
435 422 send_email=not is_draft, # skip notification for draft comments
436 423 )
437 424 is_inline = comment.is_inline
438 425
439 426 # get status if set !
440 427 if status:
428 # `dont_allow_on_closed_pull_request = True` means
441 429 # if latest status was from pull request and it's closed
442 430 # disallow changing status !
443 # dont_allow_on_closed_pull_request = True !
444 431
445 432 try:
446 433 ChangesetStatusModel().set_status(
447 434 self.db_repo.repo_id,
448 435 status,
449 436 self._rhodecode_db_user.user_id,
450 437 comment,
451 revision=current_id,
438 revision=commit_id,
452 439 dont_allow_on_closed_pull_request=True
453 440 )
454 441 except StatusChangeOnClosedPullRequestError:
455 442 msg = _('Changing the status of a commit associated with '
456 443 'a closed pull request is not allowed')
457 444 log.exception(msg)
458 445 h.flash(msg, category='warning')
459 446 raise HTTPFound(h.route_path(
460 447 'repo_commit', repo_name=self.db_repo_name,
461 commit_id=current_id))
448 commit_id=commit_id))
449
450 Session().flush()
451 # this is somehow required to get access to some relationship
452 # loaded on comment
453 Session().refresh(comment)
462 454
463 455 # skip notifications for drafts
464 456 if not is_draft:
465 commit = self.db_repo.get_commit(current_id)
466 457 CommentsModel().trigger_commit_comment_hook(
467 458 self.db_repo, self._rhodecode_user, 'create',
468 459 data={'comment': comment, 'commit': commit})
469 460
470 461 comment_id = comment.comment_id
471 462 data[comment_id] = {
472 463 'target_id': target_elem_id
473 464 }
465 Session().flush()
466
474 467 c.co = comment
475 468 c.at_version_num = 0
476 469 c.is_new = True
477 470 rendered_comment = render(
478 471 'rhodecode:templates/changeset/changeset_comment_block.mako',
479 472 self._get_template_context(c), self.request)
480 473
481 474 data[comment_id].update(comment.get_dict())
482 475 data[comment_id].update({'rendered_text': rendered_comment})
483 476
484 # skip channelstream for draft comments
485 if not is_draft:
486 comment_broadcast_channel = channelstream.comment_channel(
487 self.db_repo_name, commit_obj=commit)
488
489 comment_data = data
490 posted_comment_type = 'inline' if is_inline else 'general'
491 channelstream.comment_channelstream_push(
492 self.request, comment_broadcast_channel, self._rhodecode_user,
493 _('posted a new {} comment').format(posted_comment_type),
494 comment_data=comment_data)
495
496 477 # finalize, commit and redirect
497 478 Session().commit()
498 479
480 # skip channelstream for draft comments
481 if not all_drafts:
482 comment_broadcast_channel = channelstream.comment_channel(
483 self.db_repo_name, commit_obj=commit)
484
485 comment_data = data
486 posted_comment_type = 'inline' if is_inline else 'general'
487 if len(data) == 1:
488 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
489 else:
490 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
491
492 channelstream.comment_channelstream_push(
493 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
494 comment_data=comment_data)
495
499 496 return data
500 497
501 498 @LoginRequired()
502 499 @NotAnonymous()
503 500 @HasRepoPermissionAnyDecorator(
504 501 'repository.read', 'repository.write', 'repository.admin')
505 502 @CSRFRequired()
506 503 @view_config(
504 route_name='repo_commit_comment_create', request_method='POST',
505 renderer='json_ext')
506 def repo_commit_comment_create(self):
507 _ = self.request.translate
508 commit_id = self.request.matchdict['commit_id']
509
510 multi_commit_ids = []
511 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
512 if _commit_id not in ['', None, EmptyCommit.raw_id]:
513 if _commit_id not in multi_commit_ids:
514 multi_commit_ids.append(_commit_id)
515
516 commit_ids = multi_commit_ids or [commit_id]
517
518 data = []
519 # Multiple comments for each passed commit id
520 for current_id in filter(None, commit_ids):
521 comment_data = {
522 'comment_type': self.request.POST.get('comment_type'),
523 'text': self.request.POST.get('text'),
524 'status': self.request.POST.get('changeset_status', None),
525 'is_draft': self.request.POST.get('draft'),
526 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
527 'close_pull_request': self.request.POST.get('close_pull_request'),
528 'f_path': self.request.POST.get('f_path'),
529 'line': self.request.POST.get('line'),
530 }
531 comment = self._commit_comments_create(commit_id=current_id, comments=[comment_data])
532 data.append(comment)
533
534 return data if len(data) > 1 else data[0]
535
536 @LoginRequired()
537 @NotAnonymous()
538 @HasRepoPermissionAnyDecorator(
539 'repository.read', 'repository.write', 'repository.admin')
540 @CSRFRequired()
541 @view_config(
507 542 route_name='repo_commit_comment_preview', request_method='POST',
508 543 renderer='string', xhr=True)
509 544 def repo_commit_comment_preview(self):
510 545 # Technically a CSRF token is not needed as no state changes with this
511 546 # call. However, as this is a POST is better to have it, so automated
512 547 # tools don't flag it as potential CSRF.
513 548 # Post is required because the payload could be bigger than the maximum
514 549 # allowed by GET.
515 550
516 551 text = self.request.POST.get('text')
517 552 renderer = self.request.POST.get('renderer') or 'rst'
518 553 if text:
519 554 return h.render(text, renderer=renderer, mentions=True,
520 555 repo_name=self.db_repo_name)
521 556 return ''
522 557
523 558 @LoginRequired()
524 559 @HasRepoPermissionAnyDecorator(
525 560 'repository.read', 'repository.write', 'repository.admin')
526 561 @CSRFRequired()
527 562 @view_config(
528 563 route_name='repo_commit_comment_history_view', request_method='POST',
529 564 renderer='string', xhr=True)
530 565 def repo_commit_comment_history_view(self):
531 566 c = self.load_default_context()
532 567
533 568 comment_history_id = self.request.matchdict['comment_history_id']
534 569 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
535 570 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
536 571
537 572 if is_repo_comment:
538 573 c.comment_history = comment_history
539 574
540 575 rendered_comment = render(
541 576 'rhodecode:templates/changeset/comment_history.mako',
542 577 self._get_template_context(c)
543 578 , self.request)
544 579 return rendered_comment
545 580 else:
546 581 log.warning('No permissions for user %s to show comment_history_id: %s',
547 582 self._rhodecode_db_user, comment_history_id)
548 583 raise HTTPNotFound()
549 584
550 585 @LoginRequired()
551 586 @NotAnonymous()
552 587 @HasRepoPermissionAnyDecorator(
553 588 'repository.read', 'repository.write', 'repository.admin')
554 589 @CSRFRequired()
555 590 @view_config(
556 591 route_name='repo_commit_comment_attachment_upload', request_method='POST',
557 592 renderer='json_ext', xhr=True)
558 593 def repo_commit_comment_attachment_upload(self):
559 594 c = self.load_default_context()
560 595 upload_key = 'attachment'
561 596
562 597 file_obj = self.request.POST.get(upload_key)
563 598
564 599 if file_obj is None:
565 600 self.request.response.status = 400
566 601 return {'store_fid': None,
567 602 'access_path': None,
568 603 'error': '{} data field is missing'.format(upload_key)}
569 604
570 605 if not hasattr(file_obj, 'filename'):
571 606 self.request.response.status = 400
572 607 return {'store_fid': None,
573 608 'access_path': None,
574 609 'error': 'filename cannot be read from the data field'}
575 610
576 611 filename = file_obj.filename
577 612 file_display_name = filename
578 613
579 614 metadata = {
580 615 'user_uploaded': {'username': self._rhodecode_user.username,
581 616 'user_id': self._rhodecode_user.user_id,
582 617 'ip': self._rhodecode_user.ip_addr}}
583 618
584 619 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
585 620 allowed_extensions = [
586 621 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
587 622 '.pptx', '.txt', '.xlsx', '.zip']
588 623 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
589 624
590 625 try:
591 626 storage = store_utils.get_file_storage(self.request.registry.settings)
592 627 store_uid, metadata = storage.save_file(
593 628 file_obj.file, filename, extra_metadata=metadata,
594 629 extensions=allowed_extensions, max_filesize=max_file_size)
595 630 except FileNotAllowedException:
596 631 self.request.response.status = 400
597 632 permitted_extensions = ', '.join(allowed_extensions)
598 633 error_msg = 'File `{}` is not allowed. ' \
599 634 'Only following extensions are permitted: {}'.format(
600 635 filename, permitted_extensions)
601 636 return {'store_fid': None,
602 637 'access_path': None,
603 638 'error': error_msg}
604 639 except FileOverSizeException:
605 640 self.request.response.status = 400
606 641 limit_mb = h.format_byte_size_binary(max_file_size)
607 642 return {'store_fid': None,
608 643 'access_path': None,
609 644 'error': 'File {} is exceeding allowed limit of {}.'.format(
610 645 filename, limit_mb)}
611 646
612 647 try:
613 648 entry = FileStore.create(
614 649 file_uid=store_uid, filename=metadata["filename"],
615 650 file_hash=metadata["sha256"], file_size=metadata["size"],
616 651 file_display_name=file_display_name,
617 652 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
618 653 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
619 654 scope_repo_id=self.db_repo.repo_id
620 655 )
621 656 Session().add(entry)
622 657 Session().commit()
623 658 log.debug('Stored upload in DB as %s', entry)
624 659 except Exception:
625 660 log.exception('Failed to store file %s', filename)
626 661 self.request.response.status = 400
627 662 return {'store_fid': None,
628 663 'access_path': None,
629 664 'error': 'File {} failed to store in DB.'.format(filename)}
630 665
631 666 Session().commit()
632 667
633 668 return {
634 669 'store_fid': store_uid,
635 670 'access_path': h.route_path(
636 671 'download_file', fid=store_uid),
637 672 'fqn_access_path': h.route_url(
638 673 'download_file', fid=store_uid),
639 674 'repo_access_path': h.route_path(
640 675 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
641 676 'repo_fqn_access_path': h.route_url(
642 677 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
643 678 }
644 679
645 680 @LoginRequired()
646 681 @NotAnonymous()
647 682 @HasRepoPermissionAnyDecorator(
648 683 'repository.read', 'repository.write', 'repository.admin')
649 684 @CSRFRequired()
650 685 @view_config(
651 686 route_name='repo_commit_comment_delete', request_method='POST',
652 687 renderer='json_ext')
653 688 def repo_commit_comment_delete(self):
654 689 commit_id = self.request.matchdict['commit_id']
655 690 comment_id = self.request.matchdict['comment_id']
656 691
657 692 comment = ChangesetComment.get_or_404(comment_id)
658 693 if not comment:
659 694 log.debug('Comment with id:%s not found, skipping', comment_id)
660 695 # comment already deleted in another call probably
661 696 return True
662 697
663 698 if comment.immutable:
664 699 # don't allow deleting comments that are immutable
665 700 raise HTTPForbidden()
666 701
667 702 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
668 703 super_admin = h.HasPermissionAny('hg.admin')()
669 704 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
670 705 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
671 706 comment_repo_admin = is_repo_admin and is_repo_comment
672 707
673 708 if super_admin or comment_owner or comment_repo_admin:
674 709 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
675 710 Session().commit()
676 711 return True
677 712 else:
678 713 log.warning('No permissions for user %s to delete comment_id: %s',
679 714 self._rhodecode_db_user, comment_id)
680 715 raise HTTPNotFound()
681 716
682 717 @LoginRequired()
683 718 @NotAnonymous()
684 719 @HasRepoPermissionAnyDecorator(
685 720 'repository.read', 'repository.write', 'repository.admin')
686 721 @CSRFRequired()
687 722 @view_config(
688 723 route_name='repo_commit_comment_edit', request_method='POST',
689 724 renderer='json_ext')
690 725 def repo_commit_comment_edit(self):
691 726 self.load_default_context()
692 727
728 commit_id = self.request.matchdict['commit_id']
693 729 comment_id = self.request.matchdict['comment_id']
694 730 comment = ChangesetComment.get_or_404(comment_id)
695 731
696 732 if comment.immutable:
697 733 # don't allow deleting comments that are immutable
698 734 raise HTTPForbidden()
699 735
700 736 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
701 737 super_admin = h.HasPermissionAny('hg.admin')()
702 738 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
703 739 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
704 740 comment_repo_admin = is_repo_admin and is_repo_comment
705 741
706 742 if super_admin or comment_owner or comment_repo_admin:
707 743 text = self.request.POST.get('text')
708 744 version = self.request.POST.get('version')
709 745 if text == comment.text:
710 746 log.warning(
711 747 'Comment(repo): '
712 748 'Trying to create new version '
713 749 'with the same comment body {}'.format(
714 750 comment_id,
715 751 )
716 752 )
717 753 raise HTTPNotFound()
718 754
719 755 if version.isdigit():
720 756 version = int(version)
721 757 else:
722 758 log.warning(
723 759 'Comment(repo): Wrong version type {} {} '
724 760 'for comment {}'.format(
725 761 version,
726 762 type(version),
727 763 comment_id,
728 764 )
729 765 )
730 766 raise HTTPNotFound()
731 767
732 768 try:
733 769 comment_history = CommentsModel().edit(
734 770 comment_id=comment_id,
735 771 text=text,
736 772 auth_user=self._rhodecode_user,
737 773 version=version,
738 774 )
739 775 except CommentVersionMismatch:
740 776 raise HTTPConflict()
741 777
742 778 if not comment_history:
743 779 raise HTTPNotFound()
744 780
745 commit_id = self.request.matchdict['commit_id']
746 commit = self.db_repo.get_commit(commit_id)
747 CommentsModel().trigger_commit_comment_hook(
748 self.db_repo, self._rhodecode_user, 'edit',
749 data={'comment': comment, 'commit': commit})
781 if not comment.draft:
782 commit = self.db_repo.get_commit(commit_id)
783 CommentsModel().trigger_commit_comment_hook(
784 self.db_repo, self._rhodecode_user, 'edit',
785 data={'comment': comment, 'commit': commit})
750 786
751 787 Session().commit()
752 788 return {
753 789 'comment_history_id': comment_history.comment_history_id,
754 790 'comment_id': comment.comment_id,
755 791 'comment_version': comment_history.version,
756 792 'comment_author_username': comment_history.author.username,
757 793 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
758 794 'comment_created_on': h.age_component(comment_history.created_on,
759 795 time_is_local=True),
760 796 }
761 797 else:
762 798 log.warning('No permissions for user %s to edit comment_id: %s',
763 799 self._rhodecode_db_user, comment_id)
764 800 raise HTTPNotFound()
765 801
766 802 @LoginRequired()
767 803 @HasRepoPermissionAnyDecorator(
768 804 'repository.read', 'repository.write', 'repository.admin')
769 805 @view_config(
770 806 route_name='repo_commit_data', request_method='GET',
771 807 renderer='json_ext', xhr=True)
772 808 def repo_commit_data(self):
773 809 commit_id = self.request.matchdict['commit_id']
774 810 self.load_default_context()
775 811
776 812 try:
777 813 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
778 814 except CommitDoesNotExistError as e:
779 815 return EmptyCommit(message=str(e))
780 816
781 817 @LoginRequired()
782 818 @HasRepoPermissionAnyDecorator(
783 819 'repository.read', 'repository.write', 'repository.admin')
784 820 @view_config(
785 821 route_name='repo_commit_children', request_method='GET',
786 822 renderer='json_ext', xhr=True)
787 823 def repo_commit_children(self):
788 824 commit_id = self.request.matchdict['commit_id']
789 825 self.load_default_context()
790 826
791 827 try:
792 828 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
793 829 children = commit.children
794 830 except CommitDoesNotExistError:
795 831 children = []
796 832
797 833 result = {"results": children}
798 834 return result
799 835
800 836 @LoginRequired()
801 837 @HasRepoPermissionAnyDecorator(
802 838 'repository.read', 'repository.write', 'repository.admin')
803 839 @view_config(
804 840 route_name='repo_commit_parents', request_method='GET',
805 841 renderer='json_ext')
806 842 def repo_commit_parents(self):
807 843 commit_id = self.request.matchdict['commit_id']
808 844 self.load_default_context()
809 845
810 846 try:
811 847 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
812 848 parents = commit.parents
813 849 except CommitDoesNotExistError:
814 850 parents = []
815 851 result = {"results": parents}
816 852 return result
@@ -1,1857 +1,1854 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-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 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 43 from rhodecode.lib.vcs.backends.base import (
44 44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 48 from rhodecode.model.comment import CommentsModel
49 49 from rhodecode.model.db import (
50 50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 51 PullRequestReviewers)
52 52 from rhodecode.model.forms import PullRequestForm
53 53 from rhodecode.model.meta import Session
54 54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 55 from rhodecode.model.scm import ScmModel
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61 61
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 66 # backward compat., we use for OLD PRs a plain renderer
67 67 c.renderer = 'plain'
68 68 return c
69 69
70 70 def _get_pull_requests_list(
71 71 self, repo_name, source, filter_type, opened_by, statuses):
72 72
73 73 draw, start, limit = self._extract_chunk(self.request)
74 74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 75 _render = self.request.get_partial_renderer(
76 76 'rhodecode:templates/data_table/_dt_elements.mako')
77 77
78 78 # pagination
79 79
80 80 if filter_type == 'awaiting_review':
81 81 pull_requests = PullRequestModel().get_awaiting_review(
82 82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
83 83 statuses=statuses, offset=start, length=limit,
84 84 order_by=order_by, order_dir=order_dir)
85 85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 86 repo_name, search_q=search_q, source=source, statuses=statuses,
87 87 opened_by=opened_by)
88 88 elif filter_type == 'awaiting_my_review':
89 89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
91 91 user_id=self._rhodecode_user.user_id, statuses=statuses,
92 92 offset=start, length=limit, order_by=order_by,
93 93 order_dir=order_dir)
94 94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
95 95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
96 96 statuses=statuses, opened_by=opened_by)
97 97 else:
98 98 pull_requests = PullRequestModel().get_all(
99 99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
100 100 statuses=statuses, offset=start, length=limit,
101 101 order_by=order_by, order_dir=order_dir)
102 102 pull_requests_total_count = PullRequestModel().count_all(
103 103 repo_name, search_q=search_q, source=source, statuses=statuses,
104 104 opened_by=opened_by)
105 105
106 106 data = []
107 107 comments_model = CommentsModel()
108 108 for pr in pull_requests:
109 109 comments_count = comments_model.get_all_comments(
110 110 self.db_repo.repo_id, pull_request=pr,
111 111 include_drafts=False, count_only=True)
112 112
113 113 data.append({
114 114 'name': _render('pullrequest_name',
115 115 pr.pull_request_id, pr.pull_request_state,
116 116 pr.work_in_progress, pr.target_repo.repo_name,
117 117 short=True),
118 118 'name_raw': pr.pull_request_id,
119 119 'status': _render('pullrequest_status',
120 120 pr.calculated_review_status()),
121 121 'title': _render('pullrequest_title', pr.title, pr.description),
122 122 'description': h.escape(pr.description),
123 123 'updated_on': _render('pullrequest_updated_on',
124 124 h.datetime_to_time(pr.updated_on)),
125 125 'updated_on_raw': h.datetime_to_time(pr.updated_on),
126 126 'created_on': _render('pullrequest_updated_on',
127 127 h.datetime_to_time(pr.created_on)),
128 128 'created_on_raw': h.datetime_to_time(pr.created_on),
129 129 'state': pr.pull_request_state,
130 130 'author': _render('pullrequest_author',
131 131 pr.author.full_contact, ),
132 132 'author_raw': pr.author.full_name,
133 133 'comments': _render('pullrequest_comments', comments_count),
134 134 'comments_raw': comments_count,
135 135 'closed': pr.is_closed(),
136 136 })
137 137
138 138 data = ({
139 139 'draw': draw,
140 140 'data': data,
141 141 'recordsTotal': pull_requests_total_count,
142 142 'recordsFiltered': pull_requests_total_count,
143 143 })
144 144 return data
145 145
146 146 @LoginRequired()
147 147 @HasRepoPermissionAnyDecorator(
148 148 'repository.read', 'repository.write', 'repository.admin')
149 149 @view_config(
150 150 route_name='pullrequest_show_all', request_method='GET',
151 151 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
152 152 def pull_request_list(self):
153 153 c = self.load_default_context()
154 154
155 155 req_get = self.request.GET
156 156 c.source = str2bool(req_get.get('source'))
157 157 c.closed = str2bool(req_get.get('closed'))
158 158 c.my = str2bool(req_get.get('my'))
159 159 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
160 160 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
161 161
162 162 c.active = 'open'
163 163 if c.my:
164 164 c.active = 'my'
165 165 if c.closed:
166 166 c.active = 'closed'
167 167 if c.awaiting_review and not c.source:
168 168 c.active = 'awaiting'
169 169 if c.source and not c.awaiting_review:
170 170 c.active = 'source'
171 171 if c.awaiting_my_review:
172 172 c.active = 'awaiting_my'
173 173
174 174 return self._get_template_context(c)
175 175
176 176 @LoginRequired()
177 177 @HasRepoPermissionAnyDecorator(
178 178 'repository.read', 'repository.write', 'repository.admin')
179 179 @view_config(
180 180 route_name='pullrequest_show_all_data', request_method='GET',
181 181 renderer='json_ext', xhr=True)
182 182 def pull_request_list_data(self):
183 183 self.load_default_context()
184 184
185 185 # additional filters
186 186 req_get = self.request.GET
187 187 source = str2bool(req_get.get('source'))
188 188 closed = str2bool(req_get.get('closed'))
189 189 my = str2bool(req_get.get('my'))
190 190 awaiting_review = str2bool(req_get.get('awaiting_review'))
191 191 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
192 192
193 193 filter_type = 'awaiting_review' if awaiting_review \
194 194 else 'awaiting_my_review' if awaiting_my_review \
195 195 else None
196 196
197 197 opened_by = None
198 198 if my:
199 199 opened_by = [self._rhodecode_user.user_id]
200 200
201 201 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
202 202 if closed:
203 203 statuses = [PullRequest.STATUS_CLOSED]
204 204
205 205 data = self._get_pull_requests_list(
206 206 repo_name=self.db_repo_name, source=source,
207 207 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
208 208
209 209 return data
210 210
211 211 def _is_diff_cache_enabled(self, target_repo):
212 212 caching_enabled = self._get_general_setting(
213 213 target_repo, 'rhodecode_diff_cache')
214 214 log.debug('Diff caching enabled: %s', caching_enabled)
215 215 return caching_enabled
216 216
217 217 def _get_diffset(self, source_repo_name, source_repo,
218 218 ancestor_commit,
219 219 source_ref_id, target_ref_id,
220 220 target_commit, source_commit, diff_limit, file_limit,
221 221 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
222 222
223 223 if use_ancestor:
224 224 # we might want to not use it for versions
225 225 target_ref_id = ancestor_commit.raw_id
226 226
227 227 vcs_diff = PullRequestModel().get_diff(
228 228 source_repo, source_ref_id, target_ref_id,
229 229 hide_whitespace_changes, diff_context)
230 230
231 231 diff_processor = diffs.DiffProcessor(
232 232 vcs_diff, format='newdiff', diff_limit=diff_limit,
233 233 file_limit=file_limit, show_full_diff=fulldiff)
234 234
235 235 _parsed = diff_processor.prepare()
236 236
237 237 diffset = codeblocks.DiffSet(
238 238 repo_name=self.db_repo_name,
239 239 source_repo_name=source_repo_name,
240 240 source_node_getter=codeblocks.diffset_node_getter(target_commit),
241 241 target_node_getter=codeblocks.diffset_node_getter(source_commit),
242 242 )
243 243 diffset = self.path_filter.render_patchset_filtered(
244 244 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
245 245
246 246 return diffset
247 247
248 248 def _get_range_diffset(self, source_scm, source_repo,
249 249 commit1, commit2, diff_limit, file_limit,
250 250 fulldiff, hide_whitespace_changes, diff_context):
251 251 vcs_diff = source_scm.get_diff(
252 252 commit1, commit2,
253 253 ignore_whitespace=hide_whitespace_changes,
254 254 context=diff_context)
255 255
256 256 diff_processor = diffs.DiffProcessor(
257 257 vcs_diff, format='newdiff', diff_limit=diff_limit,
258 258 file_limit=file_limit, show_full_diff=fulldiff)
259 259
260 260 _parsed = diff_processor.prepare()
261 261
262 262 diffset = codeblocks.DiffSet(
263 263 repo_name=source_repo.repo_name,
264 264 source_node_getter=codeblocks.diffset_node_getter(commit1),
265 265 target_node_getter=codeblocks.diffset_node_getter(commit2))
266 266
267 267 diffset = self.path_filter.render_patchset_filtered(
268 268 diffset, _parsed, commit1.raw_id, commit2.raw_id)
269 269
270 270 return diffset
271 271
272 272 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
273 273 comments_model = CommentsModel()
274 274
275 275 # GENERAL COMMENTS with versions #
276 276 q = comments_model._all_general_comments_of_pull_request(pull_request)
277 277 q = q.order_by(ChangesetComment.comment_id.asc())
278 278 if not include_drafts:
279 279 q = q.filter(ChangesetComment.draft == false())
280 280 general_comments = q
281 281
282 282 # pick comments we want to render at current version
283 283 c.comment_versions = comments_model.aggregate_comments(
284 284 general_comments, versions, c.at_version_num)
285 285
286 286 # INLINE COMMENTS with versions #
287 287 q = comments_model._all_inline_comments_of_pull_request(pull_request)
288 288 q = q.order_by(ChangesetComment.comment_id.asc())
289 289 if not include_drafts:
290 290 q = q.filter(ChangesetComment.draft == false())
291 291 inline_comments = q
292 292
293 293 c.inline_versions = comments_model.aggregate_comments(
294 294 inline_comments, versions, c.at_version_num, inline=True)
295 295
296 296 # Comments inline+general
297 297 if c.at_version:
298 298 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
299 299 c.comments = c.comment_versions[c.at_version_num]['display']
300 300 else:
301 301 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
302 302 c.comments = c.comment_versions[c.at_version_num]['until']
303 303
304 304 return general_comments, inline_comments
305 305
306 306 @LoginRequired()
307 307 @HasRepoPermissionAnyDecorator(
308 308 'repository.read', 'repository.write', 'repository.admin')
309 309 @view_config(
310 310 route_name='pullrequest_show', request_method='GET',
311 311 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
312 312 def pull_request_show(self):
313 313 _ = self.request.translate
314 314 c = self.load_default_context()
315 315
316 316 pull_request = PullRequest.get_or_404(
317 317 self.request.matchdict['pull_request_id'])
318 318 pull_request_id = pull_request.pull_request_id
319 319
320 320 c.state_progressing = pull_request.is_state_changing()
321 321 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
322 322
323 323 _new_state = {
324 324 'created': PullRequest.STATE_CREATED,
325 325 }.get(self.request.GET.get('force_state'))
326 326
327 327 if c.is_super_admin and _new_state:
328 328 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
329 329 h.flash(
330 330 _('Pull Request state was force changed to `{}`').format(_new_state),
331 331 category='success')
332 332 Session().commit()
333 333
334 334 raise HTTPFound(h.route_path(
335 335 'pullrequest_show', repo_name=self.db_repo_name,
336 336 pull_request_id=pull_request_id))
337 337
338 338 version = self.request.GET.get('version')
339 339 from_version = self.request.GET.get('from_version') or version
340 340 merge_checks = self.request.GET.get('merge_checks')
341 341 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
342 342 force_refresh = str2bool(self.request.GET.get('force_refresh'))
343 343 c.range_diff_on = self.request.GET.get('range-diff') == "1"
344 344
345 345 # fetch global flags of ignore ws or context lines
346 346 diff_context = diffs.get_diff_context(self.request)
347 347 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
348 348
349 349 (pull_request_latest,
350 350 pull_request_at_ver,
351 351 pull_request_display_obj,
352 352 at_version) = PullRequestModel().get_pr_version(
353 353 pull_request_id, version=version)
354 354
355 355 pr_closed = pull_request_latest.is_closed()
356 356
357 357 if pr_closed and (version or from_version):
358 358 # not allow to browse versions for closed PR
359 359 raise HTTPFound(h.route_path(
360 360 'pullrequest_show', repo_name=self.db_repo_name,
361 361 pull_request_id=pull_request_id))
362 362
363 363 versions = pull_request_display_obj.versions()
364 364 # used to store per-commit range diffs
365 365 c.changes = collections.OrderedDict()
366 366
367 367 c.at_version = at_version
368 368 c.at_version_num = (at_version
369 369 if at_version and at_version != PullRequest.LATEST_VER
370 370 else None)
371 371
372 372 c.at_version_index = ChangesetComment.get_index_from_version(
373 373 c.at_version_num, versions)
374 374
375 375 (prev_pull_request_latest,
376 376 prev_pull_request_at_ver,
377 377 prev_pull_request_display_obj,
378 378 prev_at_version) = PullRequestModel().get_pr_version(
379 379 pull_request_id, version=from_version)
380 380
381 381 c.from_version = prev_at_version
382 382 c.from_version_num = (prev_at_version
383 383 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
384 384 else None)
385 385 c.from_version_index = ChangesetComment.get_index_from_version(
386 386 c.from_version_num, versions)
387 387
388 388 # define if we're in COMPARE mode or VIEW at version mode
389 389 compare = at_version != prev_at_version
390 390
391 391 # pull_requests repo_name we opened it against
392 392 # ie. target_repo must match
393 393 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
394 394 log.warning('Mismatch between the current repo: %s, and target %s',
395 395 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
396 396 raise HTTPNotFound()
397 397
398 398 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
399 399
400 400 c.pull_request = pull_request_display_obj
401 401 c.renderer = pull_request_at_ver.description_renderer or c.renderer
402 402 c.pull_request_latest = pull_request_latest
403 403
404 404 # inject latest version
405 405 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
406 406 c.versions = versions + [latest_ver]
407 407
408 408 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
409 409 c.allowed_to_change_status = False
410 410 c.allowed_to_update = False
411 411 c.allowed_to_merge = False
412 412 c.allowed_to_delete = False
413 413 c.allowed_to_comment = False
414 414 c.allowed_to_close = False
415 415 else:
416 416 can_change_status = PullRequestModel().check_user_change_status(
417 417 pull_request_at_ver, self._rhodecode_user)
418 418 c.allowed_to_change_status = can_change_status and not pr_closed
419 419
420 420 c.allowed_to_update = PullRequestModel().check_user_update(
421 421 pull_request_latest, self._rhodecode_user) and not pr_closed
422 422 c.allowed_to_merge = PullRequestModel().check_user_merge(
423 423 pull_request_latest, self._rhodecode_user) and not pr_closed
424 424 c.allowed_to_delete = PullRequestModel().check_user_delete(
425 425 pull_request_latest, self._rhodecode_user) and not pr_closed
426 426 c.allowed_to_comment = not pr_closed
427 427 c.allowed_to_close = c.allowed_to_merge and not pr_closed
428 428
429 429 c.forbid_adding_reviewers = False
430 430 c.forbid_author_to_review = False
431 431 c.forbid_commit_author_to_review = False
432 432
433 433 if pull_request_latest.reviewer_data and \
434 434 'rules' in pull_request_latest.reviewer_data:
435 435 rules = pull_request_latest.reviewer_data['rules'] or {}
436 436 try:
437 437 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
438 438 c.forbid_author_to_review = rules.get('forbid_author_to_review')
439 439 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
440 440 except Exception:
441 441 pass
442 442
443 443 # check merge capabilities
444 444 _merge_check = MergeCheck.validate(
445 445 pull_request_latest, auth_user=self._rhodecode_user,
446 446 translator=self.request.translate,
447 447 force_shadow_repo_refresh=force_refresh)
448 448
449 449 c.pr_merge_errors = _merge_check.error_details
450 450 c.pr_merge_possible = not _merge_check.failed
451 451 c.pr_merge_message = _merge_check.merge_msg
452 452 c.pr_merge_source_commit = _merge_check.source_commit
453 453 c.pr_merge_target_commit = _merge_check.target_commit
454 454
455 455 c.pr_merge_info = MergeCheck.get_merge_conditions(
456 456 pull_request_latest, translator=self.request.translate)
457 457
458 458 c.pull_request_review_status = _merge_check.review_status
459 459 if merge_checks:
460 460 self.request.override_renderer = \
461 461 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
462 462 return self._get_template_context(c)
463 463
464 464 c.reviewers_count = pull_request.reviewers_count
465 465 c.observers_count = pull_request.observers_count
466 466
467 467 # reviewers and statuses
468 468 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
469 469 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
470 470 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
471 471
472 472 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
473 473 member_reviewer = h.reviewer_as_json(
474 474 member, reasons=reasons, mandatory=mandatory,
475 475 role=review_obj.role,
476 476 user_group=review_obj.rule_user_group_data()
477 477 )
478 478
479 479 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
480 480 member_reviewer['review_status'] = current_review_status
481 481 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
482 482 member_reviewer['allowed_to_update'] = c.allowed_to_update
483 483 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
484 484
485 485 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
486 486
487 487 for observer_obj, member in pull_request_at_ver.observers():
488 488 member_observer = h.reviewer_as_json(
489 489 member, reasons=[], mandatory=False,
490 490 role=observer_obj.role,
491 491 user_group=observer_obj.rule_user_group_data()
492 492 )
493 493 member_observer['allowed_to_update'] = c.allowed_to_update
494 494 c.pull_request_set_observers_data_json['observers'].append(member_observer)
495 495
496 496 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
497 497
498 498 general_comments, inline_comments = \
499 499 self.register_comments_vars(c, pull_request_latest, versions)
500 500
501 501 # TODOs
502 502 c.unresolved_comments = CommentsModel() \
503 503 .get_pull_request_unresolved_todos(pull_request_latest)
504 504 c.resolved_comments = CommentsModel() \
505 505 .get_pull_request_resolved_todos(pull_request_latest)
506 506
507 507 # if we use version, then do not show later comments
508 508 # than current version
509 509 display_inline_comments = collections.defaultdict(
510 510 lambda: collections.defaultdict(list))
511 511 for co in inline_comments:
512 512 if c.at_version_num:
513 513 # pick comments that are at least UPTO given version, so we
514 514 # don't render comments for higher version
515 515 should_render = co.pull_request_version_id and \
516 516 co.pull_request_version_id <= c.at_version_num
517 517 else:
518 518 # showing all, for 'latest'
519 519 should_render = True
520 520
521 521 if should_render:
522 522 display_inline_comments[co.f_path][co.line_no].append(co)
523 523
524 524 # load diff data into template context, if we use compare mode then
525 525 # diff is calculated based on changes between versions of PR
526 526
527 527 source_repo = pull_request_at_ver.source_repo
528 528 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
529 529
530 530 target_repo = pull_request_at_ver.target_repo
531 531 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
532 532
533 533 if compare:
534 534 # in compare switch the diff base to latest commit from prev version
535 535 target_ref_id = prev_pull_request_display_obj.revisions[0]
536 536
537 537 # despite opening commits for bookmarks/branches/tags, we always
538 538 # convert this to rev to prevent changes after bookmark or branch change
539 539 c.source_ref_type = 'rev'
540 540 c.source_ref = source_ref_id
541 541
542 542 c.target_ref_type = 'rev'
543 543 c.target_ref = target_ref_id
544 544
545 545 c.source_repo = source_repo
546 546 c.target_repo = target_repo
547 547
548 548 c.commit_ranges = []
549 549 source_commit = EmptyCommit()
550 550 target_commit = EmptyCommit()
551 551 c.missing_requirements = False
552 552
553 553 source_scm = source_repo.scm_instance()
554 554 target_scm = target_repo.scm_instance()
555 555
556 556 shadow_scm = None
557 557 try:
558 558 shadow_scm = pull_request_latest.get_shadow_repo()
559 559 except Exception:
560 560 log.debug('Failed to get shadow repo', exc_info=True)
561 561 # try first the existing source_repo, and then shadow
562 562 # repo if we can obtain one
563 563 commits_source_repo = source_scm
564 564 if shadow_scm:
565 565 commits_source_repo = shadow_scm
566 566
567 567 c.commits_source_repo = commits_source_repo
568 568 c.ancestor = None # set it to None, to hide it from PR view
569 569
570 570 # empty version means latest, so we keep this to prevent
571 571 # double caching
572 572 version_normalized = version or PullRequest.LATEST_VER
573 573 from_version_normalized = from_version or PullRequest.LATEST_VER
574 574
575 575 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
576 576 cache_file_path = diff_cache_exist(
577 577 cache_path, 'pull_request', pull_request_id, version_normalized,
578 578 from_version_normalized, source_ref_id, target_ref_id,
579 579 hide_whitespace_changes, diff_context, c.fulldiff)
580 580
581 581 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
582 582 force_recache = self.get_recache_flag()
583 583
584 584 cached_diff = None
585 585 if caching_enabled:
586 586 cached_diff = load_cached_diff(cache_file_path)
587 587
588 588 has_proper_commit_cache = (
589 589 cached_diff and cached_diff.get('commits')
590 590 and len(cached_diff.get('commits', [])) == 5
591 591 and cached_diff.get('commits')[0]
592 592 and cached_diff.get('commits')[3])
593 593
594 594 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
595 595 diff_commit_cache = \
596 596 (ancestor_commit, commit_cache, missing_requirements,
597 597 source_commit, target_commit) = cached_diff['commits']
598 598 else:
599 599 # NOTE(marcink): we reach potentially unreachable errors when a PR has
600 600 # merge errors resulting in potentially hidden commits in the shadow repo.
601 601 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
602 602 and _merge_check.merge_response
603 603 maybe_unreachable = maybe_unreachable \
604 604 and _merge_check.merge_response.metadata.get('unresolved_files')
605 605 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
606 606 diff_commit_cache = \
607 607 (ancestor_commit, commit_cache, missing_requirements,
608 608 source_commit, target_commit) = self.get_commits(
609 609 commits_source_repo,
610 610 pull_request_at_ver,
611 611 source_commit,
612 612 source_ref_id,
613 613 source_scm,
614 614 target_commit,
615 615 target_ref_id,
616 616 target_scm,
617 617 maybe_unreachable=maybe_unreachable)
618 618
619 619 # register our commit range
620 620 for comm in commit_cache.values():
621 621 c.commit_ranges.append(comm)
622 622
623 623 c.missing_requirements = missing_requirements
624 624 c.ancestor_commit = ancestor_commit
625 625 c.statuses = source_repo.statuses(
626 626 [x.raw_id for x in c.commit_ranges])
627 627
628 628 # auto collapse if we have more than limit
629 629 collapse_limit = diffs.DiffProcessor._collapse_commits_over
630 630 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
631 631 c.compare_mode = compare
632 632
633 633 # diff_limit is the old behavior, will cut off the whole diff
634 634 # if the limit is applied otherwise will just hide the
635 635 # big files from the front-end
636 636 diff_limit = c.visual.cut_off_limit_diff
637 637 file_limit = c.visual.cut_off_limit_file
638 638
639 639 c.missing_commits = False
640 640 if (c.missing_requirements
641 641 or isinstance(source_commit, EmptyCommit)
642 642 or source_commit == target_commit):
643 643
644 644 c.missing_commits = True
645 645 else:
646 646 c.inline_comments = display_inline_comments
647 647
648 648 use_ancestor = True
649 649 if from_version_normalized != version_normalized:
650 650 use_ancestor = False
651 651
652 652 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
653 653 if not force_recache and has_proper_diff_cache:
654 654 c.diffset = cached_diff['diff']
655 655 else:
656 656 try:
657 657 c.diffset = self._get_diffset(
658 658 c.source_repo.repo_name, commits_source_repo,
659 659 c.ancestor_commit,
660 660 source_ref_id, target_ref_id,
661 661 target_commit, source_commit,
662 662 diff_limit, file_limit, c.fulldiff,
663 663 hide_whitespace_changes, diff_context,
664 664 use_ancestor=use_ancestor
665 665 )
666 666
667 667 # save cached diff
668 668 if caching_enabled:
669 669 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
670 670 except CommitDoesNotExistError:
671 671 log.exception('Failed to generate diffset')
672 672 c.missing_commits = True
673 673
674 674 if not c.missing_commits:
675 675
676 676 c.limited_diff = c.diffset.limited_diff
677 677
678 678 # calculate removed files that are bound to comments
679 679 comment_deleted_files = [
680 680 fname for fname in display_inline_comments
681 681 if fname not in c.diffset.file_stats]
682 682
683 683 c.deleted_files_comments = collections.defaultdict(dict)
684 684 for fname, per_line_comments in display_inline_comments.items():
685 685 if fname in comment_deleted_files:
686 686 c.deleted_files_comments[fname]['stats'] = 0
687 687 c.deleted_files_comments[fname]['comments'] = list()
688 688 for lno, comments in per_line_comments.items():
689 689 c.deleted_files_comments[fname]['comments'].extend(comments)
690 690
691 691 # maybe calculate the range diff
692 692 if c.range_diff_on:
693 693 # TODO(marcink): set whitespace/context
694 694 context_lcl = 3
695 695 ign_whitespace_lcl = False
696 696
697 697 for commit in c.commit_ranges:
698 698 commit2 = commit
699 699 commit1 = commit.first_parent
700 700
701 701 range_diff_cache_file_path = diff_cache_exist(
702 702 cache_path, 'diff', commit.raw_id,
703 703 ign_whitespace_lcl, context_lcl, c.fulldiff)
704 704
705 705 cached_diff = None
706 706 if caching_enabled:
707 707 cached_diff = load_cached_diff(range_diff_cache_file_path)
708 708
709 709 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
710 710 if not force_recache and has_proper_diff_cache:
711 711 diffset = cached_diff['diff']
712 712 else:
713 713 diffset = self._get_range_diffset(
714 714 commits_source_repo, source_repo,
715 715 commit1, commit2, diff_limit, file_limit,
716 716 c.fulldiff, ign_whitespace_lcl, context_lcl
717 717 )
718 718
719 719 # save cached diff
720 720 if caching_enabled:
721 721 cache_diff(range_diff_cache_file_path, diffset, None)
722 722
723 723 c.changes[commit.raw_id] = diffset
724 724
725 725 # this is a hack to properly display links, when creating PR, the
726 726 # compare view and others uses different notation, and
727 727 # compare_commits.mako renders links based on the target_repo.
728 728 # We need to swap that here to generate it properly on the html side
729 729 c.target_repo = c.source_repo
730 730
731 731 c.commit_statuses = ChangesetStatus.STATUSES
732 732
733 733 c.show_version_changes = not pr_closed
734 734 if c.show_version_changes:
735 735 cur_obj = pull_request_at_ver
736 736 prev_obj = prev_pull_request_at_ver
737 737
738 738 old_commit_ids = prev_obj.revisions
739 739 new_commit_ids = cur_obj.revisions
740 740 commit_changes = PullRequestModel()._calculate_commit_id_changes(
741 741 old_commit_ids, new_commit_ids)
742 742 c.commit_changes_summary = commit_changes
743 743
744 744 # calculate the diff for commits between versions
745 745 c.commit_changes = []
746 746
747 747 def mark(cs, fw):
748 748 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
749 749
750 750 for c_type, raw_id in mark(commit_changes.added, 'a') \
751 751 + mark(commit_changes.removed, 'r') \
752 752 + mark(commit_changes.common, 'c'):
753 753
754 754 if raw_id in commit_cache:
755 755 commit = commit_cache[raw_id]
756 756 else:
757 757 try:
758 758 commit = commits_source_repo.get_commit(raw_id)
759 759 except CommitDoesNotExistError:
760 760 # in case we fail extracting still use "dummy" commit
761 761 # for display in commit diff
762 762 commit = h.AttributeDict(
763 763 {'raw_id': raw_id,
764 764 'message': 'EMPTY or MISSING COMMIT'})
765 765 c.commit_changes.append([c_type, commit])
766 766
767 767 # current user review statuses for each version
768 768 c.review_versions = {}
769 769 is_reviewer = PullRequestModel().is_user_reviewer(
770 770 pull_request, self._rhodecode_user)
771 771 if is_reviewer:
772 772 for co in general_comments:
773 773 if co.author.user_id == self._rhodecode_user.user_id:
774 774 status = co.status_change
775 775 if status:
776 776 _ver_pr = status[0].comment.pull_request_version_id
777 777 c.review_versions[_ver_pr] = status[0]
778 778
779 779 return self._get_template_context(c)
780 780
781 781 def get_commits(
782 782 self, commits_source_repo, pull_request_at_ver, source_commit,
783 783 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
784 784 maybe_unreachable=False):
785 785
786 786 commit_cache = collections.OrderedDict()
787 787 missing_requirements = False
788 788
789 789 try:
790 790 pre_load = ["author", "date", "message", "branch", "parents"]
791 791
792 792 pull_request_commits = pull_request_at_ver.revisions
793 793 log.debug('Loading %s commits from %s',
794 794 len(pull_request_commits), commits_source_repo)
795 795
796 796 for rev in pull_request_commits:
797 797 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
798 798 maybe_unreachable=maybe_unreachable)
799 799 commit_cache[comm.raw_id] = comm
800 800
801 801 # Order here matters, we first need to get target, and then
802 802 # the source
803 803 target_commit = commits_source_repo.get_commit(
804 804 commit_id=safe_str(target_ref_id))
805 805
806 806 source_commit = commits_source_repo.get_commit(
807 807 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
808 808 except CommitDoesNotExistError:
809 809 log.warning('Failed to get commit from `{}` repo'.format(
810 810 commits_source_repo), exc_info=True)
811 811 except RepositoryRequirementError:
812 812 log.warning('Failed to get all required data from repo', exc_info=True)
813 813 missing_requirements = True
814 814
815 815 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
816 816
817 817 try:
818 818 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
819 819 except Exception:
820 820 ancestor_commit = None
821 821
822 822 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
823 823
824 824 def assure_not_empty_repo(self):
825 825 _ = self.request.translate
826 826
827 827 try:
828 828 self.db_repo.scm_instance().get_commit()
829 829 except EmptyRepositoryError:
830 830 h.flash(h.literal(_('There are no commits yet')),
831 831 category='warning')
832 832 raise HTTPFound(
833 833 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
834 834
835 835 @LoginRequired()
836 836 @NotAnonymous()
837 837 @HasRepoPermissionAnyDecorator(
838 838 'repository.read', 'repository.write', 'repository.admin')
839 839 @view_config(
840 840 route_name='pullrequest_new', request_method='GET',
841 841 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
842 842 def pull_request_new(self):
843 843 _ = self.request.translate
844 844 c = self.load_default_context()
845 845
846 846 self.assure_not_empty_repo()
847 847 source_repo = self.db_repo
848 848
849 849 commit_id = self.request.GET.get('commit')
850 850 branch_ref = self.request.GET.get('branch')
851 851 bookmark_ref = self.request.GET.get('bookmark')
852 852
853 853 try:
854 854 source_repo_data = PullRequestModel().generate_repo_data(
855 855 source_repo, commit_id=commit_id,
856 856 branch=branch_ref, bookmark=bookmark_ref,
857 857 translator=self.request.translate)
858 858 except CommitDoesNotExistError as e:
859 859 log.exception(e)
860 860 h.flash(_('Commit does not exist'), 'error')
861 861 raise HTTPFound(
862 862 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
863 863
864 864 default_target_repo = source_repo
865 865
866 866 if source_repo.parent and c.has_origin_repo_read_perm:
867 867 parent_vcs_obj = source_repo.parent.scm_instance()
868 868 if parent_vcs_obj and not parent_vcs_obj.is_empty():
869 869 # change default if we have a parent repo
870 870 default_target_repo = source_repo.parent
871 871
872 872 target_repo_data = PullRequestModel().generate_repo_data(
873 873 default_target_repo, translator=self.request.translate)
874 874
875 875 selected_source_ref = source_repo_data['refs']['selected_ref']
876 876 title_source_ref = ''
877 877 if selected_source_ref:
878 878 title_source_ref = selected_source_ref.split(':', 2)[1]
879 879 c.default_title = PullRequestModel().generate_pullrequest_title(
880 880 source=source_repo.repo_name,
881 881 source_ref=title_source_ref,
882 882 target=default_target_repo.repo_name
883 883 )
884 884
885 885 c.default_repo_data = {
886 886 'source_repo_name': source_repo.repo_name,
887 887 'source_refs_json': json.dumps(source_repo_data),
888 888 'target_repo_name': default_target_repo.repo_name,
889 889 'target_refs_json': json.dumps(target_repo_data),
890 890 }
891 891 c.default_source_ref = selected_source_ref
892 892
893 893 return self._get_template_context(c)
894 894
895 895 @LoginRequired()
896 896 @NotAnonymous()
897 897 @HasRepoPermissionAnyDecorator(
898 898 'repository.read', 'repository.write', 'repository.admin')
899 899 @view_config(
900 900 route_name='pullrequest_repo_refs', request_method='GET',
901 901 renderer='json_ext', xhr=True)
902 902 def pull_request_repo_refs(self):
903 903 self.load_default_context()
904 904 target_repo_name = self.request.matchdict['target_repo_name']
905 905 repo = Repository.get_by_repo_name(target_repo_name)
906 906 if not repo:
907 907 raise HTTPNotFound()
908 908
909 909 target_perm = HasRepoPermissionAny(
910 910 'repository.read', 'repository.write', 'repository.admin')(
911 911 target_repo_name)
912 912 if not target_perm:
913 913 raise HTTPNotFound()
914 914
915 915 return PullRequestModel().generate_repo_data(
916 916 repo, translator=self.request.translate)
917 917
918 918 @LoginRequired()
919 919 @NotAnonymous()
920 920 @HasRepoPermissionAnyDecorator(
921 921 'repository.read', 'repository.write', 'repository.admin')
922 922 @view_config(
923 923 route_name='pullrequest_repo_targets', request_method='GET',
924 924 renderer='json_ext', xhr=True)
925 925 def pullrequest_repo_targets(self):
926 926 _ = self.request.translate
927 927 filter_query = self.request.GET.get('query')
928 928
929 929 # get the parents
930 930 parent_target_repos = []
931 931 if self.db_repo.parent:
932 932 parents_query = Repository.query() \
933 933 .order_by(func.length(Repository.repo_name)) \
934 934 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
935 935
936 936 if filter_query:
937 937 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
938 938 parents_query = parents_query.filter(
939 939 Repository.repo_name.ilike(ilike_expression))
940 940 parents = parents_query.limit(20).all()
941 941
942 942 for parent in parents:
943 943 parent_vcs_obj = parent.scm_instance()
944 944 if parent_vcs_obj and not parent_vcs_obj.is_empty():
945 945 parent_target_repos.append(parent)
946 946
947 947 # get other forks, and repo itself
948 948 query = Repository.query() \
949 949 .order_by(func.length(Repository.repo_name)) \
950 950 .filter(
951 951 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
952 952 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
953 953 ) \
954 954 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
955 955
956 956 if filter_query:
957 957 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
958 958 query = query.filter(Repository.repo_name.ilike(ilike_expression))
959 959
960 960 limit = max(20 - len(parent_target_repos), 5) # not less then 5
961 961 target_repos = query.limit(limit).all()
962 962
963 963 all_target_repos = target_repos + parent_target_repos
964 964
965 965 repos = []
966 966 # This checks permissions to the repositories
967 967 for obj in ScmModel().get_repos(all_target_repos):
968 968 repos.append({
969 969 'id': obj['name'],
970 970 'text': obj['name'],
971 971 'type': 'repo',
972 972 'repo_id': obj['dbrepo']['repo_id'],
973 973 'repo_type': obj['dbrepo']['repo_type'],
974 974 'private': obj['dbrepo']['private'],
975 975
976 976 })
977 977
978 978 data = {
979 979 'more': False,
980 980 'results': [{
981 981 'text': _('Repositories'),
982 982 'children': repos
983 983 }] if repos else []
984 984 }
985 985 return data
986 986
987 987 @classmethod
988 988 def get_comment_ids(cls, post_data):
989 989 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
990 990
991 991 @LoginRequired()
992 992 @NotAnonymous()
993 993 @HasRepoPermissionAnyDecorator(
994 994 'repository.read', 'repository.write', 'repository.admin')
995 995 @view_config(
996 996 route_name='pullrequest_comments', request_method='POST',
997 997 renderer='string_html', xhr=True)
998 998 def pullrequest_comments(self):
999 999 self.load_default_context()
1000 1000
1001 1001 pull_request = PullRequest.get_or_404(
1002 1002 self.request.matchdict['pull_request_id'])
1003 1003 pull_request_id = pull_request.pull_request_id
1004 1004 version = self.request.GET.get('version')
1005 1005
1006 1006 _render = self.request.get_partial_renderer(
1007 1007 'rhodecode:templates/base/sidebar.mako')
1008 1008 c = _render.get_call_context()
1009 1009
1010 1010 (pull_request_latest,
1011 1011 pull_request_at_ver,
1012 1012 pull_request_display_obj,
1013 1013 at_version) = PullRequestModel().get_pr_version(
1014 1014 pull_request_id, version=version)
1015 1015 versions = pull_request_display_obj.versions()
1016 1016 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1017 1017 c.versions = versions + [latest_ver]
1018 1018
1019 1019 c.at_version = at_version
1020 1020 c.at_version_num = (at_version
1021 1021 if at_version and at_version != PullRequest.LATEST_VER
1022 1022 else None)
1023 1023
1024 1024 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1025 1025 all_comments = c.inline_comments_flat + c.comments
1026 1026
1027 1027 existing_ids = self.get_comment_ids(self.request.POST)
1028 1028 return _render('comments_table', all_comments, len(all_comments),
1029 1029 existing_ids=existing_ids)
1030 1030
1031 1031 @LoginRequired()
1032 1032 @NotAnonymous()
1033 1033 @HasRepoPermissionAnyDecorator(
1034 1034 'repository.read', 'repository.write', 'repository.admin')
1035 1035 @view_config(
1036 1036 route_name='pullrequest_todos', request_method='POST',
1037 1037 renderer='string_html', xhr=True)
1038 1038 def pullrequest_todos(self):
1039 1039 self.load_default_context()
1040 1040
1041 1041 pull_request = PullRequest.get_or_404(
1042 1042 self.request.matchdict['pull_request_id'])
1043 1043 pull_request_id = pull_request.pull_request_id
1044 1044 version = self.request.GET.get('version')
1045 1045
1046 1046 _render = self.request.get_partial_renderer(
1047 1047 'rhodecode:templates/base/sidebar.mako')
1048 1048 c = _render.get_call_context()
1049 1049 (pull_request_latest,
1050 1050 pull_request_at_ver,
1051 1051 pull_request_display_obj,
1052 1052 at_version) = PullRequestModel().get_pr_version(
1053 1053 pull_request_id, version=version)
1054 1054 versions = pull_request_display_obj.versions()
1055 1055 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1056 1056 c.versions = versions + [latest_ver]
1057 1057
1058 1058 c.at_version = at_version
1059 1059 c.at_version_num = (at_version
1060 1060 if at_version and at_version != PullRequest.LATEST_VER
1061 1061 else None)
1062 1062
1063 1063 c.unresolved_comments = CommentsModel() \
1064 1064 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1065 1065 c.resolved_comments = CommentsModel() \
1066 1066 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1067 1067
1068 1068 all_comments = c.unresolved_comments + c.resolved_comments
1069 1069 existing_ids = self.get_comment_ids(self.request.POST)
1070 1070 return _render('comments_table', all_comments, len(c.unresolved_comments),
1071 1071 todo_comments=True, existing_ids=existing_ids)
1072 1072
1073 1073 @LoginRequired()
1074 1074 @NotAnonymous()
1075 1075 @HasRepoPermissionAnyDecorator(
1076 1076 'repository.read', 'repository.write', 'repository.admin')
1077 1077 @CSRFRequired()
1078 1078 @view_config(
1079 1079 route_name='pullrequest_create', request_method='POST',
1080 1080 renderer=None)
1081 1081 def pull_request_create(self):
1082 1082 _ = self.request.translate
1083 1083 self.assure_not_empty_repo()
1084 1084 self.load_default_context()
1085 1085
1086 1086 controls = peppercorn.parse(self.request.POST.items())
1087 1087
1088 1088 try:
1089 1089 form = PullRequestForm(
1090 1090 self.request.translate, self.db_repo.repo_id)()
1091 1091 _form = form.to_python(controls)
1092 1092 except formencode.Invalid as errors:
1093 1093 if errors.error_dict.get('revisions'):
1094 1094 msg = 'Revisions: %s' % errors.error_dict['revisions']
1095 1095 elif errors.error_dict.get('pullrequest_title'):
1096 1096 msg = errors.error_dict.get('pullrequest_title')
1097 1097 else:
1098 1098 msg = _('Error creating pull request: {}').format(errors)
1099 1099 log.exception(msg)
1100 1100 h.flash(msg, 'error')
1101 1101
1102 1102 # would rather just go back to form ...
1103 1103 raise HTTPFound(
1104 1104 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1105 1105
1106 1106 source_repo = _form['source_repo']
1107 1107 source_ref = _form['source_ref']
1108 1108 target_repo = _form['target_repo']
1109 1109 target_ref = _form['target_ref']
1110 1110 commit_ids = _form['revisions'][::-1]
1111 1111 common_ancestor_id = _form['common_ancestor']
1112 1112
1113 1113 # find the ancestor for this pr
1114 1114 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1115 1115 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1116 1116
1117 1117 if not (source_db_repo or target_db_repo):
1118 1118 h.flash(_('source_repo or target repo not found'), category='error')
1119 1119 raise HTTPFound(
1120 1120 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1121 1121
1122 1122 # re-check permissions again here
1123 1123 # source_repo we must have read permissions
1124 1124
1125 1125 source_perm = HasRepoPermissionAny(
1126 1126 'repository.read', 'repository.write', 'repository.admin')(
1127 1127 source_db_repo.repo_name)
1128 1128 if not source_perm:
1129 1129 msg = _('Not Enough permissions to source repo `{}`.'.format(
1130 1130 source_db_repo.repo_name))
1131 1131 h.flash(msg, category='error')
1132 1132 # copy the args back to redirect
1133 1133 org_query = self.request.GET.mixed()
1134 1134 raise HTTPFound(
1135 1135 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1136 1136 _query=org_query))
1137 1137
1138 1138 # target repo we must have read permissions, and also later on
1139 1139 # we want to check branch permissions here
1140 1140 target_perm = HasRepoPermissionAny(
1141 1141 'repository.read', 'repository.write', 'repository.admin')(
1142 1142 target_db_repo.repo_name)
1143 1143 if not target_perm:
1144 1144 msg = _('Not Enough permissions to target repo `{}`.'.format(
1145 1145 target_db_repo.repo_name))
1146 1146 h.flash(msg, category='error')
1147 1147 # copy the args back to redirect
1148 1148 org_query = self.request.GET.mixed()
1149 1149 raise HTTPFound(
1150 1150 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1151 1151 _query=org_query))
1152 1152
1153 1153 source_scm = source_db_repo.scm_instance()
1154 1154 target_scm = target_db_repo.scm_instance()
1155 1155
1156 1156 source_ref_obj = unicode_to_reference(source_ref)
1157 1157 target_ref_obj = unicode_to_reference(target_ref)
1158 1158
1159 1159 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1160 1160 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1161 1161
1162 1162 ancestor = source_scm.get_common_ancestor(
1163 1163 source_commit.raw_id, target_commit.raw_id, target_scm)
1164 1164
1165 1165 # recalculate target ref based on ancestor
1166 1166 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1167 1167
1168 1168 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1169 1169 PullRequestModel().get_reviewer_functions()
1170 1170
1171 1171 # recalculate reviewers logic, to make sure we can validate this
1172 1172 reviewer_rules = get_default_reviewers_data(
1173 1173 self._rhodecode_db_user,
1174 1174 source_db_repo,
1175 1175 source_ref_obj,
1176 1176 target_db_repo,
1177 1177 target_ref_obj,
1178 1178 include_diff_info=False)
1179 1179
1180 1180 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1181 1181 observers = validate_observers(_form['observer_members'], reviewer_rules)
1182 1182
1183 1183 pullrequest_title = _form['pullrequest_title']
1184 1184 title_source_ref = source_ref_obj.name
1185 1185 if not pullrequest_title:
1186 1186 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1187 1187 source=source_repo,
1188 1188 source_ref=title_source_ref,
1189 1189 target=target_repo
1190 1190 )
1191 1191
1192 1192 description = _form['pullrequest_desc']
1193 1193 description_renderer = _form['description_renderer']
1194 1194
1195 1195 try:
1196 1196 pull_request = PullRequestModel().create(
1197 1197 created_by=self._rhodecode_user.user_id,
1198 1198 source_repo=source_repo,
1199 1199 source_ref=source_ref,
1200 1200 target_repo=target_repo,
1201 1201 target_ref=target_ref,
1202 1202 revisions=commit_ids,
1203 1203 common_ancestor_id=common_ancestor_id,
1204 1204 reviewers=reviewers,
1205 1205 observers=observers,
1206 1206 title=pullrequest_title,
1207 1207 description=description,
1208 1208 description_renderer=description_renderer,
1209 1209 reviewer_data=reviewer_rules,
1210 1210 auth_user=self._rhodecode_user
1211 1211 )
1212 1212 Session().commit()
1213 1213
1214 1214 h.flash(_('Successfully opened new pull request'),
1215 1215 category='success')
1216 1216 except Exception:
1217 1217 msg = _('Error occurred during creation of this pull request.')
1218 1218 log.exception(msg)
1219 1219 h.flash(msg, category='error')
1220 1220
1221 1221 # copy the args back to redirect
1222 1222 org_query = self.request.GET.mixed()
1223 1223 raise HTTPFound(
1224 1224 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1225 1225 _query=org_query))
1226 1226
1227 1227 raise HTTPFound(
1228 1228 h.route_path('pullrequest_show', repo_name=target_repo,
1229 1229 pull_request_id=pull_request.pull_request_id))
1230 1230
1231 1231 @LoginRequired()
1232 1232 @NotAnonymous()
1233 1233 @HasRepoPermissionAnyDecorator(
1234 1234 'repository.read', 'repository.write', 'repository.admin')
1235 1235 @CSRFRequired()
1236 1236 @view_config(
1237 1237 route_name='pullrequest_update', request_method='POST',
1238 1238 renderer='json_ext')
1239 1239 def pull_request_update(self):
1240 1240 pull_request = PullRequest.get_or_404(
1241 1241 self.request.matchdict['pull_request_id'])
1242 1242 _ = self.request.translate
1243 1243
1244 1244 c = self.load_default_context()
1245 1245 redirect_url = None
1246 1246
1247 1247 if pull_request.is_closed():
1248 1248 log.debug('update: forbidden because pull request is closed')
1249 1249 msg = _(u'Cannot update closed pull requests.')
1250 1250 h.flash(msg, category='error')
1251 1251 return {'response': True,
1252 1252 'redirect_url': redirect_url}
1253 1253
1254 1254 is_state_changing = pull_request.is_state_changing()
1255 1255 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1256 1256
1257 1257 # only owner or admin can update it
1258 1258 allowed_to_update = PullRequestModel().check_user_update(
1259 1259 pull_request, self._rhodecode_user)
1260 1260
1261 1261 if allowed_to_update:
1262 1262 controls = peppercorn.parse(self.request.POST.items())
1263 1263 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1264 1264
1265 1265 if 'review_members' in controls:
1266 1266 self._update_reviewers(
1267 1267 c,
1268 1268 pull_request, controls['review_members'],
1269 1269 pull_request.reviewer_data,
1270 1270 PullRequestReviewers.ROLE_REVIEWER)
1271 1271 elif 'observer_members' in controls:
1272 1272 self._update_reviewers(
1273 1273 c,
1274 1274 pull_request, controls['observer_members'],
1275 1275 pull_request.reviewer_data,
1276 1276 PullRequestReviewers.ROLE_OBSERVER)
1277 1277 elif str2bool(self.request.POST.get('update_commits', 'false')):
1278 1278 if is_state_changing:
1279 1279 log.debug('commits update: forbidden because pull request is in state %s',
1280 1280 pull_request.pull_request_state)
1281 1281 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1282 1282 u'Current state is: `{}`').format(
1283 1283 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1284 1284 h.flash(msg, category='error')
1285 1285 return {'response': True,
1286 1286 'redirect_url': redirect_url}
1287 1287
1288 1288 self._update_commits(c, pull_request)
1289 1289 if force_refresh:
1290 1290 redirect_url = h.route_path(
1291 1291 'pullrequest_show', repo_name=self.db_repo_name,
1292 1292 pull_request_id=pull_request.pull_request_id,
1293 1293 _query={"force_refresh": 1})
1294 1294 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1295 1295 self._edit_pull_request(pull_request)
1296 1296 else:
1297 1297 log.error('Unhandled update data.')
1298 1298 raise HTTPBadRequest()
1299 1299
1300 1300 return {'response': True,
1301 1301 'redirect_url': redirect_url}
1302 1302 raise HTTPForbidden()
1303 1303
1304 1304 def _edit_pull_request(self, pull_request):
1305 1305 """
1306 1306 Edit title and description
1307 1307 """
1308 1308 _ = self.request.translate
1309 1309
1310 1310 try:
1311 1311 PullRequestModel().edit(
1312 1312 pull_request,
1313 1313 self.request.POST.get('title'),
1314 1314 self.request.POST.get('description'),
1315 1315 self.request.POST.get('description_renderer'),
1316 1316 self._rhodecode_user)
1317 1317 except ValueError:
1318 1318 msg = _(u'Cannot update closed pull requests.')
1319 1319 h.flash(msg, category='error')
1320 1320 return
1321 1321 else:
1322 1322 Session().commit()
1323 1323
1324 1324 msg = _(u'Pull request title & description updated.')
1325 1325 h.flash(msg, category='success')
1326 1326 return
1327 1327
1328 1328 def _update_commits(self, c, pull_request):
1329 1329 _ = self.request.translate
1330 1330
1331 1331 with pull_request.set_state(PullRequest.STATE_UPDATING):
1332 1332 resp = PullRequestModel().update_commits(
1333 1333 pull_request, self._rhodecode_db_user)
1334 1334
1335 1335 if resp.executed:
1336 1336
1337 1337 if resp.target_changed and resp.source_changed:
1338 1338 changed = 'target and source repositories'
1339 1339 elif resp.target_changed and not resp.source_changed:
1340 1340 changed = 'target repository'
1341 1341 elif not resp.target_changed and resp.source_changed:
1342 1342 changed = 'source repository'
1343 1343 else:
1344 1344 changed = 'nothing'
1345 1345
1346 1346 msg = _(u'Pull request updated to "{source_commit_id}" with '
1347 1347 u'{count_added} added, {count_removed} removed commits. '
1348 1348 u'Source of changes: {change_source}.')
1349 1349 msg = msg.format(
1350 1350 source_commit_id=pull_request.source_ref_parts.commit_id,
1351 1351 count_added=len(resp.changes.added),
1352 1352 count_removed=len(resp.changes.removed),
1353 1353 change_source=changed)
1354 1354 h.flash(msg, category='success')
1355 1355 channelstream.pr_update_channelstream_push(
1356 1356 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1357 1357 else:
1358 1358 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1359 1359 warning_reasons = [
1360 1360 UpdateFailureReason.NO_CHANGE,
1361 1361 UpdateFailureReason.WRONG_REF_TYPE,
1362 1362 ]
1363 1363 category = 'warning' if resp.reason in warning_reasons else 'error'
1364 1364 h.flash(msg, category=category)
1365 1365
1366 1366 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1367 1367 _ = self.request.translate
1368 1368
1369 1369 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1370 1370 PullRequestModel().get_reviewer_functions()
1371 1371
1372 1372 if role == PullRequestReviewers.ROLE_REVIEWER:
1373 1373 try:
1374 1374 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1375 1375 except ValueError as e:
1376 1376 log.error('Reviewers Validation: {}'.format(e))
1377 1377 h.flash(e, category='error')
1378 1378 return
1379 1379
1380 1380 old_calculated_status = pull_request.calculated_review_status()
1381 1381 PullRequestModel().update_reviewers(
1382 1382 pull_request, reviewers, self._rhodecode_db_user)
1383 1383
1384 1384 Session().commit()
1385 1385
1386 1386 msg = _('Pull request reviewers updated.')
1387 1387 h.flash(msg, category='success')
1388 1388 channelstream.pr_update_channelstream_push(
1389 1389 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1390 1390
1391 1391 # trigger status changed if change in reviewers changes the status
1392 1392 calculated_status = pull_request.calculated_review_status()
1393 1393 if old_calculated_status != calculated_status:
1394 1394 PullRequestModel().trigger_pull_request_hook(
1395 1395 pull_request, self._rhodecode_user, 'review_status_change',
1396 1396 data={'status': calculated_status})
1397 1397
1398 1398 elif role == PullRequestReviewers.ROLE_OBSERVER:
1399 1399 try:
1400 1400 observers = validate_observers(review_members, reviewer_rules)
1401 1401 except ValueError as e:
1402 1402 log.error('Observers Validation: {}'.format(e))
1403 1403 h.flash(e, category='error')
1404 1404 return
1405 1405
1406 1406 PullRequestModel().update_observers(
1407 1407 pull_request, observers, self._rhodecode_db_user)
1408 1408
1409 1409 Session().commit()
1410 1410 msg = _('Pull request observers updated.')
1411 1411 h.flash(msg, category='success')
1412 1412 channelstream.pr_update_channelstream_push(
1413 1413 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1414 1414
1415 1415 @LoginRequired()
1416 1416 @NotAnonymous()
1417 1417 @HasRepoPermissionAnyDecorator(
1418 1418 'repository.read', 'repository.write', 'repository.admin')
1419 1419 @CSRFRequired()
1420 1420 @view_config(
1421 1421 route_name='pullrequest_merge', request_method='POST',
1422 1422 renderer='json_ext')
1423 1423 def pull_request_merge(self):
1424 1424 """
1425 1425 Merge will perform a server-side merge of the specified
1426 1426 pull request, if the pull request is approved and mergeable.
1427 1427 After successful merging, the pull request is automatically
1428 1428 closed, with a relevant comment.
1429 1429 """
1430 1430 pull_request = PullRequest.get_or_404(
1431 1431 self.request.matchdict['pull_request_id'])
1432 1432 _ = self.request.translate
1433 1433
1434 1434 if pull_request.is_state_changing():
1435 1435 log.debug('show: forbidden because pull request is in state %s',
1436 1436 pull_request.pull_request_state)
1437 1437 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1438 1438 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1439 1439 pull_request.pull_request_state)
1440 1440 h.flash(msg, category='error')
1441 1441 raise HTTPFound(
1442 1442 h.route_path('pullrequest_show',
1443 1443 repo_name=pull_request.target_repo.repo_name,
1444 1444 pull_request_id=pull_request.pull_request_id))
1445 1445
1446 1446 self.load_default_context()
1447 1447
1448 1448 with pull_request.set_state(PullRequest.STATE_UPDATING):
1449 1449 check = MergeCheck.validate(
1450 1450 pull_request, auth_user=self._rhodecode_user,
1451 1451 translator=self.request.translate)
1452 1452 merge_possible = not check.failed
1453 1453
1454 1454 for err_type, error_msg in check.errors:
1455 1455 h.flash(error_msg, category=err_type)
1456 1456
1457 1457 if merge_possible:
1458 1458 log.debug("Pre-conditions checked, trying to merge.")
1459 1459 extras = vcs_operation_context(
1460 1460 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1461 1461 username=self._rhodecode_db_user.username, action='push',
1462 1462 scm=pull_request.target_repo.repo_type)
1463 1463 with pull_request.set_state(PullRequest.STATE_UPDATING):
1464 1464 self._merge_pull_request(
1465 1465 pull_request, self._rhodecode_db_user, extras)
1466 1466 else:
1467 1467 log.debug("Pre-conditions failed, NOT merging.")
1468 1468
1469 1469 raise HTTPFound(
1470 1470 h.route_path('pullrequest_show',
1471 1471 repo_name=pull_request.target_repo.repo_name,
1472 1472 pull_request_id=pull_request.pull_request_id))
1473 1473
1474 1474 def _merge_pull_request(self, pull_request, user, extras):
1475 1475 _ = self.request.translate
1476 1476 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1477 1477
1478 1478 if merge_resp.executed:
1479 1479 log.debug("The merge was successful, closing the pull request.")
1480 1480 PullRequestModel().close_pull_request(
1481 1481 pull_request.pull_request_id, user)
1482 1482 Session().commit()
1483 1483 msg = _('Pull request was successfully merged and closed.')
1484 1484 h.flash(msg, category='success')
1485 1485 else:
1486 1486 log.debug(
1487 1487 "The merge was not successful. Merge response: %s", merge_resp)
1488 1488 msg = merge_resp.merge_status_message
1489 1489 h.flash(msg, category='error')
1490 1490
1491 1491 @LoginRequired()
1492 1492 @NotAnonymous()
1493 1493 @HasRepoPermissionAnyDecorator(
1494 1494 'repository.read', 'repository.write', 'repository.admin')
1495 1495 @CSRFRequired()
1496 1496 @view_config(
1497 1497 route_name='pullrequest_delete', request_method='POST',
1498 1498 renderer='json_ext')
1499 1499 def pull_request_delete(self):
1500 1500 _ = self.request.translate
1501 1501
1502 1502 pull_request = PullRequest.get_or_404(
1503 1503 self.request.matchdict['pull_request_id'])
1504 1504 self.load_default_context()
1505 1505
1506 1506 pr_closed = pull_request.is_closed()
1507 1507 allowed_to_delete = PullRequestModel().check_user_delete(
1508 1508 pull_request, self._rhodecode_user) and not pr_closed
1509 1509
1510 1510 # only owner can delete it !
1511 1511 if allowed_to_delete:
1512 1512 PullRequestModel().delete(pull_request, self._rhodecode_user)
1513 1513 Session().commit()
1514 1514 h.flash(_('Successfully deleted pull request'),
1515 1515 category='success')
1516 1516 raise HTTPFound(h.route_path('pullrequest_show_all',
1517 1517 repo_name=self.db_repo_name))
1518 1518
1519 1519 log.warning('user %s tried to delete pull request without access',
1520 1520 self._rhodecode_user)
1521 1521 raise HTTPNotFound()
1522 1522
1523 1523 def _pull_request_comments_create(self, pull_request, comments):
1524 1524 _ = self.request.translate
1525 1525 data = {}
1526 pull_request_id = pull_request.pull_request_id
1527 1526 if not comments:
1528 1527 return
1528 pull_request_id = pull_request.pull_request_id
1529 1529
1530 1530 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1531 1531
1532 1532 for entry in comments:
1533 1533 c = self.load_default_context()
1534 1534 comment_type = entry['comment_type']
1535 1535 text = entry['text']
1536 1536 status = entry['status']
1537 1537 is_draft = str2bool(entry['is_draft'])
1538 1538 resolves_comment_id = entry['resolves_comment_id']
1539 1539 close_pull_request = entry['close_pull_request']
1540 1540 f_path = entry['f_path']
1541 1541 line_no = entry['line']
1542 1542 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1543 1543
1544 1544 # the logic here should work like following, if we submit close
1545 1545 # pr comment, use `close_pull_request_with_comment` function
1546 1546 # else handle regular comment logic
1547 1547
1548 1548 if close_pull_request:
1549 1549 # only owner or admin or person with write permissions
1550 1550 allowed_to_close = PullRequestModel().check_user_update(
1551 1551 pull_request, self._rhodecode_user)
1552 1552 if not allowed_to_close:
1553 1553 log.debug('comment: forbidden because not allowed to close '
1554 1554 'pull request %s', pull_request_id)
1555 1555 raise HTTPForbidden()
1556 1556
1557 1557 # This also triggers `review_status_change`
1558 1558 comment, status = PullRequestModel().close_pull_request_with_comment(
1559 1559 pull_request, self._rhodecode_user, self.db_repo, message=text,
1560 1560 auth_user=self._rhodecode_user)
1561 1561 Session().flush()
1562 1562 is_inline = comment.is_inline
1563 1563
1564 1564 PullRequestModel().trigger_pull_request_hook(
1565 1565 pull_request, self._rhodecode_user, 'comment',
1566 1566 data={'comment': comment})
1567 1567
1568 1568 else:
1569 1569 # regular comment case, could be inline, or one with status.
1570 1570 # for that one we check also permissions
1571 1571 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1572 1572 allowed_to_change_status = PullRequestModel().check_user_change_status(
1573 1573 pull_request, self._rhodecode_user) and not is_draft
1574 1574
1575 1575 if status and allowed_to_change_status:
1576 1576 message = (_('Status change %(transition_icon)s %(status)s')
1577 1577 % {'transition_icon': '>',
1578 1578 'status': ChangesetStatus.get_status_lbl(status)})
1579 1579 text = text or message
1580 1580
1581 1581 comment = CommentsModel().create(
1582 1582 text=text,
1583 1583 repo=self.db_repo.repo_id,
1584 1584 user=self._rhodecode_user.user_id,
1585 1585 pull_request=pull_request,
1586 1586 f_path=f_path,
1587 1587 line_no=line_no,
1588 1588 status_change=(ChangesetStatus.get_status_lbl(status)
1589 1589 if status and allowed_to_change_status else None),
1590 1590 status_change_type=(status
1591 1591 if status and allowed_to_change_status else None),
1592 1592 comment_type=comment_type,
1593 1593 is_draft=is_draft,
1594 1594 resolves_comment_id=resolves_comment_id,
1595 1595 auth_user=self._rhodecode_user,
1596 1596 send_email=not is_draft, # skip notification for draft comments
1597 1597 )
1598 1598 is_inline = comment.is_inline
1599 1599
1600 1600 if allowed_to_change_status:
1601 1601 # calculate old status before we change it
1602 1602 old_calculated_status = pull_request.calculated_review_status()
1603 1603
1604 1604 # get status if set !
1605 1605 if status:
1606 1606 ChangesetStatusModel().set_status(
1607 1607 self.db_repo.repo_id,
1608 1608 status,
1609 1609 self._rhodecode_user.user_id,
1610 1610 comment,
1611 1611 pull_request=pull_request
1612 1612 )
1613 1613
1614 1614 Session().flush()
1615 1615 # this is somehow required to get access to some relationship
1616 1616 # loaded on comment
1617 1617 Session().refresh(comment)
1618 1618
1619 PullRequestModel().trigger_pull_request_hook(
1620 pull_request, self._rhodecode_user, 'comment',
1621 data={'comment': comment})
1619 # skip notifications for drafts
1620 if not is_draft:
1621 PullRequestModel().trigger_pull_request_hook(
1622 pull_request, self._rhodecode_user, 'comment',
1623 data={'comment': comment})
1622 1624
1623 1625 # we now calculate the status of pull request, and based on that
1624 1626 # calculation we set the commits status
1625 1627 calculated_status = pull_request.calculated_review_status()
1626 1628 if old_calculated_status != calculated_status:
1627 1629 PullRequestModel().trigger_pull_request_hook(
1628 1630 pull_request, self._rhodecode_user, 'review_status_change',
1629 1631 data={'status': calculated_status})
1630 1632
1631 1633 comment_id = comment.comment_id
1632 1634 data[comment_id] = {
1633 1635 'target_id': target_elem_id
1634 1636 }
1635 1637 Session().flush()
1636 1638
1637 1639 c.co = comment
1638 1640 c.at_version_num = None
1639 1641 c.is_new = True
1640 1642 rendered_comment = render(
1641 1643 'rhodecode:templates/changeset/changeset_comment_block.mako',
1642 1644 self._get_template_context(c), self.request)
1643 1645
1644 1646 data[comment_id].update(comment.get_dict())
1645 1647 data[comment_id].update({'rendered_text': rendered_comment})
1646 1648
1647 1649 Session().commit()
1648 1650
1649 1651 # skip channelstream for draft comments
1650 if all_drafts:
1652 if not all_drafts:
1651 1653 comment_broadcast_channel = channelstream.comment_channel(
1652 1654 self.db_repo_name, pull_request_obj=pull_request)
1653 1655
1654 1656 comment_data = data
1655 comment_type = 'inline' if is_inline else 'general'
1657 posted_comment_type = 'inline' if is_inline else 'general'
1656 1658 if len(data) == 1:
1657 msg = _('posted {} new {} comment').format(len(data), comment_type)
1659 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1658 1660 else:
1659 msg = _('posted {} new {} comments').format(len(data), comment_type)
1661 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1660 1662
1661 1663 channelstream.comment_channelstream_push(
1662 1664 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1663 1665 comment_data=comment_data)
1664 1666
1665 1667 return data
1666 1668
1667 1669 @LoginRequired()
1668 1670 @NotAnonymous()
1669 1671 @HasRepoPermissionAnyDecorator(
1670 1672 'repository.read', 'repository.write', 'repository.admin')
1671 1673 @CSRFRequired()
1672 1674 @view_config(
1673 1675 route_name='pullrequest_comment_create', request_method='POST',
1674 1676 renderer='json_ext')
1675 1677 def pull_request_comment_create(self):
1676 1678 _ = self.request.translate
1677 1679
1678 1680 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1679 1681
1680 1682 if pull_request.is_closed():
1681 1683 log.debug('comment: forbidden because pull request is closed')
1682 1684 raise HTTPForbidden()
1683 1685
1684 1686 allowed_to_comment = PullRequestModel().check_user_comment(
1685 1687 pull_request, self._rhodecode_user)
1686 1688 if not allowed_to_comment:
1687 1689 log.debug('comment: forbidden because pull request is from forbidden repo')
1688 1690 raise HTTPForbidden()
1689 1691
1690 1692 comment_data = {
1691 1693 'comment_type': self.request.POST.get('comment_type'),
1692 1694 'text': self.request.POST.get('text'),
1693 1695 'status': self.request.POST.get('changeset_status', None),
1694 1696 'is_draft': self.request.POST.get('draft'),
1695 1697 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1696 1698 'close_pull_request': self.request.POST.get('close_pull_request'),
1697 1699 'f_path': self.request.POST.get('f_path'),
1698 1700 'line': self.request.POST.get('line'),
1699 1701 }
1700 1702 data = self._pull_request_comments_create(pull_request, [comment_data])
1701 1703
1702 1704 return data
1703 1705
1704 1706 @LoginRequired()
1705 1707 @NotAnonymous()
1706 1708 @HasRepoPermissionAnyDecorator(
1707 1709 'repository.read', 'repository.write', 'repository.admin')
1708 1710 @CSRFRequired()
1709 1711 @view_config(
1710 1712 route_name='pullrequest_comment_delete', request_method='POST',
1711 1713 renderer='json_ext')
1712 1714 def pull_request_comment_delete(self):
1713 1715 pull_request = PullRequest.get_or_404(
1714 1716 self.request.matchdict['pull_request_id'])
1715 1717
1716 1718 comment = ChangesetComment.get_or_404(
1717 1719 self.request.matchdict['comment_id'])
1718 1720 comment_id = comment.comment_id
1719 1721
1720 1722 if comment.immutable:
1721 1723 # don't allow deleting comments that are immutable
1722 1724 raise HTTPForbidden()
1723 1725
1724 1726 if pull_request.is_closed():
1725 1727 log.debug('comment: forbidden because pull request is closed')
1726 1728 raise HTTPForbidden()
1727 1729
1728 1730 if not comment:
1729 1731 log.debug('Comment with id:%s not found, skipping', comment_id)
1730 1732 # comment already deleted in another call probably
1731 1733 return True
1732 1734
1733 1735 if comment.pull_request.is_closed():
1734 1736 # don't allow deleting comments on closed pull request
1735 1737 raise HTTPForbidden()
1736 1738
1737 1739 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1738 1740 super_admin = h.HasPermissionAny('hg.admin')()
1739 1741 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1740 1742 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1741 1743 comment_repo_admin = is_repo_admin and is_repo_comment
1742 1744
1743 1745 if super_admin or comment_owner or comment_repo_admin:
1744 1746 old_calculated_status = comment.pull_request.calculated_review_status()
1745 1747 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1746 1748 Session().commit()
1747 1749 calculated_status = comment.pull_request.calculated_review_status()
1748 1750 if old_calculated_status != calculated_status:
1749 1751 PullRequestModel().trigger_pull_request_hook(
1750 1752 comment.pull_request, self._rhodecode_user, 'review_status_change',
1751 1753 data={'status': calculated_status})
1752 1754 return True
1753 1755 else:
1754 1756 log.warning('No permissions for user %s to delete comment_id: %s',
1755 1757 self._rhodecode_db_user, comment_id)
1756 1758 raise HTTPNotFound()
1757 1759
1758 1760 @LoginRequired()
1759 1761 @NotAnonymous()
1760 1762 @HasRepoPermissionAnyDecorator(
1761 1763 'repository.read', 'repository.write', 'repository.admin')
1762 1764 @CSRFRequired()
1763 1765 @view_config(
1764 1766 route_name='pullrequest_comment_edit', request_method='POST',
1765 1767 renderer='json_ext')
1766 1768 def pull_request_comment_edit(self):
1767 1769 self.load_default_context()
1768 1770
1769 1771 pull_request = PullRequest.get_or_404(
1770 1772 self.request.matchdict['pull_request_id']
1771 1773 )
1772 1774 comment = ChangesetComment.get_or_404(
1773 1775 self.request.matchdict['comment_id']
1774 1776 )
1775 1777 comment_id = comment.comment_id
1776 1778
1777 1779 if comment.immutable:
1778 1780 # don't allow deleting comments that are immutable
1779 1781 raise HTTPForbidden()
1780 1782
1781 1783 if pull_request.is_closed():
1782 1784 log.debug('comment: forbidden because pull request is closed')
1783 1785 raise HTTPForbidden()
1784 1786
1785 if not comment:
1786 log.debug('Comment with id:%s not found, skipping', comment_id)
1787 # comment already deleted in another call probably
1788 return True
1789
1790 1787 if comment.pull_request.is_closed():
1791 1788 # don't allow deleting comments on closed pull request
1792 1789 raise HTTPForbidden()
1793 1790
1794 1791 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1795 1792 super_admin = h.HasPermissionAny('hg.admin')()
1796 1793 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1797 1794 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1798 1795 comment_repo_admin = is_repo_admin and is_repo_comment
1799 1796
1800 1797 if super_admin or comment_owner or comment_repo_admin:
1801 1798 text = self.request.POST.get('text')
1802 1799 version = self.request.POST.get('version')
1803 1800 if text == comment.text:
1804 1801 log.warning(
1805 1802 'Comment(PR): '
1806 1803 'Trying to create new version '
1807 1804 'with the same comment body {}'.format(
1808 1805 comment_id,
1809 1806 )
1810 1807 )
1811 1808 raise HTTPNotFound()
1812 1809
1813 1810 if version.isdigit():
1814 1811 version = int(version)
1815 1812 else:
1816 1813 log.warning(
1817 1814 'Comment(PR): Wrong version type {} {} '
1818 1815 'for comment {}'.format(
1819 1816 version,
1820 1817 type(version),
1821 1818 comment_id,
1822 1819 )
1823 1820 )
1824 1821 raise HTTPNotFound()
1825 1822
1826 1823 try:
1827 1824 comment_history = CommentsModel().edit(
1828 1825 comment_id=comment_id,
1829 1826 text=text,
1830 1827 auth_user=self._rhodecode_user,
1831 1828 version=version,
1832 1829 )
1833 1830 except CommentVersionMismatch:
1834 1831 raise HTTPConflict()
1835 1832
1836 1833 if not comment_history:
1837 1834 raise HTTPNotFound()
1838 1835
1839 1836 Session().commit()
1840
1841 PullRequestModel().trigger_pull_request_hook(
1842 pull_request, self._rhodecode_user, 'comment_edit',
1843 data={'comment': comment})
1837 if not comment.draft:
1838 PullRequestModel().trigger_pull_request_hook(
1839 pull_request, self._rhodecode_user, 'comment_edit',
1840 data={'comment': comment})
1844 1841
1845 1842 return {
1846 1843 'comment_history_id': comment_history.comment_history_id,
1847 1844 'comment_id': comment.comment_id,
1848 1845 'comment_version': comment_history.version,
1849 1846 'comment_author_username': comment_history.author.username,
1850 1847 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1851 1848 'comment_created_on': h.age_component(comment_history.created_on,
1852 1849 time_is_local=True),
1853 1850 }
1854 1851 else:
1855 1852 log.warning('No permissions for user %s to edit comment_id: %s',
1856 1853 self._rhodecode_db_user, comment_id)
1857 1854 raise HTTPNotFound()
@@ -1,746 +1,746 b''
1 1 // comments.less
2 2 // For use in RhodeCode applications;
3 3 // see style guide documentation for guidelines.
4 4
5 5
6 6 // Comments
7 7 @comment-outdated-opacity: 1.0;
8 8
9 9 .comments {
10 10 width: 100%;
11 11 }
12 12
13 13 .comments-heading {
14 14 margin-bottom: -1px;
15 15 background: @grey6;
16 16 display: block;
17 17 padding: 10px 0px;
18 18 font-size: 18px
19 19 }
20 20
21 21 #comment-tr-show {
22 22 padding: 5px 0;
23 23 }
24 24
25 25 tr.inline-comments div {
26 26 max-width: 100%;
27 27
28 28 p {
29 29 white-space: normal;
30 30 }
31 31
32 32 code, pre, .code, dd {
33 33 overflow-x: auto;
34 34 width: 1062px;
35 35 }
36 36
37 37 dd {
38 38 width: auto;
39 39 }
40 40 }
41 41
42 42 #injected_page_comments {
43 43 .comment-previous-link,
44 44 .comment-next-link,
45 45 .comment-links-divider {
46 46 display: none;
47 47 }
48 48 }
49 49
50 50 .add-comment {
51 51 margin-bottom: 10px;
52 52 }
53 53 .hide-comment-button .add-comment {
54 54 display: none;
55 55 }
56 56
57 57 .comment-bubble {
58 58 color: @grey4;
59 59 margin-top: 4px;
60 60 margin-right: 30px;
61 61 visibility: hidden;
62 62 }
63 63
64 64 .comment-draft {
65 65 float: left;
66 66 margin-right: 10px;
67 67 font-weight: 400;
68 68 color: @color-draft;
69 69 }
70 70
71 71 .comment-new {
72 72 float: left;
73 73 margin-right: 10px;
74 74 font-weight: 400;
75 75 color: @color-new;
76 76 }
77 77
78 78 .comment-label {
79 79 float: left;
80 80
81 81 padding: 0 8px 0 0;
82 82 min-height: 0;
83 83
84 84 text-align: center;
85 85 font-size: 10px;
86 86
87 87 font-family: @text-italic;
88 88 font-style: italic;
89 89 background: #fff none;
90 90 color: @grey3;
91 91 white-space: nowrap;
92 92
93 93 text-transform: uppercase;
94 94 min-width: 50px;
95 95
96 96 &.todo {
97 97 color: @color5;
98 98 font-style: italic;
99 99 font-weight: @text-bold-italic-weight;
100 100 font-family: @text-bold-italic;
101 101 }
102 102
103 103 .resolve {
104 104 cursor: pointer;
105 105 text-decoration: underline;
106 106 }
107 107
108 108 .resolved {
109 109 text-decoration: line-through;
110 110 color: @color1;
111 111 }
112 112 .resolved a {
113 113 text-decoration: line-through;
114 114 color: @color1;
115 115 }
116 116 .resolve-text {
117 117 color: @color1;
118 118 margin: 2px 8px;
119 119 font-family: @text-italic;
120 120 font-style: italic;
121 121 }
122 122 }
123 123
124 124 .has-spacer-after {
125 125 &:after {
126 126 content: ' | ';
127 127 color: @grey5;
128 128 }
129 129 }
130 130
131 131 .has-spacer-before {
132 132 &:before {
133 133 content: ' | ';
134 134 color: @grey5;
135 135 }
136 136 }
137 137
138 138 .comment {
139 139
140 140 &.comment-general {
141 141 border: 1px solid @grey5;
142 142 padding: 5px 5px 5px 5px;
143 143 }
144 144
145 145 margin: @padding 0;
146 146 padding: 4px 0 0 0;
147 147 line-height: 1em;
148 148
149 149 .rc-user {
150 150 min-width: 0;
151 151 margin: 0px .5em 0 0;
152 152
153 153 .user {
154 154 display: inline;
155 155 }
156 156 }
157 157
158 158 .meta {
159 159 position: relative;
160 160 width: 100%;
161 161 border-bottom: 1px solid @grey5;
162 162 margin: -5px 0px;
163 163 line-height: 24px;
164 164
165 165 &:hover .permalink {
166 166 visibility: visible;
167 167 color: @rcblue;
168 168 }
169 169 }
170 170
171 171 .author,
172 172 .date {
173 173 display: inline;
174 174
175 175 &:after {
176 176 content: ' | ';
177 177 color: @grey5;
178 178 }
179 179 }
180 180
181 181 .author-general img {
182 182 top: 3px;
183 183 }
184 184 .author-inline img {
185 185 top: 3px;
186 186 }
187 187
188 188 .status-change,
189 189 .permalink,
190 190 .changeset-status-lbl {
191 191 display: inline;
192 192 }
193 193
194 194 .permalink {
195 195 visibility: hidden;
196 196 }
197 197
198 198 .comment-links-divider {
199 199 display: inline;
200 200 }
201 201
202 202 .comment-links-block {
203 203 float:right;
204 204 text-align: right;
205 205 min-width: 85px;
206 206
207 207 [class^="icon-"]:before,
208 208 [class*=" icon-"]:before {
209 209 margin-left: 0;
210 210 margin-right: 0;
211 211 }
212 212 }
213 213
214 214 .comment-previous-link {
215 215 display: inline-block;
216 216
217 217 .arrow_comment_link{
218 218 cursor: pointer;
219 219 i {
220 220 font-size:10px;
221 221 }
222 222 }
223 223 .arrow_comment_link.disabled {
224 224 cursor: default;
225 225 color: @grey5;
226 226 }
227 227 }
228 228
229 229 .comment-next-link {
230 230 display: inline-block;
231 231
232 232 .arrow_comment_link{
233 233 cursor: pointer;
234 234 i {
235 235 font-size:10px;
236 236 }
237 237 }
238 238 .arrow_comment_link.disabled {
239 239 cursor: default;
240 240 color: @grey5;
241 241 }
242 242 }
243 243
244 244 .delete-comment {
245 245 display: inline-block;
246 246 color: @rcblue;
247 247
248 248 &:hover {
249 249 cursor: pointer;
250 250 }
251 251 }
252 252
253 253 .text {
254 254 clear: both;
255 255 .border-radius(@border-radius);
256 256 .box-sizing(border-box);
257 257
258 258 .markdown-block p,
259 259 .rst-block p {
260 260 margin: .5em 0 !important;
261 261 // TODO: lisa: This is needed because of other rst !important rules :[
262 262 }
263 263 }
264 264
265 265 .pr-version {
266 266 display: inline-block;
267 267 }
268 268 .pr-version-inline {
269 269 display: inline-block;
270 270 }
271 271 .pr-version-num {
272 272 font-size: 10px;
273 273 }
274 274 }
275 275
276 276 @comment-padding: 5px;
277 277
278 278 .general-comments {
279 279 .comment-outdated {
280 280 opacity: @comment-outdated-opacity;
281 281 }
282 282
283 283 .comment-outdated-label {
284 284 color: @grey3;
285 285 padding-right: 4px;
286 286 }
287 287 }
288 288
289 289 .inline-comments {
290 290
291 291 .comment {
292 292 margin: 0;
293 293 }
294 294
295 295 .comment-outdated {
296 296 opacity: @comment-outdated-opacity;
297 297 }
298 298
299 299 .comment-outdated-label {
300 300 color: @grey3;
301 301 padding-right: 4px;
302 302 }
303 303
304 304 .comment-inline {
305 305
306 306 &:first-child {
307 307 margin: 4px 4px 0 4px;
308 308 border-top: 1px solid @grey5;
309 309 border-bottom: 0 solid @grey5;
310 310 border-left: 1px solid @grey5;
311 311 border-right: 1px solid @grey5;
312 312 .border-radius-top(4px);
313 313 }
314 314
315 315 &:only-child {
316 316 margin: 4px 4px 0 4px;
317 317 border-top: 1px solid @grey5;
318 318 border-bottom: 0 solid @grey5;
319 319 border-left: 1px solid @grey5;
320 320 border-right: 1px solid @grey5;
321 321 .border-radius-top(4px);
322 322 }
323 323
324 324 background: white;
325 325 padding: @comment-padding @comment-padding;
326 326 margin: 0 4px 0 4px;
327 327 border-top: 0 solid @grey5;
328 328 border-bottom: 0 solid @grey5;
329 329 border-left: 1px solid @grey5;
330 330 border-right: 1px solid @grey5;
331 331
332 332 .text {
333 333 border: none;
334 334 }
335 335
336 336 .meta {
337 337 border-bottom: 1px solid @grey6;
338 338 margin: -5px 0px;
339 339 line-height: 24px;
340 340 }
341 341
342 342 }
343 343 .comment-selected {
344 344 border-left: 6px solid @comment-highlight-color;
345 345 }
346 346
347 347 .comment-inline-form-open {
348 348 display: block !important;
349 349 }
350 350
351 351 .comment-inline-form {
352 352 display: none;
353 353 }
354 354
355 355 .comment-inline-form-edit {
356 356 padding: 0;
357 357 margin: 0px 4px 2px 4px;
358 358 }
359 359
360 360 .reply-thread-container {
361 361 display: table;
362 362 width: 100%;
363 363 padding: 0px 4px 4px 4px;
364 364 }
365 365
366 366 .reply-thread-container-wrapper {
367 367 margin: 0 4px 4px 4px;
368 368 border-top: 0 solid @grey5;
369 369 border-bottom: 1px solid @grey5;
370 370 border-left: 1px solid @grey5;
371 371 border-right: 1px solid @grey5;
372 372 .border-radius-bottom(4px);
373 373 }
374 374
375 375 .reply-thread-gravatar {
376 376 display: table-cell;
377 377 width: 24px;
378 378 height: 24px;
379 379 padding-top: 10px;
380 380 padding-left: 10px;
381 381 background-color: #eeeeee;
382 382 vertical-align: top;
383 383 }
384 384
385 385 .reply-thread-reply-button {
386 386 display: table-cell;
387 387 width: 100%;
388 388 height: 33px;
389 389 padding: 3px 8px;
390 390 margin-left: 8px;
391 391 background-color: #eeeeee;
392 392 }
393 393
394 394 .reply-thread-reply-button .cb-comment-add-button {
395 395 border-radius: 4px;
396 396 width: 100%;
397 397 padding: 6px 2px;
398 398 text-align: left;
399 399 cursor: text;
400 400 color: @grey3;
401 401 }
402 402 .reply-thread-reply-button .cb-comment-add-button:hover {
403 403 background-color: white;
404 404 color: @grey2;
405 405 }
406 406
407 407 .reply-thread-last {
408 408 display: table-cell;
409 409 width: 10px;
410 410 }
411 411
412 412 /* Hide reply box when it's a first element,
413 413 can happen when drafts are saved but not shown to specific user,
414 414 or there are outdated comments hidden
415 415 */
416 416 .reply-thread-container-wrapper:first-child:not(.comment-form-active) {
417 417 display: none;
418 418 }
419 419
420 420 .reply-thread-container-wrapper.comment-outdated {
421 421 display: none
422 422 }
423 423
424 424 /* hide add comment button when form is open */
425 425 .comment-inline-form-open ~ .cb-comment-add-button {
426 426 display: none;
427 427 }
428 428
429 429 /* hide add comment button when only comment is being deleted */
430 430 .comment-deleting:first-child + .cb-comment-add-button {
431 431 display: none;
432 432 }
433 433
434 434 /* hide add comment button when form but no comments */
435 435 .comment-inline-form:first-child + .cb-comment-add-button {
436 436 display: none;
437 437 }
438 438
439 439 }
440 440
441 441 .show-outdated-comments {
442 442 display: inline;
443 443 color: @rcblue;
444 444 }
445 445
446 446 // Comment Form
447 447 div.comment-form {
448 448 margin-top: 20px;
449 449 }
450 450
451 451 .comment-form strong {
452 452 display: block;
453 453 margin-bottom: 15px;
454 454 }
455 455
456 456 .comment-form textarea {
457 457 width: 100%;
458 458 height: 100px;
459 459 font-family: @text-monospace;
460 460 }
461 461
462 462 form.comment-form {
463 463 margin-top: 10px;
464 464 margin-left: 10px;
465 465 }
466 466
467 467 .comment-inline-form .comment-block-ta,
468 468 .comment-form .comment-block-ta,
469 469 .comment-form .preview-box {
470 470 .border-radius(@border-radius);
471 471 .box-sizing(border-box);
472 472 background-color: white;
473 473 }
474 474
475 475 .comment-form-submit {
476 476 margin-top: 5px;
477 477 margin-left: 525px;
478 478 }
479 479
480 480 .file-comments {
481 481 display: none;
482 482 }
483 483
484 484 .comment-form .preview-box.unloaded,
485 485 .comment-inline-form .preview-box.unloaded {
486 486 height: 50px;
487 487 text-align: center;
488 488 padding: 20px;
489 489 background-color: white;
490 490 }
491 491
492 492 .comment-footer {
493 493 display: table;
494 494 width: 100%;
495 495 height: 42px;
496 496
497 497 .comment-status-box,
498 498 .cancel-button {
499 499 display: inline-block;
500 500 }
501 501
502 502 .comment-status-box {
503 503 margin-left: 10px;
504 504 }
505 505
506 506 .action-buttons {
507 507 display: table-cell;
508 508 padding: 5px 0 5px 2px;
509 509 }
510 510
511 511 .toolbar-text {
512 height: 42px;
512 height: 28px;
513 513 display: table-cell;
514 vertical-align: bottom;
514 vertical-align: baseline;
515 515 font-size: 11px;
516 516 color: @grey4;
517 517 text-align: right;
518 518
519 519 a {
520 520 color: @grey4;
521 521 }
522 522
523 523 }
524 524
525 525 .action-buttons-extra {
526 526 display: inline-block;
527 527 }
528 528 }
529 529
530 530 .comment-form {
531 531
532 532 .comment {
533 533 margin-left: 10px;
534 534 }
535 535
536 536 .comment-help {
537 537 color: @grey4;
538 538 padding: 5px 0 5px 0;
539 539 }
540 540
541 541 .comment-title {
542 542 padding: 5px 0 5px 0;
543 543 }
544 544
545 545 .comment-button {
546 546 display: inline-block;
547 547 }
548 548
549 549 .comment-button-input {
550 550 margin-right: 0;
551 551 }
552 552
553 553 #save_general {
554 554 margin-left: -6px;
555 555 }
556 556
557 557 }
558 558
559 559
560 560 .comment-form-login {
561 561 .comment-help {
562 562 padding: 0.7em; //same as the button
563 563 }
564 564
565 565 div.clearfix {
566 566 clear: both;
567 567 width: 100%;
568 568 display: block;
569 569 }
570 570 }
571 571
572 572 .comment-version-select {
573 573 margin: 0px;
574 574 border-radius: inherit;
575 575 border-color: @grey6;
576 576 height: 20px;
577 577 }
578 578
579 579 .comment-type {
580 580 margin: 0px;
581 581 border-radius: inherit;
582 582 border-color: @grey6;
583 583 }
584 584
585 585 .preview-box {
586 586 min-height: 105px;
587 587 margin-bottom: 15px;
588 588 background-color: white;
589 589 .border-radius(@border-radius);
590 590 .box-sizing(border-box);
591 591 }
592 592
593 593 .add-another-button {
594 594 margin-left: 10px;
595 595 margin-top: 10px;
596 596 margin-bottom: 10px;
597 597 }
598 598
599 599 .comment .buttons {
600 600 float: right;
601 601 margin: -1px 0px 0px 0px;
602 602 }
603 603
604 604 // Inline Comment Form
605 605 .injected_diff .comment-inline-form,
606 606 .comment-inline-form {
607 607 background-color: white;
608 608 margin-top: 4px;
609 609 margin-bottom: 10px;
610 610 }
611 611
612 612 .inline-form {
613 613 padding: 10px 7px;
614 614 }
615 615
616 616 .inline-form div {
617 617 max-width: 100%;
618 618 }
619 619
620 620 .overlay {
621 621 display: none;
622 622 position: absolute;
623 623 width: 100%;
624 624 text-align: center;
625 625 vertical-align: middle;
626 626 font-size: 16px;
627 627 background: none repeat scroll 0 0 white;
628 628
629 629 &.submitting {
630 630 display: block;
631 631 opacity: 0.5;
632 632 z-index: 100;
633 633 }
634 634 }
635 635 .comment-inline-form .overlay.submitting .overlay-text {
636 636 margin-top: 5%;
637 637 }
638 638
639 639 .comment-inline-form .clearfix,
640 640 .comment-form .clearfix {
641 641 .border-radius(@border-radius);
642 642 margin: 0px;
643 643 }
644 644
645 645
646 646 .hide-inline-form-button {
647 647 margin-left: 5px;
648 648 }
649 649 .comment-button .hide-inline-form {
650 650 background: white;
651 651 }
652 652
653 653 .comment-area {
654 654 padding: 6px 8px;
655 655 border: 1px solid @grey5;
656 656 .border-radius(@border-radius);
657 657
658 658 .resolve-action {
659 659 padding: 1px 0px 0px 6px;
660 660 }
661 661
662 662 }
663 663
664 664 comment-area-text {
665 665 color: @grey3;
666 666 }
667 667
668 668 .comment-area-header {
669 669 height: 35px;
670 670 border-bottom: 1px solid @grey5;
671 671 }
672 672
673 673 .comment-area-header .nav-links {
674 674 display: flex;
675 675 flex-flow: row wrap;
676 676 -webkit-flex-flow: row wrap;
677 677 width: 100%;
678 678 border: none;
679 679 }
680 680
681 681 .comment-area-footer {
682 682 min-height: 30px;
683 683 }
684 684
685 685 .comment-footer .toolbar {
686 686
687 687 }
688 688
689 689 .comment-attachment-uploader {
690 690 border: 1px dashed white;
691 691 border-radius: @border-radius;
692 692 margin-top: -10px;
693 693 line-height: 30px;
694 694 &.dz-drag-hover {
695 695 border-color: @grey3;
696 696 }
697 697
698 698 .dz-error-message {
699 699 padding-top: 0;
700 700 }
701 701 }
702 702
703 703 .comment-attachment-text {
704 704 clear: both;
705 705 font-size: 11px;
706 706 color: #8F8F8F;
707 707 width: 100%;
708 708 .pick-attachment {
709 709 color: #8F8F8F;
710 710 }
711 711 .pick-attachment:hover {
712 712 color: @rcblue;
713 713 }
714 714 }
715 715
716 716 .nav-links {
717 717 padding: 0;
718 718 margin: 0;
719 719 list-style: none;
720 720 height: auto;
721 721 border-bottom: 1px solid @grey5;
722 722 }
723 723 .nav-links li {
724 724 display: inline-block;
725 725 list-style-type: none;
726 726 }
727 727
728 728 .nav-links li a.disabled {
729 729 cursor: not-allowed;
730 730 }
731 731
732 732 .nav-links li.active a {
733 733 border-bottom: 2px solid @rcblue;
734 734 color: #000;
735 735 font-weight: 600;
736 736 }
737 737 .nav-links li a {
738 738 display: inline-block;
739 739 padding: 0px 10px 5px 10px;
740 740 margin-bottom: -1px;
741 741 font-size: 14px;
742 742 line-height: 28px;
743 743 color: #8f8f8f;
744 744 border-bottom: 2px solid transparent;
745 745 }
746 746
@@ -1,403 +1,404 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('favicon', '/favicon.ico', []);
16 16 pyroutes.register('robots', '/robots.txt', []);
17 17 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
18 18 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
19 19 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
20 20 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
21 21 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
22 22 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
23 23 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/_settings/integrations', ['repo_group_name']);
24 24 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']);
25 25 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/_settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
26 26 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/_settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
27 27 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/_settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
28 28 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
29 29 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
30 30 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
31 31 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
32 32 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
33 33 pyroutes.register('hovercard_user', '/_hovercard/user/%(user_id)s', ['user_id']);
34 34 pyroutes.register('hovercard_username', '/_hovercard/username/%(username)s', ['username']);
35 35 pyroutes.register('hovercard_user_group', '/_hovercard/user_group/%(user_group_id)s', ['user_group_id']);
36 36 pyroutes.register('hovercard_pull_request', '/_hovercard/pull_request/%(pull_request_id)s', ['pull_request_id']);
37 37 pyroutes.register('hovercard_repo_commit', '/_hovercard/commit/%(repo_name)s/%(commit_id)s', ['repo_name', 'commit_id']);
38 38 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
39 39 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
40 40 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
41 41 pyroutes.register('ops_ping_legacy', '/_admin/ping', []);
42 42 pyroutes.register('ops_error_test_legacy', '/_admin/error_test', []);
43 43 pyroutes.register('admin_home', '/_admin', []);
44 44 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
45 45 pyroutes.register('admin_audit_log_entry', '/_admin/audit_logs/%(audit_log_id)s', ['audit_log_id']);
46 46 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
47 47 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
48 48 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
49 49 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
50 50 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
51 51 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
52 52 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
53 53 pyroutes.register('admin_settings_exception_tracker', '/_admin/settings/exceptions', []);
54 54 pyroutes.register('admin_settings_exception_tracker_delete_all', '/_admin/settings/exceptions/delete', []);
55 55 pyroutes.register('admin_settings_exception_tracker_show', '/_admin/settings/exceptions/%(exception_id)s', ['exception_id']);
56 56 pyroutes.register('admin_settings_exception_tracker_delete', '/_admin/settings/exceptions/%(exception_id)s/delete', ['exception_id']);
57 57 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
58 58 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
59 59 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
60 60 pyroutes.register('admin_settings_process_management_data', '/_admin/settings/process_management/data', []);
61 61 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
62 62 pyroutes.register('admin_settings_process_management_master_signal', '/_admin/settings/process_management/master_signal', []);
63 63 pyroutes.register('admin_defaults_repositories', '/_admin/defaults/repositories', []);
64 64 pyroutes.register('admin_defaults_repositories_update', '/_admin/defaults/repositories/update', []);
65 65 pyroutes.register('admin_settings', '/_admin/settings', []);
66 66 pyroutes.register('admin_settings_update', '/_admin/settings/update', []);
67 67 pyroutes.register('admin_settings_global', '/_admin/settings/global', []);
68 68 pyroutes.register('admin_settings_global_update', '/_admin/settings/global/update', []);
69 69 pyroutes.register('admin_settings_vcs', '/_admin/settings/vcs', []);
70 70 pyroutes.register('admin_settings_vcs_update', '/_admin/settings/vcs/update', []);
71 71 pyroutes.register('admin_settings_vcs_svn_pattern_delete', '/_admin/settings/vcs/svn_pattern_delete', []);
72 72 pyroutes.register('admin_settings_mapping', '/_admin/settings/mapping', []);
73 73 pyroutes.register('admin_settings_mapping_update', '/_admin/settings/mapping/update', []);
74 74 pyroutes.register('admin_settings_visual', '/_admin/settings/visual', []);
75 75 pyroutes.register('admin_settings_visual_update', '/_admin/settings/visual/update', []);
76 76 pyroutes.register('admin_settings_issuetracker', '/_admin/settings/issue-tracker', []);
77 77 pyroutes.register('admin_settings_issuetracker_update', '/_admin/settings/issue-tracker/update', []);
78 78 pyroutes.register('admin_settings_issuetracker_test', '/_admin/settings/issue-tracker/test', []);
79 79 pyroutes.register('admin_settings_issuetracker_delete', '/_admin/settings/issue-tracker/delete', []);
80 80 pyroutes.register('admin_settings_email', '/_admin/settings/email', []);
81 81 pyroutes.register('admin_settings_email_update', '/_admin/settings/email/update', []);
82 82 pyroutes.register('admin_settings_hooks', '/_admin/settings/hooks', []);
83 83 pyroutes.register('admin_settings_hooks_update', '/_admin/settings/hooks/update', []);
84 84 pyroutes.register('admin_settings_hooks_delete', '/_admin/settings/hooks/delete', []);
85 85 pyroutes.register('admin_settings_search', '/_admin/settings/search', []);
86 86 pyroutes.register('admin_settings_labs', '/_admin/settings/labs', []);
87 87 pyroutes.register('admin_settings_labs_update', '/_admin/settings/labs/update', []);
88 88 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
89 89 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
90 90 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
91 91 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
92 92 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
93 93 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
94 94 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
95 95 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
96 96 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
97 97 pyroutes.register('admin_permissions_ssh_keys', '/_admin/permissions/ssh_keys', []);
98 98 pyroutes.register('admin_permissions_ssh_keys_data', '/_admin/permissions/ssh_keys/data', []);
99 99 pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []);
100 100 pyroutes.register('users', '/_admin/users', []);
101 101 pyroutes.register('users_data', '/_admin/users_data', []);
102 102 pyroutes.register('users_create', '/_admin/users/create', []);
103 103 pyroutes.register('users_new', '/_admin/users/new', []);
104 104 pyroutes.register('user_edit', '/_admin/users/%(user_id)s/edit', ['user_id']);
105 105 pyroutes.register('user_edit_advanced', '/_admin/users/%(user_id)s/edit/advanced', ['user_id']);
106 106 pyroutes.register('user_edit_global_perms', '/_admin/users/%(user_id)s/edit/global_permissions', ['user_id']);
107 107 pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']);
108 108 pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']);
109 109 pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']);
110 110 pyroutes.register('user_enable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_enable', ['user_id']);
111 111 pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
112 112 pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
113 113 pyroutes.register('user_notice_dismiss', '/_admin/users/%(user_id)s/notice_dismiss', ['user_id']);
114 114 pyroutes.register('edit_user_auth_tokens_view', '/_admin/users/%(user_id)s/edit/auth_tokens/view', ['user_id']);
115 115 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
116 116 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
117 117 pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
118 118 pyroutes.register('edit_user_ssh_keys_add', '/_admin/users/%(user_id)s/edit/ssh_keys/new', ['user_id']);
119 119 pyroutes.register('edit_user_ssh_keys_delete', '/_admin/users/%(user_id)s/edit/ssh_keys/delete', ['user_id']);
120 120 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
121 121 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
122 122 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
123 123 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
124 124 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
125 125 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
126 126 pyroutes.register('edit_user_perms_summary', '/_admin/users/%(user_id)s/edit/permissions_summary', ['user_id']);
127 127 pyroutes.register('edit_user_perms_summary_json', '/_admin/users/%(user_id)s/edit/permissions_summary/json', ['user_id']);
128 128 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
129 129 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
130 130 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
131 131 pyroutes.register('edit_user_audit_logs_download', '/_admin/users/%(user_id)s/edit/audit/download', ['user_id']);
132 132 pyroutes.register('edit_user_caches', '/_admin/users/%(user_id)s/edit/caches', ['user_id']);
133 133 pyroutes.register('edit_user_caches_update', '/_admin/users/%(user_id)s/edit/caches/update', ['user_id']);
134 134 pyroutes.register('user_groups', '/_admin/user_groups', []);
135 135 pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
136 136 pyroutes.register('user_groups_new', '/_admin/user_groups/new', []);
137 137 pyroutes.register('user_groups_create', '/_admin/user_groups/create', []);
138 138 pyroutes.register('repos', '/_admin/repos', []);
139 139 pyroutes.register('repos_data', '/_admin/repos_data', []);
140 140 pyroutes.register('repo_new', '/_admin/repos/new', []);
141 141 pyroutes.register('repo_create', '/_admin/repos/create', []);
142 142 pyroutes.register('repo_groups', '/_admin/repo_groups', []);
143 143 pyroutes.register('repo_groups_data', '/_admin/repo_groups_data', []);
144 144 pyroutes.register('repo_group_new', '/_admin/repo_group/new', []);
145 145 pyroutes.register('repo_group_create', '/_admin/repo_group/create', []);
146 146 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
147 147 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
148 148 pyroutes.register('channelstream_proxy', '/_channelstream', []);
149 149 pyroutes.register('upload_file', '/_file_store/upload', []);
150 150 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
151 151 pyroutes.register('download_file_by_token', '/_file_store/token-download/%(_auth_token)s/%(fid)s', ['_auth_token', 'fid']);
152 152 pyroutes.register('logout', '/_admin/logout', []);
153 153 pyroutes.register('reset_password', '/_admin/password_reset', []);
154 154 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
155 155 pyroutes.register('home', '/', []);
156 156 pyroutes.register('main_page_repos_data', '/_home_repos', []);
157 157 pyroutes.register('main_page_repo_groups_data', '/_home_repo_groups', []);
158 158 pyroutes.register('user_autocomplete_data', '/_users', []);
159 159 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
160 160 pyroutes.register('repo_list_data', '/_repos', []);
161 161 pyroutes.register('repo_group_list_data', '/_repo_groups', []);
162 162 pyroutes.register('goto_switcher_data', '/_goto_data', []);
163 163 pyroutes.register('markup_preview', '/_markup_preview', []);
164 164 pyroutes.register('file_preview', '/_file_preview', []);
165 165 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
166 166 pyroutes.register('journal', '/_admin/journal', []);
167 167 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
168 168 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
169 169 pyroutes.register('journal_public', '/_admin/public_journal', []);
170 170 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
171 171 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
172 172 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
173 173 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
174 174 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
175 175 pyroutes.register('repo_creating', '/%(repo_name)s/repo_creating', ['repo_name']);
176 176 pyroutes.register('repo_creating_check', '/%(repo_name)s/repo_creating_check', ['repo_name']);
177 177 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
178 178 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
179 179 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
180 180 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
181 181 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
182 182 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
183 183 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
184 184 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
185 185 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
186 186 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
187 187 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
188 188 pyroutes.register('repo_commit_comment_history_view', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_history_id)s/history_view', ['repo_name', 'commit_id', 'comment_history_id']);
189 189 pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
190 190 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
191 191 pyroutes.register('repo_commit_comment_edit', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/edit', ['repo_name', 'commit_id', 'comment_id']);
192 192 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
193 193 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
194 194 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
195 195 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
196 196 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
197 197 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
198 198 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
199 199 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
200 200 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
201 201 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
202 202 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
203 203 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
204 204 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
205 205 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
206 206 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
207 207 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
208 208 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
209 209 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
210 210 pyroutes.register('repo_files_check_head', '/%(repo_name)s/check_head/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
211 211 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
212 212 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
213 213 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
214 214 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
215 215 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
216 216 pyroutes.register('repo_files_upload_file', '/%(repo_name)s/upload_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
217 217 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
218 218 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
219 219 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
220 220 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
221 221 pyroutes.register('repo_commits', '/%(repo_name)s/commits', ['repo_name']);
222 222 pyroutes.register('repo_commits_file', '/%(repo_name)s/commits/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
223 223 pyroutes.register('repo_commits_elements', '/%(repo_name)s/commits_elements', ['repo_name']);
224 224 pyroutes.register('repo_commits_elements_file', '/%(repo_name)s/commits_elements/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
225 225 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
226 226 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
227 227 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
228 228 pyroutes.register('repo_compare', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
229 229 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
230 230 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
231 231 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
232 232 pyroutes.register('repo_fork_new', '/%(repo_name)s/fork', ['repo_name']);
233 233 pyroutes.register('repo_fork_create', '/%(repo_name)s/fork/create', ['repo_name']);
234 234 pyroutes.register('repo_forks_show_all', '/%(repo_name)s/forks', ['repo_name']);
235 235 pyroutes.register('repo_forks_data', '/%(repo_name)s/forks/data', ['repo_name']);
236 236 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
237 237 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
238 238 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
239 239 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
240 240 pyroutes.register('pullrequest_repo_targets', '/%(repo_name)s/pull-request/repo-targets', ['repo_name']);
241 241 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
242 242 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
243 243 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
244 244 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
245 245 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
246 246 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
247 247 pyroutes.register('pullrequest_comment_edit', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/edit', ['repo_name', 'pull_request_id', 'comment_id']);
248 248 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
249 249 pyroutes.register('pullrequest_comments', '/%(repo_name)s/pull-request/%(pull_request_id)s/comments', ['repo_name', 'pull_request_id']);
250 250 pyroutes.register('pullrequest_todos', '/%(repo_name)s/pull-request/%(pull_request_id)s/todos', ['repo_name', 'pull_request_id']);
251 251 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
252 252 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
253 253 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
254 254 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
255 255 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
256 256 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
257 257 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
258 258 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
259 259 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
260 260 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
261 261 pyroutes.register('edit_repo_perms_set_private', '/%(repo_name)s/settings/permissions/set_private', ['repo_name']);
262 262 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
263 263 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
264 264 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
265 265 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
266 266 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
267 267 pyroutes.register('repo_edit_toggle_locking', '/%(repo_name)s/settings/toggle_locking', ['repo_name']);
268 268 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
269 269 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
270 270 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
271 271 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
272 272 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
273 273 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
274 274 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
275 275 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
276 276 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
277 277 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
278 278 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
279 279 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
280 280 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
281 281 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
282 282 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
283 283 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
284 284 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
285 285 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed-rss', ['repo_name']);
286 286 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed-atom', ['repo_name']);
287 287 pyroutes.register('rss_feed_home_old', '/%(repo_name)s/feed/rss', ['repo_name']);
288 288 pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']);
289 289 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
290 290 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
291 291 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
292 292 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
293 293 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
294 294 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
295 295 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
296 296 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
297 297 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
298 298 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
299 299 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
300 300 pyroutes.register('edit_user_group_perms_summary_json', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary/json', ['user_group_id']);
301 301 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
302 302 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
303 303 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
304 304 pyroutes.register('edit_user_group_global_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions/update', ['user_group_id']);
305 305 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
306 306 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
307 307 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
308 308 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
309 309 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
310 310 pyroutes.register('search', '/_admin/search', []);
311 311 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
312 312 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
313 313 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
314 314 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
315 315 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
316 316 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
317 317 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
318 318 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
319 319 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
320 320 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
321 321 pyroutes.register('my_account_auth_tokens_view', '/_admin/my_account/auth_tokens/view', []);
322 322 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
323 323 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
324 324 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
325 325 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
326 326 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
327 327 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
328 328 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
329 329 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
330 330 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
331 331 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
332 332 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
333 333 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
334 334 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
335 335 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
336 336 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
337 337 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
338 338 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
339 339 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
340 340 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
341 341 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
342 342 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
343 343 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
344 344 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
345 345 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
346 346 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
347 347 pyroutes.register('gists_show', '/_admin/gists', []);
348 348 pyroutes.register('gists_new', '/_admin/gists/new', []);
349 349 pyroutes.register('gists_create', '/_admin/gists/create', []);
350 350 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
351 351 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
352 352 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
353 353 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
354 354 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
355 355 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
356 356 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
357 357 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
358 358 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
359 359 pyroutes.register('debug_style_email', '/_admin/debug_style/email/%(email_id)s', ['email_id']);
360 360 pyroutes.register('debug_style_email_plain_rendered', '/_admin/debug_style/email-rendered/%(email_id)s', ['email_id']);
361 361 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
362 362 pyroutes.register('apiv2', '/_admin/api', []);
363 363 pyroutes.register('admin_settings_license', '/_admin/settings/license', []);
364 364 pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []);
365 365 pyroutes.register('login', '/_admin/login', []);
366 366 pyroutes.register('register', '/_admin/register', []);
367 367 pyroutes.register('repo_reviewers_review_rule_new', '/%(repo_name)s/settings/review/rules/new', ['repo_name']);
368 368 pyroutes.register('repo_reviewers_review_rule_edit', '/%(repo_name)s/settings/review/rules/%(rule_id)s', ['repo_name', 'rule_id']);
369 369 pyroutes.register('repo_reviewers_review_rule_delete', '/%(repo_name)s/settings/review/rules/%(rule_id)s/delete', ['repo_name', 'rule_id']);
370 370 pyroutes.register('plugin_admin_chat', '/_admin/plugin_admin_chat/%(action)s', ['action']);
371 371 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
372 372 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
373 373 pyroutes.register('admin_settings_scheduler_show_tasks', '/_admin/settings/scheduler/_tasks', []);
374 374 pyroutes.register('admin_settings_scheduler_show_all', '/_admin/settings/scheduler', []);
375 375 pyroutes.register('admin_settings_scheduler_new', '/_admin/settings/scheduler/new', []);
376 376 pyroutes.register('admin_settings_scheduler_create', '/_admin/settings/scheduler/create', []);
377 377 pyroutes.register('admin_settings_scheduler_edit', '/_admin/settings/scheduler/%(schedule_id)s', ['schedule_id']);
378 378 pyroutes.register('admin_settings_scheduler_update', '/_admin/settings/scheduler/%(schedule_id)s/update', ['schedule_id']);
379 379 pyroutes.register('admin_settings_scheduler_delete', '/_admin/settings/scheduler/%(schedule_id)s/delete', ['schedule_id']);
380 380 pyroutes.register('admin_settings_scheduler_execute', '/_admin/settings/scheduler/%(schedule_id)s/execute', ['schedule_id']);
381 381 pyroutes.register('admin_settings_automation', '/_admin/settings/automation', []);
382 382 pyroutes.register('admin_settings_automation_update', '/_admin/settings/automation/%(entry_id)s/update', ['entry_id']);
383 383 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
384 384 pyroutes.register('admin_permissions_branch_update', '/_admin/permissions/branch/update', []);
385 385 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
386 386 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
387 387 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
388 388 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
389 389 pyroutes.register('pullrequest_draft_comments_submit', '/%(repo_name)s/pull-request/%(pull_request_id)s/draft_comments_submit', ['repo_name', 'pull_request_id']);
390 pyroutes.register('commit_draft_comments_submit', '/%(repo_name)s/changeset/%(commit_id)s/draft_comments_submit', ['repo_name', 'commit_id']);
390 391 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
391 392 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
392 393 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
393 394 pyroutes.register('repo_artifacts_get', '/%(repo_name)s/artifacts/download/%(uid)s', ['repo_name', 'uid']);
394 395 pyroutes.register('repo_artifacts_store', '/%(repo_name)s/artifacts/store', ['repo_name']);
395 396 pyroutes.register('repo_artifacts_info', '/%(repo_name)s/artifacts/info/%(uid)s', ['repo_name', 'uid']);
396 397 pyroutes.register('repo_artifacts_delete', '/%(repo_name)s/artifacts/delete/%(uid)s', ['repo_name', 'uid']);
397 398 pyroutes.register('repo_artifacts_update', '/%(repo_name)s/artifacts/update/%(uid)s', ['repo_name', 'uid']);
398 399 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
399 400 pyroutes.register('repo_automation_update', '/%(repo_name)s/settings/automation/%(entry_id)s/update', ['repo_name', 'entry_id']);
400 401 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
401 402 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
402 403 pyroutes.register('edit_repo_perms_branch_delete', '/%(repo_name)s/settings/branch_permissions/%(rule_id)s/delete', ['repo_name', 'rule_id']);
403 404 }
@@ -1,1473 +1,1486 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 var firefoxAnchorFix = function() {
20 20 // hack to make anchor links behave properly on firefox, in our inline
21 21 // comments generation when comments are injected firefox is misbehaving
22 22 // when jumping to anchor links
23 23 if (location.href.indexOf('#') > -1) {
24 24 location.href += '';
25 25 }
26 26 };
27 27
28 28 var linkifyComments = function(comments) {
29 29 var firstCommentId = null;
30 30 if (comments) {
31 31 firstCommentId = $(comments[0]).data('comment-id');
32 32 }
33 33
34 34 if (firstCommentId){
35 35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 36 }
37 37 };
38 38
39 39 var bindToggleButtons = function() {
40 40 $('.comment-toggle').on('click', function() {
41 41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 42 });
43 43 };
44 44
45 45
46 46
47 47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
48 48 failHandler = failHandler || function() {};
49 49 postData = toQueryString(postData);
50 50 var request = $.ajax({
51 51 url: url,
52 52 type: 'POST',
53 53 data: postData,
54 54 headers: {'X-PARTIAL-XHR': true}
55 55 })
56 56 .done(function (data) {
57 57 successHandler(data);
58 58 })
59 59 .fail(function (data, textStatus, errorThrown) {
60 60 failHandler(data, textStatus, errorThrown)
61 61 });
62 62 return request;
63 63 };
64 64
65 65
66 66
67 67
68 68 /* Comment form for main and inline comments */
69 69 (function(mod) {
70 70
71 71 if (typeof exports == "object" && typeof module == "object") {
72 72 // CommonJS
73 73 module.exports = mod();
74 74 }
75 75 else {
76 76 // Plain browser env
77 77 (this || window).CommentForm = mod();
78 78 }
79 79
80 80 })(function() {
81 81 "use strict";
82 82
83 83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
84 84
85 85 if (!(this instanceof CommentForm)) {
86 86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
87 87 }
88 88
89 89 // bind the element instance to our Form
90 90 $(formElement).get(0).CommentForm = this;
91 91
92 92 this.withLineNo = function(selector) {
93 93 var lineNo = this.lineNo;
94 94 if (lineNo === undefined) {
95 95 return selector
96 96 } else {
97 97 return selector + '_' + lineNo;
98 98 }
99 99 };
100 100
101 101 this.commitId = commitId;
102 102 this.pullRequestId = pullRequestId;
103 103 this.lineNo = lineNo;
104 104 this.initAutocompleteActions = initAutocompleteActions;
105 105
106 106 this.previewButton = this.withLineNo('#preview-btn');
107 107 this.previewContainer = this.withLineNo('#preview-container');
108 108
109 109 this.previewBoxSelector = this.withLineNo('#preview-box');
110 110
111 111 this.editButton = this.withLineNo('#edit-btn');
112 112 this.editContainer = this.withLineNo('#edit-container');
113 113 this.cancelButton = this.withLineNo('#cancel-btn');
114 114 this.commentType = this.withLineNo('#comment_type');
115 115
116 116 this.resolvesId = null;
117 117 this.resolvesActionId = null;
118 118
119 119 this.closesPr = '#close_pull_request';
120 120
121 121 this.cmBox = this.withLineNo('#text');
122 122 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
123 123
124 124 this.statusChange = this.withLineNo('#change_status');
125 125
126 126 this.submitForm = formElement;
127 127
128 128 this.submitButton = $(this.submitForm).find('.submit-comment-action');
129 129 this.submitButtonText = this.submitButton.val();
130 130
131 131 this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
132 132 this.submitDraftButtonText = this.submitDraftButton.val();
133 133
134 134 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
135 135 {'repo_name': templateContext.repo_name,
136 136 'commit_id': templateContext.commit_data.commit_id});
137 137
138 138 if (edit){
139 139 this.submitDraftButton.hide();
140 140 this.submitButtonText = _gettext('Update Comment');
141 141 $(this.commentType).prop('disabled', true);
142 142 $(this.commentType).addClass('disabled');
143 143 var editInfo =
144 144 '';
145 145 $(editInfo).insertBefore($(this.editButton).parent());
146 146 }
147 147
148 148 if (resolvesCommentId){
149 149 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
150 150 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
151 151 $(this.commentType).prop('disabled', true);
152 152 $(this.commentType).addClass('disabled');
153 153
154 154 // disable select
155 155 setTimeout(function() {
156 156 $(self.statusChange).select2('readonly', true);
157 157 }, 10);
158 158
159 159 var resolvedInfo = (
160 160 '<li class="resolve-action">' +
161 161 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
162 162 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
163 163 '</li>'
164 164 ).format(resolvesCommentId, _gettext('resolve comment'));
165 165 $(resolvedInfo).insertAfter($(this.commentType).parent());
166 166 }
167 167
168 168 // based on commitId, or pullRequestId decide where do we submit
169 169 // out data
170 170 if (this.commitId){
171 171 var pyurl = 'repo_commit_comment_create';
172 172 if(edit){
173 173 pyurl = 'repo_commit_comment_edit';
174 174 }
175 175 this.submitUrl = pyroutes.url(pyurl,
176 176 {'repo_name': templateContext.repo_name,
177 177 'commit_id': this.commitId,
178 178 'comment_id': comment_id});
179 179 this.selfUrl = pyroutes.url('repo_commit',
180 180 {'repo_name': templateContext.repo_name,
181 181 'commit_id': this.commitId});
182 182
183 183 } else if (this.pullRequestId) {
184 184 var pyurl = 'pullrequest_comment_create';
185 185 if(edit){
186 186 pyurl = 'pullrequest_comment_edit';
187 187 }
188 188 this.submitUrl = pyroutes.url(pyurl,
189 189 {'repo_name': templateContext.repo_name,
190 190 'pull_request_id': this.pullRequestId,
191 191 'comment_id': comment_id});
192 192 this.selfUrl = pyroutes.url('pullrequest_show',
193 193 {'repo_name': templateContext.repo_name,
194 194 'pull_request_id': this.pullRequestId});
195 195
196 196 } else {
197 197 throw new Error(
198 198 'CommentForm requires pullRequestId, or commitId to be specified.')
199 199 }
200 200
201 201 // FUNCTIONS and helpers
202 202 var self = this;
203 203
204 204 this.isInline = function(){
205 205 return this.lineNo && this.lineNo != 'general';
206 206 };
207 207
208 208 this.getCmInstance = function(){
209 209 return this.cm
210 210 };
211 211
212 212 this.setPlaceholder = function(placeholder) {
213 213 var cm = this.getCmInstance();
214 214 if (cm){
215 215 cm.setOption('placeholder', placeholder);
216 216 }
217 217 };
218 218
219 219 this.getCommentStatus = function() {
220 220 return $(this.submitForm).find(this.statusChange).val();
221 221 };
222 222
223 223 this.getCommentType = function() {
224 224 return $(this.submitForm).find(this.commentType).val();
225 225 };
226 226
227 227 this.getDraftState = function () {
228 228 var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
229 229 var data = $(submitterElem).data('isDraft');
230 230 return data
231 231 }
232 232
233 233 this.getResolvesId = function() {
234 234 return $(this.submitForm).find(this.resolvesId).val() || null;
235 235 };
236 236
237 237 this.getClosePr = function() {
238 238 return $(this.submitForm).find(this.closesPr).val() || null;
239 239 };
240 240
241 241 this.markCommentResolved = function(resolvedCommentId){
242 242 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
243 243 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
244 244 };
245 245
246 246 this.isAllowedToSubmit = function() {
247 247 var commentDisabled = $(this.submitButton).prop('disabled');
248 248 var draftDisabled = $(this.submitDraftButton).prop('disabled');
249 249 return !commentDisabled && !draftDisabled;
250 250 };
251 251
252 252 this.initStatusChangeSelector = function(){
253 253 var formatChangeStatus = function(state, escapeMarkup) {
254 254 var originalOption = state.element;
255 255 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
256 256 return tmpl
257 257 };
258 258 var formatResult = function(result, container, query, escapeMarkup) {
259 259 return formatChangeStatus(result, escapeMarkup);
260 260 };
261 261
262 262 var formatSelection = function(data, container, escapeMarkup) {
263 263 return formatChangeStatus(data, escapeMarkup);
264 264 };
265 265
266 266 $(this.submitForm).find(this.statusChange).select2({
267 267 placeholder: _gettext('Status Review'),
268 268 formatResult: formatResult,
269 269 formatSelection: formatSelection,
270 270 containerCssClass: "drop-menu status_box_menu",
271 271 dropdownCssClass: "drop-menu-dropdown",
272 272 dropdownAutoWidth: true,
273 273 minimumResultsForSearch: -1
274 274 });
275 275
276 276 $(this.submitForm).find(this.statusChange).on('change', function() {
277 277 var status = self.getCommentStatus();
278 278
279 279 if (status && !self.isInline()) {
280 280 $(self.submitButton).prop('disabled', false);
281 281 $(self.submitDraftButton).prop('disabled', false);
282 282 }
283 283
284 284 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
285 285 self.setPlaceholder(placeholderText)
286 286 })
287 287 };
288 288
289 289 // reset the comment form into it's original state
290 290 this.resetCommentFormState = function(content) {
291 291 content = content || '';
292 292
293 293 $(this.editContainer).show();
294 294 $(this.editButton).parent().addClass('active');
295 295
296 296 $(this.previewContainer).hide();
297 297 $(this.previewButton).parent().removeClass('active');
298 298
299 299 this.setActionButtonsDisabled(true);
300 300 self.cm.setValue(content);
301 301 self.cm.setOption("readOnly", false);
302 302
303 303 if (this.resolvesId) {
304 304 // destroy the resolve action
305 305 $(this.resolvesId).parent().remove();
306 306 }
307 307 // reset closingPR flag
308 308 $('.close-pr-input').remove();
309 309
310 310 $(this.statusChange).select2('readonly', false);
311 311 };
312 312
313 313 this.globalSubmitSuccessCallback = function(comment){
314 314 // default behaviour is to call GLOBAL hook, if it's registered.
315 315 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
316 316 commentFormGlobalSubmitSuccessCallback(comment);
317 317 }
318 318 };
319 319
320 320 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
321 321 return _submitAjaxPOST(url, postData, successHandler, failHandler);
322 322 };
323 323
324 324 // overwrite a submitHandler, we need to do it for inline comments
325 325 this.setHandleFormSubmit = function(callback) {
326 326 this.handleFormSubmit = callback;
327 327 };
328 328
329 329 // overwrite a submitSuccessHandler
330 330 this.setGlobalSubmitSuccessCallback = function(callback) {
331 331 this.globalSubmitSuccessCallback = callback;
332 332 };
333 333
334 334 // default handler for for submit for main comments
335 335 this.handleFormSubmit = function() {
336 336 var text = self.cm.getValue();
337 337 var status = self.getCommentStatus();
338 338 var commentType = self.getCommentType();
339 339 var isDraft = self.getDraftState();
340 340 var resolvesCommentId = self.getResolvesId();
341 341 var closePullRequest = self.getClosePr();
342 342
343 343 if (text === "" && !status) {
344 344 return;
345 345 }
346 346
347 347 var excludeCancelBtn = false;
348 348 var submitEvent = true;
349 349 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
350 350 self.cm.setOption("readOnly", true);
351 351
352 352 var postData = {
353 353 'text': text,
354 354 'changeset_status': status,
355 355 'comment_type': commentType,
356 356 'csrf_token': CSRF_TOKEN
357 357 };
358 358
359 359 if (resolvesCommentId) {
360 360 postData['resolves_comment_id'] = resolvesCommentId;
361 361 }
362 362
363 363 if (closePullRequest) {
364 364 postData['close_pull_request'] = true;
365 365 }
366 366
367 367 // submitSuccess for general comments
368 368 var submitSuccessCallback = function(json_data) {
369 369 // reload page if we change status for single commit.
370 370 if (status && self.commitId) {
371 371 location.reload(true);
372 372 } else {
373 373 // inject newly created comments, json_data is {<comment_id>: {}}
374 374 self.attachGeneralComment(json_data)
375 375
376 376 self.resetCommentFormState();
377 377 timeagoActivate();
378 378 tooltipActivate();
379 379
380 380 // mark visually which comment was resolved
381 381 if (resolvesCommentId) {
382 382 self.markCommentResolved(resolvesCommentId);
383 383 }
384 384 }
385 385
386 386 // run global callback on submit
387 387 self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
388 388
389 389 };
390 390 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
391 391 var prefix = "Error while submitting comment.\n"
392 392 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
393 393 ajaxErrorSwal(message);
394 394 self.resetCommentFormState(text);
395 395 };
396 396 self.submitAjaxPOST(
397 397 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
398 398 };
399 399
400 400 this.previewSuccessCallback = function(o) {
401 401 $(self.previewBoxSelector).html(o);
402 402 $(self.previewBoxSelector).removeClass('unloaded');
403 403
404 404 // swap buttons, making preview active
405 405 $(self.previewButton).parent().addClass('active');
406 406 $(self.editButton).parent().removeClass('active');
407 407
408 408 // unlock buttons
409 409 self.setActionButtonsDisabled(false);
410 410 };
411 411
412 412 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
413 413 excludeCancelBtn = excludeCancelBtn || false;
414 414 submitEvent = submitEvent || false;
415 415
416 416 $(this.editButton).prop('disabled', state);
417 417 $(this.previewButton).prop('disabled', state);
418 418
419 419 if (!excludeCancelBtn) {
420 420 $(this.cancelButton).prop('disabled', state);
421 421 }
422 422
423 423 var submitState = state;
424 424 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
425 425 // if the value of commit review status is set, we allow
426 426 // submit button, but only on Main form, isInline means inline
427 427 submitState = false
428 428 }
429 429
430 430 $(this.submitButton).prop('disabled', submitState);
431 431 $(this.submitDraftButton).prop('disabled', submitState);
432 432
433 433 if (submitEvent) {
434 434 var isDraft = self.getDraftState();
435 435
436 436 if (isDraft) {
437 437 $(this.submitDraftButton).val(_gettext('Saving Draft...'));
438 438 } else {
439 439 $(this.submitButton).val(_gettext('Submitting...'));
440 440 }
441 441
442 442 } else {
443 443 $(this.submitButton).val(this.submitButtonText);
444 444 $(this.submitDraftButton).val(this.submitDraftButtonText);
445 445 }
446 446
447 447 };
448 448
449 449 // lock preview/edit/submit buttons on load, but exclude cancel button
450 450 var excludeCancelBtn = true;
451 451 this.setActionButtonsDisabled(true, excludeCancelBtn);
452 452
453 453 // anonymous users don't have access to initialized CM instance
454 454 if (this.cm !== undefined){
455 455 this.cm.on('change', function(cMirror) {
456 456 if (cMirror.getValue() === "") {
457 457 self.setActionButtonsDisabled(true, excludeCancelBtn)
458 458 } else {
459 459 self.setActionButtonsDisabled(false, excludeCancelBtn)
460 460 }
461 461 });
462 462 }
463 463
464 464 $(this.editButton).on('click', function(e) {
465 465 e.preventDefault();
466 466
467 467 $(self.previewButton).parent().removeClass('active');
468 468 $(self.previewContainer).hide();
469 469
470 470 $(self.editButton).parent().addClass('active');
471 471 $(self.editContainer).show();
472 472
473 473 });
474 474
475 475 $(this.previewButton).on('click', function(e) {
476 476 e.preventDefault();
477 477 var text = self.cm.getValue();
478 478
479 479 if (text === "") {
480 480 return;
481 481 }
482 482
483 483 var postData = {
484 484 'text': text,
485 485 'renderer': templateContext.visual.default_renderer,
486 486 'csrf_token': CSRF_TOKEN
487 487 };
488 488
489 489 // lock ALL buttons on preview
490 490 self.setActionButtonsDisabled(true);
491 491
492 492 $(self.previewBoxSelector).addClass('unloaded');
493 493 $(self.previewBoxSelector).html(_gettext('Loading ...'));
494 494
495 495 $(self.editContainer).hide();
496 496 $(self.previewContainer).show();
497 497
498 498 // by default we reset state of comment preserving the text
499 499 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
500 500 var prefix = "Error while preview of comment.\n"
501 501 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
502 502 ajaxErrorSwal(message);
503 503
504 504 self.resetCommentFormState(text)
505 505 };
506 506 self.submitAjaxPOST(
507 507 self.previewUrl, postData, self.previewSuccessCallback,
508 508 previewFailCallback);
509 509
510 510 $(self.previewButton).parent().addClass('active');
511 511 $(self.editButton).parent().removeClass('active');
512 512 });
513 513
514 514 $(this.submitForm).submit(function(e) {
515 515 e.preventDefault();
516 516 var allowedToSubmit = self.isAllowedToSubmit();
517 517 if (!allowedToSubmit){
518 518 return false;
519 519 }
520 520
521 521 self.handleFormSubmit();
522 522 });
523 523
524 524 }
525 525
526 526 return CommentForm;
527 527 });
528 528
529 529 /* selector for comment versions */
530 530 var initVersionSelector = function(selector, initialData) {
531 531
532 532 var formatResult = function(result, container, query, escapeMarkup) {
533 533
534 534 return renderTemplate('commentVersion', {
535 535 show_disabled: true,
536 536 version: result.comment_version,
537 537 user_name: result.comment_author_username,
538 538 gravatar_url: result.comment_author_gravatar,
539 539 size: 16,
540 540 timeago_component: result.comment_created_on,
541 541 })
542 542 };
543 543
544 544 $(selector).select2({
545 545 placeholder: "Edited",
546 546 containerCssClass: "drop-menu-comment-history",
547 547 dropdownCssClass: "drop-menu-dropdown",
548 548 dropdownAutoWidth: true,
549 549 minimumResultsForSearch: -1,
550 550 data: initialData,
551 551 formatResult: formatResult,
552 552 });
553 553
554 554 $(selector).on('select2-selecting', function (e) {
555 555 // hide the mast as we later do preventDefault()
556 556 $("#select2-drop-mask").click();
557 557 e.preventDefault();
558 558 e.choice.action();
559 559 });
560 560
561 561 $(selector).on("select2-open", function() {
562 562 timeagoActivate();
563 563 });
564 564 };
565 565
566 566 /* comments controller */
567 567 var CommentsController = function() {
568 568 var mainComment = '#text';
569 569 var self = this;
570 570
571 571 this.showVersion = function (comment_id, comment_history_id) {
572 572
573 573 var historyViewUrl = pyroutes.url(
574 574 'repo_commit_comment_history_view',
575 575 {
576 576 'repo_name': templateContext.repo_name,
577 577 'commit_id': comment_id,
578 578 'comment_history_id': comment_history_id,
579 579 }
580 580 );
581 581 successRenderCommit = function (data) {
582 582 SwalNoAnimation.fire({
583 583 html: data,
584 584 title: '',
585 585 });
586 586 };
587 587 failRenderCommit = function () {
588 588 SwalNoAnimation.fire({
589 589 html: 'Error while loading comment history',
590 590 title: '',
591 591 });
592 592 };
593 593 _submitAjaxPOST(
594 594 historyViewUrl, {'csrf_token': CSRF_TOKEN},
595 595 successRenderCommit,
596 596 failRenderCommit
597 597 );
598 598 };
599 599
600 600 this.getLineNumber = function(node) {
601 601 var $node = $(node);
602 602 var lineNo = $node.closest('td').attr('data-line-no');
603 603 if (lineNo === undefined && $node.data('commentInline')){
604 604 lineNo = $node.data('commentLineNo')
605 605 }
606 606
607 607 return lineNo
608 608 };
609 609
610 610 this.scrollToComment = function(node, offset, outdated) {
611 611 if (offset === undefined) {
612 612 offset = 0;
613 613 }
614 614 var outdated = outdated || false;
615 615 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
616 616
617 617 if (!node) {
618 618 node = $('.comment-selected');
619 619 if (!node.length) {
620 620 node = $('comment-current')
621 621 }
622 622 }
623 623
624 624 $wrapper = $(node).closest('div.comment');
625 625
626 626 // show hidden comment when referenced.
627 627 if (!$wrapper.is(':visible')){
628 628 $wrapper.show();
629 629 }
630 630
631 631 $comment = $(node).closest(klass);
632 632 $comments = $(klass);
633 633
634 634 $('.comment-selected').removeClass('comment-selected');
635 635
636 636 var nextIdx = $(klass).index($comment) + offset;
637 637 if (nextIdx >= $comments.length) {
638 638 nextIdx = 0;
639 639 }
640 640 var $next = $(klass).eq(nextIdx);
641 641
642 642 var $cb = $next.closest('.cb');
643 643 $cb.removeClass('cb-collapsed');
644 644
645 645 var $filediffCollapseState = $cb.closest('.filediff').prev();
646 646 $filediffCollapseState.prop('checked', false);
647 647 $next.addClass('comment-selected');
648 648 scrollToElement($next);
649 649 return false;
650 650 };
651 651
652 652 this.nextComment = function(node) {
653 653 return self.scrollToComment(node, 1);
654 654 };
655 655
656 656 this.prevComment = function(node) {
657 657 return self.scrollToComment(node, -1);
658 658 };
659 659
660 660 this.nextOutdatedComment = function(node) {
661 661 return self.scrollToComment(node, 1, true);
662 662 };
663 663
664 664 this.prevOutdatedComment = function(node) {
665 665 return self.scrollToComment(node, -1, true);
666 666 };
667 667
668 668 this.cancelComment = function (node) {
669 669 var $node = $(node);
670 670 var edit = $(this).attr('edit');
671 671 var $inlineComments = $node.closest('div.inline-comments');
672 672
673 673 if (edit) {
674 674 var $general_comments = null;
675 675 if (!$inlineComments.length) {
676 676 $general_comments = $('#comments');
677 677 var $comment = $general_comments.parent().find('div.comment:hidden');
678 678 // show hidden general comment form
679 679 $('#cb-comment-general-form-placeholder').show();
680 680 } else {
681 681 var $comment = $inlineComments.find('div.comment:hidden');
682 682 }
683 683 $comment.show();
684 684 }
685 685 var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper')
686 686 $replyWrapper.removeClass('comment-form-active');
687 687
688 688 var lastComment = $inlineComments.find('.comment-inline').last();
689 689 if ($(lastComment).hasClass('comment-outdated')) {
690 690 $replyWrapper.hide();
691 691 }
692 692
693 693 $node.closest('.comment-inline-form').remove();
694 694 return false;
695 695 };
696 696
697 697 this._deleteComment = function(node) {
698 698 var $node = $(node);
699 699 var $td = $node.closest('td');
700 700 var $comment = $node.closest('.comment');
701 701 var comment_id = $($comment).data('commentId');
702 702 var isDraft = $($comment).data('commentDraft');
703 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
703
704 var pullRequestId = templateContext.pull_request_data.pull_request_id;
705 var commitId = templateContext.commit_data.commit_id;
706
707 if (pullRequestId) {
708 var url = pyroutes.url('pullrequest_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
709 } else if (commitId) {
710 var url = pyroutes.url('repo_commit_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "commit_id": commitId})
711 }
712
704 713 var postData = {
705 714 'csrf_token': CSRF_TOKEN
706 715 };
707 716
708 717 $comment.addClass('comment-deleting');
709 718 $comment.hide('fast');
710 719
711 720 var success = function(response) {
712 721 $comment.remove();
713 722
714 723 if (window.updateSticky !== undefined) {
715 724 // potentially our comments change the active window size, so we
716 725 // notify sticky elements
717 726 updateSticky()
718 727 }
719 728
720 729 if (window.refreshAllComments !== undefined && !isDraft) {
721 730 // if we have this handler, run it, and refresh all comments boxes
722 731 refreshAllComments()
723 732 }
724 733 return false;
725 734 };
726 735
727 736 var failure = function(jqXHR, textStatus, errorThrown) {
728 737 var prefix = "Error while deleting this comment.\n"
729 738 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
730 739 ajaxErrorSwal(message);
731 740
732 741 $comment.show('fast');
733 742 $comment.removeClass('comment-deleting');
734 743 return false;
735 744 };
736 745 ajaxPOST(url, postData, success, failure);
737 746
738 747 }
739 748
740 749 this.deleteComment = function(node) {
741 750 var $comment = $(node).closest('.comment');
742 751 var comment_id = $comment.attr('data-comment-id');
743 752
744 753 SwalNoAnimation.fire({
745 754 title: 'Delete this comment?',
746 755 icon: 'warning',
747 756 showCancelButton: true,
748 757 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
749 758
750 759 }).then(function(result) {
751 760 if (result.value) {
752 761 self._deleteComment(node);
753 762 }
754 763 })
755 764 };
756 765
757 766 this._finalizeDrafts = function(commentIds) {
758 767
759 // remove the drafts so we can lock them before submit.
768 var pullRequestId = templateContext.pull_request_data.pull_request_id;
769 var commitId = templateContext.commit_data.commit_id;
770
771 if (pullRequestId) {
772 var url = pyroutes.url('pullrequest_draft_comments_submit', {"repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
773 } else if (commitId) {
774 var url = pyroutes.url('commit_draft_comments_submit', {"repo_name": templateContext.repo_name, "commit_id": commitId})
775 }
776
777 // remove the drafts so we can lock them before submit.
760 778 $.each(commentIds, function(idx, val){
761 779 $('#comment-{0}'.format(val)).remove();
762 780 })
763 781
764 var params = {
765 'pull_request_id': templateContext.pull_request_data.pull_request_id,
766 'repo_name': templateContext.repo_name,
767 };
768 var url = pyroutes.url('pullrequest_draft_comments_submit', params)
769 782 var postData = {'comments': commentIds, 'csrf_token': CSRF_TOKEN};
770 783
771 784 var submitSuccessCallback = function(json_data) {
772 785 self.attachInlineComment(json_data);
773 786
774 787 if (window.refreshDraftComments !== undefined) {
775 788 // if we have this handler, run it, and refresh all comments boxes
776 789 refreshDraftComments()
777 790 }
778 791
779 792 return false;
780 793 };
781 794
782 795 ajaxPOST(url, postData, submitSuccessCallback)
783 796
784 797 }
785 798
786 799 this.finalizeDrafts = function(commentIds) {
787 800
788 801 SwalNoAnimation.fire({
789 802 title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
790 803 icon: 'warning',
791 804 showCancelButton: true,
792 805 confirmButtonText: _gettext('Yes'),
793 806
794 807 }).then(function(result) {
795 808 if (result.value) {
796 809 self._finalizeDrafts(commentIds);
797 810 }
798 811 })
799 812 };
800 813
801 814 this.toggleWideMode = function (node) {
802 815
803 816 if ($('#content').hasClass('wrapper')) {
804 817 $('#content').removeClass("wrapper");
805 818 $('#content').addClass("wide-mode-wrapper");
806 819 $(node).addClass('btn-success');
807 820 return true
808 821 } else {
809 822 $('#content').removeClass("wide-mode-wrapper");
810 823 $('#content').addClass("wrapper");
811 824 $(node).removeClass('btn-success');
812 825 return false
813 826 }
814 827
815 828 };
816 829
817 830 /**
818 831 * Turn off/on all comments in file diff
819 832 */
820 833 this.toggleDiffComments = function(node) {
821 834 // Find closes filediff container
822 835 var $filediff = $(node).closest('.filediff');
823 836 if ($(node).hasClass('toggle-on')) {
824 837 var show = false;
825 838 } else if ($(node).hasClass('toggle-off')) {
826 839 var show = true;
827 840 }
828 841
829 842 // Toggle each individual comment block, so we can un-toggle single ones
830 843 $.each($filediff.find('.toggle-comment-action'), function(idx, val) {
831 844 self.toggleLineComments($(val), show)
832 845 })
833 846
834 847 // since we change the height of the diff container that has anchor points for upper
835 848 // sticky header, we need to tell it to re-calculate those
836 849 if (window.updateSticky !== undefined) {
837 850 // potentially our comments change the active window size, so we
838 851 // notify sticky elements
839 852 updateSticky()
840 853 }
841 854
842 855 return false;
843 856 }
844 857
845 858 this.toggleLineComments = function(node, show) {
846 859
847 860 var trElem = $(node).closest('tr')
848 861
849 862 if (show === true) {
850 863 // mark outdated comments as visible before the toggle;
851 864 $(trElem).find('.comment-outdated').show();
852 865 $(trElem).removeClass('hide-line-comments');
853 866 } else if (show === false) {
854 867 $(trElem).find('.comment-outdated').hide();
855 868 $(trElem).addClass('hide-line-comments');
856 869 } else {
857 870 // mark outdated comments as visible before the toggle;
858 871 $(trElem).find('.comment-outdated').show();
859 872 $(trElem).toggleClass('hide-line-comments');
860 873 }
861 874
862 875 // since we change the height of the diff container that has anchor points for upper
863 876 // sticky header, we need to tell it to re-calculate those
864 877 if (window.updateSticky !== undefined) {
865 878 // potentially our comments change the active window size, so we
866 879 // notify sticky elements
867 880 updateSticky()
868 881 }
869 882
870 883 };
871 884
872 885 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
873 886 var pullRequestId = templateContext.pull_request_data.pull_request_id;
874 887 var commitId = templateContext.commit_data.commit_id;
875 888
876 889 var commentForm = new CommentForm(
877 890 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
878 891 var cm = commentForm.getCmInstance();
879 892
880 893 if (resolvesCommentId){
881 894 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
882 895 }
883 896
884 897 setTimeout(function() {
885 898 // callbacks
886 899 if (cm !== undefined) {
887 900 commentForm.setPlaceholder(placeholderText);
888 901 if (commentForm.isInline()) {
889 902 cm.focus();
890 903 cm.refresh();
891 904 }
892 905 }
893 906 }, 10);
894 907
895 908 // trigger scrolldown to the resolve comment, since it might be away
896 909 // from the clicked
897 910 if (resolvesCommentId){
898 911 var actionNode = $(commentForm.resolvesActionId).offset();
899 912
900 913 setTimeout(function() {
901 914 if (actionNode) {
902 915 $('body, html').animate({scrollTop: actionNode.top}, 10);
903 916 }
904 917 }, 100);
905 918 }
906 919
907 920 // add dropzone support
908 921 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
909 922 var renderer = templateContext.visual.default_renderer;
910 923 if (renderer == 'rst') {
911 924 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
912 925 if (isRendered){
913 926 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
914 927 }
915 928 } else if (renderer == 'markdown') {
916 929 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
917 930 if (isRendered){
918 931 attachmentUrl = '!' + attachmentUrl;
919 932 }
920 933 } else {
921 934 var attachmentUrl = '{}'.format(attachmentStoreUrl);
922 935 }
923 936 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
924 937
925 938 return false;
926 939 };
927 940
928 941 //see: https://www.dropzonejs.com/#configuration
929 942 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
930 943 {'repo_name': templateContext.repo_name,
931 944 'commit_id': templateContext.commit_data.commit_id})
932 945
933 946 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
934 947 if (previewTmpl !== undefined){
935 948 var selectLink = $(formElement).find('.pick-attachment').get(0);
936 949 $(formElement).find('.comment-attachment-uploader').dropzone({
937 950 url: storeUrl,
938 951 headers: {"X-CSRF-Token": CSRF_TOKEN},
939 952 paramName: function () {
940 953 return "attachment"
941 954 }, // The name that will be used to transfer the file
942 955 clickable: selectLink,
943 956 parallelUploads: 1,
944 957 maxFiles: 10,
945 958 maxFilesize: templateContext.attachment_store.max_file_size_mb,
946 959 uploadMultiple: false,
947 960 autoProcessQueue: true, // if false queue will not be processed automatically.
948 961 createImageThumbnails: false,
949 962 previewTemplate: previewTmpl.innerHTML,
950 963
951 964 accept: function (file, done) {
952 965 done();
953 966 },
954 967 init: function () {
955 968
956 969 this.on("sending", function (file, xhr, formData) {
957 970 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
958 971 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
959 972 });
960 973
961 974 this.on("success", function (file, response) {
962 975 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
963 976 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
964 977
965 978 var isRendered = false;
966 979 var ext = file.name.split('.').pop();
967 980 var imageExts = templateContext.attachment_store.image_ext;
968 981 if (imageExts.indexOf(ext) !== -1){
969 982 isRendered = true;
970 983 }
971 984
972 985 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
973 986 });
974 987
975 988 this.on("error", function (file, errorMessage, xhr) {
976 989 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
977 990
978 991 var error = null;
979 992
980 993 if (xhr !== undefined){
981 994 var httpStatus = xhr.status + " " + xhr.statusText;
982 995 if (xhr !== undefined && xhr.status >= 500) {
983 996 error = httpStatus;
984 997 }
985 998 }
986 999
987 1000 if (error === null) {
988 1001 error = errorMessage.error || errorMessage || httpStatus;
989 1002 }
990 1003 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
991 1004
992 1005 });
993 1006 }
994 1007 });
995 1008 }
996 1009 return commentForm;
997 1010 };
998 1011
999 1012 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
1000 1013
1001 1014 var tmpl = $('#cb-comment-general-form-template').html();
1002 1015 tmpl = tmpl.format(null, 'general');
1003 1016 var $form = $(tmpl);
1004 1017
1005 1018 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
1006 1019 var curForm = $formPlaceholder.find('form');
1007 1020 if (curForm){
1008 1021 curForm.remove();
1009 1022 }
1010 1023 $formPlaceholder.append($form);
1011 1024
1012 1025 var _form = $($form[0]);
1013 1026 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
1014 1027 var edit = false;
1015 1028 var comment_id = null;
1016 1029 var commentForm = this.createCommentForm(
1017 1030 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
1018 1031 commentForm.initStatusChangeSelector();
1019 1032
1020 1033 return commentForm;
1021 1034 };
1022 1035
1023 1036 this.editComment = function(node, line_no, f_path) {
1024 1037 self.edit = true;
1025 1038 var $node = $(node);
1026 1039 var $td = $node.closest('td');
1027 1040
1028 1041 var $comment = $(node).closest('.comment');
1029 1042 var comment_id = $($comment).data('commentId');
1030 1043 var isDraft = $($comment).data('commentDraft');
1031 1044 var $editForm = null
1032 1045
1033 1046 var $comments = $node.closest('div.inline-comments');
1034 1047 var $general_comments = null;
1035 1048
1036 1049 if($comments.length){
1037 1050 // inline comments setup
1038 1051 $editForm = $comments.find('.comment-inline-form');
1039 1052 line_no = self.getLineNumber(node)
1040 1053 }
1041 1054 else{
1042 1055 // general comments setup
1043 1056 $comments = $('#comments');
1044 1057 $editForm = $comments.find('.comment-inline-form');
1045 1058 line_no = $comment[0].id
1046 1059 $('#cb-comment-general-form-placeholder').hide();
1047 1060 }
1048 1061
1049 1062 if ($editForm.length === 0) {
1050 1063
1051 1064 // unhide all comments if they are hidden for a proper REPLY mode
1052 1065 var $filediff = $node.closest('.filediff');
1053 1066 $filediff.removeClass('hide-comments');
1054 1067
1055 1068 $editForm = self.createNewFormWrapper(f_path, line_no);
1056 1069 if(f_path && line_no) {
1057 1070 $editForm.addClass('comment-inline-form-edit')
1058 1071 }
1059 1072
1060 1073 $comment.after($editForm)
1061 1074
1062 1075 var _form = $($editForm[0]).find('form');
1063 1076 var autocompleteActions = ['as_note',];
1064 1077 var commentForm = this.createCommentForm(
1065 1078 _form, line_no, '', autocompleteActions, resolvesCommentId,
1066 1079 this.edit, comment_id);
1067 1080 var old_comment_text_binary = $comment.attr('data-comment-text');
1068 1081 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
1069 1082 commentForm.cm.setValue(old_comment_text);
1070 1083 $comment.hide();
1071 1084 tooltipActivate();
1072 1085
1073 1086 // set a CUSTOM submit handler for inline comment edit action.
1074 1087 commentForm.setHandleFormSubmit(function(o) {
1075 1088 var text = commentForm.cm.getValue();
1076 1089 var commentType = commentForm.getCommentType();
1077 1090
1078 1091 if (text === "") {
1079 1092 return;
1080 1093 }
1081 1094
1082 1095 if (old_comment_text == text) {
1083 1096 SwalNoAnimation.fire({
1084 1097 title: 'Unable to edit comment',
1085 1098 html: _gettext('Comment body was not changed.'),
1086 1099 });
1087 1100 return;
1088 1101 }
1089 1102 var excludeCancelBtn = false;
1090 1103 var submitEvent = true;
1091 1104 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1092 1105 commentForm.cm.setOption("readOnly", true);
1093 1106
1094 1107 // Read last version known
1095 1108 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
1096 1109 var version = versionSelector.data('lastVersion');
1097 1110
1098 1111 if (!version) {
1099 1112 version = 0;
1100 1113 }
1101 1114
1102 1115 var postData = {
1103 1116 'text': text,
1104 1117 'f_path': f_path,
1105 1118 'line': line_no,
1106 1119 'comment_type': commentType,
1107 1120 'draft': isDraft,
1108 1121 'version': version,
1109 1122 'csrf_token': CSRF_TOKEN
1110 1123 };
1111 1124
1112 1125 var submitSuccessCallback = function(json_data) {
1113 1126 $editForm.remove();
1114 1127 $comment.show();
1115 1128 var postData = {
1116 1129 'text': text,
1117 1130 'renderer': $comment.attr('data-comment-renderer'),
1118 1131 'csrf_token': CSRF_TOKEN
1119 1132 };
1120 1133
1121 1134 /* Inject new edited version selector */
1122 1135 var updateCommentVersionDropDown = function () {
1123 1136 var versionSelectId = '#comment_versions_'+comment_id;
1124 1137 var preLoadVersionData = [
1125 1138 {
1126 1139 id: json_data['comment_version'],
1127 1140 text: "v{0}".format(json_data['comment_version']),
1128 1141 action: function () {
1129 1142 Rhodecode.comments.showVersion(
1130 1143 json_data['comment_id'],
1131 1144 json_data['comment_history_id']
1132 1145 )
1133 1146 },
1134 1147 comment_version: json_data['comment_version'],
1135 1148 comment_author_username: json_data['comment_author_username'],
1136 1149 comment_author_gravatar: json_data['comment_author_gravatar'],
1137 1150 comment_created_on: json_data['comment_created_on'],
1138 1151 },
1139 1152 ]
1140 1153
1141 1154
1142 1155 if ($(versionSelectId).data('select2')) {
1143 1156 var oldData = $(versionSelectId).data('select2').opts.data.results;
1144 1157 $(versionSelectId).select2("destroy");
1145 1158 preLoadVersionData = oldData.concat(preLoadVersionData)
1146 1159 }
1147 1160
1148 1161 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1149 1162
1150 1163 $comment.attr('data-comment-text', utf8ToB64(text));
1151 1164
1152 1165 var versionSelector = $('#comment_versions_'+comment_id);
1153 1166
1154 1167 // set lastVersion so we know our last edit version
1155 1168 versionSelector.data('lastVersion', json_data['comment_version'])
1156 1169 versionSelector.parent().show();
1157 1170 }
1158 1171 updateCommentVersionDropDown();
1159 1172
1160 1173 // by default we reset state of comment preserving the text
1161 1174 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1162 1175 var prefix = "Error while editing this comment.\n"
1163 1176 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1164 1177 ajaxErrorSwal(message);
1165 1178 };
1166 1179
1167 1180 var successRenderCommit = function(o){
1168 1181 $comment.show();
1169 1182 $comment[0].lastElementChild.innerHTML = o;
1170 1183 };
1171 1184
1172 1185 var previewUrl = pyroutes.url(
1173 1186 'repo_commit_comment_preview',
1174 1187 {'repo_name': templateContext.repo_name,
1175 1188 'commit_id': templateContext.commit_data.commit_id});
1176 1189
1177 1190 _submitAjaxPOST(
1178 1191 previewUrl, postData, successRenderCommit, failRenderCommit
1179 1192 );
1180 1193
1181 1194 try {
1182 1195 var html = json_data.rendered_text;
1183 1196 var lineno = json_data.line_no;
1184 1197 var target_id = json_data.target_id;
1185 1198
1186 1199 $comments.find('.cb-comment-add-button').before(html);
1187 1200
1188 1201 // run global callback on submit
1189 1202 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1190 1203
1191 1204 } catch (e) {
1192 1205 console.error(e);
1193 1206 }
1194 1207
1195 1208 // re trigger the linkification of next/prev navigation
1196 1209 linkifyComments($('.inline-comment-injected'));
1197 1210 timeagoActivate();
1198 1211 tooltipActivate();
1199 1212
1200 1213 if (window.updateSticky !== undefined) {
1201 1214 // potentially our comments change the active window size, so we
1202 1215 // notify sticky elements
1203 1216 updateSticky()
1204 1217 }
1205 1218
1206 1219 if (window.refreshAllComments !== undefined && !isDraft) {
1207 1220 // if we have this handler, run it, and refresh all comments boxes
1208 1221 refreshAllComments()
1209 1222 }
1210 1223
1211 1224 commentForm.setActionButtonsDisabled(false);
1212 1225
1213 1226 };
1214 1227
1215 1228 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1216 1229 var prefix = "Error while editing comment.\n"
1217 1230 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1218 1231 if (jqXHR.status == 409){
1219 1232 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1220 1233 ajaxErrorSwal(message, 'Comment version mismatch.');
1221 1234 } else {
1222 1235 ajaxErrorSwal(message);
1223 1236 }
1224 1237
1225 1238 commentForm.resetCommentFormState(text)
1226 1239 };
1227 1240 commentForm.submitAjaxPOST(
1228 1241 commentForm.submitUrl, postData,
1229 1242 submitSuccessCallback,
1230 1243 submitFailCallback);
1231 1244 });
1232 1245 }
1233 1246
1234 1247 $editForm.addClass('comment-inline-form-open');
1235 1248 };
1236 1249
1237 1250 this.attachComment = function(json_data) {
1238 1251 var self = this;
1239 1252 $.each(json_data, function(idx, val) {
1240 1253 var json_data_elem = [val]
1241 1254 var isInline = val.comment_f_path && val.comment_lineno
1242 1255
1243 1256 if (isInline) {
1244 1257 self.attachInlineComment(json_data_elem)
1245 1258 } else {
1246 1259 self.attachGeneralComment(json_data_elem)
1247 1260 }
1248 1261 })
1249 1262
1250 1263 }
1251 1264
1252 1265 this.attachGeneralComment = function(json_data) {
1253 1266 $.each(json_data, function(idx, val) {
1254 1267 $('#injected_page_comments').append(val.rendered_text);
1255 1268 })
1256 1269 }
1257 1270
1258 1271 this.attachInlineComment = function(json_data) {
1259 1272
1260 1273 $.each(json_data, function (idx, val) {
1261 1274 var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
1262 1275 var html = val.rendered_text;
1263 1276 var $inlineComments = $('#' + val.target_id)
1264 1277 .find(line_qry)
1265 1278 .find('.inline-comments');
1266 1279
1267 1280 var lastComment = $inlineComments.find('.comment-inline').last();
1268 1281
1269 1282 if (lastComment.length === 0) {
1270 1283 // first comment, we append simply
1271 1284 $inlineComments.find('.reply-thread-container-wrapper').before(html);
1272 1285 } else {
1273 1286 $(lastComment).after(html)
1274 1287 }
1275 1288
1276 1289 })
1277 1290
1278 1291 };
1279 1292
1280 1293 this.createNewFormWrapper = function(f_path, line_no) {
1281 1294 // create a new reply HTML form from template
1282 1295 var tmpl = $('#cb-comment-inline-form-template').html();
1283 1296 tmpl = tmpl.format(escapeHtml(f_path), line_no);
1284 1297 return $(tmpl);
1285 1298 }
1286 1299
1287 1300 this.createComment = function(node, f_path, line_no, resolutionComment) {
1288 1301 self.edit = false;
1289 1302 var $node = $(node);
1290 1303 var $td = $node.closest('td');
1291 1304 var resolvesCommentId = resolutionComment || null;
1292 1305
1293 1306 var $replyForm = $td.find('.comment-inline-form');
1294 1307
1295 1308 // if form isn't existing, we're generating a new one and injecting it.
1296 1309 if ($replyForm.length === 0) {
1297 1310
1298 1311 // unhide/expand all comments if they are hidden for a proper REPLY mode
1299 1312 self.toggleLineComments($node, true);
1300 1313
1301 1314 $replyForm = self.createNewFormWrapper(f_path, line_no);
1302 1315
1303 1316 var $comments = $td.find('.inline-comments');
1304 1317
1305 1318 // There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
1306 1319 if ($comments.length===0) {
1307 1320 var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(f_path, line_no)
1308 1321 var $reply_container = $('#cb-comments-inline-container-template')
1309 1322 $reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
1310 1323 $td.append($($reply_container).html());
1311 1324 }
1312 1325
1313 1326 // default comment button exists, so we prepend the form for leaving initial comment
1314 1327 $td.find('.cb-comment-add-button').before($replyForm);
1315 1328 // set marker, that we have a open form
1316 1329 var $replyWrapper = $td.find('.reply-thread-container-wrapper')
1317 1330 $replyWrapper.addClass('comment-form-active');
1318 1331
1319 1332 var lastComment = $comments.find('.comment-inline').last();
1320 1333 if ($(lastComment).hasClass('comment-outdated')) {
1321 1334 $replyWrapper.show();
1322 1335 }
1323 1336
1324 1337 var _form = $($replyForm[0]).find('form');
1325 1338 var autocompleteActions = ['as_note', 'as_todo'];
1326 1339 var comment_id=null;
1327 1340 var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
1328 1341 var commentForm = self.createCommentForm(
1329 1342 _form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
1330 1343 self.edit, comment_id);
1331 1344
1332 1345 // set a CUSTOM submit handler for inline comments.
1333 1346 commentForm.setHandleFormSubmit(function(o) {
1334 1347 var text = commentForm.cm.getValue();
1335 1348 var commentType = commentForm.getCommentType();
1336 1349 var resolvesCommentId = commentForm.getResolvesId();
1337 1350 var isDraft = commentForm.getDraftState();
1338 1351
1339 1352 if (text === "") {
1340 1353 return;
1341 1354 }
1342 1355
1343 1356 if (line_no === undefined) {
1344 1357 alert('Error: unable to fetch line number for this inline comment !');
1345 1358 return;
1346 1359 }
1347 1360
1348 1361 if (f_path === undefined) {
1349 1362 alert('Error: unable to fetch file path for this inline comment !');
1350 1363 return;
1351 1364 }
1352 1365
1353 1366 var excludeCancelBtn = false;
1354 1367 var submitEvent = true;
1355 1368 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1356 1369 commentForm.cm.setOption("readOnly", true);
1357 1370 var postData = {
1358 1371 'text': text,
1359 1372 'f_path': f_path,
1360 1373 'line': line_no,
1361 1374 'comment_type': commentType,
1362 1375 'draft': isDraft,
1363 1376 'csrf_token': CSRF_TOKEN
1364 1377 };
1365 1378 if (resolvesCommentId){
1366 1379 postData['resolves_comment_id'] = resolvesCommentId;
1367 1380 }
1368 1381
1369 1382 // submitSuccess for inline commits
1370 1383 var submitSuccessCallback = function(json_data) {
1371 1384
1372 1385 $replyForm.remove();
1373 1386 $td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
1374 1387
1375 1388 try {
1376 1389
1377 1390 // inject newly created comments, json_data is {<comment_id>: {}}
1378 1391 self.attachInlineComment(json_data)
1379 1392
1380 1393 //mark visually which comment was resolved
1381 1394 if (resolvesCommentId) {
1382 1395 commentForm.markCommentResolved(resolvesCommentId);
1383 1396 }
1384 1397
1385 1398 // run global callback on submit
1386 1399 commentForm.globalSubmitSuccessCallback({
1387 1400 draft: isDraft,
1388 1401 comment_id: comment_id
1389 1402 });
1390 1403
1391 1404 } catch (e) {
1392 1405 console.error(e);
1393 1406 }
1394 1407
1395 1408 if (window.updateSticky !== undefined) {
1396 1409 // potentially our comments change the active window size, so we
1397 1410 // notify sticky elements
1398 1411 updateSticky()
1399 1412 }
1400 1413
1401 1414 if (window.refreshAllComments !== undefined && !isDraft) {
1402 1415 // if we have this handler, run it, and refresh all comments boxes
1403 1416 refreshAllComments()
1404 1417 }
1405 1418
1406 1419 commentForm.setActionButtonsDisabled(false);
1407 1420
1408 1421 // re trigger the linkification of next/prev navigation
1409 1422 linkifyComments($('.inline-comment-injected'));
1410 1423 timeagoActivate();
1411 1424 tooltipActivate();
1412 1425 };
1413 1426
1414 1427 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1415 1428 var prefix = "Error while submitting comment.\n"
1416 1429 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1417 1430 ajaxErrorSwal(message);
1418 1431 commentForm.resetCommentFormState(text)
1419 1432 };
1420 1433
1421 1434 commentForm.submitAjaxPOST(
1422 1435 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1423 1436 });
1424 1437 }
1425 1438
1426 1439 // Finally "open" our reply form, since we know there are comments and we have the "attached" old form
1427 1440 $replyForm.addClass('comment-inline-form-open');
1428 1441 tooltipActivate();
1429 1442 };
1430 1443
1431 1444 this.createResolutionComment = function(commentId){
1432 1445 // hide the trigger text
1433 1446 $('#resolve-comment-{0}'.format(commentId)).hide();
1434 1447
1435 1448 var comment = $('#comment-'+commentId);
1436 1449 var commentData = comment.data();
1437 1450 if (commentData.commentInline) {
1438 1451 var f_path = commentData.fPath;
1439 1452 var line_no = commentData.lineNo;
1440 1453 //TODO check this if we need to give f_path/line_no
1441 1454 this.createComment(comment, f_path, line_no, commentId)
1442 1455 } else {
1443 1456 this.createGeneralComment('general', "$placeholder", commentId)
1444 1457 }
1445 1458
1446 1459 return false;
1447 1460 };
1448 1461
1449 1462 this.submitResolution = function(commentId){
1450 1463 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1451 1464 var commentForm = form.get(0).CommentForm;
1452 1465
1453 1466 var cm = commentForm.getCmInstance();
1454 1467 var renderer = templateContext.visual.default_renderer;
1455 1468 if (renderer == 'rst'){
1456 1469 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1457 1470 } else if (renderer == 'markdown') {
1458 1471 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1459 1472 } else {
1460 1473 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1461 1474 }
1462 1475
1463 1476 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1464 1477 form.submit();
1465 1478 return false;
1466 1479 };
1467 1480
1468 1481 };
1469 1482
1470 1483 window.commentHelp = function(renderer) {
1471 1484 var funcData = {'renderer': renderer}
1472 1485 return renderTemplate('commentHelpHovercard', funcData)
1473 1486 } No newline at end of file
@@ -1,432 +1,430 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 // TODO: marcink switch this to pyroutes
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 27 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
30 28 </script>
31 29
32 30 <div class="box">
33 31
34 32 <div class="summary">
35 33
36 34 <div class="fieldset">
37 35 <div class="left-content">
38 36 <%
39 37 rc_user = h.discover_user(c.commit.author_email)
40 38 %>
41 39 <div class="left-content-avatar">
42 40 ${base.gravatar(c.commit.author_email, 30, tooltip=(True if rc_user else False), user=rc_user)}
43 41 </div>
44 42
45 43 <div class="left-content-message">
46 44 <div class="fieldset collapsable-content no-hide" data-toggle="summary-details">
47 45 <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 46 </div>
49 47
50 48 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none">
51 49 <div class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
52 50 </div>
53 51
54 52 <div class="fieldset" data-toggle="summary-details">
55 53 <div class="">
56 54 <table>
57 55 <tr class="file_author">
58 56
59 57 <td>
60 58 <span class="user commit-author">${h.link_to_user(rc_user or c.commit.author)}</span>
61 59 <span class="commit-date">- ${h.age_component(c.commit.date)}</span>
62 60 </td>
63 61
64 62 <td>
65 63 ## second cell for consistency with files
66 64 </td>
67 65 </tr>
68 66 </table>
69 67 </div>
70 68 </div>
71 69
72 70 </div>
73 71 </div>
74 72
75 73 <div class="right-content">
76 74
77 75 <div data-toggle="summary-details">
78 76 <div class="tags tags-main">
79 77 <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 78 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.commit.raw_id}" title="${_('Copy the full commit id')}"></i>
81 79 ${file_base.refs(c.commit)}
82 80
83 81 ## phase
84 82 % if hasattr(c.commit, 'phase') and getattr(c.commit, 'phase') != 'public':
85 83 <span class="tag phase-${c.commit.phase} tooltip" title="${_('Commit phase')}">
86 84 <i class="icon-info"></i>${c.commit.phase}
87 85 </span>
88 86 % endif
89 87
90 88 ## obsolete commits
91 89 % if getattr(c.commit, 'obsolete', False):
92 90 <span class="tag obsolete-${c.commit.obsolete} tooltip" title="${_('Evolve State')}">
93 91 ${_('obsolete')}
94 92 </span>
95 93 % endif
96 94
97 95 ## hidden commits
98 96 % if getattr(c.commit, 'hidden', False):
99 97 <span class="tag hidden-${c.commit.hidden} tooltip" title="${_('Evolve State')}">
100 98 ${_('hidden')}
101 99 </span>
102 100 % endif
103 101 </div>
104 102
105 103 <span id="parent_link" class="tag tagtag">
106 104 <a href="#parentCommit" title="${_('Parent Commit')}"><i class="icon-left icon-no-margin"></i>${_('parent')}</a>
107 105 </span>
108 106
109 107 <span id="child_link" class="tag tagtag">
110 108 <a href="#childCommit" title="${_('Child Commit')}">${_('child')}<i class="icon-right icon-no-margin"></i></a>
111 109 </span>
112 110
113 111 </div>
114 112
115 113 </div>
116 114 </div>
117 115
118 116 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
119 117 <div class="left-label-summary">
120 118 <p>${_('Diff options')}:</p>
121 119 <div class="right-label-summary">
122 120 <div class="diff-actions">
123 121 <a href="${h.route_path('repo_commit_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">
124 122 ${_('Raw Diff')}
125 123 </a>
126 124 |
127 125 <a href="${h.route_path('repo_commit_patch',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">
128 126 ${_('Patch Diff')}
129 127 </a>
130 128 |
131 129 <a href="${h.route_path('repo_commit_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(diff='download'))}">
132 130 ${_('Download Diff')}
133 131 </a>
134 132 </div>
135 133 </div>
136 134 </div>
137 135 </div>
138 136
139 137 <div class="clear-fix"></div>
140 138
141 139 <div class="btn-collapse" data-toggle="summary-details">
142 140 ${_('Show More')}
143 141 </div>
144 142
145 143 </div>
146 144
147 145 <div class="cs_files">
148 146 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
149 147 ${cbdiffs.render_diffset_menu(c.changes[c.commit.raw_id], commit=c.commit)}
150 148 ${cbdiffs.render_diffset(
151 149 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True,
152 150 inline_comments=c.inline_comments,
153 151 show_todos=False)}
154 152 </div>
155 153
156 154 ## template for inline comment form
157 155 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
158 156
159 157 ## comments heading with count
160 158 <div class="comments-heading">
161 159 <i class="icon-comment"></i>
162 160 ${_('General Comments')} ${len(c.comments)}
163 161 </div>
164 162
165 163 ## render comments
166 164 ${comment.generate_comments(c.comments)}
167 165
168 166 ## main comment form and it status
169 167 ${comment.comments(h.route_path('repo_commit_comment_create', repo_name=c.repo_name, commit_id=c.commit.raw_id),
170 168 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
171 169 </div>
172 170
173 171 ### NAV SIDEBAR
174 172 <aside class="right-sidebar right-sidebar-expanded" id="commit-nav-sticky" style="display: none">
175 173 <div class="sidenav navbar__inner" >
176 174 ## TOGGLE
177 175 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
178 176 <a href="#toggleSidebar" class="grey-link-action">
179 177
180 178 </a>
181 179 </div>
182 180
183 181 ## CONTENT
184 182 <div class="sidebar-content">
185 183
186 184 ## RULES SUMMARY/RULES
187 185 <div class="sidebar-element clear-both">
188 186 <% vote_title = _ungettext(
189 187 'Status calculated based on votes from {} reviewer',
190 188 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
191 189 %>
192 190
193 191 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
194 192 <i class="icon-circle review-status-${c.commit_review_status}"></i>
195 193 ${c.reviewers_count}
196 194 </div>
197 195 </div>
198 196
199 197 ## REVIEWERS
200 198 <div class="right-sidebar-expanded-state pr-details-title">
201 199 <span class="tooltip sidebar-heading" title="${vote_title}">
202 200 <i class="icon-circle review-status-${c.commit_review_status}"></i>
203 201 ${_('Reviewers')}
204 202 </span>
205 203 </div>
206 204
207 205 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
208 206
209 207 <table id="review_members" class="group_members">
210 208 ## This content is loaded via JS and ReviewersPanel
211 209 </table>
212 210
213 211 </div>
214 212
215 213 ## TODOs
216 214 <div class="sidebar-element clear-both">
217 215 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
218 216 <i class="icon-flag-filled"></i>
219 217 <span id="todos-count">${len(c.unresolved_comments)}</span>
220 218 </div>
221 219
222 220 <div class="right-sidebar-expanded-state pr-details-title">
223 221 ## Only show unresolved, that is only what matters
224 222 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
225 223 <i class="icon-flag-filled"></i>
226 224 TODOs
227 225 </span>
228 226
229 227 % if c.resolved_comments:
230 228 <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 229 % else:
232 230 <span class="block-right last-item noselect">Show resolved</span>
233 231 % endif
234 232
235 233 </div>
236 234
237 235 <div class="right-sidebar-expanded-state pr-details-content">
238 236 % if c.unresolved_comments + c.resolved_comments:
239 237 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True, is_pr=False)}
240 238 % else:
241 239 <table>
242 240 <tr>
243 241 <td>
244 242 ${_('No TODOs yet')}
245 243 </td>
246 244 </tr>
247 245 </table>
248 246 % endif
249 247 </div>
250 248 </div>
251 249
252 250 ## COMMENTS
253 251 <div class="sidebar-element clear-both">
254 252 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
255 253 <i class="icon-comment" style="color: #949494"></i>
256 254 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
257 255 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
258 256 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
259 257 </div>
260 258
261 259 <div class="right-sidebar-expanded-state pr-details-title">
262 260 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
263 261 <i class="icon-comment" style="color: #949494"></i>
264 262 ${_('Comments')}
265 263 </span>
266 264
267 265 </div>
268 266
269 267 <div class="right-sidebar-expanded-state pr-details-content">
270 268 % if c.inline_comments_flat + c.comments:
271 269 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments), is_pr=False)}
272 270 % else:
273 271 <table>
274 272 <tr>
275 273 <td>
276 274 ${_('No Comments yet')}
277 275 </td>
278 276 </tr>
279 277 </table>
280 278 % endif
281 279 </div>
282 280
283 281 </div>
284 282
285 283 </div>
286 284
287 285 </div>
288 286 </aside>
289 287
290 288 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
291 289 <script type="text/javascript">
292 290 window.setReviewersData = ${c.commit_set_reviewers_data_json | n};
293 291
294 292 $(document).ready(function () {
295 293 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
296 294
297 295 if ($('#trimmed_message_box').height() === boxmax) {
298 296 $('#message_expand').show();
299 297 }
300 298
301 299 $('#message_expand').on('click', function (e) {
302 300 $('#trimmed_message_box').css('max-height', 'none');
303 301 $(this).hide();
304 302 });
305 303
306 304 $('.show-inline-comments').on('click', function (e) {
307 305 var boxid = $(this).attr('data-comment-id');
308 306 var button = $(this);
309 307
310 308 if (button.hasClass("comments-visible")) {
311 309 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
312 310 $(this).hide();
313 311 });
314 312 button.removeClass("comments-visible");
315 313 } else {
316 314 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
317 315 $(this).show();
318 316 });
319 317 button.addClass("comments-visible");
320 318 }
321 319 });
322 320
323 321 // next links
324 322 $('#child_link').on('click', function (e) {
325 323 // fetch via ajax what is going to be the next link, if we have
326 324 // >1 links show them to user to choose
327 325 if (!$('#child_link').hasClass('disabled')) {
328 326 $.ajax({
329 327 url: '${h.route_path('repo_commit_children',repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
330 328 success: function (data) {
331 329 if (data.results.length === 0) {
332 330 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
333 331 }
334 332 if (data.results.length === 1) {
335 333 var commit = data.results[0];
336 334 window.location = pyroutes.url('repo_commit', {
337 335 'repo_name': '${c.repo_name}',
338 336 'commit_id': commit.raw_id
339 337 });
340 338 } else if (data.results.length === 2) {
341 339 $('#child_link').addClass('disabled');
342 340 $('#child_link').addClass('double');
343 341
344 342 var _html = '';
345 343 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
346 344 .replace('__branch__', data.results[0].branch)
347 345 .replace('__rev__', 'r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0, 6)))
348 346 .replace('__title__', data.results[0].message)
349 347 .replace('__url__', pyroutes.url('repo_commit', {
350 348 'repo_name': '${c.repo_name}',
351 349 'commit_id': data.results[0].raw_id
352 350 }));
353 351 _html += ' | ';
354 352 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
355 353 .replace('__branch__', data.results[1].branch)
356 354 .replace('__rev__', 'r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0, 6)))
357 355 .replace('__title__', data.results[1].message)
358 356 .replace('__url__', pyroutes.url('repo_commit', {
359 357 'repo_name': '${c.repo_name}',
360 358 'commit_id': data.results[1].raw_id
361 359 }));
362 360 $('#child_link').html(_html);
363 361 }
364 362 }
365 363 });
366 364 e.preventDefault();
367 365 }
368 366 });
369 367
370 368 // prev links
371 369 $('#parent_link').on('click', function (e) {
372 370 // fetch via ajax what is going to be the next link, if we have
373 371 // >1 links show them to user to choose
374 372 if (!$('#parent_link').hasClass('disabled')) {
375 373 $.ajax({
376 374 url: '${h.route_path("repo_commit_parents",repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
377 375 success: function (data) {
378 376 if (data.results.length === 0) {
379 377 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
380 378 }
381 379 if (data.results.length === 1) {
382 380 var commit = data.results[0];
383 381 window.location = pyroutes.url('repo_commit', {
384 382 'repo_name': '${c.repo_name}',
385 383 'commit_id': commit.raw_id
386 384 });
387 385 } else if (data.results.length === 2) {
388 386 $('#parent_link').addClass('disabled');
389 387 $('#parent_link').addClass('double');
390 388
391 389 var _html = '';
392 390 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
393 391 .replace('__branch__', data.results[0].branch)
394 392 .replace('__rev__', 'r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0, 6)))
395 393 .replace('__title__', data.results[0].message)
396 394 .replace('__url__', pyroutes.url('repo_commit', {
397 395 'repo_name': '${c.repo_name}',
398 396 'commit_id': data.results[0].raw_id
399 397 }));
400 398 _html += ' | ';
401 399 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
402 400 .replace('__branch__', data.results[1].branch)
403 401 .replace('__rev__', 'r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0, 6)))
404 402 .replace('__title__', data.results[1].message)
405 403 .replace('__url__', pyroutes.url('repo_commit', {
406 404 'repo_name': '${c.repo_name}',
407 405 'commit_id': data.results[1].raw_id
408 406 }));
409 407 $('#parent_link').html(_html);
410 408 }
411 409 }
412 410 });
413 411 e.preventDefault();
414 412 }
415 413 });
416 414
417 415 // browse tree @ revision
418 416 $('#files_link').on('click', function (e) {
419 417 window.location = '${h.route_path('repo_files:default_path',repo_name=c.repo_name, commit_id=c.commit.raw_id)}';
420 418 e.preventDefault();
421 419 });
422 420
423 421 reviewersController = new ReviewersController();
424 422 ReviewersPanel.init(reviewersController, null, setReviewersData);
425 423
426 424 var channel = '${c.commit_broadcast_channel}';
427 425 new ReviewerPresenceController(channel)
428 426
429 427 })
430 428 </script>
431 429
432 430 </%def>
@@ -1,557 +1,557 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6 6 <%namespace name="base" file="/base/base.mako"/>
7 7
8 8 <%!
9 9 from rhodecode.lib import html_filters
10 10 %>
11 11
12 12
13 13 <%def name="comment_block(comment, inline=False, active_pattern_entries=None, is_new=False)">
14 14
15 15 <%
16 16 from rhodecode.model.comment import CommentsModel
17 17 comment_model = CommentsModel()
18 18
19 19 comment_ver = comment.get_index_version(getattr(c, 'versions', []))
20 20 latest_ver = len(getattr(c, 'versions', []))
21 21 visible_for_user = True
22 22 if comment.draft:
23 23 visible_for_user = comment.user_id == c.rhodecode_user.user_id
24 24 %>
25 25
26 26 % if inline:
27 27 <% outdated_at_ver = comment.outdated_at_version(c.at_version_num) %>
28 28 % else:
29 29 <% outdated_at_ver = comment.older_than_version(c.at_version_num) %>
30 30 % endif
31 31
32 32 % if visible_for_user:
33 33 <div class="comment
34 34 ${'comment-inline' if inline else 'comment-general'}
35 35 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
36 36 id="comment-${comment.comment_id}"
37 37 line="${comment.line_no}"
38 38 data-comment-id="${comment.comment_id}"
39 39 data-comment-type="${comment.comment_type}"
40 40 data-comment-draft=${h.json.dumps(comment.draft)}
41 41 data-comment-renderer="${comment.renderer}"
42 42 data-comment-text="${comment.text | html_filters.base64,n}"
43 43 data-comment-f-path="${comment.f_path}"
44 44 data-comment-line-no="${comment.line_no}"
45 45 data-comment-inline=${h.json.dumps(inline)}
46 46 style="${'display: none;' if outdated_at_ver else ''}">
47 47
48 48 <div class="meta">
49 49 <div class="comment-type-label">
50 50 % if comment.draft:
51 51 <div class="tooltip comment-draft" title="${_('Draft comments are only visible to the author until submitted')}.">
52 52 DRAFT
53 53 </div>
54 54 % elif is_new:
55 55 <div class="tooltip comment-new" title="${_('This comment was added while you browsed this page')}.">
56 56 NEW
57 57 </div>
58 58 % endif
59 59
60 60 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
61 61
62 62 ## TODO COMMENT
63 63 % if comment.comment_type == 'todo':
64 64 % if comment.resolved:
65 65 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
66 66 <i class="icon-flag-filled"></i>
67 67 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
68 68 </div>
69 69 % else:
70 70 <div class="resolved tooltip" style="display: none">
71 71 <span>${comment.comment_type}</span>
72 72 </div>
73 73 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to create resolution comment.')}">
74 74 <i class="icon-flag-filled"></i>
75 75 ${comment.comment_type}
76 76 </div>
77 77 % endif
78 78 ## NOTE COMMENT
79 79 % else:
80 80 ## RESOLVED NOTE
81 81 % if comment.resolved_comment:
82 82 <div class="tooltip" title="${_('This comment resolves TODO #{}').format(comment.resolved_comment.comment_id)}">
83 83 fix
84 84 <a href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
85 85 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
86 86 </a>
87 87 </div>
88 88 ## STATUS CHANGE NOTE
89 89 % elif not comment.is_inline and comment.status_change:
90 90 <%
91 91 if comment.pull_request:
92 92 status_change_title = 'Status of review for pull request !{}'.format(comment.pull_request.pull_request_id)
93 93 else:
94 94 status_change_title = 'Status of review for commit {}'.format(h.short_id(comment.commit_id))
95 95 %>
96 96
97 97 <i class="icon-circle review-status-${comment.review_status}"></i>
98 98 <div class="changeset-status-lbl tooltip" title="${status_change_title}">
99 99 ${comment.review_status_lbl}
100 100 </div>
101 101 % else:
102 102 <div>
103 103 <i class="icon-comment"></i>
104 104 ${(comment.comment_type or 'note')}
105 105 </div>
106 106 % endif
107 107 % endif
108 108
109 109 </div>
110 110 </div>
111 111 ## NOTE 0 and .. => because we disable it for now until UI ready
112 112 % if 0 and comment.status_change:
113 113 <div class="pull-left">
114 114 <span class="tag authortag tooltip" title="${_('Status from pull request.')}">
115 115 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
116 116 ${'!{}'.format(comment.pull_request.pull_request_id)}
117 117 </a>
118 118 </span>
119 119 </div>
120 120 % endif
121 121 ## Since only author can see drafts, we don't show it
122 122 % if not comment.draft:
123 123 <div class="author ${'author-inline' if inline else 'author-general'}">
124 124 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
125 125 </div>
126 126 % endif
127 127
128 128 <div class="date">
129 129 ${h.age_component(comment.modified_at, time_is_local=True)}
130 130 </div>
131 131
132 132 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
133 133 <span class="tag authortag tooltip" title="${_('Pull request author')}">
134 134 ${_('author')}
135 135 </span>
136 136 % endif
137 137
138 138 <%
139 139 comment_version_selector = 'comment_versions_{}'.format(comment.comment_id)
140 140 %>
141 141
142 142 % if comment.history:
143 143 <div class="date">
144 144
145 145 <input id="${comment_version_selector}" name="${comment_version_selector}"
146 146 type="hidden"
147 147 data-last-version="${comment.history[-1].version}">
148 148
149 149 <script type="text/javascript">
150 150
151 151 var preLoadVersionData = [
152 152 % for comment_history in comment.history:
153 153 {
154 154 id: ${comment_history.comment_history_id},
155 155 text: 'v${comment_history.version}',
156 156 action: function () {
157 157 Rhodecode.comments.showVersion(
158 158 "${comment.comment_id}",
159 159 "${comment_history.comment_history_id}"
160 160 )
161 161 },
162 162 comment_version: "${comment_history.version}",
163 163 comment_author_username: "${comment_history.author.username}",
164 164 comment_author_gravatar: "${h.gravatar_url(comment_history.author.email, 16)}",
165 165 comment_created_on: '${h.age_component(comment_history.created_on, time_is_local=True)}',
166 166 },
167 167 % endfor
168 168 ]
169 169 initVersionSelector("#${comment_version_selector}", {results: preLoadVersionData});
170 170
171 171 </script>
172 172
173 173 </div>
174 174 % else:
175 175 <div class="date" style="display: none">
176 176 <input id="${comment_version_selector}" name="${comment_version_selector}"
177 177 type="hidden"
178 178 data-last-version="0">
179 179 </div>
180 180 %endif
181 181
182 182 <div class="comment-links-block">
183 183
184 184 % if inline:
185 185 <a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
186 186 % if outdated_at_ver:
187 187 <strong class="comment-outdated-label">outdated</strong> <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
188 188 <code class="action-divider">|</code>
189 189 % elif comment_ver:
190 190 <code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
191 191 <code class="action-divider">|</code>
192 192 % endif
193 193 </a>
194 194 % else:
195 195 % if comment_ver:
196 196
197 197 % if comment.outdated:
198 198 <a class="pr-version"
199 199 href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}"
200 200 >
201 201 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}
202 202 </a>
203 203 <code class="action-divider">|</code>
204 204 % else:
205 205 <a class="tooltip pr-version"
206 206 title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}"
207 207 href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}"
208 208 >
209 209 <code class="pr-version-num">${'v{}'.format(comment_ver)}</code>
210 210 </a>
211 211 <code class="action-divider">|</code>
212 212 % endif
213 213
214 214 % endif
215 215 % endif
216 216
217 217 <details class="details-reset details-inline-block">
218 218 <summary class="noselect"><i class="icon-options cursor-pointer"></i></summary>
219 219 <details-menu class="details-dropdown">
220 220
221 221 <div class="dropdown-item">
222 222 ${_('Comment')} #${comment.comment_id}
223 223 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${comment_model.get_url(comment,request, permalink=True, anchor='comment-{}'.format(comment.comment_id))}" title="${_('Copy permalink')}"></span>
224 224 </div>
225 225
226 226 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
227 227 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
228 228 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
229 229 ## permissions to delete
230 230 %if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
231 231 <div class="dropdown-divider"></div>
232 232 <div class="dropdown-item">
233 233 <a onclick="return Rhodecode.comments.editComment(this, '${comment.line_no}', '${comment.f_path}');" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
234 234 </div>
235 235 <div class="dropdown-item">
236 236 <a onclick="return Rhodecode.comments.deleteComment(this);" class="btn btn-link btn-sm btn-danger delete-comment">${_('Delete')}</a>
237 237 </div>
238 238 ## Only available in EE edition
239 239 % if comment.draft and c.rhodecode_edition_id == 'EE':
240 240 <div class="dropdown-item">
241 241 <a onclick="return Rhodecode.comments.finalizeDrafts([${comment.comment_id}]);" class="btn btn-link btn-sm finalize-draft-comment">${_('Submit draft')}</a>
242 242 </div>
243 243 % endif
244 244 %else:
245 245 <div class="dropdown-divider"></div>
246 246 <div class="dropdown-item">
247 247 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
248 248 </div>
249 249 <div class="dropdown-item">
250 250 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
251 251 </div>
252 252 %endif
253 253 %else:
254 254 <div class="dropdown-divider"></div>
255 255 <div class="dropdown-item">
256 256 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
257 257 </div>
258 258 <div class="dropdown-item">
259 259 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
260 260 </div>
261 261 %endif
262 262 </details-menu>
263 263 </details>
264 264
265 265 <code class="action-divider">|</code>
266 266 % if outdated_at_ver:
267 267 <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous outdated comment')}"> <i class="icon-angle-left"></i> </a>
268 268 <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="tooltip next-comment" title="${_('Jump to the next outdated comment')}"> <i class="icon-angle-right"></i></a>
269 269 % else:
270 270 <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
271 271 <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
272 272 % endif
273 273
274 274 </div>
275 275 </div>
276 276 <div class="text">
277 277 ${h.render(comment.text, renderer=comment.renderer, mentions=True, repo_name=getattr(c, 'repo_name', None), active_pattern_entries=active_pattern_entries)}
278 278 </div>
279 279
280 280 </div>
281 281 % endif
282 282 </%def>
283 283
284 284 ## generate main comments
285 285 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
286 286 <%
287 287 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
288 288 %>
289 289
290 290 <div class="general-comments" id="comments">
291 291 %for comment in comments:
292 292 <div id="comment-tr-${comment.comment_id}">
293 293 ## only render comments that are not from pull request, or from
294 294 ## pull request and a status change
295 295 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
296 296 ${comment_block(comment, active_pattern_entries=active_pattern_entries)}
297 297 %endif
298 298 </div>
299 299 %endfor
300 300 ## to anchor ajax comments
301 301 <div id="injected_page_comments"></div>
302 302 </div>
303 303 </%def>
304 304
305 305
306 306 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
307 307
308 308 <div class="comments">
309 309 <%
310 310 if is_pull_request:
311 311 placeholder = _('Leave a comment on this Pull Request.')
312 312 elif is_compare:
313 313 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
314 314 else:
315 315 placeholder = _('Leave a comment on this Commit.')
316 316 %>
317 317
318 318 % if c.rhodecode_user.username != h.DEFAULT_USER:
319 319 <div class="js-template" id="cb-comment-general-form-template">
320 320 ## template generated for injection
321 321 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
322 322 </div>
323 323
324 324 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
325 325 ## inject form here
326 326 </div>
327 327 <script type="text/javascript">
328 328 var resolvesCommentId = null;
329 329 var generalCommentForm = Rhodecode.comments.createGeneralComment(
330 330 'general', "${placeholder}", resolvesCommentId);
331 331
332 332 // set custom success callback on rangeCommit
333 333 % if is_compare:
334 334 generalCommentForm.setHandleFormSubmit(function(o) {
335 335 var self = generalCommentForm;
336 336
337 337 var text = self.cm.getValue();
338 338 var status = self.getCommentStatus();
339 339 var commentType = self.getCommentType();
340 340 var isDraft = self.getDraftState();
341 341
342 342 if (text === "" && !status) {
343 343 return;
344 344 }
345 345
346 346 // we can pick which commits we want to make the comment by
347 347 // selecting them via click on preview pane, this will alter the hidden inputs
348 348 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
349 349
350 350 var commitIds = [];
351 351 $('#changeset_compare_view_content .compare_select').each(function(el) {
352 352 var commitId = this.id.replace('row-', '');
353 353 if ($(this).hasClass('hl') || !cherryPicked) {
354 354 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
355 355 commitIds.push(commitId);
356 356 } else {
357 357 $("input[data-commit-id='{0}']".format(commitId)).val('')
358 358 }
359 359 });
360 360
361 361 self.setActionButtonsDisabled(true);
362 362 self.cm.setOption("readOnly", true);
363 363 var postData = {
364 364 'text': text,
365 365 'changeset_status': status,
366 366 'comment_type': commentType,
367 367 'draft': isDraft,
368 368 'commit_ids': commitIds,
369 369 'csrf_token': CSRF_TOKEN
370 370 };
371 371
372 372 var submitSuccessCallback = function(o) {
373 373 location.reload(true);
374 374 };
375 375 var submitFailCallback = function(){
376 376 self.resetCommentFormState(text)
377 377 };
378 378 self.submitAjaxPOST(
379 379 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
380 380 });
381 381 % endif
382 382
383 383 </script>
384 384 % else:
385 385 ## form state when not logged in
386 386 <div class="comment-form ac">
387 387
388 388 <div class="comment-area">
389 389 <div class="comment-area-header">
390 390 <ul class="nav-links clearfix">
391 391 <li class="active">
392 392 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
393 393 </li>
394 394 <li class="">
395 395 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
396 396 </li>
397 397 </ul>
398 398 </div>
399 399
400 400 <div class="comment-area-write" style="display: block;">
401 401 <div id="edit-container">
402 402 <div style="padding: 20px 0px 0px 0;">
403 403 ${_('You need to be logged in to leave comments.')}
404 404 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
405 405 </div>
406 406 </div>
407 407 <div id="preview-container" class="clearfix" style="display: none;">
408 408 <div id="preview-box" class="preview-box"></div>
409 409 </div>
410 410 </div>
411 411
412 412 <div class="comment-area-footer">
413 413 <div class="toolbar">
414 414 <div class="toolbar-text">
415 415 </div>
416 416 </div>
417 417 </div>
418 418 </div>
419 419
420 420 <div class="comment-footer">
421 421 </div>
422 422
423 423 </div>
424 424 % endif
425 425
426 426 <script type="text/javascript">
427 427 bindToggleButtons();
428 428 </script>
429 429 </div>
430 430 </%def>
431 431
432 432
433 433 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
434 434
435 435 ## comment injected based on assumption that user is logged in
436 436 <form ${('id="{}"'.format(form_id) if form_id else '') |n} action="#" method="GET">
437 437
438 438 <div class="comment-area">
439 439 <div class="comment-area-header">
440 440 <div class="pull-left">
441 441 <ul class="nav-links clearfix">
442 442 <li class="active">
443 443 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
444 444 </li>
445 445 <li class="">
446 446 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
447 447 </li>
448 448 </ul>
449 449 </div>
450 450 <div class="pull-right">
451 451 <span class="comment-area-text">${_('Mark as')}:</span>
452 452 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
453 453 % for val in c.visual.comment_types:
454 454 <option value="${val}">${val.upper()}</option>
455 455 % endfor
456 456 </select>
457 457 </div>
458 458 </div>
459 459
460 460 <div class="comment-area-write" style="display: block;">
461 461 <div id="edit-container_${lineno_id}" style="margin-top: -1px">
462 462 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
463 463 </div>
464 464 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
465 465 <div id="preview-box_${lineno_id}" class="preview-box"></div>
466 466 </div>
467 467 </div>
468 468
469 469 <div class="comment-area-footer comment-attachment-uploader">
470 470 <div class="toolbar">
471 471
472 472 <div class="comment-attachment-text">
473 473 <div class="dropzone-text">
474 474 ${_("Drag'n Drop files here or")} <span class="link pick-attachment">${_('Choose your files')}</span>.<br>
475 475 </div>
476 476 <div class="dropzone-upload" style="display:none">
477 477 <i class="icon-spin animate-spin"></i> ${_('uploading...')}
478 478 </div>
479 479 </div>
480 480
481 481 ## comments dropzone template, empty on purpose
482 482 <div style="display: none" class="comment-attachment-uploader-template">
483 483 <div class="dz-file-preview" style="margin: 0">
484 484 <div class="dz-error-message"></div>
485 485 </div>
486 486 </div>
487 487
488 488 </div>
489 489 </div>
490 490 </div>
491 491
492 492 <div class="comment-footer">
493 493
494 494 ## inject extra inputs into the form
495 495 % if form_extras and isinstance(form_extras, (list, tuple)):
496 496 <div id="comment_form_extras">
497 497 % for form_ex_el in form_extras:
498 498 ${form_ex_el|n}
499 499 % endfor
500 500 </div>
501 501 % endif
502 502
503 503 <div class="action-buttons">
504 504 % if form_type != 'inline':
505 505 <div class="action-buttons-extra"></div>
506 506 % endif
507 507
508 508 <input class="btn btn-success comment-button-input submit-comment-action" id="save_${lineno_id}" name="save" type="submit" value="${_('Add comment')}" data-is-draft=false onclick="$(this).addClass('submitter')">
509 509
510 510 % if form_type == 'inline':
511 511 % if c.rhodecode_edition_id == 'EE':
512 512 ## Disable the button for CE, the "real" validation is in the backend code anyway
513 513 <input class="btn btn-warning comment-button-input submit-draft-action" id="save_draft_${lineno_id}" name="save_draft" type="submit" value="${_('Add draft')}" data-is-draft=true onclick="$(this).addClass('submitter')">
514 514 % else:
515 515 <input class="btn btn-warning comment-button-input submit-draft-action disabled" disabled="disabled" type="submit" value="${_('Add draft')}" onclick="return false;" title="Draft comments only available in EE edition of RhodeCode">
516 516 % endif
517 517 % endif
518 518
519 519 % if review_statuses:
520 520 <div class="comment-status-box">
521 521 <select id="change_status_${lineno_id}" name="changeset_status">
522 522 <option></option> ## Placeholder
523 523 % for status, lbl in review_statuses:
524 524 <option value="${status}" data-status="${status}">${lbl}</option>
525 525 %if is_pull_request and change_status and status in ('approved', 'rejected'):
526 526 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
527 527 %endif
528 528 % endfor
529 529 </select>
530 530 </div>
531 531 % endif
532 532
533 533 ## inline for has a file, and line-number together with cancel hide button.
534 534 % if form_type == 'inline':
535 535 <input type="hidden" name="f_path" value="{0}">
536 536 <input type="hidden" name="line" value="${lineno_id}">
537 537 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
538 538 <i class="icon-cancel-circled2"></i>
539 539 </button>
540 540 % endif
541 541 </div>
542 542
543 543 <div class="toolbar-text">
544 544 <% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
545 <span>${_('Styling with {} is supported.').format(renderer_url)|n}
545 <span>${_('{} is supported.').format(renderer_url)|n}
546 546
547 547 <i class="icon-info-circled tooltip-hovercard"
548 548 data-hovercard-alt="ALT"
549 549 data-hovercard-url="javascript:commentHelp('${c.visual.default_renderer.upper()}')"
550 550 data-comment-json-b64='${h.b64(h.json.dumps({}))}'></i>
551 551 </span>
552 552 </div>
553 553 </div>
554 554
555 555 </form>
556 556
557 557 </%def> No newline at end of file
@@ -1,1035 +1,1033 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 // TODO: marcink switch this to pyroutes
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 35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
38 36 templateContext.pull_request_data.pull_request_version = '${request.GET.get('version', '')}';
39 37 </script>
40 38
41 39 <div class="box">
42 40
43 41 <div class="box pr-summary">
44 42
45 43 <div class="summary-details block-left">
46 44 <div id="pr-title">
47 45 % if c.pull_request.is_closed():
48 46 <span class="pr-title-closed-tag tag">${_('Closed')}</span>
49 47 % endif
50 48 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${c.pull_request.title}">
51 49 </div>
52 50 <div id="pr-title-edit" class="input" style="display: none;">
53 51 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
54 52 </div>
55 53
56 54 <% summary = lambda n:{False:'summary-short'}.get(n) %>
57 55 <div class="pr-details-title">
58 56 <div class="pull-left">
59 57 <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 58 ${_('Created on')}
61 59 <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 60 <span class="pr-details-title-author-pref">${_('by')}</span>
63 61 </div>
64 62
65 63 <div class="pull-left">
66 64 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
67 65 </div>
68 66
69 67 %if c.allowed_to_update:
70 68 <div class="pull-right">
71 69 <div id="edit_pull_request" class="action_button pr-save" style="display: none;">${_('Update title & description')}</div>
72 70 <div id="delete_pullrequest" class="action_button pr-save ${('' if c.allowed_to_delete else 'disabled' )}" style="display: none;">
73 71 % if c.allowed_to_delete:
74 72 ${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 73 <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 74 onclick="submitConfirm(event, this, _gettext('Confirm to delete this pull request'), _gettext('Delete'), '${'!{}'.format(c.pull_request.pull_request_id)}')"
77 75 type="submit" value="${_('Delete pull request')}">
78 76 ${h.end_form()}
79 77 % else:
80 78 <span class="tooltip" title="${_('Not allowed to delete this pull request')}">${_('Delete pull request')}</span>
81 79 % endif
82 80 </div>
83 81 <div id="open_edit_pullrequest" class="action_button">${_('Edit')}</div>
84 82 <div id="close_edit_pullrequest" class="action_button" style="display: none;">${_('Cancel')}</div>
85 83 </div>
86 84
87 85 %endif
88 86 </div>
89 87
90 88 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
91 89 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name, issues_container=c.referenced_desc_issues)}
92 90 </div>
93 91
94 92 <div id="pr-desc-edit" class="input textarea" style="display: none;">
95 93 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
96 94 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
97 95 </div>
98 96
99 97 <div id="summary" class="fields pr-details-content">
100 98
101 99 ## source
102 100 <div class="field">
103 101 <div class="label-pr-detail">
104 102 <label>${_('Commit flow')}:</label>
105 103 </div>
106 104 <div class="input">
107 105 <div class="pr-commit-flow">
108 106 ## Source
109 107 %if c.pull_request.source_ref_parts.type == 'branch':
110 108 <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 109 %else:
112 110 <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code>
113 111 %endif
114 112 ${_('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 113 &rarr;
116 114 ## Target
117 115 %if c.pull_request.target_ref_parts.type == 'branch':
118 116 <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 117 %else:
120 118 <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
121 119 %endif
122 120
123 121 ${_('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 122
125 123 <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 124 <i class="icon-angle-down">more details</i>
127 125 </a>
128 126
129 127 </div>
130 128
131 129 <div class="source-details" style="display: none">
132 130
133 131 <ul>
134 132
135 133 ## common ancestor
136 134 <li>
137 135 ${_('Common ancestor')}:
138 136 % if c.ancestor_commit:
139 137 <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 138 % else:
141 139 ${_('not available')}
142 140 % endif
143 141 </li>
144 142
145 143 ## pull url
146 144 <li>
147 145 %if h.is_hg(c.pull_request.source_repo):
148 146 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
149 147 %elif h.is_git(c.pull_request.source_repo):
150 148 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
151 149 %endif
152 150
153 151 <span>${_('Pull changes from source')}</span>: <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
154 152 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
155 153 </li>
156 154
157 155 ## Shadow repo
158 156 <li>
159 157 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
160 158 %if h.is_hg(c.pull_request.target_repo):
161 159 <% 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 160 %elif h.is_git(c.pull_request.target_repo):
163 161 <% 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 162 %endif
165 163
166 164 <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 165 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
168 166
169 167 % else:
170 168 <div class="">
171 169 ${_('Shadow repository data not available')}.
172 170 </div>
173 171 % endif
174 172 </li>
175 173
176 174 </ul>
177 175
178 176 </div>
179 177
180 178 </div>
181 179
182 180 </div>
183 181
184 182 ## versions
185 183 <div class="field">
186 184 <div class="label-pr-detail">
187 185 <label>${_('Versions')}:</label>
188 186 </div>
189 187
190 188 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
191 189 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
192 190
193 191 <div class="pr-versions">
194 192 % if c.show_version_changes:
195 193 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
196 194 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
197 195 ${_ungettext('{} version available for this pull request, ', '{} versions available for this pull request, ', len(c.versions)).format(len(c.versions))}
198 196 <a id="show-pr-versions" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
199 197 data-toggle-on="${_('show versions')}."
200 198 data-toggle-off="${_('hide versions')}.">
201 199 ${_('show versions')}.
202 200 </a>
203 201 <table>
204 202 ## SHOW ALL VERSIONS OF PR
205 203 <% ver_pr = None %>
206 204
207 205 % for data in reversed(list(enumerate(c.versions, 1))):
208 206 <% ver_pos = data[0] %>
209 207 <% ver = data[1] %>
210 208 <% ver_pr = ver.pull_request_version_id %>
211 209 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
212 210
213 211 <tr class="version-pr" style="display: ${display_row}">
214 212 <td>
215 213 <code>
216 214 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
217 215 </code>
218 216 </td>
219 217 <td>
220 218 <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 219 <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 220 </td>
223 221 <td>
224 222 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
225 223 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
226 224
227 225 </td>
228 226 <td>
229 227 % if c.at_version_num != ver_pr:
230 228 <i class="tooltip icon-comment" title="${_('Comments from pull request version v{0}').format(ver_pos)}"></i>
231 229 <code>
232 230 General:${len(c.comment_versions[ver_pr]['at'])} / Inline:${len(c.inline_versions[ver_pr]['at'])}
233 231 </code>
234 232 % endif
235 233 </td>
236 234 <td>
237 235 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
238 236 </td>
239 237 <td>
240 238 <code>${h.age_component(ver.updated_on, time_is_local=True, tooltip=False)}</code>
241 239 </td>
242 240 </tr>
243 241 % endfor
244 242
245 243 <tr>
246 244 <td colspan="6">
247 245 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
248 246 data-label-text-locked="${_('select versions to show changes')}"
249 247 data-label-text-diff="${_('show changes between versions')}"
250 248 data-label-text-show="${_('show pull request for this version')}"
251 249 >
252 250 ${_('select versions to show changes')}
253 251 </button>
254 252 </td>
255 253 </tr>
256 254 </table>
257 255 % else:
258 256 <div>
259 257 ${_('Pull request versions not available')}.
260 258 </div>
261 259 % endif
262 260 </div>
263 261 </div>
264 262
265 263 </div>
266 264
267 265 </div>
268 266
269 267
270 268 </div>
271 269
272 270 </div>
273 271
274 272 <div class="box">
275 273
276 274 % if c.state_progressing:
277 275
278 276 <h2 style="text-align: center">
279 277 ${_('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 278
281 279 % if c.is_super_admin:
282 280 <br/>
283 281 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 282 % endif
285 283 </h2>
286 284
287 285 % else:
288 286
289 287 ## Diffs rendered here
290 288 <div class="table" >
291 289 <div id="changeset_compare_view_content">
292 290 ##CS
293 291 % if c.missing_requirements:
294 292 <div class="box">
295 293 <div class="alert alert-warning">
296 294 <div>
297 295 <strong>${_('Missing requirements:')}</strong>
298 296 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
299 297 </div>
300 298 </div>
301 299 </div>
302 300 % elif c.missing_commits:
303 301 <div class="box">
304 302 <div class="alert alert-warning">
305 303 <div>
306 304 <strong>${_('Missing commits')}:</strong>
307 305 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}<br/>
308 306 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}<br/>
309 307 ${_('Consider doing a `force update commits` in case you think this is an error.')}
310 308 </div>
311 309 </div>
312 310 </div>
313 311 % elif c.pr_merge_source_commit.changed and not c.pull_request.is_closed():
314 312 <div class="box">
315 313 <div class="alert alert-info">
316 314 <div>
317 315 <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 316 </div>
319 317 </div>
320 318 </div>
321 319 % endif
322 320
323 321 <div class="compare_view_commits_title">
324 322 % if not c.compare_mode:
325 323
326 324 % if c.at_version_index:
327 325 <h4>
328 326 ${_('Showing changes at v{}, commenting is disabled.').format(c.at_version_index)}
329 327 </h4>
330 328 % endif
331 329
332 330 <div class="pull-left">
333 331 <div class="btn-group">
334 332 <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 333 % if c.collapse_all_commits:
336 334 <i class="icon-plus-squared-alt icon-no-margin"></i>
337 335 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
338 336 % else:
339 337 <i class="icon-minus-squared-alt icon-no-margin"></i>
340 338 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
341 339 % endif
342 340 </a>
343 341 </div>
344 342 </div>
345 343
346 344 <div class="pull-right">
347 345 % if c.allowed_to_update and not c.pull_request.is_closed():
348 346
349 347 <div class="btn-group btn-group-actions">
350 348 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
351 349 ${_('Update commits')}
352 350 </a>
353 351
354 352 <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 353 <i class="icon-down"></i>
356 354 </a>
357 355
358 356 <div class="btn-action-switcher-container right-align" id="update-commits-switcher">
359 357 <ul class="btn-action-switcher" role="menu" style="min-width: 300px;">
360 358 <li>
361 359 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
362 360 ${_('Force update commits')}
363 361 </a>
364 362 <div class="action-help-block">
365 363 ${_('Update commits and force refresh this pull request.')}
366 364 </div>
367 365 </li>
368 366 </ul>
369 367 </div>
370 368 </div>
371 369
372 370 % else:
373 371 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
374 372 % endif
375 373
376 374 </div>
377 375 % endif
378 376 </div>
379 377
380 378 % if not c.missing_commits:
381 379 ## COMPARE RANGE DIFF MODE
382 380 % if c.compare_mode:
383 381 % if c.at_version:
384 382 <h4>
385 383 ${_('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 384 </h4>
387 385
388 386 <div class="subtitle-compare">
389 387 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
390 388 </div>
391 389
392 390 <div class="container">
393 391 <table class="rctable compare_view_commits">
394 392 <tr>
395 393 <th></th>
396 394 <th>${_('Time')}</th>
397 395 <th>${_('Author')}</th>
398 396 <th>${_('Commit')}</th>
399 397 <th></th>
400 398 <th>${_('Description')}</th>
401 399 </tr>
402 400
403 401 % for c_type, commit in c.commit_changes:
404 402 % if c_type in ['a', 'r']:
405 403 <%
406 404 if c_type == 'a':
407 405 cc_title = _('Commit added in displayed changes')
408 406 elif c_type == 'r':
409 407 cc_title = _('Commit removed in displayed changes')
410 408 else:
411 409 cc_title = ''
412 410 %>
413 411 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
414 412 <td>
415 413 <div class="commit-change-indicator color-${c_type}-border">
416 414 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
417 415 ${c_type.upper()}
418 416 </div>
419 417 </div>
420 418 </td>
421 419 <td class="td-time">
422 420 ${h.age_component(commit.date)}
423 421 </td>
424 422 <td class="td-user">
425 423 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
426 424 </td>
427 425 <td class="td-hash">
428 426 <code>
429 427 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
430 428 r${commit.idx}:${h.short_id(commit.raw_id)}
431 429 </a>
432 430 ${h.hidden('revisions', commit.raw_id)}
433 431 </code>
434 432 </td>
435 433 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
436 434 <i class="icon-expand-linked"></i>
437 435 </td>
438 436 <td class="mid td-description">
439 437 <div class="log-container truncate-wrap">
440 438 <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 439 </div>
442 440 </td>
443 441 </tr>
444 442 % endif
445 443 % endfor
446 444 </table>
447 445 </div>
448 446
449 447 % endif
450 448
451 449 ## Regular DIFF
452 450 % else:
453 451 <%include file="/compare/compare_commits.mako" />
454 452 % endif
455 453
456 454 <div class="cs_files">
457 455 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
458 456
459 457 <%
460 458 pr_menu_data = {
461 459 'outdated_comm_count_ver': outdated_comm_count_ver,
462 460 'pull_request': c.pull_request
463 461 }
464 462 %>
465 463
466 464 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on, pull_request_menu=pr_menu_data)}
467 465
468 466 % if c.range_diff_on:
469 467 % for commit in c.commit_ranges:
470 468 ${cbdiffs.render_diffset(
471 469 c.changes[commit.raw_id],
472 470 commit=commit, use_comments=True,
473 471 collapse_when_files_over=5,
474 472 disable_new_comments=True,
475 473 deleted_files_comments=c.deleted_files_comments,
476 474 inline_comments=c.inline_comments,
477 475 pull_request_menu=pr_menu_data, show_todos=False)}
478 476 % endfor
479 477 % else:
480 478 ${cbdiffs.render_diffset(
481 479 c.diffset, use_comments=True,
482 480 collapse_when_files_over=30,
483 481 disable_new_comments=not c.allowed_to_comment,
484 482 deleted_files_comments=c.deleted_files_comments,
485 483 inline_comments=c.inline_comments,
486 484 pull_request_menu=pr_menu_data, show_todos=False)}
487 485 % endif
488 486
489 487 </div>
490 488 % else:
491 489 ## skipping commits we need to clear the view for missing commits
492 490 <div style="clear:both;"></div>
493 491 % endif
494 492
495 493 </div>
496 494 </div>
497 495
498 496 ## template for inline comment form
499 497 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
500 498
501 499 ## comments heading with count
502 500 <div class="comments-heading">
503 501 <i class="icon-comment"></i>
504 502 ${_('General Comments')} ${len(c.comments)}
505 503 </div>
506 504
507 505 ## render general comments
508 506 <div id="comment-tr-show">
509 507 % if general_outdated_comm_count_ver:
510 508 <div class="info-box">
511 509 % if general_outdated_comm_count_ver == 1:
512 510 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
513 511 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
514 512 % else:
515 513 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
516 514 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
517 515 % endif
518 516 </div>
519 517 % endif
520 518 </div>
521 519
522 520 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
523 521
524 522 % if not c.pull_request.is_closed():
525 523 ## main comment form and it status
526 524 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
527 525 pull_request_id=c.pull_request.pull_request_id),
528 526 c.pull_request_review_status,
529 527 is_pull_request=True, change_status=c.allowed_to_change_status)}
530 528
531 529 ## merge status, and merge action
532 530 <div class="pull-request-merge">
533 531 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
534 532 </div>
535 533
536 534 %endif
537 535
538 536 % endif
539 537 </div>
540 538
541 539
542 540 ### NAV SIDEBAR
543 541 <aside class="right-sidebar right-sidebar-expanded" id="pr-nav-sticky" style="display: none">
544 542 <div class="sidenav navbar__inner" >
545 543 ## TOGGLE
546 544 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
547 545 <a href="#toggleSidebar" class="grey-link-action">
548 546
549 547 </a>
550 548 </div>
551 549
552 550 ## CONTENT
553 551 <div class="sidebar-content">
554 552
555 553 ## Drafts
556 554 % if c.rhodecode_edition_id == 'EE':
557 555 <div class="sidebar-element clear-both">
558 556 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Drafts')}">
559 557 <i class="icon-comment icon-draft"></i>
560 558 <span id="comments-count">${0}</span>
561 559 </div>
562 560
563 561 <div class="right-sidebar-expanded-state pr-details-title">
564 562 <span class="sidebar-heading noselect">
565 563 <i class="icon-comment icon-draft"></i>
566 564 ${_('Drafts')}
567 565 </span>
568 566 </div>
569 567
570 568 <div id="drafts" class="right-sidebar-expanded-state pr-details-content reviewers">
571 569 ## members redering block
572 570
573 571
574 572 ???
575 573
576 574
577 575 ## end members redering block
578 576
579 577 </div>
580 578
581 579 </div>
582 580 % endif
583 581
584 582 ## RULES SUMMARY/RULES
585 583 <div class="sidebar-element clear-both">
586 584 <% vote_title = _ungettext(
587 585 'Status calculated based on votes from {} reviewer',
588 586 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
589 587 %>
590 588
591 589 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
592 590 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
593 591 ${c.reviewers_count}
594 592 </div>
595 593
596 594 ## REVIEW RULES
597 595 <div id="review_rules" style="display: none" class="">
598 596 <div class="right-sidebar-expanded-state pr-details-title">
599 597 <span class="sidebar-heading">
600 598 ${_('Reviewer rules')}
601 599 </span>
602 600
603 601 </div>
604 602 <div class="pr-reviewer-rules">
605 603 ## review rules will be appended here, by default reviewers logic
606 604 </div>
607 605 <input id="review_data" type="hidden" name="review_data" value="">
608 606 </div>
609 607
610 608 ## REVIEWERS
611 609 <div class="right-sidebar-expanded-state pr-details-title">
612 610 <span class="tooltip sidebar-heading" title="${vote_title}">
613 611 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
614 612 ${_('Reviewers')}
615 613 </span>
616 614 %if c.allowed_to_update:
617 615 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
618 616 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
619 617 %else:
620 618 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Show rules')}</span>
621 619 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
622 620 %endif
623 621 </div>
624 622
625 623 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
626 624
627 625 ## members redering block
628 626 <input type="hidden" name="__start__" value="review_members:sequence">
629 627
630 628 <table id="review_members" class="group_members">
631 629 ## This content is loaded via JS and ReviewersPanel
632 630 </table>
633 631
634 632 <input type="hidden" name="__end__" value="review_members:sequence">
635 633 ## end members redering block
636 634
637 635 %if not c.pull_request.is_closed():
638 636 <div id="add_reviewer" class="ac" style="display: none;">
639 637 %if c.allowed_to_update:
640 638 % if not c.forbid_adding_reviewers:
641 639 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px">
642 640 <input class="ac-input" id="user" name="user" placeholder="${_('Add reviewer or reviewer group')}" type="text" autocomplete="off">
643 641 <div id="reviewers_container"></div>
644 642 </div>
645 643 % endif
646 644 <div class="pull-right" style="margin-bottom: 15px">
647 645 <button data-role="reviewer" id="update_reviewers" class="btn btn-small no-margin">${_('Save Changes')}</button>
648 646 </div>
649 647 %endif
650 648 </div>
651 649 %endif
652 650 </div>
653 651 </div>
654 652
655 653 ## OBSERVERS
656 654 % if c.rhodecode_edition_id == 'EE':
657 655 <div class="sidebar-element clear-both">
658 656 <% vote_title = _ungettext(
659 657 '{} observer without voting right.',
660 658 '{} observers without voting right.', c.observers_count).format(c.observers_count)
661 659 %>
662 660
663 661 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
664 662 <i class="icon-circle-thin"></i>
665 663 ${c.observers_count}
666 664 </div>
667 665
668 666 <div class="right-sidebar-expanded-state pr-details-title">
669 667 <span class="tooltip sidebar-heading" title="${vote_title}">
670 668 <i class="icon-circle-thin"></i>
671 669 ${_('Observers')}
672 670 </span>
673 671 %if c.allowed_to_update:
674 672 <span id="open_edit_observers" class="block-right action_button last-item">${_('Edit')}</span>
675 673 <span id="close_edit_observers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
676 674 %endif
677 675 </div>
678 676
679 677 <div id="observers" class="right-sidebar-expanded-state pr-details-content reviewers">
680 678 ## members redering block
681 679 <input type="hidden" name="__start__" value="observer_members:sequence">
682 680
683 681 <table id="observer_members" class="group_members">
684 682 ## This content is loaded via JS and ReviewersPanel
685 683 </table>
686 684
687 685 <input type="hidden" name="__end__" value="observer_members:sequence">
688 686 ## end members redering block
689 687
690 688 %if not c.pull_request.is_closed():
691 689 <div id="add_observer" class="ac" style="display: none;">
692 690 %if c.allowed_to_update:
693 691 % if not c.forbid_adding_reviewers or 1:
694 692 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px" >
695 693 <input class="ac-input" id="observer" name="observer" placeholder="${_('Add observer or observer group')}" type="text" autocomplete="off">
696 694 <div id="observers_container"></div>
697 695 </div>
698 696 % endif
699 697 <div class="pull-right" style="margin-bottom: 15px">
700 698 <button data-role="observer" id="update_observers" class="btn btn-small no-margin">${_('Save Changes')}</button>
701 699 </div>
702 700 %endif
703 701 </div>
704 702 %endif
705 703 </div>
706 704 </div>
707 705 % endif
708 706
709 707 ## TODOs
710 708 <div class="sidebar-element clear-both">
711 709 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
712 710 <i class="icon-flag-filled"></i>
713 711 <span id="todos-count">${len(c.unresolved_comments)}</span>
714 712 </div>
715 713
716 714 <div class="right-sidebar-expanded-state pr-details-title">
717 715 ## Only show unresolved, that is only what matters
718 716 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
719 717 <i class="icon-flag-filled"></i>
720 718 TODOs
721 719 </span>
722 720
723 721 % if not c.at_version:
724 722 % if c.resolved_comments:
725 723 <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>
726 724 % else:
727 725 <span class="block-right last-item noselect">Show resolved</span>
728 726 % endif
729 727 % endif
730 728 </div>
731 729
732 730 <div class="right-sidebar-expanded-state pr-details-content">
733 731
734 732 % if c.at_version:
735 733 <table>
736 734 <tr>
737 735 <td class="unresolved-todo-text">${_('TODOs unavailable when browsing versions')}.</td>
738 736 </tr>
739 737 </table>
740 738 % else:
741 739 % if c.unresolved_comments + c.resolved_comments:
742 740 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True)}
743 741 % else:
744 742 <table>
745 743 <tr>
746 744 <td>
747 745 ${_('No TODOs yet')}
748 746 </td>
749 747 </tr>
750 748 </table>
751 749 % endif
752 750 % endif
753 751 </div>
754 752 </div>
755 753
756 754 ## COMMENTS
757 755 <div class="sidebar-element clear-both">
758 756 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
759 757 <i class="icon-comment" style="color: #949494"></i>
760 758 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
761 759 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
762 760 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
763 761 </div>
764 762
765 763 <div class="right-sidebar-expanded-state pr-details-title">
766 764 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
767 765 <i class="icon-comment" style="color: #949494"></i>
768 766 ${_('Comments')}
769 767
770 768 ## % if outdated_comm_count_ver:
771 769 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
772 770 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
773 771 ## </a>
774 772 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
775 773 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
776 774
777 775 ## % else:
778 776 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
779 777 ## % endif
780 778
781 779 </span>
782 780
783 781 % if outdated_comm_count_ver:
784 782 <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>
785 783 % else:
786 784 <span class="block-right last-item noselect">Show hidden</span>
787 785 % endif
788 786
789 787 </div>
790 788
791 789 <div class="right-sidebar-expanded-state pr-details-content">
792 790 % if c.inline_comments_flat + c.comments:
793 791 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
794 792 % else:
795 793 <table>
796 794 <tr>
797 795 <td>
798 796 ${_('No Comments yet')}
799 797 </td>
800 798 </tr>
801 799 </table>
802 800 % endif
803 801 </div>
804 802
805 803 </div>
806 804
807 805 ## Referenced Tickets
808 806 <div class="sidebar-element clear-both">
809 807 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Referenced Tickets')}">
810 808 <i class="icon-info-circled"></i>
811 809 ${(len(c.referenced_desc_issues) + len(c.referenced_commit_issues))}
812 810 </div>
813 811
814 812 <div class="right-sidebar-expanded-state pr-details-title">
815 813 <span class="sidebar-heading">
816 814 <i class="icon-info-circled"></i>
817 815 ${_('Referenced Tickets')}
818 816 </span>
819 817 </div>
820 818 <div class="right-sidebar-expanded-state pr-details-content">
821 819 <table>
822 820
823 821 <tr><td><code>${_('In pull request description')}:</code></td></tr>
824 822 % if c.referenced_desc_issues:
825 823 % for ticket_dict in sorted(c.referenced_desc_issues):
826 824 <tr>
827 825 <td>
828 826 <a href="${ticket_dict.get('url')}">
829 827 ${ticket_dict.get('id')}
830 828 </a>
831 829 </td>
832 830 </tr>
833 831 % endfor
834 832 % else:
835 833 <tr>
836 834 <td>
837 835 ${_('No Ticket data found.')}
838 836 </td>
839 837 </tr>
840 838 % endif
841 839
842 840 <tr><td style="padding-top: 10px"><code>${_('In commit messages')}:</code></td></tr>
843 841 % if c.referenced_commit_issues:
844 842 % for ticket_dict in sorted(c.referenced_commit_issues):
845 843 <tr>
846 844 <td>
847 845 <a href="${ticket_dict.get('url')}">
848 846 ${ticket_dict.get('id')}
849 847 </a>
850 848 </td>
851 849 </tr>
852 850 % endfor
853 851 % else:
854 852 <tr>
855 853 <td>
856 854 ${_('No Ticket data found.')}
857 855 </td>
858 856 </tr>
859 857 % endif
860 858 </table>
861 859
862 860 </div>
863 861 </div>
864 862
865 863 </div>
866 864
867 865 </div>
868 866 </aside>
869 867
870 868 ## This JS needs to be at the end
871 869 <script type="text/javascript">
872 870
873 871 versionController = new VersionController();
874 872 versionController.init();
875 873
876 874 reviewersController = new ReviewersController();
877 875 commitsController = new CommitsController();
878 876 commentsController = new CommentsController();
879 877
880 878 updateController = new UpdatePrController();
881 879
882 880 window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
883 881 window.setReviewersData = ${c.pull_request_set_reviewers_data_json | n};
884 882 window.setObserversData = ${c.pull_request_set_observers_data_json | n};
885 883
886 884 (function () {
887 885 "use strict";
888 886
889 887 // custom code mirror
890 888 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
891 889
892 890 PRDetails.init();
893 891 ReviewersPanel.init(reviewersController, reviewerRulesData, setReviewersData);
894 892 ObserversPanel.init(reviewersController, reviewerRulesData, setObserversData);
895 893
896 894 window.showOutdated = function (self) {
897 895 $('.comment-inline.comment-outdated').show();
898 896 $('.filediff-outdated').show();
899 897 $('.showOutdatedComments').hide();
900 898 $('.hideOutdatedComments').show();
901 899 };
902 900
903 901 window.hideOutdated = function (self) {
904 902 $('.comment-inline.comment-outdated').hide();
905 903 $('.filediff-outdated').hide();
906 904 $('.hideOutdatedComments').hide();
907 905 $('.showOutdatedComments').show();
908 906 };
909 907
910 908 window.refreshMergeChecks = function () {
911 909 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
912 910 $('.pull-request-merge').css('opacity', 0.3);
913 911 $('.action-buttons-extra').css('opacity', 0.3);
914 912
915 913 $('.pull-request-merge').load(
916 914 loadUrl, function () {
917 915 $('.pull-request-merge').css('opacity', 1);
918 916
919 917 $('.action-buttons-extra').css('opacity', 1);
920 918 }
921 919 );
922 920 };
923 921
924 922 window.closePullRequest = function (status) {
925 923 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
926 924 return false;
927 925 }
928 926 // inject closing flag
929 927 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
930 928 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
931 929 $(generalCommentForm.submitForm).submit();
932 930 };
933 931
934 932 //TODO this functionality is now missing
935 933 $('#show-outdated-comments').on('click', function (e) {
936 934 var button = $(this);
937 935 var outdated = $('.comment-outdated');
938 936
939 937 if (button.html() === "(Show)") {
940 938 button.html("(Hide)");
941 939 outdated.show();
942 940 } else {
943 941 button.html("(Show)");
944 942 outdated.hide();
945 943 }
946 944 });
947 945
948 946 $('#merge_pull_request_form').submit(function () {
949 947 if (!$('#merge_pull_request').attr('disabled')) {
950 948 $('#merge_pull_request').attr('disabled', 'disabled');
951 949 }
952 950 return true;
953 951 });
954 952
955 953 $('#edit_pull_request').on('click', function (e) {
956 954 var title = $('#pr-title-input').val();
957 955 var description = codeMirrorInstance.getValue();
958 956 var renderer = $('#pr-renderer-input').val();
959 957 editPullRequest(
960 958 "${c.repo_name}", "${c.pull_request.pull_request_id}",
961 959 title, description, renderer);
962 960 });
963 961
964 962 var $updateButtons = $('#update_reviewers,#update_observers');
965 963 $updateButtons.on('click', function (e) {
966 964 var role = $(this).data('role');
967 965 $updateButtons.attr('disabled', 'disabled');
968 966 $updateButtons.addClass('disabled');
969 967 $updateButtons.html(_gettext('Saving...'));
970 968 reviewersController.updateReviewers(
971 969 templateContext.repo_name,
972 970 templateContext.pull_request_data.pull_request_id,
973 971 role
974 972 );
975 973 });
976 974
977 975 // fixing issue with caches on firefox
978 976 $('#update_commits').removeAttr("disabled");
979 977
980 978 $('.show-inline-comments').on('click', function (e) {
981 979 var boxid = $(this).attr('data-comment-id');
982 980 var button = $(this);
983 981
984 982 if (button.hasClass("comments-visible")) {
985 983 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
986 984 $(this).hide();
987 985 });
988 986 button.removeClass("comments-visible");
989 987 } else {
990 988 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
991 989 $(this).show();
992 990 });
993 991 button.addClass("comments-visible");
994 992 }
995 993 });
996 994
997 995 $('.show-inline-comments').on('change', function (e) {
998 996 var show = 'none';
999 997 var target = e.currentTarget;
1000 998 if (target.checked) {
1001 999 show = ''
1002 1000 }
1003 1001 var boxid = $(target).attr('id_for');
1004 1002 var comments = $('#{0} .inline-comments'.format(boxid));
1005 1003 var fn_display = function (idx) {
1006 1004 $(this).css('display', show);
1007 1005 };
1008 1006 $(comments).each(fn_display);
1009 1007 var btns = $('#{0} .inline-comments-button'.format(boxid));
1010 1008 $(btns).each(fn_display);
1011 1009 });
1012 1010
1013 1011 // register submit callback on commentForm form to track TODOs, and refresh mergeChecks conditions
1014 1012 window.commentFormGlobalSubmitSuccessCallback = function (comment) {
1015 1013 if (!comment.draft) {
1016 1014 refreshMergeChecks();
1017 1015 }
1018 1016 };
1019 1017
1020 1018 ReviewerAutoComplete('#user', reviewersController);
1021 1019 ObserverAutoComplete('#observer', reviewersController);
1022 1020
1023 1021 })();
1024 1022
1025 1023 $(document).ready(function () {
1026 1024
1027 1025 var channel = '${c.pr_broadcast_channel}';
1028 1026 new ReviewerPresenceController(channel)
1029 1027 // register globally so inject comment logic can re-use it.
1030 1028 window.commentsController = commentsController;
1031 1029
1032 1030 })
1033 1031 </script>
1034 1032
1035 1033 </%def>
General Comments 0
You need to be logged in to leave comments. Login now