release: Merge default into stable for release preparation
This commit is contained in:
commit
b7f2caca4c
83 changed files with 2144 additions and 970 deletions
|
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 4.22.0
|
||||
current_version = 4.23.0
|
||||
message = release: Bump version {current_version} to {new_version}
|
||||
|
||||
[bumpversion:file:rhodecode/VERSION]
|
||||
|
|
|
|||
13
.release.cfg
13
.release.cfg
|
|
@ -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.22.0
|
||||
|
||||
[task:updated_translation]
|
||||
|
||||
[release]
|
||||
state = in_progress
|
||||
version = 4.23.0
|
||||
|
||||
[task:generate_js_routes]
|
||||
|
||||
[task:updated_trial_license]
|
||||
|
|
|
|||
89
docs/release-notes/release-notes-4.23.0.rst
Normal file
89
docs/release-notes/release-notes-4.23.0.rst
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
|RCE| 4.23.0 |RNS|
|
||||
------------------
|
||||
|
||||
Release Date
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- 2020-11-20
|
||||
|
||||
|
||||
New Features
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- Comments: introduced new draft comments.
|
||||
|
||||
* drafts are private to author
|
||||
* not triggering any notifications
|
||||
* sidebar doesn't display draft comments
|
||||
* They are just placeholders for longer review.
|
||||
|
||||
- Comments: when channelstream is enabled, comments are pushed live, so there's no
|
||||
need to refresh page to see other participant comments.
|
||||
New comments are marker in the sidebar.
|
||||
|
||||
- Comments: multiple changes on comments navigation/display logic.
|
||||
|
||||
* toggle icon is smarter, open/hide windows according to actions. E.g commenting opens threads
|
||||
* toggle are mor explicit
|
||||
* possible to hide/show only single threads using the toggle icon.
|
||||
* new UI for showing thread comments
|
||||
|
||||
- Reviewers: new logic for author/commit-author rules.
|
||||
It's not possible to define if author or commit author should be excluded, or always included in a review.
|
||||
- Reviewers: no reviewers would now allow a PR to be merged, unless review rules require some.
|
||||
Use case is that pr can be created without review needed, maybe just for sharing, or CI checks
|
||||
- Pull requests: save permanently the state if sorting columns for pull-request grids.
|
||||
- Commit ranges: enable combined diff compare directly from range selector.
|
||||
|
||||
|
||||
General
|
||||
^^^^^^^
|
||||
|
||||
- Authentication: enable custom names for auth plugins. It's possible to name the authentication
|
||||
buttons now for SAML plugins.
|
||||
- Login: optimized UI for login/register/password reset windows.
|
||||
- Repo mapper: make it more resilient to errors, it's better it executes and skip certain
|
||||
repositories, rather then crash whole mapper.
|
||||
- Markdown: improved styling, and fixed nl2br extensions to only do br on new elements not inline.
|
||||
- Pull requests: show pr version in the my-account and repo pr listing grids.
|
||||
- Archives: allowing to obtain archives without the commit short id in the name for
|
||||
better automation of obtained artifacts.
|
||||
New url flag called `?=with_hash=1` controls this
|
||||
- Error document: update info about stored exception retrieval.
|
||||
- Range diff: enable hovercards for commits in range-diff.
|
||||
|
||||
|
||||
Security
|
||||
^^^^^^^^
|
||||
|
||||
|
||||
|
||||
Performance
|
||||
^^^^^^^^^^^
|
||||
|
||||
- Improved logic of repo archive, now it's much faster to run archiver as VCSServer
|
||||
communication was removed, and job is delegated to VCSServer itself.
|
||||
- Improved VCSServer startup times.
|
||||
- Notifications: skip double rendering just to generate email title/desc.
|
||||
We'll re-use those now for better performance of creating notifications.
|
||||
- App: improve logging, and remove DB calls on app startup.
|
||||
|
||||
|
||||
Fixes
|
||||
^^^^^
|
||||
|
||||
- Login/register: fixed header width problem on mobile devices
|
||||
- Exception tracker: don't fail on empty request in context of celery app for example.
|
||||
- Exceptions: improved reporting of unhandled vcsserver exceptions.
|
||||
- Sidebar: fixed refresh of TODOs url.
|
||||
- Remap-rescan: fixes #5636 initial rescan problem.
|
||||
- API: fixed SVN raw diff export. The API method was inconsistent, and used different logic.
|
||||
Now it shares the same code as raw-diff from web-ui.
|
||||
|
||||
|
||||
Upgrade notes
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
- Scheduled feature release.
|
||||
Please note that now the reviewers logic changed a bit, it's possible to create a pull request
|
||||
Without any reviewers initially, and such pull request doesn't need to have an approval for merging.
|
||||
|
|
@ -9,6 +9,7 @@ Release Notes
|
|||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
release-notes-4.23.0.rst
|
||||
release-notes-4.22.0.rst
|
||||
release-notes-4.21.0.rst
|
||||
release-notes-4.20.1.rst
|
||||
|
|
|
|||
|
|
@ -1883,7 +1883,7 @@ self: super: {
|
|||
};
|
||||
};
|
||||
"rhodecode-enterprise-ce" = super.buildPythonPackage {
|
||||
name = "rhodecode-enterprise-ce-4.22.0";
|
||||
name = "rhodecode-enterprise-ce-4.23.0";
|
||||
buildInputs = [
|
||||
self."pytest"
|
||||
self."py"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
4.22.0
|
||||
4.23.0
|
||||
|
|
@ -48,7 +48,7 @@ PYRAMID_SETTINGS = {}
|
|||
EXTENSIONS = {}
|
||||
|
||||
__version__ = ('.'.join((str(each) for each in VERSION[:3])))
|
||||
__dbversion__ = 110 # defines current db version for migrations
|
||||
__dbversion__ = 112 # defines current db version for migrations
|
||||
__platform__ = platform.system()
|
||||
__license__ = 'AGPLv3, and Commercial License'
|
||||
__author__ = 'RhodeCode GmbH'
|
||||
|
|
|
|||
|
|
@ -351,7 +351,10 @@ def get_pull_request_or_error(pullrequestid):
|
|||
return pull_request
|
||||
|
||||
|
||||
def build_commit_data(commit, detail_level):
|
||||
def build_commit_data(rhodecode_vcs_repo, commit, detail_level):
|
||||
commit2 = commit
|
||||
commit1 = commit.first_parent
|
||||
|
||||
parsed_diff = []
|
||||
if detail_level == 'extended':
|
||||
for f_path in commit.added_paths:
|
||||
|
|
@ -362,8 +365,11 @@ def build_commit_data(commit, detail_level):
|
|||
parsed_diff.append(_get_commit_dict(filename=f_path, op='D'))
|
||||
|
||||
elif detail_level == 'full':
|
||||
from rhodecode.lib.diffs import DiffProcessor
|
||||
diff_processor = DiffProcessor(commit.diff())
|
||||
from rhodecode.lib import diffs
|
||||
|
||||
_diff = rhodecode_vcs_repo.get_diff(commit1, commit2,)
|
||||
diff_processor = diffs.DiffProcessor(_diff, format='newdiff', show_full_diff=True)
|
||||
|
||||
for dp in diff_processor.prepare():
|
||||
del dp['stats']['ops']
|
||||
_stats = dp['stats']
|
||||
|
|
|
|||
|
|
@ -317,17 +317,18 @@ def get_repo_changeset(request, apiuser, repoid, revision,
|
|||
'ret_type must be one of %s' % (
|
||||
','.join(_changes_details_types)))
|
||||
|
||||
vcs_repo = repo.scm_instance()
|
||||
pre_load = ['author', 'branch', 'date', 'message', 'parents',
|
||||
'status', '_commit', '_file_paths']
|
||||
|
||||
try:
|
||||
cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
|
||||
commit = repo.get_commit(commit_id=revision, pre_load=pre_load)
|
||||
except TypeError as e:
|
||||
raise JSONRPCError(safe_str(e))
|
||||
_cs_json = cs.__json__()
|
||||
_cs_json['diff'] = build_commit_data(cs, changes_details)
|
||||
_cs_json = commit.__json__()
|
||||
_cs_json['diff'] = build_commit_data(vcs_repo, commit, changes_details)
|
||||
if changes_details == 'full':
|
||||
_cs_json['refs'] = cs._get_refs()
|
||||
_cs_json['refs'] = commit._get_refs()
|
||||
return _cs_json
|
||||
|
||||
|
||||
|
|
@ -398,7 +399,7 @@ def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
|
|||
if cnt >= limit != -1:
|
||||
break
|
||||
_cs_json = commit.__json__()
|
||||
_cs_json['diff'] = build_commit_data(commit, changes_details)
|
||||
_cs_json['diff'] = build_commit_data(vcs_repo, commit, changes_details)
|
||||
if changes_details == 'full':
|
||||
_cs_json['refs'] = {
|
||||
'branches': [commit.branch],
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ from rhodecode.authentication.plugins import auth_rhodecode
|
|||
from rhodecode.events import trigger
|
||||
from rhodecode.model.db import true, UserNotice
|
||||
|
||||
from rhodecode.lib import audit_logger, rc_cache
|
||||
from rhodecode.lib import audit_logger, rc_cache, auth
|
||||
from rhodecode.lib.exceptions import (
|
||||
UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
|
||||
UserOwnsUserGroupsException, UserOwnsPullRequestsException,
|
||||
|
|
@ -295,6 +295,10 @@ class UsersView(UserAppView):
|
|||
c.allowed_extern_types = [
|
||||
(x.uid, x.get_display_name()) for x in self.get_auth_plugins()
|
||||
]
|
||||
perms = req.registry.settings.get('available_permissions')
|
||||
if not perms:
|
||||
# inject info about available permissions
|
||||
auth.set_available_permissions(req.registry.settings)
|
||||
|
||||
c.available_permissions = req.registry.settings['available_permissions']
|
||||
PermissionModel().set_global_permission_choices(
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ But please check this code
|
|||
var comment = $('#comment-'+commentId);
|
||||
var commentData = comment.data();
|
||||
if (commentData.commentInline) {
|
||||
this.createComment(comment, commentId)
|
||||
this.createComment(comment, f_path, line_no, commentId)
|
||||
} else {
|
||||
Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -702,7 +702,9 @@ class MyAccountView(BaseAppView, DataGridAppView):
|
|||
**valid_data)
|
||||
if old_email != valid_data['email']:
|
||||
old = UserEmailMap.query() \
|
||||
.filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first()
|
||||
.filter(UserEmailMap.user == c.user)\
|
||||
.filter(UserEmailMap.email == valid_data['email'])\
|
||||
.first()
|
||||
old.email = old_email
|
||||
h.flash(_('Your account was updated successfully'), category='success')
|
||||
Session().commit()
|
||||
|
|
@ -718,6 +720,7 @@ class MyAccountView(BaseAppView, DataGridAppView):
|
|||
def _get_pull_requests_list(self, statuses):
|
||||
draw, start, limit = self._extract_chunk(self.request)
|
||||
search_q, order_by, order_dir = self._extract_ordering(self.request)
|
||||
|
||||
_render = self.request.get_partial_renderer(
|
||||
'rhodecode:templates/data_table/_dt_elements.mako')
|
||||
|
||||
|
|
@ -735,7 +738,7 @@ class MyAccountView(BaseAppView, DataGridAppView):
|
|||
for pr in pull_requests:
|
||||
repo_id = pr.target_repo_id
|
||||
comments_count = comments_model.get_all_comments(
|
||||
repo_id, pull_request=pr, count_only=True)
|
||||
repo_id, pull_request=pr, include_drafts=False, count_only=True)
|
||||
owned = pr.user_id == self._rhodecode_user.user_id
|
||||
|
||||
data.append({
|
||||
|
|
@ -751,7 +754,8 @@ class MyAccountView(BaseAppView, DataGridAppView):
|
|||
'title': _render('pullrequest_title', pr.title, pr.description),
|
||||
'description': h.escape(pr.description),
|
||||
'updated_on': _render('pullrequest_updated_on',
|
||||
h.datetime_to_time(pr.updated_on)),
|
||||
h.datetime_to_time(pr.updated_on),
|
||||
pr.versions_count),
|
||||
'updated_on_raw': h.datetime_to_time(pr.updated_on),
|
||||
'created_on': _render('pullrequest_updated_on',
|
||||
h.datetime_to_time(pr.created_on)),
|
||||
|
|
|
|||
|
|
@ -355,6 +355,11 @@ def includeme(config):
|
|||
pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/todos',
|
||||
repo_route=True)
|
||||
|
||||
config.add_route(
|
||||
name='pullrequest_drafts',
|
||||
pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/drafts',
|
||||
repo_route=True)
|
||||
|
||||
# Artifacts, (EE feature)
|
||||
config.add_route(
|
||||
name='repo_artifacts_list',
|
||||
|
|
|
|||
|
|
@ -608,23 +608,23 @@ class TestPullrequestsView(object):
|
|||
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
|
||||
for comment_id in response.json.keys():
|
||||
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()
|
||||
|
|
@ -644,27 +644,27 @@ class TestPullrequestsView(object):
|
|||
)
|
||||
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',
|
||||
},
|
||||
for comment_id in response.json.keys():
|
||||
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
|
||||
)
|
||||
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()
|
||||
|
|
@ -684,26 +684,25 @@ class TestPullrequestsView(object):
|
|||
)
|
||||
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,
|
||||
for comment_id in response.json.keys():
|
||||
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
|
||||
)
|
||||
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()
|
||||
|
|
@ -722,48 +721,46 @@ class TestPullrequestsView(object):
|
|||
extra_environ=xhr_header,
|
||||
)
|
||||
assert response.json
|
||||
comment_id = response.json.get('comment_id', None)
|
||||
assert comment_id
|
||||
for comment_id in response.json.keys():
|
||||
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 = '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
|
||||
|
||||
)
|
||||
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
|
||||
|
||||
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
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ from rhodecode.model.pull_request import get_diff_info
|
|||
from rhodecode.model.db import PullRequestReviewers
|
||||
# V3 - Reviewers, with default rules data
|
||||
# v4 - Added observers metadata
|
||||
REVIEWER_API_VERSION = 'V4'
|
||||
# v5 - pr_author/commit_author include/exclude logic
|
||||
REVIEWER_API_VERSION = 'V5'
|
||||
|
||||
|
||||
def reviewer_as_json(user, reasons=None, role=None, mandatory=False, rules=None, user_group=None):
|
||||
|
|
@ -88,6 +89,7 @@ def get_default_reviewers_data(current_user, source_repo, source_ref, target_rep
|
|||
'reviewers': json_reviewers,
|
||||
'rules': {},
|
||||
'rules_data': {},
|
||||
'rules_humanized': [],
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ class RepoCommitsView(RepoAppView):
|
|||
_ = self.request.translate
|
||||
c = self.load_default_context()
|
||||
c.fulldiff = self.request.GET.get('fulldiff')
|
||||
redirect_to_combined = str2bool(self.request.GET.get('redirect_combined'))
|
||||
|
||||
# fetch global flags of ignore ws or context lines
|
||||
diff_context = get_diff_context(self.request)
|
||||
|
|
@ -117,6 +118,19 @@ class RepoCommitsView(RepoAppView):
|
|||
raise HTTPNotFound()
|
||||
single_commit = len(c.commit_ranges) == 1
|
||||
|
||||
if redirect_to_combined and not single_commit:
|
||||
source_ref = getattr(c.commit_ranges[0].parents[0]
|
||||
if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id')
|
||||
target_ref = c.commit_ranges[-1].raw_id
|
||||
next_url = h.route_path(
|
||||
'repo_compare',
|
||||
repo_name=c.repo_name,
|
||||
source_ref_type='rev',
|
||||
source_ref=source_ref,
|
||||
target_ref_type='rev',
|
||||
target_ref=target_ref)
|
||||
raise HTTPFound(next_url)
|
||||
|
||||
c.changes = OrderedDict()
|
||||
c.lines_added = 0
|
||||
c.lines_deleted = 0
|
||||
|
|
@ -366,6 +380,121 @@ class RepoCommitsView(RepoAppView):
|
|||
commit_id = self.request.matchdict['commit_id']
|
||||
return self._commit(commit_id, method='download')
|
||||
|
||||
def _commit_comments_create(self, commit_id, comments):
|
||||
_ = self.request.translate
|
||||
data = {}
|
||||
if not comments:
|
||||
return
|
||||
|
||||
commit = self.db_repo.get_commit(commit_id)
|
||||
|
||||
all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
|
||||
for entry in comments:
|
||||
c = self.load_default_context()
|
||||
comment_type = entry['comment_type']
|
||||
text = entry['text']
|
||||
status = entry['status']
|
||||
is_draft = str2bool(entry['is_draft'])
|
||||
resolves_comment_id = entry['resolves_comment_id']
|
||||
f_path = entry['f_path']
|
||||
line_no = entry['line']
|
||||
target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
|
||||
|
||||
if status:
|
||||
text = text or (_('Status change %(transition_icon)s %(status)s')
|
||||
% {'transition_icon': '>',
|
||||
'status': ChangesetStatus.get_status_lbl(status)})
|
||||
|
||||
comment = CommentsModel().create(
|
||||
text=text,
|
||||
repo=self.db_repo.repo_id,
|
||||
user=self._rhodecode_db_user.user_id,
|
||||
commit_id=commit_id,
|
||||
f_path=f_path,
|
||||
line_no=line_no,
|
||||
status_change=(ChangesetStatus.get_status_lbl(status)
|
||||
if status else None),
|
||||
status_change_type=status,
|
||||
comment_type=comment_type,
|
||||
is_draft=is_draft,
|
||||
resolves_comment_id=resolves_comment_id,
|
||||
auth_user=self._rhodecode_user,
|
||||
send_email=not is_draft, # skip notification for draft comments
|
||||
)
|
||||
is_inline = comment.is_inline
|
||||
|
||||
# get status if set !
|
||||
if status:
|
||||
# `dont_allow_on_closed_pull_request = True` means
|
||||
# if latest status was from pull request and it's closed
|
||||
# disallow changing status !
|
||||
|
||||
try:
|
||||
ChangesetStatusModel().set_status(
|
||||
self.db_repo.repo_id,
|
||||
status,
|
||||
self._rhodecode_db_user.user_id,
|
||||
comment,
|
||||
revision=commit_id,
|
||||
dont_allow_on_closed_pull_request=True
|
||||
)
|
||||
except StatusChangeOnClosedPullRequestError:
|
||||
msg = _('Changing the status of a commit associated with '
|
||||
'a closed pull request is not allowed')
|
||||
log.exception(msg)
|
||||
h.flash(msg, category='warning')
|
||||
raise HTTPFound(h.route_path(
|
||||
'repo_commit', repo_name=self.db_repo_name,
|
||||
commit_id=commit_id))
|
||||
|
||||
Session().flush()
|
||||
# this is somehow required to get access to some relationship
|
||||
# loaded on comment
|
||||
Session().refresh(comment)
|
||||
|
||||
# skip notifications for drafts
|
||||
if not is_draft:
|
||||
CommentsModel().trigger_commit_comment_hook(
|
||||
self.db_repo, self._rhodecode_user, 'create',
|
||||
data={'comment': comment, 'commit': commit})
|
||||
|
||||
comment_id = comment.comment_id
|
||||
data[comment_id] = {
|
||||
'target_id': target_elem_id
|
||||
}
|
||||
Session().flush()
|
||||
|
||||
c.co = comment
|
||||
c.at_version_num = 0
|
||||
c.is_new = True
|
||||
rendered_comment = render(
|
||||
'rhodecode:templates/changeset/changeset_comment_block.mako',
|
||||
self._get_template_context(c), self.request)
|
||||
|
||||
data[comment_id].update(comment.get_dict())
|
||||
data[comment_id].update({'rendered_text': rendered_comment})
|
||||
|
||||
# finalize, commit and redirect
|
||||
Session().commit()
|
||||
|
||||
# skip channelstream for draft comments
|
||||
if not all_drafts:
|
||||
comment_broadcast_channel = channelstream.comment_channel(
|
||||
self.db_repo_name, commit_obj=commit)
|
||||
|
||||
comment_data = data
|
||||
posted_comment_type = 'inline' if is_inline else 'general'
|
||||
if len(data) == 1:
|
||||
msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
|
||||
else:
|
||||
msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
|
||||
|
||||
channelstream.comment_channelstream_push(
|
||||
self.request, comment_broadcast_channel, self._rhodecode_user, msg,
|
||||
comment_data=comment_data)
|
||||
|
||||
return data
|
||||
|
||||
@LoginRequired()
|
||||
@NotAnonymous()
|
||||
@HasRepoPermissionAnyDecorator(
|
||||
|
|
@ -378,17 +507,6 @@ class RepoCommitsView(RepoAppView):
|
|||
_ = self.request.translate
|
||||
commit_id = self.request.matchdict['commit_id']
|
||||
|
||||
c = self.load_default_context()
|
||||
status = self.request.POST.get('changeset_status', None)
|
||||
text = self.request.POST.get('text')
|
||||
comment_type = self.request.POST.get('comment_type')
|
||||
resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
|
||||
|
||||
if status:
|
||||
text = text or (_('Status change %(transition_icon)s %(status)s')
|
||||
% {'transition_icon': '>',
|
||||
'status': ChangesetStatus.get_status_lbl(status)})
|
||||
|
||||
multi_commit_ids = []
|
||||
for _commit_id in self.request.POST.get('commit_ids', '').split(','):
|
||||
if _commit_id not in ['', None, EmptyCommit.raw_id]:
|
||||
|
|
@ -397,81 +515,23 @@ class RepoCommitsView(RepoAppView):
|
|||
|
||||
commit_ids = multi_commit_ids or [commit_id]
|
||||
|
||||
comment = None
|
||||
data = []
|
||||
# Multiple comments for each passed commit id
|
||||
for current_id in filter(None, commit_ids):
|
||||
comment = CommentsModel().create(
|
||||
text=text,
|
||||
repo=self.db_repo.repo_id,
|
||||
user=self._rhodecode_db_user.user_id,
|
||||
commit_id=current_id,
|
||||
f_path=self.request.POST.get('f_path'),
|
||||
line_no=self.request.POST.get('line'),
|
||||
status_change=(ChangesetStatus.get_status_lbl(status)
|
||||
if status else None),
|
||||
status_change_type=status,
|
||||
comment_type=comment_type,
|
||||
resolves_comment_id=resolves_comment_id,
|
||||
auth_user=self._rhodecode_user
|
||||
)
|
||||
is_inline = comment.is_inline
|
||||
comment_data = {
|
||||
'comment_type': self.request.POST.get('comment_type'),
|
||||
'text': self.request.POST.get('text'),
|
||||
'status': self.request.POST.get('changeset_status', None),
|
||||
'is_draft': self.request.POST.get('draft'),
|
||||
'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
|
||||
'close_pull_request': self.request.POST.get('close_pull_request'),
|
||||
'f_path': self.request.POST.get('f_path'),
|
||||
'line': self.request.POST.get('line'),
|
||||
}
|
||||
comment = self._commit_comments_create(commit_id=current_id, comments=[comment_data])
|
||||
data.append(comment)
|
||||
|
||||
# get status if set !
|
||||
if status:
|
||||
# if latest status was from pull request and it's closed
|
||||
# disallow changing status !
|
||||
# dont_allow_on_closed_pull_request = True !
|
||||
|
||||
try:
|
||||
ChangesetStatusModel().set_status(
|
||||
self.db_repo.repo_id,
|
||||
status,
|
||||
self._rhodecode_db_user.user_id,
|
||||
comment,
|
||||
revision=current_id,
|
||||
dont_allow_on_closed_pull_request=True
|
||||
)
|
||||
except StatusChangeOnClosedPullRequestError:
|
||||
msg = _('Changing the status of a commit associated with '
|
||||
'a closed pull request is not allowed')
|
||||
log.exception(msg)
|
||||
h.flash(msg, category='warning')
|
||||
raise HTTPFound(h.route_path(
|
||||
'repo_commit', repo_name=self.db_repo_name,
|
||||
commit_id=current_id))
|
||||
|
||||
commit = self.db_repo.get_commit(current_id)
|
||||
CommentsModel().trigger_commit_comment_hook(
|
||||
self.db_repo, self._rhodecode_user, 'create',
|
||||
data={'comment': comment, 'commit': commit})
|
||||
|
||||
# finalize, commit and redirect
|
||||
Session().commit()
|
||||
|
||||
data = {
|
||||
'target_id': h.safeid(h.safe_unicode(
|
||||
self.request.POST.get('f_path'))),
|
||||
}
|
||||
if comment:
|
||||
c.co = comment
|
||||
c.at_version_num = 0
|
||||
rendered_comment = render(
|
||||
'rhodecode:templates/changeset/changeset_comment_block.mako',
|
||||
self._get_template_context(c), self.request)
|
||||
|
||||
data.update(comment.get_dict())
|
||||
data.update({'rendered_text': rendered_comment})
|
||||
|
||||
comment_broadcast_channel = channelstream.comment_channel(
|
||||
self.db_repo_name, commit_obj=commit)
|
||||
|
||||
comment_data = data
|
||||
comment_type = 'inline' if is_inline else 'general'
|
||||
channelstream.comment_channelstream_push(
|
||||
self.request, comment_broadcast_channel, self._rhodecode_user,
|
||||
_('posted a new {} comment').format(comment_type),
|
||||
comment_data=comment_data)
|
||||
|
||||
return data
|
||||
return data if len(data) > 1 else data[0]
|
||||
|
||||
@LoginRequired()
|
||||
@NotAnonymous()
|
||||
|
|
@ -665,6 +725,7 @@ class RepoCommitsView(RepoAppView):
|
|||
def repo_commit_comment_edit(self):
|
||||
self.load_default_context()
|
||||
|
||||
commit_id = self.request.matchdict['commit_id']
|
||||
comment_id = self.request.matchdict['comment_id']
|
||||
comment = ChangesetComment.get_or_404(comment_id)
|
||||
|
||||
|
|
@ -717,11 +778,11 @@ class RepoCommitsView(RepoAppView):
|
|||
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})
|
||||
if not comment.draft:
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -325,6 +325,21 @@ class RepoFilesView(RepoAppView):
|
|||
|
||||
return lf_enabled
|
||||
|
||||
def _get_archive_name(self, db_repo_name, commit_sha, ext, subrepos=False, path_sha=''):
|
||||
# original backward compat name of archive
|
||||
clean_name = safe_str(db_repo_name.replace('/', '_'))
|
||||
|
||||
# e.g vcsserver.zip
|
||||
# e.g vcsserver-abcdefgh.zip
|
||||
# e.g vcsserver-abcdefgh-defghijk.zip
|
||||
archive_name = '{}{}{}{}{}'.format(
|
||||
clean_name,
|
||||
'-sub' if subrepos else '',
|
||||
commit_sha,
|
||||
'-{}'.format(path_sha) if path_sha else '',
|
||||
ext)
|
||||
return archive_name
|
||||
|
||||
@LoginRequired()
|
||||
@HasRepoPermissionAnyDecorator(
|
||||
'repository.read', 'repository.write', 'repository.admin')
|
||||
|
|
@ -339,6 +354,7 @@ class RepoFilesView(RepoAppView):
|
|||
default_at_path = '/'
|
||||
fname = self.request.matchdict['fname']
|
||||
subrepos = self.request.GET.get('subrepos') == 'true'
|
||||
with_hash = str2bool(self.request.GET.get('with_hash', '1'))
|
||||
at_path = self.request.GET.get('at_path') or default_at_path
|
||||
|
||||
if not self.db_repo.enable_downloads:
|
||||
|
|
@ -364,30 +380,30 @@ class RepoFilesView(RepoAppView):
|
|||
except Exception:
|
||||
return Response(_('No node at path {} for this repository').format(at_path))
|
||||
|
||||
path_sha = sha1(at_path)[:8]
|
||||
# path sha is part of subdir
|
||||
path_sha = ''
|
||||
if at_path != default_at_path:
|
||||
path_sha = sha1(at_path)[:8]
|
||||
short_sha = '-{}'.format(safe_str(commit.short_id))
|
||||
# used for cache etc
|
||||
archive_name = self._get_archive_name(
|
||||
self.db_repo_name, commit_sha=short_sha, ext=ext, subrepos=subrepos,
|
||||
path_sha=path_sha)
|
||||
|
||||
# original backward compat name of archive
|
||||
clean_name = safe_str(self.db_repo_name.replace('/', '_'))
|
||||
short_sha = safe_str(commit.short_id)
|
||||
if not with_hash:
|
||||
short_sha = ''
|
||||
path_sha = ''
|
||||
|
||||
if at_path == default_at_path:
|
||||
archive_name = '{}-{}{}{}'.format(
|
||||
clean_name,
|
||||
'-sub' if subrepos else '',
|
||||
short_sha,
|
||||
ext)
|
||||
# custom path and new name
|
||||
else:
|
||||
archive_name = '{}-{}{}-{}{}'.format(
|
||||
clean_name,
|
||||
'-sub' if subrepos else '',
|
||||
short_sha,
|
||||
path_sha,
|
||||
ext)
|
||||
# what end client gets served
|
||||
response_archive_name = self._get_archive_name(
|
||||
self.db_repo_name, commit_sha=short_sha, ext=ext, subrepos=subrepos,
|
||||
path_sha=path_sha)
|
||||
# remove extension from our archive directory name
|
||||
archive_dir_name = response_archive_name[:-len(ext)]
|
||||
|
||||
use_cached_archive = False
|
||||
archive_cache_enabled = CONFIG.get(
|
||||
'archive_cache_dir') and not self.request.GET.get('no_cache')
|
||||
archive_cache_dir = CONFIG.get('archive_cache_dir')
|
||||
archive_cache_enabled = archive_cache_dir and not self.request.GET.get('no_cache')
|
||||
cached_archive_path = None
|
||||
|
||||
if archive_cache_enabled:
|
||||
|
|
@ -403,12 +419,14 @@ class RepoFilesView(RepoAppView):
|
|||
else:
|
||||
log.debug('Archive %s is not yet cached', archive_name)
|
||||
|
||||
# generate new archive, as previous was not found in the cache
|
||||
if not use_cached_archive:
|
||||
# generate new archive
|
||||
fd, archive = tempfile.mkstemp()
|
||||
_dir = os.path.abspath(archive_cache_dir) if archive_cache_dir else None
|
||||
fd, archive = tempfile.mkstemp(dir=_dir)
|
||||
log.debug('Creating new temp archive in %s', archive)
|
||||
try:
|
||||
commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
|
||||
commit.archive_repo(archive, archive_dir_name=archive_dir_name,
|
||||
kind=fileformat, subrepos=subrepos,
|
||||
archive_at_path=at_path)
|
||||
except ImproperArchiveTypeError:
|
||||
return _('Unknown archive type')
|
||||
|
|
@ -445,8 +463,7 @@ class RepoFilesView(RepoAppView):
|
|||
yield data
|
||||
|
||||
response = Response(app_iter=get_chunked_archive(archive))
|
||||
response.content_disposition = str(
|
||||
'attachment; filename=%s' % archive_name)
|
||||
response.content_disposition = str('attachment; filename=%s' % response_archive_name)
|
||||
response.content_type = str(content_type)
|
||||
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ from rhodecode.lib.vcs.exceptions import (
|
|||
from rhodecode.model.changeset_status import ChangesetStatusModel
|
||||
from rhodecode.model.comment import CommentsModel
|
||||
from rhodecode.model.db import (
|
||||
func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
|
||||
func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
|
||||
PullRequestReviewers)
|
||||
from rhodecode.model.forms import PullRequestForm
|
||||
from rhodecode.model.meta import Session
|
||||
|
|
@ -107,7 +107,8 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
|||
comments_model = CommentsModel()
|
||||
for pr in pull_requests:
|
||||
comments_count = comments_model.get_all_comments(
|
||||
self.db_repo.repo_id, pull_request=pr, count_only=True)
|
||||
self.db_repo.repo_id, pull_request=pr,
|
||||
include_drafts=False, count_only=True)
|
||||
|
||||
data.append({
|
||||
'name': _render('pullrequest_name',
|
||||
|
|
@ -120,7 +121,8 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
|||
'title': _render('pullrequest_title', pr.title, pr.description),
|
||||
'description': h.escape(pr.description),
|
||||
'updated_on': _render('pullrequest_updated_on',
|
||||
h.datetime_to_time(pr.updated_on)),
|
||||
h.datetime_to_time(pr.updated_on),
|
||||
pr.versions_count),
|
||||
'updated_on_raw': h.datetime_to_time(pr.updated_on),
|
||||
'created_on': _render('pullrequest_updated_on',
|
||||
h.datetime_to_time(pr.created_on)),
|
||||
|
|
@ -268,12 +270,14 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
|||
|
||||
return diffset
|
||||
|
||||
def register_comments_vars(self, c, pull_request, versions):
|
||||
def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
|
||||
comments_model = CommentsModel()
|
||||
|
||||
# GENERAL COMMENTS with versions #
|
||||
q = comments_model._all_general_comments_of_pull_request(pull_request)
|
||||
q = q.order_by(ChangesetComment.comment_id.asc())
|
||||
if not include_drafts:
|
||||
q = q.filter(ChangesetComment.draft == false())
|
||||
general_comments = q
|
||||
|
||||
# pick comments we want to render at current version
|
||||
|
|
@ -283,6 +287,8 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
|||
# INLINE COMMENTS with versions #
|
||||
q = comments_model._all_inline_comments_of_pull_request(pull_request)
|
||||
q = q.order_by(ChangesetComment.comment_id.asc())
|
||||
if not include_drafts:
|
||||
q = q.filter(ChangesetComment.draft == false())
|
||||
inline_comments = q
|
||||
|
||||
c.inline_versions = comments_model.aggregate_comments(
|
||||
|
|
@ -422,16 +428,12 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
|||
c.allowed_to_close = c.allowed_to_merge and not pr_closed
|
||||
|
||||
c.forbid_adding_reviewers = False
|
||||
c.forbid_author_to_review = False
|
||||
c.forbid_commit_author_to_review = False
|
||||
|
||||
if pull_request_latest.reviewer_data and \
|
||||
'rules' in pull_request_latest.reviewer_data:
|
||||
rules = pull_request_latest.reviewer_data['rules'] or {}
|
||||
try:
|
||||
c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
|
||||
c.forbid_author_to_review = rules.get('forbid_author_to_review')
|
||||
c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
@ -499,6 +501,11 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
|||
c.resolved_comments = CommentsModel() \
|
||||
.get_pull_request_resolved_todos(pull_request_latest)
|
||||
|
||||
# Drafts
|
||||
c.draft_comments = CommentsModel().get_pull_request_drafts(
|
||||
self._rhodecode_db_user.user_id,
|
||||
pull_request_latest)
|
||||
|
||||
# if we use version, then do not show later comments
|
||||
# than current version
|
||||
display_inline_comments = collections.defaultdict(
|
||||
|
|
@ -979,8 +986,9 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
|||
}
|
||||
return data
|
||||
|
||||
def _get_existing_ids(self, post_data):
|
||||
return filter(lambda e: e, map(safe_int, aslist(post_data.get('comments'), ',')))
|
||||
@classmethod
|
||||
def get_comment_ids(cls, post_data):
|
||||
return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
|
||||
|
||||
@LoginRequired()
|
||||
@NotAnonymous()
|
||||
|
|
@ -1015,10 +1023,10 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
|||
if at_version and at_version != PullRequest.LATEST_VER
|
||||
else None)
|
||||
|
||||
self.register_comments_vars(c, pull_request_latest, versions)
|
||||
self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
|
||||
all_comments = c.inline_comments_flat + c.comments
|
||||
|
||||
existing_ids = self._get_existing_ids(self.request.POST)
|
||||
existing_ids = self.get_comment_ids(self.request.POST)
|
||||
return _render('comments_table', all_comments, len(all_comments),
|
||||
existing_ids=existing_ids)
|
||||
|
||||
|
|
@ -1055,15 +1063,57 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
|||
else None)
|
||||
|
||||
c.unresolved_comments = CommentsModel() \
|
||||
.get_pull_request_unresolved_todos(pull_request)
|
||||
.get_pull_request_unresolved_todos(pull_request, include_drafts=False)
|
||||
c.resolved_comments = CommentsModel() \
|
||||
.get_pull_request_resolved_todos(pull_request)
|
||||
.get_pull_request_resolved_todos(pull_request, include_drafts=False)
|
||||
|
||||
all_comments = c.unresolved_comments + c.resolved_comments
|
||||
existing_ids = self._get_existing_ids(self.request.POST)
|
||||
existing_ids = self.get_comment_ids(self.request.POST)
|
||||
return _render('comments_table', all_comments, len(c.unresolved_comments),
|
||||
todo_comments=True, existing_ids=existing_ids)
|
||||
|
||||
@LoginRequired()
|
||||
@NotAnonymous()
|
||||
@HasRepoPermissionAnyDecorator(
|
||||
'repository.read', 'repository.write', 'repository.admin')
|
||||
@view_config(
|
||||
route_name='pullrequest_drafts', request_method='POST',
|
||||
renderer='string_html', xhr=True)
|
||||
def pullrequest_drafts(self):
|
||||
self.load_default_context()
|
||||
|
||||
pull_request = PullRequest.get_or_404(
|
||||
self.request.matchdict['pull_request_id'])
|
||||
pull_request_id = pull_request.pull_request_id
|
||||
version = self.request.GET.get('version')
|
||||
|
||||
_render = self.request.get_partial_renderer(
|
||||
'rhodecode:templates/base/sidebar.mako')
|
||||
c = _render.get_call_context()
|
||||
|
||||
(pull_request_latest,
|
||||
pull_request_at_ver,
|
||||
pull_request_display_obj,
|
||||
at_version) = PullRequestModel().get_pr_version(
|
||||
pull_request_id, version=version)
|
||||
versions = pull_request_display_obj.versions()
|
||||
latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
|
||||
c.versions = versions + [latest_ver]
|
||||
|
||||
c.at_version = at_version
|
||||
c.at_version_num = (at_version
|
||||
if at_version and at_version != PullRequest.LATEST_VER
|
||||
else None)
|
||||
|
||||
c.draft_comments = CommentsModel() \
|
||||
.get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
|
||||
|
||||
all_comments = c.draft_comments
|
||||
|
||||
existing_ids = self.get_comment_ids(self.request.POST)
|
||||
return _render('comments_table', all_comments, len(all_comments),
|
||||
existing_ids=existing_ids, draft_comments=True)
|
||||
|
||||
@LoginRequired()
|
||||
@NotAnonymous()
|
||||
@HasRepoPermissionAnyDecorator(
|
||||
|
|
@ -1514,6 +1564,152 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
|||
self._rhodecode_user)
|
||||
raise HTTPNotFound()
|
||||
|
||||
def _pull_request_comments_create(self, pull_request, comments):
|
||||
_ = self.request.translate
|
||||
data = {}
|
||||
if not comments:
|
||||
return
|
||||
pull_request_id = pull_request.pull_request_id
|
||||
|
||||
all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
|
||||
|
||||
for entry in comments:
|
||||
c = self.load_default_context()
|
||||
comment_type = entry['comment_type']
|
||||
text = entry['text']
|
||||
status = entry['status']
|
||||
is_draft = str2bool(entry['is_draft'])
|
||||
resolves_comment_id = entry['resolves_comment_id']
|
||||
close_pull_request = entry['close_pull_request']
|
||||
f_path = entry['f_path']
|
||||
line_no = entry['line']
|
||||
target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
|
||||
|
||||
# the logic here should work like following, if we submit close
|
||||
# pr comment, use `close_pull_request_with_comment` function
|
||||
# else handle regular comment logic
|
||||
|
||||
if close_pull_request:
|
||||
# only owner or admin or person with write permissions
|
||||
allowed_to_close = PullRequestModel().check_user_update(
|
||||
pull_request, self._rhodecode_user)
|
||||
if not allowed_to_close:
|
||||
log.debug('comment: forbidden because not allowed to close '
|
||||
'pull request %s', pull_request_id)
|
||||
raise HTTPForbidden()
|
||||
|
||||
# This also triggers `review_status_change`
|
||||
comment, status = PullRequestModel().close_pull_request_with_comment(
|
||||
pull_request, self._rhodecode_user, self.db_repo, message=text,
|
||||
auth_user=self._rhodecode_user)
|
||||
Session().flush()
|
||||
is_inline = comment.is_inline
|
||||
|
||||
PullRequestModel().trigger_pull_request_hook(
|
||||
pull_request, self._rhodecode_user, 'comment',
|
||||
data={'comment': comment})
|
||||
|
||||
else:
|
||||
# regular comment case, could be inline, or one with status.
|
||||
# for that one we check also permissions
|
||||
# Additionally ENSURE if somehow draft is sent we're then unable to change status
|
||||
allowed_to_change_status = PullRequestModel().check_user_change_status(
|
||||
pull_request, self._rhodecode_user) and not is_draft
|
||||
|
||||
if status and allowed_to_change_status:
|
||||
message = (_('Status change %(transition_icon)s %(status)s')
|
||||
% {'transition_icon': '>',
|
||||
'status': ChangesetStatus.get_status_lbl(status)})
|
||||
text = text or message
|
||||
|
||||
comment = CommentsModel().create(
|
||||
text=text,
|
||||
repo=self.db_repo.repo_id,
|
||||
user=self._rhodecode_user.user_id,
|
||||
pull_request=pull_request,
|
||||
f_path=f_path,
|
||||
line_no=line_no,
|
||||
status_change=(ChangesetStatus.get_status_lbl(status)
|
||||
if status and allowed_to_change_status else None),
|
||||
status_change_type=(status
|
||||
if status and allowed_to_change_status else None),
|
||||
comment_type=comment_type,
|
||||
is_draft=is_draft,
|
||||
resolves_comment_id=resolves_comment_id,
|
||||
auth_user=self._rhodecode_user,
|
||||
send_email=not is_draft, # skip notification for draft comments
|
||||
)
|
||||
is_inline = comment.is_inline
|
||||
|
||||
if allowed_to_change_status:
|
||||
# calculate old status before we change it
|
||||
old_calculated_status = pull_request.calculated_review_status()
|
||||
|
||||
# get status if set !
|
||||
if status:
|
||||
ChangesetStatusModel().set_status(
|
||||
self.db_repo.repo_id,
|
||||
status,
|
||||
self._rhodecode_user.user_id,
|
||||
comment,
|
||||
pull_request=pull_request
|
||||
)
|
||||
|
||||
Session().flush()
|
||||
# this is somehow required to get access to some relationship
|
||||
# loaded on comment
|
||||
Session().refresh(comment)
|
||||
|
||||
# skip notifications for drafts
|
||||
if not is_draft:
|
||||
PullRequestModel().trigger_pull_request_hook(
|
||||
pull_request, self._rhodecode_user, 'comment',
|
||||
data={'comment': comment})
|
||||
|
||||
# we now calculate the status of pull request, and based on that
|
||||
# calculation we set the commits status
|
||||
calculated_status = pull_request.calculated_review_status()
|
||||
if old_calculated_status != calculated_status:
|
||||
PullRequestModel().trigger_pull_request_hook(
|
||||
pull_request, self._rhodecode_user, 'review_status_change',
|
||||
data={'status': calculated_status})
|
||||
|
||||
comment_id = comment.comment_id
|
||||
data[comment_id] = {
|
||||
'target_id': target_elem_id
|
||||
}
|
||||
Session().flush()
|
||||
|
||||
c.co = comment
|
||||
c.at_version_num = None
|
||||
c.is_new = True
|
||||
rendered_comment = render(
|
||||
'rhodecode:templates/changeset/changeset_comment_block.mako',
|
||||
self._get_template_context(c), self.request)
|
||||
|
||||
data[comment_id].update(comment.get_dict())
|
||||
data[comment_id].update({'rendered_text': rendered_comment})
|
||||
|
||||
Session().commit()
|
||||
|
||||
# skip channelstream for draft comments
|
||||
if not all_drafts:
|
||||
comment_broadcast_channel = channelstream.comment_channel(
|
||||
self.db_repo_name, pull_request_obj=pull_request)
|
||||
|
||||
comment_data = data
|
||||
posted_comment_type = 'inline' if is_inline else 'general'
|
||||
if len(data) == 1:
|
||||
msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
|
||||
else:
|
||||
msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
|
||||
|
||||
channelstream.comment_channelstream_push(
|
||||
self.request, comment_broadcast_channel, self._rhodecode_user, msg,
|
||||
comment_data=comment_data)
|
||||
|
||||
return data
|
||||
|
||||
@LoginRequired()
|
||||
@NotAnonymous()
|
||||
@HasRepoPermissionAnyDecorator(
|
||||
|
|
@ -1525,9 +1721,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
|||
def pull_request_comment_create(self):
|
||||
_ = self.request.translate
|
||||
|
||||
pull_request = PullRequest.get_or_404(
|
||||
self.request.matchdict['pull_request_id'])
|
||||
pull_request_id = pull_request.pull_request_id
|
||||
pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
|
||||
|
||||
if pull_request.is_closed():
|
||||
log.debug('comment: forbidden because pull request is closed')
|
||||
|
|
@ -1539,124 +1733,17 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
|||
log.debug('comment: forbidden because pull request is from forbidden repo')
|
||||
raise HTTPForbidden()
|
||||
|
||||
c = self.load_default_context()
|
||||
|
||||
status = self.request.POST.get('changeset_status', None)
|
||||
text = self.request.POST.get('text')
|
||||
comment_type = self.request.POST.get('comment_type')
|
||||
resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
|
||||
close_pull_request = self.request.POST.get('close_pull_request')
|
||||
|
||||
# the logic here should work like following, if we submit close
|
||||
# pr comment, use `close_pull_request_with_comment` function
|
||||
# else handle regular comment logic
|
||||
|
||||
if close_pull_request:
|
||||
# only owner or admin or person with write permissions
|
||||
allowed_to_close = PullRequestModel().check_user_update(
|
||||
pull_request, self._rhodecode_user)
|
||||
if not allowed_to_close:
|
||||
log.debug('comment: forbidden because not allowed to close '
|
||||
'pull request %s', pull_request_id)
|
||||
raise HTTPForbidden()
|
||||
|
||||
# This also triggers `review_status_change`
|
||||
comment, status = PullRequestModel().close_pull_request_with_comment(
|
||||
pull_request, self._rhodecode_user, self.db_repo, message=text,
|
||||
auth_user=self._rhodecode_user)
|
||||
Session().flush()
|
||||
is_inline = comment.is_inline
|
||||
|
||||
PullRequestModel().trigger_pull_request_hook(
|
||||
pull_request, self._rhodecode_user, 'comment',
|
||||
data={'comment': comment})
|
||||
|
||||
else:
|
||||
# regular comment case, could be inline, or one with status.
|
||||
# for that one we check also permissions
|
||||
|
||||
allowed_to_change_status = PullRequestModel().check_user_change_status(
|
||||
pull_request, self._rhodecode_user)
|
||||
|
||||
if status and allowed_to_change_status:
|
||||
message = (_('Status change %(transition_icon)s %(status)s')
|
||||
% {'transition_icon': '>',
|
||||
'status': ChangesetStatus.get_status_lbl(status)})
|
||||
text = text or message
|
||||
|
||||
comment = CommentsModel().create(
|
||||
text=text,
|
||||
repo=self.db_repo.repo_id,
|
||||
user=self._rhodecode_user.user_id,
|
||||
pull_request=pull_request,
|
||||
f_path=self.request.POST.get('f_path'),
|
||||
line_no=self.request.POST.get('line'),
|
||||
status_change=(ChangesetStatus.get_status_lbl(status)
|
||||
if status and allowed_to_change_status else None),
|
||||
status_change_type=(status
|
||||
if status and allowed_to_change_status else None),
|
||||
comment_type=comment_type,
|
||||
resolves_comment_id=resolves_comment_id,
|
||||
auth_user=self._rhodecode_user
|
||||
)
|
||||
is_inline = comment.is_inline
|
||||
|
||||
if allowed_to_change_status:
|
||||
# calculate old status before we change it
|
||||
old_calculated_status = pull_request.calculated_review_status()
|
||||
|
||||
# get status if set !
|
||||
if status:
|
||||
ChangesetStatusModel().set_status(
|
||||
self.db_repo.repo_id,
|
||||
status,
|
||||
self._rhodecode_user.user_id,
|
||||
comment,
|
||||
pull_request=pull_request
|
||||
)
|
||||
|
||||
Session().flush()
|
||||
# this is somehow required to get access to some relationship
|
||||
# loaded on comment
|
||||
Session().refresh(comment)
|
||||
|
||||
PullRequestModel().trigger_pull_request_hook(
|
||||
pull_request, self._rhodecode_user, 'comment',
|
||||
data={'comment': comment})
|
||||
|
||||
# we now calculate the status of pull request, and based on that
|
||||
# calculation we set the commits status
|
||||
calculated_status = pull_request.calculated_review_status()
|
||||
if old_calculated_status != calculated_status:
|
||||
PullRequestModel().trigger_pull_request_hook(
|
||||
pull_request, self._rhodecode_user, 'review_status_change',
|
||||
data={'status': calculated_status})
|
||||
|
||||
Session().commit()
|
||||
|
||||
data = {
|
||||
'target_id': h.safeid(h.safe_unicode(
|
||||
self.request.POST.get('f_path'))),
|
||||
comment_data = {
|
||||
'comment_type': self.request.POST.get('comment_type'),
|
||||
'text': self.request.POST.get('text'),
|
||||
'status': self.request.POST.get('changeset_status', None),
|
||||
'is_draft': self.request.POST.get('draft'),
|
||||
'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
|
||||
'close_pull_request': self.request.POST.get('close_pull_request'),
|
||||
'f_path': self.request.POST.get('f_path'),
|
||||
'line': self.request.POST.get('line'),
|
||||
}
|
||||
if comment:
|
||||
c.co = comment
|
||||
c.at_version_num = None
|
||||
rendered_comment = render(
|
||||
'rhodecode:templates/changeset/changeset_comment_block.mako',
|
||||
self._get_template_context(c), self.request)
|
||||
|
||||
data.update(comment.get_dict())
|
||||
data.update({'rendered_text': rendered_comment})
|
||||
|
||||
comment_broadcast_channel = channelstream.comment_channel(
|
||||
self.db_repo_name, pull_request_obj=pull_request)
|
||||
|
||||
comment_data = data
|
||||
comment_type = 'inline' if is_inline else 'general'
|
||||
channelstream.comment_channelstream_push(
|
||||
self.request, comment_broadcast_channel, self._rhodecode_user,
|
||||
_('posted a new {} comment').format(comment_type),
|
||||
comment_data=comment_data)
|
||||
data = self._pull_request_comments_create(pull_request, [comment_data])
|
||||
|
||||
return data
|
||||
|
||||
|
|
@ -1741,11 +1828,6 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
|||
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()
|
||||
|
|
@ -1796,10 +1878,10 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
|||
raise HTTPNotFound()
|
||||
|
||||
Session().commit()
|
||||
|
||||
PullRequestModel().trigger_pull_request_hook(
|
||||
pull_request, self._rhodecode_user, 'comment_edit',
|
||||
data={'comment': comment})
|
||||
if not comment.draft:
|
||||
PullRequestModel().trigger_pull_request_hook(
|
||||
pull_request, self._rhodecode_user, 'comment_edit',
|
||||
data={'comment': comment})
|
||||
|
||||
return {
|
||||
'comment_history_id': comment_history.comment_history_id,
|
||||
|
|
|
|||
|
|
@ -215,9 +215,10 @@ class RhodeCodeAuthPluginBase(object):
|
|||
"""
|
||||
return self._plugin_id
|
||||
|
||||
def get_display_name(self):
|
||||
def get_display_name(self, load_from_settings=False):
|
||||
"""
|
||||
Returns a translation string for displaying purposes.
|
||||
if load_from_settings is set, plugin settings can override the display name
|
||||
"""
|
||||
raise NotImplementedError('Not implemented in base class')
|
||||
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
|
|||
def get_settings_schema(self):
|
||||
return CrowdSettingsSchema()
|
||||
|
||||
def get_display_name(self):
|
||||
def get_display_name(self, load_from_settings=False):
|
||||
return _('CROWD')
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
|
|||
route_name='auth_home',
|
||||
context=HeadersAuthnResource)
|
||||
|
||||
def get_display_name(self):
|
||||
def get_display_name(self, load_from_settings=False):
|
||||
return _('Headers')
|
||||
|
||||
def get_settings_schema(self):
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
|
|||
def get_settings_schema(self):
|
||||
return JasigCasSettingsSchema()
|
||||
|
||||
def get_display_name(self):
|
||||
def get_display_name(self, load_from_settings=False):
|
||||
return _('Jasig-CAS')
|
||||
|
||||
@hybrid_property
|
||||
|
|
|
|||
|
|
@ -421,7 +421,7 @@ class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
|
|||
def get_settings_schema(self):
|
||||
return LdapSettingsSchema()
|
||||
|
||||
def get_display_name(self):
|
||||
def get_display_name(self, load_from_settings=False):
|
||||
return _('LDAP')
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
|
|||
route_name='auth_home',
|
||||
context=PamAuthnResource)
|
||||
|
||||
def get_display_name(self):
|
||||
def get_display_name(self, load_from_settings=False):
|
||||
return _('PAM')
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
|
|||
def get_settings_schema(self):
|
||||
return RhodeCodeSettingsSchema()
|
||||
|
||||
def get_display_name(self):
|
||||
def get_display_name(self, load_from_settings=False):
|
||||
return _('RhodeCode Internal')
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
|
|||
def get_settings_schema(self):
|
||||
return RhodeCodeSettingsSchema()
|
||||
|
||||
def get_display_name(self):
|
||||
def get_display_name(self, load_from_settings=False):
|
||||
return _('Rhodecode Token')
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
|
|||
from rhodecode.lib.exc_tracking import store_exception
|
||||
from rhodecode.subscribers import (
|
||||
scan_repositories_if_enabled, write_js_routes_if_enabled,
|
||||
write_metadata_if_needed, write_usage_data, inject_app_settings)
|
||||
write_metadata_if_needed, write_usage_data)
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -310,8 +310,6 @@ def includeme(config):
|
|||
|
||||
# Add subscribers.
|
||||
if load_all:
|
||||
config.add_subscriber(inject_app_settings,
|
||||
pyramid.events.ApplicationCreated)
|
||||
config.add_subscriber(scan_repositories_if_enabled,
|
||||
pyramid.events.ApplicationCreated)
|
||||
config.add_subscriber(write_metadata_if_needed,
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ markdown_tags = [
|
|||
|
||||
markdown_attrs = {
|
||||
"*": ["class", "style", "align"],
|
||||
"img": ["src", "alt", "title"],
|
||||
"img": ["src", "alt", "title", "width", "height", "hspace", "align"],
|
||||
"a": ["href", "alt", "title", "name", "data-hovercard-alt", "data-hovercard-url"],
|
||||
"abbr": ["title"],
|
||||
"acronym": ["title"],
|
||||
|
|
|
|||
|
|
@ -339,13 +339,12 @@ def comment_channelstream_push(request, comment_broadcast_channel, user, msg, **
|
|||
|
||||
comment_data = kwargs.pop('comment_data', {})
|
||||
user_data = kwargs.pop('user_data', {})
|
||||
comment_id = comment_data.get('comment_id')
|
||||
comment_id = comment_data.keys()[0] if comment_data else ''
|
||||
|
||||
message = '<strong>{}</strong> {} #{}, {}'.format(
|
||||
message = '<strong>{}</strong> {} #{}'.format(
|
||||
user.username,
|
||||
msg,
|
||||
comment_id,
|
||||
_reload_link(_('Reload page to see new comments')),
|
||||
)
|
||||
|
||||
message_obj = {
|
||||
|
|
|
|||
53
rhodecode/lib/dbmigrate/versions/111_version_4_23_0.py
Normal file
53
rhodecode/lib/dbmigrate/versions/111_version_4_23_0.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
from sqlalchemy import *
|
||||
|
||||
from alembic.migration import MigrationContext
|
||||
from alembic.operations import Operations
|
||||
|
||||
from rhodecode.lib.dbmigrate.versions import _reset_base
|
||||
from rhodecode.model import meta, 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_20_0_0 as db
|
||||
|
||||
init_model_encryption(db)
|
||||
|
||||
context = MigrationContext.configure(migrate_engine.connect())
|
||||
op = Operations(context)
|
||||
|
||||
table = db.ChangesetComment.__table__
|
||||
with op.batch_alter_table(table.name) as batch_op:
|
||||
new_column = Column('draft', Boolean(), nullable=True)
|
||||
batch_op.add_column(new_column)
|
||||
|
||||
_set_default_as_non_draft(op, meta.Session)
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
|
||||
def fixups(models, _SESSION):
|
||||
pass
|
||||
|
||||
|
||||
def _set_default_as_non_draft(op, session):
|
||||
params = {'draft': False}
|
||||
query = text(
|
||||
'UPDATE changeset_comments SET draft = :draft'
|
||||
).bindparams(**params)
|
||||
op.execute(query)
|
||||
session().commit()
|
||||
|
||||
78
rhodecode/lib/dbmigrate/versions/112_version_4_23_0.py
Normal file
78
rhodecode/lib/dbmigrate/versions/112_version_4_23_0.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
from sqlalchemy import *
|
||||
|
||||
from alembic.migration import MigrationContext
|
||||
from alembic.operations import Operations
|
||||
|
||||
from rhodecode.lib.dbmigrate.versions import _reset_base
|
||||
from rhodecode.model import meta, 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_20_0_0 as db
|
||||
|
||||
init_model_encryption(db)
|
||||
|
||||
context = MigrationContext.configure(migrate_engine.connect())
|
||||
op = Operations(context)
|
||||
|
||||
table = db.RepoReviewRule.__table__
|
||||
with op.batch_alter_table(table.name) as batch_op:
|
||||
|
||||
new_column = Column('pr_author', UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
|
||||
batch_op.add_column(new_column)
|
||||
|
||||
new_column = Column('commit_author', UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
|
||||
batch_op.add_column(new_column)
|
||||
|
||||
_migrate_review_flags_to_new_cols(op, meta.Session)
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
|
||||
def fixups(models, _SESSION):
|
||||
pass
|
||||
|
||||
|
||||
def _migrate_review_flags_to_new_cols(op, session):
|
||||
|
||||
# set defaults for pr_author
|
||||
query = text(
|
||||
'UPDATE repo_review_rules SET pr_author = :val'
|
||||
).bindparams(val='no_rule')
|
||||
op.execute(query)
|
||||
|
||||
# set defaults for commit_author
|
||||
query = text(
|
||||
'UPDATE repo_review_rules SET commit_author = :val'
|
||||
).bindparams(val='no_rule')
|
||||
op.execute(query)
|
||||
|
||||
session().commit()
|
||||
|
||||
# now change the flags to forbid based on
|
||||
# forbid_author_to_review, forbid_commit_author_to_review
|
||||
query = text(
|
||||
'UPDATE repo_review_rules SET pr_author = :val WHERE forbid_author_to_review = TRUE'
|
||||
).bindparams(val='forbid_pr_author')
|
||||
op.execute(query)
|
||||
|
||||
query = text(
|
||||
'UPDATE repo_review_rules SET commit_author = :val WHERE forbid_commit_author_to_review = TRUE'
|
||||
).bindparams(val='forbid_commit_author')
|
||||
op.execute(query)
|
||||
|
||||
session().commit()
|
||||
|
|
@ -1148,7 +1148,7 @@ class DiffLimitExceeded(Exception):
|
|||
|
||||
# NOTE(marcink): if diffs.mako change, probably this
|
||||
# needs a bump to next version
|
||||
CURRENT_DIFF_VERSION = 'v4'
|
||||
CURRENT_DIFF_VERSION = 'v5'
|
||||
|
||||
|
||||
def _cleanup_cache_file(cached_diff_file):
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ def _store_exception(exc_id, exc_type_name, exc_traceback, prefix, send_email=No
|
|||
|
||||
mail_server = app.CONFIG.get('smtp_server') or None
|
||||
send_email = send_email and mail_server
|
||||
if send_email:
|
||||
if send_email and request:
|
||||
try:
|
||||
send_exc_email(request, exc_id, exc_type_name)
|
||||
except Exception:
|
||||
|
|
|
|||
|
|
@ -18,17 +18,85 @@
|
|||
# RhodeCode Enterprise Edition, including its added features, Support services,
|
||||
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
||||
|
||||
import re
|
||||
import markdown
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
from markdown.extensions import Extension
|
||||
from markdown.extensions.fenced_code import FencedCodeExtension
|
||||
from markdown.extensions.smart_strong import SmartEmphasisExtension
|
||||
from markdown.extensions.tables import TableExtension
|
||||
from markdown.extensions.nl2br import Nl2BrExtension
|
||||
from markdown.inlinepatterns import Pattern
|
||||
|
||||
import gfm
|
||||
|
||||
|
||||
class InlineProcessor(Pattern):
|
||||
"""
|
||||
Base class that inline patterns subclass.
|
||||
This is the newer style inline processor that uses a more
|
||||
efficient and flexible search approach.
|
||||
"""
|
||||
|
||||
def __init__(self, pattern, md=None):
|
||||
"""
|
||||
Create an instant of an inline pattern.
|
||||
Keyword arguments:
|
||||
* pattern: A regular expression that matches a pattern
|
||||
"""
|
||||
self.pattern = pattern
|
||||
self.compiled_re = re.compile(pattern, re.DOTALL | re.UNICODE)
|
||||
|
||||
# Api for Markdown to pass safe_mode into instance
|
||||
self.safe_mode = False
|
||||
self.md = md
|
||||
|
||||
def handleMatch(self, m, data):
|
||||
"""Return a ElementTree element from the given match and the
|
||||
start and end index of the matched text.
|
||||
If `start` and/or `end` are returned as `None`, it will be
|
||||
assumed that the processor did not find a valid region of text.
|
||||
Subclasses should override this method.
|
||||
Keyword arguments:
|
||||
* m: A re match object containing a match of the pattern.
|
||||
* data: The buffer current under analysis
|
||||
Returns:
|
||||
* el: The ElementTree element, text or None.
|
||||
* start: The start of the region that has been matched or None.
|
||||
* end: The end of the region that has been matched or None.
|
||||
"""
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
class SimpleTagInlineProcessor(InlineProcessor):
|
||||
"""
|
||||
Return element of type `tag` with a text attribute of group(2)
|
||||
of a Pattern.
|
||||
"""
|
||||
def __init__(self, pattern, tag):
|
||||
InlineProcessor.__init__(self, pattern)
|
||||
self.tag = tag
|
||||
|
||||
def handleMatch(self, m, data): # pragma: no cover
|
||||
el = etree.Element(self.tag)
|
||||
el.text = m.group(2)
|
||||
return el, m.start(0), m.end(0)
|
||||
|
||||
|
||||
class SubstituteTagInlineProcessor(SimpleTagInlineProcessor):
|
||||
""" Return an element of type `tag` with no children. """
|
||||
def handleMatch(self, m, data):
|
||||
return etree.Element(self.tag), m.start(0), m.end(0)
|
||||
|
||||
|
||||
class Nl2BrExtension(Extension):
|
||||
BR_RE = r'\n'
|
||||
|
||||
def extendMarkdown(self, md, md_globals):
|
||||
br_tag = SubstituteTagInlineProcessor(self.BR_RE, 'br')
|
||||
md.inlinePatterns.add('nl', br_tag, '_end')
|
||||
|
||||
|
||||
class GithubFlavoredMarkdownExtension(Extension):
|
||||
"""
|
||||
An extension that is as compatible as possible with GitHub-flavored
|
||||
|
|
@ -51,6 +119,7 @@ class GithubFlavoredMarkdownExtension(Extension):
|
|||
|
||||
def extendMarkdown(self, md, md_globals):
|
||||
# Built-in extensions
|
||||
Nl2BrExtension().extendMarkdown(md, md_globals)
|
||||
FencedCodeExtension().extendMarkdown(md, md_globals)
|
||||
SmartEmphasisExtension().extendMarkdown(md, md_globals)
|
||||
TableExtension().extendMarkdown(md, md_globals)
|
||||
|
|
@ -68,7 +137,6 @@ class GithubFlavoredMarkdownExtension(Extension):
|
|||
gfm.TaskListExtension([
|
||||
('list_attrs', {'class': 'checkbox'})
|
||||
]).extendMarkdown(md, md_globals)
|
||||
Nl2BrExtension().extendMarkdown(md, md_globals)
|
||||
|
||||
|
||||
# Global Vars
|
||||
|
|
|
|||
|
|
@ -74,7 +74,11 @@ def configure_dogpile_cache(settings):
|
|||
|
||||
new_region.configure_from_config(settings, 'rc_cache.{}.'.format(region_name))
|
||||
new_region.function_key_generator = backend_key_generator(new_region.actual_backend)
|
||||
log.debug('dogpile: registering a new region %s[%s]', region_name, new_region.__dict__)
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
region_args = dict(backend=new_region.actual_backend.__class__,
|
||||
region_invalidator=new_region.region_invalidator.__class__)
|
||||
log.debug('dogpile: registering a new region `%s` %s', region_name, region_args)
|
||||
|
||||
region_meta.dogpile_cache_regions[region_name] = new_region
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -915,8 +915,9 @@ class BaseCommit(object):
|
|||
list of parent commits
|
||||
|
||||
"""
|
||||
|
||||
repository = None
|
||||
branch = None
|
||||
|
||||
"""
|
||||
Depending on the backend this should be set to the branch name of the
|
||||
commit. Backends not supporting branches on commits should leave this
|
||||
|
|
@ -1192,13 +1193,14 @@ class BaseCommit(object):
|
|||
return None
|
||||
|
||||
def archive_repo(self, archive_dest_path, kind='tgz', subrepos=None,
|
||||
prefix=None, write_metadata=False, mtime=None, archive_at_path='/'):
|
||||
archive_dir_name=None, write_metadata=False, mtime=None,
|
||||
archive_at_path='/'):
|
||||
"""
|
||||
Creates an archive containing the contents of the repository.
|
||||
|
||||
:param archive_dest_path: path to the file which to create the archive.
|
||||
:param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
|
||||
:param prefix: name of root directory in archive.
|
||||
:param archive_dir_name: name of root directory in archive.
|
||||
Default is repository name and commit's short_id joined with dash:
|
||||
``"{repo_name}-{short_id}"``.
|
||||
:param write_metadata: write a metadata file into archive.
|
||||
|
|
@ -1214,43 +1216,26 @@ class BaseCommit(object):
|
|||
'Archive kind (%s) not supported use one of %s' %
|
||||
(kind, allowed_kinds))
|
||||
|
||||
prefix = self._validate_archive_prefix(prefix)
|
||||
|
||||
archive_dir_name = self._validate_archive_prefix(archive_dir_name)
|
||||
mtime = mtime is not None or time.mktime(self.date.timetuple())
|
||||
commit_id = self.raw_id
|
||||
|
||||
file_info = []
|
||||
cur_rev = self.repository.get_commit(commit_id=self.raw_id)
|
||||
for _r, _d, files in cur_rev.walk(archive_at_path):
|
||||
for f in files:
|
||||
f_path = os.path.join(prefix, f.path)
|
||||
file_info.append(
|
||||
(f_path, f.mode, f.is_link(), f.raw_bytes))
|
||||
return self.repository._remote.archive_repo(
|
||||
archive_dest_path, kind, mtime, archive_at_path,
|
||||
archive_dir_name, commit_id)
|
||||
|
||||
if write_metadata:
|
||||
metadata = [
|
||||
('repo_name', self.repository.name),
|
||||
('commit_id', self.raw_id),
|
||||
('mtime', mtime),
|
||||
('branch', self.branch),
|
||||
('tags', ','.join(self.tags)),
|
||||
]
|
||||
meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
|
||||
file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
|
||||
|
||||
connection.Hg.archive_repo(archive_dest_path, mtime, file_info, kind)
|
||||
|
||||
def _validate_archive_prefix(self, prefix):
|
||||
if prefix is None:
|
||||
prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
|
||||
def _validate_archive_prefix(self, archive_dir_name):
|
||||
if archive_dir_name is None:
|
||||
archive_dir_name = self._ARCHIVE_PREFIX_TEMPLATE.format(
|
||||
repo_name=safe_str(self.repository.name),
|
||||
short_id=self.short_id)
|
||||
elif not isinstance(prefix, str):
|
||||
raise ValueError("prefix not a bytes object: %s" % repr(prefix))
|
||||
elif prefix.startswith('/'):
|
||||
elif not isinstance(archive_dir_name, str):
|
||||
raise ValueError("prefix not a bytes object: %s" % repr(archive_dir_name))
|
||||
elif archive_dir_name.startswith('/'):
|
||||
raise VCSError("Prefix cannot start with leading slash")
|
||||
elif prefix.strip() == '':
|
||||
elif archive_dir_name.strip() == '':
|
||||
raise VCSError("Prefix cannot be empty")
|
||||
return prefix
|
||||
return archive_dir_name
|
||||
|
||||
@LazyProperty
|
||||
def root(self):
|
||||
|
|
|
|||
|
|
@ -214,16 +214,19 @@ def map_vcs_exceptions(func):
|
|||
# to translate them to the proper exception class in the vcs
|
||||
# client layer.
|
||||
kind = getattr(e, '_vcs_kind', None)
|
||||
exc_name = getattr(e, '_vcs_server_org_exc_name', None)
|
||||
|
||||
if kind:
|
||||
if any(e.args):
|
||||
args = e.args
|
||||
args = [a for a in e.args]
|
||||
args[0] = '{}:'.format(exc_name) # prefix first arg with org exc name
|
||||
else:
|
||||
args = [__traceback_info__ or 'unhandledException']
|
||||
args = [__traceback_info__ or '{}: UnhandledException'.format(exc_name)]
|
||||
if debug or __traceback_info__ and kind not in ['unhandled', 'lookup']:
|
||||
# for other than unhandled errors also log the traceback
|
||||
# can be useful for debugging
|
||||
log.error(__traceback_info__)
|
||||
|
||||
raise _EXCEPTION_MAP[kind](*args)
|
||||
else:
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ 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 (
|
||||
false, true,
|
||||
ChangesetComment,
|
||||
User,
|
||||
Notification,
|
||||
|
|
@ -160,7 +161,7 @@ class CommentsModel(BaseModel):
|
|||
|
||||
return todos
|
||||
|
||||
def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
|
||||
def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
|
||||
|
||||
todos = Session().query(ChangesetComment) \
|
||||
.filter(ChangesetComment.pull_request == pull_request) \
|
||||
|
|
@ -168,6 +169,9 @@ class CommentsModel(BaseModel):
|
|||
.filter(ChangesetComment.comment_type
|
||||
== ChangesetComment.COMMENT_TYPE_TODO)
|
||||
|
||||
if not include_drafts:
|
||||
todos = todos.filter(ChangesetComment.draft == false())
|
||||
|
||||
if not show_outdated:
|
||||
todos = todos.filter(
|
||||
coalesce(ChangesetComment.display_state, '') !=
|
||||
|
|
@ -177,7 +181,7 @@ class CommentsModel(BaseModel):
|
|||
|
||||
return todos
|
||||
|
||||
def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
|
||||
def get_pull_request_resolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
|
||||
|
||||
todos = Session().query(ChangesetComment) \
|
||||
.filter(ChangesetComment.pull_request == pull_request) \
|
||||
|
|
@ -185,6 +189,9 @@ class CommentsModel(BaseModel):
|
|||
.filter(ChangesetComment.comment_type
|
||||
== ChangesetComment.COMMENT_TYPE_TODO)
|
||||
|
||||
if not include_drafts:
|
||||
todos = todos.filter(ChangesetComment.draft == false())
|
||||
|
||||
if not show_outdated:
|
||||
todos = todos.filter(
|
||||
coalesce(ChangesetComment.display_state, '') !=
|
||||
|
|
@ -194,7 +201,14 @@ class CommentsModel(BaseModel):
|
|||
|
||||
return todos
|
||||
|
||||
def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
|
||||
def get_pull_request_drafts(self, user_id, pull_request):
|
||||
drafts = Session().query(ChangesetComment) \
|
||||
.filter(ChangesetComment.pull_request == pull_request) \
|
||||
.filter(ChangesetComment.user_id == user_id) \
|
||||
.filter(ChangesetComment.draft == true())
|
||||
return drafts.all()
|
||||
|
||||
def get_commit_unresolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
|
||||
|
||||
todos = Session().query(ChangesetComment) \
|
||||
.filter(ChangesetComment.revision == commit_id) \
|
||||
|
|
@ -202,6 +216,9 @@ class CommentsModel(BaseModel):
|
|||
.filter(ChangesetComment.comment_type
|
||||
== ChangesetComment.COMMENT_TYPE_TODO)
|
||||
|
||||
if not include_drafts:
|
||||
todos = todos.filter(ChangesetComment.draft == false())
|
||||
|
||||
if not show_outdated:
|
||||
todos = todos.filter(
|
||||
coalesce(ChangesetComment.display_state, '') !=
|
||||
|
|
@ -211,7 +228,7 @@ class CommentsModel(BaseModel):
|
|||
|
||||
return todos
|
||||
|
||||
def get_commit_resolved_todos(self, commit_id, show_outdated=True):
|
||||
def get_commit_resolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
|
||||
|
||||
todos = Session().query(ChangesetComment) \
|
||||
.filter(ChangesetComment.revision == commit_id) \
|
||||
|
|
@ -219,6 +236,9 @@ class CommentsModel(BaseModel):
|
|||
.filter(ChangesetComment.comment_type
|
||||
== ChangesetComment.COMMENT_TYPE_TODO)
|
||||
|
||||
if not include_drafts:
|
||||
todos = todos.filter(ChangesetComment.draft == false())
|
||||
|
||||
if not show_outdated:
|
||||
todos = todos.filter(
|
||||
coalesce(ChangesetComment.display_state, '') !=
|
||||
|
|
@ -228,11 +248,15 @@ class CommentsModel(BaseModel):
|
|||
|
||||
return todos
|
||||
|
||||
def get_commit_inline_comments(self, commit_id):
|
||||
def get_commit_inline_comments(self, commit_id, include_drafts=True):
|
||||
inline_comments = Session().query(ChangesetComment) \
|
||||
.filter(ChangesetComment.line_no != None) \
|
||||
.filter(ChangesetComment.f_path != None) \
|
||||
.filter(ChangesetComment.revision == commit_id)
|
||||
|
||||
if not include_drafts:
|
||||
inline_comments = inline_comments.filter(ChangesetComment.draft == false())
|
||||
|
||||
inline_comments = inline_comments.all()
|
||||
return inline_comments
|
||||
|
||||
|
|
@ -245,7 +269,7 @@ class CommentsModel(BaseModel):
|
|||
|
||||
def create(self, text, repo, user, commit_id=None, pull_request=None,
|
||||
f_path=None, line_no=None, status_change=None,
|
||||
status_change_type=None, comment_type=None,
|
||||
status_change_type=None, comment_type=None, is_draft=False,
|
||||
resolves_comment_id=None, closing_pr=False, send_email=True,
|
||||
renderer=None, auth_user=None, extra_recipients=None):
|
||||
"""
|
||||
|
|
@ -262,6 +286,7 @@ class CommentsModel(BaseModel):
|
|||
:param line_no:
|
||||
:param status_change: Label for status change
|
||||
:param comment_type: Type of comment
|
||||
:param is_draft: is comment a draft only
|
||||
:param resolves_comment_id: id of comment which this one will resolve
|
||||
:param status_change_type: type of status change
|
||||
:param closing_pr:
|
||||
|
|
@ -288,6 +313,7 @@ class CommentsModel(BaseModel):
|
|||
validated_kwargs = schema.deserialize(dict(
|
||||
comment_body=text,
|
||||
comment_type=comment_type,
|
||||
is_draft=is_draft,
|
||||
comment_file=f_path,
|
||||
comment_line=line_no,
|
||||
renderer_type=renderer,
|
||||
|
|
@ -296,6 +322,7 @@ class CommentsModel(BaseModel):
|
|||
repo=repo.repo_id,
|
||||
user=user.user_id,
|
||||
))
|
||||
is_draft = validated_kwargs['is_draft']
|
||||
|
||||
comment = ChangesetComment()
|
||||
comment.renderer = validated_kwargs['renderer_type']
|
||||
|
|
@ -303,6 +330,7 @@ class CommentsModel(BaseModel):
|
|||
comment.f_path = validated_kwargs['comment_file']
|
||||
comment.line_no = validated_kwargs['comment_line']
|
||||
comment.comment_type = validated_kwargs['comment_type']
|
||||
comment.draft = is_draft
|
||||
|
||||
comment.repo = repo
|
||||
comment.author = user
|
||||
|
|
@ -438,9 +466,6 @@ class CommentsModel(BaseModel):
|
|||
|
||||
if send_email:
|
||||
recipients += [self._get_user(u) for u in (extra_recipients or [])]
|
||||
# pre-generate the subject for notification itself
|
||||
(subject, _e, body_plaintext) = EmailNotificationModel().render_email(
|
||||
notification_type, **kwargs)
|
||||
|
||||
mention_recipients = set(
|
||||
self._extract_mentions(text)).difference(recipients)
|
||||
|
|
@ -448,8 +473,8 @@ class CommentsModel(BaseModel):
|
|||
# create notification objects, and emails
|
||||
NotificationModel().create(
|
||||
created_by=user,
|
||||
notification_subject=subject,
|
||||
notification_body=body_plaintext,
|
||||
notification_subject='', # Filled in based on the notification_type
|
||||
notification_body='', # Filled in based on the notification_type
|
||||
notification_type=notification_type,
|
||||
recipients=recipients,
|
||||
mention_recipients=mention_recipients,
|
||||
|
|
@ -462,10 +487,11 @@ class CommentsModel(BaseModel):
|
|||
else:
|
||||
action = 'repo.commit.comment.create'
|
||||
|
||||
comment_data = comment.get_api_data()
|
||||
if not is_draft:
|
||||
comment_data = comment.get_api_data()
|
||||
|
||||
self._log_audit_action(
|
||||
action, {'data': comment_data}, auth_user, comment)
|
||||
self._log_audit_action(
|
||||
action, {'data': comment_data}, auth_user, comment)
|
||||
|
||||
return comment
|
||||
|
||||
|
|
@ -541,7 +567,8 @@ class CommentsModel(BaseModel):
|
|||
|
||||
return comment
|
||||
|
||||
def get_all_comments(self, repo_id, revision=None, pull_request=None, count_only=False):
|
||||
def get_all_comments(self, repo_id, revision=None, pull_request=None,
|
||||
include_drafts=True, count_only=False):
|
||||
q = ChangesetComment.query()\
|
||||
.filter(ChangesetComment.repo_id == repo_id)
|
||||
if revision:
|
||||
|
|
@ -551,6 +578,8 @@ class CommentsModel(BaseModel):
|
|||
q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
|
||||
else:
|
||||
raise Exception('Please specify commit or pull_request')
|
||||
if not include_drafts:
|
||||
q = q.filter(ChangesetComment.draft == false())
|
||||
q = q.order_by(ChangesetComment.created_on)
|
||||
if count_only:
|
||||
return q.count()
|
||||
|
|
@ -697,7 +726,8 @@ class CommentsModel(BaseModel):
|
|||
path=comment.f_path, diff_line=diff_line)
|
||||
except (diffs.LineNotInDiffException,
|
||||
diffs.FileNotInDiffException):
|
||||
comment.display_state = ChangesetComment.COMMENT_OUTDATED
|
||||
if not comment.draft:
|
||||
comment.display_state = ChangesetComment.COMMENT_OUTDATED
|
||||
return
|
||||
|
||||
if old_context == new_context:
|
||||
|
|
@ -707,14 +737,15 @@ class CommentsModel(BaseModel):
|
|||
new_diff_lines = new_diff_proc.find_context(
|
||||
path=comment.f_path, context=old_context,
|
||||
offset=self.DIFF_CONTEXT_BEFORE)
|
||||
if not new_diff_lines:
|
||||
if not new_diff_lines and not comment.draft:
|
||||
comment.display_state = ChangesetComment.COMMENT_OUTDATED
|
||||
else:
|
||||
new_diff_line = self._choose_closest_diff_line(
|
||||
diff_line, new_diff_lines)
|
||||
comment.line_no = _diff_to_comment_line_number(new_diff_line)
|
||||
else:
|
||||
comment.display_state = ChangesetComment.COMMENT_OUTDATED
|
||||
if not comment.draft:
|
||||
comment.display_state = ChangesetComment.COMMENT_OUTDATED
|
||||
|
||||
def _should_relocate_diff_line(self, diff_line):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -3767,6 +3767,7 @@ class ChangesetComment(Base, BaseModel):
|
|||
renderer = Column('renderer', Unicode(64), nullable=True)
|
||||
display_state = Column('display_state', Unicode(128), nullable=True)
|
||||
immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
|
||||
draft = Column('draft', Boolean(), nullable=True, default=False)
|
||||
|
||||
comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
|
||||
resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
|
||||
|
|
@ -5057,8 +5058,14 @@ class RepoReviewRule(Base, BaseModel):
|
|||
_file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
|
||||
|
||||
use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
|
||||
forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
|
||||
forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
|
||||
|
||||
# Legacy fields, just for backward compat
|
||||
_forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
|
||||
_forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
|
||||
|
||||
pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
|
||||
commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
|
||||
|
||||
forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
|
||||
|
||||
rule_users = relationship('RepoReviewRuleUser')
|
||||
|
|
@ -5094,6 +5101,22 @@ class RepoReviewRule(Base, BaseModel):
|
|||
self._validate_pattern(value)
|
||||
self._file_pattern = value or '*'
|
||||
|
||||
@hybrid_property
|
||||
def forbid_pr_author_to_review(self):
|
||||
return self.pr_author == 'forbid_pr_author'
|
||||
|
||||
@hybrid_property
|
||||
def include_pr_author_to_review(self):
|
||||
return self.pr_author == 'include_pr_author'
|
||||
|
||||
@hybrid_property
|
||||
def forbid_commit_author_to_review(self):
|
||||
return self.commit_author == 'forbid_commit_author'
|
||||
|
||||
@hybrid_property
|
||||
def include_commit_author_to_review(self):
|
||||
return self.commit_author == 'include_commit_author'
|
||||
|
||||
def matches(self, source_branch, target_branch, files_changed):
|
||||
"""
|
||||
Check if this review rule matches a branch/files in a pull request
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class NotificationModel(BaseModel):
|
|||
' of Notification got %s' % type(notification))
|
||||
|
||||
def create(
|
||||
self, created_by, notification_subject, notification_body,
|
||||
self, created_by, notification_subject='', notification_body='',
|
||||
notification_type=Notification.TYPE_MESSAGE, recipients=None,
|
||||
mention_recipients=None, with_email=True, email_kwargs=None):
|
||||
"""
|
||||
|
|
@ -64,11 +64,12 @@ class NotificationModel(BaseModel):
|
|||
|
||||
:param created_by: int, str or User instance. User who created this
|
||||
notification
|
||||
:param notification_subject: subject of notification itself
|
||||
:param notification_subject: subject of notification itself,
|
||||
it will be generated automatically from notification_type if not specified
|
||||
:param notification_body: body of notification text
|
||||
it will be generated automatically from notification_type if not specified
|
||||
:param notification_type: type of notification, based on that we
|
||||
pick templates
|
||||
|
||||
:param recipients: list of int, str or User objects, when None
|
||||
is given send to all admins
|
||||
:param mention_recipients: list of int, str or User objects,
|
||||
|
|
@ -82,14 +83,19 @@ class NotificationModel(BaseModel):
|
|||
if recipients and not getattr(recipients, '__iter__', False):
|
||||
raise Exception('recipients must be an iterable object')
|
||||
|
||||
if not (notification_subject and notification_body) and not notification_type:
|
||||
raise ValueError('notification_subject, and notification_body '
|
||||
'cannot be empty when notification_type is not specified')
|
||||
|
||||
created_by_obj = self._get_user(created_by)
|
||||
# default MAIN body if not given
|
||||
email_kwargs = email_kwargs or {'body': notification_body}
|
||||
mention_recipients = mention_recipients or set()
|
||||
|
||||
if not created_by_obj:
|
||||
raise Exception('unknown user %s' % created_by)
|
||||
|
||||
# default MAIN body if not given
|
||||
email_kwargs = email_kwargs or {'body': notification_body}
|
||||
mention_recipients = mention_recipients or set()
|
||||
|
||||
if recipients is None:
|
||||
# recipients is None means to all admins
|
||||
recipients_objs = User.query().filter(User.admin == true()).all()
|
||||
|
|
@ -113,6 +119,15 @@ class NotificationModel(BaseModel):
|
|||
# add mentioned users into recipients
|
||||
final_recipients = set(recipients_objs).union(mention_recipients)
|
||||
|
||||
(subject, email_body, email_body_plaintext) = \
|
||||
EmailNotificationModel().render_email(notification_type, **email_kwargs)
|
||||
|
||||
if not notification_subject:
|
||||
notification_subject = subject
|
||||
|
||||
if not notification_body:
|
||||
notification_body = email_body_plaintext
|
||||
|
||||
notification = Notification.create(
|
||||
created_by=created_by_obj, subject=notification_subject,
|
||||
body=notification_body, recipients=final_recipients,
|
||||
|
|
|
|||
|
|
@ -578,7 +578,7 @@ class PermissionModel(BaseModel):
|
|||
return user_group_write_permissions
|
||||
|
||||
def trigger_permission_flush(self, affected_user_ids=None):
|
||||
affected_user_ids or User.get_all_user_ids()
|
||||
affected_user_ids = 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):
|
||||
|
|
|
|||
|
|
@ -1502,15 +1502,11 @@ class PullRequestModel(BaseModel):
|
|||
'user_role': role
|
||||
}
|
||||
|
||||
# pre-generate the subject for notification itself
|
||||
(subject, _e, body_plaintext) = EmailNotificationModel().render_email(
|
||||
notification_type, **kwargs)
|
||||
|
||||
# create notification objects, and emails
|
||||
NotificationModel().create(
|
||||
created_by=current_rhodecode_user,
|
||||
notification_subject=subject,
|
||||
notification_body=body_plaintext,
|
||||
notification_subject='', # Filled in based on the notification_type
|
||||
notification_body='', # Filled in based on the notification_type
|
||||
notification_type=notification_type,
|
||||
recipients=recipients,
|
||||
email_kwargs=kwargs,
|
||||
|
|
@ -1579,14 +1575,11 @@ class PullRequestModel(BaseModel):
|
|||
'thread_ids': [pr_url],
|
||||
}
|
||||
|
||||
(subject, _e, body_plaintext) = EmailNotificationModel().render_email(
|
||||
EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
|
||||
|
||||
# create notification objects, and emails
|
||||
NotificationModel().create(
|
||||
created_by=updating_user,
|
||||
notification_subject=subject,
|
||||
notification_body=body_plaintext,
|
||||
notification_subject='', # Filled in based on the notification_type
|
||||
notification_body='', # Filled in based on the notification_type
|
||||
notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
|
||||
recipients=recipients,
|
||||
email_kwargs=email_kwargs,
|
||||
|
|
@ -2067,6 +2060,8 @@ class MergeCheck(object):
|
|||
self.error_details = OrderedDict()
|
||||
self.source_commit = AttributeDict()
|
||||
self.target_commit = AttributeDict()
|
||||
self.reviewers_count = 0
|
||||
self.observers_count = 0
|
||||
|
||||
def __repr__(self):
|
||||
return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
|
||||
|
|
@ -2128,11 +2123,12 @@ class MergeCheck(object):
|
|||
# review status, must be always present
|
||||
review_status = pull_request.calculated_review_status()
|
||||
merge_check.review_status = review_status
|
||||
merge_check.reviewers_count = pull_request.reviewers_count
|
||||
merge_check.observers_count = pull_request.observers_count
|
||||
|
||||
status_approved = review_status == ChangesetStatus.STATUS_APPROVED
|
||||
if not status_approved:
|
||||
if not status_approved and merge_check.reviewers_count:
|
||||
log.debug("MergeCheck: cannot merge, approval is pending.")
|
||||
|
||||
msg = _('Pull request reviewer approval is pending.')
|
||||
|
||||
merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
|
||||
|
|
|
|||
|
|
@ -231,6 +231,10 @@ class ScmModel(BaseModel):
|
|||
with_wire={"cache": False})
|
||||
except OSError:
|
||||
continue
|
||||
except RepositoryError:
|
||||
log.exception('Failed to create a repo')
|
||||
continue
|
||||
|
||||
log.debug('found %s paths with repositories', len(repos))
|
||||
return repos
|
||||
|
||||
|
|
|
|||
|
|
@ -425,15 +425,12 @@ class UserModel(BaseModel):
|
|||
'date': datetime.datetime.now()
|
||||
}
|
||||
notification_type = EmailNotificationModel.TYPE_REGISTRATION
|
||||
# pre-generate the subject for notification itself
|
||||
(subject, _e, body_plaintext) = EmailNotificationModel().render_email(
|
||||
notification_type, **kwargs)
|
||||
|
||||
# create notification objects, and emails
|
||||
NotificationModel().create(
|
||||
created_by=new_user,
|
||||
notification_subject=subject,
|
||||
notification_body=body_plaintext,
|
||||
notification_subject='', # Filled in based on the notification_type
|
||||
notification_body='', # Filled in based on the notification_type
|
||||
notification_type=notification_type,
|
||||
recipients=None, # all admins
|
||||
email_kwargs=kwargs,
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class CommentSchema(colander.MappingSchema):
|
|||
colander.String(),
|
||||
validator=colander.OneOf(ChangesetComment.COMMENT_TYPES),
|
||||
missing=ChangesetComment.COMMENT_TYPE_NOTE)
|
||||
|
||||
is_draft = colander.SchemaNode(colander.Boolean(),missing=False)
|
||||
comment_file = colander.SchemaNode(colander.String(), missing=None)
|
||||
comment_line = colander.SchemaNode(colander.String(), missing=None)
|
||||
status_change = colander.SchemaNode(
|
||||
|
|
|
|||
|
|
@ -162,7 +162,6 @@ input[type="button"] {
|
|||
}
|
||||
}
|
||||
|
||||
.btn-warning,
|
||||
.btn-danger,
|
||||
.revoke_perm,
|
||||
.btn-x,
|
||||
|
|
@ -196,6 +195,36 @@ input[type="button"] {
|
|||
}
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
.border ( @border-thickness, @alert3 );
|
||||
background-color: white;
|
||||
color: @alert3;
|
||||
|
||||
a {
|
||||
color: @alert3;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
.border ( @border-thickness, @alert3 );
|
||||
color: white;
|
||||
background-color: @alert3;
|
||||
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
display:none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: white;
|
||||
color: @alert3;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-approved-status {
|
||||
.border ( @border-thickness, @alert1 );
|
||||
background-color: white;
|
||||
|
|
@ -264,7 +293,6 @@ input[type="button"] {
|
|||
margin-left: -1px;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
border-left: 1px solid @grey3;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -342,7 +370,7 @@ input[type="button"] {
|
|||
color: @alert2;
|
||||
|
||||
&:hover {
|
||||
color: darken(@alert2,30%);
|
||||
color: darken(@alert2, 30%);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
|
|
@ -402,6 +430,37 @@ input[type="button"] {
|
|||
}
|
||||
|
||||
|
||||
input[type="submit"].btn-warning {
|
||||
&:extend(.btn-warning);
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:extend(.btn-warning:hover);
|
||||
}
|
||||
|
||||
&.btn-link {
|
||||
&:extend(.btn-link);
|
||||
color: @alert3;
|
||||
|
||||
&:disabled {
|
||||
color: @alert3;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
.border ( @border-thickness-buttons, @alert3 );
|
||||
background-color: white;
|
||||
color: @alert3;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// TODO: johbo: Form button tweaks, check if we can use the classes instead
|
||||
input[type="submit"] {
|
||||
&:extend(.btn-primary);
|
||||
|
|
|
|||
|
|
@ -1002,7 +1002,7 @@ input.filediff-collapse-state {
|
|||
.nav-chunk {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
margin-top: -17px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.nav-chunk.selected {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
|
||||
// Comments
|
||||
@comment-outdated-opacity: 0.6;
|
||||
@comment-outdated-opacity: 1.0;
|
||||
|
||||
.comments {
|
||||
width: 100%;
|
||||
|
|
@ -61,28 +61,37 @@ tr.inline-comments div {
|
|||
visibility: hidden;
|
||||
}
|
||||
|
||||
.comment-draft {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
font-weight: 400;
|
||||
color: @color-draft;
|
||||
}
|
||||
|
||||
.comment-new {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
font-weight: 400;
|
||||
color: @color-new;
|
||||
}
|
||||
|
||||
.comment-label {
|
||||
float: left;
|
||||
|
||||
padding: 0.4em 0.4em;
|
||||
margin: 2px 4px 0px 0px;
|
||||
display: inline-block;
|
||||
padding: 0 8px 0 0;
|
||||
min-height: 0;
|
||||
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
line-height: .8em;
|
||||
|
||||
font-family: @text-italic;
|
||||
font-style: italic;
|
||||
background: #fff none;
|
||||
color: @grey3;
|
||||
border: 1px solid @grey4;
|
||||
white-space: nowrap;
|
||||
|
||||
text-transform: uppercase;
|
||||
min-width: 50px;
|
||||
border-radius: 4px;
|
||||
|
||||
&.todo {
|
||||
color: @color5;
|
||||
|
|
@ -270,63 +279,164 @@ tr.inline-comments div {
|
|||
.comment-outdated {
|
||||
opacity: @comment-outdated-opacity;
|
||||
}
|
||||
|
||||
.comment-outdated-label {
|
||||
color: @grey3;
|
||||
padding-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-comments {
|
||||
border-radius: @border-radius;
|
||||
|
||||
.comment {
|
||||
margin: 0;
|
||||
border-radius: @border-radius;
|
||||
}
|
||||
|
||||
.comment-outdated {
|
||||
opacity: @comment-outdated-opacity;
|
||||
}
|
||||
|
||||
.comment-outdated-label {
|
||||
color: @grey3;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.comment-inline {
|
||||
|
||||
&:first-child {
|
||||
margin: 4px 4px 0 4px;
|
||||
border-top: 1px solid @grey5;
|
||||
border-bottom: 0 solid @grey5;
|
||||
border-left: 1px solid @grey5;
|
||||
border-right: 1px solid @grey5;
|
||||
.border-radius-top(4px);
|
||||
}
|
||||
|
||||
&:only-child {
|
||||
margin: 4px 4px 0 4px;
|
||||
border-top: 1px solid @grey5;
|
||||
border-bottom: 0 solid @grey5;
|
||||
border-left: 1px solid @grey5;
|
||||
border-right: 1px solid @grey5;
|
||||
.border-radius-top(4px);
|
||||
}
|
||||
|
||||
background: white;
|
||||
padding: @comment-padding @comment-padding;
|
||||
border: @comment-padding solid @grey6;
|
||||
margin: 0 4px 0 4px;
|
||||
border-top: 0 solid @grey5;
|
||||
border-bottom: 0 solid @grey5;
|
||||
border-left: 1px solid @grey5;
|
||||
border-right: 1px solid @grey5;
|
||||
|
||||
.text {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.meta {
|
||||
border-bottom: 1px solid @grey6;
|
||||
margin: -5px 0px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
}
|
||||
.comment-selected {
|
||||
border-left: 6px solid @comment-highlight-color;
|
||||
}
|
||||
|
||||
.comment-inline-form-open {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.comment-inline-form {
|
||||
padding: @comment-padding;
|
||||
display: none;
|
||||
}
|
||||
.cb-comment-add-button {
|
||||
margin: @comment-padding;
|
||||
|
||||
.comment-inline-form-edit {
|
||||
padding: 0;
|
||||
margin: 0px 4px 2px 4px;
|
||||
}
|
||||
/* hide add comment button when form is open */
|
||||
|
||||
.reply-thread-container {
|
||||
display: table;
|
||||
width: 100%;
|
||||
padding: 0px 4px 4px 4px;
|
||||
}
|
||||
|
||||
.reply-thread-container-wrapper {
|
||||
margin: 0 4px 4px 4px;
|
||||
border-top: 0 solid @grey5;
|
||||
border-bottom: 1px solid @grey5;
|
||||
border-left: 1px solid @grey5;
|
||||
border-right: 1px solid @grey5;
|
||||
.border-radius-bottom(4px);
|
||||
}
|
||||
|
||||
.reply-thread-gravatar {
|
||||
display: table-cell;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding-top: 10px;
|
||||
padding-left: 10px;
|
||||
background-color: #eeeeee;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.reply-thread-reply-button {
|
||||
display: table-cell;
|
||||
width: 100%;
|
||||
height: 33px;
|
||||
padding: 3px 8px;
|
||||
margin-left: 8px;
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
.reply-thread-reply-button .cb-comment-add-button {
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
padding: 6px 2px;
|
||||
text-align: left;
|
||||
cursor: text;
|
||||
color: @grey3;
|
||||
}
|
||||
.reply-thread-reply-button .cb-comment-add-button:hover {
|
||||
background-color: white;
|
||||
color: @grey2;
|
||||
}
|
||||
|
||||
.reply-thread-last {
|
||||
display: table-cell;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
/* Hide reply box when it's a first element,
|
||||
can happen when drafts are saved but not shown to specific user,
|
||||
or there are outdated comments hidden
|
||||
*/
|
||||
.reply-thread-container-wrapper:first-child:not(.comment-form-active) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.reply-thread-container-wrapper.comment-outdated {
|
||||
display: none
|
||||
}
|
||||
|
||||
/* hide add comment button when form is open */
|
||||
.comment-inline-form-open ~ .cb-comment-add-button {
|
||||
display: none;
|
||||
}
|
||||
.comment-inline-form-open {
|
||||
display: block;
|
||||
}
|
||||
/* hide add comment button when form but no comments */
|
||||
.comment-inline-form:first-child + .cb-comment-add-button {
|
||||
display: none;
|
||||
}
|
||||
/* hide add comment button when no comments or form */
|
||||
.cb-comment-add-button:first-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* hide add comment button when only comment is being deleted */
|
||||
.comment-deleting:first-child + .cb-comment-add-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* hide add comment button when form but no comments */
|
||||
.comment-inline-form:first-child + .cb-comment-add-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.show-outdated-comments {
|
||||
display: inline;
|
||||
|
|
@ -380,23 +490,36 @@ form.comment-form {
|
|||
}
|
||||
|
||||
.comment-footer {
|
||||
position: relative;
|
||||
display: table;
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
height: 42px;
|
||||
|
||||
.status_box,
|
||||
.comment-status-box,
|
||||
.cancel-button {
|
||||
float: left;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status_box {
|
||||
.comment-status-box {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
float: left;
|
||||
display: inline-block;
|
||||
display: table-cell;
|
||||
padding: 5px 0 5px 2px;
|
||||
}
|
||||
|
||||
.toolbar-text {
|
||||
height: 28px;
|
||||
display: table-cell;
|
||||
vertical-align: baseline;
|
||||
font-size: 11px;
|
||||
color: @grey4;
|
||||
text-align: right;
|
||||
|
||||
a {
|
||||
color: @grey4;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.action-buttons-extra {
|
||||
|
|
@ -427,10 +550,10 @@ form.comment-form {
|
|||
margin-right: 0;
|
||||
}
|
||||
|
||||
.comment-footer {
|
||||
margin-bottom: 50px;
|
||||
margin-top: 10px;
|
||||
#save_general {
|
||||
margin-left: -6px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -482,8 +605,8 @@ form.comment-form {
|
|||
.injected_diff .comment-inline-form,
|
||||
.comment-inline-form {
|
||||
background-color: white;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.inline-form {
|
||||
|
|
@ -519,9 +642,6 @@ form.comment-form {
|
|||
margin: 0px;
|
||||
}
|
||||
|
||||
.comment-inline-form .comment-footer {
|
||||
margin: 10px 0px 0px 0px;
|
||||
}
|
||||
|
||||
.hide-inline-form-button {
|
||||
margin-left: 5px;
|
||||
|
|
@ -547,6 +667,7 @@ comment-area-text {
|
|||
|
||||
.comment-area-header {
|
||||
height: 35px;
|
||||
border-bottom: 1px solid @grey5;
|
||||
}
|
||||
|
||||
.comment-area-header .nav-links {
|
||||
|
|
@ -554,6 +675,7 @@ comment-area-text {
|
|||
flex-flow: row wrap;
|
||||
-webkit-flex-flow: row wrap;
|
||||
width: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.comment-area-footer {
|
||||
|
|
@ -622,14 +744,3 @@ comment-area-text {
|
|||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.toolbar-text {
|
||||
float: right;
|
||||
font-size: 11px;
|
||||
color: @grey4;
|
||||
text-align: right;
|
||||
|
||||
a {
|
||||
color: @grey4;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -213,7 +213,6 @@ div.markdown-block pre {
|
|||
div.markdown-block img {
|
||||
border-style: none;
|
||||
background-color: #fff;
|
||||
padding-right: 20px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
|
|
@ -274,6 +273,13 @@ div.markdown-block #ws {
|
|||
background-color: @grey6;
|
||||
}
|
||||
|
||||
div.markdown-block p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
padding: 0;
|
||||
line-height: unset;
|
||||
}
|
||||
|
||||
div.markdown-block code,
|
||||
div.markdown-block pre,
|
||||
div.markdown-block #ws,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
|
||||
.loginbox {
|
||||
max-width: 65%;
|
||||
max-width: 960px;
|
||||
margin: @pagepadding auto;
|
||||
font-family: @text-light;
|
||||
border: @border-thickness solid @grey5;
|
||||
|
|
@ -22,13 +22,27 @@
|
|||
float: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
.header-account {
|
||||
min-height: 49px;
|
||||
width: 100%;
|
||||
padding: 0 35px;
|
||||
padding: 0 @header-padding;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
vertical-align: bottom;
|
||||
|
||||
background-color: @grey1;
|
||||
color: @grey5;
|
||||
|
||||
.title {
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: "";
|
||||
clear: both;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +83,7 @@
|
|||
.sign-in-image {
|
||||
display: block;
|
||||
width: 65%;
|
||||
margin: 5% auto;
|
||||
margin: 1% auto;
|
||||
}
|
||||
|
||||
.sign-in-title {
|
||||
|
|
|
|||
|
|
@ -263,9 +263,6 @@ input.inline[type="file"] {
|
|||
// HEADER
|
||||
.header {
|
||||
|
||||
// TODO: johbo: Fix login pages, so that they work without a min-height
|
||||
// for the header and then remove the min-height. I chose a smaller value
|
||||
// intentionally here to avoid rendering issues in the main navigation.
|
||||
min-height: 49px;
|
||||
min-width: 1024px;
|
||||
|
||||
|
|
@ -1143,9 +1140,8 @@ label {
|
|||
margin-left: -15px;
|
||||
}
|
||||
|
||||
#rev_range_container, #rev_range_clear, #rev_range_more {
|
||||
margin-top: -5px;
|
||||
margin-bottom: -5px;
|
||||
#rev_range_action {
|
||||
margin-bottom: -8px;
|
||||
}
|
||||
|
||||
#filter_changelog {
|
||||
|
|
@ -1591,9 +1587,9 @@ table.integrations {
|
|||
}
|
||||
.pr-details-title {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
line-height: 16px;
|
||||
|
||||
padding-bottom: 8px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: @border-thickness solid @grey5;
|
||||
|
||||
.action_button.disabled {
|
||||
|
|
@ -3212,8 +3208,13 @@ details:not([open]) > :not(summary) {
|
|||
|
||||
.sidebar-element {
|
||||
margin-top: 20px;
|
||||
|
||||
.icon-draft {
|
||||
color: @color-draft
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.right-sidebar-collapsed-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -3235,5 +3236,4 @@ details:not([open]) > :not(summary) {
|
|||
|
||||
.old-comments-marker td {
|
||||
padding-top: 15px;
|
||||
border-bottom: 1px solid @grey5;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,11 +115,9 @@ div.readme_box pre {
|
|||
div.readme_box img {
|
||||
border-style: none;
|
||||
background-color: #fff;
|
||||
padding-right: 20px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
|
||||
div.readme_box strong {
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
|
|
@ -152,6 +150,13 @@ div.readme_box a:visited {
|
|||
}
|
||||
*/
|
||||
|
||||
div.readme_box p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
padding: 0;
|
||||
line-height: unset;
|
||||
}
|
||||
|
||||
|
||||
div.readme_box button {
|
||||
font-size: @basefontsize;
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@
|
|||
|
||||
// Highlight color for lines and colors
|
||||
@comment-highlight-color: #ffd887;
|
||||
@color-draft: darken(@alert3, 30%);
|
||||
@color-new: darken(@alert1, 5%);
|
||||
|
||||
// FONTS
|
||||
@basefontsize: 13px;
|
||||
|
|
|
|||
|
|
@ -248,6 +248,7 @@ function registerRCRoutes() {
|
|||
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('pullrequest_comments', '/%(repo_name)s/pull-request/%(pull_request_id)s/comments', ['repo_name', 'pull_request_id']);
|
||||
pyroutes.register('pullrequest_todos', '/%(repo_name)s/pull-request/%(pull_request_id)s/todos', ['repo_name', 'pull_request_id']);
|
||||
pyroutes.register('pullrequest_drafts', '/%(repo_name)s/pull-request/%(pull_request_id)s/drafts', ['repo_name', 'pull_request_id']);
|
||||
pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
|
||||
pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
|
||||
pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
|
||||
|
|
@ -386,6 +387,8 @@ function registerRCRoutes() {
|
|||
pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
|
||||
pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
|
||||
pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
|
||||
pyroutes.register('pullrequest_draft_comments_submit', '/%(repo_name)s/pull-request/%(pull_request_id)s/draft_comments_submit', ['repo_name', 'pull_request_id']);
|
||||
pyroutes.register('commit_draft_comments_submit', '/%(repo_name)s/changeset/%(commit_id)s/draft_comments_submit', ['repo_name', 'commit_id']);
|
||||
pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
|
||||
pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
|
||||
pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
|
||||
|
|
|
|||
|
|
@ -71,14 +71,20 @@ export class RhodecodeApp extends PolymerElement {
|
|||
if (elem) {
|
||||
elem.handleNotification(data);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
handleComment(data) {
|
||||
if (data.message.comment_id) {
|
||||
|
||||
if (data.message.comment_data.length !== 0) {
|
||||
if (window.refreshAllComments !== undefined) {
|
||||
refreshAllComments()
|
||||
}
|
||||
var json_data = data.message.comment_data;
|
||||
|
||||
if (window.commentsController !== undefined) {
|
||||
|
||||
window.commentsController.attachComment(json_data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -704,3 +704,13 @@ var storeUserSessionAttr = function (key, val) {
|
|||
ajaxPOST(pyroutes.url('store_user_session_value'), postData, success);
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
var getUserSessionAttr = function(key) {
|
||||
var storeKey = templateContext.session_attrs;
|
||||
var val = storeKey[key]
|
||||
if (val !== undefined) {
|
||||
return JSON.parse(val)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -349,7 +349,12 @@ var initCommentBoxCodeMirror = function(CommentForm, textAreaId, triggerActions)
|
|||
};
|
||||
|
||||
var submitForm = function(cm, pred) {
|
||||
$(cm.display.input.textarea.form).submit();
|
||||
$(cm.display.input.textarea.form).find('.submit-comment-action').click();
|
||||
return CodeMirror.Pass;
|
||||
};
|
||||
|
||||
var submitFormAsDraft = function(cm, pred) {
|
||||
$(cm.display.input.textarea.form).find('.submit-draft-action').click();
|
||||
return CodeMirror.Pass;
|
||||
};
|
||||
|
||||
|
|
@ -475,9 +480,11 @@ var initCommentBoxCodeMirror = function(CommentForm, textAreaId, triggerActions)
|
|||
// submit form on Meta-Enter
|
||||
if (OSType === "mac") {
|
||||
extraKeys["Cmd-Enter"] = submitForm;
|
||||
extraKeys["Shift-Cmd-Enter"] = submitFormAsDraft;
|
||||
}
|
||||
else {
|
||||
extraKeys["Ctrl-Enter"] = submitForm;
|
||||
extraKeys["Shift-Ctrl-Enter"] = submitFormAsDraft;
|
||||
}
|
||||
|
||||
if (triggerActions) {
|
||||
|
|
|
|||
|
|
@ -124,16 +124,20 @@ var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
|
|||
this.statusChange = this.withLineNo('#change_status');
|
||||
|
||||
this.submitForm = formElement;
|
||||
this.submitButton = $(this.submitForm).find('input[type="submit"]');
|
||||
|
||||
this.submitButton = $(this.submitForm).find('.submit-comment-action');
|
||||
this.submitButtonText = this.submitButton.val();
|
||||
|
||||
this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
|
||||
this.submitDraftButtonText = this.submitDraftButton.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.submitDraftButton.hide();
|
||||
this.submitButtonText = _gettext('Update Comment');
|
||||
$(this.commentType).prop('disabled', true);
|
||||
$(this.commentType).addClass('disabled');
|
||||
var editInfo =
|
||||
|
|
@ -215,10 +219,17 @@ var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
|
|||
this.getCommentStatus = function() {
|
||||
return $(this.submitForm).find(this.statusChange).val();
|
||||
};
|
||||
|
||||
this.getCommentType = function() {
|
||||
return $(this.submitForm).find(this.commentType).val();
|
||||
};
|
||||
|
||||
this.getDraftState = function () {
|
||||
var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
|
||||
var data = $(submitterElem).data('isDraft');
|
||||
return data
|
||||
}
|
||||
|
||||
this.getResolvesId = function() {
|
||||
return $(this.submitForm).find(this.resolvesId).val() || null;
|
||||
};
|
||||
|
|
@ -233,7 +244,9 @@ var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
|
|||
};
|
||||
|
||||
this.isAllowedToSubmit = function() {
|
||||
return !$(this.submitButton).prop('disabled');
|
||||
var commentDisabled = $(this.submitButton).prop('disabled');
|
||||
var draftDisabled = $(this.submitDraftButton).prop('disabled');
|
||||
return !commentDisabled && !draftDisabled;
|
||||
};
|
||||
|
||||
this.initStatusChangeSelector = function(){
|
||||
|
|
@ -259,11 +272,13 @@ var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
|
|||
dropdownAutoWidth: true,
|
||||
minimumResultsForSearch: -1
|
||||
});
|
||||
|
||||
$(this.submitForm).find(this.statusChange).on('change', function() {
|
||||
var status = self.getCommentStatus();
|
||||
|
||||
if (status && !self.isInline()) {
|
||||
$(self.submitButton).prop('disabled', false);
|
||||
$(self.submitDraftButton).prop('disabled', false);
|
||||
}
|
||||
|
||||
var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
|
||||
|
|
@ -295,10 +310,10 @@ var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
|
|||
$(this.statusChange).select2('readonly', false);
|
||||
};
|
||||
|
||||
this.globalSubmitSuccessCallback = function(){
|
||||
this.globalSubmitSuccessCallback = function(comment){
|
||||
// default behaviour is to call GLOBAL hook, if it's registered.
|
||||
if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
|
||||
commentFormGlobalSubmitSuccessCallback();
|
||||
commentFormGlobalSubmitSuccessCallback(comment);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -321,6 +336,7 @@ var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
|
|||
var text = self.cm.getValue();
|
||||
var status = self.getCommentStatus();
|
||||
var commentType = self.getCommentType();
|
||||
var isDraft = self.getDraftState();
|
||||
var resolvesCommentId = self.getResolvesId();
|
||||
var closePullRequest = self.getClosePr();
|
||||
|
||||
|
|
@ -348,12 +364,15 @@ var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
|
|||
postData['close_pull_request'] = true;
|
||||
}
|
||||
|
||||
var submitSuccessCallback = function(o) {
|
||||
// submitSuccess for general comments
|
||||
var submitSuccessCallback = function(json_data) {
|
||||
// reload page if we change status for single commit.
|
||||
if (status && self.commitId) {
|
||||
location.reload(true);
|
||||
} else {
|
||||
$('#injected_page_comments').append(o.rendered_text);
|
||||
// inject newly created comments, json_data is {<comment_id>: {}}
|
||||
self.attachGeneralComment(json_data)
|
||||
|
||||
self.resetCommentFormState();
|
||||
timeagoActivate();
|
||||
tooltipActivate();
|
||||
|
|
@ -365,7 +384,7 @@ var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
|
|||
}
|
||||
|
||||
// run global callback on submit
|
||||
self.globalSubmitSuccessCallback();
|
||||
self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
|
||||
|
||||
};
|
||||
var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
|
||||
|
|
@ -409,10 +428,20 @@ var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
|
|||
}
|
||||
|
||||
$(this.submitButton).prop('disabled', submitState);
|
||||
$(this.submitDraftButton).prop('disabled', submitState);
|
||||
|
||||
if (submitEvent) {
|
||||
$(this.submitButton).val(_gettext('Submitting...'));
|
||||
var isDraft = self.getDraftState();
|
||||
|
||||
if (isDraft) {
|
||||
$(this.submitDraftButton).val(_gettext('Saving Draft...'));
|
||||
} else {
|
||||
$(this.submitButton).val(_gettext('Submitting...'));
|
||||
}
|
||||
|
||||
} else {
|
||||
$(this.submitButton).val(this.submitButtonText);
|
||||
$(this.submitDraftButton).val(this.submitDraftButtonText);
|
||||
}
|
||||
|
||||
};
|
||||
|
|
@ -488,6 +517,7 @@ var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
|
|||
if (!allowedToSubmit){
|
||||
return false;
|
||||
}
|
||||
|
||||
self.handleFormSubmit();
|
||||
});
|
||||
|
||||
|
|
@ -538,26 +568,6 @@ var CommentsController = function() {
|
|||
var mainComment = '#text';
|
||||
var self = this;
|
||||
|
||||
this.cancelComment = function (node) {
|
||||
var $node = $(node);
|
||||
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(
|
||||
|
|
@ -655,12 +665,51 @@ var CommentsController = function() {
|
|||
return self.scrollToComment(node, -1, true);
|
||||
};
|
||||
|
||||
this.cancelComment = function (node) {
|
||||
var $node = $(node);
|
||||
var edit = $(this).attr('edit');
|
||||
var $inlineComments = $node.closest('div.inline-comments');
|
||||
|
||||
if (edit) {
|
||||
var $general_comments = null;
|
||||
if (!$inlineComments.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 = $inlineComments.find('div.comment:hidden');
|
||||
}
|
||||
$comment.show();
|
||||
}
|
||||
var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper')
|
||||
$replyWrapper.removeClass('comment-form-active');
|
||||
|
||||
var lastComment = $inlineComments.find('.comment-inline').last();
|
||||
if ($(lastComment).hasClass('comment-outdated')) {
|
||||
$replyWrapper.hide();
|
||||
}
|
||||
|
||||
$node.closest('.comment-inline-form').remove();
|
||||
return false;
|
||||
};
|
||||
|
||||
this._deleteComment = function(node) {
|
||||
var $node = $(node);
|
||||
var $td = $node.closest('td');
|
||||
var $comment = $node.closest('.comment');
|
||||
var comment_id = $comment.attr('data-comment-id');
|
||||
var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
|
||||
var comment_id = $($comment).data('commentId');
|
||||
var isDraft = $($comment).data('commentDraft');
|
||||
|
||||
var pullRequestId = templateContext.pull_request_data.pull_request_id;
|
||||
var commitId = templateContext.commit_data.commit_id;
|
||||
|
||||
if (pullRequestId) {
|
||||
var url = pyroutes.url('pullrequest_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
|
||||
} else if (commitId) {
|
||||
var url = pyroutes.url('repo_commit_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "commit_id": commitId})
|
||||
}
|
||||
|
||||
var postData = {
|
||||
'csrf_token': CSRF_TOKEN
|
||||
};
|
||||
|
|
@ -677,10 +726,14 @@ var CommentsController = function() {
|
|||
updateSticky()
|
||||
}
|
||||
|
||||
if (window.refreshAllComments !== undefined) {
|
||||
if (window.refreshAllComments !== undefined && !isDraft) {
|
||||
// if we have this handler, run it, and refresh all comments boxes
|
||||
refreshAllComments()
|
||||
}
|
||||
else if (window.refreshDraftComments !== undefined && isDraft) {
|
||||
// if we have this handler, run it, and refresh all comments boxes
|
||||
refreshDraftComments();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
|
@ -695,8 +748,6 @@ var CommentsController = function() {
|
|||
};
|
||||
ajaxPOST(url, postData, success, failure);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
this.deleteComment = function(node) {
|
||||
|
|
@ -716,7 +767,59 @@ var CommentsController = function() {
|
|||
})
|
||||
};
|
||||
|
||||
this._finalizeDrafts = function(commentIds) {
|
||||
|
||||
var pullRequestId = templateContext.pull_request_data.pull_request_id;
|
||||
var commitId = templateContext.commit_data.commit_id;
|
||||
|
||||
if (pullRequestId) {
|
||||
var url = pyroutes.url('pullrequest_draft_comments_submit', {"repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
|
||||
} else if (commitId) {
|
||||
var url = pyroutes.url('commit_draft_comments_submit', {"repo_name": templateContext.repo_name, "commit_id": commitId})
|
||||
}
|
||||
|
||||
// remove the drafts so we can lock them before submit.
|
||||
$.each(commentIds, function(idx, val){
|
||||
$('#comment-{0}'.format(val)).remove();
|
||||
})
|
||||
|
||||
var postData = {'comments': commentIds, 'csrf_token': CSRF_TOKEN};
|
||||
|
||||
var submitSuccessCallback = function(json_data) {
|
||||
self.attachInlineComment(json_data);
|
||||
|
||||
if (window.refreshDraftComments !== undefined) {
|
||||
// if we have this handler, run it, and refresh all comments boxes
|
||||
refreshDraftComments()
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
ajaxPOST(url, postData, submitSuccessCallback)
|
||||
|
||||
}
|
||||
|
||||
this.finalizeDrafts = function(commentIds, callback) {
|
||||
|
||||
SwalNoAnimation.fire({
|
||||
title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: _gettext('Yes'),
|
||||
|
||||
}).then(function(result) {
|
||||
if (result.value) {
|
||||
if (callback !== undefined) {
|
||||
callback(result)
|
||||
}
|
||||
self._finalizeDrafts(commentIds);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
this.toggleWideMode = function (node) {
|
||||
|
||||
if ($('#content').hasClass('wrapper')) {
|
||||
$('#content').removeClass("wrapper");
|
||||
$('#content').addClass("wide-mode-wrapper");
|
||||
|
|
@ -731,18 +834,23 @@ var CommentsController = function() {
|
|||
|
||||
};
|
||||
|
||||
this.toggleComments = function(node, show) {
|
||||
/**
|
||||
* Turn off/on all comments in file diff
|
||||
*/
|
||||
this.toggleDiffComments = function(node) {
|
||||
// Find closes filediff container
|
||||
var $filediff = $(node).closest('.filediff');
|
||||
if (show === true) {
|
||||
$filediff.removeClass('hide-comments');
|
||||
} else if (show === false) {
|
||||
$filediff.find('.hide-line-comments').removeClass('hide-line-comments');
|
||||
$filediff.addClass('hide-comments');
|
||||
} else {
|
||||
$filediff.find('.hide-line-comments').removeClass('hide-line-comments');
|
||||
$filediff.toggleClass('hide-comments');
|
||||
if ($(node).hasClass('toggle-on')) {
|
||||
var show = false;
|
||||
} else if ($(node).hasClass('toggle-off')) {
|
||||
var show = true;
|
||||
}
|
||||
|
||||
// Toggle each individual comment block, so we can un-toggle single ones
|
||||
$.each($filediff.find('.toggle-comment-action'), function(idx, val) {
|
||||
self.toggleLineComments($(val), show)
|
||||
})
|
||||
|
||||
// since we change the height of the diff container that has anchor points for upper
|
||||
// sticky header, we need to tell it to re-calculate those
|
||||
if (window.updateSticky !== undefined) {
|
||||
|
|
@ -752,14 +860,33 @@ var CommentsController = function() {
|
|||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
this.toggleLineComments = function(node, show) {
|
||||
|
||||
var trElem = $(node).closest('tr')
|
||||
|
||||
if (show === true) {
|
||||
// mark outdated comments as visible before the toggle;
|
||||
$(trElem).find('.comment-outdated').show();
|
||||
$(trElem).removeClass('hide-line-comments');
|
||||
} else if (show === false) {
|
||||
$(trElem).find('.comment-outdated').hide();
|
||||
$(trElem).addClass('hide-line-comments');
|
||||
} else {
|
||||
// mark outdated comments as visible before the toggle;
|
||||
$(trElem).find('.comment-outdated').show();
|
||||
$(trElem).toggleClass('hide-line-comments');
|
||||
}
|
||||
|
||||
// since we change the height of the diff container that has anchor points for upper
|
||||
// sticky header, we need to tell it to re-calculate those
|
||||
if (window.updateSticky !== undefined) {
|
||||
// potentially our comments change the active window size, so we
|
||||
// notify sticky elements
|
||||
updateSticky()
|
||||
}
|
||||
|
||||
this.toggleLineComments = function(node) {
|
||||
self.toggleComments(node, true);
|
||||
var $node = $(node);
|
||||
// mark outdated comments as visible before the toggle;
|
||||
$(node.closest('tr')).find('.comment-outdated').show();
|
||||
$node.closest('tr').toggleClass('hide-line-comments');
|
||||
};
|
||||
|
||||
this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
|
||||
|
|
@ -913,63 +1040,58 @@ var CommentsController = function() {
|
|||
return commentForm;
|
||||
};
|
||||
|
||||
this.editComment = function(node) {
|
||||
this.editComment = function(node, line_no, f_path) {
|
||||
self.edit = true;
|
||||
var $node = $(node);
|
||||
var $td = $node.closest('td');
|
||||
|
||||
var $comment = $(node).closest('.comment');
|
||||
var comment_id = $comment.attr('data-comment-id');
|
||||
var $form = null
|
||||
var comment_id = $($comment).data('commentId');
|
||||
var isDraft = $($comment).data('commentDraft');
|
||||
var $editForm = 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)
|
||||
$editForm = $comments.find('.comment-inline-form');
|
||||
line_no = self.getLineNumber(node)
|
||||
}
|
||||
else{
|
||||
// general comments setup
|
||||
$comments = $('#comments');
|
||||
$form = $comments.find('.comment-inline-form');
|
||||
lineno = $comment[0].id
|
||||
$editForm = $comments.find('.comment-inline-form');
|
||||
line_no = $comment[0].id
|
||||
$('#cb-comment-general-form-placeholder').hide();
|
||||
}
|
||||
|
||||
this.edit = true;
|
||||
|
||||
if (!$form.length) {
|
||||
if ($editForm.length === 0) {
|
||||
|
||||
// unhide all comments if they are hidden for a proper REPLY mode
|
||||
var $filediff = $node.closest('.filediff');
|
||||
$filediff.removeClass('hide-comments');
|
||||
var f_path = $filediff.attr('data-f-path');
|
||||
|
||||
// create a new HTML from template
|
||||
$editForm = self.createNewFormWrapper(f_path, line_no);
|
||||
if(f_path && line_no) {
|
||||
$editForm.addClass('comment-inline-form-edit')
|
||||
}
|
||||
|
||||
var tmpl = $('#cb-comment-inline-form-template').html();
|
||||
tmpl = tmpl.format(escapeHtml(f_path), lineno);
|
||||
$form = $(tmpl);
|
||||
$comment.after($form)
|
||||
$comment.after($editForm)
|
||||
|
||||
var _form = $($form[0]).find('form');
|
||||
var _form = $($editForm[0]).find('form');
|
||||
var autocompleteActions = ['as_note',];
|
||||
var commentForm = this.createCommentForm(
|
||||
_form, lineno, '', autocompleteActions, resolvesCommentId,
|
||||
_form, line_no, '', 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();
|
||||
tooltipActivate();
|
||||
|
||||
$.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) {
|
||||
// set a CUSTOM submit handler for inline comment edit action.
|
||||
commentForm.setHandleFormSubmit(function(o) {
|
||||
var text = commentForm.cm.getValue();
|
||||
var commentType = commentForm.getCommentType();
|
||||
|
||||
|
|
@ -1000,14 +1122,15 @@ var CommentsController = function() {
|
|||
var postData = {
|
||||
'text': text,
|
||||
'f_path': f_path,
|
||||
'line': lineno,
|
||||
'line': line_no,
|
||||
'comment_type': commentType,
|
||||
'draft': isDraft,
|
||||
'version': version,
|
||||
'csrf_token': CSRF_TOKEN
|
||||
};
|
||||
|
||||
var submitSuccessCallback = function(json_data) {
|
||||
$form.remove();
|
||||
$editForm.remove();
|
||||
$comment.show();
|
||||
var postData = {
|
||||
'text': text,
|
||||
|
|
@ -1072,8 +1195,7 @@ var CommentsController = function() {
|
|||
'commit_id': templateContext.commit_data.commit_id});
|
||||
|
||||
_submitAjaxPOST(
|
||||
previewUrl, postData, successRenderCommit,
|
||||
failRenderCommit
|
||||
previewUrl, postData, successRenderCommit, failRenderCommit
|
||||
);
|
||||
|
||||
try {
|
||||
|
|
@ -1084,7 +1206,7 @@ var CommentsController = function() {
|
|||
$comments.find('.cb-comment-add-button').before(html);
|
||||
|
||||
// run global callback on submit
|
||||
commentForm.globalSubmitSuccessCallback();
|
||||
commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
@ -1101,10 +1223,14 @@ var CommentsController = function() {
|
|||
updateSticky()
|
||||
}
|
||||
|
||||
if (window.refreshAllComments !== undefined) {
|
||||
if (window.refreshAllComments !== undefined && !isDraft) {
|
||||
// if we have this handler, run it, and refresh all comments boxes
|
||||
refreshAllComments()
|
||||
}
|
||||
else if (window.refreshDraftComments !== undefined && isDraft) {
|
||||
// if we have this handler, run it, and refresh all comments boxes
|
||||
refreshDraftComments();
|
||||
}
|
||||
|
||||
commentForm.setActionButtonsDisabled(false);
|
||||
|
||||
|
|
@ -1129,66 +1255,122 @@ var CommentsController = function() {
|
|||
});
|
||||
}
|
||||
|
||||
$form.addClass('comment-inline-form-open');
|
||||
$editForm.addClass('comment-inline-form-open');
|
||||
};
|
||||
|
||||
this.createComment = function(node, resolutionComment) {
|
||||
var resolvesCommentId = resolutionComment || null;
|
||||
this.attachComment = function(json_data) {
|
||||
var self = this;
|
||||
$.each(json_data, function(idx, val) {
|
||||
var json_data_elem = [val]
|
||||
var isInline = val.comment_f_path && val.comment_lineno
|
||||
|
||||
if (isInline) {
|
||||
self.attachInlineComment(json_data_elem)
|
||||
} else {
|
||||
self.attachGeneralComment(json_data_elem)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
this.attachGeneralComment = function(json_data) {
|
||||
$.each(json_data, function(idx, val) {
|
||||
$('#injected_page_comments').append(val.rendered_text);
|
||||
})
|
||||
}
|
||||
|
||||
this.attachInlineComment = function(json_data) {
|
||||
|
||||
$.each(json_data, function (idx, val) {
|
||||
var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
|
||||
var html = val.rendered_text;
|
||||
var $inlineComments = $('#' + val.target_id)
|
||||
.find(line_qry)
|
||||
.find('.inline-comments');
|
||||
|
||||
var lastComment = $inlineComments.find('.comment-inline').last();
|
||||
|
||||
if (lastComment.length === 0) {
|
||||
// first comment, we append simply
|
||||
$inlineComments.find('.reply-thread-container-wrapper').before(html);
|
||||
} else {
|
||||
$(lastComment).after(html)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
this.createNewFormWrapper = function(f_path, line_no) {
|
||||
// create a new reply HTML form from template
|
||||
var tmpl = $('#cb-comment-inline-form-template').html();
|
||||
tmpl = tmpl.format(escapeHtml(f_path), line_no);
|
||||
return $(tmpl);
|
||||
}
|
||||
|
||||
this.createComment = function(node, f_path, line_no, resolutionComment) {
|
||||
self.edit = false;
|
||||
var $node = $(node);
|
||||
var $td = $node.closest('td');
|
||||
var $form = $td.find('.comment-inline-form');
|
||||
this.edit = false;
|
||||
var resolvesCommentId = resolutionComment || null;
|
||||
|
||||
if (!$form.length) {
|
||||
var $replyForm = $td.find('.comment-inline-form');
|
||||
|
||||
var $filediff = $node.closest('.filediff');
|
||||
$filediff.removeClass('hide-comments');
|
||||
var f_path = $filediff.attr('data-f-path');
|
||||
var lineno = self.getLineNumber(node);
|
||||
// create a new HTML from template
|
||||
var tmpl = $('#cb-comment-inline-form-template').html();
|
||||
tmpl = tmpl.format(escapeHtml(f_path), lineno);
|
||||
$form = $(tmpl);
|
||||
// if form isn't existing, we're generating a new one and injecting it.
|
||||
if ($replyForm.length === 0) {
|
||||
|
||||
// unhide/expand all comments if they are hidden for a proper REPLY mode
|
||||
self.toggleLineComments($node, true);
|
||||
|
||||
$replyForm = self.createNewFormWrapper(f_path, line_no);
|
||||
|
||||
var $comments = $td.find('.inline-comments');
|
||||
if (!$comments.length) {
|
||||
$comments = $(
|
||||
$('#cb-comments-inline-container-template').html());
|
||||
$td.append($comments);
|
||||
|
||||
// There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
|
||||
if ($comments.length===0) {
|
||||
var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(f_path, line_no)
|
||||
var $reply_container = $('#cb-comments-inline-container-template')
|
||||
$reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
|
||||
$td.append($($reply_container).html());
|
||||
}
|
||||
|
||||
$td.find('.cb-comment-add-button').before($form);
|
||||
// default comment button exists, so we prepend the form for leaving initial comment
|
||||
$td.find('.cb-comment-add-button').before($replyForm);
|
||||
// set marker, that we have a open form
|
||||
var $replyWrapper = $td.find('.reply-thread-container-wrapper')
|
||||
$replyWrapper.addClass('comment-form-active');
|
||||
|
||||
var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
|
||||
var _form = $($form[0]).find('form');
|
||||
var lastComment = $comments.find('.comment-inline').last();
|
||||
if ($(lastComment).hasClass('comment-outdated')) {
|
||||
$replyWrapper.show();
|
||||
}
|
||||
|
||||
var _form = $($replyForm[0]).find('form');
|
||||
var autocompleteActions = ['as_note', 'as_todo'];
|
||||
var comment_id=null;
|
||||
var commentForm = this.createCommentForm(
|
||||
_form, lineno, placeholderText, autocompleteActions, resolvesCommentId, this.edit, comment_id);
|
||||
|
||||
$.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
|
||||
form: _form,
|
||||
parent: $td[0],
|
||||
lineno: lineno,
|
||||
f_path: f_path}
|
||||
);
|
||||
var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
|
||||
var commentForm = self.createCommentForm(
|
||||
_form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
|
||||
self.edit, comment_id);
|
||||
|
||||
// set a CUSTOM submit handler for inline comments.
|
||||
commentForm.setHandleFormSubmit(function(o) {
|
||||
var text = commentForm.cm.getValue();
|
||||
var commentType = commentForm.getCommentType();
|
||||
var resolvesCommentId = commentForm.getResolvesId();
|
||||
var isDraft = commentForm.getDraftState();
|
||||
|
||||
if (text === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lineno === undefined) {
|
||||
alert('missing line !');
|
||||
if (line_no === undefined) {
|
||||
alert('Error: unable to fetch line number for this inline comment !');
|
||||
return;
|
||||
}
|
||||
|
||||
if (f_path === undefined) {
|
||||
alert('missing file path !');
|
||||
alert('Error: unable to fetch file path for this inline comment !');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1199,66 +1381,79 @@ var CommentsController = function() {
|
|||
var postData = {
|
||||
'text': text,
|
||||
'f_path': f_path,
|
||||
'line': lineno,
|
||||
'line': line_no,
|
||||
'comment_type': commentType,
|
||||
'draft': isDraft,
|
||||
'csrf_token': CSRF_TOKEN
|
||||
};
|
||||
if (resolvesCommentId){
|
||||
postData['resolves_comment_id'] = resolvesCommentId;
|
||||
}
|
||||
|
||||
// submitSuccess for inline commits
|
||||
var submitSuccessCallback = function(json_data) {
|
||||
$form.remove();
|
||||
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);
|
||||
$replyForm.remove();
|
||||
$td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
|
||||
|
||||
//mark visually which comment was resolved
|
||||
if (resolvesCommentId) {
|
||||
commentForm.markCommentResolved(resolvesCommentId);
|
||||
try {
|
||||
|
||||
// inject newly created comments, json_data is {<comment_id>: {}}
|
||||
self.attachInlineComment(json_data)
|
||||
|
||||
//mark visually which comment was resolved
|
||||
if (resolvesCommentId) {
|
||||
commentForm.markCommentResolved(resolvesCommentId);
|
||||
}
|
||||
|
||||
// run global callback on submit
|
||||
commentForm.globalSubmitSuccessCallback({
|
||||
draft: isDraft,
|
||||
comment_id: comment_id
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
if (window.refreshAllComments !== undefined) {
|
||||
if (window.refreshAllComments !== undefined && !isDraft) {
|
||||
// if we have this handler, run it, and refresh all comments boxes
|
||||
refreshAllComments()
|
||||
}
|
||||
else if (window.refreshDraftComments !== undefined && isDraft) {
|
||||
// if we have this handler, run it, and refresh all comments boxes
|
||||
refreshDraftComments();
|
||||
}
|
||||
|
||||
commentForm.setActionButtonsDisabled(false);
|
||||
|
||||
// re trigger the linkification of next/prev navigation
|
||||
linkifyComments($('.inline-comment-injected'));
|
||||
timeagoActivate();
|
||||
tooltipActivate();
|
||||
};
|
||||
|
||||
var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
|
||||
var prefix = "Error while submitting comment.\n"
|
||||
var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
|
||||
ajaxErrorSwal(message);
|
||||
commentForm.resetCommentFormState(text)
|
||||
};
|
||||
|
||||
commentForm.submitAjaxPOST(
|
||||
commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
|
||||
});
|
||||
}
|
||||
|
||||
$form.addClass('comment-inline-form-open');
|
||||
// Finally "open" our reply form, since we know there are comments and we have the "attached" old form
|
||||
$replyForm.addClass('comment-inline-form-open');
|
||||
tooltipActivate();
|
||||
};
|
||||
|
||||
this.createResolutionComment = function(commentId){
|
||||
|
|
@ -1268,9 +1463,12 @@ var CommentsController = function() {
|
|||
var comment = $('#comment-'+commentId);
|
||||
var commentData = comment.data();
|
||||
if (commentData.commentInline) {
|
||||
this.createComment(comment, commentId)
|
||||
var f_path = commentData.fPath;
|
||||
var line_no = commentData.lineNo;
|
||||
//TODO check this if we need to give f_path/line_no
|
||||
this.createComment(comment, f_path, line_no, commentId)
|
||||
} else {
|
||||
Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
|
||||
this.createGeneralComment('general', "$placeholder", commentId)
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
@ -1296,3 +1494,8 @@ var CommentsController = function() {
|
|||
};
|
||||
|
||||
};
|
||||
|
||||
window.commentHelp = function(renderer) {
|
||||
var funcData = {'renderer': renderer}
|
||||
return renderTemplate('commentHelpHovercard', funcData)
|
||||
}
|
||||
|
|
@ -42,12 +42,22 @@ window.toggleElement = function (elem, target) {
|
|||
var $elem = $(elem);
|
||||
var $target = $(target);
|
||||
|
||||
if ($target.is(':visible') || $target.length === 0) {
|
||||
if (target !== undefined) {
|
||||
var show = $target.is(':visible') || $target.length === 0;
|
||||
} else {
|
||||
var show = $elem.hasClass('toggle-off')
|
||||
}
|
||||
|
||||
if (show) {
|
||||
$target.hide();
|
||||
$elem.html($elem.data('toggleOn'))
|
||||
$elem.addClass('toggle-on')
|
||||
$elem.removeClass('toggle-off')
|
||||
} else {
|
||||
$target.show();
|
||||
$elem.html($elem.data('toggleOff'))
|
||||
$elem.addClass('toggle-off')
|
||||
$elem.removeClass('toggle-on')
|
||||
}
|
||||
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -182,83 +182,34 @@ window.ReviewersController = function () {
|
|||
if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
|
||||
// default rule, case for older repo that don't have any rules stored
|
||||
self.$rulesList.append(
|
||||
self.addRule(
|
||||
_gettext('All reviewers must vote.'))
|
||||
self.addRule(_gettext('All reviewers must vote.'))
|
||||
);
|
||||
return self.forbidUsers
|
||||
}
|
||||
|
||||
if (data.rules.voting !== undefined) {
|
||||
if (data.rules.voting < 0) {
|
||||
self.$rulesList.append(
|
||||
self.addRule(
|
||||
_gettext('All individual reviewers must vote.'))
|
||||
)
|
||||
} else if (data.rules.voting === 1) {
|
||||
self.$rulesList.append(
|
||||
self.addRule(
|
||||
_gettext('At least {0} reviewer must vote.').format(data.rules.voting))
|
||||
)
|
||||
|
||||
} else {
|
||||
self.$rulesList.append(
|
||||
self.addRule(
|
||||
_gettext('At least {0} reviewers must vote.').format(data.rules.voting))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (data.rules.voting_groups !== undefined) {
|
||||
$.each(data.rules.voting_groups, function (index, rule_data) {
|
||||
self.$rulesList.append(
|
||||
self.addRule(rule_data.text)
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
if (data.rules.use_code_authors_for_review) {
|
||||
self.$rulesList.append(
|
||||
self.addRule(
|
||||
_gettext('Reviewers picked from source code changes.'))
|
||||
)
|
||||
}
|
||||
|
||||
if (data.rules.forbid_adding_reviewers) {
|
||||
$('#add_reviewer_input').remove();
|
||||
self.$rulesList.append(
|
||||
self.addRule(
|
||||
_gettext('Adding new reviewers is forbidden.'))
|
||||
)
|
||||
}
|
||||
|
||||
if (data.rules.forbid_author_to_review) {
|
||||
self.forbidUsers.push(data.rules_data.pr_author);
|
||||
self.$rulesList.append(
|
||||
self.addRule(
|
||||
_gettext('Author is not allowed to be a reviewer.'))
|
||||
)
|
||||
if (data.rules_data !== undefined && data.rules_data.forbidden_users !== undefined) {
|
||||
$.each(data.rules_data.forbidden_users, function(idx, val){
|
||||
self.forbidUsers.push(val)
|
||||
})
|
||||
}
|
||||
|
||||
if (data.rules.forbid_commit_author_to_review) {
|
||||
|
||||
if (data.rules_data.forbidden_users) {
|
||||
$.each(data.rules_data.forbidden_users, function (index, member_data) {
|
||||
self.forbidUsers.push(member_data)
|
||||
});
|
||||
}
|
||||
|
||||
if (data.rules_humanized !== undefined && data.rules_humanized.length > 0) {
|
||||
$.each(data.rules_humanized, function(idx, val) {
|
||||
self.$rulesList.append(
|
||||
self.addRule(val)
|
||||
)
|
||||
})
|
||||
} else {
|
||||
// we don't have any rules set, so we inform users about it
|
||||
self.$rulesList.append(
|
||||
self.addRule(
|
||||
_gettext('Commit Authors are not allowed to be a reviewer.'))
|
||||
self.addRule(_gettext('No additional review rules set.'))
|
||||
)
|
||||
}
|
||||
|
||||
// we don't have any rules set, so we inform users about it
|
||||
if (self.enabledRules.length === 0) {
|
||||
self.addRule(
|
||||
_gettext('No review rules set.'))
|
||||
}
|
||||
|
||||
return self.forbidUsers
|
||||
};
|
||||
|
||||
|
|
@ -1066,14 +1017,14 @@ window.ReviewerPresenceController = function (channel) {
|
|||
this.handlePresence = function (data) {
|
||||
if (data.type == 'presence' && data.channel === self.channel) {
|
||||
this.storeUsers(data.users);
|
||||
this.render()
|
||||
this.render();
|
||||
}
|
||||
};
|
||||
|
||||
this.handleChannelUpdate = function (data) {
|
||||
if (data.channel === this.channel) {
|
||||
this.storeUsers(data.state.users);
|
||||
this.render()
|
||||
this.render();
|
||||
}
|
||||
|
||||
};
|
||||
|
|
@ -1085,6 +1036,30 @@ window.ReviewerPresenceController = function (channel) {
|
|||
|
||||
};
|
||||
|
||||
window.refreshCommentsSuccess = function(targetNode, counterNode, extraCallback) {
|
||||
var $targetElem = targetNode;
|
||||
var $counterElem = counterNode;
|
||||
|
||||
return function (data) {
|
||||
var newCount = $(data).data('counter');
|
||||
if (newCount !== undefined) {
|
||||
var callback = function () {
|
||||
$counterElem.animate({'opacity': 1.00}, 200)
|
||||
$counterElem.html(newCount);
|
||||
};
|
||||
$counterElem.animate({'opacity': 0.15}, 200, callback);
|
||||
}
|
||||
|
||||
$targetElem.css('opacity', 1);
|
||||
$targetElem.html(data);
|
||||
tooltipActivate();
|
||||
|
||||
if (extraCallback !== undefined) {
|
||||
extraCallback(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.refreshComments = function (version) {
|
||||
version = version || templateContext.pull_request_data.pull_request_version || '';
|
||||
|
||||
|
|
@ -1109,23 +1084,8 @@ window.refreshComments = function (version) {
|
|||
|
||||
var $targetElem = $('.comments-content-table');
|
||||
$targetElem.css('opacity', 0.3);
|
||||
|
||||
var success = function (data) {
|
||||
var $counterElem = $('#comments-count');
|
||||
var newCount = $(data).data('counter');
|
||||
if (newCount !== undefined) {
|
||||
var callback = function () {
|
||||
$counterElem.animate({'opacity': 1.00}, 200)
|
||||
$counterElem.html(newCount);
|
||||
};
|
||||
$counterElem.animate({'opacity': 0.15}, 200, callback);
|
||||
}
|
||||
|
||||
$targetElem.css('opacity', 1);
|
||||
$targetElem.html(data);
|
||||
tooltipActivate();
|
||||
}
|
||||
|
||||
var $counterElem = $('#comments-count');
|
||||
var success = refreshCommentsSuccess($targetElem, $counterElem);
|
||||
ajaxPOST(loadUrl, data, success, null, {})
|
||||
|
||||
}
|
||||
|
|
@ -1139,7 +1099,7 @@ window.refreshTODOs = function (version) {
|
|||
'repo_name': templateContext.repo_name,
|
||||
'version': version,
|
||||
};
|
||||
var loadUrl = pyroutes.url('pullrequest_comments', params);
|
||||
var loadUrl = pyroutes.url('pullrequest_todos', params);
|
||||
} // commit case
|
||||
else {
|
||||
return
|
||||
|
|
@ -1153,27 +1113,46 @@ window.refreshTODOs = function (version) {
|
|||
var data = {"comments": currentIDs};
|
||||
var $targetElem = $('.todos-content-table');
|
||||
$targetElem.css('opacity', 0.3);
|
||||
|
||||
var success = function (data) {
|
||||
var $counterElem = $('#todos-count')
|
||||
var newCount = $(data).data('counter');
|
||||
if (newCount !== undefined) {
|
||||
var callback = function () {
|
||||
$counterElem.animate({'opacity': 1.00}, 200)
|
||||
$counterElem.html(newCount);
|
||||
};
|
||||
$counterElem.animate({'opacity': 0.15}, 200, callback);
|
||||
}
|
||||
|
||||
$targetElem.css('opacity', 1);
|
||||
$targetElem.html(data);
|
||||
tooltipActivate();
|
||||
}
|
||||
var $counterElem = $('#todos-count');
|
||||
var success = refreshCommentsSuccess($targetElem, $counterElem);
|
||||
|
||||
ajaxPOST(loadUrl, data, success, null, {})
|
||||
|
||||
}
|
||||
|
||||
window.refreshDraftComments = function () {
|
||||
|
||||
// Pull request case
|
||||
if (templateContext.pull_request_data.pull_request_id !== null) {
|
||||
var params = {
|
||||
'pull_request_id': templateContext.pull_request_data.pull_request_id,
|
||||
'repo_name': templateContext.repo_name,
|
||||
};
|
||||
var loadUrl = pyroutes.url('pullrequest_drafts', params);
|
||||
} // commit case
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
var data = {};
|
||||
|
||||
var $targetElem = $('.drafts-content-table');
|
||||
$targetElem.css('opacity', 0.3);
|
||||
var $counterElem = $('#drafts-count');
|
||||
var extraCallback = function(data) {
|
||||
if ($(data).data('counter') == 0){
|
||||
$('#draftsTable').hide();
|
||||
} else {
|
||||
$('#draftsTable').show();
|
||||
}
|
||||
// uncheck on load the select all checkbox
|
||||
$('[name=select_all_drafts]').prop('checked', 0);
|
||||
}
|
||||
var success = refreshCommentsSuccess($targetElem, $counterElem, extraCallback);
|
||||
|
||||
ajaxPOST(loadUrl, data, success, null, {})
|
||||
};
|
||||
|
||||
window.refreshAllComments = function (version) {
|
||||
version = version || templateContext.pull_request_data.pull_request_version || '';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
/__MAIN_APP__ - launched when rhodecode-app element is attached to DOM
|
||||
/plugins/__REGISTER__ - launched after the onDomReady() code from rhodecode.js is executed
|
||||
/ui/plugins/code/anchor_focus - launched when rc starts to scroll on load to anchor on PR/Codeview
|
||||
/ui/plugins/code/comment_form_built - launched when injectInlineForm() is executed and the form object is created
|
||||
/notifications - shows new event notifications
|
||||
/connection_controller/subscribe - subscribes user to new channels
|
||||
/connection_controller/presence - receives presence change messages
|
||||
|
|
|
|||
|
|
@ -104,12 +104,6 @@ def add_request_user_context(event):
|
|||
request.environ['rc_req_id'] = req_id
|
||||
|
||||
|
||||
def inject_app_settings(event):
|
||||
settings = event.app.registry.settings
|
||||
# inject info about available permissions
|
||||
auth.set_available_permissions(settings)
|
||||
|
||||
|
||||
def scan_repositories_if_enabled(event):
|
||||
"""
|
||||
This is subscribed to the `pyramid.events.ApplicationCreated` event. It
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@
|
|||
$pullRequestListTable.DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
stateDuration: -1,
|
||||
ajax: {
|
||||
"url": "${h.route_path('my_account_pullrequests_data')}",
|
||||
"data": function (d) {
|
||||
|
|
@ -119,6 +121,10 @@
|
|||
if (data['owned']) {
|
||||
$(row).addClass('owned');
|
||||
}
|
||||
},
|
||||
"stateSaveParams": function (settings, data) {
|
||||
data.search.search = ""; // Don't save search
|
||||
data.start = 0; // don't save pagination
|
||||
}
|
||||
});
|
||||
$pullRequestListTable.on('xhr.dt', function (e, settings, json, xhr) {
|
||||
|
|
@ -129,6 +135,7 @@
|
|||
$pullRequestListTable.css('opacity', 0.3);
|
||||
});
|
||||
|
||||
|
||||
// filter
|
||||
$('#q_filter').on('keyup',
|
||||
$.debounce(250, function () {
|
||||
|
|
|
|||
|
|
@ -1225,6 +1225,14 @@
|
|||
(function () {
|
||||
"use sctrict";
|
||||
|
||||
// details block auto-hide menu
|
||||
$(document).mouseup(function(e) {
|
||||
var container = $('.details-inline-block');
|
||||
if (!container.is(e.target) && container.has(e.target).length === 0) {
|
||||
$('.details-inline-block[open]').removeAttr('open')
|
||||
}
|
||||
});
|
||||
|
||||
var $sideBar = $('.right-sidebar');
|
||||
var expanded = $sideBar.hasClass('right-sidebar-expanded');
|
||||
var sidebarState = templateContext.session_attrs.sidebarState;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
## ${sidebar.comments_table()}
|
||||
<%namespace name="base" file="/base/base.mako"/>
|
||||
|
||||
<%def name="comments_table(comments, counter_num, todo_comments=False, existing_ids=None, is_pr=True)">
|
||||
<%def name="comments_table(comments, counter_num, todo_comments=False, draft_comments=False, existing_ids=None, is_pr=True)">
|
||||
<%
|
||||
if todo_comments:
|
||||
cls_ = 'todos-content-table'
|
||||
|
|
@ -15,10 +15,13 @@
|
|||
# own comments first
|
||||
user_id = 0
|
||||
return '{}'.format(str(entry.comment_id).zfill(10000))
|
||||
elif draft_comments:
|
||||
cls_ = 'drafts-content-table'
|
||||
def sorter(entry):
|
||||
return '{}'.format(str(entry.comment_id).zfill(10000))
|
||||
else:
|
||||
cls_ = 'comments-content-table'
|
||||
def sorter(entry):
|
||||
user_id = entry.author.user_id
|
||||
return '{}'.format(str(entry.comment_id).zfill(10000))
|
||||
|
||||
existing_ids = existing_ids or []
|
||||
|
|
@ -31,8 +34,12 @@
|
|||
<%
|
||||
display = ''
|
||||
_cls = ''
|
||||
## Extra precaution to not show drafts in the sidebar for todo/comments
|
||||
if comment_obj.draft and not draft_comments:
|
||||
continue
|
||||
%>
|
||||
|
||||
|
||||
<%
|
||||
comment_ver_index = comment_obj.get_index_version(getattr(c, 'versions', []))
|
||||
prev_comment_ver_index = 0
|
||||
|
|
@ -83,6 +90,11 @@
|
|||
% endif
|
||||
|
||||
<tr class="${_cls}" style="display: ${display};" data-sidebar-comment-id="${comment_obj.comment_id}">
|
||||
% if draft_comments:
|
||||
<td style="width: 15px;">
|
||||
${h.checkbox('submit_draft', id=None, value=comment_obj.comment_id)}
|
||||
</td>
|
||||
% endif
|
||||
<td class="td-todo-number">
|
||||
<%
|
||||
version_info = ''
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@
|
|||
|
||||
<%def name="main()">
|
||||
<script type="text/javascript">
|
||||
// TODO: marcink switch this to pyroutes
|
||||
AJAX_COMMENT_DELETE_URL = "${h.route_path('repo_commit_comment_delete',repo_name=c.repo_name,commit_id=c.commit.raw_id,comment_id='__COMMENT_ID__')}";
|
||||
templateContext.commit_data.commit_id = "${c.commit.raw_id}";
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
## this is a dummy html file for partial rendering on server and sending
|
||||
## generated output via ajax after comment submit
|
||||
<%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
|
||||
${comment.comment_block(c.co, inline=c.co.is_inline)}
|
||||
${comment.comment_block(c.co, inline=c.co.is_inline, is_new=c.is_new)}
|
||||
|
|
|
|||
|
|
@ -3,20 +3,25 @@
|
|||
## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
|
||||
## ${comment.comment_block(comment)}
|
||||
##
|
||||
<%namespace name="base" file="/base/base.mako"/>
|
||||
|
||||
<%!
|
||||
from rhodecode.lib import html_filters
|
||||
%>
|
||||
|
||||
<%namespace name="base" file="/base/base.mako"/>
|
||||
<%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
|
||||
|
||||
<%def name="comment_block(comment, inline=False, active_pattern_entries=None, is_new=False)">
|
||||
|
||||
<%
|
||||
from rhodecode.model.comment import CommentsModel
|
||||
comment_model = CommentsModel()
|
||||
from rhodecode.model.comment import CommentsModel
|
||||
comment_model = CommentsModel()
|
||||
|
||||
comment_ver = comment.get_index_version(getattr(c, 'versions', []))
|
||||
latest_ver = len(getattr(c, 'versions', []))
|
||||
visible_for_user = True
|
||||
if comment.draft:
|
||||
visible_for_user = comment.user_id == c.rhodecode_user.user_id
|
||||
%>
|
||||
<% comment_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
|
||||
<% latest_ver = len(getattr(c, 'versions', [])) %>
|
||||
|
||||
% if inline:
|
||||
<% outdated_at_ver = comment.outdated_at_version(c.at_version_num) %>
|
||||
|
|
@ -24,6 +29,7 @@
|
|||
<% outdated_at_ver = comment.older_than_version(c.at_version_num) %>
|
||||
% endif
|
||||
|
||||
% if visible_for_user:
|
||||
<div class="comment
|
||||
${'comment-inline' if inline else 'comment-general'}
|
||||
${'comment-outdated' if outdated_at_ver else 'comment-current'}"
|
||||
|
|
@ -31,14 +37,26 @@
|
|||
line="${comment.line_no}"
|
||||
data-comment-id="${comment.comment_id}"
|
||||
data-comment-type="${comment.comment_type}"
|
||||
data-comment-draft=${h.json.dumps(comment.draft)}
|
||||
data-comment-renderer="${comment.renderer}"
|
||||
data-comment-text="${comment.text | html_filters.base64,n}"
|
||||
data-comment-f-path="${comment.f_path}"
|
||||
data-comment-line-no="${comment.line_no}"
|
||||
data-comment-inline=${h.json.dumps(inline)}
|
||||
style="${'display: none;' if outdated_at_ver else ''}">
|
||||
|
||||
<div class="meta">
|
||||
<div class="comment-type-label">
|
||||
% if comment.draft:
|
||||
<div class="tooltip comment-draft" title="${_('Draft comments are only visible to the author until submitted')}.">
|
||||
DRAFT
|
||||
</div>
|
||||
% elif is_new:
|
||||
<div class="tooltip comment-new" title="${_('This comment was added while you browsed this page')}.">
|
||||
NEW
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
|
||||
|
||||
## TODO COMMENT
|
||||
|
|
@ -90,7 +108,7 @@
|
|||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## NOTE 0 and .. => because we disable it for now until UI ready
|
||||
% if 0 and comment.status_change:
|
||||
<div class="pull-left">
|
||||
<span class="tag authortag tooltip" title="${_('Status from pull request.')}">
|
||||
|
|
@ -100,10 +118,12 @@
|
|||
</span>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
## Since only author can see drafts, we don't show it
|
||||
% if not comment.draft:
|
||||
<div class="author ${'author-inline' if inline else 'author-general'}">
|
||||
${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<div class="date">
|
||||
${h.age_component(comment.modified_at, time_is_local=True)}
|
||||
|
|
@ -164,7 +184,7 @@
|
|||
% if inline:
|
||||
<a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
|
||||
% if outdated_at_ver:
|
||||
<code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">outdated ${'v{}'.format(comment_ver)}</code>
|
||||
<strong class="comment-outdated-label">outdated</strong> <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
|
||||
<code class="action-divider">|</code>
|
||||
% elif comment_ver:
|
||||
<code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
|
||||
|
|
@ -210,11 +230,17 @@
|
|||
%if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="dropdown-item">
|
||||
<a onclick="return Rhodecode.comments.editComment(this);" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
|
||||
<a onclick="return Rhodecode.comments.editComment(this, '${comment.line_no}', '${comment.f_path}');" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
|
||||
</div>
|
||||
<div class="dropdown-item">
|
||||
<a onclick="return Rhodecode.comments.deleteComment(this);" class="btn btn-link btn-sm btn-danger delete-comment">${_('Delete')}</a>
|
||||
</div>
|
||||
## Only available in EE edition
|
||||
% if comment.draft and c.rhodecode_edition_id == 'EE':
|
||||
<div class="dropdown-item">
|
||||
<a onclick="return Rhodecode.comments.finalizeDrafts([${comment.comment_id}]);" class="btn btn-link btn-sm finalize-draft-comment">${_('Submit draft')}</a>
|
||||
</div>
|
||||
% endif
|
||||
%else:
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="dropdown-item">
|
||||
|
|
@ -252,6 +278,7 @@
|
|||
</div>
|
||||
|
||||
</div>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
## generate main comments
|
||||
|
|
@ -298,10 +325,9 @@
|
|||
## inject form here
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
var lineNo = 'general';
|
||||
var resolvesCommentId = null;
|
||||
var generalCommentForm = Rhodecode.comments.createGeneralComment(
|
||||
lineNo, "${placeholder}", resolvesCommentId);
|
||||
'general', "${placeholder}", resolvesCommentId);
|
||||
|
||||
// set custom success callback on rangeCommit
|
||||
% if is_compare:
|
||||
|
|
@ -311,6 +337,7 @@
|
|||
var text = self.cm.getValue();
|
||||
var status = self.getCommentStatus();
|
||||
var commentType = self.getCommentType();
|
||||
var isDraft = self.getDraftState();
|
||||
|
||||
if (text === "" && !status) {
|
||||
return;
|
||||
|
|
@ -337,6 +364,7 @@
|
|||
'text': text,
|
||||
'changeset_status': status,
|
||||
'comment_type': commentType,
|
||||
'draft': isDraft,
|
||||
'commit_ids': commitIds,
|
||||
'csrf_token': CSRF_TOKEN
|
||||
};
|
||||
|
|
@ -371,7 +399,7 @@
|
|||
|
||||
<div class="comment-area-write" style="display: block;">
|
||||
<div id="edit-container">
|
||||
<div style="padding: 40px 0">
|
||||
<div style="padding: 20px 0px 0px 0;">
|
||||
${_('You need to be logged in to leave comments.')}
|
||||
<a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
|
||||
</div>
|
||||
|
|
@ -430,7 +458,7 @@
|
|||
</div>
|
||||
|
||||
<div class="comment-area-write" style="display: block;">
|
||||
<div id="edit-container_${lineno_id}">
|
||||
<div id="edit-container_${lineno_id}" style="margin-top: -1px">
|
||||
<textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
|
||||
</div>
|
||||
<div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
|
||||
|
|
@ -477,39 +505,50 @@
|
|||
<div class="action-buttons-extra"></div>
|
||||
% endif
|
||||
|
||||
<input class="btn btn-success comment-button-input" id="save_${lineno_id}" name="save" type="submit" value="${_('Comment')}">
|
||||
<input class="btn btn-success comment-button-input submit-comment-action" id="save_${lineno_id}" name="save" type="submit" value="${_('Add comment')}" data-is-draft=false onclick="$(this).addClass('submitter')">
|
||||
|
||||
% if form_type == 'inline':
|
||||
% if c.rhodecode_edition_id == 'EE':
|
||||
## Disable the button for CE, the "real" validation is in the backend code anyway
|
||||
<input class="btn btn-warning comment-button-input submit-draft-action" id="save_draft_${lineno_id}" name="save_draft" type="submit" value="${_('Add draft')}" data-is-draft=true onclick="$(this).addClass('submitter')">
|
||||
% else:
|
||||
<input class="btn btn-warning comment-button-input submit-draft-action disabled" disabled="disabled" type="submit" value="${_('Add draft')}" onclick="return false;" title="Draft comments only available in EE edition of RhodeCode">
|
||||
% endif
|
||||
% endif
|
||||
|
||||
% if review_statuses:
|
||||
<div class="comment-status-box">
|
||||
<select id="change_status_${lineno_id}" name="changeset_status">
|
||||
<option></option> ## Placeholder
|
||||
% for status, lbl in review_statuses:
|
||||
<option value="${status}" data-status="${status}">${lbl}</option>
|
||||
%if is_pull_request and change_status and status in ('approved', 'rejected'):
|
||||
<option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
|
||||
%endif
|
||||
% endfor
|
||||
</select>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
## inline for has a file, and line-number together with cancel hide button.
|
||||
% if form_type == 'inline':
|
||||
<input type="hidden" name="f_path" value="{0}">
|
||||
<input type="hidden" name="line" value="${lineno_id}">
|
||||
<button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
|
||||
${_('Cancel')}
|
||||
<i class="icon-cancel-circled2"></i>
|
||||
</button>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
% if review_statuses:
|
||||
<div class="status_box">
|
||||
<select id="change_status_${lineno_id}" name="changeset_status">
|
||||
<option></option> ## Placeholder
|
||||
% for status, lbl in review_statuses:
|
||||
<option value="${status}" data-status="${status}">${lbl}</option>
|
||||
%if is_pull_request and change_status and status in ('approved', 'rejected'):
|
||||
<option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
|
||||
%endif
|
||||
% endfor
|
||||
</select>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<div class="toolbar-text">
|
||||
<% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
|
||||
${_('Comments parsed using {} syntax.').format(renderer_url)|n} <br/>
|
||||
<span class="tooltip" title="${_('Use @username inside this text to send notification to this RhodeCode user')}">@mention</span>
|
||||
${_('and')}
|
||||
<span class="tooltip" title="${_('Start typing with / for certain actions to be triggered via text box.')}">`/` autocomplete</span>
|
||||
${_('actions supported.')}
|
||||
<span>${_('{} is supported.').format(renderer_url)|n}
|
||||
|
||||
<i class="icon-info-circled tooltip-hovercard"
|
||||
data-hovercard-alt="ALT"
|
||||
data-hovercard-url="javascript:commentHelp('${c.visual.default_renderer.upper()}')"
|
||||
data-comment-json-b64='${h.b64(h.json.dumps({}))}'></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,9 @@
|
|||
%for commit in c.commit_ranges:
|
||||
## commit range header for each individual diff
|
||||
<h3>
|
||||
<a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a>
|
||||
<a class="tooltip-hovercard revision" data-hovercard-alt="Commit: ${commit.short_id}" data-hovercard-url="${h.route_path('hovercard_repo_commit', repo_name=c.repo_name, commit_id=commit.raw_id)}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">
|
||||
${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
${cbdiffs.render_diffset_menu(c.changes[commit.raw_id])}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
<%namespace name="base" file="/base/base.mako"/>
|
||||
<%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
|
||||
|
||||
<%def name="diff_line_anchor(commit, filename, line, type)"><%
|
||||
|
|
@ -74,24 +75,9 @@ return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
|
|||
|
||||
<div class="js-template" id="cb-comment-inline-form-template">
|
||||
<div class="comment-inline-form ac">
|
||||
|
||||
%if c.rhodecode_user.username != h.DEFAULT_USER:
|
||||
%if not c.rhodecode_user.is_default:
|
||||
## render template for inline comments
|
||||
${commentblock.comment_form(form_type='inline')}
|
||||
%else:
|
||||
${h.form('', class_='inline-form comment-form-login', method='get')}
|
||||
<div class="pull-left">
|
||||
<div class="comment-help pull-right">
|
||||
${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-button pull-right">
|
||||
<button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
|
||||
${_('Cancel')}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
${h.end_form()}
|
||||
%endif
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -287,7 +273,7 @@ return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
|
|||
<label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
|
||||
<%
|
||||
file_comments = (get_inline_comments(inline_comments, filediff.patch['filename']) or {}).values()
|
||||
total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not _c.outdated]
|
||||
total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not (_c.outdated or _c.draft)]
|
||||
%>
|
||||
<div class="filediff-collapse-indicator icon-"></div>
|
||||
|
||||
|
|
@ -327,7 +313,7 @@ return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
|
|||
</label>
|
||||
|
||||
${diff_menu(filediff, use_comments=use_comments)}
|
||||
<table data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
|
||||
<table id="file-${h.safeid(h.safe_unicode(filediff.patch['filename']))}" data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
|
||||
|
||||
## new/deleted/empty content case
|
||||
% if not filediff.hunks:
|
||||
|
|
@ -626,8 +612,10 @@ return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
|
|||
|
||||
% if use_comments:
|
||||
|
|
||||
<a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
|
||||
<span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
|
||||
<a href="#" onclick="Rhodecode.comments.toggleDiffComments(this);return toggleElement(this)"
|
||||
data-toggle-on="${_('Hide comments')}"
|
||||
data-toggle-off="${_('Show comments')}">
|
||||
<span class="hide-comment-button">${_('Hide comments')}</span>
|
||||
</a>
|
||||
% endif
|
||||
|
||||
|
|
@ -637,23 +625,37 @@ return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
|
|||
</%def>
|
||||
|
||||
|
||||
<%def name="inline_comments_container(comments, active_pattern_entries=None)">
|
||||
<%def name="inline_comments_container(comments, active_pattern_entries=None, line_no='', f_path='')">
|
||||
|
||||
<div class="inline-comments">
|
||||
%for comment in comments:
|
||||
${commentblock.comment_block(comment, inline=True, active_pattern_entries=active_pattern_entries)}
|
||||
%endfor
|
||||
% if comments and comments[-1].outdated:
|
||||
<span class="btn btn-secondary cb-comment-add-button comment-outdated}" style="display: none;}">
|
||||
${_('Add another comment')}
|
||||
</span>
|
||||
% else:
|
||||
<span onclick="return Rhodecode.comments.createComment(this)" class="btn btn-secondary cb-comment-add-button">
|
||||
${_('Add another comment')}
|
||||
</span>
|
||||
% endif
|
||||
|
||||
<%
|
||||
extra_class = ''
|
||||
extra_style = ''
|
||||
|
||||
if comments and comments[-1].outdated_at_version(c.at_version_num):
|
||||
extra_class = ' comment-outdated'
|
||||
extra_style = 'display: none;'
|
||||
|
||||
%>
|
||||
|
||||
<div class="reply-thread-container-wrapper${extra_class}" style="${extra_style}">
|
||||
<div class="reply-thread-container${extra_class}">
|
||||
<div class="reply-thread-gravatar">
|
||||
${base.gravatar(c.rhodecode_user.email, 20, tooltip=True, user=c.rhodecode_user)}
|
||||
</div>
|
||||
<div class="reply-thread-reply-button">
|
||||
## initial reply button, some JS logic can append here a FORM to leave a first comment.
|
||||
<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">Reply...</button>
|
||||
</div>
|
||||
<div class="reply-thread-last"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</%def>
|
||||
|
||||
<%!
|
||||
|
|
@ -711,16 +713,19 @@ def get_comments_for(diff_type, comments, filename, line_version, line_number):
|
|||
data-line-no="${line.original.lineno}"
|
||||
>
|
||||
|
||||
<% line_old_comments = None %>
|
||||
<% line_old_comments, line_old_comments_no_drafts = None, None %>
|
||||
%if line.original.get_comment_args:
|
||||
<% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
|
||||
<%
|
||||
line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args)
|
||||
line_old_comments_no_drafts = [c for c in line_old_comments if not c.draft] if line_old_comments else []
|
||||
has_outdated = any([x.outdated for x in line_old_comments_no_drafts])
|
||||
%>
|
||||
%endif
|
||||
%if line_old_comments:
|
||||
<% has_outdated = any([x.outdated for x in line_old_comments]) %>
|
||||
%if line_old_comments_no_drafts:
|
||||
% if has_outdated:
|
||||
<i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(line_old_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
|
||||
<i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
|
||||
% else:
|
||||
<i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(line_old_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
|
||||
<i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
|
||||
% endif
|
||||
%endif
|
||||
</td>
|
||||
|
|
@ -734,16 +739,18 @@ def get_comments_for(diff_type, comments, filename, line_version, line_number):
|
|||
<a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
|
||||
%endif
|
||||
</td>
|
||||
|
||||
<% line_no = 'o{}'.format(line.original.lineno) %>
|
||||
<td class="cb-content ${action_class(line.original.action)}"
|
||||
data-line-no="o${line.original.lineno}"
|
||||
data-line-no="${line_no}"
|
||||
>
|
||||
%if use_comments and line.original.lineno:
|
||||
${render_add_comment_button()}
|
||||
${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
|
||||
%endif
|
||||
<span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
|
||||
|
||||
%if use_comments and line.original.lineno and line_old_comments:
|
||||
${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries)}
|
||||
${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
|
||||
%endif
|
||||
|
||||
</td>
|
||||
|
|
@ -752,18 +759,20 @@ def get_comments_for(diff_type, comments, filename, line_version, line_number):
|
|||
>
|
||||
<div>
|
||||
|
||||
<% line_new_comments, line_new_comments_no_drafts = None, None %>
|
||||
%if line.modified.get_comment_args:
|
||||
<% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
|
||||
%else:
|
||||
<% line_new_comments = None%>
|
||||
<%
|
||||
line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args)
|
||||
line_new_comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
|
||||
has_outdated = any([x.outdated for x in line_new_comments_no_drafts])
|
||||
%>
|
||||
%endif
|
||||
%if line_new_comments:
|
||||
|
||||
<% has_outdated = any([x.outdated for x in line_new_comments]) %>
|
||||
%if line_new_comments_no_drafts:
|
||||
% if has_outdated:
|
||||
<i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(line_new_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
|
||||
<i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
|
||||
% else:
|
||||
<i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(line_new_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
|
||||
<i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
|
||||
% endif
|
||||
%endif
|
||||
</div>
|
||||
|
|
@ -778,22 +787,25 @@ def get_comments_for(diff_type, comments, filename, line_version, line_number):
|
|||
<a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
|
||||
%endif
|
||||
</td>
|
||||
|
||||
<% line_no = 'n{}'.format(line.modified.lineno) %>
|
||||
<td class="cb-content ${action_class(line.modified.action)}"
|
||||
data-line-no="n${line.modified.lineno}"
|
||||
data-line-no="${line_no}"
|
||||
>
|
||||
%if use_comments and line.modified.lineno:
|
||||
${render_add_comment_button()}
|
||||
${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
|
||||
%endif
|
||||
<span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
|
||||
%if use_comments and line.modified.lineno and line_new_comments:
|
||||
${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries)}
|
||||
%endif
|
||||
% if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
|
||||
<div class="nav-chunk" style="visibility: hidden">
|
||||
<i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
|
||||
</div>
|
||||
<% chunk_count +=1 %>
|
||||
% endif
|
||||
%if use_comments and line.modified.lineno and line_new_comments:
|
||||
${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
|
||||
%endif
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
%endfor
|
||||
|
|
@ -814,20 +826,22 @@ def get_comments_for(diff_type, comments, filename, line_version, line_number):
|
|||
<td class="cb-data ${action_class(action)}">
|
||||
<div>
|
||||
|
||||
%if comments_args:
|
||||
<% comments = get_comments_for('unified', inline_comments, *comments_args) %>
|
||||
%else:
|
||||
<% comments = None %>
|
||||
%endif
|
||||
<% comments, comments_no_drafts = None, None %>
|
||||
%if comments_args:
|
||||
<%
|
||||
comments = get_comments_for('unified', inline_comments, *comments_args)
|
||||
comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
|
||||
has_outdated = any([x.outdated for x in comments_no_drafts])
|
||||
%>
|
||||
%endif
|
||||
|
||||
% if comments:
|
||||
<% has_outdated = any([x.outdated for x in comments]) %>
|
||||
% if has_outdated:
|
||||
<i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
|
||||
% else:
|
||||
<i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
|
||||
% if comments_no_drafts:
|
||||
% if has_outdated:
|
||||
<i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
|
||||
% else:
|
||||
<i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
|
||||
% endif
|
||||
% endif
|
||||
% endif
|
||||
</div>
|
||||
</td>
|
||||
<td class="cb-lineno ${action_class(action)}"
|
||||
|
|
@ -850,15 +864,16 @@ def get_comments_for(diff_type, comments, filename, line_version, line_number):
|
|||
<a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
|
||||
%endif
|
||||
</td>
|
||||
<% line_no = '{}{}'.format(new_line_no and 'n' or 'o', new_line_no or old_line_no) %>
|
||||
<td class="cb-content ${action_class(action)}"
|
||||
data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
|
||||
data-line-no="${line_no}"
|
||||
>
|
||||
%if use_comments:
|
||||
${render_add_comment_button()}
|
||||
${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
|
||||
%endif
|
||||
<span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
|
||||
%if use_comments and comments:
|
||||
${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
|
||||
${inline_comments_container(comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
|
||||
%endif
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -879,10 +894,12 @@ def get_comments_for(diff_type, comments, filename, line_version, line_number):
|
|||
</%def>file changes
|
||||
|
||||
|
||||
<%def name="render_add_comment_button()">
|
||||
<button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
|
||||
<%def name="render_add_comment_button(line_no='', f_path='')">
|
||||
% if not c.rhodecode_user.is_default:
|
||||
<button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">
|
||||
<span><i class="icon-comment"></i></span>
|
||||
</button>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="render_diffset_menu(diffset, range_diff_on=None, commit=None, pull_request_menu=None)">
|
||||
|
|
|
|||
|
|
@ -108,7 +108,26 @@
|
|||
<i class="icon-cancel-circled2"></i>
|
||||
</div>
|
||||
<div class="btn btn-sm disabled" disabled="disabled" id="rev_range_more" style="display:none;">${_('Select second commit')}</div>
|
||||
<a href="#" class="btn btn-success btn-sm" id="rev_range_container" style="display:none;"></a>
|
||||
|
||||
<div id="rev_range_action" class="btn-group btn-group-actions" style="display:none;">
|
||||
<a href="#" class="btn btn-success btn-sm" id="rev_range_container" style="display:none;"></a>
|
||||
|
||||
<a class="btn btn-success btn-sm btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button">
|
||||
<i class="icon-down"></i>
|
||||
</a>
|
||||
|
||||
<div class="btn-action-switcher-container right-align">
|
||||
<ul class="btn-action-switcher" role="menu" style="min-width: 220px; width: max-content">
|
||||
<li>
|
||||
## JS fills the URL
|
||||
<a id="rev_range_combined_url" class="btn btn-primary btn-sm" href="">
|
||||
${_('Show combined diff')}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</th>
|
||||
|
||||
## commit message expand arrow
|
||||
|
|
@ -147,6 +166,9 @@
|
|||
var $commitRangeMore = $('#rev_range_more');
|
||||
var $commitRangeContainer = $('#rev_range_container');
|
||||
var $commitRangeClear = $('#rev_range_clear');
|
||||
var $commitRangeAction = $('#rev_range_action');
|
||||
var $commitRangeCombinedUrl = $('#rev_range_combined_url');
|
||||
var $compareFork = $('#compare_fork_button');
|
||||
|
||||
var checkboxRangeSelector = function(e){
|
||||
var selectedCheckboxes = [];
|
||||
|
|
@ -169,9 +191,8 @@
|
|||
}
|
||||
|
||||
if (selectedCheckboxes.length > 0) {
|
||||
$('#compare_fork_button').hide();
|
||||
$compareFork.hide();
|
||||
var commitStart = $(selectedCheckboxes[selectedCheckboxes.length-1]).data();
|
||||
|
||||
var revStart = commitStart.commitId;
|
||||
|
||||
var commitEnd = $(selectedCheckboxes[0]).data();
|
||||
|
|
@ -181,7 +202,9 @@
|
|||
var lbl_end = '{0}'.format(commitEnd.commitIdx, commitEnd.shortId);
|
||||
|
||||
var url = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}', 'commit_id': revStart+'...'+revEnd});
|
||||
var link = _gettext('Show commit range {0} ... {1}').format(lbl_start, lbl_end);
|
||||
var urlCombined = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}', 'commit_id': revStart+'...'+revEnd, 'redirect_combined': '1'});
|
||||
|
||||
var link = _gettext('Show commit range {0}<i class="icon-angle-right"></i>{1}').format(lbl_start, lbl_end);
|
||||
|
||||
if (selectedCheckboxes.length > 1) {
|
||||
$commitRangeClear.show();
|
||||
|
|
@ -192,9 +215,12 @@
|
|||
.html(link)
|
||||
.show();
|
||||
|
||||
$commitRangeCombinedUrl.attr('href', urlCombined);
|
||||
$commitRangeAction.show();
|
||||
|
||||
} else {
|
||||
$commitRangeContainer.hide();
|
||||
$commitRangeAction.hide();
|
||||
$commitRangeClear.show();
|
||||
$commitRangeMore.show();
|
||||
}
|
||||
|
|
@ -212,6 +238,7 @@
|
|||
$commitRangeContainer.hide();
|
||||
$commitRangeClear.hide();
|
||||
$commitRangeMore.hide();
|
||||
$commitRangeAction.hide();
|
||||
|
||||
%if c.branch_name:
|
||||
var _url = pyroutes.url('pullrequest_new', {'repo_name': '${c.repo_name}', 'branch':'${c.branch_name}'});
|
||||
|
|
@ -220,7 +247,7 @@
|
|||
var _url = pyroutes.url('pullrequest_new', {'repo_name': '${c.repo_name}'});
|
||||
open_new_pull_request.attr('href', _url);
|
||||
%endif
|
||||
$('#compare_fork_button').show();
|
||||
$compareFork.show();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -397,7 +397,10 @@ ${h.style_metatag(tag_type, tag)|n,trim}
|
|||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="pullrequest_updated_on(updated_on)">
|
||||
<%def name="pullrequest_updated_on(updated_on, pr_version=None)">
|
||||
% if pr_version:
|
||||
<code>v${pr_version}</code>
|
||||
% endif
|
||||
${h.age_component(h.time_to_utcdatetime(updated_on))}
|
||||
</%def>
|
||||
|
||||
|
|
@ -456,7 +459,7 @@ ${h.style_metatag(tag_type, tag)|n,trim}
|
|||
</div>
|
||||
|
||||
<div class="markup-form-area-write" style="display: block;">
|
||||
<div id="edit-container_${form_id}">
|
||||
<div id="edit-container_${form_id}" style="margin-top: -1px">
|
||||
<textarea id="${form_id}" name="${form_id}" class="comment-block-ta ac-input">${form_text if form_text else ''}</textarea>
|
||||
</div>
|
||||
<div id="preview-container_${form_id}" class="clearfix" style="display: none;">
|
||||
|
|
|
|||
|
|
@ -237,6 +237,24 @@ if (show_disabled) {
|
|||
|
||||
</script>
|
||||
|
||||
<script id="ejs_commentHelpHovercard" type="text/template" class="ejsTemplate">
|
||||
|
||||
<div>
|
||||
Use <strong>@username</strong> mention syntax to send direct notification to this RhodeCode user.<br/>
|
||||
Typing / starts autocomplete for certain action, e.g set review status, or comment type. <br/>
|
||||
<br/>
|
||||
Use <strong>Cmd/ctrl+enter</strong> to submit comment, or <strong>Shift+Cmd/ctrl+enter</strong> to submit a draft.<br/>
|
||||
<br/>
|
||||
<strong>Draft comments</strong> are private to the author, and trigger no notification to others.<br/>
|
||||
They are permanent until deleted, or converted to regular comments.<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
##// END OF EJS Templates
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -337,7 +337,6 @@ ${self.plaintext_footer()}
|
|||
div.markdown-block img {
|
||||
border-style: none;
|
||||
background-color: #fff;
|
||||
padding-right: 20px;
|
||||
max-width: 100%
|
||||
}
|
||||
|
||||
|
|
@ -395,6 +394,13 @@ ${self.plaintext_footer()}
|
|||
background-color: #eeeeee
|
||||
}
|
||||
|
||||
div.markdown-block p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
padding: 0;
|
||||
line-height: unset;
|
||||
}
|
||||
|
||||
div.markdown-block code,
|
||||
div.markdown-block pre,
|
||||
div.markdown-block #ws,
|
||||
|
|
|
|||
|
|
@ -82,8 +82,8 @@
|
|||
<p>
|
||||
<strong>Exception ID: <code><a href="${c.exception_id_url}">${c.exception_id}</a></code> </strong> <br/>
|
||||
|
||||
Super-admins can see detailed traceback information from this exception by checking the below Exception ID.<br/>
|
||||
Please include the above link for further details of this exception.
|
||||
Super-admins can see details of the above error in the exception tracker found under
|
||||
<a href="${h.route_url('admin_settings_exception_tracker')}">admin > settings > exception tracker</a>.
|
||||
</p>
|
||||
</div>
|
||||
% endif
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
</a>
|
||||
|
||||
<div class="btn-action-switcher-container right-align">
|
||||
<ul class="btn-action-switcher" role="menu" style="min-width: 200px">
|
||||
<ul class="btn-action-switcher" role="menu" style="min-width: 200px; width: max-content">
|
||||
<li>
|
||||
<a class="action_button" href="${h.route_path('repo_files_upload_file',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
|
||||
<i class="icon-upload"></i>
|
||||
|
|
@ -44,18 +44,41 @@
|
|||
% endif
|
||||
|
||||
% if c.enable_downloads:
|
||||
<% at_path = '{}'.format(request.GET.get('at') or c.commit.raw_id[:6]) %>
|
||||
<div class="btn btn-default new-file">
|
||||
% if c.f_path == '/':
|
||||
<a href="${h.route_path('repo_archivefile',repo_name=c.repo_name, fname='{}.zip'.format(c.commit.raw_id))}">
|
||||
${_('Download full tree ZIP')}
|
||||
<%
|
||||
at_path = '{}'.format(request.GET.get('at') or c.commit.raw_id[:6])
|
||||
if c.f_path == '/':
|
||||
label = _('Full tree as {}')
|
||||
_query = {'with_hash': '1'}
|
||||
else:
|
||||
label = _('This tree as {}')
|
||||
_query = {'at_path':c.f_path, 'with_hash': '1'}
|
||||
%>
|
||||
|
||||
<div class="btn-group btn-group-actions new-file">
|
||||
<a class="archive_link btn btn-default" data-ext=".zip" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname='{}{}'.format(c.commit.raw_id, '.zip'), _query=_query)}">
|
||||
<i class="icon-download"></i>
|
||||
${label.format('.zip')}
|
||||
</a>
|
||||
% else:
|
||||
<a href="${h.route_path('repo_archivefile',repo_name=c.repo_name, fname='{}.zip'.format(c.commit.raw_id), _query={'at_path':c.f_path})}">
|
||||
${_('Download this tree ZIP')}
|
||||
|
||||
<a class="tooltip btn btn-default btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more download options')}">
|
||||
<i class="icon-down"></i>
|
||||
</a>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
<div class="btn-action-switcher-container left-align">
|
||||
<ul class="btn-action-switcher" role="menu" style="min-width: 200px; width: max-content">
|
||||
% for a_type, content_type, extension in h.ARCHIVE_SPECS:
|
||||
% if extension not in ['.zip']:
|
||||
<li>
|
||||
<a class="archive_link" data-ext="${extension}" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname='{}{}'.format(c.commit.raw_id, extension), _query=_query)}">
|
||||
<i class="icon-download"></i>
|
||||
${label.format(extension)}
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
% endfor
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<div class="files-quick-filter">
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@
|
|||
· ${h.branding(c.rhodecode_name)}
|
||||
%endif
|
||||
</%def>
|
||||
|
||||
<style>body{background-color:#eeeeee;}</style>
|
||||
|
||||
<div class="loginbox">
|
||||
<div class="header">
|
||||
<div class="header-account">
|
||||
<div id="header-inner" class="title">
|
||||
<div id="logo">
|
||||
<div class="logo-wrapper">
|
||||
|
|
@ -28,12 +28,12 @@
|
|||
<div class="loginwrapper">
|
||||
<rhodecode-toast id="notifications"></rhodecode-toast>
|
||||
|
||||
<div class="left-column">
|
||||
<div class="auth-image-wrapper">
|
||||
<img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
|
||||
</div>
|
||||
|
||||
<%block name="above_login_button" />
|
||||
<div id="login" class="right-column">
|
||||
<div id="login">
|
||||
<%block name="above_login_button" />
|
||||
<!-- login -->
|
||||
<div class="sign-in-title">
|
||||
<h1>${_('Sign In using username/password')}</h1>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<style>body{background-color:#eeeeee;}</style>
|
||||
|
||||
<div class="loginbox">
|
||||
<div class="header">
|
||||
<div class="header-account">
|
||||
<div id="header-inner" class="title">
|
||||
<div id="logo">
|
||||
<div class="logo-wrapper">
|
||||
|
|
@ -27,7 +27,8 @@
|
|||
|
||||
<div class="loginwrapper">
|
||||
<rhodecode-toast id="notifications"></rhodecode-toast>
|
||||
<div class="left-column">
|
||||
|
||||
<div class="auth-image-wrapper">
|
||||
<img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
|
||||
</div>
|
||||
|
||||
|
|
@ -43,7 +44,7 @@
|
|||
</p>
|
||||
</div>
|
||||
%else:
|
||||
<div id="register" class="right-column">
|
||||
<div id="register">
|
||||
<!-- login -->
|
||||
<div class="sign-in-title">
|
||||
<h1>${_('Reset your Password')}</h1>
|
||||
|
|
|
|||
|
|
@ -32,8 +32,6 @@
|
|||
%>
|
||||
|
||||
<script type="text/javascript">
|
||||
// TODO: marcink switch this to pyroutes
|
||||
AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
|
||||
templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
|
||||
templateContext.pull_request_data.pull_request_version = '${request.GET.get('version', '')}';
|
||||
</script>
|
||||
|
|
@ -552,6 +550,42 @@
|
|||
## CONTENT
|
||||
<div class="sidebar-content">
|
||||
|
||||
## Drafts
|
||||
% if c.rhodecode_edition_id == 'EE':
|
||||
<div id="draftsTable" class="sidebar-element clear-both" style="display: ${'block' if c.draft_comments else 'none'}">
|
||||
<div class="tooltip right-sidebar-collapsed-state" style="display: none;" onclick="toggleSidebar(); return false" title="${_('Drafts')}">
|
||||
<i class="icon-comment icon-draft"></i>
|
||||
<span id="drafts-count">${len(c.draft_comments)}</span>
|
||||
</div>
|
||||
|
||||
<div class="right-sidebar-expanded-state pr-details-title">
|
||||
<span style="padding-left: 2px">
|
||||
<input name="select_all_drafts" type="checkbox" onclick="$('[name=submit_draft]').prop('checked', !$('[name=submit_draft]').prop('checked'))">
|
||||
</span>
|
||||
<span class="sidebar-heading noselect" onclick="refreshDraftComments(); return false">
|
||||
<i class="icon-comment icon-draft"></i>
|
||||
${_('Drafts')}
|
||||
</span>
|
||||
<span class="block-right action_button last-item" onclick="submitDrafts(event)">${_('Submit')}</span>
|
||||
</div>
|
||||
|
||||
<div id="drafts" class="right-sidebar-expanded-state pr-details-content reviewers">
|
||||
% if c.draft_comments:
|
||||
${sidebar.comments_table(c.draft_comments, len(c.draft_comments), draft_comments=True)}
|
||||
% else:
|
||||
<table class="drafts-content-table">
|
||||
<tr>
|
||||
<td>
|
||||
${_('No TODOs yet')}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
% endif
|
||||
|
||||
## RULES SUMMARY/RULES
|
||||
<div class="sidebar-element clear-both">
|
||||
<% vote_title = _ungettext(
|
||||
|
|
@ -678,7 +712,7 @@
|
|||
% endif
|
||||
|
||||
## TODOs
|
||||
<div class="sidebar-element clear-both">
|
||||
<div id="todosTable" class="sidebar-element clear-both">
|
||||
<div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
|
||||
<i class="icon-flag-filled"></i>
|
||||
<span id="todos-count">${len(c.unresolved_comments)}</span>
|
||||
|
|
@ -712,7 +746,7 @@
|
|||
% if c.unresolved_comments + c.resolved_comments:
|
||||
${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True)}
|
||||
% else:
|
||||
<table>
|
||||
<table class="todos-content-table">
|
||||
<tr>
|
||||
<td>
|
||||
${_('No TODOs yet')}
|
||||
|
|
@ -725,7 +759,7 @@
|
|||
</div>
|
||||
|
||||
## COMMENTS
|
||||
<div class="sidebar-element clear-both">
|
||||
<div id="commentsTable" class="sidebar-element clear-both">
|
||||
<div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
|
||||
<i class="icon-comment" style="color: #949494"></i>
|
||||
<span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
|
||||
|
|
@ -763,7 +797,7 @@
|
|||
% if c.inline_comments_flat + c.comments:
|
||||
${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
|
||||
% else:
|
||||
<table>
|
||||
<table class="comments-content-table">
|
||||
<tr>
|
||||
<td>
|
||||
${_('No Comments yet')}
|
||||
|
|
@ -846,6 +880,7 @@ versionController.init();
|
|||
|
||||
reviewersController = new ReviewersController();
|
||||
commitsController = new CommitsController();
|
||||
commentsController = new CommentsController();
|
||||
|
||||
updateController = new UpdatePrController();
|
||||
|
||||
|
|
@ -891,6 +926,23 @@ window.setObserversData = ${c.pull_request_set_observers_data_json | n};
|
|||
);
|
||||
};
|
||||
|
||||
window.submitDrafts = function (event) {
|
||||
var target = $(event.currentTarget);
|
||||
var callback = function (result) {
|
||||
target.removeAttr('onclick').html('saving...');
|
||||
}
|
||||
var draftIds = [];
|
||||
$.each($('[name=submit_draft]:checked'), function (idx, val) {
|
||||
draftIds.push(parseInt($(val).val()));
|
||||
})
|
||||
if (draftIds.length > 0) {
|
||||
Rhodecode.comments.finalizeDrafts(draftIds, callback);
|
||||
}
|
||||
else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
window.closePullRequest = function (status) {
|
||||
if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
|
||||
return false;
|
||||
|
|
@ -980,9 +1032,11 @@ window.setObserversData = ${c.pull_request_set_observers_data_json | n};
|
|||
$(btns).each(fn_display);
|
||||
});
|
||||
|
||||
// register submit callback on commentForm form to track TODOs
|
||||
window.commentFormGlobalSubmitSuccessCallback = function () {
|
||||
refreshMergeChecks();
|
||||
// register submit callback on commentForm form to track TODOs, and refresh mergeChecks conditions
|
||||
window.commentFormGlobalSubmitSuccessCallback = function (comment) {
|
||||
if (!comment.draft) {
|
||||
refreshMergeChecks();
|
||||
}
|
||||
};
|
||||
|
||||
ReviewerAutoComplete('#user', reviewersController);
|
||||
|
|
@ -994,7 +1048,8 @@ $(document).ready(function () {
|
|||
|
||||
var channel = '${c.pr_broadcast_channel}';
|
||||
new ReviewerPresenceController(channel)
|
||||
|
||||
// register globally so inject comment logic can re-use it.
|
||||
window.commentsController = commentsController;
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@ $(document).ready(function() {
|
|||
$pullRequestListTable.DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
stateSave: true,
|
||||
stateDuration: -1,
|
||||
ajax: {
|
||||
"url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
|
||||
"data": function (d) {
|
||||
|
|
@ -114,6 +116,10 @@ $(document).ready(function() {
|
|||
if (data['closed']) {
|
||||
$(row).addClass('closed');
|
||||
}
|
||||
},
|
||||
"stateSaveParams": function (settings, data) {
|
||||
data.search.search = ""; // Don't save search
|
||||
data.start = 0; // don't save pagination
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<style>body{background-color:#eeeeee;}</style>
|
||||
|
||||
<div class="loginbox">
|
||||
<div class="header">
|
||||
<div class="header-account">
|
||||
<div id="header-inner" class="title">
|
||||
<div id="logo">
|
||||
<div class="logo-wrapper">
|
||||
|
|
@ -27,11 +27,13 @@
|
|||
|
||||
<div class="loginwrapper">
|
||||
<rhodecode-toast id="notifications"></rhodecode-toast>
|
||||
<div class="left-column">
|
||||
|
||||
<div class="auth-image-wrapper">
|
||||
<img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
|
||||
</div>
|
||||
<%block name="above_register_button" />
|
||||
<div id="register" class="right-column">
|
||||
|
||||
<div id="register">
|
||||
<%block name="above_register_button" />
|
||||
<!-- login -->
|
||||
<div class="sign-in-title">
|
||||
% if external_auth_provider:
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@
|
|||
<div class="enabled pull-left" style="margin-right: 10px">
|
||||
|
||||
<div class="btn-group btn-group-actions">
|
||||
<a class="archive_link btn btn-small" data-ext=".zip" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname=c.rhodecode_db_repo.landing_ref_name+'.zip')}">
|
||||
<a class="archive_link btn btn-small" data-ext=".zip" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname=c.rhodecode_db_repo.landing_ref_name+'.zip', _query={'with_hash': '1'})}">
|
||||
<i class="icon-download"></i>
|
||||
${c.rhodecode_db_repo.landing_ref_name}.zip
|
||||
## replaced by some JS on select
|
||||
|
|
@ -198,12 +198,11 @@
|
|||
</a>
|
||||
|
||||
<div class="btn-action-switcher-container left-align">
|
||||
<ul class="btn-action-switcher" role="menu" style="min-width: 200px">
|
||||
<ul class="btn-action-switcher" role="menu" style="min-width: 200px; width: max-content">
|
||||
% for a_type, content_type, extension in h.ARCHIVE_SPECS:
|
||||
% if extension not in ['.zip']:
|
||||
<li>
|
||||
|
||||
<a class="archive_link" data-ext="${extension}" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname=c.rhodecode_db_repo.landing_ref_name+extension)}">
|
||||
<a class="archive_link" data-ext="${extension}" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname=c.rhodecode_db_repo.landing_ref_name+extension, _query={'with_hash': '1'})}">
|
||||
<i class="icon-download"></i>
|
||||
${c.rhodecode_db_repo.landing_ref_name+extension}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -49,23 +49,32 @@ class TestArchives(BackendTestMixin):
|
|||
@classmethod
|
||||
def _get_commits(cls):
|
||||
start_date = datetime.datetime(2010, 1, 1, 20)
|
||||
yield {
|
||||
'message': 'Initial Commit',
|
||||
'author': 'Joe Doe <joe.doe@example.com>',
|
||||
'date': start_date + datetime.timedelta(hours=12),
|
||||
'added': [
|
||||
FileNode('executable_0o100755', '...', mode=0o100755),
|
||||
FileNode('executable_0o100500', '...', mode=0o100500),
|
||||
FileNode('not_executable', '...', mode=0o100644),
|
||||
],
|
||||
}
|
||||
for x in range(5):
|
||||
yield {
|
||||
'message': 'Commit %d' % x,
|
||||
'author': 'Joe Doe <joe.doe@example.com>',
|
||||
'date': start_date + datetime.timedelta(hours=12 * x),
|
||||
'added': [
|
||||
FileNode(
|
||||
'%d/file_%d.txt' % (x, x), content='Foobar %d' % x),
|
||||
FileNode('%d/file_%d.txt' % (x, x), content='Foobar %d' % x),
|
||||
],
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize('compressor', ['gz', 'bz2'])
|
||||
def test_archive_tar(self, compressor):
|
||||
self.tip.archive_repo(
|
||||
self.temp_file, kind='t' + compressor, prefix='repo')
|
||||
self.temp_file, kind='t{}'.format(compressor), archive_dir_name='repo')
|
||||
out_dir = tempfile.mkdtemp()
|
||||
out_file = tarfile.open(self.temp_file, 'r|' + compressor)
|
||||
out_file = tarfile.open(self.temp_file, 'r|{}'.format(compressor))
|
||||
out_file.extractall(out_dir)
|
||||
out_file.close()
|
||||
|
||||
|
|
@ -77,8 +86,24 @@ class TestArchives(BackendTestMixin):
|
|||
|
||||
shutil.rmtree(out_dir)
|
||||
|
||||
@pytest.mark.parametrize('compressor', ['gz', 'bz2'])
|
||||
def test_archive_tar_symlink(self, compressor):
|
||||
return False
|
||||
|
||||
@pytest.mark.parametrize('compressor', ['gz', 'bz2'])
|
||||
def test_archive_tar_file_modes(self, compressor):
|
||||
self.tip.archive_repo(
|
||||
self.temp_file, kind='t{}'.format(compressor), archive_dir_name='repo')
|
||||
out_dir = tempfile.mkdtemp()
|
||||
out_file = tarfile.open(self.temp_file, 'r|{}'.format(compressor))
|
||||
out_file.extractall(out_dir)
|
||||
out_file.close()
|
||||
dest = lambda inp: os.path.join(out_dir, 'repo/' + inp)
|
||||
|
||||
assert oct(os.stat(dest('not_executable')).st_mode) == '0100644'
|
||||
|
||||
def test_archive_zip(self):
|
||||
self.tip.archive_repo(self.temp_file, kind='zip', prefix='repo')
|
||||
self.tip.archive_repo(self.temp_file, kind='zip', archive_dir_name='repo')
|
||||
out = zipfile.ZipFile(self.temp_file)
|
||||
|
||||
for x in range(5):
|
||||
|
|
@ -91,10 +116,10 @@ class TestArchives(BackendTestMixin):
|
|||
|
||||
def test_archive_zip_with_metadata(self):
|
||||
self.tip.archive_repo(self.temp_file, kind='zip',
|
||||
prefix='repo', write_metadata=True)
|
||||
archive_dir_name='repo', write_metadata=True)
|
||||
|
||||
out = zipfile.ZipFile(self.temp_file)
|
||||
metafile = out.read('.archival.txt')
|
||||
metafile = out.read('repo/.archival.txt')
|
||||
|
||||
raw_id = self.tip.raw_id
|
||||
assert 'commit_id:%s' % raw_id in metafile
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue