##// END OF EJS Templates
pull-request: extended default reviewers functionality....
marcink -
r1769:fb9baff8 default
parent child Browse files
Show More
@@ -0,0 +1,83 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22 from rhodecode.model.db import Repository
23
24
25 def route_path(name, params=None, **kwargs):
26 import urllib
27
28 base_url = {
29 'pullrequest_show_all': '/{repo_name}/pull-request',
30 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
31 }[name].format(**kwargs)
32
33 if params:
34 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
35 return base_url
36
37
38 @pytest.mark.backends("git", "hg")
39 @pytest.mark.usefixtures('autologin_user', 'app')
40 class TestPullRequestList(object):
41
42 @pytest.mark.parametrize('params, expected_title', [
43 ({'source': 0, 'closed': 1}, 'Closed Pull Requests'),
44 ({'source': 0, 'my': 1}, 'opened by me'),
45 ({'source': 0, 'awaiting_review': 1}, 'awaiting review'),
46 ({'source': 0, 'awaiting_my_review': 1}, 'awaiting my review'),
47 ({'source': 1}, 'Pull Requests from'),
48 ])
49 def test_showing_list_page(self, backend, pr_util, params, expected_title):
50 pull_request = pr_util.create_pull_request()
51
52 response = self.app.get(
53 route_path('pullrequest_show_all',
54 repo_name=pull_request.target_repo.repo_name,
55 params=params))
56
57 assert_response = response.assert_response()
58 assert_response.element_equals_to('.panel-title', expected_title)
59 element = assert_response.get_element('.panel-title')
60 element_text = assert_response._element_to_string(element)
61
62 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
63 pull_request = pr_util.create_pull_request()
64 response = self.app.get(
65 route_path('pullrequest_show_all_data',
66 repo_name=pull_request.target_repo.repo_name),
67 extra_environ=xhr_header)
68
69 assert response.json['recordsTotal'] == 1
70 assert response.json['data'][0]['description'] == 'Description'
71
72 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
73 xss_description = "<script>alert('Hi!')</script>"
74 pull_request = pr_util.create_pull_request(description=xss_description)
75
76 response = self.app.get(
77 route_path('pullrequest_show_all_data',
78 repo_name=pull_request.target_repo.repo_name),
79 extra_environ=xhr_header)
80
81 assert response.json['recordsTotal'] == 1
82 assert response.json['data'][0]['description'] == \
83 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
@@ -0,0 +1,76 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 from rhodecode.lib import helpers as h
22 from rhodecode.lib.utils2 import safe_int
23
24
25 def reviewer_as_json(user, reasons, mandatory):
26 """
27 Returns json struct of a reviewer for frontend
28
29 :param user: the reviewer
30 :param reasons: list of strings of why they are reviewers
31 :param mandatory: bool, to set user as mandatory
32 """
33
34 return {
35 'user_id': user.user_id,
36 'reasons': reasons,
37 'mandatory': mandatory,
38 'username': user.username,
39 'firstname': user.firstname,
40 'lastname': user.lastname,
41 'gravatar_link': h.gravatar_url(user.email, 14),
42 }
43
44
45 def get_default_reviewers_data(
46 current_user, source_repo, source_commit, target_repo, target_commit):
47
48 """ Return json for default reviewers of a repository """
49
50 reasons = ['Default reviewer', 'Repository owner']
51 default = reviewer_as_json(
52 user=current_user, reasons=reasons, mandatory=False)
53
54 return {
55 'api_ver': 'v1', # define version for later possible schema upgrade
56 'reviewers': [default],
57 'rules': {},
58 'rules_data': {},
59 }
60
61
62 def validate_default_reviewers(review_members, reviewer_rules):
63 """
64 Function to validate submitted reviewers against the saved rules
65
66 """
67 reviewers = []
68 reviewer_by_id = {}
69 for r in review_members:
70 reviewer_user_id = safe_int(r['user_id'])
71 entry = (reviewer_user_id, r['reasons'], r['mandatory'])
72
73 reviewer_by_id[reviewer_user_id] = entry
74 reviewers.append(entry)
75
76 return reviewers
@@ -0,0 +1,37 b''
1 import logging
2
3 from sqlalchemy import *
4 from rhodecode.model import meta
5 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_7_0_1 as db
17
18 repo_review_rule_table = db.RepoReviewRule.__table__
19
20 forbid_author_to_review = Column(
21 "forbid_author_to_review", Boolean(), nullable=True, default=False)
22 forbid_author_to_review.create(table=repo_review_rule_table)
23
24 forbid_adding_reviewers = Column(
25 "forbid_adding_reviewers", Boolean(), nullable=True, default=False)
26 forbid_adding_reviewers.create(table=repo_review_rule_table)
27
28 fixups(db, meta.Session)
29
30
31 def downgrade(migrate_engine):
32 meta = MetaData()
33 meta.bind = migrate_engine
34
35
36 def fixups(models, _SESSION):
37 pass
@@ -0,0 +1,38 b''
1 import logging
2
3 from sqlalchemy import *
4 from rhodecode.model import meta
5 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_7_0_1 as db
17
18 repo_review_rule_user_table = db.RepoReviewRuleUser.__table__
19 repo_review_rule_user_group_table = db.RepoReviewRuleUserGroup.__table__
20
21 mandatory_user = Column(
22 "mandatory", Boolean(), nullable=True, default=False)
23 mandatory_user.create(table=repo_review_rule_user_table)
24
25 mandatory_user_group = Column(
26 "mandatory", Boolean(), nullable=True, default=False)
27 mandatory_user_group.create(table=repo_review_rule_user_group_table)
28
29 fixups(db, meta.Session)
30
31
32 def downgrade(migrate_engine):
33 meta = MetaData()
34 meta.bind = migrate_engine
35
36
37 def fixups(models, _SESSION):
38 pass
@@ -0,0 +1,33 b''
1 import logging
2
3 from sqlalchemy import *
4 from rhodecode.model import meta
5 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_7_0_1 as db
17
18 pull_request_reviewers = db.PullRequestReviewers.__table__
19
20 mandatory = Column(
21 "mandatory", Boolean(), nullable=True, default=False)
22 mandatory.create(table=pull_request_reviewers)
23
24 fixups(db, meta.Session)
25
26
27 def downgrade(migrate_engine):
28 meta = MetaData()
29 meta.bind = migrate_engine
30
31
32 def fixups(models, _SESSION):
33 pass
@@ -0,0 +1,40 b''
1 import logging
2
3 from sqlalchemy import *
4 from rhodecode.model import meta
5 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
6
7 log = logging.getLogger(__name__)
8
9
10 def upgrade(migrate_engine):
11 """
12 Upgrade operations go here.
13 Don't create your own engine; bind migrate_engine to your metadata
14 """
15 _reset_base(migrate_engine)
16 from rhodecode.lib.dbmigrate.schema import db_4_7_0_1 as db
17
18 pull_request = db.PullRequest.__table__
19 pull_request_version = db.PullRequestVersion.__table__
20
21 reviewer_data_1 = Column(
22 'reviewer_data_json',
23 db.JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
24 reviewer_data_1.create(table=pull_request)
25
26 reviewer_data_2 = Column(
27 'reviewer_data_json',
28 db.JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
29 reviewer_data_2.create(table=pull_request_version)
30
31 fixups(db, meta.Session)
32
33
34 def downgrade(migrate_engine):
35 meta = MetaData()
36 meta.bind = migrate_engine
37
38
39 def fixups(models, _SESSION):
40 pass
@@ -0,0 +1,34 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import colander
22 from rhodecode.model.validation_schema import validators, preparers, types
23
24
25 class ReviewerSchema(colander.MappingSchema):
26 username = colander.SchemaNode(types.StrOrIntType())
27 reasons = colander.SchemaNode(colander.List(), missing=[])
28 mandatory = colander.SchemaNode(colander.Boolean(), missing=False)
29
30
31 class ReviewerListSchema(colander.SequenceSchema):
32 reviewers = ReviewerSchema()
33
34
@@ -51,7 +51,7 b' PYRAMID_SETTINGS = {}'
51 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__ = 73 # defines current db version for migrations
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(n).user_id for n in reviewer_names]
130 {'username': UserModel().get_by_username(
131 TEST_USER_REGULAR_LOGIN).user_id,
132 'reasons': ['added manually']},
133 {'username': UserModel().get_by_username(
134 TEST_USER_ADMIN_LOGIN).user_id,
135 'reasons': ['added manually']},
136 ]
137
123 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 pull_request.reviewers_statuses()
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 users = [x.username for x in User.get_all()]
133 added = [b.username, c.username]
125 new = [users.pop(0)]
134 removed = [a.username]
126 removed = sorted(new)
127 added = sorted(users)
128
135
129 pull_request = pr_util.create_pull_request(reviewers=new)
136 pull_request = pr_util.create_pull_request(
137 reviewers=[(a.username, ['added via API'], False)])
130
138
131 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=added)
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 not isinstance(reviewer_objects, list):
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_reasons = []
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_reasons,
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 not isinstance(reviewer_objects, list):
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_reasons = []
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_ids:
711 if reviewers:
708 added_reviewers, removed_reviewers = \
712 added_reviewers, removed_reviewers = \
709 PullRequestModel().update_reviewers(pull_request, reviewers_reasons)
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 sending pull request')
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(pull_request_id)
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.allowed_to_change_status = PullRequestModel(). \
622 can_change_status = PullRequestModel().check_user_change_status(
601 check_user_change_status(pull_request_at_ver, c.rhodecode_user) \
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 pass
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(sorted(st, key=version),
254 for x, y in (itertools.groupby(
255 version))]
255 sorted(statuses, key=version),version))]
256
256
257 pull_request_reviewers.append((o.user, o.reasons, st))
257 pull_request_reviewers.append(
258 (o.user, o.reasons, o.mandatory, statuses))
258 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 pull_request.reviewers_statuses()
3359 for reviewer, reasons, mandatory, st in
3360 pull_request.reviewers_statuses()
3352 ]
3361 ]
3353 }
3362 }
3354
3363
@@ -3438,6 +3447,8 b' class PullRequest(Base, _PullRequestBase'
3438 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 rule_user.user for rule_user in self.rule_users
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 user = v.UnicodeString(strip=True, required=True)
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 if isinstance(reviewer_object, tuple):
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(user, pull_request, reasons)
447 reviewer = PullRequestReviewers()
448 reviewer.user = user
449 reviewer.pull_request = pull_request
450 reviewer.reasons = reasons
451 reviewer.mandatory = mandatory
448 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 [(user, ['reason1', 'reason2'])]
911 :param reviewer_data: list of tuples
912 [(user, ['reason1', 'reason2'], mandatory_flag)]
907 """
913 """
908
914
909 reviewers_reasons = {}
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_reasons[user_id] = reasons
919 reviewers[user_id] = {
920 'reasons': reasons, 'mandatory': mandatory}
914
921
915 reviewers_ids = set(reviewers_reasons.keys())
922 reviewers_ids = set(reviewers.keys())
916 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(node, basestring):
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 .border-radius (0);
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,10 +16,215 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
31 */
32 var prButtonLock = function(lockEnabled, msg, scope) {
33 scope = scope || 'all';
34 if (scope == 'all'){
35 prButtonLockChecks['compare'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 } else if (scope == 'compare') {
38 prButtonLockChecks['compare'] = !lockEnabled;
39 } else if (scope == 'reviewers'){
40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 }
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 if (lockEnabled) {
44 $('#save').attr('disabled', 'disabled');
45 }
46 else if (checksMeet) {
47 $('#save').removeAttr('disabled');
48 }
49
50 if (msg) {
51 $('#pr_open_message').html(msg);
52 }
53 };
54
55
56 /**
57 Generate Title and Description for a PullRequest.
58 In case of 1 commits, the title and description is that one commit
59 in case of multiple commits, we iterate on them with max N number of commits,
60 and build description in a form
61 - commitN
62 - commitN+1
63 ...
64
65 Title is then constructed from branch names, or other references,
66 replacing '-' and '_' into spaces
67
68 * @param sourceRef
69 * @param elements
70 * @param limit
71 * @returns {*[]}
21 */
72 */
22 var removeReviewMember = function(reviewer_id, mark_delete){
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
74 var title = '';
75 var desc = '';
76
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 });
81 // only 1 commit, use commit message as title
82 if (elements.length === 1) {
83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
84 }
85 else {
86 // use reference name
87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
88 }
89
90 return [title, desc]
91 };
92
93
94
95 ReviewersController = function () {
96 var self = this;
97 this.$reviewRulesContainer = $('#review_rules');
98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
99 this.forbidReviewUsers = undefined;
100 this.$reviewMembers = $('#review_members');
101 this.currentRequest = null;
102
103 this.defaultForbidReviewUsers = function() {
104 return [
105 {'username': 'default',
106 'user_id': templateContext.default_user.user_id}
107 ];
108 };
109
110 this.hideReviewRules = function() {
111 self.$reviewRulesContainer.hide();
112 };
113
114 this.showReviewRules = function() {
115 self.$reviewRulesContainer.show();
116 };
117
118 this.addRule = function(ruleText) {
119 self.showReviewRules();
120 return '<div>- {0}</div>'.format(ruleText)
121 };
122
123 this.loadReviewRules = function(data) {
124 // reset forbidden Users
125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
126
127 // reset state of review rules
128 self.$rulesList.html('');
129
130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
131 // default rule, case for older repo that don't have any rules stored
132 self.$rulesList.append(
133 self.addRule(
134 _gettext('All reviewers must vote.'))
135 );
136 return self.forbidReviewUsers
137 }
138
139 if (data.rules.voting !== undefined) {
140 if (data.rules.voting < 0){
141 self.$rulesList.append(
142 self.addRule(
143 _gettext('All reviewers must vote.'))
144 )
145 } else if (data.rules.voting === 1) {
146 self.$rulesList.append(
147 self.addRule(
148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
149 )
150
151 } else {
152 self.$rulesList.append(
153 self.addRule(
154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
155 )
156 }
157 }
158 if (data.rules.use_code_authors_for_review) {
159 self.$rulesList.append(
160 self.addRule(
161 _gettext('Reviewers picked from source code changes.'))
162 )
163 }
164 if (data.rules.forbid_adding_reviewers) {
165 $('#add_reviewer_input').remove();
166 self.$rulesList.append(
167 self.addRule(
168 _gettext('Adding new reviewers is forbidden.'))
169 )
170 }
171 if (data.rules.forbid_author_to_review) {
172 self.forbidReviewUsers.push(data.rules_data.pr_author);
173 self.$rulesList.append(
174 self.addRule(
175 _gettext('Author is not allowed to be a reviewer.'))
176 )
177 }
178 return self.forbidReviewUsers
179 };
180
181 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
182
183 if (self.currentRequest) {
184 // make sure we cleanup old running requests before triggering this
185 // again
186 self.currentRequest.abort();
187 }
188
189 $('.calculate-reviewers').show();
190 // reset reviewer members
191 self.$reviewMembers.empty();
192
193 prButtonLock(true, null, 'reviewers');
194 $('#user').hide(); // hide user autocomplete before load
195
196 var url = pyroutes.url('repo_default_reviewers_data',
197 {
198 'repo_name': templateContext.repo_name,
199 'source_repo': sourceRepo,
200 'source_ref': sourceRef[2],
201 'target_repo': targetRepo,
202 'target_ref': targetRef[2]
203 });
204
205 self.currentRequest = $.get(url)
206 .done(function(data) {
207 self.currentRequest = null;
208
209 // review rules
210 self.loadReviewRules(data);
211
212 for (var i = 0; i < data.reviewers.length; i++) {
213 var reviewer = data.reviewers[i];
214 self.addReviewMember(
215 reviewer.user_id, reviewer.firstname,
216 reviewer.lastname, reviewer.username,
217 reviewer.gravatar_link, reviewer.reasons,
218 reviewer.mandatory);
219 }
220 $('.calculate-reviewers').hide();
221 prButtonLock(false, null, 'reviewers');
222 $('#user').show(); // show user autocomplete after load
223 });
224 };
225
226 // check those, refactor
227 this.removeReviewMember = function(reviewer_id, mark_delete) {
23 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
228 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
24
229
25 if(typeof(mark_delete) === undefined){
230 if(typeof(mark_delete) === undefined){
@@ -41,18 +246,21 b' var removeReviewMember = function(review'
41 }
246 }
42 };
247 };
43
248
44 var addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons) {
249 this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) {
45 var members = $('#review_members').get(0);
250 var members = self.$reviewMembers.get(0);
46 var reasons_html = '';
251 var reasons_html = '';
47 var reasons_inputs = '';
252 var reasons_inputs = '';
48 var reasons = reasons || [];
253 var reasons = reasons || [];
254 var mandatory = mandatory || false;
255
49 if (reasons) {
256 if (reasons) {
50 for (var i = 0; i < reasons.length; i++) {
257 for (var i = 0; i < reasons.length; i++) {
51 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
258 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
52 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
259 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
53 }
260 }
54 }
261 }
55 var tmpl = '<li id="reviewer_{2}">'+
262 var tmpl = '' +
263 '<li id="reviewer_{2}" class="reviewer_entry">'+
56 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
264 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
57 '<div class="reviewer_status">'+
265 '<div class="reviewer_status">'+
58 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
266 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
@@ -63,11 +271,27 b' var addReviewMember = function(id, fname'
63 '<input type="hidden" name="user_id" value="{2}">'+
271 '<input type="hidden" name="user_id" value="{2}">'+
64 '<input type="hidden" name="__start__" value="reasons:sequence">'+
272 '<input type="hidden" name="__start__" value="reasons:sequence">'+
65 '{3}'+
273 '{3}'+
66 '<input type="hidden" name="__end__" value="reasons:sequence">'+
274 '<input type="hidden" name="__end__" value="reasons:sequence">';
67 '<div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">' +
275
276 if (mandatory) {
277 tmpl += ''+
278 '<div class="reviewer_member_mandatory_remove">' +
68 '<i class="icon-remove-sign"></i>'+
279 '<i class="icon-remove-sign"></i>'+
69 '</div>'+
280 '</div>' +
70 '</div>'+
281 '<input type="hidden" name="mandatory" value="true">'+
282 '<div class="reviewer_member_mandatory">' +
283 '<i class="icon-lock" title="Mandatory reviewer"></i>'+
284 '</div>';
285
286 } else {
287 tmpl += ''+
288 '<input type="hidden" name="mandatory" value="false">'+
289 '<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember({2})">' +
290 '<i class="icon-remove-sign"></i>'+
291 '</div>';
292 }
293 // continue template
294 tmpl += ''+
71 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
295 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
72 '</li>' ;
296 '</li>' ;
73
297
@@ -76,17 +300,43 b' var addReviewMember = function(id, fname'
76 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
300 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
77 // check if we don't have this ID already in
301 // check if we don't have this ID already in
78 var ids = [];
302 var ids = [];
79 var _els = $('#review_members li').toArray();
303 var _els = self.$reviewMembers.find('li').toArray();
80 for (el in _els){
304 for (el in _els){
81 ids.push(_els[el].id)
305 ids.push(_els[el].id)
82 }
306 }
83 if(ids.indexOf('reviewer_'+id) == -1){
307
308 var userAllowedReview = function(userId) {
309 var allowed = true;
310 $.each(self.forbidReviewUsers, function(index, member_data) {
311 if (parseInt(userId) === member_data['user_id']) {
312 allowed = false;
313 return false // breaks the loop
314 }
315 });
316 return allowed
317 };
318
319 var userAllowed = userAllowedReview(id);
320 if (!userAllowed){
321 alert(_gettext('User `{0}` not allowed to be a reviewer').format(nname));
322 }
323 var shouldAdd = userAllowed && ids.indexOf('reviewer_'+id) == -1;
324
325 if(shouldAdd) {
84 // only add if it's not there
326 // only add if it's not there
85 members.innerHTML += element;
327 members.innerHTML += element;
86 }
328 }
87
329
88 };
330 };
89
331
332 this.updateReviewers = function(repo_name, pull_request_id){
333 var postData = '_method=put&' + $('#reviewers input').serialize();
334 _updatePullRequest(repo_name, pull_request_id, postData);
335 };
336
337 };
338
339
90 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,12 +460,14 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(
464 member_data.id, member_data.first_name, member_data.last_name,
221 member_data.username, member_data.icon_link, reasons);
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(
470 data.id, data.first_name, data.last_name,
226 data.username, data.icon_link, reasons);
471 data.username, data.icon_link, reasons);
227 }
472 }
228
473
@@ -24,7 +24,11 b''
24 </%def>
24 </%def>
25
25
26 <%def name="main_content()">
26 <%def name="main_content()">
27 % if hasattr(c, 'repo_edit_template'):
28 <%include file="${c.repo_edit_template}"/>
29 % else:
27 <%include file="/admin/repos/repo_edit_${c.active}.mako"/>
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;"></span>
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);">&times;</a>
28 onclick="javascript:deform.removeSequenceItem(this);">&times;</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>${_('Origin repository')}:</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': targetRepoName}),
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 origin and destination')}");
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(sourceRef[1],
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>${_('Destination repository')}</strong>: {0}".format(repoData['description'])
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': targetRepoName, 'target_repo_name':repoName}),
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();
@@ -581,7 +494,8 b''
581 // in case we have a pre-selected value, use it now
494 // in case we have a pre-selected value, use it now
582 $sourceRef.select2('val', '${c.default_source_ref}');
495 $sourceRef.select2('val', '${c.default_source_ref}');
583 loadRepoRefDiffPreview();
496 loadRepoRefDiffPreview();
584 loadDefaultReviewers();
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.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
51 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
52 %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>${_('Origin')}:</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,26 +362,39 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 c.allowed_to_update:
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" ></i>
369 <i class="icon-remove-sign"></i>
355 </div>
370 </div>
371 <div class="reviewer_member_mandatory">
372 <i class="icon-lock" title="Mandatory reviewer"></i>
373 </div>
374 % else:
375 %if c.allowed_to_update:
376 <div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
377 <i class="icon-remove-sign" ></i>
378 </div>
379 %endif
356 %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">
386
362 %if not c.pull_request.is_closed():
387 %if not c.pull_request.is_closed():
363 <div id="add_reviewer_input" class='ac' style="display: none;">
388 <div id="add_reviewer" class="ac" style="display: none;">
364 %if c.allowed_to_update:
389 %if c.allowed_to_update:
365 <div class="reviewer_ac">
390 % if not c.forbid_adding_reviewers:
391 <div id="add_reviewer_input" class="reviewer_ac">
366 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
392 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
367 <div id="reviewers_container"></div>
393 <div id="reviewers_container"></div>
368 </div>
394 </div>
369 <div>
395 % endif
370 <button id="update_pull_request" class="btn btn-small">${_('Save Changes')}</button>
396 <div class="pull-right">
397 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
371 </div>
398 </div>
372 %endif
399 %endif
373 </div>
400 </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_input'),
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 that = this;
690 var self = this;
663 this.editButton.on('click', function(e) { that.edit(); });
691 this.editButton.on('click', function(e) { self.edit(); });
664 this.closeButton.on('click', function(e) { that.close(); });
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 = int(location.rsplit('/', 1)[1])
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("wants you to review "
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 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;")
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,12 +1064,9 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', ['show_all', 'index', 'create'])
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):
1068 denied_actions = ['show_all', 'index', 'create']
1069 for action in denied_actions:
1070 response = app.get(url(
1070 response = app.get(url(
1071 controller='pullrequests', action=action,
1071 controller='pullrequests', action=action,
1072 repo_name=backend_svn.repo_name))
1072 repo_name=backend_svn.repo_name))
@@ -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 model.get_by_username(TEST_USER_REGULAR_LOGIN),
990 (TEST_USER_REGULAR_LOGIN, ['default1'], False),
992 model.get_by_username(TEST_USER_REGULAR2_LOGIN),
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