release: Merge default into stable for release preparation

This commit is contained in:
Milka Kuzminski 2020-11-23 08:59:11 +00:00
commit b7f2caca4c
83 changed files with 2144 additions and 970 deletions

View file

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

View file

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

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

View file

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

View file

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

View file

@ -1 +1 @@
4.22.0
4.23.0

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"],

View file

@ -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 = {

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

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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):
"""

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1002,7 +1002,7 @@ input.filediff-collapse-state {
.nav-chunk {
position: absolute;
right: 20px;
margin-top: -17px;
margin-top: -15px;
}
.nav-chunk.selected {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 () {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)">

View file

@ -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();
}
};

View file

@ -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;">

View file

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

View file

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

View file

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

View file

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

View file

@ -7,10 +7,10 @@
&middot; ${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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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