release: Merge default into stable for release preparation

This commit is contained in:
Marcin Kuzminski 2020-07-20 11:39:21 +00:00
commit 80c83f30f0
128 changed files with 9336 additions and 448 deletions

View file

@ -1,6 +1,5 @@
[bumpversion]
current_version = 4.19.3
current_version = 4.20.0
message = release: Bump version {current_version} to {new_version}
[bumpversion:file:rhodecode/VERSION]

View file

@ -5,26 +5,21 @@ done = false
done = true
[task:rc_tools_pinned]
done = true
[task:fixes_on_stable]
done = true
[task:pip2nix_generated]
done = true
[task:changelog_updated]
done = true
[task:generate_api_docs]
done = true
[release]
state = prepared
version = 4.19.3
[task:updated_translation]
[release]
state = in_progress
version = 4.20.0
[task:generate_js_routes]
[task:updated_trial_license]

View file

@ -238,7 +238,7 @@ following URL: ``{instance-URL}/_admin/ping``.
pong[rce-7880] => 203.0.113.23
.. _Markdown: http://daringfireball.net/projects/markdown/
.. _reStructured Text: http://docutils.sourceforge.net/docs/index.html
.. _reStructured Text: http://docutils.sourceforge.io/docs/index.html
Unarchiving a repository

View file

@ -75,7 +75,7 @@ Below config if for an Apache Reverse Proxy configuration.
# Url to running RhodeCode instance. This is shown as `- URL:` when
# running rccontrol status.
ProxyPass / http://127.0.0.1:10002/ timeout=7200 Keepalive=On
ProxyPass / http://127.0.0.1:10002/ connectiontimeout=7200 timeout=7200 Keepalive=On
ProxyPassReverse / http://127.0.0.1:10002/
# strict http prevents from https -> http downgrade

View file

@ -252,6 +252,7 @@ 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": <pull_request_id>

View file

@ -173,6 +173,37 @@ delete_repo
error: null
edit_comment
------------
.. py:function:: edit_comment(apiuser, message, comment_id, version, userid=<Optional:<OptionalAttr: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
fork_repo
---------
@ -236,6 +267,40 @@ fork_repo
error: null
get_comment
-----------
.. py:function:: get_comment(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
}
get_repo
--------
@ -436,7 +501,8 @@ get_repo_comments
"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

View file

@ -11,20 +11,35 @@ use the below example to insert it.
Once configured you can check the settings for your |RCE| instance on the
:menuselection:`Admin --> Settings --> Email` page.
Please be aware that both section should be changed the `[DEFAULT]` for main applications
email config, and `[server:main]` for exception tracking email
.. code-block:: ini
################################################################################
## Uncomment and replace with the email address which should receive ##
## any error reports after an application crash ##
## Additionally these settings will be used by the RhodeCode mailing system ##
################################################################################
#email_to = admin@localhost
#app_email_from = rhodecode-noreply@localhost
[DEFAULT]
; ########################################################################
; EMAIL CONFIGURATION
; These settings will be used by the RhodeCode mailing system
; ########################################################################
; prefix all emails subjects with given prefix, helps filtering out emails
#email_prefix = [RhodeCode]
; email FROM address all mails will be sent
#app_email_from = rhodecode-noreply@localhost
#smtp_server = mail.server.com
#smtp_username =
#smtp_password =
#smtp_port =
#smtp_use_tls = false
#smtp_use_ssl = true
[server:main]
; Send email with exception details when it happens
#exception_tracker.send_email = true
; Comma separated list of recipients for exception emails,
; e.g admin@rhodecode.com,devops@rhodecode.com
; Can be left empty, then emails will be sent to ALL super-admins
#exception_tracker.send_email_recipients =

View file

@ -0,0 +1,74 @@
|RCE| 4.20.0 |RNS|
------------------
Release Date
^^^^^^^^^^^^
- 2020-07-20
New Features
^^^^^^^^^^^^
- Comments: users can now edit comments body.
Editing is versioned and all older versions are kept for auditing.
- Pull requests: changed the order of close-branch after merge,
so branch heads are no longer left open after the merge.
- Diffs: added diff navigation to improve UX when browsing the full context diffs.
- Emails: set the `References` header for threading in emails with different subjects.
Only some Email clients supports this.
- Emails: added logic to allow overwriting the default email titles via rcextensions.
- Markdown: support summary/details tags to allow setting a link with expansion menu.
- Integrations: added `store_file` integration. This allows storing
selected files from repository on disk on push.
General
^^^^^^^
- License: individual users can hide license flash messages warning about upcoming
license expiration.
- Downloads: the default download commit is now the landing revision set in repo settings.
- Auth-tokens: expose all roles with explanation to help users understand it better.
- Pull requests: make auto generated title for pull requests show also source Ref type
eg. branch feature1, instead of just name of the branch.
- UI: added secondary action instead of two buttons on files page, and download page.
- Emails: reduce excessive warning logs on pre-mailer.
Security
^^^^^^^^
- Branch permissions: protect from XSS on branch rules forbidden flash message.
Performance
^^^^^^^^^^^
Fixes
^^^^^
- Pull requests: detect missing commits on diffs from new PR ancestor logic. This fixes
problem with older PRs opened before 4.19.X that had special ancestor set, which could
lead in some cases to crash when viewing older pull requests.
- Permissions: fixed a case when a duplicate permission made repository settings active on archived repository.
- Permissions: fixed missing user info on global and repository permissions pages.
- Permissions: allow users to update settings for repository groups they still own,
or have admin perms, when they don't change their name.
- Permissions: flush all when running remap and rescan.
- Repositories: fixed a bug for repo groups that didn't pre-fill the repo group from GET param.
- Repositories: allow updating repository settings for users without
store-in-root permissions in case repository name didn't change.
- Comments: fixed line display icons.
- Summary: fixed summary page total commits count.
Upgrade notes
^^^^^^^^^^^^^
- Schedule feature update.
- On Mercurial repositories we changed the order of commits when the close branch on merge features is used.
Before the commits was made after a merge leaving an open head.
This backward incompatible change now reverses that order, which is the correct way of doing it.

View file

@ -9,6 +9,7 @@ Release Notes
.. toctree::
:maxdepth: 1
release-notes-4.20.0.rst
release-notes-4.19.3.rst
release-notes-4.19.2.rst
release-notes-4.19.1.rst

View file

@ -51,9 +51,12 @@
"<%= dirs.js.src %>/plugins/jquery.pjax.js",
"<%= dirs.js.src %>/plugins/jquery.dataTables.js",
"<%= dirs.js.src %>/plugins/flavoured_checkbox.js",
"<%= dirs.js.src %>/plugins/within_viewport.js",
"<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js",
"<%= dirs.js.src %>/plugins/jquery.autocomplete.js",
"<%= dirs.js.src %>/plugins/jquery.debounce.js",
"<%= dirs.js.src %>/plugins/jquery.scrollstop.js",
"<%= dirs.js.src %>/plugins/jquery.within-viewport.js",
"<%= dirs.js.node_modules %>/mark.js/dist/jquery.mark.min.js",
"<%= dirs.js.src %>/plugins/jquery.timeago.js",
"<%= dirs.js.src %>/plugins/jquery.timeago-extension.js",

View file

@ -1819,7 +1819,7 @@ self: super: {
};
};
"rhodecode-enterprise-ce" = super.buildPythonPackage {
name = "rhodecode-enterprise-ce-4.19.3";
name = "rhodecode-enterprise-ce-4.20.0";
buildInputs = [
self."pytest"
self."py"

View file

@ -10,6 +10,8 @@ vcsserver_config_http = rhodecode/tests/vcsserver_http.ini
addopts =
--pdbcls=IPython.terminal.debugger:TerminalPdb
--strict-markers
--capture=no
--show-capture=no
markers =
vcs_operations: Mark tests depending on a running RhodeCode instance.

View file

@ -1 +1 @@
4.19.3
4.20.0

View file

@ -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'

View file

@ -88,7 +88,8 @@ class TestApi(object):
response = api_call(self.app, params)
expected = 'No such method: 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)
def test_api_disabled_user(self, request):

View file

@ -21,7 +21,7 @@
import pytest
from rhodecode.model.comment import CommentsModel
from rhodecode.model.db import UserLog, User
from rhodecode.model.db import UserLog, User, ChangesetComment
from rhodecode.model.pull_request import PullRequestModel
from rhodecode.tests import TEST_USER_ADMIN_LOGIN
from rhodecode.api.tests.utils import (
@ -218,8 +218,20 @@ class TestCommentPullRequest(object):
assert_error(id_, expected, given=response.body)
@pytest.mark.backends("git", "hg")
def test_api_comment_pull_request_non_admin_with_userid_error(
self, pr_util):
def test_api_comment_pull_request_non_admin_with_userid_error(self, pr_util):
pull_request = pr_util.create_pull_request()
id_, params = build_data(
self.apikey_regular, 'comment_pull_request',
repoid=pull_request.target_repo.repo_name,
pullrequestid=pull_request.pull_request_id,
userid=TEST_USER_ADMIN_LOGIN)
response = api_call(self.app, params)
expected = 'userid is not the same as your user'
assert_error(id_, expected, given=response.body)
@pytest.mark.backends("git", "hg")
def test_api_comment_pull_request_non_admin_with_userid_error(self, pr_util):
pull_request = pr_util.create_pull_request()
id_, params = build_data(
self.apikey_regular, 'comment_pull_request',
@ -244,3 +256,135 @@ class TestCommentPullRequest(object):
expected = 'Invalid commit_id `XXX` for this pull request.'
assert_error(id_, expected, given=response.body)
@pytest.mark.backends("git", "hg")
def test_api_edit_comment(self, pr_util):
pull_request = pr_util.create_pull_request()
id_, params = build_data(
self.apikey,
'comment_pull_request',
repoid=pull_request.target_repo.repo_name,
pullrequestid=pull_request.pull_request_id,
message='test message',
)
response = api_call(self.app, params)
json_response = response.json
comment_id = json_response['result']['comment_id']
message_after_edit = 'just message'
id_, params = build_data(
self.apikey,
'edit_comment',
comment_id=comment_id,
message=message_after_edit,
version=0,
)
response = api_call(self.app, params)
json_response = response.json
assert json_response['result']['version'] == 1
text_form_db = ChangesetComment.get(comment_id).text
assert message_after_edit == text_form_db
@pytest.mark.backends("git", "hg")
def test_api_edit_comment_wrong_version(self, pr_util):
pull_request = pr_util.create_pull_request()
id_, params = build_data(
self.apikey, 'comment_pull_request',
repoid=pull_request.target_repo.repo_name,
pullrequestid=pull_request.pull_request_id,
message='test message')
response = api_call(self.app, params)
json_response = response.json
comment_id = json_response['result']['comment_id']
message_after_edit = 'just message'
id_, params = build_data(
self.apikey_regular,
'edit_comment',
comment_id=comment_id,
message=message_after_edit,
version=1,
)
response = api_call(self.app, params)
expected = 'comment ({}) version ({}) mismatch'.format(comment_id, 1)
assert_error(id_, expected, given=response.body)
@pytest.mark.backends("git", "hg")
def test_api_edit_comment_wrong_version(self, pr_util):
pull_request = pr_util.create_pull_request()
id_, params = build_data(
self.apikey, 'comment_pull_request',
repoid=pull_request.target_repo.repo_name,
pullrequestid=pull_request.pull_request_id,
message='test message')
response = api_call(self.app, params)
json_response = response.json
comment_id = json_response['result']['comment_id']
id_, params = build_data(
self.apikey,
'edit_comment',
comment_id=comment_id,
message='',
version=0,
)
response = api_call(self.app, params)
expected = "comment ({}) can't be changed with empty string".format(comment_id, 1)
assert_error(id_, expected, given=response.body)
@pytest.mark.backends("git", "hg")
def test_api_edit_comment_wrong_user_set_by_non_admin(self, pr_util):
pull_request = pr_util.create_pull_request()
pull_request_id = pull_request.pull_request_id
id_, params = build_data(
self.apikey,
'comment_pull_request',
repoid=pull_request.target_repo.repo_name,
pullrequestid=pull_request_id,
message='test message'
)
response = api_call(self.app, params)
json_response = response.json
comment_id = json_response['result']['comment_id']
id_, params = build_data(
self.apikey_regular,
'edit_comment',
comment_id=comment_id,
message='just message',
version=0,
userid=TEST_USER_ADMIN_LOGIN
)
response = api_call(self.app, params)
expected = 'userid is not the same as your user'
assert_error(id_, expected, given=response.body)
@pytest.mark.backends("git", "hg")
def test_api_edit_comment_wrong_user_with_permissions_to_edit_comment(self, pr_util):
pull_request = pr_util.create_pull_request()
pull_request_id = pull_request.pull_request_id
id_, params = build_data(
self.apikey,
'comment_pull_request',
repoid=pull_request.target_repo.repo_name,
pullrequestid=pull_request_id,
message='test message'
)
response = api_call(self.app, params)
json_response = response.json
comment_id = json_response['result']['comment_id']
id_, params = build_data(
self.apikey_regular,
'edit_comment',
comment_id=comment_id,
message='just message',
version=0,
)
response = api_call(self.app, params)
expected = "you don't have access to edit this comment"
assert_error(id_, expected, given=response.body)

View file

@ -233,8 +233,8 @@ class TestCreateRepoGroup(object):
expected = {
'repo_group':
'Parent repository group `{}` does not exist'.format(
repo_group_name)}
u"You do not have the permissions to store "
u"repository groups inside repository group `{}`".format(repo_group_name)}
try:
assert_error(id_, expected, given=response.body)
finally:

View file

@ -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',
'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):

View file

@ -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
}

View file

@ -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)
return comments
@request.addfinalizer
def cleanup():
for comment in comments:
Session().delete(comment)
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)

View file

@ -21,7 +21,6 @@
import logging
from rhodecode import events
from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
from rhodecode.api.utils import (
has_superadmin_permission, Optional, OAttr, get_repo_or_error,
@ -36,8 +35,7 @@ from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullR
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__)
@ -292,10 +290,11 @@ def merge_pull_request(
else:
repo = pull_request.target_repo
auth_user = apiuser
if not isinstance(userid, Optional):
if (has_superadmin_permission(apiuser) or
HasRepoPermissionAnyApi('repository.admin')(
user=apiuser, repo_name=repo.repo_name)):
is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
user=apiuser, repo_name=repo.repo_name)
if has_superadmin_permission(apiuser) or is_repo_admin:
apiuser = get_user_or_error(userid)
auth_user = apiuser.AuthUser()
else:
@ -379,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": <pull_request_id>
@ -510,9 +510,9 @@ def comment_pull_request(
auth_user = apiuser
if not isinstance(userid, Optional):
if (has_superadmin_permission(apiuser) or
HasRepoPermissionAnyApi('repository.admin')(
user=apiuser, repo_name=repo.repo_name)):
is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
user=apiuser, repo_name=repo.repo_name)
if has_superadmin_permission(apiuser) or is_repo_admin:
apiuser = get_user_or_error(userid)
auth_user = apiuser.AuthUser()
else:
@ -979,10 +979,10 @@ def close_pull_request(
else:
repo = pull_request.target_repo
is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
user=apiuser, repo_name=repo.repo_name)
if not isinstance(userid, Optional):
if (has_superadmin_permission(apiuser) or
HasRepoPermissionAnyApi('repository.admin')(
user=apiuser, repo_name=repo.repo_name)):
if has_superadmin_permission(apiuser) or is_repo_admin:
apiuser = get_user_or_error(userid)
else:
raise JSONRPCError('userid is not the same as your user')

View file

@ -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
@ -44,6 +48,7 @@ from rhodecode.model.db import (
Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
ChangesetComment)
from rhodecode.model.permission import PermissionModel
from rhodecode.model.pull_request import PullRequestModel
from rhodecode.model.repo import RepoModel
from rhodecode.model.scm import ScmModel, RepoList
from rhodecode.model.settings import SettingsModel, VcsSettingsModel
@ -1719,7 +1724,8 @@ def get_repo_comments(request, apiuser, repoid,
"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
@ -1751,6 +1757,157 @@ def get_repo_comments(request, apiuser, repoid,
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)
)
if comment.pull_request:
pull_request = comment.pull_request
PullRequestModel().trigger_pull_request_hook(
pull_request, apiuser, 'comment_edit',
data={'comment': comment})
else:
db_repo = comment.repo
commit_id = comment.revision
commit = db_repo.get_commit(commit_id)
CommentsModel().trigger_commit_comment_hook(
db_repo, apiuser, 'edit',
data={'comment': comment, 'commit': commit})
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):
"""

View file

@ -69,6 +69,7 @@ class AdminRepoGroupsView(BaseAppView, DataGridAppView):
c.repo_groups = RepoGroup.groups_choices(
groups=groups_with_admin_rights,
show_empty_group=allow_empty_group)
c.personal_repo_group = self._rhodecode_user.personal_repo_group
def _can_create_repo_group(self, parent_group_id=None):
is_admin = HasPermissionAny('hg.admin')('group create controller')
@ -261,15 +262,28 @@ class AdminRepoGroupsView(BaseAppView, DataGridAppView):
# perm check for admin, create_group perm or admin of parent_group
parent_group_id = safe_int(self.request.GET.get('parent_group'))
_gr = RepoGroup.get(parent_group_id)
if not self._can_create_repo_group(parent_group_id):
raise HTTPForbidden()
self._load_form_data(c)
defaults = {} # Future proof for default of repo group
parent_group_choice = '-1'
if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
parent_group_choice = self._rhodecode_user.personal_repo_group
if parent_group_id and _gr:
if parent_group_id in [x[0] for x in c.repo_groups]:
parent_group_choice = safe_unicode(parent_group_id)
defaults.update({'group_parent_id': parent_group_choice})
data = render(
'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
self._get_template_context(c), self.request)
html = formencode.htmlfill.render(
data,
defaults=defaults,

View file

@ -169,8 +169,8 @@ class AdminReposView(BaseAppView, DataGridAppView):
c = self.load_default_context()
new_repo = self.request.GET.get('repo', '')
parent_group = safe_int(self.request.GET.get('parent_group'))
_gr = RepoGroup.get(parent_group)
parent_group_id = safe_int(self.request.GET.get('parent_group'))
_gr = RepoGroup.get(parent_group_id)
if not HasPermissionAny('hg.admin', 'hg.create.repository')():
# you're not super admin nor have global create permissions,
@ -196,9 +196,9 @@ class AdminReposView(BaseAppView, DataGridAppView):
if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
parent_group_choice = self._rhodecode_user.personal_repo_group
if parent_group and _gr:
if parent_group in [x[0] for x in c.repo_groups]:
parent_group_choice = safe_unicode(parent_group)
if parent_group_id and _gr:
if parent_group_id in [x[0] for x in c.repo_groups]:
parent_group_choice = safe_unicode(parent_group_id)
defaults.update({'repo_group': parent_group_choice})

View file

@ -47,6 +47,7 @@ from rhodecode.model.db import RhodeCodeUi, Repository
from rhodecode.model.forms import (ApplicationSettingsForm,
ApplicationUiSettingsForm, ApplicationVisualisationForm,
LabsSettingsForm, IssueTrackerPatternsForm)
from rhodecode.model.permission import PermissionModel
from rhodecode.model.repo_group import RepoGroupModel
from rhodecode.model.scm import ScmModel
@ -253,8 +254,7 @@ class AdminSettingsView(BaseAppView):
c.active = 'mapping'
rm_obsolete = self.request.POST.get('destroy', False)
invalidate_cache = self.request.POST.get('invalidate', False)
log.debug(
'rescanning repo location with destroy obsolete=%s', rm_obsolete)
log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
if invalidate_cache:
log.debug('invalidating all repositories cache')
@ -263,6 +263,8 @@ class AdminSettingsView(BaseAppView):
filesystem_repos = ScmModel().repo_scan()
added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
PermissionModel().trigger_permission_flush()
_repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
h.flash(_('Repositories successfully '
'rescanned added: %s ; removed: %s') %
@ -576,8 +578,7 @@ class AdminSettingsView(BaseAppView):
'user': self._rhodecode_db_user
}
(subject, headers, email_body,
email_body_plaintext) = EmailNotificationModel().render_email(
(subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
recipients = [test_email] if test_email else None

View file

@ -376,8 +376,7 @@ users: description edit fixes
}
template_type = email_id.split('+')[0]
(c.subject, c.headers, c.email_body,
c.email_body_plaintext) = EmailNotificationModel().render_email(
(c.subject, c.email_body, c.email_body_plaintext) = EmailNotificationModel().render_email(
template_type, **email_kwargs.get(email_id, {}))
test_email = self.request.GET.get('email')

View file

@ -302,7 +302,7 @@ class TestGistsController(TestController):
assert_response = response.assert_response()
assert_response.element_equals_to(
'div.rc-user span.user',
'<a href="/_profiles/test_admin">test_admin</a></span>')
'<a href="/_profiles/test_admin">test_admin</a>')
response.mustcontain('gist-desc')
@ -328,7 +328,7 @@ class TestGistsController(TestController):
assert_response = response.assert_response()
assert_response.element_equals_to(
'div.rc-user span.user',
'<a href="/_profiles/test_admin">test_admin</a></span>')
'<a href="/_profiles/test_admin">test_admin</a>')
response.mustcontain('gist-desc')
def test_show_as_raw(self, create_gist):

View file

@ -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',

View file

@ -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=409,
)
assert response.status_int == 409
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

View file

@ -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:
@ -114,6 +116,223 @@ class TestPullrequestsView(object):
if range_diff == "1":
response.mustcontain('Turn off: Show the diff as commit range')
def test_show_versions_of_pr(self, backend, csrf_token):
commits = [
{'message': 'initial-commit',
'added': [FileNode('test-file.txt', 'LINE1\n')]},
{'message': 'commit-1',
'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
# Above is the initial version of PR that changes a single line
# from now on we'll add 3x commit adding a nother line on each step
{'message': 'commit-2',
'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
{'message': 'commit-3',
'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
{'message': 'commit-4',
'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
]
commit_ids = backend.create_master_repo(commits)
target = backend.create_repo(heads=['initial-commit'])
source = backend.create_repo(heads=['commit-1'])
source_repo_name = source.repo_name
target_repo_name = target.repo_name
target_ref = 'branch:{branch}:{commit_id}'.format(
branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
source_ref = 'branch:{branch}:{commit_id}'.format(
branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
response = self.app.post(
route_path('pullrequest_create', repo_name=source.repo_name),
[
('source_repo', source.repo_name),
('source_ref', source_ref),
('target_repo', target.repo_name),
('target_ref', target_ref),
('common_ancestor', commit_ids['initial-commit']),
('pullrequest_title', 'Title'),
('pullrequest_desc', 'Description'),
('description_renderer', 'markdown'),
('__start__', 'review_members:sequence'),
('__start__', 'reviewer:mapping'),
('user_id', '1'),
('__start__', 'reasons:sequence'),
('reason', 'Some reason'),
('__end__', 'reasons:sequence'),
('__start__', 'rules:sequence'),
('__end__', 'rules:sequence'),
('mandatory', 'False'),
('__end__', 'reviewer:mapping'),
('__end__', 'review_members:sequence'),
('__start__', 'revisions:sequence'),
('revisions', commit_ids['commit-1']),
('__end__', 'revisions:sequence'),
('user', ''),
('csrf_token', csrf_token),
],
status=302)
location = response.headers['Location']
pull_request_id = location.rsplit('/', 1)[1]
assert pull_request_id != 'new'
pull_request = PullRequest.get(int(pull_request_id))
pull_request_id = pull_request.pull_request_id
# Show initial version of PR
response = self.app.get(
route_path('pullrequest_show',
repo_name=target_repo_name,
pull_request_id=pull_request_id))
response.mustcontain('commit-1')
response.mustcontain(no=['commit-2'])
response.mustcontain(no=['commit-3'])
response.mustcontain(no=['commit-4'])
response.mustcontain('cb-addition"></span><span>LINE2</span>')
response.mustcontain(no=['LINE3'])
response.mustcontain(no=['LINE4'])
response.mustcontain(no=['LINE5'])
# update PR #1
source_repo = Repository.get_by_repo_name(source_repo_name)
backend.pull_heads(source_repo, heads=['commit-2'])
response = self.app.post(
route_path('pullrequest_update',
repo_name=target_repo_name, pull_request_id=pull_request_id),
params={'update_commits': 'true', 'csrf_token': csrf_token})
# update PR #2
source_repo = Repository.get_by_repo_name(source_repo_name)
backend.pull_heads(source_repo, heads=['commit-3'])
response = self.app.post(
route_path('pullrequest_update',
repo_name=target_repo_name, pull_request_id=pull_request_id),
params={'update_commits': 'true', 'csrf_token': csrf_token})
# update PR #3
source_repo = Repository.get_by_repo_name(source_repo_name)
backend.pull_heads(source_repo, heads=['commit-4'])
response = self.app.post(
route_path('pullrequest_update',
repo_name=target_repo_name, pull_request_id=pull_request_id),
params={'update_commits': 'true', 'csrf_token': csrf_token})
# Show final version !
response = self.app.get(
route_path('pullrequest_show',
repo_name=target_repo_name,
pull_request_id=pull_request_id))
# 3 updates, and the latest == 4
response.mustcontain('4 versions available for this pull request')
response.mustcontain(no=['rhodecode diff rendering error'])
# initial show must have 3 commits, and 3 adds
response.mustcontain('commit-1')
response.mustcontain('commit-2')
response.mustcontain('commit-3')
response.mustcontain('commit-4')
response.mustcontain('cb-addition"></span><span>LINE2</span>')
response.mustcontain('cb-addition"></span><span>LINE3</span>')
response.mustcontain('cb-addition"></span><span>LINE4</span>')
response.mustcontain('cb-addition"></span><span>LINE5</span>')
# fetch versions
pr = PullRequest.get(pull_request_id)
versions = [x.pull_request_version_id for x in pr.versions.all()]
assert len(versions) == 3
# show v1,v2,v3,v4
def cb_line(text):
return 'cb-addition"></span><span>{}</span>'.format(text)
def cb_context(text):
return '<span class="cb-code"><span class="cb-action cb-context">' \
'</span><span>{}</span></span>'.format(text)
commit_tests = {
# in response, not in response
1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
}
diff_tests = {
1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
}
for idx, ver in enumerate(versions, 1):
response = self.app.get(
route_path('pullrequest_show',
repo_name=target_repo_name,
pull_request_id=pull_request_id,
params={'version': ver}))
response.mustcontain(no=['rhodecode diff rendering error'])
response.mustcontain('Showing changes at v{}'.format(idx))
yes, no = commit_tests[idx]
for y in yes:
response.mustcontain(y)
for n in no:
response.mustcontain(no=n)
yes, no = diff_tests[idx]
for y in yes:
response.mustcontain(cb_line(y))
for n in no:
response.mustcontain(no=n)
# show diff between versions
diff_compare_tests = {
1: (['LINE3'], ['LINE1', 'LINE2']),
2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
}
for idx, ver in enumerate(versions, 1):
adds, context = diff_compare_tests[idx]
to_ver = ver+1
if idx == 3:
to_ver = 'latest'
response = self.app.get(
route_path('pullrequest_show',
repo_name=target_repo_name,
pull_request_id=pull_request_id,
params={'from_version': versions[0], 'version': to_ver}))
response.mustcontain(no=['rhodecode diff rendering error'])
for a in adds:
response.mustcontain(cb_line(a))
for c in context:
response.mustcontain(cb_context(c))
# test version v2 -> v3
response = self.app.get(
route_path('pullrequest_show',
repo_name=target_repo_name,
pull_request_id=pull_request_id,
params={'from_version': versions[1], 'version': versions[2]}))
response.mustcontain(cb_context('LINE1'))
response.mustcontain(cb_context('LINE2'))
response.mustcontain(cb_context('LINE3'))
response.mustcontain(cb_line('LINE4'))
def test_close_status_visibility(self, pr_util, user_util, csrf_token):
# Logout
response = self.app.post(
@ -338,8 +557,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 +574,222 @@ 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
target_scm = pull_request.target_repo.scm_instance()
target_scm_name = target_scm.name
response = self.app.post(
route_path(
'pullrequest_comment_create',
repo_name=target_scm_name,
pull_request_id=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)
target_scm = pull_request.target_repo.scm_instance()
target_scm_name = target_scm.name
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=target_scm_name,
pull_request_id=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()
target_scm = pull_request.target_repo.scm_instance()
target_scm_name = target_scm.name
response = self.app.post(
route_path(
'pullrequest_comment_create',
repo_name=target_scm_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=target_scm_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()
target_scm = pull_request.target_repo.scm_instance()
target_scm_name = target_scm.name
response = self.app.post(
route_path(
'pullrequest_comment_create',
repo_name=target_scm_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=target_scm_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()
target_scm = pull_request.target_repo.scm_instance()
target_scm_name = target_scm.name
response = self.app.post(
route_path(
'pullrequest_comment_create',
repo_name=target_scm_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=target_scm_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=target_scm_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=409,
)
assert response.status_int == 409
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'},

View file

@ -20,9 +20,9 @@
import logging
import collections
from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden
from pyramid.httpexceptions import (
HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
from pyramid.view import view_config
from pyramid.renderers import render
from pyramid.response import Response
@ -39,13 +39,14 @@ from rhodecode.lib.compat import OrderedDict
from rhodecode.lib.diffs import (
cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
get_diff_whitespace_flag)
from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
import rhodecode.lib.helpers as h
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,34 @@ 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):
c = self.load_default_context()
comment_history_id = self.request.matchdict['comment_history_id']
comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
if is_repo_comment:
c.comment_history = comment_history
rendered_comment = render(
'rhodecode:templates/changeset/comment_history.mako',
self._get_template_context(c)
, self.request)
return rendered_comment
else:
log.warning('No permissions for user %s to show comment_history_id: %s',
self._rhodecode_db_user, comment_history_id)
raise HTTPNotFound()
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
@ -545,7 +574,7 @@ class RepoCommitsView(RepoAppView):
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
is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
comment_repo_admin = is_repo_admin and is_repo_comment
if super_admin or comment_owner or comment_repo_admin:
@ -557,6 +586,90 @@ 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):
self.load_default_context()
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_id == self.db_repo.repo_id
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 '
'with the same comment body {}'.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()
try:
comment_history = CommentsModel().edit(
comment_id=comment_id,
text=text,
auth_user=self._rhodecode_user,
version=version,
)
except CommentVersionMismatch:
raise HTTPConflict()
if not comment_history:
raise HTTPNotFound()
commit_id = self.request.matchdict['commit_id']
commit = self.db_repo.get_commit(commit_id)
CommentsModel().trigger_commit_comment_hook(
self.db_repo, self._rhodecode_user, 'edit',
data={'comment': comment, 'commit': commit})
Session().commit()
return {
'comment_history_id': comment_history.comment_history_id,
'comment_id': comment.comment_id,
'comment_version': comment_history.version,
'comment_author_username': comment_history.author.username,
'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
'comment_created_on': h.age_component(comment_history.created_on,
time_is_local=True),
}
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')

View file

@ -125,7 +125,7 @@ class RepoFilesView(RepoAppView):
self.db_repo_name, branch_name)
if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
message = _('Branch `{}` changes forbidden by rule {}.').format(
h.escape(branch_name), rule)
h.escape(branch_name), h.escape(rule))
h.flash(message, 'warning')
if json_mode:

View file

@ -25,7 +25,7 @@ import formencode
import formencode.htmlfill
import peppercorn
from pyramid.httpexceptions import (
HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
from pyramid.view import view_config
from pyramid.renderers import render
@ -34,6 +34,7 @@ from rhodecode.apps._base import RepoAppView, DataGridAppView
from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
from rhodecode.lib.base import vcs_operation_context
from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
from rhodecode.lib.exceptions import CommentVersionMismatch
from rhodecode.lib.ext_json import json
from rhodecode.lib.auth import (
LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
@ -213,9 +214,12 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
ancestor_commit,
source_ref_id, target_ref_id,
target_commit, source_commit, diff_limit, file_limit,
fulldiff, hide_whitespace_changes, diff_context):
fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
if use_ancestor:
# we might want to not use it for versions
target_ref_id = ancestor_commit.raw_id
target_ref_id = ancestor_commit.raw_id
vcs_diff = PullRequestModel().get_diff(
source_repo, source_ref_id, target_ref_id,
hide_whitespace_changes, diff_context)
@ -568,7 +572,6 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
c.commit_ranges.append(comm)
c.missing_requirements = missing_requirements
c.ancestor_commit = ancestor_commit
c.statuses = source_repo.statuses(
[x.raw_id for x in c.commit_ranges])
@ -593,6 +596,10 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
else:
c.inline_comments = display_inline_comments
use_ancestor = True
if from_version_normalized != version_normalized:
use_ancestor = False
has_proper_diff_cache = cached_diff and cached_diff.get('commits')
if not force_recache and has_proper_diff_cache:
c.diffset = cached_diff['diff']
@ -604,7 +611,10 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
source_ref_id, target_ref_id,
target_commit, source_commit,
diff_limit, file_limit, c.fulldiff,
hide_whitespace_changes, diff_context)
hide_whitespace_changes, diff_context,
use_ancestor=use_ancestor
)
# save cached diff
if caching_enabled:
cache_diff(cache_file_path, c.diffset, diff_commit_cache)
@ -1524,3 +1534,104 @@ 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):
self.load_default_context()
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 '
'with the same comment body {}'.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()
try:
comment_history = CommentsModel().edit(
comment_id=comment_id,
text=text,
auth_user=self._rhodecode_user,
version=version,
)
except CommentVersionMismatch:
raise HTTPConflict()
if not comment_history:
raise HTTPNotFound()
Session().commit()
PullRequestModel().trigger_pull_request_hook(
pull_request, self._rhodecode_user, 'comment_edit',
data={'comment': comment})
return {
'comment_history_id': comment_history.comment_history_id,
'comment_id': comment.comment_id,
'comment_version': comment_history.version,
'comment_author_username': comment_history.author.username,
'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
'comment_created_on': h.age_component(comment_history.created_on,
time_is_local=True),
}
else:
log.warning('No permissions for user %s to edit comment_id: %s',
self._rhodecode_db_user, comment_id)
raise HTTPNotFound()

View file

@ -743,7 +743,7 @@ def authenticate(username, password, environ=None, auth_type=None,
log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
plugin.get_id(), plugin_cache_active, cache_ttl)
user_id = user.user_id if user else None
user_id = user.user_id if user else 'no-user'
# don't cache for empty users
plugin_cache_active = plugin_cache_active and user_id
cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)

View file

@ -26,6 +26,7 @@ from .hooks import (
_pre_create_user_hook,
_create_user_hook,
_comment_commit_repo_hook,
_comment_edit_commit_repo_hook,
_delete_repo_hook,
_delete_user_hook,
_pre_push_hook,
@ -35,6 +36,7 @@ from .hooks import (
_create_pull_request_hook,
_review_pull_request_hook,
_comment_pull_request_hook,
_comment_edit_pull_request_hook,
_update_pull_request_hook,
_merge_pull_request_hook,
_close_pull_request_hook,
@ -43,6 +45,7 @@ from .hooks import (
# set as module attributes, we use those to call hooks. *do not change this*
CREATE_REPO_HOOK = _create_repo_hook
COMMENT_COMMIT_REPO_HOOK = _comment_commit_repo_hook
COMMENT_EDIT_COMMIT_REPO_HOOK = _comment_edit_commit_repo_hook
CREATE_REPO_GROUP_HOOK = _create_repo_group_hook
PRE_CREATE_USER_HOOK = _pre_create_user_hook
CREATE_USER_HOOK = _create_user_hook
@ -55,6 +58,7 @@ PULL_HOOK = _pull_hook
CREATE_PULL_REQUEST = _create_pull_request_hook
REVIEW_PULL_REQUEST = _review_pull_request_hook
COMMENT_PULL_REQUEST = _comment_pull_request_hook
COMMENT_EDIT_PULL_REQUEST = _comment_edit_pull_request_hook
UPDATE_PULL_REQUEST = _update_pull_request_hook
MERGE_PULL_REQUEST = _merge_pull_request_hook
CLOSE_PULL_REQUEST = _close_pull_request_hook

View file

@ -1,5 +1,6 @@
# This code allows override the integrations templates.
# Put this into the __init__.py file of rcextensions to override the templates
# Below code examples allows override the integrations templates, or email titles.
# Append selected parts at the end of the __init__.py file of rcextensions directory
# to override the templates
# EMAIL Integration
@ -185,3 +186,18 @@ ${commit['message']}
```
''')
# Example to modify emails default title
from rhodecode.model import notification
notification.EMAIL_PR_UPDATE_SUBJECT_TEMPLATE = '{updating_user} updated pull request. !{pr_id}: "{pr_title}"'
notification.EMAIL_PR_REVIEW_SUBJECT_TEMPLATE = '{user} requested a pull request review. !{pr_id}: "{pr_title}"'
notification.EMAIL_PR_COMMENT_SUBJECT_TEMPLATE = '{mention_prefix}{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"'
notification.EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = '{mention_prefix}[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"'
notification.EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE = '{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"'
notification.EMAIL_COMMENT_SUBJECT_TEMPLATE = '{mention_prefix}{user} left a {comment_type} on commit `{commit_id}`'
notification.EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = '{mention_prefix}[status: {status}] {user} left a {comment_type} on commit `{commit_id}`'
notification.EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE = '{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in commit `{commit_id}`'

View file

@ -82,6 +82,33 @@ def _comment_commit_repo_hook(*args, **kwargs):
return HookResponse(0, '')
@has_kwargs({
'repo_name': '',
'repo_type': '',
'description': '',
'private': '',
'created_on': '',
'enable_downloads': '',
'repo_id': '',
'user_id': '',
'enable_statistics': '',
'clone_uri': '',
'fork_id': '',
'group_id': '',
'created_by': '',
'repository': '',
'comment': '',
'commit': ''
})
def _comment_edit_commit_repo_hook(*args, **kwargs):
"""
POST CREATE REPOSITORY COMMENT ON COMMIT HOOK. This function will be executed after
a comment is made on this repository commit.
"""
return HookResponse(0, '')
@has_kwargs({
'group_name': '',
'group_parent_id': '',
@ -402,6 +429,38 @@ def _comment_pull_request_hook(*args, **kwargs):
return HookResponse(0, '')
@has_kwargs({
'server_url': 'url of instance that triggered this hook',
'config': 'path to .ini config used',
'scm': 'type of version control "git", "hg", "svn"',
'username': 'username of actor who triggered this event',
'ip': 'ip address of actor who triggered this hook',
'action': '',
'repository': 'repository name',
'pull_request_id': '',
'url': '',
'title': '',
'description': '',
'status': '',
'comment': '',
'created_on': '',
'updated_on': '',
'commit_ids': '',
'review_status': '',
'mergeable': '',
'source': '',
'target': '',
'author': '',
'reviewers': '',
})
def _comment_edit_pull_request_hook(*args, **kwargs):
"""
This hook will be executed after comment is made on a pull request
"""
return HookResponse(0, '')
@has_kwargs({
'server_url': 'url of instance that triggered this hook',
'config': 'path to .ini config used',

View file

@ -74,7 +74,7 @@ link_config = [
},
{
"name": "rst_help",
"target": "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
"target": "http://docutils.sourceforge.io/docs/user/rst/quickref.html",
"external_target": "https://docutils.sourceforge.io/docs/user/rst/quickref.html",
},
{

View file

@ -53,7 +53,8 @@ from rhodecode.events.user import ( # pragma: no cover
)
from rhodecode.events.repo import ( # pragma: no cover
RepoEvent, RepoCommitCommentEvent,
RepoEvent,
RepoCommitCommentEvent, RepoCommitCommentEditEvent,
RepoPreCreateEvent, RepoCreateEvent,
RepoPreDeleteEvent, RepoDeleteEvent,
RepoPrePushEvent, RepoPushEvent,
@ -72,8 +73,8 @@ from rhodecode.events.pullrequest import ( # pragma: no cover
PullRequestCreateEvent,
PullRequestUpdateEvent,
PullRequestCommentEvent,
PullRequestCommentEditEvent,
PullRequestReviewEvent,
PullRequestMergeEvent,
PullRequestCloseEvent,
PullRequestCommentEvent,
)

View file

@ -19,8 +19,7 @@
import logging
from rhodecode.translation import lazy_ugettext
from rhodecode.events.repo import (
RepoEvent, _commits_as_dict, _issues_as_dict)
from rhodecode.events.repo import (RepoEvent, _commits_as_dict, _issues_as_dict)
log = logging.getLogger(__name__)
@ -155,6 +154,46 @@ class PullRequestCommentEvent(PullRequestEvent):
'type': self.comment.comment_type,
'file': self.comment.f_path,
'line': self.comment.line_no,
'version': self.comment.last_version,
'url': CommentsModel().get_url(
self.comment, request=self.request),
'permalink_url': CommentsModel().get_url(
self.comment, request=self.request, permalink=True),
}
})
return data
class PullRequestCommentEditEvent(PullRequestEvent):
"""
An instance of this class is emitted as an :term:`event` after a pull
request comment is edited.
"""
name = 'pullrequest-comment-edit'
display_name = lazy_ugettext('pullrequest comment edited')
description = lazy_ugettext('Event triggered after a comment was edited on a code '
'in the pull request')
def __init__(self, pullrequest, comment):
super(PullRequestCommentEditEvent, self).__init__(pullrequest)
self.comment = comment
def as_dict(self):
from rhodecode.model.comment import CommentsModel
data = super(PullRequestCommentEditEvent, self).as_dict()
status = None
if self.comment.status_change:
status = self.comment.status_change[0].status
data.update({
'comment': {
'status': status,
'text': self.comment.text,
'type': self.comment.comment_type,
'file': self.comment.f_path,
'line': self.comment.line_no,
'version': self.comment.last_version,
'url': CommentsModel().get_url(
self.comment, request=self.request),
'permalink_url': CommentsModel().get_url(

View file

@ -211,6 +211,42 @@ class RepoCommitCommentEvent(RepoEvent):
'comment_type': self.comment.comment_type,
'comment_f_path': self.comment.f_path,
'comment_line_no': self.comment.line_no,
'comment_version': self.comment.last_version,
}
return data
class RepoCommitCommentEditEvent(RepoEvent):
"""
An instance of this class is emitted as an :term:`event` after a comment is edited
on repository commit.
"""
name = 'repo-commit-edit-comment'
display_name = lazy_ugettext('repository commit edit comment')
description = lazy_ugettext('Event triggered after a comment was edited '
'on commit inside a repository')
def __init__(self, repo, commit, comment):
super(RepoCommitCommentEditEvent, self).__init__(repo)
self.commit = commit
self.comment = comment
def as_dict(self):
data = super(RepoCommitCommentEditEvent, self).as_dict()
data['commit'] = {
'commit_id': self.commit.raw_id,
'commit_message': self.commit.message,
'commit_branch': self.commit.branch,
}
data['comment'] = {
'comment_id': self.comment.comment_id,
'comment_text': self.comment.text,
'comment_type': self.comment.comment_type,
'comment_f_path': self.comment.f_path,
'comment_line_no': self.comment.line_no,
'comment_version': self.comment.last_version,
}
return data

View file

@ -331,6 +331,26 @@ class WebhookDataHandler(CommitParsingDataHandler):
return [(url, self.headers, data)]
def repo_commit_comment_edit_handler(self, event, data):
url = self.get_base_parsed_template(data)
log.debug('register %s call(%s) to url %s', self.name, event, url)
comment_vars = [
('commit_comment_id', data['comment']['comment_id']),
('commit_comment_text', data['comment']['comment_text']),
('commit_comment_type', data['comment']['comment_type']),
('commit_comment_f_path', data['comment']['comment_f_path']),
('commit_comment_line_no', data['comment']['comment_line_no']),
('commit_comment_commit_id', data['commit']['commit_id']),
('commit_comment_commit_branch', data['commit']['commit_branch']),
('commit_comment_commit_message', data['commit']['commit_message']),
]
for k, v in comment_vars:
url = UrlTmpl(url).safe_substitute(**{k: v})
return [(url, self.headers, data)]
def repo_create_event_handler(self, event, data):
url = self.get_base_parsed_template(data)
log.debug('register %s call(%s) to url %s', self.name, event, url)
@ -360,6 +380,8 @@ class WebhookDataHandler(CommitParsingDataHandler):
return self.repo_create_event_handler(event, data)
elif isinstance(event, events.RepoCommitCommentEvent):
return self.repo_commit_comment_handler(event, data)
elif isinstance(event, events.RepoCommitCommentEditEvent):
return self.repo_commit_comment_edit_handler(event, data)
elif isinstance(event, events.PullRequestEvent):
return self.pull_request_event_handler(event, data)
else:

View file

@ -133,6 +133,8 @@ class HipchatIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
if isinstance(event, events.PullRequestCommentEvent):
text = self.format_pull_request_comment_event(event, data)
elif isinstance(event, events.PullRequestCommentEditEvent):
text = self.format_pull_request_comment_event(event, data)
elif isinstance(event, events.PullRequestReviewEvent):
text = self.format_pull_request_review_event(event, data)
elif isinstance(event, events.PullRequestEvent):

View file

@ -157,6 +157,9 @@ class SlackIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
if isinstance(event, events.PullRequestCommentEvent):
(title, text, fields, overrides) \
= self.format_pull_request_comment_event(event, data)
elif isinstance(event, events.PullRequestCommentEditEvent):
(title, text, fields, overrides) \
= self.format_pull_request_comment_event(event, data)
elif isinstance(event, events.PullRequestReviewEvent):
title, text = self.format_pull_request_review_event(event, data)
elif isinstance(event, events.PullRequestEvent):

View file

@ -144,11 +144,13 @@ class WebhookIntegrationType(IntegrationTypeBase):
events.PullRequestMergeEvent,
events.PullRequestUpdateEvent,
events.PullRequestCommentEvent,
events.PullRequestCommentEditEvent,
events.PullRequestReviewEvent,
events.PullRequestCreateEvent,
events.RepoPushEvent,
events.RepoCreateEvent,
events.RepoCommitCommentEvent,
events.RepoCommitCommentEditEvent,
]
def settings_schema(self):

View file

@ -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': '',

View file

@ -367,8 +367,7 @@ class PermOriginDict(dict):
self.perm_origin_stack = collections.OrderedDict()
def __setitem__(self, key, (perm, origin, obj_id)):
self.perm_origin_stack.setdefault(key, []).append(
(perm, origin, obj_id))
self.perm_origin_stack.setdefault(key, []).append((perm, origin, obj_id))
dict.__setitem__(self, key, perm)
@ -441,7 +440,7 @@ class PermissionCalculator(object):
def calculate(self):
if self.user_is_admin and not self.calculate_super_admin_as_user:
return self._calculate_admin_permissions()
return self._calculate_super_admin_permissions()
self._calculate_global_default_permissions()
self._calculate_global_permissions()
@ -452,9 +451,9 @@ class PermissionCalculator(object):
self._calculate_user_group_permissions()
return self._permission_structure()
def _calculate_admin_permissions(self):
def _calculate_super_admin_permissions(self):
"""
admin user have all default rights for repositories
super-admin user have all default rights for repositories
and groups set to admin
"""
self.permissions_global.add('hg.admin')
@ -774,6 +773,7 @@ class PermissionCalculator(object):
for perm in user_repo_perms:
r_k = perm.UserRepoToPerm.repository.repo_name
obj_id = perm.UserRepoToPerm.repository.repo_id
archived = perm.UserRepoToPerm.repository.archived
p = perm.Permission.permission_name
o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
@ -795,6 +795,15 @@ class PermissionCalculator(object):
o = PermOrigin.SUPER_ADMIN
self.permissions_repositories[r_k] = p, o, obj_id
# finally in case of archived repositories, we downgrade higher
# permissions to read
if archived:
current_perm = self.permissions_repositories[r_k]
if current_perm in ['repository.write', 'repository.admin']:
p = 'repository.read'
o = PermOrigin.ARCHIVED
self.permissions_repositories[r_k] = p, o, obj_id
def _calculate_repository_branch_permissions(self):
# user group for repositories permissions
user_repo_branch_perms_from_user_group = Permission\

View file

@ -384,7 +384,8 @@ def attach_context_attributes(context, request, user_id=None, is_api=None):
session_attrs = {
# defaults
"clone_url_format": "http",
"diffmode": "sideside"
"diffmode": "sideside",
"license_fingerprint": request.session.get('license_fingerprint')
}
if not is_api:

View file

@ -61,6 +61,8 @@ markdown_tags = [
"img",
"a",
"input",
"details",
"summary"
]
markdown_attrs = {

View file

@ -29,18 +29,20 @@ import time
from pyramid import compat
from pyramid_mailer.mailer import Mailer
from pyramid_mailer.message import Message
from email.utils import formatdate
import rhodecode
from rhodecode.lib import audit_logger
from rhodecode.lib.celerylib import get_logger, async_task, RequestContextTask
from rhodecode.lib.hooks_base import log_create_repository
from rhodecode.lib import hooks_base
from rhodecode.lib.utils2 import safe_int, str2bool
from rhodecode.model.db import (
Session, IntegrityError, true, Repository, RepoGroup, User)
@async_task(ignore_result=True, base=RequestContextTask)
def send_email(recipients, subject, body='', html_body='', email_config=None):
def send_email(recipients, subject, body='', html_body='', email_config=None,
extra_headers=None):
"""
Sends an email with defined parameters from the .ini files.
@ -50,6 +52,7 @@ def send_email(recipients, subject, body='', html_body='', email_config=None):
:param body: body of the mail
:param html_body: html version of body
:param email_config: specify custom configuration for mailer
:param extra_headers: specify custom headers
"""
log = get_logger(send_email)
@ -108,13 +111,23 @@ def send_email(recipients, subject, body='', html_body='', email_config=None):
# sendmail_template='',
)
if extra_headers is None:
extra_headers = {}
extra_headers.setdefault('Date', formatdate(time.time()))
if 'thread_ids' in extra_headers:
thread_ids = extra_headers.pop('thread_ids')
extra_headers['References'] = ' '.join('<{}>'.format(t) for t in thread_ids)
try:
mailer = Mailer(**email_conf)
message = Message(subject=subject,
sender=email_conf['default_sender'],
recipients=recipients,
body=body, html=html_body)
body=body, html=html_body,
extra_headers=extra_headers)
mailer.send_immediately(message)
except Exception:
@ -187,7 +200,7 @@ def create_repo(form_data, cur_user):
clone_uri=clone_uri,
)
repo = Repository.get_by_repo_name(repo_name_full)
log_create_repository(created_by=owner.username, **repo.get_dict())
hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
# update repo commit caches initially
repo.update_commit_cache()
@ -273,7 +286,7 @@ def create_repo_fork(form_data, cur_user):
clone_uri=source_repo_path,
)
repo = Repository.get_by_repo_name(repo_name_full)
log_create_repository(created_by=owner.username, **repo.get_dict())
hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
# update repo commit caches initially
config = repo._config

View file

@ -540,10 +540,11 @@ class DiffSet(object):
})
file_chunks = patch['chunks'][1:]
for hunk in file_chunks:
for i, hunk in enumerate(file_chunks, 1):
hunkbit = self.parse_hunk(hunk, source_file, target_file)
hunkbit.source_file_path = source_file_path
hunkbit.target_file_path = target_file_path
hunkbit.index = i
filediff.hunks.append(hunkbit)
# Simulate hunk on OPS type line which doesn't really contain any diff

File diff suppressed because it is too large Load diff

View 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

View file

@ -143,8 +143,7 @@ def send_exc_email(request, exc_id, exc_type_name):
'exc_traceback': read_exception(exc_id, prefix=None),
}
(subject, headers, email_body,
email_body_plaintext) = EmailNotificationModel().render_email(
(subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
EmailNotificationModel.TYPE_EMAIL_EXCEPTION, **email_kwargs)
run_task(tasks.send_email, recipients, subject,

View file

@ -177,3 +177,7 @@ class ArtifactMetadataDuplicate(ValueError):
class ArtifactMetadataBadValueType(ValueError):
pass
class CommentVersionMismatch(ValueError):
pass

View file

@ -24,6 +24,7 @@ Helper functions
Consists of functions to typically be used within templates, but also
available to Controllers. This module is available to both as 'h'.
"""
import base64
import os
import random
@ -52,7 +53,7 @@ from pygments.lexers import (
get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
from pyramid.threadlocal import get_current_request
from tempita import looper
from webhelpers2.html import literal, HTML, escape
from webhelpers2.html._autolink import _auto_link_urls
from webhelpers2.html.tools import (
@ -85,10 +86,11 @@ from rhodecode.lib.utils2 import (
from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
from rhodecode.lib.index.search_utils import get_matching_line_offsets
from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
from rhodecode.model.changeset_status import ChangesetStatusModel
from rhodecode.model.db import Permission, User, Repository
from rhodecode.model.db import Permission, User, Repository, UserApiKeys
from rhodecode.model.repo_group import RepoGroupModel
from rhodecode.model.settings import IssueTrackerSettingsModel
@ -783,13 +785,24 @@ flash = Flash()
# SCM FILTERS available via h.
#==============================================================================
from rhodecode.lib.vcs.utils import author_name, author_email
from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
from rhodecode.lib.utils2 import age, age_from_seconds
from rhodecode.model.db import User, ChangesetStatus
capitalize = lambda x: x.capitalize()
email = author_email
short_id = lambda x: x[:12]
hide_credentials = lambda x: ''.join(credentials_filter(x))
def capitalize(raw_text):
return raw_text.capitalize()
def short_id(long_id):
return long_id[:12]
def hide_credentials(url):
from rhodecode.lib.utils2 import credentials_filter
return credentials_filter(url)
import pytz
@ -948,7 +961,7 @@ def link_to_user(author, length=0, **kwargs):
if length:
display_person = shorter(display_person, length)
if user:
if user and user.username != user.DEFAULT_USER:
return link_to(
escape(display_person),
route_path('user_profile', username=user.username),
@ -1341,7 +1354,7 @@ class InitialsGravatar(object):
def generate_svg(self, svg_type=None):
img_data = self.get_img_data(svg_type)
return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
def initials_gravatar(email_address, first_name, last_name, size=30):

View file

@ -400,7 +400,7 @@ pre_create_user = ExtensionCallback(
'admin', 'created_by'))
log_create_pull_request = ExtensionCallback(
create_pull_request = ExtensionCallback(
hook_name='CREATE_PULL_REQUEST',
kwargs_keys=(
'server_url', 'config', 'scm', 'username', 'ip', 'action',
@ -409,7 +409,7 @@ log_create_pull_request = ExtensionCallback(
'mergeable', 'source', 'target', 'author', 'reviewers'))
log_merge_pull_request = ExtensionCallback(
merge_pull_request = ExtensionCallback(
hook_name='MERGE_PULL_REQUEST',
kwargs_keys=(
'server_url', 'config', 'scm', 'username', 'ip', 'action',
@ -418,7 +418,7 @@ log_merge_pull_request = ExtensionCallback(
'mergeable', 'source', 'target', 'author', 'reviewers'))
log_close_pull_request = ExtensionCallback(
close_pull_request = ExtensionCallback(
hook_name='CLOSE_PULL_REQUEST',
kwargs_keys=(
'server_url', 'config', 'scm', 'username', 'ip', 'action',
@ -427,7 +427,7 @@ log_close_pull_request = ExtensionCallback(
'mergeable', 'source', 'target', 'author', 'reviewers'))
log_review_pull_request = ExtensionCallback(
review_pull_request = ExtensionCallback(
hook_name='REVIEW_PULL_REQUEST',
kwargs_keys=(
'server_url', 'config', 'scm', 'username', 'ip', 'action',
@ -436,7 +436,7 @@ log_review_pull_request = ExtensionCallback(
'mergeable', 'source', 'target', 'author', 'reviewers'))
log_comment_pull_request = ExtensionCallback(
comment_pull_request = ExtensionCallback(
hook_name='COMMENT_PULL_REQUEST',
kwargs_keys=(
'server_url', 'config', 'scm', 'username', 'ip', 'action',
@ -445,7 +445,16 @@ log_comment_pull_request = ExtensionCallback(
'mergeable', 'source', 'target', 'author', 'reviewers'))
log_update_pull_request = ExtensionCallback(
comment_edit_pull_request = ExtensionCallback(
hook_name='COMMENT_EDIT_PULL_REQUEST',
kwargs_keys=(
'server_url', 'config', 'scm', 'username', 'ip', 'action',
'repository', 'pull_request_id', 'url', 'title', 'description',
'status', 'comment', 'created_on', 'updated_on', 'commit_ids', 'review_status',
'mergeable', 'source', 'target', 'author', 'reviewers'))
update_pull_request = ExtensionCallback(
hook_name='UPDATE_PULL_REQUEST',
kwargs_keys=(
'server_url', 'config', 'scm', 'username', 'ip', 'action',
@ -454,7 +463,7 @@ log_update_pull_request = ExtensionCallback(
'mergeable', 'source', 'target', 'author', 'reviewers'))
log_create_user = ExtensionCallback(
create_user = ExtensionCallback(
hook_name='CREATE_USER_HOOK',
kwargs_keys=(
'username', 'full_name_or_username', 'full_contact', 'user_id',
@ -465,7 +474,7 @@ log_create_user = ExtensionCallback(
'inherit_default_permissions', 'created_by', 'created_on'))
log_delete_user = ExtensionCallback(
delete_user = ExtensionCallback(
hook_name='DELETE_USER_HOOK',
kwargs_keys=(
'username', 'full_name_or_username', 'full_contact', 'user_id',
@ -476,7 +485,7 @@ log_delete_user = ExtensionCallback(
'inherit_default_permissions', 'deleted_by'))
log_create_repository = ExtensionCallback(
create_repository = ExtensionCallback(
hook_name='CREATE_REPO_HOOK',
kwargs_keys=(
'repo_name', 'repo_type', 'description', 'private', 'created_on',
@ -484,7 +493,7 @@ log_create_repository = ExtensionCallback(
'clone_uri', 'fork_id', 'group_id', 'created_by'))
log_delete_repository = ExtensionCallback(
delete_repository = ExtensionCallback(
hook_name='DELETE_REPO_HOOK',
kwargs_keys=(
'repo_name', 'repo_type', 'description', 'private', 'created_on',
@ -492,7 +501,7 @@ log_delete_repository = ExtensionCallback(
'clone_uri', 'fork_id', 'group_id', 'deleted_by', 'deleted_on'))
log_comment_commit_repository = ExtensionCallback(
comment_commit_repository = ExtensionCallback(
hook_name='COMMENT_COMMIT_REPO_HOOK',
kwargs_keys=(
'repo_name', 'repo_type', 'description', 'private', 'created_on',
@ -500,8 +509,16 @@ log_comment_commit_repository = ExtensionCallback(
'clone_uri', 'fork_id', 'group_id',
'repository', 'created_by', 'comment', 'commit'))
comment_edit_commit_repository = ExtensionCallback(
hook_name='COMMENT_EDIT_COMMIT_REPO_HOOK',
kwargs_keys=(
'repo_name', 'repo_type', 'description', 'private', 'created_on',
'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
'clone_uri', 'fork_id', 'group_id',
'repository', 'created_by', 'comment', 'commit'))
log_create_repository_group = ExtensionCallback(
create_repository_group = ExtensionCallback(
hook_name='CREATE_REPO_GROUP_HOOK',
kwargs_keys=(
'group_name', 'group_parent_id', 'group_description',

View file

@ -94,7 +94,34 @@ def trigger_comment_commit_hooks(username, repo_name, repo_type, repo, data=None
extras.commit = commit.serialize()
extras.comment = comment.get_api_data()
extras.created_by = username
hooks_base.log_comment_commit_repository(**extras)
hooks_base.comment_commit_repository(**extras)
def trigger_comment_commit_edit_hooks(username, repo_name, repo_type, repo, data=None):
"""
Triggers when a comment is edited on a commit
:param username: username who edits the comment
:param repo_name: name of target repo
:param repo_type: the type of SCM target repo
:param repo: the repo object we trigger the event for
:param data: extra data for specific events e.g {'comment': comment_obj, 'commit': commit_obj}
"""
if not _supports_repo_type(repo_type):
return
extras = _get_vcs_operation_context(username, repo_name, repo_type, 'comment_commit')
comment = data['comment']
commit = data['commit']
events.trigger(events.RepoCommitCommentEditEvent(repo, commit, comment))
extras.update(repo.get_dict())
extras.commit = commit.serialize()
extras.comment = comment.get_api_data()
extras.created_by = username
hooks_base.comment_edit_commit_repository(**extras)
def trigger_create_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
@ -113,7 +140,7 @@ def trigger_create_pull_request_hook(username, repo_name, repo_type, pull_reques
extras = _get_vcs_operation_context(username, repo_name, repo_type, 'create_pull_request')
events.trigger(events.PullRequestCreateEvent(pull_request))
extras.update(pull_request.get_api_data(with_merge_state=False))
hooks_base.log_create_pull_request(**extras)
hooks_base.create_pull_request(**extras)
def trigger_merge_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
@ -132,7 +159,7 @@ def trigger_merge_pull_request_hook(username, repo_name, repo_type, pull_request
extras = _get_vcs_operation_context(username, repo_name, repo_type, 'merge_pull_request')
events.trigger(events.PullRequestMergeEvent(pull_request))
extras.update(pull_request.get_api_data())
hooks_base.log_merge_pull_request(**extras)
hooks_base.merge_pull_request(**extras)
def trigger_close_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
@ -151,7 +178,7 @@ def trigger_close_pull_request_hook(username, repo_name, repo_type, pull_request
extras = _get_vcs_operation_context(username, repo_name, repo_type, 'close_pull_request')
events.trigger(events.PullRequestCloseEvent(pull_request))
extras.update(pull_request.get_api_data())
hooks_base.log_close_pull_request(**extras)
hooks_base.close_pull_request(**extras)
def trigger_review_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
@ -171,7 +198,7 @@ def trigger_review_pull_request_hook(username, repo_name, repo_type, pull_reques
status = data.get('status')
events.trigger(events.PullRequestReviewEvent(pull_request, status))
extras.update(pull_request.get_api_data())
hooks_base.log_review_pull_request(**extras)
hooks_base.review_pull_request(**extras)
def trigger_comment_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
@ -193,7 +220,29 @@ def trigger_comment_pull_request_hook(username, repo_name, repo_type, pull_reque
events.trigger(events.PullRequestCommentEvent(pull_request, comment))
extras.update(pull_request.get_api_data())
extras.comment = comment.get_api_data()
hooks_base.log_comment_pull_request(**extras)
hooks_base.comment_pull_request(**extras)
def trigger_comment_pull_request_edit_hook(username, repo_name, repo_type, pull_request, data=None):
"""
Triggers when a comment was edited on a pull request
:param username: username who made the edit
:param repo_name: name of target repo
:param repo_type: the type of SCM target repo
:param pull_request: the pull request that comment was made on
:param data: extra data for specific events e.g {'comment': comment_obj}
"""
if not _supports_repo_type(repo_type):
return
extras = _get_vcs_operation_context(username, repo_name, repo_type, 'comment_pull_request')
comment = data['comment']
events.trigger(events.PullRequestCommentEditEvent(pull_request, comment))
extras.update(pull_request.get_api_data())
extras.comment = comment.get_api_data()
hooks_base.comment_edit_pull_request(**extras)
def trigger_update_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
@ -212,4 +261,4 @@ def trigger_update_pull_request_hook(username, repo_name, repo_type, pull_reques
extras = _get_vcs_operation_context(username, repo_name, repo_type, 'update_pull_request')
events.trigger(events.PullRequestUpdateEvent(pull_request))
extras.update(pull_request.get_api_data())
hooks_base.log_update_pull_request(**extras)
hooks_base.update_pull_request(**extras)

View 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))

View file

@ -48,6 +48,7 @@ from .utils import (
FILE_TREE_CACHE_VER = 'v4'
LICENSE_CACHE_VER = 'v2'
def configure_dogpile_cache(settings):

View file

@ -159,7 +159,14 @@ class FileNamespaceBackend(PickleSerializer, file_backend.DBMBackend):
def __init__(self, arguments):
arguments['lock_factory'] = CustomLockFactory
super(FileNamespaceBackend, self).__init__(arguments)
db_file = arguments.get('filename')
log.debug('initialing %s DB in %s', self.__class__.__name__, db_file)
try:
super(FileNamespaceBackend, self).__init__(arguments)
except Exception:
log.error('Failed to initialize db at: %s', db_file)
raise
def __repr__(self):
return '{} `{}`'.format(self.__class__, self.filename)

View file

@ -30,6 +30,7 @@ import os
import re
import sys
import shutil
import socket
import tempfile
import traceback
import tarfile
@ -782,3 +783,18 @@ def generate_platform_uuid():
except Exception as e:
log.error('Failed to generate host uuid: %s', e)
return 'UNDEFINED'
def send_test_email(recipients, email_body='TEST EMAIL'):
"""
Simple code for generating test emails.
Usage::
from rhodecode.lib import utils
utils.send_test_email()
"""
from rhodecode.lib.celerylib import tasks, run_task
email_body = email_body_plaintext = email_body
subject = 'SUBJECT FROM: {}'.format(socket.gethostname())
tasks.send_email(recipients, subject, email_body_plaintext, email_body)

View file

@ -628,34 +628,42 @@ class MercurialRepository(BaseRepository):
push_branches=push_branches)
def _local_merge(self, target_ref, merge_message, user_name, user_email,
source_ref, use_rebase=False, dry_run=False):
source_ref, use_rebase=False, close_commit_id=None, dry_run=False):
"""
Merge the given source_revision into the checked out revision.
Returns the commit id of the merge and a boolean indicating if the
commit needs to be pushed.
"""
self._update(target_ref.commit_id, clean=True)
source_ref_commit_id = source_ref.commit_id
target_ref_commit_id = target_ref.commit_id
ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
# update our workdir to target ref, for proper merge
self._update(target_ref_commit_id, clean=True)
ancestor = self._ancestor(target_ref_commit_id, source_ref_commit_id)
is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
if ancestor == source_ref.commit_id:
# Nothing to do, the changes were already integrated
return target_ref.commit_id, False
if close_commit_id:
# NOTE(marcink): if we get the close commit, this is our new source
# which will include the close commit itself.
source_ref_commit_id = close_commit_id
elif ancestor == target_ref.commit_id and is_the_same_branch:
if ancestor == source_ref_commit_id:
# Nothing to do, the changes were already integrated
return target_ref_commit_id, False
elif ancestor == target_ref_commit_id and is_the_same_branch:
# In this case we should force a commit message
return source_ref.commit_id, True
return source_ref_commit_id, True
unresolved = None
if use_rebase:
try:
bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
target_ref.commit_id)
bookmark_name = 'rcbook%s%s' % (source_ref_commit_id, target_ref_commit_id)
self.bookmark(bookmark_name, revision=source_ref.commit_id)
self._remote.rebase(
source=source_ref.commit_id, dest=target_ref.commit_id)
source=source_ref_commit_id, dest=target_ref_commit_id)
self._remote.invalidate_vcs_cache()
self._update(bookmark_name, clean=True)
return self._identify(), True
@ -678,7 +686,7 @@ class MercurialRepository(BaseRepository):
raise
else:
try:
self._remote.merge(source_ref.commit_id)
self._remote.merge(source_ref_commit_id)
self._remote.invalidate_vcs_cache()
self._remote.commit(
message=safe_str(merge_message),
@ -820,10 +828,12 @@ class MercurialRepository(BaseRepository):
needs_push = False
if merge_possible:
try:
merge_commit_id, needs_push = shadow_repo._local_merge(
target_ref, merge_message, merger_name, merger_email,
source_ref, use_rebase=use_rebase, dry_run=dry_run)
source_ref, use_rebase=use_rebase,
close_commit_id=close_commit_id, dry_run=dry_run)
merge_possible = True
# read the state of the close action, if it

View file

@ -41,7 +41,7 @@ BACKENDS = {
ARCHIVE_SPECS = [
('tbz2', 'application/x-bzip2', 'tbz2'),
('tbz2', 'application/x-bzip2', '.tbz2'),
('tbz2', 'application/x-bzip2', '.tar.bz2'),
('tgz', 'application/x-gzip', '.tgz'),

View file

@ -21,6 +21,7 @@
"""
comments model for RhodeCode
"""
import datetime
import logging
import traceback
@ -32,10 +33,17 @@ from sqlalchemy.sql.functions import coalesce
from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
from rhodecode.lib import audit_logger
from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
from rhodecode.lib.exceptions import CommentVersionMismatch
from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
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
@ -362,13 +370,18 @@ class CommentsModel(BaseModel):
repo.repo_name,
h.route_url('repo_summary', repo_name=repo.repo_name))
commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
commit_id=commit_id)
# commit specifics
kwargs.update({
'commit': commit_obj,
'commit_message': commit_obj.message,
'commit_target_repo_url': target_repo_url,
'commit_comment_url': commit_comment_url,
'commit_comment_reply_url': commit_comment_reply_url
'commit_comment_reply_url': commit_comment_reply_url,
'commit_url': commit_url,
'thread_ids': [commit_url, commit_comment_url],
})
elif pull_request_obj:
@ -413,15 +426,14 @@ class CommentsModel(BaseModel):
'pr_comment_url': pr_comment_url,
'pr_comment_reply_url': pr_comment_reply_url,
'pr_closing': closing_pr,
'thread_ids': [pr_url, pr_comment_url],
})
recipients += [self._get_user(u) for u in (extra_recipients or [])]
if send_email:
# pre-generate the subject for notification itself
(subject,
_h, _e, # we don't care about those
body_plaintext) = EmailNotificationModel().render_email(
(subject, _e, body_plaintext) = EmailNotificationModel().render_email(
notification_type, **kwargs)
mention_recipients = set(
@ -479,6 +491,60 @@ 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.modified_at = datetime.datetime.now()
version = safe_int(version)
# NOTE(marcink): this returns initial comment + edits, so v2 from ui
# would return 3 here
comment_version = ChangesetCommentHistory.get_version(comment_id)
if isinstance(version, (int, long)) and (comment_version - version) != 1:
log.warning(
'Version mismatch comment_version {} submitted {}, skipping'.format(
comment_version-1, # -1 since note above
version
)
)
raise CommentVersionMismatch()
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 +778,7 @@ class CommentsModel(BaseModel):
.filter(ChangesetComment.line_no == None)\
.filter(ChangesetComment.f_path == None)\
.filter(ChangesetComment.pull_request == pull_request)
return comments
@staticmethod
@ -726,8 +793,7 @@ class CommentsModel(BaseModel):
if action == 'create':
trigger_hook = hooks_utils.trigger_comment_commit_hooks
elif action == 'edit':
# TODO(dan): when this is supported we trigger edit hook too
return
trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
else:
return

View file

@ -103,7 +103,12 @@ def display_user_sort(obj):
if obj.username == User.DEFAULT_USER:
return '#####'
prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
return prefix + obj.username
extra_sort_num = '1' # default
# NOTE(dan): inactive duplicates goes last
if getattr(obj, 'duplicate_perm', None):
extra_sort_num = '9'
return prefix + extra_sort_num + obj.username
def display_user_group_sort(obj):
@ -1128,14 +1133,16 @@ class UserApiKeys(Base, BaseModel):
# ApiKey role
ROLE_ALL = 'token_role_all'
ROLE_HTTP = 'token_role_http'
ROLE_VCS = 'token_role_vcs'
ROLE_API = 'token_role_api'
ROLE_HTTP = 'token_role_http'
ROLE_FEED = 'token_role_feed'
ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
# The last one is ignored in the list as we only
# use it for one action, and cannot be created by users
ROLE_PASSWORD_RESET = 'token_password_reset'
ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
@ -1200,6 +1207,22 @@ class UserApiKeys(Base, BaseModel):
cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
}.get(role, role)
@classmethod
def _get_role_description(cls, role):
return {
cls.ROLE_ALL: _('Token for all actions.'),
cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
'login using `api_access_controllers_whitelist` functionality.'),
cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
'Requires auth_token authentication plugin to be active. <br/>'
'Such Token should be used then instead of a password to '
'interact with a repository, and additionally can be '
'limited to single repository using repo scope.'),
cls.ROLE_API: _('Token limited to api calls.'),
cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
}.get(role, role)
@property
def role_humanized(self):
return self._get_role_name(self.role)
@ -3755,6 +3778,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):
@ -3777,7 +3801,7 @@ class ChangesetComment(Base, BaseModel):
def get_index_from_version(cls, pr_version, versions):
num_versions = [x.pull_request_version_id for x in versions]
try:
return num_versions.index(pr_version) +1
return num_versions.index(pr_version) + 1
except (IndexError, ValueError):
return
@ -3804,6 +3828,11 @@ class ChangesetComment(Base, BaseModel):
return self.pull_request_version_id < version
@property
def commit_id(self):
"""New style naming to stop using .revision"""
return self.revision
@property
def resolved(self):
return self.resolved_by[0] if self.resolved_by else None
@ -3816,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)
@ -3828,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,
@ -3840,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
@ -3849,6 +3887,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__ = (

View file

@ -131,15 +131,17 @@ class NotificationModel(BaseModel):
# inject current recipient
email_kwargs['recipient'] = recipient
email_kwargs['mention'] = recipient in mention_recipients
(subject, headers, email_body,
email_body_plaintext) = EmailNotificationModel().render_email(
(subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
notification_type, **email_kwargs)
log.debug(
'Creating notification email task for user:`%s`', recipient)
extra_headers = None
if 'thread_ids' in email_kwargs:
extra_headers = {'thread_ids': email_kwargs.pop('thread_ids')}
log.debug('Creating notification email task for user:`%s`', recipient)
task = run_task(
tasks.send_email, recipient.email, subject,
email_body_plaintext, email_body)
email_body_plaintext, email_body, extra_headers=extra_headers)
log.debug('Created email task: %s', task)
return notification
@ -293,6 +295,27 @@ class NotificationModel(BaseModel):
}
# Templates for Titles, that could be overwritten by rcextensions
# Title of email for pull-request update
EMAIL_PR_UPDATE_SUBJECT_TEMPLATE = ''
# Title of email for request for pull request review
EMAIL_PR_REVIEW_SUBJECT_TEMPLATE = ''
# Title of email for general comment on pull request
EMAIL_PR_COMMENT_SUBJECT_TEMPLATE = ''
# Title of email for general comment which includes status change on pull request
EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
# Title of email for inline comment on a file in pull request
EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE = ''
# Title of email for general comment on commit
EMAIL_COMMENT_SUBJECT_TEMPLATE = ''
# Title of email for general comment which includes status change on commit
EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
# Title of email for inline comment on a file in commit
EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE = ''
class EmailNotificationModel(BaseModel):
TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
@ -333,7 +356,7 @@ class EmailNotificationModel(BaseModel):
}
premailer_instance = premailer.Premailer(
cssutils_logging_level=logging.WARNING,
cssutils_logging_level=logging.ERROR,
cssutils_logging_handler=logging.getLogger().handlers[0]
if logging.getLogger().handlers else None,
)
@ -342,8 +365,7 @@ class EmailNotificationModel(BaseModel):
"""
Example usage::
(subject, headers, email_body,
email_body_plaintext) = EmailNotificationModel().render_email(
(subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
EmailNotificationModel.TYPE_TEST, **email_kwargs)
"""
@ -386,12 +408,6 @@ class EmailNotificationModel(BaseModel):
subject = email_template.render('subject', **_kwargs)
try:
headers = email_template.render('headers', **_kwargs)
except AttributeError:
# it's not defined in template, ok we can skip it
headers = ''
try:
body_plaintext = email_template.render('body_plaintext', **_kwargs)
except AttributeError:
@ -408,4 +424,4 @@ class EmailNotificationModel(BaseModel):
log.exception('Failed to parse body with premailer')
pass
return subject, headers, body, body_plaintext
return subject, body, body_plaintext

View file

@ -577,7 +577,8 @@ class PermissionModel(BaseModel):
user_group_write_permissions[p.users_group_id] = p
return user_group_write_permissions
def trigger_permission_flush(self, affected_user_ids):
def trigger_permission_flush(self, affected_user_ids=None):
affected_user_ids or User.get_all_user_ids()
events.trigger(events.UserPermissionsChange(affected_user_ids))
def flush_user_permission_caches(self, changes, affected_user_ids=None):

View file

@ -703,6 +703,8 @@ class PullRequestModel(BaseModel):
trigger_hook = hooks_utils.trigger_update_pull_request_hook
elif action == 'comment':
trigger_hook = hooks_utils.trigger_comment_pull_request_hook
elif action == 'comment_edit':
trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
else:
return
@ -1342,12 +1344,11 @@ class PullRequestModel(BaseModel):
'pull_request_source_repo_url': pr_source_repo_url,
'pull_request_url': pr_url,
'thread_ids': [pr_url],
}
# pre-generate the subject for notification itself
(subject,
_h, _e, # we don't care about those
body_plaintext) = EmailNotificationModel().render_email(
(subject, _e, body_plaintext) = EmailNotificationModel().render_email(
notification_type, **kwargs)
# create notification objects, and emails
@ -1412,11 +1413,10 @@ class PullRequestModel(BaseModel):
'added_files': file_changes.added,
'modified_files': file_changes.modified,
'removed_files': file_changes.removed,
'thread_ids': [pr_url],
}
(subject,
_h, _e, # we don't care about those
body_plaintext) = EmailNotificationModel().render_email(
(subject, _e, body_plaintext) = EmailNotificationModel().render_email(
EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
# create notification objects, and emails
@ -2053,9 +2053,9 @@ class MergeCheck(object):
repo_type = pull_request.target_repo.repo_type
close_msg = ''
if repo_type == 'hg':
close_msg = _('Source branch will be closed after merge.')
close_msg = _('Source branch will be closed before the merge.')
elif repo_type == 'git':
close_msg = _('Source branch will be deleted after merge.')
close_msg = _('Source branch will be deleted after the merge.')
merge_details['close_branch'] = dict(
details={},

View file

@ -33,7 +33,7 @@ from rhodecode import events
from rhodecode.lib.auth import HasUserGroupPermissionAny
from rhodecode.lib.caching_query import FromCache
from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError
from rhodecode.lib.hooks_base import log_delete_repository
from rhodecode.lib import hooks_base
from rhodecode.lib.user_log_filter import user_log_filter
from rhodecode.lib.utils import make_db_config
from rhodecode.lib.utils2 import (
@ -767,7 +767,7 @@ class RepoModel(BaseModel):
'deleted_by': cur_user,
'deleted_on': time.time(),
})
log_delete_repository(**old_repo_dict)
hooks_base.delete_repository(**old_repo_dict)
events.trigger(events.RepoDeleteEvent(repo))
except Exception:
log.error(traceback.format_exc())

View file

@ -308,13 +308,13 @@ class RepoGroupModel(BaseModel):
self._create_group(new_repo_group.group_name)
# trigger the post hook
from rhodecode.lib.hooks_base import log_create_repository_group
from rhodecode.lib import hooks_base
repo_group = RepoGroup.get_by_group_name(group_name)
# update repo group commit caches initially
repo_group.update_commit_cache()
log_create_repository_group(
hooks_base.create_repository_group(
created_by=user.username, **repo_group.get_dict())
# Trigger create event.

View file

@ -262,8 +262,7 @@ class UserModel(BaseModel):
from rhodecode.lib.auth import (
get_crypt_password, check_password)
from rhodecode.lib.hooks_base import (
log_create_user, check_allowed_create_user)
from rhodecode.lib import hooks_base
def _password_change(new_user, password):
old_password = new_user.password or ''
@ -327,7 +326,7 @@ class UserModel(BaseModel):
if new_active_user and strict_creation_check:
# raises UserCreationError if it's not allowed for any reason to
# create new active user, this also executes pre-create hooks
check_allowed_create_user(user_data, cur_user, strict_check=True)
hooks_base.check_allowed_create_user(user_data, cur_user, strict_check=True)
events.trigger(events.UserPreCreate(user_data))
new_user = User()
edit = False
@ -390,7 +389,7 @@ class UserModel(BaseModel):
kwargs = new_user.get_dict()
# backward compat, require api_keys present
kwargs['api_keys'] = kwargs['auth_tokens']
log_create_user(created_by=cur_user, **kwargs)
hooks_base.create_user(created_by=cur_user, **kwargs)
events.trigger(events.UserPostCreate(user_data))
return new_user
except (DatabaseError,):
@ -423,9 +422,7 @@ class UserModel(BaseModel):
}
notification_type = EmailNotificationModel.TYPE_REGISTRATION
# pre-generate the subject for notification itself
(subject,
_h, _e, # we don't care about those
body_plaintext) = EmailNotificationModel().render_email(
(subject, _e, body_plaintext) = EmailNotificationModel().render_email(
notification_type, **kwargs)
# create notification objects, and emails
@ -569,7 +566,7 @@ class UserModel(BaseModel):
def delete(self, user, cur_user=None, handle_repos=None,
handle_repo_groups=None, handle_user_groups=None,
handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
from rhodecode.lib.hooks_base import log_delete_user
from rhodecode.lib import hooks_base
if not cur_user:
cur_user = getattr(get_current_rhodecode_user(), 'username', None)
@ -638,7 +635,7 @@ class UserModel(BaseModel):
self.sa.expire(user)
self.sa.delete(user)
log_delete_user(deleted_by=cur_user, **user_data)
hooks_base.delete_user(deleted_by=cur_user, **user_data)
except Exception:
log.error(traceback.format_exc())
raise
@ -660,8 +657,7 @@ class UserModel(BaseModel):
'first_admin_email': User.get_first_super_admin().email
}
(subject, headers, email_body,
email_body_plaintext) = EmailNotificationModel().render_email(
(subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
recipients = [user_email]
@ -719,8 +715,7 @@ class UserModel(BaseModel):
'first_admin_email': User.get_first_super_admin().email
}
(subject, headers, email_body,
email_body_plaintext) = EmailNotificationModel().render_email(
(subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
**email_kwargs)

View file

@ -53,7 +53,8 @@ def deferred_can_write_to_group_validator(node, kw):
# permissions denied we expose as not existing, to prevent
# resource discovery
'permission_denied_parent_group':
_(u"Parent repository group `{}` does not exist"),
_(u"You do not have the permissions to store "
u"repository groups inside repository group `{}`"),
'permission_denied_root':
_(u"You do not have the permission to store "
u"repository groups in the root location.")
@ -100,9 +101,15 @@ def deferred_can_write_to_group_validator(node, kw):
# we want to allow this...
forbidden = not (group_admin or (group_write and create_on_write and 0))
old_name = old_values.get('group_name')
if old_name and old_name == old_values.get('submitted_repo_group_name'):
# we're editing a repository group, we didn't change the name
# we skip the check for write into parent group now
# this allows changing settings for this repo group
return
if parent_group and forbidden:
msg = messages['permission_denied_parent_group'].format(
parent_group_name)
msg = messages['permission_denied_parent_group'].format(parent_group_name)
raise colander.Invalid(node, msg)
return can_write_group_validator
@ -248,6 +255,9 @@ class RepoGroupSchema(colander.Schema):
validated_name = appstruct['repo_group_name']
# second pass to validate permissions to repo_group
if 'old_values' in self.bindings:
# save current repo name for name change checks
self.bindings['old_values']['submitted_repo_group_name'] = validated_name
second = RepoGroupAccessSchema().bind(**self.bindings)
appstruct_second = second.deserialize({'repo_group': validated_name})
# save result
@ -286,6 +296,9 @@ class RepoGroupSettingsSchema(RepoGroupSchema):
validated_name = separator.join([group.group_name, validated_name])
# second pass to validate permissions to repo_group
if 'old_values' in self.bindings:
# save current repo name for name change checks
self.bindings['old_values']['submitted_repo_group_name'] = validated_name
second = RepoGroupAccessSchema().bind(**self.bindings)
appstruct_second = second.deserialize({'repo_group': validated_name})
# save result

View file

@ -141,17 +141,23 @@ def deferred_can_write_to_group_validator(node, kw):
is_root_location = value is types.RootLocation
# NOT initialized validators, we must call them
can_create_repos_at_root = HasPermissionAny(
'hg.admin', 'hg.create.repository')
can_create_repos_at_root = HasPermissionAny('hg.admin', 'hg.create.repository')
# if values is root location, we simply need to check if we can write
# to root location !
if is_root_location:
if can_create_repos_at_root(user=request_user):
# we can create repo group inside tool-level. No more checks
# are required
return
else:
old_name = old_values.get('repo_name')
if old_name and old_name == old_values.get('submitted_repo_name'):
# since we didn't change the name, we can skip validation and
# allow current users without store-in-root permissions to update
return
# "fake" node name as repo_name, otherwise we oddly report
# the error as if it was coming form repo_group
# however repo_group is empty when using root location.
@ -372,6 +378,9 @@ class RepoSchema(colander.MappingSchema):
validated_name = appstruct['repo_name']
# second pass to validate permissions to repo_group
if 'old_values' in self.bindings:
# save current repo name for name change checks
self.bindings['old_values']['submitted_repo_name'] = validated_name
second = RepoGroupAccessSchema().bind(**self.bindings)
appstruct_second = second.deserialize({'repo_group': validated_name})
# save result
@ -429,6 +438,9 @@ class RepoSettingsSchema(RepoSchema):
validated_name = separator.join([group.group_name, validated_name])
# second pass to validate permissions to repo_group
if 'old_values' in self.bindings:
# save current repo name for name change checks
self.bindings['old_values']['submitted_repo_name'] = validated_name
second = RepoGroupAccessSchema().bind(**self.bindings)
appstruct_second = second.deserialize({'repo_group': validated_name})
# save result

View file

@ -259,21 +259,34 @@ input[type="button"] {
&:not(.open) .btn-action-switcher-container {
display: none;
}
.btn-more-option {
margin-left: -1px;
padding-left: 2px;
padding-right: 2px;
border-left: 1px solid @grey3;
}
}
.btn-action-switcher-container{
.btn-action-switcher-container {
position: absolute;
top: 30px;
left: -82px;
top: 100%;
&.left-align {
left: 0;
}
&.right-align {
right: 0;
}
}
.btn-action-switcher {
display: block;
position: relative;
z-index: 300;
min-width: 240px;
max-width: 500px;
max-width: 600px;
margin-top: 4px;
margin-bottom: 24px;
font-size: 14px;
@ -283,6 +296,7 @@ input[type="button"] {
border: 1px solid @grey4;
border-radius: 3px;
box-shadow: @dropdown-shadow;
overflow: auto;
li {
display: block;

View file

@ -998,6 +998,21 @@ input.filediff-collapse-state {
/**** END COMMENTS ****/
.nav-chunk {
position: absolute;
right: 20px;
margin-top: -17px;
}
.nav-chunk.selected {
visibility: visible !important;
}
#diff_nav {
color: @grey3;
}
}
@ -1063,6 +1078,10 @@ input.filediff-collapse-state {
background: @color5;
color: white;
}
&[op="comments"] { /* comments on file */
background: @grey4;
color: white;
}
}
}

View file

@ -65,7 +65,7 @@ tr.inline-comments div {
float: left;
padding: 0.4em 0.4em;
margin: 3px 5px 0px -10px;
margin: 2px 4px 0px 0px;
display: inline-block;
min-height: 0;
@ -76,12 +76,13 @@ tr.inline-comments div {
font-family: @text-italic;
font-style: italic;
background: #fff none;
color: @grey4;
color: @grey3;
border: 1px solid @grey4;
white-space: nowrap;
text-transform: uppercase;
min-width: 40px;
min-width: 50px;
border-radius: 4px;
&.todo {
color: @color5;
@ -253,12 +254,10 @@ tr.inline-comments div {
}
.pr-version {
float: left;
margin: 0px 4px;
display: inline-block;
}
.pr-version-inline {
float: left;
margin: 0px 4px;
display: inline-block;
}
.pr-version-num {
font-size: 10px;
@ -447,6 +446,13 @@ form.comment-form {
}
}
.comment-version-select {
margin: 0px;
border-radius: inherit;
border-color: @grey6;
height: 20px;
}
.comment-type {
margin: 0px;
border-radius: inherit;

View file

@ -97,6 +97,11 @@ input + .action-link, .action-link.first{
border-left: none;
}
.link-disabled {
color: @grey4;
cursor: default;
}
.action-link.last{
margin-right: @padding;
padding-right: @padding;

View file

@ -148,6 +148,38 @@ select.select2{height:28px;visibility:hidden}
margin: 0;
}
.drop-menu-comment-history {
.drop-menu-core;
border: none;
padding: 0 6px 0 0;
width: auto;
min-width: 0;
margin: 0;
position: relative;
display: inline-block;
line-height: 1em;
z-index: 2;
cursor: pointer;
a {
display:block;
padding: 0;
position: relative;
&:after {
position: absolute;
content: "\00A0\25BE";
right: -0.80em;
line-height: 1em;
top: -0.20em;
width: 1em;
font-size: 16px;
}
}
}
.field-sm .drop-menu {
padding: 1px 0 0 0;
a {

View file

@ -33,6 +33,12 @@ table.dataTable {
.rc-user {
white-space: nowrap;
}
.user-perm-duplicate {
color: @grey4;
a {
color: @grey4;
}
}
}
.td-email {

View file

@ -37,6 +37,10 @@
&:hover {
border-color: @grey4;
}
&.authortag {
padding: 2px;
}
}
.tag0 { .border ( @border-thickness-tags, @grey4 ); color:@grey4; }

View file

@ -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']);

View file

@ -9,6 +9,7 @@
margin: 0;
float: right;
cursor: pointer;
padding: 8px 0 8px 8px;
}
.toast-message-holder{

View file

@ -0,0 +1,91 @@
// jQuery Scrollstop Plugin v1.2.0
// https://github.com/ssorallen/jquery-scrollstop
(function (factory) {
// UMD[2] wrapper for jQuery plugins to work in AMD or in CommonJS.
//
// [2] https://github.com/umdjs/umd
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// Node/CommonJS
module.exports = factory(require('jquery'));
} else {
// Browser globals
factory(jQuery);
}
}(function ($) {
// $.event.dispatch was undocumented and was deprecated in jQuery 1.7[1]. It
// was replaced by $.event.handle in jQuery 1.9.
//
// Use the first of the available functions to support jQuery <1.8.
//
// [1] https://github.com/jquery/jquery-migrate/blob/master/src/event.js#L25
var dispatch = $.event.dispatch || $.event.handle;
var special = $.event.special,
uid1 = 'D' + (+new Date()),
uid2 = 'D' + (+new Date() + 1);
special.scrollstart = {
setup: function(data) {
var _data = $.extend({
latency: special.scrollstop.latency
}, data);
var timer,
handler = function(evt) {
var _self = this,
_args = arguments;
if (timer) {
clearTimeout(timer);
} else {
evt.type = 'scrollstart';
dispatch.apply(_self, _args);
}
timer = setTimeout(function() {
timer = null;
}, _data.latency);
};
$(this).bind('scroll', handler).data(uid1, handler);
},
teardown: function() {
$(this).unbind('scroll', $(this).data(uid1));
}
};
special.scrollstop = {
latency: 250,
setup: function(data) {
var _data = $.extend({
latency: special.scrollstop.latency
}, data);
var timer,
handler = function(evt) {
var _self = this,
_args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function() {
timer = null;
evt.type = 'scrollstop';
dispatch.apply(_self, _args);
}, _data.latency);
};
$(this).bind('scroll', handler).data(uid2, handler);
},
teardown: function() {
$(this).unbind('scroll', $(this).data(uid2));
}
};
}));

View file

@ -0,0 +1,171 @@
/**
* Within Viewport jQuery Plugin
*
* @description Companion plugin for withinviewport.js - determines whether an element is completely within the browser viewport
* @author Craig Patik, http://patik.com/
* @version 2.1.2
* @date 2019-08-16
*/
(function ($) {
/**
* $.withinviewport()
* @description jQuery method
* @param {Object} [settings] optional settings
* @return {Collection} Contains all elements that were within the viewport
*/
$.fn.withinviewport = function (settings) {
var opts;
var elems;
if (typeof settings === 'string') {
settings = {
sides: settings
};
}
opts = $.extend({}, settings, {
sides: 'all'
});
elems = [];
this.each(function () {
if (withinviewport(this, opts)) {
elems.push(this);
}
});
return $(elems);
};
// Main custom selector
$.extend($.expr[':'], {
'within-viewport': function (element) {
return withinviewport(element, 'all');
}
});
/**
* Optional enhancements and shortcuts
*
* @description Uncomment or comment these pieces as they apply to your project and coding preferences
*/
// Shorthand jQuery methods
$.fn.withinviewporttop = function (settings) {
var opts;
var elems;
if (typeof settings === 'string') {
settings = {
sides: settings
};
}
opts = $.extend({}, settings, {
sides: 'top'
});
elems = [];
this.each(function () {
if (withinviewport(this, opts)) {
elems.push(this);
}
});
return $(elems);
};
$.fn.withinviewportright = function (settings) {
var opts;
var elems;
if (typeof settings === 'string') {
settings = {
sides: settings
};
}
opts = $.extend({}, settings, {
sides: 'right'
});
elems = [];
this.each(function () {
if (withinviewport(this, opts)) {
elems.push(this);
}
});
return $(elems);
};
$.fn.withinviewportbottom = function (settings) {
var opts;
var elems;
if (typeof settings === 'string') {
settings = {
sides: settings
};
}
opts = $.extend({}, settings, {
sides: 'bottom'
});
elems = [];
this.each(function () {
if (withinviewport(this, opts)) {
elems.push(this);
}
});
return $(elems);
};
$.fn.withinviewportleft = function (settings) {
var opts;
var elems;
if (typeof settings === 'string') {
settings = {
sides: settings
};
}
opts = $.extend({}, settings, {
sides: 'left'
});
elems = [];
this.each(function () {
if (withinviewport(this, opts)) {
elems.push(this);
}
});
return $(elems);
};
// Custom jQuery selectors
$.extend($.expr[':'], {
'within-viewport-top': function (element) {
return withinviewport(element, 'top');
},
'within-viewport-right': function (element) {
return withinviewport(element, 'right');
},
'within-viewport-bottom': function (element) {
return withinviewport(element, 'bottom');
},
'within-viewport-left': function (element) {
return withinviewport(element, 'left');
}
// Example custom selector:
//,
// 'within-viewport-top-left-45': function (element) {
// return withinviewport(element, {sides:'top left', top: 45, left: 45});
// }
});
}(jQuery));

View file

@ -0,0 +1,235 @@
/**
* Within Viewport
*
* @description Determines whether an element is completely within the browser viewport
* @author Craig Patik, http://patik.com/
* @version 2.1.2
* @date 2019-08-16
*/
(function (root, name, factory) {
// AMD
if (typeof define === 'function' && define.amd) {
define([], factory);
}
// Node and CommonJS-like environments
else if (typeof module !== 'undefined' && typeof exports === 'object') {
module.exports = factory();
}
// Browser global
else {
root[name] = factory();
}
}(this, 'withinviewport', function () {
var canUseWindowDimensions = typeof window !== 'undefined' && window.innerHeight !== undefined; // IE 8 and lower fail this
/**
* Determines whether an element is within the viewport
* @param {Object} elem DOM Element (required)
* @param {Object} options Optional settings
* @return {Boolean} Whether the element was completely within the viewport
*/
var withinviewport = function withinviewport(elem, options) {
var result = false;
var metadata = {};
var config = {};
var settings;
var isWithin;
var isContainerTheWindow;
var elemBoundingRect;
var containerBoundingRect;
var containerScrollTop;
var containerScrollLeft;
var scrollBarWidths = [0, 0];
var sideNamesPattern;
var sides;
var side;
var i;
// If invoked by the jQuery plugin, get the actual DOM element
if (typeof jQuery !== 'undefined' && elem instanceof jQuery) {
elem = elem.get(0);
}
if (typeof elem !== 'object' || elem.nodeType !== 1) {
throw new Error('First argument must be an element');
}
// Look for inline settings on the element
if (elem.getAttribute('data-withinviewport-settings') && window.JSON) {
metadata = JSON.parse(elem.getAttribute('data-withinviewport-settings'));
}
// Settings argument may be a simple string (`top`, `right`, etc)
if (typeof options === 'string') {
settings = {
sides: options
};
} else {
settings = options || {};
}
// Build configuration from defaults and user-provided settings and metadata
config.container = settings.container || metadata.container || withinviewport.defaults.container || window;
config.sides = settings.sides || metadata.sides || withinviewport.defaults.sides || 'all';
config.top = settings.top || metadata.top || withinviewport.defaults.top || 0;
config.right = settings.right || metadata.right || withinviewport.defaults.right || 0;
config.bottom = settings.bottom || metadata.bottom || withinviewport.defaults.bottom || 0;
config.left = settings.left || metadata.left || withinviewport.defaults.left || 0;
// Extract the DOM node from a jQuery collection
if (typeof jQuery !== 'undefined' && config.container instanceof jQuery) {
config.container = config.container.get(0);
}
// Use the window as the container if the user specified the body or a non-element
if (config.container === document.body || config.container.nodeType !== 1) {
config.container = window;
}
isContainerTheWindow = (config.container === window);
// Element testing methods
isWithin = {
// Element is below the top edge of the viewport
top: function _isWithin_top() {
if (isContainerTheWindow) {
return (elemBoundingRect.top >= config.top);
} else {
return (elemBoundingRect.top >= containerScrollTop - (containerScrollTop - containerBoundingRect.top) + config.top);
}
},
// Element is to the left of the right edge of the viewport
right: function _isWithin_right() {
// Note that `elemBoundingRect.right` is the distance from the *left* of the viewport to the element's far right edge
if (isContainerTheWindow) {
return (elemBoundingRect.right <= (containerBoundingRect.right + containerScrollLeft) - config.right);
} else {
return (elemBoundingRect.right <= containerBoundingRect.right - scrollBarWidths[0] - config.right);
}
},
// Element is above the bottom edge of the viewport
bottom: function _isWithin_bottom() {
var containerHeight = 0;
if (isContainerTheWindow) {
if (canUseWindowDimensions) {
containerHeight = config.container.innerHeight;
} else if (document && document.documentElement) {
containerHeight = document.documentElement.clientHeight;
}
} else {
containerHeight = containerBoundingRect.bottom;
}
// Note that `elemBoundingRect.bottom` is the distance from the *top* of the viewport to the element's bottom edge
return (elemBoundingRect.bottom <= containerHeight - scrollBarWidths[1] - config.bottom);
},
// Element is to the right of the left edge of the viewport
left: function _isWithin_left() {
if (isContainerTheWindow) {
return (elemBoundingRect.left >= config.left);
} else {
return (elemBoundingRect.left >= containerScrollLeft - (containerScrollLeft - containerBoundingRect.left) + config.left);
}
},
// Element is within all four boundaries
all: function _isWithin_all() {
// Test each boundary in order of efficiency and likeliness to be false. This way we can avoid running all four functions on most elements.
// 1. Top: Quickest to calculate + most likely to be false
// 2. Bottom: Note quite as quick to calculate, but also very likely to be false
// 3-4. Left and right are both equally unlikely to be false since most sites only scroll vertically, but left is faster to calculate
return (isWithin.top() && isWithin.bottom() && isWithin.left() && isWithin.right());
}
};
// Get the element's bounding rectangle with respect to the viewport
elemBoundingRect = elem.getBoundingClientRect();
// Get viewport dimensions and offsets
if (isContainerTheWindow) {
containerBoundingRect = document.documentElement.getBoundingClientRect();
containerScrollTop = document.body.scrollTop;
containerScrollLeft = window.scrollX || document.body.scrollLeft;
} else {
containerBoundingRect = config.container.getBoundingClientRect();
containerScrollTop = config.container.scrollTop;
containerScrollLeft = config.container.scrollLeft;
}
// Don't count the space consumed by scrollbars
if (containerScrollLeft) {
scrollBarWidths[0] = 18;
}
if (containerScrollTop) {
scrollBarWidths[1] = 16;
}
// Test the element against each side of the viewport that was requested
sideNamesPattern = /^top$|^right$|^bottom$|^left$|^all$/;
// Loop through all of the sides
sides = config.sides.split(' ');
i = sides.length;
while (i--) {
side = sides[i].toLowerCase();
if (sideNamesPattern.test(side)) {
if (isWithin[side]()) {
result = true;
} else {
result = false;
// Quit as soon as the first failure is found
break;
}
}
}
return result;
};
// Default settings
withinviewport.prototype.defaults = {
container: typeof document !== 'undefined' ? document.body : {},
sides: 'all',
top: 0,
right: 0,
bottom: 0,
left: 0
};
withinviewport.defaults = withinviewport.prototype.defaults;
/**
* Optional enhancements and shortcuts
*
* @description Uncomment or comment these pieces as they apply to your project and coding preferences
*/
// Shortcut methods for each side of the viewport
// Example: `withinviewport.top(elem)` is the same as `withinviewport(elem, 'top')`
withinviewport.prototype.top = function _withinviewport_top(element) {
return withinviewport(element, 'top');
};
withinviewport.prototype.right = function _withinviewport_right(element) {
return withinviewport(element, 'right');
};
withinviewport.prototype.bottom = function _withinviewport_bottom(element) {
return withinviewport(element, 'bottom');
};
withinviewport.prototype.left = function _withinviewport_left(element) {
return withinviewport(element, 'left');
};
return withinviewport;
}));

View file

@ -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,20 @@ 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 =
'';
$(editInfo).insertBefore($(this.editButton).parent());
}
if (resolvesCommentId){
this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
@ -153,17 +164,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 +298,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();
}
};
@ -475,18 +496,97 @@ var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
return CommentForm;
});
/* selector for comment versions */
var initVersionSelector = function(selector, initialData) {
var formatResult = function(result, container, query, escapeMarkup) {
return renderTemplate('commentVersion', {
show_disabled: true,
version: result.comment_version,
user_name: result.comment_author_username,
gravatar_url: result.comment_author_gravatar,
size: 16,
timeago_component: result.comment_created_on,
})
};
$(selector).select2({
placeholder: "Edited",
containerCssClass: "drop-menu-comment-history",
dropdownCssClass: "drop-menu-dropdown",
dropdownAutoWidth: true,
minimumResultsForSearch: -1,
data: initialData,
formatResult: formatResult,
});
$(selector).on('select2-selecting', function (e) {
// hide the mast as we later do preventDefault()
$("#select2-drop-mask").click();
e.preventDefault();
e.choice.action();
});
$(selector).on("select2-open", function() {
timeagoActivate();
});
};
/* comments controller */
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 (comment_id, comment_history_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) {
SwalNoAnimation.fire({
html: data,
title: '',
});
};
failRenderCommit = function () {
SwalNoAnimation.fire({
html: 'Error while loading comment history',
title: '',
});
};
_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 +738,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 +880,234 @@ 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();
if (text === "") {
return;
}
if (old_comment_text == text) {
SwalNoAnimation.fire({
title: 'Unable to edit comment',
html: _gettext('Comment body was not changed.'),
});
return;
}
var excludeCancelBtn = false;
var submitEvent = true;
commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
commentForm.cm.setOption("readOnly", true);
// Read last version known
var versionSelector = $('#comment_versions_{0}'.format(comment_id));
var version = versionSelector.data('lastVersion');
if (!version) {
version = 0;
}
var postData = {
'text': text,
'f_path': f_path,
'line': lineno,
'comment_type': commentType,
'version': version,
'csrf_token': CSRF_TOKEN
};
var submitSuccessCallback = function(json_data) {
$form.remove();
$comment.show();
var postData = {
'text': text,
'renderer': $comment.attr('data-comment-renderer'),
'csrf_token': CSRF_TOKEN
};
/* Inject new edited version selector */
var updateCommentVersionDropDown = function () {
var versionSelectId = '#comment_versions_'+comment_id;
var preLoadVersionData = [
{
id: json_data['comment_version'],
text: "v{0}".format(json_data['comment_version']),
action: function () {
Rhodecode.comments.showVersion(
json_data['comment_id'],
json_data['comment_history_id']
)
},
comment_version: json_data['comment_version'],
comment_author_username: json_data['comment_author_username'],
comment_author_gravatar: json_data['comment_author_gravatar'],
comment_created_on: json_data['comment_created_on'],
},
]
if ($(versionSelectId).data('select2')) {
var oldData = $(versionSelectId).data('select2').opts.data.results;
$(versionSelectId).select2("destroy");
preLoadVersionData = oldData.concat(preLoadVersionData)
}
initVersionSelector(versionSelectId, {results: preLoadVersionData});
$comment.attr('data-comment-text', utf8ToB64(text));
var versionSelector = $('#comment_versions_'+comment_id);
// set lastVersion so we know our last edit version
versionSelector.data('lastVersion', json_data['comment_version'])
versionSelector.parent().show();
}
updateCommentVersionDropDown();
// by default we reset state of comment preserving the text
var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
var prefix = "Error while editing this 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);
// 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);
if (jqXHR.status == 409){
message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
ajaxErrorSwal(message, 'Comment version mismatch.');
} else {
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 +1132,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,

View file

@ -70,7 +70,7 @@ replacing '-' and '_' into spaces
* @param limit
* @returns {*[]}
*/
var getTitleAndDescription = function(sourceRef, elements, limit) {
var getTitleAndDescription = function(sourceRefType, sourceRef, elements, limit) {
var title = '';
var desc = '';
@ -85,7 +85,9 @@ var getTitleAndDescription = function(sourceRef, elements, limit) {
}
else {
// use reference name
title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
var normalizedRef = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter()
var refType = sourceRefType;
title = 'Changes from {0}: {1}'.format(refType, normalizedRef);
}
return [title, desc]

View file

@ -130,10 +130,13 @@ function formatErrorMessage(jqXHR, textStatus, errorThrown, prefix) {
}
}
function ajaxErrorSwal(message) {
function ajaxErrorSwal(message, title) {
var title = (typeof title !== 'undefined') ? title : _gettext('Ajax Request Error');
SwalNoAnimation.fire({
icon: 'error',
title: _gettext('Ajax Request Error'),
title: title,
html: '<span style="white-space: pre-line">{0}</span>'.format(message),
showClass: {
popup: 'swal2-noanimation',

View file

@ -182,3 +182,13 @@ 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(''));
}
function utf8ToB64( str ) {
return window.btoa(unescape(encodeURIComponent( str )));
}

View file

@ -11,9 +11,15 @@
<div class="panel-body">
<div class="apikeys_wrap">
<p>
${_('Authentication tokens can be used to interact with the API, or VCS-over-http. '
'Each token can have a role. Token with a role can be used only in given context, '
'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
${_('Available roles')}:
<ul>
% for role in h.UserApiKeys.ROLES:
<li>
<span class="tag disabled">${h.UserApiKeys._get_role_name(role)}</span>
<span>${h.UserApiKeys._get_role_description(role) |n}</span>
</li>
% endfor
</ul>
</p>
<table class="rctable auth_tokens">
<tr>
@ -36,7 +42,7 @@
</td>
<td class="td-wrap">${auth_token.description}</td>
<td class="td-tags">
<span class="tag disabled">${auth_token.role_humanized}</span>
<span class="tooltip tag disabled" title="${h.UserApiKeys._get_role_description(auth_token.role)}">${auth_token.role_humanized}</span>
</td>
<td class="td">${auth_token.scope_humanized}</td>
<td class="td-exp">

View file

@ -44,7 +44,12 @@
<label for="group_parent_id">${_('Repository group')}:</label>
</div>
<div class="select">
${h.select('group_parent_id',request.GET.get('parent_group'),c.repo_groups,class_="medium")}
${h.select('group_parent_id', request.GET.get('parent_group'),c.repo_groups,class_="medium")}
% if c.personal_repo_group:
<a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}">
${_('Select my personal group ({})').format(c.personal_repo_group.group_name)}
</a>
% endif
</div>
</div>
@ -106,6 +111,12 @@
setCopyPermsOption(e.val)
});
$('#group_name').focus();
$('#select_my_group').on('click', function(e){
e.preventDefault();
$("#group_parent_id").val($(this).data('personalGroupId')).trigger("change");
})
})
</script>
</%def>

View file

@ -68,10 +68,15 @@
<span class="user-perm-help-text"> - ${_('permission for other logged in users')}</span>
% endif
% else:
${h.link_to_user(_user.username)}
%if getattr(_user, 'duplicate_perm', None):
(${_('inactive duplicate')})
%endif
% if getattr(_user, 'duplicate_perm', None):
<span class="user-perm-duplicate">
${h.link_to_user(_user.username)}
<span class="tooltip" title="${_('This entry is a duplicate, most probably left-over from previously set permission. This user has a higher permission set, so this entry is inactive. Please revoke this permission manually.')}">(${_('inactive duplicate')})
</span>
</span>
% else:
${h.link_to_user(_user.username)}
% endif
% endif
</span>
</td>
@ -116,10 +121,15 @@
<span class="user-perm-help-text"> - ${_('permission for other logged in users')}</span>
% endif
% else:
${h.link_to_user(_user.username)}
%if getattr(_user, 'duplicate_perm', None):
(${_('inactive duplicate')})
%endif
% if getattr(_user, 'duplicate_perm', None):
<span class="user-perm-duplicate">
${h.link_to_user(_user.username)}
<span class="tooltip" title="${_('This entry is a duplicate, most probably left-over from previously set permission. This user has a higher permission set, so this entry is inactive. Please revoke this permission manually.')}">(${_('inactive duplicate')})
</span>
</span>
% else:
${h.link_to_user(_user.username)}
% endif
% endif
<span class="user-perm-help-text">(${_('delegated admin')})</span>
</span>

View file

@ -46,7 +46,7 @@ ${h.secure_form(h.route_path('repo_create'), request=request)}
${h.select('repo_group',request.GET.get('parent_group'),c.repo_groups,class_="medium")}
% if c.personal_repo_group:
<a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}">
${_('Select my personal group (%(repo_group_name)s)') % {'repo_group_name': c.personal_repo_group.group_name}}
${_('Select my personal group ({})').format(c.personal_repo_group.group_name)}
</a>
% endif
<span class="help-block">${_('Optionally select a group to put this repository into.')}</span>

View file

@ -167,11 +167,16 @@
<div style="margin: 0 0 20px 0" class="fake-space"></div>
<div class="field">
% if c.rhodecode_db_repo.archived:
This repository is already archived. Only super-admin users can un-archive this repository.
% else:
<button class="btn btn-small btn-warning" type="submit"
onclick="submitConfirm(event, this, _gettext('Confirm to archive this repository'), _gettext('Archive'), '${c.rhodecode_db_repo.repo_name}')"
>
${_('Archive this repository')}
</button>
% endif
</div>
<div class="field">
<span class="help-block">

View file

@ -94,10 +94,16 @@
<span class="user-perm-help-text"> - ${_('permission for other logged in users')}</span>
% endif
% else:
${h.link_to_user(_user.username)}
%if getattr(_user, 'duplicate_perm', None):
(${_('inactive duplicate')})
%endif
% if getattr(_user, 'duplicate_perm', None):
<span class="user-perm-duplicate">
${h.link_to_user(_user.username)}
<span class="tooltip" title="${_('This entry is a duplicate, most probably left-over from previously set permission. This user has a higher permission set, so this entry is inactive. Please revoke this permission manually.')}">(${_('inactive duplicate')})
</span>
</span>
% else:
${h.link_to_user(_user.username)}
% endif
%if getattr(_user, 'branch_rules', None):
% if used_by_n_rules == 1:
(${_('used by {} branch rule, requires write+ permissions').format(used_by_n_rules)})

View file

@ -74,10 +74,15 @@
<span class="user-perm-help-text"> - ${_('permission for other logged in users')}</span>
% endif
% else:
${h.link_to_user(_user.username)}
%if getattr(_user, 'duplicate_perm', None):
(${_('inactive duplicate')})
%endif
% if getattr(_user, 'duplicate_perm', None):
<span class="user-perm-duplicate">
${h.link_to_user(_user.username)}
<span class="tooltip" title="${_('This entry is a duplicate, most probably left-over from previously set permission. This user has a higher permission set, so this entry is inactive. Please revoke this permission manually.')}">(${_('inactive duplicate')})
</span>
</span>
% else:
${h.link_to_user(_user.username)}
% endif
% endif
</span>
</td>
@ -122,10 +127,15 @@
<span class="user-perm-help-text"> - ${_('permission for other logged in users')}</span>
% endif
% else:
${h.link_to_user(_user.username)}
%if getattr(_user, 'duplicate_perm', None):
(${_('inactive duplicate')})
%endif
% if getattr(_user, 'duplicate_perm', None):
<span class="user-perm-duplicate">
${h.link_to_user(_user.username)}
<span class="tooltip" title="${_('This entry is a duplicate, most probably left-over from previously set permission. This user has a higher permission set, so this entry is inactive. Please revoke this permission manually.')}">(${_('inactive duplicate')})
</span>
</span>
% else:
${h.link_to_user(_user.username)}
% endif
% endif
<span class="user-perm-help-text">(${_('delegated admin')})</span>
</span>

View file

@ -27,8 +27,8 @@
<%def name="main()">
<div class="box user_settings">
% if not c.user.active:
<div class="alert alert-warning text-center">
<strong>${_('This user is set as disabled')}</strong>
<div class="alert alert-warning text-center" style="margin: 0 0 15px 0">
<strong>${_('This user is set as non-active and disabled.')}</strong>
</div>
% endif

View file

@ -16,9 +16,15 @@
<div class="panel-body">
<div class="apikeys_wrap">
<p>
${_('Authentication tokens can be used to interact with the API, or VCS-over-http. '
'Each token can have a role. Token with a role can be used only in given context, '
'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
${_('Available roles')}:
<ul>
% for role in h.UserApiKeys.ROLES:
<li>
<span class="tag disabled">${h.UserApiKeys._get_role_name(role)}</span>
<span>${h.UserApiKeys._get_role_description(role) |n}</span>
</li>
% endfor
</ul>
</p>
<table class="rctable auth_tokens">
<tr>
@ -41,7 +47,7 @@
</td>
<td class="td-wrap">${auth_token.description}</td>
<td class="td-tags">
<span class="tag disabled">${auth_token.role_humanized}</span>
<span class="tooltip tag disabled" title="${h.UserApiKeys._get_role_description(auth_token.role)}">${auth_token.role_humanized}</span>
</td>
<td class="td">${auth_token.scope_humanized}</td>
<td class="td-exp">

View file

@ -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"/>
@ -247,7 +243,9 @@
<div class="${_class}">
${self.gravatar(email, size, tooltip=tooltip, tooltip_alt=contact, user=rc_user)}
<span class="${('user user-disabled' if show_disabled else 'user')}"> ${h.link_to_user(rc_user or contact)}</span>
<span class="${('user user-disabled' if show_disabled else 'user')}">
${h.link_to_user(rc_user or contact)}
</span>
</div>
</%def>
@ -396,7 +394,7 @@
</a>
</li>
%if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
%if not c.rhodecode_db_repo.archived and h.HasRepoPermissionAll('repository.admin')(c.repo_name):
<li class="${h.is_active('settings', active)}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li>
%endif
@ -510,7 +508,7 @@
## create action
<li>
<a href="#create-actions" onclick="return false;" class="menulink childs">
<i class="tooltip icon-plus-circled" title="${_('Create')}"></i>
<i class="icon-plus-circled"></i>
</a>
<div class="action-menu submenu">
@ -1132,6 +1130,19 @@
};
ajaxPOST(url, postData, success, failure);
}
var hideLicenseWarning = function () {
var fingerprint = templateContext.session_attrs.license_fingerprint;
storeUserSessionAttr('rc_user_session_attr.hide_license_warning', fingerprint);
$('#notifications').hide();
}
var hideLicenseError = function () {
var fingerprint = templateContext.session_attrs.license_fingerprint;
storeUserSessionAttr('rc_user_session_attr.hide_license_error', fingerprint);
$('#notifications').hide();
}
</script>
<script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
</%def>

Some files were not shown because too many files have changed in this diff Show more