##// END OF EJS Templates
release: Merge default into stable for release preparation
milka -
r4566:091769de merge stable
parent child Browse files
Show More
@@ -0,0 +1,89 b''
1 |RCE| 4.23.0 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2020-11-20
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13 - Comments: introduced new draft comments.
14
15 * drafts are private to author
16 * not triggering any notifications
17 * sidebar doesn't display draft comments
18 * They are just placeholders for longer review.
19
20 - Comments: when channelstream is enabled, comments are pushed live, so there's no
21 need to refresh page to see other participant comments.
22 New comments are marker in the sidebar.
23
24 - Comments: multiple changes on comments navigation/display logic.
25
26 * toggle icon is smarter, open/hide windows according to actions. E.g commenting opens threads
27 * toggle are mor explicit
28 * possible to hide/show only single threads using the toggle icon.
29 * new UI for showing thread comments
30
31 - Reviewers: new logic for author/commit-author rules.
32 It's not possible to define if author or commit author should be excluded, or always included in a review.
33 - Reviewers: no reviewers would now allow a PR to be merged, unless review rules require some.
34 Use case is that pr can be created without review needed, maybe just for sharing, or CI checks
35 - Pull requests: save permanently the state if sorting columns for pull-request grids.
36 - Commit ranges: enable combined diff compare directly from range selector.
37
38
39 General
40 ^^^^^^^
41
42 - Authentication: enable custom names for auth plugins. It's possible to name the authentication
43 buttons now for SAML plugins.
44 - Login: optimized UI for login/register/password reset windows.
45 - Repo mapper: make it more resilient to errors, it's better it executes and skip certain
46 repositories, rather then crash whole mapper.
47 - Markdown: improved styling, and fixed nl2br extensions to only do br on new elements not inline.
48 - Pull requests: show pr version in the my-account and repo pr listing grids.
49 - Archives: allowing to obtain archives without the commit short id in the name for
50 better automation of obtained artifacts.
51 New url flag called `?=with_hash=1` controls this
52 - Error document: update info about stored exception retrieval.
53 - Range diff: enable hovercards for commits in range-diff.
54
55
56 Security
57 ^^^^^^^^
58
59
60
61 Performance
62 ^^^^^^^^^^^
63
64 - Improved logic of repo archive, now it's much faster to run archiver as VCSServer
65 communication was removed, and job is delegated to VCSServer itself.
66 - Improved VCSServer startup times.
67 - Notifications: skip double rendering just to generate email title/desc.
68 We'll re-use those now for better performance of creating notifications.
69 - App: improve logging, and remove DB calls on app startup.
70
71
72 Fixes
73 ^^^^^
74
75 - Login/register: fixed header width problem on mobile devices
76 - Exception tracker: don't fail on empty request in context of celery app for example.
77 - Exceptions: improved reporting of unhandled vcsserver exceptions.
78 - Sidebar: fixed refresh of TODOs url.
79 - Remap-rescan: fixes #5636 initial rescan problem.
80 - API: fixed SVN raw diff export. The API method was inconsistent, and used different logic.
81 Now it shares the same code as raw-diff from web-ui.
82
83
84 Upgrade notes
85 ^^^^^^^^^^^^^
86
87 - Scheduled feature release.
88 Please note that now the reviewers logic changed a bit, it's possible to create a pull request
89 Without any reviewers initially, and such pull request doesn't need to have an approval for merging.
@@ -0,0 +1,53 b''
1 # -*- coding: utf-8 -*-
2
3 import logging
4 from sqlalchemy import *
5
6 from alembic.migration import MigrationContext
7 from alembic.operations import Operations
8
9 from rhodecode.lib.dbmigrate.versions import _reset_base
10 from rhodecode.model import meta, init_model_encryption
11
12
13 log = logging.getLogger(__name__)
14
15
16 def upgrade(migrate_engine):
17 """
18 Upgrade operations go here.
19 Don't create your own engine; bind migrate_engine to your metadata
20 """
21 _reset_base(migrate_engine)
22 from rhodecode.lib.dbmigrate.schema import db_4_20_0_0 as db
23
24 init_model_encryption(db)
25
26 context = MigrationContext.configure(migrate_engine.connect())
27 op = Operations(context)
28
29 table = db.ChangesetComment.__table__
30 with op.batch_alter_table(table.name) as batch_op:
31 new_column = Column('draft', Boolean(), nullable=True)
32 batch_op.add_column(new_column)
33
34 _set_default_as_non_draft(op, meta.Session)
35
36
37 def downgrade(migrate_engine):
38 meta = MetaData()
39 meta.bind = migrate_engine
40
41
42 def fixups(models, _SESSION):
43 pass
44
45
46 def _set_default_as_non_draft(op, session):
47 params = {'draft': False}
48 query = text(
49 'UPDATE changeset_comments SET draft = :draft'
50 ).bindparams(**params)
51 op.execute(query)
52 session().commit()
53
@@ -0,0 +1,78 b''
1 # -*- coding: utf-8 -*-
2
3 import logging
4 from sqlalchemy import *
5
6 from alembic.migration import MigrationContext
7 from alembic.operations import Operations
8
9 from rhodecode.lib.dbmigrate.versions import _reset_base
10 from rhodecode.model import meta, init_model_encryption
11
12
13 log = logging.getLogger(__name__)
14
15
16 def upgrade(migrate_engine):
17 """
18 Upgrade operations go here.
19 Don't create your own engine; bind migrate_engine to your metadata
20 """
21 _reset_base(migrate_engine)
22 from rhodecode.lib.dbmigrate.schema import db_4_20_0_0 as db
23
24 init_model_encryption(db)
25
26 context = MigrationContext.configure(migrate_engine.connect())
27 op = Operations(context)
28
29 table = db.RepoReviewRule.__table__
30 with op.batch_alter_table(table.name) as batch_op:
31
32 new_column = Column('pr_author', UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
33 batch_op.add_column(new_column)
34
35 new_column = Column('commit_author', UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
36 batch_op.add_column(new_column)
37
38 _migrate_review_flags_to_new_cols(op, meta.Session)
39
40
41 def downgrade(migrate_engine):
42 meta = MetaData()
43 meta.bind = migrate_engine
44
45
46 def fixups(models, _SESSION):
47 pass
48
49
50 def _migrate_review_flags_to_new_cols(op, session):
51
52 # set defaults for pr_author
53 query = text(
54 'UPDATE repo_review_rules SET pr_author = :val'
55 ).bindparams(val='no_rule')
56 op.execute(query)
57
58 # set defaults for commit_author
59 query = text(
60 'UPDATE repo_review_rules SET commit_author = :val'
61 ).bindparams(val='no_rule')
62 op.execute(query)
63
64 session().commit()
65
66 # now change the flags to forbid based on
67 # forbid_author_to_review, forbid_commit_author_to_review
68 query = text(
69 'UPDATE repo_review_rules SET pr_author = :val WHERE forbid_author_to_review = TRUE'
70 ).bindparams(val='forbid_pr_author')
71 op.execute(query)
72
73 query = text(
74 'UPDATE repo_review_rules SET commit_author = :val WHERE forbid_commit_author_to_review = TRUE'
75 ).bindparams(val='forbid_commit_author')
76 op.execute(query)
77
78 session().commit()
@@ -1,5 +1,5 b''
1 1 [bumpversion]
2 current_version = 4.22.0
2 current_version = 4.23.0
3 3 message = release: Bump version {current_version} to {new_version}
4 4
5 5 [bumpversion:file:rhodecode/VERSION]
@@ -5,25 +5,20 b' done = false'
5 5 done = true
6 6
7 7 [task:rc_tools_pinned]
8 done = true
9 8
10 9 [task:fixes_on_stable]
11 done = true
12 10
13 11 [task:pip2nix_generated]
14 done = true
15 12
16 13 [task:changelog_updated]
17 done = true
18 14
19 15 [task:generate_api_docs]
20 done = true
16
17 [task:updated_translation]
21 18
22 19 [release]
23 state = prepared
24 version = 4.22.0
25
26 [task:updated_translation]
20 state = in_progress
21 version = 4.23.0
27 22
28 23 [task:generate_js_routes]
29 24
@@ -9,6 +9,7 b' Release Notes'
9 9 .. toctree::
10 10 :maxdepth: 1
11 11
12 release-notes-4.23.0.rst
12 13 release-notes-4.22.0.rst
13 14 release-notes-4.21.0.rst
14 15 release-notes-4.20.1.rst
@@ -1883,7 +1883,7 b' self: super: {'
1883 1883 };
1884 1884 };
1885 1885 "rhodecode-enterprise-ce" = super.buildPythonPackage {
1886 name = "rhodecode-enterprise-ce-4.22.0";
1886 name = "rhodecode-enterprise-ce-4.23.0";
1887 1887 buildInputs = [
1888 1888 self."pytest"
1889 1889 self."py"
@@ -1,1 +1,1 b''
1 4.22.0 No newline at end of file
1 4.23.0 No newline at end of file
@@ -48,7 +48,7 b' PYRAMID_SETTINGS = {}'
48 48 EXTENSIONS = {}
49 49
50 50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
51 __dbversion__ = 110 # defines current db version for migrations
51 __dbversion__ = 112 # defines current db version for migrations
52 52 __platform__ = platform.system()
53 53 __license__ = 'AGPLv3, and Commercial License'
54 54 __author__ = 'RhodeCode GmbH'
@@ -351,7 +351,10 b' def get_pull_request_or_error(pullreques'
351 351 return pull_request
352 352
353 353
354 def build_commit_data(commit, detail_level):
354 def build_commit_data(rhodecode_vcs_repo, commit, detail_level):
355 commit2 = commit
356 commit1 = commit.first_parent
357
355 358 parsed_diff = []
356 359 if detail_level == 'extended':
357 360 for f_path in commit.added_paths:
@@ -362,8 +365,11 b' def build_commit_data(commit, detail_lev'
362 365 parsed_diff.append(_get_commit_dict(filename=f_path, op='D'))
363 366
364 367 elif detail_level == 'full':
365 from rhodecode.lib.diffs import DiffProcessor
366 diff_processor = DiffProcessor(commit.diff())
368 from rhodecode.lib import diffs
369
370 _diff = rhodecode_vcs_repo.get_diff(commit1, commit2,)
371 diff_processor = diffs.DiffProcessor(_diff, format='newdiff', show_full_diff=True)
372
367 373 for dp in diff_processor.prepare():
368 374 del dp['stats']['ops']
369 375 _stats = dp['stats']
@@ -317,17 +317,18 b' def get_repo_changeset(request, apiuser,'
317 317 'ret_type must be one of %s' % (
318 318 ','.join(_changes_details_types)))
319 319
320 vcs_repo = repo.scm_instance()
320 321 pre_load = ['author', 'branch', 'date', 'message', 'parents',
321 322 'status', '_commit', '_file_paths']
322 323
323 324 try:
324 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
325 commit = repo.get_commit(commit_id=revision, pre_load=pre_load)
325 326 except TypeError as e:
326 327 raise JSONRPCError(safe_str(e))
327 _cs_json = cs.__json__()
328 _cs_json['diff'] = build_commit_data(cs, changes_details)
328 _cs_json = commit.__json__()
329 _cs_json['diff'] = build_commit_data(vcs_repo, commit, changes_details)
329 330 if changes_details == 'full':
330 _cs_json['refs'] = cs._get_refs()
331 _cs_json['refs'] = commit._get_refs()
331 332 return _cs_json
332 333
333 334
@@ -398,7 +399,7 b' def get_repo_changesets(request, apiuser'
398 399 if cnt >= limit != -1:
399 400 break
400 401 _cs_json = commit.__json__()
401 _cs_json['diff'] = build_commit_data(commit, changes_details)
402 _cs_json['diff'] = build_commit_data(vcs_repo, commit, changes_details)
402 403 if changes_details == 'full':
403 404 _cs_json['refs'] = {
404 405 'branches': [commit.branch],
@@ -36,7 +36,7 b' from rhodecode.authentication.plugins im'
36 36 from rhodecode.events import trigger
37 37 from rhodecode.model.db import true, UserNotice
38 38
39 from rhodecode.lib import audit_logger, rc_cache
39 from rhodecode.lib import audit_logger, rc_cache, auth
40 40 from rhodecode.lib.exceptions import (
41 41 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
42 42 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
@@ -295,6 +295,10 b' class UsersView(UserAppView):'
295 295 c.allowed_extern_types = [
296 296 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
297 297 ]
298 perms = req.registry.settings.get('available_permissions')
299 if not perms:
300 # inject info about available permissions
301 auth.set_available_permissions(req.registry.settings)
298 302
299 303 c.available_permissions = req.registry.settings['available_permissions']
300 304 PermissionModel().set_global_permission_choices(
@@ -252,7 +252,7 b' But please check this code'
252 252 var comment = $('#comment-'+commentId);
253 253 var commentData = comment.data();
254 254 if (commentData.commentInline) {
255 this.createComment(comment, commentId)
255 this.createComment(comment, f_path, line_no, commentId)
256 256 } else {
257 257 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
258 258 }
@@ -702,7 +702,9 b' class MyAccountView(BaseAppView, DataGri'
702 702 **valid_data)
703 703 if old_email != valid_data['email']:
704 704 old = UserEmailMap.query() \
705 .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first()
705 .filter(UserEmailMap.user == c.user)\
706 .filter(UserEmailMap.email == valid_data['email'])\
707 .first()
706 708 old.email = old_email
707 709 h.flash(_('Your account was updated successfully'), category='success')
708 710 Session().commit()
@@ -718,6 +720,7 b' class MyAccountView(BaseAppView, DataGri'
718 720 def _get_pull_requests_list(self, statuses):
719 721 draw, start, limit = self._extract_chunk(self.request)
720 722 search_q, order_by, order_dir = self._extract_ordering(self.request)
723
721 724 _render = self.request.get_partial_renderer(
722 725 'rhodecode:templates/data_table/_dt_elements.mako')
723 726
@@ -735,7 +738,7 b' class MyAccountView(BaseAppView, DataGri'
735 738 for pr in pull_requests:
736 739 repo_id = pr.target_repo_id
737 740 comments_count = comments_model.get_all_comments(
738 repo_id, pull_request=pr, count_only=True)
741 repo_id, pull_request=pr, include_drafts=False, count_only=True)
739 742 owned = pr.user_id == self._rhodecode_user.user_id
740 743
741 744 data.append({
@@ -751,7 +754,8 b' class MyAccountView(BaseAppView, DataGri'
751 754 'title': _render('pullrequest_title', pr.title, pr.description),
752 755 'description': h.escape(pr.description),
753 756 'updated_on': _render('pullrequest_updated_on',
754 h.datetime_to_time(pr.updated_on)),
757 h.datetime_to_time(pr.updated_on),
758 pr.versions_count),
755 759 'updated_on_raw': h.datetime_to_time(pr.updated_on),
756 760 'created_on': _render('pullrequest_updated_on',
757 761 h.datetime_to_time(pr.created_on)),
@@ -355,6 +355,11 b' def includeme(config):'
355 355 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/todos',
356 356 repo_route=True)
357 357
358 config.add_route(
359 name='pullrequest_drafts',
360 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/drafts',
361 repo_route=True)
362
358 363 # Artifacts, (EE feature)
359 364 config.add_route(
360 365 name='repo_artifacts_list',
@@ -608,23 +608,23 b' class TestPullrequestsView(object):'
608 608 pull_request.source_repo, pull_request=pull_request)
609 609 assert status == ChangesetStatus.STATUS_REJECTED
610 610
611 comment_id = response.json.get('comment_id', None)
612 test_text = 'test'
613 response = self.app.post(
614 route_path(
615 'pullrequest_comment_edit',
616 repo_name=target_scm_name,
617 pull_request_id=pull_request_id,
618 comment_id=comment_id,
619 ),
620 extra_environ=xhr_header,
621 params={
622 'csrf_token': csrf_token,
623 'text': test_text,
624 },
625 status=403,
626 )
627 assert response.status_int == 403
611 for comment_id in response.json.keys():
612 test_text = 'test'
613 response = self.app.post(
614 route_path(
615 'pullrequest_comment_edit',
616 repo_name=target_scm_name,
617 pull_request_id=pull_request_id,
618 comment_id=comment_id,
619 ),
620 extra_environ=xhr_header,
621 params={
622 'csrf_token': csrf_token,
623 'text': test_text,
624 },
625 status=403,
626 )
627 assert response.status_int == 403
628 628
629 629 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
630 630 pull_request = pr_util.create_pull_request()
@@ -644,27 +644,27 b' class TestPullrequestsView(object):'
644 644 )
645 645 assert response.json
646 646
647 comment_id = response.json.get('comment_id', None)
648 assert comment_id
649 test_text = 'test'
650 self.app.post(
651 route_path(
652 'pullrequest_comment_edit',
653 repo_name=target_scm_name,
654 pull_request_id=pull_request.pull_request_id,
655 comment_id=comment_id,
656 ),
657 extra_environ=xhr_header,
658 params={
659 'csrf_token': csrf_token,
660 'text': test_text,
661 'version': '0',
662 },
647 for comment_id in response.json.keys():
648 assert comment_id
649 test_text = 'test'
650 self.app.post(
651 route_path(
652 'pullrequest_comment_edit',
653 repo_name=target_scm_name,
654 pull_request_id=pull_request.pull_request_id,
655 comment_id=comment_id,
656 ),
657 extra_environ=xhr_header,
658 params={
659 'csrf_token': csrf_token,
660 'text': test_text,
661 'version': '0',
662 },
663 663
664 )
665 text_form_db = ChangesetComment.query().filter(
666 ChangesetComment.comment_id == comment_id).first().text
667 assert test_text == text_form_db
664 )
665 text_form_db = ChangesetComment.query().filter(
666 ChangesetComment.comment_id == comment_id).first().text
667 assert test_text == text_form_db
668 668
669 669 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
670 670 pull_request = pr_util.create_pull_request()
@@ -684,26 +684,25 b' class TestPullrequestsView(object):'
684 684 )
685 685 assert response.json
686 686
687 comment_id = response.json.get('comment_id', None)
688 assert comment_id
689 test_text = 'init'
690 response = self.app.post(
691 route_path(
692 'pullrequest_comment_edit',
693 repo_name=target_scm_name,
694 pull_request_id=pull_request.pull_request_id,
695 comment_id=comment_id,
696 ),
697 extra_environ=xhr_header,
698 params={
699 'csrf_token': csrf_token,
700 'text': test_text,
701 'version': '0',
702 },
703 status=404,
687 for comment_id in response.json.keys():
688 test_text = 'init'
689 response = self.app.post(
690 route_path(
691 'pullrequest_comment_edit',
692 repo_name=target_scm_name,
693 pull_request_id=pull_request.pull_request_id,
694 comment_id=comment_id,
695 ),
696 extra_environ=xhr_header,
697 params={
698 'csrf_token': csrf_token,
699 'text': test_text,
700 'version': '0',
701 },
702 status=404,
704 703
705 )
706 assert response.status_int == 404
704 )
705 assert response.status_int == 404
707 706
708 707 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
709 708 pull_request = pr_util.create_pull_request()
@@ -722,48 +721,46 b' class TestPullrequestsView(object):'
722 721 extra_environ=xhr_header,
723 722 )
724 723 assert response.json
725 comment_id = response.json.get('comment_id', None)
726 assert comment_id
727
728 test_text = 'test'
729 self.app.post(
730 route_path(
731 'pullrequest_comment_edit',
732 repo_name=target_scm_name,
733 pull_request_id=pull_request.pull_request_id,
734 comment_id=comment_id,
735 ),
736 extra_environ=xhr_header,
737 params={
738 'csrf_token': csrf_token,
739 'text': test_text,
740 'version': '0',
741 },
724 for comment_id in response.json.keys():
725 test_text = 'test'
726 self.app.post(
727 route_path(
728 'pullrequest_comment_edit',
729 repo_name=target_scm_name,
730 pull_request_id=pull_request.pull_request_id,
731 comment_id=comment_id,
732 ),
733 extra_environ=xhr_header,
734 params={
735 'csrf_token': csrf_token,
736 'text': test_text,
737 'version': '0',
738 },
742 739
743 )
744 test_text_v2 = 'test_v2'
745 response = self.app.post(
746 route_path(
747 'pullrequest_comment_edit',
748 repo_name=target_scm_name,
749 pull_request_id=pull_request.pull_request_id,
750 comment_id=comment_id,
751 ),
752 extra_environ=xhr_header,
753 params={
754 'csrf_token': csrf_token,
755 'text': test_text_v2,
756 'version': '0',
757 },
758 status=409,
759 )
760 assert response.status_int == 409
740 )
741 test_text_v2 = 'test_v2'
742 response = self.app.post(
743 route_path(
744 'pullrequest_comment_edit',
745 repo_name=target_scm_name,
746 pull_request_id=pull_request.pull_request_id,
747 comment_id=comment_id,
748 ),
749 extra_environ=xhr_header,
750 params={
751 'csrf_token': csrf_token,
752 'text': test_text_v2,
753 'version': '0',
754 },
755 status=409,
756 )
757 assert response.status_int == 409
761 758
762 text_form_db = ChangesetComment.query().filter(
763 ChangesetComment.comment_id == comment_id).first().text
759 text_form_db = ChangesetComment.query().filter(
760 ChangesetComment.comment_id == comment_id).first().text
764 761
765 assert test_text == text_form_db
766 assert test_text_v2 != text_form_db
762 assert test_text == text_form_db
763 assert test_text_v2 != text_form_db
767 764
768 765 def test_comment_and_comment_edit_permissions_forbidden(
769 766 self, autologin_regular_user, user_regular, user_admin, pr_util,
@@ -24,7 +24,8 b' from rhodecode.model.pull_request import'
24 24 from rhodecode.model.db import PullRequestReviewers
25 25 # V3 - Reviewers, with default rules data
26 26 # v4 - Added observers metadata
27 REVIEWER_API_VERSION = 'V4'
27 # v5 - pr_author/commit_author include/exclude logic
28 REVIEWER_API_VERSION = 'V5'
28 29
29 30
30 31 def reviewer_as_json(user, reasons=None, role=None, mandatory=False, rules=None, user_group=None):
@@ -88,6 +89,7 b' def get_default_reviewers_data(current_u'
88 89 'reviewers': json_reviewers,
89 90 'rules': {},
90 91 'rules_data': {},
92 'rules_humanized': [],
91 93 }
92 94
93 95
@@ -77,6 +77,7 b' class RepoCommitsView(RepoAppView):'
77 77 _ = self.request.translate
78 78 c = self.load_default_context()
79 79 c.fulldiff = self.request.GET.get('fulldiff')
80 redirect_to_combined = str2bool(self.request.GET.get('redirect_combined'))
80 81
81 82 # fetch global flags of ignore ws or context lines
82 83 diff_context = get_diff_context(self.request)
@@ -117,6 +118,19 b' class RepoCommitsView(RepoAppView):'
117 118 raise HTTPNotFound()
118 119 single_commit = len(c.commit_ranges) == 1
119 120
121 if redirect_to_combined and not single_commit:
122 source_ref = getattr(c.commit_ranges[0].parents[0]
123 if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id')
124 target_ref = c.commit_ranges[-1].raw_id
125 next_url = h.route_path(
126 'repo_compare',
127 repo_name=c.repo_name,
128 source_ref_type='rev',
129 source_ref=source_ref,
130 target_ref_type='rev',
131 target_ref=target_ref)
132 raise HTTPFound(next_url)
133
120 134 c.changes = OrderedDict()
121 135 c.lines_added = 0
122 136 c.lines_deleted = 0
@@ -366,6 +380,121 b' class RepoCommitsView(RepoAppView):'
366 380 commit_id = self.request.matchdict['commit_id']
367 381 return self._commit(commit_id, method='download')
368 382
383 def _commit_comments_create(self, commit_id, comments):
384 _ = self.request.translate
385 data = {}
386 if not comments:
387 return
388
389 commit = self.db_repo.get_commit(commit_id)
390
391 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
392 for entry in comments:
393 c = self.load_default_context()
394 comment_type = entry['comment_type']
395 text = entry['text']
396 status = entry['status']
397 is_draft = str2bool(entry['is_draft'])
398 resolves_comment_id = entry['resolves_comment_id']
399 f_path = entry['f_path']
400 line_no = entry['line']
401 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
402
403 if status:
404 text = text or (_('Status change %(transition_icon)s %(status)s')
405 % {'transition_icon': '>',
406 'status': ChangesetStatus.get_status_lbl(status)})
407
408 comment = CommentsModel().create(
409 text=text,
410 repo=self.db_repo.repo_id,
411 user=self._rhodecode_db_user.user_id,
412 commit_id=commit_id,
413 f_path=f_path,
414 line_no=line_no,
415 status_change=(ChangesetStatus.get_status_lbl(status)
416 if status else None),
417 status_change_type=status,
418 comment_type=comment_type,
419 is_draft=is_draft,
420 resolves_comment_id=resolves_comment_id,
421 auth_user=self._rhodecode_user,
422 send_email=not is_draft, # skip notification for draft comments
423 )
424 is_inline = comment.is_inline
425
426 # get status if set !
427 if status:
428 # `dont_allow_on_closed_pull_request = True` means
429 # if latest status was from pull request and it's closed
430 # disallow changing status !
431
432 try:
433 ChangesetStatusModel().set_status(
434 self.db_repo.repo_id,
435 status,
436 self._rhodecode_db_user.user_id,
437 comment,
438 revision=commit_id,
439 dont_allow_on_closed_pull_request=True
440 )
441 except StatusChangeOnClosedPullRequestError:
442 msg = _('Changing the status of a commit associated with '
443 'a closed pull request is not allowed')
444 log.exception(msg)
445 h.flash(msg, category='warning')
446 raise HTTPFound(h.route_path(
447 'repo_commit', repo_name=self.db_repo_name,
448 commit_id=commit_id))
449
450 Session().flush()
451 # this is somehow required to get access to some relationship
452 # loaded on comment
453 Session().refresh(comment)
454
455 # skip notifications for drafts
456 if not is_draft:
457 CommentsModel().trigger_commit_comment_hook(
458 self.db_repo, self._rhodecode_user, 'create',
459 data={'comment': comment, 'commit': commit})
460
461 comment_id = comment.comment_id
462 data[comment_id] = {
463 'target_id': target_elem_id
464 }
465 Session().flush()
466
467 c.co = comment
468 c.at_version_num = 0
469 c.is_new = True
470 rendered_comment = render(
471 'rhodecode:templates/changeset/changeset_comment_block.mako',
472 self._get_template_context(c), self.request)
473
474 data[comment_id].update(comment.get_dict())
475 data[comment_id].update({'rendered_text': rendered_comment})
476
477 # finalize, commit and redirect
478 Session().commit()
479
480 # skip channelstream for draft comments
481 if not all_drafts:
482 comment_broadcast_channel = channelstream.comment_channel(
483 self.db_repo_name, commit_obj=commit)
484
485 comment_data = data
486 posted_comment_type = 'inline' if is_inline else 'general'
487 if len(data) == 1:
488 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
489 else:
490 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
491
492 channelstream.comment_channelstream_push(
493 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
494 comment_data=comment_data)
495
496 return data
497
369 498 @LoginRequired()
370 499 @NotAnonymous()
371 500 @HasRepoPermissionAnyDecorator(
@@ -378,17 +507,6 b' class RepoCommitsView(RepoAppView):'
378 507 _ = self.request.translate
379 508 commit_id = self.request.matchdict['commit_id']
380 509
381 c = self.load_default_context()
382 status = self.request.POST.get('changeset_status', None)
383 text = self.request.POST.get('text')
384 comment_type = self.request.POST.get('comment_type')
385 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
386
387 if status:
388 text = text or (_('Status change %(transition_icon)s %(status)s')
389 % {'transition_icon': '>',
390 'status': ChangesetStatus.get_status_lbl(status)})
391
392 510 multi_commit_ids = []
393 511 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
394 512 if _commit_id not in ['', None, EmptyCommit.raw_id]:
@@ -397,81 +515,23 b' class RepoCommitsView(RepoAppView):'
397 515
398 516 commit_ids = multi_commit_ids or [commit_id]
399 517
400 comment = None
518 data = []
519 # Multiple comments for each passed commit id
401 520 for current_id in filter(None, commit_ids):
402 comment = CommentsModel().create(
403 text=text,
404 repo=self.db_repo.repo_id,
405 user=self._rhodecode_db_user.user_id,
406 commit_id=current_id,
407 f_path=self.request.POST.get('f_path'),
408 line_no=self.request.POST.get('line'),
409 status_change=(ChangesetStatus.get_status_lbl(status)
410 if status else None),
411 status_change_type=status,
412 comment_type=comment_type,
413 resolves_comment_id=resolves_comment_id,
414 auth_user=self._rhodecode_user
415 )
416 is_inline = comment.is_inline
417
418 # get status if set !
419 if status:
420 # if latest status was from pull request and it's closed
421 # disallow changing status !
422 # dont_allow_on_closed_pull_request = True !
521 comment_data = {
522 'comment_type': self.request.POST.get('comment_type'),
523 'text': self.request.POST.get('text'),
524 'status': self.request.POST.get('changeset_status', None),
525 'is_draft': self.request.POST.get('draft'),
526 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
527 'close_pull_request': self.request.POST.get('close_pull_request'),
528 'f_path': self.request.POST.get('f_path'),
529 'line': self.request.POST.get('line'),
530 }
531 comment = self._commit_comments_create(commit_id=current_id, comments=[comment_data])
532 data.append(comment)
423 533
424 try:
425 ChangesetStatusModel().set_status(
426 self.db_repo.repo_id,
427 status,
428 self._rhodecode_db_user.user_id,
429 comment,
430 revision=current_id,
431 dont_allow_on_closed_pull_request=True
432 )
433 except StatusChangeOnClosedPullRequestError:
434 msg = _('Changing the status of a commit associated with '
435 'a closed pull request is not allowed')
436 log.exception(msg)
437 h.flash(msg, category='warning')
438 raise HTTPFound(h.route_path(
439 'repo_commit', repo_name=self.db_repo_name,
440 commit_id=current_id))
441
442 commit = self.db_repo.get_commit(current_id)
443 CommentsModel().trigger_commit_comment_hook(
444 self.db_repo, self._rhodecode_user, 'create',
445 data={'comment': comment, 'commit': commit})
446
447 # finalize, commit and redirect
448 Session().commit()
449
450 data = {
451 'target_id': h.safeid(h.safe_unicode(
452 self.request.POST.get('f_path'))),
453 }
454 if comment:
455 c.co = comment
456 c.at_version_num = 0
457 rendered_comment = render(
458 'rhodecode:templates/changeset/changeset_comment_block.mako',
459 self._get_template_context(c), self.request)
460
461 data.update(comment.get_dict())
462 data.update({'rendered_text': rendered_comment})
463
464 comment_broadcast_channel = channelstream.comment_channel(
465 self.db_repo_name, commit_obj=commit)
466
467 comment_data = data
468 comment_type = 'inline' if is_inline else 'general'
469 channelstream.comment_channelstream_push(
470 self.request, comment_broadcast_channel, self._rhodecode_user,
471 _('posted a new {} comment').format(comment_type),
472 comment_data=comment_data)
473
474 return data
534 return data if len(data) > 1 else data[0]
475 535
476 536 @LoginRequired()
477 537 @NotAnonymous()
@@ -665,6 +725,7 b' class RepoCommitsView(RepoAppView):'
665 725 def repo_commit_comment_edit(self):
666 726 self.load_default_context()
667 727
728 commit_id = self.request.matchdict['commit_id']
668 729 comment_id = self.request.matchdict['comment_id']
669 730 comment = ChangesetComment.get_or_404(comment_id)
670 731
@@ -717,11 +778,11 b' class RepoCommitsView(RepoAppView):'
717 778 if not comment_history:
718 779 raise HTTPNotFound()
719 780
720 commit_id = self.request.matchdict['commit_id']
721 commit = self.db_repo.get_commit(commit_id)
722 CommentsModel().trigger_commit_comment_hook(
723 self.db_repo, self._rhodecode_user, 'edit',
724 data={'comment': comment, 'commit': commit})
781 if not comment.draft:
782 commit = self.db_repo.get_commit(commit_id)
783 CommentsModel().trigger_commit_comment_hook(
784 self.db_repo, self._rhodecode_user, 'edit',
785 data={'comment': comment, 'commit': commit})
725 786
726 787 Session().commit()
727 788 return {
@@ -325,6 +325,21 b' class RepoFilesView(RepoAppView):'
325 325
326 326 return lf_enabled
327 327
328 def _get_archive_name(self, db_repo_name, commit_sha, ext, subrepos=False, path_sha=''):
329 # original backward compat name of archive
330 clean_name = safe_str(db_repo_name.replace('/', '_'))
331
332 # e.g vcsserver.zip
333 # e.g vcsserver-abcdefgh.zip
334 # e.g vcsserver-abcdefgh-defghijk.zip
335 archive_name = '{}{}{}{}{}'.format(
336 clean_name,
337 '-sub' if subrepos else '',
338 commit_sha,
339 '-{}'.format(path_sha) if path_sha else '',
340 ext)
341 return archive_name
342
328 343 @LoginRequired()
329 344 @HasRepoPermissionAnyDecorator(
330 345 'repository.read', 'repository.write', 'repository.admin')
@@ -339,6 +354,7 b' class RepoFilesView(RepoAppView):'
339 354 default_at_path = '/'
340 355 fname = self.request.matchdict['fname']
341 356 subrepos = self.request.GET.get('subrepos') == 'true'
357 with_hash = str2bool(self.request.GET.get('with_hash', '1'))
342 358 at_path = self.request.GET.get('at_path') or default_at_path
343 359
344 360 if not self.db_repo.enable_downloads:
@@ -364,30 +380,30 b' class RepoFilesView(RepoAppView):'
364 380 except Exception:
365 381 return Response(_('No node at path {} for this repository').format(at_path))
366 382
367 path_sha = sha1(at_path)[:8]
368
369 # original backward compat name of archive
370 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
371 short_sha = safe_str(commit.short_id)
383 # path sha is part of subdir
384 path_sha = ''
385 if at_path != default_at_path:
386 path_sha = sha1(at_path)[:8]
387 short_sha = '-{}'.format(safe_str(commit.short_id))
388 # used for cache etc
389 archive_name = self._get_archive_name(
390 self.db_repo_name, commit_sha=short_sha, ext=ext, subrepos=subrepos,
391 path_sha=path_sha)
372 392
373 if at_path == default_at_path:
374 archive_name = '{}-{}{}{}'.format(
375 clean_name,
376 '-sub' if subrepos else '',
377 short_sha,
378 ext)
379 # custom path and new name
380 else:
381 archive_name = '{}-{}{}-{}{}'.format(
382 clean_name,
383 '-sub' if subrepos else '',
384 short_sha,
385 path_sha,
386 ext)
393 if not with_hash:
394 short_sha = ''
395 path_sha = ''
396
397 # what end client gets served
398 response_archive_name = self._get_archive_name(
399 self.db_repo_name, commit_sha=short_sha, ext=ext, subrepos=subrepos,
400 path_sha=path_sha)
401 # remove extension from our archive directory name
402 archive_dir_name = response_archive_name[:-len(ext)]
387 403
388 404 use_cached_archive = False
389 archive_cache_enabled = CONFIG.get(
390 'archive_cache_dir') and not self.request.GET.get('no_cache')
405 archive_cache_dir = CONFIG.get('archive_cache_dir')
406 archive_cache_enabled = archive_cache_dir and not self.request.GET.get('no_cache')
391 407 cached_archive_path = None
392 408
393 409 if archive_cache_enabled:
@@ -403,12 +419,14 b' class RepoFilesView(RepoAppView):'
403 419 else:
404 420 log.debug('Archive %s is not yet cached', archive_name)
405 421
422 # generate new archive, as previous was not found in the cache
406 423 if not use_cached_archive:
407 # generate new archive
408 fd, archive = tempfile.mkstemp()
424 _dir = os.path.abspath(archive_cache_dir) if archive_cache_dir else None
425 fd, archive = tempfile.mkstemp(dir=_dir)
409 426 log.debug('Creating new temp archive in %s', archive)
410 427 try:
411 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
428 commit.archive_repo(archive, archive_dir_name=archive_dir_name,
429 kind=fileformat, subrepos=subrepos,
412 430 archive_at_path=at_path)
413 431 except ImproperArchiveTypeError:
414 432 return _('Unknown archive type')
@@ -445,8 +463,7 b' class RepoFilesView(RepoAppView):'
445 463 yield data
446 464
447 465 response = Response(app_iter=get_chunked_archive(archive))
448 response.content_disposition = str(
449 'attachment; filename=%s' % archive_name)
466 response.content_disposition = str('attachment; filename=%s' % response_archive_name)
450 467 response.content_type = str(content_type)
451 468
452 469 return response
@@ -47,7 +47,7 b' from rhodecode.lib.vcs.exceptions import'
47 47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 48 from rhodecode.model.comment import CommentsModel
49 49 from rhodecode.model.db import (
50 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 51 PullRequestReviewers)
52 52 from rhodecode.model.forms import PullRequestForm
53 53 from rhodecode.model.meta import Session
@@ -107,7 +107,8 b' class RepoPullRequestsView(RepoAppView, '
107 107 comments_model = CommentsModel()
108 108 for pr in pull_requests:
109 109 comments_count = comments_model.get_all_comments(
110 self.db_repo.repo_id, pull_request=pr, count_only=True)
110 self.db_repo.repo_id, pull_request=pr,
111 include_drafts=False, count_only=True)
111 112
112 113 data.append({
113 114 'name': _render('pullrequest_name',
@@ -120,7 +121,8 b' class RepoPullRequestsView(RepoAppView, '
120 121 'title': _render('pullrequest_title', pr.title, pr.description),
121 122 'description': h.escape(pr.description),
122 123 'updated_on': _render('pullrequest_updated_on',
123 h.datetime_to_time(pr.updated_on)),
124 h.datetime_to_time(pr.updated_on),
125 pr.versions_count),
124 126 'updated_on_raw': h.datetime_to_time(pr.updated_on),
125 127 'created_on': _render('pullrequest_updated_on',
126 128 h.datetime_to_time(pr.created_on)),
@@ -268,12 +270,14 b' class RepoPullRequestsView(RepoAppView, '
268 270
269 271 return diffset
270 272
271 def register_comments_vars(self, c, pull_request, versions):
273 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
272 274 comments_model = CommentsModel()
273 275
274 276 # GENERAL COMMENTS with versions #
275 277 q = comments_model._all_general_comments_of_pull_request(pull_request)
276 278 q = q.order_by(ChangesetComment.comment_id.asc())
279 if not include_drafts:
280 q = q.filter(ChangesetComment.draft == false())
277 281 general_comments = q
278 282
279 283 # pick comments we want to render at current version
@@ -283,6 +287,8 b' class RepoPullRequestsView(RepoAppView, '
283 287 # INLINE COMMENTS with versions #
284 288 q = comments_model._all_inline_comments_of_pull_request(pull_request)
285 289 q = q.order_by(ChangesetComment.comment_id.asc())
290 if not include_drafts:
291 q = q.filter(ChangesetComment.draft == false())
286 292 inline_comments = q
287 293
288 294 c.inline_versions = comments_model.aggregate_comments(
@@ -422,16 +428,12 b' class RepoPullRequestsView(RepoAppView, '
422 428 c.allowed_to_close = c.allowed_to_merge and not pr_closed
423 429
424 430 c.forbid_adding_reviewers = False
425 c.forbid_author_to_review = False
426 c.forbid_commit_author_to_review = False
427 431
428 432 if pull_request_latest.reviewer_data and \
429 433 'rules' in pull_request_latest.reviewer_data:
430 434 rules = pull_request_latest.reviewer_data['rules'] or {}
431 435 try:
432 436 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
433 c.forbid_author_to_review = rules.get('forbid_author_to_review')
434 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
435 437 except Exception:
436 438 pass
437 439
@@ -499,6 +501,11 b' class RepoPullRequestsView(RepoAppView, '
499 501 c.resolved_comments = CommentsModel() \
500 502 .get_pull_request_resolved_todos(pull_request_latest)
501 503
504 # Drafts
505 c.draft_comments = CommentsModel().get_pull_request_drafts(
506 self._rhodecode_db_user.user_id,
507 pull_request_latest)
508
502 509 # if we use version, then do not show later comments
503 510 # than current version
504 511 display_inline_comments = collections.defaultdict(
@@ -979,8 +986,9 b' class RepoPullRequestsView(RepoAppView, '
979 986 }
980 987 return data
981 988
982 def _get_existing_ids(self, post_data):
983 return filter(lambda e: e, map(safe_int, aslist(post_data.get('comments'), ',')))
989 @classmethod
990 def get_comment_ids(cls, post_data):
991 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
984 992
985 993 @LoginRequired()
986 994 @NotAnonymous()
@@ -1015,10 +1023,10 b' class RepoPullRequestsView(RepoAppView, '
1015 1023 if at_version and at_version != PullRequest.LATEST_VER
1016 1024 else None)
1017 1025
1018 self.register_comments_vars(c, pull_request_latest, versions)
1026 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1019 1027 all_comments = c.inline_comments_flat + c.comments
1020 1028
1021 existing_ids = self._get_existing_ids(self.request.POST)
1029 existing_ids = self.get_comment_ids(self.request.POST)
1022 1030 return _render('comments_table', all_comments, len(all_comments),
1023 1031 existing_ids=existing_ids)
1024 1032
@@ -1055,12 +1063,12 b' class RepoPullRequestsView(RepoAppView, '
1055 1063 else None)
1056 1064
1057 1065 c.unresolved_comments = CommentsModel() \
1058 .get_pull_request_unresolved_todos(pull_request)
1066 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1059 1067 c.resolved_comments = CommentsModel() \
1060 .get_pull_request_resolved_todos(pull_request)
1068 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1061 1069
1062 1070 all_comments = c.unresolved_comments + c.resolved_comments
1063 existing_ids = self._get_existing_ids(self.request.POST)
1071 existing_ids = self.get_comment_ids(self.request.POST)
1064 1072 return _render('comments_table', all_comments, len(c.unresolved_comments),
1065 1073 todo_comments=True, existing_ids=existing_ids)
1066 1074
@@ -1068,6 +1076,48 b' class RepoPullRequestsView(RepoAppView, '
1068 1076 @NotAnonymous()
1069 1077 @HasRepoPermissionAnyDecorator(
1070 1078 'repository.read', 'repository.write', 'repository.admin')
1079 @view_config(
1080 route_name='pullrequest_drafts', request_method='POST',
1081 renderer='string_html', xhr=True)
1082 def pullrequest_drafts(self):
1083 self.load_default_context()
1084
1085 pull_request = PullRequest.get_or_404(
1086 self.request.matchdict['pull_request_id'])
1087 pull_request_id = pull_request.pull_request_id
1088 version = self.request.GET.get('version')
1089
1090 _render = self.request.get_partial_renderer(
1091 'rhodecode:templates/base/sidebar.mako')
1092 c = _render.get_call_context()
1093
1094 (pull_request_latest,
1095 pull_request_at_ver,
1096 pull_request_display_obj,
1097 at_version) = PullRequestModel().get_pr_version(
1098 pull_request_id, version=version)
1099 versions = pull_request_display_obj.versions()
1100 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1101 c.versions = versions + [latest_ver]
1102
1103 c.at_version = at_version
1104 c.at_version_num = (at_version
1105 if at_version and at_version != PullRequest.LATEST_VER
1106 else None)
1107
1108 c.draft_comments = CommentsModel() \
1109 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1110
1111 all_comments = c.draft_comments
1112
1113 existing_ids = self.get_comment_ids(self.request.POST)
1114 return _render('comments_table', all_comments, len(all_comments),
1115 existing_ids=existing_ids, draft_comments=True)
1116
1117 @LoginRequired()
1118 @NotAnonymous()
1119 @HasRepoPermissionAnyDecorator(
1120 'repository.read', 'repository.write', 'repository.admin')
1071 1121 @CSRFRequired()
1072 1122 @view_config(
1073 1123 route_name='pullrequest_create', request_method='POST',
@@ -1514,6 +1564,152 b' class RepoPullRequestsView(RepoAppView, '
1514 1564 self._rhodecode_user)
1515 1565 raise HTTPNotFound()
1516 1566
1567 def _pull_request_comments_create(self, pull_request, comments):
1568 _ = self.request.translate
1569 data = {}
1570 if not comments:
1571 return
1572 pull_request_id = pull_request.pull_request_id
1573
1574 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1575
1576 for entry in comments:
1577 c = self.load_default_context()
1578 comment_type = entry['comment_type']
1579 text = entry['text']
1580 status = entry['status']
1581 is_draft = str2bool(entry['is_draft'])
1582 resolves_comment_id = entry['resolves_comment_id']
1583 close_pull_request = entry['close_pull_request']
1584 f_path = entry['f_path']
1585 line_no = entry['line']
1586 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1587
1588 # the logic here should work like following, if we submit close
1589 # pr comment, use `close_pull_request_with_comment` function
1590 # else handle regular comment logic
1591
1592 if close_pull_request:
1593 # only owner or admin or person with write permissions
1594 allowed_to_close = PullRequestModel().check_user_update(
1595 pull_request, self._rhodecode_user)
1596 if not allowed_to_close:
1597 log.debug('comment: forbidden because not allowed to close '
1598 'pull request %s', pull_request_id)
1599 raise HTTPForbidden()
1600
1601 # This also triggers `review_status_change`
1602 comment, status = PullRequestModel().close_pull_request_with_comment(
1603 pull_request, self._rhodecode_user, self.db_repo, message=text,
1604 auth_user=self._rhodecode_user)
1605 Session().flush()
1606 is_inline = comment.is_inline
1607
1608 PullRequestModel().trigger_pull_request_hook(
1609 pull_request, self._rhodecode_user, 'comment',
1610 data={'comment': comment})
1611
1612 else:
1613 # regular comment case, could be inline, or one with status.
1614 # for that one we check also permissions
1615 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1616 allowed_to_change_status = PullRequestModel().check_user_change_status(
1617 pull_request, self._rhodecode_user) and not is_draft
1618
1619 if status and allowed_to_change_status:
1620 message = (_('Status change %(transition_icon)s %(status)s')
1621 % {'transition_icon': '>',
1622 'status': ChangesetStatus.get_status_lbl(status)})
1623 text = text or message
1624
1625 comment = CommentsModel().create(
1626 text=text,
1627 repo=self.db_repo.repo_id,
1628 user=self._rhodecode_user.user_id,
1629 pull_request=pull_request,
1630 f_path=f_path,
1631 line_no=line_no,
1632 status_change=(ChangesetStatus.get_status_lbl(status)
1633 if status and allowed_to_change_status else None),
1634 status_change_type=(status
1635 if status and allowed_to_change_status else None),
1636 comment_type=comment_type,
1637 is_draft=is_draft,
1638 resolves_comment_id=resolves_comment_id,
1639 auth_user=self._rhodecode_user,
1640 send_email=not is_draft, # skip notification for draft comments
1641 )
1642 is_inline = comment.is_inline
1643
1644 if allowed_to_change_status:
1645 # calculate old status before we change it
1646 old_calculated_status = pull_request.calculated_review_status()
1647
1648 # get status if set !
1649 if status:
1650 ChangesetStatusModel().set_status(
1651 self.db_repo.repo_id,
1652 status,
1653 self._rhodecode_user.user_id,
1654 comment,
1655 pull_request=pull_request
1656 )
1657
1658 Session().flush()
1659 # this is somehow required to get access to some relationship
1660 # loaded on comment
1661 Session().refresh(comment)
1662
1663 # skip notifications for drafts
1664 if not is_draft:
1665 PullRequestModel().trigger_pull_request_hook(
1666 pull_request, self._rhodecode_user, 'comment',
1667 data={'comment': comment})
1668
1669 # we now calculate the status of pull request, and based on that
1670 # calculation we set the commits status
1671 calculated_status = pull_request.calculated_review_status()
1672 if old_calculated_status != calculated_status:
1673 PullRequestModel().trigger_pull_request_hook(
1674 pull_request, self._rhodecode_user, 'review_status_change',
1675 data={'status': calculated_status})
1676
1677 comment_id = comment.comment_id
1678 data[comment_id] = {
1679 'target_id': target_elem_id
1680 }
1681 Session().flush()
1682
1683 c.co = comment
1684 c.at_version_num = None
1685 c.is_new = True
1686 rendered_comment = render(
1687 'rhodecode:templates/changeset/changeset_comment_block.mako',
1688 self._get_template_context(c), self.request)
1689
1690 data[comment_id].update(comment.get_dict())
1691 data[comment_id].update({'rendered_text': rendered_comment})
1692
1693 Session().commit()
1694
1695 # skip channelstream for draft comments
1696 if not all_drafts:
1697 comment_broadcast_channel = channelstream.comment_channel(
1698 self.db_repo_name, pull_request_obj=pull_request)
1699
1700 comment_data = data
1701 posted_comment_type = 'inline' if is_inline else 'general'
1702 if len(data) == 1:
1703 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1704 else:
1705 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1706
1707 channelstream.comment_channelstream_push(
1708 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1709 comment_data=comment_data)
1710
1711 return data
1712
1517 1713 @LoginRequired()
1518 1714 @NotAnonymous()
1519 1715 @HasRepoPermissionAnyDecorator(
@@ -1525,9 +1721,7 b' class RepoPullRequestsView(RepoAppView, '
1525 1721 def pull_request_comment_create(self):
1526 1722 _ = self.request.translate
1527 1723
1528 pull_request = PullRequest.get_or_404(
1529 self.request.matchdict['pull_request_id'])
1530 pull_request_id = pull_request.pull_request_id
1724 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1531 1725
1532 1726 if pull_request.is_closed():
1533 1727 log.debug('comment: forbidden because pull request is closed')
@@ -1539,124 +1733,17 b' class RepoPullRequestsView(RepoAppView, '
1539 1733 log.debug('comment: forbidden because pull request is from forbidden repo')
1540 1734 raise HTTPForbidden()
1541 1735
1542 c = self.load_default_context()
1543
1544 status = self.request.POST.get('changeset_status', None)
1545 text = self.request.POST.get('text')
1546 comment_type = self.request.POST.get('comment_type')
1547 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1548 close_pull_request = self.request.POST.get('close_pull_request')
1549
1550 # the logic here should work like following, if we submit close
1551 # pr comment, use `close_pull_request_with_comment` function
1552 # else handle regular comment logic
1553
1554 if close_pull_request:
1555 # only owner or admin or person with write permissions
1556 allowed_to_close = PullRequestModel().check_user_update(
1557 pull_request, self._rhodecode_user)
1558 if not allowed_to_close:
1559 log.debug('comment: forbidden because not allowed to close '
1560 'pull request %s', pull_request_id)
1561 raise HTTPForbidden()
1562
1563 # This also triggers `review_status_change`
1564 comment, status = PullRequestModel().close_pull_request_with_comment(
1565 pull_request, self._rhodecode_user, self.db_repo, message=text,
1566 auth_user=self._rhodecode_user)
1567 Session().flush()
1568 is_inline = comment.is_inline
1569
1570 PullRequestModel().trigger_pull_request_hook(
1571 pull_request, self._rhodecode_user, 'comment',
1572 data={'comment': comment})
1573
1574 else:
1575 # regular comment case, could be inline, or one with status.
1576 # for that one we check also permissions
1577
1578 allowed_to_change_status = PullRequestModel().check_user_change_status(
1579 pull_request, self._rhodecode_user)
1580
1581 if status and allowed_to_change_status:
1582 message = (_('Status change %(transition_icon)s %(status)s')
1583 % {'transition_icon': '>',
1584 'status': ChangesetStatus.get_status_lbl(status)})
1585 text = text or message
1586
1587 comment = CommentsModel().create(
1588 text=text,
1589 repo=self.db_repo.repo_id,
1590 user=self._rhodecode_user.user_id,
1591 pull_request=pull_request,
1592 f_path=self.request.POST.get('f_path'),
1593 line_no=self.request.POST.get('line'),
1594 status_change=(ChangesetStatus.get_status_lbl(status)
1595 if status and allowed_to_change_status else None),
1596 status_change_type=(status
1597 if status and allowed_to_change_status else None),
1598 comment_type=comment_type,
1599 resolves_comment_id=resolves_comment_id,
1600 auth_user=self._rhodecode_user
1601 )
1602 is_inline = comment.is_inline
1603
1604 if allowed_to_change_status:
1605 # calculate old status before we change it
1606 old_calculated_status = pull_request.calculated_review_status()
1607
1608 # get status if set !
1609 if status:
1610 ChangesetStatusModel().set_status(
1611 self.db_repo.repo_id,
1612 status,
1613 self._rhodecode_user.user_id,
1614 comment,
1615 pull_request=pull_request
1616 )
1617
1618 Session().flush()
1619 # this is somehow required to get access to some relationship
1620 # loaded on comment
1621 Session().refresh(comment)
1622
1623 PullRequestModel().trigger_pull_request_hook(
1624 pull_request, self._rhodecode_user, 'comment',
1625 data={'comment': comment})
1626
1627 # we now calculate the status of pull request, and based on that
1628 # calculation we set the commits status
1629 calculated_status = pull_request.calculated_review_status()
1630 if old_calculated_status != calculated_status:
1631 PullRequestModel().trigger_pull_request_hook(
1632 pull_request, self._rhodecode_user, 'review_status_change',
1633 data={'status': calculated_status})
1634
1635 Session().commit()
1636
1637 data = {
1638 'target_id': h.safeid(h.safe_unicode(
1639 self.request.POST.get('f_path'))),
1736 comment_data = {
1737 'comment_type': self.request.POST.get('comment_type'),
1738 'text': self.request.POST.get('text'),
1739 'status': self.request.POST.get('changeset_status', None),
1740 'is_draft': self.request.POST.get('draft'),
1741 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1742 'close_pull_request': self.request.POST.get('close_pull_request'),
1743 'f_path': self.request.POST.get('f_path'),
1744 'line': self.request.POST.get('line'),
1640 1745 }
1641 if comment:
1642 c.co = comment
1643 c.at_version_num = None
1644 rendered_comment = render(
1645 'rhodecode:templates/changeset/changeset_comment_block.mako',
1646 self._get_template_context(c), self.request)
1647
1648 data.update(comment.get_dict())
1649 data.update({'rendered_text': rendered_comment})
1650
1651 comment_broadcast_channel = channelstream.comment_channel(
1652 self.db_repo_name, pull_request_obj=pull_request)
1653
1654 comment_data = data
1655 comment_type = 'inline' if is_inline else 'general'
1656 channelstream.comment_channelstream_push(
1657 self.request, comment_broadcast_channel, self._rhodecode_user,
1658 _('posted a new {} comment').format(comment_type),
1659 comment_data=comment_data)
1746 data = self._pull_request_comments_create(pull_request, [comment_data])
1660 1747
1661 1748 return data
1662 1749
@@ -1741,11 +1828,6 b' class RepoPullRequestsView(RepoAppView, '
1741 1828 log.debug('comment: forbidden because pull request is closed')
1742 1829 raise HTTPForbidden()
1743 1830
1744 if not comment:
1745 log.debug('Comment with id:%s not found, skipping', comment_id)
1746 # comment already deleted in another call probably
1747 return True
1748
1749 1831 if comment.pull_request.is_closed():
1750 1832 # don't allow deleting comments on closed pull request
1751 1833 raise HTTPForbidden()
@@ -1796,10 +1878,10 b' class RepoPullRequestsView(RepoAppView, '
1796 1878 raise HTTPNotFound()
1797 1879
1798 1880 Session().commit()
1799
1800 PullRequestModel().trigger_pull_request_hook(
1801 pull_request, self._rhodecode_user, 'comment_edit',
1802 data={'comment': comment})
1881 if not comment.draft:
1882 PullRequestModel().trigger_pull_request_hook(
1883 pull_request, self._rhodecode_user, 'comment_edit',
1884 data={'comment': comment})
1803 1885
1804 1886 return {
1805 1887 'comment_history_id': comment_history.comment_history_id,
@@ -215,9 +215,10 b' class RhodeCodeAuthPluginBase(object):'
215 215 """
216 216 return self._plugin_id
217 217
218 def get_display_name(self):
218 def get_display_name(self, load_from_settings=False):
219 219 """
220 220 Returns a translation string for displaying purposes.
221 if load_from_settings is set, plugin settings can override the display name
221 222 """
222 223 raise NotImplementedError('Not implemented in base class')
223 224
@@ -213,7 +213,7 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
213 213 def get_settings_schema(self):
214 214 return CrowdSettingsSchema()
215 215
216 def get_display_name(self):
216 def get_display_name(self, load_from_settings=False):
217 217 return _('CROWD')
218 218
219 219 @classmethod
@@ -95,7 +95,7 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
95 95 route_name='auth_home',
96 96 context=HeadersAuthnResource)
97 97
98 def get_display_name(self):
98 def get_display_name(self, load_from_settings=False):
99 99 return _('Headers')
100 100
101 101 def get_settings_schema(self):
@@ -89,7 +89,7 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
89 89 def get_settings_schema(self):
90 90 return JasigCasSettingsSchema()
91 91
92 def get_display_name(self):
92 def get_display_name(self, load_from_settings=False):
93 93 return _('Jasig-CAS')
94 94
95 95 @hybrid_property
@@ -421,7 +421,7 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
421 421 def get_settings_schema(self):
422 422 return LdapSettingsSchema()
423 423
424 def get_display_name(self):
424 def get_display_name(self, load_from_settings=False):
425 425 return _('LDAP')
426 426
427 427 @classmethod
@@ -95,7 +95,7 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
95 95 route_name='auth_home',
96 96 context=PamAuthnResource)
97 97
98 def get_display_name(self):
98 def get_display_name(self, load_from_settings=False):
99 99 return _('PAM')
100 100
101 101 @classmethod
@@ -75,7 +75,7 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP'
75 75 def get_settings_schema(self):
76 76 return RhodeCodeSettingsSchema()
77 77
78 def get_display_name(self):
78 def get_display_name(self, load_from_settings=False):
79 79 return _('RhodeCode Internal')
80 80
81 81 @classmethod
@@ -73,7 +73,7 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP'
73 73 def get_settings_schema(self):
74 74 return RhodeCodeSettingsSchema()
75 75
76 def get_display_name(self):
76 def get_display_name(self, load_from_settings=False):
77 77 return _('Rhodecode Token')
78 78
79 79 @classmethod
@@ -53,7 +53,7 b' from rhodecode.lib.utils2 import aslist '
53 53 from rhodecode.lib.exc_tracking import store_exception
54 54 from rhodecode.subscribers import (
55 55 scan_repositories_if_enabled, write_js_routes_if_enabled,
56 write_metadata_if_needed, write_usage_data, inject_app_settings)
56 write_metadata_if_needed, write_usage_data)
57 57
58 58
59 59 log = logging.getLogger(__name__)
@@ -310,8 +310,6 b' def includeme(config):'
310 310
311 311 # Add subscribers.
312 312 if load_all:
313 config.add_subscriber(inject_app_settings,
314 pyramid.events.ApplicationCreated)
315 313 config.add_subscriber(scan_repositories_if_enabled,
316 314 pyramid.events.ApplicationCreated)
317 315 config.add_subscriber(write_metadata_if_needed,
@@ -67,7 +67,7 b' markdown_tags = ['
67 67
68 68 markdown_attrs = {
69 69 "*": ["class", "style", "align"],
70 "img": ["src", "alt", "title"],
70 "img": ["src", "alt", "title", "width", "height", "hspace", "align"],
71 71 "a": ["href", "alt", "title", "name", "data-hovercard-alt", "data-hovercard-url"],
72 72 "abbr": ["title"],
73 73 "acronym": ["title"],
@@ -339,13 +339,12 b' def comment_channelstream_push(request, '
339 339
340 340 comment_data = kwargs.pop('comment_data', {})
341 341 user_data = kwargs.pop('user_data', {})
342 comment_id = comment_data.get('comment_id')
342 comment_id = comment_data.keys()[0] if comment_data else ''
343 343
344 message = '<strong>{}</strong> {} #{}, {}'.format(
344 message = '<strong>{}</strong> {} #{}'.format(
345 345 user.username,
346 346 msg,
347 347 comment_id,
348 _reload_link(_('Reload page to see new comments')),
349 348 )
350 349
351 350 message_obj = {
@@ -1148,7 +1148,7 b' class DiffLimitExceeded(Exception):'
1148 1148
1149 1149 # NOTE(marcink): if diffs.mako change, probably this
1150 1150 # needs a bump to next version
1151 CURRENT_DIFF_VERSION = 'v4'
1151 CURRENT_DIFF_VERSION = 'v5'
1152 1152
1153 1153
1154 1154 def _cleanup_cache_file(cached_diff_file):
@@ -110,7 +110,7 b' def _store_exception(exc_id, exc_type_na'
110 110
111 111 mail_server = app.CONFIG.get('smtp_server') or None
112 112 send_email = send_email and mail_server
113 if send_email:
113 if send_email and request:
114 114 try:
115 115 send_exc_email(request, exc_id, exc_type_name)
116 116 except Exception:
@@ -18,17 +18,85 b''
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 import re
21 22 import markdown
23 import xml.etree.ElementTree as etree
22 24
23 25 from markdown.extensions import Extension
24 26 from markdown.extensions.fenced_code import FencedCodeExtension
25 27 from markdown.extensions.smart_strong import SmartEmphasisExtension
26 28 from markdown.extensions.tables import TableExtension
27 from markdown.extensions.nl2br import Nl2BrExtension
29 from markdown.inlinepatterns import Pattern
28 30
29 31 import gfm
30 32
31 33
34 class InlineProcessor(Pattern):
35 """
36 Base class that inline patterns subclass.
37 This is the newer style inline processor that uses a more
38 efficient and flexible search approach.
39 """
40
41 def __init__(self, pattern, md=None):
42 """
43 Create an instant of an inline pattern.
44 Keyword arguments:
45 * pattern: A regular expression that matches a pattern
46 """
47 self.pattern = pattern
48 self.compiled_re = re.compile(pattern, re.DOTALL | re.UNICODE)
49
50 # Api for Markdown to pass safe_mode into instance
51 self.safe_mode = False
52 self.md = md
53
54 def handleMatch(self, m, data):
55 """Return a ElementTree element from the given match and the
56 start and end index of the matched text.
57 If `start` and/or `end` are returned as `None`, it will be
58 assumed that the processor did not find a valid region of text.
59 Subclasses should override this method.
60 Keyword arguments:
61 * m: A re match object containing a match of the pattern.
62 * data: The buffer current under analysis
63 Returns:
64 * el: The ElementTree element, text or None.
65 * start: The start of the region that has been matched or None.
66 * end: The end of the region that has been matched or None.
67 """
68 pass # pragma: no cover
69
70
71 class SimpleTagInlineProcessor(InlineProcessor):
72 """
73 Return element of type `tag` with a text attribute of group(2)
74 of a Pattern.
75 """
76 def __init__(self, pattern, tag):
77 InlineProcessor.__init__(self, pattern)
78 self.tag = tag
79
80 def handleMatch(self, m, data): # pragma: no cover
81 el = etree.Element(self.tag)
82 el.text = m.group(2)
83 return el, m.start(0), m.end(0)
84
85
86 class SubstituteTagInlineProcessor(SimpleTagInlineProcessor):
87 """ Return an element of type `tag` with no children. """
88 def handleMatch(self, m, data):
89 return etree.Element(self.tag), m.start(0), m.end(0)
90
91
92 class Nl2BrExtension(Extension):
93 BR_RE = r'\n'
94
95 def extendMarkdown(self, md, md_globals):
96 br_tag = SubstituteTagInlineProcessor(self.BR_RE, 'br')
97 md.inlinePatterns.add('nl', br_tag, '_end')
98
99
32 100 class GithubFlavoredMarkdownExtension(Extension):
33 101 """
34 102 An extension that is as compatible as possible with GitHub-flavored
@@ -51,6 +119,7 b' class GithubFlavoredMarkdownExtension(Ex'
51 119
52 120 def extendMarkdown(self, md, md_globals):
53 121 # Built-in extensions
122 Nl2BrExtension().extendMarkdown(md, md_globals)
54 123 FencedCodeExtension().extendMarkdown(md, md_globals)
55 124 SmartEmphasisExtension().extendMarkdown(md, md_globals)
56 125 TableExtension().extendMarkdown(md, md_globals)
@@ -68,7 +137,6 b' class GithubFlavoredMarkdownExtension(Ex'
68 137 gfm.TaskListExtension([
69 138 ('list_attrs', {'class': 'checkbox'})
70 139 ]).extendMarkdown(md, md_globals)
71 Nl2BrExtension().extendMarkdown(md, md_globals)
72 140
73 141
74 142 # Global Vars
@@ -74,7 +74,11 b' def configure_dogpile_cache(settings):'
74 74
75 75 new_region.configure_from_config(settings, 'rc_cache.{}.'.format(region_name))
76 76 new_region.function_key_generator = backend_key_generator(new_region.actual_backend)
77 log.debug('dogpile: registering a new region %s[%s]', region_name, new_region.__dict__)
77 if log.isEnabledFor(logging.DEBUG):
78 region_args = dict(backend=new_region.actual_backend.__class__,
79 region_invalidator=new_region.region_invalidator.__class__)
80 log.debug('dogpile: registering a new region `%s` %s', region_name, region_args)
81
78 82 region_meta.dogpile_cache_regions[region_name] = new_region
79 83
80 84
@@ -915,8 +915,9 b' class BaseCommit(object):'
915 915 list of parent commits
916 916
917 917 """
918 repository = None
919 branch = None
918 920
919 branch = None
920 921 """
921 922 Depending on the backend this should be set to the branch name of the
922 923 commit. Backends not supporting branches on commits should leave this
@@ -1192,13 +1193,14 b' class BaseCommit(object):'
1192 1193 return None
1193 1194
1194 1195 def archive_repo(self, archive_dest_path, kind='tgz', subrepos=None,
1195 prefix=None, write_metadata=False, mtime=None, archive_at_path='/'):
1196 archive_dir_name=None, write_metadata=False, mtime=None,
1197 archive_at_path='/'):
1196 1198 """
1197 1199 Creates an archive containing the contents of the repository.
1198 1200
1199 1201 :param archive_dest_path: path to the file which to create the archive.
1200 1202 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1201 :param prefix: name of root directory in archive.
1203 :param archive_dir_name: name of root directory in archive.
1202 1204 Default is repository name and commit's short_id joined with dash:
1203 1205 ``"{repo_name}-{short_id}"``.
1204 1206 :param write_metadata: write a metadata file into archive.
@@ -1214,43 +1216,26 b' class BaseCommit(object):'
1214 1216 'Archive kind (%s) not supported use one of %s' %
1215 1217 (kind, allowed_kinds))
1216 1218
1217 prefix = self._validate_archive_prefix(prefix)
1218
1219 archive_dir_name = self._validate_archive_prefix(archive_dir_name)
1219 1220 mtime = mtime is not None or time.mktime(self.date.timetuple())
1220
1221 file_info = []
1222 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
1223 for _r, _d, files in cur_rev.walk(archive_at_path):
1224 for f in files:
1225 f_path = os.path.join(prefix, f.path)
1226 file_info.append(
1227 (f_path, f.mode, f.is_link(), f.raw_bytes))
1221 commit_id = self.raw_id
1228 1222
1229 if write_metadata:
1230 metadata = [
1231 ('repo_name', self.repository.name),
1232 ('commit_id', self.raw_id),
1233 ('mtime', mtime),
1234 ('branch', self.branch),
1235 ('tags', ','.join(self.tags)),
1236 ]
1237 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
1238 file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
1223 return self.repository._remote.archive_repo(
1224 archive_dest_path, kind, mtime, archive_at_path,
1225 archive_dir_name, commit_id)
1239 1226
1240 connection.Hg.archive_repo(archive_dest_path, mtime, file_info, kind)
1241
1242 def _validate_archive_prefix(self, prefix):
1243 if prefix is None:
1244 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
1227 def _validate_archive_prefix(self, archive_dir_name):
1228 if archive_dir_name is None:
1229 archive_dir_name = self._ARCHIVE_PREFIX_TEMPLATE.format(
1245 1230 repo_name=safe_str(self.repository.name),
1246 1231 short_id=self.short_id)
1247 elif not isinstance(prefix, str):
1248 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
1249 elif prefix.startswith('/'):
1232 elif not isinstance(archive_dir_name, str):
1233 raise ValueError("prefix not a bytes object: %s" % repr(archive_dir_name))
1234 elif archive_dir_name.startswith('/'):
1250 1235 raise VCSError("Prefix cannot start with leading slash")
1251 elif prefix.strip() == '':
1236 elif archive_dir_name.strip() == '':
1252 1237 raise VCSError("Prefix cannot be empty")
1253 return prefix
1238 return archive_dir_name
1254 1239
1255 1240 @LazyProperty
1256 1241 def root(self):
@@ -214,16 +214,19 b' def map_vcs_exceptions(func):'
214 214 # to translate them to the proper exception class in the vcs
215 215 # client layer.
216 216 kind = getattr(e, '_vcs_kind', None)
217 exc_name = getattr(e, '_vcs_server_org_exc_name', None)
217 218
218 219 if kind:
219 220 if any(e.args):
220 args = e.args
221 args = [a for a in e.args]
222 args[0] = '{}:'.format(exc_name) # prefix first arg with org exc name
221 223 else:
222 args = [__traceback_info__ or 'unhandledException']
224 args = [__traceback_info__ or '{}: UnhandledException'.format(exc_name)]
223 225 if debug or __traceback_info__ and kind not in ['unhandled', 'lookup']:
224 226 # for other than unhandled errors also log the traceback
225 227 # can be useful for debugging
226 228 log.error(__traceback_info__)
229
227 230 raise _EXCEPTION_MAP[kind](*args)
228 231 else:
229 232 raise
@@ -37,6 +37,7 b' from rhodecode.lib.exceptions import Com'
37 37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
38 38 from rhodecode.model import BaseModel
39 39 from rhodecode.model.db import (
40 false, true,
40 41 ChangesetComment,
41 42 User,
42 43 Notification,
@@ -160,7 +161,7 b' class CommentsModel(BaseModel):'
160 161
161 162 return todos
162 163
163 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
164 165
165 166 todos = Session().query(ChangesetComment) \
166 167 .filter(ChangesetComment.pull_request == pull_request) \
@@ -168,6 +169,9 b' class CommentsModel(BaseModel):'
168 169 .filter(ChangesetComment.comment_type
169 170 == ChangesetComment.COMMENT_TYPE_TODO)
170 171
172 if not include_drafts:
173 todos = todos.filter(ChangesetComment.draft == false())
174
171 175 if not show_outdated:
172 176 todos = todos.filter(
173 177 coalesce(ChangesetComment.display_state, '') !=
@@ -177,7 +181,7 b' class CommentsModel(BaseModel):'
177 181
178 182 return todos
179 183
180 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
184 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
181 185
182 186 todos = Session().query(ChangesetComment) \
183 187 .filter(ChangesetComment.pull_request == pull_request) \
@@ -185,6 +189,9 b' class CommentsModel(BaseModel):'
185 189 .filter(ChangesetComment.comment_type
186 190 == ChangesetComment.COMMENT_TYPE_TODO)
187 191
192 if not include_drafts:
193 todos = todos.filter(ChangesetComment.draft == false())
194
188 195 if not show_outdated:
189 196 todos = todos.filter(
190 197 coalesce(ChangesetComment.display_state, '') !=
@@ -194,7 +201,14 b' class CommentsModel(BaseModel):'
194 201
195 202 return todos
196 203
197 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
204 def get_pull_request_drafts(self, user_id, pull_request):
205 drafts = Session().query(ChangesetComment) \
206 .filter(ChangesetComment.pull_request == pull_request) \
207 .filter(ChangesetComment.user_id == user_id) \
208 .filter(ChangesetComment.draft == true())
209 return drafts.all()
210
211 def get_commit_unresolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
198 212
199 213 todos = Session().query(ChangesetComment) \
200 214 .filter(ChangesetComment.revision == commit_id) \
@@ -202,6 +216,9 b' class CommentsModel(BaseModel):'
202 216 .filter(ChangesetComment.comment_type
203 217 == ChangesetComment.COMMENT_TYPE_TODO)
204 218
219 if not include_drafts:
220 todos = todos.filter(ChangesetComment.draft == false())
221
205 222 if not show_outdated:
206 223 todos = todos.filter(
207 224 coalesce(ChangesetComment.display_state, '') !=
@@ -211,7 +228,7 b' class CommentsModel(BaseModel):'
211 228
212 229 return todos
213 230
214 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
231 def get_commit_resolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
215 232
216 233 todos = Session().query(ChangesetComment) \
217 234 .filter(ChangesetComment.revision == commit_id) \
@@ -219,6 +236,9 b' class CommentsModel(BaseModel):'
219 236 .filter(ChangesetComment.comment_type
220 237 == ChangesetComment.COMMENT_TYPE_TODO)
221 238
239 if not include_drafts:
240 todos = todos.filter(ChangesetComment.draft == false())
241
222 242 if not show_outdated:
223 243 todos = todos.filter(
224 244 coalesce(ChangesetComment.display_state, '') !=
@@ -228,11 +248,15 b' class CommentsModel(BaseModel):'
228 248
229 249 return todos
230 250
231 def get_commit_inline_comments(self, commit_id):
251 def get_commit_inline_comments(self, commit_id, include_drafts=True):
232 252 inline_comments = Session().query(ChangesetComment) \
233 253 .filter(ChangesetComment.line_no != None) \
234 254 .filter(ChangesetComment.f_path != None) \
235 255 .filter(ChangesetComment.revision == commit_id)
256
257 if not include_drafts:
258 inline_comments = inline_comments.filter(ChangesetComment.draft == false())
259
236 260 inline_comments = inline_comments.all()
237 261 return inline_comments
238 262
@@ -245,7 +269,7 b' class CommentsModel(BaseModel):'
245 269
246 270 def create(self, text, repo, user, commit_id=None, pull_request=None,
247 271 f_path=None, line_no=None, status_change=None,
248 status_change_type=None, comment_type=None,
272 status_change_type=None, comment_type=None, is_draft=False,
249 273 resolves_comment_id=None, closing_pr=False, send_email=True,
250 274 renderer=None, auth_user=None, extra_recipients=None):
251 275 """
@@ -262,6 +286,7 b' class CommentsModel(BaseModel):'
262 286 :param line_no:
263 287 :param status_change: Label for status change
264 288 :param comment_type: Type of comment
289 :param is_draft: is comment a draft only
265 290 :param resolves_comment_id: id of comment which this one will resolve
266 291 :param status_change_type: type of status change
267 292 :param closing_pr:
@@ -288,6 +313,7 b' class CommentsModel(BaseModel):'
288 313 validated_kwargs = schema.deserialize(dict(
289 314 comment_body=text,
290 315 comment_type=comment_type,
316 is_draft=is_draft,
291 317 comment_file=f_path,
292 318 comment_line=line_no,
293 319 renderer_type=renderer,
@@ -296,6 +322,7 b' class CommentsModel(BaseModel):'
296 322 repo=repo.repo_id,
297 323 user=user.user_id,
298 324 ))
325 is_draft = validated_kwargs['is_draft']
299 326
300 327 comment = ChangesetComment()
301 328 comment.renderer = validated_kwargs['renderer_type']
@@ -303,6 +330,7 b' class CommentsModel(BaseModel):'
303 330 comment.f_path = validated_kwargs['comment_file']
304 331 comment.line_no = validated_kwargs['comment_line']
305 332 comment.comment_type = validated_kwargs['comment_type']
333 comment.draft = is_draft
306 334
307 335 comment.repo = repo
308 336 comment.author = user
@@ -438,9 +466,6 b' class CommentsModel(BaseModel):'
438 466
439 467 if send_email:
440 468 recipients += [self._get_user(u) for u in (extra_recipients or [])]
441 # pre-generate the subject for notification itself
442 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
443 notification_type, **kwargs)
444 469
445 470 mention_recipients = set(
446 471 self._extract_mentions(text)).difference(recipients)
@@ -448,8 +473,8 b' class CommentsModel(BaseModel):'
448 473 # create notification objects, and emails
449 474 NotificationModel().create(
450 475 created_by=user,
451 notification_subject=subject,
452 notification_body=body_plaintext,
476 notification_subject='', # Filled in based on the notification_type
477 notification_body='', # Filled in based on the notification_type
453 478 notification_type=notification_type,
454 479 recipients=recipients,
455 480 mention_recipients=mention_recipients,
@@ -462,10 +487,11 b' class CommentsModel(BaseModel):'
462 487 else:
463 488 action = 'repo.commit.comment.create'
464 489
465 comment_data = comment.get_api_data()
490 if not is_draft:
491 comment_data = comment.get_api_data()
466 492
467 self._log_audit_action(
468 action, {'data': comment_data}, auth_user, comment)
493 self._log_audit_action(
494 action, {'data': comment_data}, auth_user, comment)
469 495
470 496 return comment
471 497
@@ -541,7 +567,8 b' class CommentsModel(BaseModel):'
541 567
542 568 return comment
543 569
544 def get_all_comments(self, repo_id, revision=None, pull_request=None, count_only=False):
570 def get_all_comments(self, repo_id, revision=None, pull_request=None,
571 include_drafts=True, count_only=False):
545 572 q = ChangesetComment.query()\
546 573 .filter(ChangesetComment.repo_id == repo_id)
547 574 if revision:
@@ -551,6 +578,8 b' class CommentsModel(BaseModel):'
551 578 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
552 579 else:
553 580 raise Exception('Please specify commit or pull_request')
581 if not include_drafts:
582 q = q.filter(ChangesetComment.draft == false())
554 583 q = q.order_by(ChangesetComment.created_on)
555 584 if count_only:
556 585 return q.count()
@@ -697,7 +726,8 b' class CommentsModel(BaseModel):'
697 726 path=comment.f_path, diff_line=diff_line)
698 727 except (diffs.LineNotInDiffException,
699 728 diffs.FileNotInDiffException):
700 comment.display_state = ChangesetComment.COMMENT_OUTDATED
729 if not comment.draft:
730 comment.display_state = ChangesetComment.COMMENT_OUTDATED
701 731 return
702 732
703 733 if old_context == new_context:
@@ -707,14 +737,15 b' class CommentsModel(BaseModel):'
707 737 new_diff_lines = new_diff_proc.find_context(
708 738 path=comment.f_path, context=old_context,
709 739 offset=self.DIFF_CONTEXT_BEFORE)
710 if not new_diff_lines:
740 if not new_diff_lines and not comment.draft:
711 741 comment.display_state = ChangesetComment.COMMENT_OUTDATED
712 742 else:
713 743 new_diff_line = self._choose_closest_diff_line(
714 744 diff_line, new_diff_lines)
715 745 comment.line_no = _diff_to_comment_line_number(new_diff_line)
716 746 else:
717 comment.display_state = ChangesetComment.COMMENT_OUTDATED
747 if not comment.draft:
748 comment.display_state = ChangesetComment.COMMENT_OUTDATED
718 749
719 750 def _should_relocate_diff_line(self, diff_line):
720 751 """
@@ -3767,6 +3767,7 b' class ChangesetComment(Base, BaseModel):'
3767 3767 renderer = Column('renderer', Unicode(64), nullable=True)
3768 3768 display_state = Column('display_state', Unicode(128), nullable=True)
3769 3769 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3770 draft = Column('draft', Boolean(), nullable=True, default=False)
3770 3771
3771 3772 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3772 3773 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
@@ -5057,8 +5058,14 b' class RepoReviewRule(Base, BaseModel):'
5057 5058 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5058 5059
5059 5060 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5060 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5061 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5061
5062 # Legacy fields, just for backward compat
5063 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5064 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5065
5066 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5067 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5068
5062 5069 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5063 5070
5064 5071 rule_users = relationship('RepoReviewRuleUser')
@@ -5094,6 +5101,22 b' class RepoReviewRule(Base, BaseModel):'
5094 5101 self._validate_pattern(value)
5095 5102 self._file_pattern = value or '*'
5096 5103
5104 @hybrid_property
5105 def forbid_pr_author_to_review(self):
5106 return self.pr_author == 'forbid_pr_author'
5107
5108 @hybrid_property
5109 def include_pr_author_to_review(self):
5110 return self.pr_author == 'include_pr_author'
5111
5112 @hybrid_property
5113 def forbid_commit_author_to_review(self):
5114 return self.commit_author == 'forbid_commit_author'
5115
5116 @hybrid_property
5117 def include_commit_author_to_review(self):
5118 return self.commit_author == 'include_commit_author'
5119
5097 5120 def matches(self, source_branch, target_branch, files_changed):
5098 5121 """
5099 5122 Check if this review rule matches a branch/files in a pull request
@@ -55,7 +55,7 b' class NotificationModel(BaseModel):'
55 55 ' of Notification got %s' % type(notification))
56 56
57 57 def create(
58 self, created_by, notification_subject, notification_body,
58 self, created_by, notification_subject='', notification_body='',
59 59 notification_type=Notification.TYPE_MESSAGE, recipients=None,
60 60 mention_recipients=None, with_email=True, email_kwargs=None):
61 61 """
@@ -64,11 +64,12 b' class NotificationModel(BaseModel):'
64 64
65 65 :param created_by: int, str or User instance. User who created this
66 66 notification
67 :param notification_subject: subject of notification itself
67 :param notification_subject: subject of notification itself,
68 it will be generated automatically from notification_type if not specified
68 69 :param notification_body: body of notification text
70 it will be generated automatically from notification_type if not specified
69 71 :param notification_type: type of notification, based on that we
70 72 pick templates
71
72 73 :param recipients: list of int, str or User objects, when None
73 74 is given send to all admins
74 75 :param mention_recipients: list of int, str or User objects,
@@ -82,14 +83,19 b' class NotificationModel(BaseModel):'
82 83 if recipients and not getattr(recipients, '__iter__', False):
83 84 raise Exception('recipients must be an iterable object')
84 85
86 if not (notification_subject and notification_body) and not notification_type:
87 raise ValueError('notification_subject, and notification_body '
88 'cannot be empty when notification_type is not specified')
89
85 90 created_by_obj = self._get_user(created_by)
91
92 if not created_by_obj:
93 raise Exception('unknown user %s' % created_by)
94
86 95 # default MAIN body if not given
87 96 email_kwargs = email_kwargs or {'body': notification_body}
88 97 mention_recipients = mention_recipients or set()
89 98
90 if not created_by_obj:
91 raise Exception('unknown user %s' % created_by)
92
93 99 if recipients is None:
94 100 # recipients is None means to all admins
95 101 recipients_objs = User.query().filter(User.admin == true()).all()
@@ -113,6 +119,15 b' class NotificationModel(BaseModel):'
113 119 # add mentioned users into recipients
114 120 final_recipients = set(recipients_objs).union(mention_recipients)
115 121
122 (subject, email_body, email_body_plaintext) = \
123 EmailNotificationModel().render_email(notification_type, **email_kwargs)
124
125 if not notification_subject:
126 notification_subject = subject
127
128 if not notification_body:
129 notification_body = email_body_plaintext
130
116 131 notification = Notification.create(
117 132 created_by=created_by_obj, subject=notification_subject,
118 133 body=notification_body, recipients=final_recipients,
@@ -578,7 +578,7 b' class PermissionModel(BaseModel):'
578 578 return user_group_write_permissions
579 579
580 580 def trigger_permission_flush(self, affected_user_ids=None):
581 affected_user_ids or User.get_all_user_ids()
581 affected_user_ids = affected_user_ids or User.get_all_user_ids()
582 582 events.trigger(events.UserPermissionsChange(affected_user_ids))
583 583
584 584 def flush_user_permission_caches(self, changes, affected_user_ids=None):
@@ -1502,15 +1502,11 b' class PullRequestModel(BaseModel):'
1502 1502 'user_role': role
1503 1503 }
1504 1504
1505 # pre-generate the subject for notification itself
1506 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1507 notification_type, **kwargs)
1508
1509 1505 # create notification objects, and emails
1510 1506 NotificationModel().create(
1511 1507 created_by=current_rhodecode_user,
1512 notification_subject=subject,
1513 notification_body=body_plaintext,
1508 notification_subject='', # Filled in based on the notification_type
1509 notification_body='', # Filled in based on the notification_type
1514 1510 notification_type=notification_type,
1515 1511 recipients=recipients,
1516 1512 email_kwargs=kwargs,
@@ -1579,14 +1575,11 b' class PullRequestModel(BaseModel):'
1579 1575 'thread_ids': [pr_url],
1580 1576 }
1581 1577
1582 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1583 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1584
1585 1578 # create notification objects, and emails
1586 1579 NotificationModel().create(
1587 1580 created_by=updating_user,
1588 notification_subject=subject,
1589 notification_body=body_plaintext,
1581 notification_subject='', # Filled in based on the notification_type
1582 notification_body='', # Filled in based on the notification_type
1590 1583 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1591 1584 recipients=recipients,
1592 1585 email_kwargs=email_kwargs,
@@ -2067,6 +2060,8 b' class MergeCheck(object):'
2067 2060 self.error_details = OrderedDict()
2068 2061 self.source_commit = AttributeDict()
2069 2062 self.target_commit = AttributeDict()
2063 self.reviewers_count = 0
2064 self.observers_count = 0
2070 2065
2071 2066 def __repr__(self):
2072 2067 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
@@ -2128,11 +2123,12 b' class MergeCheck(object):'
2128 2123 # review status, must be always present
2129 2124 review_status = pull_request.calculated_review_status()
2130 2125 merge_check.review_status = review_status
2126 merge_check.reviewers_count = pull_request.reviewers_count
2127 merge_check.observers_count = pull_request.observers_count
2131 2128
2132 2129 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2133 if not status_approved:
2130 if not status_approved and merge_check.reviewers_count:
2134 2131 log.debug("MergeCheck: cannot merge, approval is pending.")
2135
2136 2132 msg = _('Pull request reviewer approval is pending.')
2137 2133
2138 2134 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
@@ -231,6 +231,10 b' class ScmModel(BaseModel):'
231 231 with_wire={"cache": False})
232 232 except OSError:
233 233 continue
234 except RepositoryError:
235 log.exception('Failed to create a repo')
236 continue
237
234 238 log.debug('found %s paths with repositories', len(repos))
235 239 return repos
236 240
@@ -425,15 +425,12 b' class UserModel(BaseModel):'
425 425 'date': datetime.datetime.now()
426 426 }
427 427 notification_type = EmailNotificationModel.TYPE_REGISTRATION
428 # pre-generate the subject for notification itself
429 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
430 notification_type, **kwargs)
431 428
432 429 # create notification objects, and emails
433 430 NotificationModel().create(
434 431 created_by=new_user,
435 notification_subject=subject,
436 notification_body=body_plaintext,
432 notification_subject='', # Filled in based on the notification_type
433 notification_body='', # Filled in based on the notification_type
437 434 notification_type=notification_type,
438 435 recipients=None, # all admins
439 436 email_kwargs=kwargs,
@@ -60,7 +60,7 b' class CommentSchema(colander.MappingSche'
60 60 colander.String(),
61 61 validator=colander.OneOf(ChangesetComment.COMMENT_TYPES),
62 62 missing=ChangesetComment.COMMENT_TYPE_NOTE)
63
63 is_draft = colander.SchemaNode(colander.Boolean(),missing=False)
64 64 comment_file = colander.SchemaNode(colander.String(), missing=None)
65 65 comment_line = colander.SchemaNode(colander.String(), missing=None)
66 66 status_change = colander.SchemaNode(
@@ -162,7 +162,6 b' input[type="button"] {'
162 162 }
163 163 }
164 164
165 .btn-warning,
166 165 .btn-danger,
167 166 .revoke_perm,
168 167 .btn-x,
@@ -196,6 +195,36 b' input[type="button"] {'
196 195 }
197 196 }
198 197
198 .btn-warning {
199 .border ( @border-thickness, @alert3 );
200 background-color: white;
201 color: @alert3;
202
203 a {
204 color: @alert3;
205 }
206
207 &:hover,
208 &.active {
209 .border ( @border-thickness, @alert3 );
210 color: white;
211 background-color: @alert3;
212
213 a {
214 color: white;
215 }
216 }
217
218 i {
219 display:none;
220 }
221
222 &:disabled {
223 background-color: white;
224 color: @alert3;
225 }
226 }
227
199 228 .btn-approved-status {
200 229 .border ( @border-thickness, @alert1 );
201 230 background-color: white;
@@ -264,7 +293,6 b' input[type="button"] {'
264 293 margin-left: -1px;
265 294 padding-left: 2px;
266 295 padding-right: 2px;
267 border-left: 1px solid @grey3;
268 296 }
269 297 }
270 298
@@ -342,7 +370,7 b' input[type="button"] {'
342 370 color: @alert2;
343 371
344 372 &:hover {
345 color: darken(@alert2,30%);
373 color: darken(@alert2, 30%);
346 374 }
347 375
348 376 &:disabled {
@@ -402,6 +430,37 b' input[type="button"] {'
402 430 }
403 431
404 432
433 input[type="submit"].btn-warning {
434 &:extend(.btn-warning);
435
436 &:focus {
437 outline: 0;
438 }
439
440 &:hover {
441 &:extend(.btn-warning:hover);
442 }
443
444 &.btn-link {
445 &:extend(.btn-link);
446 color: @alert3;
447
448 &:disabled {
449 color: @alert3;
450 background-color: transparent;
451 }
452 }
453
454 &:disabled {
455 .border ( @border-thickness-buttons, @alert3 );
456 background-color: white;
457 color: @alert3;
458 opacity: 0.5;
459 }
460 }
461
462
463
405 464 // TODO: johbo: Form button tweaks, check if we can use the classes instead
406 465 input[type="submit"] {
407 466 &:extend(.btn-primary);
@@ -1002,7 +1002,7 b' input.filediff-collapse-state {'
1002 1002 .nav-chunk {
1003 1003 position: absolute;
1004 1004 right: 20px;
1005 margin-top: -17px;
1005 margin-top: -15px;
1006 1006 }
1007 1007
1008 1008 .nav-chunk.selected {
@@ -4,7 +4,7 b''
4 4
5 5
6 6 // Comments
7 @comment-outdated-opacity: 0.6;
7 @comment-outdated-opacity: 1.0;
8 8
9 9 .comments {
10 10 width: 100%;
@@ -61,28 +61,37 b' tr.inline-comments div {'
61 61 visibility: hidden;
62 62 }
63 63
64 .comment-draft {
65 float: left;
66 margin-right: 10px;
67 font-weight: 400;
68 color: @color-draft;
69 }
70
71 .comment-new {
72 float: left;
73 margin-right: 10px;
74 font-weight: 400;
75 color: @color-new;
76 }
77
64 78 .comment-label {
65 79 float: left;
66 80
67 padding: 0.4em 0.4em;
68 margin: 2px 4px 0px 0px;
69 display: inline-block;
81 padding: 0 8px 0 0;
70 82 min-height: 0;
71 83
72 84 text-align: center;
73 85 font-size: 10px;
74 line-height: .8em;
75 86
76 87 font-family: @text-italic;
77 88 font-style: italic;
78 89 background: #fff none;
79 90 color: @grey3;
80 border: 1px solid @grey4;
81 91 white-space: nowrap;
82 92
83 93 text-transform: uppercase;
84 94 min-width: 50px;
85 border-radius: 4px;
86 95
87 96 &.todo {
88 97 color: @color5;
@@ -270,64 +279,165 b' tr.inline-comments div {'
270 279 .comment-outdated {
271 280 opacity: @comment-outdated-opacity;
272 281 }
282
283 .comment-outdated-label {
284 color: @grey3;
285 padding-right: 4px;
286 }
273 287 }
274 288
275 289 .inline-comments {
276 border-radius: @border-radius;
290
277 291 .comment {
278 292 margin: 0;
279 border-radius: @border-radius;
280 293 }
294
281 295 .comment-outdated {
282 296 opacity: @comment-outdated-opacity;
283 297 }
284 298
299 .comment-outdated-label {
300 color: @grey3;
301 padding-right: 4px;
302 }
303
285 304 .comment-inline {
305
306 &:first-child {
307 margin: 4px 4px 0 4px;
308 border-top: 1px solid @grey5;
309 border-bottom: 0 solid @grey5;
310 border-left: 1px solid @grey5;
311 border-right: 1px solid @grey5;
312 .border-radius-top(4px);
313 }
314
315 &:only-child {
316 margin: 4px 4px 0 4px;
317 border-top: 1px solid @grey5;
318 border-bottom: 0 solid @grey5;
319 border-left: 1px solid @grey5;
320 border-right: 1px solid @grey5;
321 .border-radius-top(4px);
322 }
323
286 324 background: white;
287 325 padding: @comment-padding @comment-padding;
288 border: @comment-padding solid @grey6;
326 margin: 0 4px 0 4px;
327 border-top: 0 solid @grey5;
328 border-bottom: 0 solid @grey5;
329 border-left: 1px solid @grey5;
330 border-right: 1px solid @grey5;
289 331
290 332 .text {
291 333 border: none;
292 334 }
335
293 336 .meta {
294 337 border-bottom: 1px solid @grey6;
295 338 margin: -5px 0px;
296 339 line-height: 24px;
297 340 }
341
298 342 }
299 343 .comment-selected {
300 344 border-left: 6px solid @comment-highlight-color;
301 345 }
346
347 .comment-inline-form-open {
348 display: block !important;
349 }
350
302 351 .comment-inline-form {
303 padding: @comment-padding;
304 352 display: none;
305 353 }
306 .cb-comment-add-button {
307 margin: @comment-padding;
354
355 .comment-inline-form-edit {
356 padding: 0;
357 margin: 0px 4px 2px 4px;
358 }
359
360 .reply-thread-container {
361 display: table;
362 width: 100%;
363 padding: 0px 4px 4px 4px;
364 }
365
366 .reply-thread-container-wrapper {
367 margin: 0 4px 4px 4px;
368 border-top: 0 solid @grey5;
369 border-bottom: 1px solid @grey5;
370 border-left: 1px solid @grey5;
371 border-right: 1px solid @grey5;
372 .border-radius-bottom(4px);
373 }
374
375 .reply-thread-gravatar {
376 display: table-cell;
377 width: 24px;
378 height: 24px;
379 padding-top: 10px;
380 padding-left: 10px;
381 background-color: #eeeeee;
382 vertical-align: top;
308 383 }
309 /* hide add comment button when form is open */
384
385 .reply-thread-reply-button {
386 display: table-cell;
387 width: 100%;
388 height: 33px;
389 padding: 3px 8px;
390 margin-left: 8px;
391 background-color: #eeeeee;
392 }
393
394 .reply-thread-reply-button .cb-comment-add-button {
395 border-radius: 4px;
396 width: 100%;
397 padding: 6px 2px;
398 text-align: left;
399 cursor: text;
400 color: @grey3;
401 }
402 .reply-thread-reply-button .cb-comment-add-button:hover {
403 background-color: white;
404 color: @grey2;
405 }
406
407 .reply-thread-last {
408 display: table-cell;
409 width: 10px;
410 }
411
412 /* Hide reply box when it's a first element,
413 can happen when drafts are saved but not shown to specific user,
414 or there are outdated comments hidden
415 */
416 .reply-thread-container-wrapper:first-child:not(.comment-form-active) {
417 display: none;
418 }
419
420 .reply-thread-container-wrapper.comment-outdated {
421 display: none
422 }
423
424 /* hide add comment button when form is open */
310 425 .comment-inline-form-open ~ .cb-comment-add-button {
311 426 display: none;
312 427 }
313 .comment-inline-form-open {
314 display: block;
315 }
316 /* hide add comment button when form but no comments */
317 .comment-inline-form:first-child + .cb-comment-add-button {
318 display: none;
319 }
320 /* hide add comment button when no comments or form */
321 .cb-comment-add-button:first-child {
322 display: none;
323 }
428
324 429 /* hide add comment button when only comment is being deleted */
325 430 .comment-deleting:first-child + .cb-comment-add-button {
326 431 display: none;
327 432 }
433
434 /* hide add comment button when form but no comments */
435 .comment-inline-form:first-child + .cb-comment-add-button {
436 display: none;
437 }
438
328 439 }
329 440
330
331 441 .show-outdated-comments {
332 442 display: inline;
333 443 color: @rcblue;
@@ -380,23 +490,36 b' form.comment-form {'
380 490 }
381 491
382 492 .comment-footer {
383 position: relative;
493 display: table;
384 494 width: 100%;
385 min-height: 42px;
495 height: 42px;
386 496
387 .status_box,
497 .comment-status-box,
388 498 .cancel-button {
389 float: left;
390 499 display: inline-block;
391 500 }
392 501
393 .status_box {
502 .comment-status-box {
394 503 margin-left: 10px;
395 504 }
396 505
397 506 .action-buttons {
398 float: left;
399 display: inline-block;
507 display: table-cell;
508 padding: 5px 0 5px 2px;
509 }
510
511 .toolbar-text {
512 height: 28px;
513 display: table-cell;
514 vertical-align: baseline;
515 font-size: 11px;
516 color: @grey4;
517 text-align: right;
518
519 a {
520 color: @grey4;
521 }
522
400 523 }
401 524
402 525 .action-buttons-extra {
@@ -427,10 +550,10 b' form.comment-form {'
427 550 margin-right: 0;
428 551 }
429 552
430 .comment-footer {
431 margin-bottom: 50px;
432 margin-top: 10px;
553 #save_general {
554 margin-left: -6px;
433 555 }
556
434 557 }
435 558
436 559
@@ -482,8 +605,8 b' form.comment-form {'
482 605 .injected_diff .comment-inline-form,
483 606 .comment-inline-form {
484 607 background-color: white;
485 margin-top: 10px;
486 margin-bottom: 20px;
608 margin-top: 4px;
609 margin-bottom: 10px;
487 610 }
488 611
489 612 .inline-form {
@@ -519,9 +642,6 b' form.comment-form {'
519 642 margin: 0px;
520 643 }
521 644
522 .comment-inline-form .comment-footer {
523 margin: 10px 0px 0px 0px;
524 }
525 645
526 646 .hide-inline-form-button {
527 647 margin-left: 5px;
@@ -547,6 +667,7 b' comment-area-text {'
547 667
548 668 .comment-area-header {
549 669 height: 35px;
670 border-bottom: 1px solid @grey5;
550 671 }
551 672
552 673 .comment-area-header .nav-links {
@@ -554,6 +675,7 b' comment-area-text {'
554 675 flex-flow: row wrap;
555 676 -webkit-flex-flow: row wrap;
556 677 width: 100%;
678 border: none;
557 679 }
558 680
559 681 .comment-area-footer {
@@ -622,14 +744,3 b' comment-area-text {'
622 744 border-bottom: 2px solid transparent;
623 745 }
624 746
625 .toolbar-text {
626 float: right;
627 font-size: 11px;
628 color: @grey4;
629 text-align: right;
630
631 a {
632 color: @grey4;
633 }
634 }
635
@@ -213,7 +213,6 b' div.markdown-block pre {'
213 213 div.markdown-block img {
214 214 border-style: none;
215 215 background-color: #fff;
216 padding-right: 20px;
217 216 max-width: 100%;
218 217 }
219 218
@@ -274,6 +273,13 b' div.markdown-block #ws {'
274 273 background-color: @grey6;
275 274 }
276 275
276 div.markdown-block p {
277 margin-top: 0;
278 margin-bottom: 16px;
279 padding: 0;
280 line-height: unset;
281 }
282
277 283 div.markdown-block code,
278 284 div.markdown-block pre,
279 285 div.markdown-block #ws,
@@ -2,7 +2,7 b''
2 2
3 3
4 4 .loginbox {
5 max-width: 65%;
5 max-width: 960px;
6 6 margin: @pagepadding auto;
7 7 font-family: @text-light;
8 8 border: @border-thickness solid @grey5;
@@ -22,13 +22,27 b''
22 22 float: none;
23 23 }
24 24
25 .header {
25 .header-account {
26 min-height: 49px;
26 27 width: 100%;
27 padding: 0 35px;
28 padding: 0 @header-padding;
28 29 box-sizing: border-box;
30 position: relative;
31 vertical-align: bottom;
32
33 background-color: @grey1;
34 color: @grey5;
29 35
30 36 .title {
31 37 padding: 0;
38 overflow: visible;
39 }
40
41 &:before,
42 &:after {
43 content: "";
44 clear: both;
45 width: 100%;
32 46 }
33 47 }
34 48
@@ -69,7 +83,7 b''
69 83 .sign-in-image {
70 84 display: block;
71 85 width: 65%;
72 margin: 5% auto;
86 margin: 1% auto;
73 87 }
74 88
75 89 .sign-in-title {
@@ -263,9 +263,6 b' input.inline[type="file"] {'
263 263 // HEADER
264 264 .header {
265 265
266 // TODO: johbo: Fix login pages, so that they work without a min-height
267 // for the header and then remove the min-height. I chose a smaller value
268 // intentionally here to avoid rendering issues in the main navigation.
269 266 min-height: 49px;
270 267 min-width: 1024px;
271 268
@@ -1143,9 +1140,8 b' label {'
1143 1140 margin-left: -15px;
1144 1141 }
1145 1142
1146 #rev_range_container, #rev_range_clear, #rev_range_more {
1147 margin-top: -5px;
1148 margin-bottom: -5px;
1143 #rev_range_action {
1144 margin-bottom: -8px;
1149 1145 }
1150 1146
1151 1147 #filter_changelog {
@@ -1591,9 +1587,9 b' table.integrations {'
1591 1587 }
1592 1588 .pr-details-title {
1593 1589 height: 20px;
1594 line-height: 20px;
1595
1596 padding-bottom: 8px;
1590 line-height: 16px;
1591
1592 padding-bottom: 4px;
1597 1593 border-bottom: @border-thickness solid @grey5;
1598 1594
1599 1595 .action_button.disabled {
@@ -3212,7 +3208,12 b' details:not([open]) > :not(summary) {'
3212 3208
3213 3209 .sidebar-element {
3214 3210 margin-top: 20px;
3215 }
3211
3212 .icon-draft {
3213 color: @color-draft
3214 }
3215 }
3216
3216 3217
3217 3218 .right-sidebar-collapsed-state {
3218 3219 display: flex;
@@ -3235,5 +3236,4 b' details:not([open]) > :not(summary) {'
3235 3236
3236 3237 .old-comments-marker td {
3237 3238 padding-top: 15px;
3238 border-bottom: 1px solid @grey5;
3239 }
3239 }
@@ -115,11 +115,9 b' div.readme_box pre {'
115 115 div.readme_box img {
116 116 border-style: none;
117 117 background-color: #fff;
118 padding-right: 20px;
119 118 max-width: 100%;
120 119 }
121 120
122
123 121 div.readme_box strong {
124 122 font-weight: 600;
125 123 margin: 0;
@@ -152,6 +150,13 b' div.readme_box a:visited {'
152 150 }
153 151 */
154 152
153 div.readme_box p {
154 margin-top: 0;
155 margin-bottom: 16px;
156 padding: 0;
157 line-height: unset;
158 }
159
155 160
156 161 div.readme_box button {
157 162 font-size: @basefontsize;
@@ -47,6 +47,8 b''
47 47
48 48 // Highlight color for lines and colors
49 49 @comment-highlight-color: #ffd887;
50 @color-draft: darken(@alert3, 30%);
51 @color-new: darken(@alert1, 5%);
50 52
51 53 // FONTS
52 54 @basefontsize: 13px;
@@ -248,6 +248,7 b' function registerRCRoutes() {'
248 248 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']);
249 249 pyroutes.register('pullrequest_comments', '/%(repo_name)s/pull-request/%(pull_request_id)s/comments', ['repo_name', 'pull_request_id']);
250 250 pyroutes.register('pullrequest_todos', '/%(repo_name)s/pull-request/%(pull_request_id)s/todos', ['repo_name', 'pull_request_id']);
251 pyroutes.register('pullrequest_drafts', '/%(repo_name)s/pull-request/%(pull_request_id)s/drafts', ['repo_name', 'pull_request_id']);
251 252 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
252 253 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
253 254 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
@@ -386,6 +387,8 b' function registerRCRoutes() {'
386 387 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
387 388 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
388 389 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
390 pyroutes.register('pullrequest_draft_comments_submit', '/%(repo_name)s/pull-request/%(pull_request_id)s/draft_comments_submit', ['repo_name', 'pull_request_id']);
391 pyroutes.register('commit_draft_comments_submit', '/%(repo_name)s/changeset/%(commit_id)s/draft_comments_submit', ['repo_name', 'commit_id']);
389 392 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
390 393 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
391 394 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
@@ -71,14 +71,20 b' export class RhodecodeApp extends Polyme'
71 71 if (elem) {
72 72 elem.handleNotification(data);
73 73 }
74
75 74 }
76 75
77 76 handleComment(data) {
78 if (data.message.comment_id) {
77
78 if (data.message.comment_data.length !== 0) {
79 79 if (window.refreshAllComments !== undefined) {
80 80 refreshAllComments()
81 81 }
82 var json_data = data.message.comment_data;
83
84 if (window.commentsController !== undefined) {
85
86 window.commentsController.attachComment(json_data)
87 }
82 88 }
83 89 }
84 90
@@ -704,3 +704,13 b' var storeUserSessionAttr = function (key'
704 704 ajaxPOST(pyroutes.url('store_user_session_value'), postData, success);
705 705 return false;
706 706 };
707
708
709 var getUserSessionAttr = function(key) {
710 var storeKey = templateContext.session_attrs;
711 var val = storeKey[key]
712 if (val !== undefined) {
713 return JSON.parse(val)
714 }
715 return null
716 }
@@ -349,7 +349,12 b' var initCommentBoxCodeMirror = function('
349 349 };
350 350
351 351 var submitForm = function(cm, pred) {
352 $(cm.display.input.textarea.form).submit();
352 $(cm.display.input.textarea.form).find('.submit-comment-action').click();
353 return CodeMirror.Pass;
354 };
355
356 var submitFormAsDraft = function(cm, pred) {
357 $(cm.display.input.textarea.form).find('.submit-draft-action').click();
353 358 return CodeMirror.Pass;
354 359 };
355 360
@@ -475,9 +480,11 b' var initCommentBoxCodeMirror = function('
475 480 // submit form on Meta-Enter
476 481 if (OSType === "mac") {
477 482 extraKeys["Cmd-Enter"] = submitForm;
483 extraKeys["Shift-Cmd-Enter"] = submitFormAsDraft;
478 484 }
479 485 else {
480 486 extraKeys["Ctrl-Enter"] = submitForm;
487 extraKeys["Shift-Ctrl-Enter"] = submitFormAsDraft;
481 488 }
482 489
483 490 if (triggerActions) {
@@ -124,16 +124,20 b' var _submitAjaxPOST = function(url, post'
124 124 this.statusChange = this.withLineNo('#change_status');
125 125
126 126 this.submitForm = formElement;
127 this.submitButton = $(this.submitForm).find('input[type="submit"]');
127
128 this.submitButton = $(this.submitForm).find('.submit-comment-action');
128 129 this.submitButtonText = this.submitButton.val();
129 130
131 this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
132 this.submitDraftButtonText = this.submitDraftButton.val();
130 133
131 134 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
132 135 {'repo_name': templateContext.repo_name,
133 136 'commit_id': templateContext.commit_data.commit_id});
134 137
135 138 if (edit){
136 this.submitButtonText = _gettext('Updated Comment');
139 this.submitDraftButton.hide();
140 this.submitButtonText = _gettext('Update Comment');
137 141 $(this.commentType).prop('disabled', true);
138 142 $(this.commentType).addClass('disabled');
139 143 var editInfo =
@@ -215,10 +219,17 b' var _submitAjaxPOST = function(url, post'
215 219 this.getCommentStatus = function() {
216 220 return $(this.submitForm).find(this.statusChange).val();
217 221 };
222
218 223 this.getCommentType = function() {
219 224 return $(this.submitForm).find(this.commentType).val();
220 225 };
221 226
227 this.getDraftState = function () {
228 var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
229 var data = $(submitterElem).data('isDraft');
230 return data
231 }
232
222 233 this.getResolvesId = function() {
223 234 return $(this.submitForm).find(this.resolvesId).val() || null;
224 235 };
@@ -233,7 +244,9 b' var _submitAjaxPOST = function(url, post'
233 244 };
234 245
235 246 this.isAllowedToSubmit = function() {
236 return !$(this.submitButton).prop('disabled');
247 var commentDisabled = $(this.submitButton).prop('disabled');
248 var draftDisabled = $(this.submitDraftButton).prop('disabled');
249 return !commentDisabled && !draftDisabled;
237 250 };
238 251
239 252 this.initStatusChangeSelector = function(){
@@ -259,11 +272,13 b' var _submitAjaxPOST = function(url, post'
259 272 dropdownAutoWidth: true,
260 273 minimumResultsForSearch: -1
261 274 });
275
262 276 $(this.submitForm).find(this.statusChange).on('change', function() {
263 277 var status = self.getCommentStatus();
264 278
265 279 if (status && !self.isInline()) {
266 280 $(self.submitButton).prop('disabled', false);
281 $(self.submitDraftButton).prop('disabled', false);
267 282 }
268 283
269 284 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
@@ -295,10 +310,10 b' var _submitAjaxPOST = function(url, post'
295 310 $(this.statusChange).select2('readonly', false);
296 311 };
297 312
298 this.globalSubmitSuccessCallback = function(){
313 this.globalSubmitSuccessCallback = function(comment){
299 314 // default behaviour is to call GLOBAL hook, if it's registered.
300 315 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
301 commentFormGlobalSubmitSuccessCallback();
316 commentFormGlobalSubmitSuccessCallback(comment);
302 317 }
303 318 };
304 319
@@ -321,6 +336,7 b' var _submitAjaxPOST = function(url, post'
321 336 var text = self.cm.getValue();
322 337 var status = self.getCommentStatus();
323 338 var commentType = self.getCommentType();
339 var isDraft = self.getDraftState();
324 340 var resolvesCommentId = self.getResolvesId();
325 341 var closePullRequest = self.getClosePr();
326 342
@@ -348,12 +364,15 b' var _submitAjaxPOST = function(url, post'
348 364 postData['close_pull_request'] = true;
349 365 }
350 366
351 var submitSuccessCallback = function(o) {
367 // submitSuccess for general comments
368 var submitSuccessCallback = function(json_data) {
352 369 // reload page if we change status for single commit.
353 370 if (status && self.commitId) {
354 371 location.reload(true);
355 372 } else {
356 $('#injected_page_comments').append(o.rendered_text);
373 // inject newly created comments, json_data is {<comment_id>: {}}
374 self.attachGeneralComment(json_data)
375
357 376 self.resetCommentFormState();
358 377 timeagoActivate();
359 378 tooltipActivate();
@@ -365,7 +384,7 b' var _submitAjaxPOST = function(url, post'
365 384 }
366 385
367 386 // run global callback on submit
368 self.globalSubmitSuccessCallback();
387 self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
369 388
370 389 };
371 390 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
@@ -409,10 +428,20 b' var _submitAjaxPOST = function(url, post'
409 428 }
410 429
411 430 $(this.submitButton).prop('disabled', submitState);
431 $(this.submitDraftButton).prop('disabled', submitState);
432
412 433 if (submitEvent) {
413 $(this.submitButton).val(_gettext('Submitting...'));
434 var isDraft = self.getDraftState();
435
436 if (isDraft) {
437 $(this.submitDraftButton).val(_gettext('Saving Draft...'));
438 } else {
439 $(this.submitButton).val(_gettext('Submitting...'));
440 }
441
414 442 } else {
415 443 $(this.submitButton).val(this.submitButtonText);
444 $(this.submitDraftButton).val(this.submitDraftButtonText);
416 445 }
417 446
418 447 };
@@ -488,6 +517,7 b' var _submitAjaxPOST = function(url, post'
488 517 if (!allowedToSubmit){
489 518 return false;
490 519 }
520
491 521 self.handleFormSubmit();
492 522 });
493 523
@@ -538,26 +568,6 b' var CommentsController = function() {'
538 568 var mainComment = '#text';
539 569 var self = this;
540 570
541 this.cancelComment = function (node) {
542 var $node = $(node);
543 var edit = $(this).attr('edit');
544 if (edit) {
545 var $general_comments = null;
546 var $inline_comments = $node.closest('div.inline-comments');
547 if (!$inline_comments.length) {
548 $general_comments = $('#comments');
549 var $comment = $general_comments.parent().find('div.comment:hidden');
550 // show hidden general comment form
551 $('#cb-comment-general-form-placeholder').show();
552 } else {
553 var $comment = $inline_comments.find('div.comment:hidden');
554 }
555 $comment.show();
556 }
557 $node.closest('.comment-inline-form').remove();
558 return false;
559 };
560
561 571 this.showVersion = function (comment_id, comment_history_id) {
562 572
563 573 var historyViewUrl = pyroutes.url(
@@ -655,12 +665,51 b' var CommentsController = function() {'
655 665 return self.scrollToComment(node, -1, true);
656 666 };
657 667
668 this.cancelComment = function (node) {
669 var $node = $(node);
670 var edit = $(this).attr('edit');
671 var $inlineComments = $node.closest('div.inline-comments');
672
673 if (edit) {
674 var $general_comments = null;
675 if (!$inlineComments.length) {
676 $general_comments = $('#comments');
677 var $comment = $general_comments.parent().find('div.comment:hidden');
678 // show hidden general comment form
679 $('#cb-comment-general-form-placeholder').show();
680 } else {
681 var $comment = $inlineComments.find('div.comment:hidden');
682 }
683 $comment.show();
684 }
685 var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper')
686 $replyWrapper.removeClass('comment-form-active');
687
688 var lastComment = $inlineComments.find('.comment-inline').last();
689 if ($(lastComment).hasClass('comment-outdated')) {
690 $replyWrapper.hide();
691 }
692
693 $node.closest('.comment-inline-form').remove();
694 return false;
695 };
696
658 697 this._deleteComment = function(node) {
659 698 var $node = $(node);
660 699 var $td = $node.closest('td');
661 700 var $comment = $node.closest('.comment');
662 var comment_id = $comment.attr('data-comment-id');
663 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
701 var comment_id = $($comment).data('commentId');
702 var isDraft = $($comment).data('commentDraft');
703
704 var pullRequestId = templateContext.pull_request_data.pull_request_id;
705 var commitId = templateContext.commit_data.commit_id;
706
707 if (pullRequestId) {
708 var url = pyroutes.url('pullrequest_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
709 } else if (commitId) {
710 var url = pyroutes.url('repo_commit_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "commit_id": commitId})
711 }
712
664 713 var postData = {
665 714 'csrf_token': CSRF_TOKEN
666 715 };
@@ -677,10 +726,14 b' var CommentsController = function() {'
677 726 updateSticky()
678 727 }
679 728
680 if (window.refreshAllComments !== undefined) {
729 if (window.refreshAllComments !== undefined && !isDraft) {
681 730 // if we have this handler, run it, and refresh all comments boxes
682 731 refreshAllComments()
683 732 }
733 else if (window.refreshDraftComments !== undefined && isDraft) {
734 // if we have this handler, run it, and refresh all comments boxes
735 refreshDraftComments();
736 }
684 737 return false;
685 738 };
686 739
@@ -695,8 +748,6 b' var CommentsController = function() {'
695 748 };
696 749 ajaxPOST(url, postData, success, failure);
697 750
698
699
700 751 }
701 752
702 753 this.deleteComment = function(node) {
@@ -716,7 +767,59 b' var CommentsController = function() {'
716 767 })
717 768 };
718 769
770 this._finalizeDrafts = function(commentIds) {
771
772 var pullRequestId = templateContext.pull_request_data.pull_request_id;
773 var commitId = templateContext.commit_data.commit_id;
774
775 if (pullRequestId) {
776 var url = pyroutes.url('pullrequest_draft_comments_submit', {"repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
777 } else if (commitId) {
778 var url = pyroutes.url('commit_draft_comments_submit', {"repo_name": templateContext.repo_name, "commit_id": commitId})
779 }
780
781 // remove the drafts so we can lock them before submit.
782 $.each(commentIds, function(idx, val){
783 $('#comment-{0}'.format(val)).remove();
784 })
785
786 var postData = {'comments': commentIds, 'csrf_token': CSRF_TOKEN};
787
788 var submitSuccessCallback = function(json_data) {
789 self.attachInlineComment(json_data);
790
791 if (window.refreshDraftComments !== undefined) {
792 // if we have this handler, run it, and refresh all comments boxes
793 refreshDraftComments()
794 }
795
796 return false;
797 };
798
799 ajaxPOST(url, postData, submitSuccessCallback)
800
801 }
802
803 this.finalizeDrafts = function(commentIds, callback) {
804
805 SwalNoAnimation.fire({
806 title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
807 icon: 'warning',
808 showCancelButton: true,
809 confirmButtonText: _gettext('Yes'),
810
811 }).then(function(result) {
812 if (result.value) {
813 if (callback !== undefined) {
814 callback(result)
815 }
816 self._finalizeDrafts(commentIds);
817 }
818 })
819 };
820
719 821 this.toggleWideMode = function (node) {
822
720 823 if ($('#content').hasClass('wrapper')) {
721 824 $('#content').removeClass("wrapper");
722 825 $('#content').addClass("wide-mode-wrapper");
@@ -731,16 +834,49 b' var CommentsController = function() {'
731 834
732 835 };
733 836
734 this.toggleComments = function(node, show) {
837 /**
838 * Turn off/on all comments in file diff
839 */
840 this.toggleDiffComments = function(node) {
841 // Find closes filediff container
735 842 var $filediff = $(node).closest('.filediff');
843 if ($(node).hasClass('toggle-on')) {
844 var show = false;
845 } else if ($(node).hasClass('toggle-off')) {
846 var show = true;
847 }
848
849 // Toggle each individual comment block, so we can un-toggle single ones
850 $.each($filediff.find('.toggle-comment-action'), function(idx, val) {
851 self.toggleLineComments($(val), show)
852 })
853
854 // since we change the height of the diff container that has anchor points for upper
855 // sticky header, we need to tell it to re-calculate those
856 if (window.updateSticky !== undefined) {
857 // potentially our comments change the active window size, so we
858 // notify sticky elements
859 updateSticky()
860 }
861
862 return false;
863 }
864
865 this.toggleLineComments = function(node, show) {
866
867 var trElem = $(node).closest('tr')
868
736 869 if (show === true) {
737 $filediff.removeClass('hide-comments');
870 // mark outdated comments as visible before the toggle;
871 $(trElem).find('.comment-outdated').show();
872 $(trElem).removeClass('hide-line-comments');
738 873 } else if (show === false) {
739 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
740 $filediff.addClass('hide-comments');
874 $(trElem).find('.comment-outdated').hide();
875 $(trElem).addClass('hide-line-comments');
741 876 } else {
742 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
743 $filediff.toggleClass('hide-comments');
877 // mark outdated comments as visible before the toggle;
878 $(trElem).find('.comment-outdated').show();
879 $(trElem).toggleClass('hide-line-comments');
744 880 }
745 881
746 882 // since we change the height of the diff container that has anchor points for upper
@@ -751,15 +887,6 b' var CommentsController = function() {'
751 887 updateSticky()
752 888 }
753 889
754 return false;
755 };
756
757 this.toggleLineComments = function(node) {
758 self.toggleComments(node, true);
759 var $node = $(node);
760 // mark outdated comments as visible before the toggle;
761 $(node.closest('tr')).find('.comment-outdated').show();
762 $node.closest('tr').toggleClass('hide-line-comments');
763 890 };
764 891
765 892 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
@@ -913,63 +1040,58 b' var CommentsController = function() {'
913 1040 return commentForm;
914 1041 };
915 1042
916 this.editComment = function(node) {
1043 this.editComment = function(node, line_no, f_path) {
1044 self.edit = true;
917 1045 var $node = $(node);
1046 var $td = $node.closest('td');
1047
918 1048 var $comment = $(node).closest('.comment');
919 var comment_id = $comment.attr('data-comment-id');
920 var $form = null
1049 var comment_id = $($comment).data('commentId');
1050 var isDraft = $($comment).data('commentDraft');
1051 var $editForm = null
921 1052
922 1053 var $comments = $node.closest('div.inline-comments');
923 1054 var $general_comments = null;
924 var lineno = null;
925 1055
926 1056 if($comments.length){
927 1057 // inline comments setup
928 $form = $comments.find('.comment-inline-form');
929 lineno = self.getLineNumber(node)
1058 $editForm = $comments.find('.comment-inline-form');
1059 line_no = self.getLineNumber(node)
930 1060 }
931 1061 else{
932 1062 // general comments setup
933 1063 $comments = $('#comments');
934 $form = $comments.find('.comment-inline-form');
935 lineno = $comment[0].id
1064 $editForm = $comments.find('.comment-inline-form');
1065 line_no = $comment[0].id
936 1066 $('#cb-comment-general-form-placeholder').hide();
937 1067 }
938 1068
939 this.edit = true;
1069 if ($editForm.length === 0) {
940 1070
941 if (!$form.length) {
942
1071 // unhide all comments if they are hidden for a proper REPLY mode
943 1072 var $filediff = $node.closest('.filediff');
944 1073 $filediff.removeClass('hide-comments');
945 var f_path = $filediff.attr('data-f-path');
946
947 // create a new HTML from template
948 1074
949 var tmpl = $('#cb-comment-inline-form-template').html();
950 tmpl = tmpl.format(escapeHtml(f_path), lineno);
951 $form = $(tmpl);
952 $comment.after($form)
1075 $editForm = self.createNewFormWrapper(f_path, line_no);
1076 if(f_path && line_no) {
1077 $editForm.addClass('comment-inline-form-edit')
1078 }
953 1079
954 var _form = $($form[0]).find('form');
1080 $comment.after($editForm)
1081
1082 var _form = $($editForm[0]).find('form');
955 1083 var autocompleteActions = ['as_note',];
956 1084 var commentForm = this.createCommentForm(
957 _form, lineno, '', autocompleteActions, resolvesCommentId,
1085 _form, line_no, '', autocompleteActions, resolvesCommentId,
958 1086 this.edit, comment_id);
959 1087 var old_comment_text_binary = $comment.attr('data-comment-text');
960 1088 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
961 1089 commentForm.cm.setValue(old_comment_text);
962 1090 $comment.hide();
1091 tooltipActivate();
963 1092
964 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
965 form: _form,
966 parent: $comments,
967 lineno: lineno,
968 f_path: f_path}
969 );
970
971 // set a CUSTOM submit handler for inline comments.
972 commentForm.setHandleFormSubmit(function(o) {
1093 // set a CUSTOM submit handler for inline comment edit action.
1094 commentForm.setHandleFormSubmit(function(o) {
973 1095 var text = commentForm.cm.getValue();
974 1096 var commentType = commentForm.getCommentType();
975 1097
@@ -1000,14 +1122,15 b' var CommentsController = function() {'
1000 1122 var postData = {
1001 1123 'text': text,
1002 1124 'f_path': f_path,
1003 'line': lineno,
1125 'line': line_no,
1004 1126 'comment_type': commentType,
1127 'draft': isDraft,
1005 1128 'version': version,
1006 1129 'csrf_token': CSRF_TOKEN
1007 1130 };
1008 1131
1009 1132 var submitSuccessCallback = function(json_data) {
1010 $form.remove();
1133 $editForm.remove();
1011 1134 $comment.show();
1012 1135 var postData = {
1013 1136 'text': text,
@@ -1072,8 +1195,7 b' var CommentsController = function() {'
1072 1195 'commit_id': templateContext.commit_data.commit_id});
1073 1196
1074 1197 _submitAjaxPOST(
1075 previewUrl, postData, successRenderCommit,
1076 failRenderCommit
1198 previewUrl, postData, successRenderCommit, failRenderCommit
1077 1199 );
1078 1200
1079 1201 try {
@@ -1084,7 +1206,7 b' var CommentsController = function() {'
1084 1206 $comments.find('.cb-comment-add-button').before(html);
1085 1207
1086 1208 // run global callback on submit
1087 commentForm.globalSubmitSuccessCallback();
1209 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1088 1210
1089 1211 } catch (e) {
1090 1212 console.error(e);
@@ -1101,10 +1223,14 b' var CommentsController = function() {'
1101 1223 updateSticky()
1102 1224 }
1103 1225
1104 if (window.refreshAllComments !== undefined) {
1226 if (window.refreshAllComments !== undefined && !isDraft) {
1105 1227 // if we have this handler, run it, and refresh all comments boxes
1106 1228 refreshAllComments()
1107 1229 }
1230 else if (window.refreshDraftComments !== undefined && isDraft) {
1231 // if we have this handler, run it, and refresh all comments boxes
1232 refreshDraftComments();
1233 }
1108 1234
1109 1235 commentForm.setActionButtonsDisabled(false);
1110 1236
@@ -1129,66 +1255,122 b' var CommentsController = function() {'
1129 1255 });
1130 1256 }
1131 1257
1132 $form.addClass('comment-inline-form-open');
1258 $editForm.addClass('comment-inline-form-open');
1133 1259 };
1134 1260
1135 this.createComment = function(node, resolutionComment) {
1136 var resolvesCommentId = resolutionComment || null;
1261 this.attachComment = function(json_data) {
1262 var self = this;
1263 $.each(json_data, function(idx, val) {
1264 var json_data_elem = [val]
1265 var isInline = val.comment_f_path && val.comment_lineno
1266
1267 if (isInline) {
1268 self.attachInlineComment(json_data_elem)
1269 } else {
1270 self.attachGeneralComment(json_data_elem)
1271 }
1272 })
1273
1274 }
1275
1276 this.attachGeneralComment = function(json_data) {
1277 $.each(json_data, function(idx, val) {
1278 $('#injected_page_comments').append(val.rendered_text);
1279 })
1280 }
1281
1282 this.attachInlineComment = function(json_data) {
1283
1284 $.each(json_data, function (idx, val) {
1285 var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
1286 var html = val.rendered_text;
1287 var $inlineComments = $('#' + val.target_id)
1288 .find(line_qry)
1289 .find('.inline-comments');
1290
1291 var lastComment = $inlineComments.find('.comment-inline').last();
1292
1293 if (lastComment.length === 0) {
1294 // first comment, we append simply
1295 $inlineComments.find('.reply-thread-container-wrapper').before(html);
1296 } else {
1297 $(lastComment).after(html)
1298 }
1299
1300 })
1301
1302 };
1303
1304 this.createNewFormWrapper = function(f_path, line_no) {
1305 // create a new reply HTML form from template
1306 var tmpl = $('#cb-comment-inline-form-template').html();
1307 tmpl = tmpl.format(escapeHtml(f_path), line_no);
1308 return $(tmpl);
1309 }
1310
1311 this.createComment = function(node, f_path, line_no, resolutionComment) {
1312 self.edit = false;
1137 1313 var $node = $(node);
1138 1314 var $td = $node.closest('td');
1139 var $form = $td.find('.comment-inline-form');
1140 this.edit = false;
1315 var resolvesCommentId = resolutionComment || null;
1141 1316
1142 if (!$form.length) {
1317 var $replyForm = $td.find('.comment-inline-form');
1143 1318
1144 var $filediff = $node.closest('.filediff');
1145 $filediff.removeClass('hide-comments');
1146 var f_path = $filediff.attr('data-f-path');
1147 var lineno = self.getLineNumber(node);
1148 // create a new HTML from template
1149 var tmpl = $('#cb-comment-inline-form-template').html();
1150 tmpl = tmpl.format(escapeHtml(f_path), lineno);
1151 $form = $(tmpl);
1319 // if form isn't existing, we're generating a new one and injecting it.
1320 if ($replyForm.length === 0) {
1321
1322 // unhide/expand all comments if they are hidden for a proper REPLY mode
1323 self.toggleLineComments($node, true);
1324
1325 $replyForm = self.createNewFormWrapper(f_path, line_no);
1152 1326
1153 1327 var $comments = $td.find('.inline-comments');
1154 if (!$comments.length) {
1155 $comments = $(
1156 $('#cb-comments-inline-container-template').html());
1157 $td.append($comments);
1328
1329 // There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
1330 if ($comments.length===0) {
1331 var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(f_path, line_no)
1332 var $reply_container = $('#cb-comments-inline-container-template')
1333 $reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
1334 $td.append($($reply_container).html());
1158 1335 }
1159 1336
1160 $td.find('.cb-comment-add-button').before($form);
1337 // default comment button exists, so we prepend the form for leaving initial comment
1338 $td.find('.cb-comment-add-button').before($replyForm);
1339 // set marker, that we have a open form
1340 var $replyWrapper = $td.find('.reply-thread-container-wrapper')
1341 $replyWrapper.addClass('comment-form-active');
1161 1342
1162 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
1163 var _form = $($form[0]).find('form');
1343 var lastComment = $comments.find('.comment-inline').last();
1344 if ($(lastComment).hasClass('comment-outdated')) {
1345 $replyWrapper.show();
1346 }
1347
1348 var _form = $($replyForm[0]).find('form');
1164 1349 var autocompleteActions = ['as_note', 'as_todo'];
1165 1350 var comment_id=null;
1166 var commentForm = this.createCommentForm(
1167 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId, this.edit, comment_id);
1168
1169 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
1170 form: _form,
1171 parent: $td[0],
1172 lineno: lineno,
1173 f_path: f_path}
1174 );
1351 var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
1352 var commentForm = self.createCommentForm(
1353 _form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
1354 self.edit, comment_id);
1175 1355
1176 1356 // set a CUSTOM submit handler for inline comments.
1177 1357 commentForm.setHandleFormSubmit(function(o) {
1178 1358 var text = commentForm.cm.getValue();
1179 1359 var commentType = commentForm.getCommentType();
1180 1360 var resolvesCommentId = commentForm.getResolvesId();
1361 var isDraft = commentForm.getDraftState();
1181 1362
1182 1363 if (text === "") {
1183 1364 return;
1184 1365 }
1185 1366
1186 if (lineno === undefined) {
1187 alert('missing line !');
1367 if (line_no === undefined) {
1368 alert('Error: unable to fetch line number for this inline comment !');
1188 1369 return;
1189 1370 }
1371
1190 1372 if (f_path === undefined) {
1191 alert('missing file path !');
1373 alert('Error: unable to fetch file path for this inline comment !');
1192 1374 return;
1193 1375 }
1194 1376
@@ -1199,66 +1381,79 b' var CommentsController = function() {'
1199 1381 var postData = {
1200 1382 'text': text,
1201 1383 'f_path': f_path,
1202 'line': lineno,
1384 'line': line_no,
1203 1385 'comment_type': commentType,
1386 'draft': isDraft,
1204 1387 'csrf_token': CSRF_TOKEN
1205 1388 };
1206 1389 if (resolvesCommentId){
1207 1390 postData['resolves_comment_id'] = resolvesCommentId;
1208 1391 }
1209 1392
1393 // submitSuccess for inline commits
1210 1394 var submitSuccessCallback = function(json_data) {
1211 $form.remove();
1212 try {
1213 var html = json_data.rendered_text;
1214 var lineno = json_data.line_no;
1215 var target_id = json_data.target_id;
1395
1396 $replyForm.remove();
1397 $td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
1398
1399 try {
1400
1401 // inject newly created comments, json_data is {<comment_id>: {}}
1402 self.attachInlineComment(json_data)
1216 1403
1217 $comments.find('.cb-comment-add-button').before(html);
1404 //mark visually which comment was resolved
1405 if (resolvesCommentId) {
1406 commentForm.markCommentResolved(resolvesCommentId);
1407 }
1218 1408
1219 //mark visually which comment was resolved
1220 if (resolvesCommentId) {
1221 commentForm.markCommentResolved(resolvesCommentId);
1409 // run global callback on submit
1410 commentForm.globalSubmitSuccessCallback({
1411 draft: isDraft,
1412 comment_id: comment_id
1413 });
1414
1415 } catch (e) {
1416 console.error(e);
1222 1417 }
1223 1418
1224 // run global callback on submit
1225 commentForm.globalSubmitSuccessCallback();
1226
1227 } catch (e) {
1228 console.error(e);
1229 }
1230
1231 // re trigger the linkification of next/prev navigation
1232 linkifyComments($('.inline-comment-injected'));
1233 timeagoActivate();
1234 tooltipActivate();
1235
1236 1419 if (window.updateSticky !== undefined) {
1237 1420 // potentially our comments change the active window size, so we
1238 1421 // notify sticky elements
1239 1422 updateSticky()
1240 1423 }
1241 1424
1242 if (window.refreshAllComments !== undefined) {
1425 if (window.refreshAllComments !== undefined && !isDraft) {
1243 1426 // if we have this handler, run it, and refresh all comments boxes
1244 1427 refreshAllComments()
1245 1428 }
1429 else if (window.refreshDraftComments !== undefined && isDraft) {
1430 // if we have this handler, run it, and refresh all comments boxes
1431 refreshDraftComments();
1432 }
1246 1433
1247 1434 commentForm.setActionButtonsDisabled(false);
1248 1435
1436 // re trigger the linkification of next/prev navigation
1437 linkifyComments($('.inline-comment-injected'));
1438 timeagoActivate();
1439 tooltipActivate();
1249 1440 };
1441
1250 1442 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1251 1443 var prefix = "Error while submitting comment.\n"
1252 1444 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1253 1445 ajaxErrorSwal(message);
1254 1446 commentForm.resetCommentFormState(text)
1255 1447 };
1448
1256 1449 commentForm.submitAjaxPOST(
1257 1450 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1258 1451 });
1259 1452 }
1260 1453
1261 $form.addClass('comment-inline-form-open');
1454 // Finally "open" our reply form, since we know there are comments and we have the "attached" old form
1455 $replyForm.addClass('comment-inline-form-open');
1456 tooltipActivate();
1262 1457 };
1263 1458
1264 1459 this.createResolutionComment = function(commentId){
@@ -1268,9 +1463,12 b' var CommentsController = function() {'
1268 1463 var comment = $('#comment-'+commentId);
1269 1464 var commentData = comment.data();
1270 1465 if (commentData.commentInline) {
1271 this.createComment(comment, commentId)
1466 var f_path = commentData.fPath;
1467 var line_no = commentData.lineNo;
1468 //TODO check this if we need to give f_path/line_no
1469 this.createComment(comment, f_path, line_no, commentId)
1272 1470 } else {
1273 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
1471 this.createGeneralComment('general', "$placeholder", commentId)
1274 1472 }
1275 1473
1276 1474 return false;
@@ -1296,3 +1494,8 b' var CommentsController = function() {'
1296 1494 };
1297 1495
1298 1496 };
1497
1498 window.commentHelp = function(renderer) {
1499 var funcData = {'renderer': renderer}
1500 return renderTemplate('commentHelpHovercard', funcData)
1501 } No newline at end of file
@@ -42,12 +42,22 b' window.toggleElement = function (elem, t'
42 42 var $elem = $(elem);
43 43 var $target = $(target);
44 44
45 if ($target.is(':visible') || $target.length === 0) {
45 if (target !== undefined) {
46 var show = $target.is(':visible') || $target.length === 0;
47 } else {
48 var show = $elem.hasClass('toggle-off')
49 }
50
51 if (show) {
46 52 $target.hide();
47 53 $elem.html($elem.data('toggleOn'))
54 $elem.addClass('toggle-on')
55 $elem.removeClass('toggle-off')
48 56 } else {
49 57 $target.show();
50 58 $elem.html($elem.data('toggleOff'))
59 $elem.addClass('toggle-off')
60 $elem.removeClass('toggle-on')
51 61 }
52 62
53 63 return false
@@ -182,83 +182,34 b' window.ReviewersController = function ()'
182 182 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
183 183 // default rule, case for older repo that don't have any rules stored
184 184 self.$rulesList.append(
185 self.addRule(
186 _gettext('All reviewers must vote.'))
185 self.addRule(_gettext('All reviewers must vote.'))
187 186 );
188 187 return self.forbidUsers
189 188 }
190 189
191 if (data.rules.voting !== undefined) {
192 if (data.rules.voting < 0) {
193 self.$rulesList.append(
194 self.addRule(
195 _gettext('All individual reviewers must vote.'))
196 )
197 } else if (data.rules.voting === 1) {
198 self.$rulesList.append(
199 self.addRule(
200 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
201 )
202
203 } else {
204 self.$rulesList.append(
205 self.addRule(
206 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
207 )
208 }
190 if (data.rules.forbid_adding_reviewers) {
191 $('#add_reviewer_input').remove();
209 192 }
210 193
211 if (data.rules.voting_groups !== undefined) {
212 $.each(data.rules.voting_groups, function (index, rule_data) {
213 self.$rulesList.append(
214 self.addRule(rule_data.text)
215 )
216 });
217 }
218
219 if (data.rules.use_code_authors_for_review) {
220 self.$rulesList.append(
221 self.addRule(
222 _gettext('Reviewers picked from source code changes.'))
223 )
194 if (data.rules_data !== undefined && data.rules_data.forbidden_users !== undefined) {
195 $.each(data.rules_data.forbidden_users, function(idx, val){
196 self.forbidUsers.push(val)
197 })
224 198 }
225 199
226 if (data.rules.forbid_adding_reviewers) {
227 $('#add_reviewer_input').remove();
200 if (data.rules_humanized !== undefined && data.rules_humanized.length > 0) {
201 $.each(data.rules_humanized, function(idx, val) {
202 self.$rulesList.append(
203 self.addRule(val)
204 )
205 })
206 } else {
207 // we don't have any rules set, so we inform users about it
228 208 self.$rulesList.append(
229 self.addRule(
230 _gettext('Adding new reviewers is forbidden.'))
231 )
232 }
233
234 if (data.rules.forbid_author_to_review) {
235 self.forbidUsers.push(data.rules_data.pr_author);
236 self.$rulesList.append(
237 self.addRule(
238 _gettext('Author is not allowed to be a reviewer.'))
209 self.addRule(_gettext('No additional review rules set.'))
239 210 )
240 211 }
241 212
242 if (data.rules.forbid_commit_author_to_review) {
243
244 if (data.rules_data.forbidden_users) {
245 $.each(data.rules_data.forbidden_users, function (index, member_data) {
246 self.forbidUsers.push(member_data)
247 });
248 }
249
250 self.$rulesList.append(
251 self.addRule(
252 _gettext('Commit Authors are not allowed to be a reviewer.'))
253 )
254 }
255
256 // we don't have any rules set, so we inform users about it
257 if (self.enabledRules.length === 0) {
258 self.addRule(
259 _gettext('No review rules set.'))
260 }
261
262 213 return self.forbidUsers
263 214 };
264 215
@@ -1066,14 +1017,14 b' window.ReviewerPresenceController = func'
1066 1017 this.handlePresence = function (data) {
1067 1018 if (data.type == 'presence' && data.channel === self.channel) {
1068 1019 this.storeUsers(data.users);
1069 this.render()
1020 this.render();
1070 1021 }
1071 1022 };
1072 1023
1073 1024 this.handleChannelUpdate = function (data) {
1074 1025 if (data.channel === this.channel) {
1075 1026 this.storeUsers(data.state.users);
1076 this.render()
1027 this.render();
1077 1028 }
1078 1029
1079 1030 };
@@ -1085,6 +1036,30 b' window.ReviewerPresenceController = func'
1085 1036
1086 1037 };
1087 1038
1039 window.refreshCommentsSuccess = function(targetNode, counterNode, extraCallback) {
1040 var $targetElem = targetNode;
1041 var $counterElem = counterNode;
1042
1043 return function (data) {
1044 var newCount = $(data).data('counter');
1045 if (newCount !== undefined) {
1046 var callback = function () {
1047 $counterElem.animate({'opacity': 1.00}, 200)
1048 $counterElem.html(newCount);
1049 };
1050 $counterElem.animate({'opacity': 0.15}, 200, callback);
1051 }
1052
1053 $targetElem.css('opacity', 1);
1054 $targetElem.html(data);
1055 tooltipActivate();
1056
1057 if (extraCallback !== undefined) {
1058 extraCallback(data)
1059 }
1060 }
1061 }
1062
1088 1063 window.refreshComments = function (version) {
1089 1064 version = version || templateContext.pull_request_data.pull_request_version || '';
1090 1065
@@ -1109,23 +1084,8 b' window.refreshComments = function (versi'
1109 1084
1110 1085 var $targetElem = $('.comments-content-table');
1111 1086 $targetElem.css('opacity', 0.3);
1112
1113 var success = function (data) {
1114 var $counterElem = $('#comments-count');
1115 var newCount = $(data).data('counter');
1116 if (newCount !== undefined) {
1117 var callback = function () {
1118 $counterElem.animate({'opacity': 1.00}, 200)
1119 $counterElem.html(newCount);
1120 };
1121 $counterElem.animate({'opacity': 0.15}, 200, callback);
1122 }
1123
1124 $targetElem.css('opacity', 1);
1125 $targetElem.html(data);
1126 tooltipActivate();
1127 }
1128
1087 var $counterElem = $('#comments-count');
1088 var success = refreshCommentsSuccess($targetElem, $counterElem);
1129 1089 ajaxPOST(loadUrl, data, success, null, {})
1130 1090
1131 1091 }
@@ -1139,7 +1099,7 b' window.refreshTODOs = function (version)'
1139 1099 'repo_name': templateContext.repo_name,
1140 1100 'version': version,
1141 1101 };
1142 var loadUrl = pyroutes.url('pullrequest_comments', params);
1102 var loadUrl = pyroutes.url('pullrequest_todos', params);
1143 1103 } // commit case
1144 1104 else {
1145 1105 return
@@ -1153,27 +1113,46 b' window.refreshTODOs = function (version)'
1153 1113 var data = {"comments": currentIDs};
1154 1114 var $targetElem = $('.todos-content-table');
1155 1115 $targetElem.css('opacity', 0.3);
1156
1157 var success = function (data) {
1158 var $counterElem = $('#todos-count')
1159 var newCount = $(data).data('counter');
1160 if (newCount !== undefined) {
1161 var callback = function () {
1162 $counterElem.animate({'opacity': 1.00}, 200)
1163 $counterElem.html(newCount);
1164 };
1165 $counterElem.animate({'opacity': 0.15}, 200, callback);
1166 }
1167
1168 $targetElem.css('opacity', 1);
1169 $targetElem.html(data);
1170 tooltipActivate();
1171 }
1116 var $counterElem = $('#todos-count');
1117 var success = refreshCommentsSuccess($targetElem, $counterElem);
1172 1118
1173 1119 ajaxPOST(loadUrl, data, success, null, {})
1174 1120
1175 1121 }
1176 1122
1123 window.refreshDraftComments = function () {
1124
1125 // Pull request case
1126 if (templateContext.pull_request_data.pull_request_id !== null) {
1127 var params = {
1128 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1129 'repo_name': templateContext.repo_name,
1130 };
1131 var loadUrl = pyroutes.url('pullrequest_drafts', params);
1132 } // commit case
1133 else {
1134 return
1135 }
1136
1137 var data = {};
1138
1139 var $targetElem = $('.drafts-content-table');
1140 $targetElem.css('opacity', 0.3);
1141 var $counterElem = $('#drafts-count');
1142 var extraCallback = function(data) {
1143 if ($(data).data('counter') == 0){
1144 $('#draftsTable').hide();
1145 } else {
1146 $('#draftsTable').show();
1147 }
1148 // uncheck on load the select all checkbox
1149 $('[name=select_all_drafts]').prop('checked', 0);
1150 }
1151 var success = refreshCommentsSuccess($targetElem, $counterElem, extraCallback);
1152
1153 ajaxPOST(loadUrl, data, success, null, {})
1154 };
1155
1177 1156 window.refreshAllComments = function (version) {
1178 1157 version = version || templateContext.pull_request_data.pull_request_version || '';
1179 1158
@@ -1,7 +1,5 b''
1 1 /__MAIN_APP__ - launched when rhodecode-app element is attached to DOM
2 2 /plugins/__REGISTER__ - launched after the onDomReady() code from rhodecode.js is executed
3 /ui/plugins/code/anchor_focus - launched when rc starts to scroll on load to anchor on PR/Codeview
4 /ui/plugins/code/comment_form_built - launched when injectInlineForm() is executed and the form object is created
5 3 /notifications - shows new event notifications
6 4 /connection_controller/subscribe - subscribes user to new channels
7 5 /connection_controller/presence - receives presence change messages
@@ -104,12 +104,6 b' def add_request_user_context(event):'
104 104 request.environ['rc_req_id'] = req_id
105 105
106 106
107 def inject_app_settings(event):
108 settings = event.app.registry.settings
109 # inject info about available permissions
110 auth.set_available_permissions(settings)
111
112
113 107 def scan_repositories_if_enabled(event):
114 108 """
115 109 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
@@ -46,6 +46,8 b''
46 46 $pullRequestListTable.DataTable({
47 47 processing: true,
48 48 serverSide: true,
49 stateSave: true,
50 stateDuration: -1,
49 51 ajax: {
50 52 "url": "${h.route_path('my_account_pullrequests_data')}",
51 53 "data": function (d) {
@@ -119,6 +121,10 b''
119 121 if (data['owned']) {
120 122 $(row).addClass('owned');
121 123 }
124 },
125 "stateSaveParams": function (settings, data) {
126 data.search.search = ""; // Don't save search
127 data.start = 0; // don't save pagination
122 128 }
123 129 });
124 130 $pullRequestListTable.on('xhr.dt', function (e, settings, json, xhr) {
@@ -129,6 +135,7 b''
129 135 $pullRequestListTable.css('opacity', 0.3);
130 136 });
131 137
138
132 139 // filter
133 140 $('#q_filter').on('keyup',
134 141 $.debounce(250, function () {
@@ -1225,6 +1225,14 b''
1225 1225 (function () {
1226 1226 "use sctrict";
1227 1227
1228 // details block auto-hide menu
1229 $(document).mouseup(function(e) {
1230 var container = $('.details-inline-block');
1231 if (!container.is(e.target) && container.has(e.target).length === 0) {
1232 $('.details-inline-block[open]').removeAttr('open')
1233 }
1234 });
1235
1228 1236 var $sideBar = $('.right-sidebar');
1229 1237 var expanded = $sideBar.hasClass('right-sidebar-expanded');
1230 1238 var sidebarState = templateContext.session_attrs.sidebarState;
@@ -4,7 +4,7 b''
4 4 ## ${sidebar.comments_table()}
5 5 <%namespace name="base" file="/base/base.mako"/>
6 6
7 <%def name="comments_table(comments, counter_num, todo_comments=False, existing_ids=None, is_pr=True)">
7 <%def name="comments_table(comments, counter_num, todo_comments=False, draft_comments=False, existing_ids=None, is_pr=True)">
8 8 <%
9 9 if todo_comments:
10 10 cls_ = 'todos-content-table'
@@ -15,10 +15,13 b''
15 15 # own comments first
16 16 user_id = 0
17 17 return '{}'.format(str(entry.comment_id).zfill(10000))
18 elif draft_comments:
19 cls_ = 'drafts-content-table'
20 def sorter(entry):
21 return '{}'.format(str(entry.comment_id).zfill(10000))
18 22 else:
19 23 cls_ = 'comments-content-table'
20 24 def sorter(entry):
21 user_id = entry.author.user_id
22 25 return '{}'.format(str(entry.comment_id).zfill(10000))
23 26
24 27 existing_ids = existing_ids or []
@@ -31,8 +34,12 b''
31 34 <%
32 35 display = ''
33 36 _cls = ''
37 ## Extra precaution to not show drafts in the sidebar for todo/comments
38 if comment_obj.draft and not draft_comments:
39 continue
34 40 %>
35 41
42
36 43 <%
37 44 comment_ver_index = comment_obj.get_index_version(getattr(c, 'versions', []))
38 45 prev_comment_ver_index = 0
@@ -83,6 +90,11 b''
83 90 % endif
84 91
85 92 <tr class="${_cls}" style="display: ${display};" data-sidebar-comment-id="${comment_obj.comment_id}">
93 % if draft_comments:
94 <td style="width: 15px;">
95 ${h.checkbox('submit_draft', id=None, value=comment_obj.comment_id)}
96 </td>
97 % endif
86 98 <td class="td-todo-number">
87 99 <%
88 100 version_info = ''
@@ -24,8 +24,6 b''
24 24
25 25 <%def name="main()">
26 26 <script type="text/javascript">
27 // TODO: marcink switch this to pyroutes
28 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__')}";
29 27 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
30 28 </script>
31 29
@@ -1,4 +1,4 b''
1 1 ## this is a dummy html file for partial rendering on server and sending
2 2 ## generated output via ajax after comment submit
3 3 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ${comment.comment_block(c.co, inline=c.co.is_inline)}
4 ${comment.comment_block(c.co, inline=c.co.is_inline, is_new=c.is_new)}
@@ -3,20 +3,25 b''
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6 <%namespace name="base" file="/base/base.mako"/>
6 7
7 8 <%!
8 9 from rhodecode.lib import html_filters
9 10 %>
10 11
11 <%namespace name="base" file="/base/base.mako"/>
12 <%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
12
13 <%def name="comment_block(comment, inline=False, active_pattern_entries=None, is_new=False)">
13 14
14 15 <%
15 from rhodecode.model.comment import CommentsModel
16 comment_model = CommentsModel()
16 from rhodecode.model.comment import CommentsModel
17 comment_model = CommentsModel()
18
19 comment_ver = comment.get_index_version(getattr(c, 'versions', []))
20 latest_ver = len(getattr(c, 'versions', []))
21 visible_for_user = True
22 if comment.draft:
23 visible_for_user = comment.user_id == c.rhodecode_user.user_id
17 24 %>
18 <% comment_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
19 <% latest_ver = len(getattr(c, 'versions', [])) %>
20 25
21 26 % if inline:
22 27 <% outdated_at_ver = comment.outdated_at_version(c.at_version_num) %>
@@ -24,6 +29,7 b''
24 29 <% outdated_at_ver = comment.older_than_version(c.at_version_num) %>
25 30 % endif
26 31
32 % if visible_for_user:
27 33 <div class="comment
28 34 ${'comment-inline' if inline else 'comment-general'}
29 35 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
@@ -31,14 +37,26 b''
31 37 line="${comment.line_no}"
32 38 data-comment-id="${comment.comment_id}"
33 39 data-comment-type="${comment.comment_type}"
40 data-comment-draft=${h.json.dumps(comment.draft)}
34 41 data-comment-renderer="${comment.renderer}"
35 42 data-comment-text="${comment.text | html_filters.base64,n}"
43 data-comment-f-path="${comment.f_path}"
36 44 data-comment-line-no="${comment.line_no}"
37 45 data-comment-inline=${h.json.dumps(inline)}
38 46 style="${'display: none;' if outdated_at_ver else ''}">
39 47
40 48 <div class="meta">
41 49 <div class="comment-type-label">
50 % if comment.draft:
51 <div class="tooltip comment-draft" title="${_('Draft comments are only visible to the author until submitted')}.">
52 DRAFT
53 </div>
54 % elif is_new:
55 <div class="tooltip comment-new" title="${_('This comment was added while you browsed this page')}.">
56 NEW
57 </div>
58 % endif
59
42 60 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
43 61
44 62 ## TODO COMMENT
@@ -90,7 +108,7 b''
90 108
91 109 </div>
92 110 </div>
93
111 ## NOTE 0 and .. => because we disable it for now until UI ready
94 112 % if 0 and comment.status_change:
95 113 <div class="pull-left">
96 114 <span class="tag authortag tooltip" title="${_('Status from pull request.')}">
@@ -100,10 +118,12 b''
100 118 </span>
101 119 </div>
102 120 % endif
103
121 ## Since only author can see drafts, we don't show it
122 % if not comment.draft:
104 123 <div class="author ${'author-inline' if inline else 'author-general'}">
105 124 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
106 125 </div>
126 % endif
107 127
108 128 <div class="date">
109 129 ${h.age_component(comment.modified_at, time_is_local=True)}
@@ -164,7 +184,7 b''
164 184 % if inline:
165 185 <a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
166 186 % if outdated_at_ver:
167 <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>
187 <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>
168 188 <code class="action-divider">|</code>
169 189 % elif comment_ver:
170 190 <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 b''
210 230 %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):
211 231 <div class="dropdown-divider"></div>
212 232 <div class="dropdown-item">
213 <a onclick="return Rhodecode.comments.editComment(this);" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
233 <a onclick="return Rhodecode.comments.editComment(this, '${comment.line_no}', '${comment.f_path}');" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
214 234 </div>
215 235 <div class="dropdown-item">
216 236 <a onclick="return Rhodecode.comments.deleteComment(this);" class="btn btn-link btn-sm btn-danger delete-comment">${_('Delete')}</a>
217 237 </div>
238 ## Only available in EE edition
239 % if comment.draft and c.rhodecode_edition_id == 'EE':
240 <div class="dropdown-item">
241 <a onclick="return Rhodecode.comments.finalizeDrafts([${comment.comment_id}]);" class="btn btn-link btn-sm finalize-draft-comment">${_('Submit draft')}</a>
242 </div>
243 % endif
218 244 %else:
219 245 <div class="dropdown-divider"></div>
220 246 <div class="dropdown-item">
@@ -252,6 +278,7 b''
252 278 </div>
253 279
254 280 </div>
281 % endif
255 282 </%def>
256 283
257 284 ## generate main comments
@@ -298,10 +325,9 b''
298 325 ## inject form here
299 326 </div>
300 327 <script type="text/javascript">
301 var lineNo = 'general';
302 328 var resolvesCommentId = null;
303 329 var generalCommentForm = Rhodecode.comments.createGeneralComment(
304 lineNo, "${placeholder}", resolvesCommentId);
330 'general', "${placeholder}", resolvesCommentId);
305 331
306 332 // set custom success callback on rangeCommit
307 333 % if is_compare:
@@ -311,6 +337,7 b''
311 337 var text = self.cm.getValue();
312 338 var status = self.getCommentStatus();
313 339 var commentType = self.getCommentType();
340 var isDraft = self.getDraftState();
314 341
315 342 if (text === "" && !status) {
316 343 return;
@@ -337,6 +364,7 b''
337 364 'text': text,
338 365 'changeset_status': status,
339 366 'comment_type': commentType,
367 'draft': isDraft,
340 368 'commit_ids': commitIds,
341 369 'csrf_token': CSRF_TOKEN
342 370 };
@@ -371,7 +399,7 b''
371 399
372 400 <div class="comment-area-write" style="display: block;">
373 401 <div id="edit-container">
374 <div style="padding: 40px 0">
402 <div style="padding: 20px 0px 0px 0;">
375 403 ${_('You need to be logged in to leave comments.')}
376 404 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
377 405 </div>
@@ -430,7 +458,7 b''
430 458 </div>
431 459
432 460 <div class="comment-area-write" style="display: block;">
433 <div id="edit-container_${lineno_id}">
461 <div id="edit-container_${lineno_id}" style="margin-top: -1px">
434 462 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
435 463 </div>
436 464 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
@@ -477,39 +505,50 b''
477 505 <div class="action-buttons-extra"></div>
478 506 % endif
479 507
480 <input class="btn btn-success comment-button-input" id="save_${lineno_id}" name="save" type="submit" value="${_('Comment')}">
508 <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')">
509
510 % if form_type == 'inline':
511 % if c.rhodecode_edition_id == 'EE':
512 ## Disable the button for CE, the "real" validation is in the backend code anyway
513 <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')">
514 % else:
515 <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">
516 % endif
517 % endif
518
519 % if review_statuses:
520 <div class="comment-status-box">
521 <select id="change_status_${lineno_id}" name="changeset_status">
522 <option></option> ## Placeholder
523 % for status, lbl in review_statuses:
524 <option value="${status}" data-status="${status}">${lbl}</option>
525 %if is_pull_request and change_status and status in ('approved', 'rejected'):
526 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
527 %endif
528 % endfor
529 </select>
530 </div>
531 % endif
481 532
482 533 ## inline for has a file, and line-number together with cancel hide button.
483 534 % if form_type == 'inline':
484 535 <input type="hidden" name="f_path" value="{0}">
485 536 <input type="hidden" name="line" value="${lineno_id}">
486 537 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
487 ${_('Cancel')}
538 <i class="icon-cancel-circled2"></i>
488 539 </button>
489 540 % endif
490 541 </div>
491 542
492 % if review_statuses:
493 <div class="status_box">
494 <select id="change_status_${lineno_id}" name="changeset_status">
495 <option></option> ## Placeholder
496 % for status, lbl in review_statuses:
497 <option value="${status}" data-status="${status}">${lbl}</option>
498 %if is_pull_request and change_status and status in ('approved', 'rejected'):
499 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
500 %endif
501 % endfor
502 </select>
503 </div>
504 % endif
505
506 543 <div class="toolbar-text">
507 544 <% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
508 ${_('Comments parsed using {} syntax.').format(renderer_url)|n} <br/>
509 <span class="tooltip" title="${_('Use @username inside this text to send notification to this RhodeCode user')}">@mention</span>
510 ${_('and')}
511 <span class="tooltip" title="${_('Start typing with / for certain actions to be triggered via text box.')}">`/` autocomplete</span>
512 ${_('actions supported.')}
545 <span>${_('{} is supported.').format(renderer_url)|n}
546
547 <i class="icon-info-circled tooltip-hovercard"
548 data-hovercard-alt="ALT"
549 data-hovercard-url="javascript:commentHelp('${c.visual.default_renderer.upper()}')"
550 data-comment-json-b64='${h.b64(h.json.dumps({}))}'></i>
551 </span>
513 552 </div>
514 553 </div>
515 554
@@ -104,7 +104,9 b''
104 104 %for commit in c.commit_ranges:
105 105 ## commit range header for each individual diff
106 106 <h3>
107 <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>
107 <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)}">
108 ${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}
109 </a>
108 110 </h3>
109 111
110 112 ${cbdiffs.render_diffset_menu(c.changes[commit.raw_id])}
@@ -1,3 +1,4 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 2 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2 3
3 4 <%def name="diff_line_anchor(commit, filename, line, type)"><%
@@ -74,24 +75,9 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
74 75
75 76 <div class="js-template" id="cb-comment-inline-form-template">
76 77 <div class="comment-inline-form ac">
77
78 %if c.rhodecode_user.username != h.DEFAULT_USER:
78 %if not c.rhodecode_user.is_default:
79 79 ## render template for inline comments
80 80 ${commentblock.comment_form(form_type='inline')}
81 %else:
82 ${h.form('', class_='inline-form comment-form-login', method='get')}
83 <div class="pull-left">
84 <div class="comment-help pull-right">
85 ${_('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>
86 </div>
87 </div>
88 <div class="comment-button pull-right">
89 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
90 ${_('Cancel')}
91 </button>
92 </div>
93 <div class="clearfix"></div>
94 ${h.end_form()}
95 81 %endif
96 82 </div>
97 83 </div>
@@ -287,7 +273,7 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
287 273 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
288 274 <%
289 275 file_comments = (get_inline_comments(inline_comments, filediff.patch['filename']) or {}).values()
290 total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not _c.outdated]
276 total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not (_c.outdated or _c.draft)]
291 277 %>
292 278 <div class="filediff-collapse-indicator icon-"></div>
293 279
@@ -327,7 +313,7 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
327 313 </label>
328 314
329 315 ${diff_menu(filediff, use_comments=use_comments)}
330 <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 '')}">
316 <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 '')}">
331 317
332 318 ## new/deleted/empty content case
333 319 % if not filediff.hunks:
@@ -626,8 +612,10 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
626 612
627 613 % if use_comments:
628 614 |
629 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
630 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
615 <a href="#" onclick="Rhodecode.comments.toggleDiffComments(this);return toggleElement(this)"
616 data-toggle-on="${_('Hide comments')}"
617 data-toggle-off="${_('Show comments')}">
618 <span class="hide-comment-button">${_('Hide comments')}</span>
631 619 </a>
632 620 % endif
633 621
@@ -637,23 +625,37 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
637 625 </%def>
638 626
639 627
640 <%def name="inline_comments_container(comments, active_pattern_entries=None)">
628 <%def name="inline_comments_container(comments, active_pattern_entries=None, line_no='', f_path='')">
641 629
642 630 <div class="inline-comments">
643 631 %for comment in comments:
644 632 ${commentblock.comment_block(comment, inline=True, active_pattern_entries=active_pattern_entries)}
645 633 %endfor
646 % if comments and comments[-1].outdated:
647 <span class="btn btn-secondary cb-comment-add-button comment-outdated}" style="display: none;}">
648 ${_('Add another comment')}
649 </span>
650 % else:
651 <span onclick="return Rhodecode.comments.createComment(this)" class="btn btn-secondary cb-comment-add-button">
652 ${_('Add another comment')}
653 </span>
654 % endif
634
635 <%
636 extra_class = ''
637 extra_style = ''
638
639 if comments and comments[-1].outdated_at_version(c.at_version_num):
640 extra_class = ' comment-outdated'
641 extra_style = 'display: none;'
642
643 %>
655 644
645 <div class="reply-thread-container-wrapper${extra_class}" style="${extra_style}">
646 <div class="reply-thread-container${extra_class}">
647 <div class="reply-thread-gravatar">
648 ${base.gravatar(c.rhodecode_user.email, 20, tooltip=True, user=c.rhodecode_user)}
649 </div>
650 <div class="reply-thread-reply-button">
651 ## initial reply button, some JS logic can append here a FORM to leave a first comment.
652 <button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">Reply...</button>
653 </div>
654 <div class="reply-thread-last"></div>
655 </div>
656 </div>
656 657 </div>
658
657 659 </%def>
658 660
659 661 <%!
@@ -711,16 +713,19 b' def get_comments_for(diff_type, comments'
711 713 data-line-no="${line.original.lineno}"
712 714 >
713 715
714 <% line_old_comments = None %>
716 <% line_old_comments, line_old_comments_no_drafts = None, None %>
715 717 %if line.original.get_comment_args:
716 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
718 <%
719 line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args)
720 line_old_comments_no_drafts = [c for c in line_old_comments if not c.draft] if line_old_comments else []
721 has_outdated = any([x.outdated for x in line_old_comments_no_drafts])
722 %>
717 723 %endif
718 %if line_old_comments:
719 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
724 %if line_old_comments_no_drafts:
720 725 % if has_outdated:
721 <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>
726 <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>
722 727 % else:
723 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(line_old_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
728 <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>
724 729 % endif
725 730 %endif
726 731 </td>
@@ -734,16 +739,18 b' def get_comments_for(diff_type, comments'
734 739 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
735 740 %endif
736 741 </td>
742
743 <% line_no = 'o{}'.format(line.original.lineno) %>
737 744 <td class="cb-content ${action_class(line.original.action)}"
738 data-line-no="o${line.original.lineno}"
745 data-line-no="${line_no}"
739 746 >
740 747 %if use_comments and line.original.lineno:
741 ${render_add_comment_button()}
748 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
742 749 %endif
743 750 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
744 751
745 752 %if use_comments and line.original.lineno and line_old_comments:
746 ${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries)}
753 ${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
747 754 %endif
748 755
749 756 </td>
@@ -752,18 +759,20 b' def get_comments_for(diff_type, comments'
752 759 >
753 760 <div>
754 761
762 <% line_new_comments, line_new_comments_no_drafts = None, None %>
755 763 %if line.modified.get_comment_args:
756 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
757 %else:
758 <% line_new_comments = None%>
764 <%
765 line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args)
766 line_new_comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
767 has_outdated = any([x.outdated for x in line_new_comments_no_drafts])
768 %>
759 769 %endif
760 %if line_new_comments:
761 770
762 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
771 %if line_new_comments_no_drafts:
763 772 % if has_outdated:
764 <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>
773 <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>
765 774 % else:
766 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(line_new_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
775 <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>
767 776 % endif
768 777 %endif
769 778 </div>
@@ -778,22 +787,25 b' def get_comments_for(diff_type, comments'
778 787 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
779 788 %endif
780 789 </td>
790
791 <% line_no = 'n{}'.format(line.modified.lineno) %>
781 792 <td class="cb-content ${action_class(line.modified.action)}"
782 data-line-no="n${line.modified.lineno}"
793 data-line-no="${line_no}"
783 794 >
784 795 %if use_comments and line.modified.lineno:
785 ${render_add_comment_button()}
796 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
786 797 %endif
787 798 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
788 %if use_comments and line.modified.lineno and line_new_comments:
789 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries)}
790 %endif
791 799 % if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
792 800 <div class="nav-chunk" style="visibility: hidden">
793 801 <i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
794 802 </div>
795 803 <% chunk_count +=1 %>
796 804 % endif
805 %if use_comments and line.modified.lineno and line_new_comments:
806 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
807 %endif
808
797 809 </td>
798 810 </tr>
799 811 %endfor
@@ -814,20 +826,22 b' def get_comments_for(diff_type, comments'
814 826 <td class="cb-data ${action_class(action)}">
815 827 <div>
816 828
817 %if comments_args:
818 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
819 %else:
820 <% comments = None %>
821 %endif
829 <% comments, comments_no_drafts = None, None %>
830 %if comments_args:
831 <%
832 comments = get_comments_for('unified', inline_comments, *comments_args)
833 comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
834 has_outdated = any([x.outdated for x in comments_no_drafts])
835 %>
836 %endif
822 837
823 % if comments:
824 <% has_outdated = any([x.outdated for x in comments]) %>
825 % if has_outdated:
826 <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>
827 % else:
828 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
838 % if comments_no_drafts:
839 % if has_outdated:
840 <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>
841 % else:
842 <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>
843 % endif
829 844 % endif
830 % endif
831 845 </div>
832 846 </td>
833 847 <td class="cb-lineno ${action_class(action)}"
@@ -850,15 +864,16 b' def get_comments_for(diff_type, comments'
850 864 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
851 865 %endif
852 866 </td>
867 <% line_no = '{}{}'.format(new_line_no and 'n' or 'o', new_line_no or old_line_no) %>
853 868 <td class="cb-content ${action_class(action)}"
854 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
869 data-line-no="${line_no}"
855 870 >
856 871 %if use_comments:
857 ${render_add_comment_button()}
872 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
858 873 %endif
859 874 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
860 875 %if use_comments and comments:
861 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
876 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
862 877 %endif
863 878 </td>
864 879 </tr>
@@ -879,10 +894,12 b' def get_comments_for(diff_type, comments'
879 894 </%def>file changes
880 895
881 896
882 <%def name="render_add_comment_button()">
883 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
897 <%def name="render_add_comment_button(line_no='', f_path='')">
898 % if not c.rhodecode_user.is_default:
899 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">
884 900 <span><i class="icon-comment"></i></span>
885 901 </button>
902 % endif
886 903 </%def>
887 904
888 905 <%def name="render_diffset_menu(diffset, range_diff_on=None, commit=None, pull_request_menu=None)">
@@ -108,7 +108,26 b''
108 108 <i class="icon-cancel-circled2"></i>
109 109 </div>
110 110 <div class="btn btn-sm disabled" disabled="disabled" id="rev_range_more" style="display:none;">${_('Select second commit')}</div>
111 <a href="#" class="btn btn-success btn-sm" id="rev_range_container" style="display:none;"></a>
111
112 <div id="rev_range_action" class="btn-group btn-group-actions" style="display:none;">
113 <a href="#" class="btn btn-success btn-sm" id="rev_range_container" style="display:none;"></a>
114
115 <a class="btn btn-success btn-sm btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button">
116 <i class="icon-down"></i>
117 </a>
118
119 <div class="btn-action-switcher-container right-align">
120 <ul class="btn-action-switcher" role="menu" style="min-width: 220px; width: max-content">
121 <li>
122 ## JS fills the URL
123 <a id="rev_range_combined_url" class="btn btn-primary btn-sm" href="">
124 ${_('Show combined diff')}
125 </a>
126 </li>
127 </ul>
128 </div>
129 </div>
130
112 131 </th>
113 132
114 133 ## commit message expand arrow
@@ -147,6 +166,9 b''
147 166 var $commitRangeMore = $('#rev_range_more');
148 167 var $commitRangeContainer = $('#rev_range_container');
149 168 var $commitRangeClear = $('#rev_range_clear');
169 var $commitRangeAction = $('#rev_range_action');
170 var $commitRangeCombinedUrl = $('#rev_range_combined_url');
171 var $compareFork = $('#compare_fork_button');
150 172
151 173 var checkboxRangeSelector = function(e){
152 174 var selectedCheckboxes = [];
@@ -169,9 +191,8 b''
169 191 }
170 192
171 193 if (selectedCheckboxes.length > 0) {
172 $('#compare_fork_button').hide();
194 $compareFork.hide();
173 195 var commitStart = $(selectedCheckboxes[selectedCheckboxes.length-1]).data();
174
175 196 var revStart = commitStart.commitId;
176 197
177 198 var commitEnd = $(selectedCheckboxes[0]).data();
@@ -181,7 +202,9 b''
181 202 var lbl_end = '{0}'.format(commitEnd.commitIdx, commitEnd.shortId);
182 203
183 204 var url = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}', 'commit_id': revStart+'...'+revEnd});
184 var link = _gettext('Show commit range {0} ... {1}').format(lbl_start, lbl_end);
205 var urlCombined = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}', 'commit_id': revStart+'...'+revEnd, 'redirect_combined': '1'});
206
207 var link = _gettext('Show commit range {0}<i class="icon-angle-right"></i>{1}').format(lbl_start, lbl_end);
185 208
186 209 if (selectedCheckboxes.length > 1) {
187 210 $commitRangeClear.show();
@@ -192,9 +215,12 b''
192 215 .html(link)
193 216 .show();
194 217
218 $commitRangeCombinedUrl.attr('href', urlCombined);
219 $commitRangeAction.show();
195 220
196 221 } else {
197 222 $commitRangeContainer.hide();
223 $commitRangeAction.hide();
198 224 $commitRangeClear.show();
199 225 $commitRangeMore.show();
200 226 }
@@ -212,6 +238,7 b''
212 238 $commitRangeContainer.hide();
213 239 $commitRangeClear.hide();
214 240 $commitRangeMore.hide();
241 $commitRangeAction.hide();
215 242
216 243 %if c.branch_name:
217 244 var _url = pyroutes.url('pullrequest_new', {'repo_name': '${c.repo_name}', 'branch':'${c.branch_name}'});
@@ -220,7 +247,7 b''
220 247 var _url = pyroutes.url('pullrequest_new', {'repo_name': '${c.repo_name}'});
221 248 open_new_pull_request.attr('href', _url);
222 249 %endif
223 $('#compare_fork_button').show();
250 $compareFork.show();
224 251 }
225 252 };
226 253
@@ -397,7 +397,10 b''
397 397 % endif
398 398 </%def>
399 399
400 <%def name="pullrequest_updated_on(updated_on)">
400 <%def name="pullrequest_updated_on(updated_on, pr_version=None)">
401 % if pr_version:
402 <code>v${pr_version}</code>
403 % endif
401 404 ${h.age_component(h.time_to_utcdatetime(updated_on))}
402 405 </%def>
403 406
@@ -456,7 +459,7 b''
456 459 </div>
457 460
458 461 <div class="markup-form-area-write" style="display: block;">
459 <div id="edit-container_${form_id}">
462 <div id="edit-container_${form_id}" style="margin-top: -1px">
460 463 <textarea id="${form_id}" name="${form_id}" class="comment-block-ta ac-input">${form_text if form_text else ''}</textarea>
461 464 </div>
462 465 <div id="preview-container_${form_id}" class="clearfix" style="display: none;">
@@ -237,6 +237,24 b' if (show_disabled) {'
237 237
238 238 </script>
239 239
240 <script id="ejs_commentHelpHovercard" type="text/template" class="ejsTemplate">
241
242 <div>
243 Use <strong>@username</strong> mention syntax to send direct notification to this RhodeCode user.<br/>
244 Typing / starts autocomplete for certain action, e.g set review status, or comment type. <br/>
245 <br/>
246 Use <strong>Cmd/ctrl+enter</strong> to submit comment, or <strong>Shift+Cmd/ctrl+enter</strong> to submit a draft.<br/>
247 <br/>
248 <strong>Draft comments</strong> are private to the author, and trigger no notification to others.<br/>
249 They are permanent until deleted, or converted to regular comments.<br/>
250 <br/>
251 <br/>
252 </div>
253
254 </script>
255
256
257
240 258 ##// END OF EJS Templates
241 259 </div>
242 260
@@ -337,7 +337,6 b' text_monospace = "\'Menlo\', \'Liberation M'
337 337 div.markdown-block img {
338 338 border-style: none;
339 339 background-color: #fff;
340 padding-right: 20px;
341 340 max-width: 100%
342 341 }
343 342
@@ -395,6 +394,13 b' text_monospace = "\'Menlo\', \'Liberation M'
395 394 background-color: #eeeeee
396 395 }
397 396
397 div.markdown-block p {
398 margin-top: 0;
399 margin-bottom: 16px;
400 padding: 0;
401 line-height: unset;
402 }
403
398 404 div.markdown-block code,
399 405 div.markdown-block pre,
400 406 div.markdown-block #ws,
@@ -82,8 +82,8 b''
82 82 <p>
83 83 <strong>Exception ID: <code><a href="${c.exception_id_url}">${c.exception_id}</a></code> </strong> <br/>
84 84
85 Super-admins can see detailed traceback information from this exception by checking the below Exception ID.<br/>
86 Please include the above link for further details of this exception.
85 Super-admins can see details of the above error in the exception tracker found under
86 <a href="${h.route_url('admin_settings_exception_tracker')}">admin > settings > exception tracker</a>.
87 87 </p>
88 88 </div>
89 89 % endif
@@ -29,7 +29,7 b''
29 29 </a>
30 30
31 31 <div class="btn-action-switcher-container right-align">
32 <ul class="btn-action-switcher" role="menu" style="min-width: 200px">
32 <ul class="btn-action-switcher" role="menu" style="min-width: 200px; width: max-content">
33 33 <li>
34 34 <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)}">
35 35 <i class="icon-upload"></i>
@@ -44,18 +44,41 b''
44 44 % endif
45 45
46 46 % if c.enable_downloads:
47 <% at_path = '{}'.format(request.GET.get('at') or c.commit.raw_id[:6]) %>
48 <div class="btn btn-default new-file">
49 % if c.f_path == '/':
50 <a href="${h.route_path('repo_archivefile',repo_name=c.repo_name, fname='{}.zip'.format(c.commit.raw_id))}">
51 ${_('Download full tree ZIP')}
47 <%
48 at_path = '{}'.format(request.GET.get('at') or c.commit.raw_id[:6])
49 if c.f_path == '/':
50 label = _('Full tree as {}')
51 _query = {'with_hash': '1'}
52 else:
53 label = _('This tree as {}')
54 _query = {'at_path':c.f_path, 'with_hash': '1'}
55 %>
56
57 <div class="btn-group btn-group-actions new-file">
58 <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)}">
59 <i class="icon-download"></i>
60 ${label.format('.zip')}
52 61 </a>
53 % else:
54 <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})}">
55 ${_('Download this tree ZIP')}
62
63 <a class="tooltip btn btn-default btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more download options')}">
64 <i class="icon-down"></i>
56 65 </a>
57 % endif
58 </div>
66
67 <div class="btn-action-switcher-container left-align">
68 <ul class="btn-action-switcher" role="menu" style="min-width: 200px; width: max-content">
69 % for a_type, content_type, extension in h.ARCHIVE_SPECS:
70 % if extension not in ['.zip']:
71 <li>
72 <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)}">
73 <i class="icon-download"></i>
74 ${label.format(extension)}
75 </a>
76 </li>
77 % endif
78 % endfor
79 </ul>
80 </div>
81 </div>
59 82 % endif
60 83
61 84 <div class="files-quick-filter">
@@ -7,10 +7,10 b''
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 <style>body{background-color:#eeeeee;}</style>
10 11
11 <style>body{background-color:#eeeeee;}</style>
12 12 <div class="loginbox">
13 <div class="header">
13 <div class="header-account">
14 14 <div id="header-inner" class="title">
15 15 <div id="logo">
16 16 <div class="logo-wrapper">
@@ -28,12 +28,12 b''
28 28 <div class="loginwrapper">
29 29 <rhodecode-toast id="notifications"></rhodecode-toast>
30 30
31 <div class="left-column">
31 <div class="auth-image-wrapper">
32 32 <img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
33 33 </div>
34 34
35 <%block name="above_login_button" />
36 <div id="login" class="right-column">
35 <div id="login">
36 <%block name="above_login_button" />
37 37 <!-- login -->
38 38 <div class="sign-in-title">
39 39 <h1>${_('Sign In using username/password')}</h1>
@@ -10,7 +10,7 b''
10 10 <style>body{background-color:#eeeeee;}</style>
11 11
12 12 <div class="loginbox">
13 <div class="header">
13 <div class="header-account">
14 14 <div id="header-inner" class="title">
15 15 <div id="logo">
16 16 <div class="logo-wrapper">
@@ -27,7 +27,8 b''
27 27
28 28 <div class="loginwrapper">
29 29 <rhodecode-toast id="notifications"></rhodecode-toast>
30 <div class="left-column">
30
31 <div class="auth-image-wrapper">
31 32 <img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
32 33 </div>
33 34
@@ -43,7 +44,7 b''
43 44 </p>
44 45 </div>
45 46 %else:
46 <div id="register" class="right-column">
47 <div id="register">
47 48 <!-- login -->
48 49 <div class="sign-in-title">
49 50 <h1>${_('Reset your Password')}</h1>
@@ -32,8 +32,6 b''
32 32 %>
33 33
34 34 <script type="text/javascript">
35 // TODO: marcink switch this to pyroutes
36 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__')}";
37 35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
38 36 templateContext.pull_request_data.pull_request_version = '${request.GET.get('version', '')}';
39 37 </script>
@@ -552,6 +550,42 b''
552 550 ## CONTENT
553 551 <div class="sidebar-content">
554 552
553 ## Drafts
554 % if c.rhodecode_edition_id == 'EE':
555 <div id="draftsTable" class="sidebar-element clear-both" style="display: ${'block' if c.draft_comments else 'none'}">
556 <div class="tooltip right-sidebar-collapsed-state" style="display: none;" onclick="toggleSidebar(); return false" title="${_('Drafts')}">
557 <i class="icon-comment icon-draft"></i>
558 <span id="drafts-count">${len(c.draft_comments)}</span>
559 </div>
560
561 <div class="right-sidebar-expanded-state pr-details-title">
562 <span style="padding-left: 2px">
563 <input name="select_all_drafts" type="checkbox" onclick="$('[name=submit_draft]').prop('checked', !$('[name=submit_draft]').prop('checked'))">
564 </span>
565 <span class="sidebar-heading noselect" onclick="refreshDraftComments(); return false">
566 <i class="icon-comment icon-draft"></i>
567 ${_('Drafts')}
568 </span>
569 <span class="block-right action_button last-item" onclick="submitDrafts(event)">${_('Submit')}</span>
570 </div>
571
572 <div id="drafts" class="right-sidebar-expanded-state pr-details-content reviewers">
573 % if c.draft_comments:
574 ${sidebar.comments_table(c.draft_comments, len(c.draft_comments), draft_comments=True)}
575 % else:
576 <table class="drafts-content-table">
577 <tr>
578 <td>
579 ${_('No TODOs yet')}
580 </td>
581 </tr>
582 </table>
583 % endif
584 </div>
585
586 </div>
587 % endif
588
555 589 ## RULES SUMMARY/RULES
556 590 <div class="sidebar-element clear-both">
557 591 <% vote_title = _ungettext(
@@ -678,7 +712,7 b''
678 712 % endif
679 713
680 714 ## TODOs
681 <div class="sidebar-element clear-both">
715 <div id="todosTable" class="sidebar-element clear-both">
682 716 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
683 717 <i class="icon-flag-filled"></i>
684 718 <span id="todos-count">${len(c.unresolved_comments)}</span>
@@ -712,7 +746,7 b''
712 746 % if c.unresolved_comments + c.resolved_comments:
713 747 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True)}
714 748 % else:
715 <table>
749 <table class="todos-content-table">
716 750 <tr>
717 751 <td>
718 752 ${_('No TODOs yet')}
@@ -725,7 +759,7 b''
725 759 </div>
726 760
727 761 ## COMMENTS
728 <div class="sidebar-element clear-both">
762 <div id="commentsTable" class="sidebar-element clear-both">
729 763 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
730 764 <i class="icon-comment" style="color: #949494"></i>
731 765 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
@@ -763,7 +797,7 b''
763 797 % if c.inline_comments_flat + c.comments:
764 798 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
765 799 % else:
766 <table>
800 <table class="comments-content-table">
767 801 <tr>
768 802 <td>
769 803 ${_('No Comments yet')}
@@ -846,6 +880,7 b' versionController.init();'
846 880
847 881 reviewersController = new ReviewersController();
848 882 commitsController = new CommitsController();
883 commentsController = new CommentsController();
849 884
850 885 updateController = new UpdatePrController();
851 886
@@ -891,6 +926,23 b' window.setObserversData = ${c.pull_reque'
891 926 );
892 927 };
893 928
929 window.submitDrafts = function (event) {
930 var target = $(event.currentTarget);
931 var callback = function (result) {
932 target.removeAttr('onclick').html('saving...');
933 }
934 var draftIds = [];
935 $.each($('[name=submit_draft]:checked'), function (idx, val) {
936 draftIds.push(parseInt($(val).val()));
937 })
938 if (draftIds.length > 0) {
939 Rhodecode.comments.finalizeDrafts(draftIds, callback);
940 }
941 else {
942
943 }
944 }
945
894 946 window.closePullRequest = function (status) {
895 947 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
896 948 return false;
@@ -980,9 +1032,11 b' window.setObserversData = ${c.pull_reque'
980 1032 $(btns).each(fn_display);
981 1033 });
982 1034
983 // register submit callback on commentForm form to track TODOs
984 window.commentFormGlobalSubmitSuccessCallback = function () {
985 refreshMergeChecks();
1035 // register submit callback on commentForm form to track TODOs, and refresh mergeChecks conditions
1036 window.commentFormGlobalSubmitSuccessCallback = function (comment) {
1037 if (!comment.draft) {
1038 refreshMergeChecks();
1039 }
986 1040 };
987 1041
988 1042 ReviewerAutoComplete('#user', reviewersController);
@@ -994,7 +1048,8 b' window.setObserversData = ${c.pull_reque'
994 1048
995 1049 var channel = '${c.pr_broadcast_channel}';
996 1050 new ReviewerPresenceController(channel)
997
1051 // register globally so inject comment logic can re-use it.
1052 window.commentsController = commentsController;
998 1053 })
999 1054 </script>
1000 1055
@@ -74,6 +74,8 b''
74 74 $pullRequestListTable.DataTable({
75 75 processing: true,
76 76 serverSide: true,
77 stateSave: true,
78 stateDuration: -1,
77 79 ajax: {
78 80 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
79 81 "data": function (d) {
@@ -114,6 +116,10 b''
114 116 if (data['closed']) {
115 117 $(row).addClass('closed');
116 118 }
119 },
120 "stateSaveParams": function (settings, data) {
121 data.search.search = ""; // Don't save search
122 data.start = 0; // don't save pagination
117 123 }
118 124 });
119 125
@@ -10,7 +10,7 b''
10 10 <style>body{background-color:#eeeeee;}</style>
11 11
12 12 <div class="loginbox">
13 <div class="header">
13 <div class="header-account">
14 14 <div id="header-inner" class="title">
15 15 <div id="logo">
16 16 <div class="logo-wrapper">
@@ -27,11 +27,13 b''
27 27
28 28 <div class="loginwrapper">
29 29 <rhodecode-toast id="notifications"></rhodecode-toast>
30 <div class="left-column">
30
31 <div class="auth-image-wrapper">
31 32 <img class="sign-in-image" src="${h.asset('images/sign-in.png')}" alt="RhodeCode"/>
32 33 </div>
33 <%block name="above_register_button" />
34 <div id="register" class="right-column">
34
35 <div id="register">
36 <%block name="above_register_button" />
35 37 <!-- login -->
36 38 <div class="sign-in-title">
37 39 % if external_auth_provider:
@@ -187,7 +187,7 b''
187 187 <div class="enabled pull-left" style="margin-right: 10px">
188 188
189 189 <div class="btn-group btn-group-actions">
190 <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')}">
190 <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'})}">
191 191 <i class="icon-download"></i>
192 192 ${c.rhodecode_db_repo.landing_ref_name}.zip
193 193 ## replaced by some JS on select
@@ -198,12 +198,11 b''
198 198 </a>
199 199
200 200 <div class="btn-action-switcher-container left-align">
201 <ul class="btn-action-switcher" role="menu" style="min-width: 200px">
201 <ul class="btn-action-switcher" role="menu" style="min-width: 200px; width: max-content">
202 202 % for a_type, content_type, extension in h.ARCHIVE_SPECS:
203 203 % if extension not in ['.zip']:
204 204 <li>
205
206 <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)}">
205 <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'})}">
207 206 <i class="icon-download"></i>
208 207 ${c.rhodecode_db_repo.landing_ref_name+extension}
209 208 </a>
@@ -49,23 +49,32 b' class TestArchives(BackendTestMixin):'
49 49 @classmethod
50 50 def _get_commits(cls):
51 51 start_date = datetime.datetime(2010, 1, 1, 20)
52 yield {
53 'message': 'Initial Commit',
54 'author': 'Joe Doe <joe.doe@example.com>',
55 'date': start_date + datetime.timedelta(hours=12),
56 'added': [
57 FileNode('executable_0o100755', '...', mode=0o100755),
58 FileNode('executable_0o100500', '...', mode=0o100500),
59 FileNode('not_executable', '...', mode=0o100644),
60 ],
61 }
52 62 for x in range(5):
53 63 yield {
54 64 'message': 'Commit %d' % x,
55 65 'author': 'Joe Doe <joe.doe@example.com>',
56 66 'date': start_date + datetime.timedelta(hours=12 * x),
57 67 'added': [
58 FileNode(
59 '%d/file_%d.txt' % (x, x), content='Foobar %d' % x),
68 FileNode('%d/file_%d.txt' % (x, x), content='Foobar %d' % x),
60 69 ],
61 70 }
62 71
63 72 @pytest.mark.parametrize('compressor', ['gz', 'bz2'])
64 73 def test_archive_tar(self, compressor):
65 74 self.tip.archive_repo(
66 self.temp_file, kind='t' + compressor, prefix='repo')
75 self.temp_file, kind='t{}'.format(compressor), archive_dir_name='repo')
67 76 out_dir = tempfile.mkdtemp()
68 out_file = tarfile.open(self.temp_file, 'r|' + compressor)
77 out_file = tarfile.open(self.temp_file, 'r|{}'.format(compressor))
69 78 out_file.extractall(out_dir)
70 79 out_file.close()
71 80
@@ -77,8 +86,24 b' class TestArchives(BackendTestMixin):'
77 86
78 87 shutil.rmtree(out_dir)
79 88
89 @pytest.mark.parametrize('compressor', ['gz', 'bz2'])
90 def test_archive_tar_symlink(self, compressor):
91 return False
92
93 @pytest.mark.parametrize('compressor', ['gz', 'bz2'])
94 def test_archive_tar_file_modes(self, compressor):
95 self.tip.archive_repo(
96 self.temp_file, kind='t{}'.format(compressor), archive_dir_name='repo')
97 out_dir = tempfile.mkdtemp()
98 out_file = tarfile.open(self.temp_file, 'r|{}'.format(compressor))
99 out_file.extractall(out_dir)
100 out_file.close()
101 dest = lambda inp: os.path.join(out_dir, 'repo/' + inp)
102
103 assert oct(os.stat(dest('not_executable')).st_mode) == '0100644'
104
80 105 def test_archive_zip(self):
81 self.tip.archive_repo(self.temp_file, kind='zip', prefix='repo')
106 self.tip.archive_repo(self.temp_file, kind='zip', archive_dir_name='repo')
82 107 out = zipfile.ZipFile(self.temp_file)
83 108
84 109 for x in range(5):
@@ -91,10 +116,10 b' class TestArchives(BackendTestMixin):'
91 116
92 117 def test_archive_zip_with_metadata(self):
93 118 self.tip.archive_repo(self.temp_file, kind='zip',
94 prefix='repo', write_metadata=True)
119 archive_dir_name='repo', write_metadata=True)
95 120
96 121 out = zipfile.ZipFile(self.temp_file)
97 metafile = out.read('.archival.txt')
122 metafile = out.read('repo/.archival.txt')
98 123
99 124 raw_id = self.tip.raw_id
100 125 assert 'commit_id:%s' % raw_id in metafile
General Comments 0
You need to be logged in to leave comments. Login now