comments: edit functionality added
This commit is contained in:
parent
f91a6454e7
commit
b9a819cbfe
18 changed files with 6683 additions and 32 deletions
|
|
@ -48,7 +48,7 @@ PYRAMID_SETTINGS = {}
|
|||
EXTENSIONS = {}
|
||||
|
||||
__version__ = ('.'.join((str(each) for each in VERSION[:3])))
|
||||
__dbversion__ = 107 # defines current db version for migrations
|
||||
__dbversion__ = 108 # defines current db version for migrations
|
||||
__platform__ = platform.system()
|
||||
__license__ = 'AGPLv3, and Commercial License'
|
||||
__author__ = 'RhodeCode GmbH'
|
||||
|
|
|
|||
|
|
@ -78,6 +78,10 @@ def includeme(config):
|
|||
name='repo_commit_comment_preview',
|
||||
pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
|
||||
|
||||
config.add_route(
|
||||
name='repo_commit_comment_history_view',
|
||||
pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_history_id}/history_view', repo_route=True)
|
||||
|
||||
config.add_route(
|
||||
name='repo_commit_comment_attachment_upload',
|
||||
pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/attachment_upload', repo_route=True)
|
||||
|
|
@ -86,6 +90,10 @@ def includeme(config):
|
|||
name='repo_commit_comment_delete',
|
||||
pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
|
||||
|
||||
config.add_route(
|
||||
name='repo_commit_comment_edit',
|
||||
pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/edit', repo_route=True)
|
||||
|
||||
# still working url for backward compat.
|
||||
config.add_route(
|
||||
name='repo_commit_raw_deprecated',
|
||||
|
|
@ -327,6 +335,11 @@ def includeme(config):
|
|||
pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment',
|
||||
repo_route=True)
|
||||
|
||||
config.add_route(
|
||||
name='pullrequest_comment_edit',
|
||||
pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/edit',
|
||||
repo_route=True, repo_accepted_types=['hg', 'git'])
|
||||
|
||||
config.add_route(
|
||||
name='pullrequest_comment_delete',
|
||||
pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ def route_path(name, params=None, **kwargs):
|
|||
'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
|
||||
'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
|
||||
'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
|
||||
'repo_commit_comment_edit': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit',
|
||||
}[name].format(**kwargs)
|
||||
|
||||
if params:
|
||||
|
|
@ -268,6 +269,164 @@ class TestRepoCommitCommentsView(TestController):
|
|||
repo_name=backend.repo_name, commit_id=commit_id))
|
||||
assert_comment_links(response, 0, 0)
|
||||
|
||||
def test_edit(self, backend):
|
||||
self.log_user()
|
||||
commit_id = backend.repo.get_commit('300').raw_id
|
||||
text = u'CommentOnCommit'
|
||||
|
||||
params = {'text': text, 'csrf_token': self.csrf_token}
|
||||
self.app.post(
|
||||
route_path(
|
||||
'repo_commit_comment_create',
|
||||
repo_name=backend.repo_name, commit_id=commit_id),
|
||||
params=params)
|
||||
|
||||
comments = ChangesetComment.query().all()
|
||||
assert len(comments) == 1
|
||||
comment_id = comments[0].comment_id
|
||||
test_text = 'test_text'
|
||||
self.app.post(
|
||||
route_path(
|
||||
'repo_commit_comment_edit',
|
||||
repo_name=backend.repo_name,
|
||||
commit_id=commit_id,
|
||||
comment_id=comment_id,
|
||||
),
|
||||
params={
|
||||
'csrf_token': self.csrf_token,
|
||||
'text': test_text,
|
||||
'version': '0',
|
||||
})
|
||||
|
||||
text_form_db = ChangesetComment.query().filter(
|
||||
ChangesetComment.comment_id == comment_id).first().text
|
||||
assert test_text == text_form_db
|
||||
|
||||
def test_edit_without_change(self, backend):
|
||||
self.log_user()
|
||||
commit_id = backend.repo.get_commit('300').raw_id
|
||||
text = u'CommentOnCommit'
|
||||
|
||||
params = {'text': text, 'csrf_token': self.csrf_token}
|
||||
self.app.post(
|
||||
route_path(
|
||||
'repo_commit_comment_create',
|
||||
repo_name=backend.repo_name, commit_id=commit_id),
|
||||
params=params)
|
||||
|
||||
comments = ChangesetComment.query().all()
|
||||
assert len(comments) == 1
|
||||
comment_id = comments[0].comment_id
|
||||
|
||||
response = self.app.post(
|
||||
route_path(
|
||||
'repo_commit_comment_edit',
|
||||
repo_name=backend.repo_name,
|
||||
commit_id=commit_id,
|
||||
comment_id=comment_id,
|
||||
),
|
||||
params={
|
||||
'csrf_token': self.csrf_token,
|
||||
'text': text,
|
||||
'version': '0',
|
||||
},
|
||||
status=404,
|
||||
)
|
||||
assert response.status_int == 404
|
||||
|
||||
def test_edit_try_edit_already_edited(self, backend):
|
||||
self.log_user()
|
||||
commit_id = backend.repo.get_commit('300').raw_id
|
||||
text = u'CommentOnCommit'
|
||||
|
||||
params = {'text': text, 'csrf_token': self.csrf_token}
|
||||
self.app.post(
|
||||
route_path(
|
||||
'repo_commit_comment_create',
|
||||
repo_name=backend.repo_name, commit_id=commit_id
|
||||
),
|
||||
params=params,
|
||||
)
|
||||
|
||||
comments = ChangesetComment.query().all()
|
||||
assert len(comments) == 1
|
||||
comment_id = comments[0].comment_id
|
||||
test_text = 'test_text'
|
||||
self.app.post(
|
||||
route_path(
|
||||
'repo_commit_comment_edit',
|
||||
repo_name=backend.repo_name,
|
||||
commit_id=commit_id,
|
||||
comment_id=comment_id,
|
||||
),
|
||||
params={
|
||||
'csrf_token': self.csrf_token,
|
||||
'text': test_text,
|
||||
'version': '0',
|
||||
}
|
||||
)
|
||||
test_text_v2 = 'test_v2'
|
||||
response = self.app.post(
|
||||
route_path(
|
||||
'repo_commit_comment_edit',
|
||||
repo_name=backend.repo_name,
|
||||
commit_id=commit_id,
|
||||
comment_id=comment_id,
|
||||
),
|
||||
params={
|
||||
'csrf_token': self.csrf_token,
|
||||
'text': test_text_v2,
|
||||
'version': '0',
|
||||
},
|
||||
status=404,
|
||||
)
|
||||
assert response.status_int == 404
|
||||
|
||||
text_form_db = ChangesetComment.query().filter(
|
||||
ChangesetComment.comment_id == comment_id).first().text
|
||||
|
||||
assert test_text == text_form_db
|
||||
assert test_text_v2 != text_form_db
|
||||
|
||||
def test_edit_forbidden_for_immutable_comments(self, backend):
|
||||
self.log_user()
|
||||
commit_id = backend.repo.get_commit('300').raw_id
|
||||
text = u'CommentOnCommit'
|
||||
|
||||
params = {'text': text, 'csrf_token': self.csrf_token, 'version': '0'}
|
||||
self.app.post(
|
||||
route_path(
|
||||
'repo_commit_comment_create',
|
||||
repo_name=backend.repo_name,
|
||||
commit_id=commit_id,
|
||||
),
|
||||
params=params
|
||||
)
|
||||
|
||||
comments = ChangesetComment.query().all()
|
||||
assert len(comments) == 1
|
||||
comment_id = comments[0].comment_id
|
||||
|
||||
comment = ChangesetComment.get(comment_id)
|
||||
comment.immutable_state = ChangesetComment.OP_IMMUTABLE
|
||||
Session().add(comment)
|
||||
Session().commit()
|
||||
|
||||
response = self.app.post(
|
||||
route_path(
|
||||
'repo_commit_comment_edit',
|
||||
repo_name=backend.repo_name,
|
||||
commit_id=commit_id,
|
||||
comment_id=comment_id,
|
||||
),
|
||||
params={
|
||||
'csrf_token': self.csrf_token,
|
||||
'text': 'test_text',
|
||||
},
|
||||
status=403,
|
||||
)
|
||||
assert response.status_int == 403
|
||||
|
||||
def test_delete_forbidden_for_immutable_comments(self, backend):
|
||||
self.log_user()
|
||||
commit_id = backend.repo.get_commit('300').raw_id
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ from rhodecode.model.db import (
|
|||
from rhodecode.model.meta import Session
|
||||
from rhodecode.model.pull_request import PullRequestModel
|
||||
from rhodecode.model.user import UserModel
|
||||
from rhodecode.model.comment import CommentsModel
|
||||
from rhodecode.tests import (
|
||||
assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
|
||||
|
||||
|
|
@ -54,6 +55,7 @@ def route_path(name, params=None, **kwargs):
|
|||
'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
|
||||
'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
|
||||
'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
|
||||
'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
|
||||
}[name].format(**kwargs)
|
||||
|
||||
if params:
|
||||
|
|
@ -338,8 +340,8 @@ class TestPullrequestsView(object):
|
|||
|
||||
response = self.app.post(
|
||||
route_path('pullrequest_comment_create',
|
||||
repo_name=pull_request.target_repo.scm_instance().name,
|
||||
pull_request_id=pull_request.pull_request_id),
|
||||
repo_name=pull_request.target_repo.scm_instance().name,
|
||||
pull_request_id=pull_request.pull_request_id),
|
||||
params={
|
||||
'close_pull_request': 'true',
|
||||
'csrf_token': csrf_token},
|
||||
|
|
@ -355,6 +357,214 @@ class TestPullrequestsView(object):
|
|||
pull_request.source_repo, pull_request=pull_request)
|
||||
assert status == ChangesetStatus.STATUS_REJECTED
|
||||
|
||||
def test_comment_and_close_pull_request_try_edit_comment(
|
||||
self, pr_util, csrf_token, xhr_header
|
||||
):
|
||||
pull_request = pr_util.create_pull_request()
|
||||
pull_request_id = pull_request.pull_request_id
|
||||
|
||||
response = self.app.post(
|
||||
route_path(
|
||||
'pullrequest_comment_create',
|
||||
repo_name=pull_request.target_repo.scm_instance().name,
|
||||
pull_request_id=pull_request.pull_request_id,
|
||||
),
|
||||
params={
|
||||
'close_pull_request': 'true',
|
||||
'csrf_token': csrf_token,
|
||||
},
|
||||
extra_environ=xhr_header)
|
||||
|
||||
assert response.json
|
||||
|
||||
pull_request = PullRequest.get(pull_request_id)
|
||||
assert pull_request.is_closed()
|
||||
|
||||
# check only the latest status, not the review status
|
||||
status = ChangesetStatusModel().get_status(
|
||||
pull_request.source_repo, pull_request=pull_request)
|
||||
assert status == ChangesetStatus.STATUS_REJECTED
|
||||
|
||||
comment_id = response.json.get('comment_id', None)
|
||||
test_text = 'test'
|
||||
response = self.app.post(
|
||||
route_path(
|
||||
'pullrequest_comment_edit',
|
||||
repo_name=pull_request.target_repo.scm_instance().name,
|
||||
pull_request_id=pull_request.pull_request_id,
|
||||
comment_id=comment_id,
|
||||
),
|
||||
extra_environ=xhr_header,
|
||||
params={
|
||||
'csrf_token': csrf_token,
|
||||
'text': test_text,
|
||||
},
|
||||
status=403,
|
||||
)
|
||||
assert response.status_int == 403
|
||||
|
||||
def test_comment_and_comment_edit(
|
||||
self, pr_util, csrf_token, xhr_header
|
||||
):
|
||||
pull_request = pr_util.create_pull_request()
|
||||
response = self.app.post(
|
||||
route_path(
|
||||
'pullrequest_comment_create',
|
||||
repo_name=pull_request.target_repo.scm_instance().name,
|
||||
pull_request_id=pull_request.pull_request_id),
|
||||
params={
|
||||
'csrf_token': csrf_token,
|
||||
'text': 'init',
|
||||
},
|
||||
extra_environ=xhr_header,
|
||||
)
|
||||
assert response.json
|
||||
|
||||
comment_id = response.json.get('comment_id', None)
|
||||
assert comment_id
|
||||
test_text = 'test'
|
||||
self.app.post(
|
||||
route_path(
|
||||
'pullrequest_comment_edit',
|
||||
repo_name=pull_request.target_repo.scm_instance().name,
|
||||
pull_request_id=pull_request.pull_request_id,
|
||||
comment_id=comment_id,
|
||||
),
|
||||
extra_environ=xhr_header,
|
||||
params={
|
||||
'csrf_token': csrf_token,
|
||||
'text': test_text,
|
||||
'version': '0',
|
||||
},
|
||||
|
||||
)
|
||||
text_form_db = ChangesetComment.query().filter(
|
||||
ChangesetComment.comment_id == comment_id).first().text
|
||||
assert test_text == text_form_db
|
||||
|
||||
def test_comment_and_comment_edit(
|
||||
self, pr_util, csrf_token, xhr_header
|
||||
):
|
||||
pull_request = pr_util.create_pull_request()
|
||||
response = self.app.post(
|
||||
route_path(
|
||||
'pullrequest_comment_create',
|
||||
repo_name=pull_request.target_repo.scm_instance().name,
|
||||
pull_request_id=pull_request.pull_request_id),
|
||||
params={
|
||||
'csrf_token': csrf_token,
|
||||
'text': 'init',
|
||||
},
|
||||
extra_environ=xhr_header,
|
||||
)
|
||||
assert response.json
|
||||
|
||||
comment_id = response.json.get('comment_id', None)
|
||||
assert comment_id
|
||||
test_text = 'init'
|
||||
response = self.app.post(
|
||||
route_path(
|
||||
'pullrequest_comment_edit',
|
||||
repo_name=pull_request.target_repo.scm_instance().name,
|
||||
pull_request_id=pull_request.pull_request_id,
|
||||
comment_id=comment_id,
|
||||
),
|
||||
extra_environ=xhr_header,
|
||||
params={
|
||||
'csrf_token': csrf_token,
|
||||
'text': test_text,
|
||||
'version': '0',
|
||||
},
|
||||
status=404,
|
||||
|
||||
)
|
||||
assert response.status_int == 404
|
||||
|
||||
def test_comment_and_try_edit_already_edited(
|
||||
self, pr_util, csrf_token, xhr_header
|
||||
):
|
||||
pull_request = pr_util.create_pull_request()
|
||||
response = self.app.post(
|
||||
route_path(
|
||||
'pullrequest_comment_create',
|
||||
repo_name=pull_request.target_repo.scm_instance().name,
|
||||
pull_request_id=pull_request.pull_request_id),
|
||||
params={
|
||||
'csrf_token': csrf_token,
|
||||
'text': 'init',
|
||||
},
|
||||
extra_environ=xhr_header,
|
||||
)
|
||||
assert response.json
|
||||
comment_id = response.json.get('comment_id', None)
|
||||
assert comment_id
|
||||
test_text = 'test'
|
||||
response = self.app.post(
|
||||
route_path(
|
||||
'pullrequest_comment_edit',
|
||||
repo_name=pull_request.target_repo.scm_instance().name,
|
||||
pull_request_id=pull_request.pull_request_id,
|
||||
comment_id=comment_id,
|
||||
),
|
||||
extra_environ=xhr_header,
|
||||
params={
|
||||
'csrf_token': csrf_token,
|
||||
'text': test_text,
|
||||
'version': '0',
|
||||
},
|
||||
|
||||
)
|
||||
test_text_v2 = 'test_v2'
|
||||
response = self.app.post(
|
||||
route_path(
|
||||
'pullrequest_comment_edit',
|
||||
repo_name=pull_request.target_repo.scm_instance().name,
|
||||
pull_request_id=pull_request.pull_request_id,
|
||||
comment_id=comment_id,
|
||||
),
|
||||
extra_environ=xhr_header,
|
||||
params={
|
||||
'csrf_token': csrf_token,
|
||||
'text': test_text_v2,
|
||||
'version': '0',
|
||||
},
|
||||
status=404,
|
||||
)
|
||||
assert response.status_int == 404
|
||||
|
||||
text_form_db = ChangesetComment.query().filter(
|
||||
ChangesetComment.comment_id == comment_id).first().text
|
||||
|
||||
assert test_text == text_form_db
|
||||
assert test_text_v2 != text_form_db
|
||||
|
||||
def test_comment_and_comment_edit_permissions_forbidden(
|
||||
self, autologin_regular_user, user_regular, user_admin, pr_util,
|
||||
csrf_token, xhr_header):
|
||||
pull_request = pr_util.create_pull_request(
|
||||
author=user_admin.username, enable_notifications=False)
|
||||
comment = CommentsModel().create(
|
||||
text='test',
|
||||
repo=pull_request.target_repo.scm_instance().name,
|
||||
user=user_admin,
|
||||
pull_request=pull_request,
|
||||
)
|
||||
response = self.app.post(
|
||||
route_path(
|
||||
'pullrequest_comment_edit',
|
||||
repo_name=pull_request.target_repo.scm_instance().name,
|
||||
pull_request_id=pull_request.pull_request_id,
|
||||
comment_id=comment.comment_id,
|
||||
),
|
||||
extra_environ=xhr_header,
|
||||
params={
|
||||
'csrf_token': csrf_token,
|
||||
'text': 'test_text',
|
||||
},
|
||||
status=403,
|
||||
)
|
||||
assert response.status_int == 403
|
||||
|
||||
def test_create_pull_request(self, backend, csrf_token):
|
||||
commits = [
|
||||
{'message': 'ancestor'},
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ from rhodecode.lib.utils2 import safe_unicode, str2bool
|
|||
from rhodecode.lib.vcs.backends.base import EmptyCommit
|
||||
from rhodecode.lib.vcs.exceptions import (
|
||||
RepositoryError, CommitDoesNotExistError)
|
||||
from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore
|
||||
from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
|
||||
ChangesetCommentHistory
|
||||
from rhodecode.model.changeset_status import ChangesetStatusModel
|
||||
from rhodecode.model.comment import CommentsModel
|
||||
from rhodecode.model.meta import Session
|
||||
|
|
@ -425,6 +426,27 @@ class RepoCommitsView(RepoAppView):
|
|||
repo_name=self.db_repo_name)
|
||||
return ''
|
||||
|
||||
@LoginRequired()
|
||||
@NotAnonymous()
|
||||
@HasRepoPermissionAnyDecorator(
|
||||
'repository.read', 'repository.write', 'repository.admin')
|
||||
@CSRFRequired()
|
||||
@view_config(
|
||||
route_name='repo_commit_comment_history_view', request_method='POST',
|
||||
renderer='string', xhr=True)
|
||||
def repo_commit_comment_history_view(self):
|
||||
commit_id = self.request.matchdict['commit_id']
|
||||
comment_history_id = self.request.matchdict['comment_history_id']
|
||||
comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
|
||||
c = self.load_default_context()
|
||||
c.comment_history = comment_history
|
||||
|
||||
rendered_comment = render(
|
||||
'rhodecode:templates/changeset/comment_history.mako',
|
||||
self._get_template_context(c)
|
||||
, self.request)
|
||||
return rendered_comment
|
||||
|
||||
@LoginRequired()
|
||||
@NotAnonymous()
|
||||
@HasRepoPermissionAnyDecorator(
|
||||
|
|
@ -557,6 +579,74 @@ class RepoCommitsView(RepoAppView):
|
|||
self._rhodecode_db_user, comment_id)
|
||||
raise HTTPNotFound()
|
||||
|
||||
@LoginRequired()
|
||||
@NotAnonymous()
|
||||
@HasRepoPermissionAnyDecorator(
|
||||
'repository.read', 'repository.write', 'repository.admin')
|
||||
@CSRFRequired()
|
||||
@view_config(
|
||||
route_name='repo_commit_comment_edit', request_method='POST',
|
||||
renderer='json_ext')
|
||||
def repo_commit_comment_edit(self):
|
||||
commit_id = self.request.matchdict['commit_id']
|
||||
comment_id = self.request.matchdict['comment_id']
|
||||
|
||||
comment = ChangesetComment.get_or_404(comment_id)
|
||||
|
||||
if comment.immutable:
|
||||
# don't allow deleting comments that are immutable
|
||||
raise HTTPForbidden()
|
||||
|
||||
is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
|
||||
super_admin = h.HasPermissionAny('hg.admin')()
|
||||
comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
|
||||
is_repo_comment = comment.repo.repo_name == self.db_repo_name
|
||||
comment_repo_admin = is_repo_admin and is_repo_comment
|
||||
|
||||
if super_admin or comment_owner or comment_repo_admin:
|
||||
text = self.request.POST.get('text')
|
||||
version = self.request.POST.get('version')
|
||||
if text == comment.text:
|
||||
log.warning(
|
||||
'Comment(repo): '
|
||||
'Trying to create new version '
|
||||
'of existing comment {}'.format(
|
||||
comment_id,
|
||||
)
|
||||
)
|
||||
raise HTTPNotFound()
|
||||
if version.isdigit():
|
||||
version = int(version)
|
||||
else:
|
||||
log.warning(
|
||||
'Comment(repo): Wrong version type {} {} '
|
||||
'for comment {}'.format(
|
||||
version,
|
||||
type(version),
|
||||
comment_id,
|
||||
)
|
||||
)
|
||||
raise HTTPNotFound()
|
||||
|
||||
comment_history = CommentsModel().edit(
|
||||
comment_id=comment_id,
|
||||
text=text,
|
||||
auth_user=self._rhodecode_user,
|
||||
version=version,
|
||||
)
|
||||
if not comment_history:
|
||||
raise HTTPNotFound()
|
||||
Session().commit()
|
||||
return {
|
||||
'comment_history_id': comment_history.comment_history_id,
|
||||
'comment_id': comment.comment_id,
|
||||
'comment_version': comment_history.version,
|
||||
}
|
||||
else:
|
||||
log.warning('No permissions for user %s to edit comment_id: %s',
|
||||
self._rhodecode_db_user, comment_id)
|
||||
raise HTTPNotFound()
|
||||
|
||||
@LoginRequired()
|
||||
@HasRepoPermissionAnyDecorator(
|
||||
'repository.read', 'repository.write', 'repository.admin')
|
||||
|
|
|
|||
|
|
@ -1518,3 +1518,90 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
|||
log.warning('No permissions for user %s to delete comment_id: %s',
|
||||
self._rhodecode_db_user, comment_id)
|
||||
raise HTTPNotFound()
|
||||
|
||||
@LoginRequired()
|
||||
@NotAnonymous()
|
||||
@HasRepoPermissionAnyDecorator(
|
||||
'repository.read', 'repository.write', 'repository.admin')
|
||||
@CSRFRequired()
|
||||
@view_config(
|
||||
route_name='pullrequest_comment_edit', request_method='POST',
|
||||
renderer='json_ext')
|
||||
def pull_request_comment_edit(self):
|
||||
pull_request = PullRequest.get_or_404(
|
||||
self.request.matchdict['pull_request_id']
|
||||
)
|
||||
comment = ChangesetComment.get_or_404(
|
||||
self.request.matchdict['comment_id']
|
||||
)
|
||||
comment_id = comment.comment_id
|
||||
|
||||
if comment.immutable:
|
||||
# don't allow deleting comments that are immutable
|
||||
raise HTTPForbidden()
|
||||
|
||||
if pull_request.is_closed():
|
||||
log.debug('comment: forbidden because pull request is closed')
|
||||
raise HTTPForbidden()
|
||||
|
||||
if not comment:
|
||||
log.debug('Comment with id:%s not found, skipping', comment_id)
|
||||
# comment already deleted in another call probably
|
||||
return True
|
||||
|
||||
if comment.pull_request.is_closed():
|
||||
# don't allow deleting comments on closed pull request
|
||||
raise HTTPForbidden()
|
||||
|
||||
is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
|
||||
super_admin = h.HasPermissionAny('hg.admin')()
|
||||
comment_owner = comment.author.user_id == self._rhodecode_user.user_id
|
||||
is_repo_comment = comment.repo.repo_name == self.db_repo_name
|
||||
comment_repo_admin = is_repo_admin and is_repo_comment
|
||||
|
||||
if super_admin or comment_owner or comment_repo_admin:
|
||||
text = self.request.POST.get('text')
|
||||
version = self.request.POST.get('version')
|
||||
if text == comment.text:
|
||||
log.warning(
|
||||
'Comment(PR): '
|
||||
'Trying to create new version '
|
||||
'of existing comment {}'.format(
|
||||
comment_id,
|
||||
)
|
||||
)
|
||||
raise HTTPNotFound()
|
||||
if version.isdigit():
|
||||
version = int(version)
|
||||
else:
|
||||
log.warning(
|
||||
'Comment(PR): Wrong version type {} {} '
|
||||
'for comment {}'.format(
|
||||
version,
|
||||
type(version),
|
||||
comment_id,
|
||||
)
|
||||
)
|
||||
raise HTTPNotFound()
|
||||
|
||||
comment_history = CommentsModel().edit(
|
||||
comment_id=comment_id,
|
||||
text=text,
|
||||
auth_user=self._rhodecode_user,
|
||||
version=version,
|
||||
)
|
||||
if not comment_history:
|
||||
raise HTTPNotFound()
|
||||
Session().commit()
|
||||
return {
|
||||
'comment_history_id': comment_history.comment_history_id,
|
||||
'comment_id': comment.comment_id,
|
||||
'comment_version': comment_history.version,
|
||||
}
|
||||
else:
|
||||
log.warning(
|
||||
'No permissions for user {} to edit comment_id: {}'.format(
|
||||
self._rhodecode_db_user, comment_id
|
||||
)
|
||||
)
|
||||
raise HTTPNotFound()
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ ACTIONS_V1 = {
|
|||
'repo.pull_request.merge': '',
|
||||
'repo.pull_request.vote': '',
|
||||
'repo.pull_request.comment.create': '',
|
||||
'repo.pull_request.comment.edit': '',
|
||||
'repo.pull_request.comment.delete': '',
|
||||
|
||||
'repo.pull_request.reviewer.add': '',
|
||||
|
|
@ -90,6 +91,7 @@ ACTIONS_V1 = {
|
|||
'repo.commit.strip': {'commit_id': ''},
|
||||
'repo.commit.comment.create': {'data': {}},
|
||||
'repo.commit.comment.delete': {'data': {}},
|
||||
'repo.commit.comment.edit': {'data': {}},
|
||||
'repo.commit.vote': '',
|
||||
|
||||
'repo.artifact.add': '',
|
||||
|
|
|
|||
5625
rhodecode/lib/dbmigrate/schema/db_4_19_0_2.py
Normal file
5625
rhodecode/lib/dbmigrate/schema/db_4_19_0_2.py
Normal file
File diff suppressed because it is too large
Load diff
35
rhodecode/lib/dbmigrate/versions/108_version_4_19_1.py
Normal file
35
rhodecode/lib/dbmigrate/versions/108_version_4_19_1.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
from sqlalchemy import *
|
||||
|
||||
from alembic.migration import MigrationContext
|
||||
from alembic.operations import Operations
|
||||
from sqlalchemy import BigInteger
|
||||
|
||||
from rhodecode.lib.dbmigrate.versions import _reset_base
|
||||
from rhodecode.model import init_model_encryption
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
"""
|
||||
Upgrade operations go here.
|
||||
Don't create your own engine; bind migrate_engine to your metadata
|
||||
"""
|
||||
_reset_base(migrate_engine)
|
||||
from rhodecode.lib.dbmigrate.schema import db_4_19_0_2 as db
|
||||
|
||||
init_model_encryption(db)
|
||||
db.ChangesetCommentHistory().__table__.create()
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
|
||||
def fixups(models, _SESSION):
|
||||
pass
|
||||
25
rhodecode/lib/html_filters.py
Normal file
25
rhodecode/lib/html_filters.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2020-2020 RhodeCode GmbH
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License, version 3
|
||||
# (only), as published by the Free Software Foundation.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This program is dual-licensed. If you wish to learn more about the
|
||||
# RhodeCode Enterprise Edition, including its added features, Support services,
|
||||
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
||||
|
||||
## base64 filter e.g ${ example | base64,n }
|
||||
def base64(text):
|
||||
import base64
|
||||
from rhodecode.lib.helpers import safe_str
|
||||
return base64.encodestring(safe_str(text))
|
||||
|
|
@ -35,7 +35,13 @@ from rhodecode.lib import audit_logger
|
|||
from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
|
||||
from rhodecode.model import BaseModel
|
||||
from rhodecode.model.db import (
|
||||
ChangesetComment, User, Notification, PullRequest, AttributeDict)
|
||||
ChangesetComment,
|
||||
User,
|
||||
Notification,
|
||||
PullRequest,
|
||||
AttributeDict,
|
||||
ChangesetCommentHistory,
|
||||
)
|
||||
from rhodecode.model.notification import NotificationModel
|
||||
from rhodecode.model.meta import Session
|
||||
from rhodecode.model.settings import VcsSettingsModel
|
||||
|
|
@ -479,6 +485,54 @@ class CommentsModel(BaseModel):
|
|||
|
||||
return comment
|
||||
|
||||
def edit(self, comment_id, text, auth_user, version):
|
||||
"""
|
||||
Change existing comment for commit or pull request.
|
||||
|
||||
:param comment_id:
|
||||
:param text:
|
||||
:param auth_user: current authenticated user calling this method
|
||||
:param version: last comment version
|
||||
"""
|
||||
if not text:
|
||||
log.warning('Missing text for comment, skipping...')
|
||||
return
|
||||
|
||||
comment = ChangesetComment.get(comment_id)
|
||||
old_comment_text = comment.text
|
||||
comment.text = text
|
||||
comment_version = ChangesetCommentHistory.get_version(comment_id)
|
||||
if (comment_version - version) != 1:
|
||||
log.warning(
|
||||
'Version mismatch, skipping... '
|
||||
'version {} but should be {}'.format(
|
||||
(version - 1),
|
||||
comment_version,
|
||||
)
|
||||
)
|
||||
return
|
||||
comment_history = ChangesetCommentHistory()
|
||||
comment_history.comment_id = comment_id
|
||||
comment_history.version = comment_version
|
||||
comment_history.created_by_user_id = auth_user.user_id
|
||||
comment_history.text = old_comment_text
|
||||
# TODO add email notification
|
||||
Session().add(comment_history)
|
||||
Session().add(comment)
|
||||
Session().flush()
|
||||
|
||||
if comment.pull_request:
|
||||
action = 'repo.pull_request.comment.edit'
|
||||
else:
|
||||
action = 'repo.commit.comment.edit'
|
||||
|
||||
comment_data = comment.get_api_data()
|
||||
comment_data['old_comment_text'] = old_comment_text
|
||||
self._log_audit_action(
|
||||
action, {'data': comment_data}, auth_user, comment)
|
||||
|
||||
return comment_history
|
||||
|
||||
def delete(self, comment, auth_user):
|
||||
"""
|
||||
Deletes given comment
|
||||
|
|
@ -712,6 +766,7 @@ class CommentsModel(BaseModel):
|
|||
.filter(ChangesetComment.line_no == None)\
|
||||
.filter(ChangesetComment.f_path == None)\
|
||||
.filter(ChangesetComment.pull_request == pull_request)
|
||||
|
||||
return comments
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -3755,6 +3755,7 @@ class ChangesetComment(Base, BaseModel):
|
|||
status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
|
||||
pull_request = relationship('PullRequest', lazy='joined')
|
||||
pull_request_version = relationship('PullRequestVersion')
|
||||
history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='joined', order_by='ChangesetCommentHistory.version')
|
||||
|
||||
@classmethod
|
||||
def get_users(cls, revision=None, pull_request_id=None):
|
||||
|
|
@ -3849,6 +3850,36 @@ class ChangesetComment(Base, BaseModel):
|
|||
return data
|
||||
|
||||
|
||||
class ChangesetCommentHistory(Base, BaseModel):
|
||||
__tablename__ = 'changeset_comments_history'
|
||||
__table_args__ = (
|
||||
Index('cch_comment_id_idx', 'comment_id'),
|
||||
base_table_args,
|
||||
)
|
||||
|
||||
comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
|
||||
comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
|
||||
version = Column("version", Integer(), nullable=False, default=0)
|
||||
created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
|
||||
text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
|
||||
created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
|
||||
deleted = Column('deleted', Boolean(), default=False)
|
||||
|
||||
author = relationship('User', lazy='joined')
|
||||
comment = relationship('ChangesetComment', cascade="all, delete")
|
||||
|
||||
@classmethod
|
||||
def get_version(cls, comment_id):
|
||||
q = Session().query(ChangesetCommentHistory).filter(
|
||||
ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
|
||||
if q.count() == 0:
|
||||
return 1
|
||||
elif q.count() >= q[0].version:
|
||||
return q.count() + 1
|
||||
else:
|
||||
return q[0].version + 1
|
||||
|
||||
|
||||
class ChangesetStatus(Base, BaseModel):
|
||||
__tablename__ = 'changeset_statuses'
|
||||
__table_args__ = (
|
||||
|
|
|
|||
|
|
@ -185,8 +185,10 @@ function registerRCRoutes() {
|
|||
pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
|
||||
pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
|
||||
pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
|
||||
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']);
|
||||
pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
|
||||
pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
|
||||
pyroutes.register('repo_commit_comment_edit', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/edit', ['repo_name', 'commit_id', 'comment_id']);
|
||||
pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
|
||||
pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
|
||||
pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
|
||||
|
|
@ -242,6 +244,7 @@ function registerRCRoutes() {
|
|||
pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
|
||||
pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
|
||||
pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
|
||||
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']);
|
||||
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']);
|
||||
pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
|
||||
pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
|
||||
|
|
|
|||
|
|
@ -80,9 +80,10 @@ var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
|
|||
})(function() {
|
||||
"use strict";
|
||||
|
||||
function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
|
||||
function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
|
||||
|
||||
if (!(this instanceof CommentForm)) {
|
||||
return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
|
||||
return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
|
||||
}
|
||||
|
||||
// bind the element instance to our Form
|
||||
|
|
@ -126,10 +127,22 @@ var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
|
|||
this.submitButton = $(this.submitForm).find('input[type="submit"]');
|
||||
this.submitButtonText = this.submitButton.val();
|
||||
|
||||
|
||||
this.previewUrl = pyroutes.url('repo_commit_comment_preview',
|
||||
{'repo_name': templateContext.repo_name,
|
||||
'commit_id': templateContext.commit_data.commit_id});
|
||||
|
||||
if (edit){
|
||||
this.submitButtonText = _gettext('Updated Comment');
|
||||
$(this.commentType).prop('disabled', true);
|
||||
$(this.commentType).addClass('disabled');
|
||||
var editInfo =
|
||||
'<div class="comment-label note" id="comment-label-6" title="line: ">' +
|
||||
'editing' +
|
||||
'</div>';
|
||||
$(editInfo).insertBefore($(this.editButton).parent());
|
||||
}
|
||||
|
||||
if (resolvesCommentId){
|
||||
this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
|
||||
this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
|
||||
|
|
@ -153,17 +166,27 @@ var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
|
|||
// based on commitId, or pullRequestId decide where do we submit
|
||||
// out data
|
||||
if (this.commitId){
|
||||
this.submitUrl = pyroutes.url('repo_commit_comment_create',
|
||||
var pyurl = 'repo_commit_comment_create';
|
||||
if(edit){
|
||||
pyurl = 'repo_commit_comment_edit';
|
||||
}
|
||||
this.submitUrl = pyroutes.url(pyurl,
|
||||
{'repo_name': templateContext.repo_name,
|
||||
'commit_id': this.commitId});
|
||||
'commit_id': this.commitId,
|
||||
'comment_id': comment_id});
|
||||
this.selfUrl = pyroutes.url('repo_commit',
|
||||
{'repo_name': templateContext.repo_name,
|
||||
'commit_id': this.commitId});
|
||||
|
||||
} else if (this.pullRequestId) {
|
||||
this.submitUrl = pyroutes.url('pullrequest_comment_create',
|
||||
var pyurl = 'pullrequest_comment_create';
|
||||
if(edit){
|
||||
pyurl = 'pullrequest_comment_edit';
|
||||
}
|
||||
this.submitUrl = pyroutes.url(pyurl,
|
||||
{'repo_name': templateContext.repo_name,
|
||||
'pull_request_id': this.pullRequestId});
|
||||
'pull_request_id': this.pullRequestId,
|
||||
'comment_id': comment_id});
|
||||
this.selfUrl = pyroutes.url('pullrequest_show',
|
||||
{'repo_name': templateContext.repo_name,
|
||||
'pull_request_id': this.pullRequestId});
|
||||
|
|
@ -277,7 +300,7 @@ var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
|
|||
this.globalSubmitSuccessCallback = function(){
|
||||
// default behaviour is to call GLOBAL hook, if it's registered.
|
||||
if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
|
||||
commentFormGlobalSubmitSuccessCallback()
|
||||
commentFormGlobalSubmitSuccessCallback();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -480,13 +503,75 @@ var CommentsController = function() {
|
|||
var mainComment = '#text';
|
||||
var self = this;
|
||||
|
||||
this.cancelComment = function(node) {
|
||||
this.cancelComment = function (node) {
|
||||
var $node = $(node);
|
||||
var $td = $node.closest('td');
|
||||
var edit = $(this).attr('edit');
|
||||
if (edit) {
|
||||
var $general_comments = null;
|
||||
var $inline_comments = $node.closest('div.inline-comments');
|
||||
if (!$inline_comments.length) {
|
||||
$general_comments = $('#comments');
|
||||
var $comment = $general_comments.parent().find('div.comment:hidden');
|
||||
// show hidden general comment form
|
||||
$('#cb-comment-general-form-placeholder').show();
|
||||
} else {
|
||||
var $comment = $inline_comments.find('div.comment:hidden');
|
||||
}
|
||||
$comment.show();
|
||||
}
|
||||
$node.closest('.comment-inline-form').remove();
|
||||
return false;
|
||||
};
|
||||
|
||||
this.showVersion = function (node) {
|
||||
var $node = $(node);
|
||||
var selectedIndex = $node.context.selectedIndex;
|
||||
var option = $node.find('option[value="'+ selectedIndex +'"]');
|
||||
var zero_option = $node.find('option[value="0"]');
|
||||
if (!option){
|
||||
return;
|
||||
}
|
||||
|
||||
// little trick to cheat onchange and allow to display the same version again
|
||||
$node.context.selectedIndex = 0;
|
||||
zero_option.text(selectedIndex);
|
||||
|
||||
var comment_history_id = option.attr('data-comment-history-id');
|
||||
var comment_id = option.attr('data-comment-id');
|
||||
var historyViewUrl = pyroutes.url(
|
||||
'repo_commit_comment_history_view',
|
||||
{
|
||||
'repo_name': templateContext.repo_name,
|
||||
'commit_id': comment_id,
|
||||
'comment_history_id': comment_history_id,
|
||||
}
|
||||
);
|
||||
successRenderCommit = function (data) {
|
||||
Swal.fire({
|
||||
html: data,
|
||||
title: '',
|
||||
showClass: {
|
||||
popup: 'swal2-noanimation',
|
||||
backdrop: 'swal2-noanimation'
|
||||
},
|
||||
});
|
||||
};
|
||||
failRenderCommit = function () {
|
||||
Swal.fire({
|
||||
html: 'Error while loading comment',
|
||||
title: '',
|
||||
showClass: {
|
||||
popup: 'swal2-noanimation',
|
||||
backdrop: 'swal2-noanimation'
|
||||
},
|
||||
});
|
||||
};
|
||||
_submitAjaxPOST(
|
||||
historyViewUrl, {'csrf_token': CSRF_TOKEN}, successRenderCommit,
|
||||
failRenderCommit
|
||||
);
|
||||
};
|
||||
|
||||
this.getLineNumber = function(node) {
|
||||
var $node = $(node);
|
||||
var lineNo = $node.closest('td').attr('data-line-no');
|
||||
|
|
@ -638,12 +723,12 @@ var CommentsController = function() {
|
|||
$node.closest('tr').toggleClass('hide-line-comments');
|
||||
};
|
||||
|
||||
this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
|
||||
this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
|
||||
var pullRequestId = templateContext.pull_request_data.pull_request_id;
|
||||
var commitId = templateContext.commit_data.commit_id;
|
||||
|
||||
var commentForm = new CommentForm(
|
||||
formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
|
||||
formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
|
||||
var cm = commentForm.getCmInstance();
|
||||
|
||||
if (resolvesCommentId){
|
||||
|
|
@ -780,18 +865,200 @@ var CommentsController = function() {
|
|||
|
||||
var _form = $($form[0]);
|
||||
var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
|
||||
var edit = false;
|
||||
var comment_id = null;
|
||||
var commentForm = this.createCommentForm(
|
||||
_form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
|
||||
_form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
|
||||
commentForm.initStatusChangeSelector();
|
||||
|
||||
return commentForm;
|
||||
};
|
||||
this.editComment = function(node) {
|
||||
var $node = $(node);
|
||||
var $comment = $(node).closest('.comment');
|
||||
var comment_id = $comment.attr('data-comment-id');
|
||||
var $form = null
|
||||
|
||||
var $comments = $node.closest('div.inline-comments');
|
||||
var $general_comments = null;
|
||||
var lineno = null;
|
||||
|
||||
if($comments.length){
|
||||
// inline comments setup
|
||||
$form = $comments.find('.comment-inline-form');
|
||||
lineno = self.getLineNumber(node)
|
||||
}
|
||||
else{
|
||||
// general comments setup
|
||||
$comments = $('#comments');
|
||||
$form = $comments.find('.comment-inline-form');
|
||||
lineno = $comment[0].id
|
||||
$('#cb-comment-general-form-placeholder').hide();
|
||||
}
|
||||
|
||||
this.edit = true;
|
||||
|
||||
if (!$form.length) {
|
||||
|
||||
var $filediff = $node.closest('.filediff');
|
||||
$filediff.removeClass('hide-comments');
|
||||
var f_path = $filediff.attr('data-f-path');
|
||||
|
||||
// create a new HTML from template
|
||||
|
||||
var tmpl = $('#cb-comment-inline-form-template').html();
|
||||
tmpl = tmpl.format(escapeHtml(f_path), lineno);
|
||||
$form = $(tmpl);
|
||||
$comment.after($form)
|
||||
|
||||
var _form = $($form[0]).find('form');
|
||||
var autocompleteActions = ['as_note',];
|
||||
var commentForm = this.createCommentForm(
|
||||
_form, lineno, '', autocompleteActions, resolvesCommentId,
|
||||
this.edit, comment_id);
|
||||
var old_comment_text_binary = $comment.attr('data-comment-text');
|
||||
var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
|
||||
commentForm.cm.setValue(old_comment_text);
|
||||
$comment.hide();
|
||||
|
||||
$.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
|
||||
form: _form,
|
||||
parent: $comments,
|
||||
lineno: lineno,
|
||||
f_path: f_path}
|
||||
);
|
||||
// set a CUSTOM submit handler for inline comments.
|
||||
commentForm.setHandleFormSubmit(function(o) {
|
||||
var text = commentForm.cm.getValue();
|
||||
var commentType = commentForm.getCommentType();
|
||||
var resolvesCommentId = commentForm.getResolvesId();
|
||||
|
||||
if (text === "") {
|
||||
return;
|
||||
}
|
||||
if (old_comment_text == text) {
|
||||
Swal.fire({
|
||||
title: 'Error',
|
||||
html: _gettext('Comment body should be changed'),
|
||||
showClass: {
|
||||
popup: 'swal2-noanimation',
|
||||
backdrop: 'swal2-noanimation'
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
var excludeCancelBtn = false;
|
||||
var submitEvent = true;
|
||||
commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
|
||||
commentForm.cm.setOption("readOnly", true);
|
||||
var dropDown = $('#comment_history_for_comment_'+comment_id);
|
||||
|
||||
var version = dropDown.children().last().val()
|
||||
if(!version){
|
||||
version = 0;
|
||||
}
|
||||
var postData = {
|
||||
'text': text,
|
||||
'f_path': f_path,
|
||||
'line': lineno,
|
||||
'comment_type': commentType,
|
||||
'csrf_token': CSRF_TOKEN,
|
||||
'version': version,
|
||||
};
|
||||
|
||||
var submitSuccessCallback = function(json_data) {
|
||||
$form.remove();
|
||||
$comment.show();
|
||||
var postData = {
|
||||
'text': text,
|
||||
'renderer': $comment.attr('data-comment-renderer'),
|
||||
'csrf_token': CSRF_TOKEN
|
||||
};
|
||||
|
||||
var updateCommentVersionDropDown = function () {
|
||||
var dropDown = $('#comment_history_for_comment_'+comment_id);
|
||||
$comment.attr('data-comment-text', btoa(text));
|
||||
var version = json_data['comment_version']
|
||||
var option = new Option(version, version);
|
||||
var $option = $(option);
|
||||
$option.attr('data-comment-history-id', json_data['comment_history_id']);
|
||||
$option.attr('data-comment-id', json_data['comment_id']);
|
||||
dropDown.append(option);
|
||||
dropDown.parent().show();
|
||||
}
|
||||
updateCommentVersionDropDown();
|
||||
// by default we reset state of comment preserving the text
|
||||
var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
|
||||
var prefix = "Error while editing of comment.\n"
|
||||
var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
|
||||
ajaxErrorSwal(message);
|
||||
|
||||
};
|
||||
var successRenderCommit = function(o){
|
||||
$comment.show();
|
||||
$comment[0].lastElementChild.innerHTML = o;
|
||||
}
|
||||
var previewUrl = pyroutes.url('repo_commit_comment_preview',
|
||||
{'repo_name': templateContext.repo_name,
|
||||
'commit_id': templateContext.commit_data.commit_id});
|
||||
|
||||
_submitAjaxPOST(
|
||||
previewUrl, postData, successRenderCommit,
|
||||
failRenderCommit
|
||||
);
|
||||
|
||||
try {
|
||||
var html = json_data.rendered_text;
|
||||
var lineno = json_data.line_no;
|
||||
var target_id = json_data.target_id;
|
||||
|
||||
$comments.find('.cb-comment-add-button').before(html);
|
||||
|
||||
//mark visually which comment was resolved
|
||||
if (resolvesCommentId) {
|
||||
commentForm.markCommentResolved(resolvesCommentId);
|
||||
}
|
||||
|
||||
// run global callback on submit
|
||||
commentForm.globalSubmitSuccessCallback();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
// re trigger the linkification of next/prev navigation
|
||||
linkifyComments($('.inline-comment-injected'));
|
||||
timeagoActivate();
|
||||
tooltipActivate();
|
||||
|
||||
if (window.updateSticky !== undefined) {
|
||||
// potentially our comments change the active window size, so we
|
||||
// notify sticky elements
|
||||
updateSticky()
|
||||
}
|
||||
|
||||
commentForm.setActionButtonsDisabled(false);
|
||||
|
||||
};
|
||||
var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
|
||||
var prefix = "Error while editing comment.\n"
|
||||
var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
|
||||
ajaxErrorSwal(message);
|
||||
commentForm.resetCommentFormState(text)
|
||||
};
|
||||
commentForm.submitAjaxPOST(
|
||||
commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
|
||||
});
|
||||
}
|
||||
|
||||
$form.addClass('comment-inline-form-open');
|
||||
};
|
||||
this.createComment = function(node, resolutionComment) {
|
||||
var resolvesCommentId = resolutionComment || null;
|
||||
var $node = $(node);
|
||||
var $td = $node.closest('td');
|
||||
var $form = $td.find('.comment-inline-form');
|
||||
this.edit = false;
|
||||
|
||||
if (!$form.length) {
|
||||
|
||||
|
|
@ -816,8 +1083,9 @@ var CommentsController = function() {
|
|||
var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
|
||||
var _form = $($form[0]).find('form');
|
||||
var autocompleteActions = ['as_note', 'as_todo'];
|
||||
var comment_id=null;
|
||||
var commentForm = this.createCommentForm(
|
||||
_form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
|
||||
_form, lineno, placeholderText, autocompleteActions, resolvesCommentId, this.edit, comment_id);
|
||||
|
||||
$.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
|
||||
form: _form,
|
||||
|
|
|
|||
|
|
@ -182,3 +182,9 @@ var htmlEnDeCode = (function() {
|
|||
htmlDecode: htmlDecode
|
||||
};
|
||||
})();
|
||||
|
||||
function b64DecodeUnicode(str) {
|
||||
return decodeURIComponent(atob(str).split('').map(function (c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
}).join(''));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
|
||||
<%!
|
||||
## base64 filter e.g ${ example | base64 }
|
||||
def base64(text):
|
||||
import base64
|
||||
from rhodecode.lib.helpers import safe_str
|
||||
return base64.encodestring(safe_str(text))
|
||||
from rhodecode.lib import html_filters
|
||||
%>
|
||||
|
||||
<%inherit file="root.mako"/>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,12 @@
|
|||
## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
|
||||
## ${comment.comment_block(comment)}
|
||||
##
|
||||
<%namespace name="base" file="/base/base.mako"/>
|
||||
|
||||
<%!
|
||||
from rhodecode.lib import html_filters
|
||||
%>
|
||||
|
||||
<%namespace name="base" file="/base/base.mako"/>
|
||||
<%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
|
||||
<% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
|
||||
<% latest_ver = len(getattr(c, 'versions', [])) %>
|
||||
|
|
@ -21,6 +25,8 @@
|
|||
line="${comment.line_no}"
|
||||
data-comment-id="${comment.comment_id}"
|
||||
data-comment-type="${comment.comment_type}"
|
||||
data-comment-renderer="${comment.renderer}"
|
||||
data-comment-text="${comment.text | html_filters.base64,n}"
|
||||
data-comment-line-no="${comment.line_no}"
|
||||
data-comment-inline=${h.json.dumps(inline)}
|
||||
style="${'display: none;' if outdated_at_ver else ''}">
|
||||
|
|
@ -60,6 +66,31 @@
|
|||
<div class="date">
|
||||
${h.age_component(comment.modified_at, time_is_local=True)}
|
||||
</div>
|
||||
% if comment.history:
|
||||
<div class="date">
|
||||
<span class="comment-area-text">${_('Comment version')}:</span>
|
||||
<select class="comment-type" id="comment_history_for_comment_${comment.comment_id}"
|
||||
onchange="return Rhodecode.comments.showVersion(this)"
|
||||
name="comment_type">
|
||||
<option style="display: none" value="0">---</option>
|
||||
%for comment_history in comment.history:
|
||||
<option
|
||||
data-comment-history-id="${comment_history.comment_history_id}",
|
||||
data-comment-id="${comment.comment_id}",
|
||||
value="${comment_history.version}">${comment_history.version}</option>
|
||||
%endfor
|
||||
</select>
|
||||
</div>
|
||||
% else:
|
||||
<div class="date" style="display: none">
|
||||
<span class="comment-area-text">${_('Comment version')}</span>
|
||||
<select class="comment-type" id="comment_history_for_comment_${comment.comment_id}"
|
||||
onchange="return Rhodecode.comments.showVersion(this)"
|
||||
name="comment_type">
|
||||
<option style="display: none" value="0">---</option>
|
||||
</select>
|
||||
</div>
|
||||
%endif
|
||||
% if inline:
|
||||
<span></span>
|
||||
% else:
|
||||
|
|
@ -136,21 +167,29 @@
|
|||
%if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
|
||||
## permissions to delete
|
||||
%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):
|
||||
## TODO: dan: add edit comment here
|
||||
<a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
|
||||
%if comment.comment_type == 'note':
|
||||
<a onclick="return Rhodecode.comments.editComment(this);"
|
||||
class="edit-comment"> ${_('Edit')}</a>
|
||||
%else:
|
||||
<button class="btn-link" disabled="disabled"> ${_('Edit')}</button>
|
||||
%endif
|
||||
| <a onclick="return Rhodecode.comments.deleteComment(this);"
|
||||
class="delete-comment"> ${_('Delete')}</a>
|
||||
%else:
|
||||
<button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
|
||||
<button class="btn-link" disabled="disabled"> ${_('Edit')}</button>
|
||||
| <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
|
||||
%endif
|
||||
%else:
|
||||
<button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
|
||||
<button class="btn-link" disabled="disabled"> ${_('Edit')}</button>
|
||||
| <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
|
||||
%endif
|
||||
|
||||
% if outdated_at_ver:
|
||||
| <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="prev-comment"> ${_('Prev')}</a>
|
||||
| <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="next-comment"> ${_('Next')}</a>
|
||||
| <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>
|
||||
| <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>
|
||||
% else:
|
||||
| <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
|
||||
| <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
|
||||
| <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
|
||||
| <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
|
||||
% endif
|
||||
|
||||
</div>
|
||||
|
|
|
|||
7
rhodecode/templates/changeset/comment_history.mako
Normal file
7
rhodecode/templates/changeset/comment_history.mako
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<%namespace name="base" file="/base/base.mako"/>
|
||||
|
||||
${c.comment_history.author.email}
|
||||
${base.gravatar_with_user(c.comment_history.author.email, 16, tooltip=True)}
|
||||
${h.age_component(c.comment_history.created_on)}
|
||||
${c.comment_history.text}
|
||||
${c.comment_history.version}
|
||||
Loading…
Add table
Add a link
Reference in a new issue