##// END OF EJS Templates
pull-request: extended default reviewers functionality....
marcink -
r1769:fb9baff8 default
parent child Browse files
Show More
@@ -0,0 +1,83 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22 from rhodecode.model.db import Repository
23
24
25 def route_path(name, params=None, **kwargs):
26 import urllib
27
28 base_url = {
29 'pullrequest_show_all': '/{repo_name}/pull-request',
30 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
31 }[name].format(**kwargs)
32
33 if params:
34 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
35 return base_url
36
37
38 @pytest.mark.backends("git", "hg")
39 @pytest.mark.usefixtures('autologin_user', 'app')
40 class TestPullRequestList(object):
41
42 @pytest.mark.parametrize('params, expected_title', [
43 ({'source': 0, 'closed': 1}, 'Closed Pull Requests'),
44 ({'source': 0, 'my': 1}, 'opened by me'),
45 ({'source': 0, 'awaiting_review': 1}, 'awaiting review'),
46 ({'source': 0, 'awaiting_my_review': 1}, 'awaiting my review'),
47 ({'source': 1}, 'Pull Requests from'),
48 ])
49 def test_showing_list_page(self, backend, pr_util, params, expected_title):
50 pull_request = pr_util.create_pull_request()
51
52 response = self.app.get(
53 route_path('pullrequest_show_all',
54 repo_name=pull_request.target_repo.repo_name,
55 params=params))
56
57 assert_response = response.assert_response()
58 assert_response.element_equals_to('.panel-title', expected_title)
59 element = assert_response.get_element('.panel-title')
60 element_text = assert_response._element_to_string(element)
61
62 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
63 pull_request = pr_util.create_pull_request()
64 response = self.app.get(
65 route_path('pullrequest_show_all_data',
66 repo_name=pull_request.target_repo.repo_name),
67 extra_environ=xhr_header)
68
69 assert response.json['recordsTotal'] == 1
70 assert response.json['data'][0]['description'] == 'Description'
71
72 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
73 xss_description = "<script>alert('Hi!')</script>"
74 pull_request = pr_util.create_pull_request(description=xss_description)
75
76 response = self.app.get(
77 route_path('pullrequest_show_all_data',
78 repo_name=pull_request.target_repo.repo_name),
79 extra_environ=xhr_header)
80
81 assert response.json['recordsTotal'] == 1
82 assert response.json['data'][0]['description'] == \
83 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
@@ -0,0 +1,76 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 from rhodecode.lib import helpers as h
22 from rhodecode.lib.utils2 import safe_int
23
24
25 def reviewer_as_json(user, reasons, mandatory):
26 """
27 Returns json struct of a reviewer for frontend
28
29 :param user: the reviewer
30 :param reasons: list of strings of why they are reviewers
31 :param mandatory: bool, to set user as mandatory
32 """
33
34 return {
35 'user_id': user.user_id,
36 'reasons': reasons,
37 'mandatory': mandatory,
38 'username': user.username,
39 'firstname': user.firstname,
40 'lastname': user.lastname,
41 'gravatar_link': h.gravatar_url(user.email, 14),
42 }
43
44
45 def get_default_reviewers_data(
46 current_user, source_repo, source_commit, target_repo, target_commit):
47
48 """ Return json for default reviewers of a repository """
49
50 reasons = ['Default reviewer', 'Repository owner']
51 default = reviewer_as_json(
52 user=current_user, reasons=reasons, mandatory=False)
53
54 return {
55 'api_ver': 'v1', # define version for later possible schema upgrade
56 'reviewers': [default],
57 'rules': {},
58 'rules_data': {},
59 }
60
61
62 def validate_default_reviewers(review_members, reviewer_rules):
63 """
64 Function to validate submitted reviewers against the saved rules
65
66 """
67 reviewers = []
68 reviewer_by_id = {}
69 for r in review_members:
70 reviewer_user_id = safe_int(r['user_id'])
71 entry = (reviewer_user_id, r['reasons'], r['mandatory'])
72
73 reviewer_by_id[reviewer_user_id] = entry
74 reviewers.append(entry)
75
76 return reviewers
@@ -0,0 +1,37 b''
1 import logging
2
3 from sqlalchemy import *
4 from rhodecode.model import meta
5 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_7_0_1 as db
17
18 repo_review_rule_table = db.RepoReviewRule.__table__
19
20 forbid_author_to_review = Column(
21 "forbid_author_to_review", Boolean(), nullable=True, default=False)
22 forbid_author_to_review.create(table=repo_review_rule_table)
23
24 forbid_adding_reviewers = Column(
25 "forbid_adding_reviewers", Boolean(), nullable=True, default=False)
26 forbid_adding_reviewers.create(table=repo_review_rule_table)
27
28 fixups(db, meta.Session)
29
30
31 def downgrade(migrate_engine):
32 meta = MetaData()
33 meta.bind = migrate_engine
34
35
36 def fixups(models, _SESSION):
37 pass
@@ -0,0 +1,38 b''
1 import logging
2
3 from sqlalchemy import *
4 from rhodecode.model import meta
5 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_7_0_1 as db
17
18 repo_review_rule_user_table = db.RepoReviewRuleUser.__table__
19 repo_review_rule_user_group_table = db.RepoReviewRuleUserGroup.__table__
20
21 mandatory_user = Column(
22 "mandatory", Boolean(), nullable=True, default=False)
23 mandatory_user.create(table=repo_review_rule_user_table)
24
25 mandatory_user_group = Column(
26 "mandatory", Boolean(), nullable=True, default=False)
27 mandatory_user_group.create(table=repo_review_rule_user_group_table)
28
29 fixups(db, meta.Session)
30
31
32 def downgrade(migrate_engine):
33 meta = MetaData()
34 meta.bind = migrate_engine
35
36
37 def fixups(models, _SESSION):
38 pass
@@ -0,0 +1,33 b''
1 import logging
2
3 from sqlalchemy import *
4 from rhodecode.model import meta
5 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_7_0_1 as db
17
18 pull_request_reviewers = db.PullRequestReviewers.__table__
19
20 mandatory = Column(
21 "mandatory", Boolean(), nullable=True, default=False)
22 mandatory.create(table=pull_request_reviewers)
23
24 fixups(db, meta.Session)
25
26
27 def downgrade(migrate_engine):
28 meta = MetaData()
29 meta.bind = migrate_engine
30
31
32 def fixups(models, _SESSION):
33 pass
@@ -0,0 +1,40 b''
1 import logging
2
3 from sqlalchemy import *
4 from rhodecode.model import meta
5 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_7_0_1 as db
17
18 pull_request = db.PullRequest.__table__
19 pull_request_version = db.PullRequestVersion.__table__
20
21 reviewer_data_1 = Column(
22 'reviewer_data_json',
23 db.JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
24 reviewer_data_1.create(table=pull_request)
25
26 reviewer_data_2 = Column(
27 'reviewer_data_json',
28 db.JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
29 reviewer_data_2.create(table=pull_request_version)
30
31 fixups(db, meta.Session)
32
33
34 def downgrade(migrate_engine):
35 meta = MetaData()
36 meta.bind = migrate_engine
37
38
39 def fixups(models, _SESSION):
40 pass
@@ -0,0 +1,34 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import colander
22 from rhodecode.model.validation_schema import validators, preparers, types
23
24
25 class ReviewerSchema(colander.MappingSchema):
26 username = colander.SchemaNode(types.StrOrIntType())
27 reasons = colander.SchemaNode(colander.List(), missing=[])
28 mandatory = colander.SchemaNode(colander.Boolean(), missing=False)
29
30
31 class ReviewerListSchema(colander.SequenceSchema):
32 reviewers = ReviewerSchema()
33
34
@@ -51,7 +51,7 b' PYRAMID_SETTINGS = {}'
51 51 EXTENSIONS = {}
52 52
53 53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 73 # defines current db version for migrations
54 __dbversion__ = 77 # defines current db version for migrations
55 55 __platform__ = platform.system()
56 56 __license__ = 'AGPLv3, and Commercial License'
57 57 __author__ = 'RhodeCode GmbH'
@@ -98,7 +98,12 b' class TestCreatePullRequestApi(object):'
98 98 def test_create_with_reviewers_specified_by_names(
99 99 self, backend, no_notifications):
100 100 data = self._prepare_data(backend)
101 reviewers = [TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN]
101 reviewers = [
102 {'username': TEST_USER_REGULAR_LOGIN,
103 'reasons': ['added manually']},
104 {'username': TEST_USER_ADMIN_LOGIN,
105 'reasons': ['added manually']},
106 ]
102 107 data['reviewers'] = reviewers
103 108 id_, params = build_data(
104 109 self.apikey_regular, 'create_pull_request', **data)
@@ -110,16 +115,26 b' class TestCreatePullRequestApi(object):'
110 115 assert result['result']['msg'] == expected_message
111 116 pull_request_id = result['result']['pull_request_id']
112 117 pull_request = PullRequestModel().get(pull_request_id)
113 actual_reviewers = [r.user.username for r in pull_request.reviewers]
118 actual_reviewers = [
119 {'username': r.user.username,
120 'reasons': ['added manually'],
121 } for r in pull_request.reviewers
122 ]
114 123 assert sorted(actual_reviewers) == sorted(reviewers)
115 124
116 125 @pytest.mark.backends("git", "hg")
117 126 def test_create_with_reviewers_specified_by_ids(
118 127 self, backend, no_notifications):
119 128 data = self._prepare_data(backend)
120 reviewer_names = [TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN]
121 129 reviewers = [
122 UserModel().get_by_username(n).user_id for n in reviewer_names]
130 {'username': UserModel().get_by_username(
131 TEST_USER_REGULAR_LOGIN).user_id,
132 'reasons': ['added manually']},
133 {'username': UserModel().get_by_username(
134 TEST_USER_ADMIN_LOGIN).user_id,
135 'reasons': ['added manually']},
136 ]
137
123 138 data['reviewers'] = reviewers
124 139 id_, params = build_data(
125 140 self.apikey_regular, 'create_pull_request', **data)
@@ -131,14 +146,17 b' class TestCreatePullRequestApi(object):'
131 146 assert result['result']['msg'] == expected_message
132 147 pull_request_id = result['result']['pull_request_id']
133 148 pull_request = PullRequestModel().get(pull_request_id)
134 actual_reviewers = [r.user.username for r in pull_request.reviewers]
135 assert sorted(actual_reviewers) == sorted(reviewer_names)
149 actual_reviewers = [
150 {'username': r.user.user_id,
151 'reasons': ['added manually'],
152 } for r in pull_request.reviewers
153 ]
154 assert sorted(actual_reviewers) == sorted(reviewers)
136 155
137 156 @pytest.mark.backends("git", "hg")
138 157 def test_create_fails_when_the_reviewer_is_not_found(self, backend):
139 158 data = self._prepare_data(backend)
140 reviewers = ['somebody']
141 data['reviewers'] = reviewers
159 data['reviewers'] = [{'username': 'somebody'}]
142 160 id_, params = build_data(
143 161 self.apikey_regular, 'create_pull_request', **data)
144 162 response = api_call(self.app, params)
@@ -153,7 +171,7 b' class TestCreatePullRequestApi(object):'
153 171 id_, params = build_data(
154 172 self.apikey_regular, 'create_pull_request', **data)
155 173 response = api_call(self.app, params)
156 expected_message = 'reviewers should be specified as a list'
174 expected_message = {u'': '"test_regular,test_admin" is not iterable'}
157 175 assert_error(id_, expected_message, given=response.body)
158 176
159 177 @pytest.mark.backends("git", "hg")
@@ -109,7 +109,8 b' class TestGetPullRequest(object):'
109 109 'reasons': reasons,
110 110 'review_status': st[0][1].status if st else 'not_reviewed',
111 111 }
112 for reviewer, reasons, st in pull_request.reviewers_statuses()
112 for reviewer, reasons, mandatory, st in
113 pull_request.reviewers_statuses()
113 114 ]
114 115 }
115 116 assert_ok(id_, expected, response.body)
@@ -119,20 +119,28 b' class TestUpdatePullRequest(object):'
119 119
120 120 @pytest.mark.backends("git", "hg")
121 121 def test_api_update_change_reviewers(
122 self, pr_util, silence_action_logger, no_notifications):
122 self, user_util, pr_util, silence_action_logger, no_notifications):
123 a = user_util.create_user()
124 b = user_util.create_user()
125 c = user_util.create_user()
126 new_reviewers = [
127 {'username': b.username,'reasons': ['updated via API'],
128 'mandatory':False},
129 {'username': c.username, 'reasons': ['updated via API'],
130 'mandatory':False},
131 ]
123 132
124 users = [x.username for x in User.get_all()]
125 new = [users.pop(0)]
126 removed = sorted(new)
127 added = sorted(users)
133 added = [b.username, c.username]
134 removed = [a.username]
128 135
129 pull_request = pr_util.create_pull_request(reviewers=new)
136 pull_request = pr_util.create_pull_request(
137 reviewers=[(a.username, ['added via API'], False)])
130 138
131 139 id_, params = build_data(
132 140 self.apikey, 'update_pull_request',
133 141 repoid=pull_request.target_repo.repo_name,
134 142 pullrequestid=pull_request.pull_request_id,
135 reviewers=added)
143 reviewers=new_reviewers)
136 144 response = api_call(self.app, params)
137 145 expected = {
138 146 "msg": "Updated pull request `{}`".format(
@@ -152,7 +160,7 b' class TestUpdatePullRequest(object):'
152 160 self.apikey, 'update_pull_request',
153 161 repoid=pull_request.target_repo.repo_name,
154 162 pullrequestid=pull_request.pull_request_id,
155 reviewers=['bad_name'])
163 reviewers=[{'username': 'bad_name'}])
156 164 response = api_call(self.app, params)
157 165
158 166 expected = 'user `bad_name` does not exist'
@@ -165,7 +173,7 b' class TestUpdatePullRequest(object):'
165 173 self.apikey, 'update_pull_request',
166 174 repoid='fake',
167 175 pullrequestid='fake',
168 reviewers=['bad_name'])
176 reviewers=[{'username': 'bad_name'}])
169 177 response = api_call(self.app, params)
170 178
171 179 expected = 'repository `fake` does not exist'
@@ -181,7 +189,7 b' class TestUpdatePullRequest(object):'
181 189 self.apikey, 'update_pull_request',
182 190 repoid=pull_request.target_repo.repo_name,
183 191 pullrequestid=999999,
184 reviewers=['bad_name'])
192 reviewers=[{'username': 'bad_name'}])
185 193 response = api_call(self.app, params)
186 194
187 195 expected = 'pull request `999999` does not exist'
@@ -21,7 +21,7 b''
21 21
22 22 import logging
23 23
24 from rhodecode.api import jsonrpc_method, JSONRPCError
24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 25 from rhodecode.api.utils import (
26 26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
@@ -34,6 +34,9 b' from rhodecode.model.comment import Comm'
34 34 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
35 35 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 36 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema.schemas.reviewer_schema import \
39 ReviewerListSchema
37 40
38 41 log = logging.getLogger(__name__)
39 42
@@ -537,7 +540,7 b' def create_pull_request('
537 540 :type reviewers: Optional(list)
538 541 Accepts username strings or objects of the format:
539 542
540 {'username': 'nick', 'reasons': ['original author']}
543 {'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}
541 544 """
542 545
543 546 source = get_repo_or_error(source_repo)
@@ -567,20 +570,19 b' def create_pull_request('
567 570 raise JSONRPCError('no common ancestor found')
568 571
569 572 reviewer_objects = Optional.extract(reviewers) or []
570 if not isinstance(reviewer_objects, list):
571 raise JSONRPCError('reviewers should be specified as a list')
573 if reviewer_objects:
574 schema = ReviewerListSchema()
575 try:
576 reviewer_objects = schema.deserialize(reviewer_objects)
577 except Invalid as err:
578 raise JSONRPCValidationError(colander_exc=err)
572 579
573 reviewers_reasons = []
580 reviewers = []
574 581 for reviewer_object in reviewer_objects:
575 reviewer_reasons = []
576 if isinstance(reviewer_object, (basestring, int)):
577 reviewer_username = reviewer_object
578 else:
579 reviewer_username = reviewer_object['username']
580 reviewer_reasons = reviewer_object.get('reasons', [])
581
582 user = get_user_or_error(reviewer_username)
583 reviewers_reasons.append((user.user_id, reviewer_reasons))
582 user = get_user_or_error(reviewer_object['username'])
583 reasons = reviewer_object['reasons']
584 mandatory = reviewer_object['mandatory']
585 reviewers.append((user.user_id, reasons, mandatory))
584 586
585 587 pull_request_model = PullRequestModel()
586 588 pull_request = pull_request_model.create(
@@ -591,7 +593,7 b' def create_pull_request('
591 593 target_ref=full_target_ref,
592 594 revisions=reversed(
593 595 [commit.raw_id for commit in reversed(commit_ranges)]),
594 reviewers=reviewers_reasons,
596 reviewers=reviewers,
595 597 title=title,
596 598 description=Optional.extract(description)
597 599 )
@@ -624,6 +626,10 b' def update_pull_request('
624 626 :type description: Optional(str)
625 627 :param reviewers: Update pull request reviewers list with new value.
626 628 :type reviewers: Optional(list)
629 Accepts username strings or objects of the format:
630
631 {'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}
632
627 633 :param update_commits: Trigger update of commits for this pull request
628 634 :type: update_commits: Optional(bool)
629 635 :param close_pull_request: Close this pull request with rejected state
@@ -669,23 +675,21 b' def update_pull_request('
669 675 'pull request `%s` update failed, pull request is closed' % (
670 676 pullrequestid,))
671 677
678
672 679 reviewer_objects = Optional.extract(reviewers) or []
673 if not isinstance(reviewer_objects, list):
674 raise JSONRPCError('reviewers should be specified as a list')
680 if reviewer_objects:
681 schema = ReviewerListSchema()
682 try:
683 reviewer_objects = schema.deserialize(reviewer_objects)
684 except Invalid as err:
685 raise JSONRPCValidationError(colander_exc=err)
675 686
676 reviewers_reasons = []
677 reviewer_ids = set()
687 reviewers = []
678 688 for reviewer_object in reviewer_objects:
679 reviewer_reasons = []
680 if isinstance(reviewer_object, (int, basestring)):
681 reviewer_username = reviewer_object
682 else:
683 reviewer_username = reviewer_object['username']
684 reviewer_reasons = reviewer_object.get('reasons', [])
685
686 user = get_user_or_error(reviewer_username)
687 reviewer_ids.add(user.user_id)
688 reviewers_reasons.append((user.user_id, reviewer_reasons))
689 user = get_user_or_error(reviewer_object['username'])
690 reasons = reviewer_object['reasons']
691 mandatory = reviewer_object['mandatory']
692 reviewers.append((user.user_id, reasons, mandatory))
689 693
690 694 title = Optional.extract(title)
691 695 description = Optional.extract(description)
@@ -704,9 +708,9 b' def update_pull_request('
704 708 Session().commit()
705 709
706 710 reviewers_changes = {"added": [], "removed": []}
707 if reviewer_ids:
711 if reviewers:
708 712 added_reviewers, removed_reviewers = \
709 PullRequestModel().update_reviewers(pull_request, reviewers_reasons)
713 PullRequestModel().update_reviewers(pull_request, reviewers)
710 714
711 715 reviewers_changes['added'] = sorted(
712 716 [get_user_or_error(n).username for n in added_reviewers])
@@ -290,6 +290,13 b' class RepoTypeRoutePredicate(object):'
290 290 else:
291 291 log.warning('Current view is not supported for repo type:%s',
292 292 rhodecode_db_repo.repo_type)
293 #
294 # h.flash(h.literal(
295 # _('Action not supported for %s.' % rhodecode_repo.alias)),
296 # category='warning')
297 # return redirect(
298 # url('summary_home', repo_name=cls.rhodecode_db_repo.repo_name))
299
293 300 return False
294 301
295 302
@@ -23,7 +23,7 b' import logging'
23 23 from pyramid.view import view_config
24 24
25 25 from rhodecode.apps._base import RepoAppView
26 from rhodecode.controllers import utils
26 from rhodecode.apps.repository.utils import get_default_reviewers_data
27 27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
28 28
29 29 log = logging.getLogger(__name__)
@@ -57,13 +57,8 b' class RepoReviewRulesView(RepoAppView):'
57 57 route_name='repo_default_reviewers_data', request_method='GET',
58 58 renderer='json_ext')
59 59 def repo_default_reviewers_data(self):
60 reasons = ['Default reviewer', 'Repository owner']
61 default = utils.reviewer_as_json(
62 user=self.db_repo.user, reasons=reasons, mandatory=False)
60 review_data = get_default_reviewers_data(
61 self.db_repo.user, None, None, None, None)
62 return review_data
63 63
64 return {
65 'api_ver': 'v1', # define version for later possible schema upgrade
66 'reviewers': [default],
67 'rules': {},
68 'rules_data': {},
69 }
64
@@ -228,8 +228,6 b' class PullrequestsController(BaseRepoCon'
228 228 target_repo = _form['target_repo']
229 229 target_ref = _form['target_ref']
230 230 commit_ids = _form['revisions'][::-1]
231 reviewers = [
232 (r['user_id'], r['reasons']) for r in _form['review_members']]
233 231
234 232 # find the ancestor for this pr
235 233 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
@@ -257,17 +255,29 b' class PullrequestsController(BaseRepoCon'
257 255 )
258 256
259 257 description = _form['pullrequest_desc']
258
259 get_default_reviewers_data, validate_default_reviewers = \
260 PullRequestModel().get_reviewer_functions()
261
262 # recalculate reviewers logic, to make sure we can validate this
263 reviewer_rules = get_default_reviewers_data(
264 c.rhodecode_user, source_db_repo, source_commit, target_db_repo,
265 target_commit)
266
267 reviewers = validate_default_reviewers(
268 _form['review_members'], reviewer_rules)
269
260 270 try:
261 271 pull_request = PullRequestModel().create(
262 272 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
263 273 target_ref, commit_ids, reviewers, pullrequest_title,
264 description
274 description, reviewer_rules
265 275 )
266 276 Session().commit()
267 277 h.flash(_('Successfully opened new pull request'),
268 278 category='success')
269 279 except Exception as e:
270 msg = _('Error occurred during sending pull request')
280 msg = _('Error occurred during creation of this pull request.')
271 281 log.exception(msg)
272 282 h.flash(msg, category='error')
273 283 return redirect(url('pullrequest_home', repo_name=repo_name))
@@ -292,7 +302,8 b' class PullrequestsController(BaseRepoCon'
292 302
293 303 if 'review_members' in controls:
294 304 self._update_reviewers(
295 pull_request_id, controls['review_members'])
305 pull_request_id, controls['review_members'],
306 pull_request.reviewer_data)
296 307 elif str2bool(request.POST.get('update_commits', 'false')):
297 308 self._update_commits(pull_request)
298 309 elif str2bool(request.POST.get('close_pull_request', 'false')):
@@ -435,10 +446,20 b' class PullrequestsController(BaseRepoCon'
435 446 merge_resp.failure_reason)
436 447 h.flash(msg, category='error')
437 448
438 def _update_reviewers(self, pull_request_id, review_members):
439 reviewers = [
440 (int(r['user_id']), r['reasons']) for r in review_members]
449 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
450
451 get_default_reviewers_data, validate_default_reviewers = \
452 PullRequestModel().get_reviewer_functions()
453
454 try:
455 reviewers = validate_default_reviewers(review_members, reviewer_rules)
456 except ValueError as e:
457 log.error('Reviewers Validation:{}'.format(e))
458 h.flash(e, category='error')
459 return
460
441 461 PullRequestModel().update_reviewers(pull_request_id, reviewers)
462 h.flash(_('Pull request reviewers updated.'), category='success')
442 463 Session().commit()
443 464
444 465 def _reject_close(self, pull_request):
@@ -490,7 +511,8 b' class PullrequestsController(BaseRepoCon'
490 511 _org_pull_request_obj = pull_request_ver.pull_request
491 512 at_version = pull_request_ver.pull_request_version_id
492 513 else:
493 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
514 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
515 pull_request_id)
494 516
495 517 pull_request_display_obj = PullRequest.get_pr_display_object(
496 518 pull_request_obj, _org_pull_request_obj)
@@ -597,9 +619,9 b' class PullrequestsController(BaseRepoCon'
597 619 c.allowed_to_comment = False
598 620 c.allowed_to_close = False
599 621 else:
600 c.allowed_to_change_status = PullRequestModel(). \
601 check_user_change_status(pull_request_at_ver, c.rhodecode_user) \
602 and not pr_closed
622 can_change_status = PullRequestModel().check_user_change_status(
623 pull_request_at_ver, c.rhodecode_user)
624 c.allowed_to_change_status = can_change_status and not pr_closed
603 625
604 626 c.allowed_to_update = PullRequestModel().check_user_update(
605 627 pull_request_latest, c.rhodecode_user) and not pr_closed
@@ -610,6 +632,18 b' class PullrequestsController(BaseRepoCon'
610 632 c.allowed_to_comment = not pr_closed
611 633 c.allowed_to_close = c.allowed_to_merge and not pr_closed
612 634
635 c.forbid_adding_reviewers = False
636 c.forbid_author_to_review = False
637
638 if pull_request_latest.reviewer_data and \
639 'rules' in pull_request_latest.reviewer_data:
640 rules = pull_request_latest.reviewer_data['rules'] or {}
641 try:
642 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
643 c.forbid_author_to_review = rules.get('forbid_author_to_review')
644 except Exception:
645 pass
646
613 647 # check merge capabilities
614 648 _merge_check = MergeCheck.validate(
615 649 pull_request_latest, user=c.rhodecode_user)
@@ -724,12 +758,18 b' class PullrequestsController(BaseRepoCon'
724 758 c.commit_ranges.append(comm)
725 759 commit_cache[comm.raw_id] = comm
726 760
761 # Order here matters, we first need to get target, and then
762 # the source
727 763 target_commit = commits_source_repo.get_commit(
728 764 commit_id=safe_str(target_ref_id))
765
729 766 source_commit = commits_source_repo.get_commit(
730 767 commit_id=safe_str(source_ref_id))
768
731 769 except CommitDoesNotExistError:
732 pass
770 log.warning(
771 'Failed to get commit from `{}` repo'.format(
772 commits_source_repo), exc_info=True)
733 773 except RepositoryRequirementError:
734 774 log.warning(
735 775 'Failed to get all required data from repo', exc_info=True)
@@ -87,23 +87,3 b' def get_commit_from_ref_name(repo, ref_n'
87 87 '%s "%s" does not exist' % (ref_type, ref_name))
88 88
89 89 return repo_scm.get_commit(commit_id)
90
91
92 def reviewer_as_json(user, reasons, mandatory):
93 """
94 Returns json struct of a reviewer for frontend
95
96 :param user: the reviewer
97 :param reasons: list of strings of why they are reviewers
98 :param mandatory: bool, to set user as mandatory
99 """
100
101 return {
102 'user_id': user.user_id,
103 'reasons': reasons,
104 'mandatory': mandatory,
105 'username': user.username,
106 'firstname': user.firstname,
107 'lastname': user.lastname,
108 'gravatar_link': h.gravatar_url(user.email, 14),
109 }
@@ -80,7 +80,7 b' class ChangesetStatusModel(BaseModel):'
80 80 """
81 81 votes = defaultdict(int)
82 82 reviewers_number = len(statuses_by_reviewers)
83 for user, reasons, statuses in statuses_by_reviewers:
83 for user, reasons, mandatory, statuses in statuses_by_reviewers:
84 84 if statuses:
85 85 ver, latest = statuses[0]
86 86 votes[latest.status] += 1
@@ -248,13 +248,14 b' class ChangesetStatusModel(BaseModel):'
248 248 for o in pull_request.reviewers:
249 249 if not o.user:
250 250 continue
251 st = commit_statuses.get(o.user.username, None)
252 if st:
253 st = [(x, list(y)[0])
254 for x, y in (itertools.groupby(sorted(st, key=version),
255 version))]
251 statuses = commit_statuses.get(o.user.username, None)
252 if statuses:
253 statuses = [(x, list(y)[0])
254 for x, y in (itertools.groupby(
255 sorted(statuses, key=version),version))]
256 256
257 pull_request_reviewers.append((o.user, o.reasons, st))
257 pull_request_reviewers.append(
258 (o.user, o.reasons, o.mandatory, statuses))
258 259 return pull_request_reviewers
259 260
260 261 def calculated_review_status(self, pull_request, reviewers_statuses=None):
@@ -3229,6 +3229,14 b' class _PullRequestBase(BaseModel):'
3229 3229 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3230 3230 merge_rev = Column('merge_rev', String(40), nullable=True)
3231 3231
3232 reviewer_data = Column(
3233 'reviewer_data_json', MutationObj.as_mutable(
3234 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3235
3236 @property
3237 def reviewer_data_json(self):
3238 return json.dumps(self.reviewer_data)
3239
3232 3240 @hybrid_property
3233 3241 def revisions(self):
3234 3242 return self._revisions.split(':') if self._revisions else []
@@ -3348,7 +3356,8 b' class _PullRequestBase(BaseModel):'
3348 3356 'reasons': reasons,
3349 3357 'review_status': st[0][1].status if st else 'not_reviewed',
3350 3358 }
3351 for reviewer, reasons, st in pull_request.reviewers_statuses()
3359 for reviewer, reasons, mandatory, st in
3360 pull_request.reviewers_statuses()
3352 3361 ]
3353 3362 }
3354 3363
@@ -3438,6 +3447,8 b' class PullRequest(Base, _PullRequestBase'
3438 3447 attrs.revisions = pull_request_obj.revisions
3439 3448
3440 3449 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3450 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3451 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3441 3452
3442 3453 return PullRequestDisplay(attrs, internal=internal_methods)
3443 3454
@@ -3516,11 +3527,6 b' class PullRequestReviewers(Base, BaseMod'
3516 3527 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3517 3528 )
3518 3529
3519 def __init__(self, user=None, pull_request=None, reasons=None):
3520 self.user = user
3521 self.pull_request = pull_request
3522 self.reasons = reasons or []
3523
3524 3530 @hybrid_property
3525 3531 def reasons(self):
3526 3532 if not self._reasons:
@@ -3545,7 +3551,7 b' class PullRequestReviewers(Base, BaseMod'
3545 3551 _reasons = Column(
3546 3552 'reason', MutationList.as_mutable(
3547 3553 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3548
3554 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3549 3555 user = relationship('User')
3550 3556 pull_request = relationship('PullRequest')
3551 3557
@@ -3849,14 +3855,17 b' class RepoReviewRuleUser(Base, BaseModel'
3849 3855 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3850 3856 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3851 3857 )
3852 repo_review_rule_user_id = Column(
3853 'repo_review_rule_user_id', Integer(), primary_key=True)
3854 repo_review_rule_id = Column("repo_review_rule_id",
3855 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3856 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3857 nullable=False)
3858 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
3859 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3860 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
3861 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3858 3862 user = relationship('User')
3859 3863
3864 def rule_data(self):
3865 return {
3866 'mandatory': self.mandatory
3867 }
3868
3860 3869
3861 3870 class RepoReviewRuleUserGroup(Base, BaseModel):
3862 3871 __tablename__ = 'repo_review_rules_users_groups'
@@ -3864,14 +3873,17 b' class RepoReviewRuleUserGroup(Base, Base'
3864 3873 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3865 3874 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3866 3875 )
3867 repo_review_rule_users_group_id = Column(
3868 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3869 repo_review_rule_id = Column("repo_review_rule_id",
3870 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3871 users_group_id = Column("users_group_id", Integer(),
3872 ForeignKey('users_groups.users_group_id'), nullable=False)
3876 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
3877 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3878 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
3879 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3873 3880 users_group = relationship('UserGroup')
3874 3881
3882 def rule_data(self):
3883 return {
3884 'mandatory': self.mandatory
3885 }
3886
3875 3887
3876 3888 class RepoReviewRule(Base, BaseModel):
3877 3889 __tablename__ = 'repo_review_rules'
@@ -3886,13 +3898,13 b' class RepoReviewRule(Base, BaseModel):'
3886 3898 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3887 3899 repo = relationship('Repository', backref='review_rules')
3888 3900
3889 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3890 default=u'*') # glob
3891 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3892 default=u'*') # glob
3893
3894 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3895 nullable=False, default=False)
3901 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3902 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3903
3904 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
3905 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
3906 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
3907
3896 3908 rule_users = relationship('RepoReviewRuleUser')
3897 3909 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3898 3910
@@ -3948,16 +3960,26 b' class RepoReviewRule(Base, BaseModel):'
3948 3960 def review_users(self):
3949 3961 """ Returns the users which this rule applies to """
3950 3962
3951 users = set()
3952 users |= set([
3953 rule_user.user for rule_user in self.rule_users
3954 if rule_user.user.active])
3955 users |= set(
3956 member.user
3957 for rule_user_group in self.rule_user_groups
3958 for member in rule_user_group.users_group.members
3959 if member.user.active
3960 )
3963 users = collections.OrderedDict()
3964
3965 for rule_user in self.rule_users:
3966 if rule_user.user.active:
3967 if rule_user.user not in users:
3968 users[rule_user.user.username] = {
3969 'user': rule_user.user,
3970 'source': 'user',
3971 'data': rule_user.rule_data()
3972 }
3973
3974 for rule_user_group in self.rule_user_groups:
3975 for member in rule_user_group.users_group.members:
3976 if member.user.active:
3977 users[member.user.username] = {
3978 'user': member.user,
3979 'source': 'user_group',
3980 'data': rule_user_group.rule_data()
3981 }
3982
3961 3983 return users
3962 3984
3963 3985 def __repr__(self):
@@ -535,12 +535,13 b' def PullRequestForm(repo_id):'
535 535 class ReviewerForm(formencode.Schema):
536 536 user_id = v.Int(not_empty=True)
537 537 reasons = All()
538 mandatory = v.StringBoolean()
538 539
539 540 class _PullRequestForm(formencode.Schema):
540 541 allow_extra_fields = True
541 542 filter_extra_fields = True
542 543
543 user = v.UnicodeString(strip=True, required=True)
544 common_ancestor = v.UnicodeString(strip=True, required=True)
544 545 source_repo = v.UnicodeString(strip=True, required=True)
545 546 source_ref = v.UnicodeString(strip=True, required=True)
546 547 target_repo = v.UnicodeString(strip=True, required=True)
@@ -415,7 +415,9 b' class PullRequestModel(BaseModel):'
415 415 .all()
416 416
417 417 def create(self, created_by, source_repo, source_ref, target_repo,
418 target_ref, revisions, reviewers, title, description=None):
418 target_ref, revisions, reviewers, title, description=None,
419 reviewer_data=None):
420
419 421 created_by_user = self._get_user(created_by)
420 422 source_repo = self._get_repo(source_repo)
421 423 target_repo = self._get_repo(target_repo)
@@ -429,6 +431,7 b' class PullRequestModel(BaseModel):'
429 431 pull_request.title = title
430 432 pull_request.description = description
431 433 pull_request.author = created_by_user
434 pull_request.reviewer_data = reviewer_data
432 435
433 436 Session().add(pull_request)
434 437 Session().flush()
@@ -436,15 +439,16 b' class PullRequestModel(BaseModel):'
436 439 reviewer_ids = set()
437 440 # members / reviewers
438 441 for reviewer_object in reviewers:
439 if isinstance(reviewer_object, tuple):
440 user_id, reasons = reviewer_object
441 else:
442 user_id, reasons = reviewer_object, []
442 user_id, reasons, mandatory = reviewer_object
443 443
444 444 user = self._get_user(user_id)
445 445 reviewer_ids.add(user.user_id)
446 446
447 reviewer = PullRequestReviewers(user, pull_request, reasons)
447 reviewer = PullRequestReviewers()
448 reviewer.user = user
449 reviewer.pull_request = pull_request
450 reviewer.reasons = reasons
451 reviewer.mandatory = mandatory
448 452 Session().add(reviewer)
449 453
450 454 # Set approval status to "Under Review" for all commits which are
@@ -763,6 +767,7 b' class PullRequestModel(BaseModel):'
763 767 version._last_merge_status = pull_request._last_merge_status
764 768 version.shadow_merge_ref = pull_request.shadow_merge_ref
765 769 version.merge_rev = pull_request.merge_rev
770 version.reviewer_data = pull_request.reviewer_data
766 771
767 772 version.revisions = pull_request.revisions
768 773 version.pull_request = pull_request
@@ -903,16 +908,18 b' class PullRequestModel(BaseModel):'
903 908 Update the reviewers in the pull request
904 909
905 910 :param pull_request: the pr to update
906 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
911 :param reviewer_data: list of tuples
912 [(user, ['reason1', 'reason2'], mandatory_flag)]
907 913 """
908 914
909 reviewers_reasons = {}
910 for user_id, reasons in reviewer_data:
915 reviewers = {}
916 for user_id, reasons, mandatory in reviewer_data:
911 917 if isinstance(user_id, (int, basestring)):
912 918 user_id = self._get_user(user_id).user_id
913 reviewers_reasons[user_id] = reasons
919 reviewers[user_id] = {
920 'reasons': reasons, 'mandatory': mandatory}
914 921
915 reviewers_ids = set(reviewers_reasons.keys())
922 reviewers_ids = set(reviewers.keys())
916 923 pull_request = self.__get_pull_request(pull_request)
917 924 current_reviewers = PullRequestReviewers.query()\
918 925 .filter(PullRequestReviewers.pull_request ==
@@ -928,8 +935,12 b' class PullRequestModel(BaseModel):'
928 935 for uid in ids_to_add:
929 936 changed = True
930 937 _usr = self._get_user(uid)
931 reasons = reviewers_reasons[uid]
932 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
938 reviewer = PullRequestReviewers()
939 reviewer.user = _usr
940 reviewer.pull_request = pull_request
941 reviewer.reasons = reviewers[uid]['reasons']
942 # NOTE(marcink): mandatory shouldn't be changed now
943 #reviewer.mandatory = reviewers[uid]['reasons']
933 944 Session().add(reviewer)
934 945
935 946 for uid in ids_to_remove:
@@ -1369,6 +1380,23 b' class PullRequestModel(BaseModel):'
1369 1380 action=action, pr_id=pull_request.pull_request_id),
1370 1381 pull_request.target_repo)
1371 1382
1383 def get_reviewer_functions(self):
1384 """
1385 Fetches functions for validation and fetching default reviewers.
1386 If available we use the EE package, else we fallback to CE
1387 package functions
1388 """
1389 try:
1390 from rc_reviewers.utils import get_default_reviewers_data
1391 from rc_reviewers.utils import validate_default_reviewers
1392 except ImportError:
1393 from rhodecode.apps.repository.utils import \
1394 get_default_reviewers_data
1395 from rhodecode.apps.repository.utils import \
1396 validate_default_reviewers
1397
1398 return get_default_reviewers_data, validate_default_reviewers
1399
1372 1400
1373 1401 class MergeCheck(object):
1374 1402 """
@@ -190,7 +190,7 b' class UserGroupType(UserOrUserGroupType)'
190 190
191 191 class StrOrIntType(colander.String):
192 192 def deserialize(self, node, cstruct):
193 if isinstance(node, basestring):
193 if isinstance(cstruct, basestring):
194 194 return super(StrOrIntType, self).deserialize(node, cstruct)
195 195 else:
196 196 return colander.Integer().deserialize(node, cstruct)
@@ -62,6 +62,10 b' input[type="button"] {'
62 62 text-shadow: none;
63 63 }
64 64
65 &.no-margin {
66 margin: 0 0 0 0;
67 }
68
65 69 }
66 70
67 71
@@ -270,11 +274,17 b' input[type="button"] {'
270 274 font-size: inherit;
271 275 color: @rcblue;
272 276 border: none;
273 .border-radius (0);
277 border-radius: 0;
274 278 background-color: transparent;
275 279
280 &.last-item {
281 border: none;
282 padding: 0 0 0 0;
283 }
284
276 285 &:last-child {
277 286 border: none;
287 padding: 0 0 0 0;
278 288 }
279 289
280 290 &:hover {
@@ -12,7 +12,7 b''
12 12
13 13 .control-label {
14 14 width: 200px;
15 padding: 10px;
15 padding: 10px 0px;
16 16 float: left;
17 17 }
18 18 .control-inputs {
@@ -359,9 +359,26 b' ul.auth_plugins {'
359 359 }
360 360 }
361 361
362 .pr-mergeinfo {
363 clear: both;
364 margin: .5em 0;
365
366 input {
367 min-width: 100% !important;
368 padding: 0 !important;
369 border: 0;
370 }
371 }
372
362 373 .pr-pullinfo {
363 374 clear: both;
364 375 margin: .5em 0;
376
377 input {
378 min-width: 100% !important;
379 padding: 0 !important;
380 border: 0;
381 }
365 382 }
366 383
367 384 #pr-title-input {
@@ -1298,6 +1315,11 b' table.integrations {'
1298 1315 width: 100%;
1299 1316 margin-bottom: 8px;
1300 1317 }
1318
1319 .reviewer_entry {
1320 min-height: 55px;
1321 }
1322
1301 1323 .reviewers_member {
1302 1324 width: 100%;
1303 1325 overflow: auto;
@@ -1332,6 +1354,8 b' table.integrations {'
1332 1354 }
1333 1355 }
1334 1356
1357 .reviewer_member_mandatory,
1358 .reviewer_member_mandatory_remove,
1335 1359 .reviewer_member_remove {
1336 1360 position: absolute;
1337 1361 right: 0;
@@ -1341,6 +1365,15 b' table.integrations {'
1341 1365 padding: 0;
1342 1366 color: black;
1343 1367 }
1368
1369 .reviewer_member_mandatory_remove {
1370 color: @grey4;
1371 }
1372
1373 .reviewer_member_mandatory {
1374 padding-top:20px;
1375 }
1376
1344 1377 .reviewer_member_status {
1345 1378 margin-top: 5px;
1346 1379 }
@@ -1370,6 +1403,11 b' table.integrations {'
1370 1403 .pr-description {
1371 1404 white-space:pre-wrap;
1372 1405 }
1406
1407 .pr-reviewer-rules {
1408 padding: 10px 0px 20px 0px;
1409 }
1410
1373 1411 .group_members {
1374 1412 margin-top: 0;
1375 1413 padding: 0;
@@ -16,10 +16,215 b''
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19
20 var prButtonLockChecks = {
21 'compare': false,
22 'reviewers': false
23 };
24
19 25 /**
20 * Pull request reviewers
26 * lock button until all checks and loads are made. E.g reviewer calculation
27 * should prevent from submitting a PR
28 * @param lockEnabled
29 * @param msg
30 * @param scope
31 */
32 var prButtonLock = function(lockEnabled, msg, scope) {
33 scope = scope || 'all';
34 if (scope == 'all'){
35 prButtonLockChecks['compare'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 } else if (scope == 'compare') {
38 prButtonLockChecks['compare'] = !lockEnabled;
39 } else if (scope == 'reviewers'){
40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 }
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 if (lockEnabled) {
44 $('#save').attr('disabled', 'disabled');
45 }
46 else if (checksMeet) {
47 $('#save').removeAttr('disabled');
48 }
49
50 if (msg) {
51 $('#pr_open_message').html(msg);
52 }
53 };
54
55
56 /**
57 Generate Title and Description for a PullRequest.
58 In case of 1 commits, the title and description is that one commit
59 in case of multiple commits, we iterate on them with max N number of commits,
60 and build description in a form
61 - commitN
62 - commitN+1
63 ...
64
65 Title is then constructed from branch names, or other references,
66 replacing '-' and '_' into spaces
67
68 * @param sourceRef
69 * @param elements
70 * @param limit
71 * @returns {*[]}
21 72 */
22 var removeReviewMember = function(reviewer_id, mark_delete){
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
74 var title = '';
75 var desc = '';
76
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 });
81 // only 1 commit, use commit message as title
82 if (elements.length === 1) {
83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
84 }
85 else {
86 // use reference name
87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
88 }
89
90 return [title, desc]
91 };
92
93
94
95 ReviewersController = function () {
96 var self = this;
97 this.$reviewRulesContainer = $('#review_rules');
98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
99 this.forbidReviewUsers = undefined;
100 this.$reviewMembers = $('#review_members');
101 this.currentRequest = null;
102
103 this.defaultForbidReviewUsers = function() {
104 return [
105 {'username': 'default',
106 'user_id': templateContext.default_user.user_id}
107 ];
108 };
109
110 this.hideReviewRules = function() {
111 self.$reviewRulesContainer.hide();
112 };
113
114 this.showReviewRules = function() {
115 self.$reviewRulesContainer.show();
116 };
117
118 this.addRule = function(ruleText) {
119 self.showReviewRules();
120 return '<div>- {0}</div>'.format(ruleText)
121 };
122
123 this.loadReviewRules = function(data) {
124 // reset forbidden Users
125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
126
127 // reset state of review rules
128 self.$rulesList.html('');
129
130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
131 // default rule, case for older repo that don't have any rules stored
132 self.$rulesList.append(
133 self.addRule(
134 _gettext('All reviewers must vote.'))
135 );
136 return self.forbidReviewUsers
137 }
138
139 if (data.rules.voting !== undefined) {
140 if (data.rules.voting < 0){
141 self.$rulesList.append(
142 self.addRule(
143 _gettext('All reviewers must vote.'))
144 )
145 } else if (data.rules.voting === 1) {
146 self.$rulesList.append(
147 self.addRule(
148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
149 )
150
151 } else {
152 self.$rulesList.append(
153 self.addRule(
154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
155 )
156 }
157 }
158 if (data.rules.use_code_authors_for_review) {
159 self.$rulesList.append(
160 self.addRule(
161 _gettext('Reviewers picked from source code changes.'))
162 )
163 }
164 if (data.rules.forbid_adding_reviewers) {
165 $('#add_reviewer_input').remove();
166 self.$rulesList.append(
167 self.addRule(
168 _gettext('Adding new reviewers is forbidden.'))
169 )
170 }
171 if (data.rules.forbid_author_to_review) {
172 self.forbidReviewUsers.push(data.rules_data.pr_author);
173 self.$rulesList.append(
174 self.addRule(
175 _gettext('Author is not allowed to be a reviewer.'))
176 )
177 }
178 return self.forbidReviewUsers
179 };
180
181 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
182
183 if (self.currentRequest) {
184 // make sure we cleanup old running requests before triggering this
185 // again
186 self.currentRequest.abort();
187 }
188
189 $('.calculate-reviewers').show();
190 // reset reviewer members
191 self.$reviewMembers.empty();
192
193 prButtonLock(true, null, 'reviewers');
194 $('#user').hide(); // hide user autocomplete before load
195
196 var url = pyroutes.url('repo_default_reviewers_data',
197 {
198 'repo_name': templateContext.repo_name,
199 'source_repo': sourceRepo,
200 'source_ref': sourceRef[2],
201 'target_repo': targetRepo,
202 'target_ref': targetRef[2]
203 });
204
205 self.currentRequest = $.get(url)
206 .done(function(data) {
207 self.currentRequest = null;
208
209 // review rules
210 self.loadReviewRules(data);
211
212 for (var i = 0; i < data.reviewers.length; i++) {
213 var reviewer = data.reviewers[i];
214 self.addReviewMember(
215 reviewer.user_id, reviewer.firstname,
216 reviewer.lastname, reviewer.username,
217 reviewer.gravatar_link, reviewer.reasons,
218 reviewer.mandatory);
219 }
220 $('.calculate-reviewers').hide();
221 prButtonLock(false, null, 'reviewers');
222 $('#user').show(); // show user autocomplete after load
223 });
224 };
225
226 // check those, refactor
227 this.removeReviewMember = function(reviewer_id, mark_delete) {
23 228 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
24 229
25 230 if(typeof(mark_delete) === undefined){
@@ -41,18 +246,21 b' var removeReviewMember = function(review'
41 246 }
42 247 };
43 248
44 var addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons) {
45 var members = $('#review_members').get(0);
249 this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) {
250 var members = self.$reviewMembers.get(0);
46 251 var reasons_html = '';
47 252 var reasons_inputs = '';
48 253 var reasons = reasons || [];
254 var mandatory = mandatory || false;
255
49 256 if (reasons) {
50 257 for (var i = 0; i < reasons.length; i++) {
51 258 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
52 259 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
53 260 }
54 261 }
55 var tmpl = '<li id="reviewer_{2}">'+
262 var tmpl = '' +
263 '<li id="reviewer_{2}" class="reviewer_entry">'+
56 264 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
57 265 '<div class="reviewer_status">'+
58 266 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
@@ -63,11 +271,27 b' var addReviewMember = function(id, fname'
63 271 '<input type="hidden" name="user_id" value="{2}">'+
64 272 '<input type="hidden" name="__start__" value="reasons:sequence">'+
65 273 '{3}'+
66 '<input type="hidden" name="__end__" value="reasons:sequence">'+
67 '<div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">' +
274 '<input type="hidden" name="__end__" value="reasons:sequence">';
275
276 if (mandatory) {
277 tmpl += ''+
278 '<div class="reviewer_member_mandatory_remove">' +
68 279 '<i class="icon-remove-sign"></i>'+
69 280 '</div>'+
70 '</div>'+
281 '<input type="hidden" name="mandatory" value="true">'+
282 '<div class="reviewer_member_mandatory">' +
283 '<i class="icon-lock" title="Mandatory reviewer"></i>'+
284 '</div>';
285
286 } else {
287 tmpl += ''+
288 '<input type="hidden" name="mandatory" value="false">'+
289 '<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember({2})">' +
290 '<i class="icon-remove-sign"></i>'+
291 '</div>';
292 }
293 // continue template
294 tmpl += ''+
71 295 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
72 296 '</li>' ;
73 297
@@ -76,17 +300,43 b' var addReviewMember = function(id, fname'
76 300 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
77 301 // check if we don't have this ID already in
78 302 var ids = [];
79 var _els = $('#review_members li').toArray();
303 var _els = self.$reviewMembers.find('li').toArray();
80 304 for (el in _els){
81 305 ids.push(_els[el].id)
82 306 }
83 if(ids.indexOf('reviewer_'+id) == -1){
307
308 var userAllowedReview = function(userId) {
309 var allowed = true;
310 $.each(self.forbidReviewUsers, function(index, member_data) {
311 if (parseInt(userId) === member_data['user_id']) {
312 allowed = false;
313 return false // breaks the loop
314 }
315 });
316 return allowed
317 };
318
319 var userAllowed = userAllowedReview(id);
320 if (!userAllowed){
321 alert(_gettext('User `{0}` not allowed to be a reviewer').format(nname));
322 }
323 var shouldAdd = userAllowed && ids.indexOf('reviewer_'+id) == -1;
324
325 if(shouldAdd) {
84 326 // only add if it's not there
85 327 members.innerHTML += element;
86 328 }
87 329
88 330 };
89 331
332 this.updateReviewers = function(repo_name, pull_request_id){
333 var postData = '_method=put&' + $('#reviewers input').serialize();
334 _updatePullRequest(repo_name, pull_request_id, postData);
335 };
336
337 };
338
339
90 340 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
91 341 var url = pyroutes.url(
92 342 'pullrequest_update',
@@ -102,13 +352,6 b' var _updatePullRequest = function(repo_n'
102 352 ajaxPOST(url, postData, success);
103 353 };
104 354
105 var updateReviewers = function(reviewers_ids, repo_name, pull_request_id){
106 if (reviewers_ids === undefined){
107 var postData = '_method=put&' + $('#reviewers input').serialize();
108 _updatePullRequest(repo_name, pull_request_id, postData);
109 }
110 };
111
112 355 /**
113 356 * PULL REQUEST reject & close
114 357 */
@@ -207,7 +450,7 b' var ReviewerAutoComplete = function(inpu'
207 450 showNoSuggestionNotice: true,
208 451 tabDisabled: true,
209 452 autoSelectFirst: true,
210 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true },
453 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
211 454 formatResult: autocompleteFormatResult,
212 455 lookupFilter: autocompleteFilterResult,
213 456 onSelect: function(element, data) {
@@ -217,12 +460,14 b' var ReviewerAutoComplete = function(inpu'
217 460 reasons.push(_gettext('member of "{0}"').format(data.value_display));
218 461
219 462 $.each(data.members, function(index, member_data) {
220 addReviewMember(member_data.id, member_data.first_name, member_data.last_name,
463 reviewersController.addReviewMember(
464 member_data.id, member_data.first_name, member_data.last_name,
221 465 member_data.username, member_data.icon_link, reasons);
222 466 })
223 467
224 468 } else {
225 addReviewMember(data.id, data.first_name, data.last_name,
469 reviewersController.addReviewMember(
470 data.id, data.first_name, data.last_name,
226 471 data.username, data.icon_link, reasons);
227 472 }
228 473
@@ -24,7 +24,11 b''
24 24 </%def>
25 25
26 26 <%def name="main_content()">
27 % if hasattr(c, 'repo_edit_template'):
28 <%include file="${c.repo_edit_template}"/>
29 % else:
27 30 <%include file="/admin/repos/repo_edit_${c.active}.mako"/>
31 % endif
28 32 </%def>
29 33
30 34
@@ -6,6 +6,7 b''
6 6 <a href="${h.url('changeset_home', repo_name=c.repo_name, revision=c.ancestor)}">
7 7 ${h.short_id(c.ancestor)}
8 8 </a>. ${_('Compare was calculated based on this shared commit.')}
9 <input id="common_ancestor" type="hidden" name="common_ancestor" value="${c.ancestor}">
9 10 </div>
10 11 %endif
11 12
@@ -32,21 +32,21 b''
32 32 tal:attributes="prototype prototype"/>
33 33
34 34 <div class="panel panel-default">
35 <div class="panel-heading">${title}</div>
36 35 <div class="panel-body">
37 36
38 37 <div class="deform-seq-container"
39 38 id="${oid}-orderable">
40 39 <div tal:define="subfields [ x[1] for x in subfields ]"
41 40 tal:repeat="subfield subfields"
42 tal:replace="structure subfield.render_template(item_tmpl,
43 parent=field)" />
41 tal:replace="structure subfield.render_template(item_tmpl,parent=field)" />
44 42 <span class="deform-insert-before"
45 43 tal:attributes="
46 44 min_len min_len;
47 45 max_len max_len;
48 46 now_len now_len;
49 orderable orderable;"></span>
47 orderable orderable;">
48
49 </span>
50 50 </div>
51 51
52 52 <div style="clear: both"></div>
@@ -78,12 +78,12 b''
78 78 placeholder: '<span class="glyphicon glyphicon-arrow-right placeholder"></span>',
79 79 onDragStart: function ($item, container, _super) {
80 80 var offset = $item.offset(),
81 pointer = container.rootGroup.pointer
81 pointer = container.rootGroup.pointer;
82 82
83 83 adjustment = {
84 84 left: pointer.left - offset.left,
85 85 top: pointer.top - offset.top
86 }
86 };
87 87
88 88 _super($item, container)
89 89 },
@@ -6,6 +6,7 b''
6 6 oid oid|field.oid"
7 7 class="form-group row deform-seq-item ${field.error and error_class or ''} ${field.widget.item_css_class or ''}"
8 8 i18n:domain="deform">
9
9 10 <div class="deform-seq-item-group">
10 11 <span tal:replace="structure field.serialize(cstruct)"/>
11 12 <tal:errors condition="field.error and not hidden"
@@ -17,13 +18,8 b''
17 18 i18n:translate="">${msg}</p>
18 19 </tal:errors>
19 20 </div>
21
20 22 <div class="deform-seq-item-handle" style="padding:0">
21 <!-- sequence_item -->
22 <span class="deform-order-button close glyphicon glyphicon-resize-vertical"
23 id="${oid}-order"
24 tal:condition="not hidden"
25 title="Reorder (via drag and drop)"
26 i18n:attributes="title"></span>
27 23 <a class="deform-close-button close"
28 24 id="${oid}-close"
29 25 tal:condition="not field.widget.hidden"
@@ -31,5 +27,5 b''
31 27 i18n:attributes="title"
32 28 onclick="javascript:deform.removeSequenceItem(this);">&times;</a>
33 29 </div>
34 <!-- /sequence_item -->
30
35 31 </div>
@@ -62,7 +62,7 b''
62 62
63 63 ##ORG
64 64 <div class="content">
65 <strong>${_('Origin repository')}:</strong>
65 <strong>${_('Source repository')}:</strong>
66 66 ${c.rhodecode_db_repo.description}
67 67 </div>
68 68 <div class="content">
@@ -102,6 +102,17 b''
102 102 </div>
103 103 </div>
104 104 <div>
105 ## REIEW RULES
106 <div id="review_rules" style="display: none" class="reviewers-title block-right">
107 <div class="pr-details-title">
108 ${_('Reviewer rules')}
109 </div>
110 <div class="pr-reviewer-rules">
111 ## review rules will be appended here, by default reviewers logic
112 </div>
113 </div>
114
115 ## REVIEWERS
105 116 <div class="reviewers-title block-right">
106 117 <div class="pr-details-title">
107 118 ${_('Pull request reviewers')}
@@ -137,7 +148,6 b''
137 148 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
138 149 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
139 150 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
140 var targetRepoName = '${c.repo_name}';
141 151
142 152 var $pullRequestForm = $('#pull_request_form');
143 153 var $sourceRepo = $('#source_repo', $pullRequestForm);
@@ -145,6 +155,12 b''
145 155 var $sourceRef = $('#source_ref', $pullRequestForm);
146 156 var $targetRef = $('#target_ref', $pullRequestForm);
147 157
158 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
159 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
160
161 var targetRepo = function() { return $targetRepo.eq(0).val() };
162 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
163
148 164 var calculateContainerWidth = function() {
149 165 var maxWidth = 0;
150 166 var repoSelect2Containers = ['#source_repo', '#target_repo'];
@@ -204,6 +220,8 b''
204 220 // custom code mirror
205 221 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
206 222
223 reviewersController = new ReviewersController();
224
207 225 var queryTargetRepo = function(self, query) {
208 226 // cache ALL results if query is empty
209 227 var cacheKey = query.term || '__';
@@ -213,7 +231,7 b''
213 231 query.callback({results: cachedData.results});
214 232 } else {
215 233 $.ajax({
216 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': targetRepoName}),
234 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': templateContext.repo_name}),
217 235 data: {query: query.term},
218 236 dataType: 'json',
219 237 type: 'GET',
@@ -246,55 +264,21 b''
246 264 query.callback({results: data.results});
247 265 };
248 266
249
250 var prButtonLockChecks = {
251 'compare': false,
252 'reviewers': false
253 };
254
255 var prButtonLock = function(lockEnabled, msg, scope) {
256 scope = scope || 'all';
257 if (scope == 'all'){
258 prButtonLockChecks['compare'] = !lockEnabled;
259 prButtonLockChecks['reviewers'] = !lockEnabled;
260 } else if (scope == 'compare') {
261 prButtonLockChecks['compare'] = !lockEnabled;
262 } else if (scope == 'reviewers'){
263 prButtonLockChecks['reviewers'] = !lockEnabled;
264 }
265 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
266 if (lockEnabled) {
267 $('#save').attr('disabled', 'disabled');
268 }
269 else if (checksMeet) {
270 $('#save').removeAttr('disabled');
271 }
272
273 if (msg) {
274 $('#pr_open_message').html(msg);
275 }
276 };
277
278 267 var loadRepoRefDiffPreview = function() {
279 var sourceRepo = $sourceRepo.eq(0).val();
280 var sourceRef = $sourceRef.eq(0).val().split(':');
281
282 var targetRepo = $targetRepo.eq(0).val();
283 var targetRef = $targetRef.eq(0).val().split(':');
284 268
285 269 var url_data = {
286 'repo_name': targetRepo,
287 'target_repo': sourceRepo,
288 'source_ref': targetRef[2],
270 'repo_name': targetRepo(),
271 'target_repo': sourceRepo(),
272 'source_ref': targetRef()[2],
289 273 'source_ref_type': 'rev',
290 'target_ref': sourceRef[2],
274 'target_ref': sourceRef()[2],
291 275 'target_ref_type': 'rev',
292 276 'merge': true,
293 277 '_': Date.now() // bypass browser caching
294 278 }; // gather the source/target ref and repo here
295 279
296 if (sourceRef.length !== 3 || targetRef.length !== 3) {
297 prButtonLock(true, "${_('Please select origin and destination')}");
280 if (sourceRef().length !== 3 || targetRef().length !== 3) {
281 prButtonLock(true, "${_('Please select source and target')}");
298 282 return;
299 283 }
300 284 var url = pyroutes.url('compare_url', url_data);
@@ -315,10 +299,11 b''
315 299 .done(function(data) {
316 300 loadRepoRefDiffPreview._currentRequest = null;
317 301 $('#pull_request_overview').html(data);
302
318 303 var commitElements = $(data).find('tr[commit_id]');
319 304
320 var prTitleAndDesc = getTitleAndDescription(sourceRef[1],
321 commitElements, 5);
305 var prTitleAndDesc = getTitleAndDescription(
306 sourceRef()[1], commitElements, 5);
322 307
323 308 var title = prTitleAndDesc[0];
324 309 var proposedDescription = prTitleAndDesc[1];
@@ -366,43 +351,6 b''
366 351 });
367 352 };
368 353
369 /**
370 Generate Title and Description for a PullRequest.
371 In case of 1 commits, the title and description is that one commit
372 in case of multiple commits, we iterate on them with max N number of commits,
373 and build description in a form
374 - commitN
375 - commitN+1
376 ...
377
378 Title is then constructed from branch names, or other references,
379 replacing '-' and '_' into spaces
380
381 * @param sourceRef
382 * @param elements
383 * @param limit
384 * @returns {*[]}
385 */
386 var getTitleAndDescription = function(sourceRef, elements, limit) {
387 var title = '';
388 var desc = '';
389
390 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
391 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
392 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
393 });
394 // only 1 commit, use commit message as title
395 if (elements.length == 1) {
396 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
397 }
398 else {
399 // use reference name
400 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
401 }
402
403 return [title, desc]
404 };
405
406 354 var Select2Box = function(element, overrides) {
407 355 var globalDefaults = {
408 356 dropdownAutoWidth: true,
@@ -459,7 +407,7 b''
459 407 var targetRepoChanged = function(repoData) {
460 408 // generate new DESC of target repo displayed next to select
461 409 $('#target_repo_desc').html(
462 "<strong>${_('Destination repository')}</strong>: {0}".format(repoData['description'])
410 "<strong>${_('Target repository')}</strong>: {0}".format(repoData['description'])
463 411 );
464 412
465 413 // generate dynamic select2 for refs.
@@ -468,8 +416,7 b''
468 416
469 417 };
470 418
471 var sourceRefSelect2 = Select2Box(
472 $sourceRef, {
419 var sourceRefSelect2 = Select2Box($sourceRef, {
473 420 placeholder: "${_('Select commit reference')}",
474 421 query: function(query) {
475 422 var initialData = defaultSourceRepoData['refs']['select2_refs'];
@@ -499,12 +446,14 b''
499 446
500 447 $sourceRef.on('change', function(e){
501 448 loadRepoRefDiffPreview();
502 loadDefaultReviewers();
449 reviewersController.loadDefaultReviewers(
450 sourceRepo(), sourceRef(), targetRepo(), targetRef());
503 451 });
504 452
505 453 $targetRef.on('change', function(e){
506 454 loadRepoRefDiffPreview();
507 loadDefaultReviewers();
455 reviewersController.loadDefaultReviewers(
456 sourceRepo(), sourceRef(), targetRepo(), targetRef());
508 457 });
509 458
510 459 $targetRepo.on('change', function(e){
@@ -515,7 +464,7 b''
515 464
516 465 $.ajax({
517 466 url: pyroutes.url('pullrequest_repo_refs',
518 {'repo_name': targetRepoName, 'target_repo_name':repoName}),
467 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
519 468 data: {},
520 469 dataType: 'json',
521 470 type: 'GET',
@@ -531,43 +480,7 b''
531 480
532 481 });
533 482
534 var loadDefaultReviewers = function() {
535 if (loadDefaultReviewers._currentRequest) {
536 loadDefaultReviewers._currentRequest.abort();
537 }
538 $('.calculate-reviewers').show();
539 prButtonLock(true, null, 'reviewers');
540
541 var url = pyroutes.url('repo_default_reviewers_data', {'repo_name': targetRepoName});
542
543 var sourceRepo = $sourceRepo.eq(0).val();
544 var sourceRef = $sourceRef.eq(0).val().split(':');
545 var targetRepo = $targetRepo.eq(0).val();
546 var targetRef = $targetRef.eq(0).val().split(':');
547 url += '?source_repo=' + sourceRepo;
548 url += '&source_ref=' + sourceRef[2];
549 url += '&target_repo=' + targetRepo;
550 url += '&target_ref=' + targetRef[2];
551
552 loadDefaultReviewers._currentRequest = $.get(url)
553 .done(function(data) {
554 loadDefaultReviewers._currentRequest = null;
555
556 // reset && add the reviewer based on selected repo
557 $('#review_members').html('');
558 for (var i = 0; i < data.reviewers.length; i++) {
559 var reviewer = data.reviewers[i];
560 addReviewMember(
561 reviewer.user_id, reviewer.firstname,
562 reviewer.lastname, reviewer.username,
563 reviewer.gravatar_link, reviewer.reasons);
564 }
565 $('.calculate-reviewers').hide();
566 prButtonLock(false, null, 'reviewers');
567 });
568 };
569
570 prButtonLock(true, "${_('Please select origin and destination')}", 'all');
483 prButtonLock(true, "${_('Please select source and target')}", 'all');
571 484
572 485 // auto-load on init, the target refs select2
573 486 calculateContainerWidth();
@@ -581,7 +494,8 b''
581 494 // in case we have a pre-selected value, use it now
582 495 $sourceRef.select2('val', '${c.default_source_ref}');
583 496 loadRepoRefDiffPreview();
584 loadDefaultReviewers();
497 reviewersController.loadDefaultReviewers(
498 sourceRepo(), sourceRef(), targetRepo(), targetRef());
585 499 % endif
586 500
587 501 ReviewerAutoComplete('#user');
@@ -48,13 +48,13 b''
48 48 <div class="summary-details block-left">
49 49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
50 50 <div class="pr-details-title">
51 <a href="${h.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
51 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
52 52 %if c.allowed_to_update:
53 53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
54 54 % if c.allowed_to_delete:
55 55 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
56 56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
57 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
57 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
58 58 ${h.end_form()}
59 59 % else:
60 60 ${_('Delete')}
@@ -68,7 +68,7 b''
68 68 <div id="summary" class="fields pr-details-content">
69 69 <div class="field">
70 70 <div class="label-summary">
71 <label>${_('Origin')}:</label>
71 <label>${_('Source')}:</label>
72 72 </div>
73 73 <div class="input">
74 74 <div class="pr-origininfo">
@@ -297,7 +297,7 b''
297 297 <div id="pr-save" class="field" style="display: none;">
298 298 <div class="label-summary"></div>
299 299 <div class="input">
300 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
300 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
301 301 </div>
302 302 </div>
303 303 </div>
@@ -316,13 +316,27 b''
316 316 </li>
317 317 </ul>
318 318 </div>
319
320 ## REVIEW RULES
321 <div id="review_rules" style="display: none" class="reviewers-title block-right">
322 <div class="pr-details-title">
323 ${_('Reviewer rules')}
324 %if c.allowed_to_update:
325 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
326 %endif
327 </div>
328 <div class="pr-reviewer-rules">
329 ## review rules will be appended here, by default reviewers logic
330 </div>
331 <input id="review_data" type="hidden" name="review_data" value="">
332 </div>
333
319 334 ## REVIEWERS
320 335 <div class="reviewers-title block-right">
321 336 <div class="pr-details-title">
322 337 ${_('Pull request reviewers')}
323 338 %if c.allowed_to_update:
324 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
325 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
339 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
326 340 %endif
327 341 </div>
328 342 </div>
@@ -330,8 +344,8 b''
330 344 ## members goes here !
331 345 <input type="hidden" name="__start__" value="review_members:sequence">
332 346 <ul id="review_members" class="group_members">
333 %for member,reasons,status in c.pull_request_reviewers:
334 <li id="reviewer_${member.user_id}">
347 %for member,reasons,mandatory,status in c.pull_request_reviewers:
348 <li id="reviewer_${member.user_id}" class="reviewer_entry">
335 349 <div class="reviewers_member">
336 350 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
337 351 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
@@ -348,26 +362,39 b''
348 362 %endfor
349 363 <input type="hidden" name="__end__" value="reasons:sequence">
350 364 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
365 <input type="hidden" name="mandatory" value="${mandatory}"/>
351 366 <input type="hidden" name="__end__" value="reviewer:mapping">
352 %if c.allowed_to_update:
353 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
367 % if mandatory:
368 <div class="reviewer_member_mandatory_remove">
354 369 <i class="icon-remove-sign" ></i>
355 370 </div>
371 <div class="reviewer_member_mandatory">
372 <i class="icon-lock" title="Mandatory reviewer"></i>
373 </div>
374 % else:
375 %if c.allowed_to_update:
376 <div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
377 <i class="icon-remove-sign" ></i>
378 </div>
379 %endif
356 380 %endif
357 381 </div>
358 382 </li>
359 383 %endfor
360 384 </ul>
361 385 <input type="hidden" name="__end__" value="review_members:sequence">
386
362 387 %if not c.pull_request.is_closed():
363 <div id="add_reviewer_input" class='ac' style="display: none;">
388 <div id="add_reviewer" class="ac" style="display: none;">
364 389 %if c.allowed_to_update:
365 <div class="reviewer_ac">
390 % if not c.forbid_adding_reviewers:
391 <div id="add_reviewer_input" class="reviewer_ac">
366 392 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
367 393 <div id="reviewers_container"></div>
368 394 </div>
369 <div>
370 <button id="update_pull_request" class="btn btn-small">${_('Save Changes')}</button>
395 % endif
396 <div class="pull-right">
397 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
371 398 </div>
372 399 %endif
373 400 </div>
@@ -429,7 +456,7 b''
429 456
430 457 <div class="pull-right">
431 458 % if c.allowed_to_update and not c.pull_request.is_closed():
432 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
459 <a id="update_commits" class="btn btn-primary no-margin pull-right">${_('Update commits')}</a>
433 460 % else:
434 461 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
435 462 % endif
@@ -615,9 +642,10 b''
615 642 versionController = new VersionController();
616 643 versionController.init();
617 644
645 reviewersController = new ReviewersController();
618 646
619 647 $(function(){
620 ReviewerAutoComplete('#user');
648
621 649 // custom code mirror
622 650 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
623 651
@@ -655,13 +683,13 b''
655 683 var ReviewersPanel = {
656 684 editButton: $('#open_edit_reviewers'),
657 685 closeButton: $('#close_edit_reviewers'),
658 addButton: $('#add_reviewer_input'),
659 removeButtons: $('.reviewer_member_remove'),
686 addButton: $('#add_reviewer'),
687 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove,.reviewer_member_mandatory'),
660 688
661 689 init: function() {
662 var that = this;
663 this.editButton.on('click', function(e) { that.edit(); });
664 this.closeButton.on('click', function(e) { that.close(); });
690 var self = this;
691 this.editButton.on('click', function(e) { self.edit(); });
692 this.closeButton.on('click', function(e) { self.close(); });
665 693 },
666 694
667 695 edit: function(event) {
@@ -669,6 +697,9 b''
669 697 this.closeButton.show();
670 698 this.addButton.show();
671 699 this.removeButtons.css('visibility', 'visible');
700 // review rules
701 reviewersController.loadReviewRules(
702 ${c.pull_request.reviewer_data_json | n});
672 703 },
673 704
674 705 close: function(event) {
@@ -676,6 +707,8 b''
676 707 this.closeButton.hide();
677 708 this.addButton.hide();
678 709 this.removeButtons.css('visibility', 'hidden');
710 // hide review rules
711 reviewersController.hideReviewRules()
679 712 }
680 713 };
681 714
@@ -774,7 +807,8 b''
774 807 $(this).attr('disabled', 'disabled');
775 808 $(this).addClass('disabled');
776 809 $(this).html(_gettext('Saving...'));
777 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
810 reviewersController.updateReviewers(
811 "${c.repo_name}", "${c.pull_request.pull_request_id}");
778 812 });
779 813
780 814 $('#update_commits').on('click', function(e){
@@ -784,14 +818,16 b''
784 818 $(e.currentTarget).removeClass('btn-primary');
785 819 $(e.currentTarget).text(_gettext('Updating...'));
786 820 if(isDisabled){
787 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
821 updateCommits(
822 "${c.repo_name}", "${c.pull_request.pull_request_id}");
788 823 }
789 824 });
790 825 // fixing issue with caches on firefox
791 826 $('#update_commits').removeAttr("disabled");
792 827
793 828 $('#close_pull_request').on('click', function(e){
794 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
829 closePullRequest(
830 "${c.repo_name}", "${c.pull_request.pull_request_id}");
795 831 });
796 832
797 833 $('.show-inline-comments').on('click', function(e){
@@ -818,6 +854,8 b''
818 854 // initial injection
819 855 injectCloseAction();
820 856
857 ReviewerAutoComplete('#user');
858
821 859 })
822 860 </script>
823 861
@@ -25,7 +25,8 b' import pytest'
25 25
26 26 from rhodecode.config.routing import ADMIN_PREFIX
27 27 from rhodecode.tests import (
28 assert_session_flash, url, HG_REPO, TEST_USER_ADMIN_LOGIN)
28 assert_session_flash, url, HG_REPO, TEST_USER_ADMIN_LOGIN,
29 no_newline_id_generator)
29 30 from rhodecode.tests.fixture import Fixture
30 31 from rhodecode.tests.utils import AssertResponse, get_session_from_response
31 32 from rhodecode.lib.auth import check_password
@@ -122,7 +123,7 b' class TestLoginController(object):'
122 123 'ftp://some.ftp.server',
123 124 'http://other.domain',
124 125 '/\r\nX-Forwarded-Host: http://example.org',
125 ])
126 ], ids=no_newline_id_generator)
126 127 def test_login_bad_came_froms(self, url_came_from):
127 128 _url = '{}?came_from={}'.format(login_url, url_came_from)
128 129 response = self.app.post(
@@ -295,8 +295,8 b' class TestPullrequestsController:'
295 295 def test_comment_force_close_pull_request(self, pr_util, csrf_token):
296 296 pull_request = pr_util.create_pull_request()
297 297 pull_request_id = pull_request.pull_request_id
298 reviewers_data = [(1, ['reason']), (2, ['reason2'])]
299 PullRequestModel().update_reviewers(pull_request_id, reviewers_data)
298 PullRequestModel().update_reviewers(
299 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)])
300 300 author = pull_request.user_id
301 301 repo = pull_request.target_repo.repo_id
302 302 self.app.post(
@@ -345,6 +345,7 b' class TestPullrequestsController:'
345 345 ('source_ref', 'branch:default:' + commit_ids['change2']),
346 346 ('target_repo', target.repo_name),
347 347 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
348 ('common_ancestor', commit_ids['ancestor']),
348 349 ('pullrequest_desc', 'Description'),
349 350 ('pullrequest_title', 'Title'),
350 351 ('__start__', 'review_members:sequence'),
@@ -353,6 +354,7 b' class TestPullrequestsController:'
353 354 ('__start__', 'reasons:sequence'),
354 355 ('reason', 'Some reason'),
355 356 ('__end__', 'reasons:sequence'),
357 ('mandatory', 'False'),
356 358 ('__end__', 'reviewer:mapping'),
357 359 ('__end__', 'review_members:sequence'),
358 360 ('__start__', 'revisions:sequence'),
@@ -365,8 +367,9 b' class TestPullrequestsController:'
365 367 status=302)
366 368
367 369 location = response.headers['Location']
368 pull_request_id = int(location.rsplit('/', 1)[1])
369 pull_request = PullRequest.get(pull_request_id)
370 pull_request_id = location.rsplit('/', 1)[1]
371 assert pull_request_id != 'new'
372 pull_request = PullRequest.get(int(pull_request_id))
370 373
371 374 # check that we have now both revisions
372 375 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
@@ -403,6 +406,7 b' class TestPullrequestsController:'
403 406 ('source_ref', 'branch:default:' + commit_ids['change']),
404 407 ('target_repo', target.repo_name),
405 408 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
409 ('common_ancestor', commit_ids['ancestor']),
406 410 ('pullrequest_desc', 'Description'),
407 411 ('pullrequest_title', 'Title'),
408 412 ('__start__', 'review_members:sequence'),
@@ -411,6 +415,7 b' class TestPullrequestsController:'
411 415 ('__start__', 'reasons:sequence'),
412 416 ('reason', 'Some reason'),
413 417 ('__end__', 'reasons:sequence'),
418 ('mandatory', 'False'),
414 419 ('__end__', 'reviewer:mapping'),
415 420 ('__end__', 'review_members:sequence'),
416 421 ('__start__', 'revisions:sequence'),
@@ -422,21 +427,22 b' class TestPullrequestsController:'
422 427 status=302)
423 428
424 429 location = response.headers['Location']
425 pull_request_id = int(location.rsplit('/', 1)[1])
426 pull_request = PullRequest.get(pull_request_id)
430
431 pull_request_id = location.rsplit('/', 1)[1]
432 assert pull_request_id != 'new'
433 pull_request = PullRequest.get(int(pull_request_id))
427 434
428 435 # Check that a notification was made
429 436 notifications = Notification.query()\
430 437 .filter(Notification.created_by == pull_request.author.user_id,
431 438 Notification.type_ == Notification.TYPE_PULL_REQUEST,
432 Notification.subject.contains("wants you to review "
433 "pull request #%d"
434 % pull_request_id))
439 Notification.subject.contains(
440 "wants you to review pull request #%s" % pull_request_id))
435 441 assert len(notifications.all()) == 1
436 442
437 443 # Change reviewers and check that a notification was made
438 444 PullRequestModel().update_reviewers(
439 pull_request.pull_request_id, [(1, [])])
445 pull_request.pull_request_id, [(1, [], False)])
440 446 assert len(notifications.all()) == 2
441 447
442 448 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
@@ -467,6 +473,7 b' class TestPullrequestsController:'
467 473 ('source_ref', 'branch:default:' + commit_ids['change']),
468 474 ('target_repo', target.repo_name),
469 475 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
476 ('common_ancestor', commit_ids['ancestor']),
470 477 ('pullrequest_desc', 'Description'),
471 478 ('pullrequest_title', 'Title'),
472 479 ('__start__', 'review_members:sequence'),
@@ -475,6 +482,7 b' class TestPullrequestsController:'
475 482 ('__start__', 'reasons:sequence'),
476 483 ('reason', 'Some reason'),
477 484 ('__end__', 'reasons:sequence'),
485 ('mandatory', 'False'),
478 486 ('__end__', 'reviewer:mapping'),
479 487 ('__end__', 'review_members:sequence'),
480 488 ('__start__', 'revisions:sequence'),
@@ -486,8 +494,10 b' class TestPullrequestsController:'
486 494 status=302)
487 495
488 496 location = response.headers['Location']
489 pull_request_id = int(location.rsplit('/', 1)[1])
490 pull_request = PullRequest.get(pull_request_id)
497
498 pull_request_id = location.rsplit('/', 1)[1]
499 assert pull_request_id != 'new'
500 pull_request = PullRequest.get(int(pull_request_id))
491 501
492 502 # target_ref has to point to the ancestor's commit_id in order to
493 503 # show the correct diff
@@ -954,14 +964,7 b' class TestPullrequestsController:'
954 964 assert target.text.strip() == 'tag: target'
955 965 assert target.getchildren() == []
956 966
957 def test_description_is_escaped_on_index_page(self, backend, pr_util):
958 xss_description = "<script>alert('Hi!')</script>"
959 pull_request = pr_util.create_pull_request(description=xss_description)
960 response = self.app.get(url(
961 controller='pullrequests', action='show_all',
962 repo_name=pull_request.target_repo.repo_name))
963 response.mustcontain(
964 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;")
967
965 968
966 969 @pytest.mark.parametrize('mergeable', [True, False])
967 970 def test_shadow_repository_link(
@@ -1061,12 +1064,9 b' def assert_pull_request_status(pull_requ'
1061 1064 assert status == expected_status
1062 1065
1063 1066
1064 @pytest.mark.parametrize('action', ['show_all', 'index', 'create'])
1067 @pytest.mark.parametrize('action', ['index', 'create'])
1065 1068 @pytest.mark.usefixtures("autologin_user")
1066 def test_redirects_to_repo_summary_for_svn_repositories(
1067 backend_svn, app, action):
1068 denied_actions = ['show_all', 'index', 'create']
1069 for action in denied_actions:
1069 def test_redirects_to_repo_summary_for_svn_repositories(backend_svn, app, action):
1070 1070 response = app.get(url(
1071 1071 controller='pullrequests', action=action,
1072 1072 repo_name=backend_svn.repo_name))
@@ -116,7 +116,7 b' class TestPullRequestModel:'
116 116
117 117 def test_get_awaiting_my_review(self, pull_request):
118 118 PullRequestModel().update_reviewers(
119 pull_request, [(pull_request.author, ['author'])])
119 pull_request, [(pull_request.author, ['author'], False)])
120 120 prs = PullRequestModel().get_awaiting_my_review(
121 121 pull_request.target_repo, user_id=pull_request.author.user_id)
122 122 assert isinstance(prs, list)
@@ -124,7 +124,7 b' class TestPullRequestModel:'
124 124
125 125 def test_count_awaiting_my_review(self, pull_request):
126 126 PullRequestModel().update_reviewers(
127 pull_request, [(pull_request.author, ['author'])])
127 pull_request, [(pull_request.author, ['author'], False)])
128 128 pr_count = PullRequestModel().count_awaiting_my_review(
129 129 pull_request.target_repo, user_id=pull_request.author.user_id)
130 130 assert pr_count == 1
@@ -986,10 +986,9 b' class PRTestUtility(object):'
986 986 return reference
987 987
988 988 def _get_reviewers(self):
989 model = UserModel()
990 989 return [
991 model.get_by_username(TEST_USER_REGULAR_LOGIN),
992 model.get_by_username(TEST_USER_REGULAR2_LOGIN),
990 (TEST_USER_REGULAR_LOGIN, ['default1'], False),
991 (TEST_USER_REGULAR2_LOGIN, ['default2'], False),
993 992 ]
994 993
995 994 def update_source_repository(self, head=None):
General Comments 0
You need to be logged in to leave comments. Login now