diff --git a/rhodecode/api/tests/test_api.py b/rhodecode/api/tests/test_api.py --- a/rhodecode/api/tests/test_api.py +++ b/rhodecode/api/tests/test_api.py @@ -87,8 +87,9 @@ class TestApi(object): id_, params = build_data(self.apikey, 'comment', args='xx') response = api_call(self.app, params) expected = 'No such method: comment. ' \ - 'Similar methods: changeset_comment, comment_pull_request, edit_comment, ' \ - 'get_pull_request_comments, comment_commit, get_repo_comments' + 'Similar methods: changeset_comment, comment_pull_request, ' \ + 'get_pull_request_comments, comment_commit, edit_comment, ' \ + 'get_comment, get_repo_comments' assert_error(id_, expected, given=response.body) def test_api_disabled_user(self, request): diff --git a/rhodecode/api/tests/test_get_method.py b/rhodecode/api/tests/test_get_method.py --- a/rhodecode/api/tests/test_get_method.py +++ b/rhodecode/api/tests/test_get_method.py @@ -37,8 +37,10 @@ class TestGetMethod(object): id_, params = build_data(self.apikey, 'get_method', pattern='*comment*') response = api_call(self.app, params) - expected = ['changeset_comment', 'comment_pull_request', 'edit_comment', - 'get_pull_request_comments', 'comment_commit', 'get_repo_comments'] + expected = [ + '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) def test_get_methods_on_single_match(self): diff --git a/rhodecode/api/tests/test_get_pull_request_comments.py b/rhodecode/api/tests/test_get_pull_request_comments.py --- a/rhodecode/api/tests/test_get_pull_request_comments.py +++ b/rhodecode/api/tests/test_get_pull_request_comments.py @@ -61,6 +61,7 @@ class TestGetPullRequestComments(object) 'comment_type': 'note', 'comment_resolved_by': None, 'pull_request_version': None, + 'comment_last_version': 0, 'comment_commit_id': None, 'comment_pull_request_id': pull_request.pull_request_id } diff --git a/rhodecode/api/tests/test_get_repo_comments.py b/rhodecode/api/tests/test_get_repo_comments.py --- a/rhodecode/api/tests/test_get_repo_comments.py +++ b/rhodecode/api/tests/test_get_repo_comments.py @@ -42,26 +42,27 @@ def make_repo_comments_factory(request): comments = [] # general - CommentsModel().create( + comment = CommentsModel().create( text='General Comment', repo=repo, user=user, commit_id=commit_id, comment_type=ChangesetComment.COMMENT_TYPE_NOTE, send_email=False) + comments.append(comment) # inline - CommentsModel().create( + comment = CommentsModel().create( text='Inline Comment', repo=repo, user=user, commit_id=commit_id, f_path=file_0, line_no='n1', comment_type=ChangesetComment.COMMENT_TYPE_NOTE, send_email=False) + comments.append(comment) # todo - CommentsModel().create( + comment = CommentsModel().create( text='INLINE TODO Comment', repo=repo, user=user, commit_id=commit_id, f_path=file_0, line_no='n1', comment_type=ChangesetComment.COMMENT_TYPE_TODO, send_email=False) + comments.append(comment) - @request.addfinalizer - def cleanup(): - for comment in comments: - Session().delete(comment) + return comments + return Make() @@ -108,3 +109,34 @@ class TestGetRepo(object): id_, params = build_data(self.apikey, 'get_repo_comments', **api_call_params) response = api_call(self.app, params) 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) diff --git a/rhodecode/api/views/pull_request_api.py b/rhodecode/api/views/pull_request_api.py --- a/rhodecode/api/views/pull_request_api.py +++ b/rhodecode/api/views/pull_request_api.py @@ -27,7 +27,6 @@ from rhodecode.api.utils import ( get_pull_request_or_error, get_commit_or_error, get_user_or_error, validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions) from rhodecode.lib.auth import (HasRepoPermissionAnyApi) -from rhodecode.lib.exceptions import CommentVersionMismatch from rhodecode.lib.base import vcs_operation_context from rhodecode.lib.utils2 import str2bool from rhodecode.model.changeset_status import ChangesetStatusModel @@ -36,8 +35,7 @@ from rhodecode.model.db import Session, from rhodecode.model.pull_request import PullRequestModel, MergeCheck from rhodecode.model.settings import SettingsModel from rhodecode.model.validation_schema import Invalid -from rhodecode.model.validation_schema.schemas.reviewer_schema import( - ReviewerListSchema) +from rhodecode.model.validation_schema.schemas.reviewer_schema import ReviewerListSchema log = logging.getLogger(__name__) @@ -380,6 +378,7 @@ def get_pull_request_comments( }, "comment_text": "Example text", "comment_type": null, + "comment_last_version: 0, "pull_request_version": null, "comment_commit_id": None, "comment_pull_request_id": @@ -633,79 +632,6 @@ def comment_pull_request( @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 : - result : { - "comment_history_id": "", - "version": "", - }, - 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() def create_pull_request( request, apiuser, source_repo, target_repo, source_ref, target_ref, owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''), diff --git a/rhodecode/api/views/repo_api.py b/rhodecode/api/views/repo_api.py --- a/rhodecode/api/views/repo_api.py +++ b/rhodecode/api/views/repo_api.py @@ -31,11 +31,15 @@ from rhodecode.api.utils import ( validate_set_owner_permissions) from rhodecode.lib import audit_logger, rc_cache 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.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.exceptions import StatusChangeOnClosedPullRequestError +from rhodecode.lib.exceptions import ( + StatusChangeOnClosedPullRequestError, CommentVersionMismatch) from rhodecode.lib.vcs import RepositoryError from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError from rhodecode.model.changeset_status import ChangesetStatusModel @@ -1719,7 +1723,8 @@ def get_repo_comments(request, apiuser, "comment_resolved_by": null, "comment_status": [], "comment_text": "This file needs a header", - "comment_type": "todo" + "comment_type": "todo", + "comment_last_version: 0 } ], "error" : null @@ -1752,6 +1757,143 @@ def get_repo_comments(request, apiuser, @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" : , + "result" : { + "comment_author": , + "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 : + result : { + "comment": "", + "version": "", + }, + 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() def grant_user_permission(request, apiuser, repoid, userid, perm): """ Grant permissions for the specified user on the given repository, diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -3845,6 +3845,13 @@ class ChangesetComment(Base, BaseModel): def is_inline(self): 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): return self.get_index_from_version( self.pull_request_version_id, versions) @@ -3857,6 +3864,7 @@ class ChangesetComment(Base, BaseModel): def get_api_data(self): comment = self + data = { 'comment_id': comment.comment_id, 'comment_type': comment.comment_type, @@ -3869,6 +3877,7 @@ class ChangesetComment(Base, BaseModel): 'comment_resolved_by': self.resolved, 'comment_commit_id': comment.revision, 'comment_pull_request_id': comment.pull_request_id, + 'comment_last_version': self.last_version } return data