##// END OF EJS Templates
comments: small fixes for range commits comments
milka -
r4551:d3ec0df2 default
parent child Browse files
Show More
@@ -1,802 +1,802 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
81 81 # fetch global flags of ignore ws or context lines
82 82 diff_context = get_diff_context(self.request)
83 83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
84 84
85 85 # diff_limit will cut off the whole diff if the limit is applied
86 86 # otherwise it will just hide the big files from the front-end
87 87 diff_limit = c.visual.cut_off_limit_diff
88 88 file_limit = c.visual.cut_off_limit_file
89 89
90 90 # get ranges of commit ids if preset
91 91 commit_range = commit_id_range.split('...')[:2]
92 92
93 93 try:
94 94 pre_load = ['affected_files', 'author', 'branch', 'date',
95 95 'message', 'parents']
96 96 if self.rhodecode_vcs_repo.alias == 'hg':
97 97 pre_load += ['hidden', 'obsolete', 'phase']
98 98
99 99 if len(commit_range) == 2:
100 100 commits = self.rhodecode_vcs_repo.get_commits(
101 101 start_id=commit_range[0], end_id=commit_range[1],
102 102 pre_load=pre_load, translate_tags=False)
103 103 commits = list(commits)
104 104 else:
105 105 commits = [self.rhodecode_vcs_repo.get_commit(
106 106 commit_id=commit_id_range, pre_load=pre_load)]
107 107
108 108 c.commit_ranges = commits
109 109 if not c.commit_ranges:
110 110 raise RepositoryError('The commit range returned an empty result')
111 111 except CommitDoesNotExistError as e:
112 112 msg = _('No such commit exists. Org exception: `{}`').format(e)
113 113 h.flash(msg, category='error')
114 114 raise HTTPNotFound()
115 115 except Exception:
116 116 log.exception("General failure")
117 117 raise HTTPNotFound()
118 118 single_commit = len(c.commit_ranges) == 1
119 119
120 120 c.changes = OrderedDict()
121 121 c.lines_added = 0
122 122 c.lines_deleted = 0
123 123
124 124 # auto collapse if we have more than limit
125 125 collapse_limit = diffs.DiffProcessor._collapse_commits_over
126 126 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
127 127
128 128 c.commit_statuses = ChangesetStatus.STATUSES
129 129 c.inline_comments = []
130 130 c.files = []
131 131
132 132 c.comments = []
133 133 c.unresolved_comments = []
134 134 c.resolved_comments = []
135 135
136 136 # Single commit
137 137 if single_commit:
138 138 commit = c.commit_ranges[0]
139 139 c.comments = CommentsModel().get_comments(
140 140 self.db_repo.repo_id,
141 141 revision=commit.raw_id)
142 142
143 143 # comments from PR
144 144 statuses = ChangesetStatusModel().get_statuses(
145 145 self.db_repo.repo_id, commit.raw_id,
146 146 with_revisions=True)
147 147
148 148 prs = set()
149 149 reviewers = list()
150 150 reviewers_duplicates = set() # to not have duplicates from multiple votes
151 151 for c_status in statuses:
152 152
153 153 # extract associated pull-requests from votes
154 154 if c_status.pull_request:
155 155 prs.add(c_status.pull_request)
156 156
157 157 # extract reviewers
158 158 _user_id = c_status.author.user_id
159 159 if _user_id not in reviewers_duplicates:
160 160 reviewers.append(
161 161 StrictAttributeDict({
162 162 'user': c_status.author,
163 163
164 164 # fake attributed for commit, page that we don't have
165 165 # but we share the display with PR page
166 166 'mandatory': False,
167 167 'reasons': [],
168 168 'rule_user_group_data': lambda: None
169 169 })
170 170 )
171 171 reviewers_duplicates.add(_user_id)
172 172
173 173 c.reviewers_count = len(reviewers)
174 174 c.observers_count = 0
175 175
176 176 # from associated statuses, check the pull requests, and
177 177 # show comments from them
178 178 for pr in prs:
179 179 c.comments.extend(pr.comments)
180 180
181 181 c.unresolved_comments = CommentsModel()\
182 182 .get_commit_unresolved_todos(commit.raw_id)
183 183 c.resolved_comments = CommentsModel()\
184 184 .get_commit_resolved_todos(commit.raw_id)
185 185
186 186 c.inline_comments_flat = CommentsModel()\
187 187 .get_commit_inline_comments(commit.raw_id)
188 188
189 189 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
190 190 statuses, reviewers)
191 191
192 192 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
193 193
194 194 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
195 195
196 196 for review_obj, member, reasons, mandatory, status in review_statuses:
197 197 member_reviewer = h.reviewer_as_json(
198 198 member, reasons=reasons, mandatory=mandatory, role=None,
199 199 user_group=None
200 200 )
201 201
202 202 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
203 203 member_reviewer['review_status'] = current_review_status
204 204 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
205 205 member_reviewer['allowed_to_update'] = False
206 206 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
207 207
208 208 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
209 209
210 210 # NOTE(marcink): this uses the same voting logic as in pull-requests
211 211 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
212 212 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
213 213
214 214 diff = None
215 215 # Iterate over ranges (default commit view is always one commit)
216 216 for commit in c.commit_ranges:
217 217 c.changes[commit.raw_id] = []
218 218
219 219 commit2 = commit
220 220 commit1 = commit.first_parent
221 221
222 222 if method == 'show':
223 223 inline_comments = CommentsModel().get_inline_comments(
224 224 self.db_repo.repo_id, revision=commit.raw_id)
225 225 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
226 226 inline_comments))
227 227 c.inline_comments = inline_comments
228 228
229 229 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
230 230 self.db_repo)
231 231 cache_file_path = diff_cache_exist(
232 232 cache_path, 'diff', commit.raw_id,
233 233 hide_whitespace_changes, diff_context, c.fulldiff)
234 234
235 235 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
236 236 force_recache = str2bool(self.request.GET.get('force_recache'))
237 237
238 238 cached_diff = None
239 239 if caching_enabled:
240 240 cached_diff = load_cached_diff(cache_file_path)
241 241
242 242 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
243 243 if not force_recache and has_proper_diff_cache:
244 244 diffset = cached_diff['diff']
245 245 else:
246 246 vcs_diff = self.rhodecode_vcs_repo.get_diff(
247 247 commit1, commit2,
248 248 ignore_whitespace=hide_whitespace_changes,
249 249 context=diff_context)
250 250
251 251 diff_processor = diffs.DiffProcessor(
252 252 vcs_diff, format='newdiff', diff_limit=diff_limit,
253 253 file_limit=file_limit, show_full_diff=c.fulldiff)
254 254
255 255 _parsed = diff_processor.prepare()
256 256
257 257 diffset = codeblocks.DiffSet(
258 258 repo_name=self.db_repo_name,
259 259 source_node_getter=codeblocks.diffset_node_getter(commit1),
260 260 target_node_getter=codeblocks.diffset_node_getter(commit2))
261 261
262 262 diffset = self.path_filter.render_patchset_filtered(
263 263 diffset, _parsed, commit1.raw_id, commit2.raw_id)
264 264
265 265 # save cached diff
266 266 if caching_enabled:
267 267 cache_diff(cache_file_path, diffset, None)
268 268
269 269 c.limited_diff = diffset.limited_diff
270 270 c.changes[commit.raw_id] = diffset
271 271 else:
272 272 # TODO(marcink): no cache usage here...
273 273 _diff = self.rhodecode_vcs_repo.get_diff(
274 274 commit1, commit2,
275 275 ignore_whitespace=hide_whitespace_changes, context=diff_context)
276 276 diff_processor = diffs.DiffProcessor(
277 277 _diff, format='newdiff', diff_limit=diff_limit,
278 278 file_limit=file_limit, show_full_diff=c.fulldiff)
279 279 # downloads/raw we only need RAW diff nothing else
280 280 diff = self.path_filter.get_raw_patch(diff_processor)
281 281 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
282 282
283 283 # sort comments by how they were generated
284 284 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
285 285 c.at_version_num = None
286 286
287 287 if len(c.commit_ranges) == 1:
288 288 c.commit = c.commit_ranges[0]
289 289 c.parent_tmpl = ''.join(
290 290 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
291 291
292 292 if method == 'download':
293 293 response = Response(diff)
294 294 response.content_type = 'text/plain'
295 295 response.content_disposition = (
296 296 'attachment; filename=%s.diff' % commit_id_range[:12])
297 297 return response
298 298 elif method == 'patch':
299 299 c.diff = safe_unicode(diff)
300 300 patch = render(
301 301 'rhodecode:templates/changeset/patch_changeset.mako',
302 302 self._get_template_context(c), self.request)
303 303 response = Response(patch)
304 304 response.content_type = 'text/plain'
305 305 return response
306 306 elif method == 'raw':
307 307 response = Response(diff)
308 308 response.content_type = 'text/plain'
309 309 return response
310 310 elif method == 'show':
311 311 if len(c.commit_ranges) == 1:
312 312 html = render(
313 313 'rhodecode:templates/changeset/changeset.mako',
314 314 self._get_template_context(c), self.request)
315 315 return Response(html)
316 316 else:
317 317 c.ancestor = None
318 318 c.target_repo = self.db_repo
319 319 html = render(
320 320 'rhodecode:templates/changeset/changeset_range.mako',
321 321 self._get_template_context(c), self.request)
322 322 return Response(html)
323 323
324 324 raise HTTPBadRequest()
325 325
326 326 @LoginRequired()
327 327 @HasRepoPermissionAnyDecorator(
328 328 'repository.read', 'repository.write', 'repository.admin')
329 329 @view_config(
330 330 route_name='repo_commit', request_method='GET',
331 331 renderer=None)
332 332 def repo_commit_show(self):
333 333 commit_id = self.request.matchdict['commit_id']
334 334 return self._commit(commit_id, method='show')
335 335
336 336 @LoginRequired()
337 337 @HasRepoPermissionAnyDecorator(
338 338 'repository.read', 'repository.write', 'repository.admin')
339 339 @view_config(
340 340 route_name='repo_commit_raw', request_method='GET',
341 341 renderer=None)
342 342 @view_config(
343 343 route_name='repo_commit_raw_deprecated', request_method='GET',
344 344 renderer=None)
345 345 def repo_commit_raw(self):
346 346 commit_id = self.request.matchdict['commit_id']
347 347 return self._commit(commit_id, method='raw')
348 348
349 349 @LoginRequired()
350 350 @HasRepoPermissionAnyDecorator(
351 351 'repository.read', 'repository.write', 'repository.admin')
352 352 @view_config(
353 353 route_name='repo_commit_patch', request_method='GET',
354 354 renderer=None)
355 355 def repo_commit_patch(self):
356 356 commit_id = self.request.matchdict['commit_id']
357 357 return self._commit(commit_id, method='patch')
358 358
359 359 @LoginRequired()
360 360 @HasRepoPermissionAnyDecorator(
361 361 'repository.read', 'repository.write', 'repository.admin')
362 362 @view_config(
363 363 route_name='repo_commit_download', request_method='GET',
364 364 renderer=None)
365 365 def repo_commit_download(self):
366 366 commit_id = self.request.matchdict['commit_id']
367 367 return self._commit(commit_id, method='download')
368 368
369 369 @LoginRequired()
370 370 @NotAnonymous()
371 371 @HasRepoPermissionAnyDecorator(
372 372 'repository.read', 'repository.write', 'repository.admin')
373 373 @CSRFRequired()
374 374 @view_config(
375 375 route_name='repo_commit_comment_create', request_method='POST',
376 376 renderer='json_ext')
377 377 def repo_commit_comment_create(self):
378 378 _ = self.request.translate
379 379 commit_id = self.request.matchdict['commit_id']
380 380
381 381 c = self.load_default_context()
382 382 status = self.request.POST.get('changeset_status', None)
383 383 is_draft = str2bool(self.request.POST.get('draft'))
384 384 text = self.request.POST.get('text')
385 385 comment_type = self.request.POST.get('comment_type')
386 386 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
387 387 f_path = self.request.POST.get('f_path')
388 388 line_no = self.request.POST.get('line')
389 389 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
390 390
391 391 if status:
392 392 text = text or (_('Status change %(transition_icon)s %(status)s')
393 393 % {'transition_icon': '>',
394 394 'status': ChangesetStatus.get_status_lbl(status)})
395 395
396 396 multi_commit_ids = []
397 397 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
398 398 if _commit_id not in ['', None, EmptyCommit.raw_id]:
399 399 if _commit_id not in multi_commit_ids:
400 400 multi_commit_ids.append(_commit_id)
401 401
402 402 commit_ids = multi_commit_ids or [commit_id]
403 403
404 404 data = {}
405 405 # Multiple comments for each passed commit id
406 406 for current_id in filter(None, commit_ids):
407 407 comment = CommentsModel().create(
408 408 text=text,
409 409 repo=self.db_repo.repo_id,
410 410 user=self._rhodecode_db_user.user_id,
411 411 commit_id=current_id,
412 412 f_path=f_path,
413 413 line_no=line_no,
414 414 status_change=(ChangesetStatus.get_status_lbl(status)
415 415 if status else None),
416 416 status_change_type=status,
417 417 comment_type=comment_type,
418 418 is_draft=is_draft,
419 419 resolves_comment_id=resolves_comment_id,
420 420 auth_user=self._rhodecode_user,
421 421 send_email=not is_draft, # skip notification for draft comments
422 422 )
423 423 is_inline = comment.is_inline
424 424
425 425 # get status if set !
426 426 if status:
427 427 # if latest status was from pull request and it's closed
428 428 # disallow changing status !
429 429 # dont_allow_on_closed_pull_request = True !
430 430
431 431 try:
432 432 ChangesetStatusModel().set_status(
433 433 self.db_repo.repo_id,
434 434 status,
435 435 self._rhodecode_db_user.user_id,
436 436 comment,
437 437 revision=current_id,
438 438 dont_allow_on_closed_pull_request=True
439 439 )
440 440 except StatusChangeOnClosedPullRequestError:
441 441 msg = _('Changing the status of a commit associated with '
442 442 'a closed pull request is not allowed')
443 443 log.exception(msg)
444 444 h.flash(msg, category='warning')
445 445 raise HTTPFound(h.route_path(
446 446 'repo_commit', repo_name=self.db_repo_name,
447 447 commit_id=current_id))
448 448
449 449 # skip notifications for drafts
450 450 if not is_draft:
451 451 commit = self.db_repo.get_commit(current_id)
452 452 CommentsModel().trigger_commit_comment_hook(
453 453 self.db_repo, self._rhodecode_user, 'create',
454 454 data={'comment': comment, 'commit': commit})
455 455
456 456 comment_id = comment.comment_id
457 457 data[comment_id] = {
458 458 'target_id': target_elem_id
459 459 }
460 460 c.co = comment
461 461 c.at_version_num = 0
462 462 c.is_new = True
463 463 rendered_comment = render(
464 464 'rhodecode:templates/changeset/changeset_comment_block.mako',
465 465 self._get_template_context(c), self.request)
466 466
467 467 data[comment_id].update(comment.get_dict())
468 468 data[comment_id].update({'rendered_text': rendered_comment})
469 469
470 470 # skip channelstream for draft comments
471 471 if not is_draft:
472 472 comment_broadcast_channel = channelstream.comment_channel(
473 473 self.db_repo_name, commit_obj=commit)
474 474
475 475 comment_data = data
476 comment_type = 'inline' if is_inline else 'general'
476 posted_comment_type = 'inline' if is_inline else 'general'
477 477 channelstream.comment_channelstream_push(
478 478 self.request, comment_broadcast_channel, self._rhodecode_user,
479 _('posted a new {} comment').format(comment_type),
479 _('posted a new {} comment').format(posted_comment_type),
480 480 comment_data=comment_data)
481 481
482 482 # finalize, commit and redirect
483 483 Session().commit()
484 484
485 485 return data
486 486
487 487 @LoginRequired()
488 488 @NotAnonymous()
489 489 @HasRepoPermissionAnyDecorator(
490 490 'repository.read', 'repository.write', 'repository.admin')
491 491 @CSRFRequired()
492 492 @view_config(
493 493 route_name='repo_commit_comment_preview', request_method='POST',
494 494 renderer='string', xhr=True)
495 495 def repo_commit_comment_preview(self):
496 496 # Technically a CSRF token is not needed as no state changes with this
497 497 # call. However, as this is a POST is better to have it, so automated
498 498 # tools don't flag it as potential CSRF.
499 499 # Post is required because the payload could be bigger than the maximum
500 500 # allowed by GET.
501 501
502 502 text = self.request.POST.get('text')
503 503 renderer = self.request.POST.get('renderer') or 'rst'
504 504 if text:
505 505 return h.render(text, renderer=renderer, mentions=True,
506 506 repo_name=self.db_repo_name)
507 507 return ''
508 508
509 509 @LoginRequired()
510 510 @HasRepoPermissionAnyDecorator(
511 511 'repository.read', 'repository.write', 'repository.admin')
512 512 @CSRFRequired()
513 513 @view_config(
514 514 route_name='repo_commit_comment_history_view', request_method='POST',
515 515 renderer='string', xhr=True)
516 516 def repo_commit_comment_history_view(self):
517 517 c = self.load_default_context()
518 518
519 519 comment_history_id = self.request.matchdict['comment_history_id']
520 520 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
521 521 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
522 522
523 523 if is_repo_comment:
524 524 c.comment_history = comment_history
525 525
526 526 rendered_comment = render(
527 527 'rhodecode:templates/changeset/comment_history.mako',
528 528 self._get_template_context(c)
529 529 , self.request)
530 530 return rendered_comment
531 531 else:
532 532 log.warning('No permissions for user %s to show comment_history_id: %s',
533 533 self._rhodecode_db_user, comment_history_id)
534 534 raise HTTPNotFound()
535 535
536 536 @LoginRequired()
537 537 @NotAnonymous()
538 538 @HasRepoPermissionAnyDecorator(
539 539 'repository.read', 'repository.write', 'repository.admin')
540 540 @CSRFRequired()
541 541 @view_config(
542 542 route_name='repo_commit_comment_attachment_upload', request_method='POST',
543 543 renderer='json_ext', xhr=True)
544 544 def repo_commit_comment_attachment_upload(self):
545 545 c = self.load_default_context()
546 546 upload_key = 'attachment'
547 547
548 548 file_obj = self.request.POST.get(upload_key)
549 549
550 550 if file_obj is None:
551 551 self.request.response.status = 400
552 552 return {'store_fid': None,
553 553 'access_path': None,
554 554 'error': '{} data field is missing'.format(upload_key)}
555 555
556 556 if not hasattr(file_obj, 'filename'):
557 557 self.request.response.status = 400
558 558 return {'store_fid': None,
559 559 'access_path': None,
560 560 'error': 'filename cannot be read from the data field'}
561 561
562 562 filename = file_obj.filename
563 563 file_display_name = filename
564 564
565 565 metadata = {
566 566 'user_uploaded': {'username': self._rhodecode_user.username,
567 567 'user_id': self._rhodecode_user.user_id,
568 568 'ip': self._rhodecode_user.ip_addr}}
569 569
570 570 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
571 571 allowed_extensions = [
572 572 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
573 573 '.pptx', '.txt', '.xlsx', '.zip']
574 574 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
575 575
576 576 try:
577 577 storage = store_utils.get_file_storage(self.request.registry.settings)
578 578 store_uid, metadata = storage.save_file(
579 579 file_obj.file, filename, extra_metadata=metadata,
580 580 extensions=allowed_extensions, max_filesize=max_file_size)
581 581 except FileNotAllowedException:
582 582 self.request.response.status = 400
583 583 permitted_extensions = ', '.join(allowed_extensions)
584 584 error_msg = 'File `{}` is not allowed. ' \
585 585 'Only following extensions are permitted: {}'.format(
586 586 filename, permitted_extensions)
587 587 return {'store_fid': None,
588 588 'access_path': None,
589 589 'error': error_msg}
590 590 except FileOverSizeException:
591 591 self.request.response.status = 400
592 592 limit_mb = h.format_byte_size_binary(max_file_size)
593 593 return {'store_fid': None,
594 594 'access_path': None,
595 595 'error': 'File {} is exceeding allowed limit of {}.'.format(
596 596 filename, limit_mb)}
597 597
598 598 try:
599 599 entry = FileStore.create(
600 600 file_uid=store_uid, filename=metadata["filename"],
601 601 file_hash=metadata["sha256"], file_size=metadata["size"],
602 602 file_display_name=file_display_name,
603 603 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
604 604 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
605 605 scope_repo_id=self.db_repo.repo_id
606 606 )
607 607 Session().add(entry)
608 608 Session().commit()
609 609 log.debug('Stored upload in DB as %s', entry)
610 610 except Exception:
611 611 log.exception('Failed to store file %s', filename)
612 612 self.request.response.status = 400
613 613 return {'store_fid': None,
614 614 'access_path': None,
615 615 'error': 'File {} failed to store in DB.'.format(filename)}
616 616
617 617 Session().commit()
618 618
619 619 return {
620 620 'store_fid': store_uid,
621 621 'access_path': h.route_path(
622 622 'download_file', fid=store_uid),
623 623 'fqn_access_path': h.route_url(
624 624 'download_file', fid=store_uid),
625 625 'repo_access_path': h.route_path(
626 626 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
627 627 'repo_fqn_access_path': h.route_url(
628 628 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
629 629 }
630 630
631 631 @LoginRequired()
632 632 @NotAnonymous()
633 633 @HasRepoPermissionAnyDecorator(
634 634 'repository.read', 'repository.write', 'repository.admin')
635 635 @CSRFRequired()
636 636 @view_config(
637 637 route_name='repo_commit_comment_delete', request_method='POST',
638 638 renderer='json_ext')
639 639 def repo_commit_comment_delete(self):
640 640 commit_id = self.request.matchdict['commit_id']
641 641 comment_id = self.request.matchdict['comment_id']
642 642
643 643 comment = ChangesetComment.get_or_404(comment_id)
644 644 if not comment:
645 645 log.debug('Comment with id:%s not found, skipping', comment_id)
646 646 # comment already deleted in another call probably
647 647 return True
648 648
649 649 if comment.immutable:
650 650 # don't allow deleting comments that are immutable
651 651 raise HTTPForbidden()
652 652
653 653 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
654 654 super_admin = h.HasPermissionAny('hg.admin')()
655 655 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
656 656 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
657 657 comment_repo_admin = is_repo_admin and is_repo_comment
658 658
659 659 if super_admin or comment_owner or comment_repo_admin:
660 660 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
661 661 Session().commit()
662 662 return True
663 663 else:
664 664 log.warning('No permissions for user %s to delete comment_id: %s',
665 665 self._rhodecode_db_user, comment_id)
666 666 raise HTTPNotFound()
667 667
668 668 @LoginRequired()
669 669 @NotAnonymous()
670 670 @HasRepoPermissionAnyDecorator(
671 671 'repository.read', 'repository.write', 'repository.admin')
672 672 @CSRFRequired()
673 673 @view_config(
674 674 route_name='repo_commit_comment_edit', request_method='POST',
675 675 renderer='json_ext')
676 676 def repo_commit_comment_edit(self):
677 677 self.load_default_context()
678 678
679 679 comment_id = self.request.matchdict['comment_id']
680 680 comment = ChangesetComment.get_or_404(comment_id)
681 681
682 682 if comment.immutable:
683 683 # don't allow deleting comments that are immutable
684 684 raise HTTPForbidden()
685 685
686 686 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
687 687 super_admin = h.HasPermissionAny('hg.admin')()
688 688 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
689 689 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
690 690 comment_repo_admin = is_repo_admin and is_repo_comment
691 691
692 692 if super_admin or comment_owner or comment_repo_admin:
693 693 text = self.request.POST.get('text')
694 694 version = self.request.POST.get('version')
695 695 if text == comment.text:
696 696 log.warning(
697 697 'Comment(repo): '
698 698 'Trying to create new version '
699 699 'with the same comment body {}'.format(
700 700 comment_id,
701 701 )
702 702 )
703 703 raise HTTPNotFound()
704 704
705 705 if version.isdigit():
706 706 version = int(version)
707 707 else:
708 708 log.warning(
709 709 'Comment(repo): Wrong version type {} {} '
710 710 'for comment {}'.format(
711 711 version,
712 712 type(version),
713 713 comment_id,
714 714 )
715 715 )
716 716 raise HTTPNotFound()
717 717
718 718 try:
719 719 comment_history = CommentsModel().edit(
720 720 comment_id=comment_id,
721 721 text=text,
722 722 auth_user=self._rhodecode_user,
723 723 version=version,
724 724 )
725 725 except CommentVersionMismatch:
726 726 raise HTTPConflict()
727 727
728 728 if not comment_history:
729 729 raise HTTPNotFound()
730 730
731 731 commit_id = self.request.matchdict['commit_id']
732 732 commit = self.db_repo.get_commit(commit_id)
733 733 CommentsModel().trigger_commit_comment_hook(
734 734 self.db_repo, self._rhodecode_user, 'edit',
735 735 data={'comment': comment, 'commit': commit})
736 736
737 737 Session().commit()
738 738 return {
739 739 'comment_history_id': comment_history.comment_history_id,
740 740 'comment_id': comment.comment_id,
741 741 'comment_version': comment_history.version,
742 742 'comment_author_username': comment_history.author.username,
743 743 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
744 744 'comment_created_on': h.age_component(comment_history.created_on,
745 745 time_is_local=True),
746 746 }
747 747 else:
748 748 log.warning('No permissions for user %s to edit comment_id: %s',
749 749 self._rhodecode_db_user, comment_id)
750 750 raise HTTPNotFound()
751 751
752 752 @LoginRequired()
753 753 @HasRepoPermissionAnyDecorator(
754 754 'repository.read', 'repository.write', 'repository.admin')
755 755 @view_config(
756 756 route_name='repo_commit_data', request_method='GET',
757 757 renderer='json_ext', xhr=True)
758 758 def repo_commit_data(self):
759 759 commit_id = self.request.matchdict['commit_id']
760 760 self.load_default_context()
761 761
762 762 try:
763 763 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
764 764 except CommitDoesNotExistError as e:
765 765 return EmptyCommit(message=str(e))
766 766
767 767 @LoginRequired()
768 768 @HasRepoPermissionAnyDecorator(
769 769 'repository.read', 'repository.write', 'repository.admin')
770 770 @view_config(
771 771 route_name='repo_commit_children', request_method='GET',
772 772 renderer='json_ext', xhr=True)
773 773 def repo_commit_children(self):
774 774 commit_id = self.request.matchdict['commit_id']
775 775 self.load_default_context()
776 776
777 777 try:
778 778 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
779 779 children = commit.children
780 780 except CommitDoesNotExistError:
781 781 children = []
782 782
783 783 result = {"results": children}
784 784 return result
785 785
786 786 @LoginRequired()
787 787 @HasRepoPermissionAnyDecorator(
788 788 'repository.read', 'repository.write', 'repository.admin')
789 789 @view_config(
790 790 route_name='repo_commit_parents', request_method='GET',
791 791 renderer='json_ext')
792 792 def repo_commit_parents(self):
793 793 commit_id = self.request.matchdict['commit_id']
794 794 self.load_default_context()
795 795
796 796 try:
797 797 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
798 798 parents = commit.parents
799 799 except CommitDoesNotExistError:
800 800 parents = []
801 801 result = {"results": parents}
802 802 return result
@@ -1,750 +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 512 height: 42px;
513 513 display: table-cell;
514 514 vertical-align: bottom;
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 p {
524 padding: 0;
525 margin: 0;
526 }
527 523 }
528 524
529 525 .action-buttons-extra {
530 526 display: inline-block;
531 527 }
532 528 }
533 529
534 530 .comment-form {
535 531
536 532 .comment {
537 533 margin-left: 10px;
538 534 }
539 535
540 536 .comment-help {
541 537 color: @grey4;
542 538 padding: 5px 0 5px 0;
543 539 }
544 540
545 541 .comment-title {
546 542 padding: 5px 0 5px 0;
547 543 }
548 544
549 545 .comment-button {
550 546 display: inline-block;
551 547 }
552 548
553 549 .comment-button-input {
554 550 margin-right: 0;
555 551 }
556 552
557 553 #save_general {
558 554 margin-left: -6px;
559 555 }
560 556
561 557 }
562 558
563 559
564 560 .comment-form-login {
565 561 .comment-help {
566 562 padding: 0.7em; //same as the button
567 563 }
568 564
569 565 div.clearfix {
570 566 clear: both;
571 567 width: 100%;
572 568 display: block;
573 569 }
574 570 }
575 571
576 572 .comment-version-select {
577 573 margin: 0px;
578 574 border-radius: inherit;
579 575 border-color: @grey6;
580 576 height: 20px;
581 577 }
582 578
583 579 .comment-type {
584 580 margin: 0px;
585 581 border-radius: inherit;
586 582 border-color: @grey6;
587 583 }
588 584
589 585 .preview-box {
590 586 min-height: 105px;
591 587 margin-bottom: 15px;
592 588 background-color: white;
593 589 .border-radius(@border-radius);
594 590 .box-sizing(border-box);
595 591 }
596 592
597 593 .add-another-button {
598 594 margin-left: 10px;
599 595 margin-top: 10px;
600 596 margin-bottom: 10px;
601 597 }
602 598
603 599 .comment .buttons {
604 600 float: right;
605 601 margin: -1px 0px 0px 0px;
606 602 }
607 603
608 604 // Inline Comment Form
609 605 .injected_diff .comment-inline-form,
610 606 .comment-inline-form {
611 607 background-color: white;
612 608 margin-top: 4px;
613 609 margin-bottom: 10px;
614 610 }
615 611
616 612 .inline-form {
617 613 padding: 10px 7px;
618 614 }
619 615
620 616 .inline-form div {
621 617 max-width: 100%;
622 618 }
623 619
624 620 .overlay {
625 621 display: none;
626 622 position: absolute;
627 623 width: 100%;
628 624 text-align: center;
629 625 vertical-align: middle;
630 626 font-size: 16px;
631 627 background: none repeat scroll 0 0 white;
632 628
633 629 &.submitting {
634 630 display: block;
635 631 opacity: 0.5;
636 632 z-index: 100;
637 633 }
638 634 }
639 635 .comment-inline-form .overlay.submitting .overlay-text {
640 636 margin-top: 5%;
641 637 }
642 638
643 639 .comment-inline-form .clearfix,
644 640 .comment-form .clearfix {
645 641 .border-radius(@border-radius);
646 642 margin: 0px;
647 643 }
648 644
649 645
650 646 .hide-inline-form-button {
651 647 margin-left: 5px;
652 648 }
653 649 .comment-button .hide-inline-form {
654 650 background: white;
655 651 }
656 652
657 653 .comment-area {
658 654 padding: 6px 8px;
659 655 border: 1px solid @grey5;
660 656 .border-radius(@border-radius);
661 657
662 658 .resolve-action {
663 659 padding: 1px 0px 0px 6px;
664 660 }
665 661
666 662 }
667 663
668 664 comment-area-text {
669 665 color: @grey3;
670 666 }
671 667
672 668 .comment-area-header {
673 669 height: 35px;
674 670 border-bottom: 1px solid @grey5;
675 671 }
676 672
677 673 .comment-area-header .nav-links {
678 674 display: flex;
679 675 flex-flow: row wrap;
680 676 -webkit-flex-flow: row wrap;
681 677 width: 100%;
682 678 border: none;
683 679 }
684 680
685 681 .comment-area-footer {
686 682 min-height: 30px;
687 683 }
688 684
689 685 .comment-footer .toolbar {
690 686
691 687 }
692 688
693 689 .comment-attachment-uploader {
694 690 border: 1px dashed white;
695 691 border-radius: @border-radius;
696 692 margin-top: -10px;
697 693 line-height: 30px;
698 694 &.dz-drag-hover {
699 695 border-color: @grey3;
700 696 }
701 697
702 698 .dz-error-message {
703 699 padding-top: 0;
704 700 }
705 701 }
706 702
707 703 .comment-attachment-text {
708 704 clear: both;
709 705 font-size: 11px;
710 706 color: #8F8F8F;
711 707 width: 100%;
712 708 .pick-attachment {
713 709 color: #8F8F8F;
714 710 }
715 711 .pick-attachment:hover {
716 712 color: @rcblue;
717 713 }
718 714 }
719 715
720 716 .nav-links {
721 717 padding: 0;
722 718 margin: 0;
723 719 list-style: none;
724 720 height: auto;
725 721 border-bottom: 1px solid @grey5;
726 722 }
727 723 .nav-links li {
728 724 display: inline-block;
729 725 list-style-type: none;
730 726 }
731 727
732 728 .nav-links li a.disabled {
733 729 cursor: not-allowed;
734 730 }
735 731
736 732 .nav-links li.active a {
737 733 border-bottom: 2px solid @rcblue;
738 734 color: #000;
739 735 font-weight: 600;
740 736 }
741 737 .nav-links li a {
742 738 display: inline-block;
743 739 padding: 0px 10px 5px 10px;
744 740 margin-bottom: -1px;
745 741 font-size: 14px;
746 742 line-height: 28px;
747 743 color: #8f8f8f;
748 744 border-bottom: 2px solid transparent;
749 745 }
750 746
@@ -1,558 +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 var lineNo = 'general';
329 328 var resolvesCommentId = null;
330 329 var generalCommentForm = Rhodecode.comments.createGeneralComment(
331 lineNo, "${placeholder}", resolvesCommentId);
330 'general', "${placeholder}", resolvesCommentId);
332 331
333 332 // set custom success callback on rangeCommit
334 333 % if is_compare:
335 334 generalCommentForm.setHandleFormSubmit(function(o) {
336 335 var self = generalCommentForm;
337 336
338 337 var text = self.cm.getValue();
339 338 var status = self.getCommentStatus();
340 339 var commentType = self.getCommentType();
341 340 var isDraft = self.getDraftState();
342 341
343 342 if (text === "" && !status) {
344 343 return;
345 344 }
346 345
347 346 // we can pick which commits we want to make the comment by
348 347 // selecting them via click on preview pane, this will alter the hidden inputs
349 348 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
350 349
351 350 var commitIds = [];
352 351 $('#changeset_compare_view_content .compare_select').each(function(el) {
353 352 var commitId = this.id.replace('row-', '');
354 353 if ($(this).hasClass('hl') || !cherryPicked) {
355 354 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
356 355 commitIds.push(commitId);
357 356 } else {
358 357 $("input[data-commit-id='{0}']".format(commitId)).val('')
359 358 }
360 359 });
361 360
362 361 self.setActionButtonsDisabled(true);
363 362 self.cm.setOption("readOnly", true);
364 363 var postData = {
365 364 'text': text,
366 365 'changeset_status': status,
367 366 'comment_type': commentType,
368 367 'draft': isDraft,
369 368 'commit_ids': commitIds,
370 369 'csrf_token': CSRF_TOKEN
371 370 };
372 371
373 372 var submitSuccessCallback = function(o) {
374 373 location.reload(true);
375 374 };
376 375 var submitFailCallback = function(){
377 376 self.resetCommentFormState(text)
378 377 };
379 378 self.submitAjaxPOST(
380 379 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
381 380 });
382 381 % endif
383 382
384 383 </script>
385 384 % else:
386 385 ## form state when not logged in
387 386 <div class="comment-form ac">
388 387
389 388 <div class="comment-area">
390 389 <div class="comment-area-header">
391 390 <ul class="nav-links clearfix">
392 391 <li class="active">
393 392 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
394 393 </li>
395 394 <li class="">
396 395 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
397 396 </li>
398 397 </ul>
399 398 </div>
400 399
401 400 <div class="comment-area-write" style="display: block;">
402 401 <div id="edit-container">
403 402 <div style="padding: 20px 0px 0px 0;">
404 403 ${_('You need to be logged in to leave comments.')}
405 404 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
406 405 </div>
407 406 </div>
408 407 <div id="preview-container" class="clearfix" style="display: none;">
409 408 <div id="preview-box" class="preview-box"></div>
410 409 </div>
411 410 </div>
412 411
413 412 <div class="comment-area-footer">
414 413 <div class="toolbar">
415 414 <div class="toolbar-text">
416 415 </div>
417 416 </div>
418 417 </div>
419 418 </div>
420 419
421 420 <div class="comment-footer">
422 421 </div>
423 422
424 423 </div>
425 424 % endif
426 425
427 426 <script type="text/javascript">
428 427 bindToggleButtons();
429 428 </script>
430 429 </div>
431 430 </%def>
432 431
433 432
434 433 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
435 434
436 435 ## comment injected based on assumption that user is logged in
437 436 <form ${('id="{}"'.format(form_id) if form_id else '') |n} action="#" method="GET">
438 437
439 438 <div class="comment-area">
440 439 <div class="comment-area-header">
441 440 <div class="pull-left">
442 441 <ul class="nav-links clearfix">
443 442 <li class="active">
444 443 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
445 444 </li>
446 445 <li class="">
447 446 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
448 447 </li>
449 448 </ul>
450 449 </div>
451 450 <div class="pull-right">
452 451 <span class="comment-area-text">${_('Mark as')}:</span>
453 452 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
454 453 % for val in c.visual.comment_types:
455 454 <option value="${val}">${val.upper()}</option>
456 455 % endfor
457 456 </select>
458 457 </div>
459 458 </div>
460 459
461 460 <div class="comment-area-write" style="display: block;">
462 461 <div id="edit-container_${lineno_id}" style="margin-top: -1px">
463 462 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
464 463 </div>
465 464 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
466 465 <div id="preview-box_${lineno_id}" class="preview-box"></div>
467 466 </div>
468 467 </div>
469 468
470 469 <div class="comment-area-footer comment-attachment-uploader">
471 470 <div class="toolbar">
472 471
473 472 <div class="comment-attachment-text">
474 473 <div class="dropzone-text">
475 474 ${_("Drag'n Drop files here or")} <span class="link pick-attachment">${_('Choose your files')}</span>.<br>
476 475 </div>
477 476 <div class="dropzone-upload" style="display:none">
478 477 <i class="icon-spin animate-spin"></i> ${_('uploading...')}
479 478 </div>
480 479 </div>
481 480
482 481 ## comments dropzone template, empty on purpose
483 482 <div style="display: none" class="comment-attachment-uploader-template">
484 483 <div class="dz-file-preview" style="margin: 0">
485 484 <div class="dz-error-message"></div>
486 485 </div>
487 486 </div>
488 487
489 488 </div>
490 489 </div>
491 490 </div>
492 491
493 492 <div class="comment-footer">
494 493
495 494 ## inject extra inputs into the form
496 495 % if form_extras and isinstance(form_extras, (list, tuple)):
497 496 <div id="comment_form_extras">
498 497 % for form_ex_el in form_extras:
499 498 ${form_ex_el|n}
500 499 % endfor
501 500 </div>
502 501 % endif
503 502
504 503 <div class="action-buttons">
505 504 % if form_type != 'inline':
506 505 <div class="action-buttons-extra"></div>
507 506 % endif
508 507
509 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')">
510 509
511 510 % if form_type == 'inline':
512 511 % if c.rhodecode_edition_id == 'EE':
513 512 ## Disable the button for CE, the "real" validation is in the backend code anyway
514 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')">
515 514 % else:
516 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">
517 516 % endif
518 517 % endif
519 518
520 519 % if review_statuses:
521 520 <div class="comment-status-box">
522 521 <select id="change_status_${lineno_id}" name="changeset_status">
523 522 <option></option> ## Placeholder
524 523 % for status, lbl in review_statuses:
525 524 <option value="${status}" data-status="${status}">${lbl}</option>
526 525 %if is_pull_request and change_status and status in ('approved', 'rejected'):
527 526 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
528 527 %endif
529 528 % endfor
530 529 </select>
531 530 </div>
532 531 % endif
533 532
534 533 ## inline for has a file, and line-number together with cancel hide button.
535 534 % if form_type == 'inline':
536 535 <input type="hidden" name="f_path" value="{0}">
537 536 <input type="hidden" name="line" value="${lineno_id}">
538 537 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
539 538 <i class="icon-cancel-circled2"></i>
540 539 </button>
541 540 % endif
542 541 </div>
543 542
544 543 <div class="toolbar-text">
545 544 <% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
546 <p>${_('Styling with {} is supported.').format(renderer_url)|n}
545 <span>${_('Styling with {} is supported.').format(renderer_url)|n}
547 546
548 547 <i class="icon-info-circled tooltip-hovercard"
549 548 data-hovercard-alt="ALT"
550 549 data-hovercard-url="javascript:commentHelp('${c.visual.default_renderer.upper()}')"
551 550 data-comment-json-b64='${h.b64(h.json.dumps({}))}'></i>
552 </p>
551 </span>
553 552 </div>
554 553 </div>
555 554
556 555 </form>
557 556
558 557 </%def> No newline at end of file
@@ -1,125 +1,123 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s Commits') % c.repo_name} -
6 6 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}
7 7 ...
8 8 r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
9 9 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
10 10 %if c.rhodecode_name:
11 11 &middot; ${h.branding(c.rhodecode_name)}
12 12 %endif
13 13 </%def>
14 14
15 15 <%def name="breadcrumbs_links()"></%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
27 27 <div class="box">
28 28 <div class="summary changeset">
29 29 <div class="summary-detail">
30 30 <div class="summary-detail-header">
31 31 <span class="breadcrumbs files_location">
32 32 <h4>
33 33 ${_('Commit Range')}
34 34 </h4>
35 35 </span>
36 36
37 37 <div class="clear-fix"></div>
38 38 </div>
39 39
40 40 <div class="fieldset">
41 41 <div class="left-label-summary">
42 42 <p class="spacing">${_('Range')}:</p>
43 43 <div class="right-label-summary">
44 44 <div class="code-header" >
45 45 <div class="compare_header">
46 46 <code class="fieldset-text-line">
47 47 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}
48 48 ...
49 49 r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
50 50 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
51 51 </code>
52 52 </div>
53 53 </div>
54 54 </div>
55 55 </div>
56 56 </div>
57 57
58 58 <div class="fieldset">
59 59 <div class="left-label-summary">
60 60 <p class="spacing">${_('Diff Option')}:</p>
61 61 <div class="right-label-summary">
62 62 <div class="code-header" >
63 63 <div class="compare_header">
64 64 <a class="btn btn-primary" href="${h.route_path('repo_compare',
65 65 repo_name=c.repo_name,
66 66 source_ref_type='rev',
67 67 source_ref=getattr(c.commit_ranges[0].parents[0] if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id'),
68 68 target_ref_type='rev',
69 69 target_ref=c.commit_ranges[-1].raw_id)}"
70 70 >
71 71 ${_('Show combined diff')}
72 72 </a>
73 73 </div>
74 74 </div>
75 75 </div>
76 76 </div>
77 77 </div>
78 78
79 79 <div class="clear-fix"></div>
80 80 </div> <!-- end summary-detail -->
81 81 </div> <!-- end summary -->
82 82
83 83 <div id="changeset_compare_view_content">
84 84 <div class="pull-left">
85 85 <div class="btn-group">
86 86 <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)} >
87 87 % if c.collapse_all_commits:
88 88 <i class="icon-plus-squared-alt icon-no-margin"></i>
89 89 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
90 90 % else:
91 91 <i class="icon-minus-squared-alt icon-no-margin"></i>
92 92 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
93 93 % endif
94 94 </a>
95 95 </div>
96 96 </div>
97 97 ## Commit range generated below
98 98 <%include file="../compare/compare_commits.mako"/>
99 99 <div class="cs_files">
100 100 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
101 101 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
102 102 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
103 103
104 104 %for commit in c.commit_ranges:
105 105 ## commit range header for each individual diff
106 106 <h3>
107
108
109 107 <a class="tooltip-hovercard revision" data-hovercard-alt="Commit: ${commit.short_id}" data-hovercard-url="${h.route_path('hovercard_repo_commit', repo_name=c.repo_name, commit_id=commit.raw_id)}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">
110 108 ${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}
111 109 </a>
112 110 </h3>
113 111
114 112 ${cbdiffs.render_diffset_menu(c.changes[commit.raw_id])}
115 113 ${cbdiffs.render_diffset(
116 114 diffset=c.changes[commit.raw_id],
117 115 collapse_when_files_over=5,
118 116 commit=commit,
119 117 )}
120 118 %endfor
121 119 </div>
122 120 </div>
123 121 </div>
124 122
125 123 </%def>
General Comments 0
You need to be logged in to leave comments. Login now