##// END OF EJS Templates
comments: edit functionality added
wuboo -
r4401:f098a3f9 default
parent child Browse files
Show More

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

1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -0,0 +1,35 b''
1 # -*- coding: utf-8 -*-
2
3 import logging
4 from sqlalchemy import *
5
6 from alembic.migration import MigrationContext
7 from alembic.operations import Operations
8 from sqlalchemy import BigInteger
9
10 from rhodecode.lib.dbmigrate.versions import _reset_base
11 from rhodecode.model import init_model_encryption
12
13
14 log = logging.getLogger(__name__)
15
16
17 def upgrade(migrate_engine):
18 """
19 Upgrade operations go here.
20 Don't create your own engine; bind migrate_engine to your metadata
21 """
22 _reset_base(migrate_engine)
23 from rhodecode.lib.dbmigrate.schema import db_4_19_0_2 as db
24
25 init_model_encryption(db)
26 db.ChangesetCommentHistory().__table__.create()
27
28
29 def downgrade(migrate_engine):
30 meta = MetaData()
31 meta.bind = migrate_engine
32
33
34 def fixups(models, _SESSION):
35 pass
@@ -0,0 +1,25 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2020-2020 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 ## base64 filter e.g ${ example | base64,n }
22 def base64(text):
23 import base64
24 from rhodecode.lib.helpers import safe_str
25 return base64.encodestring(safe_str(text))
@@ -0,0 +1,7 b''
1 <%namespace name="base" file="/base/base.mako"/>
2
3 ${c.comment_history.author.email}
4 ${base.gravatar_with_user(c.comment_history.author.email, 16, tooltip=True)}
5 ${h.age_component(c.comment_history.created_on)}
6 ${c.comment_history.text}
7 ${c.comment_history.version} No newline at end of file
@@ -48,7 +48,7 b' PYRAMID_SETTINGS = {}'
48 48 EXTENSIONS = {}
49 49
50 50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
51 __dbversion__ = 107 # defines current db version for migrations
51 __dbversion__ = 108 # defines current db version for migrations
52 52 __platform__ = platform.system()
53 53 __license__ = 'AGPLv3, and Commercial License'
54 54 __author__ = 'RhodeCode GmbH'
@@ -79,6 +79,10 b' def includeme(config):'
79 79 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
80 80
81 81 config.add_route(
82 name='repo_commit_comment_history_view',
83 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_history_id}/history_view', repo_route=True)
84
85 config.add_route(
82 86 name='repo_commit_comment_attachment_upload',
83 87 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/attachment_upload', repo_route=True)
84 88
@@ -86,6 +90,10 b' def includeme(config):'
86 90 name='repo_commit_comment_delete',
87 91 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
88 92
93 config.add_route(
94 name='repo_commit_comment_edit',
95 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/edit', repo_route=True)
96
89 97 # still working url for backward compat.
90 98 config.add_route(
91 99 name='repo_commit_raw_deprecated',
@@ -328,6 +336,11 b' def includeme(config):'
328 336 repo_route=True)
329 337
330 338 config.add_route(
339 name='pullrequest_comment_edit',
340 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/edit',
341 repo_route=True, repo_accepted_types=['hg', 'git'])
342
343 config.add_route(
331 344 name='pullrequest_comment_delete',
332 345 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
333 346 repo_route=True, repo_accepted_types=['hg', 'git'])
@@ -35,6 +35,7 b' def route_path(name, params=None, **kwar'
35 35 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
36 36 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
37 37 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
38 'repo_commit_comment_edit': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit',
38 39 }[name].format(**kwargs)
39 40
40 41 if params:
@@ -268,6 +269,164 b' class TestRepoCommitCommentsView(TestCon'
268 269 repo_name=backend.repo_name, commit_id=commit_id))
269 270 assert_comment_links(response, 0, 0)
270 271
272 def test_edit(self, backend):
273 self.log_user()
274 commit_id = backend.repo.get_commit('300').raw_id
275 text = u'CommentOnCommit'
276
277 params = {'text': text, 'csrf_token': self.csrf_token}
278 self.app.post(
279 route_path(
280 'repo_commit_comment_create',
281 repo_name=backend.repo_name, commit_id=commit_id),
282 params=params)
283
284 comments = ChangesetComment.query().all()
285 assert len(comments) == 1
286 comment_id = comments[0].comment_id
287 test_text = 'test_text'
288 self.app.post(
289 route_path(
290 'repo_commit_comment_edit',
291 repo_name=backend.repo_name,
292 commit_id=commit_id,
293 comment_id=comment_id,
294 ),
295 params={
296 'csrf_token': self.csrf_token,
297 'text': test_text,
298 'version': '0',
299 })
300
301 text_form_db = ChangesetComment.query().filter(
302 ChangesetComment.comment_id == comment_id).first().text
303 assert test_text == text_form_db
304
305 def test_edit_without_change(self, backend):
306 self.log_user()
307 commit_id = backend.repo.get_commit('300').raw_id
308 text = u'CommentOnCommit'
309
310 params = {'text': text, 'csrf_token': self.csrf_token}
311 self.app.post(
312 route_path(
313 'repo_commit_comment_create',
314 repo_name=backend.repo_name, commit_id=commit_id),
315 params=params)
316
317 comments = ChangesetComment.query().all()
318 assert len(comments) == 1
319 comment_id = comments[0].comment_id
320
321 response = self.app.post(
322 route_path(
323 'repo_commit_comment_edit',
324 repo_name=backend.repo_name,
325 commit_id=commit_id,
326 comment_id=comment_id,
327 ),
328 params={
329 'csrf_token': self.csrf_token,
330 'text': text,
331 'version': '0',
332 },
333 status=404,
334 )
335 assert response.status_int == 404
336
337 def test_edit_try_edit_already_edited(self, backend):
338 self.log_user()
339 commit_id = backend.repo.get_commit('300').raw_id
340 text = u'CommentOnCommit'
341
342 params = {'text': text, 'csrf_token': self.csrf_token}
343 self.app.post(
344 route_path(
345 'repo_commit_comment_create',
346 repo_name=backend.repo_name, commit_id=commit_id
347 ),
348 params=params,
349 )
350
351 comments = ChangesetComment.query().all()
352 assert len(comments) == 1
353 comment_id = comments[0].comment_id
354 test_text = 'test_text'
355 self.app.post(
356 route_path(
357 'repo_commit_comment_edit',
358 repo_name=backend.repo_name,
359 commit_id=commit_id,
360 comment_id=comment_id,
361 ),
362 params={
363 'csrf_token': self.csrf_token,
364 'text': test_text,
365 'version': '0',
366 }
367 )
368 test_text_v2 = 'test_v2'
369 response = self.app.post(
370 route_path(
371 'repo_commit_comment_edit',
372 repo_name=backend.repo_name,
373 commit_id=commit_id,
374 comment_id=comment_id,
375 ),
376 params={
377 'csrf_token': self.csrf_token,
378 'text': test_text_v2,
379 'version': '0',
380 },
381 status=404,
382 )
383 assert response.status_int == 404
384
385 text_form_db = ChangesetComment.query().filter(
386 ChangesetComment.comment_id == comment_id).first().text
387
388 assert test_text == text_form_db
389 assert test_text_v2 != text_form_db
390
391 def test_edit_forbidden_for_immutable_comments(self, backend):
392 self.log_user()
393 commit_id = backend.repo.get_commit('300').raw_id
394 text = u'CommentOnCommit'
395
396 params = {'text': text, 'csrf_token': self.csrf_token, 'version': '0'}
397 self.app.post(
398 route_path(
399 'repo_commit_comment_create',
400 repo_name=backend.repo_name,
401 commit_id=commit_id,
402 ),
403 params=params
404 )
405
406 comments = ChangesetComment.query().all()
407 assert len(comments) == 1
408 comment_id = comments[0].comment_id
409
410 comment = ChangesetComment.get(comment_id)
411 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
412 Session().add(comment)
413 Session().commit()
414
415 response = self.app.post(
416 route_path(
417 'repo_commit_comment_edit',
418 repo_name=backend.repo_name,
419 commit_id=commit_id,
420 comment_id=comment_id,
421 ),
422 params={
423 'csrf_token': self.csrf_token,
424 'text': 'test_text',
425 },
426 status=403,
427 )
428 assert response.status_int == 403
429
271 430 def test_delete_forbidden_for_immutable_comments(self, backend):
272 431 self.log_user()
273 432 commit_id = backend.repo.get_commit('300').raw_id
@@ -30,6 +30,7 b' from rhodecode.model.db import ('
30 30 from rhodecode.model.meta import Session
31 31 from rhodecode.model.pull_request import PullRequestModel
32 32 from rhodecode.model.user import UserModel
33 from rhodecode.model.comment import CommentsModel
33 34 from rhodecode.tests import (
34 35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 36
@@ -54,6 +55,7 b' def route_path(name, params=None, **kwar'
54 55 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
55 56 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
56 57 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
58 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
57 59 }[name].format(**kwargs)
58 60
59 61 if params:
@@ -355,6 +357,214 b' class TestPullrequestsView(object):'
355 357 pull_request.source_repo, pull_request=pull_request)
356 358 assert status == ChangesetStatus.STATUS_REJECTED
357 359
360 def test_comment_and_close_pull_request_try_edit_comment(
361 self, pr_util, csrf_token, xhr_header
362 ):
363 pull_request = pr_util.create_pull_request()
364 pull_request_id = pull_request.pull_request_id
365
366 response = self.app.post(
367 route_path(
368 'pullrequest_comment_create',
369 repo_name=pull_request.target_repo.scm_instance().name,
370 pull_request_id=pull_request.pull_request_id,
371 ),
372 params={
373 'close_pull_request': 'true',
374 'csrf_token': csrf_token,
375 },
376 extra_environ=xhr_header)
377
378 assert response.json
379
380 pull_request = PullRequest.get(pull_request_id)
381 assert pull_request.is_closed()
382
383 # check only the latest status, not the review status
384 status = ChangesetStatusModel().get_status(
385 pull_request.source_repo, pull_request=pull_request)
386 assert status == ChangesetStatus.STATUS_REJECTED
387
388 comment_id = response.json.get('comment_id', None)
389 test_text = 'test'
390 response = self.app.post(
391 route_path(
392 'pullrequest_comment_edit',
393 repo_name=pull_request.target_repo.scm_instance().name,
394 pull_request_id=pull_request.pull_request_id,
395 comment_id=comment_id,
396 ),
397 extra_environ=xhr_header,
398 params={
399 'csrf_token': csrf_token,
400 'text': test_text,
401 },
402 status=403,
403 )
404 assert response.status_int == 403
405
406 def test_comment_and_comment_edit(
407 self, pr_util, csrf_token, xhr_header
408 ):
409 pull_request = pr_util.create_pull_request()
410 response = self.app.post(
411 route_path(
412 'pullrequest_comment_create',
413 repo_name=pull_request.target_repo.scm_instance().name,
414 pull_request_id=pull_request.pull_request_id),
415 params={
416 'csrf_token': csrf_token,
417 'text': 'init',
418 },
419 extra_environ=xhr_header,
420 )
421 assert response.json
422
423 comment_id = response.json.get('comment_id', None)
424 assert comment_id
425 test_text = 'test'
426 self.app.post(
427 route_path(
428 'pullrequest_comment_edit',
429 repo_name=pull_request.target_repo.scm_instance().name,
430 pull_request_id=pull_request.pull_request_id,
431 comment_id=comment_id,
432 ),
433 extra_environ=xhr_header,
434 params={
435 'csrf_token': csrf_token,
436 'text': test_text,
437 'version': '0',
438 },
439
440 )
441 text_form_db = ChangesetComment.query().filter(
442 ChangesetComment.comment_id == comment_id).first().text
443 assert test_text == text_form_db
444
445 def test_comment_and_comment_edit(
446 self, pr_util, csrf_token, xhr_header
447 ):
448 pull_request = pr_util.create_pull_request()
449 response = self.app.post(
450 route_path(
451 'pullrequest_comment_create',
452 repo_name=pull_request.target_repo.scm_instance().name,
453 pull_request_id=pull_request.pull_request_id),
454 params={
455 'csrf_token': csrf_token,
456 'text': 'init',
457 },
458 extra_environ=xhr_header,
459 )
460 assert response.json
461
462 comment_id = response.json.get('comment_id', None)
463 assert comment_id
464 test_text = 'init'
465 response = self.app.post(
466 route_path(
467 'pullrequest_comment_edit',
468 repo_name=pull_request.target_repo.scm_instance().name,
469 pull_request_id=pull_request.pull_request_id,
470 comment_id=comment_id,
471 ),
472 extra_environ=xhr_header,
473 params={
474 'csrf_token': csrf_token,
475 'text': test_text,
476 'version': '0',
477 },
478 status=404,
479
480 )
481 assert response.status_int == 404
482
483 def test_comment_and_try_edit_already_edited(
484 self, pr_util, csrf_token, xhr_header
485 ):
486 pull_request = pr_util.create_pull_request()
487 response = self.app.post(
488 route_path(
489 'pullrequest_comment_create',
490 repo_name=pull_request.target_repo.scm_instance().name,
491 pull_request_id=pull_request.pull_request_id),
492 params={
493 'csrf_token': csrf_token,
494 'text': 'init',
495 },
496 extra_environ=xhr_header,
497 )
498 assert response.json
499 comment_id = response.json.get('comment_id', None)
500 assert comment_id
501 test_text = 'test'
502 response = self.app.post(
503 route_path(
504 'pullrequest_comment_edit',
505 repo_name=pull_request.target_repo.scm_instance().name,
506 pull_request_id=pull_request.pull_request_id,
507 comment_id=comment_id,
508 ),
509 extra_environ=xhr_header,
510 params={
511 'csrf_token': csrf_token,
512 'text': test_text,
513 'version': '0',
514 },
515
516 )
517 test_text_v2 = 'test_v2'
518 response = self.app.post(
519 route_path(
520 'pullrequest_comment_edit',
521 repo_name=pull_request.target_repo.scm_instance().name,
522 pull_request_id=pull_request.pull_request_id,
523 comment_id=comment_id,
524 ),
525 extra_environ=xhr_header,
526 params={
527 'csrf_token': csrf_token,
528 'text': test_text_v2,
529 'version': '0',
530 },
531 status=404,
532 )
533 assert response.status_int == 404
534
535 text_form_db = ChangesetComment.query().filter(
536 ChangesetComment.comment_id == comment_id).first().text
537
538 assert test_text == text_form_db
539 assert test_text_v2 != text_form_db
540
541 def test_comment_and_comment_edit_permissions_forbidden(
542 self, autologin_regular_user, user_regular, user_admin, pr_util,
543 csrf_token, xhr_header):
544 pull_request = pr_util.create_pull_request(
545 author=user_admin.username, enable_notifications=False)
546 comment = CommentsModel().create(
547 text='test',
548 repo=pull_request.target_repo.scm_instance().name,
549 user=user_admin,
550 pull_request=pull_request,
551 )
552 response = self.app.post(
553 route_path(
554 'pullrequest_comment_edit',
555 repo_name=pull_request.target_repo.scm_instance().name,
556 pull_request_id=pull_request.pull_request_id,
557 comment_id=comment.comment_id,
558 ),
559 extra_environ=xhr_header,
560 params={
561 'csrf_token': csrf_token,
562 'text': 'test_text',
563 },
564 status=403,
565 )
566 assert response.status_int == 403
567
358 568 def test_create_pull_request(self, backend, csrf_token):
359 569 commits = [
360 570 {'message': 'ancestor'},
@@ -45,7 +45,8 b' from rhodecode.lib.utils2 import safe_un'
45 45 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 46 from rhodecode.lib.vcs.exceptions import (
47 47 RepositoryError, CommitDoesNotExistError)
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
49 ChangesetCommentHistory
49 50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 51 from rhodecode.model.comment import CommentsModel
51 52 from rhodecode.model.meta import Session
@@ -431,6 +432,27 b' class RepoCommitsView(RepoAppView):'
431 432 'repository.read', 'repository.write', 'repository.admin')
432 433 @CSRFRequired()
433 434 @view_config(
435 route_name='repo_commit_comment_history_view', request_method='POST',
436 renderer='string', xhr=True)
437 def repo_commit_comment_history_view(self):
438 commit_id = self.request.matchdict['commit_id']
439 comment_history_id = self.request.matchdict['comment_history_id']
440 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
441 c = self.load_default_context()
442 c.comment_history = comment_history
443
444 rendered_comment = render(
445 'rhodecode:templates/changeset/comment_history.mako',
446 self._get_template_context(c)
447 , self.request)
448 return rendered_comment
449
450 @LoginRequired()
451 @NotAnonymous()
452 @HasRepoPermissionAnyDecorator(
453 'repository.read', 'repository.write', 'repository.admin')
454 @CSRFRequired()
455 @view_config(
434 456 route_name='repo_commit_comment_attachment_upload', request_method='POST',
435 457 renderer='json_ext', xhr=True)
436 458 def repo_commit_comment_attachment_upload(self):
@@ -558,6 +580,74 b' class RepoCommitsView(RepoAppView):'
558 580 raise HTTPNotFound()
559 581
560 582 @LoginRequired()
583 @NotAnonymous()
584 @HasRepoPermissionAnyDecorator(
585 'repository.read', 'repository.write', 'repository.admin')
586 @CSRFRequired()
587 @view_config(
588 route_name='repo_commit_comment_edit', request_method='POST',
589 renderer='json_ext')
590 def repo_commit_comment_edit(self):
591 commit_id = self.request.matchdict['commit_id']
592 comment_id = self.request.matchdict['comment_id']
593
594 comment = ChangesetComment.get_or_404(comment_id)
595
596 if comment.immutable:
597 # don't allow deleting comments that are immutable
598 raise HTTPForbidden()
599
600 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
601 super_admin = h.HasPermissionAny('hg.admin')()
602 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
603 is_repo_comment = comment.repo.repo_name == self.db_repo_name
604 comment_repo_admin = is_repo_admin and is_repo_comment
605
606 if super_admin or comment_owner or comment_repo_admin:
607 text = self.request.POST.get('text')
608 version = self.request.POST.get('version')
609 if text == comment.text:
610 log.warning(
611 'Comment(repo): '
612 'Trying to create new version '
613 'of existing comment {}'.format(
614 comment_id,
615 )
616 )
617 raise HTTPNotFound()
618 if version.isdigit():
619 version = int(version)
620 else:
621 log.warning(
622 'Comment(repo): Wrong version type {} {} '
623 'for comment {}'.format(
624 version,
625 type(version),
626 comment_id,
627 )
628 )
629 raise HTTPNotFound()
630
631 comment_history = CommentsModel().edit(
632 comment_id=comment_id,
633 text=text,
634 auth_user=self._rhodecode_user,
635 version=version,
636 )
637 if not comment_history:
638 raise HTTPNotFound()
639 Session().commit()
640 return {
641 'comment_history_id': comment_history.comment_history_id,
642 'comment_id': comment.comment_id,
643 'comment_version': comment_history.version,
644 }
645 else:
646 log.warning('No permissions for user %s to edit comment_id: %s',
647 self._rhodecode_db_user, comment_id)
648 raise HTTPNotFound()
649
650 @LoginRequired()
561 651 @HasRepoPermissionAnyDecorator(
562 652 'repository.read', 'repository.write', 'repository.admin')
563 653 @view_config(
@@ -1518,3 +1518,90 b' class RepoPullRequestsView(RepoAppView, '
1518 1518 log.warning('No permissions for user %s to delete comment_id: %s',
1519 1519 self._rhodecode_db_user, comment_id)
1520 1520 raise HTTPNotFound()
1521
1522 @LoginRequired()
1523 @NotAnonymous()
1524 @HasRepoPermissionAnyDecorator(
1525 'repository.read', 'repository.write', 'repository.admin')
1526 @CSRFRequired()
1527 @view_config(
1528 route_name='pullrequest_comment_edit', request_method='POST',
1529 renderer='json_ext')
1530 def pull_request_comment_edit(self):
1531 pull_request = PullRequest.get_or_404(
1532 self.request.matchdict['pull_request_id']
1533 )
1534 comment = ChangesetComment.get_or_404(
1535 self.request.matchdict['comment_id']
1536 )
1537 comment_id = comment.comment_id
1538
1539 if comment.immutable:
1540 # don't allow deleting comments that are immutable
1541 raise HTTPForbidden()
1542
1543 if pull_request.is_closed():
1544 log.debug('comment: forbidden because pull request is closed')
1545 raise HTTPForbidden()
1546
1547 if not comment:
1548 log.debug('Comment with id:%s not found, skipping', comment_id)
1549 # comment already deleted in another call probably
1550 return True
1551
1552 if comment.pull_request.is_closed():
1553 # don't allow deleting comments on closed pull request
1554 raise HTTPForbidden()
1555
1556 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1557 super_admin = h.HasPermissionAny('hg.admin')()
1558 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1559 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1560 comment_repo_admin = is_repo_admin and is_repo_comment
1561
1562 if super_admin or comment_owner or comment_repo_admin:
1563 text = self.request.POST.get('text')
1564 version = self.request.POST.get('version')
1565 if text == comment.text:
1566 log.warning(
1567 'Comment(PR): '
1568 'Trying to create new version '
1569 'of existing comment {}'.format(
1570 comment_id,
1571 )
1572 )
1573 raise HTTPNotFound()
1574 if version.isdigit():
1575 version = int(version)
1576 else:
1577 log.warning(
1578 'Comment(PR): Wrong version type {} {} '
1579 'for comment {}'.format(
1580 version,
1581 type(version),
1582 comment_id,
1583 )
1584 )
1585 raise HTTPNotFound()
1586
1587 comment_history = CommentsModel().edit(
1588 comment_id=comment_id,
1589 text=text,
1590 auth_user=self._rhodecode_user,
1591 version=version,
1592 )
1593 if not comment_history:
1594 raise HTTPNotFound()
1595 Session().commit()
1596 return {
1597 'comment_history_id': comment_history.comment_history_id,
1598 'comment_id': comment.comment_id,
1599 'comment_version': comment_history.version,
1600 }
1601 else:
1602 log.warning(
1603 'No permissions for user {} to edit comment_id: {}'.format(
1604 self._rhodecode_db_user, comment_id
1605 )
1606 )
1607 raise HTTPNotFound()
@@ -82,6 +82,7 b' ACTIONS_V1 = {'
82 82 'repo.pull_request.merge': '',
83 83 'repo.pull_request.vote': '',
84 84 'repo.pull_request.comment.create': '',
85 'repo.pull_request.comment.edit': '',
85 86 'repo.pull_request.comment.delete': '',
86 87
87 88 'repo.pull_request.reviewer.add': '',
@@ -90,6 +91,7 b' ACTIONS_V1 = {'
90 91 'repo.commit.strip': {'commit_id': ''},
91 92 'repo.commit.comment.create': {'data': {}},
92 93 'repo.commit.comment.delete': {'data': {}},
94 'repo.commit.comment.edit': {'data': {}},
93 95 'repo.commit.vote': '',
94 96
95 97 'repo.artifact.add': '',
@@ -35,7 +35,13 b' from rhodecode.lib import audit_logger'
35 35 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
36 36 from rhodecode.model import BaseModel
37 37 from rhodecode.model.db import (
38 ChangesetComment, User, Notification, PullRequest, AttributeDict)
38 ChangesetComment,
39 User,
40 Notification,
41 PullRequest,
42 AttributeDict,
43 ChangesetCommentHistory,
44 )
39 45 from rhodecode.model.notification import NotificationModel
40 46 from rhodecode.model.meta import Session
41 47 from rhodecode.model.settings import VcsSettingsModel
@@ -479,6 +485,54 b' class CommentsModel(BaseModel):'
479 485
480 486 return comment
481 487
488 def edit(self, comment_id, text, auth_user, version):
489 """
490 Change existing comment for commit or pull request.
491
492 :param comment_id:
493 :param text:
494 :param auth_user: current authenticated user calling this method
495 :param version: last comment version
496 """
497 if not text:
498 log.warning('Missing text for comment, skipping...')
499 return
500
501 comment = ChangesetComment.get(comment_id)
502 old_comment_text = comment.text
503 comment.text = text
504 comment_version = ChangesetCommentHistory.get_version(comment_id)
505 if (comment_version - version) != 1:
506 log.warning(
507 'Version mismatch, skipping... '
508 'version {} but should be {}'.format(
509 (version - 1),
510 comment_version,
511 )
512 )
513 return
514 comment_history = ChangesetCommentHistory()
515 comment_history.comment_id = comment_id
516 comment_history.version = comment_version
517 comment_history.created_by_user_id = auth_user.user_id
518 comment_history.text = old_comment_text
519 # TODO add email notification
520 Session().add(comment_history)
521 Session().add(comment)
522 Session().flush()
523
524 if comment.pull_request:
525 action = 'repo.pull_request.comment.edit'
526 else:
527 action = 'repo.commit.comment.edit'
528
529 comment_data = comment.get_api_data()
530 comment_data['old_comment_text'] = old_comment_text
531 self._log_audit_action(
532 action, {'data': comment_data}, auth_user, comment)
533
534 return comment_history
535
482 536 def delete(self, comment, auth_user):
483 537 """
484 538 Deletes given comment
@@ -712,6 +766,7 b' class CommentsModel(BaseModel):'
712 766 .filter(ChangesetComment.line_no == None)\
713 767 .filter(ChangesetComment.f_path == None)\
714 768 .filter(ChangesetComment.pull_request == pull_request)
769
715 770 return comments
716 771
717 772 @staticmethod
@@ -3755,6 +3755,7 b' class ChangesetComment(Base, BaseModel):'
3755 3755 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
3756 3756 pull_request = relationship('PullRequest', lazy='joined')
3757 3757 pull_request_version = relationship('PullRequestVersion')
3758 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='joined', order_by='ChangesetCommentHistory.version')
3758 3759
3759 3760 @classmethod
3760 3761 def get_users(cls, revision=None, pull_request_id=None):
@@ -3849,6 +3850,36 b' class ChangesetComment(Base, BaseModel):'
3849 3850 return data
3850 3851
3851 3852
3853 class ChangesetCommentHistory(Base, BaseModel):
3854 __tablename__ = 'changeset_comments_history'
3855 __table_args__ = (
3856 Index('cch_comment_id_idx', 'comment_id'),
3857 base_table_args,
3858 )
3859
3860 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3861 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3862 version = Column("version", Integer(), nullable=False, default=0)
3863 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3864 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3865 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3866 deleted = Column('deleted', Boolean(), default=False)
3867
3868 author = relationship('User', lazy='joined')
3869 comment = relationship('ChangesetComment', cascade="all, delete")
3870
3871 @classmethod
3872 def get_version(cls, comment_id):
3873 q = Session().query(ChangesetCommentHistory).filter(
3874 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
3875 if q.count() == 0:
3876 return 1
3877 elif q.count() >= q[0].version:
3878 return q.count() + 1
3879 else:
3880 return q[0].version + 1
3881
3882
3852 3883 class ChangesetStatus(Base, BaseModel):
3853 3884 __tablename__ = 'changeset_statuses'
3854 3885 __table_args__ = (
@@ -185,8 +185,10 b' function registerRCRoutes() {'
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 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']);
188 189 pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
189 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 pyroutes.register('repo_commit_comment_edit', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/edit', ['repo_name', 'commit_id', 'comment_id']);
190 192 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
191 193 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
192 194 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
@@ -242,6 +244,7 b' function registerRCRoutes() {'
242 244 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
243 245 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
244 246 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
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']);
245 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']);
246 249 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
247 250 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
@@ -80,9 +80,10 b' var _submitAjaxPOST = function(url, post'
80 80 })(function() {
81 81 "use strict";
82 82
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
84
84 85 if (!(this instanceof CommentForm)) {
85 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
86 87 }
87 88
88 89 // bind the element instance to our Form
@@ -126,10 +127,22 b' var _submitAjaxPOST = function(url, post'
126 127 this.submitButton = $(this.submitForm).find('input[type="submit"]');
127 128 this.submitButtonText = this.submitButton.val();
128 129
130
129 131 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
130 132 {'repo_name': templateContext.repo_name,
131 133 'commit_id': templateContext.commit_data.commit_id});
132 134
135 if (edit){
136 this.submitButtonText = _gettext('Updated Comment');
137 $(this.commentType).prop('disabled', true);
138 $(this.commentType).addClass('disabled');
139 var editInfo =
140 '<div class="comment-label note" id="comment-label-6" title="line: ">' +
141 'editing' +
142 '</div>';
143 $(editInfo).insertBefore($(this.editButton).parent());
144 }
145
133 146 if (resolvesCommentId){
134 147 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
135 148 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
@@ -153,17 +166,27 b' var _submitAjaxPOST = function(url, post'
153 166 // based on commitId, or pullRequestId decide where do we submit
154 167 // out data
155 168 if (this.commitId){
156 this.submitUrl = pyroutes.url('repo_commit_comment_create',
169 var pyurl = 'repo_commit_comment_create';
170 if(edit){
171 pyurl = 'repo_commit_comment_edit';
172 }
173 this.submitUrl = pyroutes.url(pyurl,
157 174 {'repo_name': templateContext.repo_name,
158 'commit_id': this.commitId});
175 'commit_id': this.commitId,
176 'comment_id': comment_id});
159 177 this.selfUrl = pyroutes.url('repo_commit',
160 178 {'repo_name': templateContext.repo_name,
161 179 'commit_id': this.commitId});
162 180
163 181 } else if (this.pullRequestId) {
164 this.submitUrl = pyroutes.url('pullrequest_comment_create',
182 var pyurl = 'pullrequest_comment_create';
183 if(edit){
184 pyurl = 'pullrequest_comment_edit';
185 }
186 this.submitUrl = pyroutes.url(pyurl,
165 187 {'repo_name': templateContext.repo_name,
166 'pull_request_id': this.pullRequestId});
188 'pull_request_id': this.pullRequestId,
189 'comment_id': comment_id});
167 190 this.selfUrl = pyroutes.url('pullrequest_show',
168 191 {'repo_name': templateContext.repo_name,
169 192 'pull_request_id': this.pullRequestId});
@@ -277,7 +300,7 b' var _submitAjaxPOST = function(url, post'
277 300 this.globalSubmitSuccessCallback = function(){
278 301 // default behaviour is to call GLOBAL hook, if it's registered.
279 302 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
280 commentFormGlobalSubmitSuccessCallback()
303 commentFormGlobalSubmitSuccessCallback();
281 304 }
282 305 };
283 306
@@ -482,11 +505,73 b' var CommentsController = function() {'
482 505
483 506 this.cancelComment = function(node) {
484 507 var $node = $(node);
485 var $td = $node.closest('td');
508 var edit = $(this).attr('edit');
509 if (edit) {
510 var $general_comments = null;
511 var $inline_comments = $node.closest('div.inline-comments');
512 if (!$inline_comments.length) {
513 $general_comments = $('#comments');
514 var $comment = $general_comments.parent().find('div.comment:hidden');
515 // show hidden general comment form
516 $('#cb-comment-general-form-placeholder').show();
517 } else {
518 var $comment = $inline_comments.find('div.comment:hidden');
519 }
520 $comment.show();
521 }
486 522 $node.closest('.comment-inline-form').remove();
487 523 return false;
488 524 };
489 525
526 this.showVersion = function (node) {
527 var $node = $(node);
528 var selectedIndex = $node.context.selectedIndex;
529 var option = $node.find('option[value="'+ selectedIndex +'"]');
530 var zero_option = $node.find('option[value="0"]');
531 if (!option){
532 return;
533 }
534
535 // little trick to cheat onchange and allow to display the same version again
536 $node.context.selectedIndex = 0;
537 zero_option.text(selectedIndex);
538
539 var comment_history_id = option.attr('data-comment-history-id');
540 var comment_id = option.attr('data-comment-id');
541 var historyViewUrl = pyroutes.url(
542 'repo_commit_comment_history_view',
543 {
544 'repo_name': templateContext.repo_name,
545 'commit_id': comment_id,
546 'comment_history_id': comment_history_id,
547 }
548 );
549 successRenderCommit = function (data) {
550 Swal.fire({
551 html: data,
552 title: '',
553 showClass: {
554 popup: 'swal2-noanimation',
555 backdrop: 'swal2-noanimation'
556 },
557 });
558 };
559 failRenderCommit = function () {
560 Swal.fire({
561 html: 'Error while loading comment',
562 title: '',
563 showClass: {
564 popup: 'swal2-noanimation',
565 backdrop: 'swal2-noanimation'
566 },
567 });
568 };
569 _submitAjaxPOST(
570 historyViewUrl, {'csrf_token': CSRF_TOKEN}, successRenderCommit,
571 failRenderCommit
572 );
573 };
574
490 575 this.getLineNumber = function(node) {
491 576 var $node = $(node);
492 577 var lineNo = $node.closest('td').attr('data-line-no');
@@ -638,12 +723,12 b' var CommentsController = function() {'
638 723 $node.closest('tr').toggleClass('hide-line-comments');
639 724 };
640 725
641 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
726 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
642 727 var pullRequestId = templateContext.pull_request_data.pull_request_id;
643 728 var commitId = templateContext.commit_data.commit_id;
644 729
645 730 var commentForm = new CommentForm(
646 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
731 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
647 732 var cm = commentForm.getCmInstance();
648 733
649 734 if (resolvesCommentId){
@@ -780,18 +865,200 b' var CommentsController = function() {'
780 865
781 866 var _form = $($form[0]);
782 867 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
868 var edit = false;
869 var comment_id = null;
783 870 var commentForm = this.createCommentForm(
784 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
871 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
785 872 commentForm.initStatusChangeSelector();
786 873
787 874 return commentForm;
788 875 };
876 this.editComment = function(node) {
877 var $node = $(node);
878 var $comment = $(node).closest('.comment');
879 var comment_id = $comment.attr('data-comment-id');
880 var $form = null
789 881
882 var $comments = $node.closest('div.inline-comments');
883 var $general_comments = null;
884 var lineno = null;
885
886 if($comments.length){
887 // inline comments setup
888 $form = $comments.find('.comment-inline-form');
889 lineno = self.getLineNumber(node)
890 }
891 else{
892 // general comments setup
893 $comments = $('#comments');
894 $form = $comments.find('.comment-inline-form');
895 lineno = $comment[0].id
896 $('#cb-comment-general-form-placeholder').hide();
897 }
898
899 this.edit = true;
900
901 if (!$form.length) {
902
903 var $filediff = $node.closest('.filediff');
904 $filediff.removeClass('hide-comments');
905 var f_path = $filediff.attr('data-f-path');
906
907 // create a new HTML from template
908
909 var tmpl = $('#cb-comment-inline-form-template').html();
910 tmpl = tmpl.format(escapeHtml(f_path), lineno);
911 $form = $(tmpl);
912 $comment.after($form)
913
914 var _form = $($form[0]).find('form');
915 var autocompleteActions = ['as_note',];
916 var commentForm = this.createCommentForm(
917 _form, lineno, '', autocompleteActions, resolvesCommentId,
918 this.edit, comment_id);
919 var old_comment_text_binary = $comment.attr('data-comment-text');
920 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
921 commentForm.cm.setValue(old_comment_text);
922 $comment.hide();
923
924 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
925 form: _form,
926 parent: $comments,
927 lineno: lineno,
928 f_path: f_path}
929 );
930 // set a CUSTOM submit handler for inline comments.
931 commentForm.setHandleFormSubmit(function(o) {
932 var text = commentForm.cm.getValue();
933 var commentType = commentForm.getCommentType();
934 var resolvesCommentId = commentForm.getResolvesId();
935
936 if (text === "") {
937 return;
938 }
939 if (old_comment_text == text) {
940 Swal.fire({
941 title: 'Error',
942 html: _gettext('Comment body should be changed'),
943 showClass: {
944 popup: 'swal2-noanimation',
945 backdrop: 'swal2-noanimation'
946 },
947 });
948 return;
949 }
950 var excludeCancelBtn = false;
951 var submitEvent = true;
952 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
953 commentForm.cm.setOption("readOnly", true);
954 var dropDown = $('#comment_history_for_comment_'+comment_id);
955
956 var version = dropDown.children().last().val()
957 if(!version){
958 version = 0;
959 }
960 var postData = {
961 'text': text,
962 'f_path': f_path,
963 'line': lineno,
964 'comment_type': commentType,
965 'csrf_token': CSRF_TOKEN,
966 'version': version,
967 };
968
969 var submitSuccessCallback = function(json_data) {
970 $form.remove();
971 $comment.show();
972 var postData = {
973 'text': text,
974 'renderer': $comment.attr('data-comment-renderer'),
975 'csrf_token': CSRF_TOKEN
976 };
977
978 var updateCommentVersionDropDown = function () {
979 var dropDown = $('#comment_history_for_comment_'+comment_id);
980 $comment.attr('data-comment-text', btoa(text));
981 var version = json_data['comment_version']
982 var option = new Option(version, version);
983 var $option = $(option);
984 $option.attr('data-comment-history-id', json_data['comment_history_id']);
985 $option.attr('data-comment-id', json_data['comment_id']);
986 dropDown.append(option);
987 dropDown.parent().show();
988 }
989 updateCommentVersionDropDown();
990 // by default we reset state of comment preserving the text
991 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
992 var prefix = "Error while editing of comment.\n"
993 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
994 ajaxErrorSwal(message);
995
996 };
997 var successRenderCommit = function(o){
998 $comment.show();
999 $comment[0].lastElementChild.innerHTML = o;
1000 }
1001 var previewUrl = pyroutes.url('repo_commit_comment_preview',
1002 {'repo_name': templateContext.repo_name,
1003 'commit_id': templateContext.commit_data.commit_id});
1004
1005 _submitAjaxPOST(
1006 previewUrl, postData, successRenderCommit,
1007 failRenderCommit
1008 );
1009
1010 try {
1011 var html = json_data.rendered_text;
1012 var lineno = json_data.line_no;
1013 var target_id = json_data.target_id;
1014
1015 $comments.find('.cb-comment-add-button').before(html);
1016
1017 //mark visually which comment was resolved
1018 if (resolvesCommentId) {
1019 commentForm.markCommentResolved(resolvesCommentId);
1020 }
1021
1022 // run global callback on submit
1023 commentForm.globalSubmitSuccessCallback();
1024
1025 } catch (e) {
1026 console.error(e);
1027 }
1028
1029 // re trigger the linkification of next/prev navigation
1030 linkifyComments($('.inline-comment-injected'));
1031 timeagoActivate();
1032 tooltipActivate();
1033
1034 if (window.updateSticky !== undefined) {
1035 // potentially our comments change the active window size, so we
1036 // notify sticky elements
1037 updateSticky()
1038 }
1039
1040 commentForm.setActionButtonsDisabled(false);
1041
1042 };
1043 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1044 var prefix = "Error while editing comment.\n"
1045 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1046 ajaxErrorSwal(message);
1047 commentForm.resetCommentFormState(text)
1048 };
1049 commentForm.submitAjaxPOST(
1050 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1051 });
1052 }
1053
1054 $form.addClass('comment-inline-form-open');
1055 };
790 1056 this.createComment = function(node, resolutionComment) {
791 1057 var resolvesCommentId = resolutionComment || null;
792 1058 var $node = $(node);
793 1059 var $td = $node.closest('td');
794 1060 var $form = $td.find('.comment-inline-form');
1061 this.edit = false;
795 1062
796 1063 if (!$form.length) {
797 1064
@@ -816,8 +1083,9 b' var CommentsController = function() {'
816 1083 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
817 1084 var _form = $($form[0]).find('form');
818 1085 var autocompleteActions = ['as_note', 'as_todo'];
1086 var comment_id=null;
819 1087 var commentForm = this.createCommentForm(
820 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
1088 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId, this.edit, comment_id);
821 1089
822 1090 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
823 1091 form: _form,
@@ -182,3 +182,9 b' var htmlEnDeCode = (function() {'
182 182 htmlDecode: htmlDecode
183 183 };
184 184 })();
185
186 function b64DecodeUnicode(str) {
187 return decodeURIComponent(atob(str).split('').map(function (c) {
188 return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
189 }).join(''));
190 }
@@ -1,11 +1,7 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%!
4 ## base64 filter e.g ${ example | base64 }
5 def base64(text):
6 import base64
7 from rhodecode.lib.helpers import safe_str
8 return base64.encodestring(safe_str(text))
4 from rhodecode.lib import html_filters
9 5 %>
10 6
11 7 <%inherit file="root.mako"/>
@@ -3,8 +3,12 b''
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6
7 <%!
8 from rhodecode.lib import html_filters
9 %>
10
6 11 <%namespace name="base" file="/base/base.mako"/>
7
8 12 <%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
9 13 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10 14 <% latest_ver = len(getattr(c, 'versions', [])) %>
@@ -21,6 +25,8 b''
21 25 line="${comment.line_no}"
22 26 data-comment-id="${comment.comment_id}"
23 27 data-comment-type="${comment.comment_type}"
28 data-comment-renderer="${comment.renderer}"
29 data-comment-text="${comment.text | html_filters.base64,n}"
24 30 data-comment-line-no="${comment.line_no}"
25 31 data-comment-inline=${h.json.dumps(inline)}
26 32 style="${'display: none;' if outdated_at_ver else ''}">
@@ -60,6 +66,31 b''
60 66 <div class="date">
61 67 ${h.age_component(comment.modified_at, time_is_local=True)}
62 68 </div>
69 % if comment.history:
70 <div class="date">
71 <span class="comment-area-text">${_('Comment version')}:</span>
72 <select class="comment-type" id="comment_history_for_comment_${comment.comment_id}"
73 onchange="return Rhodecode.comments.showVersion(this)"
74 name="comment_type">
75 <option style="display: none" value="0">---</option>
76 %for comment_history in comment.history:
77 <option
78 data-comment-history-id="${comment_history.comment_history_id}",
79 data-comment-id="${comment.comment_id}",
80 value="${comment_history.version}">${comment_history.version}</option>
81 %endfor
82 </select>
83 </div>
84 % else:
85 <div class="date" style="display: none">
86 <span class="comment-area-text">${_('Comment version')}</span>
87 <select class="comment-type" id="comment_history_for_comment_${comment.comment_id}"
88 onchange="return Rhodecode.comments.showVersion(this)"
89 name="comment_type">
90 <option style="display: none" value="0">---</option>
91 </select>
92 </div>
93 %endif
63 94 % if inline:
64 95 <span></span>
65 96 % else:
@@ -136,21 +167,29 b''
136 167 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
137 168 ## permissions to delete
138 169 %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):
139 ## TODO: dan: add edit comment here
140 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
170 %if comment.comment_type == 'note':
171 <a onclick="return Rhodecode.comments.editComment(this);"
172 class="edit-comment"> ${_('Edit')}</a>
141 173 %else:
142 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
174 <button class="btn-link" disabled="disabled"> ${_('Edit')}</button>
175 %endif
176 | <a onclick="return Rhodecode.comments.deleteComment(this);"
177 class="delete-comment"> ${_('Delete')}</a>
178 %else:
179 <button class="btn-link" disabled="disabled"> ${_('Edit')}</button>
180 | <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
143 181 %endif
144 182 %else:
145 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
183 <button class="btn-link" disabled="disabled"> ${_('Edit')}</button>
184 | <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
146 185 %endif
147 186
148 187 % if outdated_at_ver:
149 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="prev-comment"> ${_('Prev')}</a>
150 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="next-comment"> ${_('Next')}</a>
188 | <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>
189 | <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>
151 190 % else:
152 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
153 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
191 | <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
192 | <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
154 193 % endif
155 194
156 195 </div>
General Comments 0
You need to be logged in to leave comments. Login now