api: added get_comment method, and return versions for comments to allow simple edits via API.

This commit is contained in:
Marcin Kuzminski 2020-07-10 11:49:52 +02:00
parent 97a12ce1b9
commit 9cadf302eb
7 changed files with 204 additions and 91 deletions

View file

@ -87,8 +87,9 @@ class TestApi(object):
id_, params = build_data(self.apikey, 'comment', args='xx') id_, params = build_data(self.apikey, 'comment', args='xx')
response = api_call(self.app, params) response = api_call(self.app, params)
expected = 'No such method: comment. ' \ expected = 'No such method: comment. ' \
'Similar methods: changeset_comment, comment_pull_request, edit_comment, ' \ 'Similar methods: changeset_comment, comment_pull_request, ' \
'get_pull_request_comments, comment_commit, get_repo_comments' 'get_pull_request_comments, comment_commit, edit_comment, ' \
'get_comment, get_repo_comments'
assert_error(id_, expected, given=response.body) assert_error(id_, expected, given=response.body)
def test_api_disabled_user(self, request): def test_api_disabled_user(self, request):

View file

@ -37,8 +37,10 @@ class TestGetMethod(object):
id_, params = build_data(self.apikey, 'get_method', pattern='*comment*') id_, params = build_data(self.apikey, 'get_method', pattern='*comment*')
response = api_call(self.app, params) response = api_call(self.app, params)
expected = ['changeset_comment', 'comment_pull_request', 'edit_comment', expected = [
'get_pull_request_comments', 'comment_commit', 'get_repo_comments'] 'changeset_comment', 'comment_pull_request', 'get_pull_request_comments',
'comment_commit', 'edit_comment', 'get_comment', 'get_repo_comments'
]
assert_ok(id_, expected, given=response.body) assert_ok(id_, expected, given=response.body)
def test_get_methods_on_single_match(self): def test_get_methods_on_single_match(self):

View file

@ -61,6 +61,7 @@ class TestGetPullRequestComments(object):
'comment_type': 'note', 'comment_type': 'note',
'comment_resolved_by': None, 'comment_resolved_by': None,
'pull_request_version': None, 'pull_request_version': None,
'comment_last_version': 0,
'comment_commit_id': None, 'comment_commit_id': None,
'comment_pull_request_id': pull_request.pull_request_id 'comment_pull_request_id': pull_request.pull_request_id
} }

View file

@ -42,26 +42,27 @@ def make_repo_comments_factory(request):
comments = [] comments = []
# general # general
CommentsModel().create( comment = CommentsModel().create(
text='General Comment', repo=repo, user=user, commit_id=commit_id, text='General Comment', repo=repo, user=user, commit_id=commit_id,
comment_type=ChangesetComment.COMMENT_TYPE_NOTE, send_email=False) comment_type=ChangesetComment.COMMENT_TYPE_NOTE, send_email=False)
comments.append(comment)
# inline # inline
CommentsModel().create( comment = CommentsModel().create(
text='Inline Comment', repo=repo, user=user, commit_id=commit_id, text='Inline Comment', repo=repo, user=user, commit_id=commit_id,
f_path=file_0, line_no='n1', f_path=file_0, line_no='n1',
comment_type=ChangesetComment.COMMENT_TYPE_NOTE, send_email=False) comment_type=ChangesetComment.COMMENT_TYPE_NOTE, send_email=False)
comments.append(comment)
# todo # todo
CommentsModel().create( comment = CommentsModel().create(
text='INLINE TODO Comment', repo=repo, user=user, commit_id=commit_id, text='INLINE TODO Comment', repo=repo, user=user, commit_id=commit_id,
f_path=file_0, line_no='n1', f_path=file_0, line_no='n1',
comment_type=ChangesetComment.COMMENT_TYPE_TODO, send_email=False) comment_type=ChangesetComment.COMMENT_TYPE_TODO, send_email=False)
comments.append(comment)
return comments
@request.addfinalizer
def cleanup():
for comment in comments:
Session().delete(comment)
return Make() return Make()
@ -108,3 +109,34 @@ class TestGetRepo(object):
id_, params = build_data(self.apikey, 'get_repo_comments', **api_call_params) id_, params = build_data(self.apikey, 'get_repo_comments', **api_call_params)
response = api_call(self.app, params) response = api_call(self.app, params)
assert_error(id_, expected, given=response.body) assert_error(id_, expected, given=response.body)
def test_api_get_comment(self, make_repo_comments_factory, backend_hg):
commits = [{'message': 'A'}, {'message': 'B'}]
repo = backend_hg.create_repo(commits=commits)
comments = make_repo_comments_factory.make_comments(repo)
comment_ids = [x.comment_id for x in comments]
Session().commit()
for comment_id in comment_ids:
id_, params = build_data(self.apikey, 'get_comment',
**{'comment_id': comment_id})
response = api_call(self.app, params)
result = assert_call_ok(id_, given=response.body)
assert result['comment_id'] == comment_id
def test_api_get_comment_no_access(self, make_repo_comments_factory, backend_hg, user_util):
commits = [{'message': 'A'}, {'message': 'B'}]
repo = backend_hg.create_repo(commits=commits)
comments = make_repo_comments_factory.make_comments(repo)
comment_id = comments[0].comment_id
test_user = user_util.create_user()
user_util.grant_user_permission_to_repo(repo, test_user, 'repository.none')
id_, params = build_data(test_user.api_key, 'get_comment',
**{'comment_id': comment_id})
response = api_call(self.app, params)
assert_error(id_,
expected='comment `{}` does not exist'.format(comment_id),
given=response.body)

View file

@ -27,7 +27,6 @@ from rhodecode.api.utils import (
get_pull_request_or_error, get_commit_or_error, get_user_or_error, get_pull_request_or_error, get_commit_or_error, get_user_or_error,
validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions) validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
from rhodecode.lib.auth import (HasRepoPermissionAnyApi) from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
from rhodecode.lib.exceptions import CommentVersionMismatch
from rhodecode.lib.base import vcs_operation_context from rhodecode.lib.base import vcs_operation_context
from rhodecode.lib.utils2 import str2bool from rhodecode.lib.utils2 import str2bool
from rhodecode.model.changeset_status import ChangesetStatusModel from rhodecode.model.changeset_status import ChangesetStatusModel
@ -36,8 +35,7 @@ from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullR
from rhodecode.model.pull_request import PullRequestModel, MergeCheck from rhodecode.model.pull_request import PullRequestModel, MergeCheck
from rhodecode.model.settings import SettingsModel from rhodecode.model.settings import SettingsModel
from rhodecode.model.validation_schema import Invalid from rhodecode.model.validation_schema import Invalid
from rhodecode.model.validation_schema.schemas.reviewer_schema import( from rhodecode.model.validation_schema.schemas.reviewer_schema import ReviewerListSchema
ReviewerListSchema)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -380,6 +378,7 @@ def get_pull_request_comments(
}, },
"comment_text": "Example text", "comment_text": "Example text",
"comment_type": null, "comment_type": null,
"comment_last_version: 0,
"pull_request_version": null, "pull_request_version": null,
"comment_commit_id": None, "comment_commit_id": None,
"comment_pull_request_id": <pull_request_id> "comment_pull_request_id": <pull_request_id>
@ -632,79 +631,6 @@ def comment_pull_request(
return data return data
@jsonrpc_method()
def edit_comment(
request, apiuser, message, comment_id, version,
userid=Optional(OAttr('apiuser')),
):
"""
Edit comment on the pull request or commit,
specified by the `comment_id` and version. Initially version should be 0
:param apiuser: This is filled automatically from the |authtoken|.
:type apiuser: AuthUser
:param comment_id: Specify the comment_id for editing
:type comment_id: int
:param version: version of the comment that will be created, starts from 0
:type version: int
:param message: The text content of the comment.
:type message: str
:param userid: Comment on the pull request as this user
:type userid: Optional(str or int)
Example output:
.. code-block:: bash
id : <id_given_in_input>
result : {
"comment_history_id": "<Integer>",
"version": "<Integer>",
},
error : null
"""
auth_user = apiuser
comment = ChangesetComment.get(comment_id)
is_super_admin = has_superadmin_permission(apiuser)
is_repo_admin = HasRepoPermissionAnyApi('repository.admin') \
(user=apiuser, repo_name=comment.repo.repo_name)
if not isinstance(userid, Optional):
if is_super_admin or is_repo_admin:
apiuser = get_user_or_error(userid)
auth_user = apiuser.AuthUser()
else:
raise JSONRPCError('userid is not the same as your user')
comment_author = comment.author.user_id == auth_user.user_id
if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
raise JSONRPCError("you don't have access to edit this comment")
try:
comment_history = CommentsModel().edit(
comment_id=comment_id,
text=message,
auth_user=auth_user,
version=version,
)
Session().commit()
except CommentVersionMismatch:
raise JSONRPCError(
'comment ({}) version ({}) mismatch'.format(comment_id, version)
)
if not comment_history and not message:
raise JSONRPCError(
"comment ({}) can't be changed with empty string".format(comment_id)
)
data = {
'comment_history_id': comment_history.comment_history_id if comment_history else None,
'version': comment_history.version if comment_history else None,
}
return data
@jsonrpc_method() @jsonrpc_method()
def create_pull_request( def create_pull_request(
request, apiuser, source_repo, target_repo, source_ref, target_ref, request, apiuser, source_repo, target_repo, source_ref, target_ref,

View file

@ -31,11 +31,15 @@ from rhodecode.api.utils import (
validate_set_owner_permissions) validate_set_owner_permissions)
from rhodecode.lib import audit_logger, rc_cache from rhodecode.lib import audit_logger, rc_cache
from rhodecode.lib import repo_maintenance from rhodecode.lib import repo_maintenance
from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi from rhodecode.lib.auth import (
HasPermissionAnyApi, HasUserGroupPermissionAnyApi,
HasRepoPermissionAnyApi)
from rhodecode.lib.celerylib.utils import get_task_id from rhodecode.lib.celerylib.utils import get_task_id
from rhodecode.lib.utils2 import str2bool, time_to_datetime, safe_str, safe_int, safe_unicode from rhodecode.lib.utils2 import (
str2bool, time_to_datetime, safe_str, safe_int, safe_unicode)
from rhodecode.lib.ext_json import json from rhodecode.lib.ext_json import json
from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError from rhodecode.lib.exceptions import (
StatusChangeOnClosedPullRequestError, CommentVersionMismatch)
from rhodecode.lib.vcs import RepositoryError from rhodecode.lib.vcs import RepositoryError
from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
from rhodecode.model.changeset_status import ChangesetStatusModel from rhodecode.model.changeset_status import ChangesetStatusModel
@ -1719,7 +1723,8 @@ def get_repo_comments(request, apiuser, repoid,
"comment_resolved_by": null, "comment_resolved_by": null,
"comment_status": [], "comment_status": [],
"comment_text": "This file needs a header", "comment_text": "This file needs a header",
"comment_type": "todo" "comment_type": "todo",
"comment_last_version: 0
} }
], ],
"error" : null "error" : null
@ -1751,6 +1756,143 @@ def get_repo_comments(request, apiuser, repoid,
return comments return comments
@jsonrpc_method()
def get_comment(request, apiuser, comment_id):
"""
Get single comment from repository or pull_request
:param apiuser: This is filled automatically from the |authtoken|.
:type apiuser: AuthUser
:param comment_id: comment id found in the URL of comment
:type comment_id: str or int
Example error output:
.. code-block:: bash
{
"id" : <id_given_in_input>,
"result" : {
"comment_author": <USER_DETAILS>,
"comment_created_on": "2017-02-01T14:38:16.309",
"comment_f_path": "file.txt",
"comment_id": 282,
"comment_lineno": "n1",
"comment_resolved_by": null,
"comment_status": [],
"comment_text": "This file needs a header",
"comment_type": "todo",
"comment_last_version: 0
},
"error" : null
}
"""
comment = ChangesetComment.get(comment_id)
if not comment:
raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
perms = ('repository.read', 'repository.write', 'repository.admin')
has_comment_perm = HasRepoPermissionAnyApi(*perms)\
(user=apiuser, repo_name=comment.repo.repo_name)
if not has_comment_perm:
raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
return comment
@jsonrpc_method()
def edit_comment(request, apiuser, message, comment_id, version,
userid=Optional(OAttr('apiuser'))):
"""
Edit comment on the pull request or commit,
specified by the `comment_id` and version. Initially version should be 0
:param apiuser: This is filled automatically from the |authtoken|.
:type apiuser: AuthUser
:param comment_id: Specify the comment_id for editing
:type comment_id: int
:param version: version of the comment that will be created, starts from 0
:type version: int
:param message: The text content of the comment.
:type message: str
:param userid: Comment on the pull request as this user
:type userid: Optional(str or int)
Example output:
.. code-block:: bash
id : <id_given_in_input>
result : {
"comment": "<comment data>",
"version": "<Integer>",
},
error : null
"""
auth_user = apiuser
comment = ChangesetComment.get(comment_id)
if not comment:
raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
is_super_admin = has_superadmin_permission(apiuser)
is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
(user=apiuser, repo_name=comment.repo.repo_name)
if not isinstance(userid, Optional):
if is_super_admin or is_repo_admin:
apiuser = get_user_or_error(userid)
auth_user = apiuser.AuthUser()
else:
raise JSONRPCError('userid is not the same as your user')
comment_author = comment.author.user_id == auth_user.user_id
if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
raise JSONRPCError("you don't have access to edit this comment")
try:
comment_history = CommentsModel().edit(
comment_id=comment_id,
text=message,
auth_user=auth_user,
version=version,
)
Session().commit()
except CommentVersionMismatch:
raise JSONRPCError(
'comment ({}) version ({}) mismatch'.format(comment_id, version)
)
if not comment_history and not message:
raise JSONRPCError(
"comment ({}) can't be changed with empty string".format(comment_id)
)
data = {
'comment': comment,
'version': comment_history.version if comment_history else None,
}
return data
# TODO(marcink): write this with all required logic for deleting a comments in PR or commits
# @jsonrpc_method()
# def delete_comment(request, apiuser, comment_id):
# auth_user = apiuser
#
# comment = ChangesetComment.get(comment_id)
# if not comment:
# raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
#
# is_super_admin = has_superadmin_permission(apiuser)
# is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
# (user=apiuser, repo_name=comment.repo.repo_name)
#
# comment_author = comment.author.user_id == auth_user.user_id
# if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
# raise JSONRPCError("you don't have access to edit this comment")
@jsonrpc_method() @jsonrpc_method()
def grant_user_permission(request, apiuser, repoid, userid, perm): def grant_user_permission(request, apiuser, repoid, userid, perm):
""" """

View file

@ -3845,6 +3845,13 @@ class ChangesetComment(Base, BaseModel):
def is_inline(self): def is_inline(self):
return self.line_no and self.f_path return self.line_no and self.f_path
@property
def last_version(self):
version = 0
if self.history:
version = self.history[-1].version
return version
def get_index_version(self, versions): def get_index_version(self, versions):
return self.get_index_from_version( return self.get_index_from_version(
self.pull_request_version_id, versions) self.pull_request_version_id, versions)
@ -3857,6 +3864,7 @@ class ChangesetComment(Base, BaseModel):
def get_api_data(self): def get_api_data(self):
comment = self comment = self
data = { data = {
'comment_id': comment.comment_id, 'comment_id': comment.comment_id,
'comment_type': comment.comment_type, 'comment_type': comment.comment_type,
@ -3869,6 +3877,7 @@ class ChangesetComment(Base, BaseModel):
'comment_resolved_by': self.resolved, 'comment_resolved_by': self.resolved,
'comment_commit_id': comment.revision, 'comment_commit_id': comment.revision,
'comment_pull_request_id': comment.pull_request_id, 'comment_pull_request_id': comment.pull_request_id,
'comment_last_version': self.last_version
} }
return data return data