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 | EXTENSIONS = {} |
|
51 | EXTENSIONS = {} | |
52 |
|
52 | |||
53 | __version__ = ('.'.join((str(each) for each in VERSION[:3]))) |
|
53 | __version__ = ('.'.join((str(each) for each in VERSION[:3]))) | |
54 |
__dbversion__ = 7 |
|
54 | __dbversion__ = 77 # defines current db version for migrations | |
55 | __platform__ = platform.system() |
|
55 | __platform__ = platform.system() | |
56 | __license__ = 'AGPLv3, and Commercial License' |
|
56 | __license__ = 'AGPLv3, and Commercial License' | |
57 | __author__ = 'RhodeCode GmbH' |
|
57 | __author__ = 'RhodeCode GmbH' |
@@ -98,7 +98,12 b' class TestCreatePullRequestApi(object):' | |||||
98 | def test_create_with_reviewers_specified_by_names( |
|
98 | def test_create_with_reviewers_specified_by_names( | |
99 | self, backend, no_notifications): |
|
99 | self, backend, no_notifications): | |
100 | data = self._prepare_data(backend) |
|
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 | data['reviewers'] = reviewers |
|
107 | data['reviewers'] = reviewers | |
103 | id_, params = build_data( |
|
108 | id_, params = build_data( | |
104 | self.apikey_regular, 'create_pull_request', **data) |
|
109 | self.apikey_regular, 'create_pull_request', **data) | |
@@ -110,16 +115,26 b' class TestCreatePullRequestApi(object):' | |||||
110 | assert result['result']['msg'] == expected_message |
|
115 | assert result['result']['msg'] == expected_message | |
111 | pull_request_id = result['result']['pull_request_id'] |
|
116 | pull_request_id = result['result']['pull_request_id'] | |
112 | pull_request = PullRequestModel().get(pull_request_id) |
|
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 | assert sorted(actual_reviewers) == sorted(reviewers) |
|
123 | assert sorted(actual_reviewers) == sorted(reviewers) | |
115 |
|
124 | |||
116 | @pytest.mark.backends("git", "hg") |
|
125 | @pytest.mark.backends("git", "hg") | |
117 | def test_create_with_reviewers_specified_by_ids( |
|
126 | def test_create_with_reviewers_specified_by_ids( | |
118 | self, backend, no_notifications): |
|
127 | self, backend, no_notifications): | |
119 | data = self._prepare_data(backend) |
|
128 | data = self._prepare_data(backend) | |
120 | reviewer_names = [TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN] |
|
|||
121 | reviewers = [ |
|
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 | data['reviewers'] = reviewers |
|
138 | data['reviewers'] = reviewers | |
124 | id_, params = build_data( |
|
139 | id_, params = build_data( | |
125 | self.apikey_regular, 'create_pull_request', **data) |
|
140 | self.apikey_regular, 'create_pull_request', **data) | |
@@ -131,14 +146,17 b' class TestCreatePullRequestApi(object):' | |||||
131 | assert result['result']['msg'] == expected_message |
|
146 | assert result['result']['msg'] == expected_message | |
132 | pull_request_id = result['result']['pull_request_id'] |
|
147 | pull_request_id = result['result']['pull_request_id'] | |
133 | pull_request = PullRequestModel().get(pull_request_id) |
|
148 | pull_request = PullRequestModel().get(pull_request_id) | |
134 | actual_reviewers = [r.user.username for r in pull_request.reviewers] |
|
149 | actual_reviewers = [ | |
135 | assert sorted(actual_reviewers) == sorted(reviewer_names) |
|
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 | @pytest.mark.backends("git", "hg") |
|
156 | @pytest.mark.backends("git", "hg") | |
138 | def test_create_fails_when_the_reviewer_is_not_found(self, backend): |
|
157 | def test_create_fails_when_the_reviewer_is_not_found(self, backend): | |
139 | data = self._prepare_data(backend) |
|
158 | data = self._prepare_data(backend) | |
140 | reviewers = ['somebody'] |
|
159 | data['reviewers'] = [{'username': 'somebody'}] | |
141 | data['reviewers'] = reviewers |
|
|||
142 | id_, params = build_data( |
|
160 | id_, params = build_data( | |
143 | self.apikey_regular, 'create_pull_request', **data) |
|
161 | self.apikey_regular, 'create_pull_request', **data) | |
144 | response = api_call(self.app, params) |
|
162 | response = api_call(self.app, params) | |
@@ -153,7 +171,7 b' class TestCreatePullRequestApi(object):' | |||||
153 | id_, params = build_data( |
|
171 | id_, params = build_data( | |
154 | self.apikey_regular, 'create_pull_request', **data) |
|
172 | self.apikey_regular, 'create_pull_request', **data) | |
155 | response = api_call(self.app, params) |
|
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 | assert_error(id_, expected_message, given=response.body) |
|
175 | assert_error(id_, expected_message, given=response.body) | |
158 |
|
176 | |||
159 | @pytest.mark.backends("git", "hg") |
|
177 | @pytest.mark.backends("git", "hg") |
@@ -109,7 +109,8 b' class TestGetPullRequest(object):' | |||||
109 | 'reasons': reasons, |
|
109 | 'reasons': reasons, | |
110 | 'review_status': st[0][1].status if st else 'not_reviewed', |
|
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 | assert_ok(id_, expected, response.body) |
|
116 | assert_ok(id_, expected, response.body) |
@@ -119,20 +119,28 b' class TestUpdatePullRequest(object):' | |||||
119 |
|
119 | |||
120 | @pytest.mark.backends("git", "hg") |
|
120 | @pytest.mark.backends("git", "hg") | |
121 | def test_api_update_change_reviewers( |
|
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 |
|
|
133 | added = [b.username, c.username] | |
125 |
|
|
134 | removed = [a.username] | |
126 | removed = sorted(new) |
|
|||
127 | added = sorted(users) |
|
|||
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 | id_, params = build_data( |
|
139 | id_, params = build_data( | |
132 | self.apikey, 'update_pull_request', |
|
140 | self.apikey, 'update_pull_request', | |
133 | repoid=pull_request.target_repo.repo_name, |
|
141 | repoid=pull_request.target_repo.repo_name, | |
134 | pullrequestid=pull_request.pull_request_id, |
|
142 | pullrequestid=pull_request.pull_request_id, | |
135 |
reviewers= |
|
143 | reviewers=new_reviewers) | |
136 | response = api_call(self.app, params) |
|
144 | response = api_call(self.app, params) | |
137 | expected = { |
|
145 | expected = { | |
138 | "msg": "Updated pull request `{}`".format( |
|
146 | "msg": "Updated pull request `{}`".format( | |
@@ -152,7 +160,7 b' class TestUpdatePullRequest(object):' | |||||
152 | self.apikey, 'update_pull_request', |
|
160 | self.apikey, 'update_pull_request', | |
153 | repoid=pull_request.target_repo.repo_name, |
|
161 | repoid=pull_request.target_repo.repo_name, | |
154 | pullrequestid=pull_request.pull_request_id, |
|
162 | pullrequestid=pull_request.pull_request_id, | |
155 | reviewers=['bad_name']) |
|
163 | reviewers=[{'username': 'bad_name'}]) | |
156 | response = api_call(self.app, params) |
|
164 | response = api_call(self.app, params) | |
157 |
|
165 | |||
158 | expected = 'user `bad_name` does not exist' |
|
166 | expected = 'user `bad_name` does not exist' | |
@@ -165,7 +173,7 b' class TestUpdatePullRequest(object):' | |||||
165 | self.apikey, 'update_pull_request', |
|
173 | self.apikey, 'update_pull_request', | |
166 | repoid='fake', |
|
174 | repoid='fake', | |
167 | pullrequestid='fake', |
|
175 | pullrequestid='fake', | |
168 | reviewers=['bad_name']) |
|
176 | reviewers=[{'username': 'bad_name'}]) | |
169 | response = api_call(self.app, params) |
|
177 | response = api_call(self.app, params) | |
170 |
|
178 | |||
171 | expected = 'repository `fake` does not exist' |
|
179 | expected = 'repository `fake` does not exist' | |
@@ -181,7 +189,7 b' class TestUpdatePullRequest(object):' | |||||
181 | self.apikey, 'update_pull_request', |
|
189 | self.apikey, 'update_pull_request', | |
182 | repoid=pull_request.target_repo.repo_name, |
|
190 | repoid=pull_request.target_repo.repo_name, | |
183 | pullrequestid=999999, |
|
191 | pullrequestid=999999, | |
184 | reviewers=['bad_name']) |
|
192 | reviewers=[{'username': 'bad_name'}]) | |
185 | response = api_call(self.app, params) |
|
193 | response = api_call(self.app, params) | |
186 |
|
194 | |||
187 | expected = 'pull request `999999` does not exist' |
|
195 | expected = 'pull request `999999` does not exist' |
@@ -21,7 +21,7 b'' | |||||
21 |
|
21 | |||
22 | import logging |
|
22 | import logging | |
23 |
|
23 | |||
24 | from rhodecode.api import jsonrpc_method, JSONRPCError |
|
24 | from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError | |
25 | from rhodecode.api.utils import ( |
|
25 | from rhodecode.api.utils import ( | |
26 | has_superadmin_permission, Optional, OAttr, get_repo_or_error, |
|
26 | has_superadmin_permission, Optional, OAttr, get_repo_or_error, | |
27 | get_pull_request_or_error, get_commit_or_error, get_user_or_error, |
|
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 | from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment |
|
34 | from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment | |
35 | from rhodecode.model.pull_request import PullRequestModel, MergeCheck |
|
35 | from rhodecode.model.pull_request import PullRequestModel, MergeCheck | |
36 | from rhodecode.model.settings import SettingsModel |
|
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 | log = logging.getLogger(__name__) |
|
41 | log = logging.getLogger(__name__) | |
39 |
|
42 | |||
@@ -537,7 +540,7 b' def create_pull_request(' | |||||
537 | :type reviewers: Optional(list) |
|
540 | :type reviewers: Optional(list) | |
538 | Accepts username strings or objects of the format: |
|
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 | source = get_repo_or_error(source_repo) |
|
546 | source = get_repo_or_error(source_repo) | |
@@ -567,20 +570,19 b' def create_pull_request(' | |||||
567 | raise JSONRPCError('no common ancestor found') |
|
570 | raise JSONRPCError('no common ancestor found') | |
568 |
|
571 | |||
569 | reviewer_objects = Optional.extract(reviewers) or [] |
|
572 | reviewer_objects = Optional.extract(reviewers) or [] | |
570 |
if |
|
573 | if reviewer_objects: | |
571 | raise JSONRPCError('reviewers should be specified as a list') |
|
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 | for reviewer_object in reviewer_objects: |
|
581 | for reviewer_object in reviewer_objects: | |
575 | reviewer_reasons = [] |
|
582 | user = get_user_or_error(reviewer_object['username']) | |
576 | if isinstance(reviewer_object, (basestring, int)): |
|
583 | reasons = reviewer_object['reasons'] | |
577 | reviewer_username = reviewer_object |
|
584 | mandatory = reviewer_object['mandatory'] | |
578 | else: |
|
585 | reviewers.append((user.user_id, reasons, mandatory)) | |
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)) |
|
|||
584 |
|
586 | |||
585 | pull_request_model = PullRequestModel() |
|
587 | pull_request_model = PullRequestModel() | |
586 | pull_request = pull_request_model.create( |
|
588 | pull_request = pull_request_model.create( | |
@@ -591,7 +593,7 b' def create_pull_request(' | |||||
591 | target_ref=full_target_ref, |
|
593 | target_ref=full_target_ref, | |
592 | revisions=reversed( |
|
594 | revisions=reversed( | |
593 | [commit.raw_id for commit in reversed(commit_ranges)]), |
|
595 | [commit.raw_id for commit in reversed(commit_ranges)]), | |
594 |
reviewers=reviewers |
|
596 | reviewers=reviewers, | |
595 | title=title, |
|
597 | title=title, | |
596 | description=Optional.extract(description) |
|
598 | description=Optional.extract(description) | |
597 | ) |
|
599 | ) | |
@@ -624,6 +626,10 b' def update_pull_request(' | |||||
624 | :type description: Optional(str) |
|
626 | :type description: Optional(str) | |
625 | :param reviewers: Update pull request reviewers list with new value. |
|
627 | :param reviewers: Update pull request reviewers list with new value. | |
626 | :type reviewers: Optional(list) |
|
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 | :param update_commits: Trigger update of commits for this pull request |
|
633 | :param update_commits: Trigger update of commits for this pull request | |
628 | :type: update_commits: Optional(bool) |
|
634 | :type: update_commits: Optional(bool) | |
629 | :param close_pull_request: Close this pull request with rejected state |
|
635 | :param close_pull_request: Close this pull request with rejected state | |
@@ -669,23 +675,21 b' def update_pull_request(' | |||||
669 | 'pull request `%s` update failed, pull request is closed' % ( |
|
675 | 'pull request `%s` update failed, pull request is closed' % ( | |
670 | pullrequestid,)) |
|
676 | pullrequestid,)) | |
671 |
|
677 | |||
|
678 | ||||
672 | reviewer_objects = Optional.extract(reviewers) or [] |
|
679 | reviewer_objects = Optional.extract(reviewers) or [] | |
673 |
if |
|
680 | if reviewer_objects: | |
674 | raise JSONRPCError('reviewers should be specified as a list') |
|
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 |
|
687 | reviewers = [] | |
677 | reviewer_ids = set() |
|
|||
678 | for reviewer_object in reviewer_objects: |
|
688 | for reviewer_object in reviewer_objects: | |
679 | reviewer_reasons = [] |
|
689 | user = get_user_or_error(reviewer_object['username']) | |
680 | if isinstance(reviewer_object, (int, basestring)): |
|
690 | reasons = reviewer_object['reasons'] | |
681 | reviewer_username = reviewer_object |
|
691 | mandatory = reviewer_object['mandatory'] | |
682 | else: |
|
692 | reviewers.append((user.user_id, reasons, mandatory)) | |
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 |
|
693 | |||
690 | title = Optional.extract(title) |
|
694 | title = Optional.extract(title) | |
691 | description = Optional.extract(description) |
|
695 | description = Optional.extract(description) | |
@@ -704,9 +708,9 b' def update_pull_request(' | |||||
704 | Session().commit() |
|
708 | Session().commit() | |
705 |
|
709 | |||
706 | reviewers_changes = {"added": [], "removed": []} |
|
710 | reviewers_changes = {"added": [], "removed": []} | |
707 |
if reviewer |
|
711 | if reviewers: | |
708 | added_reviewers, removed_reviewers = \ |
|
712 | added_reviewers, removed_reviewers = \ | |
709 |
PullRequestModel().update_reviewers(pull_request, reviewers |
|
713 | PullRequestModel().update_reviewers(pull_request, reviewers) | |
710 |
|
714 | |||
711 | reviewers_changes['added'] = sorted( |
|
715 | reviewers_changes['added'] = sorted( | |
712 | [get_user_or_error(n).username for n in added_reviewers]) |
|
716 | [get_user_or_error(n).username for n in added_reviewers]) |
@@ -290,6 +290,13 b' class RepoTypeRoutePredicate(object):' | |||||
290 | else: |
|
290 | else: | |
291 | log.warning('Current view is not supported for repo type:%s', |
|
291 | log.warning('Current view is not supported for repo type:%s', | |
292 | rhodecode_db_repo.repo_type) |
|
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 | return False |
|
300 | return False | |
294 |
|
301 | |||
295 |
|
302 |
@@ -23,7 +23,7 b' import logging' | |||||
23 | from pyramid.view import view_config |
|
23 | from pyramid.view import view_config | |
24 |
|
24 | |||
25 | from rhodecode.apps._base import RepoAppView |
|
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 | from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator |
|
27 | from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator | |
28 |
|
28 | |||
29 | log = logging.getLogger(__name__) |
|
29 | log = logging.getLogger(__name__) | |
@@ -57,13 +57,8 b' class RepoReviewRulesView(RepoAppView):' | |||||
57 | route_name='repo_default_reviewers_data', request_method='GET', |
|
57 | route_name='repo_default_reviewers_data', request_method='GET', | |
58 | renderer='json_ext') |
|
58 | renderer='json_ext') | |
59 | def repo_default_reviewers_data(self): |
|
59 | def repo_default_reviewers_data(self): | |
60 | reasons = ['Default reviewer', 'Repository owner'] |
|
60 | review_data = get_default_reviewers_data( | |
61 | default = utils.reviewer_as_json( |
|
61 | self.db_repo.user, None, None, None, None) | |
62 | user=self.db_repo.user, reasons=reasons, mandatory=False) |
|
62 | return review_data | |
63 |
|
63 | |||
64 | return { |
|
64 | ||
65 | 'api_ver': 'v1', # define version for later possible schema upgrade |
|
|||
66 | 'reviewers': [default], |
|
|||
67 | 'rules': {}, |
|
|||
68 | 'rules_data': {}, |
|
|||
69 | } |
|
@@ -228,8 +228,6 b' class PullrequestsController(BaseRepoCon' | |||||
228 | target_repo = _form['target_repo'] |
|
228 | target_repo = _form['target_repo'] | |
229 | target_ref = _form['target_ref'] |
|
229 | target_ref = _form['target_ref'] | |
230 | commit_ids = _form['revisions'][::-1] |
|
230 | commit_ids = _form['revisions'][::-1] | |
231 | reviewers = [ |
|
|||
232 | (r['user_id'], r['reasons']) for r in _form['review_members']] |
|
|||
233 |
|
231 | |||
234 | # find the ancestor for this pr |
|
232 | # find the ancestor for this pr | |
235 | source_db_repo = Repository.get_by_repo_name(_form['source_repo']) |
|
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 | description = _form['pullrequest_desc'] |
|
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 | try: |
|
270 | try: | |
261 | pull_request = PullRequestModel().create( |
|
271 | pull_request = PullRequestModel().create( | |
262 | c.rhodecode_user.user_id, source_repo, source_ref, target_repo, |
|
272 | c.rhodecode_user.user_id, source_repo, source_ref, target_repo, | |
263 | target_ref, commit_ids, reviewers, pullrequest_title, |
|
273 | target_ref, commit_ids, reviewers, pullrequest_title, | |
264 | description |
|
274 | description, reviewer_rules | |
265 | ) |
|
275 | ) | |
266 | Session().commit() |
|
276 | Session().commit() | |
267 | h.flash(_('Successfully opened new pull request'), |
|
277 | h.flash(_('Successfully opened new pull request'), | |
268 | category='success') |
|
278 | category='success') | |
269 | except Exception as e: |
|
279 | except Exception as e: | |
270 |
msg = _('Error occurred during |
|
280 | msg = _('Error occurred during creation of this pull request.') | |
271 | log.exception(msg) |
|
281 | log.exception(msg) | |
272 | h.flash(msg, category='error') |
|
282 | h.flash(msg, category='error') | |
273 | return redirect(url('pullrequest_home', repo_name=repo_name)) |
|
283 | return redirect(url('pullrequest_home', repo_name=repo_name)) | |
@@ -292,7 +302,8 b' class PullrequestsController(BaseRepoCon' | |||||
292 |
|
302 | |||
293 | if 'review_members' in controls: |
|
303 | if 'review_members' in controls: | |
294 | self._update_reviewers( |
|
304 | self._update_reviewers( | |
295 |
pull_request_id, controls['review_members'] |
|
305 | pull_request_id, controls['review_members'], | |
|
306 | pull_request.reviewer_data) | |||
296 | elif str2bool(request.POST.get('update_commits', 'false')): |
|
307 | elif str2bool(request.POST.get('update_commits', 'false')): | |
297 | self._update_commits(pull_request) |
|
308 | self._update_commits(pull_request) | |
298 | elif str2bool(request.POST.get('close_pull_request', 'false')): |
|
309 | elif str2bool(request.POST.get('close_pull_request', 'false')): | |
@@ -435,10 +446,20 b' class PullrequestsController(BaseRepoCon' | |||||
435 | merge_resp.failure_reason) |
|
446 | merge_resp.failure_reason) | |
436 | h.flash(msg, category='error') |
|
447 | h.flash(msg, category='error') | |
437 |
|
448 | |||
438 | def _update_reviewers(self, pull_request_id, review_members): |
|
449 | def _update_reviewers(self, pull_request_id, review_members, reviewer_rules): | |
439 | reviewers = [ |
|
450 | ||
440 | (int(r['user_id']), r['reasons']) for r in review_members] |
|
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 | PullRequestModel().update_reviewers(pull_request_id, reviewers) |
|
461 | PullRequestModel().update_reviewers(pull_request_id, reviewers) | |
|
462 | h.flash(_('Pull request reviewers updated.'), category='success') | |||
442 | Session().commit() |
|
463 | Session().commit() | |
443 |
|
464 | |||
444 | def _reject_close(self, pull_request): |
|
465 | def _reject_close(self, pull_request): | |
@@ -490,7 +511,8 b' class PullrequestsController(BaseRepoCon' | |||||
490 | _org_pull_request_obj = pull_request_ver.pull_request |
|
511 | _org_pull_request_obj = pull_request_ver.pull_request | |
491 | at_version = pull_request_ver.pull_request_version_id |
|
512 | at_version = pull_request_ver.pull_request_version_id | |
492 | else: |
|
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 | pull_request_display_obj = PullRequest.get_pr_display_object( |
|
517 | pull_request_display_obj = PullRequest.get_pr_display_object( | |
496 | pull_request_obj, _org_pull_request_obj) |
|
518 | pull_request_obj, _org_pull_request_obj) | |
@@ -597,9 +619,9 b' class PullrequestsController(BaseRepoCon' | |||||
597 | c.allowed_to_comment = False |
|
619 | c.allowed_to_comment = False | |
598 | c.allowed_to_close = False |
|
620 | c.allowed_to_close = False | |
599 | else: |
|
621 | else: | |
600 |
c |
|
622 | can_change_status = PullRequestModel().check_user_change_status( | |
601 |
|
|
623 | pull_request_at_ver, c.rhodecode_user) | |
602 | and not pr_closed |
|
624 | c.allowed_to_change_status = can_change_status and not pr_closed | |
603 |
|
625 | |||
604 | c.allowed_to_update = PullRequestModel().check_user_update( |
|
626 | c.allowed_to_update = PullRequestModel().check_user_update( | |
605 | pull_request_latest, c.rhodecode_user) and not pr_closed |
|
627 | pull_request_latest, c.rhodecode_user) and not pr_closed | |
@@ -610,6 +632,18 b' class PullrequestsController(BaseRepoCon' | |||||
610 | c.allowed_to_comment = not pr_closed |
|
632 | c.allowed_to_comment = not pr_closed | |
611 | c.allowed_to_close = c.allowed_to_merge and not pr_closed |
|
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 | # check merge capabilities |
|
647 | # check merge capabilities | |
614 | _merge_check = MergeCheck.validate( |
|
648 | _merge_check = MergeCheck.validate( | |
615 | pull_request_latest, user=c.rhodecode_user) |
|
649 | pull_request_latest, user=c.rhodecode_user) | |
@@ -724,12 +758,18 b' class PullrequestsController(BaseRepoCon' | |||||
724 | c.commit_ranges.append(comm) |
|
758 | c.commit_ranges.append(comm) | |
725 | commit_cache[comm.raw_id] = comm |
|
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 | target_commit = commits_source_repo.get_commit( |
|
763 | target_commit = commits_source_repo.get_commit( | |
728 | commit_id=safe_str(target_ref_id)) |
|
764 | commit_id=safe_str(target_ref_id)) | |
|
765 | ||||
729 | source_commit = commits_source_repo.get_commit( |
|
766 | source_commit = commits_source_repo.get_commit( | |
730 | commit_id=safe_str(source_ref_id)) |
|
767 | commit_id=safe_str(source_ref_id)) | |
|
768 | ||||
731 | except CommitDoesNotExistError: |
|
769 | except CommitDoesNotExistError: | |
732 |
|
|
770 | log.warning( | |
|
771 | 'Failed to get commit from `{}` repo'.format( | |||
|
772 | commits_source_repo), exc_info=True) | |||
733 | except RepositoryRequirementError: |
|
773 | except RepositoryRequirementError: | |
734 | log.warning( |
|
774 | log.warning( | |
735 | 'Failed to get all required data from repo', exc_info=True) |
|
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 | '%s "%s" does not exist' % (ref_type, ref_name)) |
|
87 | '%s "%s" does not exist' % (ref_type, ref_name)) | |
88 |
|
88 | |||
89 | return repo_scm.get_commit(commit_id) |
|
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 | votes = defaultdict(int) |
|
81 | votes = defaultdict(int) | |
82 | reviewers_number = len(statuses_by_reviewers) |
|
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 | if statuses: |
|
84 | if statuses: | |
85 | ver, latest = statuses[0] |
|
85 | ver, latest = statuses[0] | |
86 | votes[latest.status] += 1 |
|
86 | votes[latest.status] += 1 | |
@@ -248,13 +248,14 b' class ChangesetStatusModel(BaseModel):' | |||||
248 | for o in pull_request.reviewers: |
|
248 | for o in pull_request.reviewers: | |
249 | if not o.user: |
|
249 | if not o.user: | |
250 | continue |
|
250 | continue | |
251 | st = commit_statuses.get(o.user.username, None) |
|
251 | statuses = commit_statuses.get(o.user.username, None) | |
252 | if st: |
|
252 | if statuses: | |
253 | st = [(x, list(y)[0]) |
|
253 | statuses = [(x, list(y)[0]) | |
254 |
for x, y in (itertools.groupby( |
|
254 | for x, y in (itertools.groupby( | |
255 | version))] |
|
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 | return pull_request_reviewers |
|
259 | return pull_request_reviewers | |
259 |
|
260 | |||
260 | def calculated_review_status(self, pull_request, reviewers_statuses=None): |
|
261 | def calculated_review_status(self, pull_request, reviewers_statuses=None): |
@@ -3229,6 +3229,14 b' class _PullRequestBase(BaseModel):' | |||||
3229 | _last_merge_status = Column('merge_status', Integer(), nullable=True) |
|
3229 | _last_merge_status = Column('merge_status', Integer(), nullable=True) | |
3230 | merge_rev = Column('merge_rev', String(40), nullable=True) |
|
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 | @hybrid_property |
|
3240 | @hybrid_property | |
3233 | def revisions(self): |
|
3241 | def revisions(self): | |
3234 | return self._revisions.split(':') if self._revisions else [] |
|
3242 | return self._revisions.split(':') if self._revisions else [] | |
@@ -3348,7 +3356,8 b' class _PullRequestBase(BaseModel):' | |||||
3348 | 'reasons': reasons, |
|
3356 | 'reasons': reasons, | |
3349 | 'review_status': st[0][1].status if st else 'not_reviewed', |
|
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 | attrs.revisions = pull_request_obj.revisions |
|
3447 | attrs.revisions = pull_request_obj.revisions | |
3439 |
|
3448 | |||
3440 | attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref |
|
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 | return PullRequestDisplay(attrs, internal=internal_methods) |
|
3453 | return PullRequestDisplay(attrs, internal=internal_methods) | |
3443 |
|
3454 | |||
@@ -3516,11 +3527,6 b' class PullRequestReviewers(Base, BaseMod' | |||||
3516 | 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, |
|
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 | @hybrid_property |
|
3530 | @hybrid_property | |
3525 | def reasons(self): |
|
3531 | def reasons(self): | |
3526 | if not self._reasons: |
|
3532 | if not self._reasons: | |
@@ -3545,7 +3551,7 b' class PullRequestReviewers(Base, BaseMod' | |||||
3545 | _reasons = Column( |
|
3551 | _reasons = Column( | |
3546 | 'reason', MutationList.as_mutable( |
|
3552 | 'reason', MutationList.as_mutable( | |
3547 | JsonType('list', dialect_map=dict(mysql=UnicodeText(16384))))) |
|
3553 | JsonType('list', dialect_map=dict(mysql=UnicodeText(16384))))) | |
3548 |
|
3554 | mandatory = Column("mandatory", Boolean(), nullable=False, default=False) | ||
3549 | user = relationship('User') |
|
3555 | user = relationship('User') | |
3550 | pull_request = relationship('PullRequest') |
|
3556 | pull_request = relationship('PullRequest') | |
3551 |
|
3557 | |||
@@ -3849,14 +3855,17 b' class RepoReviewRuleUser(Base, BaseModel' | |||||
3849 | {'extend_existing': True, 'mysql_engine': 'InnoDB', |
|
3855 | {'extend_existing': True, 'mysql_engine': 'InnoDB', | |
3850 | 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,} |
|
3856 | 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,} | |
3851 | ) |
|
3857 | ) | |
3852 | repo_review_rule_user_id = Column( |
|
3858 | repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True) | |
3853 | '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')) | |
3854 | repo_review_rule_id = Column("repo_review_rule_id", |
|
3860 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False) | |
3855 | Integer(), ForeignKey('repo_review_rules.repo_review_rule_id')) |
|
3861 | mandatory = Column("mandatory", Boolean(), nullable=False, default=False) | |
3856 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), |
|
|||
3857 | nullable=False) |
|
|||
3858 | user = relationship('User') |
|
3862 | user = relationship('User') | |
3859 |
|
3863 | |||
|
3864 | def rule_data(self): | |||
|
3865 | return { | |||
|
3866 | 'mandatory': self.mandatory | |||
|
3867 | } | |||
|
3868 | ||||
3860 |
|
3869 | |||
3861 | class RepoReviewRuleUserGroup(Base, BaseModel): |
|
3870 | class RepoReviewRuleUserGroup(Base, BaseModel): | |
3862 | __tablename__ = 'repo_review_rules_users_groups' |
|
3871 | __tablename__ = 'repo_review_rules_users_groups' | |
@@ -3864,14 +3873,17 b' class RepoReviewRuleUserGroup(Base, Base' | |||||
3864 | {'extend_existing': True, 'mysql_engine': 'InnoDB', |
|
3873 | {'extend_existing': True, 'mysql_engine': 'InnoDB', | |
3865 | 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,} |
|
3874 | 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,} | |
3866 | ) |
|
3875 | ) | |
3867 | repo_review_rule_users_group_id = Column( |
|
3876 | repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True) | |
3868 | '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')) | |
3869 | repo_review_rule_id = Column("repo_review_rule_id", |
|
3878 | users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False) | |
3870 | Integer(), ForeignKey('repo_review_rules.repo_review_rule_id')) |
|
3879 | mandatory = Column("mandatory", Boolean(), nullable=False, default=False) | |
3871 | users_group_id = Column("users_group_id", Integer(), |
|
|||
3872 | ForeignKey('users_groups.users_group_id'), nullable=False) |
|
|||
3873 | users_group = relationship('UserGroup') |
|
3880 | users_group = relationship('UserGroup') | |
3874 |
|
3881 | |||
|
3882 | def rule_data(self): | |||
|
3883 | return { | |||
|
3884 | 'mandatory': self.mandatory | |||
|
3885 | } | |||
|
3886 | ||||
3875 |
|
3887 | |||
3876 | class RepoReviewRule(Base, BaseModel): |
|
3888 | class RepoReviewRule(Base, BaseModel): | |
3877 | __tablename__ = 'repo_review_rules' |
|
3889 | __tablename__ = 'repo_review_rules' | |
@@ -3886,13 +3898,13 b' class RepoReviewRule(Base, BaseModel):' | |||||
3886 | "repo_id", Integer(), ForeignKey('repositories.repo_id')) |
|
3898 | "repo_id", Integer(), ForeignKey('repositories.repo_id')) | |
3887 | repo = relationship('Repository', backref='review_rules') |
|
3899 | repo = relationship('Repository', backref='review_rules') | |
3888 |
|
3900 | |||
3889 | _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), |
|
3901 | _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob | |
3890 | default=u'*') # glob |
|
3902 | _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob | |
3891 | _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), |
|
3903 | ||
3892 | default=u'*') # glob |
|
3904 | use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False) | |
3893 |
|
3905 | forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False) | ||
3894 | use_authors_for_review = Column("use_authors_for_review", Boolean(), |
|
3906 | forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False) | |
3895 | nullable=False, default=False) |
|
3907 | ||
3896 | rule_users = relationship('RepoReviewRuleUser') |
|
3908 | rule_users = relationship('RepoReviewRuleUser') | |
3897 | rule_user_groups = relationship('RepoReviewRuleUserGroup') |
|
3909 | rule_user_groups = relationship('RepoReviewRuleUserGroup') | |
3898 |
|
3910 | |||
@@ -3948,16 +3960,26 b' class RepoReviewRule(Base, BaseModel):' | |||||
3948 | def review_users(self): |
|
3960 | def review_users(self): | |
3949 | """ Returns the users which this rule applies to """ |
|
3961 | """ Returns the users which this rule applies to """ | |
3950 |
|
3962 | |||
3951 | users = set() |
|
3963 | users = collections.OrderedDict() | |
3952 | users |= set([ |
|
3964 | ||
3953 |
|
|
3965 | for rule_user in self.rule_users: | |
3954 |
if rule_user.user.active |
|
3966 | if rule_user.user.active: | |
3955 | users |= set( |
|
3967 | if rule_user.user not in users: | |
3956 | member.user |
|
3968 | users[rule_user.user.username] = { | |
3957 | for rule_user_group in self.rule_user_groups |
|
3969 | 'user': rule_user.user, | |
3958 | for member in rule_user_group.users_group.members |
|
3970 | 'source': 'user', | |
3959 | if member.user.active |
|
3971 | 'data': rule_user.rule_data() | |
3960 | ) |
|
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 | return users |
|
3983 | return users | |
3962 |
|
3984 | |||
3963 | def __repr__(self): |
|
3985 | def __repr__(self): |
@@ -535,12 +535,13 b' def PullRequestForm(repo_id):' | |||||
535 | class ReviewerForm(formencode.Schema): |
|
535 | class ReviewerForm(formencode.Schema): | |
536 | user_id = v.Int(not_empty=True) |
|
536 | user_id = v.Int(not_empty=True) | |
537 | reasons = All() |
|
537 | reasons = All() | |
|
538 | mandatory = v.StringBoolean() | |||
538 |
|
539 | |||
539 | class _PullRequestForm(formencode.Schema): |
|
540 | class _PullRequestForm(formencode.Schema): | |
540 | allow_extra_fields = True |
|
541 | allow_extra_fields = True | |
541 | filter_extra_fields = True |
|
542 | filter_extra_fields = True | |
542 |
|
543 | |||
543 |
|
|
544 | common_ancestor = v.UnicodeString(strip=True, required=True) | |
544 | source_repo = v.UnicodeString(strip=True, required=True) |
|
545 | source_repo = v.UnicodeString(strip=True, required=True) | |
545 | source_ref = v.UnicodeString(strip=True, required=True) |
|
546 | source_ref = v.UnicodeString(strip=True, required=True) | |
546 | target_repo = v.UnicodeString(strip=True, required=True) |
|
547 | target_repo = v.UnicodeString(strip=True, required=True) |
@@ -415,7 +415,9 b' class PullRequestModel(BaseModel):' | |||||
415 | .all() |
|
415 | .all() | |
416 |
|
416 | |||
417 | def create(self, created_by, source_repo, source_ref, target_repo, |
|
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 | created_by_user = self._get_user(created_by) |
|
421 | created_by_user = self._get_user(created_by) | |
420 | source_repo = self._get_repo(source_repo) |
|
422 | source_repo = self._get_repo(source_repo) | |
421 | target_repo = self._get_repo(target_repo) |
|
423 | target_repo = self._get_repo(target_repo) | |
@@ -429,6 +431,7 b' class PullRequestModel(BaseModel):' | |||||
429 | pull_request.title = title |
|
431 | pull_request.title = title | |
430 | pull_request.description = description |
|
432 | pull_request.description = description | |
431 | pull_request.author = created_by_user |
|
433 | pull_request.author = created_by_user | |
|
434 | pull_request.reviewer_data = reviewer_data | |||
432 |
|
435 | |||
433 | Session().add(pull_request) |
|
436 | Session().add(pull_request) | |
434 | Session().flush() |
|
437 | Session().flush() | |
@@ -436,15 +439,16 b' class PullRequestModel(BaseModel):' | |||||
436 | reviewer_ids = set() |
|
439 | reviewer_ids = set() | |
437 | # members / reviewers |
|
440 | # members / reviewers | |
438 | for reviewer_object in reviewers: |
|
441 | for reviewer_object in reviewers: | |
439 |
|
|
442 | user_id, reasons, mandatory = reviewer_object | |
440 | user_id, reasons = reviewer_object |
|
|||
441 | else: |
|
|||
442 | user_id, reasons = reviewer_object, [] |
|
|||
443 |
|
443 | |||
444 | user = self._get_user(user_id) |
|
444 | user = self._get_user(user_id) | |
445 | reviewer_ids.add(user.user_id) |
|
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 | Session().add(reviewer) |
|
452 | Session().add(reviewer) | |
449 |
|
453 | |||
450 | # Set approval status to "Under Review" for all commits which are |
|
454 | # Set approval status to "Under Review" for all commits which are | |
@@ -763,6 +767,7 b' class PullRequestModel(BaseModel):' | |||||
763 | version._last_merge_status = pull_request._last_merge_status |
|
767 | version._last_merge_status = pull_request._last_merge_status | |
764 | version.shadow_merge_ref = pull_request.shadow_merge_ref |
|
768 | version.shadow_merge_ref = pull_request.shadow_merge_ref | |
765 | version.merge_rev = pull_request.merge_rev |
|
769 | version.merge_rev = pull_request.merge_rev | |
|
770 | version.reviewer_data = pull_request.reviewer_data | |||
766 |
|
771 | |||
767 | version.revisions = pull_request.revisions |
|
772 | version.revisions = pull_request.revisions | |
768 | version.pull_request = pull_request |
|
773 | version.pull_request = pull_request | |
@@ -903,16 +908,18 b' class PullRequestModel(BaseModel):' | |||||
903 | Update the reviewers in the pull request |
|
908 | Update the reviewers in the pull request | |
904 |
|
909 | |||
905 | :param pull_request: the pr to update |
|
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 |
|
915 | reviewers = {} | |
910 | for user_id, reasons in reviewer_data: |
|
916 | for user_id, reasons, mandatory in reviewer_data: | |
911 | if isinstance(user_id, (int, basestring)): |
|
917 | if isinstance(user_id, (int, basestring)): | |
912 | user_id = self._get_user(user_id).user_id |
|
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 | pull_request = self.__get_pull_request(pull_request) |
|
923 | pull_request = self.__get_pull_request(pull_request) | |
917 | current_reviewers = PullRequestReviewers.query()\ |
|
924 | current_reviewers = PullRequestReviewers.query()\ | |
918 | .filter(PullRequestReviewers.pull_request == |
|
925 | .filter(PullRequestReviewers.pull_request == | |
@@ -928,8 +935,12 b' class PullRequestModel(BaseModel):' | |||||
928 | for uid in ids_to_add: |
|
935 | for uid in ids_to_add: | |
929 | changed = True |
|
936 | changed = True | |
930 | _usr = self._get_user(uid) |
|
937 | _usr = self._get_user(uid) | |
931 | reasons = reviewers_reasons[uid] |
|
938 | reviewer = PullRequestReviewers() | |
932 | reviewer = PullRequestReviewers(_usr, pull_request, reasons) |
|
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 | Session().add(reviewer) |
|
944 | Session().add(reviewer) | |
934 |
|
945 | |||
935 | for uid in ids_to_remove: |
|
946 | for uid in ids_to_remove: | |
@@ -1369,6 +1380,23 b' class PullRequestModel(BaseModel):' | |||||
1369 | action=action, pr_id=pull_request.pull_request_id), |
|
1380 | action=action, pr_id=pull_request.pull_request_id), | |
1370 | pull_request.target_repo) |
|
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 | class MergeCheck(object): |
|
1401 | class MergeCheck(object): | |
1374 | """ |
|
1402 | """ |
@@ -190,7 +190,7 b' class UserGroupType(UserOrUserGroupType)' | |||||
190 |
|
190 | |||
191 | class StrOrIntType(colander.String): |
|
191 | class StrOrIntType(colander.String): | |
192 | def deserialize(self, node, cstruct): |
|
192 | def deserialize(self, node, cstruct): | |
193 |
if isinstance( |
|
193 | if isinstance(cstruct, basestring): | |
194 | return super(StrOrIntType, self).deserialize(node, cstruct) |
|
194 | return super(StrOrIntType, self).deserialize(node, cstruct) | |
195 | else: |
|
195 | else: | |
196 | return colander.Integer().deserialize(node, cstruct) |
|
196 | return colander.Integer().deserialize(node, cstruct) |
@@ -62,6 +62,10 b' input[type="button"] {' | |||||
62 | text-shadow: none; |
|
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 | font-size: inherit; |
|
274 | font-size: inherit; | |
271 | color: @rcblue; |
|
275 | color: @rcblue; | |
272 | border: none; |
|
276 | border: none; | |
273 |
|
|
277 | border-radius: 0; | |
274 | background-color: transparent; |
|
278 | background-color: transparent; | |
275 |
|
279 | |||
|
280 | &.last-item { | |||
|
281 | border: none; | |||
|
282 | padding: 0 0 0 0; | |||
|
283 | } | |||
|
284 | ||||
276 | &:last-child { |
|
285 | &:last-child { | |
277 | border: none; |
|
286 | border: none; | |
|
287 | padding: 0 0 0 0; | |||
278 | } |
|
288 | } | |
279 |
|
289 | |||
280 | &:hover { |
|
290 | &:hover { |
@@ -12,7 +12,7 b'' | |||||
12 |
|
12 | |||
13 | .control-label { |
|
13 | .control-label { | |
14 | width: 200px; |
|
14 | width: 200px; | |
15 | padding: 10px; |
|
15 | padding: 10px 0px; | |
16 | float: left; |
|
16 | float: left; | |
17 | } |
|
17 | } | |
18 | .control-inputs { |
|
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 | .pr-pullinfo { |
|
373 | .pr-pullinfo { | |
363 | clear: both; |
|
374 | clear: both; | |
364 | margin: .5em 0; |
|
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 | #pr-title-input { |
|
384 | #pr-title-input { | |
@@ -1298,6 +1315,11 b' table.integrations {' | |||||
1298 | width: 100%; |
|
1315 | width: 100%; | |
1299 | margin-bottom: 8px; |
|
1316 | margin-bottom: 8px; | |
1300 | } |
|
1317 | } | |
|
1318 | ||||
|
1319 | .reviewer_entry { | |||
|
1320 | min-height: 55px; | |||
|
1321 | } | |||
|
1322 | ||||
1301 | .reviewers_member { |
|
1323 | .reviewers_member { | |
1302 | width: 100%; |
|
1324 | width: 100%; | |
1303 | overflow: auto; |
|
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 | .reviewer_member_remove { |
|
1359 | .reviewer_member_remove { | |
1336 | position: absolute; |
|
1360 | position: absolute; | |
1337 | right: 0; |
|
1361 | right: 0; | |
@@ -1341,6 +1365,15 b' table.integrations {' | |||||
1341 | padding: 0; |
|
1365 | padding: 0; | |
1342 | color: black; |
|
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 | .reviewer_member_status { |
|
1377 | .reviewer_member_status { | |
1345 | margin-top: 5px; |
|
1378 | margin-top: 5px; | |
1346 | } |
|
1379 | } | |
@@ -1370,6 +1403,11 b' table.integrations {' | |||||
1370 | .pr-description { |
|
1403 | .pr-description { | |
1371 | white-space:pre-wrap; |
|
1404 | white-space:pre-wrap; | |
1372 | } |
|
1405 | } | |
|
1406 | ||||
|
1407 | .pr-reviewer-rules { | |||
|
1408 | padding: 10px 0px 20px 0px; | |||
|
1409 | } | |||
|
1410 | ||||
1373 | .group_members { |
|
1411 | .group_members { | |
1374 | margin-top: 0; |
|
1412 | margin-top: 0; | |
1375 | padding: 0; |
|
1413 | padding: 0; |
@@ -16,77 +16,327 b'' | |||||
16 | // # RhodeCode Enterprise Edition, including its added features, Support services, |
|
16 | // # RhodeCode Enterprise Edition, including its added features, Support services, | |
17 | // # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
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){ |
|
32 | var prButtonLock = function(lockEnabled, msg, scope) { | |
23 | var reviewer = $('#reviewer_{0}'.format(reviewer_id)); |
|
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){ |
|
50 | if (msg) { | |
26 | mark_delete = false; |
|
51 | $('#pr_open_message').html(msg); | |
27 | } |
|
52 | } | |
|
53 | }; | |||
28 |
|
54 | |||
29 | if(mark_delete === true){ |
|
55 | ||
30 | if (reviewer){ |
|
56 | /** | |
31 | // now delete the input |
|
57 | Generate Title and Description for a PullRequest. | |
32 | $('#reviewer_{0} input'.format(reviewer_id)).remove(); |
|
58 | In case of 1 commits, the title and description is that one commit | |
33 | // mark as to-delete |
|
59 | in case of multiple commits, we iterate on them with max N number of commits, | |
34 | var obj = $('#reviewer_{0}_name'.format(reviewer_id)); |
|
60 | and build description in a form | |
35 | obj.addClass('to-delete'); |
|
61 | - commitN | |
36 | obj.css({"text-decoration":"line-through", "opacity": 0.5}); |
|
62 | - commitN+1 | |
37 | } |
|
63 | ... | |
38 | } |
|
64 | ||
39 | else{ |
|
65 | Title is then constructed from branch names, or other references, | |
40 | $('#reviewer_{0}'.format(reviewer_id)).remove(); |
|
66 | replacing '-' and '_' into spaces | |
41 | } |
|
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) { |
|
93 | ||
45 | var members = $('#review_members').get(0); |
|
94 | ||
46 | var reasons_html = ''; |
|
95 | ReviewersController = function () { | |
47 | var reasons_inputs = ''; |
|
96 | var self = this; | |
48 | var reasons = reasons || []; |
|
97 | this.$reviewRulesContainer = $('#review_rules'); | |
49 | if (reasons) { |
|
98 | this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules'); | |
50 | for (var i = 0; i < reasons.length; i++) { |
|
99 | this.forbidReviewUsers = undefined; | |
51 | reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]); |
|
100 | this.$reviewMembers = $('#review_members'); | |
52 | reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">'; |
|
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 | } |
|
188 | ||
55 | var tmpl = '<li id="reviewer_{2}">'+ |
|
189 | $('.calculate-reviewers').show(); | |
56 | '<input type="hidden" name="__start__" value="reviewer:mapping">'+ |
|
190 | // reset reviewer members | |
57 | '<div class="reviewer_status">'+ |
|
191 | self.$reviewMembers.empty(); | |
58 | '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+ |
|
192 | ||
59 | '</div>'+ |
|
193 | prButtonLock(true, null, 'reviewers'); | |
60 | '<img alt="gravatar" class="gravatar" src="{0}"/>'+ |
|
194 | $('#user').hide(); // hide user autocomplete before load | |
61 | '<span class="reviewer_name user">{1}</span>'+ |
|
195 | ||
62 | reasons_html + |
|
196 | var url = pyroutes.url('repo_default_reviewers_data', | |
63 | '<input type="hidden" name="user_id" value="{2}">'+ |
|
197 | { | |
64 | '<input type="hidden" name="__start__" value="reasons:sequence">'+ |
|
198 | 'repo_name': templateContext.repo_name, | |
65 | '{3}'+ |
|
199 | 'source_repo': sourceRepo, | |
66 | '<input type="hidden" name="__end__" value="reasons:sequence">'+ |
|
200 | 'source_ref': sourceRef[2], | |
67 | '<div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">' + |
|
201 | 'target_repo': targetRepo, | |
68 | '<i class="icon-remove-sign"></i>'+ |
|
202 | 'target_ref': targetRef[2] | |
69 | '</div>'+ |
|
203 | }); | |
70 | '</div>'+ |
|
204 | ||
71 | '<input type="hidden" name="__end__" value="reviewer:mapping">'+ |
|
205 | self.currentRequest = $.get(url) | |
72 | '</li>' ; |
|
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( |
|
256 | if (reasons) { | |
75 | nname, escapeHtml(fname), escapeHtml(lname)); |
|
257 | for (var i = 0; i < reasons.length; i++) { | |
76 | var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs); |
|
258 | reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]); | |
77 | // check if we don't have this ID already in |
|
259 | reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">'; | |
78 | var ids = []; |
|
260 | } | |
79 | var _els = $('#review_members li').toArray(); |
|
261 | } | |
80 | for (el in _els){ |
|
262 | var tmpl = '' + | |
81 | ids.push(_els[el].id) |
|
263 | '<li id="reviewer_{2}" class="reviewer_entry">'+ | |
82 | } |
|
264 | '<input type="hidden" name="__start__" value="reviewer:mapping">'+ | |
83 | if(ids.indexOf('reviewer_'+id) == -1){ |
|
265 | '<div class="reviewer_status">'+ | |
84 | // only add if it's not there |
|
266 | '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+ | |
85 | members.innerHTML += element; |
|
267 | '</div>'+ | |
86 | } |
|
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 | var _updatePullRequest = function(repo_name, pull_request_id, postData) { |
|
340 | var _updatePullRequest = function(repo_name, pull_request_id, postData) { | |
91 | var url = pyroutes.url( |
|
341 | var url = pyroutes.url( | |
92 | 'pullrequest_update', |
|
342 | 'pullrequest_update', | |
@@ -102,13 +352,6 b' var _updatePullRequest = function(repo_n' | |||||
102 | ajaxPOST(url, postData, success); |
|
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 | * PULL REQUEST reject & close |
|
356 | * PULL REQUEST reject & close | |
114 | */ |
|
357 | */ | |
@@ -207,7 +450,7 b' var ReviewerAutoComplete = function(inpu' | |||||
207 | showNoSuggestionNotice: true, |
|
450 | showNoSuggestionNotice: true, | |
208 | tabDisabled: true, |
|
451 | tabDisabled: true, | |
209 | autoSelectFirst: true, |
|
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 | formatResult: autocompleteFormatResult, |
|
454 | formatResult: autocompleteFormatResult, | |
212 | lookupFilter: autocompleteFilterResult, |
|
455 | lookupFilter: autocompleteFilterResult, | |
213 | onSelect: function(element, data) { |
|
456 | onSelect: function(element, data) { | |
@@ -217,13 +460,15 b' var ReviewerAutoComplete = function(inpu' | |||||
217 | reasons.push(_gettext('member of "{0}"').format(data.value_display)); |
|
460 | reasons.push(_gettext('member of "{0}"').format(data.value_display)); | |
218 |
|
461 | |||
219 | $.each(data.members, function(index, member_data) { |
|
462 | $.each(data.members, function(index, member_data) { | |
220 | addReviewMember(member_data.id, member_data.first_name, member_data.last_name, |
|
463 | reviewersController.addReviewMember( | |
221 |
|
|
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 | } else { |
|
468 | } else { | |
225 | addReviewMember(data.id, data.first_name, data.last_name, |
|
469 | reviewersController.addReviewMember( | |
226 | data.username, data.icon_link, reasons); |
|
470 | data.id, data.first_name, data.last_name, | |
|
471 | data.username, data.icon_link, reasons); | |||
227 | } |
|
472 | } | |
228 |
|
473 | |||
229 | $(inputId).val(''); |
|
474 | $(inputId).val(''); |
@@ -24,7 +24,11 b'' | |||||
24 | </%def> |
|
24 | </%def> | |
25 |
|
25 | |||
26 | <%def name="main_content()"> |
|
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 | </%def> |
|
32 | </%def> | |
29 |
|
33 | |||
30 |
|
34 |
@@ -6,6 +6,7 b'' | |||||
6 | <a href="${h.url('changeset_home', repo_name=c.repo_name, revision=c.ancestor)}"> |
|
6 | <a href="${h.url('changeset_home', repo_name=c.repo_name, revision=c.ancestor)}"> | |
7 | ${h.short_id(c.ancestor)} |
|
7 | ${h.short_id(c.ancestor)} | |
8 | </a>. ${_('Compare was calculated based on this shared commit.')} |
|
8 | </a>. ${_('Compare was calculated based on this shared commit.')} | |
|
9 | <input id="common_ancestor" type="hidden" name="common_ancestor" value="${c.ancestor}"> | |||
9 | </div> |
|
10 | </div> | |
10 | %endif |
|
11 | %endif | |
11 |
|
12 |
@@ -32,21 +32,21 b'' | |||||
32 | tal:attributes="prototype prototype"/> |
|
32 | tal:attributes="prototype prototype"/> | |
33 |
|
33 | |||
34 | <div class="panel panel-default"> |
|
34 | <div class="panel panel-default"> | |
35 | <div class="panel-heading">${title}</div> |
|
|||
36 | <div class="panel-body"> |
|
35 | <div class="panel-body"> | |
37 |
|
36 | |||
38 | <div class="deform-seq-container" |
|
37 | <div class="deform-seq-container" | |
39 | id="${oid}-orderable"> |
|
38 | id="${oid}-orderable"> | |
40 | <div tal:define="subfields [ x[1] for x in subfields ]" |
|
39 | <div tal:define="subfields [ x[1] for x in subfields ]" | |
41 | tal:repeat="subfield subfields" |
|
40 | tal:repeat="subfield subfields" | |
42 | tal:replace="structure subfield.render_template(item_tmpl, |
|
41 | tal:replace="structure subfield.render_template(item_tmpl,parent=field)" /> | |
43 | parent=field)" /> |
|
|||
44 | <span class="deform-insert-before" |
|
42 | <span class="deform-insert-before" | |
45 | tal:attributes=" |
|
43 | tal:attributes=" | |
46 | min_len min_len; |
|
44 | min_len min_len; | |
47 | max_len max_len; |
|
45 | max_len max_len; | |
48 | now_len now_len; |
|
46 | now_len now_len; | |
49 |
orderable orderable;"> |
|
47 | orderable orderable;"> | |
|
48 | ||||
|
49 | </span> | |||
50 | </div> |
|
50 | </div> | |
51 |
|
51 | |||
52 | <div style="clear: both"></div> |
|
52 | <div style="clear: both"></div> | |
@@ -78,12 +78,12 b'' | |||||
78 | placeholder: '<span class="glyphicon glyphicon-arrow-right placeholder"></span>', |
|
78 | placeholder: '<span class="glyphicon glyphicon-arrow-right placeholder"></span>', | |
79 | onDragStart: function ($item, container, _super) { |
|
79 | onDragStart: function ($item, container, _super) { | |
80 | var offset = $item.offset(), |
|
80 | var offset = $item.offset(), | |
81 | pointer = container.rootGroup.pointer |
|
81 | pointer = container.rootGroup.pointer; | |
82 |
|
82 | |||
83 | adjustment = { |
|
83 | adjustment = { | |
84 | left: pointer.left - offset.left, |
|
84 | left: pointer.left - offset.left, | |
85 | top: pointer.top - offset.top |
|
85 | top: pointer.top - offset.top | |
86 | } |
|
86 | }; | |
87 |
|
87 | |||
88 | _super($item, container) |
|
88 | _super($item, container) | |
89 | }, |
|
89 | }, |
@@ -6,6 +6,7 b'' | |||||
6 | oid oid|field.oid" |
|
6 | oid oid|field.oid" | |
7 | class="form-group row deform-seq-item ${field.error and error_class or ''} ${field.widget.item_css_class or ''}" |
|
7 | class="form-group row deform-seq-item ${field.error and error_class or ''} ${field.widget.item_css_class or ''}" | |
8 | i18n:domain="deform"> |
|
8 | i18n:domain="deform"> | |
|
9 | ||||
9 | <div class="deform-seq-item-group"> |
|
10 | <div class="deform-seq-item-group"> | |
10 | <span tal:replace="structure field.serialize(cstruct)"/> |
|
11 | <span tal:replace="structure field.serialize(cstruct)"/> | |
11 | <tal:errors condition="field.error and not hidden" |
|
12 | <tal:errors condition="field.error and not hidden" | |
@@ -17,13 +18,8 b'' | |||||
17 | i18n:translate="">${msg}</p> |
|
18 | i18n:translate="">${msg}</p> | |
18 | </tal:errors> |
|
19 | </tal:errors> | |
19 | </div> |
|
20 | </div> | |
|
21 | ||||
20 | <div class="deform-seq-item-handle" style="padding:0"> |
|
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 | <a class="deform-close-button close" |
|
23 | <a class="deform-close-button close" | |
28 | id="${oid}-close" |
|
24 | id="${oid}-close" | |
29 | tal:condition="not field.widget.hidden" |
|
25 | tal:condition="not field.widget.hidden" | |
@@ -31,5 +27,5 b'' | |||||
31 | i18n:attributes="title" |
|
27 | i18n:attributes="title" | |
32 | onclick="javascript:deform.removeSequenceItem(this);">×</a> |
|
28 | onclick="javascript:deform.removeSequenceItem(this);">×</a> | |
33 | </div> |
|
29 | </div> | |
34 | <!-- /sequence_item --> |
|
30 | ||
35 | </div> |
|
31 | </div> |
@@ -62,7 +62,7 b'' | |||||
62 |
|
62 | |||
63 | ##ORG |
|
63 | ##ORG | |
64 | <div class="content"> |
|
64 | <div class="content"> | |
65 |
<strong>${_(' |
|
65 | <strong>${_('Source repository')}:</strong> | |
66 | ${c.rhodecode_db_repo.description} |
|
66 | ${c.rhodecode_db_repo.description} | |
67 | </div> |
|
67 | </div> | |
68 | <div class="content"> |
|
68 | <div class="content"> | |
@@ -102,6 +102,17 b'' | |||||
102 | </div> |
|
102 | </div> | |
103 | </div> |
|
103 | </div> | |
104 | <div> |
|
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 | <div class="reviewers-title block-right"> |
|
116 | <div class="reviewers-title block-right"> | |
106 | <div class="pr-details-title"> |
|
117 | <div class="pr-details-title"> | |
107 | ${_('Pull request reviewers')} |
|
118 | ${_('Pull request reviewers')} | |
@@ -137,7 +148,6 b'' | |||||
137 | var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n}; |
|
148 | var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n}; | |
138 | var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}'; |
|
149 | var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}'; | |
139 | var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n}; |
|
150 | var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n}; | |
140 | var targetRepoName = '${c.repo_name}'; |
|
|||
141 |
|
151 | |||
142 | var $pullRequestForm = $('#pull_request_form'); |
|
152 | var $pullRequestForm = $('#pull_request_form'); | |
143 | var $sourceRepo = $('#source_repo', $pullRequestForm); |
|
153 | var $sourceRepo = $('#source_repo', $pullRequestForm); | |
@@ -145,6 +155,12 b'' | |||||
145 | var $sourceRef = $('#source_ref', $pullRequestForm); |
|
155 | var $sourceRef = $('#source_ref', $pullRequestForm); | |
146 | var $targetRef = $('#target_ref', $pullRequestForm); |
|
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 | var calculateContainerWidth = function() { |
|
164 | var calculateContainerWidth = function() { | |
149 | var maxWidth = 0; |
|
165 | var maxWidth = 0; | |
150 | var repoSelect2Containers = ['#source_repo', '#target_repo']; |
|
166 | var repoSelect2Containers = ['#source_repo', '#target_repo']; | |
@@ -204,6 +220,8 b'' | |||||
204 | // custom code mirror |
|
220 | // custom code mirror | |
205 | var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc'); |
|
221 | var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc'); | |
206 |
|
222 | |||
|
223 | reviewersController = new ReviewersController(); | |||
|
224 | ||||
207 | var queryTargetRepo = function(self, query) { |
|
225 | var queryTargetRepo = function(self, query) { | |
208 | // cache ALL results if query is empty |
|
226 | // cache ALL results if query is empty | |
209 | var cacheKey = query.term || '__'; |
|
227 | var cacheKey = query.term || '__'; | |
@@ -213,7 +231,7 b'' | |||||
213 | query.callback({results: cachedData.results}); |
|
231 | query.callback({results: cachedData.results}); | |
214 | } else { |
|
232 | } else { | |
215 | $.ajax({ |
|
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 | data: {query: query.term}, |
|
235 | data: {query: query.term}, | |
218 | dataType: 'json', |
|
236 | dataType: 'json', | |
219 | type: 'GET', |
|
237 | type: 'GET', | |
@@ -246,55 +264,21 b'' | |||||
246 | query.callback({results: data.results}); |
|
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 | var loadRepoRefDiffPreview = function() { |
|
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 | var url_data = { |
|
269 | var url_data = { | |
286 | 'repo_name': targetRepo, |
|
270 | 'repo_name': targetRepo(), | |
287 | 'target_repo': sourceRepo, |
|
271 | 'target_repo': sourceRepo(), | |
288 | 'source_ref': targetRef[2], |
|
272 | 'source_ref': targetRef()[2], | |
289 | 'source_ref_type': 'rev', |
|
273 | 'source_ref_type': 'rev', | |
290 | 'target_ref': sourceRef[2], |
|
274 | 'target_ref': sourceRef()[2], | |
291 | 'target_ref_type': 'rev', |
|
275 | 'target_ref_type': 'rev', | |
292 | 'merge': true, |
|
276 | 'merge': true, | |
293 | '_': Date.now() // bypass browser caching |
|
277 | '_': Date.now() // bypass browser caching | |
294 | }; // gather the source/target ref and repo here |
|
278 | }; // gather the source/target ref and repo here | |
295 |
|
279 | |||
296 | if (sourceRef.length !== 3 || targetRef.length !== 3) { |
|
280 | if (sourceRef().length !== 3 || targetRef().length !== 3) { | |
297 |
prButtonLock(true, "${_('Please select |
|
281 | prButtonLock(true, "${_('Please select source and target')}"); | |
298 | return; |
|
282 | return; | |
299 | } |
|
283 | } | |
300 | var url = pyroutes.url('compare_url', url_data); |
|
284 | var url = pyroutes.url('compare_url', url_data); | |
@@ -315,10 +299,11 b'' | |||||
315 | .done(function(data) { |
|
299 | .done(function(data) { | |
316 | loadRepoRefDiffPreview._currentRequest = null; |
|
300 | loadRepoRefDiffPreview._currentRequest = null; | |
317 | $('#pull_request_overview').html(data); |
|
301 | $('#pull_request_overview').html(data); | |
|
302 | ||||
318 | var commitElements = $(data).find('tr[commit_id]'); |
|
303 | var commitElements = $(data).find('tr[commit_id]'); | |
319 |
|
304 | |||
320 |
var prTitleAndDesc = getTitleAndDescription( |
|
305 | var prTitleAndDesc = getTitleAndDescription( | |
321 | commitElements, 5); |
|
306 | sourceRef()[1], commitElements, 5); | |
322 |
|
307 | |||
323 | var title = prTitleAndDesc[0]; |
|
308 | var title = prTitleAndDesc[0]; | |
324 | var proposedDescription = prTitleAndDesc[1]; |
|
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 | var Select2Box = function(element, overrides) { |
|
354 | var Select2Box = function(element, overrides) { | |
407 | var globalDefaults = { |
|
355 | var globalDefaults = { | |
408 | dropdownAutoWidth: true, |
|
356 | dropdownAutoWidth: true, | |
@@ -459,7 +407,7 b'' | |||||
459 | var targetRepoChanged = function(repoData) { |
|
407 | var targetRepoChanged = function(repoData) { | |
460 | // generate new DESC of target repo displayed next to select |
|
408 | // generate new DESC of target repo displayed next to select | |
461 | $('#target_repo_desc').html( |
|
409 | $('#target_repo_desc').html( | |
462 |
"<strong>${_(' |
|
410 | "<strong>${_('Target repository')}</strong>: {0}".format(repoData['description']) | |
463 | ); |
|
411 | ); | |
464 |
|
412 | |||
465 | // generate dynamic select2 for refs. |
|
413 | // generate dynamic select2 for refs. | |
@@ -468,8 +416,7 b'' | |||||
468 |
|
416 | |||
469 | }; |
|
417 | }; | |
470 |
|
418 | |||
471 | var sourceRefSelect2 = Select2Box( |
|
419 | var sourceRefSelect2 = Select2Box($sourceRef, { | |
472 | $sourceRef, { |
|
|||
473 | placeholder: "${_('Select commit reference')}", |
|
420 | placeholder: "${_('Select commit reference')}", | |
474 | query: function(query) { |
|
421 | query: function(query) { | |
475 | var initialData = defaultSourceRepoData['refs']['select2_refs']; |
|
422 | var initialData = defaultSourceRepoData['refs']['select2_refs']; | |
@@ -499,12 +446,14 b'' | |||||
499 |
|
446 | |||
500 | $sourceRef.on('change', function(e){ |
|
447 | $sourceRef.on('change', function(e){ | |
501 | loadRepoRefDiffPreview(); |
|
448 | loadRepoRefDiffPreview(); | |
502 |
loadDefaultReviewers( |
|
449 | reviewersController.loadDefaultReviewers( | |
|
450 | sourceRepo(), sourceRef(), targetRepo(), targetRef()); | |||
503 | }); |
|
451 | }); | |
504 |
|
452 | |||
505 | $targetRef.on('change', function(e){ |
|
453 | $targetRef.on('change', function(e){ | |
506 | loadRepoRefDiffPreview(); |
|
454 | loadRepoRefDiffPreview(); | |
507 |
loadDefaultReviewers( |
|
455 | reviewersController.loadDefaultReviewers( | |
|
456 | sourceRepo(), sourceRef(), targetRepo(), targetRef()); | |||
508 | }); |
|
457 | }); | |
509 |
|
458 | |||
510 | $targetRepo.on('change', function(e){ |
|
459 | $targetRepo.on('change', function(e){ | |
@@ -515,7 +464,7 b'' | |||||
515 |
|
464 | |||
516 | $.ajax({ |
|
465 | $.ajax({ | |
517 | url: pyroutes.url('pullrequest_repo_refs', |
|
466 | url: pyroutes.url('pullrequest_repo_refs', | |
518 |
{'repo_name': t |
|
467 | {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}), | |
519 | data: {}, |
|
468 | data: {}, | |
520 | dataType: 'json', |
|
469 | dataType: 'json', | |
521 | type: 'GET', |
|
470 | type: 'GET', | |
@@ -531,43 +480,7 b'' | |||||
531 |
|
480 | |||
532 | }); |
|
481 | }); | |
533 |
|
482 | |||
534 | var loadDefaultReviewers = function() { |
|
483 | prButtonLock(true, "${_('Please select source and target')}", 'all'); | |
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'); |
|
|||
571 |
|
484 | |||
572 | // auto-load on init, the target refs select2 |
|
485 | // auto-load on init, the target refs select2 | |
573 | calculateContainerWidth(); |
|
486 | calculateContainerWidth(); | |
@@ -578,10 +491,11 b'' | |||||
578 | }); |
|
491 | }); | |
579 |
|
492 | |||
580 | % if c.default_source_ref: |
|
493 | % if c.default_source_ref: | |
581 |
|
|
494 | // in case we have a pre-selected value, use it now | |
582 |
|
|
495 | $sourceRef.select2('val', '${c.default_source_ref}'); | |
583 |
|
|
496 | loadRepoRefDiffPreview(); | |
584 |
|
|
497 | reviewersController.loadDefaultReviewers( | |
|
498 | sourceRepo(), sourceRef(), targetRepo(), targetRef()); | |||
585 | % endif |
|
499 | % endif | |
586 |
|
500 | |||
587 | ReviewerAutoComplete('#user'); |
|
501 | ReviewerAutoComplete('#user'); |
@@ -48,13 +48,13 b'' | |||||
48 | <div class="summary-details block-left"> |
|
48 | <div class="summary-details block-left"> | |
49 | <% summary = lambda n:{False:'summary-short'}.get(n) %> |
|
49 | <% summary = lambda n:{False:'summary-short'}.get(n) %> | |
50 | <div class="pr-details-title"> |
|
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 | %if c.allowed_to_update: |
|
52 | %if c.allowed_to_update: | |
53 | <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0"> |
|
53 | <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0"> | |
54 | % if c.allowed_to_delete: |
|
54 | % if c.allowed_to_delete: | |
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')} |
|
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 | ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'), |
|
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 | ${h.end_form()} |
|
58 | ${h.end_form()} | |
59 | % else: |
|
59 | % else: | |
60 | ${_('Delete')} |
|
60 | ${_('Delete')} | |
@@ -68,7 +68,7 b'' | |||||
68 | <div id="summary" class="fields pr-details-content"> |
|
68 | <div id="summary" class="fields pr-details-content"> | |
69 | <div class="field"> |
|
69 | <div class="field"> | |
70 | <div class="label-summary"> |
|
70 | <div class="label-summary"> | |
71 |
<label>${_(' |
|
71 | <label>${_('Source')}:</label> | |
72 | </div> |
|
72 | </div> | |
73 | <div class="input"> |
|
73 | <div class="input"> | |
74 | <div class="pr-origininfo"> |
|
74 | <div class="pr-origininfo"> | |
@@ -297,7 +297,7 b'' | |||||
297 | <div id="pr-save" class="field" style="display: none;"> |
|
297 | <div id="pr-save" class="field" style="display: none;"> | |
298 | <div class="label-summary"></div> |
|
298 | <div class="label-summary"></div> | |
299 | <div class="input"> |
|
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 | </div> |
|
301 | </div> | |
302 | </div> |
|
302 | </div> | |
303 | </div> |
|
303 | </div> | |
@@ -316,13 +316,27 b'' | |||||
316 | </li> |
|
316 | </li> | |
317 | </ul> |
|
317 | </ul> | |
318 | </div> |
|
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 | ## REVIEWERS |
|
334 | ## REVIEWERS | |
320 | <div class="reviewers-title block-right"> |
|
335 | <div class="reviewers-title block-right"> | |
321 | <div class="pr-details-title"> |
|
336 | <div class="pr-details-title"> | |
322 | ${_('Pull request reviewers')} |
|
337 | ${_('Pull request reviewers')} | |
323 | %if c.allowed_to_update: |
|
338 | %if c.allowed_to_update: | |
324 | <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span> |
|
339 | <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span> | |
325 | <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span> |
|
|||
326 | %endif |
|
340 | %endif | |
327 | </div> |
|
341 | </div> | |
328 | </div> |
|
342 | </div> | |
@@ -330,8 +344,8 b'' | |||||
330 | ## members goes here ! |
|
344 | ## members goes here ! | |
331 | <input type="hidden" name="__start__" value="review_members:sequence"> |
|
345 | <input type="hidden" name="__start__" value="review_members:sequence"> | |
332 | <ul id="review_members" class="group_members"> |
|
346 | <ul id="review_members" class="group_members"> | |
333 | %for member,reasons,status in c.pull_request_reviewers: |
|
347 | %for member,reasons,mandatory,status in c.pull_request_reviewers: | |
334 | <li id="reviewer_${member.user_id}"> |
|
348 | <li id="reviewer_${member.user_id}" class="reviewer_entry"> | |
335 | <div class="reviewers_member"> |
|
349 | <div class="reviewers_member"> | |
336 | <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}"> |
|
350 | <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}"> | |
337 | <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div> |
|
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 | %endfor |
|
362 | %endfor | |
349 | <input type="hidden" name="__end__" value="reasons:sequence"> |
|
363 | <input type="hidden" name="__end__" value="reasons:sequence"> | |
350 | <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" /> |
|
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 | <input type="hidden" name="__end__" value="reviewer:mapping"> |
|
366 | <input type="hidden" name="__end__" value="reviewer:mapping"> | |
352 |
%if |
|
367 | % if mandatory: | |
353 | <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;"> |
|
368 | <div class="reviewer_member_mandatory_remove"> | |
354 |
<i class="icon-remove-sign" |
|
369 | <i class="icon-remove-sign"></i> | |
355 | </div> |
|
370 | </div> | |
356 | %endif |
|
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 | </div> |
|
381 | </div> | |
358 | </li> |
|
382 | </li> | |
359 | %endfor |
|
383 | %endfor | |
360 | </ul> |
|
384 | </ul> | |
361 | <input type="hidden" name="__end__" value="review_members:sequence"> |
|
385 | <input type="hidden" name="__end__" value="review_members:sequence"> | |
362 | %if not c.pull_request.is_closed(): |
|
386 | ||
363 | <div id="add_reviewer_input" class='ac' style="display: none;"> |
|
387 | %if not c.pull_request.is_closed(): | |
364 | %if c.allowed_to_update: |
|
388 | <div id="add_reviewer" class="ac" style="display: none;"> | |
365 | <div class="reviewer_ac"> |
|
389 | %if c.allowed_to_update: | |
366 | ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))} |
|
390 | % if not c.forbid_adding_reviewers: | |
367 |
<div id="reviewer |
|
391 | <div id="add_reviewer_input" class="reviewer_ac"> | |
368 | </div> |
|
392 | ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))} | |
369 | <div> |
|
393 | <div id="reviewers_container"></div> | |
370 | <button id="update_pull_request" class="btn btn-small">${_('Save Changes')}</button> |
|
394 | </div> | |
371 |
|
|
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 | %endif |
|
401 | %endif | |
373 | </div> |
|
|||
374 | %endif |
|
|||
375 | </div> |
|
402 | </div> | |
376 | </div> |
|
403 | </div> | |
377 | </div> |
|
404 | </div> | |
@@ -429,7 +456,7 b'' | |||||
429 |
|
456 | |||
430 | <div class="pull-right"> |
|
457 | <div class="pull-right"> | |
431 | % if c.allowed_to_update and not c.pull_request.is_closed(): |
|
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 | % else: |
|
460 | % else: | |
434 | <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a> |
|
461 | <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a> | |
435 | % endif |
|
462 | % endif | |
@@ -615,9 +642,10 b'' | |||||
615 | versionController = new VersionController(); |
|
642 | versionController = new VersionController(); | |
616 | versionController.init(); |
|
643 | versionController.init(); | |
617 |
|
644 | |||
|
645 | reviewersController = new ReviewersController(); | |||
618 |
|
646 | |||
619 | $(function(){ |
|
647 | $(function(){ | |
620 | ReviewerAutoComplete('#user'); |
|
648 | ||
621 | // custom code mirror |
|
649 | // custom code mirror | |
622 | var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input'); |
|
650 | var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input'); | |
623 |
|
651 | |||
@@ -655,13 +683,13 b'' | |||||
655 | var ReviewersPanel = { |
|
683 | var ReviewersPanel = { | |
656 | editButton: $('#open_edit_reviewers'), |
|
684 | editButton: $('#open_edit_reviewers'), | |
657 | closeButton: $('#close_edit_reviewers'), |
|
685 | closeButton: $('#close_edit_reviewers'), | |
658 |
addButton: $('#add_reviewer |
|
686 | addButton: $('#add_reviewer'), | |
659 | removeButtons: $('.reviewer_member_remove'), |
|
687 | removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove,.reviewer_member_mandatory'), | |
660 |
|
688 | |||
661 | init: function() { |
|
689 | init: function() { | |
662 |
var |
|
690 | var self = this; | |
663 |
this.editButton.on('click', function(e) { |
|
691 | this.editButton.on('click', function(e) { self.edit(); }); | |
664 |
this.closeButton.on('click', function(e) { |
|
692 | this.closeButton.on('click', function(e) { self.close(); }); | |
665 | }, |
|
693 | }, | |
666 |
|
694 | |||
667 | edit: function(event) { |
|
695 | edit: function(event) { | |
@@ -669,6 +697,9 b'' | |||||
669 | this.closeButton.show(); |
|
697 | this.closeButton.show(); | |
670 | this.addButton.show(); |
|
698 | this.addButton.show(); | |
671 | this.removeButtons.css('visibility', 'visible'); |
|
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 | close: function(event) { |
|
705 | close: function(event) { | |
@@ -676,6 +707,8 b'' | |||||
676 | this.closeButton.hide(); |
|
707 | this.closeButton.hide(); | |
677 | this.addButton.hide(); |
|
708 | this.addButton.hide(); | |
678 | this.removeButtons.css('visibility', 'hidden'); |
|
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 | $(this).attr('disabled', 'disabled'); |
|
807 | $(this).attr('disabled', 'disabled'); | |
775 | $(this).addClass('disabled'); |
|
808 | $(this).addClass('disabled'); | |
776 | $(this).html(_gettext('Saving...')); |
|
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 | $('#update_commits').on('click', function(e){ |
|
814 | $('#update_commits').on('click', function(e){ | |
@@ -784,14 +818,16 b'' | |||||
784 | $(e.currentTarget).removeClass('btn-primary'); |
|
818 | $(e.currentTarget).removeClass('btn-primary'); | |
785 | $(e.currentTarget).text(_gettext('Updating...')); |
|
819 | $(e.currentTarget).text(_gettext('Updating...')); | |
786 | if(isDisabled){ |
|
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 | // fixing issue with caches on firefox |
|
825 | // fixing issue with caches on firefox | |
791 | $('#update_commits').removeAttr("disabled"); |
|
826 | $('#update_commits').removeAttr("disabled"); | |
792 |
|
827 | |||
793 | $('#close_pull_request').on('click', function(e){ |
|
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 | $('.show-inline-comments').on('click', function(e){ |
|
833 | $('.show-inline-comments').on('click', function(e){ | |
@@ -818,6 +854,8 b'' | |||||
818 | // initial injection |
|
854 | // initial injection | |
819 | injectCloseAction(); |
|
855 | injectCloseAction(); | |
820 |
|
856 | |||
|
857 | ReviewerAutoComplete('#user'); | |||
|
858 | ||||
821 | }) |
|
859 | }) | |
822 | </script> |
|
860 | </script> | |
823 |
|
861 |
@@ -25,7 +25,8 b' import pytest' | |||||
25 |
|
25 | |||
26 | from rhodecode.config.routing import ADMIN_PREFIX |
|
26 | from rhodecode.config.routing import ADMIN_PREFIX | |
27 | from rhodecode.tests import ( |
|
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 | from rhodecode.tests.fixture import Fixture |
|
30 | from rhodecode.tests.fixture import Fixture | |
30 | from rhodecode.tests.utils import AssertResponse, get_session_from_response |
|
31 | from rhodecode.tests.utils import AssertResponse, get_session_from_response | |
31 | from rhodecode.lib.auth import check_password |
|
32 | from rhodecode.lib.auth import check_password | |
@@ -122,7 +123,7 b' class TestLoginController(object):' | |||||
122 | 'ftp://some.ftp.server', |
|
123 | 'ftp://some.ftp.server', | |
123 | 'http://other.domain', |
|
124 | 'http://other.domain', | |
124 | '/\r\nX-Forwarded-Host: http://example.org', |
|
125 | '/\r\nX-Forwarded-Host: http://example.org', | |
125 | ]) |
|
126 | ], ids=no_newline_id_generator) | |
126 | def test_login_bad_came_froms(self, url_came_from): |
|
127 | def test_login_bad_came_froms(self, url_came_from): | |
127 | _url = '{}?came_from={}'.format(login_url, url_came_from) |
|
128 | _url = '{}?came_from={}'.format(login_url, url_came_from) | |
128 | response = self.app.post( |
|
129 | response = self.app.post( |
@@ -295,8 +295,8 b' class TestPullrequestsController:' | |||||
295 | def test_comment_force_close_pull_request(self, pr_util, csrf_token): |
|
295 | def test_comment_force_close_pull_request(self, pr_util, csrf_token): | |
296 | pull_request = pr_util.create_pull_request() |
|
296 | pull_request = pr_util.create_pull_request() | |
297 | pull_request_id = pull_request.pull_request_id |
|
297 | pull_request_id = pull_request.pull_request_id | |
298 | reviewers_data = [(1, ['reason']), (2, ['reason2'])] |
|
298 | PullRequestModel().update_reviewers( | |
299 | PullRequestModel().update_reviewers(pull_request_id, reviewers_data) |
|
299 | pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)]) | |
300 | author = pull_request.user_id |
|
300 | author = pull_request.user_id | |
301 | repo = pull_request.target_repo.repo_id |
|
301 | repo = pull_request.target_repo.repo_id | |
302 | self.app.post( |
|
302 | self.app.post( | |
@@ -345,6 +345,7 b' class TestPullrequestsController:' | |||||
345 | ('source_ref', 'branch:default:' + commit_ids['change2']), |
|
345 | ('source_ref', 'branch:default:' + commit_ids['change2']), | |
346 | ('target_repo', target.repo_name), |
|
346 | ('target_repo', target.repo_name), | |
347 | ('target_ref', 'branch:default:' + commit_ids['ancestor']), |
|
347 | ('target_ref', 'branch:default:' + commit_ids['ancestor']), | |
|
348 | ('common_ancestor', commit_ids['ancestor']), | |||
348 | ('pullrequest_desc', 'Description'), |
|
349 | ('pullrequest_desc', 'Description'), | |
349 | ('pullrequest_title', 'Title'), |
|
350 | ('pullrequest_title', 'Title'), | |
350 | ('__start__', 'review_members:sequence'), |
|
351 | ('__start__', 'review_members:sequence'), | |
@@ -353,6 +354,7 b' class TestPullrequestsController:' | |||||
353 | ('__start__', 'reasons:sequence'), |
|
354 | ('__start__', 'reasons:sequence'), | |
354 | ('reason', 'Some reason'), |
|
355 | ('reason', 'Some reason'), | |
355 | ('__end__', 'reasons:sequence'), |
|
356 | ('__end__', 'reasons:sequence'), | |
|
357 | ('mandatory', 'False'), | |||
356 | ('__end__', 'reviewer:mapping'), |
|
358 | ('__end__', 'reviewer:mapping'), | |
357 | ('__end__', 'review_members:sequence'), |
|
359 | ('__end__', 'review_members:sequence'), | |
358 | ('__start__', 'revisions:sequence'), |
|
360 | ('__start__', 'revisions:sequence'), | |
@@ -365,8 +367,9 b' class TestPullrequestsController:' | |||||
365 | status=302) |
|
367 | status=302) | |
366 |
|
368 | |||
367 | location = response.headers['Location'] |
|
369 | location = response.headers['Location'] | |
368 |
pull_request_id = |
|
370 | pull_request_id = location.rsplit('/', 1)[1] | |
369 | pull_request = PullRequest.get(pull_request_id) |
|
371 | assert pull_request_id != 'new' | |
|
372 | pull_request = PullRequest.get(int(pull_request_id)) | |||
370 |
|
373 | |||
371 | # check that we have now both revisions |
|
374 | # check that we have now both revisions | |
372 | assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']] |
|
375 | assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']] | |
@@ -403,6 +406,7 b' class TestPullrequestsController:' | |||||
403 | ('source_ref', 'branch:default:' + commit_ids['change']), |
|
406 | ('source_ref', 'branch:default:' + commit_ids['change']), | |
404 | ('target_repo', target.repo_name), |
|
407 | ('target_repo', target.repo_name), | |
405 | ('target_ref', 'branch:default:' + commit_ids['ancestor-child']), |
|
408 | ('target_ref', 'branch:default:' + commit_ids['ancestor-child']), | |
|
409 | ('common_ancestor', commit_ids['ancestor']), | |||
406 | ('pullrequest_desc', 'Description'), |
|
410 | ('pullrequest_desc', 'Description'), | |
407 | ('pullrequest_title', 'Title'), |
|
411 | ('pullrequest_title', 'Title'), | |
408 | ('__start__', 'review_members:sequence'), |
|
412 | ('__start__', 'review_members:sequence'), | |
@@ -411,6 +415,7 b' class TestPullrequestsController:' | |||||
411 | ('__start__', 'reasons:sequence'), |
|
415 | ('__start__', 'reasons:sequence'), | |
412 | ('reason', 'Some reason'), |
|
416 | ('reason', 'Some reason'), | |
413 | ('__end__', 'reasons:sequence'), |
|
417 | ('__end__', 'reasons:sequence'), | |
|
418 | ('mandatory', 'False'), | |||
414 | ('__end__', 'reviewer:mapping'), |
|
419 | ('__end__', 'reviewer:mapping'), | |
415 | ('__end__', 'review_members:sequence'), |
|
420 | ('__end__', 'review_members:sequence'), | |
416 | ('__start__', 'revisions:sequence'), |
|
421 | ('__start__', 'revisions:sequence'), | |
@@ -422,21 +427,22 b' class TestPullrequestsController:' | |||||
422 | status=302) |
|
427 | status=302) | |
423 |
|
428 | |||
424 | location = response.headers['Location'] |
|
429 | location = response.headers['Location'] | |
425 | pull_request_id = int(location.rsplit('/', 1)[1]) |
|
430 | ||
426 | pull_request = PullRequest.get(pull_request_id) |
|
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 | # Check that a notification was made |
|
435 | # Check that a notification was made | |
429 | notifications = Notification.query()\ |
|
436 | notifications = Notification.query()\ | |
430 | .filter(Notification.created_by == pull_request.author.user_id, |
|
437 | .filter(Notification.created_by == pull_request.author.user_id, | |
431 | Notification.type_ == Notification.TYPE_PULL_REQUEST, |
|
438 | Notification.type_ == Notification.TYPE_PULL_REQUEST, | |
432 |
Notification.subject.contains( |
|
439 | Notification.subject.contains( | |
433 | "pull request #%d" |
|
440 | "wants you to review pull request #%s" % pull_request_id)) | |
434 | % pull_request_id)) |
|
|||
435 | assert len(notifications.all()) == 1 |
|
441 | assert len(notifications.all()) == 1 | |
436 |
|
442 | |||
437 | # Change reviewers and check that a notification was made |
|
443 | # Change reviewers and check that a notification was made | |
438 | PullRequestModel().update_reviewers( |
|
444 | PullRequestModel().update_reviewers( | |
439 | pull_request.pull_request_id, [(1, [])]) |
|
445 | pull_request.pull_request_id, [(1, [], False)]) | |
440 | assert len(notifications.all()) == 2 |
|
446 | assert len(notifications.all()) == 2 | |
441 |
|
447 | |||
442 | def test_create_pull_request_stores_ancestor_commit_id(self, backend, |
|
448 | def test_create_pull_request_stores_ancestor_commit_id(self, backend, | |
@@ -467,6 +473,7 b' class TestPullrequestsController:' | |||||
467 | ('source_ref', 'branch:default:' + commit_ids['change']), |
|
473 | ('source_ref', 'branch:default:' + commit_ids['change']), | |
468 | ('target_repo', target.repo_name), |
|
474 | ('target_repo', target.repo_name), | |
469 | ('target_ref', 'branch:default:' + commit_ids['ancestor-child']), |
|
475 | ('target_ref', 'branch:default:' + commit_ids['ancestor-child']), | |
|
476 | ('common_ancestor', commit_ids['ancestor']), | |||
470 | ('pullrequest_desc', 'Description'), |
|
477 | ('pullrequest_desc', 'Description'), | |
471 | ('pullrequest_title', 'Title'), |
|
478 | ('pullrequest_title', 'Title'), | |
472 | ('__start__', 'review_members:sequence'), |
|
479 | ('__start__', 'review_members:sequence'), | |
@@ -475,6 +482,7 b' class TestPullrequestsController:' | |||||
475 | ('__start__', 'reasons:sequence'), |
|
482 | ('__start__', 'reasons:sequence'), | |
476 | ('reason', 'Some reason'), |
|
483 | ('reason', 'Some reason'), | |
477 | ('__end__', 'reasons:sequence'), |
|
484 | ('__end__', 'reasons:sequence'), | |
|
485 | ('mandatory', 'False'), | |||
478 | ('__end__', 'reviewer:mapping'), |
|
486 | ('__end__', 'reviewer:mapping'), | |
479 | ('__end__', 'review_members:sequence'), |
|
487 | ('__end__', 'review_members:sequence'), | |
480 | ('__start__', 'revisions:sequence'), |
|
488 | ('__start__', 'revisions:sequence'), | |
@@ -486,8 +494,10 b' class TestPullrequestsController:' | |||||
486 | status=302) |
|
494 | status=302) | |
487 |
|
495 | |||
488 | location = response.headers['Location'] |
|
496 | location = response.headers['Location'] | |
489 | pull_request_id = int(location.rsplit('/', 1)[1]) |
|
497 | ||
490 | pull_request = PullRequest.get(pull_request_id) |
|
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 | # target_ref has to point to the ancestor's commit_id in order to |
|
502 | # target_ref has to point to the ancestor's commit_id in order to | |
493 | # show the correct diff |
|
503 | # show the correct diff | |
@@ -954,14 +964,7 b' class TestPullrequestsController:' | |||||
954 | assert target.text.strip() == 'tag: target' |
|
964 | assert target.text.strip() == 'tag: target' | |
955 | assert target.getchildren() == [] |
|
965 | assert target.getchildren() == [] | |
956 |
|
966 | |||
957 | def test_description_is_escaped_on_index_page(self, backend, pr_util): |
|
967 | ||
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>") |
|
|||
965 |
|
968 | |||
966 | @pytest.mark.parametrize('mergeable', [True, False]) |
|
969 | @pytest.mark.parametrize('mergeable', [True, False]) | |
967 | def test_shadow_repository_link( |
|
970 | def test_shadow_repository_link( | |
@@ -1061,23 +1064,20 b' def assert_pull_request_status(pull_requ' | |||||
1061 | assert status == expected_status |
|
1064 | assert status == expected_status | |
1062 |
|
1065 | |||
1063 |
|
1066 | |||
1064 |
@pytest.mark.parametrize('action', [' |
|
1067 | @pytest.mark.parametrize('action', ['index', 'create']) | |
1065 | @pytest.mark.usefixtures("autologin_user") |
|
1068 | @pytest.mark.usefixtures("autologin_user") | |
1066 | def test_redirects_to_repo_summary_for_svn_repositories( |
|
1069 | def test_redirects_to_repo_summary_for_svn_repositories(backend_svn, app, action): | |
1067 | backend_svn, app, action): |
|
1070 | response = app.get(url( | |
1068 | denied_actions = ['show_all', 'index', 'create'] |
|
1071 | controller='pullrequests', action=action, | |
1069 | for action in denied_actions: |
|
1072 | repo_name=backend_svn.repo_name)) | |
1070 | response = app.get(url( |
|
1073 | assert response.status_int == 302 | |
1071 | controller='pullrequests', action=action, |
|
|||
1072 | repo_name=backend_svn.repo_name)) |
|
|||
1073 | assert response.status_int == 302 |
|
|||
1074 |
|
1074 | |||
1075 |
|
|
1075 | # Not allowed, redirect to the summary | |
1076 |
|
|
1076 | redirected = response.follow() | |
1077 |
|
|
1077 | summary_url = url('summary_home', repo_name=backend_svn.repo_name) | |
1078 |
|
1078 | |||
1079 |
|
|
1079 | # URL adds leading slash and path doesn't have it | |
1080 |
|
|
1080 | assert redirected.req.path == summary_url | |
1081 |
|
1081 | |||
1082 |
|
1082 | |||
1083 | def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp): |
|
1083 | def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp): |
@@ -116,7 +116,7 b' class TestPullRequestModel:' | |||||
116 |
|
116 | |||
117 | def test_get_awaiting_my_review(self, pull_request): |
|
117 | def test_get_awaiting_my_review(self, pull_request): | |
118 | PullRequestModel().update_reviewers( |
|
118 | PullRequestModel().update_reviewers( | |
119 | pull_request, [(pull_request.author, ['author'])]) |
|
119 | pull_request, [(pull_request.author, ['author'], False)]) | |
120 | prs = PullRequestModel().get_awaiting_my_review( |
|
120 | prs = PullRequestModel().get_awaiting_my_review( | |
121 | pull_request.target_repo, user_id=pull_request.author.user_id) |
|
121 | pull_request.target_repo, user_id=pull_request.author.user_id) | |
122 | assert isinstance(prs, list) |
|
122 | assert isinstance(prs, list) | |
@@ -124,7 +124,7 b' class TestPullRequestModel:' | |||||
124 |
|
124 | |||
125 | def test_count_awaiting_my_review(self, pull_request): |
|
125 | def test_count_awaiting_my_review(self, pull_request): | |
126 | PullRequestModel().update_reviewers( |
|
126 | PullRequestModel().update_reviewers( | |
127 | pull_request, [(pull_request.author, ['author'])]) |
|
127 | pull_request, [(pull_request.author, ['author'], False)]) | |
128 | pr_count = PullRequestModel().count_awaiting_my_review( |
|
128 | pr_count = PullRequestModel().count_awaiting_my_review( | |
129 | pull_request.target_repo, user_id=pull_request.author.user_id) |
|
129 | pull_request.target_repo, user_id=pull_request.author.user_id) | |
130 | assert pr_count == 1 |
|
130 | assert pr_count == 1 |
@@ -986,10 +986,9 b' class PRTestUtility(object):' | |||||
986 | return reference |
|
986 | return reference | |
987 |
|
987 | |||
988 | def _get_reviewers(self): |
|
988 | def _get_reviewers(self): | |
989 | model = UserModel() |
|
|||
990 | return [ |
|
989 | return [ | |
991 |
|
|
990 | (TEST_USER_REGULAR_LOGIN, ['default1'], False), | |
992 |
|
|
991 | (TEST_USER_REGULAR2_LOGIN, ['default2'], False), | |
993 | ] |
|
992 | ] | |
994 |
|
993 | |||
995 | def update_source_repository(self, head=None): |
|
994 | def update_source_repository(self, head=None): |
General Comments 0
You need to be logged in to leave comments.
Login now