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 | "<script>alert('Hi!')</script>" |
@@ -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__ = 7 |
|
|
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( |
|
|
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 |
|
|
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 |
|
|
|
125 |
|
|
|
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( |
|
|
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= |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
711 | if reviewers: | |
|
708 | 712 | added_reviewers, removed_reviewers = \ |
|
709 |
PullRequestModel().update_reviewers(pull_request, reviewers |
|
|
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 |
|
|
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( |
|
|
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 |
|
|
601 |
|
|
|
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 |
|
|
|
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( |
|
|
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( |
|
|
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 |
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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( |
|
|
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 |
|
|
911 | :param reviewer_data: list of tuples | |
|
912 | [(user, ['reason1', 'reason2'], mandatory_flag)] | |
|
907 | 913 | """ |
|
908 | 914 | |
|
909 |
reviewers |
|
|
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 |
|
|
919 | reviewers[user_id] = { | |
|
920 | 'reasons': reasons, 'mandatory': mandatory} | |
|
914 | 921 | |
|
915 |
reviewers_ids = set(reviewers |
|
|
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( |
|
|
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 |
|
|
|
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,77 +16,327 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 | |
|
21 | 31 | */ |
|
22 | var removeReviewMember = function(reviewer_id, mark_delete){ | |
|
23 | var reviewer = $('#reviewer_{0}'.format(reviewer_id)); | |
|
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 | } | |
|
24 | 49 | |
|
25 | if(typeof(mark_delete) === undefined){ | |
|
26 | mark_delete = false; | |
|
27 | } | |
|
50 | if (msg) { | |
|
51 | $('#pr_open_message').html(msg); | |
|
52 | } | |
|
53 | }; | |
|
28 | 54 | |
|
29 | if(mark_delete === true){ | |
|
30 | if (reviewer){ | |
|
31 | // now delete the input | |
|
32 | $('#reviewer_{0} input'.format(reviewer_id)).remove(); | |
|
33 | // mark as to-delete | |
|
34 | var obj = $('#reviewer_{0}_name'.format(reviewer_id)); | |
|
35 | obj.addClass('to-delete'); | |
|
36 | obj.css({"text-decoration":"line-through", "opacity": 0.5}); | |
|
37 | } | |
|
38 | } | |
|
39 | else{ | |
|
40 | $('#reviewer_{0}'.format(reviewer_id)).remove(); | |
|
41 | } | |
|
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 {*[]} | |
|
72 | */ | |
|
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] | |
|
42 | 91 | }; |
|
43 | 92 | |
|
44 | var addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons) { | |
|
45 | var members = $('#review_members').get(0); | |
|
46 | var reasons_html = ''; | |
|
47 | var reasons_inputs = ''; | |
|
48 | var reasons = reasons || []; | |
|
49 | if (reasons) { | |
|
50 | for (var i = 0; i < reasons.length; i++) { | |
|
51 | reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]); | |
|
52 | reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">'; | |
|
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(); | |
|
53 | 187 | } |
|
54 | } | |
|
55 | var tmpl = '<li id="reviewer_{2}">'+ | |
|
56 | '<input type="hidden" name="__start__" value="reviewer:mapping">'+ | |
|
57 | '<div class="reviewer_status">'+ | |
|
58 | '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+ | |
|
59 | '</div>'+ | |
|
60 | '<img alt="gravatar" class="gravatar" src="{0}"/>'+ | |
|
61 | '<span class="reviewer_name user">{1}</span>'+ | |
|
62 | reasons_html + | |
|
63 | '<input type="hidden" name="user_id" value="{2}">'+ | |
|
64 | '<input type="hidden" name="__start__" value="reasons:sequence">'+ | |
|
65 | '{3}'+ | |
|
66 | '<input type="hidden" name="__end__" value="reasons:sequence">'+ | |
|
67 | '<div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">' + | |
|
68 | '<i class="icon-remove-sign"></i>'+ | |
|
69 | '</div>'+ | |
|
70 | '</div>'+ | |
|
71 | '<input type="hidden" name="__end__" value="reviewer:mapping">'+ | |
|
72 | '</li>' ; | |
|
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) { | |
|
228 | var reviewer = $('#reviewer_{0}'.format(reviewer_id)); | |
|
229 | ||
|
230 | if(typeof(mark_delete) === undefined){ | |
|
231 | mark_delete = false; | |
|
232 | } | |
|
233 | ||
|
234 | if(mark_delete === true){ | |
|
235 | if (reviewer){ | |
|
236 | // now delete the input | |
|
237 | $('#reviewer_{0} input'.format(reviewer_id)).remove(); | |
|
238 | // mark as to-delete | |
|
239 | var obj = $('#reviewer_{0}_name'.format(reviewer_id)); | |
|
240 | obj.addClass('to-delete'); | |
|
241 | obj.css({"text-decoration":"line-through", "opacity": 0.5}); | |
|
242 | } | |
|
243 | } | |
|
244 | else{ | |
|
245 | $('#reviewer_{0}'.format(reviewer_id)).remove(); | |
|
246 | } | |
|
247 | }; | |
|
248 | ||
|
249 | this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) { | |
|
250 | var members = self.$reviewMembers.get(0); | |
|
251 | var reasons_html = ''; | |
|
252 | var reasons_inputs = ''; | |
|
253 | var reasons = reasons || []; | |
|
254 | var mandatory = mandatory || false; | |
|
73 | 255 | |
|
74 | var displayname = "{0} ({1} {2})".format( | |
|
75 | nname, escapeHtml(fname), escapeHtml(lname)); | |
|
76 | var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs); | |
|
77 | // check if we don't have this ID already in | |
|
78 | var ids = []; | |
|
79 | var _els = $('#review_members li').toArray(); | |
|
80 | for (el in _els){ | |
|
81 | ids.push(_els[el].id) | |
|
82 | } | |
|
83 | if(ids.indexOf('reviewer_'+id) == -1){ | |
|
84 | // only add if it's not there | |
|
85 | members.innerHTML += element; | |
|
86 | } | |
|
256 | if (reasons) { | |
|
257 | for (var i = 0; i < reasons.length; i++) { | |
|
258 | reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]); | |
|
259 | reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">'; | |
|
260 | } | |
|
261 | } | |
|
262 | var tmpl = '' + | |
|
263 | '<li id="reviewer_{2}" class="reviewer_entry">'+ | |
|
264 | '<input type="hidden" name="__start__" value="reviewer:mapping">'+ | |
|
265 | '<div class="reviewer_status">'+ | |
|
266 | '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+ | |
|
267 | '</div>'+ | |
|
268 | '<img alt="gravatar" class="gravatar" src="{0}"/>'+ | |
|
269 | '<span class="reviewer_name user">{1}</span>'+ | |
|
270 | reasons_html + | |
|
271 | '<input type="hidden" name="user_id" value="{2}">'+ | |
|
272 | '<input type="hidden" name="__start__" value="reasons:sequence">'+ | |
|
273 | '{3}'+ | |
|
274 | '<input type="hidden" name="__end__" value="reasons:sequence">'; | |
|
275 | ||
|
276 | if (mandatory) { | |
|
277 | tmpl += ''+ | |
|
278 | '<div class="reviewer_member_mandatory_remove">' + | |
|
279 | '<i class="icon-remove-sign"></i>'+ | |
|
280 | '</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 += ''+ | |
|
295 | '<input type="hidden" name="__end__" value="reviewer:mapping">'+ | |
|
296 | '</li>' ; | |
|
297 | ||
|
298 | var displayname = "{0} ({1} {2})".format( | |
|
299 | nname, escapeHtml(fname), escapeHtml(lname)); | |
|
300 | var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs); | |
|
301 | // check if we don't have this ID already in | |
|
302 | var ids = []; | |
|
303 | var _els = self.$reviewMembers.find('li').toArray(); | |
|
304 | for (el in _els){ | |
|
305 | ids.push(_els[el].id) | |
|
306 | } | |
|
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) { | |
|
326 | // only add if it's not there | |
|
327 | members.innerHTML += element; | |
|
328 | } | |
|
329 | ||
|
330 | }; | |
|
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 | }; | |
|
87 | 336 | |
|
88 | 337 | }; |
|
89 | 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,13 +460,15 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, | |
|
221 |
|
|
|
463 | reviewersController.addReviewMember( | |
|
464 | member_data.id, member_data.first_name, member_data.last_name, | |
|
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, | |
|
226 | data.username, data.icon_link, reasons); | |
|
469 | reviewersController.addReviewMember( | |
|
470 | data.id, data.first_name, data.last_name, | |
|
471 | data.username, data.icon_link, reasons); | |
|
227 | 472 | } |
|
228 | 473 | |
|
229 | 474 | $(inputId).val(''); |
@@ -24,7 +24,11 b'' | |||
|
24 | 24 | </%def> |
|
25 | 25 | |
|
26 | 26 | <%def name="main_content()"> |
|
27 | <%include file="/admin/repos/repo_edit_${c.active}.mako"/> | |
|
27 | % if hasattr(c, 'repo_edit_template'): | |
|
28 | <%include file="${c.repo_edit_template}"/> | |
|
29 | % else: | |
|
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;"> |
|
|
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);">×</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>${_(' |
|
|
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': t |
|
|
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 |
|
|
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( |
|
|
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>${_(' |
|
|
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': t |
|
|
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(); |
@@ -578,10 +491,11 b'' | |||
|
578 | 491 | }); |
|
579 | 492 | |
|
580 | 493 | % if c.default_source_ref: |
|
581 |
|
|
|
582 |
|
|
|
583 |
|
|
|
584 |
|
|
|
494 | // in case we have a pre-selected value, use it now | |
|
495 | $sourceRef.select2('val', '${c.default_source_ref}'); | |
|
496 | loadRepoRefDiffPreview(); | |
|
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. |
|
|
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>${_(' |
|
|
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,30 +362,43 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 |
|
|
353 | <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;"> | |
|
354 |
<i class="icon-remove-sign" |
|
|
355 | </div> | |
|
356 | %endif | |
|
367 | % if mandatory: | |
|
368 | <div class="reviewer_member_mandatory_remove"> | |
|
369 | <i class="icon-remove-sign"></i> | |
|
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 | |
|
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"> |
|
362 | %if not c.pull_request.is_closed(): | |
|
363 | <div id="add_reviewer_input" class='ac' style="display: none;"> | |
|
364 | %if c.allowed_to_update: | |
|
365 | <div class="reviewer_ac"> | |
|
366 | ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))} | |
|
367 |
<div id="reviewer |
|
|
368 | </div> | |
|
369 | <div> | |
|
370 | <button id="update_pull_request" class="btn btn-small">${_('Save Changes')}</button> | |
|
371 |
|
|
|
386 | ||
|
387 | %if not c.pull_request.is_closed(): | |
|
388 | <div id="add_reviewer" class="ac" style="display: none;"> | |
|
389 | %if c.allowed_to_update: | |
|
390 | % if not c.forbid_adding_reviewers: | |
|
391 | <div id="add_reviewer_input" class="reviewer_ac"> | |
|
392 | ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))} | |
|
393 | <div id="reviewers_container"></div> | |
|
394 | </div> | |
|
395 | % endif | |
|
396 | <div class="pull-right"> | |
|
397 | <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button> | |
|
398 | </div> | |
|
399 | %endif | |
|
400 | </div> | |
|
372 | 401 | %endif |
|
373 | </div> | |
|
374 | %endif | |
|
375 | 402 | </div> |
|
376 | 403 | </div> |
|
377 | 404 | </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 |
|
|
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 |
|
|
663 |
this.editButton.on('click', function(e) { |
|
|
664 |
this.closeButton.on('click', function(e) { |
|
|
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 = |
|
|
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( |
|
|
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 | "<script>alert('Hi!')</script>") | |
|
967 | ||
|
965 | 968 | |
|
966 | 969 | @pytest.mark.parametrize('mergeable', [True, False]) |
|
967 | 970 | def test_shadow_repository_link( |
@@ -1061,23 +1064,20 b' def assert_pull_request_status(pull_requ' | |||
|
1061 | 1064 | assert status == expected_status |
|
1062 | 1065 | |
|
1063 | 1066 | |
|
1064 |
@pytest.mark.parametrize('action', [' |
|
|
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: | |
|
1070 | response = app.get(url( | |
|
1071 | controller='pullrequests', action=action, | |
|
1072 | repo_name=backend_svn.repo_name)) | |
|
1073 | assert response.status_int == 302 | |
|
1069 | def test_redirects_to_repo_summary_for_svn_repositories(backend_svn, app, action): | |
|
1070 | response = app.get(url( | |
|
1071 | controller='pullrequests', action=action, | |
|
1072 | repo_name=backend_svn.repo_name)) | |
|
1073 | assert response.status_int == 302 | |
|
1074 | 1074 | |
|
1075 |
|
|
|
1076 |
|
|
|
1077 |
|
|
|
1075 | # Not allowed, redirect to the summary | |
|
1076 | redirected = response.follow() | |
|
1077 | summary_url = url('summary_home', repo_name=backend_svn.repo_name) | |
|
1078 | 1078 | |
|
1079 |
|
|
|
1080 |
|
|
|
1079 | # URL adds leading slash and path doesn't have it | |
|
1080 | assert redirected.req.path == summary_url | |
|
1081 | 1081 | |
|
1082 | 1082 | |
|
1083 | 1083 | def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp): |
@@ -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 |
|
|
|
992 |
|
|
|
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