##// END OF EJS Templates
reviewers: store reviewer reasons to database, fixes #4238
dan -
r873:930d1a1f default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,121 +1,122 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import mock
22 import mock
23 import pytest
23 import pytest
24 import urlobject
24 import urlobject
25 from pylons import url
25 from pylons import url
26
26
27 from rhodecode.api.tests.utils import (
27 from rhodecode.api.tests.utils import (
28 build_data, api_call, assert_error, assert_ok)
28 build_data, api_call, assert_error, assert_ok)
29
29
30 pytestmark = pytest.mark.backends("git", "hg")
30 pytestmark = pytest.mark.backends("git", "hg")
31
31
32
32
33 @pytest.mark.usefixtures("testuser_api", "app")
33 @pytest.mark.usefixtures("testuser_api", "app")
34 class TestGetPullRequest(object):
34 class TestGetPullRequest(object):
35
35
36 def test_api_get_pull_request(self, pr_util):
36 def test_api_get_pull_request(self, pr_util):
37 pull_request = pr_util.create_pull_request(mergeable=True)
37 pull_request = pr_util.create_pull_request(mergeable=True)
38 id_, params = build_data(
38 id_, params = build_data(
39 self.apikey, 'get_pull_request',
39 self.apikey, 'get_pull_request',
40 repoid=pull_request.target_repo.repo_name,
40 repoid=pull_request.target_repo.repo_name,
41 pullrequestid=pull_request.pull_request_id)
41 pullrequestid=pull_request.pull_request_id)
42
42
43 response = api_call(self.app, params)
43 response = api_call(self.app, params)
44
44
45 assert response.status == '200 OK'
45 assert response.status == '200 OK'
46
46
47 url_obj = urlobject.URLObject(
47 url_obj = urlobject.URLObject(
48 url(
48 url(
49 'pullrequest_show',
49 'pullrequest_show',
50 repo_name=pull_request.target_repo.repo_name,
50 repo_name=pull_request.target_repo.repo_name,
51 pull_request_id=pull_request.pull_request_id, qualified=True))
51 pull_request_id=pull_request.pull_request_id, qualified=True))
52 pr_url = unicode(
52 pr_url = unicode(
53 url_obj.with_netloc('test.example.com:80'))
53 url_obj.with_netloc('test.example.com:80'))
54 source_url = unicode(
54 source_url = unicode(
55 pull_request.source_repo.clone_url()
55 pull_request.source_repo.clone_url()
56 .with_netloc('test.example.com:80'))
56 .with_netloc('test.example.com:80'))
57 target_url = unicode(
57 target_url = unicode(
58 pull_request.target_repo.clone_url()
58 pull_request.target_repo.clone_url()
59 .with_netloc('test.example.com:80'))
59 .with_netloc('test.example.com:80'))
60 expected = {
60 expected = {
61 'pull_request_id': pull_request.pull_request_id,
61 'pull_request_id': pull_request.pull_request_id,
62 'url': pr_url,
62 'url': pr_url,
63 'title': pull_request.title,
63 'title': pull_request.title,
64 'description': pull_request.description,
64 'description': pull_request.description,
65 'status': pull_request.status,
65 'status': pull_request.status,
66 'created_on': pull_request.created_on,
66 'created_on': pull_request.created_on,
67 'updated_on': pull_request.updated_on,
67 'updated_on': pull_request.updated_on,
68 'commit_ids': pull_request.revisions,
68 'commit_ids': pull_request.revisions,
69 'review_status': pull_request.calculated_review_status(),
69 'review_status': pull_request.calculated_review_status(),
70 'mergeable': {
70 'mergeable': {
71 'status': True,
71 'status': True,
72 'message': 'This pull request can be automatically merged.',
72 'message': 'This pull request can be automatically merged.',
73 },
73 },
74 'source': {
74 'source': {
75 'clone_url': source_url,
75 'clone_url': source_url,
76 'repository': pull_request.source_repo.repo_name,
76 'repository': pull_request.source_repo.repo_name,
77 'reference': {
77 'reference': {
78 'name': pull_request.source_ref_parts.name,
78 'name': pull_request.source_ref_parts.name,
79 'type': pull_request.source_ref_parts.type,
79 'type': pull_request.source_ref_parts.type,
80 'commit_id': pull_request.source_ref_parts.commit_id,
80 'commit_id': pull_request.source_ref_parts.commit_id,
81 },
81 },
82 },
82 },
83 'target': {
83 'target': {
84 'clone_url': target_url,
84 'clone_url': target_url,
85 'repository': pull_request.target_repo.repo_name,
85 'repository': pull_request.target_repo.repo_name,
86 'reference': {
86 'reference': {
87 'name': pull_request.target_ref_parts.name,
87 'name': pull_request.target_ref_parts.name,
88 'type': pull_request.target_ref_parts.type,
88 'type': pull_request.target_ref_parts.type,
89 'commit_id': pull_request.target_ref_parts.commit_id,
89 'commit_id': pull_request.target_ref_parts.commit_id,
90 },
90 },
91 },
91 },
92 'author': pull_request.author.get_api_data(include_secrets=False,
92 'author': pull_request.author.get_api_data(include_secrets=False,
93 details='basic'),
93 details='basic'),
94 'reviewers': [
94 'reviewers': [
95 {
95 {
96 'user': reviewer.get_api_data(include_secrets=False,
96 'user': reviewer.get_api_data(include_secrets=False,
97 details='basic'),
97 details='basic'),
98 'reasons': reasons,
98 'review_status': st[0][1].status if st else 'not_reviewed',
99 'review_status': st[0][1].status if st else 'not_reviewed',
99 }
100 }
100 for reviewer, st in pull_request.reviewers_statuses()
101 for reviewer, reasons, st in pull_request.reviewers_statuses()
101 ]
102 ]
102 }
103 }
103 assert_ok(id_, expected, response.body)
104 assert_ok(id_, expected, response.body)
104
105
105 def test_api_get_pull_request_repo_error(self):
106 def test_api_get_pull_request_repo_error(self):
106 id_, params = build_data(
107 id_, params = build_data(
107 self.apikey, 'get_pull_request',
108 self.apikey, 'get_pull_request',
108 repoid=666, pullrequestid=1)
109 repoid=666, pullrequestid=1)
109 response = api_call(self.app, params)
110 response = api_call(self.app, params)
110
111
111 expected = 'repository `666` does not exist'
112 expected = 'repository `666` does not exist'
112 assert_error(id_, expected, given=response.body)
113 assert_error(id_, expected, given=response.body)
113
114
114 def test_api_get_pull_request_pull_request_error(self):
115 def test_api_get_pull_request_pull_request_error(self):
115 id_, params = build_data(
116 id_, params = build_data(
116 self.apikey, 'get_pull_request',
117 self.apikey, 'get_pull_request',
117 repoid=1, pullrequestid=666)
118 repoid=1, pullrequestid=666)
118 response = api_call(self.app, params)
119 response = api_call(self.app, params)
119
120
120 expected = 'pull request `666` does not exist'
121 expected = 'pull request `666` does not exist'
121 assert_error(id_, expected, given=response.body)
122 assert_error(id_, expected, given=response.body)
@@ -1,635 +1,660 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
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
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,
28 has_repo_permissions, resolve_ref_or_error)
28 has_repo_permissions, resolve_ref_or_error)
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.base import vcs_operation_context
30 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.utils2 import str2bool
31 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.model.changeset_status import ChangesetStatusModel
32 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.comment import ChangesetCommentsModel
33 from rhodecode.model.comment import ChangesetCommentsModel
34 from rhodecode.model.db import Session, ChangesetStatus
34 from rhodecode.model.db import Session, ChangesetStatus
35 from rhodecode.model.pull_request import PullRequestModel
35 from rhodecode.model.pull_request import PullRequestModel
36 from rhodecode.model.settings import SettingsModel
36 from rhodecode.model.settings import SettingsModel
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 @jsonrpc_method()
41 @jsonrpc_method()
42 def get_pull_request(request, apiuser, repoid, pullrequestid):
42 def get_pull_request(request, apiuser, repoid, pullrequestid):
43 """
43 """
44 Get a pull request based on the given ID.
44 Get a pull request based on the given ID.
45
45
46 :param apiuser: This is filled automatically from the |authtoken|.
46 :param apiuser: This is filled automatically from the |authtoken|.
47 :type apiuser: AuthUser
47 :type apiuser: AuthUser
48 :param repoid: Repository name or repository ID from where the pull
48 :param repoid: Repository name or repository ID from where the pull
49 request was opened.
49 request was opened.
50 :type repoid: str or int
50 :type repoid: str or int
51 :param pullrequestid: ID of the requested pull request.
51 :param pullrequestid: ID of the requested pull request.
52 :type pullrequestid: int
52 :type pullrequestid: int
53
53
54 Example output:
54 Example output:
55
55
56 .. code-block:: bash
56 .. code-block:: bash
57
57
58 "id": <id_given_in_input>,
58 "id": <id_given_in_input>,
59 "result":
59 "result":
60 {
60 {
61 "pull_request_id": "<pull_request_id>",
61 "pull_request_id": "<pull_request_id>",
62 "url": "<url>",
62 "url": "<url>",
63 "title": "<title>",
63 "title": "<title>",
64 "description": "<description>",
64 "description": "<description>",
65 "status" : "<status>",
65 "status" : "<status>",
66 "created_on": "<date_time_created>",
66 "created_on": "<date_time_created>",
67 "updated_on": "<date_time_updated>",
67 "updated_on": "<date_time_updated>",
68 "commit_ids": [
68 "commit_ids": [
69 ...
69 ...
70 "<commit_id>",
70 "<commit_id>",
71 "<commit_id>",
71 "<commit_id>",
72 ...
72 ...
73 ],
73 ],
74 "review_status": "<review_status>",
74 "review_status": "<review_status>",
75 "mergeable": {
75 "mergeable": {
76 "status": "<bool>",
76 "status": "<bool>",
77 "message": "<message>",
77 "message": "<message>",
78 },
78 },
79 "source": {
79 "source": {
80 "clone_url": "<clone_url>",
80 "clone_url": "<clone_url>",
81 "repository": "<repository_name>",
81 "repository": "<repository_name>",
82 "reference":
82 "reference":
83 {
83 {
84 "name": "<name>",
84 "name": "<name>",
85 "type": "<type>",
85 "type": "<type>",
86 "commit_id": "<commit_id>",
86 "commit_id": "<commit_id>",
87 }
87 }
88 },
88 },
89 "target": {
89 "target": {
90 "clone_url": "<clone_url>",
90 "clone_url": "<clone_url>",
91 "repository": "<repository_name>",
91 "repository": "<repository_name>",
92 "reference":
92 "reference":
93 {
93 {
94 "name": "<name>",
94 "name": "<name>",
95 "type": "<type>",
95 "type": "<type>",
96 "commit_id": "<commit_id>",
96 "commit_id": "<commit_id>",
97 }
97 }
98 },
98 },
99 "author": <user_obj>,
99 "author": <user_obj>,
100 "reviewers": [
100 "reviewers": [
101 ...
101 ...
102 {
102 {
103 "user": "<user_obj>",
103 "user": "<user_obj>",
104 "review_status": "<review_status>",
104 "review_status": "<review_status>",
105 }
105 }
106 ...
106 ...
107 ]
107 ]
108 },
108 },
109 "error": null
109 "error": null
110 """
110 """
111 get_repo_or_error(repoid)
111 get_repo_or_error(repoid)
112 pull_request = get_pull_request_or_error(pullrequestid)
112 pull_request = get_pull_request_or_error(pullrequestid)
113 if not PullRequestModel().check_user_read(
113 if not PullRequestModel().check_user_read(
114 pull_request, apiuser, api=True):
114 pull_request, apiuser, api=True):
115 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
115 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
116 data = pull_request.get_api_data()
116 data = pull_request.get_api_data()
117 return data
117 return data
118
118
119
119
120 @jsonrpc_method()
120 @jsonrpc_method()
121 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
121 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
122 """
122 """
123 Get all pull requests from the repository specified in `repoid`.
123 Get all pull requests from the repository specified in `repoid`.
124
124
125 :param apiuser: This is filled automatically from the |authtoken|.
125 :param apiuser: This is filled automatically from the |authtoken|.
126 :type apiuser: AuthUser
126 :type apiuser: AuthUser
127 :param repoid: Repository name or repository ID.
127 :param repoid: Repository name or repository ID.
128 :type repoid: str or int
128 :type repoid: str or int
129 :param status: Only return pull requests with the specified status.
129 :param status: Only return pull requests with the specified status.
130 Valid options are.
130 Valid options are.
131 * ``new`` (default)
131 * ``new`` (default)
132 * ``open``
132 * ``open``
133 * ``closed``
133 * ``closed``
134 :type status: str
134 :type status: str
135
135
136 Example output:
136 Example output:
137
137
138 .. code-block:: bash
138 .. code-block:: bash
139
139
140 "id": <id_given_in_input>,
140 "id": <id_given_in_input>,
141 "result":
141 "result":
142 [
142 [
143 ...
143 ...
144 {
144 {
145 "pull_request_id": "<pull_request_id>",
145 "pull_request_id": "<pull_request_id>",
146 "url": "<url>",
146 "url": "<url>",
147 "title" : "<title>",
147 "title" : "<title>",
148 "description": "<description>",
148 "description": "<description>",
149 "status": "<status>",
149 "status": "<status>",
150 "created_on": "<date_time_created>",
150 "created_on": "<date_time_created>",
151 "updated_on": "<date_time_updated>",
151 "updated_on": "<date_time_updated>",
152 "commit_ids": [
152 "commit_ids": [
153 ...
153 ...
154 "<commit_id>",
154 "<commit_id>",
155 "<commit_id>",
155 "<commit_id>",
156 ...
156 ...
157 ],
157 ],
158 "review_status": "<review_status>",
158 "review_status": "<review_status>",
159 "mergeable": {
159 "mergeable": {
160 "status": "<bool>",
160 "status": "<bool>",
161 "message: "<message>",
161 "message: "<message>",
162 },
162 },
163 "source": {
163 "source": {
164 "clone_url": "<clone_url>",
164 "clone_url": "<clone_url>",
165 "reference":
165 "reference":
166 {
166 {
167 "name": "<name>",
167 "name": "<name>",
168 "type": "<type>",
168 "type": "<type>",
169 "commit_id": "<commit_id>",
169 "commit_id": "<commit_id>",
170 }
170 }
171 },
171 },
172 "target": {
172 "target": {
173 "clone_url": "<clone_url>",
173 "clone_url": "<clone_url>",
174 "reference":
174 "reference":
175 {
175 {
176 "name": "<name>",
176 "name": "<name>",
177 "type": "<type>",
177 "type": "<type>",
178 "commit_id": "<commit_id>",
178 "commit_id": "<commit_id>",
179 }
179 }
180 },
180 },
181 "author": <user_obj>,
181 "author": <user_obj>,
182 "reviewers": [
182 "reviewers": [
183 ...
183 ...
184 {
184 {
185 "user": "<user_obj>",
185 "user": "<user_obj>",
186 "review_status": "<review_status>",
186 "review_status": "<review_status>",
187 }
187 }
188 ...
188 ...
189 ]
189 ]
190 }
190 }
191 ...
191 ...
192 ],
192 ],
193 "error": null
193 "error": null
194
194
195 """
195 """
196 repo = get_repo_or_error(repoid)
196 repo = get_repo_or_error(repoid)
197 if not has_superadmin_permission(apiuser):
197 if not has_superadmin_permission(apiuser):
198 _perms = (
198 _perms = (
199 'repository.admin', 'repository.write', 'repository.read',)
199 'repository.admin', 'repository.write', 'repository.read',)
200 has_repo_permissions(apiuser, repoid, repo, _perms)
200 has_repo_permissions(apiuser, repoid, repo, _perms)
201
201
202 status = Optional.extract(status)
202 status = Optional.extract(status)
203 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
203 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
204 data = [pr.get_api_data() for pr in pull_requests]
204 data = [pr.get_api_data() for pr in pull_requests]
205 return data
205 return data
206
206
207
207
208 @jsonrpc_method()
208 @jsonrpc_method()
209 def merge_pull_request(request, apiuser, repoid, pullrequestid,
209 def merge_pull_request(request, apiuser, repoid, pullrequestid,
210 userid=Optional(OAttr('apiuser'))):
210 userid=Optional(OAttr('apiuser'))):
211 """
211 """
212 Merge the pull request specified by `pullrequestid` into its target
212 Merge the pull request specified by `pullrequestid` into its target
213 repository.
213 repository.
214
214
215 :param apiuser: This is filled automatically from the |authtoken|.
215 :param apiuser: This is filled automatically from the |authtoken|.
216 :type apiuser: AuthUser
216 :type apiuser: AuthUser
217 :param repoid: The Repository name or repository ID of the
217 :param repoid: The Repository name or repository ID of the
218 target repository to which the |pr| is to be merged.
218 target repository to which the |pr| is to be merged.
219 :type repoid: str or int
219 :type repoid: str or int
220 :param pullrequestid: ID of the pull request which shall be merged.
220 :param pullrequestid: ID of the pull request which shall be merged.
221 :type pullrequestid: int
221 :type pullrequestid: int
222 :param userid: Merge the pull request as this user.
222 :param userid: Merge the pull request as this user.
223 :type userid: Optional(str or int)
223 :type userid: Optional(str or int)
224
224
225 Example output:
225 Example output:
226
226
227 .. code-block:: bash
227 .. code-block:: bash
228
228
229 "id": <id_given_in_input>,
229 "id": <id_given_in_input>,
230 "result":
230 "result":
231 {
231 {
232 "executed": "<bool>",
232 "executed": "<bool>",
233 "failure_reason": "<int>",
233 "failure_reason": "<int>",
234 "merge_commit_id": "<merge_commit_id>",
234 "merge_commit_id": "<merge_commit_id>",
235 "possible": "<bool>"
235 "possible": "<bool>"
236 },
236 },
237 "error": null
237 "error": null
238
238
239 """
239 """
240 repo = get_repo_or_error(repoid)
240 repo = get_repo_or_error(repoid)
241 if not isinstance(userid, Optional):
241 if not isinstance(userid, Optional):
242 if (has_superadmin_permission(apiuser) or
242 if (has_superadmin_permission(apiuser) or
243 HasRepoPermissionAnyApi('repository.admin')(
243 HasRepoPermissionAnyApi('repository.admin')(
244 user=apiuser, repo_name=repo.repo_name)):
244 user=apiuser, repo_name=repo.repo_name)):
245 apiuser = get_user_or_error(userid)
245 apiuser = get_user_or_error(userid)
246 else:
246 else:
247 raise JSONRPCError('userid is not the same as your user')
247 raise JSONRPCError('userid is not the same as your user')
248
248
249 pull_request = get_pull_request_or_error(pullrequestid)
249 pull_request = get_pull_request_or_error(pullrequestid)
250 if not PullRequestModel().check_user_merge(
250 if not PullRequestModel().check_user_merge(
251 pull_request, apiuser, api=True):
251 pull_request, apiuser, api=True):
252 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
252 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
253 if pull_request.is_closed():
253 if pull_request.is_closed():
254 raise JSONRPCError(
254 raise JSONRPCError(
255 'pull request `%s` merge failed, pull request is closed' % (
255 'pull request `%s` merge failed, pull request is closed' % (
256 pullrequestid,))
256 pullrequestid,))
257
257
258 target_repo = pull_request.target_repo
258 target_repo = pull_request.target_repo
259 extras = vcs_operation_context(
259 extras = vcs_operation_context(
260 request.environ, repo_name=target_repo.repo_name,
260 request.environ, repo_name=target_repo.repo_name,
261 username=apiuser.username, action='push',
261 username=apiuser.username, action='push',
262 scm=target_repo.repo_type)
262 scm=target_repo.repo_type)
263 data = PullRequestModel().merge(pull_request, apiuser, extras=extras)
263 data = PullRequestModel().merge(pull_request, apiuser, extras=extras)
264 if data.executed:
264 if data.executed:
265 PullRequestModel().close_pull_request(
265 PullRequestModel().close_pull_request(
266 pull_request.pull_request_id, apiuser)
266 pull_request.pull_request_id, apiuser)
267
267
268 Session().commit()
268 Session().commit()
269 return data
269 return data
270
270
271
271
272 @jsonrpc_method()
272 @jsonrpc_method()
273 def close_pull_request(request, apiuser, repoid, pullrequestid,
273 def close_pull_request(request, apiuser, repoid, pullrequestid,
274 userid=Optional(OAttr('apiuser'))):
274 userid=Optional(OAttr('apiuser'))):
275 """
275 """
276 Close the pull request specified by `pullrequestid`.
276 Close the pull request specified by `pullrequestid`.
277
277
278 :param apiuser: This is filled automatically from the |authtoken|.
278 :param apiuser: This is filled automatically from the |authtoken|.
279 :type apiuser: AuthUser
279 :type apiuser: AuthUser
280 :param repoid: Repository name or repository ID to which the pull
280 :param repoid: Repository name or repository ID to which the pull
281 request belongs.
281 request belongs.
282 :type repoid: str or int
282 :type repoid: str or int
283 :param pullrequestid: ID of the pull request to be closed.
283 :param pullrequestid: ID of the pull request to be closed.
284 :type pullrequestid: int
284 :type pullrequestid: int
285 :param userid: Close the pull request as this user.
285 :param userid: Close the pull request as this user.
286 :type userid: Optional(str or int)
286 :type userid: Optional(str or int)
287
287
288 Example output:
288 Example output:
289
289
290 .. code-block:: bash
290 .. code-block:: bash
291
291
292 "id": <id_given_in_input>,
292 "id": <id_given_in_input>,
293 "result":
293 "result":
294 {
294 {
295 "pull_request_id": "<int>",
295 "pull_request_id": "<int>",
296 "closed": "<bool>"
296 "closed": "<bool>"
297 },
297 },
298 "error": null
298 "error": null
299
299
300 """
300 """
301 repo = get_repo_or_error(repoid)
301 repo = get_repo_or_error(repoid)
302 if not isinstance(userid, Optional):
302 if not isinstance(userid, Optional):
303 if (has_superadmin_permission(apiuser) or
303 if (has_superadmin_permission(apiuser) or
304 HasRepoPermissionAnyApi('repository.admin')(
304 HasRepoPermissionAnyApi('repository.admin')(
305 user=apiuser, repo_name=repo.repo_name)):
305 user=apiuser, repo_name=repo.repo_name)):
306 apiuser = get_user_or_error(userid)
306 apiuser = get_user_or_error(userid)
307 else:
307 else:
308 raise JSONRPCError('userid is not the same as your user')
308 raise JSONRPCError('userid is not the same as your user')
309
309
310 pull_request = get_pull_request_or_error(pullrequestid)
310 pull_request = get_pull_request_or_error(pullrequestid)
311 if not PullRequestModel().check_user_update(
311 if not PullRequestModel().check_user_update(
312 pull_request, apiuser, api=True):
312 pull_request, apiuser, api=True):
313 raise JSONRPCError(
313 raise JSONRPCError(
314 'pull request `%s` close failed, no permission to close.' % (
314 'pull request `%s` close failed, no permission to close.' % (
315 pullrequestid,))
315 pullrequestid,))
316 if pull_request.is_closed():
316 if pull_request.is_closed():
317 raise JSONRPCError(
317 raise JSONRPCError(
318 'pull request `%s` is already closed' % (pullrequestid,))
318 'pull request `%s` is already closed' % (pullrequestid,))
319
319
320 PullRequestModel().close_pull_request(
320 PullRequestModel().close_pull_request(
321 pull_request.pull_request_id, apiuser)
321 pull_request.pull_request_id, apiuser)
322 Session().commit()
322 Session().commit()
323 data = {
323 data = {
324 'pull_request_id': pull_request.pull_request_id,
324 'pull_request_id': pull_request.pull_request_id,
325 'closed': True,
325 'closed': True,
326 }
326 }
327 return data
327 return data
328
328
329
329
330 @jsonrpc_method()
330 @jsonrpc_method()
331 def comment_pull_request(request, apiuser, repoid, pullrequestid,
331 def comment_pull_request(request, apiuser, repoid, pullrequestid,
332 message=Optional(None), status=Optional(None),
332 message=Optional(None), status=Optional(None),
333 userid=Optional(OAttr('apiuser'))):
333 userid=Optional(OAttr('apiuser'))):
334 """
334 """
335 Comment on the pull request specified with the `pullrequestid`,
335 Comment on the pull request specified with the `pullrequestid`,
336 in the |repo| specified by the `repoid`, and optionally change the
336 in the |repo| specified by the `repoid`, and optionally change the
337 review status.
337 review status.
338
338
339 :param apiuser: This is filled automatically from the |authtoken|.
339 :param apiuser: This is filled automatically from the |authtoken|.
340 :type apiuser: AuthUser
340 :type apiuser: AuthUser
341 :param repoid: The repository name or repository ID.
341 :param repoid: The repository name or repository ID.
342 :type repoid: str or int
342 :type repoid: str or int
343 :param pullrequestid: The pull request ID.
343 :param pullrequestid: The pull request ID.
344 :type pullrequestid: int
344 :type pullrequestid: int
345 :param message: The text content of the comment.
345 :param message: The text content of the comment.
346 :type message: str
346 :type message: str
347 :param status: (**Optional**) Set the approval status of the pull
347 :param status: (**Optional**) Set the approval status of the pull
348 request. Valid options are:
348 request. Valid options are:
349 * not_reviewed
349 * not_reviewed
350 * approved
350 * approved
351 * rejected
351 * rejected
352 * under_review
352 * under_review
353 :type status: str
353 :type status: str
354 :param userid: Comment on the pull request as this user
354 :param userid: Comment on the pull request as this user
355 :type userid: Optional(str or int)
355 :type userid: Optional(str or int)
356
356
357 Example output:
357 Example output:
358
358
359 .. code-block:: bash
359 .. code-block:: bash
360
360
361 id : <id_given_in_input>
361 id : <id_given_in_input>
362 result :
362 result :
363 {
363 {
364 "pull_request_id": "<Integer>",
364 "pull_request_id": "<Integer>",
365 "comment_id": "<Integer>"
365 "comment_id": "<Integer>"
366 }
366 }
367 error : null
367 error : null
368 """
368 """
369 repo = get_repo_or_error(repoid)
369 repo = get_repo_or_error(repoid)
370 if not isinstance(userid, Optional):
370 if not isinstance(userid, Optional):
371 if (has_superadmin_permission(apiuser) or
371 if (has_superadmin_permission(apiuser) or
372 HasRepoPermissionAnyApi('repository.admin')(
372 HasRepoPermissionAnyApi('repository.admin')(
373 user=apiuser, repo_name=repo.repo_name)):
373 user=apiuser, repo_name=repo.repo_name)):
374 apiuser = get_user_or_error(userid)
374 apiuser = get_user_or_error(userid)
375 else:
375 else:
376 raise JSONRPCError('userid is not the same as your user')
376 raise JSONRPCError('userid is not the same as your user')
377
377
378 pull_request = get_pull_request_or_error(pullrequestid)
378 pull_request = get_pull_request_or_error(pullrequestid)
379 if not PullRequestModel().check_user_read(
379 if not PullRequestModel().check_user_read(
380 pull_request, apiuser, api=True):
380 pull_request, apiuser, api=True):
381 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
381 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
382 message = Optional.extract(message)
382 message = Optional.extract(message)
383 status = Optional.extract(status)
383 status = Optional.extract(status)
384 if not message and not status:
384 if not message and not status:
385 raise JSONRPCError('message and status parameter missing')
385 raise JSONRPCError('message and status parameter missing')
386
386
387 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
387 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
388 status is not None):
388 status is not None):
389 raise JSONRPCError('unknown comment status`%s`' % status)
389 raise JSONRPCError('unknown comment status`%s`' % status)
390
390
391 allowed_to_change_status = PullRequestModel().check_user_change_status(
391 allowed_to_change_status = PullRequestModel().check_user_change_status(
392 pull_request, apiuser)
392 pull_request, apiuser)
393 text = message
393 text = message
394 if status and allowed_to_change_status:
394 if status and allowed_to_change_status:
395 st_message = (('Status change %(transition_icon)s %(status)s')
395 st_message = (('Status change %(transition_icon)s %(status)s')
396 % {'transition_icon': '>',
396 % {'transition_icon': '>',
397 'status': ChangesetStatus.get_status_lbl(status)})
397 'status': ChangesetStatus.get_status_lbl(status)})
398 text = message or st_message
398 text = message or st_message
399
399
400 rc_config = SettingsModel().get_all_settings()
400 rc_config = SettingsModel().get_all_settings()
401 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
401 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
402 comment = ChangesetCommentsModel().create(
402 comment = ChangesetCommentsModel().create(
403 text=text,
403 text=text,
404 repo=pull_request.target_repo.repo_id,
404 repo=pull_request.target_repo.repo_id,
405 user=apiuser.user_id,
405 user=apiuser.user_id,
406 pull_request=pull_request.pull_request_id,
406 pull_request=pull_request.pull_request_id,
407 f_path=None,
407 f_path=None,
408 line_no=None,
408 line_no=None,
409 status_change=(ChangesetStatus.get_status_lbl(status)
409 status_change=(ChangesetStatus.get_status_lbl(status)
410 if status and allowed_to_change_status else None),
410 if status and allowed_to_change_status else None),
411 status_change_type=(status
411 status_change_type=(status
412 if status and allowed_to_change_status else None),
412 if status and allowed_to_change_status else None),
413 closing_pr=False,
413 closing_pr=False,
414 renderer=renderer
414 renderer=renderer
415 )
415 )
416
416
417 if allowed_to_change_status and status:
417 if allowed_to_change_status and status:
418 ChangesetStatusModel().set_status(
418 ChangesetStatusModel().set_status(
419 pull_request.target_repo.repo_id,
419 pull_request.target_repo.repo_id,
420 status,
420 status,
421 apiuser.user_id,
421 apiuser.user_id,
422 comment,
422 comment,
423 pull_request=pull_request.pull_request_id
423 pull_request=pull_request.pull_request_id
424 )
424 )
425 Session().flush()
425 Session().flush()
426
426
427 Session().commit()
427 Session().commit()
428 data = {
428 data = {
429 'pull_request_id': pull_request.pull_request_id,
429 'pull_request_id': pull_request.pull_request_id,
430 'comment_id': comment.comment_id,
430 'comment_id': comment.comment_id,
431 'status': status
431 'status': status
432 }
432 }
433 return data
433 return data
434
434
435
435
436 @jsonrpc_method()
436 @jsonrpc_method()
437 def create_pull_request(
437 def create_pull_request(
438 request, apiuser, source_repo, target_repo, source_ref, target_ref,
438 request, apiuser, source_repo, target_repo, source_ref, target_ref,
439 title, description=Optional(''), reviewers=Optional(None)):
439 title, description=Optional(''), reviewers=Optional(None)):
440 """
440 """
441 Creates a new pull request.
441 Creates a new pull request.
442
442
443 Accepts refs in the following formats:
443 Accepts refs in the following formats:
444
444
445 * branch:<branch_name>:<sha>
445 * branch:<branch_name>:<sha>
446 * branch:<branch_name>
446 * branch:<branch_name>
447 * bookmark:<bookmark_name>:<sha> (Mercurial only)
447 * bookmark:<bookmark_name>:<sha> (Mercurial only)
448 * bookmark:<bookmark_name> (Mercurial only)
448 * bookmark:<bookmark_name> (Mercurial only)
449
449
450 :param apiuser: This is filled automatically from the |authtoken|.
450 :param apiuser: This is filled automatically from the |authtoken|.
451 :type apiuser: AuthUser
451 :type apiuser: AuthUser
452 :param source_repo: Set the source repository name.
452 :param source_repo: Set the source repository name.
453 :type source_repo: str
453 :type source_repo: str
454 :param target_repo: Set the target repository name.
454 :param target_repo: Set the target repository name.
455 :type target_repo: str
455 :type target_repo: str
456 :param source_ref: Set the source ref name.
456 :param source_ref: Set the source ref name.
457 :type source_ref: str
457 :type source_ref: str
458 :param target_ref: Set the target ref name.
458 :param target_ref: Set the target ref name.
459 :type target_ref: str
459 :type target_ref: str
460 :param title: Set the pull request title.
460 :param title: Set the pull request title.
461 :type title: str
461 :type title: str
462 :param description: Set the pull request description.
462 :param description: Set the pull request description.
463 :type description: Optional(str)
463 :type description: Optional(str)
464 :param reviewers: Set the new pull request reviewers list.
464 :param reviewers: Set the new pull request reviewers list.
465 :type reviewers: Optional(list)
465 :type reviewers: Optional(list)
466 Accepts username strings or objects of the format:
467 {
468 'username': 'nick', 'reasons': ['original author']
469 }
466 """
470 """
471
467 source = get_repo_or_error(source_repo)
472 source = get_repo_or_error(source_repo)
468 target = get_repo_or_error(target_repo)
473 target = get_repo_or_error(target_repo)
469 if not has_superadmin_permission(apiuser):
474 if not has_superadmin_permission(apiuser):
470 _perms = ('repository.admin', 'repository.write', 'repository.read',)
475 _perms = ('repository.admin', 'repository.write', 'repository.read',)
471 has_repo_permissions(apiuser, source_repo, source, _perms)
476 has_repo_permissions(apiuser, source_repo, source, _perms)
472
477
473 full_source_ref = resolve_ref_or_error(source_ref, source)
478 full_source_ref = resolve_ref_or_error(source_ref, source)
474 full_target_ref = resolve_ref_or_error(target_ref, target)
479 full_target_ref = resolve_ref_or_error(target_ref, target)
475 source_commit = get_commit_or_error(full_source_ref, source)
480 source_commit = get_commit_or_error(full_source_ref, source)
476 target_commit = get_commit_or_error(full_target_ref, target)
481 target_commit = get_commit_or_error(full_target_ref, target)
477 source_scm = source.scm_instance()
482 source_scm = source.scm_instance()
478 target_scm = target.scm_instance()
483 target_scm = target.scm_instance()
479
484
480 commit_ranges = target_scm.compare(
485 commit_ranges = target_scm.compare(
481 target_commit.raw_id, source_commit.raw_id, source_scm,
486 target_commit.raw_id, source_commit.raw_id, source_scm,
482 merge=True, pre_load=[])
487 merge=True, pre_load=[])
483
488
484 ancestor = target_scm.get_common_ancestor(
489 ancestor = target_scm.get_common_ancestor(
485 target_commit.raw_id, source_commit.raw_id, source_scm)
490 target_commit.raw_id, source_commit.raw_id, source_scm)
486
491
487 if not commit_ranges:
492 if not commit_ranges:
488 raise JSONRPCError('no commits found')
493 raise JSONRPCError('no commits found')
489
494
490 if not ancestor:
495 if not ancestor:
491 raise JSONRPCError('no common ancestor found')
496 raise JSONRPCError('no common ancestor found')
492
497
493 reviewer_names = Optional.extract(reviewers) or []
498 reviewer_objects = Optional.extract(reviewers) or []
494 if not isinstance(reviewer_names, list):
499 if not isinstance(reviewer_objects, list):
495 raise JSONRPCError('reviewers should be specified as a list')
500 raise JSONRPCError('reviewers should be specified as a list')
496
501
497 reviewer_users = [get_user_or_error(n) for n in reviewer_names]
502 reviewers_reasons = []
498 reviewer_ids = [u.user_id for u in reviewer_users]
503 for reviewer_object in reviewer_objects:
504 reviewer_reasons = []
505 if isinstance(reviewer_object, (basestring, int)):
506 reviewer_username = reviewer_object
507 else:
508 reviewer_username = reviewer_object['username']
509 reviewer_reasons = reviewer_object.get('reasons', [])
510
511 user = get_user_or_error(reviewer_username)
512 reviewers_reasons.append((user.user_id, reviewer_reasons))
499
513
500 pull_request_model = PullRequestModel()
514 pull_request_model = PullRequestModel()
501 pull_request = pull_request_model.create(
515 pull_request = pull_request_model.create(
502 created_by=apiuser.user_id,
516 created_by=apiuser.user_id,
503 source_repo=source_repo,
517 source_repo=source_repo,
504 source_ref=full_source_ref,
518 source_ref=full_source_ref,
505 target_repo=target_repo,
519 target_repo=target_repo,
506 target_ref=full_target_ref,
520 target_ref=full_target_ref,
507 revisions=reversed(
521 revisions=reversed(
508 [commit.raw_id for commit in reversed(commit_ranges)]),
522 [commit.raw_id for commit in reversed(commit_ranges)]),
509 reviewers=reviewer_ids,
523 reviewers=reviewers_reasons,
510 title=title,
524 title=title,
511 description=Optional.extract(description)
525 description=Optional.extract(description)
512 )
526 )
513
527
514 Session().commit()
528 Session().commit()
515 data = {
529 data = {
516 'msg': 'Created new pull request `{}`'.format(title),
530 'msg': 'Created new pull request `{}`'.format(title),
517 'pull_request_id': pull_request.pull_request_id,
531 'pull_request_id': pull_request.pull_request_id,
518 }
532 }
519 return data
533 return data
520
534
521
535
522 @jsonrpc_method()
536 @jsonrpc_method()
523 def update_pull_request(
537 def update_pull_request(
524 request, apiuser, repoid, pullrequestid, title=Optional(''),
538 request, apiuser, repoid, pullrequestid, title=Optional(''),
525 description=Optional(''), reviewers=Optional(None),
539 description=Optional(''), reviewers=Optional(None),
526 update_commits=Optional(None), close_pull_request=Optional(None)):
540 update_commits=Optional(None), close_pull_request=Optional(None)):
527 """
541 """
528 Updates a pull request.
542 Updates a pull request.
529
543
530 :param apiuser: This is filled automatically from the |authtoken|.
544 :param apiuser: This is filled automatically from the |authtoken|.
531 :type apiuser: AuthUser
545 :type apiuser: AuthUser
532 :param repoid: The repository name or repository ID.
546 :param repoid: The repository name or repository ID.
533 :type repoid: str or int
547 :type repoid: str or int
534 :param pullrequestid: The pull request ID.
548 :param pullrequestid: The pull request ID.
535 :type pullrequestid: int
549 :type pullrequestid: int
536 :param title: Set the pull request title.
550 :param title: Set the pull request title.
537 :type title: str
551 :type title: str
538 :param description: Update pull request description.
552 :param description: Update pull request description.
539 :type description: Optional(str)
553 :type description: Optional(str)
540 :param reviewers: Update pull request reviewers list with new value.
554 :param reviewers: Update pull request reviewers list with new value.
541 :type reviewers: Optional(list)
555 :type reviewers: Optional(list)
542 :param update_commits: Trigger update of commits for this pull request
556 :param update_commits: Trigger update of commits for this pull request
543 :type: update_commits: Optional(bool)
557 :type: update_commits: Optional(bool)
544 :param close_pull_request: Close this pull request with rejected state
558 :param close_pull_request: Close this pull request with rejected state
545 :type: close_pull_request: Optional(bool)
559 :type: close_pull_request: Optional(bool)
546
560
547 Example output:
561 Example output:
548
562
549 .. code-block:: bash
563 .. code-block:: bash
550
564
551 id : <id_given_in_input>
565 id : <id_given_in_input>
552 result :
566 result :
553 {
567 {
554 "msg": "Updated pull request `63`",
568 "msg": "Updated pull request `63`",
555 "pull_request": <pull_request_object>,
569 "pull_request": <pull_request_object>,
556 "updated_reviewers": {
570 "updated_reviewers": {
557 "added": [
571 "added": [
558 "username"
572 "username"
559 ],
573 ],
560 "removed": []
574 "removed": []
561 },
575 },
562 "updated_commits": {
576 "updated_commits": {
563 "added": [
577 "added": [
564 "<sha1_hash>"
578 "<sha1_hash>"
565 ],
579 ],
566 "common": [
580 "common": [
567 "<sha1_hash>",
581 "<sha1_hash>",
568 "<sha1_hash>",
582 "<sha1_hash>",
569 ],
583 ],
570 "removed": []
584 "removed": []
571 }
585 }
572 }
586 }
573 error : null
587 error : null
574 """
588 """
575
589
576 repo = get_repo_or_error(repoid)
590 repo = get_repo_or_error(repoid)
577 pull_request = get_pull_request_or_error(pullrequestid)
591 pull_request = get_pull_request_or_error(pullrequestid)
578 if not PullRequestModel().check_user_update(
592 if not PullRequestModel().check_user_update(
579 pull_request, apiuser, api=True):
593 pull_request, apiuser, api=True):
580 raise JSONRPCError(
594 raise JSONRPCError(
581 'pull request `%s` update failed, no permission to update.' % (
595 'pull request `%s` update failed, no permission to update.' % (
582 pullrequestid,))
596 pullrequestid,))
583 if pull_request.is_closed():
597 if pull_request.is_closed():
584 raise JSONRPCError(
598 raise JSONRPCError(
585 'pull request `%s` update failed, pull request is closed' % (
599 'pull request `%s` update failed, pull request is closed' % (
586 pullrequestid,))
600 pullrequestid,))
587
601
588 reviewer_names = Optional.extract(reviewers) or []
602 reviewer_objects = Optional.extract(reviewers) or []
589 if not isinstance(reviewer_names, list):
603 if not isinstance(reviewer_objects, list):
590 raise JSONRPCError('reviewers should be specified as a list')
604 raise JSONRPCError('reviewers should be specified as a list')
591
605
592 reviewer_users = [get_user_or_error(n) for n in reviewer_names]
606 reviewers_reasons = []
593 reviewer_ids = [u.user_id for u in reviewer_users]
607 reviewer_ids = set()
608 for reviewer_object in reviewer_objects:
609 reviewer_reasons = []
610 if isinstance(reviewer_object, (int, basestring)):
611 reviewer_username = reviewer_object
612 else:
613 reviewer_username = reviewer_object['username']
614 reviewer_reasons = reviewer_object.get('reasons', [])
615
616 user = get_user_or_error(reviewer_username)
617 reviewer_ids.add(user.user_id)
618 reviewers_reasons.append((user.user_id, reviewer_reasons))
594
619
595 title = Optional.extract(title)
620 title = Optional.extract(title)
596 description = Optional.extract(description)
621 description = Optional.extract(description)
597 if title or description:
622 if title or description:
598 PullRequestModel().edit(
623 PullRequestModel().edit(
599 pull_request, title or pull_request.title,
624 pull_request, title or pull_request.title,
600 description or pull_request.description)
625 description or pull_request.description)
601 Session().commit()
626 Session().commit()
602
627
603 commit_changes = {"added": [], "common": [], "removed": []}
628 commit_changes = {"added": [], "common": [], "removed": []}
604 if str2bool(Optional.extract(update_commits)):
629 if str2bool(Optional.extract(update_commits)):
605 if PullRequestModel().has_valid_update_type(pull_request):
630 if PullRequestModel().has_valid_update_type(pull_request):
606 _version, _commit_changes = PullRequestModel().update_commits(
631 _version, _commit_changes = PullRequestModel().update_commits(
607 pull_request)
632 pull_request)
608 commit_changes = _commit_changes or commit_changes
633 commit_changes = _commit_changes or commit_changes
609 Session().commit()
634 Session().commit()
610
635
611 reviewers_changes = {"added": [], "removed": []}
636 reviewers_changes = {"added": [], "removed": []}
612 if reviewer_ids:
637 if reviewer_ids:
613 added_reviewers, removed_reviewers = \
638 added_reviewers, removed_reviewers = \
614 PullRequestModel().update_reviewers(pull_request, reviewer_ids)
639 PullRequestModel().update_reviewers(pull_request, reviewers_reasons)
615
640
616 reviewers_changes['added'] = sorted(
641 reviewers_changes['added'] = sorted(
617 [get_user_or_error(n).username for n in added_reviewers])
642 [get_user_or_error(n).username for n in added_reviewers])
618 reviewers_changes['removed'] = sorted(
643 reviewers_changes['removed'] = sorted(
619 [get_user_or_error(n).username for n in removed_reviewers])
644 [get_user_or_error(n).username for n in removed_reviewers])
620 Session().commit()
645 Session().commit()
621
646
622 if str2bool(Optional.extract(close_pull_request)):
647 if str2bool(Optional.extract(close_pull_request)):
623 PullRequestModel().close_pull_request_with_comment(
648 PullRequestModel().close_pull_request_with_comment(
624 pull_request, apiuser, repo)
649 pull_request, apiuser, repo)
625 Session().commit()
650 Session().commit()
626
651
627 data = {
652 data = {
628 'msg': 'Updated pull request `{}`'.format(
653 'msg': 'Updated pull request `{}`'.format(
629 pull_request.pull_request_id),
654 pull_request.pull_request_id),
630 'pull_request': pull_request.get_api_data(),
655 'pull_request': pull_request.get_api_data(),
631 'updated_commits': commit_changes,
656 'updated_commits': commit_changes,
632 'updated_reviewers': reviewers_changes
657 'updated_reviewers': reviewers_changes
633 }
658 }
634 return data
659 return data
635
660
@@ -1,885 +1,891 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24
24
25 import peppercorn
25 import formencode
26 import formencode
26 import logging
27 import logging
27
28
28 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
29 from pylons import request, tmpl_context as c, url
30 from pylons import request, tmpl_context as c, url
30 from pylons.controllers.util import redirect
31 from pylons.controllers.util import redirect
31 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
32 from pyramid.threadlocal import get_current_registry
33 from pyramid.threadlocal import get_current_registry
33 from sqlalchemy.sql import func
34 from sqlalchemy.sql import func
34 from sqlalchemy.sql.expression import or_
35 from sqlalchemy.sql.expression import or_
35
36
36 from rhodecode import events
37 from rhodecode import events
37 from rhodecode.lib import auth, diffs, helpers as h
38 from rhodecode.lib import auth, diffs, helpers as h
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.base import (
40 from rhodecode.lib.base import (
40 BaseRepoController, render, vcs_operation_context)
41 BaseRepoController, render, vcs_operation_context)
41 from rhodecode.lib.auth import (
42 from rhodecode.lib.auth import (
42 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
43 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
43 HasAcceptedRepoType, XHRRequired)
44 HasAcceptedRepoType, XHRRequired)
44 from rhodecode.lib.channelstream import channelstream_request
45 from rhodecode.lib.channelstream import channelstream_request
45 from rhodecode.lib.utils import jsonify
46 from rhodecode.lib.utils import jsonify
46 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
47 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
47 from rhodecode.lib.vcs.backends.base import EmptyCommit
48 from rhodecode.lib.vcs.backends.base import EmptyCommit
48 from rhodecode.lib.vcs.exceptions import (
49 from rhodecode.lib.vcs.exceptions import (
49 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError)
50 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError)
50 from rhodecode.lib.diffs import LimitedDiffContainer
51 from rhodecode.lib.diffs import LimitedDiffContainer
51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.comment import ChangesetCommentsModel
53 from rhodecode.model.comment import ChangesetCommentsModel
53 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
54 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
54 Repository
55 Repository
55 from rhodecode.model.forms import PullRequestForm
56 from rhodecode.model.forms import PullRequestForm
56 from rhodecode.model.meta import Session
57 from rhodecode.model.meta import Session
57 from rhodecode.model.pull_request import PullRequestModel
58 from rhodecode.model.pull_request import PullRequestModel
58
59
59 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
60
61
61
62
62 class PullrequestsController(BaseRepoController):
63 class PullrequestsController(BaseRepoController):
63 def __before__(self):
64 def __before__(self):
64 super(PullrequestsController, self).__before__()
65 super(PullrequestsController, self).__before__()
65
66
66 def _load_compare_data(self, pull_request, enable_comments=True):
67 def _load_compare_data(self, pull_request, enable_comments=True):
67 """
68 """
68 Load context data needed for generating compare diff
69 Load context data needed for generating compare diff
69
70
70 :param pull_request: object related to the request
71 :param pull_request: object related to the request
71 :param enable_comments: flag to determine if comments are included
72 :param enable_comments: flag to determine if comments are included
72 """
73 """
73 source_repo = pull_request.source_repo
74 source_repo = pull_request.source_repo
74 source_ref_id = pull_request.source_ref_parts.commit_id
75 source_ref_id = pull_request.source_ref_parts.commit_id
75
76
76 target_repo = pull_request.target_repo
77 target_repo = pull_request.target_repo
77 target_ref_id = pull_request.target_ref_parts.commit_id
78 target_ref_id = pull_request.target_ref_parts.commit_id
78
79
79 # despite opening commits for bookmarks/branches/tags, we always
80 # despite opening commits for bookmarks/branches/tags, we always
80 # convert this to rev to prevent changes after bookmark or branch change
81 # convert this to rev to prevent changes after bookmark or branch change
81 c.source_ref_type = 'rev'
82 c.source_ref_type = 'rev'
82 c.source_ref = source_ref_id
83 c.source_ref = source_ref_id
83
84
84 c.target_ref_type = 'rev'
85 c.target_ref_type = 'rev'
85 c.target_ref = target_ref_id
86 c.target_ref = target_ref_id
86
87
87 c.source_repo = source_repo
88 c.source_repo = source_repo
88 c.target_repo = target_repo
89 c.target_repo = target_repo
89
90
90 c.fulldiff = bool(request.GET.get('fulldiff'))
91 c.fulldiff = bool(request.GET.get('fulldiff'))
91
92
92 # diff_limit is the old behavior, will cut off the whole diff
93 # diff_limit is the old behavior, will cut off the whole diff
93 # if the limit is applied otherwise will just hide the
94 # if the limit is applied otherwise will just hide the
94 # big files from the front-end
95 # big files from the front-end
95 diff_limit = self.cut_off_limit_diff
96 diff_limit = self.cut_off_limit_diff
96 file_limit = self.cut_off_limit_file
97 file_limit = self.cut_off_limit_file
97
98
98 pre_load = ["author", "branch", "date", "message"]
99 pre_load = ["author", "branch", "date", "message"]
99
100
100 c.commit_ranges = []
101 c.commit_ranges = []
101 source_commit = EmptyCommit()
102 source_commit = EmptyCommit()
102 target_commit = EmptyCommit()
103 target_commit = EmptyCommit()
103 c.missing_requirements = False
104 c.missing_requirements = False
104 try:
105 try:
105 c.commit_ranges = [
106 c.commit_ranges = [
106 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
107 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
107 for rev in pull_request.revisions]
108 for rev in pull_request.revisions]
108
109
109 c.statuses = source_repo.statuses(
110 c.statuses = source_repo.statuses(
110 [x.raw_id for x in c.commit_ranges])
111 [x.raw_id for x in c.commit_ranges])
111
112
112 target_commit = source_repo.get_commit(
113 target_commit = source_repo.get_commit(
113 commit_id=safe_str(target_ref_id))
114 commit_id=safe_str(target_ref_id))
114 source_commit = source_repo.get_commit(
115 source_commit = source_repo.get_commit(
115 commit_id=safe_str(source_ref_id))
116 commit_id=safe_str(source_ref_id))
116 except RepositoryRequirementError:
117 except RepositoryRequirementError:
117 c.missing_requirements = True
118 c.missing_requirements = True
118
119
119 c.missing_commits = False
120 c.missing_commits = False
120 if (c.missing_requirements or
121 if (c.missing_requirements or
121 isinstance(source_commit, EmptyCommit) or
122 isinstance(source_commit, EmptyCommit) or
122 source_commit == target_commit):
123 source_commit == target_commit):
123 _parsed = []
124 _parsed = []
124 c.missing_commits = True
125 c.missing_commits = True
125 else:
126 else:
126 vcs_diff = PullRequestModel().get_diff(pull_request)
127 vcs_diff = PullRequestModel().get_diff(pull_request)
127 diff_processor = diffs.DiffProcessor(
128 diff_processor = diffs.DiffProcessor(
128 vcs_diff, format='gitdiff', diff_limit=diff_limit,
129 vcs_diff, format='gitdiff', diff_limit=diff_limit,
129 file_limit=file_limit, show_full_diff=c.fulldiff)
130 file_limit=file_limit, show_full_diff=c.fulldiff)
130 _parsed = diff_processor.prepare()
131 _parsed = diff_processor.prepare()
131
132
132 c.limited_diff = isinstance(_parsed, LimitedDiffContainer)
133 c.limited_diff = isinstance(_parsed, LimitedDiffContainer)
133
134
134 c.files = []
135 c.files = []
135 c.changes = {}
136 c.changes = {}
136 c.lines_added = 0
137 c.lines_added = 0
137 c.lines_deleted = 0
138 c.lines_deleted = 0
138 c.included_files = []
139 c.included_files = []
139 c.deleted_files = []
140 c.deleted_files = []
140
141
141 for f in _parsed:
142 for f in _parsed:
142 st = f['stats']
143 st = f['stats']
143 c.lines_added += st['added']
144 c.lines_added += st['added']
144 c.lines_deleted += st['deleted']
145 c.lines_deleted += st['deleted']
145
146
146 fid = h.FID('', f['filename'])
147 fid = h.FID('', f['filename'])
147 c.files.append([fid, f['operation'], f['filename'], f['stats']])
148 c.files.append([fid, f['operation'], f['filename'], f['stats']])
148 c.included_files.append(f['filename'])
149 c.included_files.append(f['filename'])
149 html_diff = diff_processor.as_html(enable_comments=enable_comments,
150 html_diff = diff_processor.as_html(enable_comments=enable_comments,
150 parsed_lines=[f])
151 parsed_lines=[f])
151 c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
152 c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
152
153
153 def _extract_ordering(self, request):
154 def _extract_ordering(self, request):
154 column_index = safe_int(request.GET.get('order[0][column]'))
155 column_index = safe_int(request.GET.get('order[0][column]'))
155 order_dir = request.GET.get('order[0][dir]', 'desc')
156 order_dir = request.GET.get('order[0][dir]', 'desc')
156 order_by = request.GET.get(
157 order_by = request.GET.get(
157 'columns[%s][data][sort]' % column_index, 'name_raw')
158 'columns[%s][data][sort]' % column_index, 'name_raw')
158 return order_by, order_dir
159 return order_by, order_dir
159
160
160 @LoginRequired()
161 @LoginRequired()
161 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
162 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
162 'repository.admin')
163 'repository.admin')
163 @HasAcceptedRepoType('git', 'hg')
164 @HasAcceptedRepoType('git', 'hg')
164 def show_all(self, repo_name):
165 def show_all(self, repo_name):
165 # filter types
166 # filter types
166 c.active = 'open'
167 c.active = 'open'
167 c.source = str2bool(request.GET.get('source'))
168 c.source = str2bool(request.GET.get('source'))
168 c.closed = str2bool(request.GET.get('closed'))
169 c.closed = str2bool(request.GET.get('closed'))
169 c.my = str2bool(request.GET.get('my'))
170 c.my = str2bool(request.GET.get('my'))
170 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
171 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
171 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
172 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
172 c.repo_name = repo_name
173 c.repo_name = repo_name
173
174
174 opened_by = None
175 opened_by = None
175 if c.my:
176 if c.my:
176 c.active = 'my'
177 c.active = 'my'
177 opened_by = [c.rhodecode_user.user_id]
178 opened_by = [c.rhodecode_user.user_id]
178
179
179 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
180 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
180 if c.closed:
181 if c.closed:
181 c.active = 'closed'
182 c.active = 'closed'
182 statuses = [PullRequest.STATUS_CLOSED]
183 statuses = [PullRequest.STATUS_CLOSED]
183
184
184 if c.awaiting_review and not c.source:
185 if c.awaiting_review and not c.source:
185 c.active = 'awaiting'
186 c.active = 'awaiting'
186 if c.source and not c.awaiting_review:
187 if c.source and not c.awaiting_review:
187 c.active = 'source'
188 c.active = 'source'
188 if c.awaiting_my_review:
189 if c.awaiting_my_review:
189 c.active = 'awaiting_my'
190 c.active = 'awaiting_my'
190
191
191 data = self._get_pull_requests_list(
192 data = self._get_pull_requests_list(
192 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
193 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
193 if not request.is_xhr:
194 if not request.is_xhr:
194 c.data = json.dumps(data['data'])
195 c.data = json.dumps(data['data'])
195 c.records_total = data['recordsTotal']
196 c.records_total = data['recordsTotal']
196 return render('/pullrequests/pullrequests.html')
197 return render('/pullrequests/pullrequests.html')
197 else:
198 else:
198 return json.dumps(data)
199 return json.dumps(data)
199
200
200 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
201 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
201 # pagination
202 # pagination
202 start = safe_int(request.GET.get('start'), 0)
203 start = safe_int(request.GET.get('start'), 0)
203 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
204 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
204 order_by, order_dir = self._extract_ordering(request)
205 order_by, order_dir = self._extract_ordering(request)
205
206
206 if c.awaiting_review:
207 if c.awaiting_review:
207 pull_requests = PullRequestModel().get_awaiting_review(
208 pull_requests = PullRequestModel().get_awaiting_review(
208 repo_name, source=c.source, opened_by=opened_by,
209 repo_name, source=c.source, opened_by=opened_by,
209 statuses=statuses, offset=start, length=length,
210 statuses=statuses, offset=start, length=length,
210 order_by=order_by, order_dir=order_dir)
211 order_by=order_by, order_dir=order_dir)
211 pull_requests_total_count = PullRequestModel(
212 pull_requests_total_count = PullRequestModel(
212 ).count_awaiting_review(
213 ).count_awaiting_review(
213 repo_name, source=c.source, statuses=statuses,
214 repo_name, source=c.source, statuses=statuses,
214 opened_by=opened_by)
215 opened_by=opened_by)
215 elif c.awaiting_my_review:
216 elif c.awaiting_my_review:
216 pull_requests = PullRequestModel().get_awaiting_my_review(
217 pull_requests = PullRequestModel().get_awaiting_my_review(
217 repo_name, source=c.source, opened_by=opened_by,
218 repo_name, source=c.source, opened_by=opened_by,
218 user_id=c.rhodecode_user.user_id, statuses=statuses,
219 user_id=c.rhodecode_user.user_id, statuses=statuses,
219 offset=start, length=length, order_by=order_by,
220 offset=start, length=length, order_by=order_by,
220 order_dir=order_dir)
221 order_dir=order_dir)
221 pull_requests_total_count = PullRequestModel(
222 pull_requests_total_count = PullRequestModel(
222 ).count_awaiting_my_review(
223 ).count_awaiting_my_review(
223 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
224 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
224 statuses=statuses, opened_by=opened_by)
225 statuses=statuses, opened_by=opened_by)
225 else:
226 else:
226 pull_requests = PullRequestModel().get_all(
227 pull_requests = PullRequestModel().get_all(
227 repo_name, source=c.source, opened_by=opened_by,
228 repo_name, source=c.source, opened_by=opened_by,
228 statuses=statuses, offset=start, length=length,
229 statuses=statuses, offset=start, length=length,
229 order_by=order_by, order_dir=order_dir)
230 order_by=order_by, order_dir=order_dir)
230 pull_requests_total_count = PullRequestModel().count_all(
231 pull_requests_total_count = PullRequestModel().count_all(
231 repo_name, source=c.source, statuses=statuses,
232 repo_name, source=c.source, statuses=statuses,
232 opened_by=opened_by)
233 opened_by=opened_by)
233
234
234 from rhodecode.lib.utils import PartialRenderer
235 from rhodecode.lib.utils import PartialRenderer
235 _render = PartialRenderer('data_table/_dt_elements.html')
236 _render = PartialRenderer('data_table/_dt_elements.html')
236 data = []
237 data = []
237 for pr in pull_requests:
238 for pr in pull_requests:
238 comments = ChangesetCommentsModel().get_all_comments(
239 comments = ChangesetCommentsModel().get_all_comments(
239 c.rhodecode_db_repo.repo_id, pull_request=pr)
240 c.rhodecode_db_repo.repo_id, pull_request=pr)
240
241
241 data.append({
242 data.append({
242 'name': _render('pullrequest_name',
243 'name': _render('pullrequest_name',
243 pr.pull_request_id, pr.target_repo.repo_name),
244 pr.pull_request_id, pr.target_repo.repo_name),
244 'name_raw': pr.pull_request_id,
245 'name_raw': pr.pull_request_id,
245 'status': _render('pullrequest_status',
246 'status': _render('pullrequest_status',
246 pr.calculated_review_status()),
247 pr.calculated_review_status()),
247 'title': _render(
248 'title': _render(
248 'pullrequest_title', pr.title, pr.description),
249 'pullrequest_title', pr.title, pr.description),
249 'description': h.escape(pr.description),
250 'description': h.escape(pr.description),
250 'updated_on': _render('pullrequest_updated_on',
251 'updated_on': _render('pullrequest_updated_on',
251 h.datetime_to_time(pr.updated_on)),
252 h.datetime_to_time(pr.updated_on)),
252 'updated_on_raw': h.datetime_to_time(pr.updated_on),
253 'updated_on_raw': h.datetime_to_time(pr.updated_on),
253 'created_on': _render('pullrequest_updated_on',
254 'created_on': _render('pullrequest_updated_on',
254 h.datetime_to_time(pr.created_on)),
255 h.datetime_to_time(pr.created_on)),
255 'created_on_raw': h.datetime_to_time(pr.created_on),
256 'created_on_raw': h.datetime_to_time(pr.created_on),
256 'author': _render('pullrequest_author',
257 'author': _render('pullrequest_author',
257 pr.author.full_contact, ),
258 pr.author.full_contact, ),
258 'author_raw': pr.author.full_name,
259 'author_raw': pr.author.full_name,
259 'comments': _render('pullrequest_comments', len(comments)),
260 'comments': _render('pullrequest_comments', len(comments)),
260 'comments_raw': len(comments),
261 'comments_raw': len(comments),
261 'closed': pr.is_closed(),
262 'closed': pr.is_closed(),
262 })
263 })
263 # json used to render the grid
264 # json used to render the grid
264 data = ({
265 data = ({
265 'data': data,
266 'data': data,
266 'recordsTotal': pull_requests_total_count,
267 'recordsTotal': pull_requests_total_count,
267 'recordsFiltered': pull_requests_total_count,
268 'recordsFiltered': pull_requests_total_count,
268 })
269 })
269 return data
270 return data
270
271
271 @LoginRequired()
272 @LoginRequired()
272 @NotAnonymous()
273 @NotAnonymous()
273 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
274 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
274 'repository.admin')
275 'repository.admin')
275 @HasAcceptedRepoType('git', 'hg')
276 @HasAcceptedRepoType('git', 'hg')
276 def index(self):
277 def index(self):
277 source_repo = c.rhodecode_db_repo
278 source_repo = c.rhodecode_db_repo
278
279
279 try:
280 try:
280 source_repo.scm_instance().get_commit()
281 source_repo.scm_instance().get_commit()
281 except EmptyRepositoryError:
282 except EmptyRepositoryError:
282 h.flash(h.literal(_('There are no commits yet')),
283 h.flash(h.literal(_('There are no commits yet')),
283 category='warning')
284 category='warning')
284 redirect(url('summary_home', repo_name=source_repo.repo_name))
285 redirect(url('summary_home', repo_name=source_repo.repo_name))
285
286
286 commit_id = request.GET.get('commit')
287 commit_id = request.GET.get('commit')
287 branch_ref = request.GET.get('branch')
288 branch_ref = request.GET.get('branch')
288 bookmark_ref = request.GET.get('bookmark')
289 bookmark_ref = request.GET.get('bookmark')
289
290
290 try:
291 try:
291 source_repo_data = PullRequestModel().generate_repo_data(
292 source_repo_data = PullRequestModel().generate_repo_data(
292 source_repo, commit_id=commit_id,
293 source_repo, commit_id=commit_id,
293 branch=branch_ref, bookmark=bookmark_ref)
294 branch=branch_ref, bookmark=bookmark_ref)
294 except CommitDoesNotExistError as e:
295 except CommitDoesNotExistError as e:
295 log.exception(e)
296 log.exception(e)
296 h.flash(_('Commit does not exist'), 'error')
297 h.flash(_('Commit does not exist'), 'error')
297 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
298 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
298
299
299 default_target_repo = source_repo
300 default_target_repo = source_repo
300 if (source_repo.parent and
301 if (source_repo.parent and
301 not source_repo.parent.scm_instance().is_empty()):
302 not source_repo.parent.scm_instance().is_empty()):
302 # change default if we have a parent repo
303 # change default if we have a parent repo
303 default_target_repo = source_repo.parent
304 default_target_repo = source_repo.parent
304
305
305 target_repo_data = PullRequestModel().generate_repo_data(
306 target_repo_data = PullRequestModel().generate_repo_data(
306 default_target_repo)
307 default_target_repo)
307
308
308 selected_source_ref = source_repo_data['refs']['selected_ref']
309 selected_source_ref = source_repo_data['refs']['selected_ref']
309
310
310 title_source_ref = selected_source_ref.split(':', 2)[1]
311 title_source_ref = selected_source_ref.split(':', 2)[1]
311 c.default_title = PullRequestModel().generate_pullrequest_title(
312 c.default_title = PullRequestModel().generate_pullrequest_title(
312 source=source_repo.repo_name,
313 source=source_repo.repo_name,
313 source_ref=title_source_ref,
314 source_ref=title_source_ref,
314 target=default_target_repo.repo_name
315 target=default_target_repo.repo_name
315 )
316 )
316
317
317 c.default_repo_data = {
318 c.default_repo_data = {
318 'source_repo_name': source_repo.repo_name,
319 'source_repo_name': source_repo.repo_name,
319 'source_refs_json': json.dumps(source_repo_data),
320 'source_refs_json': json.dumps(source_repo_data),
320 'target_repo_name': default_target_repo.repo_name,
321 'target_repo_name': default_target_repo.repo_name,
321 'target_refs_json': json.dumps(target_repo_data),
322 'target_refs_json': json.dumps(target_repo_data),
322 }
323 }
323 c.default_source_ref = selected_source_ref
324 c.default_source_ref = selected_source_ref
324
325
325 return render('/pullrequests/pullrequest.html')
326 return render('/pullrequests/pullrequest.html')
326
327
327 @LoginRequired()
328 @LoginRequired()
328 @NotAnonymous()
329 @NotAnonymous()
329 @XHRRequired()
330 @XHRRequired()
330 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
331 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
331 'repository.admin')
332 'repository.admin')
332 @jsonify
333 @jsonify
333 def get_repo_refs(self, repo_name, target_repo_name):
334 def get_repo_refs(self, repo_name, target_repo_name):
334 repo = Repository.get_by_repo_name(target_repo_name)
335 repo = Repository.get_by_repo_name(target_repo_name)
335 if not repo:
336 if not repo:
336 raise HTTPNotFound
337 raise HTTPNotFound
337 return PullRequestModel().generate_repo_data(repo)
338 return PullRequestModel().generate_repo_data(repo)
338
339
339 @LoginRequired()
340 @LoginRequired()
340 @NotAnonymous()
341 @NotAnonymous()
341 @XHRRequired()
342 @XHRRequired()
342 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
343 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
343 'repository.admin')
344 'repository.admin')
344 @jsonify
345 @jsonify
345 def get_repo_destinations(self, repo_name):
346 def get_repo_destinations(self, repo_name):
346 repo = Repository.get_by_repo_name(repo_name)
347 repo = Repository.get_by_repo_name(repo_name)
347 if not repo:
348 if not repo:
348 raise HTTPNotFound
349 raise HTTPNotFound
349 filter_query = request.GET.get('query')
350 filter_query = request.GET.get('query')
350
351
351 query = Repository.query() \
352 query = Repository.query() \
352 .order_by(func.length(Repository.repo_name)) \
353 .order_by(func.length(Repository.repo_name)) \
353 .filter(or_(
354 .filter(or_(
354 Repository.repo_name == repo.repo_name,
355 Repository.repo_name == repo.repo_name,
355 Repository.fork_id == repo.repo_id))
356 Repository.fork_id == repo.repo_id))
356
357
357 if filter_query:
358 if filter_query:
358 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
359 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
359 query = query.filter(
360 query = query.filter(
360 Repository.repo_name.ilike(ilike_expression))
361 Repository.repo_name.ilike(ilike_expression))
361
362
362 add_parent = False
363 add_parent = False
363 if repo.parent:
364 if repo.parent:
364 if filter_query in repo.parent.repo_name:
365 if filter_query in repo.parent.repo_name:
365 if not repo.parent.scm_instance().is_empty():
366 if not repo.parent.scm_instance().is_empty():
366 add_parent = True
367 add_parent = True
367
368
368 limit = 20 - 1 if add_parent else 20
369 limit = 20 - 1 if add_parent else 20
369 all_repos = query.limit(limit).all()
370 all_repos = query.limit(limit).all()
370 if add_parent:
371 if add_parent:
371 all_repos += [repo.parent]
372 all_repos += [repo.parent]
372
373
373 repos = []
374 repos = []
374 for obj in self.scm_model.get_repos(all_repos):
375 for obj in self.scm_model.get_repos(all_repos):
375 repos.append({
376 repos.append({
376 'id': obj['name'],
377 'id': obj['name'],
377 'text': obj['name'],
378 'text': obj['name'],
378 'type': 'repo',
379 'type': 'repo',
379 'obj': obj['dbrepo']
380 'obj': obj['dbrepo']
380 })
381 })
381
382
382 data = {
383 data = {
383 'more': False,
384 'more': False,
384 'results': [{
385 'results': [{
385 'text': _('Repositories'),
386 'text': _('Repositories'),
386 'children': repos
387 'children': repos
387 }] if repos else []
388 }] if repos else []
388 }
389 }
389 return data
390 return data
390
391
391 @LoginRequired()
392 @LoginRequired()
392 @NotAnonymous()
393 @NotAnonymous()
393 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
394 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
394 'repository.admin')
395 'repository.admin')
395 @HasAcceptedRepoType('git', 'hg')
396 @HasAcceptedRepoType('git', 'hg')
396 @auth.CSRFRequired()
397 @auth.CSRFRequired()
397 def create(self, repo_name):
398 def create(self, repo_name):
398 repo = Repository.get_by_repo_name(repo_name)
399 repo = Repository.get_by_repo_name(repo_name)
399 if not repo:
400 if not repo:
400 raise HTTPNotFound
401 raise HTTPNotFound
401
402
403 controls = peppercorn.parse(request.POST.items())
404
402 try:
405 try:
403 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
406 _form = PullRequestForm(repo.repo_id)().to_python(controls)
404 except formencode.Invalid as errors:
407 except formencode.Invalid as errors:
405 if errors.error_dict.get('revisions'):
408 if errors.error_dict.get('revisions'):
406 msg = 'Revisions: %s' % errors.error_dict['revisions']
409 msg = 'Revisions: %s' % errors.error_dict['revisions']
407 elif errors.error_dict.get('pullrequest_title'):
410 elif errors.error_dict.get('pullrequest_title'):
408 msg = _('Pull request requires a title with min. 3 chars')
411 msg = _('Pull request requires a title with min. 3 chars')
409 else:
412 else:
410 msg = _('Error creating pull request: {}').format(errors)
413 msg = _('Error creating pull request: {}').format(errors)
411 log.exception(msg)
414 log.exception(msg)
412 h.flash(msg, 'error')
415 h.flash(msg, 'error')
413
416
414 # would rather just go back to form ...
417 # would rather just go back to form ...
415 return redirect(url('pullrequest_home', repo_name=repo_name))
418 return redirect(url('pullrequest_home', repo_name=repo_name))
416
419
417 source_repo = _form['source_repo']
420 source_repo = _form['source_repo']
418 source_ref = _form['source_ref']
421 source_ref = _form['source_ref']
419 target_repo = _form['target_repo']
422 target_repo = _form['target_repo']
420 target_ref = _form['target_ref']
423 target_ref = _form['target_ref']
421 commit_ids = _form['revisions'][::-1]
424 commit_ids = _form['revisions'][::-1]
422 reviewers = _form['review_members']
425 reviewers = [
426 (r['user_id'], r['reasons']) for r in _form['review_members']]
423
427
424 # find the ancestor for this pr
428 # find the ancestor for this pr
425 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
429 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
426 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
430 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
427
431
428 source_scm = source_db_repo.scm_instance()
432 source_scm = source_db_repo.scm_instance()
429 target_scm = target_db_repo.scm_instance()
433 target_scm = target_db_repo.scm_instance()
430
434
431 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
435 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
432 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
436 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
433
437
434 ancestor = source_scm.get_common_ancestor(
438 ancestor = source_scm.get_common_ancestor(
435 source_commit.raw_id, target_commit.raw_id, target_scm)
439 source_commit.raw_id, target_commit.raw_id, target_scm)
436
440
437 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
441 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
438 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
442 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
439
443
440 pullrequest_title = _form['pullrequest_title']
444 pullrequest_title = _form['pullrequest_title']
441 title_source_ref = source_ref.split(':', 2)[1]
445 title_source_ref = source_ref.split(':', 2)[1]
442 if not pullrequest_title:
446 if not pullrequest_title:
443 pullrequest_title = PullRequestModel().generate_pullrequest_title(
447 pullrequest_title = PullRequestModel().generate_pullrequest_title(
444 source=source_repo,
448 source=source_repo,
445 source_ref=title_source_ref,
449 source_ref=title_source_ref,
446 target=target_repo
450 target=target_repo
447 )
451 )
448
452
449 description = _form['pullrequest_desc']
453 description = _form['pullrequest_desc']
450 try:
454 try:
451 pull_request = PullRequestModel().create(
455 pull_request = PullRequestModel().create(
452 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
456 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
453 target_ref, commit_ids, reviewers, pullrequest_title,
457 target_ref, commit_ids, reviewers, pullrequest_title,
454 description
458 description
455 )
459 )
456 Session().commit()
460 Session().commit()
457 h.flash(_('Successfully opened new pull request'),
461 h.flash(_('Successfully opened new pull request'),
458 category='success')
462 category='success')
459 except Exception as e:
463 except Exception as e:
460 msg = _('Error occurred during sending pull request')
464 msg = _('Error occurred during sending pull request')
461 log.exception(msg)
465 log.exception(msg)
462 h.flash(msg, category='error')
466 h.flash(msg, category='error')
463 return redirect(url('pullrequest_home', repo_name=repo_name))
467 return redirect(url('pullrequest_home', repo_name=repo_name))
464
468
465 return redirect(url('pullrequest_show', repo_name=target_repo,
469 return redirect(url('pullrequest_show', repo_name=target_repo,
466 pull_request_id=pull_request.pull_request_id))
470 pull_request_id=pull_request.pull_request_id))
467
471
468 @LoginRequired()
472 @LoginRequired()
469 @NotAnonymous()
473 @NotAnonymous()
470 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
474 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
471 'repository.admin')
475 'repository.admin')
472 @auth.CSRFRequired()
476 @auth.CSRFRequired()
473 @jsonify
477 @jsonify
474 def update(self, repo_name, pull_request_id):
478 def update(self, repo_name, pull_request_id):
475 pull_request_id = safe_int(pull_request_id)
479 pull_request_id = safe_int(pull_request_id)
476 pull_request = PullRequest.get_or_404(pull_request_id)
480 pull_request = PullRequest.get_or_404(pull_request_id)
477 # only owner or admin can update it
481 # only owner or admin can update it
478 allowed_to_update = PullRequestModel().check_user_update(
482 allowed_to_update = PullRequestModel().check_user_update(
479 pull_request, c.rhodecode_user)
483 pull_request, c.rhodecode_user)
480 if allowed_to_update:
484 if allowed_to_update:
481 if 'reviewers_ids' in request.POST:
485 controls = peppercorn.parse(request.POST.items())
482 self._update_reviewers(pull_request_id)
486
487 if 'review_members' in controls:
488 self._update_reviewers(
489 pull_request_id, controls['review_members'])
483 elif str2bool(request.POST.get('update_commits', 'false')):
490 elif str2bool(request.POST.get('update_commits', 'false')):
484 self._update_commits(pull_request)
491 self._update_commits(pull_request)
485 elif str2bool(request.POST.get('close_pull_request', 'false')):
492 elif str2bool(request.POST.get('close_pull_request', 'false')):
486 self._reject_close(pull_request)
493 self._reject_close(pull_request)
487 elif str2bool(request.POST.get('edit_pull_request', 'false')):
494 elif str2bool(request.POST.get('edit_pull_request', 'false')):
488 self._edit_pull_request(pull_request)
495 self._edit_pull_request(pull_request)
489 else:
496 else:
490 raise HTTPBadRequest()
497 raise HTTPBadRequest()
491 return True
498 return True
492 raise HTTPForbidden()
499 raise HTTPForbidden()
493
500
494 def _edit_pull_request(self, pull_request):
501 def _edit_pull_request(self, pull_request):
495 try:
502 try:
496 PullRequestModel().edit(
503 PullRequestModel().edit(
497 pull_request, request.POST.get('title'),
504 pull_request, request.POST.get('title'),
498 request.POST.get('description'))
505 request.POST.get('description'))
499 except ValueError:
506 except ValueError:
500 msg = _(u'Cannot update closed pull requests.')
507 msg = _(u'Cannot update closed pull requests.')
501 h.flash(msg, category='error')
508 h.flash(msg, category='error')
502 return
509 return
503 else:
510 else:
504 Session().commit()
511 Session().commit()
505
512
506 msg = _(u'Pull request title & description updated.')
513 msg = _(u'Pull request title & description updated.')
507 h.flash(msg, category='success')
514 h.flash(msg, category='success')
508 return
515 return
509
516
510 def _update_commits(self, pull_request):
517 def _update_commits(self, pull_request):
511 try:
518 try:
512 if PullRequestModel().has_valid_update_type(pull_request):
519 if PullRequestModel().has_valid_update_type(pull_request):
513 updated_version, changes = PullRequestModel().update_commits(
520 updated_version, changes = PullRequestModel().update_commits(
514 pull_request)
521 pull_request)
515 if updated_version:
522 if updated_version:
516 msg = _(
523 msg = _(
517 u'Pull request updated to "{source_commit_id}" with '
524 u'Pull request updated to "{source_commit_id}" with '
518 u'{count_added} added, {count_removed} removed '
525 u'{count_added} added, {count_removed} removed '
519 u'commits.'
526 u'commits.'
520 ).format(
527 ).format(
521 source_commit_id=pull_request.source_ref_parts.commit_id,
528 source_commit_id=pull_request.source_ref_parts.commit_id,
522 count_added=len(changes.added),
529 count_added=len(changes.added),
523 count_removed=len(changes.removed))
530 count_removed=len(changes.removed))
524 h.flash(msg, category='success')
531 h.flash(msg, category='success')
525 registry = get_current_registry()
532 registry = get_current_registry()
526 rhodecode_plugins = getattr(registry,
533 rhodecode_plugins = getattr(registry,
527 'rhodecode_plugins', {})
534 'rhodecode_plugins', {})
528 channelstream_config = rhodecode_plugins.get(
535 channelstream_config = rhodecode_plugins.get(
529 'channelstream', {})
536 'channelstream', {})
530 if channelstream_config.get('enabled'):
537 if channelstream_config.get('enabled'):
531 message = msg + ' - <a onclick="' \
538 message = msg + ' - <a onclick="' \
532 'window.location.reload()">' \
539 'window.location.reload()">' \
533 '<strong>{}</strong></a>'.format(
540 '<strong>{}</strong></a>'.format(
534 _('Reload page')
541 _('Reload page')
535 )
542 )
536 channel = '/repo${}$/pr/{}'.format(
543 channel = '/repo${}$/pr/{}'.format(
537 pull_request.target_repo.repo_name,
544 pull_request.target_repo.repo_name,
538 pull_request.pull_request_id
545 pull_request.pull_request_id
539 )
546 )
540 payload = {
547 payload = {
541 'type': 'message',
548 'type': 'message',
542 'user': 'system',
549 'user': 'system',
543 'exclude_users': [request.user.username],
550 'exclude_users': [request.user.username],
544 'channel': channel,
551 'channel': channel,
545 'message': {
552 'message': {
546 'message': message,
553 'message': message,
547 'level': 'success',
554 'level': 'success',
548 'topic': '/notifications'
555 'topic': '/notifications'
549 }
556 }
550 }
557 }
551 channelstream_request(channelstream_config, [payload],
558 channelstream_request(channelstream_config, [payload],
552 '/message', raise_exc=False)
559 '/message', raise_exc=False)
553 else:
560 else:
554 h.flash(_("Nothing changed in pull request."),
561 h.flash(_("Nothing changed in pull request."),
555 category='warning')
562 category='warning')
556 else:
563 else:
557 msg = _(
564 msg = _(
558 u"Skipping update of pull request due to reference "
565 u"Skipping update of pull request due to reference "
559 u"type: {reference_type}"
566 u"type: {reference_type}"
560 ).format(reference_type=pull_request.source_ref_parts.type)
567 ).format(reference_type=pull_request.source_ref_parts.type)
561 h.flash(msg, category='warning')
568 h.flash(msg, category='warning')
562 except CommitDoesNotExistError:
569 except CommitDoesNotExistError:
563 h.flash(
570 h.flash(
564 _(u'Update failed due to missing commits.'), category='error')
571 _(u'Update failed due to missing commits.'), category='error')
565
572
566 @auth.CSRFRequired()
573 @auth.CSRFRequired()
567 @LoginRequired()
574 @LoginRequired()
568 @NotAnonymous()
575 @NotAnonymous()
569 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
576 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
570 'repository.admin')
577 'repository.admin')
571 def merge(self, repo_name, pull_request_id):
578 def merge(self, repo_name, pull_request_id):
572 """
579 """
573 POST /{repo_name}/pull-request/{pull_request_id}
580 POST /{repo_name}/pull-request/{pull_request_id}
574
581
575 Merge will perform a server-side merge of the specified
582 Merge will perform a server-side merge of the specified
576 pull request, if the pull request is approved and mergeable.
583 pull request, if the pull request is approved and mergeable.
577 After succesfull merging, the pull request is automatically
584 After succesfull merging, the pull request is automatically
578 closed, with a relevant comment.
585 closed, with a relevant comment.
579 """
586 """
580 pull_request_id = safe_int(pull_request_id)
587 pull_request_id = safe_int(pull_request_id)
581 pull_request = PullRequest.get_or_404(pull_request_id)
588 pull_request = PullRequest.get_or_404(pull_request_id)
582 user = c.rhodecode_user
589 user = c.rhodecode_user
583
590
584 if self._meets_merge_pre_conditions(pull_request, user):
591 if self._meets_merge_pre_conditions(pull_request, user):
585 log.debug("Pre-conditions checked, trying to merge.")
592 log.debug("Pre-conditions checked, trying to merge.")
586 extras = vcs_operation_context(
593 extras = vcs_operation_context(
587 request.environ, repo_name=pull_request.target_repo.repo_name,
594 request.environ, repo_name=pull_request.target_repo.repo_name,
588 username=user.username, action='push',
595 username=user.username, action='push',
589 scm=pull_request.target_repo.repo_type)
596 scm=pull_request.target_repo.repo_type)
590 self._merge_pull_request(pull_request, user, extras)
597 self._merge_pull_request(pull_request, user, extras)
591
598
592 return redirect(url(
599 return redirect(url(
593 'pullrequest_show',
600 'pullrequest_show',
594 repo_name=pull_request.target_repo.repo_name,
601 repo_name=pull_request.target_repo.repo_name,
595 pull_request_id=pull_request.pull_request_id))
602 pull_request_id=pull_request.pull_request_id))
596
603
597 def _meets_merge_pre_conditions(self, pull_request, user):
604 def _meets_merge_pre_conditions(self, pull_request, user):
598 if not PullRequestModel().check_user_merge(pull_request, user):
605 if not PullRequestModel().check_user_merge(pull_request, user):
599 raise HTTPForbidden()
606 raise HTTPForbidden()
600
607
601 merge_status, msg = PullRequestModel().merge_status(pull_request)
608 merge_status, msg = PullRequestModel().merge_status(pull_request)
602 if not merge_status:
609 if not merge_status:
603 log.debug("Cannot merge, not mergeable.")
610 log.debug("Cannot merge, not mergeable.")
604 h.flash(msg, category='error')
611 h.flash(msg, category='error')
605 return False
612 return False
606
613
607 if (pull_request.calculated_review_status()
614 if (pull_request.calculated_review_status()
608 is not ChangesetStatus.STATUS_APPROVED):
615 is not ChangesetStatus.STATUS_APPROVED):
609 log.debug("Cannot merge, approval is pending.")
616 log.debug("Cannot merge, approval is pending.")
610 msg = _('Pull request reviewer approval is pending.')
617 msg = _('Pull request reviewer approval is pending.')
611 h.flash(msg, category='error')
618 h.flash(msg, category='error')
612 return False
619 return False
613 return True
620 return True
614
621
615 def _merge_pull_request(self, pull_request, user, extras):
622 def _merge_pull_request(self, pull_request, user, extras):
616 merge_resp = PullRequestModel().merge(
623 merge_resp = PullRequestModel().merge(
617 pull_request, user, extras=extras)
624 pull_request, user, extras=extras)
618
625
619 if merge_resp.executed:
626 if merge_resp.executed:
620 log.debug("The merge was successful, closing the pull request.")
627 log.debug("The merge was successful, closing the pull request.")
621 PullRequestModel().close_pull_request(
628 PullRequestModel().close_pull_request(
622 pull_request.pull_request_id, user)
629 pull_request.pull_request_id, user)
623 Session().commit()
630 Session().commit()
624 msg = _('Pull request was successfully merged and closed.')
631 msg = _('Pull request was successfully merged and closed.')
625 h.flash(msg, category='success')
632 h.flash(msg, category='success')
626 else:
633 else:
627 log.debug(
634 log.debug(
628 "The merge was not successful. Merge response: %s",
635 "The merge was not successful. Merge response: %s",
629 merge_resp)
636 merge_resp)
630 msg = PullRequestModel().merge_status_message(
637 msg = PullRequestModel().merge_status_message(
631 merge_resp.failure_reason)
638 merge_resp.failure_reason)
632 h.flash(msg, category='error')
639 h.flash(msg, category='error')
633
640
634 def _update_reviewers(self, pull_request_id):
641 def _update_reviewers(self, pull_request_id, review_members):
635 reviewers_ids = map(int, filter(
642 reviewers = [
636 lambda v: v not in [None, ''],
643 (int(r['user_id']), r['reasons']) for r in review_members]
637 request.POST.get('reviewers_ids', '').split(',')))
644 PullRequestModel().update_reviewers(pull_request_id, reviewers)
638 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
639 Session().commit()
645 Session().commit()
640
646
641 def _reject_close(self, pull_request):
647 def _reject_close(self, pull_request):
642 if pull_request.is_closed():
648 if pull_request.is_closed():
643 raise HTTPForbidden()
649 raise HTTPForbidden()
644
650
645 PullRequestModel().close_pull_request_with_comment(
651 PullRequestModel().close_pull_request_with_comment(
646 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
652 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
647 Session().commit()
653 Session().commit()
648
654
649 @LoginRequired()
655 @LoginRequired()
650 @NotAnonymous()
656 @NotAnonymous()
651 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
657 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
652 'repository.admin')
658 'repository.admin')
653 @auth.CSRFRequired()
659 @auth.CSRFRequired()
654 @jsonify
660 @jsonify
655 def delete(self, repo_name, pull_request_id):
661 def delete(self, repo_name, pull_request_id):
656 pull_request_id = safe_int(pull_request_id)
662 pull_request_id = safe_int(pull_request_id)
657 pull_request = PullRequest.get_or_404(pull_request_id)
663 pull_request = PullRequest.get_or_404(pull_request_id)
658 # only owner can delete it !
664 # only owner can delete it !
659 if pull_request.author.user_id == c.rhodecode_user.user_id:
665 if pull_request.author.user_id == c.rhodecode_user.user_id:
660 PullRequestModel().delete(pull_request)
666 PullRequestModel().delete(pull_request)
661 Session().commit()
667 Session().commit()
662 h.flash(_('Successfully deleted pull request'),
668 h.flash(_('Successfully deleted pull request'),
663 category='success')
669 category='success')
664 return redirect(url('my_account_pullrequests'))
670 return redirect(url('my_account_pullrequests'))
665 raise HTTPForbidden()
671 raise HTTPForbidden()
666
672
667 @LoginRequired()
673 @LoginRequired()
668 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
674 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
669 'repository.admin')
675 'repository.admin')
670 def show(self, repo_name, pull_request_id):
676 def show(self, repo_name, pull_request_id):
671 pull_request_id = safe_int(pull_request_id)
677 pull_request_id = safe_int(pull_request_id)
672 c.pull_request = PullRequest.get_or_404(pull_request_id)
678 c.pull_request = PullRequest.get_or_404(pull_request_id)
673
679
674 c.template_context['pull_request_data']['pull_request_id'] = \
680 c.template_context['pull_request_data']['pull_request_id'] = \
675 pull_request_id
681 pull_request_id
676
682
677 # pull_requests repo_name we opened it against
683 # pull_requests repo_name we opened it against
678 # ie. target_repo must match
684 # ie. target_repo must match
679 if repo_name != c.pull_request.target_repo.repo_name:
685 if repo_name != c.pull_request.target_repo.repo_name:
680 raise HTTPNotFound
686 raise HTTPNotFound
681
687
682 c.allowed_to_change_status = PullRequestModel(). \
688 c.allowed_to_change_status = PullRequestModel(). \
683 check_user_change_status(c.pull_request, c.rhodecode_user)
689 check_user_change_status(c.pull_request, c.rhodecode_user)
684 c.allowed_to_update = PullRequestModel().check_user_update(
690 c.allowed_to_update = PullRequestModel().check_user_update(
685 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
691 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
686 c.allowed_to_merge = PullRequestModel().check_user_merge(
692 c.allowed_to_merge = PullRequestModel().check_user_merge(
687 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
693 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
688
694
689 cc_model = ChangesetCommentsModel()
695 cc_model = ChangesetCommentsModel()
690
696
691 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
697 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
692
698
693 c.pull_request_review_status = c.pull_request.calculated_review_status()
699 c.pull_request_review_status = c.pull_request.calculated_review_status()
694 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
700 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
695 c.pull_request)
701 c.pull_request)
696 c.approval_msg = None
702 c.approval_msg = None
697 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
703 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
698 c.approval_msg = _('Reviewer approval is pending.')
704 c.approval_msg = _('Reviewer approval is pending.')
699 c.pr_merge_status = False
705 c.pr_merge_status = False
700 # load compare data into template context
706 # load compare data into template context
701 enable_comments = not c.pull_request.is_closed()
707 enable_comments = not c.pull_request.is_closed()
702 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
708 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
703
709
704 # this is a hack to properly display links, when creating PR, the
710 # this is a hack to properly display links, when creating PR, the
705 # compare view and others uses different notation, and
711 # compare view and others uses different notation, and
706 # compare_commits.html renders links based on the target_repo.
712 # compare_commits.html renders links based on the target_repo.
707 # We need to swap that here to generate it properly on the html side
713 # We need to swap that here to generate it properly on the html side
708 c.target_repo = c.source_repo
714 c.target_repo = c.source_repo
709
715
710 # inline comments
716 # inline comments
711 c.inline_cnt = 0
717 c.inline_cnt = 0
712 c.inline_comments = cc_model.get_inline_comments(
718 c.inline_comments = cc_model.get_inline_comments(
713 c.rhodecode_db_repo.repo_id,
719 c.rhodecode_db_repo.repo_id,
714 pull_request=pull_request_id).items()
720 pull_request=pull_request_id).items()
715 # count inline comments
721 # count inline comments
716 for __, lines in c.inline_comments:
722 for __, lines in c.inline_comments:
717 for comments in lines.values():
723 for comments in lines.values():
718 c.inline_cnt += len(comments)
724 c.inline_cnt += len(comments)
719
725
720 # outdated comments
726 # outdated comments
721 c.outdated_cnt = 0
727 c.outdated_cnt = 0
722 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
728 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
723 c.outdated_comments = cc_model.get_outdated_comments(
729 c.outdated_comments = cc_model.get_outdated_comments(
724 c.rhodecode_db_repo.repo_id,
730 c.rhodecode_db_repo.repo_id,
725 pull_request=c.pull_request)
731 pull_request=c.pull_request)
726 # Count outdated comments and check for deleted files
732 # Count outdated comments and check for deleted files
727 for file_name, lines in c.outdated_comments.iteritems():
733 for file_name, lines in c.outdated_comments.iteritems():
728 for comments in lines.values():
734 for comments in lines.values():
729 c.outdated_cnt += len(comments)
735 c.outdated_cnt += len(comments)
730 if file_name not in c.included_files:
736 if file_name not in c.included_files:
731 c.deleted_files.append(file_name)
737 c.deleted_files.append(file_name)
732 else:
738 else:
733 c.outdated_comments = {}
739 c.outdated_comments = {}
734
740
735 # comments
741 # comments
736 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
742 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
737 pull_request=pull_request_id)
743 pull_request=pull_request_id)
738
744
739 if c.allowed_to_update:
745 if c.allowed_to_update:
740 force_close = ('forced_closed', _('Close Pull Request'))
746 force_close = ('forced_closed', _('Close Pull Request'))
741 statuses = ChangesetStatus.STATUSES + [force_close]
747 statuses = ChangesetStatus.STATUSES + [force_close]
742 else:
748 else:
743 statuses = ChangesetStatus.STATUSES
749 statuses = ChangesetStatus.STATUSES
744 c.commit_statuses = statuses
750 c.commit_statuses = statuses
745
751
746 c.ancestor = None # TODO: add ancestor here
752 c.ancestor = None # TODO: add ancestor here
747
753
748 return render('/pullrequests/pullrequest_show.html')
754 return render('/pullrequests/pullrequest_show.html')
749
755
750 @LoginRequired()
756 @LoginRequired()
751 @NotAnonymous()
757 @NotAnonymous()
752 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
758 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
753 'repository.admin')
759 'repository.admin')
754 @auth.CSRFRequired()
760 @auth.CSRFRequired()
755 @jsonify
761 @jsonify
756 def comment(self, repo_name, pull_request_id):
762 def comment(self, repo_name, pull_request_id):
757 pull_request_id = safe_int(pull_request_id)
763 pull_request_id = safe_int(pull_request_id)
758 pull_request = PullRequest.get_or_404(pull_request_id)
764 pull_request = PullRequest.get_or_404(pull_request_id)
759 if pull_request.is_closed():
765 if pull_request.is_closed():
760 raise HTTPForbidden()
766 raise HTTPForbidden()
761
767
762 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
768 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
763 # as a changeset status, still we want to send it in one value.
769 # as a changeset status, still we want to send it in one value.
764 status = request.POST.get('changeset_status', None)
770 status = request.POST.get('changeset_status', None)
765 text = request.POST.get('text')
771 text = request.POST.get('text')
766 if status and '_closed' in status:
772 if status and '_closed' in status:
767 close_pr = True
773 close_pr = True
768 status = status.replace('_closed', '')
774 status = status.replace('_closed', '')
769 else:
775 else:
770 close_pr = False
776 close_pr = False
771
777
772 forced = (status == 'forced')
778 forced = (status == 'forced')
773 if forced:
779 if forced:
774 status = 'rejected'
780 status = 'rejected'
775
781
776 allowed_to_change_status = PullRequestModel().check_user_change_status(
782 allowed_to_change_status = PullRequestModel().check_user_change_status(
777 pull_request, c.rhodecode_user)
783 pull_request, c.rhodecode_user)
778
784
779 if status and allowed_to_change_status:
785 if status and allowed_to_change_status:
780 message = (_('Status change %(transition_icon)s %(status)s')
786 message = (_('Status change %(transition_icon)s %(status)s')
781 % {'transition_icon': '>',
787 % {'transition_icon': '>',
782 'status': ChangesetStatus.get_status_lbl(status)})
788 'status': ChangesetStatus.get_status_lbl(status)})
783 if close_pr:
789 if close_pr:
784 message = _('Closing with') + ' ' + message
790 message = _('Closing with') + ' ' + message
785 text = text or message
791 text = text or message
786 comm = ChangesetCommentsModel().create(
792 comm = ChangesetCommentsModel().create(
787 text=text,
793 text=text,
788 repo=c.rhodecode_db_repo.repo_id,
794 repo=c.rhodecode_db_repo.repo_id,
789 user=c.rhodecode_user.user_id,
795 user=c.rhodecode_user.user_id,
790 pull_request=pull_request_id,
796 pull_request=pull_request_id,
791 f_path=request.POST.get('f_path'),
797 f_path=request.POST.get('f_path'),
792 line_no=request.POST.get('line'),
798 line_no=request.POST.get('line'),
793 status_change=(ChangesetStatus.get_status_lbl(status)
799 status_change=(ChangesetStatus.get_status_lbl(status)
794 if status and allowed_to_change_status else None),
800 if status and allowed_to_change_status else None),
795 status_change_type=(status
801 status_change_type=(status
796 if status and allowed_to_change_status else None),
802 if status and allowed_to_change_status else None),
797 closing_pr=close_pr
803 closing_pr=close_pr
798 )
804 )
799
805
800
806
801
807
802 if allowed_to_change_status:
808 if allowed_to_change_status:
803 old_calculated_status = pull_request.calculated_review_status()
809 old_calculated_status = pull_request.calculated_review_status()
804 # get status if set !
810 # get status if set !
805 if status:
811 if status:
806 ChangesetStatusModel().set_status(
812 ChangesetStatusModel().set_status(
807 c.rhodecode_db_repo.repo_id,
813 c.rhodecode_db_repo.repo_id,
808 status,
814 status,
809 c.rhodecode_user.user_id,
815 c.rhodecode_user.user_id,
810 comm,
816 comm,
811 pull_request=pull_request_id
817 pull_request=pull_request_id
812 )
818 )
813
819
814 Session().flush()
820 Session().flush()
815 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
821 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
816 # we now calculate the status of pull request, and based on that
822 # we now calculate the status of pull request, and based on that
817 # calculation we set the commits status
823 # calculation we set the commits status
818 calculated_status = pull_request.calculated_review_status()
824 calculated_status = pull_request.calculated_review_status()
819 if old_calculated_status != calculated_status:
825 if old_calculated_status != calculated_status:
820 PullRequestModel()._trigger_pull_request_hook(
826 PullRequestModel()._trigger_pull_request_hook(
821 pull_request, c.rhodecode_user, 'review_status_change')
827 pull_request, c.rhodecode_user, 'review_status_change')
822
828
823 calculated_status_lbl = ChangesetStatus.get_status_lbl(
829 calculated_status_lbl = ChangesetStatus.get_status_lbl(
824 calculated_status)
830 calculated_status)
825
831
826 if close_pr:
832 if close_pr:
827 status_completed = (
833 status_completed = (
828 calculated_status in [ChangesetStatus.STATUS_APPROVED,
834 calculated_status in [ChangesetStatus.STATUS_APPROVED,
829 ChangesetStatus.STATUS_REJECTED])
835 ChangesetStatus.STATUS_REJECTED])
830 if forced or status_completed:
836 if forced or status_completed:
831 PullRequestModel().close_pull_request(
837 PullRequestModel().close_pull_request(
832 pull_request_id, c.rhodecode_user)
838 pull_request_id, c.rhodecode_user)
833 else:
839 else:
834 h.flash(_('Closing pull request on other statuses than '
840 h.flash(_('Closing pull request on other statuses than '
835 'rejected or approved is forbidden. '
841 'rejected or approved is forbidden. '
836 'Calculated status from all reviewers '
842 'Calculated status from all reviewers '
837 'is currently: %s') % calculated_status_lbl,
843 'is currently: %s') % calculated_status_lbl,
838 category='warning')
844 category='warning')
839
845
840 Session().commit()
846 Session().commit()
841
847
842 if not request.is_xhr:
848 if not request.is_xhr:
843 return redirect(h.url('pullrequest_show', repo_name=repo_name,
849 return redirect(h.url('pullrequest_show', repo_name=repo_name,
844 pull_request_id=pull_request_id))
850 pull_request_id=pull_request_id))
845
851
846 data = {
852 data = {
847 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
853 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
848 }
854 }
849 if comm:
855 if comm:
850 c.co = comm
856 c.co = comm
851 data.update(comm.get_dict())
857 data.update(comm.get_dict())
852 data.update({'rendered_text':
858 data.update({'rendered_text':
853 render('changeset/changeset_comment_block.html')})
859 render('changeset/changeset_comment_block.html')})
854
860
855 return data
861 return data
856
862
857 @LoginRequired()
863 @LoginRequired()
858 @NotAnonymous()
864 @NotAnonymous()
859 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
865 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
860 'repository.admin')
866 'repository.admin')
861 @auth.CSRFRequired()
867 @auth.CSRFRequired()
862 @jsonify
868 @jsonify
863 def delete_comment(self, repo_name, comment_id):
869 def delete_comment(self, repo_name, comment_id):
864 return self._delete_comment(comment_id)
870 return self._delete_comment(comment_id)
865
871
866 def _delete_comment(self, comment_id):
872 def _delete_comment(self, comment_id):
867 comment_id = safe_int(comment_id)
873 comment_id = safe_int(comment_id)
868 co = ChangesetComment.get_or_404(comment_id)
874 co = ChangesetComment.get_or_404(comment_id)
869 if co.pull_request.is_closed():
875 if co.pull_request.is_closed():
870 # don't allow deleting comments on closed pull request
876 # don't allow deleting comments on closed pull request
871 raise HTTPForbidden()
877 raise HTTPForbidden()
872
878
873 is_owner = co.author.user_id == c.rhodecode_user.user_id
879 is_owner = co.author.user_id == c.rhodecode_user.user_id
874 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
880 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
875 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
881 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
876 old_calculated_status = co.pull_request.calculated_review_status()
882 old_calculated_status = co.pull_request.calculated_review_status()
877 ChangesetCommentsModel().delete(comment=co)
883 ChangesetCommentsModel().delete(comment=co)
878 Session().commit()
884 Session().commit()
879 calculated_status = co.pull_request.calculated_review_status()
885 calculated_status = co.pull_request.calculated_review_status()
880 if old_calculated_status != calculated_status:
886 if old_calculated_status != calculated_status:
881 PullRequestModel()._trigger_pull_request_hook(
887 PullRequestModel()._trigger_pull_request_hook(
882 co.pull_request, c.rhodecode_user, 'review_status_change')
888 co.pull_request, c.rhodecode_user, 'review_status_change')
883 return True
889 return True
884 else:
890 else:
885 raise HTTPForbidden()
891 raise HTTPForbidden()
@@ -1,3642 +1,3658 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import sys
27 import sys
28 import time
28 import time
29 import hashlib
29 import hashlib
30 import logging
30 import logging
31 import datetime
31 import datetime
32 import warnings
32 import warnings
33 import ipaddress
33 import ipaddress
34 import functools
34 import functools
35 import traceback
35 import traceback
36 import collections
36 import collections
37
37
38
38
39 from sqlalchemy import *
39 from sqlalchemy import *
40 from sqlalchemy.exc import IntegrityError
40 from sqlalchemy.exc import IntegrityError
41 from sqlalchemy.ext.declarative import declared_attr
41 from sqlalchemy.ext.declarative import declared_attr
42 from sqlalchemy.ext.hybrid import hybrid_property
42 from sqlalchemy.ext.hybrid import hybrid_property
43 from sqlalchemy.orm import (
43 from sqlalchemy.orm import (
44 relationship, joinedload, class_mapper, validates, aliased)
44 relationship, joinedload, class_mapper, validates, aliased)
45 from sqlalchemy.sql.expression import true
45 from sqlalchemy.sql.expression import true
46 from beaker.cache import cache_region, region_invalidate
46 from beaker.cache import cache_region, region_invalidate
47 from webob.exc import HTTPNotFound
47 from webob.exc import HTTPNotFound
48 from zope.cachedescriptors.property import Lazy as LazyProperty
48 from zope.cachedescriptors.property import Lazy as LazyProperty
49
49
50 from pylons import url
50 from pylons import url
51 from pylons.i18n.translation import lazy_ugettext as _
51 from pylons.i18n.translation import lazy_ugettext as _
52
52
53 from rhodecode.lib.vcs import get_backend, get_vcs_instance
53 from rhodecode.lib.vcs import get_backend, get_vcs_instance
54 from rhodecode.lib.vcs.utils.helpers import get_scm
54 from rhodecode.lib.vcs.utils.helpers import get_scm
55 from rhodecode.lib.vcs.exceptions import VCSError
55 from rhodecode.lib.vcs.exceptions import VCSError
56 from rhodecode.lib.vcs.backends.base import (
56 from rhodecode.lib.vcs.backends.base import (
57 EmptyCommit, Reference, MergeFailureReason)
57 EmptyCommit, Reference, MergeFailureReason)
58 from rhodecode.lib.utils2 import (
58 from rhodecode.lib.utils2 import (
59 str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe,
59 str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe,
60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
61 glob2re)
61 glob2re)
62 from rhodecode.lib.jsonalchemy import MutationObj, JsonType, JSONDict
62 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, JSONDict
63 from rhodecode.lib.ext_json import json
63 from rhodecode.lib.ext_json import json
64 from rhodecode.lib.caching_query import FromCache
64 from rhodecode.lib.caching_query import FromCache
65 from rhodecode.lib.encrypt import AESCipher
65 from rhodecode.lib.encrypt import AESCipher
66
66
67 from rhodecode.model.meta import Base, Session
67 from rhodecode.model.meta import Base, Session
68
68
69 URL_SEP = '/'
69 URL_SEP = '/'
70 log = logging.getLogger(__name__)
70 log = logging.getLogger(__name__)
71
71
72 # =============================================================================
72 # =============================================================================
73 # BASE CLASSES
73 # BASE CLASSES
74 # =============================================================================
74 # =============================================================================
75
75
76 # this is propagated from .ini file rhodecode.encrypted_values.secret or
76 # this is propagated from .ini file rhodecode.encrypted_values.secret or
77 # beaker.session.secret if first is not set.
77 # beaker.session.secret if first is not set.
78 # and initialized at environment.py
78 # and initialized at environment.py
79 ENCRYPTION_KEY = None
79 ENCRYPTION_KEY = None
80
80
81 # used to sort permissions by types, '#' used here is not allowed to be in
81 # used to sort permissions by types, '#' used here is not allowed to be in
82 # usernames, and it's very early in sorted string.printable table.
82 # usernames, and it's very early in sorted string.printable table.
83 PERMISSION_TYPE_SORT = {
83 PERMISSION_TYPE_SORT = {
84 'admin': '####',
84 'admin': '####',
85 'write': '###',
85 'write': '###',
86 'read': '##',
86 'read': '##',
87 'none': '#',
87 'none': '#',
88 }
88 }
89
89
90
90
91 def display_sort(obj):
91 def display_sort(obj):
92 """
92 """
93 Sort function used to sort permissions in .permissions() function of
93 Sort function used to sort permissions in .permissions() function of
94 Repository, RepoGroup, UserGroup. Also it put the default user in front
94 Repository, RepoGroup, UserGroup. Also it put the default user in front
95 of all other resources
95 of all other resources
96 """
96 """
97
97
98 if obj.username == User.DEFAULT_USER:
98 if obj.username == User.DEFAULT_USER:
99 return '#####'
99 return '#####'
100 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
100 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
101 return prefix + obj.username
101 return prefix + obj.username
102
102
103
103
104 def _hash_key(k):
104 def _hash_key(k):
105 return md5_safe(k)
105 return md5_safe(k)
106
106
107
107
108 class EncryptedTextValue(TypeDecorator):
108 class EncryptedTextValue(TypeDecorator):
109 """
109 """
110 Special column for encrypted long text data, use like::
110 Special column for encrypted long text data, use like::
111
111
112 value = Column("encrypted_value", EncryptedValue(), nullable=False)
112 value = Column("encrypted_value", EncryptedValue(), nullable=False)
113
113
114 This column is intelligent so if value is in unencrypted form it return
114 This column is intelligent so if value is in unencrypted form it return
115 unencrypted form, but on save it always encrypts
115 unencrypted form, but on save it always encrypts
116 """
116 """
117 impl = Text
117 impl = Text
118
118
119 def process_bind_param(self, value, dialect):
119 def process_bind_param(self, value, dialect):
120 if not value:
120 if not value:
121 return value
121 return value
122 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
122 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
123 # protect against double encrypting if someone manually starts
123 # protect against double encrypting if someone manually starts
124 # doing
124 # doing
125 raise ValueError('value needs to be in unencrypted format, ie. '
125 raise ValueError('value needs to be in unencrypted format, ie. '
126 'not starting with enc$aes')
126 'not starting with enc$aes')
127 return 'enc$aes_hmac$%s' % AESCipher(
127 return 'enc$aes_hmac$%s' % AESCipher(
128 ENCRYPTION_KEY, hmac=True).encrypt(value)
128 ENCRYPTION_KEY, hmac=True).encrypt(value)
129
129
130 def process_result_value(self, value, dialect):
130 def process_result_value(self, value, dialect):
131 import rhodecode
131 import rhodecode
132
132
133 if not value:
133 if not value:
134 return value
134 return value
135
135
136 parts = value.split('$', 3)
136 parts = value.split('$', 3)
137 if not len(parts) == 3:
137 if not len(parts) == 3:
138 # probably not encrypted values
138 # probably not encrypted values
139 return value
139 return value
140 else:
140 else:
141 if parts[0] != 'enc':
141 if parts[0] != 'enc':
142 # parts ok but without our header ?
142 # parts ok but without our header ?
143 return value
143 return value
144 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
144 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
145 'rhodecode.encrypted_values.strict') or True)
145 'rhodecode.encrypted_values.strict') or True)
146 # at that stage we know it's our encryption
146 # at that stage we know it's our encryption
147 if parts[1] == 'aes':
147 if parts[1] == 'aes':
148 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
148 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
149 elif parts[1] == 'aes_hmac':
149 elif parts[1] == 'aes_hmac':
150 decrypted_data = AESCipher(
150 decrypted_data = AESCipher(
151 ENCRYPTION_KEY, hmac=True,
151 ENCRYPTION_KEY, hmac=True,
152 strict_verification=enc_strict_mode).decrypt(parts[2])
152 strict_verification=enc_strict_mode).decrypt(parts[2])
153 else:
153 else:
154 raise ValueError(
154 raise ValueError(
155 'Encryption type part is wrong, must be `aes` '
155 'Encryption type part is wrong, must be `aes` '
156 'or `aes_hmac`, got `%s` instead' % (parts[1]))
156 'or `aes_hmac`, got `%s` instead' % (parts[1]))
157 return decrypted_data
157 return decrypted_data
158
158
159
159
160 class BaseModel(object):
160 class BaseModel(object):
161 """
161 """
162 Base Model for all classes
162 Base Model for all classes
163 """
163 """
164
164
165 @classmethod
165 @classmethod
166 def _get_keys(cls):
166 def _get_keys(cls):
167 """return column names for this model """
167 """return column names for this model """
168 return class_mapper(cls).c.keys()
168 return class_mapper(cls).c.keys()
169
169
170 def get_dict(self):
170 def get_dict(self):
171 """
171 """
172 return dict with keys and values corresponding
172 return dict with keys and values corresponding
173 to this model data """
173 to this model data """
174
174
175 d = {}
175 d = {}
176 for k in self._get_keys():
176 for k in self._get_keys():
177 d[k] = getattr(self, k)
177 d[k] = getattr(self, k)
178
178
179 # also use __json__() if present to get additional fields
179 # also use __json__() if present to get additional fields
180 _json_attr = getattr(self, '__json__', None)
180 _json_attr = getattr(self, '__json__', None)
181 if _json_attr:
181 if _json_attr:
182 # update with attributes from __json__
182 # update with attributes from __json__
183 if callable(_json_attr):
183 if callable(_json_attr):
184 _json_attr = _json_attr()
184 _json_attr = _json_attr()
185 for k, val in _json_attr.iteritems():
185 for k, val in _json_attr.iteritems():
186 d[k] = val
186 d[k] = val
187 return d
187 return d
188
188
189 def get_appstruct(self):
189 def get_appstruct(self):
190 """return list with keys and values tuples corresponding
190 """return list with keys and values tuples corresponding
191 to this model data """
191 to this model data """
192
192
193 l = []
193 l = []
194 for k in self._get_keys():
194 for k in self._get_keys():
195 l.append((k, getattr(self, k),))
195 l.append((k, getattr(self, k),))
196 return l
196 return l
197
197
198 def populate_obj(self, populate_dict):
198 def populate_obj(self, populate_dict):
199 """populate model with data from given populate_dict"""
199 """populate model with data from given populate_dict"""
200
200
201 for k in self._get_keys():
201 for k in self._get_keys():
202 if k in populate_dict:
202 if k in populate_dict:
203 setattr(self, k, populate_dict[k])
203 setattr(self, k, populate_dict[k])
204
204
205 @classmethod
205 @classmethod
206 def query(cls):
206 def query(cls):
207 return Session().query(cls)
207 return Session().query(cls)
208
208
209 @classmethod
209 @classmethod
210 def get(cls, id_):
210 def get(cls, id_):
211 if id_:
211 if id_:
212 return cls.query().get(id_)
212 return cls.query().get(id_)
213
213
214 @classmethod
214 @classmethod
215 def get_or_404(cls, id_):
215 def get_or_404(cls, id_):
216 try:
216 try:
217 id_ = int(id_)
217 id_ = int(id_)
218 except (TypeError, ValueError):
218 except (TypeError, ValueError):
219 raise HTTPNotFound
219 raise HTTPNotFound
220
220
221 res = cls.query().get(id_)
221 res = cls.query().get(id_)
222 if not res:
222 if not res:
223 raise HTTPNotFound
223 raise HTTPNotFound
224 return res
224 return res
225
225
226 @classmethod
226 @classmethod
227 def getAll(cls):
227 def getAll(cls):
228 # deprecated and left for backward compatibility
228 # deprecated and left for backward compatibility
229 return cls.get_all()
229 return cls.get_all()
230
230
231 @classmethod
231 @classmethod
232 def get_all(cls):
232 def get_all(cls):
233 return cls.query().all()
233 return cls.query().all()
234
234
235 @classmethod
235 @classmethod
236 def delete(cls, id_):
236 def delete(cls, id_):
237 obj = cls.query().get(id_)
237 obj = cls.query().get(id_)
238 Session().delete(obj)
238 Session().delete(obj)
239
239
240 @classmethod
240 @classmethod
241 def identity_cache(cls, session, attr_name, value):
241 def identity_cache(cls, session, attr_name, value):
242 exist_in_session = []
242 exist_in_session = []
243 for (item_cls, pkey), instance in session.identity_map.items():
243 for (item_cls, pkey), instance in session.identity_map.items():
244 if cls == item_cls and getattr(instance, attr_name) == value:
244 if cls == item_cls and getattr(instance, attr_name) == value:
245 exist_in_session.append(instance)
245 exist_in_session.append(instance)
246 if exist_in_session:
246 if exist_in_session:
247 if len(exist_in_session) == 1:
247 if len(exist_in_session) == 1:
248 return exist_in_session[0]
248 return exist_in_session[0]
249 log.exception(
249 log.exception(
250 'multiple objects with attr %s and '
250 'multiple objects with attr %s and '
251 'value %s found with same name: %r',
251 'value %s found with same name: %r',
252 attr_name, value, exist_in_session)
252 attr_name, value, exist_in_session)
253
253
254 def __repr__(self):
254 def __repr__(self):
255 if hasattr(self, '__unicode__'):
255 if hasattr(self, '__unicode__'):
256 # python repr needs to return str
256 # python repr needs to return str
257 try:
257 try:
258 return safe_str(self.__unicode__())
258 return safe_str(self.__unicode__())
259 except UnicodeDecodeError:
259 except UnicodeDecodeError:
260 pass
260 pass
261 return '<DB:%s>' % (self.__class__.__name__)
261 return '<DB:%s>' % (self.__class__.__name__)
262
262
263
263
264 class RhodeCodeSetting(Base, BaseModel):
264 class RhodeCodeSetting(Base, BaseModel):
265 __tablename__ = 'rhodecode_settings'
265 __tablename__ = 'rhodecode_settings'
266 __table_args__ = (
266 __table_args__ = (
267 UniqueConstraint('app_settings_name'),
267 UniqueConstraint('app_settings_name'),
268 {'extend_existing': True, 'mysql_engine': 'InnoDB',
268 {'extend_existing': True, 'mysql_engine': 'InnoDB',
269 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
269 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
270 )
270 )
271
271
272 SETTINGS_TYPES = {
272 SETTINGS_TYPES = {
273 'str': safe_str,
273 'str': safe_str,
274 'int': safe_int,
274 'int': safe_int,
275 'unicode': safe_unicode,
275 'unicode': safe_unicode,
276 'bool': str2bool,
276 'bool': str2bool,
277 'list': functools.partial(aslist, sep=',')
277 'list': functools.partial(aslist, sep=',')
278 }
278 }
279 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
279 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
280 GLOBAL_CONF_KEY = 'app_settings'
280 GLOBAL_CONF_KEY = 'app_settings'
281
281
282 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
282 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
283 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
283 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
284 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
284 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
285 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
285 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
286
286
287 def __init__(self, key='', val='', type='unicode'):
287 def __init__(self, key='', val='', type='unicode'):
288 self.app_settings_name = key
288 self.app_settings_name = key
289 self.app_settings_type = type
289 self.app_settings_type = type
290 self.app_settings_value = val
290 self.app_settings_value = val
291
291
292 @validates('_app_settings_value')
292 @validates('_app_settings_value')
293 def validate_settings_value(self, key, val):
293 def validate_settings_value(self, key, val):
294 assert type(val) == unicode
294 assert type(val) == unicode
295 return val
295 return val
296
296
297 @hybrid_property
297 @hybrid_property
298 def app_settings_value(self):
298 def app_settings_value(self):
299 v = self._app_settings_value
299 v = self._app_settings_value
300 _type = self.app_settings_type
300 _type = self.app_settings_type
301 if _type:
301 if _type:
302 _type = self.app_settings_type.split('.')[0]
302 _type = self.app_settings_type.split('.')[0]
303 # decode the encrypted value
303 # decode the encrypted value
304 if 'encrypted' in self.app_settings_type:
304 if 'encrypted' in self.app_settings_type:
305 cipher = EncryptedTextValue()
305 cipher = EncryptedTextValue()
306 v = safe_unicode(cipher.process_result_value(v, None))
306 v = safe_unicode(cipher.process_result_value(v, None))
307
307
308 converter = self.SETTINGS_TYPES.get(_type) or \
308 converter = self.SETTINGS_TYPES.get(_type) or \
309 self.SETTINGS_TYPES['unicode']
309 self.SETTINGS_TYPES['unicode']
310 return converter(v)
310 return converter(v)
311
311
312 @app_settings_value.setter
312 @app_settings_value.setter
313 def app_settings_value(self, val):
313 def app_settings_value(self, val):
314 """
314 """
315 Setter that will always make sure we use unicode in app_settings_value
315 Setter that will always make sure we use unicode in app_settings_value
316
316
317 :param val:
317 :param val:
318 """
318 """
319 val = safe_unicode(val)
319 val = safe_unicode(val)
320 # encode the encrypted value
320 # encode the encrypted value
321 if 'encrypted' in self.app_settings_type:
321 if 'encrypted' in self.app_settings_type:
322 cipher = EncryptedTextValue()
322 cipher = EncryptedTextValue()
323 val = safe_unicode(cipher.process_bind_param(val, None))
323 val = safe_unicode(cipher.process_bind_param(val, None))
324 self._app_settings_value = val
324 self._app_settings_value = val
325
325
326 @hybrid_property
326 @hybrid_property
327 def app_settings_type(self):
327 def app_settings_type(self):
328 return self._app_settings_type
328 return self._app_settings_type
329
329
330 @app_settings_type.setter
330 @app_settings_type.setter
331 def app_settings_type(self, val):
331 def app_settings_type(self, val):
332 if val.split('.')[0] not in self.SETTINGS_TYPES:
332 if val.split('.')[0] not in self.SETTINGS_TYPES:
333 raise Exception('type must be one of %s got %s'
333 raise Exception('type must be one of %s got %s'
334 % (self.SETTINGS_TYPES.keys(), val))
334 % (self.SETTINGS_TYPES.keys(), val))
335 self._app_settings_type = val
335 self._app_settings_type = val
336
336
337 def __unicode__(self):
337 def __unicode__(self):
338 return u"<%s('%s:%s[%s]')>" % (
338 return u"<%s('%s:%s[%s]')>" % (
339 self.__class__.__name__,
339 self.__class__.__name__,
340 self.app_settings_name, self.app_settings_value,
340 self.app_settings_name, self.app_settings_value,
341 self.app_settings_type
341 self.app_settings_type
342 )
342 )
343
343
344
344
345 class RhodeCodeUi(Base, BaseModel):
345 class RhodeCodeUi(Base, BaseModel):
346 __tablename__ = 'rhodecode_ui'
346 __tablename__ = 'rhodecode_ui'
347 __table_args__ = (
347 __table_args__ = (
348 UniqueConstraint('ui_key'),
348 UniqueConstraint('ui_key'),
349 {'extend_existing': True, 'mysql_engine': 'InnoDB',
349 {'extend_existing': True, 'mysql_engine': 'InnoDB',
350 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
350 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
351 )
351 )
352
352
353 HOOK_REPO_SIZE = 'changegroup.repo_size'
353 HOOK_REPO_SIZE = 'changegroup.repo_size'
354 # HG
354 # HG
355 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
355 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
356 HOOK_PULL = 'outgoing.pull_logger'
356 HOOK_PULL = 'outgoing.pull_logger'
357 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
357 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
358 HOOK_PUSH = 'changegroup.push_logger'
358 HOOK_PUSH = 'changegroup.push_logger'
359
359
360 # TODO: johbo: Unify way how hooks are configured for git and hg,
360 # TODO: johbo: Unify way how hooks are configured for git and hg,
361 # git part is currently hardcoded.
361 # git part is currently hardcoded.
362
362
363 # SVN PATTERNS
363 # SVN PATTERNS
364 SVN_BRANCH_ID = 'vcs_svn_branch'
364 SVN_BRANCH_ID = 'vcs_svn_branch'
365 SVN_TAG_ID = 'vcs_svn_tag'
365 SVN_TAG_ID = 'vcs_svn_tag'
366
366
367 ui_id = Column(
367 ui_id = Column(
368 "ui_id", Integer(), nullable=False, unique=True, default=None,
368 "ui_id", Integer(), nullable=False, unique=True, default=None,
369 primary_key=True)
369 primary_key=True)
370 ui_section = Column(
370 ui_section = Column(
371 "ui_section", String(255), nullable=True, unique=None, default=None)
371 "ui_section", String(255), nullable=True, unique=None, default=None)
372 ui_key = Column(
372 ui_key = Column(
373 "ui_key", String(255), nullable=True, unique=None, default=None)
373 "ui_key", String(255), nullable=True, unique=None, default=None)
374 ui_value = Column(
374 ui_value = Column(
375 "ui_value", String(255), nullable=True, unique=None, default=None)
375 "ui_value", String(255), nullable=True, unique=None, default=None)
376 ui_active = Column(
376 ui_active = Column(
377 "ui_active", Boolean(), nullable=True, unique=None, default=True)
377 "ui_active", Boolean(), nullable=True, unique=None, default=True)
378
378
379 def __repr__(self):
379 def __repr__(self):
380 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
380 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
381 self.ui_key, self.ui_value)
381 self.ui_key, self.ui_value)
382
382
383
383
384 class RepoRhodeCodeSetting(Base, BaseModel):
384 class RepoRhodeCodeSetting(Base, BaseModel):
385 __tablename__ = 'repo_rhodecode_settings'
385 __tablename__ = 'repo_rhodecode_settings'
386 __table_args__ = (
386 __table_args__ = (
387 UniqueConstraint(
387 UniqueConstraint(
388 'app_settings_name', 'repository_id',
388 'app_settings_name', 'repository_id',
389 name='uq_repo_rhodecode_setting_name_repo_id'),
389 name='uq_repo_rhodecode_setting_name_repo_id'),
390 {'extend_existing': True, 'mysql_engine': 'InnoDB',
390 {'extend_existing': True, 'mysql_engine': 'InnoDB',
391 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
391 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
392 )
392 )
393
393
394 repository_id = Column(
394 repository_id = Column(
395 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
395 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
396 nullable=False)
396 nullable=False)
397 app_settings_id = Column(
397 app_settings_id = Column(
398 "app_settings_id", Integer(), nullable=False, unique=True,
398 "app_settings_id", Integer(), nullable=False, unique=True,
399 default=None, primary_key=True)
399 default=None, primary_key=True)
400 app_settings_name = Column(
400 app_settings_name = Column(
401 "app_settings_name", String(255), nullable=True, unique=None,
401 "app_settings_name", String(255), nullable=True, unique=None,
402 default=None)
402 default=None)
403 _app_settings_value = Column(
403 _app_settings_value = Column(
404 "app_settings_value", String(4096), nullable=True, unique=None,
404 "app_settings_value", String(4096), nullable=True, unique=None,
405 default=None)
405 default=None)
406 _app_settings_type = Column(
406 _app_settings_type = Column(
407 "app_settings_type", String(255), nullable=True, unique=None,
407 "app_settings_type", String(255), nullable=True, unique=None,
408 default=None)
408 default=None)
409
409
410 repository = relationship('Repository')
410 repository = relationship('Repository')
411
411
412 def __init__(self, repository_id, key='', val='', type='unicode'):
412 def __init__(self, repository_id, key='', val='', type='unicode'):
413 self.repository_id = repository_id
413 self.repository_id = repository_id
414 self.app_settings_name = key
414 self.app_settings_name = key
415 self.app_settings_type = type
415 self.app_settings_type = type
416 self.app_settings_value = val
416 self.app_settings_value = val
417
417
418 @validates('_app_settings_value')
418 @validates('_app_settings_value')
419 def validate_settings_value(self, key, val):
419 def validate_settings_value(self, key, val):
420 assert type(val) == unicode
420 assert type(val) == unicode
421 return val
421 return val
422
422
423 @hybrid_property
423 @hybrid_property
424 def app_settings_value(self):
424 def app_settings_value(self):
425 v = self._app_settings_value
425 v = self._app_settings_value
426 type_ = self.app_settings_type
426 type_ = self.app_settings_type
427 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
427 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
428 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
428 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
429 return converter(v)
429 return converter(v)
430
430
431 @app_settings_value.setter
431 @app_settings_value.setter
432 def app_settings_value(self, val):
432 def app_settings_value(self, val):
433 """
433 """
434 Setter that will always make sure we use unicode in app_settings_value
434 Setter that will always make sure we use unicode in app_settings_value
435
435
436 :param val:
436 :param val:
437 """
437 """
438 self._app_settings_value = safe_unicode(val)
438 self._app_settings_value = safe_unicode(val)
439
439
440 @hybrid_property
440 @hybrid_property
441 def app_settings_type(self):
441 def app_settings_type(self):
442 return self._app_settings_type
442 return self._app_settings_type
443
443
444 @app_settings_type.setter
444 @app_settings_type.setter
445 def app_settings_type(self, val):
445 def app_settings_type(self, val):
446 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
446 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
447 if val not in SETTINGS_TYPES:
447 if val not in SETTINGS_TYPES:
448 raise Exception('type must be one of %s got %s'
448 raise Exception('type must be one of %s got %s'
449 % (SETTINGS_TYPES.keys(), val))
449 % (SETTINGS_TYPES.keys(), val))
450 self._app_settings_type = val
450 self._app_settings_type = val
451
451
452 def __unicode__(self):
452 def __unicode__(self):
453 return u"<%s('%s:%s:%s[%s]')>" % (
453 return u"<%s('%s:%s:%s[%s]')>" % (
454 self.__class__.__name__, self.repository.repo_name,
454 self.__class__.__name__, self.repository.repo_name,
455 self.app_settings_name, self.app_settings_value,
455 self.app_settings_name, self.app_settings_value,
456 self.app_settings_type
456 self.app_settings_type
457 )
457 )
458
458
459
459
460 class RepoRhodeCodeUi(Base, BaseModel):
460 class RepoRhodeCodeUi(Base, BaseModel):
461 __tablename__ = 'repo_rhodecode_ui'
461 __tablename__ = 'repo_rhodecode_ui'
462 __table_args__ = (
462 __table_args__ = (
463 UniqueConstraint(
463 UniqueConstraint(
464 'repository_id', 'ui_section', 'ui_key',
464 'repository_id', 'ui_section', 'ui_key',
465 name='uq_repo_rhodecode_ui_repository_id_section_key'),
465 name='uq_repo_rhodecode_ui_repository_id_section_key'),
466 {'extend_existing': True, 'mysql_engine': 'InnoDB',
466 {'extend_existing': True, 'mysql_engine': 'InnoDB',
467 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
467 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
468 )
468 )
469
469
470 repository_id = Column(
470 repository_id = Column(
471 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
471 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
472 nullable=False)
472 nullable=False)
473 ui_id = Column(
473 ui_id = Column(
474 "ui_id", Integer(), nullable=False, unique=True, default=None,
474 "ui_id", Integer(), nullable=False, unique=True, default=None,
475 primary_key=True)
475 primary_key=True)
476 ui_section = Column(
476 ui_section = Column(
477 "ui_section", String(255), nullable=True, unique=None, default=None)
477 "ui_section", String(255), nullable=True, unique=None, default=None)
478 ui_key = Column(
478 ui_key = Column(
479 "ui_key", String(255), nullable=True, unique=None, default=None)
479 "ui_key", String(255), nullable=True, unique=None, default=None)
480 ui_value = Column(
480 ui_value = Column(
481 "ui_value", String(255), nullable=True, unique=None, default=None)
481 "ui_value", String(255), nullable=True, unique=None, default=None)
482 ui_active = Column(
482 ui_active = Column(
483 "ui_active", Boolean(), nullable=True, unique=None, default=True)
483 "ui_active", Boolean(), nullable=True, unique=None, default=True)
484
484
485 repository = relationship('Repository')
485 repository = relationship('Repository')
486
486
487 def __repr__(self):
487 def __repr__(self):
488 return '<%s[%s:%s]%s=>%s]>' % (
488 return '<%s[%s:%s]%s=>%s]>' % (
489 self.__class__.__name__, self.repository.repo_name,
489 self.__class__.__name__, self.repository.repo_name,
490 self.ui_section, self.ui_key, self.ui_value)
490 self.ui_section, self.ui_key, self.ui_value)
491
491
492
492
493 class User(Base, BaseModel):
493 class User(Base, BaseModel):
494 __tablename__ = 'users'
494 __tablename__ = 'users'
495 __table_args__ = (
495 __table_args__ = (
496 UniqueConstraint('username'), UniqueConstraint('email'),
496 UniqueConstraint('username'), UniqueConstraint('email'),
497 Index('u_username_idx', 'username'),
497 Index('u_username_idx', 'username'),
498 Index('u_email_idx', 'email'),
498 Index('u_email_idx', 'email'),
499 {'extend_existing': True, 'mysql_engine': 'InnoDB',
499 {'extend_existing': True, 'mysql_engine': 'InnoDB',
500 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
500 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
501 )
501 )
502 DEFAULT_USER = 'default'
502 DEFAULT_USER = 'default'
503 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
503 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
504 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
504 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
505
505
506 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
506 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
507 username = Column("username", String(255), nullable=True, unique=None, default=None)
507 username = Column("username", String(255), nullable=True, unique=None, default=None)
508 password = Column("password", String(255), nullable=True, unique=None, default=None)
508 password = Column("password", String(255), nullable=True, unique=None, default=None)
509 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
509 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
510 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
510 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
511 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
511 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
512 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
512 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
513 _email = Column("email", String(255), nullable=True, unique=None, default=None)
513 _email = Column("email", String(255), nullable=True, unique=None, default=None)
514 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
514 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
515 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
515 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
516 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
516 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
517 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
517 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
518 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
518 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
519 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
519 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
520 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
520 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
521
521
522 user_log = relationship('UserLog')
522 user_log = relationship('UserLog')
523 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
523 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
524
524
525 repositories = relationship('Repository')
525 repositories = relationship('Repository')
526 repository_groups = relationship('RepoGroup')
526 repository_groups = relationship('RepoGroup')
527 user_groups = relationship('UserGroup')
527 user_groups = relationship('UserGroup')
528
528
529 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
529 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
530 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
530 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
531
531
532 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
532 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
533 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
533 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
534 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
534 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
535
535
536 group_member = relationship('UserGroupMember', cascade='all')
536 group_member = relationship('UserGroupMember', cascade='all')
537
537
538 notifications = relationship('UserNotification', cascade='all')
538 notifications = relationship('UserNotification', cascade='all')
539 # notifications assigned to this user
539 # notifications assigned to this user
540 user_created_notifications = relationship('Notification', cascade='all')
540 user_created_notifications = relationship('Notification', cascade='all')
541 # comments created by this user
541 # comments created by this user
542 user_comments = relationship('ChangesetComment', cascade='all')
542 user_comments = relationship('ChangesetComment', cascade='all')
543 # user profile extra info
543 # user profile extra info
544 user_emails = relationship('UserEmailMap', cascade='all')
544 user_emails = relationship('UserEmailMap', cascade='all')
545 user_ip_map = relationship('UserIpMap', cascade='all')
545 user_ip_map = relationship('UserIpMap', cascade='all')
546 user_auth_tokens = relationship('UserApiKeys', cascade='all')
546 user_auth_tokens = relationship('UserApiKeys', cascade='all')
547 # gists
547 # gists
548 user_gists = relationship('Gist', cascade='all')
548 user_gists = relationship('Gist', cascade='all')
549 # user pull requests
549 # user pull requests
550 user_pull_requests = relationship('PullRequest', cascade='all')
550 user_pull_requests = relationship('PullRequest', cascade='all')
551 # external identities
551 # external identities
552 extenal_identities = relationship(
552 extenal_identities = relationship(
553 'ExternalIdentity',
553 'ExternalIdentity',
554 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
554 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
555 cascade='all')
555 cascade='all')
556
556
557 def __unicode__(self):
557 def __unicode__(self):
558 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
558 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
559 self.user_id, self.username)
559 self.user_id, self.username)
560
560
561 @hybrid_property
561 @hybrid_property
562 def email(self):
562 def email(self):
563 return self._email
563 return self._email
564
564
565 @email.setter
565 @email.setter
566 def email(self, val):
566 def email(self, val):
567 self._email = val.lower() if val else None
567 self._email = val.lower() if val else None
568
568
569 @property
569 @property
570 def firstname(self):
570 def firstname(self):
571 # alias for future
571 # alias for future
572 return self.name
572 return self.name
573
573
574 @property
574 @property
575 def emails(self):
575 def emails(self):
576 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
576 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
577 return [self.email] + [x.email for x in other]
577 return [self.email] + [x.email for x in other]
578
578
579 @property
579 @property
580 def auth_tokens(self):
580 def auth_tokens(self):
581 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
581 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
582
582
583 @property
583 @property
584 def extra_auth_tokens(self):
584 def extra_auth_tokens(self):
585 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
585 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
586
586
587 @property
587 @property
588 def feed_token(self):
588 def feed_token(self):
589 feed_tokens = UserApiKeys.query()\
589 feed_tokens = UserApiKeys.query()\
590 .filter(UserApiKeys.user == self)\
590 .filter(UserApiKeys.user == self)\
591 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
591 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
592 .all()
592 .all()
593 if feed_tokens:
593 if feed_tokens:
594 return feed_tokens[0].api_key
594 return feed_tokens[0].api_key
595 else:
595 else:
596 # use the main token so we don't end up with nothing...
596 # use the main token so we don't end up with nothing...
597 return self.api_key
597 return self.api_key
598
598
599 @classmethod
599 @classmethod
600 def extra_valid_auth_tokens(cls, user, role=None):
600 def extra_valid_auth_tokens(cls, user, role=None):
601 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
601 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
602 .filter(or_(UserApiKeys.expires == -1,
602 .filter(or_(UserApiKeys.expires == -1,
603 UserApiKeys.expires >= time.time()))
603 UserApiKeys.expires >= time.time()))
604 if role:
604 if role:
605 tokens = tokens.filter(or_(UserApiKeys.role == role,
605 tokens = tokens.filter(or_(UserApiKeys.role == role,
606 UserApiKeys.role == UserApiKeys.ROLE_ALL))
606 UserApiKeys.role == UserApiKeys.ROLE_ALL))
607 return tokens.all()
607 return tokens.all()
608
608
609 @property
609 @property
610 def ip_addresses(self):
610 def ip_addresses(self):
611 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
611 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
612 return [x.ip_addr for x in ret]
612 return [x.ip_addr for x in ret]
613
613
614 @property
614 @property
615 def username_and_name(self):
615 def username_and_name(self):
616 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
616 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
617
617
618 @property
618 @property
619 def username_or_name_or_email(self):
619 def username_or_name_or_email(self):
620 full_name = self.full_name if self.full_name is not ' ' else None
620 full_name = self.full_name if self.full_name is not ' ' else None
621 return self.username or full_name or self.email
621 return self.username or full_name or self.email
622
622
623 @property
623 @property
624 def full_name(self):
624 def full_name(self):
625 return '%s %s' % (self.firstname, self.lastname)
625 return '%s %s' % (self.firstname, self.lastname)
626
626
627 @property
627 @property
628 def full_name_or_username(self):
628 def full_name_or_username(self):
629 return ('%s %s' % (self.firstname, self.lastname)
629 return ('%s %s' % (self.firstname, self.lastname)
630 if (self.firstname and self.lastname) else self.username)
630 if (self.firstname and self.lastname) else self.username)
631
631
632 @property
632 @property
633 def full_contact(self):
633 def full_contact(self):
634 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
634 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
635
635
636 @property
636 @property
637 def short_contact(self):
637 def short_contact(self):
638 return '%s %s' % (self.firstname, self.lastname)
638 return '%s %s' % (self.firstname, self.lastname)
639
639
640 @property
640 @property
641 def is_admin(self):
641 def is_admin(self):
642 return self.admin
642 return self.admin
643
643
644 @property
644 @property
645 def AuthUser(self):
645 def AuthUser(self):
646 """
646 """
647 Returns instance of AuthUser for this user
647 Returns instance of AuthUser for this user
648 """
648 """
649 from rhodecode.lib.auth import AuthUser
649 from rhodecode.lib.auth import AuthUser
650 return AuthUser(user_id=self.user_id, api_key=self.api_key,
650 return AuthUser(user_id=self.user_id, api_key=self.api_key,
651 username=self.username)
651 username=self.username)
652
652
653 @hybrid_property
653 @hybrid_property
654 def user_data(self):
654 def user_data(self):
655 if not self._user_data:
655 if not self._user_data:
656 return {}
656 return {}
657
657
658 try:
658 try:
659 return json.loads(self._user_data)
659 return json.loads(self._user_data)
660 except TypeError:
660 except TypeError:
661 return {}
661 return {}
662
662
663 @user_data.setter
663 @user_data.setter
664 def user_data(self, val):
664 def user_data(self, val):
665 if not isinstance(val, dict):
665 if not isinstance(val, dict):
666 raise Exception('user_data must be dict, got %s' % type(val))
666 raise Exception('user_data must be dict, got %s' % type(val))
667 try:
667 try:
668 self._user_data = json.dumps(val)
668 self._user_data = json.dumps(val)
669 except Exception:
669 except Exception:
670 log.error(traceback.format_exc())
670 log.error(traceback.format_exc())
671
671
672 @classmethod
672 @classmethod
673 def get_by_username(cls, username, case_insensitive=False,
673 def get_by_username(cls, username, case_insensitive=False,
674 cache=False, identity_cache=False):
674 cache=False, identity_cache=False):
675 session = Session()
675 session = Session()
676
676
677 if case_insensitive:
677 if case_insensitive:
678 q = cls.query().filter(
678 q = cls.query().filter(
679 func.lower(cls.username) == func.lower(username))
679 func.lower(cls.username) == func.lower(username))
680 else:
680 else:
681 q = cls.query().filter(cls.username == username)
681 q = cls.query().filter(cls.username == username)
682
682
683 if cache:
683 if cache:
684 if identity_cache:
684 if identity_cache:
685 val = cls.identity_cache(session, 'username', username)
685 val = cls.identity_cache(session, 'username', username)
686 if val:
686 if val:
687 return val
687 return val
688 else:
688 else:
689 q = q.options(
689 q = q.options(
690 FromCache("sql_cache_short",
690 FromCache("sql_cache_short",
691 "get_user_by_name_%s" % _hash_key(username)))
691 "get_user_by_name_%s" % _hash_key(username)))
692
692
693 return q.scalar()
693 return q.scalar()
694
694
695 @classmethod
695 @classmethod
696 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
696 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
697 q = cls.query().filter(cls.api_key == auth_token)
697 q = cls.query().filter(cls.api_key == auth_token)
698
698
699 if cache:
699 if cache:
700 q = q.options(FromCache("sql_cache_short",
700 q = q.options(FromCache("sql_cache_short",
701 "get_auth_token_%s" % auth_token))
701 "get_auth_token_%s" % auth_token))
702 res = q.scalar()
702 res = q.scalar()
703
703
704 if fallback and not res:
704 if fallback and not res:
705 #fallback to additional keys
705 #fallback to additional keys
706 _res = UserApiKeys.query()\
706 _res = UserApiKeys.query()\
707 .filter(UserApiKeys.api_key == auth_token)\
707 .filter(UserApiKeys.api_key == auth_token)\
708 .filter(or_(UserApiKeys.expires == -1,
708 .filter(or_(UserApiKeys.expires == -1,
709 UserApiKeys.expires >= time.time()))\
709 UserApiKeys.expires >= time.time()))\
710 .first()
710 .first()
711 if _res:
711 if _res:
712 res = _res.user
712 res = _res.user
713 return res
713 return res
714
714
715 @classmethod
715 @classmethod
716 def get_by_email(cls, email, case_insensitive=False, cache=False):
716 def get_by_email(cls, email, case_insensitive=False, cache=False):
717
717
718 if case_insensitive:
718 if case_insensitive:
719 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
719 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
720
720
721 else:
721 else:
722 q = cls.query().filter(cls.email == email)
722 q = cls.query().filter(cls.email == email)
723
723
724 if cache:
724 if cache:
725 q = q.options(FromCache("sql_cache_short",
725 q = q.options(FromCache("sql_cache_short",
726 "get_email_key_%s" % _hash_key(email)))
726 "get_email_key_%s" % _hash_key(email)))
727
727
728 ret = q.scalar()
728 ret = q.scalar()
729 if ret is None:
729 if ret is None:
730 q = UserEmailMap.query()
730 q = UserEmailMap.query()
731 # try fetching in alternate email map
731 # try fetching in alternate email map
732 if case_insensitive:
732 if case_insensitive:
733 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
733 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
734 else:
734 else:
735 q = q.filter(UserEmailMap.email == email)
735 q = q.filter(UserEmailMap.email == email)
736 q = q.options(joinedload(UserEmailMap.user))
736 q = q.options(joinedload(UserEmailMap.user))
737 if cache:
737 if cache:
738 q = q.options(FromCache("sql_cache_short",
738 q = q.options(FromCache("sql_cache_short",
739 "get_email_map_key_%s" % email))
739 "get_email_map_key_%s" % email))
740 ret = getattr(q.scalar(), 'user', None)
740 ret = getattr(q.scalar(), 'user', None)
741
741
742 return ret
742 return ret
743
743
744 @classmethod
744 @classmethod
745 def get_from_cs_author(cls, author):
745 def get_from_cs_author(cls, author):
746 """
746 """
747 Tries to get User objects out of commit author string
747 Tries to get User objects out of commit author string
748
748
749 :param author:
749 :param author:
750 """
750 """
751 from rhodecode.lib.helpers import email, author_name
751 from rhodecode.lib.helpers import email, author_name
752 # Valid email in the attribute passed, see if they're in the system
752 # Valid email in the attribute passed, see if they're in the system
753 _email = email(author)
753 _email = email(author)
754 if _email:
754 if _email:
755 user = cls.get_by_email(_email, case_insensitive=True)
755 user = cls.get_by_email(_email, case_insensitive=True)
756 if user:
756 if user:
757 return user
757 return user
758 # Maybe we can match by username?
758 # Maybe we can match by username?
759 _author = author_name(author)
759 _author = author_name(author)
760 user = cls.get_by_username(_author, case_insensitive=True)
760 user = cls.get_by_username(_author, case_insensitive=True)
761 if user:
761 if user:
762 return user
762 return user
763
763
764 def update_userdata(self, **kwargs):
764 def update_userdata(self, **kwargs):
765 usr = self
765 usr = self
766 old = usr.user_data
766 old = usr.user_data
767 old.update(**kwargs)
767 old.update(**kwargs)
768 usr.user_data = old
768 usr.user_data = old
769 Session().add(usr)
769 Session().add(usr)
770 log.debug('updated userdata with ', kwargs)
770 log.debug('updated userdata with ', kwargs)
771
771
772 def update_lastlogin(self):
772 def update_lastlogin(self):
773 """Update user lastlogin"""
773 """Update user lastlogin"""
774 self.last_login = datetime.datetime.now()
774 self.last_login = datetime.datetime.now()
775 Session().add(self)
775 Session().add(self)
776 log.debug('updated user %s lastlogin', self.username)
776 log.debug('updated user %s lastlogin', self.username)
777
777
778 def update_lastactivity(self):
778 def update_lastactivity(self):
779 """Update user lastactivity"""
779 """Update user lastactivity"""
780 usr = self
780 usr = self
781 old = usr.user_data
781 old = usr.user_data
782 old.update({'last_activity': time.time()})
782 old.update({'last_activity': time.time()})
783 usr.user_data = old
783 usr.user_data = old
784 Session().add(usr)
784 Session().add(usr)
785 log.debug('updated user %s lastactivity', usr.username)
785 log.debug('updated user %s lastactivity', usr.username)
786
786
787 def update_password(self, new_password, change_api_key=False):
787 def update_password(self, new_password, change_api_key=False):
788 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
788 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
789
789
790 self.password = get_crypt_password(new_password)
790 self.password = get_crypt_password(new_password)
791 if change_api_key:
791 if change_api_key:
792 self.api_key = generate_auth_token(self.username)
792 self.api_key = generate_auth_token(self.username)
793 Session().add(self)
793 Session().add(self)
794
794
795 @classmethod
795 @classmethod
796 def get_first_super_admin(cls):
796 def get_first_super_admin(cls):
797 user = User.query().filter(User.admin == true()).first()
797 user = User.query().filter(User.admin == true()).first()
798 if user is None:
798 if user is None:
799 raise Exception('FATAL: Missing administrative account!')
799 raise Exception('FATAL: Missing administrative account!')
800 return user
800 return user
801
801
802 @classmethod
802 @classmethod
803 def get_all_super_admins(cls):
803 def get_all_super_admins(cls):
804 """
804 """
805 Returns all admin accounts sorted by username
805 Returns all admin accounts sorted by username
806 """
806 """
807 return User.query().filter(User.admin == true())\
807 return User.query().filter(User.admin == true())\
808 .order_by(User.username.asc()).all()
808 .order_by(User.username.asc()).all()
809
809
810 @classmethod
810 @classmethod
811 def get_default_user(cls, cache=False):
811 def get_default_user(cls, cache=False):
812 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
812 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
813 if user is None:
813 if user is None:
814 raise Exception('FATAL: Missing default account!')
814 raise Exception('FATAL: Missing default account!')
815 return user
815 return user
816
816
817 def _get_default_perms(self, user, suffix=''):
817 def _get_default_perms(self, user, suffix=''):
818 from rhodecode.model.permission import PermissionModel
818 from rhodecode.model.permission import PermissionModel
819 return PermissionModel().get_default_perms(user.user_perms, suffix)
819 return PermissionModel().get_default_perms(user.user_perms, suffix)
820
820
821 def get_default_perms(self, suffix=''):
821 def get_default_perms(self, suffix=''):
822 return self._get_default_perms(self, suffix)
822 return self._get_default_perms(self, suffix)
823
823
824 def get_api_data(self, include_secrets=False, details='full'):
824 def get_api_data(self, include_secrets=False, details='full'):
825 """
825 """
826 Common function for generating user related data for API
826 Common function for generating user related data for API
827
827
828 :param include_secrets: By default secrets in the API data will be replaced
828 :param include_secrets: By default secrets in the API data will be replaced
829 by a placeholder value to prevent exposing this data by accident. In case
829 by a placeholder value to prevent exposing this data by accident. In case
830 this data shall be exposed, set this flag to ``True``.
830 this data shall be exposed, set this flag to ``True``.
831
831
832 :param details: details can be 'basic|full' basic gives only a subset of
832 :param details: details can be 'basic|full' basic gives only a subset of
833 the available user information that includes user_id, name and emails.
833 the available user information that includes user_id, name and emails.
834 """
834 """
835 user = self
835 user = self
836 user_data = self.user_data
836 user_data = self.user_data
837 data = {
837 data = {
838 'user_id': user.user_id,
838 'user_id': user.user_id,
839 'username': user.username,
839 'username': user.username,
840 'firstname': user.name,
840 'firstname': user.name,
841 'lastname': user.lastname,
841 'lastname': user.lastname,
842 'email': user.email,
842 'email': user.email,
843 'emails': user.emails,
843 'emails': user.emails,
844 }
844 }
845 if details == 'basic':
845 if details == 'basic':
846 return data
846 return data
847
847
848 api_key_length = 40
848 api_key_length = 40
849 api_key_replacement = '*' * api_key_length
849 api_key_replacement = '*' * api_key_length
850
850
851 extras = {
851 extras = {
852 'api_key': api_key_replacement,
852 'api_key': api_key_replacement,
853 'api_keys': [api_key_replacement],
853 'api_keys': [api_key_replacement],
854 'active': user.active,
854 'active': user.active,
855 'admin': user.admin,
855 'admin': user.admin,
856 'extern_type': user.extern_type,
856 'extern_type': user.extern_type,
857 'extern_name': user.extern_name,
857 'extern_name': user.extern_name,
858 'last_login': user.last_login,
858 'last_login': user.last_login,
859 'ip_addresses': user.ip_addresses,
859 'ip_addresses': user.ip_addresses,
860 'language': user_data.get('language')
860 'language': user_data.get('language')
861 }
861 }
862 data.update(extras)
862 data.update(extras)
863
863
864 if include_secrets:
864 if include_secrets:
865 data['api_key'] = user.api_key
865 data['api_key'] = user.api_key
866 data['api_keys'] = user.auth_tokens
866 data['api_keys'] = user.auth_tokens
867 return data
867 return data
868
868
869 def __json__(self):
869 def __json__(self):
870 data = {
870 data = {
871 'full_name': self.full_name,
871 'full_name': self.full_name,
872 'full_name_or_username': self.full_name_or_username,
872 'full_name_or_username': self.full_name_or_username,
873 'short_contact': self.short_contact,
873 'short_contact': self.short_contact,
874 'full_contact': self.full_contact,
874 'full_contact': self.full_contact,
875 }
875 }
876 data.update(self.get_api_data())
876 data.update(self.get_api_data())
877 return data
877 return data
878
878
879
879
880 class UserApiKeys(Base, BaseModel):
880 class UserApiKeys(Base, BaseModel):
881 __tablename__ = 'user_api_keys'
881 __tablename__ = 'user_api_keys'
882 __table_args__ = (
882 __table_args__ = (
883 Index('uak_api_key_idx', 'api_key'),
883 Index('uak_api_key_idx', 'api_key'),
884 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
884 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
885 UniqueConstraint('api_key'),
885 UniqueConstraint('api_key'),
886 {'extend_existing': True, 'mysql_engine': 'InnoDB',
886 {'extend_existing': True, 'mysql_engine': 'InnoDB',
887 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
887 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
888 )
888 )
889 __mapper_args__ = {}
889 __mapper_args__ = {}
890
890
891 # ApiKey role
891 # ApiKey role
892 ROLE_ALL = 'token_role_all'
892 ROLE_ALL = 'token_role_all'
893 ROLE_HTTP = 'token_role_http'
893 ROLE_HTTP = 'token_role_http'
894 ROLE_VCS = 'token_role_vcs'
894 ROLE_VCS = 'token_role_vcs'
895 ROLE_API = 'token_role_api'
895 ROLE_API = 'token_role_api'
896 ROLE_FEED = 'token_role_feed'
896 ROLE_FEED = 'token_role_feed'
897 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
897 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
898
898
899 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
899 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
900 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
900 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
901 api_key = Column("api_key", String(255), nullable=False, unique=True)
901 api_key = Column("api_key", String(255), nullable=False, unique=True)
902 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
902 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
903 expires = Column('expires', Float(53), nullable=False)
903 expires = Column('expires', Float(53), nullable=False)
904 role = Column('role', String(255), nullable=True)
904 role = Column('role', String(255), nullable=True)
905 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
905 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
906
906
907 user = relationship('User', lazy='joined')
907 user = relationship('User', lazy='joined')
908
908
909 @classmethod
909 @classmethod
910 def _get_role_name(cls, role):
910 def _get_role_name(cls, role):
911 return {
911 return {
912 cls.ROLE_ALL: _('all'),
912 cls.ROLE_ALL: _('all'),
913 cls.ROLE_HTTP: _('http/web interface'),
913 cls.ROLE_HTTP: _('http/web interface'),
914 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
914 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
915 cls.ROLE_API: _('api calls'),
915 cls.ROLE_API: _('api calls'),
916 cls.ROLE_FEED: _('feed access'),
916 cls.ROLE_FEED: _('feed access'),
917 }.get(role, role)
917 }.get(role, role)
918
918
919 @property
919 @property
920 def expired(self):
920 def expired(self):
921 if self.expires == -1:
921 if self.expires == -1:
922 return False
922 return False
923 return time.time() > self.expires
923 return time.time() > self.expires
924
924
925 @property
925 @property
926 def role_humanized(self):
926 def role_humanized(self):
927 return self._get_role_name(self.role)
927 return self._get_role_name(self.role)
928
928
929
929
930 class UserEmailMap(Base, BaseModel):
930 class UserEmailMap(Base, BaseModel):
931 __tablename__ = 'user_email_map'
931 __tablename__ = 'user_email_map'
932 __table_args__ = (
932 __table_args__ = (
933 Index('uem_email_idx', 'email'),
933 Index('uem_email_idx', 'email'),
934 UniqueConstraint('email'),
934 UniqueConstraint('email'),
935 {'extend_existing': True, 'mysql_engine': 'InnoDB',
935 {'extend_existing': True, 'mysql_engine': 'InnoDB',
936 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
936 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
937 )
937 )
938 __mapper_args__ = {}
938 __mapper_args__ = {}
939
939
940 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
940 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
941 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
941 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
942 _email = Column("email", String(255), nullable=True, unique=False, default=None)
942 _email = Column("email", String(255), nullable=True, unique=False, default=None)
943 user = relationship('User', lazy='joined')
943 user = relationship('User', lazy='joined')
944
944
945 @validates('_email')
945 @validates('_email')
946 def validate_email(self, key, email):
946 def validate_email(self, key, email):
947 # check if this email is not main one
947 # check if this email is not main one
948 main_email = Session().query(User).filter(User.email == email).scalar()
948 main_email = Session().query(User).filter(User.email == email).scalar()
949 if main_email is not None:
949 if main_email is not None:
950 raise AttributeError('email %s is present is user table' % email)
950 raise AttributeError('email %s is present is user table' % email)
951 return email
951 return email
952
952
953 @hybrid_property
953 @hybrid_property
954 def email(self):
954 def email(self):
955 return self._email
955 return self._email
956
956
957 @email.setter
957 @email.setter
958 def email(self, val):
958 def email(self, val):
959 self._email = val.lower() if val else None
959 self._email = val.lower() if val else None
960
960
961
961
962 class UserIpMap(Base, BaseModel):
962 class UserIpMap(Base, BaseModel):
963 __tablename__ = 'user_ip_map'
963 __tablename__ = 'user_ip_map'
964 __table_args__ = (
964 __table_args__ = (
965 UniqueConstraint('user_id', 'ip_addr'),
965 UniqueConstraint('user_id', 'ip_addr'),
966 {'extend_existing': True, 'mysql_engine': 'InnoDB',
966 {'extend_existing': True, 'mysql_engine': 'InnoDB',
967 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
967 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
968 )
968 )
969 __mapper_args__ = {}
969 __mapper_args__ = {}
970
970
971 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
971 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
972 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
972 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
973 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
973 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
974 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
974 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
975 description = Column("description", String(10000), nullable=True, unique=None, default=None)
975 description = Column("description", String(10000), nullable=True, unique=None, default=None)
976 user = relationship('User', lazy='joined')
976 user = relationship('User', lazy='joined')
977
977
978 @classmethod
978 @classmethod
979 def _get_ip_range(cls, ip_addr):
979 def _get_ip_range(cls, ip_addr):
980 net = ipaddress.ip_network(ip_addr, strict=False)
980 net = ipaddress.ip_network(ip_addr, strict=False)
981 return [str(net.network_address), str(net.broadcast_address)]
981 return [str(net.network_address), str(net.broadcast_address)]
982
982
983 def __json__(self):
983 def __json__(self):
984 return {
984 return {
985 'ip_addr': self.ip_addr,
985 'ip_addr': self.ip_addr,
986 'ip_range': self._get_ip_range(self.ip_addr),
986 'ip_range': self._get_ip_range(self.ip_addr),
987 }
987 }
988
988
989 def __unicode__(self):
989 def __unicode__(self):
990 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
990 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
991 self.user_id, self.ip_addr)
991 self.user_id, self.ip_addr)
992
992
993 class UserLog(Base, BaseModel):
993 class UserLog(Base, BaseModel):
994 __tablename__ = 'user_logs'
994 __tablename__ = 'user_logs'
995 __table_args__ = (
995 __table_args__ = (
996 {'extend_existing': True, 'mysql_engine': 'InnoDB',
996 {'extend_existing': True, 'mysql_engine': 'InnoDB',
997 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
997 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
998 )
998 )
999 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
999 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1000 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1000 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1001 username = Column("username", String(255), nullable=True, unique=None, default=None)
1001 username = Column("username", String(255), nullable=True, unique=None, default=None)
1002 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1002 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1003 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1003 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1004 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1004 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1005 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1005 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1006 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1006 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1007
1007
1008 def __unicode__(self):
1008 def __unicode__(self):
1009 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1009 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1010 self.repository_name,
1010 self.repository_name,
1011 self.action)
1011 self.action)
1012
1012
1013 @property
1013 @property
1014 def action_as_day(self):
1014 def action_as_day(self):
1015 return datetime.date(*self.action_date.timetuple()[:3])
1015 return datetime.date(*self.action_date.timetuple()[:3])
1016
1016
1017 user = relationship('User')
1017 user = relationship('User')
1018 repository = relationship('Repository', cascade='')
1018 repository = relationship('Repository', cascade='')
1019
1019
1020
1020
1021 class UserGroup(Base, BaseModel):
1021 class UserGroup(Base, BaseModel):
1022 __tablename__ = 'users_groups'
1022 __tablename__ = 'users_groups'
1023 __table_args__ = (
1023 __table_args__ = (
1024 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1024 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1025 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1025 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1026 )
1026 )
1027
1027
1028 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1028 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1029 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1029 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1030 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1030 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1031 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1031 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1032 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1032 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1033 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1033 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1034 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1034 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1035 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1035 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1036
1036
1037 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1037 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1038 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1038 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1039 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1039 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1040 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1040 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1041 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1041 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1042 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1042 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1043
1043
1044 user = relationship('User')
1044 user = relationship('User')
1045
1045
1046 @hybrid_property
1046 @hybrid_property
1047 def group_data(self):
1047 def group_data(self):
1048 if not self._group_data:
1048 if not self._group_data:
1049 return {}
1049 return {}
1050
1050
1051 try:
1051 try:
1052 return json.loads(self._group_data)
1052 return json.loads(self._group_data)
1053 except TypeError:
1053 except TypeError:
1054 return {}
1054 return {}
1055
1055
1056 @group_data.setter
1056 @group_data.setter
1057 def group_data(self, val):
1057 def group_data(self, val):
1058 try:
1058 try:
1059 self._group_data = json.dumps(val)
1059 self._group_data = json.dumps(val)
1060 except Exception:
1060 except Exception:
1061 log.error(traceback.format_exc())
1061 log.error(traceback.format_exc())
1062
1062
1063 def __unicode__(self):
1063 def __unicode__(self):
1064 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1064 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1065 self.users_group_id,
1065 self.users_group_id,
1066 self.users_group_name)
1066 self.users_group_name)
1067
1067
1068 @classmethod
1068 @classmethod
1069 def get_by_group_name(cls, group_name, cache=False,
1069 def get_by_group_name(cls, group_name, cache=False,
1070 case_insensitive=False):
1070 case_insensitive=False):
1071 if case_insensitive:
1071 if case_insensitive:
1072 q = cls.query().filter(func.lower(cls.users_group_name) ==
1072 q = cls.query().filter(func.lower(cls.users_group_name) ==
1073 func.lower(group_name))
1073 func.lower(group_name))
1074
1074
1075 else:
1075 else:
1076 q = cls.query().filter(cls.users_group_name == group_name)
1076 q = cls.query().filter(cls.users_group_name == group_name)
1077 if cache:
1077 if cache:
1078 q = q.options(FromCache(
1078 q = q.options(FromCache(
1079 "sql_cache_short",
1079 "sql_cache_short",
1080 "get_group_%s" % _hash_key(group_name)))
1080 "get_group_%s" % _hash_key(group_name)))
1081 return q.scalar()
1081 return q.scalar()
1082
1082
1083 @classmethod
1083 @classmethod
1084 def get(cls, user_group_id, cache=False):
1084 def get(cls, user_group_id, cache=False):
1085 user_group = cls.query()
1085 user_group = cls.query()
1086 if cache:
1086 if cache:
1087 user_group = user_group.options(FromCache("sql_cache_short",
1087 user_group = user_group.options(FromCache("sql_cache_short",
1088 "get_users_group_%s" % user_group_id))
1088 "get_users_group_%s" % user_group_id))
1089 return user_group.get(user_group_id)
1089 return user_group.get(user_group_id)
1090
1090
1091 def permissions(self, with_admins=True, with_owner=True):
1091 def permissions(self, with_admins=True, with_owner=True):
1092 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1092 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1093 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1093 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1094 joinedload(UserUserGroupToPerm.user),
1094 joinedload(UserUserGroupToPerm.user),
1095 joinedload(UserUserGroupToPerm.permission),)
1095 joinedload(UserUserGroupToPerm.permission),)
1096
1096
1097 # get owners and admins and permissions. We do a trick of re-writing
1097 # get owners and admins and permissions. We do a trick of re-writing
1098 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1098 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1099 # has a global reference and changing one object propagates to all
1099 # has a global reference and changing one object propagates to all
1100 # others. This means if admin is also an owner admin_row that change
1100 # others. This means if admin is also an owner admin_row that change
1101 # would propagate to both objects
1101 # would propagate to both objects
1102 perm_rows = []
1102 perm_rows = []
1103 for _usr in q.all():
1103 for _usr in q.all():
1104 usr = AttributeDict(_usr.user.get_dict())
1104 usr = AttributeDict(_usr.user.get_dict())
1105 usr.permission = _usr.permission.permission_name
1105 usr.permission = _usr.permission.permission_name
1106 perm_rows.append(usr)
1106 perm_rows.append(usr)
1107
1107
1108 # filter the perm rows by 'default' first and then sort them by
1108 # filter the perm rows by 'default' first and then sort them by
1109 # admin,write,read,none permissions sorted again alphabetically in
1109 # admin,write,read,none permissions sorted again alphabetically in
1110 # each group
1110 # each group
1111 perm_rows = sorted(perm_rows, key=display_sort)
1111 perm_rows = sorted(perm_rows, key=display_sort)
1112
1112
1113 _admin_perm = 'usergroup.admin'
1113 _admin_perm = 'usergroup.admin'
1114 owner_row = []
1114 owner_row = []
1115 if with_owner:
1115 if with_owner:
1116 usr = AttributeDict(self.user.get_dict())
1116 usr = AttributeDict(self.user.get_dict())
1117 usr.owner_row = True
1117 usr.owner_row = True
1118 usr.permission = _admin_perm
1118 usr.permission = _admin_perm
1119 owner_row.append(usr)
1119 owner_row.append(usr)
1120
1120
1121 super_admin_rows = []
1121 super_admin_rows = []
1122 if with_admins:
1122 if with_admins:
1123 for usr in User.get_all_super_admins():
1123 for usr in User.get_all_super_admins():
1124 # if this admin is also owner, don't double the record
1124 # if this admin is also owner, don't double the record
1125 if usr.user_id == owner_row[0].user_id:
1125 if usr.user_id == owner_row[0].user_id:
1126 owner_row[0].admin_row = True
1126 owner_row[0].admin_row = True
1127 else:
1127 else:
1128 usr = AttributeDict(usr.get_dict())
1128 usr = AttributeDict(usr.get_dict())
1129 usr.admin_row = True
1129 usr.admin_row = True
1130 usr.permission = _admin_perm
1130 usr.permission = _admin_perm
1131 super_admin_rows.append(usr)
1131 super_admin_rows.append(usr)
1132
1132
1133 return super_admin_rows + owner_row + perm_rows
1133 return super_admin_rows + owner_row + perm_rows
1134
1134
1135 def permission_user_groups(self):
1135 def permission_user_groups(self):
1136 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1136 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1137 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1137 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1138 joinedload(UserGroupUserGroupToPerm.target_user_group),
1138 joinedload(UserGroupUserGroupToPerm.target_user_group),
1139 joinedload(UserGroupUserGroupToPerm.permission),)
1139 joinedload(UserGroupUserGroupToPerm.permission),)
1140
1140
1141 perm_rows = []
1141 perm_rows = []
1142 for _user_group in q.all():
1142 for _user_group in q.all():
1143 usr = AttributeDict(_user_group.user_group.get_dict())
1143 usr = AttributeDict(_user_group.user_group.get_dict())
1144 usr.permission = _user_group.permission.permission_name
1144 usr.permission = _user_group.permission.permission_name
1145 perm_rows.append(usr)
1145 perm_rows.append(usr)
1146
1146
1147 return perm_rows
1147 return perm_rows
1148
1148
1149 def _get_default_perms(self, user_group, suffix=''):
1149 def _get_default_perms(self, user_group, suffix=''):
1150 from rhodecode.model.permission import PermissionModel
1150 from rhodecode.model.permission import PermissionModel
1151 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1151 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1152
1152
1153 def get_default_perms(self, suffix=''):
1153 def get_default_perms(self, suffix=''):
1154 return self._get_default_perms(self, suffix)
1154 return self._get_default_perms(self, suffix)
1155
1155
1156 def get_api_data(self, with_group_members=True, include_secrets=False):
1156 def get_api_data(self, with_group_members=True, include_secrets=False):
1157 """
1157 """
1158 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1158 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1159 basically forwarded.
1159 basically forwarded.
1160
1160
1161 """
1161 """
1162 user_group = self
1162 user_group = self
1163
1163
1164 data = {
1164 data = {
1165 'users_group_id': user_group.users_group_id,
1165 'users_group_id': user_group.users_group_id,
1166 'group_name': user_group.users_group_name,
1166 'group_name': user_group.users_group_name,
1167 'group_description': user_group.user_group_description,
1167 'group_description': user_group.user_group_description,
1168 'active': user_group.users_group_active,
1168 'active': user_group.users_group_active,
1169 'owner': user_group.user.username,
1169 'owner': user_group.user.username,
1170 }
1170 }
1171 if with_group_members:
1171 if with_group_members:
1172 users = []
1172 users = []
1173 for user in user_group.members:
1173 for user in user_group.members:
1174 user = user.user
1174 user = user.user
1175 users.append(user.get_api_data(include_secrets=include_secrets))
1175 users.append(user.get_api_data(include_secrets=include_secrets))
1176 data['users'] = users
1176 data['users'] = users
1177
1177
1178 return data
1178 return data
1179
1179
1180
1180
1181 class UserGroupMember(Base, BaseModel):
1181 class UserGroupMember(Base, BaseModel):
1182 __tablename__ = 'users_groups_members'
1182 __tablename__ = 'users_groups_members'
1183 __table_args__ = (
1183 __table_args__ = (
1184 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1184 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1185 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1185 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1186 )
1186 )
1187
1187
1188 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1188 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1189 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1189 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1190 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1190 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1191
1191
1192 user = relationship('User', lazy='joined')
1192 user = relationship('User', lazy='joined')
1193 users_group = relationship('UserGroup')
1193 users_group = relationship('UserGroup')
1194
1194
1195 def __init__(self, gr_id='', u_id=''):
1195 def __init__(self, gr_id='', u_id=''):
1196 self.users_group_id = gr_id
1196 self.users_group_id = gr_id
1197 self.user_id = u_id
1197 self.user_id = u_id
1198
1198
1199
1199
1200 class RepositoryField(Base, BaseModel):
1200 class RepositoryField(Base, BaseModel):
1201 __tablename__ = 'repositories_fields'
1201 __tablename__ = 'repositories_fields'
1202 __table_args__ = (
1202 __table_args__ = (
1203 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1203 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1204 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1204 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1205 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1205 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1206 )
1206 )
1207 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1207 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1208
1208
1209 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1209 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1210 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1210 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1211 field_key = Column("field_key", String(250))
1211 field_key = Column("field_key", String(250))
1212 field_label = Column("field_label", String(1024), nullable=False)
1212 field_label = Column("field_label", String(1024), nullable=False)
1213 field_value = Column("field_value", String(10000), nullable=False)
1213 field_value = Column("field_value", String(10000), nullable=False)
1214 field_desc = Column("field_desc", String(1024), nullable=False)
1214 field_desc = Column("field_desc", String(1024), nullable=False)
1215 field_type = Column("field_type", String(255), nullable=False, unique=None)
1215 field_type = Column("field_type", String(255), nullable=False, unique=None)
1216 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1216 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1217
1217
1218 repository = relationship('Repository')
1218 repository = relationship('Repository')
1219
1219
1220 @property
1220 @property
1221 def field_key_prefixed(self):
1221 def field_key_prefixed(self):
1222 return 'ex_%s' % self.field_key
1222 return 'ex_%s' % self.field_key
1223
1223
1224 @classmethod
1224 @classmethod
1225 def un_prefix_key(cls, key):
1225 def un_prefix_key(cls, key):
1226 if key.startswith(cls.PREFIX):
1226 if key.startswith(cls.PREFIX):
1227 return key[len(cls.PREFIX):]
1227 return key[len(cls.PREFIX):]
1228 return key
1228 return key
1229
1229
1230 @classmethod
1230 @classmethod
1231 def get_by_key_name(cls, key, repo):
1231 def get_by_key_name(cls, key, repo):
1232 row = cls.query()\
1232 row = cls.query()\
1233 .filter(cls.repository == repo)\
1233 .filter(cls.repository == repo)\
1234 .filter(cls.field_key == key).scalar()
1234 .filter(cls.field_key == key).scalar()
1235 return row
1235 return row
1236
1236
1237
1237
1238 class Repository(Base, BaseModel):
1238 class Repository(Base, BaseModel):
1239 __tablename__ = 'repositories'
1239 __tablename__ = 'repositories'
1240 __table_args__ = (
1240 __table_args__ = (
1241 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1241 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1242 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1242 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1243 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1243 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1244 )
1244 )
1245 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1245 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1246 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1246 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1247
1247
1248 STATE_CREATED = 'repo_state_created'
1248 STATE_CREATED = 'repo_state_created'
1249 STATE_PENDING = 'repo_state_pending'
1249 STATE_PENDING = 'repo_state_pending'
1250 STATE_ERROR = 'repo_state_error'
1250 STATE_ERROR = 'repo_state_error'
1251
1251
1252 LOCK_AUTOMATIC = 'lock_auto'
1252 LOCK_AUTOMATIC = 'lock_auto'
1253 LOCK_API = 'lock_api'
1253 LOCK_API = 'lock_api'
1254 LOCK_WEB = 'lock_web'
1254 LOCK_WEB = 'lock_web'
1255 LOCK_PULL = 'lock_pull'
1255 LOCK_PULL = 'lock_pull'
1256
1256
1257 NAME_SEP = URL_SEP
1257 NAME_SEP = URL_SEP
1258
1258
1259 repo_id = Column(
1259 repo_id = Column(
1260 "repo_id", Integer(), nullable=False, unique=True, default=None,
1260 "repo_id", Integer(), nullable=False, unique=True, default=None,
1261 primary_key=True)
1261 primary_key=True)
1262 _repo_name = Column(
1262 _repo_name = Column(
1263 "repo_name", Text(), nullable=False, default=None)
1263 "repo_name", Text(), nullable=False, default=None)
1264 _repo_name_hash = Column(
1264 _repo_name_hash = Column(
1265 "repo_name_hash", String(255), nullable=False, unique=True)
1265 "repo_name_hash", String(255), nullable=False, unique=True)
1266 repo_state = Column("repo_state", String(255), nullable=True)
1266 repo_state = Column("repo_state", String(255), nullable=True)
1267
1267
1268 clone_uri = Column(
1268 clone_uri = Column(
1269 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1269 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1270 default=None)
1270 default=None)
1271 repo_type = Column(
1271 repo_type = Column(
1272 "repo_type", String(255), nullable=False, unique=False, default=None)
1272 "repo_type", String(255), nullable=False, unique=False, default=None)
1273 user_id = Column(
1273 user_id = Column(
1274 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1274 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1275 unique=False, default=None)
1275 unique=False, default=None)
1276 private = Column(
1276 private = Column(
1277 "private", Boolean(), nullable=True, unique=None, default=None)
1277 "private", Boolean(), nullable=True, unique=None, default=None)
1278 enable_statistics = Column(
1278 enable_statistics = Column(
1279 "statistics", Boolean(), nullable=True, unique=None, default=True)
1279 "statistics", Boolean(), nullable=True, unique=None, default=True)
1280 enable_downloads = Column(
1280 enable_downloads = Column(
1281 "downloads", Boolean(), nullable=True, unique=None, default=True)
1281 "downloads", Boolean(), nullable=True, unique=None, default=True)
1282 description = Column(
1282 description = Column(
1283 "description", String(10000), nullable=True, unique=None, default=None)
1283 "description", String(10000), nullable=True, unique=None, default=None)
1284 created_on = Column(
1284 created_on = Column(
1285 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1285 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1286 default=datetime.datetime.now)
1286 default=datetime.datetime.now)
1287 updated_on = Column(
1287 updated_on = Column(
1288 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1288 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1289 default=datetime.datetime.now)
1289 default=datetime.datetime.now)
1290 _landing_revision = Column(
1290 _landing_revision = Column(
1291 "landing_revision", String(255), nullable=False, unique=False,
1291 "landing_revision", String(255), nullable=False, unique=False,
1292 default=None)
1292 default=None)
1293 enable_locking = Column(
1293 enable_locking = Column(
1294 "enable_locking", Boolean(), nullable=False, unique=None,
1294 "enable_locking", Boolean(), nullable=False, unique=None,
1295 default=False)
1295 default=False)
1296 _locked = Column(
1296 _locked = Column(
1297 "locked", String(255), nullable=True, unique=False, default=None)
1297 "locked", String(255), nullable=True, unique=False, default=None)
1298 _changeset_cache = Column(
1298 _changeset_cache = Column(
1299 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1299 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1300
1300
1301 fork_id = Column(
1301 fork_id = Column(
1302 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1302 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1303 nullable=True, unique=False, default=None)
1303 nullable=True, unique=False, default=None)
1304 group_id = Column(
1304 group_id = Column(
1305 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1305 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1306 unique=False, default=None)
1306 unique=False, default=None)
1307
1307
1308 user = relationship('User', lazy='joined')
1308 user = relationship('User', lazy='joined')
1309 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1309 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1310 group = relationship('RepoGroup', lazy='joined')
1310 group = relationship('RepoGroup', lazy='joined')
1311 repo_to_perm = relationship(
1311 repo_to_perm = relationship(
1312 'UserRepoToPerm', cascade='all',
1312 'UserRepoToPerm', cascade='all',
1313 order_by='UserRepoToPerm.repo_to_perm_id')
1313 order_by='UserRepoToPerm.repo_to_perm_id')
1314 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1314 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1315 stats = relationship('Statistics', cascade='all', uselist=False)
1315 stats = relationship('Statistics', cascade='all', uselist=False)
1316
1316
1317 followers = relationship(
1317 followers = relationship(
1318 'UserFollowing',
1318 'UserFollowing',
1319 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1319 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1320 cascade='all')
1320 cascade='all')
1321 extra_fields = relationship(
1321 extra_fields = relationship(
1322 'RepositoryField', cascade="all, delete, delete-orphan")
1322 'RepositoryField', cascade="all, delete, delete-orphan")
1323 logs = relationship('UserLog')
1323 logs = relationship('UserLog')
1324 comments = relationship(
1324 comments = relationship(
1325 'ChangesetComment', cascade="all, delete, delete-orphan")
1325 'ChangesetComment', cascade="all, delete, delete-orphan")
1326 pull_requests_source = relationship(
1326 pull_requests_source = relationship(
1327 'PullRequest',
1327 'PullRequest',
1328 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1328 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1329 cascade="all, delete, delete-orphan")
1329 cascade="all, delete, delete-orphan")
1330 pull_requests_target = relationship(
1330 pull_requests_target = relationship(
1331 'PullRequest',
1331 'PullRequest',
1332 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1332 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1333 cascade="all, delete, delete-orphan")
1333 cascade="all, delete, delete-orphan")
1334 ui = relationship('RepoRhodeCodeUi', cascade="all")
1334 ui = relationship('RepoRhodeCodeUi', cascade="all")
1335 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1335 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1336 integrations = relationship('Integration',
1336 integrations = relationship('Integration',
1337 cascade="all, delete, delete-orphan")
1337 cascade="all, delete, delete-orphan")
1338
1338
1339 def __unicode__(self):
1339 def __unicode__(self):
1340 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1340 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1341 safe_unicode(self.repo_name))
1341 safe_unicode(self.repo_name))
1342
1342
1343 @hybrid_property
1343 @hybrid_property
1344 def landing_rev(self):
1344 def landing_rev(self):
1345 # always should return [rev_type, rev]
1345 # always should return [rev_type, rev]
1346 if self._landing_revision:
1346 if self._landing_revision:
1347 _rev_info = self._landing_revision.split(':')
1347 _rev_info = self._landing_revision.split(':')
1348 if len(_rev_info) < 2:
1348 if len(_rev_info) < 2:
1349 _rev_info.insert(0, 'rev')
1349 _rev_info.insert(0, 'rev')
1350 return [_rev_info[0], _rev_info[1]]
1350 return [_rev_info[0], _rev_info[1]]
1351 return [None, None]
1351 return [None, None]
1352
1352
1353 @landing_rev.setter
1353 @landing_rev.setter
1354 def landing_rev(self, val):
1354 def landing_rev(self, val):
1355 if ':' not in val:
1355 if ':' not in val:
1356 raise ValueError('value must be delimited with `:` and consist '
1356 raise ValueError('value must be delimited with `:` and consist '
1357 'of <rev_type>:<rev>, got %s instead' % val)
1357 'of <rev_type>:<rev>, got %s instead' % val)
1358 self._landing_revision = val
1358 self._landing_revision = val
1359
1359
1360 @hybrid_property
1360 @hybrid_property
1361 def locked(self):
1361 def locked(self):
1362 if self._locked:
1362 if self._locked:
1363 user_id, timelocked, reason = self._locked.split(':')
1363 user_id, timelocked, reason = self._locked.split(':')
1364 lock_values = int(user_id), timelocked, reason
1364 lock_values = int(user_id), timelocked, reason
1365 else:
1365 else:
1366 lock_values = [None, None, None]
1366 lock_values = [None, None, None]
1367 return lock_values
1367 return lock_values
1368
1368
1369 @locked.setter
1369 @locked.setter
1370 def locked(self, val):
1370 def locked(self, val):
1371 if val and isinstance(val, (list, tuple)):
1371 if val and isinstance(val, (list, tuple)):
1372 self._locked = ':'.join(map(str, val))
1372 self._locked = ':'.join(map(str, val))
1373 else:
1373 else:
1374 self._locked = None
1374 self._locked = None
1375
1375
1376 @hybrid_property
1376 @hybrid_property
1377 def changeset_cache(self):
1377 def changeset_cache(self):
1378 from rhodecode.lib.vcs.backends.base import EmptyCommit
1378 from rhodecode.lib.vcs.backends.base import EmptyCommit
1379 dummy = EmptyCommit().__json__()
1379 dummy = EmptyCommit().__json__()
1380 if not self._changeset_cache:
1380 if not self._changeset_cache:
1381 return dummy
1381 return dummy
1382 try:
1382 try:
1383 return json.loads(self._changeset_cache)
1383 return json.loads(self._changeset_cache)
1384 except TypeError:
1384 except TypeError:
1385 return dummy
1385 return dummy
1386 except Exception:
1386 except Exception:
1387 log.error(traceback.format_exc())
1387 log.error(traceback.format_exc())
1388 return dummy
1388 return dummy
1389
1389
1390 @changeset_cache.setter
1390 @changeset_cache.setter
1391 def changeset_cache(self, val):
1391 def changeset_cache(self, val):
1392 try:
1392 try:
1393 self._changeset_cache = json.dumps(val)
1393 self._changeset_cache = json.dumps(val)
1394 except Exception:
1394 except Exception:
1395 log.error(traceback.format_exc())
1395 log.error(traceback.format_exc())
1396
1396
1397 @hybrid_property
1397 @hybrid_property
1398 def repo_name(self):
1398 def repo_name(self):
1399 return self._repo_name
1399 return self._repo_name
1400
1400
1401 @repo_name.setter
1401 @repo_name.setter
1402 def repo_name(self, value):
1402 def repo_name(self, value):
1403 self._repo_name = value
1403 self._repo_name = value
1404 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1404 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1405
1405
1406 @classmethod
1406 @classmethod
1407 def normalize_repo_name(cls, repo_name):
1407 def normalize_repo_name(cls, repo_name):
1408 """
1408 """
1409 Normalizes os specific repo_name to the format internally stored inside
1409 Normalizes os specific repo_name to the format internally stored inside
1410 database using URL_SEP
1410 database using URL_SEP
1411
1411
1412 :param cls:
1412 :param cls:
1413 :param repo_name:
1413 :param repo_name:
1414 """
1414 """
1415 return cls.NAME_SEP.join(repo_name.split(os.sep))
1415 return cls.NAME_SEP.join(repo_name.split(os.sep))
1416
1416
1417 @classmethod
1417 @classmethod
1418 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1418 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1419 session = Session()
1419 session = Session()
1420 q = session.query(cls).filter(cls.repo_name == repo_name)
1420 q = session.query(cls).filter(cls.repo_name == repo_name)
1421
1421
1422 if cache:
1422 if cache:
1423 if identity_cache:
1423 if identity_cache:
1424 val = cls.identity_cache(session, 'repo_name', repo_name)
1424 val = cls.identity_cache(session, 'repo_name', repo_name)
1425 if val:
1425 if val:
1426 return val
1426 return val
1427 else:
1427 else:
1428 q = q.options(
1428 q = q.options(
1429 FromCache("sql_cache_short",
1429 FromCache("sql_cache_short",
1430 "get_repo_by_name_%s" % _hash_key(repo_name)))
1430 "get_repo_by_name_%s" % _hash_key(repo_name)))
1431
1431
1432 return q.scalar()
1432 return q.scalar()
1433
1433
1434 @classmethod
1434 @classmethod
1435 def get_by_full_path(cls, repo_full_path):
1435 def get_by_full_path(cls, repo_full_path):
1436 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1436 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1437 repo_name = cls.normalize_repo_name(repo_name)
1437 repo_name = cls.normalize_repo_name(repo_name)
1438 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1438 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1439
1439
1440 @classmethod
1440 @classmethod
1441 def get_repo_forks(cls, repo_id):
1441 def get_repo_forks(cls, repo_id):
1442 return cls.query().filter(Repository.fork_id == repo_id)
1442 return cls.query().filter(Repository.fork_id == repo_id)
1443
1443
1444 @classmethod
1444 @classmethod
1445 def base_path(cls):
1445 def base_path(cls):
1446 """
1446 """
1447 Returns base path when all repos are stored
1447 Returns base path when all repos are stored
1448
1448
1449 :param cls:
1449 :param cls:
1450 """
1450 """
1451 q = Session().query(RhodeCodeUi)\
1451 q = Session().query(RhodeCodeUi)\
1452 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1452 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1453 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1453 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1454 return q.one().ui_value
1454 return q.one().ui_value
1455
1455
1456 @classmethod
1456 @classmethod
1457 def is_valid(cls, repo_name):
1457 def is_valid(cls, repo_name):
1458 """
1458 """
1459 returns True if given repo name is a valid filesystem repository
1459 returns True if given repo name is a valid filesystem repository
1460
1460
1461 :param cls:
1461 :param cls:
1462 :param repo_name:
1462 :param repo_name:
1463 """
1463 """
1464 from rhodecode.lib.utils import is_valid_repo
1464 from rhodecode.lib.utils import is_valid_repo
1465
1465
1466 return is_valid_repo(repo_name, cls.base_path())
1466 return is_valid_repo(repo_name, cls.base_path())
1467
1467
1468 @classmethod
1468 @classmethod
1469 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1469 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1470 case_insensitive=True):
1470 case_insensitive=True):
1471 q = Repository.query()
1471 q = Repository.query()
1472
1472
1473 if not isinstance(user_id, Optional):
1473 if not isinstance(user_id, Optional):
1474 q = q.filter(Repository.user_id == user_id)
1474 q = q.filter(Repository.user_id == user_id)
1475
1475
1476 if not isinstance(group_id, Optional):
1476 if not isinstance(group_id, Optional):
1477 q = q.filter(Repository.group_id == group_id)
1477 q = q.filter(Repository.group_id == group_id)
1478
1478
1479 if case_insensitive:
1479 if case_insensitive:
1480 q = q.order_by(func.lower(Repository.repo_name))
1480 q = q.order_by(func.lower(Repository.repo_name))
1481 else:
1481 else:
1482 q = q.order_by(Repository.repo_name)
1482 q = q.order_by(Repository.repo_name)
1483 return q.all()
1483 return q.all()
1484
1484
1485 @property
1485 @property
1486 def forks(self):
1486 def forks(self):
1487 """
1487 """
1488 Return forks of this repo
1488 Return forks of this repo
1489 """
1489 """
1490 return Repository.get_repo_forks(self.repo_id)
1490 return Repository.get_repo_forks(self.repo_id)
1491
1491
1492 @property
1492 @property
1493 def parent(self):
1493 def parent(self):
1494 """
1494 """
1495 Returns fork parent
1495 Returns fork parent
1496 """
1496 """
1497 return self.fork
1497 return self.fork
1498
1498
1499 @property
1499 @property
1500 def just_name(self):
1500 def just_name(self):
1501 return self.repo_name.split(self.NAME_SEP)[-1]
1501 return self.repo_name.split(self.NAME_SEP)[-1]
1502
1502
1503 @property
1503 @property
1504 def groups_with_parents(self):
1504 def groups_with_parents(self):
1505 groups = []
1505 groups = []
1506 if self.group is None:
1506 if self.group is None:
1507 return groups
1507 return groups
1508
1508
1509 cur_gr = self.group
1509 cur_gr = self.group
1510 groups.insert(0, cur_gr)
1510 groups.insert(0, cur_gr)
1511 while 1:
1511 while 1:
1512 gr = getattr(cur_gr, 'parent_group', None)
1512 gr = getattr(cur_gr, 'parent_group', None)
1513 cur_gr = cur_gr.parent_group
1513 cur_gr = cur_gr.parent_group
1514 if gr is None:
1514 if gr is None:
1515 break
1515 break
1516 groups.insert(0, gr)
1516 groups.insert(0, gr)
1517
1517
1518 return groups
1518 return groups
1519
1519
1520 @property
1520 @property
1521 def groups_and_repo(self):
1521 def groups_and_repo(self):
1522 return self.groups_with_parents, self
1522 return self.groups_with_parents, self
1523
1523
1524 @LazyProperty
1524 @LazyProperty
1525 def repo_path(self):
1525 def repo_path(self):
1526 """
1526 """
1527 Returns base full path for that repository means where it actually
1527 Returns base full path for that repository means where it actually
1528 exists on a filesystem
1528 exists on a filesystem
1529 """
1529 """
1530 q = Session().query(RhodeCodeUi).filter(
1530 q = Session().query(RhodeCodeUi).filter(
1531 RhodeCodeUi.ui_key == self.NAME_SEP)
1531 RhodeCodeUi.ui_key == self.NAME_SEP)
1532 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1532 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1533 return q.one().ui_value
1533 return q.one().ui_value
1534
1534
1535 @property
1535 @property
1536 def repo_full_path(self):
1536 def repo_full_path(self):
1537 p = [self.repo_path]
1537 p = [self.repo_path]
1538 # we need to split the name by / since this is how we store the
1538 # we need to split the name by / since this is how we store the
1539 # names in the database, but that eventually needs to be converted
1539 # names in the database, but that eventually needs to be converted
1540 # into a valid system path
1540 # into a valid system path
1541 p += self.repo_name.split(self.NAME_SEP)
1541 p += self.repo_name.split(self.NAME_SEP)
1542 return os.path.join(*map(safe_unicode, p))
1542 return os.path.join(*map(safe_unicode, p))
1543
1543
1544 @property
1544 @property
1545 def cache_keys(self):
1545 def cache_keys(self):
1546 """
1546 """
1547 Returns associated cache keys for that repo
1547 Returns associated cache keys for that repo
1548 """
1548 """
1549 return CacheKey.query()\
1549 return CacheKey.query()\
1550 .filter(CacheKey.cache_args == self.repo_name)\
1550 .filter(CacheKey.cache_args == self.repo_name)\
1551 .order_by(CacheKey.cache_key)\
1551 .order_by(CacheKey.cache_key)\
1552 .all()
1552 .all()
1553
1553
1554 def get_new_name(self, repo_name):
1554 def get_new_name(self, repo_name):
1555 """
1555 """
1556 returns new full repository name based on assigned group and new new
1556 returns new full repository name based on assigned group and new new
1557
1557
1558 :param group_name:
1558 :param group_name:
1559 """
1559 """
1560 path_prefix = self.group.full_path_splitted if self.group else []
1560 path_prefix = self.group.full_path_splitted if self.group else []
1561 return self.NAME_SEP.join(path_prefix + [repo_name])
1561 return self.NAME_SEP.join(path_prefix + [repo_name])
1562
1562
1563 @property
1563 @property
1564 def _config(self):
1564 def _config(self):
1565 """
1565 """
1566 Returns db based config object.
1566 Returns db based config object.
1567 """
1567 """
1568 from rhodecode.lib.utils import make_db_config
1568 from rhodecode.lib.utils import make_db_config
1569 return make_db_config(clear_session=False, repo=self)
1569 return make_db_config(clear_session=False, repo=self)
1570
1570
1571 def permissions(self, with_admins=True, with_owner=True):
1571 def permissions(self, with_admins=True, with_owner=True):
1572 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1572 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1573 q = q.options(joinedload(UserRepoToPerm.repository),
1573 q = q.options(joinedload(UserRepoToPerm.repository),
1574 joinedload(UserRepoToPerm.user),
1574 joinedload(UserRepoToPerm.user),
1575 joinedload(UserRepoToPerm.permission),)
1575 joinedload(UserRepoToPerm.permission),)
1576
1576
1577 # get owners and admins and permissions. We do a trick of re-writing
1577 # get owners and admins and permissions. We do a trick of re-writing
1578 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1578 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1579 # has a global reference and changing one object propagates to all
1579 # has a global reference and changing one object propagates to all
1580 # others. This means if admin is also an owner admin_row that change
1580 # others. This means if admin is also an owner admin_row that change
1581 # would propagate to both objects
1581 # would propagate to both objects
1582 perm_rows = []
1582 perm_rows = []
1583 for _usr in q.all():
1583 for _usr in q.all():
1584 usr = AttributeDict(_usr.user.get_dict())
1584 usr = AttributeDict(_usr.user.get_dict())
1585 usr.permission = _usr.permission.permission_name
1585 usr.permission = _usr.permission.permission_name
1586 perm_rows.append(usr)
1586 perm_rows.append(usr)
1587
1587
1588 # filter the perm rows by 'default' first and then sort them by
1588 # filter the perm rows by 'default' first and then sort them by
1589 # admin,write,read,none permissions sorted again alphabetically in
1589 # admin,write,read,none permissions sorted again alphabetically in
1590 # each group
1590 # each group
1591 perm_rows = sorted(perm_rows, key=display_sort)
1591 perm_rows = sorted(perm_rows, key=display_sort)
1592
1592
1593 _admin_perm = 'repository.admin'
1593 _admin_perm = 'repository.admin'
1594 owner_row = []
1594 owner_row = []
1595 if with_owner:
1595 if with_owner:
1596 usr = AttributeDict(self.user.get_dict())
1596 usr = AttributeDict(self.user.get_dict())
1597 usr.owner_row = True
1597 usr.owner_row = True
1598 usr.permission = _admin_perm
1598 usr.permission = _admin_perm
1599 owner_row.append(usr)
1599 owner_row.append(usr)
1600
1600
1601 super_admin_rows = []
1601 super_admin_rows = []
1602 if with_admins:
1602 if with_admins:
1603 for usr in User.get_all_super_admins():
1603 for usr in User.get_all_super_admins():
1604 # if this admin is also owner, don't double the record
1604 # if this admin is also owner, don't double the record
1605 if usr.user_id == owner_row[0].user_id:
1605 if usr.user_id == owner_row[0].user_id:
1606 owner_row[0].admin_row = True
1606 owner_row[0].admin_row = True
1607 else:
1607 else:
1608 usr = AttributeDict(usr.get_dict())
1608 usr = AttributeDict(usr.get_dict())
1609 usr.admin_row = True
1609 usr.admin_row = True
1610 usr.permission = _admin_perm
1610 usr.permission = _admin_perm
1611 super_admin_rows.append(usr)
1611 super_admin_rows.append(usr)
1612
1612
1613 return super_admin_rows + owner_row + perm_rows
1613 return super_admin_rows + owner_row + perm_rows
1614
1614
1615 def permission_user_groups(self):
1615 def permission_user_groups(self):
1616 q = UserGroupRepoToPerm.query().filter(
1616 q = UserGroupRepoToPerm.query().filter(
1617 UserGroupRepoToPerm.repository == self)
1617 UserGroupRepoToPerm.repository == self)
1618 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1618 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1619 joinedload(UserGroupRepoToPerm.users_group),
1619 joinedload(UserGroupRepoToPerm.users_group),
1620 joinedload(UserGroupRepoToPerm.permission),)
1620 joinedload(UserGroupRepoToPerm.permission),)
1621
1621
1622 perm_rows = []
1622 perm_rows = []
1623 for _user_group in q.all():
1623 for _user_group in q.all():
1624 usr = AttributeDict(_user_group.users_group.get_dict())
1624 usr = AttributeDict(_user_group.users_group.get_dict())
1625 usr.permission = _user_group.permission.permission_name
1625 usr.permission = _user_group.permission.permission_name
1626 perm_rows.append(usr)
1626 perm_rows.append(usr)
1627
1627
1628 return perm_rows
1628 return perm_rows
1629
1629
1630 def get_api_data(self, include_secrets=False):
1630 def get_api_data(self, include_secrets=False):
1631 """
1631 """
1632 Common function for generating repo api data
1632 Common function for generating repo api data
1633
1633
1634 :param include_secrets: See :meth:`User.get_api_data`.
1634 :param include_secrets: See :meth:`User.get_api_data`.
1635
1635
1636 """
1636 """
1637 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1637 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1638 # move this methods on models level.
1638 # move this methods on models level.
1639 from rhodecode.model.settings import SettingsModel
1639 from rhodecode.model.settings import SettingsModel
1640
1640
1641 repo = self
1641 repo = self
1642 _user_id, _time, _reason = self.locked
1642 _user_id, _time, _reason = self.locked
1643
1643
1644 data = {
1644 data = {
1645 'repo_id': repo.repo_id,
1645 'repo_id': repo.repo_id,
1646 'repo_name': repo.repo_name,
1646 'repo_name': repo.repo_name,
1647 'repo_type': repo.repo_type,
1647 'repo_type': repo.repo_type,
1648 'clone_uri': repo.clone_uri or '',
1648 'clone_uri': repo.clone_uri or '',
1649 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1649 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1650 'private': repo.private,
1650 'private': repo.private,
1651 'created_on': repo.created_on,
1651 'created_on': repo.created_on,
1652 'description': repo.description,
1652 'description': repo.description,
1653 'landing_rev': repo.landing_rev,
1653 'landing_rev': repo.landing_rev,
1654 'owner': repo.user.username,
1654 'owner': repo.user.username,
1655 'fork_of': repo.fork.repo_name if repo.fork else None,
1655 'fork_of': repo.fork.repo_name if repo.fork else None,
1656 'enable_statistics': repo.enable_statistics,
1656 'enable_statistics': repo.enable_statistics,
1657 'enable_locking': repo.enable_locking,
1657 'enable_locking': repo.enable_locking,
1658 'enable_downloads': repo.enable_downloads,
1658 'enable_downloads': repo.enable_downloads,
1659 'last_changeset': repo.changeset_cache,
1659 'last_changeset': repo.changeset_cache,
1660 'locked_by': User.get(_user_id).get_api_data(
1660 'locked_by': User.get(_user_id).get_api_data(
1661 include_secrets=include_secrets) if _user_id else None,
1661 include_secrets=include_secrets) if _user_id else None,
1662 'locked_date': time_to_datetime(_time) if _time else None,
1662 'locked_date': time_to_datetime(_time) if _time else None,
1663 'lock_reason': _reason if _reason else None,
1663 'lock_reason': _reason if _reason else None,
1664 }
1664 }
1665
1665
1666 # TODO: mikhail: should be per-repo settings here
1666 # TODO: mikhail: should be per-repo settings here
1667 rc_config = SettingsModel().get_all_settings()
1667 rc_config = SettingsModel().get_all_settings()
1668 repository_fields = str2bool(
1668 repository_fields = str2bool(
1669 rc_config.get('rhodecode_repository_fields'))
1669 rc_config.get('rhodecode_repository_fields'))
1670 if repository_fields:
1670 if repository_fields:
1671 for f in self.extra_fields:
1671 for f in self.extra_fields:
1672 data[f.field_key_prefixed] = f.field_value
1672 data[f.field_key_prefixed] = f.field_value
1673
1673
1674 return data
1674 return data
1675
1675
1676 @classmethod
1676 @classmethod
1677 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1677 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1678 if not lock_time:
1678 if not lock_time:
1679 lock_time = time.time()
1679 lock_time = time.time()
1680 if not lock_reason:
1680 if not lock_reason:
1681 lock_reason = cls.LOCK_AUTOMATIC
1681 lock_reason = cls.LOCK_AUTOMATIC
1682 repo.locked = [user_id, lock_time, lock_reason]
1682 repo.locked = [user_id, lock_time, lock_reason]
1683 Session().add(repo)
1683 Session().add(repo)
1684 Session().commit()
1684 Session().commit()
1685
1685
1686 @classmethod
1686 @classmethod
1687 def unlock(cls, repo):
1687 def unlock(cls, repo):
1688 repo.locked = None
1688 repo.locked = None
1689 Session().add(repo)
1689 Session().add(repo)
1690 Session().commit()
1690 Session().commit()
1691
1691
1692 @classmethod
1692 @classmethod
1693 def getlock(cls, repo):
1693 def getlock(cls, repo):
1694 return repo.locked
1694 return repo.locked
1695
1695
1696 def is_user_lock(self, user_id):
1696 def is_user_lock(self, user_id):
1697 if self.lock[0]:
1697 if self.lock[0]:
1698 lock_user_id = safe_int(self.lock[0])
1698 lock_user_id = safe_int(self.lock[0])
1699 user_id = safe_int(user_id)
1699 user_id = safe_int(user_id)
1700 # both are ints, and they are equal
1700 # both are ints, and they are equal
1701 return all([lock_user_id, user_id]) and lock_user_id == user_id
1701 return all([lock_user_id, user_id]) and lock_user_id == user_id
1702
1702
1703 return False
1703 return False
1704
1704
1705 def get_locking_state(self, action, user_id, only_when_enabled=True):
1705 def get_locking_state(self, action, user_id, only_when_enabled=True):
1706 """
1706 """
1707 Checks locking on this repository, if locking is enabled and lock is
1707 Checks locking on this repository, if locking is enabled and lock is
1708 present returns a tuple of make_lock, locked, locked_by.
1708 present returns a tuple of make_lock, locked, locked_by.
1709 make_lock can have 3 states None (do nothing) True, make lock
1709 make_lock can have 3 states None (do nothing) True, make lock
1710 False release lock, This value is later propagated to hooks, which
1710 False release lock, This value is later propagated to hooks, which
1711 do the locking. Think about this as signals passed to hooks what to do.
1711 do the locking. Think about this as signals passed to hooks what to do.
1712
1712
1713 """
1713 """
1714 # TODO: johbo: This is part of the business logic and should be moved
1714 # TODO: johbo: This is part of the business logic and should be moved
1715 # into the RepositoryModel.
1715 # into the RepositoryModel.
1716
1716
1717 if action not in ('push', 'pull'):
1717 if action not in ('push', 'pull'):
1718 raise ValueError("Invalid action value: %s" % repr(action))
1718 raise ValueError("Invalid action value: %s" % repr(action))
1719
1719
1720 # defines if locked error should be thrown to user
1720 # defines if locked error should be thrown to user
1721 currently_locked = False
1721 currently_locked = False
1722 # defines if new lock should be made, tri-state
1722 # defines if new lock should be made, tri-state
1723 make_lock = None
1723 make_lock = None
1724 repo = self
1724 repo = self
1725 user = User.get(user_id)
1725 user = User.get(user_id)
1726
1726
1727 lock_info = repo.locked
1727 lock_info = repo.locked
1728
1728
1729 if repo and (repo.enable_locking or not only_when_enabled):
1729 if repo and (repo.enable_locking or not only_when_enabled):
1730 if action == 'push':
1730 if action == 'push':
1731 # check if it's already locked !, if it is compare users
1731 # check if it's already locked !, if it is compare users
1732 locked_by_user_id = lock_info[0]
1732 locked_by_user_id = lock_info[0]
1733 if user.user_id == locked_by_user_id:
1733 if user.user_id == locked_by_user_id:
1734 log.debug(
1734 log.debug(
1735 'Got `push` action from user %s, now unlocking', user)
1735 'Got `push` action from user %s, now unlocking', user)
1736 # unlock if we have push from user who locked
1736 # unlock if we have push from user who locked
1737 make_lock = False
1737 make_lock = False
1738 else:
1738 else:
1739 # we're not the same user who locked, ban with
1739 # we're not the same user who locked, ban with
1740 # code defined in settings (default is 423 HTTP Locked) !
1740 # code defined in settings (default is 423 HTTP Locked) !
1741 log.debug('Repo %s is currently locked by %s', repo, user)
1741 log.debug('Repo %s is currently locked by %s', repo, user)
1742 currently_locked = True
1742 currently_locked = True
1743 elif action == 'pull':
1743 elif action == 'pull':
1744 # [0] user [1] date
1744 # [0] user [1] date
1745 if lock_info[0] and lock_info[1]:
1745 if lock_info[0] and lock_info[1]:
1746 log.debug('Repo %s is currently locked by %s', repo, user)
1746 log.debug('Repo %s is currently locked by %s', repo, user)
1747 currently_locked = True
1747 currently_locked = True
1748 else:
1748 else:
1749 log.debug('Setting lock on repo %s by %s', repo, user)
1749 log.debug('Setting lock on repo %s by %s', repo, user)
1750 make_lock = True
1750 make_lock = True
1751
1751
1752 else:
1752 else:
1753 log.debug('Repository %s do not have locking enabled', repo)
1753 log.debug('Repository %s do not have locking enabled', repo)
1754
1754
1755 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1755 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1756 make_lock, currently_locked, lock_info)
1756 make_lock, currently_locked, lock_info)
1757
1757
1758 from rhodecode.lib.auth import HasRepoPermissionAny
1758 from rhodecode.lib.auth import HasRepoPermissionAny
1759 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1759 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1760 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1760 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1761 # if we don't have at least write permission we cannot make a lock
1761 # if we don't have at least write permission we cannot make a lock
1762 log.debug('lock state reset back to FALSE due to lack '
1762 log.debug('lock state reset back to FALSE due to lack '
1763 'of at least read permission')
1763 'of at least read permission')
1764 make_lock = False
1764 make_lock = False
1765
1765
1766 return make_lock, currently_locked, lock_info
1766 return make_lock, currently_locked, lock_info
1767
1767
1768 @property
1768 @property
1769 def last_db_change(self):
1769 def last_db_change(self):
1770 return self.updated_on
1770 return self.updated_on
1771
1771
1772 @property
1772 @property
1773 def clone_uri_hidden(self):
1773 def clone_uri_hidden(self):
1774 clone_uri = self.clone_uri
1774 clone_uri = self.clone_uri
1775 if clone_uri:
1775 if clone_uri:
1776 import urlobject
1776 import urlobject
1777 url_obj = urlobject.URLObject(clone_uri)
1777 url_obj = urlobject.URLObject(clone_uri)
1778 if url_obj.password:
1778 if url_obj.password:
1779 clone_uri = url_obj.with_password('*****')
1779 clone_uri = url_obj.with_password('*****')
1780 return clone_uri
1780 return clone_uri
1781
1781
1782 def clone_url(self, **override):
1782 def clone_url(self, **override):
1783 qualified_home_url = url('home', qualified=True)
1783 qualified_home_url = url('home', qualified=True)
1784
1784
1785 uri_tmpl = None
1785 uri_tmpl = None
1786 if 'with_id' in override:
1786 if 'with_id' in override:
1787 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1787 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1788 del override['with_id']
1788 del override['with_id']
1789
1789
1790 if 'uri_tmpl' in override:
1790 if 'uri_tmpl' in override:
1791 uri_tmpl = override['uri_tmpl']
1791 uri_tmpl = override['uri_tmpl']
1792 del override['uri_tmpl']
1792 del override['uri_tmpl']
1793
1793
1794 # we didn't override our tmpl from **overrides
1794 # we didn't override our tmpl from **overrides
1795 if not uri_tmpl:
1795 if not uri_tmpl:
1796 uri_tmpl = self.DEFAULT_CLONE_URI
1796 uri_tmpl = self.DEFAULT_CLONE_URI
1797 try:
1797 try:
1798 from pylons import tmpl_context as c
1798 from pylons import tmpl_context as c
1799 uri_tmpl = c.clone_uri_tmpl
1799 uri_tmpl = c.clone_uri_tmpl
1800 except Exception:
1800 except Exception:
1801 # in any case if we call this outside of request context,
1801 # in any case if we call this outside of request context,
1802 # ie, not having tmpl_context set up
1802 # ie, not having tmpl_context set up
1803 pass
1803 pass
1804
1804
1805 return get_clone_url(uri_tmpl=uri_tmpl,
1805 return get_clone_url(uri_tmpl=uri_tmpl,
1806 qualifed_home_url=qualified_home_url,
1806 qualifed_home_url=qualified_home_url,
1807 repo_name=self.repo_name,
1807 repo_name=self.repo_name,
1808 repo_id=self.repo_id, **override)
1808 repo_id=self.repo_id, **override)
1809
1809
1810 def set_state(self, state):
1810 def set_state(self, state):
1811 self.repo_state = state
1811 self.repo_state = state
1812 Session().add(self)
1812 Session().add(self)
1813 #==========================================================================
1813 #==========================================================================
1814 # SCM PROPERTIES
1814 # SCM PROPERTIES
1815 #==========================================================================
1815 #==========================================================================
1816
1816
1817 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1817 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1818 return get_commit_safe(
1818 return get_commit_safe(
1819 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1819 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1820
1820
1821 def get_changeset(self, rev=None, pre_load=None):
1821 def get_changeset(self, rev=None, pre_load=None):
1822 warnings.warn("Use get_commit", DeprecationWarning)
1822 warnings.warn("Use get_commit", DeprecationWarning)
1823 commit_id = None
1823 commit_id = None
1824 commit_idx = None
1824 commit_idx = None
1825 if isinstance(rev, basestring):
1825 if isinstance(rev, basestring):
1826 commit_id = rev
1826 commit_id = rev
1827 else:
1827 else:
1828 commit_idx = rev
1828 commit_idx = rev
1829 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1829 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1830 pre_load=pre_load)
1830 pre_load=pre_load)
1831
1831
1832 def get_landing_commit(self):
1832 def get_landing_commit(self):
1833 """
1833 """
1834 Returns landing commit, or if that doesn't exist returns the tip
1834 Returns landing commit, or if that doesn't exist returns the tip
1835 """
1835 """
1836 _rev_type, _rev = self.landing_rev
1836 _rev_type, _rev = self.landing_rev
1837 commit = self.get_commit(_rev)
1837 commit = self.get_commit(_rev)
1838 if isinstance(commit, EmptyCommit):
1838 if isinstance(commit, EmptyCommit):
1839 return self.get_commit()
1839 return self.get_commit()
1840 return commit
1840 return commit
1841
1841
1842 def update_commit_cache(self, cs_cache=None, config=None):
1842 def update_commit_cache(self, cs_cache=None, config=None):
1843 """
1843 """
1844 Update cache of last changeset for repository, keys should be::
1844 Update cache of last changeset for repository, keys should be::
1845
1845
1846 short_id
1846 short_id
1847 raw_id
1847 raw_id
1848 revision
1848 revision
1849 parents
1849 parents
1850 message
1850 message
1851 date
1851 date
1852 author
1852 author
1853
1853
1854 :param cs_cache:
1854 :param cs_cache:
1855 """
1855 """
1856 from rhodecode.lib.vcs.backends.base import BaseChangeset
1856 from rhodecode.lib.vcs.backends.base import BaseChangeset
1857 if cs_cache is None:
1857 if cs_cache is None:
1858 # use no-cache version here
1858 # use no-cache version here
1859 scm_repo = self.scm_instance(cache=False, config=config)
1859 scm_repo = self.scm_instance(cache=False, config=config)
1860 if scm_repo:
1860 if scm_repo:
1861 cs_cache = scm_repo.get_commit(
1861 cs_cache = scm_repo.get_commit(
1862 pre_load=["author", "date", "message", "parents"])
1862 pre_load=["author", "date", "message", "parents"])
1863 else:
1863 else:
1864 cs_cache = EmptyCommit()
1864 cs_cache = EmptyCommit()
1865
1865
1866 if isinstance(cs_cache, BaseChangeset):
1866 if isinstance(cs_cache, BaseChangeset):
1867 cs_cache = cs_cache.__json__()
1867 cs_cache = cs_cache.__json__()
1868
1868
1869 def is_outdated(new_cs_cache):
1869 def is_outdated(new_cs_cache):
1870 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1870 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1871 new_cs_cache['revision'] != self.changeset_cache['revision']):
1871 new_cs_cache['revision'] != self.changeset_cache['revision']):
1872 return True
1872 return True
1873 return False
1873 return False
1874
1874
1875 # check if we have maybe already latest cached revision
1875 # check if we have maybe already latest cached revision
1876 if is_outdated(cs_cache) or not self.changeset_cache:
1876 if is_outdated(cs_cache) or not self.changeset_cache:
1877 _default = datetime.datetime.fromtimestamp(0)
1877 _default = datetime.datetime.fromtimestamp(0)
1878 last_change = cs_cache.get('date') or _default
1878 last_change = cs_cache.get('date') or _default
1879 log.debug('updated repo %s with new cs cache %s',
1879 log.debug('updated repo %s with new cs cache %s',
1880 self.repo_name, cs_cache)
1880 self.repo_name, cs_cache)
1881 self.updated_on = last_change
1881 self.updated_on = last_change
1882 self.changeset_cache = cs_cache
1882 self.changeset_cache = cs_cache
1883 Session().add(self)
1883 Session().add(self)
1884 Session().commit()
1884 Session().commit()
1885 else:
1885 else:
1886 log.debug('Skipping update_commit_cache for repo:`%s` '
1886 log.debug('Skipping update_commit_cache for repo:`%s` '
1887 'commit already with latest changes', self.repo_name)
1887 'commit already with latest changes', self.repo_name)
1888
1888
1889 @property
1889 @property
1890 def tip(self):
1890 def tip(self):
1891 return self.get_commit('tip')
1891 return self.get_commit('tip')
1892
1892
1893 @property
1893 @property
1894 def author(self):
1894 def author(self):
1895 return self.tip.author
1895 return self.tip.author
1896
1896
1897 @property
1897 @property
1898 def last_change(self):
1898 def last_change(self):
1899 return self.scm_instance().last_change
1899 return self.scm_instance().last_change
1900
1900
1901 def get_comments(self, revisions=None):
1901 def get_comments(self, revisions=None):
1902 """
1902 """
1903 Returns comments for this repository grouped by revisions
1903 Returns comments for this repository grouped by revisions
1904
1904
1905 :param revisions: filter query by revisions only
1905 :param revisions: filter query by revisions only
1906 """
1906 """
1907 cmts = ChangesetComment.query()\
1907 cmts = ChangesetComment.query()\
1908 .filter(ChangesetComment.repo == self)
1908 .filter(ChangesetComment.repo == self)
1909 if revisions:
1909 if revisions:
1910 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1910 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1911 grouped = collections.defaultdict(list)
1911 grouped = collections.defaultdict(list)
1912 for cmt in cmts.all():
1912 for cmt in cmts.all():
1913 grouped[cmt.revision].append(cmt)
1913 grouped[cmt.revision].append(cmt)
1914 return grouped
1914 return grouped
1915
1915
1916 def statuses(self, revisions=None):
1916 def statuses(self, revisions=None):
1917 """
1917 """
1918 Returns statuses for this repository
1918 Returns statuses for this repository
1919
1919
1920 :param revisions: list of revisions to get statuses for
1920 :param revisions: list of revisions to get statuses for
1921 """
1921 """
1922 statuses = ChangesetStatus.query()\
1922 statuses = ChangesetStatus.query()\
1923 .filter(ChangesetStatus.repo == self)\
1923 .filter(ChangesetStatus.repo == self)\
1924 .filter(ChangesetStatus.version == 0)
1924 .filter(ChangesetStatus.version == 0)
1925
1925
1926 if revisions:
1926 if revisions:
1927 # Try doing the filtering in chunks to avoid hitting limits
1927 # Try doing the filtering in chunks to avoid hitting limits
1928 size = 500
1928 size = 500
1929 status_results = []
1929 status_results = []
1930 for chunk in xrange(0, len(revisions), size):
1930 for chunk in xrange(0, len(revisions), size):
1931 status_results += statuses.filter(
1931 status_results += statuses.filter(
1932 ChangesetStatus.revision.in_(
1932 ChangesetStatus.revision.in_(
1933 revisions[chunk: chunk+size])
1933 revisions[chunk: chunk+size])
1934 ).all()
1934 ).all()
1935 else:
1935 else:
1936 status_results = statuses.all()
1936 status_results = statuses.all()
1937
1937
1938 grouped = {}
1938 grouped = {}
1939
1939
1940 # maybe we have open new pullrequest without a status?
1940 # maybe we have open new pullrequest without a status?
1941 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1941 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1942 status_lbl = ChangesetStatus.get_status_lbl(stat)
1942 status_lbl = ChangesetStatus.get_status_lbl(stat)
1943 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1943 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1944 for rev in pr.revisions:
1944 for rev in pr.revisions:
1945 pr_id = pr.pull_request_id
1945 pr_id = pr.pull_request_id
1946 pr_repo = pr.target_repo.repo_name
1946 pr_repo = pr.target_repo.repo_name
1947 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1947 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1948
1948
1949 for stat in status_results:
1949 for stat in status_results:
1950 pr_id = pr_repo = None
1950 pr_id = pr_repo = None
1951 if stat.pull_request:
1951 if stat.pull_request:
1952 pr_id = stat.pull_request.pull_request_id
1952 pr_id = stat.pull_request.pull_request_id
1953 pr_repo = stat.pull_request.target_repo.repo_name
1953 pr_repo = stat.pull_request.target_repo.repo_name
1954 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1954 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1955 pr_id, pr_repo]
1955 pr_id, pr_repo]
1956 return grouped
1956 return grouped
1957
1957
1958 # ==========================================================================
1958 # ==========================================================================
1959 # SCM CACHE INSTANCE
1959 # SCM CACHE INSTANCE
1960 # ==========================================================================
1960 # ==========================================================================
1961
1961
1962 def scm_instance(self, **kwargs):
1962 def scm_instance(self, **kwargs):
1963 import rhodecode
1963 import rhodecode
1964
1964
1965 # Passing a config will not hit the cache currently only used
1965 # Passing a config will not hit the cache currently only used
1966 # for repo2dbmapper
1966 # for repo2dbmapper
1967 config = kwargs.pop('config', None)
1967 config = kwargs.pop('config', None)
1968 cache = kwargs.pop('cache', None)
1968 cache = kwargs.pop('cache', None)
1969 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1969 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1970 # if cache is NOT defined use default global, else we have a full
1970 # if cache is NOT defined use default global, else we have a full
1971 # control over cache behaviour
1971 # control over cache behaviour
1972 if cache is None and full_cache and not config:
1972 if cache is None and full_cache and not config:
1973 return self._get_instance_cached()
1973 return self._get_instance_cached()
1974 return self._get_instance(cache=bool(cache), config=config)
1974 return self._get_instance(cache=bool(cache), config=config)
1975
1975
1976 def _get_instance_cached(self):
1976 def _get_instance_cached(self):
1977 @cache_region('long_term')
1977 @cache_region('long_term')
1978 def _get_repo(cache_key):
1978 def _get_repo(cache_key):
1979 return self._get_instance()
1979 return self._get_instance()
1980
1980
1981 invalidator_context = CacheKey.repo_context_cache(
1981 invalidator_context = CacheKey.repo_context_cache(
1982 _get_repo, self.repo_name, None, thread_scoped=True)
1982 _get_repo, self.repo_name, None, thread_scoped=True)
1983
1983
1984 with invalidator_context as context:
1984 with invalidator_context as context:
1985 context.invalidate()
1985 context.invalidate()
1986 repo = context.compute()
1986 repo = context.compute()
1987
1987
1988 return repo
1988 return repo
1989
1989
1990 def _get_instance(self, cache=True, config=None):
1990 def _get_instance(self, cache=True, config=None):
1991 config = config or self._config
1991 config = config or self._config
1992 custom_wire = {
1992 custom_wire = {
1993 'cache': cache # controls the vcs.remote cache
1993 'cache': cache # controls the vcs.remote cache
1994 }
1994 }
1995
1995
1996 repo = get_vcs_instance(
1996 repo = get_vcs_instance(
1997 repo_path=safe_str(self.repo_full_path),
1997 repo_path=safe_str(self.repo_full_path),
1998 config=config,
1998 config=config,
1999 with_wire=custom_wire,
1999 with_wire=custom_wire,
2000 create=False)
2000 create=False)
2001
2001
2002 return repo
2002 return repo
2003
2003
2004 def __json__(self):
2004 def __json__(self):
2005 return {'landing_rev': self.landing_rev}
2005 return {'landing_rev': self.landing_rev}
2006
2006
2007 def get_dict(self):
2007 def get_dict(self):
2008
2008
2009 # Since we transformed `repo_name` to a hybrid property, we need to
2009 # Since we transformed `repo_name` to a hybrid property, we need to
2010 # keep compatibility with the code which uses `repo_name` field.
2010 # keep compatibility with the code which uses `repo_name` field.
2011
2011
2012 result = super(Repository, self).get_dict()
2012 result = super(Repository, self).get_dict()
2013 result['repo_name'] = result.pop('_repo_name', None)
2013 result['repo_name'] = result.pop('_repo_name', None)
2014 return result
2014 return result
2015
2015
2016
2016
2017 class RepoGroup(Base, BaseModel):
2017 class RepoGroup(Base, BaseModel):
2018 __tablename__ = 'groups'
2018 __tablename__ = 'groups'
2019 __table_args__ = (
2019 __table_args__ = (
2020 UniqueConstraint('group_name', 'group_parent_id'),
2020 UniqueConstraint('group_name', 'group_parent_id'),
2021 CheckConstraint('group_id != group_parent_id'),
2021 CheckConstraint('group_id != group_parent_id'),
2022 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2022 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2023 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2023 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2024 )
2024 )
2025 __mapper_args__ = {'order_by': 'group_name'}
2025 __mapper_args__ = {'order_by': 'group_name'}
2026
2026
2027 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2027 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2028
2028
2029 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2029 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2030 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2030 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2031 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2031 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2032 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2032 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2033 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2033 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2035 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2035 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2036
2036
2037 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2037 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2038 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2038 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2039 parent_group = relationship('RepoGroup', remote_side=group_id)
2039 parent_group = relationship('RepoGroup', remote_side=group_id)
2040 user = relationship('User')
2040 user = relationship('User')
2041 integrations = relationship('Integration',
2041 integrations = relationship('Integration',
2042 cascade="all, delete, delete-orphan")
2042 cascade="all, delete, delete-orphan")
2043
2043
2044 def __init__(self, group_name='', parent_group=None):
2044 def __init__(self, group_name='', parent_group=None):
2045 self.group_name = group_name
2045 self.group_name = group_name
2046 self.parent_group = parent_group
2046 self.parent_group = parent_group
2047
2047
2048 def __unicode__(self):
2048 def __unicode__(self):
2049 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2049 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2050 self.group_name)
2050 self.group_name)
2051
2051
2052 @classmethod
2052 @classmethod
2053 def _generate_choice(cls, repo_group):
2053 def _generate_choice(cls, repo_group):
2054 from webhelpers.html import literal as _literal
2054 from webhelpers.html import literal as _literal
2055 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2055 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2056 return repo_group.group_id, _name(repo_group.full_path_splitted)
2056 return repo_group.group_id, _name(repo_group.full_path_splitted)
2057
2057
2058 @classmethod
2058 @classmethod
2059 def groups_choices(cls, groups=None, show_empty_group=True):
2059 def groups_choices(cls, groups=None, show_empty_group=True):
2060 if not groups:
2060 if not groups:
2061 groups = cls.query().all()
2061 groups = cls.query().all()
2062
2062
2063 repo_groups = []
2063 repo_groups = []
2064 if show_empty_group:
2064 if show_empty_group:
2065 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2065 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2066
2066
2067 repo_groups.extend([cls._generate_choice(x) for x in groups])
2067 repo_groups.extend([cls._generate_choice(x) for x in groups])
2068
2068
2069 repo_groups = sorted(
2069 repo_groups = sorted(
2070 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2070 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2071 return repo_groups
2071 return repo_groups
2072
2072
2073 @classmethod
2073 @classmethod
2074 def url_sep(cls):
2074 def url_sep(cls):
2075 return URL_SEP
2075 return URL_SEP
2076
2076
2077 @classmethod
2077 @classmethod
2078 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2078 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2079 if case_insensitive:
2079 if case_insensitive:
2080 gr = cls.query().filter(func.lower(cls.group_name)
2080 gr = cls.query().filter(func.lower(cls.group_name)
2081 == func.lower(group_name))
2081 == func.lower(group_name))
2082 else:
2082 else:
2083 gr = cls.query().filter(cls.group_name == group_name)
2083 gr = cls.query().filter(cls.group_name == group_name)
2084 if cache:
2084 if cache:
2085 gr = gr.options(FromCache(
2085 gr = gr.options(FromCache(
2086 "sql_cache_short",
2086 "sql_cache_short",
2087 "get_group_%s" % _hash_key(group_name)))
2087 "get_group_%s" % _hash_key(group_name)))
2088 return gr.scalar()
2088 return gr.scalar()
2089
2089
2090 @classmethod
2090 @classmethod
2091 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2091 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2092 case_insensitive=True):
2092 case_insensitive=True):
2093 q = RepoGroup.query()
2093 q = RepoGroup.query()
2094
2094
2095 if not isinstance(user_id, Optional):
2095 if not isinstance(user_id, Optional):
2096 q = q.filter(RepoGroup.user_id == user_id)
2096 q = q.filter(RepoGroup.user_id == user_id)
2097
2097
2098 if not isinstance(group_id, Optional):
2098 if not isinstance(group_id, Optional):
2099 q = q.filter(RepoGroup.group_parent_id == group_id)
2099 q = q.filter(RepoGroup.group_parent_id == group_id)
2100
2100
2101 if case_insensitive:
2101 if case_insensitive:
2102 q = q.order_by(func.lower(RepoGroup.group_name))
2102 q = q.order_by(func.lower(RepoGroup.group_name))
2103 else:
2103 else:
2104 q = q.order_by(RepoGroup.group_name)
2104 q = q.order_by(RepoGroup.group_name)
2105 return q.all()
2105 return q.all()
2106
2106
2107 @property
2107 @property
2108 def parents(self):
2108 def parents(self):
2109 parents_recursion_limit = 10
2109 parents_recursion_limit = 10
2110 groups = []
2110 groups = []
2111 if self.parent_group is None:
2111 if self.parent_group is None:
2112 return groups
2112 return groups
2113 cur_gr = self.parent_group
2113 cur_gr = self.parent_group
2114 groups.insert(0, cur_gr)
2114 groups.insert(0, cur_gr)
2115 cnt = 0
2115 cnt = 0
2116 while 1:
2116 while 1:
2117 cnt += 1
2117 cnt += 1
2118 gr = getattr(cur_gr, 'parent_group', None)
2118 gr = getattr(cur_gr, 'parent_group', None)
2119 cur_gr = cur_gr.parent_group
2119 cur_gr = cur_gr.parent_group
2120 if gr is None:
2120 if gr is None:
2121 break
2121 break
2122 if cnt == parents_recursion_limit:
2122 if cnt == parents_recursion_limit:
2123 # this will prevent accidental infinit loops
2123 # this will prevent accidental infinit loops
2124 log.error(('more than %s parents found for group %s, stopping '
2124 log.error(('more than %s parents found for group %s, stopping '
2125 'recursive parent fetching' % (parents_recursion_limit, self)))
2125 'recursive parent fetching' % (parents_recursion_limit, self)))
2126 break
2126 break
2127
2127
2128 groups.insert(0, gr)
2128 groups.insert(0, gr)
2129 return groups
2129 return groups
2130
2130
2131 @property
2131 @property
2132 def children(self):
2132 def children(self):
2133 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2133 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2134
2134
2135 @property
2135 @property
2136 def name(self):
2136 def name(self):
2137 return self.group_name.split(RepoGroup.url_sep())[-1]
2137 return self.group_name.split(RepoGroup.url_sep())[-1]
2138
2138
2139 @property
2139 @property
2140 def full_path(self):
2140 def full_path(self):
2141 return self.group_name
2141 return self.group_name
2142
2142
2143 @property
2143 @property
2144 def full_path_splitted(self):
2144 def full_path_splitted(self):
2145 return self.group_name.split(RepoGroup.url_sep())
2145 return self.group_name.split(RepoGroup.url_sep())
2146
2146
2147 @property
2147 @property
2148 def repositories(self):
2148 def repositories(self):
2149 return Repository.query()\
2149 return Repository.query()\
2150 .filter(Repository.group == self)\
2150 .filter(Repository.group == self)\
2151 .order_by(Repository.repo_name)
2151 .order_by(Repository.repo_name)
2152
2152
2153 @property
2153 @property
2154 def repositories_recursive_count(self):
2154 def repositories_recursive_count(self):
2155 cnt = self.repositories.count()
2155 cnt = self.repositories.count()
2156
2156
2157 def children_count(group):
2157 def children_count(group):
2158 cnt = 0
2158 cnt = 0
2159 for child in group.children:
2159 for child in group.children:
2160 cnt += child.repositories.count()
2160 cnt += child.repositories.count()
2161 cnt += children_count(child)
2161 cnt += children_count(child)
2162 return cnt
2162 return cnt
2163
2163
2164 return cnt + children_count(self)
2164 return cnt + children_count(self)
2165
2165
2166 def _recursive_objects(self, include_repos=True):
2166 def _recursive_objects(self, include_repos=True):
2167 all_ = []
2167 all_ = []
2168
2168
2169 def _get_members(root_gr):
2169 def _get_members(root_gr):
2170 if include_repos:
2170 if include_repos:
2171 for r in root_gr.repositories:
2171 for r in root_gr.repositories:
2172 all_.append(r)
2172 all_.append(r)
2173 childs = root_gr.children.all()
2173 childs = root_gr.children.all()
2174 if childs:
2174 if childs:
2175 for gr in childs:
2175 for gr in childs:
2176 all_.append(gr)
2176 all_.append(gr)
2177 _get_members(gr)
2177 _get_members(gr)
2178
2178
2179 _get_members(self)
2179 _get_members(self)
2180 return [self] + all_
2180 return [self] + all_
2181
2181
2182 def recursive_groups_and_repos(self):
2182 def recursive_groups_and_repos(self):
2183 """
2183 """
2184 Recursive return all groups, with repositories in those groups
2184 Recursive return all groups, with repositories in those groups
2185 """
2185 """
2186 return self._recursive_objects()
2186 return self._recursive_objects()
2187
2187
2188 def recursive_groups(self):
2188 def recursive_groups(self):
2189 """
2189 """
2190 Returns all children groups for this group including children of children
2190 Returns all children groups for this group including children of children
2191 """
2191 """
2192 return self._recursive_objects(include_repos=False)
2192 return self._recursive_objects(include_repos=False)
2193
2193
2194 def get_new_name(self, group_name):
2194 def get_new_name(self, group_name):
2195 """
2195 """
2196 returns new full group name based on parent and new name
2196 returns new full group name based on parent and new name
2197
2197
2198 :param group_name:
2198 :param group_name:
2199 """
2199 """
2200 path_prefix = (self.parent_group.full_path_splitted if
2200 path_prefix = (self.parent_group.full_path_splitted if
2201 self.parent_group else [])
2201 self.parent_group else [])
2202 return RepoGroup.url_sep().join(path_prefix + [group_name])
2202 return RepoGroup.url_sep().join(path_prefix + [group_name])
2203
2203
2204 def permissions(self, with_admins=True, with_owner=True):
2204 def permissions(self, with_admins=True, with_owner=True):
2205 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2205 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2206 q = q.options(joinedload(UserRepoGroupToPerm.group),
2206 q = q.options(joinedload(UserRepoGroupToPerm.group),
2207 joinedload(UserRepoGroupToPerm.user),
2207 joinedload(UserRepoGroupToPerm.user),
2208 joinedload(UserRepoGroupToPerm.permission),)
2208 joinedload(UserRepoGroupToPerm.permission),)
2209
2209
2210 # get owners and admins and permissions. We do a trick of re-writing
2210 # get owners and admins and permissions. We do a trick of re-writing
2211 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2211 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2212 # has a global reference and changing one object propagates to all
2212 # has a global reference and changing one object propagates to all
2213 # others. This means if admin is also an owner admin_row that change
2213 # others. This means if admin is also an owner admin_row that change
2214 # would propagate to both objects
2214 # would propagate to both objects
2215 perm_rows = []
2215 perm_rows = []
2216 for _usr in q.all():
2216 for _usr in q.all():
2217 usr = AttributeDict(_usr.user.get_dict())
2217 usr = AttributeDict(_usr.user.get_dict())
2218 usr.permission = _usr.permission.permission_name
2218 usr.permission = _usr.permission.permission_name
2219 perm_rows.append(usr)
2219 perm_rows.append(usr)
2220
2220
2221 # filter the perm rows by 'default' first and then sort them by
2221 # filter the perm rows by 'default' first and then sort them by
2222 # admin,write,read,none permissions sorted again alphabetically in
2222 # admin,write,read,none permissions sorted again alphabetically in
2223 # each group
2223 # each group
2224 perm_rows = sorted(perm_rows, key=display_sort)
2224 perm_rows = sorted(perm_rows, key=display_sort)
2225
2225
2226 _admin_perm = 'group.admin'
2226 _admin_perm = 'group.admin'
2227 owner_row = []
2227 owner_row = []
2228 if with_owner:
2228 if with_owner:
2229 usr = AttributeDict(self.user.get_dict())
2229 usr = AttributeDict(self.user.get_dict())
2230 usr.owner_row = True
2230 usr.owner_row = True
2231 usr.permission = _admin_perm
2231 usr.permission = _admin_perm
2232 owner_row.append(usr)
2232 owner_row.append(usr)
2233
2233
2234 super_admin_rows = []
2234 super_admin_rows = []
2235 if with_admins:
2235 if with_admins:
2236 for usr in User.get_all_super_admins():
2236 for usr in User.get_all_super_admins():
2237 # if this admin is also owner, don't double the record
2237 # if this admin is also owner, don't double the record
2238 if usr.user_id == owner_row[0].user_id:
2238 if usr.user_id == owner_row[0].user_id:
2239 owner_row[0].admin_row = True
2239 owner_row[0].admin_row = True
2240 else:
2240 else:
2241 usr = AttributeDict(usr.get_dict())
2241 usr = AttributeDict(usr.get_dict())
2242 usr.admin_row = True
2242 usr.admin_row = True
2243 usr.permission = _admin_perm
2243 usr.permission = _admin_perm
2244 super_admin_rows.append(usr)
2244 super_admin_rows.append(usr)
2245
2245
2246 return super_admin_rows + owner_row + perm_rows
2246 return super_admin_rows + owner_row + perm_rows
2247
2247
2248 def permission_user_groups(self):
2248 def permission_user_groups(self):
2249 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2249 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2250 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2250 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2251 joinedload(UserGroupRepoGroupToPerm.users_group),
2251 joinedload(UserGroupRepoGroupToPerm.users_group),
2252 joinedload(UserGroupRepoGroupToPerm.permission),)
2252 joinedload(UserGroupRepoGroupToPerm.permission),)
2253
2253
2254 perm_rows = []
2254 perm_rows = []
2255 for _user_group in q.all():
2255 for _user_group in q.all():
2256 usr = AttributeDict(_user_group.users_group.get_dict())
2256 usr = AttributeDict(_user_group.users_group.get_dict())
2257 usr.permission = _user_group.permission.permission_name
2257 usr.permission = _user_group.permission.permission_name
2258 perm_rows.append(usr)
2258 perm_rows.append(usr)
2259
2259
2260 return perm_rows
2260 return perm_rows
2261
2261
2262 def get_api_data(self):
2262 def get_api_data(self):
2263 """
2263 """
2264 Common function for generating api data
2264 Common function for generating api data
2265
2265
2266 """
2266 """
2267 group = self
2267 group = self
2268 data = {
2268 data = {
2269 'group_id': group.group_id,
2269 'group_id': group.group_id,
2270 'group_name': group.group_name,
2270 'group_name': group.group_name,
2271 'group_description': group.group_description,
2271 'group_description': group.group_description,
2272 'parent_group': group.parent_group.group_name if group.parent_group else None,
2272 'parent_group': group.parent_group.group_name if group.parent_group else None,
2273 'repositories': [x.repo_name for x in group.repositories],
2273 'repositories': [x.repo_name for x in group.repositories],
2274 'owner': group.user.username,
2274 'owner': group.user.username,
2275 }
2275 }
2276 return data
2276 return data
2277
2277
2278
2278
2279 class Permission(Base, BaseModel):
2279 class Permission(Base, BaseModel):
2280 __tablename__ = 'permissions'
2280 __tablename__ = 'permissions'
2281 __table_args__ = (
2281 __table_args__ = (
2282 Index('p_perm_name_idx', 'permission_name'),
2282 Index('p_perm_name_idx', 'permission_name'),
2283 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2283 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2284 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2284 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2285 )
2285 )
2286 PERMS = [
2286 PERMS = [
2287 ('hg.admin', _('RhodeCode Super Administrator')),
2287 ('hg.admin', _('RhodeCode Super Administrator')),
2288
2288
2289 ('repository.none', _('Repository no access')),
2289 ('repository.none', _('Repository no access')),
2290 ('repository.read', _('Repository read access')),
2290 ('repository.read', _('Repository read access')),
2291 ('repository.write', _('Repository write access')),
2291 ('repository.write', _('Repository write access')),
2292 ('repository.admin', _('Repository admin access')),
2292 ('repository.admin', _('Repository admin access')),
2293
2293
2294 ('group.none', _('Repository group no access')),
2294 ('group.none', _('Repository group no access')),
2295 ('group.read', _('Repository group read access')),
2295 ('group.read', _('Repository group read access')),
2296 ('group.write', _('Repository group write access')),
2296 ('group.write', _('Repository group write access')),
2297 ('group.admin', _('Repository group admin access')),
2297 ('group.admin', _('Repository group admin access')),
2298
2298
2299 ('usergroup.none', _('User group no access')),
2299 ('usergroup.none', _('User group no access')),
2300 ('usergroup.read', _('User group read access')),
2300 ('usergroup.read', _('User group read access')),
2301 ('usergroup.write', _('User group write access')),
2301 ('usergroup.write', _('User group write access')),
2302 ('usergroup.admin', _('User group admin access')),
2302 ('usergroup.admin', _('User group admin access')),
2303
2303
2304 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2304 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2305 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2305 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2306
2306
2307 ('hg.usergroup.create.false', _('User Group creation disabled')),
2307 ('hg.usergroup.create.false', _('User Group creation disabled')),
2308 ('hg.usergroup.create.true', _('User Group creation enabled')),
2308 ('hg.usergroup.create.true', _('User Group creation enabled')),
2309
2309
2310 ('hg.create.none', _('Repository creation disabled')),
2310 ('hg.create.none', _('Repository creation disabled')),
2311 ('hg.create.repository', _('Repository creation enabled')),
2311 ('hg.create.repository', _('Repository creation enabled')),
2312 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2312 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2313 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2313 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2314
2314
2315 ('hg.fork.none', _('Repository forking disabled')),
2315 ('hg.fork.none', _('Repository forking disabled')),
2316 ('hg.fork.repository', _('Repository forking enabled')),
2316 ('hg.fork.repository', _('Repository forking enabled')),
2317
2317
2318 ('hg.register.none', _('Registration disabled')),
2318 ('hg.register.none', _('Registration disabled')),
2319 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2319 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2320 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2320 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2321
2321
2322 ('hg.extern_activate.manual', _('Manual activation of external account')),
2322 ('hg.extern_activate.manual', _('Manual activation of external account')),
2323 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2323 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2324
2324
2325 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2325 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2326 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2326 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2327 ]
2327 ]
2328
2328
2329 # definition of system default permissions for DEFAULT user
2329 # definition of system default permissions for DEFAULT user
2330 DEFAULT_USER_PERMISSIONS = [
2330 DEFAULT_USER_PERMISSIONS = [
2331 'repository.read',
2331 'repository.read',
2332 'group.read',
2332 'group.read',
2333 'usergroup.read',
2333 'usergroup.read',
2334 'hg.create.repository',
2334 'hg.create.repository',
2335 'hg.repogroup.create.false',
2335 'hg.repogroup.create.false',
2336 'hg.usergroup.create.false',
2336 'hg.usergroup.create.false',
2337 'hg.create.write_on_repogroup.true',
2337 'hg.create.write_on_repogroup.true',
2338 'hg.fork.repository',
2338 'hg.fork.repository',
2339 'hg.register.manual_activate',
2339 'hg.register.manual_activate',
2340 'hg.extern_activate.auto',
2340 'hg.extern_activate.auto',
2341 'hg.inherit_default_perms.true',
2341 'hg.inherit_default_perms.true',
2342 ]
2342 ]
2343
2343
2344 # defines which permissions are more important higher the more important
2344 # defines which permissions are more important higher the more important
2345 # Weight defines which permissions are more important.
2345 # Weight defines which permissions are more important.
2346 # The higher number the more important.
2346 # The higher number the more important.
2347 PERM_WEIGHTS = {
2347 PERM_WEIGHTS = {
2348 'repository.none': 0,
2348 'repository.none': 0,
2349 'repository.read': 1,
2349 'repository.read': 1,
2350 'repository.write': 3,
2350 'repository.write': 3,
2351 'repository.admin': 4,
2351 'repository.admin': 4,
2352
2352
2353 'group.none': 0,
2353 'group.none': 0,
2354 'group.read': 1,
2354 'group.read': 1,
2355 'group.write': 3,
2355 'group.write': 3,
2356 'group.admin': 4,
2356 'group.admin': 4,
2357
2357
2358 'usergroup.none': 0,
2358 'usergroup.none': 0,
2359 'usergroup.read': 1,
2359 'usergroup.read': 1,
2360 'usergroup.write': 3,
2360 'usergroup.write': 3,
2361 'usergroup.admin': 4,
2361 'usergroup.admin': 4,
2362
2362
2363 'hg.repogroup.create.false': 0,
2363 'hg.repogroup.create.false': 0,
2364 'hg.repogroup.create.true': 1,
2364 'hg.repogroup.create.true': 1,
2365
2365
2366 'hg.usergroup.create.false': 0,
2366 'hg.usergroup.create.false': 0,
2367 'hg.usergroup.create.true': 1,
2367 'hg.usergroup.create.true': 1,
2368
2368
2369 'hg.fork.none': 0,
2369 'hg.fork.none': 0,
2370 'hg.fork.repository': 1,
2370 'hg.fork.repository': 1,
2371 'hg.create.none': 0,
2371 'hg.create.none': 0,
2372 'hg.create.repository': 1
2372 'hg.create.repository': 1
2373 }
2373 }
2374
2374
2375 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2375 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2376 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2376 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2377 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2377 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2378
2378
2379 def __unicode__(self):
2379 def __unicode__(self):
2380 return u"<%s('%s:%s')>" % (
2380 return u"<%s('%s:%s')>" % (
2381 self.__class__.__name__, self.permission_id, self.permission_name
2381 self.__class__.__name__, self.permission_id, self.permission_name
2382 )
2382 )
2383
2383
2384 @classmethod
2384 @classmethod
2385 def get_by_key(cls, key):
2385 def get_by_key(cls, key):
2386 return cls.query().filter(cls.permission_name == key).scalar()
2386 return cls.query().filter(cls.permission_name == key).scalar()
2387
2387
2388 @classmethod
2388 @classmethod
2389 def get_default_repo_perms(cls, user_id, repo_id=None):
2389 def get_default_repo_perms(cls, user_id, repo_id=None):
2390 q = Session().query(UserRepoToPerm, Repository, Permission)\
2390 q = Session().query(UserRepoToPerm, Repository, Permission)\
2391 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2391 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2392 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2392 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2393 .filter(UserRepoToPerm.user_id == user_id)
2393 .filter(UserRepoToPerm.user_id == user_id)
2394 if repo_id:
2394 if repo_id:
2395 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2395 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2396 return q.all()
2396 return q.all()
2397
2397
2398 @classmethod
2398 @classmethod
2399 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2399 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2400 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2400 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2401 .join(
2401 .join(
2402 Permission,
2402 Permission,
2403 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2403 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2404 .join(
2404 .join(
2405 Repository,
2405 Repository,
2406 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2406 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2407 .join(
2407 .join(
2408 UserGroup,
2408 UserGroup,
2409 UserGroupRepoToPerm.users_group_id ==
2409 UserGroupRepoToPerm.users_group_id ==
2410 UserGroup.users_group_id)\
2410 UserGroup.users_group_id)\
2411 .join(
2411 .join(
2412 UserGroupMember,
2412 UserGroupMember,
2413 UserGroupRepoToPerm.users_group_id ==
2413 UserGroupRepoToPerm.users_group_id ==
2414 UserGroupMember.users_group_id)\
2414 UserGroupMember.users_group_id)\
2415 .filter(
2415 .filter(
2416 UserGroupMember.user_id == user_id,
2416 UserGroupMember.user_id == user_id,
2417 UserGroup.users_group_active == true())
2417 UserGroup.users_group_active == true())
2418 if repo_id:
2418 if repo_id:
2419 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2419 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2420 return q.all()
2420 return q.all()
2421
2421
2422 @classmethod
2422 @classmethod
2423 def get_default_group_perms(cls, user_id, repo_group_id=None):
2423 def get_default_group_perms(cls, user_id, repo_group_id=None):
2424 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2424 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2425 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2425 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2426 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2426 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2427 .filter(UserRepoGroupToPerm.user_id == user_id)
2427 .filter(UserRepoGroupToPerm.user_id == user_id)
2428 if repo_group_id:
2428 if repo_group_id:
2429 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2429 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2430 return q.all()
2430 return q.all()
2431
2431
2432 @classmethod
2432 @classmethod
2433 def get_default_group_perms_from_user_group(
2433 def get_default_group_perms_from_user_group(
2434 cls, user_id, repo_group_id=None):
2434 cls, user_id, repo_group_id=None):
2435 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2435 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2436 .join(
2436 .join(
2437 Permission,
2437 Permission,
2438 UserGroupRepoGroupToPerm.permission_id ==
2438 UserGroupRepoGroupToPerm.permission_id ==
2439 Permission.permission_id)\
2439 Permission.permission_id)\
2440 .join(
2440 .join(
2441 RepoGroup,
2441 RepoGroup,
2442 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2442 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2443 .join(
2443 .join(
2444 UserGroup,
2444 UserGroup,
2445 UserGroupRepoGroupToPerm.users_group_id ==
2445 UserGroupRepoGroupToPerm.users_group_id ==
2446 UserGroup.users_group_id)\
2446 UserGroup.users_group_id)\
2447 .join(
2447 .join(
2448 UserGroupMember,
2448 UserGroupMember,
2449 UserGroupRepoGroupToPerm.users_group_id ==
2449 UserGroupRepoGroupToPerm.users_group_id ==
2450 UserGroupMember.users_group_id)\
2450 UserGroupMember.users_group_id)\
2451 .filter(
2451 .filter(
2452 UserGroupMember.user_id == user_id,
2452 UserGroupMember.user_id == user_id,
2453 UserGroup.users_group_active == true())
2453 UserGroup.users_group_active == true())
2454 if repo_group_id:
2454 if repo_group_id:
2455 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2455 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2456 return q.all()
2456 return q.all()
2457
2457
2458 @classmethod
2458 @classmethod
2459 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2459 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2460 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2460 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2461 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2461 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2462 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2462 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2463 .filter(UserUserGroupToPerm.user_id == user_id)
2463 .filter(UserUserGroupToPerm.user_id == user_id)
2464 if user_group_id:
2464 if user_group_id:
2465 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2465 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2466 return q.all()
2466 return q.all()
2467
2467
2468 @classmethod
2468 @classmethod
2469 def get_default_user_group_perms_from_user_group(
2469 def get_default_user_group_perms_from_user_group(
2470 cls, user_id, user_group_id=None):
2470 cls, user_id, user_group_id=None):
2471 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2471 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2472 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2472 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2473 .join(
2473 .join(
2474 Permission,
2474 Permission,
2475 UserGroupUserGroupToPerm.permission_id ==
2475 UserGroupUserGroupToPerm.permission_id ==
2476 Permission.permission_id)\
2476 Permission.permission_id)\
2477 .join(
2477 .join(
2478 TargetUserGroup,
2478 TargetUserGroup,
2479 UserGroupUserGroupToPerm.target_user_group_id ==
2479 UserGroupUserGroupToPerm.target_user_group_id ==
2480 TargetUserGroup.users_group_id)\
2480 TargetUserGroup.users_group_id)\
2481 .join(
2481 .join(
2482 UserGroup,
2482 UserGroup,
2483 UserGroupUserGroupToPerm.user_group_id ==
2483 UserGroupUserGroupToPerm.user_group_id ==
2484 UserGroup.users_group_id)\
2484 UserGroup.users_group_id)\
2485 .join(
2485 .join(
2486 UserGroupMember,
2486 UserGroupMember,
2487 UserGroupUserGroupToPerm.user_group_id ==
2487 UserGroupUserGroupToPerm.user_group_id ==
2488 UserGroupMember.users_group_id)\
2488 UserGroupMember.users_group_id)\
2489 .filter(
2489 .filter(
2490 UserGroupMember.user_id == user_id,
2490 UserGroupMember.user_id == user_id,
2491 UserGroup.users_group_active == true())
2491 UserGroup.users_group_active == true())
2492 if user_group_id:
2492 if user_group_id:
2493 q = q.filter(
2493 q = q.filter(
2494 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2494 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2495
2495
2496 return q.all()
2496 return q.all()
2497
2497
2498
2498
2499 class UserRepoToPerm(Base, BaseModel):
2499 class UserRepoToPerm(Base, BaseModel):
2500 __tablename__ = 'repo_to_perm'
2500 __tablename__ = 'repo_to_perm'
2501 __table_args__ = (
2501 __table_args__ = (
2502 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2502 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2503 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2503 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2504 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2504 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2505 )
2505 )
2506 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2506 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2507 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2507 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2508 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2508 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2509 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2509 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2510
2510
2511 user = relationship('User')
2511 user = relationship('User')
2512 repository = relationship('Repository')
2512 repository = relationship('Repository')
2513 permission = relationship('Permission')
2513 permission = relationship('Permission')
2514
2514
2515 @classmethod
2515 @classmethod
2516 def create(cls, user, repository, permission):
2516 def create(cls, user, repository, permission):
2517 n = cls()
2517 n = cls()
2518 n.user = user
2518 n.user = user
2519 n.repository = repository
2519 n.repository = repository
2520 n.permission = permission
2520 n.permission = permission
2521 Session().add(n)
2521 Session().add(n)
2522 return n
2522 return n
2523
2523
2524 def __unicode__(self):
2524 def __unicode__(self):
2525 return u'<%s => %s >' % (self.user, self.repository)
2525 return u'<%s => %s >' % (self.user, self.repository)
2526
2526
2527
2527
2528 class UserUserGroupToPerm(Base, BaseModel):
2528 class UserUserGroupToPerm(Base, BaseModel):
2529 __tablename__ = 'user_user_group_to_perm'
2529 __tablename__ = 'user_user_group_to_perm'
2530 __table_args__ = (
2530 __table_args__ = (
2531 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2531 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2532 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2532 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2533 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2533 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2534 )
2534 )
2535 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2535 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2536 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2536 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2537 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2537 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2538 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2538 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2539
2539
2540 user = relationship('User')
2540 user = relationship('User')
2541 user_group = relationship('UserGroup')
2541 user_group = relationship('UserGroup')
2542 permission = relationship('Permission')
2542 permission = relationship('Permission')
2543
2543
2544 @classmethod
2544 @classmethod
2545 def create(cls, user, user_group, permission):
2545 def create(cls, user, user_group, permission):
2546 n = cls()
2546 n = cls()
2547 n.user = user
2547 n.user = user
2548 n.user_group = user_group
2548 n.user_group = user_group
2549 n.permission = permission
2549 n.permission = permission
2550 Session().add(n)
2550 Session().add(n)
2551 return n
2551 return n
2552
2552
2553 def __unicode__(self):
2553 def __unicode__(self):
2554 return u'<%s => %s >' % (self.user, self.user_group)
2554 return u'<%s => %s >' % (self.user, self.user_group)
2555
2555
2556
2556
2557 class UserToPerm(Base, BaseModel):
2557 class UserToPerm(Base, BaseModel):
2558 __tablename__ = 'user_to_perm'
2558 __tablename__ = 'user_to_perm'
2559 __table_args__ = (
2559 __table_args__ = (
2560 UniqueConstraint('user_id', 'permission_id'),
2560 UniqueConstraint('user_id', 'permission_id'),
2561 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2561 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2562 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2562 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2563 )
2563 )
2564 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2564 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2565 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2565 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2566 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2566 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2567
2567
2568 user = relationship('User')
2568 user = relationship('User')
2569 permission = relationship('Permission', lazy='joined')
2569 permission = relationship('Permission', lazy='joined')
2570
2570
2571 def __unicode__(self):
2571 def __unicode__(self):
2572 return u'<%s => %s >' % (self.user, self.permission)
2572 return u'<%s => %s >' % (self.user, self.permission)
2573
2573
2574
2574
2575 class UserGroupRepoToPerm(Base, BaseModel):
2575 class UserGroupRepoToPerm(Base, BaseModel):
2576 __tablename__ = 'users_group_repo_to_perm'
2576 __tablename__ = 'users_group_repo_to_perm'
2577 __table_args__ = (
2577 __table_args__ = (
2578 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2578 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2579 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2579 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2580 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2580 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2581 )
2581 )
2582 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2582 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2583 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2583 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2584 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2584 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2585 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2585 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2586
2586
2587 users_group = relationship('UserGroup')
2587 users_group = relationship('UserGroup')
2588 permission = relationship('Permission')
2588 permission = relationship('Permission')
2589 repository = relationship('Repository')
2589 repository = relationship('Repository')
2590
2590
2591 @classmethod
2591 @classmethod
2592 def create(cls, users_group, repository, permission):
2592 def create(cls, users_group, repository, permission):
2593 n = cls()
2593 n = cls()
2594 n.users_group = users_group
2594 n.users_group = users_group
2595 n.repository = repository
2595 n.repository = repository
2596 n.permission = permission
2596 n.permission = permission
2597 Session().add(n)
2597 Session().add(n)
2598 return n
2598 return n
2599
2599
2600 def __unicode__(self):
2600 def __unicode__(self):
2601 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2601 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2602
2602
2603
2603
2604 class UserGroupUserGroupToPerm(Base, BaseModel):
2604 class UserGroupUserGroupToPerm(Base, BaseModel):
2605 __tablename__ = 'user_group_user_group_to_perm'
2605 __tablename__ = 'user_group_user_group_to_perm'
2606 __table_args__ = (
2606 __table_args__ = (
2607 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2607 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2608 CheckConstraint('target_user_group_id != user_group_id'),
2608 CheckConstraint('target_user_group_id != user_group_id'),
2609 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2609 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2610 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2610 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2611 )
2611 )
2612 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2612 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2613 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2613 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2614 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2614 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2615 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2615 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2616
2616
2617 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2617 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2618 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2618 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2619 permission = relationship('Permission')
2619 permission = relationship('Permission')
2620
2620
2621 @classmethod
2621 @classmethod
2622 def create(cls, target_user_group, user_group, permission):
2622 def create(cls, target_user_group, user_group, permission):
2623 n = cls()
2623 n = cls()
2624 n.target_user_group = target_user_group
2624 n.target_user_group = target_user_group
2625 n.user_group = user_group
2625 n.user_group = user_group
2626 n.permission = permission
2626 n.permission = permission
2627 Session().add(n)
2627 Session().add(n)
2628 return n
2628 return n
2629
2629
2630 def __unicode__(self):
2630 def __unicode__(self):
2631 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2631 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2632
2632
2633
2633
2634 class UserGroupToPerm(Base, BaseModel):
2634 class UserGroupToPerm(Base, BaseModel):
2635 __tablename__ = 'users_group_to_perm'
2635 __tablename__ = 'users_group_to_perm'
2636 __table_args__ = (
2636 __table_args__ = (
2637 UniqueConstraint('users_group_id', 'permission_id',),
2637 UniqueConstraint('users_group_id', 'permission_id',),
2638 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2638 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2639 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2639 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2640 )
2640 )
2641 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2641 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2642 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2642 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2643 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2643 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2644
2644
2645 users_group = relationship('UserGroup')
2645 users_group = relationship('UserGroup')
2646 permission = relationship('Permission')
2646 permission = relationship('Permission')
2647
2647
2648
2648
2649 class UserRepoGroupToPerm(Base, BaseModel):
2649 class UserRepoGroupToPerm(Base, BaseModel):
2650 __tablename__ = 'user_repo_group_to_perm'
2650 __tablename__ = 'user_repo_group_to_perm'
2651 __table_args__ = (
2651 __table_args__ = (
2652 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2652 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2653 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2653 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2654 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2654 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2655 )
2655 )
2656
2656
2657 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2657 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2658 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2658 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2659 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2659 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2660 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2660 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2661
2661
2662 user = relationship('User')
2662 user = relationship('User')
2663 group = relationship('RepoGroup')
2663 group = relationship('RepoGroup')
2664 permission = relationship('Permission')
2664 permission = relationship('Permission')
2665
2665
2666 @classmethod
2666 @classmethod
2667 def create(cls, user, repository_group, permission):
2667 def create(cls, user, repository_group, permission):
2668 n = cls()
2668 n = cls()
2669 n.user = user
2669 n.user = user
2670 n.group = repository_group
2670 n.group = repository_group
2671 n.permission = permission
2671 n.permission = permission
2672 Session().add(n)
2672 Session().add(n)
2673 return n
2673 return n
2674
2674
2675
2675
2676 class UserGroupRepoGroupToPerm(Base, BaseModel):
2676 class UserGroupRepoGroupToPerm(Base, BaseModel):
2677 __tablename__ = 'users_group_repo_group_to_perm'
2677 __tablename__ = 'users_group_repo_group_to_perm'
2678 __table_args__ = (
2678 __table_args__ = (
2679 UniqueConstraint('users_group_id', 'group_id'),
2679 UniqueConstraint('users_group_id', 'group_id'),
2680 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2680 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2681 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2681 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2682 )
2682 )
2683
2683
2684 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2684 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2685 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2685 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2686 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2686 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2687 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2687 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2688
2688
2689 users_group = relationship('UserGroup')
2689 users_group = relationship('UserGroup')
2690 permission = relationship('Permission')
2690 permission = relationship('Permission')
2691 group = relationship('RepoGroup')
2691 group = relationship('RepoGroup')
2692
2692
2693 @classmethod
2693 @classmethod
2694 def create(cls, user_group, repository_group, permission):
2694 def create(cls, user_group, repository_group, permission):
2695 n = cls()
2695 n = cls()
2696 n.users_group = user_group
2696 n.users_group = user_group
2697 n.group = repository_group
2697 n.group = repository_group
2698 n.permission = permission
2698 n.permission = permission
2699 Session().add(n)
2699 Session().add(n)
2700 return n
2700 return n
2701
2701
2702 def __unicode__(self):
2702 def __unicode__(self):
2703 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2703 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2704
2704
2705
2705
2706 class Statistics(Base, BaseModel):
2706 class Statistics(Base, BaseModel):
2707 __tablename__ = 'statistics'
2707 __tablename__ = 'statistics'
2708 __table_args__ = (
2708 __table_args__ = (
2709 UniqueConstraint('repository_id'),
2709 UniqueConstraint('repository_id'),
2710 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2710 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2711 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2711 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2712 )
2712 )
2713 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2713 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2714 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2714 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2715 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2715 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2716 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2716 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2717 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2717 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2718 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2718 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2719
2719
2720 repository = relationship('Repository', single_parent=True)
2720 repository = relationship('Repository', single_parent=True)
2721
2721
2722
2722
2723 class UserFollowing(Base, BaseModel):
2723 class UserFollowing(Base, BaseModel):
2724 __tablename__ = 'user_followings'
2724 __tablename__ = 'user_followings'
2725 __table_args__ = (
2725 __table_args__ = (
2726 UniqueConstraint('user_id', 'follows_repository_id'),
2726 UniqueConstraint('user_id', 'follows_repository_id'),
2727 UniqueConstraint('user_id', 'follows_user_id'),
2727 UniqueConstraint('user_id', 'follows_user_id'),
2728 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2728 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2729 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2729 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2730 )
2730 )
2731
2731
2732 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2732 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2733 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2733 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2734 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2734 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2735 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2735 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2736 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2736 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2737
2737
2738 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2738 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2739
2739
2740 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2740 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2741 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2741 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2742
2742
2743 @classmethod
2743 @classmethod
2744 def get_repo_followers(cls, repo_id):
2744 def get_repo_followers(cls, repo_id):
2745 return cls.query().filter(cls.follows_repo_id == repo_id)
2745 return cls.query().filter(cls.follows_repo_id == repo_id)
2746
2746
2747
2747
2748 class CacheKey(Base, BaseModel):
2748 class CacheKey(Base, BaseModel):
2749 __tablename__ = 'cache_invalidation'
2749 __tablename__ = 'cache_invalidation'
2750 __table_args__ = (
2750 __table_args__ = (
2751 UniqueConstraint('cache_key'),
2751 UniqueConstraint('cache_key'),
2752 Index('key_idx', 'cache_key'),
2752 Index('key_idx', 'cache_key'),
2753 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2753 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2754 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2754 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2755 )
2755 )
2756 CACHE_TYPE_ATOM = 'ATOM'
2756 CACHE_TYPE_ATOM = 'ATOM'
2757 CACHE_TYPE_RSS = 'RSS'
2757 CACHE_TYPE_RSS = 'RSS'
2758 CACHE_TYPE_README = 'README'
2758 CACHE_TYPE_README = 'README'
2759
2759
2760 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2760 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2761 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2761 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2762 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2762 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2763 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2763 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2764
2764
2765 def __init__(self, cache_key, cache_args=''):
2765 def __init__(self, cache_key, cache_args=''):
2766 self.cache_key = cache_key
2766 self.cache_key = cache_key
2767 self.cache_args = cache_args
2767 self.cache_args = cache_args
2768 self.cache_active = False
2768 self.cache_active = False
2769
2769
2770 def __unicode__(self):
2770 def __unicode__(self):
2771 return u"<%s('%s:%s[%s]')>" % (
2771 return u"<%s('%s:%s[%s]')>" % (
2772 self.__class__.__name__,
2772 self.__class__.__name__,
2773 self.cache_id, self.cache_key, self.cache_active)
2773 self.cache_id, self.cache_key, self.cache_active)
2774
2774
2775 def _cache_key_partition(self):
2775 def _cache_key_partition(self):
2776 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2776 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2777 return prefix, repo_name, suffix
2777 return prefix, repo_name, suffix
2778
2778
2779 def get_prefix(self):
2779 def get_prefix(self):
2780 """
2780 """
2781 Try to extract prefix from existing cache key. The key could consist
2781 Try to extract prefix from existing cache key. The key could consist
2782 of prefix, repo_name, suffix
2782 of prefix, repo_name, suffix
2783 """
2783 """
2784 # this returns prefix, repo_name, suffix
2784 # this returns prefix, repo_name, suffix
2785 return self._cache_key_partition()[0]
2785 return self._cache_key_partition()[0]
2786
2786
2787 def get_suffix(self):
2787 def get_suffix(self):
2788 """
2788 """
2789 get suffix that might have been used in _get_cache_key to
2789 get suffix that might have been used in _get_cache_key to
2790 generate self.cache_key. Only used for informational purposes
2790 generate self.cache_key. Only used for informational purposes
2791 in repo_edit.html.
2791 in repo_edit.html.
2792 """
2792 """
2793 # prefix, repo_name, suffix
2793 # prefix, repo_name, suffix
2794 return self._cache_key_partition()[2]
2794 return self._cache_key_partition()[2]
2795
2795
2796 @classmethod
2796 @classmethod
2797 def delete_all_cache(cls):
2797 def delete_all_cache(cls):
2798 """
2798 """
2799 Delete all cache keys from database.
2799 Delete all cache keys from database.
2800 Should only be run when all instances are down and all entries
2800 Should only be run when all instances are down and all entries
2801 thus stale.
2801 thus stale.
2802 """
2802 """
2803 cls.query().delete()
2803 cls.query().delete()
2804 Session().commit()
2804 Session().commit()
2805
2805
2806 @classmethod
2806 @classmethod
2807 def get_cache_key(cls, repo_name, cache_type):
2807 def get_cache_key(cls, repo_name, cache_type):
2808 """
2808 """
2809
2809
2810 Generate a cache key for this process of RhodeCode instance.
2810 Generate a cache key for this process of RhodeCode instance.
2811 Prefix most likely will be process id or maybe explicitly set
2811 Prefix most likely will be process id or maybe explicitly set
2812 instance_id from .ini file.
2812 instance_id from .ini file.
2813 """
2813 """
2814 import rhodecode
2814 import rhodecode
2815 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2815 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2816
2816
2817 repo_as_unicode = safe_unicode(repo_name)
2817 repo_as_unicode = safe_unicode(repo_name)
2818 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2818 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2819 if cache_type else repo_as_unicode
2819 if cache_type else repo_as_unicode
2820
2820
2821 return u'{}{}'.format(prefix, key)
2821 return u'{}{}'.format(prefix, key)
2822
2822
2823 @classmethod
2823 @classmethod
2824 def set_invalidate(cls, repo_name, delete=False):
2824 def set_invalidate(cls, repo_name, delete=False):
2825 """
2825 """
2826 Mark all caches of a repo as invalid in the database.
2826 Mark all caches of a repo as invalid in the database.
2827 """
2827 """
2828
2828
2829 try:
2829 try:
2830 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2830 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2831 if delete:
2831 if delete:
2832 log.debug('cache objects deleted for repo %s',
2832 log.debug('cache objects deleted for repo %s',
2833 safe_str(repo_name))
2833 safe_str(repo_name))
2834 qry.delete()
2834 qry.delete()
2835 else:
2835 else:
2836 log.debug('cache objects marked as invalid for repo %s',
2836 log.debug('cache objects marked as invalid for repo %s',
2837 safe_str(repo_name))
2837 safe_str(repo_name))
2838 qry.update({"cache_active": False})
2838 qry.update({"cache_active": False})
2839
2839
2840 Session().commit()
2840 Session().commit()
2841 except Exception:
2841 except Exception:
2842 log.exception(
2842 log.exception(
2843 'Cache key invalidation failed for repository %s',
2843 'Cache key invalidation failed for repository %s',
2844 safe_str(repo_name))
2844 safe_str(repo_name))
2845 Session().rollback()
2845 Session().rollback()
2846
2846
2847 @classmethod
2847 @classmethod
2848 def get_active_cache(cls, cache_key):
2848 def get_active_cache(cls, cache_key):
2849 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2849 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2850 if inv_obj:
2850 if inv_obj:
2851 return inv_obj
2851 return inv_obj
2852 return None
2852 return None
2853
2853
2854 @classmethod
2854 @classmethod
2855 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2855 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2856 thread_scoped=False):
2856 thread_scoped=False):
2857 """
2857 """
2858 @cache_region('long_term')
2858 @cache_region('long_term')
2859 def _heavy_calculation(cache_key):
2859 def _heavy_calculation(cache_key):
2860 return 'result'
2860 return 'result'
2861
2861
2862 cache_context = CacheKey.repo_context_cache(
2862 cache_context = CacheKey.repo_context_cache(
2863 _heavy_calculation, repo_name, cache_type)
2863 _heavy_calculation, repo_name, cache_type)
2864
2864
2865 with cache_context as context:
2865 with cache_context as context:
2866 context.invalidate()
2866 context.invalidate()
2867 computed = context.compute()
2867 computed = context.compute()
2868
2868
2869 assert computed == 'result'
2869 assert computed == 'result'
2870 """
2870 """
2871 from rhodecode.lib import caches
2871 from rhodecode.lib import caches
2872 return caches.InvalidationContext(
2872 return caches.InvalidationContext(
2873 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2873 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2874
2874
2875
2875
2876 class ChangesetComment(Base, BaseModel):
2876 class ChangesetComment(Base, BaseModel):
2877 __tablename__ = 'changeset_comments'
2877 __tablename__ = 'changeset_comments'
2878 __table_args__ = (
2878 __table_args__ = (
2879 Index('cc_revision_idx', 'revision'),
2879 Index('cc_revision_idx', 'revision'),
2880 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2880 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2881 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2881 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2882 )
2882 )
2883
2883
2884 COMMENT_OUTDATED = u'comment_outdated'
2884 COMMENT_OUTDATED = u'comment_outdated'
2885
2885
2886 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2886 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2887 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2887 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2888 revision = Column('revision', String(40), nullable=True)
2888 revision = Column('revision', String(40), nullable=True)
2889 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2889 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2890 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2890 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2891 line_no = Column('line_no', Unicode(10), nullable=True)
2891 line_no = Column('line_no', Unicode(10), nullable=True)
2892 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2892 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2893 f_path = Column('f_path', Unicode(1000), nullable=True)
2893 f_path = Column('f_path', Unicode(1000), nullable=True)
2894 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2894 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2895 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2895 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2896 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2896 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2897 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2897 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2898 renderer = Column('renderer', Unicode(64), nullable=True)
2898 renderer = Column('renderer', Unicode(64), nullable=True)
2899 display_state = Column('display_state', Unicode(128), nullable=True)
2899 display_state = Column('display_state', Unicode(128), nullable=True)
2900
2900
2901 author = relationship('User', lazy='joined')
2901 author = relationship('User', lazy='joined')
2902 repo = relationship('Repository')
2902 repo = relationship('Repository')
2903 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2903 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2904 pull_request = relationship('PullRequest', lazy='joined')
2904 pull_request = relationship('PullRequest', lazy='joined')
2905 pull_request_version = relationship('PullRequestVersion')
2905 pull_request_version = relationship('PullRequestVersion')
2906
2906
2907 @classmethod
2907 @classmethod
2908 def get_users(cls, revision=None, pull_request_id=None):
2908 def get_users(cls, revision=None, pull_request_id=None):
2909 """
2909 """
2910 Returns user associated with this ChangesetComment. ie those
2910 Returns user associated with this ChangesetComment. ie those
2911 who actually commented
2911 who actually commented
2912
2912
2913 :param cls:
2913 :param cls:
2914 :param revision:
2914 :param revision:
2915 """
2915 """
2916 q = Session().query(User)\
2916 q = Session().query(User)\
2917 .join(ChangesetComment.author)
2917 .join(ChangesetComment.author)
2918 if revision:
2918 if revision:
2919 q = q.filter(cls.revision == revision)
2919 q = q.filter(cls.revision == revision)
2920 elif pull_request_id:
2920 elif pull_request_id:
2921 q = q.filter(cls.pull_request_id == pull_request_id)
2921 q = q.filter(cls.pull_request_id == pull_request_id)
2922 return q.all()
2922 return q.all()
2923
2923
2924 def render(self, mentions=False):
2924 def render(self, mentions=False):
2925 from rhodecode.lib import helpers as h
2925 from rhodecode.lib import helpers as h
2926 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2926 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2927
2927
2928 def __repr__(self):
2928 def __repr__(self):
2929 if self.comment_id:
2929 if self.comment_id:
2930 return '<DB:ChangesetComment #%s>' % self.comment_id
2930 return '<DB:ChangesetComment #%s>' % self.comment_id
2931 else:
2931 else:
2932 return '<DB:ChangesetComment at %#x>' % id(self)
2932 return '<DB:ChangesetComment at %#x>' % id(self)
2933
2933
2934
2934
2935 class ChangesetStatus(Base, BaseModel):
2935 class ChangesetStatus(Base, BaseModel):
2936 __tablename__ = 'changeset_statuses'
2936 __tablename__ = 'changeset_statuses'
2937 __table_args__ = (
2937 __table_args__ = (
2938 Index('cs_revision_idx', 'revision'),
2938 Index('cs_revision_idx', 'revision'),
2939 Index('cs_version_idx', 'version'),
2939 Index('cs_version_idx', 'version'),
2940 UniqueConstraint('repo_id', 'revision', 'version'),
2940 UniqueConstraint('repo_id', 'revision', 'version'),
2941 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2941 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2942 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2942 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2943 )
2943 )
2944 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2944 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2945 STATUS_APPROVED = 'approved'
2945 STATUS_APPROVED = 'approved'
2946 STATUS_REJECTED = 'rejected'
2946 STATUS_REJECTED = 'rejected'
2947 STATUS_UNDER_REVIEW = 'under_review'
2947 STATUS_UNDER_REVIEW = 'under_review'
2948
2948
2949 STATUSES = [
2949 STATUSES = [
2950 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2950 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2951 (STATUS_APPROVED, _("Approved")),
2951 (STATUS_APPROVED, _("Approved")),
2952 (STATUS_REJECTED, _("Rejected")),
2952 (STATUS_REJECTED, _("Rejected")),
2953 (STATUS_UNDER_REVIEW, _("Under Review")),
2953 (STATUS_UNDER_REVIEW, _("Under Review")),
2954 ]
2954 ]
2955
2955
2956 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2956 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2957 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2957 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2958 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2958 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2959 revision = Column('revision', String(40), nullable=False)
2959 revision = Column('revision', String(40), nullable=False)
2960 status = Column('status', String(128), nullable=False, default=DEFAULT)
2960 status = Column('status', String(128), nullable=False, default=DEFAULT)
2961 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2961 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2962 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2962 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2963 version = Column('version', Integer(), nullable=False, default=0)
2963 version = Column('version', Integer(), nullable=False, default=0)
2964 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2964 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2965
2965
2966 author = relationship('User', lazy='joined')
2966 author = relationship('User', lazy='joined')
2967 repo = relationship('Repository')
2967 repo = relationship('Repository')
2968 comment = relationship('ChangesetComment', lazy='joined')
2968 comment = relationship('ChangesetComment', lazy='joined')
2969 pull_request = relationship('PullRequest', lazy='joined')
2969 pull_request = relationship('PullRequest', lazy='joined')
2970
2970
2971 def __unicode__(self):
2971 def __unicode__(self):
2972 return u"<%s('%s[%s]:%s')>" % (
2972 return u"<%s('%s[%s]:%s')>" % (
2973 self.__class__.__name__,
2973 self.__class__.__name__,
2974 self.status, self.version, self.author
2974 self.status, self.version, self.author
2975 )
2975 )
2976
2976
2977 @classmethod
2977 @classmethod
2978 def get_status_lbl(cls, value):
2978 def get_status_lbl(cls, value):
2979 return dict(cls.STATUSES).get(value)
2979 return dict(cls.STATUSES).get(value)
2980
2980
2981 @property
2981 @property
2982 def status_lbl(self):
2982 def status_lbl(self):
2983 return ChangesetStatus.get_status_lbl(self.status)
2983 return ChangesetStatus.get_status_lbl(self.status)
2984
2984
2985
2985
2986 class _PullRequestBase(BaseModel):
2986 class _PullRequestBase(BaseModel):
2987 """
2987 """
2988 Common attributes of pull request and version entries.
2988 Common attributes of pull request and version entries.
2989 """
2989 """
2990
2990
2991 # .status values
2991 # .status values
2992 STATUS_NEW = u'new'
2992 STATUS_NEW = u'new'
2993 STATUS_OPEN = u'open'
2993 STATUS_OPEN = u'open'
2994 STATUS_CLOSED = u'closed'
2994 STATUS_CLOSED = u'closed'
2995
2995
2996 title = Column('title', Unicode(255), nullable=True)
2996 title = Column('title', Unicode(255), nullable=True)
2997 description = Column(
2997 description = Column(
2998 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
2998 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
2999 nullable=True)
2999 nullable=True)
3000 # new/open/closed status of pull request (not approve/reject/etc)
3000 # new/open/closed status of pull request (not approve/reject/etc)
3001 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3001 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3002 created_on = Column(
3002 created_on = Column(
3003 'created_on', DateTime(timezone=False), nullable=False,
3003 'created_on', DateTime(timezone=False), nullable=False,
3004 default=datetime.datetime.now)
3004 default=datetime.datetime.now)
3005 updated_on = Column(
3005 updated_on = Column(
3006 'updated_on', DateTime(timezone=False), nullable=False,
3006 'updated_on', DateTime(timezone=False), nullable=False,
3007 default=datetime.datetime.now)
3007 default=datetime.datetime.now)
3008
3008
3009 @declared_attr
3009 @declared_attr
3010 def user_id(cls):
3010 def user_id(cls):
3011 return Column(
3011 return Column(
3012 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3012 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3013 unique=None)
3013 unique=None)
3014
3014
3015 # 500 revisions max
3015 # 500 revisions max
3016 _revisions = Column(
3016 _revisions = Column(
3017 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3017 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3018
3018
3019 @declared_attr
3019 @declared_attr
3020 def source_repo_id(cls):
3020 def source_repo_id(cls):
3021 # TODO: dan: rename column to source_repo_id
3021 # TODO: dan: rename column to source_repo_id
3022 return Column(
3022 return Column(
3023 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3023 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3024 nullable=False)
3024 nullable=False)
3025
3025
3026 source_ref = Column('org_ref', Unicode(255), nullable=False)
3026 source_ref = Column('org_ref', Unicode(255), nullable=False)
3027
3027
3028 @declared_attr
3028 @declared_attr
3029 def target_repo_id(cls):
3029 def target_repo_id(cls):
3030 # TODO: dan: rename column to target_repo_id
3030 # TODO: dan: rename column to target_repo_id
3031 return Column(
3031 return Column(
3032 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3032 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3033 nullable=False)
3033 nullable=False)
3034
3034
3035 target_ref = Column('other_ref', Unicode(255), nullable=False)
3035 target_ref = Column('other_ref', Unicode(255), nullable=False)
3036
3036
3037 # TODO: dan: rename column to last_merge_source_rev
3037 # TODO: dan: rename column to last_merge_source_rev
3038 _last_merge_source_rev = Column(
3038 _last_merge_source_rev = Column(
3039 'last_merge_org_rev', String(40), nullable=True)
3039 'last_merge_org_rev', String(40), nullable=True)
3040 # TODO: dan: rename column to last_merge_target_rev
3040 # TODO: dan: rename column to last_merge_target_rev
3041 _last_merge_target_rev = Column(
3041 _last_merge_target_rev = Column(
3042 'last_merge_other_rev', String(40), nullable=True)
3042 'last_merge_other_rev', String(40), nullable=True)
3043 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3043 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3044 merge_rev = Column('merge_rev', String(40), nullable=True)
3044 merge_rev = Column('merge_rev', String(40), nullable=True)
3045
3045
3046 @hybrid_property
3046 @hybrid_property
3047 def revisions(self):
3047 def revisions(self):
3048 return self._revisions.split(':') if self._revisions else []
3048 return self._revisions.split(':') if self._revisions else []
3049
3049
3050 @revisions.setter
3050 @revisions.setter
3051 def revisions(self, val):
3051 def revisions(self, val):
3052 self._revisions = ':'.join(val)
3052 self._revisions = ':'.join(val)
3053
3053
3054 @declared_attr
3054 @declared_attr
3055 def author(cls):
3055 def author(cls):
3056 return relationship('User', lazy='joined')
3056 return relationship('User', lazy='joined')
3057
3057
3058 @declared_attr
3058 @declared_attr
3059 def source_repo(cls):
3059 def source_repo(cls):
3060 return relationship(
3060 return relationship(
3061 'Repository',
3061 'Repository',
3062 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3062 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3063
3063
3064 @property
3064 @property
3065 def source_ref_parts(self):
3065 def source_ref_parts(self):
3066 refs = self.source_ref.split(':')
3066 refs = self.source_ref.split(':')
3067 return Reference(refs[0], refs[1], refs[2])
3067 return Reference(refs[0], refs[1], refs[2])
3068
3068
3069 @declared_attr
3069 @declared_attr
3070 def target_repo(cls):
3070 def target_repo(cls):
3071 return relationship(
3071 return relationship(
3072 'Repository',
3072 'Repository',
3073 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3073 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3074
3074
3075 @property
3075 @property
3076 def target_ref_parts(self):
3076 def target_ref_parts(self):
3077 refs = self.target_ref.split(':')
3077 refs = self.target_ref.split(':')
3078 return Reference(refs[0], refs[1], refs[2])
3078 return Reference(refs[0], refs[1], refs[2])
3079
3079
3080
3080
3081 class PullRequest(Base, _PullRequestBase):
3081 class PullRequest(Base, _PullRequestBase):
3082 __tablename__ = 'pull_requests'
3082 __tablename__ = 'pull_requests'
3083 __table_args__ = (
3083 __table_args__ = (
3084 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3084 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3085 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3085 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3086 )
3086 )
3087
3087
3088 pull_request_id = Column(
3088 pull_request_id = Column(
3089 'pull_request_id', Integer(), nullable=False, primary_key=True)
3089 'pull_request_id', Integer(), nullable=False, primary_key=True)
3090
3090
3091 def __repr__(self):
3091 def __repr__(self):
3092 if self.pull_request_id:
3092 if self.pull_request_id:
3093 return '<DB:PullRequest #%s>' % self.pull_request_id
3093 return '<DB:PullRequest #%s>' % self.pull_request_id
3094 else:
3094 else:
3095 return '<DB:PullRequest at %#x>' % id(self)
3095 return '<DB:PullRequest at %#x>' % id(self)
3096
3096
3097 reviewers = relationship('PullRequestReviewers',
3097 reviewers = relationship('PullRequestReviewers',
3098 cascade="all, delete, delete-orphan")
3098 cascade="all, delete, delete-orphan")
3099 statuses = relationship('ChangesetStatus')
3099 statuses = relationship('ChangesetStatus')
3100 comments = relationship('ChangesetComment',
3100 comments = relationship('ChangesetComment',
3101 cascade="all, delete, delete-orphan")
3101 cascade="all, delete, delete-orphan")
3102 versions = relationship('PullRequestVersion',
3102 versions = relationship('PullRequestVersion',
3103 cascade="all, delete, delete-orphan")
3103 cascade="all, delete, delete-orphan")
3104
3104
3105 def is_closed(self):
3105 def is_closed(self):
3106 return self.status == self.STATUS_CLOSED
3106 return self.status == self.STATUS_CLOSED
3107
3107
3108 def get_api_data(self):
3108 def get_api_data(self):
3109 from rhodecode.model.pull_request import PullRequestModel
3109 from rhodecode.model.pull_request import PullRequestModel
3110 pull_request = self
3110 pull_request = self
3111 merge_status = PullRequestModel().merge_status(pull_request)
3111 merge_status = PullRequestModel().merge_status(pull_request)
3112 data = {
3112 data = {
3113 'pull_request_id': pull_request.pull_request_id,
3113 'pull_request_id': pull_request.pull_request_id,
3114 'url': url('pullrequest_show', repo_name=self.target_repo.repo_name,
3114 'url': url('pullrequest_show', repo_name=self.target_repo.repo_name,
3115 pull_request_id=self.pull_request_id,
3115 pull_request_id=self.pull_request_id,
3116 qualified=True),
3116 qualified=True),
3117 'title': pull_request.title,
3117 'title': pull_request.title,
3118 'description': pull_request.description,
3118 'description': pull_request.description,
3119 'status': pull_request.status,
3119 'status': pull_request.status,
3120 'created_on': pull_request.created_on,
3120 'created_on': pull_request.created_on,
3121 'updated_on': pull_request.updated_on,
3121 'updated_on': pull_request.updated_on,
3122 'commit_ids': pull_request.revisions,
3122 'commit_ids': pull_request.revisions,
3123 'review_status': pull_request.calculated_review_status(),
3123 'review_status': pull_request.calculated_review_status(),
3124 'mergeable': {
3124 'mergeable': {
3125 'status': merge_status[0],
3125 'status': merge_status[0],
3126 'message': unicode(merge_status[1]),
3126 'message': unicode(merge_status[1]),
3127 },
3127 },
3128 'source': {
3128 'source': {
3129 'clone_url': pull_request.source_repo.clone_url(),
3129 'clone_url': pull_request.source_repo.clone_url(),
3130 'repository': pull_request.source_repo.repo_name,
3130 'repository': pull_request.source_repo.repo_name,
3131 'reference': {
3131 'reference': {
3132 'name': pull_request.source_ref_parts.name,
3132 'name': pull_request.source_ref_parts.name,
3133 'type': pull_request.source_ref_parts.type,
3133 'type': pull_request.source_ref_parts.type,
3134 'commit_id': pull_request.source_ref_parts.commit_id,
3134 'commit_id': pull_request.source_ref_parts.commit_id,
3135 },
3135 },
3136 },
3136 },
3137 'target': {
3137 'target': {
3138 'clone_url': pull_request.target_repo.clone_url(),
3138 'clone_url': pull_request.target_repo.clone_url(),
3139 'repository': pull_request.target_repo.repo_name,
3139 'repository': pull_request.target_repo.repo_name,
3140 'reference': {
3140 'reference': {
3141 'name': pull_request.target_ref_parts.name,
3141 'name': pull_request.target_ref_parts.name,
3142 'type': pull_request.target_ref_parts.type,
3142 'type': pull_request.target_ref_parts.type,
3143 'commit_id': pull_request.target_ref_parts.commit_id,
3143 'commit_id': pull_request.target_ref_parts.commit_id,
3144 },
3144 },
3145 },
3145 },
3146 'author': pull_request.author.get_api_data(include_secrets=False,
3146 'author': pull_request.author.get_api_data(include_secrets=False,
3147 details='basic'),
3147 details='basic'),
3148 'reviewers': [
3148 'reviewers': [
3149 {
3149 {
3150 'user': reviewer.get_api_data(include_secrets=False,
3150 'user': reviewer.get_api_data(include_secrets=False,
3151 details='basic'),
3151 details='basic'),
3152 'reasons': reasons,
3152 'review_status': st[0][1].status if st else 'not_reviewed',
3153 'review_status': st[0][1].status if st else 'not_reviewed',
3153 }
3154 }
3154 for reviewer, st in pull_request.reviewers_statuses()
3155 for reviewer, reasons, st in pull_request.reviewers_statuses()
3155 ]
3156 ]
3156 }
3157 }
3157
3158
3158 return data
3159 return data
3159
3160
3160 def __json__(self):
3161 def __json__(self):
3161 return {
3162 return {
3162 'revisions': self.revisions,
3163 'revisions': self.revisions,
3163 }
3164 }
3164
3165
3165 def calculated_review_status(self):
3166 def calculated_review_status(self):
3166 # TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html
3167 # TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html
3167 # because it's tricky on how to use ChangesetStatusModel from there
3168 # because it's tricky on how to use ChangesetStatusModel from there
3168 warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning)
3169 warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning)
3169 from rhodecode.model.changeset_status import ChangesetStatusModel
3170 from rhodecode.model.changeset_status import ChangesetStatusModel
3170 return ChangesetStatusModel().calculated_review_status(self)
3171 return ChangesetStatusModel().calculated_review_status(self)
3171
3172
3172 def reviewers_statuses(self):
3173 def reviewers_statuses(self):
3173 warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning)
3174 warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning)
3174 from rhodecode.model.changeset_status import ChangesetStatusModel
3175 from rhodecode.model.changeset_status import ChangesetStatusModel
3175 return ChangesetStatusModel().reviewers_statuses(self)
3176 return ChangesetStatusModel().reviewers_statuses(self)
3176
3177
3177
3178
3178 class PullRequestVersion(Base, _PullRequestBase):
3179 class PullRequestVersion(Base, _PullRequestBase):
3179 __tablename__ = 'pull_request_versions'
3180 __tablename__ = 'pull_request_versions'
3180 __table_args__ = (
3181 __table_args__ = (
3181 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3182 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3182 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3183 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3183 )
3184 )
3184
3185
3185 pull_request_version_id = Column(
3186 pull_request_version_id = Column(
3186 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3187 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3187 pull_request_id = Column(
3188 pull_request_id = Column(
3188 'pull_request_id', Integer(),
3189 'pull_request_id', Integer(),
3189 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3190 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3190 pull_request = relationship('PullRequest')
3191 pull_request = relationship('PullRequest')
3191
3192
3192 def __repr__(self):
3193 def __repr__(self):
3193 if self.pull_request_version_id:
3194 if self.pull_request_version_id:
3194 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3195 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3195 else:
3196 else:
3196 return '<DB:PullRequestVersion at %#x>' % id(self)
3197 return '<DB:PullRequestVersion at %#x>' % id(self)
3197
3198
3198
3199
3199 class PullRequestReviewers(Base, BaseModel):
3200 class PullRequestReviewers(Base, BaseModel):
3200 __tablename__ = 'pull_request_reviewers'
3201 __tablename__ = 'pull_request_reviewers'
3201 __table_args__ = (
3202 __table_args__ = (
3202 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3203 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3203 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3204 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3204 )
3205 )
3205
3206
3206 def __init__(self, user=None, pull_request=None):
3207 def __init__(self, user=None, pull_request=None, reasons=None):
3207 self.user = user
3208 self.user = user
3208 self.pull_request = pull_request
3209 self.pull_request = pull_request
3210 self.reasons = reasons or []
3211
3212 @hybrid_property
3213 def reasons(self):
3214 if not self._reasons:
3215 return []
3216 return self._reasons
3217
3218 @reasons.setter
3219 def reasons(self, val):
3220 val = val or []
3221 if any(not isinstance(x, basestring) for x in val):
3222 raise Exception('invalid reasons type, must be list of strings')
3223 self._reasons = val
3209
3224
3210 pull_requests_reviewers_id = Column(
3225 pull_requests_reviewers_id = Column(
3211 'pull_requests_reviewers_id', Integer(), nullable=False,
3226 'pull_requests_reviewers_id', Integer(), nullable=False,
3212 primary_key=True)
3227 primary_key=True)
3213 pull_request_id = Column(
3228 pull_request_id = Column(
3214 "pull_request_id", Integer(),
3229 "pull_request_id", Integer(),
3215 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3230 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3216 user_id = Column(
3231 user_id = Column(
3217 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3232 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3218 reason = Column('reason',
3233 _reasons = Column(
3219 UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
3234 'reason', MutationList.as_mutable(
3235 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3220
3236
3221 user = relationship('User')
3237 user = relationship('User')
3222 pull_request = relationship('PullRequest')
3238 pull_request = relationship('PullRequest')
3223
3239
3224
3240
3225 class Notification(Base, BaseModel):
3241 class Notification(Base, BaseModel):
3226 __tablename__ = 'notifications'
3242 __tablename__ = 'notifications'
3227 __table_args__ = (
3243 __table_args__ = (
3228 Index('notification_type_idx', 'type'),
3244 Index('notification_type_idx', 'type'),
3229 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3245 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3230 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3246 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3231 )
3247 )
3232
3248
3233 TYPE_CHANGESET_COMMENT = u'cs_comment'
3249 TYPE_CHANGESET_COMMENT = u'cs_comment'
3234 TYPE_MESSAGE = u'message'
3250 TYPE_MESSAGE = u'message'
3235 TYPE_MENTION = u'mention'
3251 TYPE_MENTION = u'mention'
3236 TYPE_REGISTRATION = u'registration'
3252 TYPE_REGISTRATION = u'registration'
3237 TYPE_PULL_REQUEST = u'pull_request'
3253 TYPE_PULL_REQUEST = u'pull_request'
3238 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3254 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3239
3255
3240 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3256 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3241 subject = Column('subject', Unicode(512), nullable=True)
3257 subject = Column('subject', Unicode(512), nullable=True)
3242 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3258 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3243 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3259 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3244 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3260 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3245 type_ = Column('type', Unicode(255))
3261 type_ = Column('type', Unicode(255))
3246
3262
3247 created_by_user = relationship('User')
3263 created_by_user = relationship('User')
3248 notifications_to_users = relationship('UserNotification', lazy='joined',
3264 notifications_to_users = relationship('UserNotification', lazy='joined',
3249 cascade="all, delete, delete-orphan")
3265 cascade="all, delete, delete-orphan")
3250
3266
3251 @property
3267 @property
3252 def recipients(self):
3268 def recipients(self):
3253 return [x.user for x in UserNotification.query()\
3269 return [x.user for x in UserNotification.query()\
3254 .filter(UserNotification.notification == self)\
3270 .filter(UserNotification.notification == self)\
3255 .order_by(UserNotification.user_id.asc()).all()]
3271 .order_by(UserNotification.user_id.asc()).all()]
3256
3272
3257 @classmethod
3273 @classmethod
3258 def create(cls, created_by, subject, body, recipients, type_=None):
3274 def create(cls, created_by, subject, body, recipients, type_=None):
3259 if type_ is None:
3275 if type_ is None:
3260 type_ = Notification.TYPE_MESSAGE
3276 type_ = Notification.TYPE_MESSAGE
3261
3277
3262 notification = cls()
3278 notification = cls()
3263 notification.created_by_user = created_by
3279 notification.created_by_user = created_by
3264 notification.subject = subject
3280 notification.subject = subject
3265 notification.body = body
3281 notification.body = body
3266 notification.type_ = type_
3282 notification.type_ = type_
3267 notification.created_on = datetime.datetime.now()
3283 notification.created_on = datetime.datetime.now()
3268
3284
3269 for u in recipients:
3285 for u in recipients:
3270 assoc = UserNotification()
3286 assoc = UserNotification()
3271 assoc.notification = notification
3287 assoc.notification = notification
3272
3288
3273 # if created_by is inside recipients mark his notification
3289 # if created_by is inside recipients mark his notification
3274 # as read
3290 # as read
3275 if u.user_id == created_by.user_id:
3291 if u.user_id == created_by.user_id:
3276 assoc.read = True
3292 assoc.read = True
3277
3293
3278 u.notifications.append(assoc)
3294 u.notifications.append(assoc)
3279 Session().add(notification)
3295 Session().add(notification)
3280
3296
3281 return notification
3297 return notification
3282
3298
3283 @property
3299 @property
3284 def description(self):
3300 def description(self):
3285 from rhodecode.model.notification import NotificationModel
3301 from rhodecode.model.notification import NotificationModel
3286 return NotificationModel().make_description(self)
3302 return NotificationModel().make_description(self)
3287
3303
3288
3304
3289 class UserNotification(Base, BaseModel):
3305 class UserNotification(Base, BaseModel):
3290 __tablename__ = 'user_to_notification'
3306 __tablename__ = 'user_to_notification'
3291 __table_args__ = (
3307 __table_args__ = (
3292 UniqueConstraint('user_id', 'notification_id'),
3308 UniqueConstraint('user_id', 'notification_id'),
3293 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3309 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3294 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3310 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3295 )
3311 )
3296 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3312 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3297 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3313 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3298 read = Column('read', Boolean, default=False)
3314 read = Column('read', Boolean, default=False)
3299 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3315 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3300
3316
3301 user = relationship('User', lazy="joined")
3317 user = relationship('User', lazy="joined")
3302 notification = relationship('Notification', lazy="joined",
3318 notification = relationship('Notification', lazy="joined",
3303 order_by=lambda: Notification.created_on.desc(),)
3319 order_by=lambda: Notification.created_on.desc(),)
3304
3320
3305 def mark_as_read(self):
3321 def mark_as_read(self):
3306 self.read = True
3322 self.read = True
3307 Session().add(self)
3323 Session().add(self)
3308
3324
3309
3325
3310 class Gist(Base, BaseModel):
3326 class Gist(Base, BaseModel):
3311 __tablename__ = 'gists'
3327 __tablename__ = 'gists'
3312 __table_args__ = (
3328 __table_args__ = (
3313 Index('g_gist_access_id_idx', 'gist_access_id'),
3329 Index('g_gist_access_id_idx', 'gist_access_id'),
3314 Index('g_created_on_idx', 'created_on'),
3330 Index('g_created_on_idx', 'created_on'),
3315 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3331 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3316 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3332 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3317 )
3333 )
3318 GIST_PUBLIC = u'public'
3334 GIST_PUBLIC = u'public'
3319 GIST_PRIVATE = u'private'
3335 GIST_PRIVATE = u'private'
3320 DEFAULT_FILENAME = u'gistfile1.txt'
3336 DEFAULT_FILENAME = u'gistfile1.txt'
3321
3337
3322 ACL_LEVEL_PUBLIC = u'acl_public'
3338 ACL_LEVEL_PUBLIC = u'acl_public'
3323 ACL_LEVEL_PRIVATE = u'acl_private'
3339 ACL_LEVEL_PRIVATE = u'acl_private'
3324
3340
3325 gist_id = Column('gist_id', Integer(), primary_key=True)
3341 gist_id = Column('gist_id', Integer(), primary_key=True)
3326 gist_access_id = Column('gist_access_id', Unicode(250))
3342 gist_access_id = Column('gist_access_id', Unicode(250))
3327 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3343 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3328 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3344 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3329 gist_expires = Column('gist_expires', Float(53), nullable=False)
3345 gist_expires = Column('gist_expires', Float(53), nullable=False)
3330 gist_type = Column('gist_type', Unicode(128), nullable=False)
3346 gist_type = Column('gist_type', Unicode(128), nullable=False)
3331 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3347 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3332 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3348 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3333 acl_level = Column('acl_level', Unicode(128), nullable=True)
3349 acl_level = Column('acl_level', Unicode(128), nullable=True)
3334
3350
3335 owner = relationship('User')
3351 owner = relationship('User')
3336
3352
3337 def __repr__(self):
3353 def __repr__(self):
3338 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3354 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3339
3355
3340 @classmethod
3356 @classmethod
3341 def get_or_404(cls, id_):
3357 def get_or_404(cls, id_):
3342 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3358 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3343 if not res:
3359 if not res:
3344 raise HTTPNotFound
3360 raise HTTPNotFound
3345 return res
3361 return res
3346
3362
3347 @classmethod
3363 @classmethod
3348 def get_by_access_id(cls, gist_access_id):
3364 def get_by_access_id(cls, gist_access_id):
3349 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3365 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3350
3366
3351 def gist_url(self):
3367 def gist_url(self):
3352 import rhodecode
3368 import rhodecode
3353 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3369 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3354 if alias_url:
3370 if alias_url:
3355 return alias_url.replace('{gistid}', self.gist_access_id)
3371 return alias_url.replace('{gistid}', self.gist_access_id)
3356
3372
3357 return url('gist', gist_id=self.gist_access_id, qualified=True)
3373 return url('gist', gist_id=self.gist_access_id, qualified=True)
3358
3374
3359 @classmethod
3375 @classmethod
3360 def base_path(cls):
3376 def base_path(cls):
3361 """
3377 """
3362 Returns base path when all gists are stored
3378 Returns base path when all gists are stored
3363
3379
3364 :param cls:
3380 :param cls:
3365 """
3381 """
3366 from rhodecode.model.gist import GIST_STORE_LOC
3382 from rhodecode.model.gist import GIST_STORE_LOC
3367 q = Session().query(RhodeCodeUi)\
3383 q = Session().query(RhodeCodeUi)\
3368 .filter(RhodeCodeUi.ui_key == URL_SEP)
3384 .filter(RhodeCodeUi.ui_key == URL_SEP)
3369 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3385 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3370 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3386 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3371
3387
3372 def get_api_data(self):
3388 def get_api_data(self):
3373 """
3389 """
3374 Common function for generating gist related data for API
3390 Common function for generating gist related data for API
3375 """
3391 """
3376 gist = self
3392 gist = self
3377 data = {
3393 data = {
3378 'gist_id': gist.gist_id,
3394 'gist_id': gist.gist_id,
3379 'type': gist.gist_type,
3395 'type': gist.gist_type,
3380 'access_id': gist.gist_access_id,
3396 'access_id': gist.gist_access_id,
3381 'description': gist.gist_description,
3397 'description': gist.gist_description,
3382 'url': gist.gist_url(),
3398 'url': gist.gist_url(),
3383 'expires': gist.gist_expires,
3399 'expires': gist.gist_expires,
3384 'created_on': gist.created_on,
3400 'created_on': gist.created_on,
3385 'modified_at': gist.modified_at,
3401 'modified_at': gist.modified_at,
3386 'content': None,
3402 'content': None,
3387 'acl_level': gist.acl_level,
3403 'acl_level': gist.acl_level,
3388 }
3404 }
3389 return data
3405 return data
3390
3406
3391 def __json__(self):
3407 def __json__(self):
3392 data = dict(
3408 data = dict(
3393 )
3409 )
3394 data.update(self.get_api_data())
3410 data.update(self.get_api_data())
3395 return data
3411 return data
3396 # SCM functions
3412 # SCM functions
3397
3413
3398 def scm_instance(self, **kwargs):
3414 def scm_instance(self, **kwargs):
3399 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3415 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3400 return get_vcs_instance(
3416 return get_vcs_instance(
3401 repo_path=safe_str(full_repo_path), create=False)
3417 repo_path=safe_str(full_repo_path), create=False)
3402
3418
3403
3419
3404 class DbMigrateVersion(Base, BaseModel):
3420 class DbMigrateVersion(Base, BaseModel):
3405 __tablename__ = 'db_migrate_version'
3421 __tablename__ = 'db_migrate_version'
3406 __table_args__ = (
3422 __table_args__ = (
3407 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3423 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3408 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3424 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3409 )
3425 )
3410 repository_id = Column('repository_id', String(250), primary_key=True)
3426 repository_id = Column('repository_id', String(250), primary_key=True)
3411 repository_path = Column('repository_path', Text)
3427 repository_path = Column('repository_path', Text)
3412 version = Column('version', Integer)
3428 version = Column('version', Integer)
3413
3429
3414
3430
3415 class ExternalIdentity(Base, BaseModel):
3431 class ExternalIdentity(Base, BaseModel):
3416 __tablename__ = 'external_identities'
3432 __tablename__ = 'external_identities'
3417 __table_args__ = (
3433 __table_args__ = (
3418 Index('local_user_id_idx', 'local_user_id'),
3434 Index('local_user_id_idx', 'local_user_id'),
3419 Index('external_id_idx', 'external_id'),
3435 Index('external_id_idx', 'external_id'),
3420 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3436 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3421 'mysql_charset': 'utf8'})
3437 'mysql_charset': 'utf8'})
3422
3438
3423 external_id = Column('external_id', Unicode(255), default=u'',
3439 external_id = Column('external_id', Unicode(255), default=u'',
3424 primary_key=True)
3440 primary_key=True)
3425 external_username = Column('external_username', Unicode(1024), default=u'')
3441 external_username = Column('external_username', Unicode(1024), default=u'')
3426 local_user_id = Column('local_user_id', Integer(),
3442 local_user_id = Column('local_user_id', Integer(),
3427 ForeignKey('users.user_id'), primary_key=True)
3443 ForeignKey('users.user_id'), primary_key=True)
3428 provider_name = Column('provider_name', Unicode(255), default=u'',
3444 provider_name = Column('provider_name', Unicode(255), default=u'',
3429 primary_key=True)
3445 primary_key=True)
3430 access_token = Column('access_token', String(1024), default=u'')
3446 access_token = Column('access_token', String(1024), default=u'')
3431 alt_token = Column('alt_token', String(1024), default=u'')
3447 alt_token = Column('alt_token', String(1024), default=u'')
3432 token_secret = Column('token_secret', String(1024), default=u'')
3448 token_secret = Column('token_secret', String(1024), default=u'')
3433
3449
3434 @classmethod
3450 @classmethod
3435 def by_external_id_and_provider(cls, external_id, provider_name,
3451 def by_external_id_and_provider(cls, external_id, provider_name,
3436 local_user_id=None):
3452 local_user_id=None):
3437 """
3453 """
3438 Returns ExternalIdentity instance based on search params
3454 Returns ExternalIdentity instance based on search params
3439
3455
3440 :param external_id:
3456 :param external_id:
3441 :param provider_name:
3457 :param provider_name:
3442 :return: ExternalIdentity
3458 :return: ExternalIdentity
3443 """
3459 """
3444 query = cls.query()
3460 query = cls.query()
3445 query = query.filter(cls.external_id == external_id)
3461 query = query.filter(cls.external_id == external_id)
3446 query = query.filter(cls.provider_name == provider_name)
3462 query = query.filter(cls.provider_name == provider_name)
3447 if local_user_id:
3463 if local_user_id:
3448 query = query.filter(cls.local_user_id == local_user_id)
3464 query = query.filter(cls.local_user_id == local_user_id)
3449 return query.first()
3465 return query.first()
3450
3466
3451 @classmethod
3467 @classmethod
3452 def user_by_external_id_and_provider(cls, external_id, provider_name):
3468 def user_by_external_id_and_provider(cls, external_id, provider_name):
3453 """
3469 """
3454 Returns User instance based on search params
3470 Returns User instance based on search params
3455
3471
3456 :param external_id:
3472 :param external_id:
3457 :param provider_name:
3473 :param provider_name:
3458 :return: User
3474 :return: User
3459 """
3475 """
3460 query = User.query()
3476 query = User.query()
3461 query = query.filter(cls.external_id == external_id)
3477 query = query.filter(cls.external_id == external_id)
3462 query = query.filter(cls.provider_name == provider_name)
3478 query = query.filter(cls.provider_name == provider_name)
3463 query = query.filter(User.user_id == cls.local_user_id)
3479 query = query.filter(User.user_id == cls.local_user_id)
3464 return query.first()
3480 return query.first()
3465
3481
3466 @classmethod
3482 @classmethod
3467 def by_local_user_id(cls, local_user_id):
3483 def by_local_user_id(cls, local_user_id):
3468 """
3484 """
3469 Returns all tokens for user
3485 Returns all tokens for user
3470
3486
3471 :param local_user_id:
3487 :param local_user_id:
3472 :return: ExternalIdentity
3488 :return: ExternalIdentity
3473 """
3489 """
3474 query = cls.query()
3490 query = cls.query()
3475 query = query.filter(cls.local_user_id == local_user_id)
3491 query = query.filter(cls.local_user_id == local_user_id)
3476 return query
3492 return query
3477
3493
3478
3494
3479 class Integration(Base, BaseModel):
3495 class Integration(Base, BaseModel):
3480 __tablename__ = 'integrations'
3496 __tablename__ = 'integrations'
3481 __table_args__ = (
3497 __table_args__ = (
3482 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3498 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3483 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3499 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3484 )
3500 )
3485
3501
3486 integration_id = Column('integration_id', Integer(), primary_key=True)
3502 integration_id = Column('integration_id', Integer(), primary_key=True)
3487 integration_type = Column('integration_type', String(255))
3503 integration_type = Column('integration_type', String(255))
3488 enabled = Column('enabled', Boolean(), nullable=False)
3504 enabled = Column('enabled', Boolean(), nullable=False)
3489 name = Column('name', String(255), nullable=False)
3505 name = Column('name', String(255), nullable=False)
3490 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3506 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3491 default=False)
3507 default=False)
3492
3508
3493 settings = Column(
3509 settings = Column(
3494 'settings_json', MutationObj.as_mutable(
3510 'settings_json', MutationObj.as_mutable(
3495 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3511 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3496 repo_id = Column(
3512 repo_id = Column(
3497 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3513 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3498 nullable=True, unique=None, default=None)
3514 nullable=True, unique=None, default=None)
3499 repo = relationship('Repository', lazy='joined')
3515 repo = relationship('Repository', lazy='joined')
3500
3516
3501 repo_group_id = Column(
3517 repo_group_id = Column(
3502 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3518 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3503 nullable=True, unique=None, default=None)
3519 nullable=True, unique=None, default=None)
3504 repo_group = relationship('RepoGroup', lazy='joined')
3520 repo_group = relationship('RepoGroup', lazy='joined')
3505
3521
3506 @property
3522 @property
3507 def scope(self):
3523 def scope(self):
3508 if self.repo:
3524 if self.repo:
3509 return repr(self.repo)
3525 return repr(self.repo)
3510 if self.repo_group:
3526 if self.repo_group:
3511 if self.child_repos_only:
3527 if self.child_repos_only:
3512 return repr(self.repo_group) + ' (child repos only)'
3528 return repr(self.repo_group) + ' (child repos only)'
3513 else:
3529 else:
3514 return repr(self.repo_group) + ' (recursive)'
3530 return repr(self.repo_group) + ' (recursive)'
3515 if self.child_repos_only:
3531 if self.child_repos_only:
3516 return 'root_repos'
3532 return 'root_repos'
3517 return 'global'
3533 return 'global'
3518
3534
3519 def __repr__(self):
3535 def __repr__(self):
3520 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3536 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3521
3537
3522
3538
3523 class RepoReviewRuleUser(Base, BaseModel):
3539 class RepoReviewRuleUser(Base, BaseModel):
3524 __tablename__ = 'repo_review_rules_users'
3540 __tablename__ = 'repo_review_rules_users'
3525 __table_args__ = (
3541 __table_args__ = (
3526 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3542 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3527 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3543 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3528 )
3544 )
3529 repo_review_rule_user_id = Column(
3545 repo_review_rule_user_id = Column(
3530 'repo_review_rule_user_id', Integer(), primary_key=True)
3546 'repo_review_rule_user_id', Integer(), primary_key=True)
3531 repo_review_rule_id = Column("repo_review_rule_id",
3547 repo_review_rule_id = Column("repo_review_rule_id",
3532 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3548 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3533 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3549 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3534 nullable=False)
3550 nullable=False)
3535 user = relationship('User')
3551 user = relationship('User')
3536
3552
3537
3553
3538 class RepoReviewRuleUserGroup(Base, BaseModel):
3554 class RepoReviewRuleUserGroup(Base, BaseModel):
3539 __tablename__ = 'repo_review_rules_users_groups'
3555 __tablename__ = 'repo_review_rules_users_groups'
3540 __table_args__ = (
3556 __table_args__ = (
3541 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3557 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3542 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3558 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3543 )
3559 )
3544 repo_review_rule_users_group_id = Column(
3560 repo_review_rule_users_group_id = Column(
3545 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3561 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3546 repo_review_rule_id = Column("repo_review_rule_id",
3562 repo_review_rule_id = Column("repo_review_rule_id",
3547 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3563 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3548 users_group_id = Column("users_group_id", Integer(),
3564 users_group_id = Column("users_group_id", Integer(),
3549 ForeignKey('users_groups.users_group_id'), nullable=False)
3565 ForeignKey('users_groups.users_group_id'), nullable=False)
3550 users_group = relationship('UserGroup')
3566 users_group = relationship('UserGroup')
3551
3567
3552
3568
3553 class RepoReviewRule(Base, BaseModel):
3569 class RepoReviewRule(Base, BaseModel):
3554 __tablename__ = 'repo_review_rules'
3570 __tablename__ = 'repo_review_rules'
3555 __table_args__ = (
3571 __table_args__ = (
3556 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3572 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3557 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3573 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3558 )
3574 )
3559
3575
3560 repo_review_rule_id = Column(
3576 repo_review_rule_id = Column(
3561 'repo_review_rule_id', Integer(), primary_key=True)
3577 'repo_review_rule_id', Integer(), primary_key=True)
3562 repo_id = Column(
3578 repo_id = Column(
3563 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3579 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3564 repo = relationship('Repository', backref='review_rules')
3580 repo = relationship('Repository', backref='review_rules')
3565
3581
3566 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3582 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3567 default=u'*') # glob
3583 default=u'*') # glob
3568 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3584 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3569 default=u'*') # glob
3585 default=u'*') # glob
3570
3586
3571 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3587 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3572 nullable=False, default=False)
3588 nullable=False, default=False)
3573 rule_users = relationship('RepoReviewRuleUser')
3589 rule_users = relationship('RepoReviewRuleUser')
3574 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3590 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3575
3591
3576 @hybrid_property
3592 @hybrid_property
3577 def branch_pattern(self):
3593 def branch_pattern(self):
3578 return self._branch_pattern or '*'
3594 return self._branch_pattern or '*'
3579
3595
3580 def _validate_glob(self, value):
3596 def _validate_glob(self, value):
3581 re.compile('^' + glob2re(value) + '$')
3597 re.compile('^' + glob2re(value) + '$')
3582
3598
3583 @branch_pattern.setter
3599 @branch_pattern.setter
3584 def branch_pattern(self, value):
3600 def branch_pattern(self, value):
3585 self._validate_glob(value)
3601 self._validate_glob(value)
3586 self._branch_pattern = value or '*'
3602 self._branch_pattern = value or '*'
3587
3603
3588 @hybrid_property
3604 @hybrid_property
3589 def file_pattern(self):
3605 def file_pattern(self):
3590 return self._file_pattern or '*'
3606 return self._file_pattern or '*'
3591
3607
3592 @file_pattern.setter
3608 @file_pattern.setter
3593 def file_pattern(self, value):
3609 def file_pattern(self, value):
3594 self._validate_glob(value)
3610 self._validate_glob(value)
3595 self._file_pattern = value or '*'
3611 self._file_pattern = value or '*'
3596
3612
3597 def matches(self, branch, files_changed):
3613 def matches(self, branch, files_changed):
3598 """
3614 """
3599 Check if this review rule matches a branch/files in a pull request
3615 Check if this review rule matches a branch/files in a pull request
3600
3616
3601 :param branch: branch name for the commit
3617 :param branch: branch name for the commit
3602 :param files_changed: list of file paths changed in the pull request
3618 :param files_changed: list of file paths changed in the pull request
3603 """
3619 """
3604
3620
3605 branch = branch or ''
3621 branch = branch or ''
3606 files_changed = files_changed or []
3622 files_changed = files_changed or []
3607
3623
3608 branch_matches = True
3624 branch_matches = True
3609 if branch:
3625 if branch:
3610 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3626 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3611 branch_matches = bool(branch_regex.search(branch))
3627 branch_matches = bool(branch_regex.search(branch))
3612
3628
3613 files_matches = True
3629 files_matches = True
3614 if self.file_pattern != '*':
3630 if self.file_pattern != '*':
3615 files_matches = False
3631 files_matches = False
3616 file_regex = re.compile(glob2re(self.file_pattern))
3632 file_regex = re.compile(glob2re(self.file_pattern))
3617 for filename in files_changed:
3633 for filename in files_changed:
3618 if file_regex.search(filename):
3634 if file_regex.search(filename):
3619 files_matches = True
3635 files_matches = True
3620 break
3636 break
3621
3637
3622 return branch_matches and files_matches
3638 return branch_matches and files_matches
3623
3639
3624 @property
3640 @property
3625 def review_users(self):
3641 def review_users(self):
3626 """ Returns the users which this rule applies to """
3642 """ Returns the users which this rule applies to """
3627
3643
3628 users = set()
3644 users = set()
3629 users |= set([
3645 users |= set([
3630 rule_user.user for rule_user in self.rule_users
3646 rule_user.user for rule_user in self.rule_users
3631 if rule_user.user.active])
3647 if rule_user.user.active])
3632 users |= set(
3648 users |= set(
3633 member.user
3649 member.user
3634 for rule_user_group in self.rule_user_groups
3650 for rule_user_group in self.rule_user_groups
3635 for member in rule_user_group.users_group.members
3651 for member in rule_user_group.users_group.members
3636 if member.user.active
3652 if member.user.active
3637 )
3653 )
3638 return users
3654 return users
3639
3655
3640 def __repr__(self):
3656 def __repr__(self):
3641 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3657 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3642 self.repo_review_rule_id, self.repo)
3658 self.repo_review_rule_id, self.repo)
@@ -1,34 +1,34 b''
1 import logging
1 import logging
2 import datetime
2 import datetime
3
3
4 from sqlalchemy import *
4 from sqlalchemy import *
5 from sqlalchemy.exc import DatabaseError
5 from sqlalchemy.exc import DatabaseError
6 from sqlalchemy.orm import relation, backref, class_mapper, joinedload
6 from sqlalchemy.orm import relation, backref, class_mapper, joinedload
7 from sqlalchemy.orm.session import Session
7 from sqlalchemy.orm.session import Session
8 from sqlalchemy.ext.declarative import declarative_base
8 from sqlalchemy.ext.declarative import declarative_base
9
9
10 from rhodecode.lib.dbmigrate.migrate import *
10 from rhodecode.lib.dbmigrate.migrate import *
11 from rhodecode.lib.dbmigrate.migrate.changeset import *
11 from rhodecode.lib.dbmigrate.migrate.changeset import *
12 from rhodecode.lib.utils2 import str2bool
12 from rhodecode.lib.utils2 import str2bool
13
13
14 from rhodecode.model.meta import Base
14 from rhodecode.model.meta import Base
15 from rhodecode.model import meta
15 from rhodecode.model import meta
16 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
16 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
17
17
18 log = logging.getLogger(__name__)
18 log = logging.getLogger(__name__)
19
19
20
20
21 def upgrade(migrate_engine):
21 def upgrade(migrate_engine):
22 """
22 """
23 Upgrade operations go here.
23 Upgrade operations go here.
24 Don't create your own engine; bind migrate_engine to your metadata
24 Don't create your own engine; bind migrate_engine to your metadata
25 """
25 """
26 _reset_base(migrate_engine)
26 _reset_base(migrate_engine)
27 from rhodecode.lib.dbmigrate.schema import db_4_5_0_0
27 from rhodecode.lib.dbmigrate.schema import db_4_5_0_0
28
28
29 db_4_5_0_0.PullRequestReviewers.reason.create(
29 db_4_5_0_0.PullRequestReviewers.reasons.create(
30 table=db_4_5_0_0.PullRequestReviewers.__table__)
30 table=db_4_5_0_0.PullRequestReviewers.__table__)
31
31
32 def downgrade(migrate_engine):
32 def downgrade(migrate_engine):
33 meta = MetaData()
33 meta = MetaData()
34 meta.bind = migrate_engine
34 meta.bind = migrate_engine
@@ -1,268 +1,268 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Changeset status conttroller
22 Changeset status conttroller
23 """
23 """
24
24
25 import itertools
25 import itertools
26 import logging
26 import logging
27 from collections import defaultdict
27 from collections import defaultdict
28
28
29 from rhodecode.model import BaseModel
29 from rhodecode.model import BaseModel
30 from rhodecode.model.db import ChangesetStatus, ChangesetComment, PullRequest
30 from rhodecode.model.db import ChangesetStatus, ChangesetComment, PullRequest
31 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
31 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
32 from rhodecode.lib.markup_renderer import (
32 from rhodecode.lib.markup_renderer import (
33 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
33 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 class ChangesetStatusModel(BaseModel):
38 class ChangesetStatusModel(BaseModel):
39
39
40 cls = ChangesetStatus
40 cls = ChangesetStatus
41
41
42 def __get_changeset_status(self, changeset_status):
42 def __get_changeset_status(self, changeset_status):
43 return self._get_instance(ChangesetStatus, changeset_status)
43 return self._get_instance(ChangesetStatus, changeset_status)
44
44
45 def __get_pull_request(self, pull_request):
45 def __get_pull_request(self, pull_request):
46 return self._get_instance(PullRequest, pull_request)
46 return self._get_instance(PullRequest, pull_request)
47
47
48 def _get_status_query(self, repo, revision, pull_request,
48 def _get_status_query(self, repo, revision, pull_request,
49 with_revisions=False):
49 with_revisions=False):
50 repo = self._get_repo(repo)
50 repo = self._get_repo(repo)
51
51
52 q = ChangesetStatus.query()\
52 q = ChangesetStatus.query()\
53 .filter(ChangesetStatus.repo == repo)
53 .filter(ChangesetStatus.repo == repo)
54 if not with_revisions:
54 if not with_revisions:
55 q = q.filter(ChangesetStatus.version == 0)
55 q = q.filter(ChangesetStatus.version == 0)
56
56
57 if revision:
57 if revision:
58 q = q.filter(ChangesetStatus.revision == revision)
58 q = q.filter(ChangesetStatus.revision == revision)
59 elif pull_request:
59 elif pull_request:
60 pull_request = self.__get_pull_request(pull_request)
60 pull_request = self.__get_pull_request(pull_request)
61 # TODO: johbo: Think about the impact of this join, there must
61 # TODO: johbo: Think about the impact of this join, there must
62 # be a reason why ChangesetStatus and ChanagesetComment is linked
62 # be a reason why ChangesetStatus and ChanagesetComment is linked
63 # to the pull request. Might be that we want to do the same for
63 # to the pull request. Might be that we want to do the same for
64 # the pull_request_version_id.
64 # the pull_request_version_id.
65 q = q.join(ChangesetComment).filter(
65 q = q.join(ChangesetComment).filter(
66 ChangesetStatus.pull_request == pull_request,
66 ChangesetStatus.pull_request == pull_request,
67 ChangesetComment.pull_request_version_id == None)
67 ChangesetComment.pull_request_version_id == None)
68 else:
68 else:
69 raise Exception('Please specify revision or pull_request')
69 raise Exception('Please specify revision or pull_request')
70 q = q.order_by(ChangesetStatus.version.asc())
70 q = q.order_by(ChangesetStatus.version.asc())
71 return q
71 return q
72
72
73 def calculate_status(self, statuses_by_reviewers):
73 def calculate_status(self, statuses_by_reviewers):
74 """
74 """
75 Given the approval statuses from reviewers, calculates final approval
75 Given the approval statuses from reviewers, calculates final approval
76 status. There can only be 3 results, all approved, all rejected. If
76 status. There can only be 3 results, all approved, all rejected. If
77 there is no consensus the PR is under review.
77 there is no consensus the PR is under review.
78
78
79 :param statuses_by_reviewers:
79 :param statuses_by_reviewers:
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, statuses in statuses_by_reviewers:
83 for user, reasons, 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
87 else:
87 else:
88 votes[ChangesetStatus.DEFAULT] += 1
88 votes[ChangesetStatus.DEFAULT] += 1
89
89
90 # all approved
90 # all approved
91 if votes.get(ChangesetStatus.STATUS_APPROVED) == reviewers_number:
91 if votes.get(ChangesetStatus.STATUS_APPROVED) == reviewers_number:
92 return ChangesetStatus.STATUS_APPROVED
92 return ChangesetStatus.STATUS_APPROVED
93
93
94 # all rejected
94 # all rejected
95 if votes.get(ChangesetStatus.STATUS_REJECTED) == reviewers_number:
95 if votes.get(ChangesetStatus.STATUS_REJECTED) == reviewers_number:
96 return ChangesetStatus.STATUS_REJECTED
96 return ChangesetStatus.STATUS_REJECTED
97
97
98 return ChangesetStatus.STATUS_UNDER_REVIEW
98 return ChangesetStatus.STATUS_UNDER_REVIEW
99
99
100 def get_statuses(self, repo, revision=None, pull_request=None,
100 def get_statuses(self, repo, revision=None, pull_request=None,
101 with_revisions=False):
101 with_revisions=False):
102 q = self._get_status_query(repo, revision, pull_request,
102 q = self._get_status_query(repo, revision, pull_request,
103 with_revisions)
103 with_revisions)
104 return q.all()
104 return q.all()
105
105
106 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
106 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
107 """
107 """
108 Returns latest status of changeset for given revision or for given
108 Returns latest status of changeset for given revision or for given
109 pull request. Statuses are versioned inside a table itself and
109 pull request. Statuses are versioned inside a table itself and
110 version == 0 is always the current one
110 version == 0 is always the current one
111
111
112 :param repo:
112 :param repo:
113 :param revision: 40char hash or None
113 :param revision: 40char hash or None
114 :param pull_request: pull_request reference
114 :param pull_request: pull_request reference
115 :param as_str: return status as string not object
115 :param as_str: return status as string not object
116 """
116 """
117 q = self._get_status_query(repo, revision, pull_request)
117 q = self._get_status_query(repo, revision, pull_request)
118
118
119 # need to use first here since there can be multiple statuses
119 # need to use first here since there can be multiple statuses
120 # returned from pull_request
120 # returned from pull_request
121 status = q.first()
121 status = q.first()
122 if as_str:
122 if as_str:
123 status = status.status if status else status
123 status = status.status if status else status
124 st = status or ChangesetStatus.DEFAULT
124 st = status or ChangesetStatus.DEFAULT
125 return str(st)
125 return str(st)
126 return status
126 return status
127
127
128 def _render_auto_status_message(
128 def _render_auto_status_message(
129 self, status, commit_id=None, pull_request=None):
129 self, status, commit_id=None, pull_request=None):
130 """
130 """
131 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
131 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
132 so it's always looking the same disregarding on which default
132 so it's always looking the same disregarding on which default
133 renderer system is using.
133 renderer system is using.
134
134
135 :param status: status text to change into
135 :param status: status text to change into
136 :param commit_id: the commit_id we change the status for
136 :param commit_id: the commit_id we change the status for
137 :param pull_request: the pull request we change the status for
137 :param pull_request: the pull request we change the status for
138 """
138 """
139
139
140 new_status = ChangesetStatus.get_status_lbl(status)
140 new_status = ChangesetStatus.get_status_lbl(status)
141
141
142 params = {
142 params = {
143 'new_status_label': new_status,
143 'new_status_label': new_status,
144 'pull_request': pull_request,
144 'pull_request': pull_request,
145 'commit_id': commit_id,
145 'commit_id': commit_id,
146 }
146 }
147 renderer = RstTemplateRenderer()
147 renderer = RstTemplateRenderer()
148 return renderer.render('auto_status_change.mako', **params)
148 return renderer.render('auto_status_change.mako', **params)
149
149
150 def set_status(self, repo, status, user, comment=None, revision=None,
150 def set_status(self, repo, status, user, comment=None, revision=None,
151 pull_request=None, dont_allow_on_closed_pull_request=False):
151 pull_request=None, dont_allow_on_closed_pull_request=False):
152 """
152 """
153 Creates new status for changeset or updates the old ones bumping their
153 Creates new status for changeset or updates the old ones bumping their
154 version, leaving the current status at
154 version, leaving the current status at
155
155
156 :param repo:
156 :param repo:
157 :param revision:
157 :param revision:
158 :param status:
158 :param status:
159 :param user:
159 :param user:
160 :param comment:
160 :param comment:
161 :param dont_allow_on_closed_pull_request: don't allow a status change
161 :param dont_allow_on_closed_pull_request: don't allow a status change
162 if last status was for pull request and it's closed. We shouldn't
162 if last status was for pull request and it's closed. We shouldn't
163 mess around this manually
163 mess around this manually
164 """
164 """
165 repo = self._get_repo(repo)
165 repo = self._get_repo(repo)
166
166
167 q = ChangesetStatus.query()
167 q = ChangesetStatus.query()
168
168
169 if revision:
169 if revision:
170 q = q.filter(ChangesetStatus.repo == repo)
170 q = q.filter(ChangesetStatus.repo == repo)
171 q = q.filter(ChangesetStatus.revision == revision)
171 q = q.filter(ChangesetStatus.revision == revision)
172 elif pull_request:
172 elif pull_request:
173 pull_request = self.__get_pull_request(pull_request)
173 pull_request = self.__get_pull_request(pull_request)
174 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
174 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
175 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
175 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
176 cur_statuses = q.all()
176 cur_statuses = q.all()
177
177
178 # if statuses exists and last is associated with a closed pull request
178 # if statuses exists and last is associated with a closed pull request
179 # we need to check if we can allow this status change
179 # we need to check if we can allow this status change
180 if (dont_allow_on_closed_pull_request and cur_statuses
180 if (dont_allow_on_closed_pull_request and cur_statuses
181 and getattr(cur_statuses[0].pull_request, 'status', '')
181 and getattr(cur_statuses[0].pull_request, 'status', '')
182 == PullRequest.STATUS_CLOSED):
182 == PullRequest.STATUS_CLOSED):
183 raise StatusChangeOnClosedPullRequestError(
183 raise StatusChangeOnClosedPullRequestError(
184 'Changing status on closed pull request is not allowed'
184 'Changing status on closed pull request is not allowed'
185 )
185 )
186
186
187 # update all current statuses with older version
187 # update all current statuses with older version
188 if cur_statuses:
188 if cur_statuses:
189 for st in cur_statuses:
189 for st in cur_statuses:
190 st.version += 1
190 st.version += 1
191 self.sa.add(st)
191 self.sa.add(st)
192
192
193 def _create_status(user, repo, status, comment, revision, pull_request):
193 def _create_status(user, repo, status, comment, revision, pull_request):
194 new_status = ChangesetStatus()
194 new_status = ChangesetStatus()
195 new_status.author = self._get_user(user)
195 new_status.author = self._get_user(user)
196 new_status.repo = self._get_repo(repo)
196 new_status.repo = self._get_repo(repo)
197 new_status.status = status
197 new_status.status = status
198 new_status.comment = comment
198 new_status.comment = comment
199 new_status.revision = revision
199 new_status.revision = revision
200 new_status.pull_request = pull_request
200 new_status.pull_request = pull_request
201 return new_status
201 return new_status
202
202
203 if not comment:
203 if not comment:
204 from rhodecode.model.comment import ChangesetCommentsModel
204 from rhodecode.model.comment import ChangesetCommentsModel
205 comment = ChangesetCommentsModel().create(
205 comment = ChangesetCommentsModel().create(
206 text=self._render_auto_status_message(
206 text=self._render_auto_status_message(
207 status, commit_id=revision, pull_request=pull_request),
207 status, commit_id=revision, pull_request=pull_request),
208 repo=repo,
208 repo=repo,
209 user=user,
209 user=user,
210 pull_request=pull_request,
210 pull_request=pull_request,
211 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
211 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
212 )
212 )
213
213
214 if revision:
214 if revision:
215 new_status = _create_status(
215 new_status = _create_status(
216 user=user, repo=repo, status=status, comment=comment,
216 user=user, repo=repo, status=status, comment=comment,
217 revision=revision, pull_request=pull_request)
217 revision=revision, pull_request=pull_request)
218 self.sa.add(new_status)
218 self.sa.add(new_status)
219 return new_status
219 return new_status
220 elif pull_request:
220 elif pull_request:
221 # pull request can have more than one revision associated to it
221 # pull request can have more than one revision associated to it
222 # we need to create new version for each one
222 # we need to create new version for each one
223 new_statuses = []
223 new_statuses = []
224 repo = pull_request.source_repo
224 repo = pull_request.source_repo
225 for rev in pull_request.revisions:
225 for rev in pull_request.revisions:
226 new_status = _create_status(
226 new_status = _create_status(
227 user=user, repo=repo, status=status, comment=comment,
227 user=user, repo=repo, status=status, comment=comment,
228 revision=rev, pull_request=pull_request)
228 revision=rev, pull_request=pull_request)
229 new_statuses.append(new_status)
229 new_statuses.append(new_status)
230 self.sa.add(new_status)
230 self.sa.add(new_status)
231 return new_statuses
231 return new_statuses
232
232
233 def reviewers_statuses(self, pull_request):
233 def reviewers_statuses(self, pull_request):
234 _commit_statuses = self.get_statuses(
234 _commit_statuses = self.get_statuses(
235 pull_request.source_repo,
235 pull_request.source_repo,
236 pull_request=pull_request,
236 pull_request=pull_request,
237 with_revisions=True)
237 with_revisions=True)
238
238
239 commit_statuses = defaultdict(list)
239 commit_statuses = defaultdict(list)
240 for st in _commit_statuses:
240 for st in _commit_statuses:
241 commit_statuses[st.author.username] += [st]
241 commit_statuses[st.author.username] += [st]
242
242
243 pull_request_reviewers = []
243 pull_request_reviewers = []
244
244
245 def version(commit_status):
245 def version(commit_status):
246 return commit_status.version
246 return commit_status.version
247
247
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 st = commit_statuses.get(o.user.username, None)
252 if st:
252 if st:
253 st = [(x, list(y)[0])
253 st = [(x, list(y)[0])
254 for x, y in (itertools.groupby(sorted(st, key=version),
254 for x, y in (itertools.groupby(sorted(st, key=version),
255 version))]
255 version))]
256
256
257 pull_request_reviewers.append([o.user, st])
257 pull_request_reviewers.append((o.user, o.reasons, st))
258 return pull_request_reviewers
258 return pull_request_reviewers
259
259
260 def calculated_review_status(self, pull_request, reviewers_statuses=None):
260 def calculated_review_status(self, pull_request, reviewers_statuses=None):
261 """
261 """
262 calculate pull request status based on reviewers, it should be a list
262 calculate pull request status based on reviewers, it should be a list
263 of two element lists.
263 of two element lists.
264
264
265 :param reviewers_statuses:
265 :param reviewers_statuses:
266 """
266 """
267 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
267 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
268 return self.calculate_status(reviewers)
268 return self.calculate_status(reviewers)
@@ -1,3642 +1,3658 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import sys
27 import sys
28 import time
28 import time
29 import hashlib
29 import hashlib
30 import logging
30 import logging
31 import datetime
31 import datetime
32 import warnings
32 import warnings
33 import ipaddress
33 import ipaddress
34 import functools
34 import functools
35 import traceback
35 import traceback
36 import collections
36 import collections
37
37
38
38
39 from sqlalchemy import *
39 from sqlalchemy import *
40 from sqlalchemy.exc import IntegrityError
40 from sqlalchemy.exc import IntegrityError
41 from sqlalchemy.ext.declarative import declared_attr
41 from sqlalchemy.ext.declarative import declared_attr
42 from sqlalchemy.ext.hybrid import hybrid_property
42 from sqlalchemy.ext.hybrid import hybrid_property
43 from sqlalchemy.orm import (
43 from sqlalchemy.orm import (
44 relationship, joinedload, class_mapper, validates, aliased)
44 relationship, joinedload, class_mapper, validates, aliased)
45 from sqlalchemy.sql.expression import true
45 from sqlalchemy.sql.expression import true
46 from beaker.cache import cache_region, region_invalidate
46 from beaker.cache import cache_region, region_invalidate
47 from webob.exc import HTTPNotFound
47 from webob.exc import HTTPNotFound
48 from zope.cachedescriptors.property import Lazy as LazyProperty
48 from zope.cachedescriptors.property import Lazy as LazyProperty
49
49
50 from pylons import url
50 from pylons import url
51 from pylons.i18n.translation import lazy_ugettext as _
51 from pylons.i18n.translation import lazy_ugettext as _
52
52
53 from rhodecode.lib.vcs import get_backend, get_vcs_instance
53 from rhodecode.lib.vcs import get_backend, get_vcs_instance
54 from rhodecode.lib.vcs.utils.helpers import get_scm
54 from rhodecode.lib.vcs.utils.helpers import get_scm
55 from rhodecode.lib.vcs.exceptions import VCSError
55 from rhodecode.lib.vcs.exceptions import VCSError
56 from rhodecode.lib.vcs.backends.base import (
56 from rhodecode.lib.vcs.backends.base import (
57 EmptyCommit, Reference, MergeFailureReason)
57 EmptyCommit, Reference, MergeFailureReason)
58 from rhodecode.lib.utils2 import (
58 from rhodecode.lib.utils2 import (
59 str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe,
59 str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe,
60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
61 glob2re)
61 glob2re)
62 from rhodecode.lib.jsonalchemy import MutationObj, JsonType, JSONDict
62 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, JSONDict
63 from rhodecode.lib.ext_json import json
63 from rhodecode.lib.ext_json import json
64 from rhodecode.lib.caching_query import FromCache
64 from rhodecode.lib.caching_query import FromCache
65 from rhodecode.lib.encrypt import AESCipher
65 from rhodecode.lib.encrypt import AESCipher
66
66
67 from rhodecode.model.meta import Base, Session
67 from rhodecode.model.meta import Base, Session
68
68
69 URL_SEP = '/'
69 URL_SEP = '/'
70 log = logging.getLogger(__name__)
70 log = logging.getLogger(__name__)
71
71
72 # =============================================================================
72 # =============================================================================
73 # BASE CLASSES
73 # BASE CLASSES
74 # =============================================================================
74 # =============================================================================
75
75
76 # this is propagated from .ini file rhodecode.encrypted_values.secret or
76 # this is propagated from .ini file rhodecode.encrypted_values.secret or
77 # beaker.session.secret if first is not set.
77 # beaker.session.secret if first is not set.
78 # and initialized at environment.py
78 # and initialized at environment.py
79 ENCRYPTION_KEY = None
79 ENCRYPTION_KEY = None
80
80
81 # used to sort permissions by types, '#' used here is not allowed to be in
81 # used to sort permissions by types, '#' used here is not allowed to be in
82 # usernames, and it's very early in sorted string.printable table.
82 # usernames, and it's very early in sorted string.printable table.
83 PERMISSION_TYPE_SORT = {
83 PERMISSION_TYPE_SORT = {
84 'admin': '####',
84 'admin': '####',
85 'write': '###',
85 'write': '###',
86 'read': '##',
86 'read': '##',
87 'none': '#',
87 'none': '#',
88 }
88 }
89
89
90
90
91 def display_sort(obj):
91 def display_sort(obj):
92 """
92 """
93 Sort function used to sort permissions in .permissions() function of
93 Sort function used to sort permissions in .permissions() function of
94 Repository, RepoGroup, UserGroup. Also it put the default user in front
94 Repository, RepoGroup, UserGroup. Also it put the default user in front
95 of all other resources
95 of all other resources
96 """
96 """
97
97
98 if obj.username == User.DEFAULT_USER:
98 if obj.username == User.DEFAULT_USER:
99 return '#####'
99 return '#####'
100 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
100 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
101 return prefix + obj.username
101 return prefix + obj.username
102
102
103
103
104 def _hash_key(k):
104 def _hash_key(k):
105 return md5_safe(k)
105 return md5_safe(k)
106
106
107
107
108 class EncryptedTextValue(TypeDecorator):
108 class EncryptedTextValue(TypeDecorator):
109 """
109 """
110 Special column for encrypted long text data, use like::
110 Special column for encrypted long text data, use like::
111
111
112 value = Column("encrypted_value", EncryptedValue(), nullable=False)
112 value = Column("encrypted_value", EncryptedValue(), nullable=False)
113
113
114 This column is intelligent so if value is in unencrypted form it return
114 This column is intelligent so if value is in unencrypted form it return
115 unencrypted form, but on save it always encrypts
115 unencrypted form, but on save it always encrypts
116 """
116 """
117 impl = Text
117 impl = Text
118
118
119 def process_bind_param(self, value, dialect):
119 def process_bind_param(self, value, dialect):
120 if not value:
120 if not value:
121 return value
121 return value
122 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
122 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
123 # protect against double encrypting if someone manually starts
123 # protect against double encrypting if someone manually starts
124 # doing
124 # doing
125 raise ValueError('value needs to be in unencrypted format, ie. '
125 raise ValueError('value needs to be in unencrypted format, ie. '
126 'not starting with enc$aes')
126 'not starting with enc$aes')
127 return 'enc$aes_hmac$%s' % AESCipher(
127 return 'enc$aes_hmac$%s' % AESCipher(
128 ENCRYPTION_KEY, hmac=True).encrypt(value)
128 ENCRYPTION_KEY, hmac=True).encrypt(value)
129
129
130 def process_result_value(self, value, dialect):
130 def process_result_value(self, value, dialect):
131 import rhodecode
131 import rhodecode
132
132
133 if not value:
133 if not value:
134 return value
134 return value
135
135
136 parts = value.split('$', 3)
136 parts = value.split('$', 3)
137 if not len(parts) == 3:
137 if not len(parts) == 3:
138 # probably not encrypted values
138 # probably not encrypted values
139 return value
139 return value
140 else:
140 else:
141 if parts[0] != 'enc':
141 if parts[0] != 'enc':
142 # parts ok but without our header ?
142 # parts ok but without our header ?
143 return value
143 return value
144 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
144 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
145 'rhodecode.encrypted_values.strict') or True)
145 'rhodecode.encrypted_values.strict') or True)
146 # at that stage we know it's our encryption
146 # at that stage we know it's our encryption
147 if parts[1] == 'aes':
147 if parts[1] == 'aes':
148 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
148 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
149 elif parts[1] == 'aes_hmac':
149 elif parts[1] == 'aes_hmac':
150 decrypted_data = AESCipher(
150 decrypted_data = AESCipher(
151 ENCRYPTION_KEY, hmac=True,
151 ENCRYPTION_KEY, hmac=True,
152 strict_verification=enc_strict_mode).decrypt(parts[2])
152 strict_verification=enc_strict_mode).decrypt(parts[2])
153 else:
153 else:
154 raise ValueError(
154 raise ValueError(
155 'Encryption type part is wrong, must be `aes` '
155 'Encryption type part is wrong, must be `aes` '
156 'or `aes_hmac`, got `%s` instead' % (parts[1]))
156 'or `aes_hmac`, got `%s` instead' % (parts[1]))
157 return decrypted_data
157 return decrypted_data
158
158
159
159
160 class BaseModel(object):
160 class BaseModel(object):
161 """
161 """
162 Base Model for all classes
162 Base Model for all classes
163 """
163 """
164
164
165 @classmethod
165 @classmethod
166 def _get_keys(cls):
166 def _get_keys(cls):
167 """return column names for this model """
167 """return column names for this model """
168 return class_mapper(cls).c.keys()
168 return class_mapper(cls).c.keys()
169
169
170 def get_dict(self):
170 def get_dict(self):
171 """
171 """
172 return dict with keys and values corresponding
172 return dict with keys and values corresponding
173 to this model data """
173 to this model data """
174
174
175 d = {}
175 d = {}
176 for k in self._get_keys():
176 for k in self._get_keys():
177 d[k] = getattr(self, k)
177 d[k] = getattr(self, k)
178
178
179 # also use __json__() if present to get additional fields
179 # also use __json__() if present to get additional fields
180 _json_attr = getattr(self, '__json__', None)
180 _json_attr = getattr(self, '__json__', None)
181 if _json_attr:
181 if _json_attr:
182 # update with attributes from __json__
182 # update with attributes from __json__
183 if callable(_json_attr):
183 if callable(_json_attr):
184 _json_attr = _json_attr()
184 _json_attr = _json_attr()
185 for k, val in _json_attr.iteritems():
185 for k, val in _json_attr.iteritems():
186 d[k] = val
186 d[k] = val
187 return d
187 return d
188
188
189 def get_appstruct(self):
189 def get_appstruct(self):
190 """return list with keys and values tuples corresponding
190 """return list with keys and values tuples corresponding
191 to this model data """
191 to this model data """
192
192
193 l = []
193 l = []
194 for k in self._get_keys():
194 for k in self._get_keys():
195 l.append((k, getattr(self, k),))
195 l.append((k, getattr(self, k),))
196 return l
196 return l
197
197
198 def populate_obj(self, populate_dict):
198 def populate_obj(self, populate_dict):
199 """populate model with data from given populate_dict"""
199 """populate model with data from given populate_dict"""
200
200
201 for k in self._get_keys():
201 for k in self._get_keys():
202 if k in populate_dict:
202 if k in populate_dict:
203 setattr(self, k, populate_dict[k])
203 setattr(self, k, populate_dict[k])
204
204
205 @classmethod
205 @classmethod
206 def query(cls):
206 def query(cls):
207 return Session().query(cls)
207 return Session().query(cls)
208
208
209 @classmethod
209 @classmethod
210 def get(cls, id_):
210 def get(cls, id_):
211 if id_:
211 if id_:
212 return cls.query().get(id_)
212 return cls.query().get(id_)
213
213
214 @classmethod
214 @classmethod
215 def get_or_404(cls, id_):
215 def get_or_404(cls, id_):
216 try:
216 try:
217 id_ = int(id_)
217 id_ = int(id_)
218 except (TypeError, ValueError):
218 except (TypeError, ValueError):
219 raise HTTPNotFound
219 raise HTTPNotFound
220
220
221 res = cls.query().get(id_)
221 res = cls.query().get(id_)
222 if not res:
222 if not res:
223 raise HTTPNotFound
223 raise HTTPNotFound
224 return res
224 return res
225
225
226 @classmethod
226 @classmethod
227 def getAll(cls):
227 def getAll(cls):
228 # deprecated and left for backward compatibility
228 # deprecated and left for backward compatibility
229 return cls.get_all()
229 return cls.get_all()
230
230
231 @classmethod
231 @classmethod
232 def get_all(cls):
232 def get_all(cls):
233 return cls.query().all()
233 return cls.query().all()
234
234
235 @classmethod
235 @classmethod
236 def delete(cls, id_):
236 def delete(cls, id_):
237 obj = cls.query().get(id_)
237 obj = cls.query().get(id_)
238 Session().delete(obj)
238 Session().delete(obj)
239
239
240 @classmethod
240 @classmethod
241 def identity_cache(cls, session, attr_name, value):
241 def identity_cache(cls, session, attr_name, value):
242 exist_in_session = []
242 exist_in_session = []
243 for (item_cls, pkey), instance in session.identity_map.items():
243 for (item_cls, pkey), instance in session.identity_map.items():
244 if cls == item_cls and getattr(instance, attr_name) == value:
244 if cls == item_cls and getattr(instance, attr_name) == value:
245 exist_in_session.append(instance)
245 exist_in_session.append(instance)
246 if exist_in_session:
246 if exist_in_session:
247 if len(exist_in_session) == 1:
247 if len(exist_in_session) == 1:
248 return exist_in_session[0]
248 return exist_in_session[0]
249 log.exception(
249 log.exception(
250 'multiple objects with attr %s and '
250 'multiple objects with attr %s and '
251 'value %s found with same name: %r',
251 'value %s found with same name: %r',
252 attr_name, value, exist_in_session)
252 attr_name, value, exist_in_session)
253
253
254 def __repr__(self):
254 def __repr__(self):
255 if hasattr(self, '__unicode__'):
255 if hasattr(self, '__unicode__'):
256 # python repr needs to return str
256 # python repr needs to return str
257 try:
257 try:
258 return safe_str(self.__unicode__())
258 return safe_str(self.__unicode__())
259 except UnicodeDecodeError:
259 except UnicodeDecodeError:
260 pass
260 pass
261 return '<DB:%s>' % (self.__class__.__name__)
261 return '<DB:%s>' % (self.__class__.__name__)
262
262
263
263
264 class RhodeCodeSetting(Base, BaseModel):
264 class RhodeCodeSetting(Base, BaseModel):
265 __tablename__ = 'rhodecode_settings'
265 __tablename__ = 'rhodecode_settings'
266 __table_args__ = (
266 __table_args__ = (
267 UniqueConstraint('app_settings_name'),
267 UniqueConstraint('app_settings_name'),
268 {'extend_existing': True, 'mysql_engine': 'InnoDB',
268 {'extend_existing': True, 'mysql_engine': 'InnoDB',
269 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
269 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
270 )
270 )
271
271
272 SETTINGS_TYPES = {
272 SETTINGS_TYPES = {
273 'str': safe_str,
273 'str': safe_str,
274 'int': safe_int,
274 'int': safe_int,
275 'unicode': safe_unicode,
275 'unicode': safe_unicode,
276 'bool': str2bool,
276 'bool': str2bool,
277 'list': functools.partial(aslist, sep=',')
277 'list': functools.partial(aslist, sep=',')
278 }
278 }
279 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
279 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
280 GLOBAL_CONF_KEY = 'app_settings'
280 GLOBAL_CONF_KEY = 'app_settings'
281
281
282 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
282 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
283 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
283 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
284 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
284 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
285 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
285 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
286
286
287 def __init__(self, key='', val='', type='unicode'):
287 def __init__(self, key='', val='', type='unicode'):
288 self.app_settings_name = key
288 self.app_settings_name = key
289 self.app_settings_type = type
289 self.app_settings_type = type
290 self.app_settings_value = val
290 self.app_settings_value = val
291
291
292 @validates('_app_settings_value')
292 @validates('_app_settings_value')
293 def validate_settings_value(self, key, val):
293 def validate_settings_value(self, key, val):
294 assert type(val) == unicode
294 assert type(val) == unicode
295 return val
295 return val
296
296
297 @hybrid_property
297 @hybrid_property
298 def app_settings_value(self):
298 def app_settings_value(self):
299 v = self._app_settings_value
299 v = self._app_settings_value
300 _type = self.app_settings_type
300 _type = self.app_settings_type
301 if _type:
301 if _type:
302 _type = self.app_settings_type.split('.')[0]
302 _type = self.app_settings_type.split('.')[0]
303 # decode the encrypted value
303 # decode the encrypted value
304 if 'encrypted' in self.app_settings_type:
304 if 'encrypted' in self.app_settings_type:
305 cipher = EncryptedTextValue()
305 cipher = EncryptedTextValue()
306 v = safe_unicode(cipher.process_result_value(v, None))
306 v = safe_unicode(cipher.process_result_value(v, None))
307
307
308 converter = self.SETTINGS_TYPES.get(_type) or \
308 converter = self.SETTINGS_TYPES.get(_type) or \
309 self.SETTINGS_TYPES['unicode']
309 self.SETTINGS_TYPES['unicode']
310 return converter(v)
310 return converter(v)
311
311
312 @app_settings_value.setter
312 @app_settings_value.setter
313 def app_settings_value(self, val):
313 def app_settings_value(self, val):
314 """
314 """
315 Setter that will always make sure we use unicode in app_settings_value
315 Setter that will always make sure we use unicode in app_settings_value
316
316
317 :param val:
317 :param val:
318 """
318 """
319 val = safe_unicode(val)
319 val = safe_unicode(val)
320 # encode the encrypted value
320 # encode the encrypted value
321 if 'encrypted' in self.app_settings_type:
321 if 'encrypted' in self.app_settings_type:
322 cipher = EncryptedTextValue()
322 cipher = EncryptedTextValue()
323 val = safe_unicode(cipher.process_bind_param(val, None))
323 val = safe_unicode(cipher.process_bind_param(val, None))
324 self._app_settings_value = val
324 self._app_settings_value = val
325
325
326 @hybrid_property
326 @hybrid_property
327 def app_settings_type(self):
327 def app_settings_type(self):
328 return self._app_settings_type
328 return self._app_settings_type
329
329
330 @app_settings_type.setter
330 @app_settings_type.setter
331 def app_settings_type(self, val):
331 def app_settings_type(self, val):
332 if val.split('.')[0] not in self.SETTINGS_TYPES:
332 if val.split('.')[0] not in self.SETTINGS_TYPES:
333 raise Exception('type must be one of %s got %s'
333 raise Exception('type must be one of %s got %s'
334 % (self.SETTINGS_TYPES.keys(), val))
334 % (self.SETTINGS_TYPES.keys(), val))
335 self._app_settings_type = val
335 self._app_settings_type = val
336
336
337 def __unicode__(self):
337 def __unicode__(self):
338 return u"<%s('%s:%s[%s]')>" % (
338 return u"<%s('%s:%s[%s]')>" % (
339 self.__class__.__name__,
339 self.__class__.__name__,
340 self.app_settings_name, self.app_settings_value,
340 self.app_settings_name, self.app_settings_value,
341 self.app_settings_type
341 self.app_settings_type
342 )
342 )
343
343
344
344
345 class RhodeCodeUi(Base, BaseModel):
345 class RhodeCodeUi(Base, BaseModel):
346 __tablename__ = 'rhodecode_ui'
346 __tablename__ = 'rhodecode_ui'
347 __table_args__ = (
347 __table_args__ = (
348 UniqueConstraint('ui_key'),
348 UniqueConstraint('ui_key'),
349 {'extend_existing': True, 'mysql_engine': 'InnoDB',
349 {'extend_existing': True, 'mysql_engine': 'InnoDB',
350 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
350 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
351 )
351 )
352
352
353 HOOK_REPO_SIZE = 'changegroup.repo_size'
353 HOOK_REPO_SIZE = 'changegroup.repo_size'
354 # HG
354 # HG
355 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
355 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
356 HOOK_PULL = 'outgoing.pull_logger'
356 HOOK_PULL = 'outgoing.pull_logger'
357 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
357 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
358 HOOK_PUSH = 'changegroup.push_logger'
358 HOOK_PUSH = 'changegroup.push_logger'
359
359
360 # TODO: johbo: Unify way how hooks are configured for git and hg,
360 # TODO: johbo: Unify way how hooks are configured for git and hg,
361 # git part is currently hardcoded.
361 # git part is currently hardcoded.
362
362
363 # SVN PATTERNS
363 # SVN PATTERNS
364 SVN_BRANCH_ID = 'vcs_svn_branch'
364 SVN_BRANCH_ID = 'vcs_svn_branch'
365 SVN_TAG_ID = 'vcs_svn_tag'
365 SVN_TAG_ID = 'vcs_svn_tag'
366
366
367 ui_id = Column(
367 ui_id = Column(
368 "ui_id", Integer(), nullable=False, unique=True, default=None,
368 "ui_id", Integer(), nullable=False, unique=True, default=None,
369 primary_key=True)
369 primary_key=True)
370 ui_section = Column(
370 ui_section = Column(
371 "ui_section", String(255), nullable=True, unique=None, default=None)
371 "ui_section", String(255), nullable=True, unique=None, default=None)
372 ui_key = Column(
372 ui_key = Column(
373 "ui_key", String(255), nullable=True, unique=None, default=None)
373 "ui_key", String(255), nullable=True, unique=None, default=None)
374 ui_value = Column(
374 ui_value = Column(
375 "ui_value", String(255), nullable=True, unique=None, default=None)
375 "ui_value", String(255), nullable=True, unique=None, default=None)
376 ui_active = Column(
376 ui_active = Column(
377 "ui_active", Boolean(), nullable=True, unique=None, default=True)
377 "ui_active", Boolean(), nullable=True, unique=None, default=True)
378
378
379 def __repr__(self):
379 def __repr__(self):
380 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
380 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
381 self.ui_key, self.ui_value)
381 self.ui_key, self.ui_value)
382
382
383
383
384 class RepoRhodeCodeSetting(Base, BaseModel):
384 class RepoRhodeCodeSetting(Base, BaseModel):
385 __tablename__ = 'repo_rhodecode_settings'
385 __tablename__ = 'repo_rhodecode_settings'
386 __table_args__ = (
386 __table_args__ = (
387 UniqueConstraint(
387 UniqueConstraint(
388 'app_settings_name', 'repository_id',
388 'app_settings_name', 'repository_id',
389 name='uq_repo_rhodecode_setting_name_repo_id'),
389 name='uq_repo_rhodecode_setting_name_repo_id'),
390 {'extend_existing': True, 'mysql_engine': 'InnoDB',
390 {'extend_existing': True, 'mysql_engine': 'InnoDB',
391 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
391 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
392 )
392 )
393
393
394 repository_id = Column(
394 repository_id = Column(
395 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
395 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
396 nullable=False)
396 nullable=False)
397 app_settings_id = Column(
397 app_settings_id = Column(
398 "app_settings_id", Integer(), nullable=False, unique=True,
398 "app_settings_id", Integer(), nullable=False, unique=True,
399 default=None, primary_key=True)
399 default=None, primary_key=True)
400 app_settings_name = Column(
400 app_settings_name = Column(
401 "app_settings_name", String(255), nullable=True, unique=None,
401 "app_settings_name", String(255), nullable=True, unique=None,
402 default=None)
402 default=None)
403 _app_settings_value = Column(
403 _app_settings_value = Column(
404 "app_settings_value", String(4096), nullable=True, unique=None,
404 "app_settings_value", String(4096), nullable=True, unique=None,
405 default=None)
405 default=None)
406 _app_settings_type = Column(
406 _app_settings_type = Column(
407 "app_settings_type", String(255), nullable=True, unique=None,
407 "app_settings_type", String(255), nullable=True, unique=None,
408 default=None)
408 default=None)
409
409
410 repository = relationship('Repository')
410 repository = relationship('Repository')
411
411
412 def __init__(self, repository_id, key='', val='', type='unicode'):
412 def __init__(self, repository_id, key='', val='', type='unicode'):
413 self.repository_id = repository_id
413 self.repository_id = repository_id
414 self.app_settings_name = key
414 self.app_settings_name = key
415 self.app_settings_type = type
415 self.app_settings_type = type
416 self.app_settings_value = val
416 self.app_settings_value = val
417
417
418 @validates('_app_settings_value')
418 @validates('_app_settings_value')
419 def validate_settings_value(self, key, val):
419 def validate_settings_value(self, key, val):
420 assert type(val) == unicode
420 assert type(val) == unicode
421 return val
421 return val
422
422
423 @hybrid_property
423 @hybrid_property
424 def app_settings_value(self):
424 def app_settings_value(self):
425 v = self._app_settings_value
425 v = self._app_settings_value
426 type_ = self.app_settings_type
426 type_ = self.app_settings_type
427 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
427 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
428 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
428 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
429 return converter(v)
429 return converter(v)
430
430
431 @app_settings_value.setter
431 @app_settings_value.setter
432 def app_settings_value(self, val):
432 def app_settings_value(self, val):
433 """
433 """
434 Setter that will always make sure we use unicode in app_settings_value
434 Setter that will always make sure we use unicode in app_settings_value
435
435
436 :param val:
436 :param val:
437 """
437 """
438 self._app_settings_value = safe_unicode(val)
438 self._app_settings_value = safe_unicode(val)
439
439
440 @hybrid_property
440 @hybrid_property
441 def app_settings_type(self):
441 def app_settings_type(self):
442 return self._app_settings_type
442 return self._app_settings_type
443
443
444 @app_settings_type.setter
444 @app_settings_type.setter
445 def app_settings_type(self, val):
445 def app_settings_type(self, val):
446 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
446 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
447 if val not in SETTINGS_TYPES:
447 if val not in SETTINGS_TYPES:
448 raise Exception('type must be one of %s got %s'
448 raise Exception('type must be one of %s got %s'
449 % (SETTINGS_TYPES.keys(), val))
449 % (SETTINGS_TYPES.keys(), val))
450 self._app_settings_type = val
450 self._app_settings_type = val
451
451
452 def __unicode__(self):
452 def __unicode__(self):
453 return u"<%s('%s:%s:%s[%s]')>" % (
453 return u"<%s('%s:%s:%s[%s]')>" % (
454 self.__class__.__name__, self.repository.repo_name,
454 self.__class__.__name__, self.repository.repo_name,
455 self.app_settings_name, self.app_settings_value,
455 self.app_settings_name, self.app_settings_value,
456 self.app_settings_type
456 self.app_settings_type
457 )
457 )
458
458
459
459
460 class RepoRhodeCodeUi(Base, BaseModel):
460 class RepoRhodeCodeUi(Base, BaseModel):
461 __tablename__ = 'repo_rhodecode_ui'
461 __tablename__ = 'repo_rhodecode_ui'
462 __table_args__ = (
462 __table_args__ = (
463 UniqueConstraint(
463 UniqueConstraint(
464 'repository_id', 'ui_section', 'ui_key',
464 'repository_id', 'ui_section', 'ui_key',
465 name='uq_repo_rhodecode_ui_repository_id_section_key'),
465 name='uq_repo_rhodecode_ui_repository_id_section_key'),
466 {'extend_existing': True, 'mysql_engine': 'InnoDB',
466 {'extend_existing': True, 'mysql_engine': 'InnoDB',
467 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
467 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
468 )
468 )
469
469
470 repository_id = Column(
470 repository_id = Column(
471 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
471 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
472 nullable=False)
472 nullable=False)
473 ui_id = Column(
473 ui_id = Column(
474 "ui_id", Integer(), nullable=False, unique=True, default=None,
474 "ui_id", Integer(), nullable=False, unique=True, default=None,
475 primary_key=True)
475 primary_key=True)
476 ui_section = Column(
476 ui_section = Column(
477 "ui_section", String(255), nullable=True, unique=None, default=None)
477 "ui_section", String(255), nullable=True, unique=None, default=None)
478 ui_key = Column(
478 ui_key = Column(
479 "ui_key", String(255), nullable=True, unique=None, default=None)
479 "ui_key", String(255), nullable=True, unique=None, default=None)
480 ui_value = Column(
480 ui_value = Column(
481 "ui_value", String(255), nullable=True, unique=None, default=None)
481 "ui_value", String(255), nullable=True, unique=None, default=None)
482 ui_active = Column(
482 ui_active = Column(
483 "ui_active", Boolean(), nullable=True, unique=None, default=True)
483 "ui_active", Boolean(), nullable=True, unique=None, default=True)
484
484
485 repository = relationship('Repository')
485 repository = relationship('Repository')
486
486
487 def __repr__(self):
487 def __repr__(self):
488 return '<%s[%s:%s]%s=>%s]>' % (
488 return '<%s[%s:%s]%s=>%s]>' % (
489 self.__class__.__name__, self.repository.repo_name,
489 self.__class__.__name__, self.repository.repo_name,
490 self.ui_section, self.ui_key, self.ui_value)
490 self.ui_section, self.ui_key, self.ui_value)
491
491
492
492
493 class User(Base, BaseModel):
493 class User(Base, BaseModel):
494 __tablename__ = 'users'
494 __tablename__ = 'users'
495 __table_args__ = (
495 __table_args__ = (
496 UniqueConstraint('username'), UniqueConstraint('email'),
496 UniqueConstraint('username'), UniqueConstraint('email'),
497 Index('u_username_idx', 'username'),
497 Index('u_username_idx', 'username'),
498 Index('u_email_idx', 'email'),
498 Index('u_email_idx', 'email'),
499 {'extend_existing': True, 'mysql_engine': 'InnoDB',
499 {'extend_existing': True, 'mysql_engine': 'InnoDB',
500 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
500 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
501 )
501 )
502 DEFAULT_USER = 'default'
502 DEFAULT_USER = 'default'
503 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
503 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
504 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
504 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
505
505
506 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
506 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
507 username = Column("username", String(255), nullable=True, unique=None, default=None)
507 username = Column("username", String(255), nullable=True, unique=None, default=None)
508 password = Column("password", String(255), nullable=True, unique=None, default=None)
508 password = Column("password", String(255), nullable=True, unique=None, default=None)
509 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
509 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
510 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
510 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
511 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
511 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
512 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
512 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
513 _email = Column("email", String(255), nullable=True, unique=None, default=None)
513 _email = Column("email", String(255), nullable=True, unique=None, default=None)
514 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
514 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
515 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
515 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
516 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
516 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
517 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
517 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
518 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
518 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
519 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
519 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
520 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
520 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
521
521
522 user_log = relationship('UserLog')
522 user_log = relationship('UserLog')
523 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
523 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
524
524
525 repositories = relationship('Repository')
525 repositories = relationship('Repository')
526 repository_groups = relationship('RepoGroup')
526 repository_groups = relationship('RepoGroup')
527 user_groups = relationship('UserGroup')
527 user_groups = relationship('UserGroup')
528
528
529 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
529 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
530 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
530 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
531
531
532 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
532 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
533 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
533 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
534 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
534 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
535
535
536 group_member = relationship('UserGroupMember', cascade='all')
536 group_member = relationship('UserGroupMember', cascade='all')
537
537
538 notifications = relationship('UserNotification', cascade='all')
538 notifications = relationship('UserNotification', cascade='all')
539 # notifications assigned to this user
539 # notifications assigned to this user
540 user_created_notifications = relationship('Notification', cascade='all')
540 user_created_notifications = relationship('Notification', cascade='all')
541 # comments created by this user
541 # comments created by this user
542 user_comments = relationship('ChangesetComment', cascade='all')
542 user_comments = relationship('ChangesetComment', cascade='all')
543 # user profile extra info
543 # user profile extra info
544 user_emails = relationship('UserEmailMap', cascade='all')
544 user_emails = relationship('UserEmailMap', cascade='all')
545 user_ip_map = relationship('UserIpMap', cascade='all')
545 user_ip_map = relationship('UserIpMap', cascade='all')
546 user_auth_tokens = relationship('UserApiKeys', cascade='all')
546 user_auth_tokens = relationship('UserApiKeys', cascade='all')
547 # gists
547 # gists
548 user_gists = relationship('Gist', cascade='all')
548 user_gists = relationship('Gist', cascade='all')
549 # user pull requests
549 # user pull requests
550 user_pull_requests = relationship('PullRequest', cascade='all')
550 user_pull_requests = relationship('PullRequest', cascade='all')
551 # external identities
551 # external identities
552 extenal_identities = relationship(
552 extenal_identities = relationship(
553 'ExternalIdentity',
553 'ExternalIdentity',
554 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
554 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
555 cascade='all')
555 cascade='all')
556
556
557 def __unicode__(self):
557 def __unicode__(self):
558 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
558 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
559 self.user_id, self.username)
559 self.user_id, self.username)
560
560
561 @hybrid_property
561 @hybrid_property
562 def email(self):
562 def email(self):
563 return self._email
563 return self._email
564
564
565 @email.setter
565 @email.setter
566 def email(self, val):
566 def email(self, val):
567 self._email = val.lower() if val else None
567 self._email = val.lower() if val else None
568
568
569 @property
569 @property
570 def firstname(self):
570 def firstname(self):
571 # alias for future
571 # alias for future
572 return self.name
572 return self.name
573
573
574 @property
574 @property
575 def emails(self):
575 def emails(self):
576 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
576 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
577 return [self.email] + [x.email for x in other]
577 return [self.email] + [x.email for x in other]
578
578
579 @property
579 @property
580 def auth_tokens(self):
580 def auth_tokens(self):
581 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
581 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
582
582
583 @property
583 @property
584 def extra_auth_tokens(self):
584 def extra_auth_tokens(self):
585 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
585 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
586
586
587 @property
587 @property
588 def feed_token(self):
588 def feed_token(self):
589 feed_tokens = UserApiKeys.query()\
589 feed_tokens = UserApiKeys.query()\
590 .filter(UserApiKeys.user == self)\
590 .filter(UserApiKeys.user == self)\
591 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
591 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
592 .all()
592 .all()
593 if feed_tokens:
593 if feed_tokens:
594 return feed_tokens[0].api_key
594 return feed_tokens[0].api_key
595 else:
595 else:
596 # use the main token so we don't end up with nothing...
596 # use the main token so we don't end up with nothing...
597 return self.api_key
597 return self.api_key
598
598
599 @classmethod
599 @classmethod
600 def extra_valid_auth_tokens(cls, user, role=None):
600 def extra_valid_auth_tokens(cls, user, role=None):
601 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
601 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
602 .filter(or_(UserApiKeys.expires == -1,
602 .filter(or_(UserApiKeys.expires == -1,
603 UserApiKeys.expires >= time.time()))
603 UserApiKeys.expires >= time.time()))
604 if role:
604 if role:
605 tokens = tokens.filter(or_(UserApiKeys.role == role,
605 tokens = tokens.filter(or_(UserApiKeys.role == role,
606 UserApiKeys.role == UserApiKeys.ROLE_ALL))
606 UserApiKeys.role == UserApiKeys.ROLE_ALL))
607 return tokens.all()
607 return tokens.all()
608
608
609 @property
609 @property
610 def ip_addresses(self):
610 def ip_addresses(self):
611 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
611 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
612 return [x.ip_addr for x in ret]
612 return [x.ip_addr for x in ret]
613
613
614 @property
614 @property
615 def username_and_name(self):
615 def username_and_name(self):
616 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
616 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
617
617
618 @property
618 @property
619 def username_or_name_or_email(self):
619 def username_or_name_or_email(self):
620 full_name = self.full_name if self.full_name is not ' ' else None
620 full_name = self.full_name if self.full_name is not ' ' else None
621 return self.username or full_name or self.email
621 return self.username or full_name or self.email
622
622
623 @property
623 @property
624 def full_name(self):
624 def full_name(self):
625 return '%s %s' % (self.firstname, self.lastname)
625 return '%s %s' % (self.firstname, self.lastname)
626
626
627 @property
627 @property
628 def full_name_or_username(self):
628 def full_name_or_username(self):
629 return ('%s %s' % (self.firstname, self.lastname)
629 return ('%s %s' % (self.firstname, self.lastname)
630 if (self.firstname and self.lastname) else self.username)
630 if (self.firstname and self.lastname) else self.username)
631
631
632 @property
632 @property
633 def full_contact(self):
633 def full_contact(self):
634 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
634 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
635
635
636 @property
636 @property
637 def short_contact(self):
637 def short_contact(self):
638 return '%s %s' % (self.firstname, self.lastname)
638 return '%s %s' % (self.firstname, self.lastname)
639
639
640 @property
640 @property
641 def is_admin(self):
641 def is_admin(self):
642 return self.admin
642 return self.admin
643
643
644 @property
644 @property
645 def AuthUser(self):
645 def AuthUser(self):
646 """
646 """
647 Returns instance of AuthUser for this user
647 Returns instance of AuthUser for this user
648 """
648 """
649 from rhodecode.lib.auth import AuthUser
649 from rhodecode.lib.auth import AuthUser
650 return AuthUser(user_id=self.user_id, api_key=self.api_key,
650 return AuthUser(user_id=self.user_id, api_key=self.api_key,
651 username=self.username)
651 username=self.username)
652
652
653 @hybrid_property
653 @hybrid_property
654 def user_data(self):
654 def user_data(self):
655 if not self._user_data:
655 if not self._user_data:
656 return {}
656 return {}
657
657
658 try:
658 try:
659 return json.loads(self._user_data)
659 return json.loads(self._user_data)
660 except TypeError:
660 except TypeError:
661 return {}
661 return {}
662
662
663 @user_data.setter
663 @user_data.setter
664 def user_data(self, val):
664 def user_data(self, val):
665 if not isinstance(val, dict):
665 if not isinstance(val, dict):
666 raise Exception('user_data must be dict, got %s' % type(val))
666 raise Exception('user_data must be dict, got %s' % type(val))
667 try:
667 try:
668 self._user_data = json.dumps(val)
668 self._user_data = json.dumps(val)
669 except Exception:
669 except Exception:
670 log.error(traceback.format_exc())
670 log.error(traceback.format_exc())
671
671
672 @classmethod
672 @classmethod
673 def get_by_username(cls, username, case_insensitive=False,
673 def get_by_username(cls, username, case_insensitive=False,
674 cache=False, identity_cache=False):
674 cache=False, identity_cache=False):
675 session = Session()
675 session = Session()
676
676
677 if case_insensitive:
677 if case_insensitive:
678 q = cls.query().filter(
678 q = cls.query().filter(
679 func.lower(cls.username) == func.lower(username))
679 func.lower(cls.username) == func.lower(username))
680 else:
680 else:
681 q = cls.query().filter(cls.username == username)
681 q = cls.query().filter(cls.username == username)
682
682
683 if cache:
683 if cache:
684 if identity_cache:
684 if identity_cache:
685 val = cls.identity_cache(session, 'username', username)
685 val = cls.identity_cache(session, 'username', username)
686 if val:
686 if val:
687 return val
687 return val
688 else:
688 else:
689 q = q.options(
689 q = q.options(
690 FromCache("sql_cache_short",
690 FromCache("sql_cache_short",
691 "get_user_by_name_%s" % _hash_key(username)))
691 "get_user_by_name_%s" % _hash_key(username)))
692
692
693 return q.scalar()
693 return q.scalar()
694
694
695 @classmethod
695 @classmethod
696 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
696 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
697 q = cls.query().filter(cls.api_key == auth_token)
697 q = cls.query().filter(cls.api_key == auth_token)
698
698
699 if cache:
699 if cache:
700 q = q.options(FromCache("sql_cache_short",
700 q = q.options(FromCache("sql_cache_short",
701 "get_auth_token_%s" % auth_token))
701 "get_auth_token_%s" % auth_token))
702 res = q.scalar()
702 res = q.scalar()
703
703
704 if fallback and not res:
704 if fallback and not res:
705 #fallback to additional keys
705 #fallback to additional keys
706 _res = UserApiKeys.query()\
706 _res = UserApiKeys.query()\
707 .filter(UserApiKeys.api_key == auth_token)\
707 .filter(UserApiKeys.api_key == auth_token)\
708 .filter(or_(UserApiKeys.expires == -1,
708 .filter(or_(UserApiKeys.expires == -1,
709 UserApiKeys.expires >= time.time()))\
709 UserApiKeys.expires >= time.time()))\
710 .first()
710 .first()
711 if _res:
711 if _res:
712 res = _res.user
712 res = _res.user
713 return res
713 return res
714
714
715 @classmethod
715 @classmethod
716 def get_by_email(cls, email, case_insensitive=False, cache=False):
716 def get_by_email(cls, email, case_insensitive=False, cache=False):
717
717
718 if case_insensitive:
718 if case_insensitive:
719 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
719 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
720
720
721 else:
721 else:
722 q = cls.query().filter(cls.email == email)
722 q = cls.query().filter(cls.email == email)
723
723
724 if cache:
724 if cache:
725 q = q.options(FromCache("sql_cache_short",
725 q = q.options(FromCache("sql_cache_short",
726 "get_email_key_%s" % _hash_key(email)))
726 "get_email_key_%s" % _hash_key(email)))
727
727
728 ret = q.scalar()
728 ret = q.scalar()
729 if ret is None:
729 if ret is None:
730 q = UserEmailMap.query()
730 q = UserEmailMap.query()
731 # try fetching in alternate email map
731 # try fetching in alternate email map
732 if case_insensitive:
732 if case_insensitive:
733 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
733 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
734 else:
734 else:
735 q = q.filter(UserEmailMap.email == email)
735 q = q.filter(UserEmailMap.email == email)
736 q = q.options(joinedload(UserEmailMap.user))
736 q = q.options(joinedload(UserEmailMap.user))
737 if cache:
737 if cache:
738 q = q.options(FromCache("sql_cache_short",
738 q = q.options(FromCache("sql_cache_short",
739 "get_email_map_key_%s" % email))
739 "get_email_map_key_%s" % email))
740 ret = getattr(q.scalar(), 'user', None)
740 ret = getattr(q.scalar(), 'user', None)
741
741
742 return ret
742 return ret
743
743
744 @classmethod
744 @classmethod
745 def get_from_cs_author(cls, author):
745 def get_from_cs_author(cls, author):
746 """
746 """
747 Tries to get User objects out of commit author string
747 Tries to get User objects out of commit author string
748
748
749 :param author:
749 :param author:
750 """
750 """
751 from rhodecode.lib.helpers import email, author_name
751 from rhodecode.lib.helpers import email, author_name
752 # Valid email in the attribute passed, see if they're in the system
752 # Valid email in the attribute passed, see if they're in the system
753 _email = email(author)
753 _email = email(author)
754 if _email:
754 if _email:
755 user = cls.get_by_email(_email, case_insensitive=True)
755 user = cls.get_by_email(_email, case_insensitive=True)
756 if user:
756 if user:
757 return user
757 return user
758 # Maybe we can match by username?
758 # Maybe we can match by username?
759 _author = author_name(author)
759 _author = author_name(author)
760 user = cls.get_by_username(_author, case_insensitive=True)
760 user = cls.get_by_username(_author, case_insensitive=True)
761 if user:
761 if user:
762 return user
762 return user
763
763
764 def update_userdata(self, **kwargs):
764 def update_userdata(self, **kwargs):
765 usr = self
765 usr = self
766 old = usr.user_data
766 old = usr.user_data
767 old.update(**kwargs)
767 old.update(**kwargs)
768 usr.user_data = old
768 usr.user_data = old
769 Session().add(usr)
769 Session().add(usr)
770 log.debug('updated userdata with ', kwargs)
770 log.debug('updated userdata with ', kwargs)
771
771
772 def update_lastlogin(self):
772 def update_lastlogin(self):
773 """Update user lastlogin"""
773 """Update user lastlogin"""
774 self.last_login = datetime.datetime.now()
774 self.last_login = datetime.datetime.now()
775 Session().add(self)
775 Session().add(self)
776 log.debug('updated user %s lastlogin', self.username)
776 log.debug('updated user %s lastlogin', self.username)
777
777
778 def update_lastactivity(self):
778 def update_lastactivity(self):
779 """Update user lastactivity"""
779 """Update user lastactivity"""
780 usr = self
780 usr = self
781 old = usr.user_data
781 old = usr.user_data
782 old.update({'last_activity': time.time()})
782 old.update({'last_activity': time.time()})
783 usr.user_data = old
783 usr.user_data = old
784 Session().add(usr)
784 Session().add(usr)
785 log.debug('updated user %s lastactivity', usr.username)
785 log.debug('updated user %s lastactivity', usr.username)
786
786
787 def update_password(self, new_password, change_api_key=False):
787 def update_password(self, new_password, change_api_key=False):
788 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
788 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
789
789
790 self.password = get_crypt_password(new_password)
790 self.password = get_crypt_password(new_password)
791 if change_api_key:
791 if change_api_key:
792 self.api_key = generate_auth_token(self.username)
792 self.api_key = generate_auth_token(self.username)
793 Session().add(self)
793 Session().add(self)
794
794
795 @classmethod
795 @classmethod
796 def get_first_super_admin(cls):
796 def get_first_super_admin(cls):
797 user = User.query().filter(User.admin == true()).first()
797 user = User.query().filter(User.admin == true()).first()
798 if user is None:
798 if user is None:
799 raise Exception('FATAL: Missing administrative account!')
799 raise Exception('FATAL: Missing administrative account!')
800 return user
800 return user
801
801
802 @classmethod
802 @classmethod
803 def get_all_super_admins(cls):
803 def get_all_super_admins(cls):
804 """
804 """
805 Returns all admin accounts sorted by username
805 Returns all admin accounts sorted by username
806 """
806 """
807 return User.query().filter(User.admin == true())\
807 return User.query().filter(User.admin == true())\
808 .order_by(User.username.asc()).all()
808 .order_by(User.username.asc()).all()
809
809
810 @classmethod
810 @classmethod
811 def get_default_user(cls, cache=False):
811 def get_default_user(cls, cache=False):
812 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
812 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
813 if user is None:
813 if user is None:
814 raise Exception('FATAL: Missing default account!')
814 raise Exception('FATAL: Missing default account!')
815 return user
815 return user
816
816
817 def _get_default_perms(self, user, suffix=''):
817 def _get_default_perms(self, user, suffix=''):
818 from rhodecode.model.permission import PermissionModel
818 from rhodecode.model.permission import PermissionModel
819 return PermissionModel().get_default_perms(user.user_perms, suffix)
819 return PermissionModel().get_default_perms(user.user_perms, suffix)
820
820
821 def get_default_perms(self, suffix=''):
821 def get_default_perms(self, suffix=''):
822 return self._get_default_perms(self, suffix)
822 return self._get_default_perms(self, suffix)
823
823
824 def get_api_data(self, include_secrets=False, details='full'):
824 def get_api_data(self, include_secrets=False, details='full'):
825 """
825 """
826 Common function for generating user related data for API
826 Common function for generating user related data for API
827
827
828 :param include_secrets: By default secrets in the API data will be replaced
828 :param include_secrets: By default secrets in the API data will be replaced
829 by a placeholder value to prevent exposing this data by accident. In case
829 by a placeholder value to prevent exposing this data by accident. In case
830 this data shall be exposed, set this flag to ``True``.
830 this data shall be exposed, set this flag to ``True``.
831
831
832 :param details: details can be 'basic|full' basic gives only a subset of
832 :param details: details can be 'basic|full' basic gives only a subset of
833 the available user information that includes user_id, name and emails.
833 the available user information that includes user_id, name and emails.
834 """
834 """
835 user = self
835 user = self
836 user_data = self.user_data
836 user_data = self.user_data
837 data = {
837 data = {
838 'user_id': user.user_id,
838 'user_id': user.user_id,
839 'username': user.username,
839 'username': user.username,
840 'firstname': user.name,
840 'firstname': user.name,
841 'lastname': user.lastname,
841 'lastname': user.lastname,
842 'email': user.email,
842 'email': user.email,
843 'emails': user.emails,
843 'emails': user.emails,
844 }
844 }
845 if details == 'basic':
845 if details == 'basic':
846 return data
846 return data
847
847
848 api_key_length = 40
848 api_key_length = 40
849 api_key_replacement = '*' * api_key_length
849 api_key_replacement = '*' * api_key_length
850
850
851 extras = {
851 extras = {
852 'api_key': api_key_replacement,
852 'api_key': api_key_replacement,
853 'api_keys': [api_key_replacement],
853 'api_keys': [api_key_replacement],
854 'active': user.active,
854 'active': user.active,
855 'admin': user.admin,
855 'admin': user.admin,
856 'extern_type': user.extern_type,
856 'extern_type': user.extern_type,
857 'extern_name': user.extern_name,
857 'extern_name': user.extern_name,
858 'last_login': user.last_login,
858 'last_login': user.last_login,
859 'ip_addresses': user.ip_addresses,
859 'ip_addresses': user.ip_addresses,
860 'language': user_data.get('language')
860 'language': user_data.get('language')
861 }
861 }
862 data.update(extras)
862 data.update(extras)
863
863
864 if include_secrets:
864 if include_secrets:
865 data['api_key'] = user.api_key
865 data['api_key'] = user.api_key
866 data['api_keys'] = user.auth_tokens
866 data['api_keys'] = user.auth_tokens
867 return data
867 return data
868
868
869 def __json__(self):
869 def __json__(self):
870 data = {
870 data = {
871 'full_name': self.full_name,
871 'full_name': self.full_name,
872 'full_name_or_username': self.full_name_or_username,
872 'full_name_or_username': self.full_name_or_username,
873 'short_contact': self.short_contact,
873 'short_contact': self.short_contact,
874 'full_contact': self.full_contact,
874 'full_contact': self.full_contact,
875 }
875 }
876 data.update(self.get_api_data())
876 data.update(self.get_api_data())
877 return data
877 return data
878
878
879
879
880 class UserApiKeys(Base, BaseModel):
880 class UserApiKeys(Base, BaseModel):
881 __tablename__ = 'user_api_keys'
881 __tablename__ = 'user_api_keys'
882 __table_args__ = (
882 __table_args__ = (
883 Index('uak_api_key_idx', 'api_key'),
883 Index('uak_api_key_idx', 'api_key'),
884 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
884 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
885 UniqueConstraint('api_key'),
885 UniqueConstraint('api_key'),
886 {'extend_existing': True, 'mysql_engine': 'InnoDB',
886 {'extend_existing': True, 'mysql_engine': 'InnoDB',
887 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
887 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
888 )
888 )
889 __mapper_args__ = {}
889 __mapper_args__ = {}
890
890
891 # ApiKey role
891 # ApiKey role
892 ROLE_ALL = 'token_role_all'
892 ROLE_ALL = 'token_role_all'
893 ROLE_HTTP = 'token_role_http'
893 ROLE_HTTP = 'token_role_http'
894 ROLE_VCS = 'token_role_vcs'
894 ROLE_VCS = 'token_role_vcs'
895 ROLE_API = 'token_role_api'
895 ROLE_API = 'token_role_api'
896 ROLE_FEED = 'token_role_feed'
896 ROLE_FEED = 'token_role_feed'
897 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
897 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
898
898
899 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
899 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
900 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
900 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
901 api_key = Column("api_key", String(255), nullable=False, unique=True)
901 api_key = Column("api_key", String(255), nullable=False, unique=True)
902 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
902 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
903 expires = Column('expires', Float(53), nullable=False)
903 expires = Column('expires', Float(53), nullable=False)
904 role = Column('role', String(255), nullable=True)
904 role = Column('role', String(255), nullable=True)
905 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
905 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
906
906
907 user = relationship('User', lazy='joined')
907 user = relationship('User', lazy='joined')
908
908
909 @classmethod
909 @classmethod
910 def _get_role_name(cls, role):
910 def _get_role_name(cls, role):
911 return {
911 return {
912 cls.ROLE_ALL: _('all'),
912 cls.ROLE_ALL: _('all'),
913 cls.ROLE_HTTP: _('http/web interface'),
913 cls.ROLE_HTTP: _('http/web interface'),
914 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
914 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
915 cls.ROLE_API: _('api calls'),
915 cls.ROLE_API: _('api calls'),
916 cls.ROLE_FEED: _('feed access'),
916 cls.ROLE_FEED: _('feed access'),
917 }.get(role, role)
917 }.get(role, role)
918
918
919 @property
919 @property
920 def expired(self):
920 def expired(self):
921 if self.expires == -1:
921 if self.expires == -1:
922 return False
922 return False
923 return time.time() > self.expires
923 return time.time() > self.expires
924
924
925 @property
925 @property
926 def role_humanized(self):
926 def role_humanized(self):
927 return self._get_role_name(self.role)
927 return self._get_role_name(self.role)
928
928
929
929
930 class UserEmailMap(Base, BaseModel):
930 class UserEmailMap(Base, BaseModel):
931 __tablename__ = 'user_email_map'
931 __tablename__ = 'user_email_map'
932 __table_args__ = (
932 __table_args__ = (
933 Index('uem_email_idx', 'email'),
933 Index('uem_email_idx', 'email'),
934 UniqueConstraint('email'),
934 UniqueConstraint('email'),
935 {'extend_existing': True, 'mysql_engine': 'InnoDB',
935 {'extend_existing': True, 'mysql_engine': 'InnoDB',
936 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
936 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
937 )
937 )
938 __mapper_args__ = {}
938 __mapper_args__ = {}
939
939
940 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
940 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
941 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
941 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
942 _email = Column("email", String(255), nullable=True, unique=False, default=None)
942 _email = Column("email", String(255), nullable=True, unique=False, default=None)
943 user = relationship('User', lazy='joined')
943 user = relationship('User', lazy='joined')
944
944
945 @validates('_email')
945 @validates('_email')
946 def validate_email(self, key, email):
946 def validate_email(self, key, email):
947 # check if this email is not main one
947 # check if this email is not main one
948 main_email = Session().query(User).filter(User.email == email).scalar()
948 main_email = Session().query(User).filter(User.email == email).scalar()
949 if main_email is not None:
949 if main_email is not None:
950 raise AttributeError('email %s is present is user table' % email)
950 raise AttributeError('email %s is present is user table' % email)
951 return email
951 return email
952
952
953 @hybrid_property
953 @hybrid_property
954 def email(self):
954 def email(self):
955 return self._email
955 return self._email
956
956
957 @email.setter
957 @email.setter
958 def email(self, val):
958 def email(self, val):
959 self._email = val.lower() if val else None
959 self._email = val.lower() if val else None
960
960
961
961
962 class UserIpMap(Base, BaseModel):
962 class UserIpMap(Base, BaseModel):
963 __tablename__ = 'user_ip_map'
963 __tablename__ = 'user_ip_map'
964 __table_args__ = (
964 __table_args__ = (
965 UniqueConstraint('user_id', 'ip_addr'),
965 UniqueConstraint('user_id', 'ip_addr'),
966 {'extend_existing': True, 'mysql_engine': 'InnoDB',
966 {'extend_existing': True, 'mysql_engine': 'InnoDB',
967 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
967 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
968 )
968 )
969 __mapper_args__ = {}
969 __mapper_args__ = {}
970
970
971 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
971 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
972 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
972 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
973 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
973 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
974 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
974 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
975 description = Column("description", String(10000), nullable=True, unique=None, default=None)
975 description = Column("description", String(10000), nullable=True, unique=None, default=None)
976 user = relationship('User', lazy='joined')
976 user = relationship('User', lazy='joined')
977
977
978 @classmethod
978 @classmethod
979 def _get_ip_range(cls, ip_addr):
979 def _get_ip_range(cls, ip_addr):
980 net = ipaddress.ip_network(ip_addr, strict=False)
980 net = ipaddress.ip_network(ip_addr, strict=False)
981 return [str(net.network_address), str(net.broadcast_address)]
981 return [str(net.network_address), str(net.broadcast_address)]
982
982
983 def __json__(self):
983 def __json__(self):
984 return {
984 return {
985 'ip_addr': self.ip_addr,
985 'ip_addr': self.ip_addr,
986 'ip_range': self._get_ip_range(self.ip_addr),
986 'ip_range': self._get_ip_range(self.ip_addr),
987 }
987 }
988
988
989 def __unicode__(self):
989 def __unicode__(self):
990 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
990 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
991 self.user_id, self.ip_addr)
991 self.user_id, self.ip_addr)
992
992
993 class UserLog(Base, BaseModel):
993 class UserLog(Base, BaseModel):
994 __tablename__ = 'user_logs'
994 __tablename__ = 'user_logs'
995 __table_args__ = (
995 __table_args__ = (
996 {'extend_existing': True, 'mysql_engine': 'InnoDB',
996 {'extend_existing': True, 'mysql_engine': 'InnoDB',
997 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
997 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
998 )
998 )
999 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
999 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1000 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1000 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1001 username = Column("username", String(255), nullable=True, unique=None, default=None)
1001 username = Column("username", String(255), nullable=True, unique=None, default=None)
1002 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1002 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1003 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1003 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1004 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1004 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1005 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1005 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1006 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1006 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1007
1007
1008 def __unicode__(self):
1008 def __unicode__(self):
1009 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1009 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1010 self.repository_name,
1010 self.repository_name,
1011 self.action)
1011 self.action)
1012
1012
1013 @property
1013 @property
1014 def action_as_day(self):
1014 def action_as_day(self):
1015 return datetime.date(*self.action_date.timetuple()[:3])
1015 return datetime.date(*self.action_date.timetuple()[:3])
1016
1016
1017 user = relationship('User')
1017 user = relationship('User')
1018 repository = relationship('Repository', cascade='')
1018 repository = relationship('Repository', cascade='')
1019
1019
1020
1020
1021 class UserGroup(Base, BaseModel):
1021 class UserGroup(Base, BaseModel):
1022 __tablename__ = 'users_groups'
1022 __tablename__ = 'users_groups'
1023 __table_args__ = (
1023 __table_args__ = (
1024 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1024 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1025 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1025 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1026 )
1026 )
1027
1027
1028 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1028 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1029 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1029 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1030 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1030 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1031 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1031 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1032 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1032 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1033 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1033 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1034 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1034 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1035 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1035 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1036
1036
1037 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1037 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1038 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1038 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1039 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1039 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1040 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1040 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1041 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1041 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1042 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1042 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1043
1043
1044 user = relationship('User')
1044 user = relationship('User')
1045
1045
1046 @hybrid_property
1046 @hybrid_property
1047 def group_data(self):
1047 def group_data(self):
1048 if not self._group_data:
1048 if not self._group_data:
1049 return {}
1049 return {}
1050
1050
1051 try:
1051 try:
1052 return json.loads(self._group_data)
1052 return json.loads(self._group_data)
1053 except TypeError:
1053 except TypeError:
1054 return {}
1054 return {}
1055
1055
1056 @group_data.setter
1056 @group_data.setter
1057 def group_data(self, val):
1057 def group_data(self, val):
1058 try:
1058 try:
1059 self._group_data = json.dumps(val)
1059 self._group_data = json.dumps(val)
1060 except Exception:
1060 except Exception:
1061 log.error(traceback.format_exc())
1061 log.error(traceback.format_exc())
1062
1062
1063 def __unicode__(self):
1063 def __unicode__(self):
1064 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1064 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1065 self.users_group_id,
1065 self.users_group_id,
1066 self.users_group_name)
1066 self.users_group_name)
1067
1067
1068 @classmethod
1068 @classmethod
1069 def get_by_group_name(cls, group_name, cache=False,
1069 def get_by_group_name(cls, group_name, cache=False,
1070 case_insensitive=False):
1070 case_insensitive=False):
1071 if case_insensitive:
1071 if case_insensitive:
1072 q = cls.query().filter(func.lower(cls.users_group_name) ==
1072 q = cls.query().filter(func.lower(cls.users_group_name) ==
1073 func.lower(group_name))
1073 func.lower(group_name))
1074
1074
1075 else:
1075 else:
1076 q = cls.query().filter(cls.users_group_name == group_name)
1076 q = cls.query().filter(cls.users_group_name == group_name)
1077 if cache:
1077 if cache:
1078 q = q.options(FromCache(
1078 q = q.options(FromCache(
1079 "sql_cache_short",
1079 "sql_cache_short",
1080 "get_group_%s" % _hash_key(group_name)))
1080 "get_group_%s" % _hash_key(group_name)))
1081 return q.scalar()
1081 return q.scalar()
1082
1082
1083 @classmethod
1083 @classmethod
1084 def get(cls, user_group_id, cache=False):
1084 def get(cls, user_group_id, cache=False):
1085 user_group = cls.query()
1085 user_group = cls.query()
1086 if cache:
1086 if cache:
1087 user_group = user_group.options(FromCache("sql_cache_short",
1087 user_group = user_group.options(FromCache("sql_cache_short",
1088 "get_users_group_%s" % user_group_id))
1088 "get_users_group_%s" % user_group_id))
1089 return user_group.get(user_group_id)
1089 return user_group.get(user_group_id)
1090
1090
1091 def permissions(self, with_admins=True, with_owner=True):
1091 def permissions(self, with_admins=True, with_owner=True):
1092 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1092 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1093 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1093 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1094 joinedload(UserUserGroupToPerm.user),
1094 joinedload(UserUserGroupToPerm.user),
1095 joinedload(UserUserGroupToPerm.permission),)
1095 joinedload(UserUserGroupToPerm.permission),)
1096
1096
1097 # get owners and admins and permissions. We do a trick of re-writing
1097 # get owners and admins and permissions. We do a trick of re-writing
1098 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1098 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1099 # has a global reference and changing one object propagates to all
1099 # has a global reference and changing one object propagates to all
1100 # others. This means if admin is also an owner admin_row that change
1100 # others. This means if admin is also an owner admin_row that change
1101 # would propagate to both objects
1101 # would propagate to both objects
1102 perm_rows = []
1102 perm_rows = []
1103 for _usr in q.all():
1103 for _usr in q.all():
1104 usr = AttributeDict(_usr.user.get_dict())
1104 usr = AttributeDict(_usr.user.get_dict())
1105 usr.permission = _usr.permission.permission_name
1105 usr.permission = _usr.permission.permission_name
1106 perm_rows.append(usr)
1106 perm_rows.append(usr)
1107
1107
1108 # filter the perm rows by 'default' first and then sort them by
1108 # filter the perm rows by 'default' first and then sort them by
1109 # admin,write,read,none permissions sorted again alphabetically in
1109 # admin,write,read,none permissions sorted again alphabetically in
1110 # each group
1110 # each group
1111 perm_rows = sorted(perm_rows, key=display_sort)
1111 perm_rows = sorted(perm_rows, key=display_sort)
1112
1112
1113 _admin_perm = 'usergroup.admin'
1113 _admin_perm = 'usergroup.admin'
1114 owner_row = []
1114 owner_row = []
1115 if with_owner:
1115 if with_owner:
1116 usr = AttributeDict(self.user.get_dict())
1116 usr = AttributeDict(self.user.get_dict())
1117 usr.owner_row = True
1117 usr.owner_row = True
1118 usr.permission = _admin_perm
1118 usr.permission = _admin_perm
1119 owner_row.append(usr)
1119 owner_row.append(usr)
1120
1120
1121 super_admin_rows = []
1121 super_admin_rows = []
1122 if with_admins:
1122 if with_admins:
1123 for usr in User.get_all_super_admins():
1123 for usr in User.get_all_super_admins():
1124 # if this admin is also owner, don't double the record
1124 # if this admin is also owner, don't double the record
1125 if usr.user_id == owner_row[0].user_id:
1125 if usr.user_id == owner_row[0].user_id:
1126 owner_row[0].admin_row = True
1126 owner_row[0].admin_row = True
1127 else:
1127 else:
1128 usr = AttributeDict(usr.get_dict())
1128 usr = AttributeDict(usr.get_dict())
1129 usr.admin_row = True
1129 usr.admin_row = True
1130 usr.permission = _admin_perm
1130 usr.permission = _admin_perm
1131 super_admin_rows.append(usr)
1131 super_admin_rows.append(usr)
1132
1132
1133 return super_admin_rows + owner_row + perm_rows
1133 return super_admin_rows + owner_row + perm_rows
1134
1134
1135 def permission_user_groups(self):
1135 def permission_user_groups(self):
1136 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1136 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1137 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1137 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1138 joinedload(UserGroupUserGroupToPerm.target_user_group),
1138 joinedload(UserGroupUserGroupToPerm.target_user_group),
1139 joinedload(UserGroupUserGroupToPerm.permission),)
1139 joinedload(UserGroupUserGroupToPerm.permission),)
1140
1140
1141 perm_rows = []
1141 perm_rows = []
1142 for _user_group in q.all():
1142 for _user_group in q.all():
1143 usr = AttributeDict(_user_group.user_group.get_dict())
1143 usr = AttributeDict(_user_group.user_group.get_dict())
1144 usr.permission = _user_group.permission.permission_name
1144 usr.permission = _user_group.permission.permission_name
1145 perm_rows.append(usr)
1145 perm_rows.append(usr)
1146
1146
1147 return perm_rows
1147 return perm_rows
1148
1148
1149 def _get_default_perms(self, user_group, suffix=''):
1149 def _get_default_perms(self, user_group, suffix=''):
1150 from rhodecode.model.permission import PermissionModel
1150 from rhodecode.model.permission import PermissionModel
1151 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1151 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1152
1152
1153 def get_default_perms(self, suffix=''):
1153 def get_default_perms(self, suffix=''):
1154 return self._get_default_perms(self, suffix)
1154 return self._get_default_perms(self, suffix)
1155
1155
1156 def get_api_data(self, with_group_members=True, include_secrets=False):
1156 def get_api_data(self, with_group_members=True, include_secrets=False):
1157 """
1157 """
1158 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1158 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1159 basically forwarded.
1159 basically forwarded.
1160
1160
1161 """
1161 """
1162 user_group = self
1162 user_group = self
1163
1163
1164 data = {
1164 data = {
1165 'users_group_id': user_group.users_group_id,
1165 'users_group_id': user_group.users_group_id,
1166 'group_name': user_group.users_group_name,
1166 'group_name': user_group.users_group_name,
1167 'group_description': user_group.user_group_description,
1167 'group_description': user_group.user_group_description,
1168 'active': user_group.users_group_active,
1168 'active': user_group.users_group_active,
1169 'owner': user_group.user.username,
1169 'owner': user_group.user.username,
1170 }
1170 }
1171 if with_group_members:
1171 if with_group_members:
1172 users = []
1172 users = []
1173 for user in user_group.members:
1173 for user in user_group.members:
1174 user = user.user
1174 user = user.user
1175 users.append(user.get_api_data(include_secrets=include_secrets))
1175 users.append(user.get_api_data(include_secrets=include_secrets))
1176 data['users'] = users
1176 data['users'] = users
1177
1177
1178 return data
1178 return data
1179
1179
1180
1180
1181 class UserGroupMember(Base, BaseModel):
1181 class UserGroupMember(Base, BaseModel):
1182 __tablename__ = 'users_groups_members'
1182 __tablename__ = 'users_groups_members'
1183 __table_args__ = (
1183 __table_args__ = (
1184 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1184 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1185 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1185 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1186 )
1186 )
1187
1187
1188 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1188 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1189 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1189 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1190 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1190 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1191
1191
1192 user = relationship('User', lazy='joined')
1192 user = relationship('User', lazy='joined')
1193 users_group = relationship('UserGroup')
1193 users_group = relationship('UserGroup')
1194
1194
1195 def __init__(self, gr_id='', u_id=''):
1195 def __init__(self, gr_id='', u_id=''):
1196 self.users_group_id = gr_id
1196 self.users_group_id = gr_id
1197 self.user_id = u_id
1197 self.user_id = u_id
1198
1198
1199
1199
1200 class RepositoryField(Base, BaseModel):
1200 class RepositoryField(Base, BaseModel):
1201 __tablename__ = 'repositories_fields'
1201 __tablename__ = 'repositories_fields'
1202 __table_args__ = (
1202 __table_args__ = (
1203 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1203 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1204 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1204 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1205 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1205 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1206 )
1206 )
1207 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1207 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1208
1208
1209 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1209 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1210 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1210 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1211 field_key = Column("field_key", String(250))
1211 field_key = Column("field_key", String(250))
1212 field_label = Column("field_label", String(1024), nullable=False)
1212 field_label = Column("field_label", String(1024), nullable=False)
1213 field_value = Column("field_value", String(10000), nullable=False)
1213 field_value = Column("field_value", String(10000), nullable=False)
1214 field_desc = Column("field_desc", String(1024), nullable=False)
1214 field_desc = Column("field_desc", String(1024), nullable=False)
1215 field_type = Column("field_type", String(255), nullable=False, unique=None)
1215 field_type = Column("field_type", String(255), nullable=False, unique=None)
1216 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1216 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1217
1217
1218 repository = relationship('Repository')
1218 repository = relationship('Repository')
1219
1219
1220 @property
1220 @property
1221 def field_key_prefixed(self):
1221 def field_key_prefixed(self):
1222 return 'ex_%s' % self.field_key
1222 return 'ex_%s' % self.field_key
1223
1223
1224 @classmethod
1224 @classmethod
1225 def un_prefix_key(cls, key):
1225 def un_prefix_key(cls, key):
1226 if key.startswith(cls.PREFIX):
1226 if key.startswith(cls.PREFIX):
1227 return key[len(cls.PREFIX):]
1227 return key[len(cls.PREFIX):]
1228 return key
1228 return key
1229
1229
1230 @classmethod
1230 @classmethod
1231 def get_by_key_name(cls, key, repo):
1231 def get_by_key_name(cls, key, repo):
1232 row = cls.query()\
1232 row = cls.query()\
1233 .filter(cls.repository == repo)\
1233 .filter(cls.repository == repo)\
1234 .filter(cls.field_key == key).scalar()
1234 .filter(cls.field_key == key).scalar()
1235 return row
1235 return row
1236
1236
1237
1237
1238 class Repository(Base, BaseModel):
1238 class Repository(Base, BaseModel):
1239 __tablename__ = 'repositories'
1239 __tablename__ = 'repositories'
1240 __table_args__ = (
1240 __table_args__ = (
1241 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1241 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1242 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1242 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1243 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1243 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1244 )
1244 )
1245 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1245 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1246 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1246 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1247
1247
1248 STATE_CREATED = 'repo_state_created'
1248 STATE_CREATED = 'repo_state_created'
1249 STATE_PENDING = 'repo_state_pending'
1249 STATE_PENDING = 'repo_state_pending'
1250 STATE_ERROR = 'repo_state_error'
1250 STATE_ERROR = 'repo_state_error'
1251
1251
1252 LOCK_AUTOMATIC = 'lock_auto'
1252 LOCK_AUTOMATIC = 'lock_auto'
1253 LOCK_API = 'lock_api'
1253 LOCK_API = 'lock_api'
1254 LOCK_WEB = 'lock_web'
1254 LOCK_WEB = 'lock_web'
1255 LOCK_PULL = 'lock_pull'
1255 LOCK_PULL = 'lock_pull'
1256
1256
1257 NAME_SEP = URL_SEP
1257 NAME_SEP = URL_SEP
1258
1258
1259 repo_id = Column(
1259 repo_id = Column(
1260 "repo_id", Integer(), nullable=False, unique=True, default=None,
1260 "repo_id", Integer(), nullable=False, unique=True, default=None,
1261 primary_key=True)
1261 primary_key=True)
1262 _repo_name = Column(
1262 _repo_name = Column(
1263 "repo_name", Text(), nullable=False, default=None)
1263 "repo_name", Text(), nullable=False, default=None)
1264 _repo_name_hash = Column(
1264 _repo_name_hash = Column(
1265 "repo_name_hash", String(255), nullable=False, unique=True)
1265 "repo_name_hash", String(255), nullable=False, unique=True)
1266 repo_state = Column("repo_state", String(255), nullable=True)
1266 repo_state = Column("repo_state", String(255), nullable=True)
1267
1267
1268 clone_uri = Column(
1268 clone_uri = Column(
1269 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1269 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1270 default=None)
1270 default=None)
1271 repo_type = Column(
1271 repo_type = Column(
1272 "repo_type", String(255), nullable=False, unique=False, default=None)
1272 "repo_type", String(255), nullable=False, unique=False, default=None)
1273 user_id = Column(
1273 user_id = Column(
1274 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1274 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1275 unique=False, default=None)
1275 unique=False, default=None)
1276 private = Column(
1276 private = Column(
1277 "private", Boolean(), nullable=True, unique=None, default=None)
1277 "private", Boolean(), nullable=True, unique=None, default=None)
1278 enable_statistics = Column(
1278 enable_statistics = Column(
1279 "statistics", Boolean(), nullable=True, unique=None, default=True)
1279 "statistics", Boolean(), nullable=True, unique=None, default=True)
1280 enable_downloads = Column(
1280 enable_downloads = Column(
1281 "downloads", Boolean(), nullable=True, unique=None, default=True)
1281 "downloads", Boolean(), nullable=True, unique=None, default=True)
1282 description = Column(
1282 description = Column(
1283 "description", String(10000), nullable=True, unique=None, default=None)
1283 "description", String(10000), nullable=True, unique=None, default=None)
1284 created_on = Column(
1284 created_on = Column(
1285 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1285 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1286 default=datetime.datetime.now)
1286 default=datetime.datetime.now)
1287 updated_on = Column(
1287 updated_on = Column(
1288 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1288 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1289 default=datetime.datetime.now)
1289 default=datetime.datetime.now)
1290 _landing_revision = Column(
1290 _landing_revision = Column(
1291 "landing_revision", String(255), nullable=False, unique=False,
1291 "landing_revision", String(255), nullable=False, unique=False,
1292 default=None)
1292 default=None)
1293 enable_locking = Column(
1293 enable_locking = Column(
1294 "enable_locking", Boolean(), nullable=False, unique=None,
1294 "enable_locking", Boolean(), nullable=False, unique=None,
1295 default=False)
1295 default=False)
1296 _locked = Column(
1296 _locked = Column(
1297 "locked", String(255), nullable=True, unique=False, default=None)
1297 "locked", String(255), nullable=True, unique=False, default=None)
1298 _changeset_cache = Column(
1298 _changeset_cache = Column(
1299 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1299 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1300
1300
1301 fork_id = Column(
1301 fork_id = Column(
1302 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1302 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1303 nullable=True, unique=False, default=None)
1303 nullable=True, unique=False, default=None)
1304 group_id = Column(
1304 group_id = Column(
1305 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1305 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1306 unique=False, default=None)
1306 unique=False, default=None)
1307
1307
1308 user = relationship('User', lazy='joined')
1308 user = relationship('User', lazy='joined')
1309 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1309 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1310 group = relationship('RepoGroup', lazy='joined')
1310 group = relationship('RepoGroup', lazy='joined')
1311 repo_to_perm = relationship(
1311 repo_to_perm = relationship(
1312 'UserRepoToPerm', cascade='all',
1312 'UserRepoToPerm', cascade='all',
1313 order_by='UserRepoToPerm.repo_to_perm_id')
1313 order_by='UserRepoToPerm.repo_to_perm_id')
1314 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1314 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1315 stats = relationship('Statistics', cascade='all', uselist=False)
1315 stats = relationship('Statistics', cascade='all', uselist=False)
1316
1316
1317 followers = relationship(
1317 followers = relationship(
1318 'UserFollowing',
1318 'UserFollowing',
1319 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1319 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1320 cascade='all')
1320 cascade='all')
1321 extra_fields = relationship(
1321 extra_fields = relationship(
1322 'RepositoryField', cascade="all, delete, delete-orphan")
1322 'RepositoryField', cascade="all, delete, delete-orphan")
1323 logs = relationship('UserLog')
1323 logs = relationship('UserLog')
1324 comments = relationship(
1324 comments = relationship(
1325 'ChangesetComment', cascade="all, delete, delete-orphan")
1325 'ChangesetComment', cascade="all, delete, delete-orphan")
1326 pull_requests_source = relationship(
1326 pull_requests_source = relationship(
1327 'PullRequest',
1327 'PullRequest',
1328 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1328 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1329 cascade="all, delete, delete-orphan")
1329 cascade="all, delete, delete-orphan")
1330 pull_requests_target = relationship(
1330 pull_requests_target = relationship(
1331 'PullRequest',
1331 'PullRequest',
1332 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1332 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1333 cascade="all, delete, delete-orphan")
1333 cascade="all, delete, delete-orphan")
1334 ui = relationship('RepoRhodeCodeUi', cascade="all")
1334 ui = relationship('RepoRhodeCodeUi', cascade="all")
1335 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1335 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1336 integrations = relationship('Integration',
1336 integrations = relationship('Integration',
1337 cascade="all, delete, delete-orphan")
1337 cascade="all, delete, delete-orphan")
1338
1338
1339 def __unicode__(self):
1339 def __unicode__(self):
1340 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1340 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1341 safe_unicode(self.repo_name))
1341 safe_unicode(self.repo_name))
1342
1342
1343 @hybrid_property
1343 @hybrid_property
1344 def landing_rev(self):
1344 def landing_rev(self):
1345 # always should return [rev_type, rev]
1345 # always should return [rev_type, rev]
1346 if self._landing_revision:
1346 if self._landing_revision:
1347 _rev_info = self._landing_revision.split(':')
1347 _rev_info = self._landing_revision.split(':')
1348 if len(_rev_info) < 2:
1348 if len(_rev_info) < 2:
1349 _rev_info.insert(0, 'rev')
1349 _rev_info.insert(0, 'rev')
1350 return [_rev_info[0], _rev_info[1]]
1350 return [_rev_info[0], _rev_info[1]]
1351 return [None, None]
1351 return [None, None]
1352
1352
1353 @landing_rev.setter
1353 @landing_rev.setter
1354 def landing_rev(self, val):
1354 def landing_rev(self, val):
1355 if ':' not in val:
1355 if ':' not in val:
1356 raise ValueError('value must be delimited with `:` and consist '
1356 raise ValueError('value must be delimited with `:` and consist '
1357 'of <rev_type>:<rev>, got %s instead' % val)
1357 'of <rev_type>:<rev>, got %s instead' % val)
1358 self._landing_revision = val
1358 self._landing_revision = val
1359
1359
1360 @hybrid_property
1360 @hybrid_property
1361 def locked(self):
1361 def locked(self):
1362 if self._locked:
1362 if self._locked:
1363 user_id, timelocked, reason = self._locked.split(':')
1363 user_id, timelocked, reason = self._locked.split(':')
1364 lock_values = int(user_id), timelocked, reason
1364 lock_values = int(user_id), timelocked, reason
1365 else:
1365 else:
1366 lock_values = [None, None, None]
1366 lock_values = [None, None, None]
1367 return lock_values
1367 return lock_values
1368
1368
1369 @locked.setter
1369 @locked.setter
1370 def locked(self, val):
1370 def locked(self, val):
1371 if val and isinstance(val, (list, tuple)):
1371 if val and isinstance(val, (list, tuple)):
1372 self._locked = ':'.join(map(str, val))
1372 self._locked = ':'.join(map(str, val))
1373 else:
1373 else:
1374 self._locked = None
1374 self._locked = None
1375
1375
1376 @hybrid_property
1376 @hybrid_property
1377 def changeset_cache(self):
1377 def changeset_cache(self):
1378 from rhodecode.lib.vcs.backends.base import EmptyCommit
1378 from rhodecode.lib.vcs.backends.base import EmptyCommit
1379 dummy = EmptyCommit().__json__()
1379 dummy = EmptyCommit().__json__()
1380 if not self._changeset_cache:
1380 if not self._changeset_cache:
1381 return dummy
1381 return dummy
1382 try:
1382 try:
1383 return json.loads(self._changeset_cache)
1383 return json.loads(self._changeset_cache)
1384 except TypeError:
1384 except TypeError:
1385 return dummy
1385 return dummy
1386 except Exception:
1386 except Exception:
1387 log.error(traceback.format_exc())
1387 log.error(traceback.format_exc())
1388 return dummy
1388 return dummy
1389
1389
1390 @changeset_cache.setter
1390 @changeset_cache.setter
1391 def changeset_cache(self, val):
1391 def changeset_cache(self, val):
1392 try:
1392 try:
1393 self._changeset_cache = json.dumps(val)
1393 self._changeset_cache = json.dumps(val)
1394 except Exception:
1394 except Exception:
1395 log.error(traceback.format_exc())
1395 log.error(traceback.format_exc())
1396
1396
1397 @hybrid_property
1397 @hybrid_property
1398 def repo_name(self):
1398 def repo_name(self):
1399 return self._repo_name
1399 return self._repo_name
1400
1400
1401 @repo_name.setter
1401 @repo_name.setter
1402 def repo_name(self, value):
1402 def repo_name(self, value):
1403 self._repo_name = value
1403 self._repo_name = value
1404 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1404 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1405
1405
1406 @classmethod
1406 @classmethod
1407 def normalize_repo_name(cls, repo_name):
1407 def normalize_repo_name(cls, repo_name):
1408 """
1408 """
1409 Normalizes os specific repo_name to the format internally stored inside
1409 Normalizes os specific repo_name to the format internally stored inside
1410 database using URL_SEP
1410 database using URL_SEP
1411
1411
1412 :param cls:
1412 :param cls:
1413 :param repo_name:
1413 :param repo_name:
1414 """
1414 """
1415 return cls.NAME_SEP.join(repo_name.split(os.sep))
1415 return cls.NAME_SEP.join(repo_name.split(os.sep))
1416
1416
1417 @classmethod
1417 @classmethod
1418 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1418 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1419 session = Session()
1419 session = Session()
1420 q = session.query(cls).filter(cls.repo_name == repo_name)
1420 q = session.query(cls).filter(cls.repo_name == repo_name)
1421
1421
1422 if cache:
1422 if cache:
1423 if identity_cache:
1423 if identity_cache:
1424 val = cls.identity_cache(session, 'repo_name', repo_name)
1424 val = cls.identity_cache(session, 'repo_name', repo_name)
1425 if val:
1425 if val:
1426 return val
1426 return val
1427 else:
1427 else:
1428 q = q.options(
1428 q = q.options(
1429 FromCache("sql_cache_short",
1429 FromCache("sql_cache_short",
1430 "get_repo_by_name_%s" % _hash_key(repo_name)))
1430 "get_repo_by_name_%s" % _hash_key(repo_name)))
1431
1431
1432 return q.scalar()
1432 return q.scalar()
1433
1433
1434 @classmethod
1434 @classmethod
1435 def get_by_full_path(cls, repo_full_path):
1435 def get_by_full_path(cls, repo_full_path):
1436 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1436 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1437 repo_name = cls.normalize_repo_name(repo_name)
1437 repo_name = cls.normalize_repo_name(repo_name)
1438 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1438 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1439
1439
1440 @classmethod
1440 @classmethod
1441 def get_repo_forks(cls, repo_id):
1441 def get_repo_forks(cls, repo_id):
1442 return cls.query().filter(Repository.fork_id == repo_id)
1442 return cls.query().filter(Repository.fork_id == repo_id)
1443
1443
1444 @classmethod
1444 @classmethod
1445 def base_path(cls):
1445 def base_path(cls):
1446 """
1446 """
1447 Returns base path when all repos are stored
1447 Returns base path when all repos are stored
1448
1448
1449 :param cls:
1449 :param cls:
1450 """
1450 """
1451 q = Session().query(RhodeCodeUi)\
1451 q = Session().query(RhodeCodeUi)\
1452 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1452 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1453 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1453 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1454 return q.one().ui_value
1454 return q.one().ui_value
1455
1455
1456 @classmethod
1456 @classmethod
1457 def is_valid(cls, repo_name):
1457 def is_valid(cls, repo_name):
1458 """
1458 """
1459 returns True if given repo name is a valid filesystem repository
1459 returns True if given repo name is a valid filesystem repository
1460
1460
1461 :param cls:
1461 :param cls:
1462 :param repo_name:
1462 :param repo_name:
1463 """
1463 """
1464 from rhodecode.lib.utils import is_valid_repo
1464 from rhodecode.lib.utils import is_valid_repo
1465
1465
1466 return is_valid_repo(repo_name, cls.base_path())
1466 return is_valid_repo(repo_name, cls.base_path())
1467
1467
1468 @classmethod
1468 @classmethod
1469 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1469 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1470 case_insensitive=True):
1470 case_insensitive=True):
1471 q = Repository.query()
1471 q = Repository.query()
1472
1472
1473 if not isinstance(user_id, Optional):
1473 if not isinstance(user_id, Optional):
1474 q = q.filter(Repository.user_id == user_id)
1474 q = q.filter(Repository.user_id == user_id)
1475
1475
1476 if not isinstance(group_id, Optional):
1476 if not isinstance(group_id, Optional):
1477 q = q.filter(Repository.group_id == group_id)
1477 q = q.filter(Repository.group_id == group_id)
1478
1478
1479 if case_insensitive:
1479 if case_insensitive:
1480 q = q.order_by(func.lower(Repository.repo_name))
1480 q = q.order_by(func.lower(Repository.repo_name))
1481 else:
1481 else:
1482 q = q.order_by(Repository.repo_name)
1482 q = q.order_by(Repository.repo_name)
1483 return q.all()
1483 return q.all()
1484
1484
1485 @property
1485 @property
1486 def forks(self):
1486 def forks(self):
1487 """
1487 """
1488 Return forks of this repo
1488 Return forks of this repo
1489 """
1489 """
1490 return Repository.get_repo_forks(self.repo_id)
1490 return Repository.get_repo_forks(self.repo_id)
1491
1491
1492 @property
1492 @property
1493 def parent(self):
1493 def parent(self):
1494 """
1494 """
1495 Returns fork parent
1495 Returns fork parent
1496 """
1496 """
1497 return self.fork
1497 return self.fork
1498
1498
1499 @property
1499 @property
1500 def just_name(self):
1500 def just_name(self):
1501 return self.repo_name.split(self.NAME_SEP)[-1]
1501 return self.repo_name.split(self.NAME_SEP)[-1]
1502
1502
1503 @property
1503 @property
1504 def groups_with_parents(self):
1504 def groups_with_parents(self):
1505 groups = []
1505 groups = []
1506 if self.group is None:
1506 if self.group is None:
1507 return groups
1507 return groups
1508
1508
1509 cur_gr = self.group
1509 cur_gr = self.group
1510 groups.insert(0, cur_gr)
1510 groups.insert(0, cur_gr)
1511 while 1:
1511 while 1:
1512 gr = getattr(cur_gr, 'parent_group', None)
1512 gr = getattr(cur_gr, 'parent_group', None)
1513 cur_gr = cur_gr.parent_group
1513 cur_gr = cur_gr.parent_group
1514 if gr is None:
1514 if gr is None:
1515 break
1515 break
1516 groups.insert(0, gr)
1516 groups.insert(0, gr)
1517
1517
1518 return groups
1518 return groups
1519
1519
1520 @property
1520 @property
1521 def groups_and_repo(self):
1521 def groups_and_repo(self):
1522 return self.groups_with_parents, self
1522 return self.groups_with_parents, self
1523
1523
1524 @LazyProperty
1524 @LazyProperty
1525 def repo_path(self):
1525 def repo_path(self):
1526 """
1526 """
1527 Returns base full path for that repository means where it actually
1527 Returns base full path for that repository means where it actually
1528 exists on a filesystem
1528 exists on a filesystem
1529 """
1529 """
1530 q = Session().query(RhodeCodeUi).filter(
1530 q = Session().query(RhodeCodeUi).filter(
1531 RhodeCodeUi.ui_key == self.NAME_SEP)
1531 RhodeCodeUi.ui_key == self.NAME_SEP)
1532 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1532 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1533 return q.one().ui_value
1533 return q.one().ui_value
1534
1534
1535 @property
1535 @property
1536 def repo_full_path(self):
1536 def repo_full_path(self):
1537 p = [self.repo_path]
1537 p = [self.repo_path]
1538 # we need to split the name by / since this is how we store the
1538 # we need to split the name by / since this is how we store the
1539 # names in the database, but that eventually needs to be converted
1539 # names in the database, but that eventually needs to be converted
1540 # into a valid system path
1540 # into a valid system path
1541 p += self.repo_name.split(self.NAME_SEP)
1541 p += self.repo_name.split(self.NAME_SEP)
1542 return os.path.join(*map(safe_unicode, p))
1542 return os.path.join(*map(safe_unicode, p))
1543
1543
1544 @property
1544 @property
1545 def cache_keys(self):
1545 def cache_keys(self):
1546 """
1546 """
1547 Returns associated cache keys for that repo
1547 Returns associated cache keys for that repo
1548 """
1548 """
1549 return CacheKey.query()\
1549 return CacheKey.query()\
1550 .filter(CacheKey.cache_args == self.repo_name)\
1550 .filter(CacheKey.cache_args == self.repo_name)\
1551 .order_by(CacheKey.cache_key)\
1551 .order_by(CacheKey.cache_key)\
1552 .all()
1552 .all()
1553
1553
1554 def get_new_name(self, repo_name):
1554 def get_new_name(self, repo_name):
1555 """
1555 """
1556 returns new full repository name based on assigned group and new new
1556 returns new full repository name based on assigned group and new new
1557
1557
1558 :param group_name:
1558 :param group_name:
1559 """
1559 """
1560 path_prefix = self.group.full_path_splitted if self.group else []
1560 path_prefix = self.group.full_path_splitted if self.group else []
1561 return self.NAME_SEP.join(path_prefix + [repo_name])
1561 return self.NAME_SEP.join(path_prefix + [repo_name])
1562
1562
1563 @property
1563 @property
1564 def _config(self):
1564 def _config(self):
1565 """
1565 """
1566 Returns db based config object.
1566 Returns db based config object.
1567 """
1567 """
1568 from rhodecode.lib.utils import make_db_config
1568 from rhodecode.lib.utils import make_db_config
1569 return make_db_config(clear_session=False, repo=self)
1569 return make_db_config(clear_session=False, repo=self)
1570
1570
1571 def permissions(self, with_admins=True, with_owner=True):
1571 def permissions(self, with_admins=True, with_owner=True):
1572 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1572 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1573 q = q.options(joinedload(UserRepoToPerm.repository),
1573 q = q.options(joinedload(UserRepoToPerm.repository),
1574 joinedload(UserRepoToPerm.user),
1574 joinedload(UserRepoToPerm.user),
1575 joinedload(UserRepoToPerm.permission),)
1575 joinedload(UserRepoToPerm.permission),)
1576
1576
1577 # get owners and admins and permissions. We do a trick of re-writing
1577 # get owners and admins and permissions. We do a trick of re-writing
1578 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1578 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1579 # has a global reference and changing one object propagates to all
1579 # has a global reference and changing one object propagates to all
1580 # others. This means if admin is also an owner admin_row that change
1580 # others. This means if admin is also an owner admin_row that change
1581 # would propagate to both objects
1581 # would propagate to both objects
1582 perm_rows = []
1582 perm_rows = []
1583 for _usr in q.all():
1583 for _usr in q.all():
1584 usr = AttributeDict(_usr.user.get_dict())
1584 usr = AttributeDict(_usr.user.get_dict())
1585 usr.permission = _usr.permission.permission_name
1585 usr.permission = _usr.permission.permission_name
1586 perm_rows.append(usr)
1586 perm_rows.append(usr)
1587
1587
1588 # filter the perm rows by 'default' first and then sort them by
1588 # filter the perm rows by 'default' first and then sort them by
1589 # admin,write,read,none permissions sorted again alphabetically in
1589 # admin,write,read,none permissions sorted again alphabetically in
1590 # each group
1590 # each group
1591 perm_rows = sorted(perm_rows, key=display_sort)
1591 perm_rows = sorted(perm_rows, key=display_sort)
1592
1592
1593 _admin_perm = 'repository.admin'
1593 _admin_perm = 'repository.admin'
1594 owner_row = []
1594 owner_row = []
1595 if with_owner:
1595 if with_owner:
1596 usr = AttributeDict(self.user.get_dict())
1596 usr = AttributeDict(self.user.get_dict())
1597 usr.owner_row = True
1597 usr.owner_row = True
1598 usr.permission = _admin_perm
1598 usr.permission = _admin_perm
1599 owner_row.append(usr)
1599 owner_row.append(usr)
1600
1600
1601 super_admin_rows = []
1601 super_admin_rows = []
1602 if with_admins:
1602 if with_admins:
1603 for usr in User.get_all_super_admins():
1603 for usr in User.get_all_super_admins():
1604 # if this admin is also owner, don't double the record
1604 # if this admin is also owner, don't double the record
1605 if usr.user_id == owner_row[0].user_id:
1605 if usr.user_id == owner_row[0].user_id:
1606 owner_row[0].admin_row = True
1606 owner_row[0].admin_row = True
1607 else:
1607 else:
1608 usr = AttributeDict(usr.get_dict())
1608 usr = AttributeDict(usr.get_dict())
1609 usr.admin_row = True
1609 usr.admin_row = True
1610 usr.permission = _admin_perm
1610 usr.permission = _admin_perm
1611 super_admin_rows.append(usr)
1611 super_admin_rows.append(usr)
1612
1612
1613 return super_admin_rows + owner_row + perm_rows
1613 return super_admin_rows + owner_row + perm_rows
1614
1614
1615 def permission_user_groups(self):
1615 def permission_user_groups(self):
1616 q = UserGroupRepoToPerm.query().filter(
1616 q = UserGroupRepoToPerm.query().filter(
1617 UserGroupRepoToPerm.repository == self)
1617 UserGroupRepoToPerm.repository == self)
1618 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1618 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1619 joinedload(UserGroupRepoToPerm.users_group),
1619 joinedload(UserGroupRepoToPerm.users_group),
1620 joinedload(UserGroupRepoToPerm.permission),)
1620 joinedload(UserGroupRepoToPerm.permission),)
1621
1621
1622 perm_rows = []
1622 perm_rows = []
1623 for _user_group in q.all():
1623 for _user_group in q.all():
1624 usr = AttributeDict(_user_group.users_group.get_dict())
1624 usr = AttributeDict(_user_group.users_group.get_dict())
1625 usr.permission = _user_group.permission.permission_name
1625 usr.permission = _user_group.permission.permission_name
1626 perm_rows.append(usr)
1626 perm_rows.append(usr)
1627
1627
1628 return perm_rows
1628 return perm_rows
1629
1629
1630 def get_api_data(self, include_secrets=False):
1630 def get_api_data(self, include_secrets=False):
1631 """
1631 """
1632 Common function for generating repo api data
1632 Common function for generating repo api data
1633
1633
1634 :param include_secrets: See :meth:`User.get_api_data`.
1634 :param include_secrets: See :meth:`User.get_api_data`.
1635
1635
1636 """
1636 """
1637 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1637 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1638 # move this methods on models level.
1638 # move this methods on models level.
1639 from rhodecode.model.settings import SettingsModel
1639 from rhodecode.model.settings import SettingsModel
1640
1640
1641 repo = self
1641 repo = self
1642 _user_id, _time, _reason = self.locked
1642 _user_id, _time, _reason = self.locked
1643
1643
1644 data = {
1644 data = {
1645 'repo_id': repo.repo_id,
1645 'repo_id': repo.repo_id,
1646 'repo_name': repo.repo_name,
1646 'repo_name': repo.repo_name,
1647 'repo_type': repo.repo_type,
1647 'repo_type': repo.repo_type,
1648 'clone_uri': repo.clone_uri or '',
1648 'clone_uri': repo.clone_uri or '',
1649 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1649 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1650 'private': repo.private,
1650 'private': repo.private,
1651 'created_on': repo.created_on,
1651 'created_on': repo.created_on,
1652 'description': repo.description,
1652 'description': repo.description,
1653 'landing_rev': repo.landing_rev,
1653 'landing_rev': repo.landing_rev,
1654 'owner': repo.user.username,
1654 'owner': repo.user.username,
1655 'fork_of': repo.fork.repo_name if repo.fork else None,
1655 'fork_of': repo.fork.repo_name if repo.fork else None,
1656 'enable_statistics': repo.enable_statistics,
1656 'enable_statistics': repo.enable_statistics,
1657 'enable_locking': repo.enable_locking,
1657 'enable_locking': repo.enable_locking,
1658 'enable_downloads': repo.enable_downloads,
1658 'enable_downloads': repo.enable_downloads,
1659 'last_changeset': repo.changeset_cache,
1659 'last_changeset': repo.changeset_cache,
1660 'locked_by': User.get(_user_id).get_api_data(
1660 'locked_by': User.get(_user_id).get_api_data(
1661 include_secrets=include_secrets) if _user_id else None,
1661 include_secrets=include_secrets) if _user_id else None,
1662 'locked_date': time_to_datetime(_time) if _time else None,
1662 'locked_date': time_to_datetime(_time) if _time else None,
1663 'lock_reason': _reason if _reason else None,
1663 'lock_reason': _reason if _reason else None,
1664 }
1664 }
1665
1665
1666 # TODO: mikhail: should be per-repo settings here
1666 # TODO: mikhail: should be per-repo settings here
1667 rc_config = SettingsModel().get_all_settings()
1667 rc_config = SettingsModel().get_all_settings()
1668 repository_fields = str2bool(
1668 repository_fields = str2bool(
1669 rc_config.get('rhodecode_repository_fields'))
1669 rc_config.get('rhodecode_repository_fields'))
1670 if repository_fields:
1670 if repository_fields:
1671 for f in self.extra_fields:
1671 for f in self.extra_fields:
1672 data[f.field_key_prefixed] = f.field_value
1672 data[f.field_key_prefixed] = f.field_value
1673
1673
1674 return data
1674 return data
1675
1675
1676 @classmethod
1676 @classmethod
1677 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1677 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1678 if not lock_time:
1678 if not lock_time:
1679 lock_time = time.time()
1679 lock_time = time.time()
1680 if not lock_reason:
1680 if not lock_reason:
1681 lock_reason = cls.LOCK_AUTOMATIC
1681 lock_reason = cls.LOCK_AUTOMATIC
1682 repo.locked = [user_id, lock_time, lock_reason]
1682 repo.locked = [user_id, lock_time, lock_reason]
1683 Session().add(repo)
1683 Session().add(repo)
1684 Session().commit()
1684 Session().commit()
1685
1685
1686 @classmethod
1686 @classmethod
1687 def unlock(cls, repo):
1687 def unlock(cls, repo):
1688 repo.locked = None
1688 repo.locked = None
1689 Session().add(repo)
1689 Session().add(repo)
1690 Session().commit()
1690 Session().commit()
1691
1691
1692 @classmethod
1692 @classmethod
1693 def getlock(cls, repo):
1693 def getlock(cls, repo):
1694 return repo.locked
1694 return repo.locked
1695
1695
1696 def is_user_lock(self, user_id):
1696 def is_user_lock(self, user_id):
1697 if self.lock[0]:
1697 if self.lock[0]:
1698 lock_user_id = safe_int(self.lock[0])
1698 lock_user_id = safe_int(self.lock[0])
1699 user_id = safe_int(user_id)
1699 user_id = safe_int(user_id)
1700 # both are ints, and they are equal
1700 # both are ints, and they are equal
1701 return all([lock_user_id, user_id]) and lock_user_id == user_id
1701 return all([lock_user_id, user_id]) and lock_user_id == user_id
1702
1702
1703 return False
1703 return False
1704
1704
1705 def get_locking_state(self, action, user_id, only_when_enabled=True):
1705 def get_locking_state(self, action, user_id, only_when_enabled=True):
1706 """
1706 """
1707 Checks locking on this repository, if locking is enabled and lock is
1707 Checks locking on this repository, if locking is enabled and lock is
1708 present returns a tuple of make_lock, locked, locked_by.
1708 present returns a tuple of make_lock, locked, locked_by.
1709 make_lock can have 3 states None (do nothing) True, make lock
1709 make_lock can have 3 states None (do nothing) True, make lock
1710 False release lock, This value is later propagated to hooks, which
1710 False release lock, This value is later propagated to hooks, which
1711 do the locking. Think about this as signals passed to hooks what to do.
1711 do the locking. Think about this as signals passed to hooks what to do.
1712
1712
1713 """
1713 """
1714 # TODO: johbo: This is part of the business logic and should be moved
1714 # TODO: johbo: This is part of the business logic and should be moved
1715 # into the RepositoryModel.
1715 # into the RepositoryModel.
1716
1716
1717 if action not in ('push', 'pull'):
1717 if action not in ('push', 'pull'):
1718 raise ValueError("Invalid action value: %s" % repr(action))
1718 raise ValueError("Invalid action value: %s" % repr(action))
1719
1719
1720 # defines if locked error should be thrown to user
1720 # defines if locked error should be thrown to user
1721 currently_locked = False
1721 currently_locked = False
1722 # defines if new lock should be made, tri-state
1722 # defines if new lock should be made, tri-state
1723 make_lock = None
1723 make_lock = None
1724 repo = self
1724 repo = self
1725 user = User.get(user_id)
1725 user = User.get(user_id)
1726
1726
1727 lock_info = repo.locked
1727 lock_info = repo.locked
1728
1728
1729 if repo and (repo.enable_locking or not only_when_enabled):
1729 if repo and (repo.enable_locking or not only_when_enabled):
1730 if action == 'push':
1730 if action == 'push':
1731 # check if it's already locked !, if it is compare users
1731 # check if it's already locked !, if it is compare users
1732 locked_by_user_id = lock_info[0]
1732 locked_by_user_id = lock_info[0]
1733 if user.user_id == locked_by_user_id:
1733 if user.user_id == locked_by_user_id:
1734 log.debug(
1734 log.debug(
1735 'Got `push` action from user %s, now unlocking', user)
1735 'Got `push` action from user %s, now unlocking', user)
1736 # unlock if we have push from user who locked
1736 # unlock if we have push from user who locked
1737 make_lock = False
1737 make_lock = False
1738 else:
1738 else:
1739 # we're not the same user who locked, ban with
1739 # we're not the same user who locked, ban with
1740 # code defined in settings (default is 423 HTTP Locked) !
1740 # code defined in settings (default is 423 HTTP Locked) !
1741 log.debug('Repo %s is currently locked by %s', repo, user)
1741 log.debug('Repo %s is currently locked by %s', repo, user)
1742 currently_locked = True
1742 currently_locked = True
1743 elif action == 'pull':
1743 elif action == 'pull':
1744 # [0] user [1] date
1744 # [0] user [1] date
1745 if lock_info[0] and lock_info[1]:
1745 if lock_info[0] and lock_info[1]:
1746 log.debug('Repo %s is currently locked by %s', repo, user)
1746 log.debug('Repo %s is currently locked by %s', repo, user)
1747 currently_locked = True
1747 currently_locked = True
1748 else:
1748 else:
1749 log.debug('Setting lock on repo %s by %s', repo, user)
1749 log.debug('Setting lock on repo %s by %s', repo, user)
1750 make_lock = True
1750 make_lock = True
1751
1751
1752 else:
1752 else:
1753 log.debug('Repository %s do not have locking enabled', repo)
1753 log.debug('Repository %s do not have locking enabled', repo)
1754
1754
1755 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1755 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1756 make_lock, currently_locked, lock_info)
1756 make_lock, currently_locked, lock_info)
1757
1757
1758 from rhodecode.lib.auth import HasRepoPermissionAny
1758 from rhodecode.lib.auth import HasRepoPermissionAny
1759 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1759 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1760 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1760 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1761 # if we don't have at least write permission we cannot make a lock
1761 # if we don't have at least write permission we cannot make a lock
1762 log.debug('lock state reset back to FALSE due to lack '
1762 log.debug('lock state reset back to FALSE due to lack '
1763 'of at least read permission')
1763 'of at least read permission')
1764 make_lock = False
1764 make_lock = False
1765
1765
1766 return make_lock, currently_locked, lock_info
1766 return make_lock, currently_locked, lock_info
1767
1767
1768 @property
1768 @property
1769 def last_db_change(self):
1769 def last_db_change(self):
1770 return self.updated_on
1770 return self.updated_on
1771
1771
1772 @property
1772 @property
1773 def clone_uri_hidden(self):
1773 def clone_uri_hidden(self):
1774 clone_uri = self.clone_uri
1774 clone_uri = self.clone_uri
1775 if clone_uri:
1775 if clone_uri:
1776 import urlobject
1776 import urlobject
1777 url_obj = urlobject.URLObject(clone_uri)
1777 url_obj = urlobject.URLObject(clone_uri)
1778 if url_obj.password:
1778 if url_obj.password:
1779 clone_uri = url_obj.with_password('*****')
1779 clone_uri = url_obj.with_password('*****')
1780 return clone_uri
1780 return clone_uri
1781
1781
1782 def clone_url(self, **override):
1782 def clone_url(self, **override):
1783 qualified_home_url = url('home', qualified=True)
1783 qualified_home_url = url('home', qualified=True)
1784
1784
1785 uri_tmpl = None
1785 uri_tmpl = None
1786 if 'with_id' in override:
1786 if 'with_id' in override:
1787 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1787 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1788 del override['with_id']
1788 del override['with_id']
1789
1789
1790 if 'uri_tmpl' in override:
1790 if 'uri_tmpl' in override:
1791 uri_tmpl = override['uri_tmpl']
1791 uri_tmpl = override['uri_tmpl']
1792 del override['uri_tmpl']
1792 del override['uri_tmpl']
1793
1793
1794 # we didn't override our tmpl from **overrides
1794 # we didn't override our tmpl from **overrides
1795 if not uri_tmpl:
1795 if not uri_tmpl:
1796 uri_tmpl = self.DEFAULT_CLONE_URI
1796 uri_tmpl = self.DEFAULT_CLONE_URI
1797 try:
1797 try:
1798 from pylons import tmpl_context as c
1798 from pylons import tmpl_context as c
1799 uri_tmpl = c.clone_uri_tmpl
1799 uri_tmpl = c.clone_uri_tmpl
1800 except Exception:
1800 except Exception:
1801 # in any case if we call this outside of request context,
1801 # in any case if we call this outside of request context,
1802 # ie, not having tmpl_context set up
1802 # ie, not having tmpl_context set up
1803 pass
1803 pass
1804
1804
1805 return get_clone_url(uri_tmpl=uri_tmpl,
1805 return get_clone_url(uri_tmpl=uri_tmpl,
1806 qualifed_home_url=qualified_home_url,
1806 qualifed_home_url=qualified_home_url,
1807 repo_name=self.repo_name,
1807 repo_name=self.repo_name,
1808 repo_id=self.repo_id, **override)
1808 repo_id=self.repo_id, **override)
1809
1809
1810 def set_state(self, state):
1810 def set_state(self, state):
1811 self.repo_state = state
1811 self.repo_state = state
1812 Session().add(self)
1812 Session().add(self)
1813 #==========================================================================
1813 #==========================================================================
1814 # SCM PROPERTIES
1814 # SCM PROPERTIES
1815 #==========================================================================
1815 #==========================================================================
1816
1816
1817 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1817 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1818 return get_commit_safe(
1818 return get_commit_safe(
1819 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1819 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1820
1820
1821 def get_changeset(self, rev=None, pre_load=None):
1821 def get_changeset(self, rev=None, pre_load=None):
1822 warnings.warn("Use get_commit", DeprecationWarning)
1822 warnings.warn("Use get_commit", DeprecationWarning)
1823 commit_id = None
1823 commit_id = None
1824 commit_idx = None
1824 commit_idx = None
1825 if isinstance(rev, basestring):
1825 if isinstance(rev, basestring):
1826 commit_id = rev
1826 commit_id = rev
1827 else:
1827 else:
1828 commit_idx = rev
1828 commit_idx = rev
1829 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1829 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1830 pre_load=pre_load)
1830 pre_load=pre_load)
1831
1831
1832 def get_landing_commit(self):
1832 def get_landing_commit(self):
1833 """
1833 """
1834 Returns landing commit, or if that doesn't exist returns the tip
1834 Returns landing commit, or if that doesn't exist returns the tip
1835 """
1835 """
1836 _rev_type, _rev = self.landing_rev
1836 _rev_type, _rev = self.landing_rev
1837 commit = self.get_commit(_rev)
1837 commit = self.get_commit(_rev)
1838 if isinstance(commit, EmptyCommit):
1838 if isinstance(commit, EmptyCommit):
1839 return self.get_commit()
1839 return self.get_commit()
1840 return commit
1840 return commit
1841
1841
1842 def update_commit_cache(self, cs_cache=None, config=None):
1842 def update_commit_cache(self, cs_cache=None, config=None):
1843 """
1843 """
1844 Update cache of last changeset for repository, keys should be::
1844 Update cache of last changeset for repository, keys should be::
1845
1845
1846 short_id
1846 short_id
1847 raw_id
1847 raw_id
1848 revision
1848 revision
1849 parents
1849 parents
1850 message
1850 message
1851 date
1851 date
1852 author
1852 author
1853
1853
1854 :param cs_cache:
1854 :param cs_cache:
1855 """
1855 """
1856 from rhodecode.lib.vcs.backends.base import BaseChangeset
1856 from rhodecode.lib.vcs.backends.base import BaseChangeset
1857 if cs_cache is None:
1857 if cs_cache is None:
1858 # use no-cache version here
1858 # use no-cache version here
1859 scm_repo = self.scm_instance(cache=False, config=config)
1859 scm_repo = self.scm_instance(cache=False, config=config)
1860 if scm_repo:
1860 if scm_repo:
1861 cs_cache = scm_repo.get_commit(
1861 cs_cache = scm_repo.get_commit(
1862 pre_load=["author", "date", "message", "parents"])
1862 pre_load=["author", "date", "message", "parents"])
1863 else:
1863 else:
1864 cs_cache = EmptyCommit()
1864 cs_cache = EmptyCommit()
1865
1865
1866 if isinstance(cs_cache, BaseChangeset):
1866 if isinstance(cs_cache, BaseChangeset):
1867 cs_cache = cs_cache.__json__()
1867 cs_cache = cs_cache.__json__()
1868
1868
1869 def is_outdated(new_cs_cache):
1869 def is_outdated(new_cs_cache):
1870 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1870 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1871 new_cs_cache['revision'] != self.changeset_cache['revision']):
1871 new_cs_cache['revision'] != self.changeset_cache['revision']):
1872 return True
1872 return True
1873 return False
1873 return False
1874
1874
1875 # check if we have maybe already latest cached revision
1875 # check if we have maybe already latest cached revision
1876 if is_outdated(cs_cache) or not self.changeset_cache:
1876 if is_outdated(cs_cache) or not self.changeset_cache:
1877 _default = datetime.datetime.fromtimestamp(0)
1877 _default = datetime.datetime.fromtimestamp(0)
1878 last_change = cs_cache.get('date') or _default
1878 last_change = cs_cache.get('date') or _default
1879 log.debug('updated repo %s with new cs cache %s',
1879 log.debug('updated repo %s with new cs cache %s',
1880 self.repo_name, cs_cache)
1880 self.repo_name, cs_cache)
1881 self.updated_on = last_change
1881 self.updated_on = last_change
1882 self.changeset_cache = cs_cache
1882 self.changeset_cache = cs_cache
1883 Session().add(self)
1883 Session().add(self)
1884 Session().commit()
1884 Session().commit()
1885 else:
1885 else:
1886 log.debug('Skipping update_commit_cache for repo:`%s` '
1886 log.debug('Skipping update_commit_cache for repo:`%s` '
1887 'commit already with latest changes', self.repo_name)
1887 'commit already with latest changes', self.repo_name)
1888
1888
1889 @property
1889 @property
1890 def tip(self):
1890 def tip(self):
1891 return self.get_commit('tip')
1891 return self.get_commit('tip')
1892
1892
1893 @property
1893 @property
1894 def author(self):
1894 def author(self):
1895 return self.tip.author
1895 return self.tip.author
1896
1896
1897 @property
1897 @property
1898 def last_change(self):
1898 def last_change(self):
1899 return self.scm_instance().last_change
1899 return self.scm_instance().last_change
1900
1900
1901 def get_comments(self, revisions=None):
1901 def get_comments(self, revisions=None):
1902 """
1902 """
1903 Returns comments for this repository grouped by revisions
1903 Returns comments for this repository grouped by revisions
1904
1904
1905 :param revisions: filter query by revisions only
1905 :param revisions: filter query by revisions only
1906 """
1906 """
1907 cmts = ChangesetComment.query()\
1907 cmts = ChangesetComment.query()\
1908 .filter(ChangesetComment.repo == self)
1908 .filter(ChangesetComment.repo == self)
1909 if revisions:
1909 if revisions:
1910 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1910 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1911 grouped = collections.defaultdict(list)
1911 grouped = collections.defaultdict(list)
1912 for cmt in cmts.all():
1912 for cmt in cmts.all():
1913 grouped[cmt.revision].append(cmt)
1913 grouped[cmt.revision].append(cmt)
1914 return grouped
1914 return grouped
1915
1915
1916 def statuses(self, revisions=None):
1916 def statuses(self, revisions=None):
1917 """
1917 """
1918 Returns statuses for this repository
1918 Returns statuses for this repository
1919
1919
1920 :param revisions: list of revisions to get statuses for
1920 :param revisions: list of revisions to get statuses for
1921 """
1921 """
1922 statuses = ChangesetStatus.query()\
1922 statuses = ChangesetStatus.query()\
1923 .filter(ChangesetStatus.repo == self)\
1923 .filter(ChangesetStatus.repo == self)\
1924 .filter(ChangesetStatus.version == 0)
1924 .filter(ChangesetStatus.version == 0)
1925
1925
1926 if revisions:
1926 if revisions:
1927 # Try doing the filtering in chunks to avoid hitting limits
1927 # Try doing the filtering in chunks to avoid hitting limits
1928 size = 500
1928 size = 500
1929 status_results = []
1929 status_results = []
1930 for chunk in xrange(0, len(revisions), size):
1930 for chunk in xrange(0, len(revisions), size):
1931 status_results += statuses.filter(
1931 status_results += statuses.filter(
1932 ChangesetStatus.revision.in_(
1932 ChangesetStatus.revision.in_(
1933 revisions[chunk: chunk+size])
1933 revisions[chunk: chunk+size])
1934 ).all()
1934 ).all()
1935 else:
1935 else:
1936 status_results = statuses.all()
1936 status_results = statuses.all()
1937
1937
1938 grouped = {}
1938 grouped = {}
1939
1939
1940 # maybe we have open new pullrequest without a status?
1940 # maybe we have open new pullrequest without a status?
1941 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1941 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1942 status_lbl = ChangesetStatus.get_status_lbl(stat)
1942 status_lbl = ChangesetStatus.get_status_lbl(stat)
1943 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1943 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1944 for rev in pr.revisions:
1944 for rev in pr.revisions:
1945 pr_id = pr.pull_request_id
1945 pr_id = pr.pull_request_id
1946 pr_repo = pr.target_repo.repo_name
1946 pr_repo = pr.target_repo.repo_name
1947 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1947 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1948
1948
1949 for stat in status_results:
1949 for stat in status_results:
1950 pr_id = pr_repo = None
1950 pr_id = pr_repo = None
1951 if stat.pull_request:
1951 if stat.pull_request:
1952 pr_id = stat.pull_request.pull_request_id
1952 pr_id = stat.pull_request.pull_request_id
1953 pr_repo = stat.pull_request.target_repo.repo_name
1953 pr_repo = stat.pull_request.target_repo.repo_name
1954 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1954 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1955 pr_id, pr_repo]
1955 pr_id, pr_repo]
1956 return grouped
1956 return grouped
1957
1957
1958 # ==========================================================================
1958 # ==========================================================================
1959 # SCM CACHE INSTANCE
1959 # SCM CACHE INSTANCE
1960 # ==========================================================================
1960 # ==========================================================================
1961
1961
1962 def scm_instance(self, **kwargs):
1962 def scm_instance(self, **kwargs):
1963 import rhodecode
1963 import rhodecode
1964
1964
1965 # Passing a config will not hit the cache currently only used
1965 # Passing a config will not hit the cache currently only used
1966 # for repo2dbmapper
1966 # for repo2dbmapper
1967 config = kwargs.pop('config', None)
1967 config = kwargs.pop('config', None)
1968 cache = kwargs.pop('cache', None)
1968 cache = kwargs.pop('cache', None)
1969 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1969 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1970 # if cache is NOT defined use default global, else we have a full
1970 # if cache is NOT defined use default global, else we have a full
1971 # control over cache behaviour
1971 # control over cache behaviour
1972 if cache is None and full_cache and not config:
1972 if cache is None and full_cache and not config:
1973 return self._get_instance_cached()
1973 return self._get_instance_cached()
1974 return self._get_instance(cache=bool(cache), config=config)
1974 return self._get_instance(cache=bool(cache), config=config)
1975
1975
1976 def _get_instance_cached(self):
1976 def _get_instance_cached(self):
1977 @cache_region('long_term')
1977 @cache_region('long_term')
1978 def _get_repo(cache_key):
1978 def _get_repo(cache_key):
1979 return self._get_instance()
1979 return self._get_instance()
1980
1980
1981 invalidator_context = CacheKey.repo_context_cache(
1981 invalidator_context = CacheKey.repo_context_cache(
1982 _get_repo, self.repo_name, None, thread_scoped=True)
1982 _get_repo, self.repo_name, None, thread_scoped=True)
1983
1983
1984 with invalidator_context as context:
1984 with invalidator_context as context:
1985 context.invalidate()
1985 context.invalidate()
1986 repo = context.compute()
1986 repo = context.compute()
1987
1987
1988 return repo
1988 return repo
1989
1989
1990 def _get_instance(self, cache=True, config=None):
1990 def _get_instance(self, cache=True, config=None):
1991 config = config or self._config
1991 config = config or self._config
1992 custom_wire = {
1992 custom_wire = {
1993 'cache': cache # controls the vcs.remote cache
1993 'cache': cache # controls the vcs.remote cache
1994 }
1994 }
1995
1995
1996 repo = get_vcs_instance(
1996 repo = get_vcs_instance(
1997 repo_path=safe_str(self.repo_full_path),
1997 repo_path=safe_str(self.repo_full_path),
1998 config=config,
1998 config=config,
1999 with_wire=custom_wire,
1999 with_wire=custom_wire,
2000 create=False)
2000 create=False)
2001
2001
2002 return repo
2002 return repo
2003
2003
2004 def __json__(self):
2004 def __json__(self):
2005 return {'landing_rev': self.landing_rev}
2005 return {'landing_rev': self.landing_rev}
2006
2006
2007 def get_dict(self):
2007 def get_dict(self):
2008
2008
2009 # Since we transformed `repo_name` to a hybrid property, we need to
2009 # Since we transformed `repo_name` to a hybrid property, we need to
2010 # keep compatibility with the code which uses `repo_name` field.
2010 # keep compatibility with the code which uses `repo_name` field.
2011
2011
2012 result = super(Repository, self).get_dict()
2012 result = super(Repository, self).get_dict()
2013 result['repo_name'] = result.pop('_repo_name', None)
2013 result['repo_name'] = result.pop('_repo_name', None)
2014 return result
2014 return result
2015
2015
2016
2016
2017 class RepoGroup(Base, BaseModel):
2017 class RepoGroup(Base, BaseModel):
2018 __tablename__ = 'groups'
2018 __tablename__ = 'groups'
2019 __table_args__ = (
2019 __table_args__ = (
2020 UniqueConstraint('group_name', 'group_parent_id'),
2020 UniqueConstraint('group_name', 'group_parent_id'),
2021 CheckConstraint('group_id != group_parent_id'),
2021 CheckConstraint('group_id != group_parent_id'),
2022 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2022 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2023 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2023 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2024 )
2024 )
2025 __mapper_args__ = {'order_by': 'group_name'}
2025 __mapper_args__ = {'order_by': 'group_name'}
2026
2026
2027 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2027 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2028
2028
2029 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2029 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2030 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2030 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2031 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2031 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2032 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2032 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2033 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2033 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2035 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2035 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2036
2036
2037 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2037 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2038 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2038 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2039 parent_group = relationship('RepoGroup', remote_side=group_id)
2039 parent_group = relationship('RepoGroup', remote_side=group_id)
2040 user = relationship('User')
2040 user = relationship('User')
2041 integrations = relationship('Integration',
2041 integrations = relationship('Integration',
2042 cascade="all, delete, delete-orphan")
2042 cascade="all, delete, delete-orphan")
2043
2043
2044 def __init__(self, group_name='', parent_group=None):
2044 def __init__(self, group_name='', parent_group=None):
2045 self.group_name = group_name
2045 self.group_name = group_name
2046 self.parent_group = parent_group
2046 self.parent_group = parent_group
2047
2047
2048 def __unicode__(self):
2048 def __unicode__(self):
2049 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2049 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2050 self.group_name)
2050 self.group_name)
2051
2051
2052 @classmethod
2052 @classmethod
2053 def _generate_choice(cls, repo_group):
2053 def _generate_choice(cls, repo_group):
2054 from webhelpers.html import literal as _literal
2054 from webhelpers.html import literal as _literal
2055 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2055 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2056 return repo_group.group_id, _name(repo_group.full_path_splitted)
2056 return repo_group.group_id, _name(repo_group.full_path_splitted)
2057
2057
2058 @classmethod
2058 @classmethod
2059 def groups_choices(cls, groups=None, show_empty_group=True):
2059 def groups_choices(cls, groups=None, show_empty_group=True):
2060 if not groups:
2060 if not groups:
2061 groups = cls.query().all()
2061 groups = cls.query().all()
2062
2062
2063 repo_groups = []
2063 repo_groups = []
2064 if show_empty_group:
2064 if show_empty_group:
2065 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2065 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2066
2066
2067 repo_groups.extend([cls._generate_choice(x) for x in groups])
2067 repo_groups.extend([cls._generate_choice(x) for x in groups])
2068
2068
2069 repo_groups = sorted(
2069 repo_groups = sorted(
2070 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2070 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2071 return repo_groups
2071 return repo_groups
2072
2072
2073 @classmethod
2073 @classmethod
2074 def url_sep(cls):
2074 def url_sep(cls):
2075 return URL_SEP
2075 return URL_SEP
2076
2076
2077 @classmethod
2077 @classmethod
2078 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2078 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2079 if case_insensitive:
2079 if case_insensitive:
2080 gr = cls.query().filter(func.lower(cls.group_name)
2080 gr = cls.query().filter(func.lower(cls.group_name)
2081 == func.lower(group_name))
2081 == func.lower(group_name))
2082 else:
2082 else:
2083 gr = cls.query().filter(cls.group_name == group_name)
2083 gr = cls.query().filter(cls.group_name == group_name)
2084 if cache:
2084 if cache:
2085 gr = gr.options(FromCache(
2085 gr = gr.options(FromCache(
2086 "sql_cache_short",
2086 "sql_cache_short",
2087 "get_group_%s" % _hash_key(group_name)))
2087 "get_group_%s" % _hash_key(group_name)))
2088 return gr.scalar()
2088 return gr.scalar()
2089
2089
2090 @classmethod
2090 @classmethod
2091 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2091 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2092 case_insensitive=True):
2092 case_insensitive=True):
2093 q = RepoGroup.query()
2093 q = RepoGroup.query()
2094
2094
2095 if not isinstance(user_id, Optional):
2095 if not isinstance(user_id, Optional):
2096 q = q.filter(RepoGroup.user_id == user_id)
2096 q = q.filter(RepoGroup.user_id == user_id)
2097
2097
2098 if not isinstance(group_id, Optional):
2098 if not isinstance(group_id, Optional):
2099 q = q.filter(RepoGroup.group_parent_id == group_id)
2099 q = q.filter(RepoGroup.group_parent_id == group_id)
2100
2100
2101 if case_insensitive:
2101 if case_insensitive:
2102 q = q.order_by(func.lower(RepoGroup.group_name))
2102 q = q.order_by(func.lower(RepoGroup.group_name))
2103 else:
2103 else:
2104 q = q.order_by(RepoGroup.group_name)
2104 q = q.order_by(RepoGroup.group_name)
2105 return q.all()
2105 return q.all()
2106
2106
2107 @property
2107 @property
2108 def parents(self):
2108 def parents(self):
2109 parents_recursion_limit = 10
2109 parents_recursion_limit = 10
2110 groups = []
2110 groups = []
2111 if self.parent_group is None:
2111 if self.parent_group is None:
2112 return groups
2112 return groups
2113 cur_gr = self.parent_group
2113 cur_gr = self.parent_group
2114 groups.insert(0, cur_gr)
2114 groups.insert(0, cur_gr)
2115 cnt = 0
2115 cnt = 0
2116 while 1:
2116 while 1:
2117 cnt += 1
2117 cnt += 1
2118 gr = getattr(cur_gr, 'parent_group', None)
2118 gr = getattr(cur_gr, 'parent_group', None)
2119 cur_gr = cur_gr.parent_group
2119 cur_gr = cur_gr.parent_group
2120 if gr is None:
2120 if gr is None:
2121 break
2121 break
2122 if cnt == parents_recursion_limit:
2122 if cnt == parents_recursion_limit:
2123 # this will prevent accidental infinit loops
2123 # this will prevent accidental infinit loops
2124 log.error(('more than %s parents found for group %s, stopping '
2124 log.error(('more than %s parents found for group %s, stopping '
2125 'recursive parent fetching' % (parents_recursion_limit, self)))
2125 'recursive parent fetching' % (parents_recursion_limit, self)))
2126 break
2126 break
2127
2127
2128 groups.insert(0, gr)
2128 groups.insert(0, gr)
2129 return groups
2129 return groups
2130
2130
2131 @property
2131 @property
2132 def children(self):
2132 def children(self):
2133 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2133 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2134
2134
2135 @property
2135 @property
2136 def name(self):
2136 def name(self):
2137 return self.group_name.split(RepoGroup.url_sep())[-1]
2137 return self.group_name.split(RepoGroup.url_sep())[-1]
2138
2138
2139 @property
2139 @property
2140 def full_path(self):
2140 def full_path(self):
2141 return self.group_name
2141 return self.group_name
2142
2142
2143 @property
2143 @property
2144 def full_path_splitted(self):
2144 def full_path_splitted(self):
2145 return self.group_name.split(RepoGroup.url_sep())
2145 return self.group_name.split(RepoGroup.url_sep())
2146
2146
2147 @property
2147 @property
2148 def repositories(self):
2148 def repositories(self):
2149 return Repository.query()\
2149 return Repository.query()\
2150 .filter(Repository.group == self)\
2150 .filter(Repository.group == self)\
2151 .order_by(Repository.repo_name)
2151 .order_by(Repository.repo_name)
2152
2152
2153 @property
2153 @property
2154 def repositories_recursive_count(self):
2154 def repositories_recursive_count(self):
2155 cnt = self.repositories.count()
2155 cnt = self.repositories.count()
2156
2156
2157 def children_count(group):
2157 def children_count(group):
2158 cnt = 0
2158 cnt = 0
2159 for child in group.children:
2159 for child in group.children:
2160 cnt += child.repositories.count()
2160 cnt += child.repositories.count()
2161 cnt += children_count(child)
2161 cnt += children_count(child)
2162 return cnt
2162 return cnt
2163
2163
2164 return cnt + children_count(self)
2164 return cnt + children_count(self)
2165
2165
2166 def _recursive_objects(self, include_repos=True):
2166 def _recursive_objects(self, include_repos=True):
2167 all_ = []
2167 all_ = []
2168
2168
2169 def _get_members(root_gr):
2169 def _get_members(root_gr):
2170 if include_repos:
2170 if include_repos:
2171 for r in root_gr.repositories:
2171 for r in root_gr.repositories:
2172 all_.append(r)
2172 all_.append(r)
2173 childs = root_gr.children.all()
2173 childs = root_gr.children.all()
2174 if childs:
2174 if childs:
2175 for gr in childs:
2175 for gr in childs:
2176 all_.append(gr)
2176 all_.append(gr)
2177 _get_members(gr)
2177 _get_members(gr)
2178
2178
2179 _get_members(self)
2179 _get_members(self)
2180 return [self] + all_
2180 return [self] + all_
2181
2181
2182 def recursive_groups_and_repos(self):
2182 def recursive_groups_and_repos(self):
2183 """
2183 """
2184 Recursive return all groups, with repositories in those groups
2184 Recursive return all groups, with repositories in those groups
2185 """
2185 """
2186 return self._recursive_objects()
2186 return self._recursive_objects()
2187
2187
2188 def recursive_groups(self):
2188 def recursive_groups(self):
2189 """
2189 """
2190 Returns all children groups for this group including children of children
2190 Returns all children groups for this group including children of children
2191 """
2191 """
2192 return self._recursive_objects(include_repos=False)
2192 return self._recursive_objects(include_repos=False)
2193
2193
2194 def get_new_name(self, group_name):
2194 def get_new_name(self, group_name):
2195 """
2195 """
2196 returns new full group name based on parent and new name
2196 returns new full group name based on parent and new name
2197
2197
2198 :param group_name:
2198 :param group_name:
2199 """
2199 """
2200 path_prefix = (self.parent_group.full_path_splitted if
2200 path_prefix = (self.parent_group.full_path_splitted if
2201 self.parent_group else [])
2201 self.parent_group else [])
2202 return RepoGroup.url_sep().join(path_prefix + [group_name])
2202 return RepoGroup.url_sep().join(path_prefix + [group_name])
2203
2203
2204 def permissions(self, with_admins=True, with_owner=True):
2204 def permissions(self, with_admins=True, with_owner=True):
2205 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2205 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2206 q = q.options(joinedload(UserRepoGroupToPerm.group),
2206 q = q.options(joinedload(UserRepoGroupToPerm.group),
2207 joinedload(UserRepoGroupToPerm.user),
2207 joinedload(UserRepoGroupToPerm.user),
2208 joinedload(UserRepoGroupToPerm.permission),)
2208 joinedload(UserRepoGroupToPerm.permission),)
2209
2209
2210 # get owners and admins and permissions. We do a trick of re-writing
2210 # get owners and admins and permissions. We do a trick of re-writing
2211 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2211 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2212 # has a global reference and changing one object propagates to all
2212 # has a global reference and changing one object propagates to all
2213 # others. This means if admin is also an owner admin_row that change
2213 # others. This means if admin is also an owner admin_row that change
2214 # would propagate to both objects
2214 # would propagate to both objects
2215 perm_rows = []
2215 perm_rows = []
2216 for _usr in q.all():
2216 for _usr in q.all():
2217 usr = AttributeDict(_usr.user.get_dict())
2217 usr = AttributeDict(_usr.user.get_dict())
2218 usr.permission = _usr.permission.permission_name
2218 usr.permission = _usr.permission.permission_name
2219 perm_rows.append(usr)
2219 perm_rows.append(usr)
2220
2220
2221 # filter the perm rows by 'default' first and then sort them by
2221 # filter the perm rows by 'default' first and then sort them by
2222 # admin,write,read,none permissions sorted again alphabetically in
2222 # admin,write,read,none permissions sorted again alphabetically in
2223 # each group
2223 # each group
2224 perm_rows = sorted(perm_rows, key=display_sort)
2224 perm_rows = sorted(perm_rows, key=display_sort)
2225
2225
2226 _admin_perm = 'group.admin'
2226 _admin_perm = 'group.admin'
2227 owner_row = []
2227 owner_row = []
2228 if with_owner:
2228 if with_owner:
2229 usr = AttributeDict(self.user.get_dict())
2229 usr = AttributeDict(self.user.get_dict())
2230 usr.owner_row = True
2230 usr.owner_row = True
2231 usr.permission = _admin_perm
2231 usr.permission = _admin_perm
2232 owner_row.append(usr)
2232 owner_row.append(usr)
2233
2233
2234 super_admin_rows = []
2234 super_admin_rows = []
2235 if with_admins:
2235 if with_admins:
2236 for usr in User.get_all_super_admins():
2236 for usr in User.get_all_super_admins():
2237 # if this admin is also owner, don't double the record
2237 # if this admin is also owner, don't double the record
2238 if usr.user_id == owner_row[0].user_id:
2238 if usr.user_id == owner_row[0].user_id:
2239 owner_row[0].admin_row = True
2239 owner_row[0].admin_row = True
2240 else:
2240 else:
2241 usr = AttributeDict(usr.get_dict())
2241 usr = AttributeDict(usr.get_dict())
2242 usr.admin_row = True
2242 usr.admin_row = True
2243 usr.permission = _admin_perm
2243 usr.permission = _admin_perm
2244 super_admin_rows.append(usr)
2244 super_admin_rows.append(usr)
2245
2245
2246 return super_admin_rows + owner_row + perm_rows
2246 return super_admin_rows + owner_row + perm_rows
2247
2247
2248 def permission_user_groups(self):
2248 def permission_user_groups(self):
2249 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2249 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2250 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2250 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2251 joinedload(UserGroupRepoGroupToPerm.users_group),
2251 joinedload(UserGroupRepoGroupToPerm.users_group),
2252 joinedload(UserGroupRepoGroupToPerm.permission),)
2252 joinedload(UserGroupRepoGroupToPerm.permission),)
2253
2253
2254 perm_rows = []
2254 perm_rows = []
2255 for _user_group in q.all():
2255 for _user_group in q.all():
2256 usr = AttributeDict(_user_group.users_group.get_dict())
2256 usr = AttributeDict(_user_group.users_group.get_dict())
2257 usr.permission = _user_group.permission.permission_name
2257 usr.permission = _user_group.permission.permission_name
2258 perm_rows.append(usr)
2258 perm_rows.append(usr)
2259
2259
2260 return perm_rows
2260 return perm_rows
2261
2261
2262 def get_api_data(self):
2262 def get_api_data(self):
2263 """
2263 """
2264 Common function for generating api data
2264 Common function for generating api data
2265
2265
2266 """
2266 """
2267 group = self
2267 group = self
2268 data = {
2268 data = {
2269 'group_id': group.group_id,
2269 'group_id': group.group_id,
2270 'group_name': group.group_name,
2270 'group_name': group.group_name,
2271 'group_description': group.group_description,
2271 'group_description': group.group_description,
2272 'parent_group': group.parent_group.group_name if group.parent_group else None,
2272 'parent_group': group.parent_group.group_name if group.parent_group else None,
2273 'repositories': [x.repo_name for x in group.repositories],
2273 'repositories': [x.repo_name for x in group.repositories],
2274 'owner': group.user.username,
2274 'owner': group.user.username,
2275 }
2275 }
2276 return data
2276 return data
2277
2277
2278
2278
2279 class Permission(Base, BaseModel):
2279 class Permission(Base, BaseModel):
2280 __tablename__ = 'permissions'
2280 __tablename__ = 'permissions'
2281 __table_args__ = (
2281 __table_args__ = (
2282 Index('p_perm_name_idx', 'permission_name'),
2282 Index('p_perm_name_idx', 'permission_name'),
2283 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2283 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2284 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2284 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2285 )
2285 )
2286 PERMS = [
2286 PERMS = [
2287 ('hg.admin', _('RhodeCode Super Administrator')),
2287 ('hg.admin', _('RhodeCode Super Administrator')),
2288
2288
2289 ('repository.none', _('Repository no access')),
2289 ('repository.none', _('Repository no access')),
2290 ('repository.read', _('Repository read access')),
2290 ('repository.read', _('Repository read access')),
2291 ('repository.write', _('Repository write access')),
2291 ('repository.write', _('Repository write access')),
2292 ('repository.admin', _('Repository admin access')),
2292 ('repository.admin', _('Repository admin access')),
2293
2293
2294 ('group.none', _('Repository group no access')),
2294 ('group.none', _('Repository group no access')),
2295 ('group.read', _('Repository group read access')),
2295 ('group.read', _('Repository group read access')),
2296 ('group.write', _('Repository group write access')),
2296 ('group.write', _('Repository group write access')),
2297 ('group.admin', _('Repository group admin access')),
2297 ('group.admin', _('Repository group admin access')),
2298
2298
2299 ('usergroup.none', _('User group no access')),
2299 ('usergroup.none', _('User group no access')),
2300 ('usergroup.read', _('User group read access')),
2300 ('usergroup.read', _('User group read access')),
2301 ('usergroup.write', _('User group write access')),
2301 ('usergroup.write', _('User group write access')),
2302 ('usergroup.admin', _('User group admin access')),
2302 ('usergroup.admin', _('User group admin access')),
2303
2303
2304 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2304 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2305 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2305 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2306
2306
2307 ('hg.usergroup.create.false', _('User Group creation disabled')),
2307 ('hg.usergroup.create.false', _('User Group creation disabled')),
2308 ('hg.usergroup.create.true', _('User Group creation enabled')),
2308 ('hg.usergroup.create.true', _('User Group creation enabled')),
2309
2309
2310 ('hg.create.none', _('Repository creation disabled')),
2310 ('hg.create.none', _('Repository creation disabled')),
2311 ('hg.create.repository', _('Repository creation enabled')),
2311 ('hg.create.repository', _('Repository creation enabled')),
2312 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2312 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2313 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2313 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2314
2314
2315 ('hg.fork.none', _('Repository forking disabled')),
2315 ('hg.fork.none', _('Repository forking disabled')),
2316 ('hg.fork.repository', _('Repository forking enabled')),
2316 ('hg.fork.repository', _('Repository forking enabled')),
2317
2317
2318 ('hg.register.none', _('Registration disabled')),
2318 ('hg.register.none', _('Registration disabled')),
2319 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2319 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2320 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2320 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2321
2321
2322 ('hg.extern_activate.manual', _('Manual activation of external account')),
2322 ('hg.extern_activate.manual', _('Manual activation of external account')),
2323 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2323 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2324
2324
2325 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2325 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2326 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2326 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2327 ]
2327 ]
2328
2328
2329 # definition of system default permissions for DEFAULT user
2329 # definition of system default permissions for DEFAULT user
2330 DEFAULT_USER_PERMISSIONS = [
2330 DEFAULT_USER_PERMISSIONS = [
2331 'repository.read',
2331 'repository.read',
2332 'group.read',
2332 'group.read',
2333 'usergroup.read',
2333 'usergroup.read',
2334 'hg.create.repository',
2334 'hg.create.repository',
2335 'hg.repogroup.create.false',
2335 'hg.repogroup.create.false',
2336 'hg.usergroup.create.false',
2336 'hg.usergroup.create.false',
2337 'hg.create.write_on_repogroup.true',
2337 'hg.create.write_on_repogroup.true',
2338 'hg.fork.repository',
2338 'hg.fork.repository',
2339 'hg.register.manual_activate',
2339 'hg.register.manual_activate',
2340 'hg.extern_activate.auto',
2340 'hg.extern_activate.auto',
2341 'hg.inherit_default_perms.true',
2341 'hg.inherit_default_perms.true',
2342 ]
2342 ]
2343
2343
2344 # defines which permissions are more important higher the more important
2344 # defines which permissions are more important higher the more important
2345 # Weight defines which permissions are more important.
2345 # Weight defines which permissions are more important.
2346 # The higher number the more important.
2346 # The higher number the more important.
2347 PERM_WEIGHTS = {
2347 PERM_WEIGHTS = {
2348 'repository.none': 0,
2348 'repository.none': 0,
2349 'repository.read': 1,
2349 'repository.read': 1,
2350 'repository.write': 3,
2350 'repository.write': 3,
2351 'repository.admin': 4,
2351 'repository.admin': 4,
2352
2352
2353 'group.none': 0,
2353 'group.none': 0,
2354 'group.read': 1,
2354 'group.read': 1,
2355 'group.write': 3,
2355 'group.write': 3,
2356 'group.admin': 4,
2356 'group.admin': 4,
2357
2357
2358 'usergroup.none': 0,
2358 'usergroup.none': 0,
2359 'usergroup.read': 1,
2359 'usergroup.read': 1,
2360 'usergroup.write': 3,
2360 'usergroup.write': 3,
2361 'usergroup.admin': 4,
2361 'usergroup.admin': 4,
2362
2362
2363 'hg.repogroup.create.false': 0,
2363 'hg.repogroup.create.false': 0,
2364 'hg.repogroup.create.true': 1,
2364 'hg.repogroup.create.true': 1,
2365
2365
2366 'hg.usergroup.create.false': 0,
2366 'hg.usergroup.create.false': 0,
2367 'hg.usergroup.create.true': 1,
2367 'hg.usergroup.create.true': 1,
2368
2368
2369 'hg.fork.none': 0,
2369 'hg.fork.none': 0,
2370 'hg.fork.repository': 1,
2370 'hg.fork.repository': 1,
2371 'hg.create.none': 0,
2371 'hg.create.none': 0,
2372 'hg.create.repository': 1
2372 'hg.create.repository': 1
2373 }
2373 }
2374
2374
2375 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2375 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2376 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2376 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2377 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2377 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2378
2378
2379 def __unicode__(self):
2379 def __unicode__(self):
2380 return u"<%s('%s:%s')>" % (
2380 return u"<%s('%s:%s')>" % (
2381 self.__class__.__name__, self.permission_id, self.permission_name
2381 self.__class__.__name__, self.permission_id, self.permission_name
2382 )
2382 )
2383
2383
2384 @classmethod
2384 @classmethod
2385 def get_by_key(cls, key):
2385 def get_by_key(cls, key):
2386 return cls.query().filter(cls.permission_name == key).scalar()
2386 return cls.query().filter(cls.permission_name == key).scalar()
2387
2387
2388 @classmethod
2388 @classmethod
2389 def get_default_repo_perms(cls, user_id, repo_id=None):
2389 def get_default_repo_perms(cls, user_id, repo_id=None):
2390 q = Session().query(UserRepoToPerm, Repository, Permission)\
2390 q = Session().query(UserRepoToPerm, Repository, Permission)\
2391 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2391 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2392 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2392 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2393 .filter(UserRepoToPerm.user_id == user_id)
2393 .filter(UserRepoToPerm.user_id == user_id)
2394 if repo_id:
2394 if repo_id:
2395 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2395 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2396 return q.all()
2396 return q.all()
2397
2397
2398 @classmethod
2398 @classmethod
2399 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2399 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2400 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2400 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2401 .join(
2401 .join(
2402 Permission,
2402 Permission,
2403 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2403 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2404 .join(
2404 .join(
2405 Repository,
2405 Repository,
2406 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2406 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2407 .join(
2407 .join(
2408 UserGroup,
2408 UserGroup,
2409 UserGroupRepoToPerm.users_group_id ==
2409 UserGroupRepoToPerm.users_group_id ==
2410 UserGroup.users_group_id)\
2410 UserGroup.users_group_id)\
2411 .join(
2411 .join(
2412 UserGroupMember,
2412 UserGroupMember,
2413 UserGroupRepoToPerm.users_group_id ==
2413 UserGroupRepoToPerm.users_group_id ==
2414 UserGroupMember.users_group_id)\
2414 UserGroupMember.users_group_id)\
2415 .filter(
2415 .filter(
2416 UserGroupMember.user_id == user_id,
2416 UserGroupMember.user_id == user_id,
2417 UserGroup.users_group_active == true())
2417 UserGroup.users_group_active == true())
2418 if repo_id:
2418 if repo_id:
2419 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2419 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2420 return q.all()
2420 return q.all()
2421
2421
2422 @classmethod
2422 @classmethod
2423 def get_default_group_perms(cls, user_id, repo_group_id=None):
2423 def get_default_group_perms(cls, user_id, repo_group_id=None):
2424 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2424 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2425 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2425 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2426 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2426 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2427 .filter(UserRepoGroupToPerm.user_id == user_id)
2427 .filter(UserRepoGroupToPerm.user_id == user_id)
2428 if repo_group_id:
2428 if repo_group_id:
2429 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2429 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2430 return q.all()
2430 return q.all()
2431
2431
2432 @classmethod
2432 @classmethod
2433 def get_default_group_perms_from_user_group(
2433 def get_default_group_perms_from_user_group(
2434 cls, user_id, repo_group_id=None):
2434 cls, user_id, repo_group_id=None):
2435 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2435 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2436 .join(
2436 .join(
2437 Permission,
2437 Permission,
2438 UserGroupRepoGroupToPerm.permission_id ==
2438 UserGroupRepoGroupToPerm.permission_id ==
2439 Permission.permission_id)\
2439 Permission.permission_id)\
2440 .join(
2440 .join(
2441 RepoGroup,
2441 RepoGroup,
2442 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2442 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2443 .join(
2443 .join(
2444 UserGroup,
2444 UserGroup,
2445 UserGroupRepoGroupToPerm.users_group_id ==
2445 UserGroupRepoGroupToPerm.users_group_id ==
2446 UserGroup.users_group_id)\
2446 UserGroup.users_group_id)\
2447 .join(
2447 .join(
2448 UserGroupMember,
2448 UserGroupMember,
2449 UserGroupRepoGroupToPerm.users_group_id ==
2449 UserGroupRepoGroupToPerm.users_group_id ==
2450 UserGroupMember.users_group_id)\
2450 UserGroupMember.users_group_id)\
2451 .filter(
2451 .filter(
2452 UserGroupMember.user_id == user_id,
2452 UserGroupMember.user_id == user_id,
2453 UserGroup.users_group_active == true())
2453 UserGroup.users_group_active == true())
2454 if repo_group_id:
2454 if repo_group_id:
2455 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2455 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2456 return q.all()
2456 return q.all()
2457
2457
2458 @classmethod
2458 @classmethod
2459 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2459 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2460 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2460 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2461 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2461 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2462 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2462 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2463 .filter(UserUserGroupToPerm.user_id == user_id)
2463 .filter(UserUserGroupToPerm.user_id == user_id)
2464 if user_group_id:
2464 if user_group_id:
2465 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2465 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2466 return q.all()
2466 return q.all()
2467
2467
2468 @classmethod
2468 @classmethod
2469 def get_default_user_group_perms_from_user_group(
2469 def get_default_user_group_perms_from_user_group(
2470 cls, user_id, user_group_id=None):
2470 cls, user_id, user_group_id=None):
2471 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2471 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2472 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2472 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2473 .join(
2473 .join(
2474 Permission,
2474 Permission,
2475 UserGroupUserGroupToPerm.permission_id ==
2475 UserGroupUserGroupToPerm.permission_id ==
2476 Permission.permission_id)\
2476 Permission.permission_id)\
2477 .join(
2477 .join(
2478 TargetUserGroup,
2478 TargetUserGroup,
2479 UserGroupUserGroupToPerm.target_user_group_id ==
2479 UserGroupUserGroupToPerm.target_user_group_id ==
2480 TargetUserGroup.users_group_id)\
2480 TargetUserGroup.users_group_id)\
2481 .join(
2481 .join(
2482 UserGroup,
2482 UserGroup,
2483 UserGroupUserGroupToPerm.user_group_id ==
2483 UserGroupUserGroupToPerm.user_group_id ==
2484 UserGroup.users_group_id)\
2484 UserGroup.users_group_id)\
2485 .join(
2485 .join(
2486 UserGroupMember,
2486 UserGroupMember,
2487 UserGroupUserGroupToPerm.user_group_id ==
2487 UserGroupUserGroupToPerm.user_group_id ==
2488 UserGroupMember.users_group_id)\
2488 UserGroupMember.users_group_id)\
2489 .filter(
2489 .filter(
2490 UserGroupMember.user_id == user_id,
2490 UserGroupMember.user_id == user_id,
2491 UserGroup.users_group_active == true())
2491 UserGroup.users_group_active == true())
2492 if user_group_id:
2492 if user_group_id:
2493 q = q.filter(
2493 q = q.filter(
2494 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2494 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2495
2495
2496 return q.all()
2496 return q.all()
2497
2497
2498
2498
2499 class UserRepoToPerm(Base, BaseModel):
2499 class UserRepoToPerm(Base, BaseModel):
2500 __tablename__ = 'repo_to_perm'
2500 __tablename__ = 'repo_to_perm'
2501 __table_args__ = (
2501 __table_args__ = (
2502 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2502 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2503 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2503 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2504 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2504 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2505 )
2505 )
2506 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2506 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2507 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2507 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2508 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2508 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2509 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2509 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2510
2510
2511 user = relationship('User')
2511 user = relationship('User')
2512 repository = relationship('Repository')
2512 repository = relationship('Repository')
2513 permission = relationship('Permission')
2513 permission = relationship('Permission')
2514
2514
2515 @classmethod
2515 @classmethod
2516 def create(cls, user, repository, permission):
2516 def create(cls, user, repository, permission):
2517 n = cls()
2517 n = cls()
2518 n.user = user
2518 n.user = user
2519 n.repository = repository
2519 n.repository = repository
2520 n.permission = permission
2520 n.permission = permission
2521 Session().add(n)
2521 Session().add(n)
2522 return n
2522 return n
2523
2523
2524 def __unicode__(self):
2524 def __unicode__(self):
2525 return u'<%s => %s >' % (self.user, self.repository)
2525 return u'<%s => %s >' % (self.user, self.repository)
2526
2526
2527
2527
2528 class UserUserGroupToPerm(Base, BaseModel):
2528 class UserUserGroupToPerm(Base, BaseModel):
2529 __tablename__ = 'user_user_group_to_perm'
2529 __tablename__ = 'user_user_group_to_perm'
2530 __table_args__ = (
2530 __table_args__ = (
2531 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2531 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2532 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2532 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2533 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2533 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2534 )
2534 )
2535 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2535 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2536 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2536 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2537 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2537 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2538 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2538 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2539
2539
2540 user = relationship('User')
2540 user = relationship('User')
2541 user_group = relationship('UserGroup')
2541 user_group = relationship('UserGroup')
2542 permission = relationship('Permission')
2542 permission = relationship('Permission')
2543
2543
2544 @classmethod
2544 @classmethod
2545 def create(cls, user, user_group, permission):
2545 def create(cls, user, user_group, permission):
2546 n = cls()
2546 n = cls()
2547 n.user = user
2547 n.user = user
2548 n.user_group = user_group
2548 n.user_group = user_group
2549 n.permission = permission
2549 n.permission = permission
2550 Session().add(n)
2550 Session().add(n)
2551 return n
2551 return n
2552
2552
2553 def __unicode__(self):
2553 def __unicode__(self):
2554 return u'<%s => %s >' % (self.user, self.user_group)
2554 return u'<%s => %s >' % (self.user, self.user_group)
2555
2555
2556
2556
2557 class UserToPerm(Base, BaseModel):
2557 class UserToPerm(Base, BaseModel):
2558 __tablename__ = 'user_to_perm'
2558 __tablename__ = 'user_to_perm'
2559 __table_args__ = (
2559 __table_args__ = (
2560 UniqueConstraint('user_id', 'permission_id'),
2560 UniqueConstraint('user_id', 'permission_id'),
2561 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2561 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2562 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2562 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2563 )
2563 )
2564 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2564 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2565 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2565 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2566 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2566 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2567
2567
2568 user = relationship('User')
2568 user = relationship('User')
2569 permission = relationship('Permission', lazy='joined')
2569 permission = relationship('Permission', lazy='joined')
2570
2570
2571 def __unicode__(self):
2571 def __unicode__(self):
2572 return u'<%s => %s >' % (self.user, self.permission)
2572 return u'<%s => %s >' % (self.user, self.permission)
2573
2573
2574
2574
2575 class UserGroupRepoToPerm(Base, BaseModel):
2575 class UserGroupRepoToPerm(Base, BaseModel):
2576 __tablename__ = 'users_group_repo_to_perm'
2576 __tablename__ = 'users_group_repo_to_perm'
2577 __table_args__ = (
2577 __table_args__ = (
2578 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2578 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2579 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2579 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2580 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2580 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2581 )
2581 )
2582 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2582 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2583 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2583 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2584 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2584 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2585 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2585 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2586
2586
2587 users_group = relationship('UserGroup')
2587 users_group = relationship('UserGroup')
2588 permission = relationship('Permission')
2588 permission = relationship('Permission')
2589 repository = relationship('Repository')
2589 repository = relationship('Repository')
2590
2590
2591 @classmethod
2591 @classmethod
2592 def create(cls, users_group, repository, permission):
2592 def create(cls, users_group, repository, permission):
2593 n = cls()
2593 n = cls()
2594 n.users_group = users_group
2594 n.users_group = users_group
2595 n.repository = repository
2595 n.repository = repository
2596 n.permission = permission
2596 n.permission = permission
2597 Session().add(n)
2597 Session().add(n)
2598 return n
2598 return n
2599
2599
2600 def __unicode__(self):
2600 def __unicode__(self):
2601 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2601 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2602
2602
2603
2603
2604 class UserGroupUserGroupToPerm(Base, BaseModel):
2604 class UserGroupUserGroupToPerm(Base, BaseModel):
2605 __tablename__ = 'user_group_user_group_to_perm'
2605 __tablename__ = 'user_group_user_group_to_perm'
2606 __table_args__ = (
2606 __table_args__ = (
2607 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2607 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2608 CheckConstraint('target_user_group_id != user_group_id'),
2608 CheckConstraint('target_user_group_id != user_group_id'),
2609 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2609 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2610 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2610 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2611 )
2611 )
2612 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2612 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2613 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2613 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2614 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2614 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2615 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2615 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2616
2616
2617 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2617 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2618 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2618 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2619 permission = relationship('Permission')
2619 permission = relationship('Permission')
2620
2620
2621 @classmethod
2621 @classmethod
2622 def create(cls, target_user_group, user_group, permission):
2622 def create(cls, target_user_group, user_group, permission):
2623 n = cls()
2623 n = cls()
2624 n.target_user_group = target_user_group
2624 n.target_user_group = target_user_group
2625 n.user_group = user_group
2625 n.user_group = user_group
2626 n.permission = permission
2626 n.permission = permission
2627 Session().add(n)
2627 Session().add(n)
2628 return n
2628 return n
2629
2629
2630 def __unicode__(self):
2630 def __unicode__(self):
2631 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2631 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2632
2632
2633
2633
2634 class UserGroupToPerm(Base, BaseModel):
2634 class UserGroupToPerm(Base, BaseModel):
2635 __tablename__ = 'users_group_to_perm'
2635 __tablename__ = 'users_group_to_perm'
2636 __table_args__ = (
2636 __table_args__ = (
2637 UniqueConstraint('users_group_id', 'permission_id',),
2637 UniqueConstraint('users_group_id', 'permission_id',),
2638 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2638 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2639 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2639 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2640 )
2640 )
2641 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2641 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2642 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2642 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2643 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2643 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2644
2644
2645 users_group = relationship('UserGroup')
2645 users_group = relationship('UserGroup')
2646 permission = relationship('Permission')
2646 permission = relationship('Permission')
2647
2647
2648
2648
2649 class UserRepoGroupToPerm(Base, BaseModel):
2649 class UserRepoGroupToPerm(Base, BaseModel):
2650 __tablename__ = 'user_repo_group_to_perm'
2650 __tablename__ = 'user_repo_group_to_perm'
2651 __table_args__ = (
2651 __table_args__ = (
2652 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2652 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2653 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2653 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2654 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2654 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2655 )
2655 )
2656
2656
2657 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2657 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2658 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2658 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2659 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2659 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2660 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2660 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2661
2661
2662 user = relationship('User')
2662 user = relationship('User')
2663 group = relationship('RepoGroup')
2663 group = relationship('RepoGroup')
2664 permission = relationship('Permission')
2664 permission = relationship('Permission')
2665
2665
2666 @classmethod
2666 @classmethod
2667 def create(cls, user, repository_group, permission):
2667 def create(cls, user, repository_group, permission):
2668 n = cls()
2668 n = cls()
2669 n.user = user
2669 n.user = user
2670 n.group = repository_group
2670 n.group = repository_group
2671 n.permission = permission
2671 n.permission = permission
2672 Session().add(n)
2672 Session().add(n)
2673 return n
2673 return n
2674
2674
2675
2675
2676 class UserGroupRepoGroupToPerm(Base, BaseModel):
2676 class UserGroupRepoGroupToPerm(Base, BaseModel):
2677 __tablename__ = 'users_group_repo_group_to_perm'
2677 __tablename__ = 'users_group_repo_group_to_perm'
2678 __table_args__ = (
2678 __table_args__ = (
2679 UniqueConstraint('users_group_id', 'group_id'),
2679 UniqueConstraint('users_group_id', 'group_id'),
2680 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2680 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2681 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2681 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2682 )
2682 )
2683
2683
2684 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2684 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2685 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2685 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2686 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2686 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2687 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2687 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2688
2688
2689 users_group = relationship('UserGroup')
2689 users_group = relationship('UserGroup')
2690 permission = relationship('Permission')
2690 permission = relationship('Permission')
2691 group = relationship('RepoGroup')
2691 group = relationship('RepoGroup')
2692
2692
2693 @classmethod
2693 @classmethod
2694 def create(cls, user_group, repository_group, permission):
2694 def create(cls, user_group, repository_group, permission):
2695 n = cls()
2695 n = cls()
2696 n.users_group = user_group
2696 n.users_group = user_group
2697 n.group = repository_group
2697 n.group = repository_group
2698 n.permission = permission
2698 n.permission = permission
2699 Session().add(n)
2699 Session().add(n)
2700 return n
2700 return n
2701
2701
2702 def __unicode__(self):
2702 def __unicode__(self):
2703 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2703 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2704
2704
2705
2705
2706 class Statistics(Base, BaseModel):
2706 class Statistics(Base, BaseModel):
2707 __tablename__ = 'statistics'
2707 __tablename__ = 'statistics'
2708 __table_args__ = (
2708 __table_args__ = (
2709 UniqueConstraint('repository_id'),
2709 UniqueConstraint('repository_id'),
2710 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2710 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2711 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2711 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2712 )
2712 )
2713 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2713 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2714 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2714 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2715 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2715 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2716 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2716 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2717 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2717 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2718 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2718 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2719
2719
2720 repository = relationship('Repository', single_parent=True)
2720 repository = relationship('Repository', single_parent=True)
2721
2721
2722
2722
2723 class UserFollowing(Base, BaseModel):
2723 class UserFollowing(Base, BaseModel):
2724 __tablename__ = 'user_followings'
2724 __tablename__ = 'user_followings'
2725 __table_args__ = (
2725 __table_args__ = (
2726 UniqueConstraint('user_id', 'follows_repository_id'),
2726 UniqueConstraint('user_id', 'follows_repository_id'),
2727 UniqueConstraint('user_id', 'follows_user_id'),
2727 UniqueConstraint('user_id', 'follows_user_id'),
2728 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2728 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2729 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2729 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2730 )
2730 )
2731
2731
2732 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2732 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2733 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2733 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2734 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2734 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2735 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2735 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2736 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2736 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2737
2737
2738 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2738 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2739
2739
2740 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2740 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2741 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2741 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2742
2742
2743 @classmethod
2743 @classmethod
2744 def get_repo_followers(cls, repo_id):
2744 def get_repo_followers(cls, repo_id):
2745 return cls.query().filter(cls.follows_repo_id == repo_id)
2745 return cls.query().filter(cls.follows_repo_id == repo_id)
2746
2746
2747
2747
2748 class CacheKey(Base, BaseModel):
2748 class CacheKey(Base, BaseModel):
2749 __tablename__ = 'cache_invalidation'
2749 __tablename__ = 'cache_invalidation'
2750 __table_args__ = (
2750 __table_args__ = (
2751 UniqueConstraint('cache_key'),
2751 UniqueConstraint('cache_key'),
2752 Index('key_idx', 'cache_key'),
2752 Index('key_idx', 'cache_key'),
2753 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2753 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2754 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2754 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2755 )
2755 )
2756 CACHE_TYPE_ATOM = 'ATOM'
2756 CACHE_TYPE_ATOM = 'ATOM'
2757 CACHE_TYPE_RSS = 'RSS'
2757 CACHE_TYPE_RSS = 'RSS'
2758 CACHE_TYPE_README = 'README'
2758 CACHE_TYPE_README = 'README'
2759
2759
2760 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2760 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2761 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2761 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2762 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2762 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2763 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2763 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2764
2764
2765 def __init__(self, cache_key, cache_args=''):
2765 def __init__(self, cache_key, cache_args=''):
2766 self.cache_key = cache_key
2766 self.cache_key = cache_key
2767 self.cache_args = cache_args
2767 self.cache_args = cache_args
2768 self.cache_active = False
2768 self.cache_active = False
2769
2769
2770 def __unicode__(self):
2770 def __unicode__(self):
2771 return u"<%s('%s:%s[%s]')>" % (
2771 return u"<%s('%s:%s[%s]')>" % (
2772 self.__class__.__name__,
2772 self.__class__.__name__,
2773 self.cache_id, self.cache_key, self.cache_active)
2773 self.cache_id, self.cache_key, self.cache_active)
2774
2774
2775 def _cache_key_partition(self):
2775 def _cache_key_partition(self):
2776 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2776 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2777 return prefix, repo_name, suffix
2777 return prefix, repo_name, suffix
2778
2778
2779 def get_prefix(self):
2779 def get_prefix(self):
2780 """
2780 """
2781 Try to extract prefix from existing cache key. The key could consist
2781 Try to extract prefix from existing cache key. The key could consist
2782 of prefix, repo_name, suffix
2782 of prefix, repo_name, suffix
2783 """
2783 """
2784 # this returns prefix, repo_name, suffix
2784 # this returns prefix, repo_name, suffix
2785 return self._cache_key_partition()[0]
2785 return self._cache_key_partition()[0]
2786
2786
2787 def get_suffix(self):
2787 def get_suffix(self):
2788 """
2788 """
2789 get suffix that might have been used in _get_cache_key to
2789 get suffix that might have been used in _get_cache_key to
2790 generate self.cache_key. Only used for informational purposes
2790 generate self.cache_key. Only used for informational purposes
2791 in repo_edit.html.
2791 in repo_edit.html.
2792 """
2792 """
2793 # prefix, repo_name, suffix
2793 # prefix, repo_name, suffix
2794 return self._cache_key_partition()[2]
2794 return self._cache_key_partition()[2]
2795
2795
2796 @classmethod
2796 @classmethod
2797 def delete_all_cache(cls):
2797 def delete_all_cache(cls):
2798 """
2798 """
2799 Delete all cache keys from database.
2799 Delete all cache keys from database.
2800 Should only be run when all instances are down and all entries
2800 Should only be run when all instances are down and all entries
2801 thus stale.
2801 thus stale.
2802 """
2802 """
2803 cls.query().delete()
2803 cls.query().delete()
2804 Session().commit()
2804 Session().commit()
2805
2805
2806 @classmethod
2806 @classmethod
2807 def get_cache_key(cls, repo_name, cache_type):
2807 def get_cache_key(cls, repo_name, cache_type):
2808 """
2808 """
2809
2809
2810 Generate a cache key for this process of RhodeCode instance.
2810 Generate a cache key for this process of RhodeCode instance.
2811 Prefix most likely will be process id or maybe explicitly set
2811 Prefix most likely will be process id or maybe explicitly set
2812 instance_id from .ini file.
2812 instance_id from .ini file.
2813 """
2813 """
2814 import rhodecode
2814 import rhodecode
2815 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2815 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2816
2816
2817 repo_as_unicode = safe_unicode(repo_name)
2817 repo_as_unicode = safe_unicode(repo_name)
2818 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2818 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2819 if cache_type else repo_as_unicode
2819 if cache_type else repo_as_unicode
2820
2820
2821 return u'{}{}'.format(prefix, key)
2821 return u'{}{}'.format(prefix, key)
2822
2822
2823 @classmethod
2823 @classmethod
2824 def set_invalidate(cls, repo_name, delete=False):
2824 def set_invalidate(cls, repo_name, delete=False):
2825 """
2825 """
2826 Mark all caches of a repo as invalid in the database.
2826 Mark all caches of a repo as invalid in the database.
2827 """
2827 """
2828
2828
2829 try:
2829 try:
2830 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2830 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2831 if delete:
2831 if delete:
2832 log.debug('cache objects deleted for repo %s',
2832 log.debug('cache objects deleted for repo %s',
2833 safe_str(repo_name))
2833 safe_str(repo_name))
2834 qry.delete()
2834 qry.delete()
2835 else:
2835 else:
2836 log.debug('cache objects marked as invalid for repo %s',
2836 log.debug('cache objects marked as invalid for repo %s',
2837 safe_str(repo_name))
2837 safe_str(repo_name))
2838 qry.update({"cache_active": False})
2838 qry.update({"cache_active": False})
2839
2839
2840 Session().commit()
2840 Session().commit()
2841 except Exception:
2841 except Exception:
2842 log.exception(
2842 log.exception(
2843 'Cache key invalidation failed for repository %s',
2843 'Cache key invalidation failed for repository %s',
2844 safe_str(repo_name))
2844 safe_str(repo_name))
2845 Session().rollback()
2845 Session().rollback()
2846
2846
2847 @classmethod
2847 @classmethod
2848 def get_active_cache(cls, cache_key):
2848 def get_active_cache(cls, cache_key):
2849 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2849 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2850 if inv_obj:
2850 if inv_obj:
2851 return inv_obj
2851 return inv_obj
2852 return None
2852 return None
2853
2853
2854 @classmethod
2854 @classmethod
2855 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2855 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2856 thread_scoped=False):
2856 thread_scoped=False):
2857 """
2857 """
2858 @cache_region('long_term')
2858 @cache_region('long_term')
2859 def _heavy_calculation(cache_key):
2859 def _heavy_calculation(cache_key):
2860 return 'result'
2860 return 'result'
2861
2861
2862 cache_context = CacheKey.repo_context_cache(
2862 cache_context = CacheKey.repo_context_cache(
2863 _heavy_calculation, repo_name, cache_type)
2863 _heavy_calculation, repo_name, cache_type)
2864
2864
2865 with cache_context as context:
2865 with cache_context as context:
2866 context.invalidate()
2866 context.invalidate()
2867 computed = context.compute()
2867 computed = context.compute()
2868
2868
2869 assert computed == 'result'
2869 assert computed == 'result'
2870 """
2870 """
2871 from rhodecode.lib import caches
2871 from rhodecode.lib import caches
2872 return caches.InvalidationContext(
2872 return caches.InvalidationContext(
2873 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2873 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2874
2874
2875
2875
2876 class ChangesetComment(Base, BaseModel):
2876 class ChangesetComment(Base, BaseModel):
2877 __tablename__ = 'changeset_comments'
2877 __tablename__ = 'changeset_comments'
2878 __table_args__ = (
2878 __table_args__ = (
2879 Index('cc_revision_idx', 'revision'),
2879 Index('cc_revision_idx', 'revision'),
2880 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2880 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2881 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2881 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2882 )
2882 )
2883
2883
2884 COMMENT_OUTDATED = u'comment_outdated'
2884 COMMENT_OUTDATED = u'comment_outdated'
2885
2885
2886 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2886 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2887 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2887 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2888 revision = Column('revision', String(40), nullable=True)
2888 revision = Column('revision', String(40), nullable=True)
2889 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2889 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2890 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2890 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2891 line_no = Column('line_no', Unicode(10), nullable=True)
2891 line_no = Column('line_no', Unicode(10), nullable=True)
2892 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2892 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2893 f_path = Column('f_path', Unicode(1000), nullable=True)
2893 f_path = Column('f_path', Unicode(1000), nullable=True)
2894 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2894 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2895 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2895 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2896 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2896 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2897 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2897 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2898 renderer = Column('renderer', Unicode(64), nullable=True)
2898 renderer = Column('renderer', Unicode(64), nullable=True)
2899 display_state = Column('display_state', Unicode(128), nullable=True)
2899 display_state = Column('display_state', Unicode(128), nullable=True)
2900
2900
2901 author = relationship('User', lazy='joined')
2901 author = relationship('User', lazy='joined')
2902 repo = relationship('Repository')
2902 repo = relationship('Repository')
2903 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2903 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2904 pull_request = relationship('PullRequest', lazy='joined')
2904 pull_request = relationship('PullRequest', lazy='joined')
2905 pull_request_version = relationship('PullRequestVersion')
2905 pull_request_version = relationship('PullRequestVersion')
2906
2906
2907 @classmethod
2907 @classmethod
2908 def get_users(cls, revision=None, pull_request_id=None):
2908 def get_users(cls, revision=None, pull_request_id=None):
2909 """
2909 """
2910 Returns user associated with this ChangesetComment. ie those
2910 Returns user associated with this ChangesetComment. ie those
2911 who actually commented
2911 who actually commented
2912
2912
2913 :param cls:
2913 :param cls:
2914 :param revision:
2914 :param revision:
2915 """
2915 """
2916 q = Session().query(User)\
2916 q = Session().query(User)\
2917 .join(ChangesetComment.author)
2917 .join(ChangesetComment.author)
2918 if revision:
2918 if revision:
2919 q = q.filter(cls.revision == revision)
2919 q = q.filter(cls.revision == revision)
2920 elif pull_request_id:
2920 elif pull_request_id:
2921 q = q.filter(cls.pull_request_id == pull_request_id)
2921 q = q.filter(cls.pull_request_id == pull_request_id)
2922 return q.all()
2922 return q.all()
2923
2923
2924 def render(self, mentions=False):
2924 def render(self, mentions=False):
2925 from rhodecode.lib import helpers as h
2925 from rhodecode.lib import helpers as h
2926 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2926 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2927
2927
2928 def __repr__(self):
2928 def __repr__(self):
2929 if self.comment_id:
2929 if self.comment_id:
2930 return '<DB:ChangesetComment #%s>' % self.comment_id
2930 return '<DB:ChangesetComment #%s>' % self.comment_id
2931 else:
2931 else:
2932 return '<DB:ChangesetComment at %#x>' % id(self)
2932 return '<DB:ChangesetComment at %#x>' % id(self)
2933
2933
2934
2934
2935 class ChangesetStatus(Base, BaseModel):
2935 class ChangesetStatus(Base, BaseModel):
2936 __tablename__ = 'changeset_statuses'
2936 __tablename__ = 'changeset_statuses'
2937 __table_args__ = (
2937 __table_args__ = (
2938 Index('cs_revision_idx', 'revision'),
2938 Index('cs_revision_idx', 'revision'),
2939 Index('cs_version_idx', 'version'),
2939 Index('cs_version_idx', 'version'),
2940 UniqueConstraint('repo_id', 'revision', 'version'),
2940 UniqueConstraint('repo_id', 'revision', 'version'),
2941 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2941 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2942 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2942 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2943 )
2943 )
2944 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2944 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2945 STATUS_APPROVED = 'approved'
2945 STATUS_APPROVED = 'approved'
2946 STATUS_REJECTED = 'rejected'
2946 STATUS_REJECTED = 'rejected'
2947 STATUS_UNDER_REVIEW = 'under_review'
2947 STATUS_UNDER_REVIEW = 'under_review'
2948
2948
2949 STATUSES = [
2949 STATUSES = [
2950 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2950 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2951 (STATUS_APPROVED, _("Approved")),
2951 (STATUS_APPROVED, _("Approved")),
2952 (STATUS_REJECTED, _("Rejected")),
2952 (STATUS_REJECTED, _("Rejected")),
2953 (STATUS_UNDER_REVIEW, _("Under Review")),
2953 (STATUS_UNDER_REVIEW, _("Under Review")),
2954 ]
2954 ]
2955
2955
2956 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2956 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2957 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2957 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2958 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2958 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2959 revision = Column('revision', String(40), nullable=False)
2959 revision = Column('revision', String(40), nullable=False)
2960 status = Column('status', String(128), nullable=False, default=DEFAULT)
2960 status = Column('status', String(128), nullable=False, default=DEFAULT)
2961 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2961 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2962 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2962 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2963 version = Column('version', Integer(), nullable=False, default=0)
2963 version = Column('version', Integer(), nullable=False, default=0)
2964 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2964 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2965
2965
2966 author = relationship('User', lazy='joined')
2966 author = relationship('User', lazy='joined')
2967 repo = relationship('Repository')
2967 repo = relationship('Repository')
2968 comment = relationship('ChangesetComment', lazy='joined')
2968 comment = relationship('ChangesetComment', lazy='joined')
2969 pull_request = relationship('PullRequest', lazy='joined')
2969 pull_request = relationship('PullRequest', lazy='joined')
2970
2970
2971 def __unicode__(self):
2971 def __unicode__(self):
2972 return u"<%s('%s[%s]:%s')>" % (
2972 return u"<%s('%s[%s]:%s')>" % (
2973 self.__class__.__name__,
2973 self.__class__.__name__,
2974 self.status, self.version, self.author
2974 self.status, self.version, self.author
2975 )
2975 )
2976
2976
2977 @classmethod
2977 @classmethod
2978 def get_status_lbl(cls, value):
2978 def get_status_lbl(cls, value):
2979 return dict(cls.STATUSES).get(value)
2979 return dict(cls.STATUSES).get(value)
2980
2980
2981 @property
2981 @property
2982 def status_lbl(self):
2982 def status_lbl(self):
2983 return ChangesetStatus.get_status_lbl(self.status)
2983 return ChangesetStatus.get_status_lbl(self.status)
2984
2984
2985
2985
2986 class _PullRequestBase(BaseModel):
2986 class _PullRequestBase(BaseModel):
2987 """
2987 """
2988 Common attributes of pull request and version entries.
2988 Common attributes of pull request and version entries.
2989 """
2989 """
2990
2990
2991 # .status values
2991 # .status values
2992 STATUS_NEW = u'new'
2992 STATUS_NEW = u'new'
2993 STATUS_OPEN = u'open'
2993 STATUS_OPEN = u'open'
2994 STATUS_CLOSED = u'closed'
2994 STATUS_CLOSED = u'closed'
2995
2995
2996 title = Column('title', Unicode(255), nullable=True)
2996 title = Column('title', Unicode(255), nullable=True)
2997 description = Column(
2997 description = Column(
2998 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
2998 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
2999 nullable=True)
2999 nullable=True)
3000 # new/open/closed status of pull request (not approve/reject/etc)
3000 # new/open/closed status of pull request (not approve/reject/etc)
3001 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3001 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3002 created_on = Column(
3002 created_on = Column(
3003 'created_on', DateTime(timezone=False), nullable=False,
3003 'created_on', DateTime(timezone=False), nullable=False,
3004 default=datetime.datetime.now)
3004 default=datetime.datetime.now)
3005 updated_on = Column(
3005 updated_on = Column(
3006 'updated_on', DateTime(timezone=False), nullable=False,
3006 'updated_on', DateTime(timezone=False), nullable=False,
3007 default=datetime.datetime.now)
3007 default=datetime.datetime.now)
3008
3008
3009 @declared_attr
3009 @declared_attr
3010 def user_id(cls):
3010 def user_id(cls):
3011 return Column(
3011 return Column(
3012 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3012 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3013 unique=None)
3013 unique=None)
3014
3014
3015 # 500 revisions max
3015 # 500 revisions max
3016 _revisions = Column(
3016 _revisions = Column(
3017 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3017 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3018
3018
3019 @declared_attr
3019 @declared_attr
3020 def source_repo_id(cls):
3020 def source_repo_id(cls):
3021 # TODO: dan: rename column to source_repo_id
3021 # TODO: dan: rename column to source_repo_id
3022 return Column(
3022 return Column(
3023 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3023 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3024 nullable=False)
3024 nullable=False)
3025
3025
3026 source_ref = Column('org_ref', Unicode(255), nullable=False)
3026 source_ref = Column('org_ref', Unicode(255), nullable=False)
3027
3027
3028 @declared_attr
3028 @declared_attr
3029 def target_repo_id(cls):
3029 def target_repo_id(cls):
3030 # TODO: dan: rename column to target_repo_id
3030 # TODO: dan: rename column to target_repo_id
3031 return Column(
3031 return Column(
3032 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3032 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3033 nullable=False)
3033 nullable=False)
3034
3034
3035 target_ref = Column('other_ref', Unicode(255), nullable=False)
3035 target_ref = Column('other_ref', Unicode(255), nullable=False)
3036
3036
3037 # TODO: dan: rename column to last_merge_source_rev
3037 # TODO: dan: rename column to last_merge_source_rev
3038 _last_merge_source_rev = Column(
3038 _last_merge_source_rev = Column(
3039 'last_merge_org_rev', String(40), nullable=True)
3039 'last_merge_org_rev', String(40), nullable=True)
3040 # TODO: dan: rename column to last_merge_target_rev
3040 # TODO: dan: rename column to last_merge_target_rev
3041 _last_merge_target_rev = Column(
3041 _last_merge_target_rev = Column(
3042 'last_merge_other_rev', String(40), nullable=True)
3042 'last_merge_other_rev', String(40), nullable=True)
3043 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3043 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3044 merge_rev = Column('merge_rev', String(40), nullable=True)
3044 merge_rev = Column('merge_rev', String(40), nullable=True)
3045
3045
3046 @hybrid_property
3046 @hybrid_property
3047 def revisions(self):
3047 def revisions(self):
3048 return self._revisions.split(':') if self._revisions else []
3048 return self._revisions.split(':') if self._revisions else []
3049
3049
3050 @revisions.setter
3050 @revisions.setter
3051 def revisions(self, val):
3051 def revisions(self, val):
3052 self._revisions = ':'.join(val)
3052 self._revisions = ':'.join(val)
3053
3053
3054 @declared_attr
3054 @declared_attr
3055 def author(cls):
3055 def author(cls):
3056 return relationship('User', lazy='joined')
3056 return relationship('User', lazy='joined')
3057
3057
3058 @declared_attr
3058 @declared_attr
3059 def source_repo(cls):
3059 def source_repo(cls):
3060 return relationship(
3060 return relationship(
3061 'Repository',
3061 'Repository',
3062 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3062 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3063
3063
3064 @property
3064 @property
3065 def source_ref_parts(self):
3065 def source_ref_parts(self):
3066 refs = self.source_ref.split(':')
3066 refs = self.source_ref.split(':')
3067 return Reference(refs[0], refs[1], refs[2])
3067 return Reference(refs[0], refs[1], refs[2])
3068
3068
3069 @declared_attr
3069 @declared_attr
3070 def target_repo(cls):
3070 def target_repo(cls):
3071 return relationship(
3071 return relationship(
3072 'Repository',
3072 'Repository',
3073 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3073 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3074
3074
3075 @property
3075 @property
3076 def target_ref_parts(self):
3076 def target_ref_parts(self):
3077 refs = self.target_ref.split(':')
3077 refs = self.target_ref.split(':')
3078 return Reference(refs[0], refs[1], refs[2])
3078 return Reference(refs[0], refs[1], refs[2])
3079
3079
3080
3080
3081 class PullRequest(Base, _PullRequestBase):
3081 class PullRequest(Base, _PullRequestBase):
3082 __tablename__ = 'pull_requests'
3082 __tablename__ = 'pull_requests'
3083 __table_args__ = (
3083 __table_args__ = (
3084 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3084 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3085 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3085 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3086 )
3086 )
3087
3087
3088 pull_request_id = Column(
3088 pull_request_id = Column(
3089 'pull_request_id', Integer(), nullable=False, primary_key=True)
3089 'pull_request_id', Integer(), nullable=False, primary_key=True)
3090
3090
3091 def __repr__(self):
3091 def __repr__(self):
3092 if self.pull_request_id:
3092 if self.pull_request_id:
3093 return '<DB:PullRequest #%s>' % self.pull_request_id
3093 return '<DB:PullRequest #%s>' % self.pull_request_id
3094 else:
3094 else:
3095 return '<DB:PullRequest at %#x>' % id(self)
3095 return '<DB:PullRequest at %#x>' % id(self)
3096
3096
3097 reviewers = relationship('PullRequestReviewers',
3097 reviewers = relationship('PullRequestReviewers',
3098 cascade="all, delete, delete-orphan")
3098 cascade="all, delete, delete-orphan")
3099 statuses = relationship('ChangesetStatus')
3099 statuses = relationship('ChangesetStatus')
3100 comments = relationship('ChangesetComment',
3100 comments = relationship('ChangesetComment',
3101 cascade="all, delete, delete-orphan")
3101 cascade="all, delete, delete-orphan")
3102 versions = relationship('PullRequestVersion',
3102 versions = relationship('PullRequestVersion',
3103 cascade="all, delete, delete-orphan")
3103 cascade="all, delete, delete-orphan")
3104
3104
3105 def is_closed(self):
3105 def is_closed(self):
3106 return self.status == self.STATUS_CLOSED
3106 return self.status == self.STATUS_CLOSED
3107
3107
3108 def get_api_data(self):
3108 def get_api_data(self):
3109 from rhodecode.model.pull_request import PullRequestModel
3109 from rhodecode.model.pull_request import PullRequestModel
3110 pull_request = self
3110 pull_request = self
3111 merge_status = PullRequestModel().merge_status(pull_request)
3111 merge_status = PullRequestModel().merge_status(pull_request)
3112 data = {
3112 data = {
3113 'pull_request_id': pull_request.pull_request_id,
3113 'pull_request_id': pull_request.pull_request_id,
3114 'url': url('pullrequest_show', repo_name=self.target_repo.repo_name,
3114 'url': url('pullrequest_show', repo_name=self.target_repo.repo_name,
3115 pull_request_id=self.pull_request_id,
3115 pull_request_id=self.pull_request_id,
3116 qualified=True),
3116 qualified=True),
3117 'title': pull_request.title,
3117 'title': pull_request.title,
3118 'description': pull_request.description,
3118 'description': pull_request.description,
3119 'status': pull_request.status,
3119 'status': pull_request.status,
3120 'created_on': pull_request.created_on,
3120 'created_on': pull_request.created_on,
3121 'updated_on': pull_request.updated_on,
3121 'updated_on': pull_request.updated_on,
3122 'commit_ids': pull_request.revisions,
3122 'commit_ids': pull_request.revisions,
3123 'review_status': pull_request.calculated_review_status(),
3123 'review_status': pull_request.calculated_review_status(),
3124 'mergeable': {
3124 'mergeable': {
3125 'status': merge_status[0],
3125 'status': merge_status[0],
3126 'message': unicode(merge_status[1]),
3126 'message': unicode(merge_status[1]),
3127 },
3127 },
3128 'source': {
3128 'source': {
3129 'clone_url': pull_request.source_repo.clone_url(),
3129 'clone_url': pull_request.source_repo.clone_url(),
3130 'repository': pull_request.source_repo.repo_name,
3130 'repository': pull_request.source_repo.repo_name,
3131 'reference': {
3131 'reference': {
3132 'name': pull_request.source_ref_parts.name,
3132 'name': pull_request.source_ref_parts.name,
3133 'type': pull_request.source_ref_parts.type,
3133 'type': pull_request.source_ref_parts.type,
3134 'commit_id': pull_request.source_ref_parts.commit_id,
3134 'commit_id': pull_request.source_ref_parts.commit_id,
3135 },
3135 },
3136 },
3136 },
3137 'target': {
3137 'target': {
3138 'clone_url': pull_request.target_repo.clone_url(),
3138 'clone_url': pull_request.target_repo.clone_url(),
3139 'repository': pull_request.target_repo.repo_name,
3139 'repository': pull_request.target_repo.repo_name,
3140 'reference': {
3140 'reference': {
3141 'name': pull_request.target_ref_parts.name,
3141 'name': pull_request.target_ref_parts.name,
3142 'type': pull_request.target_ref_parts.type,
3142 'type': pull_request.target_ref_parts.type,
3143 'commit_id': pull_request.target_ref_parts.commit_id,
3143 'commit_id': pull_request.target_ref_parts.commit_id,
3144 },
3144 },
3145 },
3145 },
3146 'author': pull_request.author.get_api_data(include_secrets=False,
3146 'author': pull_request.author.get_api_data(include_secrets=False,
3147 details='basic'),
3147 details='basic'),
3148 'reviewers': [
3148 'reviewers': [
3149 {
3149 {
3150 'user': reviewer.get_api_data(include_secrets=False,
3150 'user': reviewer.get_api_data(include_secrets=False,
3151 details='basic'),
3151 details='basic'),
3152 'reasons': reasons,
3152 'review_status': st[0][1].status if st else 'not_reviewed',
3153 'review_status': st[0][1].status if st else 'not_reviewed',
3153 }
3154 }
3154 for reviewer, st in pull_request.reviewers_statuses()
3155 for reviewer, reasons, st in pull_request.reviewers_statuses()
3155 ]
3156 ]
3156 }
3157 }
3157
3158
3158 return data
3159 return data
3159
3160
3160 def __json__(self):
3161 def __json__(self):
3161 return {
3162 return {
3162 'revisions': self.revisions,
3163 'revisions': self.revisions,
3163 }
3164 }
3164
3165
3165 def calculated_review_status(self):
3166 def calculated_review_status(self):
3166 # TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html
3167 # TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html
3167 # because it's tricky on how to use ChangesetStatusModel from there
3168 # because it's tricky on how to use ChangesetStatusModel from there
3168 warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning)
3169 warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning)
3169 from rhodecode.model.changeset_status import ChangesetStatusModel
3170 from rhodecode.model.changeset_status import ChangesetStatusModel
3170 return ChangesetStatusModel().calculated_review_status(self)
3171 return ChangesetStatusModel().calculated_review_status(self)
3171
3172
3172 def reviewers_statuses(self):
3173 def reviewers_statuses(self):
3173 warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning)
3174 warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning)
3174 from rhodecode.model.changeset_status import ChangesetStatusModel
3175 from rhodecode.model.changeset_status import ChangesetStatusModel
3175 return ChangesetStatusModel().reviewers_statuses(self)
3176 return ChangesetStatusModel().reviewers_statuses(self)
3176
3177
3177
3178
3178 class PullRequestVersion(Base, _PullRequestBase):
3179 class PullRequestVersion(Base, _PullRequestBase):
3179 __tablename__ = 'pull_request_versions'
3180 __tablename__ = 'pull_request_versions'
3180 __table_args__ = (
3181 __table_args__ = (
3181 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3182 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3182 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3183 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3183 )
3184 )
3184
3185
3185 pull_request_version_id = Column(
3186 pull_request_version_id = Column(
3186 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3187 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3187 pull_request_id = Column(
3188 pull_request_id = Column(
3188 'pull_request_id', Integer(),
3189 'pull_request_id', Integer(),
3189 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3190 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3190 pull_request = relationship('PullRequest')
3191 pull_request = relationship('PullRequest')
3191
3192
3192 def __repr__(self):
3193 def __repr__(self):
3193 if self.pull_request_version_id:
3194 if self.pull_request_version_id:
3194 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3195 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3195 else:
3196 else:
3196 return '<DB:PullRequestVersion at %#x>' % id(self)
3197 return '<DB:PullRequestVersion at %#x>' % id(self)
3197
3198
3198
3199
3199 class PullRequestReviewers(Base, BaseModel):
3200 class PullRequestReviewers(Base, BaseModel):
3200 __tablename__ = 'pull_request_reviewers'
3201 __tablename__ = 'pull_request_reviewers'
3201 __table_args__ = (
3202 __table_args__ = (
3202 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3203 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3203 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3204 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3204 )
3205 )
3205
3206
3206 def __init__(self, user=None, pull_request=None):
3207 def __init__(self, user=None, pull_request=None, reasons=None):
3207 self.user = user
3208 self.user = user
3208 self.pull_request = pull_request
3209 self.pull_request = pull_request
3210 self.reasons = reasons or []
3211
3212 @hybrid_property
3213 def reasons(self):
3214 if not self._reasons:
3215 return []
3216 return self._reasons
3217
3218 @reasons.setter
3219 def reasons(self, val):
3220 val = val or []
3221 if any(not isinstance(x, basestring) for x in val):
3222 raise Exception('invalid reasons type, must be list of strings')
3223 self._reasons = val
3209
3224
3210 pull_requests_reviewers_id = Column(
3225 pull_requests_reviewers_id = Column(
3211 'pull_requests_reviewers_id', Integer(), nullable=False,
3226 'pull_requests_reviewers_id', Integer(), nullable=False,
3212 primary_key=True)
3227 primary_key=True)
3213 pull_request_id = Column(
3228 pull_request_id = Column(
3214 "pull_request_id", Integer(),
3229 "pull_request_id", Integer(),
3215 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3230 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3216 user_id = Column(
3231 user_id = Column(
3217 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3232 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3218 reason = Column('reason',
3233 _reasons = Column(
3219 UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
3234 'reason', MutationList.as_mutable(
3235 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3220
3236
3221 user = relationship('User')
3237 user = relationship('User')
3222 pull_request = relationship('PullRequest')
3238 pull_request = relationship('PullRequest')
3223
3239
3224
3240
3225 class Notification(Base, BaseModel):
3241 class Notification(Base, BaseModel):
3226 __tablename__ = 'notifications'
3242 __tablename__ = 'notifications'
3227 __table_args__ = (
3243 __table_args__ = (
3228 Index('notification_type_idx', 'type'),
3244 Index('notification_type_idx', 'type'),
3229 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3245 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3230 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3246 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3231 )
3247 )
3232
3248
3233 TYPE_CHANGESET_COMMENT = u'cs_comment'
3249 TYPE_CHANGESET_COMMENT = u'cs_comment'
3234 TYPE_MESSAGE = u'message'
3250 TYPE_MESSAGE = u'message'
3235 TYPE_MENTION = u'mention'
3251 TYPE_MENTION = u'mention'
3236 TYPE_REGISTRATION = u'registration'
3252 TYPE_REGISTRATION = u'registration'
3237 TYPE_PULL_REQUEST = u'pull_request'
3253 TYPE_PULL_REQUEST = u'pull_request'
3238 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3254 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3239
3255
3240 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3256 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3241 subject = Column('subject', Unicode(512), nullable=True)
3257 subject = Column('subject', Unicode(512), nullable=True)
3242 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3258 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3243 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3259 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3244 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3260 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3245 type_ = Column('type', Unicode(255))
3261 type_ = Column('type', Unicode(255))
3246
3262
3247 created_by_user = relationship('User')
3263 created_by_user = relationship('User')
3248 notifications_to_users = relationship('UserNotification', lazy='joined',
3264 notifications_to_users = relationship('UserNotification', lazy='joined',
3249 cascade="all, delete, delete-orphan")
3265 cascade="all, delete, delete-orphan")
3250
3266
3251 @property
3267 @property
3252 def recipients(self):
3268 def recipients(self):
3253 return [x.user for x in UserNotification.query()\
3269 return [x.user for x in UserNotification.query()\
3254 .filter(UserNotification.notification == self)\
3270 .filter(UserNotification.notification == self)\
3255 .order_by(UserNotification.user_id.asc()).all()]
3271 .order_by(UserNotification.user_id.asc()).all()]
3256
3272
3257 @classmethod
3273 @classmethod
3258 def create(cls, created_by, subject, body, recipients, type_=None):
3274 def create(cls, created_by, subject, body, recipients, type_=None):
3259 if type_ is None:
3275 if type_ is None:
3260 type_ = Notification.TYPE_MESSAGE
3276 type_ = Notification.TYPE_MESSAGE
3261
3277
3262 notification = cls()
3278 notification = cls()
3263 notification.created_by_user = created_by
3279 notification.created_by_user = created_by
3264 notification.subject = subject
3280 notification.subject = subject
3265 notification.body = body
3281 notification.body = body
3266 notification.type_ = type_
3282 notification.type_ = type_
3267 notification.created_on = datetime.datetime.now()
3283 notification.created_on = datetime.datetime.now()
3268
3284
3269 for u in recipients:
3285 for u in recipients:
3270 assoc = UserNotification()
3286 assoc = UserNotification()
3271 assoc.notification = notification
3287 assoc.notification = notification
3272
3288
3273 # if created_by is inside recipients mark his notification
3289 # if created_by is inside recipients mark his notification
3274 # as read
3290 # as read
3275 if u.user_id == created_by.user_id:
3291 if u.user_id == created_by.user_id:
3276 assoc.read = True
3292 assoc.read = True
3277
3293
3278 u.notifications.append(assoc)
3294 u.notifications.append(assoc)
3279 Session().add(notification)
3295 Session().add(notification)
3280
3296
3281 return notification
3297 return notification
3282
3298
3283 @property
3299 @property
3284 def description(self):
3300 def description(self):
3285 from rhodecode.model.notification import NotificationModel
3301 from rhodecode.model.notification import NotificationModel
3286 return NotificationModel().make_description(self)
3302 return NotificationModel().make_description(self)
3287
3303
3288
3304
3289 class UserNotification(Base, BaseModel):
3305 class UserNotification(Base, BaseModel):
3290 __tablename__ = 'user_to_notification'
3306 __tablename__ = 'user_to_notification'
3291 __table_args__ = (
3307 __table_args__ = (
3292 UniqueConstraint('user_id', 'notification_id'),
3308 UniqueConstraint('user_id', 'notification_id'),
3293 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3309 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3294 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3310 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3295 )
3311 )
3296 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3312 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3297 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3313 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3298 read = Column('read', Boolean, default=False)
3314 read = Column('read', Boolean, default=False)
3299 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3315 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3300
3316
3301 user = relationship('User', lazy="joined")
3317 user = relationship('User', lazy="joined")
3302 notification = relationship('Notification', lazy="joined",
3318 notification = relationship('Notification', lazy="joined",
3303 order_by=lambda: Notification.created_on.desc(),)
3319 order_by=lambda: Notification.created_on.desc(),)
3304
3320
3305 def mark_as_read(self):
3321 def mark_as_read(self):
3306 self.read = True
3322 self.read = True
3307 Session().add(self)
3323 Session().add(self)
3308
3324
3309
3325
3310 class Gist(Base, BaseModel):
3326 class Gist(Base, BaseModel):
3311 __tablename__ = 'gists'
3327 __tablename__ = 'gists'
3312 __table_args__ = (
3328 __table_args__ = (
3313 Index('g_gist_access_id_idx', 'gist_access_id'),
3329 Index('g_gist_access_id_idx', 'gist_access_id'),
3314 Index('g_created_on_idx', 'created_on'),
3330 Index('g_created_on_idx', 'created_on'),
3315 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3331 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3316 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3332 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3317 )
3333 )
3318 GIST_PUBLIC = u'public'
3334 GIST_PUBLIC = u'public'
3319 GIST_PRIVATE = u'private'
3335 GIST_PRIVATE = u'private'
3320 DEFAULT_FILENAME = u'gistfile1.txt'
3336 DEFAULT_FILENAME = u'gistfile1.txt'
3321
3337
3322 ACL_LEVEL_PUBLIC = u'acl_public'
3338 ACL_LEVEL_PUBLIC = u'acl_public'
3323 ACL_LEVEL_PRIVATE = u'acl_private'
3339 ACL_LEVEL_PRIVATE = u'acl_private'
3324
3340
3325 gist_id = Column('gist_id', Integer(), primary_key=True)
3341 gist_id = Column('gist_id', Integer(), primary_key=True)
3326 gist_access_id = Column('gist_access_id', Unicode(250))
3342 gist_access_id = Column('gist_access_id', Unicode(250))
3327 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3343 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3328 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3344 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3329 gist_expires = Column('gist_expires', Float(53), nullable=False)
3345 gist_expires = Column('gist_expires', Float(53), nullable=False)
3330 gist_type = Column('gist_type', Unicode(128), nullable=False)
3346 gist_type = Column('gist_type', Unicode(128), nullable=False)
3331 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3347 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3332 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3348 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3333 acl_level = Column('acl_level', Unicode(128), nullable=True)
3349 acl_level = Column('acl_level', Unicode(128), nullable=True)
3334
3350
3335 owner = relationship('User')
3351 owner = relationship('User')
3336
3352
3337 def __repr__(self):
3353 def __repr__(self):
3338 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3354 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3339
3355
3340 @classmethod
3356 @classmethod
3341 def get_or_404(cls, id_):
3357 def get_or_404(cls, id_):
3342 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3358 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3343 if not res:
3359 if not res:
3344 raise HTTPNotFound
3360 raise HTTPNotFound
3345 return res
3361 return res
3346
3362
3347 @classmethod
3363 @classmethod
3348 def get_by_access_id(cls, gist_access_id):
3364 def get_by_access_id(cls, gist_access_id):
3349 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3365 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3350
3366
3351 def gist_url(self):
3367 def gist_url(self):
3352 import rhodecode
3368 import rhodecode
3353 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3369 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3354 if alias_url:
3370 if alias_url:
3355 return alias_url.replace('{gistid}', self.gist_access_id)
3371 return alias_url.replace('{gistid}', self.gist_access_id)
3356
3372
3357 return url('gist', gist_id=self.gist_access_id, qualified=True)
3373 return url('gist', gist_id=self.gist_access_id, qualified=True)
3358
3374
3359 @classmethod
3375 @classmethod
3360 def base_path(cls):
3376 def base_path(cls):
3361 """
3377 """
3362 Returns base path when all gists are stored
3378 Returns base path when all gists are stored
3363
3379
3364 :param cls:
3380 :param cls:
3365 """
3381 """
3366 from rhodecode.model.gist import GIST_STORE_LOC
3382 from rhodecode.model.gist import GIST_STORE_LOC
3367 q = Session().query(RhodeCodeUi)\
3383 q = Session().query(RhodeCodeUi)\
3368 .filter(RhodeCodeUi.ui_key == URL_SEP)
3384 .filter(RhodeCodeUi.ui_key == URL_SEP)
3369 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3385 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3370 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3386 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3371
3387
3372 def get_api_data(self):
3388 def get_api_data(self):
3373 """
3389 """
3374 Common function for generating gist related data for API
3390 Common function for generating gist related data for API
3375 """
3391 """
3376 gist = self
3392 gist = self
3377 data = {
3393 data = {
3378 'gist_id': gist.gist_id,
3394 'gist_id': gist.gist_id,
3379 'type': gist.gist_type,
3395 'type': gist.gist_type,
3380 'access_id': gist.gist_access_id,
3396 'access_id': gist.gist_access_id,
3381 'description': gist.gist_description,
3397 'description': gist.gist_description,
3382 'url': gist.gist_url(),
3398 'url': gist.gist_url(),
3383 'expires': gist.gist_expires,
3399 'expires': gist.gist_expires,
3384 'created_on': gist.created_on,
3400 'created_on': gist.created_on,
3385 'modified_at': gist.modified_at,
3401 'modified_at': gist.modified_at,
3386 'content': None,
3402 'content': None,
3387 'acl_level': gist.acl_level,
3403 'acl_level': gist.acl_level,
3388 }
3404 }
3389 return data
3405 return data
3390
3406
3391 def __json__(self):
3407 def __json__(self):
3392 data = dict(
3408 data = dict(
3393 )
3409 )
3394 data.update(self.get_api_data())
3410 data.update(self.get_api_data())
3395 return data
3411 return data
3396 # SCM functions
3412 # SCM functions
3397
3413
3398 def scm_instance(self, **kwargs):
3414 def scm_instance(self, **kwargs):
3399 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3415 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3400 return get_vcs_instance(
3416 return get_vcs_instance(
3401 repo_path=safe_str(full_repo_path), create=False)
3417 repo_path=safe_str(full_repo_path), create=False)
3402
3418
3403
3419
3404 class DbMigrateVersion(Base, BaseModel):
3420 class DbMigrateVersion(Base, BaseModel):
3405 __tablename__ = 'db_migrate_version'
3421 __tablename__ = 'db_migrate_version'
3406 __table_args__ = (
3422 __table_args__ = (
3407 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3423 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3408 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3424 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3409 )
3425 )
3410 repository_id = Column('repository_id', String(250), primary_key=True)
3426 repository_id = Column('repository_id', String(250), primary_key=True)
3411 repository_path = Column('repository_path', Text)
3427 repository_path = Column('repository_path', Text)
3412 version = Column('version', Integer)
3428 version = Column('version', Integer)
3413
3429
3414
3430
3415 class ExternalIdentity(Base, BaseModel):
3431 class ExternalIdentity(Base, BaseModel):
3416 __tablename__ = 'external_identities'
3432 __tablename__ = 'external_identities'
3417 __table_args__ = (
3433 __table_args__ = (
3418 Index('local_user_id_idx', 'local_user_id'),
3434 Index('local_user_id_idx', 'local_user_id'),
3419 Index('external_id_idx', 'external_id'),
3435 Index('external_id_idx', 'external_id'),
3420 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3436 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3421 'mysql_charset': 'utf8'})
3437 'mysql_charset': 'utf8'})
3422
3438
3423 external_id = Column('external_id', Unicode(255), default=u'',
3439 external_id = Column('external_id', Unicode(255), default=u'',
3424 primary_key=True)
3440 primary_key=True)
3425 external_username = Column('external_username', Unicode(1024), default=u'')
3441 external_username = Column('external_username', Unicode(1024), default=u'')
3426 local_user_id = Column('local_user_id', Integer(),
3442 local_user_id = Column('local_user_id', Integer(),
3427 ForeignKey('users.user_id'), primary_key=True)
3443 ForeignKey('users.user_id'), primary_key=True)
3428 provider_name = Column('provider_name', Unicode(255), default=u'',
3444 provider_name = Column('provider_name', Unicode(255), default=u'',
3429 primary_key=True)
3445 primary_key=True)
3430 access_token = Column('access_token', String(1024), default=u'')
3446 access_token = Column('access_token', String(1024), default=u'')
3431 alt_token = Column('alt_token', String(1024), default=u'')
3447 alt_token = Column('alt_token', String(1024), default=u'')
3432 token_secret = Column('token_secret', String(1024), default=u'')
3448 token_secret = Column('token_secret', String(1024), default=u'')
3433
3449
3434 @classmethod
3450 @classmethod
3435 def by_external_id_and_provider(cls, external_id, provider_name,
3451 def by_external_id_and_provider(cls, external_id, provider_name,
3436 local_user_id=None):
3452 local_user_id=None):
3437 """
3453 """
3438 Returns ExternalIdentity instance based on search params
3454 Returns ExternalIdentity instance based on search params
3439
3455
3440 :param external_id:
3456 :param external_id:
3441 :param provider_name:
3457 :param provider_name:
3442 :return: ExternalIdentity
3458 :return: ExternalIdentity
3443 """
3459 """
3444 query = cls.query()
3460 query = cls.query()
3445 query = query.filter(cls.external_id == external_id)
3461 query = query.filter(cls.external_id == external_id)
3446 query = query.filter(cls.provider_name == provider_name)
3462 query = query.filter(cls.provider_name == provider_name)
3447 if local_user_id:
3463 if local_user_id:
3448 query = query.filter(cls.local_user_id == local_user_id)
3464 query = query.filter(cls.local_user_id == local_user_id)
3449 return query.first()
3465 return query.first()
3450
3466
3451 @classmethod
3467 @classmethod
3452 def user_by_external_id_and_provider(cls, external_id, provider_name):
3468 def user_by_external_id_and_provider(cls, external_id, provider_name):
3453 """
3469 """
3454 Returns User instance based on search params
3470 Returns User instance based on search params
3455
3471
3456 :param external_id:
3472 :param external_id:
3457 :param provider_name:
3473 :param provider_name:
3458 :return: User
3474 :return: User
3459 """
3475 """
3460 query = User.query()
3476 query = User.query()
3461 query = query.filter(cls.external_id == external_id)
3477 query = query.filter(cls.external_id == external_id)
3462 query = query.filter(cls.provider_name == provider_name)
3478 query = query.filter(cls.provider_name == provider_name)
3463 query = query.filter(User.user_id == cls.local_user_id)
3479 query = query.filter(User.user_id == cls.local_user_id)
3464 return query.first()
3480 return query.first()
3465
3481
3466 @classmethod
3482 @classmethod
3467 def by_local_user_id(cls, local_user_id):
3483 def by_local_user_id(cls, local_user_id):
3468 """
3484 """
3469 Returns all tokens for user
3485 Returns all tokens for user
3470
3486
3471 :param local_user_id:
3487 :param local_user_id:
3472 :return: ExternalIdentity
3488 :return: ExternalIdentity
3473 """
3489 """
3474 query = cls.query()
3490 query = cls.query()
3475 query = query.filter(cls.local_user_id == local_user_id)
3491 query = query.filter(cls.local_user_id == local_user_id)
3476 return query
3492 return query
3477
3493
3478
3494
3479 class Integration(Base, BaseModel):
3495 class Integration(Base, BaseModel):
3480 __tablename__ = 'integrations'
3496 __tablename__ = 'integrations'
3481 __table_args__ = (
3497 __table_args__ = (
3482 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3498 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3483 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3499 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3484 )
3500 )
3485
3501
3486 integration_id = Column('integration_id', Integer(), primary_key=True)
3502 integration_id = Column('integration_id', Integer(), primary_key=True)
3487 integration_type = Column('integration_type', String(255))
3503 integration_type = Column('integration_type', String(255))
3488 enabled = Column('enabled', Boolean(), nullable=False)
3504 enabled = Column('enabled', Boolean(), nullable=False)
3489 name = Column('name', String(255), nullable=False)
3505 name = Column('name', String(255), nullable=False)
3490 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3506 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3491 default=False)
3507 default=False)
3492
3508
3493 settings = Column(
3509 settings = Column(
3494 'settings_json', MutationObj.as_mutable(
3510 'settings_json', MutationObj.as_mutable(
3495 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3511 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3496 repo_id = Column(
3512 repo_id = Column(
3497 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3513 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3498 nullable=True, unique=None, default=None)
3514 nullable=True, unique=None, default=None)
3499 repo = relationship('Repository', lazy='joined')
3515 repo = relationship('Repository', lazy='joined')
3500
3516
3501 repo_group_id = Column(
3517 repo_group_id = Column(
3502 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3518 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3503 nullable=True, unique=None, default=None)
3519 nullable=True, unique=None, default=None)
3504 repo_group = relationship('RepoGroup', lazy='joined')
3520 repo_group = relationship('RepoGroup', lazy='joined')
3505
3521
3506 @property
3522 @property
3507 def scope(self):
3523 def scope(self):
3508 if self.repo:
3524 if self.repo:
3509 return repr(self.repo)
3525 return repr(self.repo)
3510 if self.repo_group:
3526 if self.repo_group:
3511 if self.child_repos_only:
3527 if self.child_repos_only:
3512 return repr(self.repo_group) + ' (child repos only)'
3528 return repr(self.repo_group) + ' (child repos only)'
3513 else:
3529 else:
3514 return repr(self.repo_group) + ' (recursive)'
3530 return repr(self.repo_group) + ' (recursive)'
3515 if self.child_repos_only:
3531 if self.child_repos_only:
3516 return 'root_repos'
3532 return 'root_repos'
3517 return 'global'
3533 return 'global'
3518
3534
3519 def __repr__(self):
3535 def __repr__(self):
3520 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3536 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3521
3537
3522
3538
3523 class RepoReviewRuleUser(Base, BaseModel):
3539 class RepoReviewRuleUser(Base, BaseModel):
3524 __tablename__ = 'repo_review_rules_users'
3540 __tablename__ = 'repo_review_rules_users'
3525 __table_args__ = (
3541 __table_args__ = (
3526 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3542 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3527 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3543 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3528 )
3544 )
3529 repo_review_rule_user_id = Column(
3545 repo_review_rule_user_id = Column(
3530 'repo_review_rule_user_id', Integer(), primary_key=True)
3546 'repo_review_rule_user_id', Integer(), primary_key=True)
3531 repo_review_rule_id = Column("repo_review_rule_id",
3547 repo_review_rule_id = Column("repo_review_rule_id",
3532 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3548 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3533 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3549 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3534 nullable=False)
3550 nullable=False)
3535 user = relationship('User')
3551 user = relationship('User')
3536
3552
3537
3553
3538 class RepoReviewRuleUserGroup(Base, BaseModel):
3554 class RepoReviewRuleUserGroup(Base, BaseModel):
3539 __tablename__ = 'repo_review_rules_users_groups'
3555 __tablename__ = 'repo_review_rules_users_groups'
3540 __table_args__ = (
3556 __table_args__ = (
3541 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3557 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3542 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3558 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3543 )
3559 )
3544 repo_review_rule_users_group_id = Column(
3560 repo_review_rule_users_group_id = Column(
3545 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3561 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3546 repo_review_rule_id = Column("repo_review_rule_id",
3562 repo_review_rule_id = Column("repo_review_rule_id",
3547 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3563 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3548 users_group_id = Column("users_group_id", Integer(),
3564 users_group_id = Column("users_group_id", Integer(),
3549 ForeignKey('users_groups.users_group_id'), nullable=False)
3565 ForeignKey('users_groups.users_group_id'), nullable=False)
3550 users_group = relationship('UserGroup')
3566 users_group = relationship('UserGroup')
3551
3567
3552
3568
3553 class RepoReviewRule(Base, BaseModel):
3569 class RepoReviewRule(Base, BaseModel):
3554 __tablename__ = 'repo_review_rules'
3570 __tablename__ = 'repo_review_rules'
3555 __table_args__ = (
3571 __table_args__ = (
3556 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3572 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3557 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3573 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3558 )
3574 )
3559
3575
3560 repo_review_rule_id = Column(
3576 repo_review_rule_id = Column(
3561 'repo_review_rule_id', Integer(), primary_key=True)
3577 'repo_review_rule_id', Integer(), primary_key=True)
3562 repo_id = Column(
3578 repo_id = Column(
3563 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3579 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3564 repo = relationship('Repository', backref='review_rules')
3580 repo = relationship('Repository', backref='review_rules')
3565
3581
3566 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3582 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3567 default=u'*') # glob
3583 default=u'*') # glob
3568 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3584 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3569 default=u'*') # glob
3585 default=u'*') # glob
3570
3586
3571 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3587 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3572 nullable=False, default=False)
3588 nullable=False, default=False)
3573 rule_users = relationship('RepoReviewRuleUser')
3589 rule_users = relationship('RepoReviewRuleUser')
3574 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3590 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3575
3591
3576 @hybrid_property
3592 @hybrid_property
3577 def branch_pattern(self):
3593 def branch_pattern(self):
3578 return self._branch_pattern or '*'
3594 return self._branch_pattern or '*'
3579
3595
3580 def _validate_glob(self, value):
3596 def _validate_glob(self, value):
3581 re.compile('^' + glob2re(value) + '$')
3597 re.compile('^' + glob2re(value) + '$')
3582
3598
3583 @branch_pattern.setter
3599 @branch_pattern.setter
3584 def branch_pattern(self, value):
3600 def branch_pattern(self, value):
3585 self._validate_glob(value)
3601 self._validate_glob(value)
3586 self._branch_pattern = value or '*'
3602 self._branch_pattern = value or '*'
3587
3603
3588 @hybrid_property
3604 @hybrid_property
3589 def file_pattern(self):
3605 def file_pattern(self):
3590 return self._file_pattern or '*'
3606 return self._file_pattern or '*'
3591
3607
3592 @file_pattern.setter
3608 @file_pattern.setter
3593 def file_pattern(self, value):
3609 def file_pattern(self, value):
3594 self._validate_glob(value)
3610 self._validate_glob(value)
3595 self._file_pattern = value or '*'
3611 self._file_pattern = value or '*'
3596
3612
3597 def matches(self, branch, files_changed):
3613 def matches(self, branch, files_changed):
3598 """
3614 """
3599 Check if this review rule matches a branch/files in a pull request
3615 Check if this review rule matches a branch/files in a pull request
3600
3616
3601 :param branch: branch name for the commit
3617 :param branch: branch name for the commit
3602 :param files_changed: list of file paths changed in the pull request
3618 :param files_changed: list of file paths changed in the pull request
3603 """
3619 """
3604
3620
3605 branch = branch or ''
3621 branch = branch or ''
3606 files_changed = files_changed or []
3622 files_changed = files_changed or []
3607
3623
3608 branch_matches = True
3624 branch_matches = True
3609 if branch:
3625 if branch:
3610 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3626 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3611 branch_matches = bool(branch_regex.search(branch))
3627 branch_matches = bool(branch_regex.search(branch))
3612
3628
3613 files_matches = True
3629 files_matches = True
3614 if self.file_pattern != '*':
3630 if self.file_pattern != '*':
3615 files_matches = False
3631 files_matches = False
3616 file_regex = re.compile(glob2re(self.file_pattern))
3632 file_regex = re.compile(glob2re(self.file_pattern))
3617 for filename in files_changed:
3633 for filename in files_changed:
3618 if file_regex.search(filename):
3634 if file_regex.search(filename):
3619 files_matches = True
3635 files_matches = True
3620 break
3636 break
3621
3637
3622 return branch_matches and files_matches
3638 return branch_matches and files_matches
3623
3639
3624 @property
3640 @property
3625 def review_users(self):
3641 def review_users(self):
3626 """ Returns the users which this rule applies to """
3642 """ Returns the users which this rule applies to """
3627
3643
3628 users = set()
3644 users = set()
3629 users |= set([
3645 users |= set([
3630 rule_user.user for rule_user in self.rule_users
3646 rule_user.user for rule_user in self.rule_users
3631 if rule_user.user.active])
3647 if rule_user.user.active])
3632 users |= set(
3648 users |= set(
3633 member.user
3649 member.user
3634 for rule_user_group in self.rule_user_groups
3650 for rule_user_group in self.rule_user_groups
3635 for member in rule_user_group.users_group.members
3651 for member in rule_user_group.users_group.members
3636 if member.user.active
3652 if member.user.active
3637 )
3653 )
3638 return users
3654 return users
3639
3655
3640 def __repr__(self):
3656 def __repr__(self):
3641 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3657 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3642 self.repo_review_rule_id, self.repo)
3658 self.repo_review_rule_id, self.repo)
@@ -1,547 +1,551 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 this is forms validation classes
22 this is forms validation classes
23 http://formencode.org/module-formencode.validators.html
23 http://formencode.org/module-formencode.validators.html
24 for list off all availible validators
24 for list off all availible validators
25
25
26 we can create our own validators
26 we can create our own validators
27
27
28 The table below outlines the options which can be used in a schema in addition to the validators themselves
28 The table below outlines the options which can be used in a schema in addition to the validators themselves
29 pre_validators [] These validators will be applied before the schema
29 pre_validators [] These validators will be applied before the schema
30 chained_validators [] These validators will be applied after the schema
30 chained_validators [] These validators will be applied after the schema
31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
35
35
36
36
37 <name> = formencode.validators.<name of validator>
37 <name> = formencode.validators.<name of validator>
38 <name> must equal form name
38 <name> must equal form name
39 list=[1,2,3,4,5]
39 list=[1,2,3,4,5]
40 for SELECT use formencode.All(OneOf(list), Int())
40 for SELECT use formencode.All(OneOf(list), Int())
41
41
42 """
42 """
43
43
44 import deform
44 import deform
45 import logging
45 import logging
46 import formencode
46 import formencode
47
47
48 from pkg_resources import resource_filename
48 from pkg_resources import resource_filename
49 from formencode import All, Pipe
49 from formencode import All, Pipe
50
50
51 from pylons.i18n.translation import _
51 from pylons.i18n.translation import _
52
52
53 from rhodecode import BACKENDS
53 from rhodecode import BACKENDS
54 from rhodecode.lib import helpers
54 from rhodecode.lib import helpers
55 from rhodecode.model import validators as v
55 from rhodecode.model import validators as v
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 deform_templates = resource_filename('deform', 'templates')
60 deform_templates = resource_filename('deform', 'templates')
61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
62 search_path = (rhodecode_templates, deform_templates)
62 search_path = (rhodecode_templates, deform_templates)
63
63
64
64
65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
67 def __call__(self, template_name, **kw):
67 def __call__(self, template_name, **kw):
68 kw['h'] = helpers
68 kw['h'] = helpers
69 return self.load(template_name)(**kw)
69 return self.load(template_name)(**kw)
70
70
71
71
72 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
72 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
73 deform.Form.set_default_renderer(form_renderer)
73 deform.Form.set_default_renderer(form_renderer)
74
74
75
75
76 def LoginForm():
76 def LoginForm():
77 class _LoginForm(formencode.Schema):
77 class _LoginForm(formencode.Schema):
78 allow_extra_fields = True
78 allow_extra_fields = True
79 filter_extra_fields = True
79 filter_extra_fields = True
80 username = v.UnicodeString(
80 username = v.UnicodeString(
81 strip=True,
81 strip=True,
82 min=1,
82 min=1,
83 not_empty=True,
83 not_empty=True,
84 messages={
84 messages={
85 'empty': _(u'Please enter a login'),
85 'empty': _(u'Please enter a login'),
86 'tooShort': _(u'Enter a value %(min)i characters long or more')
86 'tooShort': _(u'Enter a value %(min)i characters long or more')
87 }
87 }
88 )
88 )
89
89
90 password = v.UnicodeString(
90 password = v.UnicodeString(
91 strip=False,
91 strip=False,
92 min=3,
92 min=3,
93 not_empty=True,
93 not_empty=True,
94 messages={
94 messages={
95 'empty': _(u'Please enter a password'),
95 'empty': _(u'Please enter a password'),
96 'tooShort': _(u'Enter %(min)i characters or more')}
96 'tooShort': _(u'Enter %(min)i characters or more')}
97 )
97 )
98
98
99 remember = v.StringBoolean(if_missing=False)
99 remember = v.StringBoolean(if_missing=False)
100
100
101 chained_validators = [v.ValidAuth()]
101 chained_validators = [v.ValidAuth()]
102 return _LoginForm
102 return _LoginForm
103
103
104
104
105 def UserForm(edit=False, available_languages=[], old_data={}):
105 def UserForm(edit=False, available_languages=[], old_data={}):
106 class _UserForm(formencode.Schema):
106 class _UserForm(formencode.Schema):
107 allow_extra_fields = True
107 allow_extra_fields = True
108 filter_extra_fields = True
108 filter_extra_fields = True
109 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
109 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
110 v.ValidUsername(edit, old_data))
110 v.ValidUsername(edit, old_data))
111 if edit:
111 if edit:
112 new_password = All(
112 new_password = All(
113 v.ValidPassword(),
113 v.ValidPassword(),
114 v.UnicodeString(strip=False, min=6, not_empty=False)
114 v.UnicodeString(strip=False, min=6, not_empty=False)
115 )
115 )
116 password_confirmation = All(
116 password_confirmation = All(
117 v.ValidPassword(),
117 v.ValidPassword(),
118 v.UnicodeString(strip=False, min=6, not_empty=False),
118 v.UnicodeString(strip=False, min=6, not_empty=False),
119 )
119 )
120 admin = v.StringBoolean(if_missing=False)
120 admin = v.StringBoolean(if_missing=False)
121 else:
121 else:
122 password = All(
122 password = All(
123 v.ValidPassword(),
123 v.ValidPassword(),
124 v.UnicodeString(strip=False, min=6, not_empty=True)
124 v.UnicodeString(strip=False, min=6, not_empty=True)
125 )
125 )
126 password_confirmation = All(
126 password_confirmation = All(
127 v.ValidPassword(),
127 v.ValidPassword(),
128 v.UnicodeString(strip=False, min=6, not_empty=False)
128 v.UnicodeString(strip=False, min=6, not_empty=False)
129 )
129 )
130
130
131 password_change = v.StringBoolean(if_missing=False)
131 password_change = v.StringBoolean(if_missing=False)
132 create_repo_group = v.StringBoolean(if_missing=False)
132 create_repo_group = v.StringBoolean(if_missing=False)
133
133
134 active = v.StringBoolean(if_missing=False)
134 active = v.StringBoolean(if_missing=False)
135 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
135 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
136 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
136 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
137 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
137 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
138 extern_name = v.UnicodeString(strip=True)
138 extern_name = v.UnicodeString(strip=True)
139 extern_type = v.UnicodeString(strip=True)
139 extern_type = v.UnicodeString(strip=True)
140 language = v.OneOf(available_languages, hideList=False,
140 language = v.OneOf(available_languages, hideList=False,
141 testValueList=True, if_missing=None)
141 testValueList=True, if_missing=None)
142 chained_validators = [v.ValidPasswordsMatch()]
142 chained_validators = [v.ValidPasswordsMatch()]
143 return _UserForm
143 return _UserForm
144
144
145
145
146 def UserGroupForm(edit=False, old_data=None, available_members=None,
146 def UserGroupForm(edit=False, old_data=None, available_members=None,
147 allow_disabled=False):
147 allow_disabled=False):
148 old_data = old_data or {}
148 old_data = old_data or {}
149 available_members = available_members or []
149 available_members = available_members or []
150
150
151 class _UserGroupForm(formencode.Schema):
151 class _UserGroupForm(formencode.Schema):
152 allow_extra_fields = True
152 allow_extra_fields = True
153 filter_extra_fields = True
153 filter_extra_fields = True
154
154
155 users_group_name = All(
155 users_group_name = All(
156 v.UnicodeString(strip=True, min=1, not_empty=True),
156 v.UnicodeString(strip=True, min=1, not_empty=True),
157 v.ValidUserGroup(edit, old_data)
157 v.ValidUserGroup(edit, old_data)
158 )
158 )
159 user_group_description = v.UnicodeString(strip=True, min=1,
159 user_group_description = v.UnicodeString(strip=True, min=1,
160 not_empty=False)
160 not_empty=False)
161
161
162 users_group_active = v.StringBoolean(if_missing=False)
162 users_group_active = v.StringBoolean(if_missing=False)
163
163
164 if edit:
164 if edit:
165 users_group_members = v.OneOf(
165 users_group_members = v.OneOf(
166 available_members, hideList=False, testValueList=True,
166 available_members, hideList=False, testValueList=True,
167 if_missing=None, not_empty=False
167 if_missing=None, not_empty=False
168 )
168 )
169 # this is user group owner
169 # this is user group owner
170 user = All(
170 user = All(
171 v.UnicodeString(not_empty=True),
171 v.UnicodeString(not_empty=True),
172 v.ValidRepoUser(allow_disabled))
172 v.ValidRepoUser(allow_disabled))
173 return _UserGroupForm
173 return _UserGroupForm
174
174
175
175
176 def RepoGroupForm(edit=False, old_data=None, available_groups=None,
176 def RepoGroupForm(edit=False, old_data=None, available_groups=None,
177 can_create_in_root=False, allow_disabled=False):
177 can_create_in_root=False, allow_disabled=False):
178 old_data = old_data or {}
178 old_data = old_data or {}
179 available_groups = available_groups or []
179 available_groups = available_groups or []
180
180
181 class _RepoGroupForm(formencode.Schema):
181 class _RepoGroupForm(formencode.Schema):
182 allow_extra_fields = True
182 allow_extra_fields = True
183 filter_extra_fields = False
183 filter_extra_fields = False
184
184
185 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
185 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
186 v.SlugifyName(),)
186 v.SlugifyName(),)
187 group_description = v.UnicodeString(strip=True, min=1,
187 group_description = v.UnicodeString(strip=True, min=1,
188 not_empty=False)
188 not_empty=False)
189 group_copy_permissions = v.StringBoolean(if_missing=False)
189 group_copy_permissions = v.StringBoolean(if_missing=False)
190
190
191 group_parent_id = v.OneOf(available_groups, hideList=False,
191 group_parent_id = v.OneOf(available_groups, hideList=False,
192 testValueList=True, not_empty=True)
192 testValueList=True, not_empty=True)
193 enable_locking = v.StringBoolean(if_missing=False)
193 enable_locking = v.StringBoolean(if_missing=False)
194 chained_validators = [
194 chained_validators = [
195 v.ValidRepoGroup(edit, old_data, can_create_in_root)]
195 v.ValidRepoGroup(edit, old_data, can_create_in_root)]
196
196
197 if edit:
197 if edit:
198 # this is repo group owner
198 # this is repo group owner
199 user = All(
199 user = All(
200 v.UnicodeString(not_empty=True),
200 v.UnicodeString(not_empty=True),
201 v.ValidRepoUser(allow_disabled))
201 v.ValidRepoUser(allow_disabled))
202
202
203 return _RepoGroupForm
203 return _RepoGroupForm
204
204
205
205
206 def RegisterForm(edit=False, old_data={}):
206 def RegisterForm(edit=False, old_data={}):
207 class _RegisterForm(formencode.Schema):
207 class _RegisterForm(formencode.Schema):
208 allow_extra_fields = True
208 allow_extra_fields = True
209 filter_extra_fields = True
209 filter_extra_fields = True
210 username = All(
210 username = All(
211 v.ValidUsername(edit, old_data),
211 v.ValidUsername(edit, old_data),
212 v.UnicodeString(strip=True, min=1, not_empty=True)
212 v.UnicodeString(strip=True, min=1, not_empty=True)
213 )
213 )
214 password = All(
214 password = All(
215 v.ValidPassword(),
215 v.ValidPassword(),
216 v.UnicodeString(strip=False, min=6, not_empty=True)
216 v.UnicodeString(strip=False, min=6, not_empty=True)
217 )
217 )
218 password_confirmation = All(
218 password_confirmation = All(
219 v.ValidPassword(),
219 v.ValidPassword(),
220 v.UnicodeString(strip=False, min=6, not_empty=True)
220 v.UnicodeString(strip=False, min=6, not_empty=True)
221 )
221 )
222 active = v.StringBoolean(if_missing=False)
222 active = v.StringBoolean(if_missing=False)
223 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
223 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
224 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
224 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
225 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
225 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
226
226
227 chained_validators = [v.ValidPasswordsMatch()]
227 chained_validators = [v.ValidPasswordsMatch()]
228
228
229 return _RegisterForm
229 return _RegisterForm
230
230
231
231
232 def PasswordResetForm():
232 def PasswordResetForm():
233 class _PasswordResetForm(formencode.Schema):
233 class _PasswordResetForm(formencode.Schema):
234 allow_extra_fields = True
234 allow_extra_fields = True
235 filter_extra_fields = True
235 filter_extra_fields = True
236 email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
236 email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
237 return _PasswordResetForm
237 return _PasswordResetForm
238
238
239
239
240 def RepoForm(edit=False, old_data=None, repo_groups=None, landing_revs=None,
240 def RepoForm(edit=False, old_data=None, repo_groups=None, landing_revs=None,
241 allow_disabled=False):
241 allow_disabled=False):
242 old_data = old_data or {}
242 old_data = old_data or {}
243 repo_groups = repo_groups or []
243 repo_groups = repo_groups or []
244 landing_revs = landing_revs or []
244 landing_revs = landing_revs or []
245 supported_backends = BACKENDS.keys()
245 supported_backends = BACKENDS.keys()
246
246
247 class _RepoForm(formencode.Schema):
247 class _RepoForm(formencode.Schema):
248 allow_extra_fields = True
248 allow_extra_fields = True
249 filter_extra_fields = False
249 filter_extra_fields = False
250 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
250 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
251 v.SlugifyName())
251 v.SlugifyName())
252 repo_group = All(v.CanWriteGroup(old_data),
252 repo_group = All(v.CanWriteGroup(old_data),
253 v.OneOf(repo_groups, hideList=True))
253 v.OneOf(repo_groups, hideList=True))
254 repo_type = v.OneOf(supported_backends, required=False,
254 repo_type = v.OneOf(supported_backends, required=False,
255 if_missing=old_data.get('repo_type'))
255 if_missing=old_data.get('repo_type'))
256 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
256 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
257 repo_private = v.StringBoolean(if_missing=False)
257 repo_private = v.StringBoolean(if_missing=False)
258 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
258 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
259 repo_copy_permissions = v.StringBoolean(if_missing=False)
259 repo_copy_permissions = v.StringBoolean(if_missing=False)
260 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
260 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
261
261
262 repo_enable_statistics = v.StringBoolean(if_missing=False)
262 repo_enable_statistics = v.StringBoolean(if_missing=False)
263 repo_enable_downloads = v.StringBoolean(if_missing=False)
263 repo_enable_downloads = v.StringBoolean(if_missing=False)
264 repo_enable_locking = v.StringBoolean(if_missing=False)
264 repo_enable_locking = v.StringBoolean(if_missing=False)
265
265
266 if edit:
266 if edit:
267 # this is repo owner
267 # this is repo owner
268 user = All(
268 user = All(
269 v.UnicodeString(not_empty=True),
269 v.UnicodeString(not_empty=True),
270 v.ValidRepoUser(allow_disabled))
270 v.ValidRepoUser(allow_disabled))
271 clone_uri_change = v.UnicodeString(
271 clone_uri_change = v.UnicodeString(
272 not_empty=False, if_missing=v.Missing)
272 not_empty=False, if_missing=v.Missing)
273
273
274 chained_validators = [v.ValidCloneUri(),
274 chained_validators = [v.ValidCloneUri(),
275 v.ValidRepoName(edit, old_data)]
275 v.ValidRepoName(edit, old_data)]
276 return _RepoForm
276 return _RepoForm
277
277
278
278
279 def RepoPermsForm():
279 def RepoPermsForm():
280 class _RepoPermsForm(formencode.Schema):
280 class _RepoPermsForm(formencode.Schema):
281 allow_extra_fields = True
281 allow_extra_fields = True
282 filter_extra_fields = False
282 filter_extra_fields = False
283 chained_validators = [v.ValidPerms(type_='repo')]
283 chained_validators = [v.ValidPerms(type_='repo')]
284 return _RepoPermsForm
284 return _RepoPermsForm
285
285
286
286
287 def RepoGroupPermsForm(valid_recursive_choices):
287 def RepoGroupPermsForm(valid_recursive_choices):
288 class _RepoGroupPermsForm(formencode.Schema):
288 class _RepoGroupPermsForm(formencode.Schema):
289 allow_extra_fields = True
289 allow_extra_fields = True
290 filter_extra_fields = False
290 filter_extra_fields = False
291 recursive = v.OneOf(valid_recursive_choices)
291 recursive = v.OneOf(valid_recursive_choices)
292 chained_validators = [v.ValidPerms(type_='repo_group')]
292 chained_validators = [v.ValidPerms(type_='repo_group')]
293 return _RepoGroupPermsForm
293 return _RepoGroupPermsForm
294
294
295
295
296 def UserGroupPermsForm():
296 def UserGroupPermsForm():
297 class _UserPermsForm(formencode.Schema):
297 class _UserPermsForm(formencode.Schema):
298 allow_extra_fields = True
298 allow_extra_fields = True
299 filter_extra_fields = False
299 filter_extra_fields = False
300 chained_validators = [v.ValidPerms(type_='user_group')]
300 chained_validators = [v.ValidPerms(type_='user_group')]
301 return _UserPermsForm
301 return _UserPermsForm
302
302
303
303
304 def RepoFieldForm():
304 def RepoFieldForm():
305 class _RepoFieldForm(formencode.Schema):
305 class _RepoFieldForm(formencode.Schema):
306 filter_extra_fields = True
306 filter_extra_fields = True
307 allow_extra_fields = True
307 allow_extra_fields = True
308
308
309 new_field_key = All(v.FieldKey(),
309 new_field_key = All(v.FieldKey(),
310 v.UnicodeString(strip=True, min=3, not_empty=True))
310 v.UnicodeString(strip=True, min=3, not_empty=True))
311 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
311 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
312 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
312 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
313 if_missing='str')
313 if_missing='str')
314 new_field_label = v.UnicodeString(not_empty=False)
314 new_field_label = v.UnicodeString(not_empty=False)
315 new_field_desc = v.UnicodeString(not_empty=False)
315 new_field_desc = v.UnicodeString(not_empty=False)
316
316
317 return _RepoFieldForm
317 return _RepoFieldForm
318
318
319
319
320 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
320 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
321 repo_groups=[], landing_revs=[]):
321 repo_groups=[], landing_revs=[]):
322 class _RepoForkForm(formencode.Schema):
322 class _RepoForkForm(formencode.Schema):
323 allow_extra_fields = True
323 allow_extra_fields = True
324 filter_extra_fields = False
324 filter_extra_fields = False
325 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
325 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
326 v.SlugifyName())
326 v.SlugifyName())
327 repo_group = All(v.CanWriteGroup(),
327 repo_group = All(v.CanWriteGroup(),
328 v.OneOf(repo_groups, hideList=True))
328 v.OneOf(repo_groups, hideList=True))
329 repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
329 repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
330 description = v.UnicodeString(strip=True, min=1, not_empty=True)
330 description = v.UnicodeString(strip=True, min=1, not_empty=True)
331 private = v.StringBoolean(if_missing=False)
331 private = v.StringBoolean(if_missing=False)
332 copy_permissions = v.StringBoolean(if_missing=False)
332 copy_permissions = v.StringBoolean(if_missing=False)
333 fork_parent_id = v.UnicodeString()
333 fork_parent_id = v.UnicodeString()
334 chained_validators = [v.ValidForkName(edit, old_data)]
334 chained_validators = [v.ValidForkName(edit, old_data)]
335 landing_rev = v.OneOf(landing_revs, hideList=True)
335 landing_rev = v.OneOf(landing_revs, hideList=True)
336
336
337 return _RepoForkForm
337 return _RepoForkForm
338
338
339
339
340 def ApplicationSettingsForm():
340 def ApplicationSettingsForm():
341 class _ApplicationSettingsForm(formencode.Schema):
341 class _ApplicationSettingsForm(formencode.Schema):
342 allow_extra_fields = True
342 allow_extra_fields = True
343 filter_extra_fields = False
343 filter_extra_fields = False
344 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
344 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
345 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
345 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
346 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
346 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
347 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
347 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
348 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
348 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
349 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
349 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
350
350
351 return _ApplicationSettingsForm
351 return _ApplicationSettingsForm
352
352
353
353
354 def ApplicationVisualisationForm():
354 def ApplicationVisualisationForm():
355 class _ApplicationVisualisationForm(formencode.Schema):
355 class _ApplicationVisualisationForm(formencode.Schema):
356 allow_extra_fields = True
356 allow_extra_fields = True
357 filter_extra_fields = False
357 filter_extra_fields = False
358 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
358 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
359 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
359 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
360 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
360 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
361
361
362 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
362 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
363 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
363 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
364 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
364 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
365 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
365 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
366 rhodecode_show_version = v.StringBoolean(if_missing=False)
366 rhodecode_show_version = v.StringBoolean(if_missing=False)
367 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
367 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
368 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
368 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
369 rhodecode_gravatar_url = v.UnicodeString(min=3)
369 rhodecode_gravatar_url = v.UnicodeString(min=3)
370 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
370 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
371 rhodecode_support_url = v.UnicodeString()
371 rhodecode_support_url = v.UnicodeString()
372 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
372 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
373 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
373 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
374
374
375 return _ApplicationVisualisationForm
375 return _ApplicationVisualisationForm
376
376
377
377
378 class _BaseVcsSettingsForm(formencode.Schema):
378 class _BaseVcsSettingsForm(formencode.Schema):
379 allow_extra_fields = True
379 allow_extra_fields = True
380 filter_extra_fields = False
380 filter_extra_fields = False
381 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
381 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
382 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
382 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
383 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
383 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
384
384
385 extensions_largefiles = v.StringBoolean(if_missing=False)
385 extensions_largefiles = v.StringBoolean(if_missing=False)
386 phases_publish = v.StringBoolean(if_missing=False)
386 phases_publish = v.StringBoolean(if_missing=False)
387
387
388 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
388 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
389 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
389 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
390 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
390 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
391
391
392 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
392 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
393 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
393 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
394
394
395
395
396 def ApplicationUiSettingsForm():
396 def ApplicationUiSettingsForm():
397 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
397 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
398 web_push_ssl = v.StringBoolean(if_missing=False)
398 web_push_ssl = v.StringBoolean(if_missing=False)
399 paths_root_path = All(
399 paths_root_path = All(
400 v.ValidPath(),
400 v.ValidPath(),
401 v.UnicodeString(strip=True, min=1, not_empty=True)
401 v.UnicodeString(strip=True, min=1, not_empty=True)
402 )
402 )
403 extensions_hgsubversion = v.StringBoolean(if_missing=False)
403 extensions_hgsubversion = v.StringBoolean(if_missing=False)
404 extensions_hggit = v.StringBoolean(if_missing=False)
404 extensions_hggit = v.StringBoolean(if_missing=False)
405 new_svn_branch = v.ValidSvnPattern(section='vcs_svn_branch')
405 new_svn_branch = v.ValidSvnPattern(section='vcs_svn_branch')
406 new_svn_tag = v.ValidSvnPattern(section='vcs_svn_tag')
406 new_svn_tag = v.ValidSvnPattern(section='vcs_svn_tag')
407
407
408 return _ApplicationUiSettingsForm
408 return _ApplicationUiSettingsForm
409
409
410
410
411 def RepoVcsSettingsForm(repo_name):
411 def RepoVcsSettingsForm(repo_name):
412 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
412 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
413 inherit_global_settings = v.StringBoolean(if_missing=False)
413 inherit_global_settings = v.StringBoolean(if_missing=False)
414 new_svn_branch = v.ValidSvnPattern(
414 new_svn_branch = v.ValidSvnPattern(
415 section='vcs_svn_branch', repo_name=repo_name)
415 section='vcs_svn_branch', repo_name=repo_name)
416 new_svn_tag = v.ValidSvnPattern(
416 new_svn_tag = v.ValidSvnPattern(
417 section='vcs_svn_tag', repo_name=repo_name)
417 section='vcs_svn_tag', repo_name=repo_name)
418
418
419 return _RepoVcsSettingsForm
419 return _RepoVcsSettingsForm
420
420
421
421
422 def LabsSettingsForm():
422 def LabsSettingsForm():
423 class _LabSettingsForm(formencode.Schema):
423 class _LabSettingsForm(formencode.Schema):
424 allow_extra_fields = True
424 allow_extra_fields = True
425 filter_extra_fields = False
425 filter_extra_fields = False
426
426
427 return _LabSettingsForm
427 return _LabSettingsForm
428
428
429
429
430 def ApplicationPermissionsForm(register_choices, extern_activate_choices):
430 def ApplicationPermissionsForm(register_choices, extern_activate_choices):
431 class _DefaultPermissionsForm(formencode.Schema):
431 class _DefaultPermissionsForm(formencode.Schema):
432 allow_extra_fields = True
432 allow_extra_fields = True
433 filter_extra_fields = True
433 filter_extra_fields = True
434
434
435 anonymous = v.StringBoolean(if_missing=False)
435 anonymous = v.StringBoolean(if_missing=False)
436 default_register = v.OneOf(register_choices)
436 default_register = v.OneOf(register_choices)
437 default_register_message = v.UnicodeString()
437 default_register_message = v.UnicodeString()
438 default_extern_activate = v.OneOf(extern_activate_choices)
438 default_extern_activate = v.OneOf(extern_activate_choices)
439
439
440 return _DefaultPermissionsForm
440 return _DefaultPermissionsForm
441
441
442
442
443 def ObjectPermissionsForm(repo_perms_choices, group_perms_choices,
443 def ObjectPermissionsForm(repo_perms_choices, group_perms_choices,
444 user_group_perms_choices):
444 user_group_perms_choices):
445 class _ObjectPermissionsForm(formencode.Schema):
445 class _ObjectPermissionsForm(formencode.Schema):
446 allow_extra_fields = True
446 allow_extra_fields = True
447 filter_extra_fields = True
447 filter_extra_fields = True
448 overwrite_default_repo = v.StringBoolean(if_missing=False)
448 overwrite_default_repo = v.StringBoolean(if_missing=False)
449 overwrite_default_group = v.StringBoolean(if_missing=False)
449 overwrite_default_group = v.StringBoolean(if_missing=False)
450 overwrite_default_user_group = v.StringBoolean(if_missing=False)
450 overwrite_default_user_group = v.StringBoolean(if_missing=False)
451 default_repo_perm = v.OneOf(repo_perms_choices)
451 default_repo_perm = v.OneOf(repo_perms_choices)
452 default_group_perm = v.OneOf(group_perms_choices)
452 default_group_perm = v.OneOf(group_perms_choices)
453 default_user_group_perm = v.OneOf(user_group_perms_choices)
453 default_user_group_perm = v.OneOf(user_group_perms_choices)
454
454
455 return _ObjectPermissionsForm
455 return _ObjectPermissionsForm
456
456
457
457
458 def UserPermissionsForm(create_choices, create_on_write_choices,
458 def UserPermissionsForm(create_choices, create_on_write_choices,
459 repo_group_create_choices, user_group_create_choices,
459 repo_group_create_choices, user_group_create_choices,
460 fork_choices, inherit_default_permissions_choices):
460 fork_choices, inherit_default_permissions_choices):
461 class _DefaultPermissionsForm(formencode.Schema):
461 class _DefaultPermissionsForm(formencode.Schema):
462 allow_extra_fields = True
462 allow_extra_fields = True
463 filter_extra_fields = True
463 filter_extra_fields = True
464
464
465 anonymous = v.StringBoolean(if_missing=False)
465 anonymous = v.StringBoolean(if_missing=False)
466
466
467 default_repo_create = v.OneOf(create_choices)
467 default_repo_create = v.OneOf(create_choices)
468 default_repo_create_on_write = v.OneOf(create_on_write_choices)
468 default_repo_create_on_write = v.OneOf(create_on_write_choices)
469 default_user_group_create = v.OneOf(user_group_create_choices)
469 default_user_group_create = v.OneOf(user_group_create_choices)
470 default_repo_group_create = v.OneOf(repo_group_create_choices)
470 default_repo_group_create = v.OneOf(repo_group_create_choices)
471 default_fork_create = v.OneOf(fork_choices)
471 default_fork_create = v.OneOf(fork_choices)
472 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
472 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
473
473
474 return _DefaultPermissionsForm
474 return _DefaultPermissionsForm
475
475
476
476
477 def UserIndividualPermissionsForm():
477 def UserIndividualPermissionsForm():
478 class _DefaultPermissionsForm(formencode.Schema):
478 class _DefaultPermissionsForm(formencode.Schema):
479 allow_extra_fields = True
479 allow_extra_fields = True
480 filter_extra_fields = True
480 filter_extra_fields = True
481
481
482 inherit_default_permissions = v.StringBoolean(if_missing=False)
482 inherit_default_permissions = v.StringBoolean(if_missing=False)
483
483
484 return _DefaultPermissionsForm
484 return _DefaultPermissionsForm
485
485
486
486
487 def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
487 def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
488 class _DefaultsForm(formencode.Schema):
488 class _DefaultsForm(formencode.Schema):
489 allow_extra_fields = True
489 allow_extra_fields = True
490 filter_extra_fields = True
490 filter_extra_fields = True
491 default_repo_type = v.OneOf(supported_backends)
491 default_repo_type = v.OneOf(supported_backends)
492 default_repo_private = v.StringBoolean(if_missing=False)
492 default_repo_private = v.StringBoolean(if_missing=False)
493 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
493 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
494 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
494 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
495 default_repo_enable_locking = v.StringBoolean(if_missing=False)
495 default_repo_enable_locking = v.StringBoolean(if_missing=False)
496
496
497 return _DefaultsForm
497 return _DefaultsForm
498
498
499
499
500 def AuthSettingsForm():
500 def AuthSettingsForm():
501 class _AuthSettingsForm(formencode.Schema):
501 class _AuthSettingsForm(formencode.Schema):
502 allow_extra_fields = True
502 allow_extra_fields = True
503 filter_extra_fields = True
503 filter_extra_fields = True
504 auth_plugins = All(v.ValidAuthPlugins(),
504 auth_plugins = All(v.ValidAuthPlugins(),
505 v.UniqueListFromString()(not_empty=True))
505 v.UniqueListFromString()(not_empty=True))
506
506
507 return _AuthSettingsForm
507 return _AuthSettingsForm
508
508
509
509
510 def UserExtraEmailForm():
510 def UserExtraEmailForm():
511 class _UserExtraEmailForm(formencode.Schema):
511 class _UserExtraEmailForm(formencode.Schema):
512 email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
512 email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
513 return _UserExtraEmailForm
513 return _UserExtraEmailForm
514
514
515
515
516 def UserExtraIpForm():
516 def UserExtraIpForm():
517 class _UserExtraIpForm(formencode.Schema):
517 class _UserExtraIpForm(formencode.Schema):
518 ip = v.ValidIp()(not_empty=True)
518 ip = v.ValidIp()(not_empty=True)
519 return _UserExtraIpForm
519 return _UserExtraIpForm
520
520
521
521
522
522 def PullRequestForm(repo_id):
523 def PullRequestForm(repo_id):
524 class ReviewerForm(formencode.Schema):
525 user_id = v.Int(not_empty=True)
526 reasons = All()
527
523 class _PullRequestForm(formencode.Schema):
528 class _PullRequestForm(formencode.Schema):
524 allow_extra_fields = True
529 allow_extra_fields = True
525 filter_extra_fields = True
530 filter_extra_fields = True
526
531
527 user = v.UnicodeString(strip=True, required=True)
532 user = v.UnicodeString(strip=True, required=True)
528 source_repo = v.UnicodeString(strip=True, required=True)
533 source_repo = v.UnicodeString(strip=True, required=True)
529 source_ref = v.UnicodeString(strip=True, required=True)
534 source_ref = v.UnicodeString(strip=True, required=True)
530 target_repo = v.UnicodeString(strip=True, required=True)
535 target_repo = v.UnicodeString(strip=True, required=True)
531 target_ref = v.UnicodeString(strip=True, required=True)
536 target_ref = v.UnicodeString(strip=True, required=True)
532 revisions = All(#v.NotReviewedRevisions(repo_id)(),
537 revisions = All(#v.NotReviewedRevisions(repo_id)(),
533 v.UniqueList()(not_empty=True))
538 v.UniqueList()(not_empty=True))
534 review_members = v.UniqueList(convert=int)(not_empty=True)
539 review_members = formencode.ForEach(ReviewerForm())
535
536 pullrequest_title = v.UnicodeString(strip=True, required=True)
540 pullrequest_title = v.UnicodeString(strip=True, required=True)
537 pullrequest_desc = v.UnicodeString(strip=True, required=False)
541 pullrequest_desc = v.UnicodeString(strip=True, required=False)
538
542
539 return _PullRequestForm
543 return _PullRequestForm
540
544
541
545
542 def IssueTrackerPatternsForm():
546 def IssueTrackerPatternsForm():
543 class _IssueTrackerPatternsForm(formencode.Schema):
547 class _IssueTrackerPatternsForm(formencode.Schema):
544 allow_extra_fields = True
548 allow_extra_fields = True
545 filter_extra_fields = False
549 filter_extra_fields = False
546 chained_validators = [v.ValidPattern()]
550 chained_validators = [v.ValidPattern()]
547 return _IssueTrackerPatternsForm
551 return _IssueTrackerPatternsForm
@@ -1,1154 +1,1177 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26 from collections import namedtuple
26 from collections import namedtuple
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pylons.i18n.translation import lazy_ugettext
32 from pylons.i18n.translation import lazy_ugettext
33
33
34 from rhodecode.lib import helpers as h, hooks_utils, diffs
34 from rhodecode.lib import helpers as h, hooks_utils, diffs
35 from rhodecode.lib.compat import OrderedDict
35 from rhodecode.lib.compat import OrderedDict
36 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
36 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
37 from rhodecode.lib.markup_renderer import (
37 from rhodecode.lib.markup_renderer import (
38 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
38 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
39 from rhodecode.lib.utils import action_logger
39 from rhodecode.lib.utils import action_logger
40 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
40 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
41 from rhodecode.lib.vcs.backends.base import (
41 from rhodecode.lib.vcs.backends.base import (
42 Reference, MergeResponse, MergeFailureReason)
42 Reference, MergeResponse, MergeFailureReason)
43 from rhodecode.lib.vcs.conf import settings as vcs_settings
43 from rhodecode.lib.vcs.conf import settings as vcs_settings
44 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
45 CommitDoesNotExistError, EmptyRepositoryError)
45 CommitDoesNotExistError, EmptyRepositoryError)
46 from rhodecode.model import BaseModel
46 from rhodecode.model import BaseModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.comment import ChangesetCommentsModel
48 from rhodecode.model.comment import ChangesetCommentsModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 PullRequest, PullRequestReviewers, ChangesetStatus,
50 PullRequest, PullRequestReviewers, ChangesetStatus,
51 PullRequestVersion, ChangesetComment)
51 PullRequestVersion, ChangesetComment)
52 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
53 from rhodecode.model.notification import NotificationModel, \
53 from rhodecode.model.notification import NotificationModel, \
54 EmailNotificationModel
54 EmailNotificationModel
55 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
56 from rhodecode.model.settings import VcsSettingsModel
56 from rhodecode.model.settings import VcsSettingsModel
57
57
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61
61
62 class PullRequestModel(BaseModel):
62 class PullRequestModel(BaseModel):
63
63
64 cls = PullRequest
64 cls = PullRequest
65
65
66 DIFF_CONTEXT = 3
66 DIFF_CONTEXT = 3
67
67
68 MERGE_STATUS_MESSAGES = {
68 MERGE_STATUS_MESSAGES = {
69 MergeFailureReason.NONE: lazy_ugettext(
69 MergeFailureReason.NONE: lazy_ugettext(
70 'This pull request can be automatically merged.'),
70 'This pull request can be automatically merged.'),
71 MergeFailureReason.UNKNOWN: lazy_ugettext(
71 MergeFailureReason.UNKNOWN: lazy_ugettext(
72 'This pull request cannot be merged because of an unhandled'
72 'This pull request cannot be merged because of an unhandled'
73 ' exception.'),
73 ' exception.'),
74 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
74 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
75 'This pull request cannot be merged because of conflicts.'),
75 'This pull request cannot be merged because of conflicts.'),
76 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
76 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
77 'This pull request could not be merged because push to target'
77 'This pull request could not be merged because push to target'
78 ' failed.'),
78 ' failed.'),
79 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
79 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
80 'This pull request cannot be merged because the target is not a'
80 'This pull request cannot be merged because the target is not a'
81 ' head.'),
81 ' head.'),
82 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
82 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
83 'This pull request cannot be merged because the source contains'
83 'This pull request cannot be merged because the source contains'
84 ' more branches than the target.'),
84 ' more branches than the target.'),
85 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
85 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
86 'This pull request cannot be merged because the target has'
86 'This pull request cannot be merged because the target has'
87 ' multiple heads.'),
87 ' multiple heads.'),
88 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
88 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
89 'This pull request cannot be merged because the target repository'
89 'This pull request cannot be merged because the target repository'
90 ' is locked.'),
90 ' is locked.'),
91 MergeFailureReason.MISSING_COMMIT: lazy_ugettext(
91 MergeFailureReason.MISSING_COMMIT: lazy_ugettext(
92 'This pull request cannot be merged because the target or the '
92 'This pull request cannot be merged because the target or the '
93 'source reference is missing.'),
93 'source reference is missing.'),
94 }
94 }
95
95
96 def __get_pull_request(self, pull_request):
96 def __get_pull_request(self, pull_request):
97 return self._get_instance(PullRequest, pull_request)
97 return self._get_instance(PullRequest, pull_request)
98
98
99 def _check_perms(self, perms, pull_request, user, api=False):
99 def _check_perms(self, perms, pull_request, user, api=False):
100 if not api:
100 if not api:
101 return h.HasRepoPermissionAny(*perms)(
101 return h.HasRepoPermissionAny(*perms)(
102 user=user, repo_name=pull_request.target_repo.repo_name)
102 user=user, repo_name=pull_request.target_repo.repo_name)
103 else:
103 else:
104 return h.HasRepoPermissionAnyApi(*perms)(
104 return h.HasRepoPermissionAnyApi(*perms)(
105 user=user, repo_name=pull_request.target_repo.repo_name)
105 user=user, repo_name=pull_request.target_repo.repo_name)
106
106
107 def check_user_read(self, pull_request, user, api=False):
107 def check_user_read(self, pull_request, user, api=False):
108 _perms = ('repository.admin', 'repository.write', 'repository.read',)
108 _perms = ('repository.admin', 'repository.write', 'repository.read',)
109 return self._check_perms(_perms, pull_request, user, api)
109 return self._check_perms(_perms, pull_request, user, api)
110
110
111 def check_user_merge(self, pull_request, user, api=False):
111 def check_user_merge(self, pull_request, user, api=False):
112 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
112 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
113 return self._check_perms(_perms, pull_request, user, api)
113 return self._check_perms(_perms, pull_request, user, api)
114
114
115 def check_user_update(self, pull_request, user, api=False):
115 def check_user_update(self, pull_request, user, api=False):
116 owner = user.user_id == pull_request.user_id
116 owner = user.user_id == pull_request.user_id
117 return self.check_user_merge(pull_request, user, api) or owner
117 return self.check_user_merge(pull_request, user, api) or owner
118
118
119 def check_user_change_status(self, pull_request, user, api=False):
119 def check_user_change_status(self, pull_request, user, api=False):
120 reviewer = user.user_id in [x.user_id for x in
120 reviewer = user.user_id in [x.user_id for x in
121 pull_request.reviewers]
121 pull_request.reviewers]
122 return self.check_user_update(pull_request, user, api) or reviewer
122 return self.check_user_update(pull_request, user, api) or reviewer
123
123
124 def get(self, pull_request):
124 def get(self, pull_request):
125 return self.__get_pull_request(pull_request)
125 return self.__get_pull_request(pull_request)
126
126
127 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
127 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
128 opened_by=None, order_by=None,
128 opened_by=None, order_by=None,
129 order_dir='desc'):
129 order_dir='desc'):
130 repo = self._get_repo(repo_name)
130 repo = self._get_repo(repo_name)
131 q = PullRequest.query()
131 q = PullRequest.query()
132 # source or target
132 # source or target
133 if source:
133 if source:
134 q = q.filter(PullRequest.source_repo == repo)
134 q = q.filter(PullRequest.source_repo == repo)
135 else:
135 else:
136 q = q.filter(PullRequest.target_repo == repo)
136 q = q.filter(PullRequest.target_repo == repo)
137
137
138 # closed,opened
138 # closed,opened
139 if statuses:
139 if statuses:
140 q = q.filter(PullRequest.status.in_(statuses))
140 q = q.filter(PullRequest.status.in_(statuses))
141
141
142 # opened by filter
142 # opened by filter
143 if opened_by:
143 if opened_by:
144 q = q.filter(PullRequest.user_id.in_(opened_by))
144 q = q.filter(PullRequest.user_id.in_(opened_by))
145
145
146 if order_by:
146 if order_by:
147 order_map = {
147 order_map = {
148 'name_raw': PullRequest.pull_request_id,
148 'name_raw': PullRequest.pull_request_id,
149 'title': PullRequest.title,
149 'title': PullRequest.title,
150 'updated_on_raw': PullRequest.updated_on
150 'updated_on_raw': PullRequest.updated_on
151 }
151 }
152 if order_dir == 'asc':
152 if order_dir == 'asc':
153 q = q.order_by(order_map[order_by].asc())
153 q = q.order_by(order_map[order_by].asc())
154 else:
154 else:
155 q = q.order_by(order_map[order_by].desc())
155 q = q.order_by(order_map[order_by].desc())
156
156
157 return q
157 return q
158
158
159 def count_all(self, repo_name, source=False, statuses=None,
159 def count_all(self, repo_name, source=False, statuses=None,
160 opened_by=None):
160 opened_by=None):
161 """
161 """
162 Count the number of pull requests for a specific repository.
162 Count the number of pull requests for a specific repository.
163
163
164 :param repo_name: target or source repo
164 :param repo_name: target or source repo
165 :param source: boolean flag to specify if repo_name refers to source
165 :param source: boolean flag to specify if repo_name refers to source
166 :param statuses: list of pull request statuses
166 :param statuses: list of pull request statuses
167 :param opened_by: author user of the pull request
167 :param opened_by: author user of the pull request
168 :returns: int number of pull requests
168 :returns: int number of pull requests
169 """
169 """
170 q = self._prepare_get_all_query(
170 q = self._prepare_get_all_query(
171 repo_name, source=source, statuses=statuses, opened_by=opened_by)
171 repo_name, source=source, statuses=statuses, opened_by=opened_by)
172
172
173 return q.count()
173 return q.count()
174
174
175 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
175 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
176 offset=0, length=None, order_by=None, order_dir='desc'):
176 offset=0, length=None, order_by=None, order_dir='desc'):
177 """
177 """
178 Get all pull requests for a specific repository.
178 Get all pull requests for a specific repository.
179
179
180 :param repo_name: target or source repo
180 :param repo_name: target or source repo
181 :param source: boolean flag to specify if repo_name refers to source
181 :param source: boolean flag to specify if repo_name refers to source
182 :param statuses: list of pull request statuses
182 :param statuses: list of pull request statuses
183 :param opened_by: author user of the pull request
183 :param opened_by: author user of the pull request
184 :param offset: pagination offset
184 :param offset: pagination offset
185 :param length: length of returned list
185 :param length: length of returned list
186 :param order_by: order of the returned list
186 :param order_by: order of the returned list
187 :param order_dir: 'asc' or 'desc' ordering direction
187 :param order_dir: 'asc' or 'desc' ordering direction
188 :returns: list of pull requests
188 :returns: list of pull requests
189 """
189 """
190 q = self._prepare_get_all_query(
190 q = self._prepare_get_all_query(
191 repo_name, source=source, statuses=statuses, opened_by=opened_by,
191 repo_name, source=source, statuses=statuses, opened_by=opened_by,
192 order_by=order_by, order_dir=order_dir)
192 order_by=order_by, order_dir=order_dir)
193
193
194 if length:
194 if length:
195 pull_requests = q.limit(length).offset(offset).all()
195 pull_requests = q.limit(length).offset(offset).all()
196 else:
196 else:
197 pull_requests = q.all()
197 pull_requests = q.all()
198
198
199 return pull_requests
199 return pull_requests
200
200
201 def count_awaiting_review(self, repo_name, source=False, statuses=None,
201 def count_awaiting_review(self, repo_name, source=False, statuses=None,
202 opened_by=None):
202 opened_by=None):
203 """
203 """
204 Count the number of pull requests for a specific repository that are
204 Count the number of pull requests for a specific repository that are
205 awaiting review.
205 awaiting review.
206
206
207 :param repo_name: target or source repo
207 :param repo_name: target or source repo
208 :param source: boolean flag to specify if repo_name refers to source
208 :param source: boolean flag to specify if repo_name refers to source
209 :param statuses: list of pull request statuses
209 :param statuses: list of pull request statuses
210 :param opened_by: author user of the pull request
210 :param opened_by: author user of the pull request
211 :returns: int number of pull requests
211 :returns: int number of pull requests
212 """
212 """
213 pull_requests = self.get_awaiting_review(
213 pull_requests = self.get_awaiting_review(
214 repo_name, source=source, statuses=statuses, opened_by=opened_by)
214 repo_name, source=source, statuses=statuses, opened_by=opened_by)
215
215
216 return len(pull_requests)
216 return len(pull_requests)
217
217
218 def get_awaiting_review(self, repo_name, source=False, statuses=None,
218 def get_awaiting_review(self, repo_name, source=False, statuses=None,
219 opened_by=None, offset=0, length=None,
219 opened_by=None, offset=0, length=None,
220 order_by=None, order_dir='desc'):
220 order_by=None, order_dir='desc'):
221 """
221 """
222 Get all pull requests for a specific repository that are awaiting
222 Get all pull requests for a specific repository that are awaiting
223 review.
223 review.
224
224
225 :param repo_name: target or source repo
225 :param repo_name: target or source repo
226 :param source: boolean flag to specify if repo_name refers to source
226 :param source: boolean flag to specify if repo_name refers to source
227 :param statuses: list of pull request statuses
227 :param statuses: list of pull request statuses
228 :param opened_by: author user of the pull request
228 :param opened_by: author user of the pull request
229 :param offset: pagination offset
229 :param offset: pagination offset
230 :param length: length of returned list
230 :param length: length of returned list
231 :param order_by: order of the returned list
231 :param order_by: order of the returned list
232 :param order_dir: 'asc' or 'desc' ordering direction
232 :param order_dir: 'asc' or 'desc' ordering direction
233 :returns: list of pull requests
233 :returns: list of pull requests
234 """
234 """
235 pull_requests = self.get_all(
235 pull_requests = self.get_all(
236 repo_name, source=source, statuses=statuses, opened_by=opened_by,
236 repo_name, source=source, statuses=statuses, opened_by=opened_by,
237 order_by=order_by, order_dir=order_dir)
237 order_by=order_by, order_dir=order_dir)
238
238
239 _filtered_pull_requests = []
239 _filtered_pull_requests = []
240 for pr in pull_requests:
240 for pr in pull_requests:
241 status = pr.calculated_review_status()
241 status = pr.calculated_review_status()
242 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
242 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
243 ChangesetStatus.STATUS_UNDER_REVIEW]:
243 ChangesetStatus.STATUS_UNDER_REVIEW]:
244 _filtered_pull_requests.append(pr)
244 _filtered_pull_requests.append(pr)
245 if length:
245 if length:
246 return _filtered_pull_requests[offset:offset+length]
246 return _filtered_pull_requests[offset:offset+length]
247 else:
247 else:
248 return _filtered_pull_requests
248 return _filtered_pull_requests
249
249
250 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
250 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
251 opened_by=None, user_id=None):
251 opened_by=None, user_id=None):
252 """
252 """
253 Count the number of pull requests for a specific repository that are
253 Count the number of pull requests for a specific repository that are
254 awaiting review from a specific user.
254 awaiting review from a specific user.
255
255
256 :param repo_name: target or source repo
256 :param repo_name: target or source repo
257 :param source: boolean flag to specify if repo_name refers to source
257 :param source: boolean flag to specify if repo_name refers to source
258 :param statuses: list of pull request statuses
258 :param statuses: list of pull request statuses
259 :param opened_by: author user of the pull request
259 :param opened_by: author user of the pull request
260 :param user_id: reviewer user of the pull request
260 :param user_id: reviewer user of the pull request
261 :returns: int number of pull requests
261 :returns: int number of pull requests
262 """
262 """
263 pull_requests = self.get_awaiting_my_review(
263 pull_requests = self.get_awaiting_my_review(
264 repo_name, source=source, statuses=statuses, opened_by=opened_by,
264 repo_name, source=source, statuses=statuses, opened_by=opened_by,
265 user_id=user_id)
265 user_id=user_id)
266
266
267 return len(pull_requests)
267 return len(pull_requests)
268
268
269 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
269 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
270 opened_by=None, user_id=None, offset=0,
270 opened_by=None, user_id=None, offset=0,
271 length=None, order_by=None, order_dir='desc'):
271 length=None, order_by=None, order_dir='desc'):
272 """
272 """
273 Get all pull requests for a specific repository that are awaiting
273 Get all pull requests for a specific repository that are awaiting
274 review from a specific user.
274 review from a specific user.
275
275
276 :param repo_name: target or source repo
276 :param repo_name: target or source repo
277 :param source: boolean flag to specify if repo_name refers to source
277 :param source: boolean flag to specify if repo_name refers to source
278 :param statuses: list of pull request statuses
278 :param statuses: list of pull request statuses
279 :param opened_by: author user of the pull request
279 :param opened_by: author user of the pull request
280 :param user_id: reviewer user of the pull request
280 :param user_id: reviewer user of the pull request
281 :param offset: pagination offset
281 :param offset: pagination offset
282 :param length: length of returned list
282 :param length: length of returned list
283 :param order_by: order of the returned list
283 :param order_by: order of the returned list
284 :param order_dir: 'asc' or 'desc' ordering direction
284 :param order_dir: 'asc' or 'desc' ordering direction
285 :returns: list of pull requests
285 :returns: list of pull requests
286 """
286 """
287 pull_requests = self.get_all(
287 pull_requests = self.get_all(
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
289 order_by=order_by, order_dir=order_dir)
289 order_by=order_by, order_dir=order_dir)
290
290
291 _my = PullRequestModel().get_not_reviewed(user_id)
291 _my = PullRequestModel().get_not_reviewed(user_id)
292 my_participation = []
292 my_participation = []
293 for pr in pull_requests:
293 for pr in pull_requests:
294 if pr in _my:
294 if pr in _my:
295 my_participation.append(pr)
295 my_participation.append(pr)
296 _filtered_pull_requests = my_participation
296 _filtered_pull_requests = my_participation
297 if length:
297 if length:
298 return _filtered_pull_requests[offset:offset+length]
298 return _filtered_pull_requests[offset:offset+length]
299 else:
299 else:
300 return _filtered_pull_requests
300 return _filtered_pull_requests
301
301
302 def get_not_reviewed(self, user_id):
302 def get_not_reviewed(self, user_id):
303 return [
303 return [
304 x.pull_request for x in PullRequestReviewers.query().filter(
304 x.pull_request for x in PullRequestReviewers.query().filter(
305 PullRequestReviewers.user_id == user_id).all()
305 PullRequestReviewers.user_id == user_id).all()
306 ]
306 ]
307
307
308 def get_versions(self, pull_request):
308 def get_versions(self, pull_request):
309 """
309 """
310 returns version of pull request sorted by ID descending
310 returns version of pull request sorted by ID descending
311 """
311 """
312 return PullRequestVersion.query()\
312 return PullRequestVersion.query()\
313 .filter(PullRequestVersion.pull_request == pull_request)\
313 .filter(PullRequestVersion.pull_request == pull_request)\
314 .order_by(PullRequestVersion.pull_request_version_id.asc())\
314 .order_by(PullRequestVersion.pull_request_version_id.asc())\
315 .all()
315 .all()
316
316
317 def create(self, created_by, source_repo, source_ref, target_repo,
317 def create(self, created_by, source_repo, source_ref, target_repo,
318 target_ref, revisions, reviewers, title, description=None):
318 target_ref, revisions, reviewers, title, description=None):
319 created_by_user = self._get_user(created_by)
319 created_by_user = self._get_user(created_by)
320 source_repo = self._get_repo(source_repo)
320 source_repo = self._get_repo(source_repo)
321 target_repo = self._get_repo(target_repo)
321 target_repo = self._get_repo(target_repo)
322
322
323 pull_request = PullRequest()
323 pull_request = PullRequest()
324 pull_request.source_repo = source_repo
324 pull_request.source_repo = source_repo
325 pull_request.source_ref = source_ref
325 pull_request.source_ref = source_ref
326 pull_request.target_repo = target_repo
326 pull_request.target_repo = target_repo
327 pull_request.target_ref = target_ref
327 pull_request.target_ref = target_ref
328 pull_request.revisions = revisions
328 pull_request.revisions = revisions
329 pull_request.title = title
329 pull_request.title = title
330 pull_request.description = description
330 pull_request.description = description
331 pull_request.author = created_by_user
331 pull_request.author = created_by_user
332
332
333 Session().add(pull_request)
333 Session().add(pull_request)
334 Session().flush()
334 Session().flush()
335
335
336 reviewer_ids = set()
336 # members / reviewers
337 # members / reviewers
337 for user_id in set(reviewers):
338 for reviewer_object in reviewers:
339 if isinstance(reviewer_object, tuple):
340 user_id, reasons = reviewer_object
341 else:
342 user_id, reasons = reviewer_object, []
343
338 user = self._get_user(user_id)
344 user = self._get_user(user_id)
339 reviewer = PullRequestReviewers(user, pull_request)
345 reviewer_ids.add(user.user_id)
346
347 reviewer = PullRequestReviewers(user, pull_request, reasons)
340 Session().add(reviewer)
348 Session().add(reviewer)
341
349
342 # Set approval status to "Under Review" for all commits which are
350 # Set approval status to "Under Review" for all commits which are
343 # part of this pull request.
351 # part of this pull request.
344 ChangesetStatusModel().set_status(
352 ChangesetStatusModel().set_status(
345 repo=target_repo,
353 repo=target_repo,
346 status=ChangesetStatus.STATUS_UNDER_REVIEW,
354 status=ChangesetStatus.STATUS_UNDER_REVIEW,
347 user=created_by_user,
355 user=created_by_user,
348 pull_request=pull_request
356 pull_request=pull_request
349 )
357 )
350
358
351 self.notify_reviewers(pull_request, reviewers)
359 self.notify_reviewers(pull_request, reviewer_ids)
352 self._trigger_pull_request_hook(
360 self._trigger_pull_request_hook(
353 pull_request, created_by_user, 'create')
361 pull_request, created_by_user, 'create')
354
362
355 return pull_request
363 return pull_request
356
364
357 def _trigger_pull_request_hook(self, pull_request, user, action):
365 def _trigger_pull_request_hook(self, pull_request, user, action):
358 pull_request = self.__get_pull_request(pull_request)
366 pull_request = self.__get_pull_request(pull_request)
359 target_scm = pull_request.target_repo.scm_instance()
367 target_scm = pull_request.target_repo.scm_instance()
360 if action == 'create':
368 if action == 'create':
361 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
369 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
362 elif action == 'merge':
370 elif action == 'merge':
363 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
371 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
364 elif action == 'close':
372 elif action == 'close':
365 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
373 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
366 elif action == 'review_status_change':
374 elif action == 'review_status_change':
367 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
375 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
368 elif action == 'update':
376 elif action == 'update':
369 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
377 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
370 else:
378 else:
371 return
379 return
372
380
373 trigger_hook(
381 trigger_hook(
374 username=user.username,
382 username=user.username,
375 repo_name=pull_request.target_repo.repo_name,
383 repo_name=pull_request.target_repo.repo_name,
376 repo_alias=target_scm.alias,
384 repo_alias=target_scm.alias,
377 pull_request=pull_request)
385 pull_request=pull_request)
378
386
379 def _get_commit_ids(self, pull_request):
387 def _get_commit_ids(self, pull_request):
380 """
388 """
381 Return the commit ids of the merged pull request.
389 Return the commit ids of the merged pull request.
382
390
383 This method is not dealing correctly yet with the lack of autoupdates
391 This method is not dealing correctly yet with the lack of autoupdates
384 nor with the implicit target updates.
392 nor with the implicit target updates.
385 For example: if a commit in the source repo is already in the target it
393 For example: if a commit in the source repo is already in the target it
386 will be reported anyways.
394 will be reported anyways.
387 """
395 """
388 merge_rev = pull_request.merge_rev
396 merge_rev = pull_request.merge_rev
389 if merge_rev is None:
397 if merge_rev is None:
390 raise ValueError('This pull request was not merged yet')
398 raise ValueError('This pull request was not merged yet')
391
399
392 commit_ids = list(pull_request.revisions)
400 commit_ids = list(pull_request.revisions)
393 if merge_rev not in commit_ids:
401 if merge_rev not in commit_ids:
394 commit_ids.append(merge_rev)
402 commit_ids.append(merge_rev)
395
403
396 return commit_ids
404 return commit_ids
397
405
398 def merge(self, pull_request, user, extras):
406 def merge(self, pull_request, user, extras):
399 log.debug("Merging pull request %s", pull_request.pull_request_id)
407 log.debug("Merging pull request %s", pull_request.pull_request_id)
400 merge_state = self._merge_pull_request(pull_request, user, extras)
408 merge_state = self._merge_pull_request(pull_request, user, extras)
401 if merge_state.executed:
409 if merge_state.executed:
402 log.debug(
410 log.debug(
403 "Merge was successful, updating the pull request comments.")
411 "Merge was successful, updating the pull request comments.")
404 self._comment_and_close_pr(pull_request, user, merge_state)
412 self._comment_and_close_pr(pull_request, user, merge_state)
405 self._log_action('user_merged_pull_request', user, pull_request)
413 self._log_action('user_merged_pull_request', user, pull_request)
406 else:
414 else:
407 log.warn("Merge failed, not updating the pull request.")
415 log.warn("Merge failed, not updating the pull request.")
408 return merge_state
416 return merge_state
409
417
410 def _merge_pull_request(self, pull_request, user, extras):
418 def _merge_pull_request(self, pull_request, user, extras):
411 target_vcs = pull_request.target_repo.scm_instance()
419 target_vcs = pull_request.target_repo.scm_instance()
412 source_vcs = pull_request.source_repo.scm_instance()
420 source_vcs = pull_request.source_repo.scm_instance()
413 target_ref = self._refresh_reference(
421 target_ref = self._refresh_reference(
414 pull_request.target_ref_parts, target_vcs)
422 pull_request.target_ref_parts, target_vcs)
415
423
416 message = _(
424 message = _(
417 'Merge pull request #%(pr_id)s from '
425 'Merge pull request #%(pr_id)s from '
418 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
426 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
419 'pr_id': pull_request.pull_request_id,
427 'pr_id': pull_request.pull_request_id,
420 'source_repo': source_vcs.name,
428 'source_repo': source_vcs.name,
421 'source_ref_name': pull_request.source_ref_parts.name,
429 'source_ref_name': pull_request.source_ref_parts.name,
422 'pr_title': pull_request.title
430 'pr_title': pull_request.title
423 }
431 }
424
432
425 workspace_id = self._workspace_id(pull_request)
433 workspace_id = self._workspace_id(pull_request)
426 use_rebase = self._use_rebase_for_merging(pull_request)
434 use_rebase = self._use_rebase_for_merging(pull_request)
427
435
428 callback_daemon, extras = prepare_callback_daemon(
436 callback_daemon, extras = prepare_callback_daemon(
429 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
437 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
430 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
438 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
431
439
432 with callback_daemon:
440 with callback_daemon:
433 # TODO: johbo: Implement a clean way to run a config_override
441 # TODO: johbo: Implement a clean way to run a config_override
434 # for a single call.
442 # for a single call.
435 target_vcs.config.set(
443 target_vcs.config.set(
436 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
444 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
437 merge_state = target_vcs.merge(
445 merge_state = target_vcs.merge(
438 target_ref, source_vcs, pull_request.source_ref_parts,
446 target_ref, source_vcs, pull_request.source_ref_parts,
439 workspace_id, user_name=user.username,
447 workspace_id, user_name=user.username,
440 user_email=user.email, message=message, use_rebase=use_rebase)
448 user_email=user.email, message=message, use_rebase=use_rebase)
441 return merge_state
449 return merge_state
442
450
443 def _comment_and_close_pr(self, pull_request, user, merge_state):
451 def _comment_and_close_pr(self, pull_request, user, merge_state):
444 pull_request.merge_rev = merge_state.merge_commit_id
452 pull_request.merge_rev = merge_state.merge_commit_id
445 pull_request.updated_on = datetime.datetime.now()
453 pull_request.updated_on = datetime.datetime.now()
446
454
447 ChangesetCommentsModel().create(
455 ChangesetCommentsModel().create(
448 text=unicode(_('Pull request merged and closed')),
456 text=unicode(_('Pull request merged and closed')),
449 repo=pull_request.target_repo.repo_id,
457 repo=pull_request.target_repo.repo_id,
450 user=user.user_id,
458 user=user.user_id,
451 pull_request=pull_request.pull_request_id,
459 pull_request=pull_request.pull_request_id,
452 f_path=None,
460 f_path=None,
453 line_no=None,
461 line_no=None,
454 closing_pr=True
462 closing_pr=True
455 )
463 )
456
464
457 Session().add(pull_request)
465 Session().add(pull_request)
458 Session().flush()
466 Session().flush()
459 # TODO: paris: replace invalidation with less radical solution
467 # TODO: paris: replace invalidation with less radical solution
460 ScmModel().mark_for_invalidation(
468 ScmModel().mark_for_invalidation(
461 pull_request.target_repo.repo_name)
469 pull_request.target_repo.repo_name)
462 self._trigger_pull_request_hook(pull_request, user, 'merge')
470 self._trigger_pull_request_hook(pull_request, user, 'merge')
463
471
464 def has_valid_update_type(self, pull_request):
472 def has_valid_update_type(self, pull_request):
465 source_ref_type = pull_request.source_ref_parts.type
473 source_ref_type = pull_request.source_ref_parts.type
466 return source_ref_type in ['book', 'branch', 'tag']
474 return source_ref_type in ['book', 'branch', 'tag']
467
475
468 def update_commits(self, pull_request):
476 def update_commits(self, pull_request):
469 """
477 """
470 Get the updated list of commits for the pull request
478 Get the updated list of commits for the pull request
471 and return the new pull request version and the list
479 and return the new pull request version and the list
472 of commits processed by this update action
480 of commits processed by this update action
473 """
481 """
474
482
475 pull_request = self.__get_pull_request(pull_request)
483 pull_request = self.__get_pull_request(pull_request)
476 source_ref_type = pull_request.source_ref_parts.type
484 source_ref_type = pull_request.source_ref_parts.type
477 source_ref_name = pull_request.source_ref_parts.name
485 source_ref_name = pull_request.source_ref_parts.name
478 source_ref_id = pull_request.source_ref_parts.commit_id
486 source_ref_id = pull_request.source_ref_parts.commit_id
479
487
480 if not self.has_valid_update_type(pull_request):
488 if not self.has_valid_update_type(pull_request):
481 log.debug(
489 log.debug(
482 "Skipping update of pull request %s due to ref type: %s",
490 "Skipping update of pull request %s due to ref type: %s",
483 pull_request, source_ref_type)
491 pull_request, source_ref_type)
484 return (None, None)
492 return (None, None)
485
493
486 source_repo = pull_request.source_repo.scm_instance()
494 source_repo = pull_request.source_repo.scm_instance()
487 source_commit = source_repo.get_commit(commit_id=source_ref_name)
495 source_commit = source_repo.get_commit(commit_id=source_ref_name)
488 if source_ref_id == source_commit.raw_id:
496 if source_ref_id == source_commit.raw_id:
489 log.debug("Nothing changed in pull request %s", pull_request)
497 log.debug("Nothing changed in pull request %s", pull_request)
490 return (None, None)
498 return (None, None)
491
499
492 # Finally there is a need for an update
500 # Finally there is a need for an update
493 pull_request_version = self._create_version_from_snapshot(pull_request)
501 pull_request_version = self._create_version_from_snapshot(pull_request)
494 self._link_comments_to_version(pull_request_version)
502 self._link_comments_to_version(pull_request_version)
495
503
496 target_ref_type = pull_request.target_ref_parts.type
504 target_ref_type = pull_request.target_ref_parts.type
497 target_ref_name = pull_request.target_ref_parts.name
505 target_ref_name = pull_request.target_ref_parts.name
498 target_ref_id = pull_request.target_ref_parts.commit_id
506 target_ref_id = pull_request.target_ref_parts.commit_id
499 target_repo = pull_request.target_repo.scm_instance()
507 target_repo = pull_request.target_repo.scm_instance()
500
508
501 if target_ref_type in ('tag', 'branch', 'book'):
509 if target_ref_type in ('tag', 'branch', 'book'):
502 target_commit = target_repo.get_commit(target_ref_name)
510 target_commit = target_repo.get_commit(target_ref_name)
503 else:
511 else:
504 target_commit = target_repo.get_commit(target_ref_id)
512 target_commit = target_repo.get_commit(target_ref_id)
505
513
506 # re-compute commit ids
514 # re-compute commit ids
507 old_commit_ids = set(pull_request.revisions)
515 old_commit_ids = set(pull_request.revisions)
508 pre_load = ["author", "branch", "date", "message"]
516 pre_load = ["author", "branch", "date", "message"]
509 commit_ranges = target_repo.compare(
517 commit_ranges = target_repo.compare(
510 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
518 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
511 pre_load=pre_load)
519 pre_load=pre_load)
512
520
513 ancestor = target_repo.get_common_ancestor(
521 ancestor = target_repo.get_common_ancestor(
514 target_commit.raw_id, source_commit.raw_id, source_repo)
522 target_commit.raw_id, source_commit.raw_id, source_repo)
515
523
516 pull_request.source_ref = '%s:%s:%s' % (
524 pull_request.source_ref = '%s:%s:%s' % (
517 source_ref_type, source_ref_name, source_commit.raw_id)
525 source_ref_type, source_ref_name, source_commit.raw_id)
518 pull_request.target_ref = '%s:%s:%s' % (
526 pull_request.target_ref = '%s:%s:%s' % (
519 target_ref_type, target_ref_name, ancestor)
527 target_ref_type, target_ref_name, ancestor)
520 pull_request.revisions = [
528 pull_request.revisions = [
521 commit.raw_id for commit in reversed(commit_ranges)]
529 commit.raw_id for commit in reversed(commit_ranges)]
522 pull_request.updated_on = datetime.datetime.now()
530 pull_request.updated_on = datetime.datetime.now()
523 Session().add(pull_request)
531 Session().add(pull_request)
524 new_commit_ids = set(pull_request.revisions)
532 new_commit_ids = set(pull_request.revisions)
525
533
526 changes = self._calculate_commit_id_changes(
534 changes = self._calculate_commit_id_changes(
527 old_commit_ids, new_commit_ids)
535 old_commit_ids, new_commit_ids)
528
536
529 old_diff_data, new_diff_data = self._generate_update_diffs(
537 old_diff_data, new_diff_data = self._generate_update_diffs(
530 pull_request, pull_request_version)
538 pull_request, pull_request_version)
531
539
532 ChangesetCommentsModel().outdate_comments(
540 ChangesetCommentsModel().outdate_comments(
533 pull_request, old_diff_data=old_diff_data,
541 pull_request, old_diff_data=old_diff_data,
534 new_diff_data=new_diff_data)
542 new_diff_data=new_diff_data)
535
543
536 file_changes = self._calculate_file_changes(
544 file_changes = self._calculate_file_changes(
537 old_diff_data, new_diff_data)
545 old_diff_data, new_diff_data)
538
546
539 # Add an automatic comment to the pull request
547 # Add an automatic comment to the pull request
540 update_comment = ChangesetCommentsModel().create(
548 update_comment = ChangesetCommentsModel().create(
541 text=self._render_update_message(changes, file_changes),
549 text=self._render_update_message(changes, file_changes),
542 repo=pull_request.target_repo,
550 repo=pull_request.target_repo,
543 user=pull_request.author,
551 user=pull_request.author,
544 pull_request=pull_request,
552 pull_request=pull_request,
545 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
553 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
546
554
547 # Update status to "Under Review" for added commits
555 # Update status to "Under Review" for added commits
548 for commit_id in changes.added:
556 for commit_id in changes.added:
549 ChangesetStatusModel().set_status(
557 ChangesetStatusModel().set_status(
550 repo=pull_request.source_repo,
558 repo=pull_request.source_repo,
551 status=ChangesetStatus.STATUS_UNDER_REVIEW,
559 status=ChangesetStatus.STATUS_UNDER_REVIEW,
552 comment=update_comment,
560 comment=update_comment,
553 user=pull_request.author,
561 user=pull_request.author,
554 pull_request=pull_request,
562 pull_request=pull_request,
555 revision=commit_id)
563 revision=commit_id)
556
564
557 log.debug(
565 log.debug(
558 'Updated pull request %s, added_ids: %s, common_ids: %s, '
566 'Updated pull request %s, added_ids: %s, common_ids: %s, '
559 'removed_ids: %s', pull_request.pull_request_id,
567 'removed_ids: %s', pull_request.pull_request_id,
560 changes.added, changes.common, changes.removed)
568 changes.added, changes.common, changes.removed)
561 log.debug('Updated pull request with the following file changes: %s',
569 log.debug('Updated pull request with the following file changes: %s',
562 file_changes)
570 file_changes)
563
571
564 log.info(
572 log.info(
565 "Updated pull request %s from commit %s to commit %s, "
573 "Updated pull request %s from commit %s to commit %s, "
566 "stored new version %s of this pull request.",
574 "stored new version %s of this pull request.",
567 pull_request.pull_request_id, source_ref_id,
575 pull_request.pull_request_id, source_ref_id,
568 pull_request.source_ref_parts.commit_id,
576 pull_request.source_ref_parts.commit_id,
569 pull_request_version.pull_request_version_id)
577 pull_request_version.pull_request_version_id)
570 Session().commit()
578 Session().commit()
571 self._trigger_pull_request_hook(pull_request, pull_request.author,
579 self._trigger_pull_request_hook(pull_request, pull_request.author,
572 'update')
580 'update')
581
573 return (pull_request_version, changes)
582 return (pull_request_version, changes)
574
583
575 def _create_version_from_snapshot(self, pull_request):
584 def _create_version_from_snapshot(self, pull_request):
576 version = PullRequestVersion()
585 version = PullRequestVersion()
577 version.title = pull_request.title
586 version.title = pull_request.title
578 version.description = pull_request.description
587 version.description = pull_request.description
579 version.status = pull_request.status
588 version.status = pull_request.status
580 version.created_on = pull_request.created_on
589 version.created_on = pull_request.created_on
581 version.updated_on = pull_request.updated_on
590 version.updated_on = pull_request.updated_on
582 version.user_id = pull_request.user_id
591 version.user_id = pull_request.user_id
583 version.source_repo = pull_request.source_repo
592 version.source_repo = pull_request.source_repo
584 version.source_ref = pull_request.source_ref
593 version.source_ref = pull_request.source_ref
585 version.target_repo = pull_request.target_repo
594 version.target_repo = pull_request.target_repo
586 version.target_ref = pull_request.target_ref
595 version.target_ref = pull_request.target_ref
587
596
588 version._last_merge_source_rev = pull_request._last_merge_source_rev
597 version._last_merge_source_rev = pull_request._last_merge_source_rev
589 version._last_merge_target_rev = pull_request._last_merge_target_rev
598 version._last_merge_target_rev = pull_request._last_merge_target_rev
590 version._last_merge_status = pull_request._last_merge_status
599 version._last_merge_status = pull_request._last_merge_status
591 version.merge_rev = pull_request.merge_rev
600 version.merge_rev = pull_request.merge_rev
592
601
593 version.revisions = pull_request.revisions
602 version.revisions = pull_request.revisions
594 version.pull_request = pull_request
603 version.pull_request = pull_request
595 Session().add(version)
604 Session().add(version)
596 Session().flush()
605 Session().flush()
597
606
598 return version
607 return version
599
608
600 def _generate_update_diffs(self, pull_request, pull_request_version):
609 def _generate_update_diffs(self, pull_request, pull_request_version):
601 diff_context = (
610 diff_context = (
602 self.DIFF_CONTEXT +
611 self.DIFF_CONTEXT +
603 ChangesetCommentsModel.needed_extra_diff_context())
612 ChangesetCommentsModel.needed_extra_diff_context())
604 old_diff = self._get_diff_from_pr_or_version(
613 old_diff = self._get_diff_from_pr_or_version(
605 pull_request_version, context=diff_context)
614 pull_request_version, context=diff_context)
606 new_diff = self._get_diff_from_pr_or_version(
615 new_diff = self._get_diff_from_pr_or_version(
607 pull_request, context=diff_context)
616 pull_request, context=diff_context)
608
617
609 old_diff_data = diffs.DiffProcessor(old_diff)
618 old_diff_data = diffs.DiffProcessor(old_diff)
610 old_diff_data.prepare()
619 old_diff_data.prepare()
611 new_diff_data = diffs.DiffProcessor(new_diff)
620 new_diff_data = diffs.DiffProcessor(new_diff)
612 new_diff_data.prepare()
621 new_diff_data.prepare()
613
622
614 return old_diff_data, new_diff_data
623 return old_diff_data, new_diff_data
615
624
616 def _link_comments_to_version(self, pull_request_version):
625 def _link_comments_to_version(self, pull_request_version):
617 """
626 """
618 Link all unlinked comments of this pull request to the given version.
627 Link all unlinked comments of this pull request to the given version.
619
628
620 :param pull_request_version: The `PullRequestVersion` to which
629 :param pull_request_version: The `PullRequestVersion` to which
621 the comments shall be linked.
630 the comments shall be linked.
622
631
623 """
632 """
624 pull_request = pull_request_version.pull_request
633 pull_request = pull_request_version.pull_request
625 comments = ChangesetComment.query().filter(
634 comments = ChangesetComment.query().filter(
626 # TODO: johbo: Should we query for the repo at all here?
635 # TODO: johbo: Should we query for the repo at all here?
627 # Pending decision on how comments of PRs are to be related
636 # Pending decision on how comments of PRs are to be related
628 # to either the source repo, the target repo or no repo at all.
637 # to either the source repo, the target repo or no repo at all.
629 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
638 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
630 ChangesetComment.pull_request == pull_request,
639 ChangesetComment.pull_request == pull_request,
631 ChangesetComment.pull_request_version == None)
640 ChangesetComment.pull_request_version == None)
632
641
633 # TODO: johbo: Find out why this breaks if it is done in a bulk
642 # TODO: johbo: Find out why this breaks if it is done in a bulk
634 # operation.
643 # operation.
635 for comment in comments:
644 for comment in comments:
636 comment.pull_request_version_id = (
645 comment.pull_request_version_id = (
637 pull_request_version.pull_request_version_id)
646 pull_request_version.pull_request_version_id)
638 Session().add(comment)
647 Session().add(comment)
639
648
640 def _calculate_commit_id_changes(self, old_ids, new_ids):
649 def _calculate_commit_id_changes(self, old_ids, new_ids):
641 added = new_ids.difference(old_ids)
650 added = new_ids.difference(old_ids)
642 common = old_ids.intersection(new_ids)
651 common = old_ids.intersection(new_ids)
643 removed = old_ids.difference(new_ids)
652 removed = old_ids.difference(new_ids)
644 return ChangeTuple(added, common, removed)
653 return ChangeTuple(added, common, removed)
645
654
646 def _calculate_file_changes(self, old_diff_data, new_diff_data):
655 def _calculate_file_changes(self, old_diff_data, new_diff_data):
647
656
648 old_files = OrderedDict()
657 old_files = OrderedDict()
649 for diff_data in old_diff_data.parsed_diff:
658 for diff_data in old_diff_data.parsed_diff:
650 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
659 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
651
660
652 added_files = []
661 added_files = []
653 modified_files = []
662 modified_files = []
654 removed_files = []
663 removed_files = []
655 for diff_data in new_diff_data.parsed_diff:
664 for diff_data in new_diff_data.parsed_diff:
656 new_filename = diff_data['filename']
665 new_filename = diff_data['filename']
657 new_hash = md5_safe(diff_data['raw_diff'])
666 new_hash = md5_safe(diff_data['raw_diff'])
658
667
659 old_hash = old_files.get(new_filename)
668 old_hash = old_files.get(new_filename)
660 if not old_hash:
669 if not old_hash:
661 # file is not present in old diff, means it's added
670 # file is not present in old diff, means it's added
662 added_files.append(new_filename)
671 added_files.append(new_filename)
663 else:
672 else:
664 if new_hash != old_hash:
673 if new_hash != old_hash:
665 modified_files.append(new_filename)
674 modified_files.append(new_filename)
666 # now remove a file from old, since we have seen it already
675 # now remove a file from old, since we have seen it already
667 del old_files[new_filename]
676 del old_files[new_filename]
668
677
669 # removed files is when there are present in old, but not in NEW,
678 # removed files is when there are present in old, but not in NEW,
670 # since we remove old files that are present in new diff, left-overs
679 # since we remove old files that are present in new diff, left-overs
671 # if any should be the removed files
680 # if any should be the removed files
672 removed_files.extend(old_files.keys())
681 removed_files.extend(old_files.keys())
673
682
674 return FileChangeTuple(added_files, modified_files, removed_files)
683 return FileChangeTuple(added_files, modified_files, removed_files)
675
684
676 def _render_update_message(self, changes, file_changes):
685 def _render_update_message(self, changes, file_changes):
677 """
686 """
678 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
687 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
679 so it's always looking the same disregarding on which default
688 so it's always looking the same disregarding on which default
680 renderer system is using.
689 renderer system is using.
681
690
682 :param changes: changes named tuple
691 :param changes: changes named tuple
683 :param file_changes: file changes named tuple
692 :param file_changes: file changes named tuple
684
693
685 """
694 """
686 new_status = ChangesetStatus.get_status_lbl(
695 new_status = ChangesetStatus.get_status_lbl(
687 ChangesetStatus.STATUS_UNDER_REVIEW)
696 ChangesetStatus.STATUS_UNDER_REVIEW)
688
697
689 changed_files = (
698 changed_files = (
690 file_changes.added + file_changes.modified + file_changes.removed)
699 file_changes.added + file_changes.modified + file_changes.removed)
691
700
692 params = {
701 params = {
693 'under_review_label': new_status,
702 'under_review_label': new_status,
694 'added_commits': changes.added,
703 'added_commits': changes.added,
695 'removed_commits': changes.removed,
704 'removed_commits': changes.removed,
696 'changed_files': changed_files,
705 'changed_files': changed_files,
697 'added_files': file_changes.added,
706 'added_files': file_changes.added,
698 'modified_files': file_changes.modified,
707 'modified_files': file_changes.modified,
699 'removed_files': file_changes.removed,
708 'removed_files': file_changes.removed,
700 }
709 }
701 renderer = RstTemplateRenderer()
710 renderer = RstTemplateRenderer()
702 return renderer.render('pull_request_update.mako', **params)
711 return renderer.render('pull_request_update.mako', **params)
703
712
704 def edit(self, pull_request, title, description):
713 def edit(self, pull_request, title, description):
705 pull_request = self.__get_pull_request(pull_request)
714 pull_request = self.__get_pull_request(pull_request)
706 if pull_request.is_closed():
715 if pull_request.is_closed():
707 raise ValueError('This pull request is closed')
716 raise ValueError('This pull request is closed')
708 if title:
717 if title:
709 pull_request.title = title
718 pull_request.title = title
710 pull_request.description = description
719 pull_request.description = description
711 pull_request.updated_on = datetime.datetime.now()
720 pull_request.updated_on = datetime.datetime.now()
712 Session().add(pull_request)
721 Session().add(pull_request)
713
722
714 def update_reviewers(self, pull_request, reviewers_ids):
723 def update_reviewers(self, pull_request, reviewer_data):
715 reviewers_ids = set(reviewers_ids)
724 """
725 Update the reviewers in the pull request
726
727 :param pull_request: the pr to update
728 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
729 """
730
731 reviewers_reasons = {}
732 for user_id, reasons in reviewer_data:
733 if isinstance(user_id, (int, basestring)):
734 user_id = self._get_user(user_id).user_id
735 reviewers_reasons[user_id] = reasons
736
737 reviewers_ids = set(reviewers_reasons.keys())
716 pull_request = self.__get_pull_request(pull_request)
738 pull_request = self.__get_pull_request(pull_request)
717 current_reviewers = PullRequestReviewers.query()\
739 current_reviewers = PullRequestReviewers.query()\
718 .filter(PullRequestReviewers.pull_request ==
740 .filter(PullRequestReviewers.pull_request ==
719 pull_request).all()
741 pull_request).all()
720 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
742 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
721
743
722 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
744 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
723 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
745 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
724
746
725 log.debug("Adding %s reviewers", ids_to_add)
747 log.debug("Adding %s reviewers", ids_to_add)
726 log.debug("Removing %s reviewers", ids_to_remove)
748 log.debug("Removing %s reviewers", ids_to_remove)
727 changed = False
749 changed = False
728 for uid in ids_to_add:
750 for uid in ids_to_add:
729 changed = True
751 changed = True
730 _usr = self._get_user(uid)
752 _usr = self._get_user(uid)
731 reviewer = PullRequestReviewers(_usr, pull_request)
753 reasons = reviewers_reasons[uid]
754 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
732 Session().add(reviewer)
755 Session().add(reviewer)
733
756
734 self.notify_reviewers(pull_request, ids_to_add)
757 self.notify_reviewers(pull_request, ids_to_add)
735
758
736 for uid in ids_to_remove:
759 for uid in ids_to_remove:
737 changed = True
760 changed = True
738 reviewer = PullRequestReviewers.query()\
761 reviewer = PullRequestReviewers.query()\
739 .filter(PullRequestReviewers.user_id == uid,
762 .filter(PullRequestReviewers.user_id == uid,
740 PullRequestReviewers.pull_request == pull_request)\
763 PullRequestReviewers.pull_request == pull_request)\
741 .scalar()
764 .scalar()
742 if reviewer:
765 if reviewer:
743 Session().delete(reviewer)
766 Session().delete(reviewer)
744 if changed:
767 if changed:
745 pull_request.updated_on = datetime.datetime.now()
768 pull_request.updated_on = datetime.datetime.now()
746 Session().add(pull_request)
769 Session().add(pull_request)
747
770
748 return ids_to_add, ids_to_remove
771 return ids_to_add, ids_to_remove
749
772
750 def get_url(self, pull_request):
773 def get_url(self, pull_request):
751 return h.url('pullrequest_show',
774 return h.url('pullrequest_show',
752 repo_name=safe_str(pull_request.target_repo.repo_name),
775 repo_name=safe_str(pull_request.target_repo.repo_name),
753 pull_request_id=pull_request.pull_request_id,
776 pull_request_id=pull_request.pull_request_id,
754 qualified=True)
777 qualified=True)
755
778
756 def notify_reviewers(self, pull_request, reviewers_ids):
779 def notify_reviewers(self, pull_request, reviewers_ids):
757 # notification to reviewers
780 # notification to reviewers
758 if not reviewers_ids:
781 if not reviewers_ids:
759 return
782 return
760
783
761 pull_request_obj = pull_request
784 pull_request_obj = pull_request
762 # get the current participants of this pull request
785 # get the current participants of this pull request
763 recipients = reviewers_ids
786 recipients = reviewers_ids
764 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
787 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
765
788
766 pr_source_repo = pull_request_obj.source_repo
789 pr_source_repo = pull_request_obj.source_repo
767 pr_target_repo = pull_request_obj.target_repo
790 pr_target_repo = pull_request_obj.target_repo
768
791
769 pr_url = h.url(
792 pr_url = h.url(
770 'pullrequest_show',
793 'pullrequest_show',
771 repo_name=pr_target_repo.repo_name,
794 repo_name=pr_target_repo.repo_name,
772 pull_request_id=pull_request_obj.pull_request_id,
795 pull_request_id=pull_request_obj.pull_request_id,
773 qualified=True,)
796 qualified=True,)
774
797
775 # set some variables for email notification
798 # set some variables for email notification
776 pr_target_repo_url = h.url(
799 pr_target_repo_url = h.url(
777 'summary_home',
800 'summary_home',
778 repo_name=pr_target_repo.repo_name,
801 repo_name=pr_target_repo.repo_name,
779 qualified=True)
802 qualified=True)
780
803
781 pr_source_repo_url = h.url(
804 pr_source_repo_url = h.url(
782 'summary_home',
805 'summary_home',
783 repo_name=pr_source_repo.repo_name,
806 repo_name=pr_source_repo.repo_name,
784 qualified=True)
807 qualified=True)
785
808
786 # pull request specifics
809 # pull request specifics
787 pull_request_commits = [
810 pull_request_commits = [
788 (x.raw_id, x.message)
811 (x.raw_id, x.message)
789 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
812 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
790
813
791 kwargs = {
814 kwargs = {
792 'user': pull_request.author,
815 'user': pull_request.author,
793 'pull_request': pull_request_obj,
816 'pull_request': pull_request_obj,
794 'pull_request_commits': pull_request_commits,
817 'pull_request_commits': pull_request_commits,
795
818
796 'pull_request_target_repo': pr_target_repo,
819 'pull_request_target_repo': pr_target_repo,
797 'pull_request_target_repo_url': pr_target_repo_url,
820 'pull_request_target_repo_url': pr_target_repo_url,
798
821
799 'pull_request_source_repo': pr_source_repo,
822 'pull_request_source_repo': pr_source_repo,
800 'pull_request_source_repo_url': pr_source_repo_url,
823 'pull_request_source_repo_url': pr_source_repo_url,
801
824
802 'pull_request_url': pr_url,
825 'pull_request_url': pr_url,
803 }
826 }
804
827
805 # pre-generate the subject for notification itself
828 # pre-generate the subject for notification itself
806 (subject,
829 (subject,
807 _h, _e, # we don't care about those
830 _h, _e, # we don't care about those
808 body_plaintext) = EmailNotificationModel().render_email(
831 body_plaintext) = EmailNotificationModel().render_email(
809 notification_type, **kwargs)
832 notification_type, **kwargs)
810
833
811 # create notification objects, and emails
834 # create notification objects, and emails
812 NotificationModel().create(
835 NotificationModel().create(
813 created_by=pull_request.author,
836 created_by=pull_request.author,
814 notification_subject=subject,
837 notification_subject=subject,
815 notification_body=body_plaintext,
838 notification_body=body_plaintext,
816 notification_type=notification_type,
839 notification_type=notification_type,
817 recipients=recipients,
840 recipients=recipients,
818 email_kwargs=kwargs,
841 email_kwargs=kwargs,
819 )
842 )
820
843
821 def delete(self, pull_request):
844 def delete(self, pull_request):
822 pull_request = self.__get_pull_request(pull_request)
845 pull_request = self.__get_pull_request(pull_request)
823 self._cleanup_merge_workspace(pull_request)
846 self._cleanup_merge_workspace(pull_request)
824 Session().delete(pull_request)
847 Session().delete(pull_request)
825
848
826 def close_pull_request(self, pull_request, user):
849 def close_pull_request(self, pull_request, user):
827 pull_request = self.__get_pull_request(pull_request)
850 pull_request = self.__get_pull_request(pull_request)
828 self._cleanup_merge_workspace(pull_request)
851 self._cleanup_merge_workspace(pull_request)
829 pull_request.status = PullRequest.STATUS_CLOSED
852 pull_request.status = PullRequest.STATUS_CLOSED
830 pull_request.updated_on = datetime.datetime.now()
853 pull_request.updated_on = datetime.datetime.now()
831 Session().add(pull_request)
854 Session().add(pull_request)
832 self._trigger_pull_request_hook(
855 self._trigger_pull_request_hook(
833 pull_request, pull_request.author, 'close')
856 pull_request, pull_request.author, 'close')
834 self._log_action('user_closed_pull_request', user, pull_request)
857 self._log_action('user_closed_pull_request', user, pull_request)
835
858
836 def close_pull_request_with_comment(self, pull_request, user, repo,
859 def close_pull_request_with_comment(self, pull_request, user, repo,
837 message=None):
860 message=None):
838 status = ChangesetStatus.STATUS_REJECTED
861 status = ChangesetStatus.STATUS_REJECTED
839
862
840 if not message:
863 if not message:
841 message = (
864 message = (
842 _('Status change %(transition_icon)s %(status)s') % {
865 _('Status change %(transition_icon)s %(status)s') % {
843 'transition_icon': '>',
866 'transition_icon': '>',
844 'status': ChangesetStatus.get_status_lbl(status)})
867 'status': ChangesetStatus.get_status_lbl(status)})
845
868
846 internal_message = _('Closing with') + ' ' + message
869 internal_message = _('Closing with') + ' ' + message
847
870
848 comm = ChangesetCommentsModel().create(
871 comm = ChangesetCommentsModel().create(
849 text=internal_message,
872 text=internal_message,
850 repo=repo.repo_id,
873 repo=repo.repo_id,
851 user=user.user_id,
874 user=user.user_id,
852 pull_request=pull_request.pull_request_id,
875 pull_request=pull_request.pull_request_id,
853 f_path=None,
876 f_path=None,
854 line_no=None,
877 line_no=None,
855 status_change=ChangesetStatus.get_status_lbl(status),
878 status_change=ChangesetStatus.get_status_lbl(status),
856 status_change_type=status,
879 status_change_type=status,
857 closing_pr=True
880 closing_pr=True
858 )
881 )
859
882
860 ChangesetStatusModel().set_status(
883 ChangesetStatusModel().set_status(
861 repo.repo_id,
884 repo.repo_id,
862 status,
885 status,
863 user.user_id,
886 user.user_id,
864 comm,
887 comm,
865 pull_request=pull_request.pull_request_id
888 pull_request=pull_request.pull_request_id
866 )
889 )
867 Session().flush()
890 Session().flush()
868
891
869 PullRequestModel().close_pull_request(
892 PullRequestModel().close_pull_request(
870 pull_request.pull_request_id, user)
893 pull_request.pull_request_id, user)
871
894
872 def merge_status(self, pull_request):
895 def merge_status(self, pull_request):
873 if not self._is_merge_enabled(pull_request):
896 if not self._is_merge_enabled(pull_request):
874 return False, _('Server-side pull request merging is disabled.')
897 return False, _('Server-side pull request merging is disabled.')
875 if pull_request.is_closed():
898 if pull_request.is_closed():
876 return False, _('This pull request is closed.')
899 return False, _('This pull request is closed.')
877 merge_possible, msg = self._check_repo_requirements(
900 merge_possible, msg = self._check_repo_requirements(
878 target=pull_request.target_repo, source=pull_request.source_repo)
901 target=pull_request.target_repo, source=pull_request.source_repo)
879 if not merge_possible:
902 if not merge_possible:
880 return merge_possible, msg
903 return merge_possible, msg
881
904
882 try:
905 try:
883 resp = self._try_merge(pull_request)
906 resp = self._try_merge(pull_request)
884 status = resp.possible, self.merge_status_message(
907 status = resp.possible, self.merge_status_message(
885 resp.failure_reason)
908 resp.failure_reason)
886 except NotImplementedError:
909 except NotImplementedError:
887 status = False, _('Pull request merging is not supported.')
910 status = False, _('Pull request merging is not supported.')
888
911
889 return status
912 return status
890
913
891 def _check_repo_requirements(self, target, source):
914 def _check_repo_requirements(self, target, source):
892 """
915 """
893 Check if `target` and `source` have compatible requirements.
916 Check if `target` and `source` have compatible requirements.
894
917
895 Currently this is just checking for largefiles.
918 Currently this is just checking for largefiles.
896 """
919 """
897 target_has_largefiles = self._has_largefiles(target)
920 target_has_largefiles = self._has_largefiles(target)
898 source_has_largefiles = self._has_largefiles(source)
921 source_has_largefiles = self._has_largefiles(source)
899 merge_possible = True
922 merge_possible = True
900 message = u''
923 message = u''
901
924
902 if target_has_largefiles != source_has_largefiles:
925 if target_has_largefiles != source_has_largefiles:
903 merge_possible = False
926 merge_possible = False
904 if source_has_largefiles:
927 if source_has_largefiles:
905 message = _(
928 message = _(
906 'Target repository large files support is disabled.')
929 'Target repository large files support is disabled.')
907 else:
930 else:
908 message = _(
931 message = _(
909 'Source repository large files support is disabled.')
932 'Source repository large files support is disabled.')
910
933
911 return merge_possible, message
934 return merge_possible, message
912
935
913 def _has_largefiles(self, repo):
936 def _has_largefiles(self, repo):
914 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
937 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
915 'extensions', 'largefiles')
938 'extensions', 'largefiles')
916 return largefiles_ui and largefiles_ui[0].active
939 return largefiles_ui and largefiles_ui[0].active
917
940
918 def _try_merge(self, pull_request):
941 def _try_merge(self, pull_request):
919 """
942 """
920 Try to merge the pull request and return the merge status.
943 Try to merge the pull request and return the merge status.
921 """
944 """
922 log.debug(
945 log.debug(
923 "Trying out if the pull request %s can be merged.",
946 "Trying out if the pull request %s can be merged.",
924 pull_request.pull_request_id)
947 pull_request.pull_request_id)
925 target_vcs = pull_request.target_repo.scm_instance()
948 target_vcs = pull_request.target_repo.scm_instance()
926 target_ref = self._refresh_reference(
949 target_ref = self._refresh_reference(
927 pull_request.target_ref_parts, target_vcs)
950 pull_request.target_ref_parts, target_vcs)
928
951
929 target_locked = pull_request.target_repo.locked
952 target_locked = pull_request.target_repo.locked
930 if target_locked and target_locked[0]:
953 if target_locked and target_locked[0]:
931 log.debug("The target repository is locked.")
954 log.debug("The target repository is locked.")
932 merge_state = MergeResponse(
955 merge_state = MergeResponse(
933 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
956 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
934 elif self._needs_merge_state_refresh(pull_request, target_ref):
957 elif self._needs_merge_state_refresh(pull_request, target_ref):
935 log.debug("Refreshing the merge status of the repository.")
958 log.debug("Refreshing the merge status of the repository.")
936 merge_state = self._refresh_merge_state(
959 merge_state = self._refresh_merge_state(
937 pull_request, target_vcs, target_ref)
960 pull_request, target_vcs, target_ref)
938 else:
961 else:
939 possible = pull_request.\
962 possible = pull_request.\
940 _last_merge_status == MergeFailureReason.NONE
963 _last_merge_status == MergeFailureReason.NONE
941 merge_state = MergeResponse(
964 merge_state = MergeResponse(
942 possible, False, None, pull_request._last_merge_status)
965 possible, False, None, pull_request._last_merge_status)
943 log.debug("Merge response: %s", merge_state)
966 log.debug("Merge response: %s", merge_state)
944 return merge_state
967 return merge_state
945
968
946 def _refresh_reference(self, reference, vcs_repository):
969 def _refresh_reference(self, reference, vcs_repository):
947 if reference.type in ('branch', 'book'):
970 if reference.type in ('branch', 'book'):
948 name_or_id = reference.name
971 name_or_id = reference.name
949 else:
972 else:
950 name_or_id = reference.commit_id
973 name_or_id = reference.commit_id
951 refreshed_commit = vcs_repository.get_commit(name_or_id)
974 refreshed_commit = vcs_repository.get_commit(name_or_id)
952 refreshed_reference = Reference(
975 refreshed_reference = Reference(
953 reference.type, reference.name, refreshed_commit.raw_id)
976 reference.type, reference.name, refreshed_commit.raw_id)
954 return refreshed_reference
977 return refreshed_reference
955
978
956 def _needs_merge_state_refresh(self, pull_request, target_reference):
979 def _needs_merge_state_refresh(self, pull_request, target_reference):
957 return not(
980 return not(
958 pull_request.revisions and
981 pull_request.revisions and
959 pull_request.revisions[0] == pull_request._last_merge_source_rev and
982 pull_request.revisions[0] == pull_request._last_merge_source_rev and
960 target_reference.commit_id == pull_request._last_merge_target_rev)
983 target_reference.commit_id == pull_request._last_merge_target_rev)
961
984
962 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
985 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
963 workspace_id = self._workspace_id(pull_request)
986 workspace_id = self._workspace_id(pull_request)
964 source_vcs = pull_request.source_repo.scm_instance()
987 source_vcs = pull_request.source_repo.scm_instance()
965 use_rebase = self._use_rebase_for_merging(pull_request)
988 use_rebase = self._use_rebase_for_merging(pull_request)
966 merge_state = target_vcs.merge(
989 merge_state = target_vcs.merge(
967 target_reference, source_vcs, pull_request.source_ref_parts,
990 target_reference, source_vcs, pull_request.source_ref_parts,
968 workspace_id, dry_run=True, use_rebase=use_rebase)
991 workspace_id, dry_run=True, use_rebase=use_rebase)
969
992
970 # Do not store the response if there was an unknown error.
993 # Do not store the response if there was an unknown error.
971 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
994 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
972 pull_request._last_merge_source_rev = pull_request.\
995 pull_request._last_merge_source_rev = pull_request.\
973 source_ref_parts.commit_id
996 source_ref_parts.commit_id
974 pull_request._last_merge_target_rev = target_reference.commit_id
997 pull_request._last_merge_target_rev = target_reference.commit_id
975 pull_request._last_merge_status = (
998 pull_request._last_merge_status = (
976 merge_state.failure_reason)
999 merge_state.failure_reason)
977 Session().add(pull_request)
1000 Session().add(pull_request)
978 Session().flush()
1001 Session().flush()
979
1002
980 return merge_state
1003 return merge_state
981
1004
982 def _workspace_id(self, pull_request):
1005 def _workspace_id(self, pull_request):
983 workspace_id = 'pr-%s' % pull_request.pull_request_id
1006 workspace_id = 'pr-%s' % pull_request.pull_request_id
984 return workspace_id
1007 return workspace_id
985
1008
986 def merge_status_message(self, status_code):
1009 def merge_status_message(self, status_code):
987 """
1010 """
988 Return a human friendly error message for the given merge status code.
1011 Return a human friendly error message for the given merge status code.
989 """
1012 """
990 return self.MERGE_STATUS_MESSAGES[status_code]
1013 return self.MERGE_STATUS_MESSAGES[status_code]
991
1014
992 def generate_repo_data(self, repo, commit_id=None, branch=None,
1015 def generate_repo_data(self, repo, commit_id=None, branch=None,
993 bookmark=None):
1016 bookmark=None):
994 all_refs, selected_ref = \
1017 all_refs, selected_ref = \
995 self._get_repo_pullrequest_sources(
1018 self._get_repo_pullrequest_sources(
996 repo.scm_instance(), commit_id=commit_id,
1019 repo.scm_instance(), commit_id=commit_id,
997 branch=branch, bookmark=bookmark)
1020 branch=branch, bookmark=bookmark)
998
1021
999 refs_select2 = []
1022 refs_select2 = []
1000 for element in all_refs:
1023 for element in all_refs:
1001 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1024 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1002 refs_select2.append({'text': element[1], 'children': children})
1025 refs_select2.append({'text': element[1], 'children': children})
1003
1026
1004 return {
1027 return {
1005 'user': {
1028 'user': {
1006 'user_id': repo.user.user_id,
1029 'user_id': repo.user.user_id,
1007 'username': repo.user.username,
1030 'username': repo.user.username,
1008 'firstname': repo.user.firstname,
1031 'firstname': repo.user.firstname,
1009 'lastname': repo.user.lastname,
1032 'lastname': repo.user.lastname,
1010 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1033 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1011 },
1034 },
1012 'description': h.chop_at_smart(repo.description, '\n'),
1035 'description': h.chop_at_smart(repo.description, '\n'),
1013 'refs': {
1036 'refs': {
1014 'all_refs': all_refs,
1037 'all_refs': all_refs,
1015 'selected_ref': selected_ref,
1038 'selected_ref': selected_ref,
1016 'select2_refs': refs_select2
1039 'select2_refs': refs_select2
1017 }
1040 }
1018 }
1041 }
1019
1042
1020 def generate_pullrequest_title(self, source, source_ref, target):
1043 def generate_pullrequest_title(self, source, source_ref, target):
1021 return u'{source}#{at_ref} to {target}'.format(
1044 return u'{source}#{at_ref} to {target}'.format(
1022 source=source,
1045 source=source,
1023 at_ref=source_ref,
1046 at_ref=source_ref,
1024 target=target,
1047 target=target,
1025 )
1048 )
1026
1049
1027 def _cleanup_merge_workspace(self, pull_request):
1050 def _cleanup_merge_workspace(self, pull_request):
1028 # Merging related cleanup
1051 # Merging related cleanup
1029 target_scm = pull_request.target_repo.scm_instance()
1052 target_scm = pull_request.target_repo.scm_instance()
1030 workspace_id = 'pr-%s' % pull_request.pull_request_id
1053 workspace_id = 'pr-%s' % pull_request.pull_request_id
1031
1054
1032 try:
1055 try:
1033 target_scm.cleanup_merge_workspace(workspace_id)
1056 target_scm.cleanup_merge_workspace(workspace_id)
1034 except NotImplementedError:
1057 except NotImplementedError:
1035 pass
1058 pass
1036
1059
1037 def _get_repo_pullrequest_sources(
1060 def _get_repo_pullrequest_sources(
1038 self, repo, commit_id=None, branch=None, bookmark=None):
1061 self, repo, commit_id=None, branch=None, bookmark=None):
1039 """
1062 """
1040 Return a structure with repo's interesting commits, suitable for
1063 Return a structure with repo's interesting commits, suitable for
1041 the selectors in pullrequest controller
1064 the selectors in pullrequest controller
1042
1065
1043 :param commit_id: a commit that must be in the list somehow
1066 :param commit_id: a commit that must be in the list somehow
1044 and selected by default
1067 and selected by default
1045 :param branch: a branch that must be in the list and selected
1068 :param branch: a branch that must be in the list and selected
1046 by default - even if closed
1069 by default - even if closed
1047 :param bookmark: a bookmark that must be in the list and selected
1070 :param bookmark: a bookmark that must be in the list and selected
1048 """
1071 """
1049
1072
1050 commit_id = safe_str(commit_id) if commit_id else None
1073 commit_id = safe_str(commit_id) if commit_id else None
1051 branch = safe_str(branch) if branch else None
1074 branch = safe_str(branch) if branch else None
1052 bookmark = safe_str(bookmark) if bookmark else None
1075 bookmark = safe_str(bookmark) if bookmark else None
1053
1076
1054 selected = None
1077 selected = None
1055
1078
1056 # order matters: first source that has commit_id in it will be selected
1079 # order matters: first source that has commit_id in it will be selected
1057 sources = []
1080 sources = []
1058 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1081 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1059 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1082 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1060
1083
1061 if commit_id:
1084 if commit_id:
1062 ref_commit = (h.short_id(commit_id), commit_id)
1085 ref_commit = (h.short_id(commit_id), commit_id)
1063 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1086 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1064
1087
1065 sources.append(
1088 sources.append(
1066 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1089 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1067 )
1090 )
1068
1091
1069 groups = []
1092 groups = []
1070 for group_key, ref_list, group_name, match in sources:
1093 for group_key, ref_list, group_name, match in sources:
1071 group_refs = []
1094 group_refs = []
1072 for ref_name, ref_id in ref_list:
1095 for ref_name, ref_id in ref_list:
1073 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1096 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1074 group_refs.append((ref_key, ref_name))
1097 group_refs.append((ref_key, ref_name))
1075
1098
1076 if not selected:
1099 if not selected:
1077 if set([commit_id, match]) & set([ref_id, ref_name]):
1100 if set([commit_id, match]) & set([ref_id, ref_name]):
1078 selected = ref_key
1101 selected = ref_key
1079
1102
1080 if group_refs:
1103 if group_refs:
1081 groups.append((group_refs, group_name))
1104 groups.append((group_refs, group_name))
1082
1105
1083 if not selected:
1106 if not selected:
1084 ref = commit_id or branch or bookmark
1107 ref = commit_id or branch or bookmark
1085 if ref:
1108 if ref:
1086 raise CommitDoesNotExistError(
1109 raise CommitDoesNotExistError(
1087 'No commit refs could be found matching: %s' % ref)
1110 'No commit refs could be found matching: %s' % ref)
1088 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1111 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1089 selected = 'branch:%s:%s' % (
1112 selected = 'branch:%s:%s' % (
1090 repo.DEFAULT_BRANCH_NAME,
1113 repo.DEFAULT_BRANCH_NAME,
1091 repo.branches[repo.DEFAULT_BRANCH_NAME]
1114 repo.branches[repo.DEFAULT_BRANCH_NAME]
1092 )
1115 )
1093 elif repo.commit_ids:
1116 elif repo.commit_ids:
1094 rev = repo.commit_ids[0]
1117 rev = repo.commit_ids[0]
1095 selected = 'rev:%s:%s' % (rev, rev)
1118 selected = 'rev:%s:%s' % (rev, rev)
1096 else:
1119 else:
1097 raise EmptyRepositoryError()
1120 raise EmptyRepositoryError()
1098 return groups, selected
1121 return groups, selected
1099
1122
1100 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1123 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1101 pull_request = self.__get_pull_request(pull_request)
1124 pull_request = self.__get_pull_request(pull_request)
1102 return self._get_diff_from_pr_or_version(pull_request, context=context)
1125 return self._get_diff_from_pr_or_version(pull_request, context=context)
1103
1126
1104 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1127 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1105 source_repo = pr_or_version.source_repo
1128 source_repo = pr_or_version.source_repo
1106
1129
1107 # we swap org/other ref since we run a simple diff on one repo
1130 # we swap org/other ref since we run a simple diff on one repo
1108 target_ref_id = pr_or_version.target_ref_parts.commit_id
1131 target_ref_id = pr_or_version.target_ref_parts.commit_id
1109 source_ref_id = pr_or_version.source_ref_parts.commit_id
1132 source_ref_id = pr_or_version.source_ref_parts.commit_id
1110 target_commit = source_repo.get_commit(
1133 target_commit = source_repo.get_commit(
1111 commit_id=safe_str(target_ref_id))
1134 commit_id=safe_str(target_ref_id))
1112 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1135 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1113 vcs_repo = source_repo.scm_instance()
1136 vcs_repo = source_repo.scm_instance()
1114
1137
1115 # TODO: johbo: In the context of an update, we cannot reach
1138 # TODO: johbo: In the context of an update, we cannot reach
1116 # the old commit anymore with our normal mechanisms. It needs
1139 # the old commit anymore with our normal mechanisms. It needs
1117 # some sort of special support in the vcs layer to avoid this
1140 # some sort of special support in the vcs layer to avoid this
1118 # workaround.
1141 # workaround.
1119 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1142 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1120 vcs_repo.alias == 'git'):
1143 vcs_repo.alias == 'git'):
1121 source_commit.raw_id = safe_str(source_ref_id)
1144 source_commit.raw_id = safe_str(source_ref_id)
1122
1145
1123 log.debug('calculating diff between '
1146 log.debug('calculating diff between '
1124 'source_ref:%s and target_ref:%s for repo `%s`',
1147 'source_ref:%s and target_ref:%s for repo `%s`',
1125 target_ref_id, source_ref_id,
1148 target_ref_id, source_ref_id,
1126 safe_unicode(vcs_repo.path))
1149 safe_unicode(vcs_repo.path))
1127
1150
1128 vcs_diff = vcs_repo.get_diff(
1151 vcs_diff = vcs_repo.get_diff(
1129 commit1=target_commit, commit2=source_commit, context=context)
1152 commit1=target_commit, commit2=source_commit, context=context)
1130 return vcs_diff
1153 return vcs_diff
1131
1154
1132 def _is_merge_enabled(self, pull_request):
1155 def _is_merge_enabled(self, pull_request):
1133 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1156 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1134 settings = settings_model.get_general_settings()
1157 settings = settings_model.get_general_settings()
1135 return settings.get('rhodecode_pr_merge_enabled', False)
1158 return settings.get('rhodecode_pr_merge_enabled', False)
1136
1159
1137 def _use_rebase_for_merging(self, pull_request):
1160 def _use_rebase_for_merging(self, pull_request):
1138 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1161 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1139 settings = settings_model.get_general_settings()
1162 settings = settings_model.get_general_settings()
1140 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1163 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1141
1164
1142 def _log_action(self, action, user, pull_request):
1165 def _log_action(self, action, user, pull_request):
1143 action_logger(
1166 action_logger(
1144 user,
1167 user,
1145 '{action}:{pr_id}'.format(
1168 '{action}:{pr_id}'.format(
1146 action=action, pr_id=pull_request.pull_request_id),
1169 action=action, pr_id=pull_request.pull_request_id),
1147 pull_request.target_repo)
1170 pull_request.target_repo)
1148
1171
1149
1172
1150 ChangeTuple = namedtuple('ChangeTuple',
1173 ChangeTuple = namedtuple('ChangeTuple',
1151 ['added', 'common', 'removed'])
1174 ['added', 'common', 'removed'])
1152
1175
1153 FileChangeTuple = namedtuple('FileChangeTuple',
1176 FileChangeTuple = namedtuple('FileChangeTuple',
1154 ['added', 'modified', 'removed'])
1177 ['added', 'modified', 'removed'])
@@ -1,2167 +1,2170 b''
1 //Primary CSS
1 //Primary CSS
2
2
3 //--- IMPORTS ------------------//
3 //--- IMPORTS ------------------//
4
4
5 @import 'helpers';
5 @import 'helpers';
6 @import 'mixins';
6 @import 'mixins';
7 @import 'rcicons';
7 @import 'rcicons';
8 @import 'fonts';
8 @import 'fonts';
9 @import 'variables';
9 @import 'variables';
10 @import 'bootstrap-variables';
10 @import 'bootstrap-variables';
11 @import 'form-bootstrap';
11 @import 'form-bootstrap';
12 @import 'codemirror';
12 @import 'codemirror';
13 @import 'legacy_code_styles';
13 @import 'legacy_code_styles';
14 @import 'progress-bar';
14 @import 'progress-bar';
15
15
16 @import 'type';
16 @import 'type';
17 @import 'alerts';
17 @import 'alerts';
18 @import 'buttons';
18 @import 'buttons';
19 @import 'tags';
19 @import 'tags';
20 @import 'code-block';
20 @import 'code-block';
21 @import 'examples';
21 @import 'examples';
22 @import 'login';
22 @import 'login';
23 @import 'main-content';
23 @import 'main-content';
24 @import 'select2';
24 @import 'select2';
25 @import 'comments';
25 @import 'comments';
26 @import 'panels-bootstrap';
26 @import 'panels-bootstrap';
27 @import 'panels';
27 @import 'panels';
28 @import 'deform';
28 @import 'deform';
29
29
30 //--- BASE ------------------//
30 //--- BASE ------------------//
31 .noscript-error {
31 .noscript-error {
32 top: 0;
32 top: 0;
33 left: 0;
33 left: 0;
34 width: 100%;
34 width: 100%;
35 z-index: 101;
35 z-index: 101;
36 text-align: center;
36 text-align: center;
37 font-family: @text-semibold;
37 font-family: @text-semibold;
38 font-size: 120%;
38 font-size: 120%;
39 color: white;
39 color: white;
40 background-color: @alert2;
40 background-color: @alert2;
41 padding: 5px 0 5px 0;
41 padding: 5px 0 5px 0;
42 }
42 }
43
43
44 html {
44 html {
45 display: table;
45 display: table;
46 height: 100%;
46 height: 100%;
47 width: 100%;
47 width: 100%;
48 }
48 }
49
49
50 body {
50 body {
51 display: table-cell;
51 display: table-cell;
52 width: 100%;
52 width: 100%;
53 }
53 }
54
54
55 //--- LAYOUT ------------------//
55 //--- LAYOUT ------------------//
56
56
57 .hidden{
57 .hidden{
58 display: none !important;
58 display: none !important;
59 }
59 }
60
60
61 .box{
61 .box{
62 float: left;
62 float: left;
63 width: 100%;
63 width: 100%;
64 }
64 }
65
65
66 .browser-header {
66 .browser-header {
67 clear: both;
67 clear: both;
68 }
68 }
69 .main {
69 .main {
70 clear: both;
70 clear: both;
71 padding:0 0 @pagepadding;
71 padding:0 0 @pagepadding;
72 height: auto;
72 height: auto;
73
73
74 &:after { //clearfix
74 &:after { //clearfix
75 content:"";
75 content:"";
76 clear:both;
76 clear:both;
77 width:100%;
77 width:100%;
78 display:block;
78 display:block;
79 }
79 }
80 }
80 }
81
81
82 .action-link{
82 .action-link{
83 margin-left: @padding;
83 margin-left: @padding;
84 padding-left: @padding;
84 padding-left: @padding;
85 border-left: @border-thickness solid @border-default-color;
85 border-left: @border-thickness solid @border-default-color;
86 }
86 }
87
87
88 input + .action-link, .action-link.first{
88 input + .action-link, .action-link.first{
89 border-left: none;
89 border-left: none;
90 }
90 }
91
91
92 .action-link.last{
92 .action-link.last{
93 margin-right: @padding;
93 margin-right: @padding;
94 padding-right: @padding;
94 padding-right: @padding;
95 }
95 }
96
96
97 .action-link.active,
97 .action-link.active,
98 .action-link.active a{
98 .action-link.active a{
99 color: @grey4;
99 color: @grey4;
100 }
100 }
101
101
102 ul.simple-list{
102 ul.simple-list{
103 list-style: none;
103 list-style: none;
104 margin: 0;
104 margin: 0;
105 padding: 0;
105 padding: 0;
106 }
106 }
107
107
108 .main-content {
108 .main-content {
109 padding-bottom: @pagepadding;
109 padding-bottom: @pagepadding;
110 }
110 }
111
111
112 .wrapper {
112 .wrapper {
113 position: relative;
113 position: relative;
114 max-width: @wrapper-maxwidth;
114 max-width: @wrapper-maxwidth;
115 margin: 0 auto;
115 margin: 0 auto;
116 }
116 }
117
117
118 #content {
118 #content {
119 clear: both;
119 clear: both;
120 padding: 0 @contentpadding;
120 padding: 0 @contentpadding;
121 }
121 }
122
122
123 .advanced-settings-fields{
123 .advanced-settings-fields{
124 input{
124 input{
125 margin-left: @textmargin;
125 margin-left: @textmargin;
126 margin-right: @padding/2;
126 margin-right: @padding/2;
127 }
127 }
128 }
128 }
129
129
130 .cs_files_title {
130 .cs_files_title {
131 margin: @pagepadding 0 0;
131 margin: @pagepadding 0 0;
132 }
132 }
133
133
134 input.inline[type="file"] {
134 input.inline[type="file"] {
135 display: inline;
135 display: inline;
136 }
136 }
137
137
138 .error_page {
138 .error_page {
139 margin: 10% auto;
139 margin: 10% auto;
140
140
141 h1 {
141 h1 {
142 color: @grey2;
142 color: @grey2;
143 }
143 }
144
144
145 .alert {
145 .alert {
146 margin: @padding 0;
146 margin: @padding 0;
147 }
147 }
148
148
149 .error-branding {
149 .error-branding {
150 font-family: @text-semibold;
150 font-family: @text-semibold;
151 color: @grey4;
151 color: @grey4;
152 }
152 }
153
153
154 .error_message {
154 .error_message {
155 font-family: @text-regular;
155 font-family: @text-regular;
156 }
156 }
157
157
158 .sidebar {
158 .sidebar {
159 min-height: 275px;
159 min-height: 275px;
160 margin: 0;
160 margin: 0;
161 padding: 0 0 @sidebarpadding @sidebarpadding;
161 padding: 0 0 @sidebarpadding @sidebarpadding;
162 border: none;
162 border: none;
163 }
163 }
164
164
165 .main-content {
165 .main-content {
166 position: relative;
166 position: relative;
167 margin: 0 @sidebarpadding @sidebarpadding;
167 margin: 0 @sidebarpadding @sidebarpadding;
168 padding: 0 0 0 @sidebarpadding;
168 padding: 0 0 0 @sidebarpadding;
169 border-left: @border-thickness solid @grey5;
169 border-left: @border-thickness solid @grey5;
170
170
171 @media (max-width:767px) {
171 @media (max-width:767px) {
172 clear: both;
172 clear: both;
173 width: 100%;
173 width: 100%;
174 margin: 0;
174 margin: 0;
175 border: none;
175 border: none;
176 }
176 }
177 }
177 }
178
178
179 .inner-column {
179 .inner-column {
180 float: left;
180 float: left;
181 width: 29.75%;
181 width: 29.75%;
182 min-height: 150px;
182 min-height: 150px;
183 margin: @sidebarpadding 2% 0 0;
183 margin: @sidebarpadding 2% 0 0;
184 padding: 0 2% 0 0;
184 padding: 0 2% 0 0;
185 border-right: @border-thickness solid @grey5;
185 border-right: @border-thickness solid @grey5;
186
186
187 @media (max-width:767px) {
187 @media (max-width:767px) {
188 clear: both;
188 clear: both;
189 width: 100%;
189 width: 100%;
190 border: none;
190 border: none;
191 }
191 }
192
192
193 ul {
193 ul {
194 padding-left: 1.25em;
194 padding-left: 1.25em;
195 }
195 }
196
196
197 &:last-child {
197 &:last-child {
198 margin: @sidebarpadding 0 0;
198 margin: @sidebarpadding 0 0;
199 border: none;
199 border: none;
200 }
200 }
201
201
202 h4 {
202 h4 {
203 margin: 0 0 @padding;
203 margin: 0 0 @padding;
204 font-family: @text-semibold;
204 font-family: @text-semibold;
205 }
205 }
206 }
206 }
207 }
207 }
208 .error-page-logo {
208 .error-page-logo {
209 width: 130px;
209 width: 130px;
210 height: 160px;
210 height: 160px;
211 }
211 }
212
212
213 // HEADER
213 // HEADER
214 .header {
214 .header {
215
215
216 // TODO: johbo: Fix login pages, so that they work without a min-height
216 // TODO: johbo: Fix login pages, so that they work without a min-height
217 // for the header and then remove the min-height. I chose a smaller value
217 // for the header and then remove the min-height. I chose a smaller value
218 // intentionally here to avoid rendering issues in the main navigation.
218 // intentionally here to avoid rendering issues in the main navigation.
219 min-height: 49px;
219 min-height: 49px;
220
220
221 position: relative;
221 position: relative;
222 vertical-align: bottom;
222 vertical-align: bottom;
223 padding: 0 @header-padding;
223 padding: 0 @header-padding;
224 background-color: @grey2;
224 background-color: @grey2;
225 color: @grey5;
225 color: @grey5;
226
226
227 .title {
227 .title {
228 overflow: visible;
228 overflow: visible;
229 }
229 }
230
230
231 &:before,
231 &:before,
232 &:after {
232 &:after {
233 content: "";
233 content: "";
234 clear: both;
234 clear: both;
235 width: 100%;
235 width: 100%;
236 }
236 }
237
237
238 // TODO: johbo: Avoids breaking "Repositories" chooser
238 // TODO: johbo: Avoids breaking "Repositories" chooser
239 .select2-container .select2-choice .select2-arrow {
239 .select2-container .select2-choice .select2-arrow {
240 display: none;
240 display: none;
241 }
241 }
242 }
242 }
243
243
244 #header-inner {
244 #header-inner {
245 &.title {
245 &.title {
246 margin: 0;
246 margin: 0;
247 }
247 }
248 &:before,
248 &:before,
249 &:after {
249 &:after {
250 content: "";
250 content: "";
251 clear: both;
251 clear: both;
252 }
252 }
253 }
253 }
254
254
255 // Gists
255 // Gists
256 #files_data {
256 #files_data {
257 clear: both; //for firefox
257 clear: both; //for firefox
258 }
258 }
259 #gistid {
259 #gistid {
260 margin-right: @padding;
260 margin-right: @padding;
261 }
261 }
262
262
263 // Global Settings Editor
263 // Global Settings Editor
264 .textarea.editor {
264 .textarea.editor {
265 float: left;
265 float: left;
266 position: relative;
266 position: relative;
267 max-width: @texteditor-width;
267 max-width: @texteditor-width;
268
268
269 select {
269 select {
270 position: absolute;
270 position: absolute;
271 top:10px;
271 top:10px;
272 right:0;
272 right:0;
273 }
273 }
274
274
275 .CodeMirror {
275 .CodeMirror {
276 margin: 0;
276 margin: 0;
277 }
277 }
278
278
279 .help-block {
279 .help-block {
280 margin: 0 0 @padding;
280 margin: 0 0 @padding;
281 padding:.5em;
281 padding:.5em;
282 background-color: @grey6;
282 background-color: @grey6;
283 }
283 }
284 }
284 }
285
285
286 ul.auth_plugins {
286 ul.auth_plugins {
287 margin: @padding 0 @padding @legend-width;
287 margin: @padding 0 @padding @legend-width;
288 padding: 0;
288 padding: 0;
289
289
290 li {
290 li {
291 margin-bottom: @padding;
291 margin-bottom: @padding;
292 line-height: 1em;
292 line-height: 1em;
293 list-style-type: none;
293 list-style-type: none;
294
294
295 .auth_buttons .btn {
295 .auth_buttons .btn {
296 margin-right: @padding;
296 margin-right: @padding;
297 }
297 }
298
298
299 &:before { content: none; }
299 &:before { content: none; }
300 }
300 }
301 }
301 }
302
302
303
303
304 // My Account PR list
304 // My Account PR list
305
305
306 #show_closed {
306 #show_closed {
307 margin: 0 1em 0 0;
307 margin: 0 1em 0 0;
308 }
308 }
309
309
310 .pullrequestlist {
310 .pullrequestlist {
311 .closed {
311 .closed {
312 background-color: @grey6;
312 background-color: @grey6;
313 }
313 }
314 .td-status {
314 .td-status {
315 padding-left: .5em;
315 padding-left: .5em;
316 }
316 }
317 .log-container .truncate {
317 .log-container .truncate {
318 height: 2.75em;
318 height: 2.75em;
319 white-space: pre-line;
319 white-space: pre-line;
320 }
320 }
321 table.rctable .user {
321 table.rctable .user {
322 padding-left: 0;
322 padding-left: 0;
323 }
323 }
324 table.rctable {
324 table.rctable {
325 td.td-description,
325 td.td-description,
326 .rc-user {
326 .rc-user {
327 min-width: auto;
327 min-width: auto;
328 }
328 }
329 }
329 }
330 }
330 }
331
331
332 // Pull Requests
332 // Pull Requests
333
333
334 .pullrequests_section_head {
334 .pullrequests_section_head {
335 display: block;
335 display: block;
336 clear: both;
336 clear: both;
337 margin: @padding 0;
337 margin: @padding 0;
338 font-family: @text-bold;
338 font-family: @text-bold;
339 }
339 }
340
340
341 .pr-origininfo, .pr-targetinfo {
341 .pr-origininfo, .pr-targetinfo {
342 position: relative;
342 position: relative;
343
343
344 .tag {
344 .tag {
345 display: inline-block;
345 display: inline-block;
346 margin: 0 1em .5em 0;
346 margin: 0 1em .5em 0;
347 }
347 }
348
348
349 .clone-url {
349 .clone-url {
350 display: inline-block;
350 display: inline-block;
351 margin: 0 0 .5em 0;
351 margin: 0 0 .5em 0;
352 padding: 0;
352 padding: 0;
353 line-height: 1.2em;
353 line-height: 1.2em;
354 }
354 }
355 }
355 }
356
356
357 .pr-pullinfo {
357 .pr-pullinfo {
358 clear: both;
358 clear: both;
359 margin: .5em 0;
359 margin: .5em 0;
360 }
360 }
361
361
362 #pr-title-input {
362 #pr-title-input {
363 width: 72%;
363 width: 72%;
364 font-size: 1em;
364 font-size: 1em;
365 font-family: @text-bold;
365 font-family: @text-bold;
366 margin: 0;
366 margin: 0;
367 padding: 0 0 0 @padding/4;
367 padding: 0 0 0 @padding/4;
368 line-height: 1.7em;
368 line-height: 1.7em;
369 color: @text-color;
369 color: @text-color;
370 letter-spacing: .02em;
370 letter-spacing: .02em;
371 }
371 }
372
372
373 #pullrequest_title {
373 #pullrequest_title {
374 width: 100%;
374 width: 100%;
375 box-sizing: border-box;
375 box-sizing: border-box;
376 }
376 }
377
377
378 #pr_open_message {
378 #pr_open_message {
379 border: @border-thickness solid #fff;
379 border: @border-thickness solid #fff;
380 border-radius: @border-radius;
380 border-radius: @border-radius;
381 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
381 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
382 text-align: right;
382 text-align: right;
383 overflow: hidden;
383 overflow: hidden;
384 }
384 }
385
385
386 .pr-submit-button {
386 .pr-submit-button {
387 float: right;
387 float: right;
388 margin: 0 0 0 5px;
388 margin: 0 0 0 5px;
389 }
389 }
390
390
391 .pr-spacing-container {
391 .pr-spacing-container {
392 padding: 20px;
392 padding: 20px;
393 clear: both
393 clear: both
394 }
394 }
395
395
396 #pr-description-input {
396 #pr-description-input {
397 margin-bottom: 0;
397 margin-bottom: 0;
398 }
398 }
399
399
400 .pr-description-label {
400 .pr-description-label {
401 vertical-align: top;
401 vertical-align: top;
402 }
402 }
403
403
404 .perms_section_head {
404 .perms_section_head {
405 min-width: 625px;
405 min-width: 625px;
406
406
407 h2 {
407 h2 {
408 margin-bottom: 0;
408 margin-bottom: 0;
409 }
409 }
410
410
411 .label-checkbox {
411 .label-checkbox {
412 float: left;
412 float: left;
413 }
413 }
414
414
415 &.field {
415 &.field {
416 margin: @space 0 @padding;
416 margin: @space 0 @padding;
417 }
417 }
418
418
419 &:first-child.field {
419 &:first-child.field {
420 margin-top: 0;
420 margin-top: 0;
421
421
422 .label {
422 .label {
423 margin-top: 0;
423 margin-top: 0;
424 padding-top: 0;
424 padding-top: 0;
425 }
425 }
426
426
427 .radios {
427 .radios {
428 padding-top: 0;
428 padding-top: 0;
429 }
429 }
430 }
430 }
431
431
432 .radios {
432 .radios {
433 float: right;
433 float: right;
434 position: relative;
434 position: relative;
435 width: 405px;
435 width: 405px;
436 }
436 }
437 }
437 }
438
438
439 //--- MODULES ------------------//
439 //--- MODULES ------------------//
440
440
441
441
442 // Server Announcement
442 // Server Announcement
443 #server-announcement {
443 #server-announcement {
444 width: 95%;
444 width: 95%;
445 margin: @padding auto;
445 margin: @padding auto;
446 padding: @padding;
446 padding: @padding;
447 border-width: 2px;
447 border-width: 2px;
448 border-style: solid;
448 border-style: solid;
449 .border-radius(2px);
449 .border-radius(2px);
450 font-family: @text-bold;
450 font-family: @text-bold;
451
451
452 &.info { border-color: @alert4; background-color: @alert4-inner; }
452 &.info { border-color: @alert4; background-color: @alert4-inner; }
453 &.warning { border-color: @alert3; background-color: @alert3-inner; }
453 &.warning { border-color: @alert3; background-color: @alert3-inner; }
454 &.error { border-color: @alert2; background-color: @alert2-inner; }
454 &.error { border-color: @alert2; background-color: @alert2-inner; }
455 &.success { border-color: @alert1; background-color: @alert1-inner; }
455 &.success { border-color: @alert1; background-color: @alert1-inner; }
456 &.neutral { border-color: @grey3; background-color: @grey6; }
456 &.neutral { border-color: @grey3; background-color: @grey6; }
457 }
457 }
458
458
459 // Fixed Sidebar Column
459 // Fixed Sidebar Column
460 .sidebar-col-wrapper {
460 .sidebar-col-wrapper {
461 padding-left: @sidebar-all-width;
461 padding-left: @sidebar-all-width;
462
462
463 .sidebar {
463 .sidebar {
464 width: @sidebar-width;
464 width: @sidebar-width;
465 margin-left: -@sidebar-all-width;
465 margin-left: -@sidebar-all-width;
466 }
466 }
467 }
467 }
468
468
469 .sidebar-col-wrapper.scw-small {
469 .sidebar-col-wrapper.scw-small {
470 padding-left: @sidebar-small-all-width;
470 padding-left: @sidebar-small-all-width;
471
471
472 .sidebar {
472 .sidebar {
473 width: @sidebar-small-width;
473 width: @sidebar-small-width;
474 margin-left: -@sidebar-small-all-width;
474 margin-left: -@sidebar-small-all-width;
475 }
475 }
476 }
476 }
477
477
478
478
479 // FOOTER
479 // FOOTER
480 #footer {
480 #footer {
481 padding: 0;
481 padding: 0;
482 text-align: center;
482 text-align: center;
483 vertical-align: middle;
483 vertical-align: middle;
484 color: @grey2;
484 color: @grey2;
485 background-color: @grey6;
485 background-color: @grey6;
486
486
487 p {
487 p {
488 margin: 0;
488 margin: 0;
489 padding: 1em;
489 padding: 1em;
490 line-height: 1em;
490 line-height: 1em;
491 }
491 }
492
492
493 .server-instance { //server instance
493 .server-instance { //server instance
494 display: none;
494 display: none;
495 }
495 }
496
496
497 .title {
497 .title {
498 float: none;
498 float: none;
499 margin: 0 auto;
499 margin: 0 auto;
500 }
500 }
501 }
501 }
502
502
503 button.close {
503 button.close {
504 padding: 0;
504 padding: 0;
505 cursor: pointer;
505 cursor: pointer;
506 background: transparent;
506 background: transparent;
507 border: 0;
507 border: 0;
508 .box-shadow(none);
508 .box-shadow(none);
509 -webkit-appearance: none;
509 -webkit-appearance: none;
510 }
510 }
511
511
512 .close {
512 .close {
513 float: right;
513 float: right;
514 font-size: 21px;
514 font-size: 21px;
515 font-family: @text-bootstrap;
515 font-family: @text-bootstrap;
516 line-height: 1em;
516 line-height: 1em;
517 font-weight: bold;
517 font-weight: bold;
518 color: @grey2;
518 color: @grey2;
519
519
520 &:hover,
520 &:hover,
521 &:focus {
521 &:focus {
522 color: @grey1;
522 color: @grey1;
523 text-decoration: none;
523 text-decoration: none;
524 cursor: pointer;
524 cursor: pointer;
525 }
525 }
526 }
526 }
527
527
528 // GRID
528 // GRID
529 .sorting,
529 .sorting,
530 .sorting_desc,
530 .sorting_desc,
531 .sorting_asc {
531 .sorting_asc {
532 cursor: pointer;
532 cursor: pointer;
533 }
533 }
534 .sorting_desc:after {
534 .sorting_desc:after {
535 content: "\00A0\25B2";
535 content: "\00A0\25B2";
536 font-size: .75em;
536 font-size: .75em;
537 }
537 }
538 .sorting_asc:after {
538 .sorting_asc:after {
539 content: "\00A0\25BC";
539 content: "\00A0\25BC";
540 font-size: .68em;
540 font-size: .68em;
541 }
541 }
542
542
543
543
544 .user_auth_tokens {
544 .user_auth_tokens {
545
545
546 &.truncate {
546 &.truncate {
547 white-space: nowrap;
547 white-space: nowrap;
548 overflow: hidden;
548 overflow: hidden;
549 text-overflow: ellipsis;
549 text-overflow: ellipsis;
550 }
550 }
551
551
552 .fields .field .input {
552 .fields .field .input {
553 margin: 0;
553 margin: 0;
554 }
554 }
555
555
556 input#description {
556 input#description {
557 width: 100px;
557 width: 100px;
558 margin: 0;
558 margin: 0;
559 }
559 }
560
560
561 .drop-menu {
561 .drop-menu {
562 // TODO: johbo: Remove this, should work out of the box when
562 // TODO: johbo: Remove this, should work out of the box when
563 // having multiple inputs inline
563 // having multiple inputs inline
564 margin: 0 0 0 5px;
564 margin: 0 0 0 5px;
565 }
565 }
566 }
566 }
567 #user_list_table {
567 #user_list_table {
568 .closed {
568 .closed {
569 background-color: @grey6;
569 background-color: @grey6;
570 }
570 }
571 }
571 }
572
572
573
573
574 input {
574 input {
575 &.disabled {
575 &.disabled {
576 opacity: .5;
576 opacity: .5;
577 }
577 }
578 }
578 }
579
579
580 // remove extra padding in firefox
580 // remove extra padding in firefox
581 input::-moz-focus-inner { border:0; padding:0 }
581 input::-moz-focus-inner { border:0; padding:0 }
582
582
583 .adjacent input {
583 .adjacent input {
584 margin-bottom: @padding;
584 margin-bottom: @padding;
585 }
585 }
586
586
587 .permissions_boxes {
587 .permissions_boxes {
588 display: block;
588 display: block;
589 }
589 }
590
590
591 //TODO: lisa: this should be in tables
591 //TODO: lisa: this should be in tables
592 .show_more_col {
592 .show_more_col {
593 width: 20px;
593 width: 20px;
594 }
594 }
595
595
596 //FORMS
596 //FORMS
597
597
598 .medium-inline,
598 .medium-inline,
599 input#description.medium-inline {
599 input#description.medium-inline {
600 display: inline;
600 display: inline;
601 width: @medium-inline-input-width;
601 width: @medium-inline-input-width;
602 min-width: 100px;
602 min-width: 100px;
603 }
603 }
604
604
605 select {
605 select {
606 //reset
606 //reset
607 -webkit-appearance: none;
607 -webkit-appearance: none;
608 -moz-appearance: none;
608 -moz-appearance: none;
609
609
610 display: inline-block;
610 display: inline-block;
611 height: 28px;
611 height: 28px;
612 width: auto;
612 width: auto;
613 margin: 0 @padding @padding 0;
613 margin: 0 @padding @padding 0;
614 padding: 0 18px 0 8px;
614 padding: 0 18px 0 8px;
615 line-height:1em;
615 line-height:1em;
616 font-size: @basefontsize;
616 font-size: @basefontsize;
617 border: @border-thickness solid @rcblue;
617 border: @border-thickness solid @rcblue;
618 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
618 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
619 color: @rcblue;
619 color: @rcblue;
620
620
621 &:after {
621 &:after {
622 content: "\00A0\25BE";
622 content: "\00A0\25BE";
623 }
623 }
624
624
625 &:focus {
625 &:focus {
626 outline: none;
626 outline: none;
627 }
627 }
628 }
628 }
629
629
630 option {
630 option {
631 &:focus {
631 &:focus {
632 outline: none;
632 outline: none;
633 }
633 }
634 }
634 }
635
635
636 input,
636 input,
637 textarea {
637 textarea {
638 padding: @input-padding;
638 padding: @input-padding;
639 border: @input-border-thickness solid @border-highlight-color;
639 border: @input-border-thickness solid @border-highlight-color;
640 .border-radius (@border-radius);
640 .border-radius (@border-radius);
641 font-family: @text-light;
641 font-family: @text-light;
642 font-size: @basefontsize;
642 font-size: @basefontsize;
643
643
644 &.input-sm {
644 &.input-sm {
645 padding: 5px;
645 padding: 5px;
646 }
646 }
647
647
648 &#description {
648 &#description {
649 min-width: @input-description-minwidth;
649 min-width: @input-description-minwidth;
650 min-height: 1em;
650 min-height: 1em;
651 padding: 10px;
651 padding: 10px;
652 }
652 }
653 }
653 }
654
654
655 .field-sm {
655 .field-sm {
656 input,
656 input,
657 textarea {
657 textarea {
658 padding: 5px;
658 padding: 5px;
659 }
659 }
660 }
660 }
661
661
662 textarea {
662 textarea {
663 display: block;
663 display: block;
664 clear: both;
664 clear: both;
665 width: 100%;
665 width: 100%;
666 min-height: 100px;
666 min-height: 100px;
667 margin-bottom: @padding;
667 margin-bottom: @padding;
668 .box-sizing(border-box);
668 .box-sizing(border-box);
669 overflow: auto;
669 overflow: auto;
670 }
670 }
671
671
672 label {
672 label {
673 font-family: @text-light;
673 font-family: @text-light;
674 }
674 }
675
675
676 // GRAVATARS
676 // GRAVATARS
677 // centers gravatar on username to the right
677 // centers gravatar on username to the right
678
678
679 .gravatar {
679 .gravatar {
680 display: inline;
680 display: inline;
681 min-width: 16px;
681 min-width: 16px;
682 min-height: 16px;
682 min-height: 16px;
683 margin: -5px 0;
683 margin: -5px 0;
684 padding: 0;
684 padding: 0;
685 line-height: 1em;
685 line-height: 1em;
686 border: 1px solid @grey4;
686 border: 1px solid @grey4;
687
687
688 &.gravatar-large {
688 &.gravatar-large {
689 margin: -0.5em .25em -0.5em 0;
689 margin: -0.5em .25em -0.5em 0;
690 }
690 }
691
691
692 & + .user {
692 & + .user {
693 display: inline;
693 display: inline;
694 margin: 0;
694 margin: 0;
695 padding: 0 0 0 .17em;
695 padding: 0 0 0 .17em;
696 line-height: 1em;
696 line-height: 1em;
697 }
697 }
698 }
698 }
699
699
700 .user-inline-data {
700 .user-inline-data {
701 display: inline-block;
701 display: inline-block;
702 float: left;
702 float: left;
703 padding-left: .5em;
703 padding-left: .5em;
704 line-height: 1.3em;
704 line-height: 1.3em;
705 }
705 }
706
706
707 .rc-user { // gravatar + user wrapper
707 .rc-user { // gravatar + user wrapper
708 float: left;
708 float: left;
709 position: relative;
709 position: relative;
710 min-width: 100px;
710 min-width: 100px;
711 max-width: 200px;
711 max-width: 200px;
712 min-height: (@gravatar-size + @border-thickness * 2); // account for border
712 min-height: (@gravatar-size + @border-thickness * 2); // account for border
713 display: block;
713 display: block;
714 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
714 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
715
715
716
716
717 .gravatar {
717 .gravatar {
718 display: block;
718 display: block;
719 position: absolute;
719 position: absolute;
720 top: 0;
720 top: 0;
721 left: 0;
721 left: 0;
722 min-width: @gravatar-size;
722 min-width: @gravatar-size;
723 min-height: @gravatar-size;
723 min-height: @gravatar-size;
724 margin: 0;
724 margin: 0;
725 }
725 }
726
726
727 .user {
727 .user {
728 display: block;
728 display: block;
729 max-width: 175px;
729 max-width: 175px;
730 padding-top: 2px;
730 padding-top: 2px;
731 overflow: hidden;
731 overflow: hidden;
732 text-overflow: ellipsis;
732 text-overflow: ellipsis;
733 }
733 }
734 }
734 }
735
735
736 .gist-gravatar,
736 .gist-gravatar,
737 .journal_container {
737 .journal_container {
738 .gravatar-large {
738 .gravatar-large {
739 margin: 0 .5em -10px 0;
739 margin: 0 .5em -10px 0;
740 }
740 }
741 }
741 }
742
742
743
743
744 // ADMIN SETTINGS
744 // ADMIN SETTINGS
745
745
746 // Tag Patterns
746 // Tag Patterns
747 .tag_patterns {
747 .tag_patterns {
748 .tag_input {
748 .tag_input {
749 margin-bottom: @padding;
749 margin-bottom: @padding;
750 }
750 }
751 }
751 }
752
752
753 .locked_input {
753 .locked_input {
754 position: relative;
754 position: relative;
755
755
756 input {
756 input {
757 display: inline;
757 display: inline;
758 margin-top: 3px;
758 margin-top: 3px;
759 }
759 }
760
760
761 br {
761 br {
762 display: none;
762 display: none;
763 }
763 }
764
764
765 .error-message {
765 .error-message {
766 float: left;
766 float: left;
767 width: 100%;
767 width: 100%;
768 }
768 }
769
769
770 .lock_input_button {
770 .lock_input_button {
771 display: inline;
771 display: inline;
772 }
772 }
773
773
774 .help-block {
774 .help-block {
775 clear: both;
775 clear: both;
776 }
776 }
777 }
777 }
778
778
779 // Notifications
779 // Notifications
780
780
781 .notifications_buttons {
781 .notifications_buttons {
782 margin: 0 0 @space 0;
782 margin: 0 0 @space 0;
783 padding: 0;
783 padding: 0;
784
784
785 .btn {
785 .btn {
786 display: inline-block;
786 display: inline-block;
787 }
787 }
788 }
788 }
789
789
790 .notification-list {
790 .notification-list {
791
791
792 div {
792 div {
793 display: inline-block;
793 display: inline-block;
794 vertical-align: middle;
794 vertical-align: middle;
795 }
795 }
796
796
797 .container {
797 .container {
798 display: block;
798 display: block;
799 margin: 0 0 @padding 0;
799 margin: 0 0 @padding 0;
800 }
800 }
801
801
802 .delete-notifications {
802 .delete-notifications {
803 margin-left: @padding;
803 margin-left: @padding;
804 text-align: right;
804 text-align: right;
805 cursor: pointer;
805 cursor: pointer;
806 }
806 }
807
807
808 .read-notifications {
808 .read-notifications {
809 margin-left: @padding/2;
809 margin-left: @padding/2;
810 text-align: right;
810 text-align: right;
811 width: 35px;
811 width: 35px;
812 cursor: pointer;
812 cursor: pointer;
813 }
813 }
814
814
815 .icon-minus-sign {
815 .icon-minus-sign {
816 color: @alert2;
816 color: @alert2;
817 }
817 }
818
818
819 .icon-ok-sign {
819 .icon-ok-sign {
820 color: @alert1;
820 color: @alert1;
821 }
821 }
822 }
822 }
823
823
824 .user_settings {
824 .user_settings {
825 float: left;
825 float: left;
826 clear: both;
826 clear: both;
827 display: block;
827 display: block;
828 width: 100%;
828 width: 100%;
829
829
830 .gravatar_box {
830 .gravatar_box {
831 margin-bottom: @padding;
831 margin-bottom: @padding;
832
832
833 &:after {
833 &:after {
834 content: " ";
834 content: " ";
835 clear: both;
835 clear: both;
836 width: 100%;
836 width: 100%;
837 }
837 }
838 }
838 }
839
839
840 .fields .field {
840 .fields .field {
841 clear: both;
841 clear: both;
842 }
842 }
843 }
843 }
844
844
845 .advanced_settings {
845 .advanced_settings {
846 margin-bottom: @space;
846 margin-bottom: @space;
847
847
848 .help-block {
848 .help-block {
849 margin-left: 0;
849 margin-left: 0;
850 }
850 }
851
851
852 button + .help-block {
852 button + .help-block {
853 margin-top: @padding;
853 margin-top: @padding;
854 }
854 }
855 }
855 }
856
856
857 // admin settings radio buttons and labels
857 // admin settings radio buttons and labels
858 .label-2 {
858 .label-2 {
859 float: left;
859 float: left;
860 width: @label2-width;
860 width: @label2-width;
861
861
862 label {
862 label {
863 color: @grey1;
863 color: @grey1;
864 }
864 }
865 }
865 }
866 .checkboxes {
866 .checkboxes {
867 float: left;
867 float: left;
868 width: @checkboxes-width;
868 width: @checkboxes-width;
869 margin-bottom: @padding;
869 margin-bottom: @padding;
870
870
871 .checkbox {
871 .checkbox {
872 width: 100%;
872 width: 100%;
873
873
874 label {
874 label {
875 margin: 0;
875 margin: 0;
876 padding: 0;
876 padding: 0;
877 }
877 }
878 }
878 }
879
879
880 .checkbox + .checkbox {
880 .checkbox + .checkbox {
881 display: inline-block;
881 display: inline-block;
882 }
882 }
883
883
884 label {
884 label {
885 margin-right: 1em;
885 margin-right: 1em;
886 }
886 }
887 }
887 }
888
888
889 // CHANGELOG
889 // CHANGELOG
890 .container_header {
890 .container_header {
891 float: left;
891 float: left;
892 display: block;
892 display: block;
893 width: 100%;
893 width: 100%;
894 margin: @padding 0 @padding;
894 margin: @padding 0 @padding;
895
895
896 #filter_changelog {
896 #filter_changelog {
897 float: left;
897 float: left;
898 margin-right: @padding;
898 margin-right: @padding;
899 }
899 }
900
900
901 .breadcrumbs_light {
901 .breadcrumbs_light {
902 display: inline-block;
902 display: inline-block;
903 }
903 }
904 }
904 }
905
905
906 .info_box {
906 .info_box {
907 float: right;
907 float: right;
908 }
908 }
909
909
910
910
911 #graph_nodes {
911 #graph_nodes {
912 padding-top: 43px;
912 padding-top: 43px;
913 }
913 }
914
914
915 #graph_content{
915 #graph_content{
916
916
917 // adjust for table headers so that graph renders properly
917 // adjust for table headers so that graph renders properly
918 // #graph_nodes padding - table cell padding
918 // #graph_nodes padding - table cell padding
919 padding-top: (@space - (@basefontsize * 2.4));
919 padding-top: (@space - (@basefontsize * 2.4));
920
920
921 &.graph_full_width {
921 &.graph_full_width {
922 width: 100%;
922 width: 100%;
923 max-width: 100%;
923 max-width: 100%;
924 }
924 }
925 }
925 }
926
926
927 #graph {
927 #graph {
928 .flag_status {
928 .flag_status {
929 margin: 0;
929 margin: 0;
930 }
930 }
931
931
932 .pagination-left {
932 .pagination-left {
933 float: left;
933 float: left;
934 clear: both;
934 clear: both;
935 }
935 }
936
936
937 .log-container {
937 .log-container {
938 max-width: 345px;
938 max-width: 345px;
939
939
940 .message{
940 .message{
941 max-width: 340px;
941 max-width: 340px;
942 }
942 }
943 }
943 }
944
944
945 .graph-col-wrapper {
945 .graph-col-wrapper {
946 padding-left: 110px;
946 padding-left: 110px;
947
947
948 #graph_nodes {
948 #graph_nodes {
949 width: 100px;
949 width: 100px;
950 margin-left: -110px;
950 margin-left: -110px;
951 float: left;
951 float: left;
952 clear: left;
952 clear: left;
953 }
953 }
954 }
954 }
955 }
955 }
956
956
957 #filter_changelog {
957 #filter_changelog {
958 float: left;
958 float: left;
959 }
959 }
960
960
961
961
962 //--- THEME ------------------//
962 //--- THEME ------------------//
963
963
964 #logo {
964 #logo {
965 float: left;
965 float: left;
966 margin: 9px 0 0 0;
966 margin: 9px 0 0 0;
967
967
968 .header {
968 .header {
969 background-color: transparent;
969 background-color: transparent;
970 }
970 }
971
971
972 a {
972 a {
973 display: inline-block;
973 display: inline-block;
974 }
974 }
975
975
976 img {
976 img {
977 height:30px;
977 height:30px;
978 }
978 }
979 }
979 }
980
980
981 .logo-wrapper {
981 .logo-wrapper {
982 float:left;
982 float:left;
983 }
983 }
984
984
985 .branding{
985 .branding{
986 float: left;
986 float: left;
987 padding: 9px 2px;
987 padding: 9px 2px;
988 line-height: 1em;
988 line-height: 1em;
989 font-size: @navigation-fontsize;
989 font-size: @navigation-fontsize;
990 }
990 }
991
991
992 img {
992 img {
993 border: none;
993 border: none;
994 outline: none;
994 outline: none;
995 }
995 }
996 user-profile-header
996 user-profile-header
997 label {
997 label {
998
998
999 input[type="checkbox"] {
999 input[type="checkbox"] {
1000 margin-right: 1em;
1000 margin-right: 1em;
1001 }
1001 }
1002 input[type="radio"] {
1002 input[type="radio"] {
1003 margin-right: 1em;
1003 margin-right: 1em;
1004 }
1004 }
1005 }
1005 }
1006
1006
1007 .flag_status {
1007 .flag_status {
1008 margin: 2px 8px 6px 2px;
1008 margin: 2px 8px 6px 2px;
1009 &.under_review {
1009 &.under_review {
1010 .circle(5px, @alert3);
1010 .circle(5px, @alert3);
1011 }
1011 }
1012 &.approved {
1012 &.approved {
1013 .circle(5px, @alert1);
1013 .circle(5px, @alert1);
1014 }
1014 }
1015 &.rejected,
1015 &.rejected,
1016 &.forced_closed{
1016 &.forced_closed{
1017 .circle(5px, @alert2);
1017 .circle(5px, @alert2);
1018 }
1018 }
1019 &.not_reviewed {
1019 &.not_reviewed {
1020 .circle(5px, @grey5);
1020 .circle(5px, @grey5);
1021 }
1021 }
1022 }
1022 }
1023
1023
1024 .flag_status_comment_box {
1024 .flag_status_comment_box {
1025 margin: 5px 6px 0px 2px;
1025 margin: 5px 6px 0px 2px;
1026 }
1026 }
1027 .test_pattern_preview {
1027 .test_pattern_preview {
1028 margin: @space 0;
1028 margin: @space 0;
1029
1029
1030 p {
1030 p {
1031 margin-bottom: 0;
1031 margin-bottom: 0;
1032 border-bottom: @border-thickness solid @border-default-color;
1032 border-bottom: @border-thickness solid @border-default-color;
1033 color: @grey3;
1033 color: @grey3;
1034 }
1034 }
1035
1035
1036 .btn {
1036 .btn {
1037 margin-bottom: @padding;
1037 margin-bottom: @padding;
1038 }
1038 }
1039 }
1039 }
1040 #test_pattern_result {
1040 #test_pattern_result {
1041 display: none;
1041 display: none;
1042 &:extend(pre);
1042 &:extend(pre);
1043 padding: .9em;
1043 padding: .9em;
1044 color: @grey3;
1044 color: @grey3;
1045 background-color: @grey7;
1045 background-color: @grey7;
1046 border-right: @border-thickness solid @border-default-color;
1046 border-right: @border-thickness solid @border-default-color;
1047 border-bottom: @border-thickness solid @border-default-color;
1047 border-bottom: @border-thickness solid @border-default-color;
1048 border-left: @border-thickness solid @border-default-color;
1048 border-left: @border-thickness solid @border-default-color;
1049 }
1049 }
1050
1050
1051 #repo_vcs_settings {
1051 #repo_vcs_settings {
1052 #inherit_overlay_vcs_default {
1052 #inherit_overlay_vcs_default {
1053 display: none;
1053 display: none;
1054 }
1054 }
1055 #inherit_overlay_vcs_custom {
1055 #inherit_overlay_vcs_custom {
1056 display: custom;
1056 display: custom;
1057 }
1057 }
1058 &.inherited {
1058 &.inherited {
1059 #inherit_overlay_vcs_default {
1059 #inherit_overlay_vcs_default {
1060 display: block;
1060 display: block;
1061 }
1061 }
1062 #inherit_overlay_vcs_custom {
1062 #inherit_overlay_vcs_custom {
1063 display: none;
1063 display: none;
1064 }
1064 }
1065 }
1065 }
1066 }
1066 }
1067
1067
1068 .issue-tracker-link {
1068 .issue-tracker-link {
1069 color: @rcblue;
1069 color: @rcblue;
1070 }
1070 }
1071
1071
1072 // Issue Tracker Table Show/Hide
1072 // Issue Tracker Table Show/Hide
1073 #repo_issue_tracker {
1073 #repo_issue_tracker {
1074 #inherit_overlay {
1074 #inherit_overlay {
1075 display: none;
1075 display: none;
1076 }
1076 }
1077 #custom_overlay {
1077 #custom_overlay {
1078 display: custom;
1078 display: custom;
1079 }
1079 }
1080 &.inherited {
1080 &.inherited {
1081 #inherit_overlay {
1081 #inherit_overlay {
1082 display: block;
1082 display: block;
1083 }
1083 }
1084 #custom_overlay {
1084 #custom_overlay {
1085 display: none;
1085 display: none;
1086 }
1086 }
1087 }
1087 }
1088 }
1088 }
1089 table.issuetracker {
1089 table.issuetracker {
1090 &.readonly {
1090 &.readonly {
1091 tr, td {
1091 tr, td {
1092 color: @grey3;
1092 color: @grey3;
1093 }
1093 }
1094 }
1094 }
1095 .edit {
1095 .edit {
1096 display: none;
1096 display: none;
1097 }
1097 }
1098 .editopen {
1098 .editopen {
1099 .edit {
1099 .edit {
1100 display: inline;
1100 display: inline;
1101 }
1101 }
1102 .entry {
1102 .entry {
1103 display: none;
1103 display: none;
1104 }
1104 }
1105 }
1105 }
1106 tr td.td-action {
1106 tr td.td-action {
1107 min-width: 117px;
1107 min-width: 117px;
1108 }
1108 }
1109 td input {
1109 td input {
1110 max-width: none;
1110 max-width: none;
1111 min-width: 30px;
1111 min-width: 30px;
1112 width: 80%;
1112 width: 80%;
1113 }
1113 }
1114 .issuetracker_pref input {
1114 .issuetracker_pref input {
1115 width: 40%;
1115 width: 40%;
1116 }
1116 }
1117 input.edit_issuetracker_update {
1117 input.edit_issuetracker_update {
1118 margin-right: 0;
1118 margin-right: 0;
1119 width: auto;
1119 width: auto;
1120 }
1120 }
1121 }
1121 }
1122
1122
1123 table.integrations {
1123 table.integrations {
1124 .td-icon {
1124 .td-icon {
1125 width: 20px;
1125 width: 20px;
1126 .integration-icon {
1126 .integration-icon {
1127 height: 20px;
1127 height: 20px;
1128 width: 20px;
1128 width: 20px;
1129 }
1129 }
1130 }
1130 }
1131 }
1131 }
1132
1132
1133 .integrations {
1133 .integrations {
1134 a.integration-box {
1134 a.integration-box {
1135 color: @text-color;
1135 color: @text-color;
1136 &:hover {
1136 &:hover {
1137 .panel {
1137 .panel {
1138 background: #fbfbfb;
1138 background: #fbfbfb;
1139 }
1139 }
1140 }
1140 }
1141 .integration-icon {
1141 .integration-icon {
1142 width: 30px;
1142 width: 30px;
1143 height: 30px;
1143 height: 30px;
1144 margin-right: 20px;
1144 margin-right: 20px;
1145 float: left;
1145 float: left;
1146 }
1146 }
1147
1147
1148 .panel-body {
1148 .panel-body {
1149 padding: 10px;
1149 padding: 10px;
1150 }
1150 }
1151 .panel {
1151 .panel {
1152 margin-bottom: 10px;
1152 margin-bottom: 10px;
1153 }
1153 }
1154 h2 {
1154 h2 {
1155 display: inline-block;
1155 display: inline-block;
1156 margin: 0;
1156 margin: 0;
1157 min-width: 140px;
1157 min-width: 140px;
1158 }
1158 }
1159 }
1159 }
1160 }
1160 }
1161
1161
1162 //Permissions Settings
1162 //Permissions Settings
1163 #add_perm {
1163 #add_perm {
1164 margin: 0 0 @padding;
1164 margin: 0 0 @padding;
1165 cursor: pointer;
1165 cursor: pointer;
1166 }
1166 }
1167
1167
1168 .perm_ac {
1168 .perm_ac {
1169 input {
1169 input {
1170 width: 95%;
1170 width: 95%;
1171 }
1171 }
1172 }
1172 }
1173
1173
1174 .autocomplete-suggestions {
1174 .autocomplete-suggestions {
1175 width: auto !important; // overrides autocomplete.js
1175 width: auto !important; // overrides autocomplete.js
1176 margin: 0;
1176 margin: 0;
1177 border: @border-thickness solid @rcblue;
1177 border: @border-thickness solid @rcblue;
1178 border-radius: @border-radius;
1178 border-radius: @border-radius;
1179 color: @rcblue;
1179 color: @rcblue;
1180 background-color: white;
1180 background-color: white;
1181 }
1181 }
1182 .autocomplete-selected {
1182 .autocomplete-selected {
1183 background: #F0F0F0;
1183 background: #F0F0F0;
1184 }
1184 }
1185 .ac-container-wrap {
1185 .ac-container-wrap {
1186 margin: 0;
1186 margin: 0;
1187 padding: 8px;
1187 padding: 8px;
1188 border-bottom: @border-thickness solid @rclightblue;
1188 border-bottom: @border-thickness solid @rclightblue;
1189 list-style-type: none;
1189 list-style-type: none;
1190 cursor: pointer;
1190 cursor: pointer;
1191
1191
1192 &:hover {
1192 &:hover {
1193 background-color: @rclightblue;
1193 background-color: @rclightblue;
1194 }
1194 }
1195
1195
1196 img {
1196 img {
1197 margin-right: 1em;
1197 margin-right: 1em;
1198 }
1198 }
1199
1199
1200 strong {
1200 strong {
1201 font-weight: normal;
1201 font-weight: normal;
1202 }
1202 }
1203 }
1203 }
1204
1204
1205 // Settings Dropdown
1205 // Settings Dropdown
1206 .user-menu .container {
1206 .user-menu .container {
1207 padding: 0 4px;
1207 padding: 0 4px;
1208 margin: 0;
1208 margin: 0;
1209 }
1209 }
1210
1210
1211 .user-menu .gravatar {
1211 .user-menu .gravatar {
1212 cursor: pointer;
1212 cursor: pointer;
1213 }
1213 }
1214
1214
1215 .codeblock {
1215 .codeblock {
1216 margin-bottom: @padding;
1216 margin-bottom: @padding;
1217 clear: both;
1217 clear: both;
1218
1218
1219 .stats{
1219 .stats{
1220 overflow: hidden;
1220 overflow: hidden;
1221 }
1221 }
1222
1222
1223 .message{
1223 .message{
1224 textarea{
1224 textarea{
1225 margin: 0;
1225 margin: 0;
1226 }
1226 }
1227 }
1227 }
1228
1228
1229 .code-header {
1229 .code-header {
1230 .stats {
1230 .stats {
1231 line-height: 2em;
1231 line-height: 2em;
1232
1232
1233 .revision_id {
1233 .revision_id {
1234 margin-left: 0;
1234 margin-left: 0;
1235 }
1235 }
1236 .buttons {
1236 .buttons {
1237 padding-right: 0;
1237 padding-right: 0;
1238 }
1238 }
1239 }
1239 }
1240
1240
1241 .item{
1241 .item{
1242 margin-right: 0.5em;
1242 margin-right: 0.5em;
1243 }
1243 }
1244 }
1244 }
1245
1245
1246 #editor_container{
1246 #editor_container{
1247 position: relative;
1247 position: relative;
1248 margin: @padding;
1248 margin: @padding;
1249 }
1249 }
1250 }
1250 }
1251
1251
1252 #file_history_container {
1252 #file_history_container {
1253 display: none;
1253 display: none;
1254 }
1254 }
1255
1255
1256 .file-history-inner {
1256 .file-history-inner {
1257 margin-bottom: 10px;
1257 margin-bottom: 10px;
1258 }
1258 }
1259
1259
1260 // Pull Requests
1260 // Pull Requests
1261 .summary-details {
1261 .summary-details {
1262 width: 72%;
1262 width: 72%;
1263 }
1263 }
1264 .pr-summary {
1264 .pr-summary {
1265 border-bottom: @border-thickness solid @grey5;
1265 border-bottom: @border-thickness solid @grey5;
1266 margin-bottom: @space;
1266 margin-bottom: @space;
1267 }
1267 }
1268 .reviewers-title {
1268 .reviewers-title {
1269 width: 25%;
1269 width: 25%;
1270 min-width: 200px;
1270 min-width: 200px;
1271 }
1271 }
1272 .reviewers {
1272 .reviewers {
1273 width: 25%;
1273 width: 25%;
1274 min-width: 200px;
1274 min-width: 200px;
1275 }
1275 }
1276 .reviewers ul li {
1276 .reviewers ul li {
1277 position: relative;
1277 position: relative;
1278 width: 100%;
1278 width: 100%;
1279 margin-bottom: 8px;
1279 margin-bottom: 8px;
1280 }
1280 }
1281 .reviewers_member {
1281 .reviewers_member {
1282 width: 100%;
1282 width: 100%;
1283 overflow: auto;
1283 overflow: auto;
1284 }
1284 }
1285 .reviewer_reason {
1286 padding-left: 20px;
1287 }
1285 .reviewer_status {
1288 .reviewer_status {
1286 display: inline-block;
1289 display: inline-block;
1287 vertical-align: top;
1290 vertical-align: top;
1288 width: 7%;
1291 width: 7%;
1289 min-width: 20px;
1292 min-width: 20px;
1290 height: 1.2em;
1293 height: 1.2em;
1291 margin-top: 3px;
1294 margin-top: 3px;
1292 line-height: 1em;
1295 line-height: 1em;
1293 }
1296 }
1294
1297
1295 .reviewer_name {
1298 .reviewer_name {
1296 display: inline-block;
1299 display: inline-block;
1297 max-width: 83%;
1300 max-width: 83%;
1298 padding-right: 20px;
1301 padding-right: 20px;
1299 vertical-align: middle;
1302 vertical-align: middle;
1300 line-height: 1;
1303 line-height: 1;
1301
1304
1302 .rc-user {
1305 .rc-user {
1303 min-width: 0;
1306 min-width: 0;
1304 margin: -2px 1em 0 0;
1307 margin: -2px 1em 0 0;
1305 }
1308 }
1306
1309
1307 .reviewer {
1310 .reviewer {
1308 float: left;
1311 float: left;
1309 }
1312 }
1310
1313
1311 &.to-delete {
1314 &.to-delete {
1312 .user,
1315 .user,
1313 .reviewer {
1316 .reviewer {
1314 text-decoration: line-through;
1317 text-decoration: line-through;
1315 }
1318 }
1316 }
1319 }
1317 }
1320 }
1318
1321
1319 .reviewer_member_remove {
1322 .reviewer_member_remove {
1320 position: absolute;
1323 position: absolute;
1321 right: 0;
1324 right: 0;
1322 top: 0;
1325 top: 0;
1323 width: 16px;
1326 width: 16px;
1324 margin-bottom: 10px;
1327 margin-bottom: 10px;
1325 padding: 0;
1328 padding: 0;
1326 color: black;
1329 color: black;
1327 }
1330 }
1328 .reviewer_member_status {
1331 .reviewer_member_status {
1329 margin-top: 5px;
1332 margin-top: 5px;
1330 }
1333 }
1331 .pr-summary #summary{
1334 .pr-summary #summary{
1332 width: 100%;
1335 width: 100%;
1333 }
1336 }
1334 .pr-summary .action_button:hover {
1337 .pr-summary .action_button:hover {
1335 border: 0;
1338 border: 0;
1336 cursor: pointer;
1339 cursor: pointer;
1337 }
1340 }
1338 .pr-details-title {
1341 .pr-details-title {
1339 padding-bottom: 8px;
1342 padding-bottom: 8px;
1340 border-bottom: @border-thickness solid @grey5;
1343 border-bottom: @border-thickness solid @grey5;
1341 .action_button {
1344 .action_button {
1342 color: @rcblue;
1345 color: @rcblue;
1343 }
1346 }
1344 }
1347 }
1345 .pr-details-content {
1348 .pr-details-content {
1346 margin-top: @textmargin;
1349 margin-top: @textmargin;
1347 margin-bottom: @textmargin;
1350 margin-bottom: @textmargin;
1348 }
1351 }
1349 .pr-description {
1352 .pr-description {
1350 white-space:pre-wrap;
1353 white-space:pre-wrap;
1351 }
1354 }
1352 .group_members {
1355 .group_members {
1353 margin-top: 0;
1356 margin-top: 0;
1354 padding: 0;
1357 padding: 0;
1355 list-style: outside none none;
1358 list-style: outside none none;
1356 }
1359 }
1357 .reviewer_ac .ac-input {
1360 .reviewer_ac .ac-input {
1358 width: 92%;
1361 width: 92%;
1359 margin-bottom: 1em;
1362 margin-bottom: 1em;
1360 }
1363 }
1361 #update_commits {
1364 #update_commits {
1362 float: right;
1365 float: right;
1363 }
1366 }
1364 .compare_view_commits tr{
1367 .compare_view_commits tr{
1365 height: 20px;
1368 height: 20px;
1366 }
1369 }
1367 .compare_view_commits td {
1370 .compare_view_commits td {
1368 vertical-align: top;
1371 vertical-align: top;
1369 padding-top: 10px;
1372 padding-top: 10px;
1370 }
1373 }
1371 .compare_view_commits .author {
1374 .compare_view_commits .author {
1372 margin-left: 5px;
1375 margin-left: 5px;
1373 }
1376 }
1374
1377
1375 .compare_view_files {
1378 .compare_view_files {
1376 width: 100%;
1379 width: 100%;
1377
1380
1378 td {
1381 td {
1379 vertical-align: middle;
1382 vertical-align: middle;
1380 }
1383 }
1381 }
1384 }
1382
1385
1383 .compare_view_filepath {
1386 .compare_view_filepath {
1384 color: @grey1;
1387 color: @grey1;
1385 }
1388 }
1386
1389
1387 .show_more {
1390 .show_more {
1388 display: inline-block;
1391 display: inline-block;
1389 position: relative;
1392 position: relative;
1390 vertical-align: middle;
1393 vertical-align: middle;
1391 width: 4px;
1394 width: 4px;
1392 height: @basefontsize;
1395 height: @basefontsize;
1393
1396
1394 &:after {
1397 &:after {
1395 content: "\00A0\25BE";
1398 content: "\00A0\25BE";
1396 display: inline-block;
1399 display: inline-block;
1397 width:10px;
1400 width:10px;
1398 line-height: 5px;
1401 line-height: 5px;
1399 font-size: 12px;
1402 font-size: 12px;
1400 cursor: pointer;
1403 cursor: pointer;
1401 }
1404 }
1402 }
1405 }
1403
1406
1404 .journal_more .show_more {
1407 .journal_more .show_more {
1405 display: inline;
1408 display: inline;
1406
1409
1407 &:after {
1410 &:after {
1408 content: none;
1411 content: none;
1409 }
1412 }
1410 }
1413 }
1411
1414
1412 .open .show_more:after,
1415 .open .show_more:after,
1413 .select2-dropdown-open .show_more:after {
1416 .select2-dropdown-open .show_more:after {
1414 .rotate(180deg);
1417 .rotate(180deg);
1415 margin-left: 4px;
1418 margin-left: 4px;
1416 }
1419 }
1417
1420
1418
1421
1419 .compare_view_commits .collapse_commit:after {
1422 .compare_view_commits .collapse_commit:after {
1420 cursor: pointer;
1423 cursor: pointer;
1421 content: "\00A0\25B4";
1424 content: "\00A0\25B4";
1422 margin-left: -3px;
1425 margin-left: -3px;
1423 font-size: 17px;
1426 font-size: 17px;
1424 color: @grey4;
1427 color: @grey4;
1425 }
1428 }
1426
1429
1427 .diff_links {
1430 .diff_links {
1428 margin-left: 8px;
1431 margin-left: 8px;
1429 }
1432 }
1430
1433
1431 p.ancestor {
1434 p.ancestor {
1432 margin: @padding 0;
1435 margin: @padding 0;
1433 }
1436 }
1434
1437
1435 .cs_icon_td input[type="checkbox"] {
1438 .cs_icon_td input[type="checkbox"] {
1436 display: none;
1439 display: none;
1437 }
1440 }
1438
1441
1439 .cs_icon_td .expand_file_icon:after {
1442 .cs_icon_td .expand_file_icon:after {
1440 cursor: pointer;
1443 cursor: pointer;
1441 content: "\00A0\25B6";
1444 content: "\00A0\25B6";
1442 font-size: 12px;
1445 font-size: 12px;
1443 color: @grey4;
1446 color: @grey4;
1444 }
1447 }
1445
1448
1446 .cs_icon_td .collapse_file_icon:after {
1449 .cs_icon_td .collapse_file_icon:after {
1447 cursor: pointer;
1450 cursor: pointer;
1448 content: "\00A0\25BC";
1451 content: "\00A0\25BC";
1449 font-size: 12px;
1452 font-size: 12px;
1450 color: @grey4;
1453 color: @grey4;
1451 }
1454 }
1452
1455
1453 /*new binary
1456 /*new binary
1454 NEW_FILENODE = 1
1457 NEW_FILENODE = 1
1455 DEL_FILENODE = 2
1458 DEL_FILENODE = 2
1456 MOD_FILENODE = 3
1459 MOD_FILENODE = 3
1457 RENAMED_FILENODE = 4
1460 RENAMED_FILENODE = 4
1458 COPIED_FILENODE = 5
1461 COPIED_FILENODE = 5
1459 CHMOD_FILENODE = 6
1462 CHMOD_FILENODE = 6
1460 BIN_FILENODE = 7
1463 BIN_FILENODE = 7
1461 */
1464 */
1462 .cs_files_expand {
1465 .cs_files_expand {
1463 font-size: @basefontsize + 5px;
1466 font-size: @basefontsize + 5px;
1464 line-height: 1.8em;
1467 line-height: 1.8em;
1465 float: right;
1468 float: right;
1466 }
1469 }
1467
1470
1468 .cs_files_expand span{
1471 .cs_files_expand span{
1469 color: @rcblue;
1472 color: @rcblue;
1470 cursor: pointer;
1473 cursor: pointer;
1471 }
1474 }
1472 .cs_files {
1475 .cs_files {
1473 clear: both;
1476 clear: both;
1474 padding-bottom: @padding;
1477 padding-bottom: @padding;
1475
1478
1476 .cur_cs {
1479 .cur_cs {
1477 margin: 10px 2px;
1480 margin: 10px 2px;
1478 font-weight: bold;
1481 font-weight: bold;
1479 }
1482 }
1480
1483
1481 .node {
1484 .node {
1482 float: left;
1485 float: left;
1483 }
1486 }
1484
1487
1485 .changes {
1488 .changes {
1486 float: right;
1489 float: right;
1487 color: white;
1490 color: white;
1488 font-size: @basefontsize - 4px;
1491 font-size: @basefontsize - 4px;
1489 margin-top: 4px;
1492 margin-top: 4px;
1490 opacity: 0.6;
1493 opacity: 0.6;
1491 filter: Alpha(opacity=60); /* IE8 and earlier */
1494 filter: Alpha(opacity=60); /* IE8 and earlier */
1492
1495
1493 .added {
1496 .added {
1494 background-color: @alert1;
1497 background-color: @alert1;
1495 float: left;
1498 float: left;
1496 text-align: center;
1499 text-align: center;
1497 }
1500 }
1498
1501
1499 .deleted {
1502 .deleted {
1500 background-color: @alert2;
1503 background-color: @alert2;
1501 float: left;
1504 float: left;
1502 text-align: center;
1505 text-align: center;
1503 }
1506 }
1504
1507
1505 .bin {
1508 .bin {
1506 background-color: @alert1;
1509 background-color: @alert1;
1507 text-align: center;
1510 text-align: center;
1508 }
1511 }
1509
1512
1510 /*new binary*/
1513 /*new binary*/
1511 .bin.bin1 {
1514 .bin.bin1 {
1512 background-color: @alert1;
1515 background-color: @alert1;
1513 text-align: center;
1516 text-align: center;
1514 }
1517 }
1515
1518
1516 /*deleted binary*/
1519 /*deleted binary*/
1517 .bin.bin2 {
1520 .bin.bin2 {
1518 background-color: @alert2;
1521 background-color: @alert2;
1519 text-align: center;
1522 text-align: center;
1520 }
1523 }
1521
1524
1522 /*mod binary*/
1525 /*mod binary*/
1523 .bin.bin3 {
1526 .bin.bin3 {
1524 background-color: @grey2;
1527 background-color: @grey2;
1525 text-align: center;
1528 text-align: center;
1526 }
1529 }
1527
1530
1528 /*rename file*/
1531 /*rename file*/
1529 .bin.bin4 {
1532 .bin.bin4 {
1530 background-color: @alert4;
1533 background-color: @alert4;
1531 text-align: center;
1534 text-align: center;
1532 }
1535 }
1533
1536
1534 /*copied file*/
1537 /*copied file*/
1535 .bin.bin5 {
1538 .bin.bin5 {
1536 background-color: @alert4;
1539 background-color: @alert4;
1537 text-align: center;
1540 text-align: center;
1538 }
1541 }
1539
1542
1540 /*chmod file*/
1543 /*chmod file*/
1541 .bin.bin6 {
1544 .bin.bin6 {
1542 background-color: @grey2;
1545 background-color: @grey2;
1543 text-align: center;
1546 text-align: center;
1544 }
1547 }
1545 }
1548 }
1546 }
1549 }
1547
1550
1548 .cs_files .cs_added, .cs_files .cs_A,
1551 .cs_files .cs_added, .cs_files .cs_A,
1549 .cs_files .cs_added, .cs_files .cs_M,
1552 .cs_files .cs_added, .cs_files .cs_M,
1550 .cs_files .cs_added, .cs_files .cs_D {
1553 .cs_files .cs_added, .cs_files .cs_D {
1551 height: 16px;
1554 height: 16px;
1552 padding-right: 10px;
1555 padding-right: 10px;
1553 margin-top: 7px;
1556 margin-top: 7px;
1554 text-align: left;
1557 text-align: left;
1555 }
1558 }
1556
1559
1557 .cs_icon_td {
1560 .cs_icon_td {
1558 min-width: 16px;
1561 min-width: 16px;
1559 width: 16px;
1562 width: 16px;
1560 }
1563 }
1561
1564
1562 .pull-request-merge {
1565 .pull-request-merge {
1563 padding: 10px 0;
1566 padding: 10px 0;
1564 margin-top: 10px;
1567 margin-top: 10px;
1565 margin-bottom: 20px;
1568 margin-bottom: 20px;
1566 }
1569 }
1567
1570
1568 .pull-request-merge .pull-request-wrap {
1571 .pull-request-merge .pull-request-wrap {
1569 height: 25px;
1572 height: 25px;
1570 padding: 5px 0;
1573 padding: 5px 0;
1571 }
1574 }
1572
1575
1573 .pull-request-merge span {
1576 .pull-request-merge span {
1574 margin-right: 10px;
1577 margin-right: 10px;
1575 }
1578 }
1576 #close_pull_request {
1579 #close_pull_request {
1577 margin-right: 0px;
1580 margin-right: 0px;
1578 }
1581 }
1579
1582
1580 .empty_data {
1583 .empty_data {
1581 color: @grey4;
1584 color: @grey4;
1582 }
1585 }
1583
1586
1584 #changeset_compare_view_content {
1587 #changeset_compare_view_content {
1585 margin-bottom: @space;
1588 margin-bottom: @space;
1586 clear: both;
1589 clear: both;
1587 width: 100%;
1590 width: 100%;
1588 box-sizing: border-box;
1591 box-sizing: border-box;
1589 .border-radius(@border-radius);
1592 .border-radius(@border-radius);
1590
1593
1591 .help-block {
1594 .help-block {
1592 margin: @padding 0;
1595 margin: @padding 0;
1593 color: @text-color;
1596 color: @text-color;
1594 }
1597 }
1595
1598
1596 .empty_data {
1599 .empty_data {
1597 margin: @padding 0;
1600 margin: @padding 0;
1598 }
1601 }
1599
1602
1600 .alert {
1603 .alert {
1601 margin-bottom: @space;
1604 margin-bottom: @space;
1602 }
1605 }
1603 }
1606 }
1604
1607
1605 .table_disp {
1608 .table_disp {
1606 .status {
1609 .status {
1607 width: auto;
1610 width: auto;
1608
1611
1609 .flag_status {
1612 .flag_status {
1610 float: left;
1613 float: left;
1611 }
1614 }
1612 }
1615 }
1613 }
1616 }
1614
1617
1615 .status_box_menu {
1618 .status_box_menu {
1616 margin: 0;
1619 margin: 0;
1617 }
1620 }
1618
1621
1619 .notification-table{
1622 .notification-table{
1620 margin-bottom: @space;
1623 margin-bottom: @space;
1621 display: table;
1624 display: table;
1622 width: 100%;
1625 width: 100%;
1623
1626
1624 .container{
1627 .container{
1625 display: table-row;
1628 display: table-row;
1626
1629
1627 .notification-header{
1630 .notification-header{
1628 border-bottom: @border-thickness solid @border-default-color;
1631 border-bottom: @border-thickness solid @border-default-color;
1629 }
1632 }
1630
1633
1631 .notification-subject{
1634 .notification-subject{
1632 display: table-cell;
1635 display: table-cell;
1633 }
1636 }
1634 }
1637 }
1635 }
1638 }
1636
1639
1637 // Notifications
1640 // Notifications
1638 .notification-header{
1641 .notification-header{
1639 display: table;
1642 display: table;
1640 width: 100%;
1643 width: 100%;
1641 padding: floor(@basefontsize/2) 0;
1644 padding: floor(@basefontsize/2) 0;
1642 line-height: 1em;
1645 line-height: 1em;
1643
1646
1644 .desc, .delete-notifications, .read-notifications{
1647 .desc, .delete-notifications, .read-notifications{
1645 display: table-cell;
1648 display: table-cell;
1646 text-align: left;
1649 text-align: left;
1647 }
1650 }
1648
1651
1649 .desc{
1652 .desc{
1650 width: 1163px;
1653 width: 1163px;
1651 }
1654 }
1652
1655
1653 .delete-notifications, .read-notifications{
1656 .delete-notifications, .read-notifications{
1654 width: 35px;
1657 width: 35px;
1655 min-width: 35px; //fixes when only one button is displayed
1658 min-width: 35px; //fixes when only one button is displayed
1656 }
1659 }
1657 }
1660 }
1658
1661
1659 .notification-body {
1662 .notification-body {
1660 .markdown-block,
1663 .markdown-block,
1661 .rst-block {
1664 .rst-block {
1662 padding: @padding 0;
1665 padding: @padding 0;
1663 }
1666 }
1664
1667
1665 .notification-subject {
1668 .notification-subject {
1666 padding: @textmargin 0;
1669 padding: @textmargin 0;
1667 border-bottom: @border-thickness solid @border-default-color;
1670 border-bottom: @border-thickness solid @border-default-color;
1668 }
1671 }
1669 }
1672 }
1670
1673
1671
1674
1672 .notifications_buttons{
1675 .notifications_buttons{
1673 float: right;
1676 float: right;
1674 }
1677 }
1675
1678
1676 #notification-status{
1679 #notification-status{
1677 display: inline;
1680 display: inline;
1678 }
1681 }
1679
1682
1680 // Repositories
1683 // Repositories
1681
1684
1682 #summary.fields{
1685 #summary.fields{
1683 display: table;
1686 display: table;
1684
1687
1685 .field{
1688 .field{
1686 display: table-row;
1689 display: table-row;
1687
1690
1688 .label-summary{
1691 .label-summary{
1689 display: table-cell;
1692 display: table-cell;
1690 min-width: @label-summary-minwidth;
1693 min-width: @label-summary-minwidth;
1691 padding-top: @padding/2;
1694 padding-top: @padding/2;
1692 padding-bottom: @padding/2;
1695 padding-bottom: @padding/2;
1693 padding-right: @padding/2;
1696 padding-right: @padding/2;
1694 }
1697 }
1695
1698
1696 .input{
1699 .input{
1697 display: table-cell;
1700 display: table-cell;
1698 padding: @padding/2;
1701 padding: @padding/2;
1699
1702
1700 input{
1703 input{
1701 min-width: 29em;
1704 min-width: 29em;
1702 padding: @padding/4;
1705 padding: @padding/4;
1703 }
1706 }
1704 }
1707 }
1705 .statistics, .downloads{
1708 .statistics, .downloads{
1706 .disabled{
1709 .disabled{
1707 color: @grey4;
1710 color: @grey4;
1708 }
1711 }
1709 }
1712 }
1710 }
1713 }
1711 }
1714 }
1712
1715
1713 #summary{
1716 #summary{
1714 width: 70%;
1717 width: 70%;
1715 }
1718 }
1716
1719
1717
1720
1718 // Journal
1721 // Journal
1719 .journal.title {
1722 .journal.title {
1720 h5 {
1723 h5 {
1721 float: left;
1724 float: left;
1722 margin: 0;
1725 margin: 0;
1723 width: 70%;
1726 width: 70%;
1724 }
1727 }
1725
1728
1726 ul {
1729 ul {
1727 float: right;
1730 float: right;
1728 display: inline-block;
1731 display: inline-block;
1729 margin: 0;
1732 margin: 0;
1730 width: 30%;
1733 width: 30%;
1731 text-align: right;
1734 text-align: right;
1732
1735
1733 li {
1736 li {
1734 display: inline;
1737 display: inline;
1735 font-size: @journal-fontsize;
1738 font-size: @journal-fontsize;
1736 line-height: 1em;
1739 line-height: 1em;
1737
1740
1738 &:before { content: none; }
1741 &:before { content: none; }
1739 }
1742 }
1740 }
1743 }
1741 }
1744 }
1742
1745
1743 .filterexample {
1746 .filterexample {
1744 position: absolute;
1747 position: absolute;
1745 top: 95px;
1748 top: 95px;
1746 left: @contentpadding;
1749 left: @contentpadding;
1747 color: @rcblue;
1750 color: @rcblue;
1748 font-size: 11px;
1751 font-size: 11px;
1749 font-family: @text-regular;
1752 font-family: @text-regular;
1750 cursor: help;
1753 cursor: help;
1751
1754
1752 &:hover {
1755 &:hover {
1753 color: @rcdarkblue;
1756 color: @rcdarkblue;
1754 }
1757 }
1755
1758
1756 @media (max-width:768px) {
1759 @media (max-width:768px) {
1757 position: relative;
1760 position: relative;
1758 top: auto;
1761 top: auto;
1759 left: auto;
1762 left: auto;
1760 display: block;
1763 display: block;
1761 }
1764 }
1762 }
1765 }
1763
1766
1764
1767
1765 #journal{
1768 #journal{
1766 margin-bottom: @space;
1769 margin-bottom: @space;
1767
1770
1768 .journal_day{
1771 .journal_day{
1769 margin-bottom: @textmargin/2;
1772 margin-bottom: @textmargin/2;
1770 padding-bottom: @textmargin/2;
1773 padding-bottom: @textmargin/2;
1771 font-size: @journal-fontsize;
1774 font-size: @journal-fontsize;
1772 border-bottom: @border-thickness solid @border-default-color;
1775 border-bottom: @border-thickness solid @border-default-color;
1773 }
1776 }
1774
1777
1775 .journal_container{
1778 .journal_container{
1776 margin-bottom: @space;
1779 margin-bottom: @space;
1777
1780
1778 .journal_user{
1781 .journal_user{
1779 display: inline-block;
1782 display: inline-block;
1780 }
1783 }
1781 .journal_action_container{
1784 .journal_action_container{
1782 display: block;
1785 display: block;
1783 margin-top: @textmargin;
1786 margin-top: @textmargin;
1784
1787
1785 div{
1788 div{
1786 display: inline;
1789 display: inline;
1787 }
1790 }
1788
1791
1789 div.journal_action_params{
1792 div.journal_action_params{
1790 display: block;
1793 display: block;
1791 }
1794 }
1792
1795
1793 div.journal_repo:after{
1796 div.journal_repo:after{
1794 content: "\A";
1797 content: "\A";
1795 white-space: pre;
1798 white-space: pre;
1796 }
1799 }
1797
1800
1798 div.date{
1801 div.date{
1799 display: block;
1802 display: block;
1800 margin-bottom: @textmargin;
1803 margin-bottom: @textmargin;
1801 }
1804 }
1802 }
1805 }
1803 }
1806 }
1804 }
1807 }
1805
1808
1806 // Files
1809 // Files
1807 .edit-file-title {
1810 .edit-file-title {
1808 border-bottom: @border-thickness solid @border-default-color;
1811 border-bottom: @border-thickness solid @border-default-color;
1809
1812
1810 .breadcrumbs {
1813 .breadcrumbs {
1811 margin-bottom: 0;
1814 margin-bottom: 0;
1812 }
1815 }
1813 }
1816 }
1814
1817
1815 .edit-file-fieldset {
1818 .edit-file-fieldset {
1816 margin-top: @sidebarpadding;
1819 margin-top: @sidebarpadding;
1817
1820
1818 .fieldset {
1821 .fieldset {
1819 .left-label {
1822 .left-label {
1820 width: 13%;
1823 width: 13%;
1821 }
1824 }
1822 .right-content {
1825 .right-content {
1823 width: 87%;
1826 width: 87%;
1824 max-width: 100%;
1827 max-width: 100%;
1825 }
1828 }
1826 .filename-label {
1829 .filename-label {
1827 margin-top: 13px;
1830 margin-top: 13px;
1828 }
1831 }
1829 .commit-message-label {
1832 .commit-message-label {
1830 margin-top: 4px;
1833 margin-top: 4px;
1831 }
1834 }
1832 .file-upload-input {
1835 .file-upload-input {
1833 input {
1836 input {
1834 display: none;
1837 display: none;
1835 }
1838 }
1836 }
1839 }
1837 p {
1840 p {
1838 margin-top: 5px;
1841 margin-top: 5px;
1839 }
1842 }
1840
1843
1841 }
1844 }
1842 .custom-path-link {
1845 .custom-path-link {
1843 margin-left: 5px;
1846 margin-left: 5px;
1844 }
1847 }
1845 #commit {
1848 #commit {
1846 resize: vertical;
1849 resize: vertical;
1847 }
1850 }
1848 }
1851 }
1849
1852
1850 .delete-file-preview {
1853 .delete-file-preview {
1851 max-height: 250px;
1854 max-height: 250px;
1852 }
1855 }
1853
1856
1854 .new-file,
1857 .new-file,
1855 #filter_activate,
1858 #filter_activate,
1856 #filter_deactivate {
1859 #filter_deactivate {
1857 float: left;
1860 float: left;
1858 margin: 0 0 0 15px;
1861 margin: 0 0 0 15px;
1859 }
1862 }
1860
1863
1861 h3.files_location{
1864 h3.files_location{
1862 line-height: 2.4em;
1865 line-height: 2.4em;
1863 }
1866 }
1864
1867
1865 .browser-nav {
1868 .browser-nav {
1866 display: table;
1869 display: table;
1867 margin-bottom: @space;
1870 margin-bottom: @space;
1868
1871
1869
1872
1870 .info_box {
1873 .info_box {
1871 display: inline-table;
1874 display: inline-table;
1872 height: 2.5em;
1875 height: 2.5em;
1873
1876
1874 .browser-cur-rev, .info_box_elem {
1877 .browser-cur-rev, .info_box_elem {
1875 display: table-cell;
1878 display: table-cell;
1876 vertical-align: middle;
1879 vertical-align: middle;
1877 }
1880 }
1878
1881
1879 .info_box_elem {
1882 .info_box_elem {
1880 border-top: @border-thickness solid @rcblue;
1883 border-top: @border-thickness solid @rcblue;
1881 border-bottom: @border-thickness solid @rcblue;
1884 border-bottom: @border-thickness solid @rcblue;
1882
1885
1883 #at_rev, a {
1886 #at_rev, a {
1884 padding: 0.6em 0.9em;
1887 padding: 0.6em 0.9em;
1885 margin: 0;
1888 margin: 0;
1886 .box-shadow(none);
1889 .box-shadow(none);
1887 border: 0;
1890 border: 0;
1888 height: 12px;
1891 height: 12px;
1889 }
1892 }
1890
1893
1891 input#at_rev {
1894 input#at_rev {
1892 max-width: 50px;
1895 max-width: 50px;
1893 text-align: right;
1896 text-align: right;
1894 }
1897 }
1895
1898
1896 &.previous {
1899 &.previous {
1897 border: @border-thickness solid @rcblue;
1900 border: @border-thickness solid @rcblue;
1898 .disabled {
1901 .disabled {
1899 color: @grey4;
1902 color: @grey4;
1900 cursor: not-allowed;
1903 cursor: not-allowed;
1901 }
1904 }
1902 }
1905 }
1903
1906
1904 &.next {
1907 &.next {
1905 border: @border-thickness solid @rcblue;
1908 border: @border-thickness solid @rcblue;
1906 .disabled {
1909 .disabled {
1907 color: @grey4;
1910 color: @grey4;
1908 cursor: not-allowed;
1911 cursor: not-allowed;
1909 }
1912 }
1910 }
1913 }
1911 }
1914 }
1912
1915
1913 .browser-cur-rev {
1916 .browser-cur-rev {
1914
1917
1915 span{
1918 span{
1916 margin: 0;
1919 margin: 0;
1917 color: @rcblue;
1920 color: @rcblue;
1918 height: 12px;
1921 height: 12px;
1919 display: inline-block;
1922 display: inline-block;
1920 padding: 0.7em 1em ;
1923 padding: 0.7em 1em ;
1921 border: @border-thickness solid @rcblue;
1924 border: @border-thickness solid @rcblue;
1922 margin-right: @padding;
1925 margin-right: @padding;
1923 }
1926 }
1924 }
1927 }
1925 }
1928 }
1926
1929
1927 .search_activate {
1930 .search_activate {
1928 display: table-cell;
1931 display: table-cell;
1929 vertical-align: middle;
1932 vertical-align: middle;
1930
1933
1931 input, label{
1934 input, label{
1932 margin: 0;
1935 margin: 0;
1933 padding: 0;
1936 padding: 0;
1934 }
1937 }
1935
1938
1936 input{
1939 input{
1937 margin-left: @textmargin;
1940 margin-left: @textmargin;
1938 }
1941 }
1939
1942
1940 }
1943 }
1941 }
1944 }
1942
1945
1943 .browser-cur-rev{
1946 .browser-cur-rev{
1944 margin-bottom: @textmargin;
1947 margin-bottom: @textmargin;
1945 }
1948 }
1946
1949
1947 #node_filter_box_loading{
1950 #node_filter_box_loading{
1948 .info_text;
1951 .info_text;
1949 }
1952 }
1950
1953
1951 .browser-search {
1954 .browser-search {
1952 margin: -25px 0px 5px 0px;
1955 margin: -25px 0px 5px 0px;
1953 }
1956 }
1954
1957
1955 .node-filter {
1958 .node-filter {
1956 font-size: @repo-title-fontsize;
1959 font-size: @repo-title-fontsize;
1957 padding: 4px 0px 0px 0px;
1960 padding: 4px 0px 0px 0px;
1958
1961
1959 .node-filter-path {
1962 .node-filter-path {
1960 float: left;
1963 float: left;
1961 color: @grey4;
1964 color: @grey4;
1962 }
1965 }
1963 .node-filter-input {
1966 .node-filter-input {
1964 float: left;
1967 float: left;
1965 margin: -2px 0px 0px 2px;
1968 margin: -2px 0px 0px 2px;
1966 input {
1969 input {
1967 padding: 2px;
1970 padding: 2px;
1968 border: none;
1971 border: none;
1969 font-size: @repo-title-fontsize;
1972 font-size: @repo-title-fontsize;
1970 }
1973 }
1971 }
1974 }
1972 }
1975 }
1973
1976
1974
1977
1975 .browser-result{
1978 .browser-result{
1976 td a{
1979 td a{
1977 margin-left: 0.5em;
1980 margin-left: 0.5em;
1978 display: inline-block;
1981 display: inline-block;
1979
1982
1980 em{
1983 em{
1981 font-family: @text-bold;
1984 font-family: @text-bold;
1982 }
1985 }
1983 }
1986 }
1984 }
1987 }
1985
1988
1986 .browser-highlight{
1989 .browser-highlight{
1987 background-color: @grey5-alpha;
1990 background-color: @grey5-alpha;
1988 }
1991 }
1989
1992
1990
1993
1991 // Search
1994 // Search
1992
1995
1993 .search-form{
1996 .search-form{
1994 #q {
1997 #q {
1995 width: @search-form-width;
1998 width: @search-form-width;
1996 }
1999 }
1997 .fields{
2000 .fields{
1998 margin: 0 0 @space;
2001 margin: 0 0 @space;
1999 }
2002 }
2000
2003
2001 label{
2004 label{
2002 display: inline-block;
2005 display: inline-block;
2003 margin-right: @textmargin;
2006 margin-right: @textmargin;
2004 padding-top: 0.25em;
2007 padding-top: 0.25em;
2005 }
2008 }
2006
2009
2007
2010
2008 .results{
2011 .results{
2009 clear: both;
2012 clear: both;
2010 margin: 0 0 @padding;
2013 margin: 0 0 @padding;
2011 }
2014 }
2012 }
2015 }
2013
2016
2014 div.search-feedback-items {
2017 div.search-feedback-items {
2015 display: inline-block;
2018 display: inline-block;
2016 padding:0px 0px 0px 96px;
2019 padding:0px 0px 0px 96px;
2017 }
2020 }
2018
2021
2019 div.search-code-body {
2022 div.search-code-body {
2020 background-color: #ffffff; padding: 5px 0 5px 10px;
2023 background-color: #ffffff; padding: 5px 0 5px 10px;
2021 pre {
2024 pre {
2022 .match { background-color: #faffa6;}
2025 .match { background-color: #faffa6;}
2023 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2026 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2024 }
2027 }
2025 }
2028 }
2026
2029
2027 .expand_commit.search {
2030 .expand_commit.search {
2028 .show_more.open {
2031 .show_more.open {
2029 height: auto;
2032 height: auto;
2030 max-height: none;
2033 max-height: none;
2031 }
2034 }
2032 }
2035 }
2033
2036
2034 .search-results {
2037 .search-results {
2035
2038
2036 h2 {
2039 h2 {
2037 margin-bottom: 0;
2040 margin-bottom: 0;
2038 }
2041 }
2039 .codeblock {
2042 .codeblock {
2040 border: none;
2043 border: none;
2041 background: transparent;
2044 background: transparent;
2042 }
2045 }
2043
2046
2044 .codeblock-header {
2047 .codeblock-header {
2045 border: none;
2048 border: none;
2046 background: transparent;
2049 background: transparent;
2047 }
2050 }
2048
2051
2049 .code-body {
2052 .code-body {
2050 border: @border-thickness solid @border-default-color;
2053 border: @border-thickness solid @border-default-color;
2051 .border-radius(@border-radius);
2054 .border-radius(@border-radius);
2052 }
2055 }
2053
2056
2054 .td-commit {
2057 .td-commit {
2055 &:extend(pre);
2058 &:extend(pre);
2056 border-bottom: @border-thickness solid @border-default-color;
2059 border-bottom: @border-thickness solid @border-default-color;
2057 }
2060 }
2058
2061
2059 .message {
2062 .message {
2060 height: auto;
2063 height: auto;
2061 max-width: 350px;
2064 max-width: 350px;
2062 white-space: normal;
2065 white-space: normal;
2063 text-overflow: initial;
2066 text-overflow: initial;
2064 overflow: visible;
2067 overflow: visible;
2065
2068
2066 .match { background-color: #faffa6;}
2069 .match { background-color: #faffa6;}
2067 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2070 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2068 }
2071 }
2069
2072
2070 }
2073 }
2071
2074
2072 table.rctable td.td-search-results div {
2075 table.rctable td.td-search-results div {
2073 max-width: 100%;
2076 max-width: 100%;
2074 }
2077 }
2075
2078
2076 #tip-box, .tip-box{
2079 #tip-box, .tip-box{
2077 padding: @menupadding/2;
2080 padding: @menupadding/2;
2078 display: block;
2081 display: block;
2079 border: @border-thickness solid @border-highlight-color;
2082 border: @border-thickness solid @border-highlight-color;
2080 .border-radius(@border-radius);
2083 .border-radius(@border-radius);
2081 background-color: white;
2084 background-color: white;
2082 z-index: 99;
2085 z-index: 99;
2083 white-space: pre-wrap;
2086 white-space: pre-wrap;
2084 }
2087 }
2085
2088
2086 #linktt {
2089 #linktt {
2087 width: 79px;
2090 width: 79px;
2088 }
2091 }
2089
2092
2090 #help_kb .modal-content{
2093 #help_kb .modal-content{
2091 max-width: 750px;
2094 max-width: 750px;
2092 margin: 10% auto;
2095 margin: 10% auto;
2093
2096
2094 table{
2097 table{
2095 td,th{
2098 td,th{
2096 border-bottom: none;
2099 border-bottom: none;
2097 line-height: 2.5em;
2100 line-height: 2.5em;
2098 }
2101 }
2099 th{
2102 th{
2100 padding-bottom: @textmargin/2;
2103 padding-bottom: @textmargin/2;
2101 }
2104 }
2102 td.keys{
2105 td.keys{
2103 text-align: center;
2106 text-align: center;
2104 }
2107 }
2105 }
2108 }
2106
2109
2107 .block-left{
2110 .block-left{
2108 width: 45%;
2111 width: 45%;
2109 margin-right: 5%;
2112 margin-right: 5%;
2110 }
2113 }
2111 .modal-footer{
2114 .modal-footer{
2112 clear: both;
2115 clear: both;
2113 }
2116 }
2114 .key.tag{
2117 .key.tag{
2115 padding: 0.5em;
2118 padding: 0.5em;
2116 background-color: @rcblue;
2119 background-color: @rcblue;
2117 color: white;
2120 color: white;
2118 border-color: @rcblue;
2121 border-color: @rcblue;
2119 .box-shadow(none);
2122 .box-shadow(none);
2120 }
2123 }
2121 }
2124 }
2122
2125
2123
2126
2124
2127
2125 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2128 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2126
2129
2127 @import 'statistics-graph';
2130 @import 'statistics-graph';
2128 @import 'tables';
2131 @import 'tables';
2129 @import 'forms';
2132 @import 'forms';
2130 @import 'diff';
2133 @import 'diff';
2131 @import 'summary';
2134 @import 'summary';
2132 @import 'navigation';
2135 @import 'navigation';
2133
2136
2134 //--- SHOW/HIDE SECTIONS --//
2137 //--- SHOW/HIDE SECTIONS --//
2135
2138
2136 .btn-collapse {
2139 .btn-collapse {
2137 float: right;
2140 float: right;
2138 text-align: right;
2141 text-align: right;
2139 font-family: @text-light;
2142 font-family: @text-light;
2140 font-size: @basefontsize;
2143 font-size: @basefontsize;
2141 cursor: pointer;
2144 cursor: pointer;
2142 border: none;
2145 border: none;
2143 color: @rcblue;
2146 color: @rcblue;
2144 }
2147 }
2145
2148
2146 table.rctable,
2149 table.rctable,
2147 table.dataTable {
2150 table.dataTable {
2148 .btn-collapse {
2151 .btn-collapse {
2149 float: right;
2152 float: right;
2150 text-align: right;
2153 text-align: right;
2151 }
2154 }
2152 }
2155 }
2153
2156
2154
2157
2155 // TODO: johbo: Fix for IE10, this avoids that we see a border
2158 // TODO: johbo: Fix for IE10, this avoids that we see a border
2156 // and padding around checkboxes and radio boxes. Move to the right place,
2159 // and padding around checkboxes and radio boxes. Move to the right place,
2157 // or better: Remove this once we did the form refactoring.
2160 // or better: Remove this once we did the form refactoring.
2158 input[type=checkbox],
2161 input[type=checkbox],
2159 input[type=radio] {
2162 input[type=radio] {
2160 padding: 0;
2163 padding: 0;
2161 border: none;
2164 border: none;
2162 }
2165 }
2163
2166
2164 .toggle-ajax-spinner{
2167 .toggle-ajax-spinner{
2165 height: 16px;
2168 height: 16px;
2166 width: 16px;
2169 width: 16px;
2167 }
2170 }
@@ -1,214 +1,217 b''
1 // # Copyright (C) 2010-2016 RhodeCode GmbH
1 // # Copyright (C) 2010-2016 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
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 /**
19 /**
20 * Pull request reviewers
20 * Pull request reviewers
21 */
21 */
22 var removeReviewMember = function(reviewer_id, mark_delete){
22 var removeReviewMember = function(reviewer_id, mark_delete){
23 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
23 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
24
24
25 if(typeof(mark_delete) === undefined){
25 if(typeof(mark_delete) === undefined){
26 mark_delete = false;
26 mark_delete = false;
27 }
27 }
28
28
29 if(mark_delete === true){
29 if(mark_delete === true){
30 if (reviewer){
30 if (reviewer){
31 // mark as to-remove
31 // mark as to-remove
32 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
32 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
33 obj.addClass('to-delete');
33 obj.addClass('to-delete');
34 // now delete the input
34 // now delete the input
35 $('#reviewer_{0}_input'.format(reviewer_id)).remove();
35 $('#reviewer_{0} input'.format(reviewer_id)).remove();
36 }
36 }
37 }
37 }
38 else{
38 else{
39 $('#reviewer_{0}'.format(reviewer_id)).remove();
39 $('#reviewer_{0}'.format(reviewer_id)).remove();
40 }
40 }
41 };
41 };
42
42
43 var addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons) {
43 var addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons) {
44 var members = $('#review_members').get(0);
44 var members = $('#review_members').get(0);
45 var reasons_html = '';
45 var reasons_html = '';
46 var reasons_inputs = '';
47 var reasons = reasons || [];
46 if (reasons) {
48 if (reasons) {
47 for (var i = 0; i < reasons.length; i++) {
49 for (var i = 0; i < reasons.length; i++) {
48 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(
50 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
49 reasons[i]
51 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
50 );
51 }
52 }
52 }
53 }
53 var tmpl = '<li id="reviewer_{2}">'+
54 var tmpl = '<li id="reviewer_{2}">'+
55 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
54 '<div class="reviewer_status">'+
56 '<div class="reviewer_status">'+
55 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
57 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
56 '</div>'+
58 '</div>'+
57 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
59 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
58 '<span class="reviewer_name user">{1}</span>'+
60 '<span class="reviewer_name user">{1}</span>'+
59 reasons_html +
61 reasons_html +
60 '<input type="hidden" value="{2}" name="review_members" />'+
62 '<input type="hidden" name="user_id" value="{2}">'+
63 '<input type="hidden" name="__start__" value="reasons:sequence">'+
64 '{3}'+
65 '<input type="hidden" name="__end__" value="reasons:sequence">'+
61 '<div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">' +
66 '<div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">' +
62 '<i class="icon-remove-sign"></i>'+
67 '<i class="icon-remove-sign"></i>'+
63 '</div>'+
68 '</div>'+
64 '</div>'+
69 '</div>'+
70 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
65 '</li>' ;
71 '</li>' ;
72
66 var displayname = "{0} ({1} {2})".format(
73 var displayname = "{0} ({1} {2})".format(
67 nname, escapeHtml(fname), escapeHtml(lname));
74 nname, escapeHtml(fname), escapeHtml(lname));
68 var element = tmpl.format(gravatar_link,displayname,id);
75 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
69 // check if we don't have this ID already in
76 // check if we don't have this ID already in
70 var ids = [];
77 var ids = [];
71 var _els = $('#review_members li').toArray();
78 var _els = $('#review_members li').toArray();
72 for (el in _els){
79 for (el in _els){
73 ids.push(_els[el].id)
80 ids.push(_els[el].id)
74 }
81 }
75 if(ids.indexOf('reviewer_'+id) == -1){
82 if(ids.indexOf('reviewer_'+id) == -1){
76 // only add if it's not there
83 // only add if it's not there
77 members.innerHTML += element;
84 members.innerHTML += element;
78 }
85 }
79
86
80 };
87 };
81
88
82 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
89 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
83 var url = pyroutes.url(
90 var url = pyroutes.url(
84 'pullrequest_update',
91 'pullrequest_update',
85 {"repo_name": repo_name, "pull_request_id": pull_request_id});
92 {"repo_name": repo_name, "pull_request_id": pull_request_id});
86 postData.csrf_token = CSRF_TOKEN;
93 if (typeof postData === 'string' ) {
94 postData += '&csrf_token=' + CSRF_TOKEN;
95 } else {
96 postData.csrf_token = CSRF_TOKEN;
97 }
87 var success = function(o) {
98 var success = function(o) {
88 window.location.reload();
99 window.location.reload();
89 };
100 };
90 ajaxPOST(url, postData, success);
101 ajaxPOST(url, postData, success);
91 };
102 };
92
103
93 var updateReviewers = function(reviewers_ids, repo_name, pull_request_id){
104 var updateReviewers = function(reviewers_ids, repo_name, pull_request_id){
94 if (reviewers_ids === undefined){
105 if (reviewers_ids === undefined){
95 var reviewers_ids = [];
106 var postData = '_method=put&' + $('#reviewers input').serialize();
96 var ids = $('#review_members input').toArray();
107 _updatePullRequest(repo_name, pull_request_id, postData);
97 for(var i=0; i<ids.length;i++){
98 var id = ids[i].value
99 reviewers_ids.push(id);
100 }
101 }
108 }
102 var postData = {
103 '_method':'put',
104 'reviewers_ids': reviewers_ids};
105 _updatePullRequest(repo_name, pull_request_id, postData);
106 };
109 };
107
110
108 /**
111 /**
109 * PULL REQUEST reject & close
112 * PULL REQUEST reject & close
110 */
113 */
111 var closePullRequest = function(repo_name, pull_request_id) {
114 var closePullRequest = function(repo_name, pull_request_id) {
112 var postData = {
115 var postData = {
113 '_method': 'put',
116 '_method': 'put',
114 'close_pull_request': true};
117 'close_pull_request': true};
115 _updatePullRequest(repo_name, pull_request_id, postData);
118 _updatePullRequest(repo_name, pull_request_id, postData);
116 };
119 };
117
120
118 /**
121 /**
119 * PULL REQUEST update commits
122 * PULL REQUEST update commits
120 */
123 */
121 var updateCommits = function(repo_name, pull_request_id) {
124 var updateCommits = function(repo_name, pull_request_id) {
122 var postData = {
125 var postData = {
123 '_method': 'put',
126 '_method': 'put',
124 'update_commits': true};
127 'update_commits': true};
125 _updatePullRequest(repo_name, pull_request_id, postData);
128 _updatePullRequest(repo_name, pull_request_id, postData);
126 };
129 };
127
130
128
131
129 /**
132 /**
130 * PULL REQUEST edit info
133 * PULL REQUEST edit info
131 */
134 */
132 var editPullRequest = function(repo_name, pull_request_id, title, description) {
135 var editPullRequest = function(repo_name, pull_request_id, title, description) {
133 var url = pyroutes.url(
136 var url = pyroutes.url(
134 'pullrequest_update',
137 'pullrequest_update',
135 {"repo_name": repo_name, "pull_request_id": pull_request_id});
138 {"repo_name": repo_name, "pull_request_id": pull_request_id});
136
139
137 var postData = {
140 var postData = {
138 '_method': 'put',
141 '_method': 'put',
139 'title': title,
142 'title': title,
140 'description': description,
143 'description': description,
141 'edit_pull_request': true,
144 'edit_pull_request': true,
142 'csrf_token': CSRF_TOKEN
145 'csrf_token': CSRF_TOKEN
143 };
146 };
144 var success = function(o) {
147 var success = function(o) {
145 window.location.reload();
148 window.location.reload();
146 };
149 };
147 ajaxPOST(url, postData, success);
150 ajaxPOST(url, postData, success);
148 };
151 };
149
152
150 var initPullRequestsCodeMirror = function (textAreaId) {
153 var initPullRequestsCodeMirror = function (textAreaId) {
151 var ta = $(textAreaId).get(0);
154 var ta = $(textAreaId).get(0);
152 var initialHeight = '100px';
155 var initialHeight = '100px';
153
156
154 // default options
157 // default options
155 var codeMirrorOptions = {
158 var codeMirrorOptions = {
156 mode: "text",
159 mode: "text",
157 lineNumbers: false,
160 lineNumbers: false,
158 indentUnit: 4,
161 indentUnit: 4,
159 theme: 'rc-input'
162 theme: 'rc-input'
160 };
163 };
161
164
162 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
165 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
163 // marker for manually set description
166 // marker for manually set description
164 codeMirrorInstance._userDefinedDesc = false;
167 codeMirrorInstance._userDefinedDesc = false;
165 codeMirrorInstance.setSize(null, initialHeight);
168 codeMirrorInstance.setSize(null, initialHeight);
166 codeMirrorInstance.on("change", function(instance, changeObj) {
169 codeMirrorInstance.on("change", function(instance, changeObj) {
167 var height = initialHeight;
170 var height = initialHeight;
168 var lines = instance.lineCount();
171 var lines = instance.lineCount();
169 if (lines > 6 && lines < 20) {
172 if (lines > 6 && lines < 20) {
170 height = "auto"
173 height = "auto"
171 }
174 }
172 else if (lines >= 20) {
175 else if (lines >= 20) {
173 height = 20 * 15;
176 height = 20 * 15;
174 }
177 }
175 instance.setSize(null, height);
178 instance.setSize(null, height);
176
179
177 // detect if the change was trigger by auto desc, or user input
180 // detect if the change was trigger by auto desc, or user input
178 changeOrigin = changeObj.origin;
181 changeOrigin = changeObj.origin;
179
182
180 if (changeOrigin === "setValue") {
183 if (changeOrigin === "setValue") {
181 cmLog.debug('Change triggered by setValue');
184 cmLog.debug('Change triggered by setValue');
182 }
185 }
183 else {
186 else {
184 cmLog.debug('user triggered change !');
187 cmLog.debug('user triggered change !');
185 // set special marker to indicate user has created an input.
188 // set special marker to indicate user has created an input.
186 instance._userDefinedDesc = true;
189 instance._userDefinedDesc = true;
187 }
190 }
188
191
189 });
192 });
190
193
191 return codeMirrorInstance
194 return codeMirrorInstance
192 };
195 };
193
196
194 /**
197 /**
195 * Reviewer autocomplete
198 * Reviewer autocomplete
196 */
199 */
197 var ReviewerAutoComplete = function(input_id) {
200 var ReviewerAutoComplete = function(input_id) {
198 $('#'+input_id).autocomplete({
201 $('#'+input_id).autocomplete({
199 serviceUrl: pyroutes.url('user_autocomplete_data'),
202 serviceUrl: pyroutes.url('user_autocomplete_data'),
200 minChars:2,
203 minChars:2,
201 maxHeight:400,
204 maxHeight:400,
202 deferRequestBy: 300, //miliseconds
205 deferRequestBy: 300, //miliseconds
203 showNoSuggestionNotice: true,
206 showNoSuggestionNotice: true,
204 tabDisabled: true,
207 tabDisabled: true,
205 autoSelectFirst: true,
208 autoSelectFirst: true,
206 formatResult: autocompleteFormatResult,
209 formatResult: autocompleteFormatResult,
207 lookupFilter: autocompleteFilterResult,
210 lookupFilter: autocompleteFilterResult,
208 onSelect: function(suggestion, data){
211 onSelect: function(suggestion, data){
209 addReviewMember(data.id, data.first_name, data.last_name,
212 addReviewMember(data.id, data.first_name, data.last_name,
210 data.username, data.icon_link);
213 data.username, data.icon_link);
211 $('#'+input_id).val('');
214 $('#'+input_id).val('');
212 }
215 }
213 });
216 });
214 };
217 };
@@ -1,59 +1,62 b''
1 // # Copyright (C) 2010-2016 RhodeCode GmbH
1 // # Copyright (C) 2010-2016 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
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 /**
19 /**
20 * turns objects into GET query string
20 * turns objects into GET query string
21 */
21 */
22 var toQueryString = function(o) {
22 var toQueryString = function(o) {
23 if(typeof o === 'string') {
24 return o;
25 }
23 if(typeof o !== 'object') {
26 if(typeof o !== 'object') {
24 return false;
27 return false;
25 }
28 }
26 var _p, _qs = [];
29 var _p, _qs = [];
27 for(_p in o) {
30 for(_p in o) {
28 _qs.push(encodeURIComponent(_p) + '=' + encodeURIComponent(o[_p]));
31 _qs.push(encodeURIComponent(_p) + '=' + encodeURIComponent(o[_p]));
29 }
32 }
30 return _qs.join('&');
33 return _qs.join('&');
31 };
34 };
32
35
33 /**
36 /**
34 * ajax call wrappers
37 * ajax call wrappers
35 */
38 */
36 var ajaxGET = function(url, success) {
39 var ajaxGET = function(url, success) {
37 var sUrl = url;
40 var sUrl = url;
38 var request = $.ajax({url: sUrl, headers: {'X-PARTIAL-XHR': true}})
41 var request = $.ajax({url: sUrl, headers: {'X-PARTIAL-XHR': true}})
39 .done(function(data){
42 .done(function(data){
40 success(data);
43 success(data);
41 })
44 })
42 .fail(function(data, textStatus, xhr){
45 .fail(function(data, textStatus, xhr){
43 alert("error processing request: " + textStatus);
46 alert("error processing request: " + textStatus);
44 });
47 });
45 return request;
48 return request;
46 };
49 };
47 var ajaxPOST = function(url,postData,success) {
50 var ajaxPOST = function(url,postData,success) {
48 var sUrl = url;
51 var sUrl = url;
49 var postData = toQueryString(postData);
52 var postData = toQueryString(postData);
50 var request = $.ajax({type: 'POST', data: postData, url: sUrl,
53 var request = $.ajax({type: 'POST', data: postData, url: sUrl,
51 headers: {'X-PARTIAL-XHR': true}})
54 headers: {'X-PARTIAL-XHR': true}})
52 .done(function(data){
55 .done(function(data){
53 success(data);
56 success(data);
54 })
57 })
55 .fail(function(data, textStatus, xhr){
58 .fail(function(data, textStatus, xhr){
56 alert("error processing request: " + textStatus);
59 alert("error processing request: " + textStatus);
57 });
60 });
58 return request;
61 return request;
59 };
62 };
@@ -1,567 +1,569 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${c.repo_name} ${_('New pull request')}
4 ${c.repo_name} ${_('New pull request')}
5 </%def>
5 </%def>
6
6
7 <%def name="breadcrumbs_links()">
7 <%def name="breadcrumbs_links()">
8 ${_('New pull request')}
8 ${_('New pull request')}
9 </%def>
9 </%def>
10
10
11 <%def name="menu_bar_nav()">
11 <%def name="menu_bar_nav()">
12 ${self.menu_items(active='repositories')}
12 ${self.menu_items(active='repositories')}
13 </%def>
13 </%def>
14
14
15 <%def name="menu_bar_subnav()">
15 <%def name="menu_bar_subnav()">
16 ${self.repo_menu(active='showpullrequest')}
16 ${self.repo_menu(active='showpullrequest')}
17 </%def>
17 </%def>
18
18
19 <%def name="main()">
19 <%def name="main()">
20 <div class="box">
20 <div class="box">
21 <div class="title">
21 <div class="title">
22 ${self.repo_page_title(c.rhodecode_db_repo)}
22 ${self.repo_page_title(c.rhodecode_db_repo)}
23 ${self.breadcrumbs()}
23 ${self.breadcrumbs()}
24 </div>
24 </div>
25
25
26 ${h.secure_form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
26 ${h.secure_form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
27 <div class="box pr-summary">
27 <div class="box pr-summary">
28
28
29 <div class="summary-details block-left">
29 <div class="summary-details block-left">
30
30
31 <div class="form">
31 <div class="form">
32 <!-- fields -->
32 <!-- fields -->
33
33
34 <div class="fields" >
34 <div class="fields" >
35
35
36 <div class="field">
36 <div class="field">
37 <div class="label">
37 <div class="label">
38 <label for="pullrequest_title">${_('Title')}:</label>
38 <label for="pullrequest_title">${_('Title')}:</label>
39 </div>
39 </div>
40 <div class="input">
40 <div class="input">
41 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
41 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
42 </div>
42 </div>
43 </div>
43 </div>
44
44
45 <div class="field">
45 <div class="field">
46 <div class="label label-textarea">
46 <div class="label label-textarea">
47 <label for="pullrequest_desc">${_('Description')}:</label>
47 <label for="pullrequest_desc">${_('Description')}:</label>
48 </div>
48 </div>
49 <div class="textarea text-area editor">
49 <div class="textarea text-area editor">
50 ${h.textarea('pullrequest_desc',size=30, )}
50 ${h.textarea('pullrequest_desc',size=30, )}
51 <span class="help-block">
51 <span class="help-block">
52 ${_('Write a short description on this pull request')}
52 ${_('Write a short description on this pull request')}
53 </span>
53 </span>
54 </div>
54 </div>
55 </div>
55 </div>
56
56
57 <div class="field">
57 <div class="field">
58 <div class="label label-textarea">
58 <div class="label label-textarea">
59 <label for="pullrequest_desc">${_('Commit flow')}:</label>
59 <label for="pullrequest_desc">${_('Commit flow')}:</label>
60 </div>
60 </div>
61
61
62 ## TODO: johbo: Abusing the "content" class here to get the
62 ## TODO: johbo: Abusing the "content" class here to get the
63 ## desired effect. Should be replaced by a proper solution.
63 ## desired effect. Should be replaced by a proper solution.
64
64
65 ##ORG
65 ##ORG
66 <div class="content">
66 <div class="content">
67 <strong>${_('Origin repository')}:</strong>
67 <strong>${_('Origin repository')}:</strong>
68 ${c.rhodecode_db_repo.description}
68 ${c.rhodecode_db_repo.description}
69 </div>
69 </div>
70 <div class="content">
70 <div class="content">
71 ${h.hidden('source_repo')}
71 ${h.hidden('source_repo')}
72 ${h.hidden('source_ref')}
72 ${h.hidden('source_ref')}
73 </div>
73 </div>
74
74
75 ##OTHER, most Probably the PARENT OF THIS FORK
75 ##OTHER, most Probably the PARENT OF THIS FORK
76 <div class="content">
76 <div class="content">
77 ## filled with JS
77 ## filled with JS
78 <div id="target_repo_desc"></div>
78 <div id="target_repo_desc"></div>
79 </div>
79 </div>
80
80
81 <div class="content">
81 <div class="content">
82 ${h.hidden('target_repo')}
82 ${h.hidden('target_repo')}
83 ${h.hidden('target_ref')}
83 ${h.hidden('target_ref')}
84 <span id="target_ref_loading" style="display: none">
84 <span id="target_ref_loading" style="display: none">
85 ${_('Loading refs...')}
85 ${_('Loading refs...')}
86 </span>
86 </span>
87 </div>
87 </div>
88 </div>
88 </div>
89
89
90 <div class="field">
90 <div class="field">
91 <div class="label label-textarea">
91 <div class="label label-textarea">
92 <label for="pullrequest_submit"></label>
92 <label for="pullrequest_submit"></label>
93 </div>
93 </div>
94 <div class="input">
94 <div class="input">
95 <div class="pr-submit-button">
95 <div class="pr-submit-button">
96 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
96 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
97 </div>
97 </div>
98 <div id="pr_open_message"></div>
98 <div id="pr_open_message"></div>
99 </div>
99 </div>
100 </div>
100 </div>
101
101
102 <div class="pr-spacing-container"></div>
102 <div class="pr-spacing-container"></div>
103 </div>
103 </div>
104 </div>
104 </div>
105 </div>
105 </div>
106 <div>
106 <div>
107 <div class="reviewers-title block-right">
107 <div class="reviewers-title block-right">
108 <div class="pr-details-title">
108 <div class="pr-details-title">
109 ${_('Pull request reviewers')}
109 ${_('Pull request reviewers')}
110 </div>
110 </div>
111 </div>
111 </div>
112 <div id="reviewers" class="block-right pr-details-content reviewers">
112 <div id="reviewers" class="block-right pr-details-content reviewers">
113 ## members goes here, filled via JS based on initial selection !
113 ## members goes here, filled via JS based on initial selection !
114 <input type="hidden" name="__start__" value="review_members:sequence">
114 <ul id="review_members" class="group_members"></ul>
115 <ul id="review_members" class="group_members"></ul>
116 <input type="hidden" name="__end__" value="review_members:sequence">
115 <div id="add_reviewer_input" class='ac'>
117 <div id="add_reviewer_input" class='ac'>
116 <div class="reviewer_ac">
118 <div class="reviewer_ac">
117 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
119 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
118 <div id="reviewers_container"></div>
120 <div id="reviewers_container"></div>
119 </div>
121 </div>
120 </div>
122 </div>
121 </div>
123 </div>
122 </div>
124 </div>
123 </div>
125 </div>
124 <div class="box">
126 <div class="box">
125 <div>
127 <div>
126 ## overview pulled by ajax
128 ## overview pulled by ajax
127 <div id="pull_request_overview"></div>
129 <div id="pull_request_overview"></div>
128 </div>
130 </div>
129 </div>
131 </div>
130 ${h.end_form()}
132 ${h.end_form()}
131 </div>
133 </div>
132
134
133 <script type="text/javascript">
135 <script type="text/javascript">
134 $(function(){
136 $(function(){
135 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
137 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
136 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
138 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
137 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
139 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
138 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
140 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
139 var targetRepoName = '${c.repo_name}';
141 var targetRepoName = '${c.repo_name}';
140
142
141 var $pullRequestForm = $('#pull_request_form');
143 var $pullRequestForm = $('#pull_request_form');
142 var $sourceRepo = $('#source_repo', $pullRequestForm);
144 var $sourceRepo = $('#source_repo', $pullRequestForm);
143 var $targetRepo = $('#target_repo', $pullRequestForm);
145 var $targetRepo = $('#target_repo', $pullRequestForm);
144 var $sourceRef = $('#source_ref', $pullRequestForm);
146 var $sourceRef = $('#source_ref', $pullRequestForm);
145 var $targetRef = $('#target_ref', $pullRequestForm);
147 var $targetRef = $('#target_ref', $pullRequestForm);
146
148
147 var calculateContainerWidth = function() {
149 var calculateContainerWidth = function() {
148 var maxWidth = 0;
150 var maxWidth = 0;
149 var repoSelect2Containers = ['#source_repo', '#target_repo'];
151 var repoSelect2Containers = ['#source_repo', '#target_repo'];
150 $.each(repoSelect2Containers, function(idx, value) {
152 $.each(repoSelect2Containers, function(idx, value) {
151 $(value).select2('container').width('auto');
153 $(value).select2('container').width('auto');
152 var curWidth = $(value).select2('container').width();
154 var curWidth = $(value).select2('container').width();
153 if (maxWidth <= curWidth) {
155 if (maxWidth <= curWidth) {
154 maxWidth = curWidth;
156 maxWidth = curWidth;
155 }
157 }
156 $.each(repoSelect2Containers, function(idx, value) {
158 $.each(repoSelect2Containers, function(idx, value) {
157 $(value).select2('container').width(maxWidth + 10);
159 $(value).select2('container').width(maxWidth + 10);
158 });
160 });
159 });
161 });
160 };
162 };
161
163
162 var initRefSelection = function(selectedRef) {
164 var initRefSelection = function(selectedRef) {
163 return function(element, callback) {
165 return function(element, callback) {
164 // translate our select2 id into a text, it's a mapping to show
166 // translate our select2 id into a text, it's a mapping to show
165 // simple label when selecting by internal ID.
167 // simple label when selecting by internal ID.
166 var id, refData;
168 var id, refData;
167 if (selectedRef === undefined) {
169 if (selectedRef === undefined) {
168 id = element.val();
170 id = element.val();
169 refData = element.val().split(':');
171 refData = element.val().split(':');
170 } else {
172 } else {
171 id = selectedRef;
173 id = selectedRef;
172 refData = selectedRef.split(':');
174 refData = selectedRef.split(':');
173 }
175 }
174
176
175 var text = refData[1];
177 var text = refData[1];
176 if (refData[0] === 'rev') {
178 if (refData[0] === 'rev') {
177 text = text.substring(0, 12);
179 text = text.substring(0, 12);
178 }
180 }
179
181
180 var data = {id: id, text: text};
182 var data = {id: id, text: text};
181
183
182 callback(data);
184 callback(data);
183 };
185 };
184 };
186 };
185
187
186 var formatRefSelection = function(item) {
188 var formatRefSelection = function(item) {
187 var prefix = '';
189 var prefix = '';
188 var refData = item.id.split(':');
190 var refData = item.id.split(':');
189 if (refData[0] === 'branch') {
191 if (refData[0] === 'branch') {
190 prefix = '<i class="icon-branch"></i>';
192 prefix = '<i class="icon-branch"></i>';
191 }
193 }
192 else if (refData[0] === 'book') {
194 else if (refData[0] === 'book') {
193 prefix = '<i class="icon-bookmark"></i>';
195 prefix = '<i class="icon-bookmark"></i>';
194 }
196 }
195 else if (refData[0] === 'tag') {
197 else if (refData[0] === 'tag') {
196 prefix = '<i class="icon-tag"></i>';
198 prefix = '<i class="icon-tag"></i>';
197 }
199 }
198
200
199 var originalOption = item.element;
201 var originalOption = item.element;
200 return prefix + item.text;
202 return prefix + item.text;
201 };
203 };
202
204
203 // custom code mirror
205 // custom code mirror
204 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
206 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
205
207
206 var queryTargetRepo = function(self, query) {
208 var queryTargetRepo = function(self, query) {
207 // cache ALL results if query is empty
209 // cache ALL results if query is empty
208 var cacheKey = query.term || '__';
210 var cacheKey = query.term || '__';
209 var cachedData = self.cachedDataSource[cacheKey];
211 var cachedData = self.cachedDataSource[cacheKey];
210
212
211 if (cachedData) {
213 if (cachedData) {
212 query.callback({results: cachedData.results});
214 query.callback({results: cachedData.results});
213 } else {
215 } else {
214 $.ajax({
216 $.ajax({
215 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': targetRepoName}),
217 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': targetRepoName}),
216 data: {query: query.term},
218 data: {query: query.term},
217 dataType: 'json',
219 dataType: 'json',
218 type: 'GET',
220 type: 'GET',
219 success: function(data) {
221 success: function(data) {
220 self.cachedDataSource[cacheKey] = data;
222 self.cachedDataSource[cacheKey] = data;
221 query.callback({results: data.results});
223 query.callback({results: data.results});
222 },
224 },
223 error: function(data, textStatus, errorThrown) {
225 error: function(data, textStatus, errorThrown) {
224 alert(
226 alert(
225 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
227 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
226 }
228 }
227 });
229 });
228 }
230 }
229 };
231 };
230
232
231 var queryTargetRefs = function(initialData, query) {
233 var queryTargetRefs = function(initialData, query) {
232 var data = {results: []};
234 var data = {results: []};
233 // filter initialData
235 // filter initialData
234 $.each(initialData, function() {
236 $.each(initialData, function() {
235 var section = this.text;
237 var section = this.text;
236 var children = [];
238 var children = [];
237 $.each(this.children, function() {
239 $.each(this.children, function() {
238 if (query.term.length === 0 ||
240 if (query.term.length === 0 ||
239 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
241 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
240 children.push({'id': this.id, 'text': this.text})
242 children.push({'id': this.id, 'text': this.text})
241 }
243 }
242 });
244 });
243 data.results.push({'text': section, 'children': children})
245 data.results.push({'text': section, 'children': children})
244 });
246 });
245 query.callback({results: data.results});
247 query.callback({results: data.results});
246 };
248 };
247
249
248 var prButtonLock = function(lockEnabled, msg) {
250 var prButtonLock = function(lockEnabled, msg) {
249 if (lockEnabled) {
251 if (lockEnabled) {
250 $('#save').attr('disabled', 'disabled');
252 $('#save').attr('disabled', 'disabled');
251 }
253 }
252 else {
254 else {
253 $('#save').removeAttr('disabled');
255 $('#save').removeAttr('disabled');
254 }
256 }
255
257
256 $('#pr_open_message').html(msg);
258 $('#pr_open_message').html(msg);
257
259
258 };
260 };
259
261
260 var loadRepoRefDiffPreview = function() {
262 var loadRepoRefDiffPreview = function() {
261 var sourceRepo = $sourceRepo.eq(0).val();
263 var sourceRepo = $sourceRepo.eq(0).val();
262 var sourceRef = $sourceRef.eq(0).val().split(':');
264 var sourceRef = $sourceRef.eq(0).val().split(':');
263
265
264 var targetRepo = $targetRepo.eq(0).val();
266 var targetRepo = $targetRepo.eq(0).val();
265 var targetRef = $targetRef.eq(0).val().split(':');
267 var targetRef = $targetRef.eq(0).val().split(':');
266
268
267 var url_data = {
269 var url_data = {
268 'repo_name': targetRepo,
270 'repo_name': targetRepo,
269 'target_repo': sourceRepo,
271 'target_repo': sourceRepo,
270 'source_ref': targetRef[2],
272 'source_ref': targetRef[2],
271 'source_ref_type': 'rev',
273 'source_ref_type': 'rev',
272 'target_ref': sourceRef[2],
274 'target_ref': sourceRef[2],
273 'target_ref_type': 'rev',
275 'target_ref_type': 'rev',
274 'merge': true,
276 'merge': true,
275 '_': Date.now() // bypass browser caching
277 '_': Date.now() // bypass browser caching
276 }; // gather the source/target ref and repo here
278 }; // gather the source/target ref and repo here
277
279
278 if (sourceRef.length !== 3 || targetRef.length !== 3) {
280 if (sourceRef.length !== 3 || targetRef.length !== 3) {
279 prButtonLock(true, "${_('Please select origin and destination')}");
281 prButtonLock(true, "${_('Please select origin and destination')}");
280 return;
282 return;
281 }
283 }
282 var url = pyroutes.url('compare_url', url_data);
284 var url = pyroutes.url('compare_url', url_data);
283
285
284 // lock PR button, so we cannot send PR before it's calculated
286 // lock PR button, so we cannot send PR before it's calculated
285 prButtonLock(true, "${_('Loading compare ...')}");
287 prButtonLock(true, "${_('Loading compare ...')}");
286
288
287 if (loadRepoRefDiffPreview._currentRequest) {
289 if (loadRepoRefDiffPreview._currentRequest) {
288 loadRepoRefDiffPreview._currentRequest.abort();
290 loadRepoRefDiffPreview._currentRequest.abort();
289 }
291 }
290
292
291 loadRepoRefDiffPreview._currentRequest = $.get(url)
293 loadRepoRefDiffPreview._currentRequest = $.get(url)
292 .error(function(data, textStatus, errorThrown) {
294 .error(function(data, textStatus, errorThrown) {
293 alert(
295 alert(
294 "Error while processing request.\nError code {0} ({1}).".format(
296 "Error while processing request.\nError code {0} ({1}).".format(
295 data.status, data.statusText));
297 data.status, data.statusText));
296 })
298 })
297 .done(function(data) {
299 .done(function(data) {
298 loadRepoRefDiffPreview._currentRequest = null;
300 loadRepoRefDiffPreview._currentRequest = null;
299 $('#pull_request_overview').html(data);
301 $('#pull_request_overview').html(data);
300 var commitElements = $(data).find('tr[commit_id]');
302 var commitElements = $(data).find('tr[commit_id]');
301
303
302 var prTitleAndDesc = getTitleAndDescription(sourceRef[1],
304 var prTitleAndDesc = getTitleAndDescription(sourceRef[1],
303 commitElements, 5);
305 commitElements, 5);
304
306
305 var title = prTitleAndDesc[0];
307 var title = prTitleAndDesc[0];
306 var proposedDescription = prTitleAndDesc[1];
308 var proposedDescription = prTitleAndDesc[1];
307
309
308 var useGeneratedTitle = (
310 var useGeneratedTitle = (
309 $('#pullrequest_title').hasClass('autogenerated-title') ||
311 $('#pullrequest_title').hasClass('autogenerated-title') ||
310 $('#pullrequest_title').val() === "");
312 $('#pullrequest_title').val() === "");
311
313
312 if (title && useGeneratedTitle) {
314 if (title && useGeneratedTitle) {
313 // use generated title if we haven't specified our own
315 // use generated title if we haven't specified our own
314 $('#pullrequest_title').val(title);
316 $('#pullrequest_title').val(title);
315 $('#pullrequest_title').addClass('autogenerated-title');
317 $('#pullrequest_title').addClass('autogenerated-title');
316
318
317 }
319 }
318
320
319 var useGeneratedDescription = (
321 var useGeneratedDescription = (
320 !codeMirrorInstance._userDefinedDesc ||
322 !codeMirrorInstance._userDefinedDesc ||
321 codeMirrorInstance.getValue() === "");
323 codeMirrorInstance.getValue() === "");
322
324
323 if (proposedDescription && useGeneratedDescription) {
325 if (proposedDescription && useGeneratedDescription) {
324 // set proposed content, if we haven't defined our own,
326 // set proposed content, if we haven't defined our own,
325 // or we don't have description written
327 // or we don't have description written
326 codeMirrorInstance._userDefinedDesc = false; // reset state
328 codeMirrorInstance._userDefinedDesc = false; // reset state
327 codeMirrorInstance.setValue(proposedDescription);
329 codeMirrorInstance.setValue(proposedDescription);
328 }
330 }
329
331
330 var msg = '';
332 var msg = '';
331 if (commitElements.length === 1) {
333 if (commitElements.length === 1) {
332 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
334 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
333 } else {
335 } else {
334 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
336 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
335 }
337 }
336
338
337 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
339 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
338
340
339 if (commitElements.length) {
341 if (commitElements.length) {
340 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
342 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
341 prButtonLock(false, msg.replace('__COMMITS__', commitsLink));
343 prButtonLock(false, msg.replace('__COMMITS__', commitsLink));
342 }
344 }
343 else {
345 else {
344 prButtonLock(true, "${_('There are no commits to merge.')}");
346 prButtonLock(true, "${_('There are no commits to merge.')}");
345 }
347 }
346
348
347
349
348 });
350 });
349 };
351 };
350
352
351 /**
353 /**
352 Generate Title and Description for a PullRequest.
354 Generate Title and Description for a PullRequest.
353 In case of 1 commits, the title and description is that one commit
355 In case of 1 commits, the title and description is that one commit
354 in case of multiple commits, we iterate on them with max N number of commits,
356 in case of multiple commits, we iterate on them with max N number of commits,
355 and build description in a form
357 and build description in a form
356 - commitN
358 - commitN
357 - commitN+1
359 - commitN+1
358 ...
360 ...
359
361
360 Title is then constructed from branch names, or other references,
362 Title is then constructed from branch names, or other references,
361 replacing '-' and '_' into spaces
363 replacing '-' and '_' into spaces
362
364
363 * @param sourceRef
365 * @param sourceRef
364 * @param elements
366 * @param elements
365 * @param limit
367 * @param limit
366 * @returns {*[]}
368 * @returns {*[]}
367 */
369 */
368 var getTitleAndDescription = function(sourceRef, elements, limit) {
370 var getTitleAndDescription = function(sourceRef, elements, limit) {
369 var title = '';
371 var title = '';
370 var desc = '';
372 var desc = '';
371
373
372 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
374 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
373 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
375 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
374 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
376 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
375 });
377 });
376 // only 1 commit, use commit message as title
378 // only 1 commit, use commit message as title
377 if (elements.length == 1) {
379 if (elements.length == 1) {
378 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
380 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
379 }
381 }
380 else {
382 else {
381 // use reference name
383 // use reference name
382 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
384 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
383 }
385 }
384
386
385 return [title, desc]
387 return [title, desc]
386 };
388 };
387
389
388 var Select2Box = function(element, overrides) {
390 var Select2Box = function(element, overrides) {
389 var globalDefaults = {
391 var globalDefaults = {
390 dropdownAutoWidth: true,
392 dropdownAutoWidth: true,
391 containerCssClass: "drop-menu",
393 containerCssClass: "drop-menu",
392 dropdownCssClass: "drop-menu-dropdown",
394 dropdownCssClass: "drop-menu-dropdown",
393 };
395 };
394
396
395 var initSelect2 = function(defaultOptions) {
397 var initSelect2 = function(defaultOptions) {
396 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
398 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
397 element.select2(options);
399 element.select2(options);
398 };
400 };
399
401
400 return {
402 return {
401 initRef: function() {
403 initRef: function() {
402 var defaultOptions = {
404 var defaultOptions = {
403 minimumResultsForSearch: 5,
405 minimumResultsForSearch: 5,
404 formatSelection: formatRefSelection
406 formatSelection: formatRefSelection
405 };
407 };
406
408
407 initSelect2(defaultOptions);
409 initSelect2(defaultOptions);
408 },
410 },
409
411
410 initRepo: function(defaultValue, readOnly) {
412 initRepo: function(defaultValue, readOnly) {
411 var defaultOptions = {
413 var defaultOptions = {
412 initSelection : function (element, callback) {
414 initSelection : function (element, callback) {
413 var data = {id: defaultValue, text: defaultValue};
415 var data = {id: defaultValue, text: defaultValue};
414 callback(data);
416 callback(data);
415 }
417 }
416 };
418 };
417
419
418 initSelect2(defaultOptions);
420 initSelect2(defaultOptions);
419
421
420 element.select2('val', defaultSourceRepo);
422 element.select2('val', defaultSourceRepo);
421 if (readOnly === true) {
423 if (readOnly === true) {
422 element.select2('readonly', true);
424 element.select2('readonly', true);
423 };
425 };
424 }
426 }
425 };
427 };
426 };
428 };
427
429
428 var initTargetRefs = function(refsData, selectedRef){
430 var initTargetRefs = function(refsData, selectedRef){
429 Select2Box($targetRef, {
431 Select2Box($targetRef, {
430 query: function(query) {
432 query: function(query) {
431 queryTargetRefs(refsData, query);
433 queryTargetRefs(refsData, query);
432 },
434 },
433 initSelection : initRefSelection(selectedRef)
435 initSelection : initRefSelection(selectedRef)
434 }).initRef();
436 }).initRef();
435
437
436 if (!(selectedRef === undefined)) {
438 if (!(selectedRef === undefined)) {
437 $targetRef.select2('val', selectedRef);
439 $targetRef.select2('val', selectedRef);
438 }
440 }
439 };
441 };
440
442
441 var targetRepoChanged = function(repoData) {
443 var targetRepoChanged = function(repoData) {
442 // generate new DESC of target repo displayed next to select
444 // generate new DESC of target repo displayed next to select
443 $('#target_repo_desc').html(
445 $('#target_repo_desc').html(
444 "<strong>${_('Destination repository')}</strong>: {0}".format(repoData['description'])
446 "<strong>${_('Destination repository')}</strong>: {0}".format(repoData['description'])
445 );
447 );
446
448
447 // generate dynamic select2 for refs.
449 // generate dynamic select2 for refs.
448 initTargetRefs(repoData['refs']['select2_refs'],
450 initTargetRefs(repoData['refs']['select2_refs'],
449 repoData['refs']['selected_ref']);
451 repoData['refs']['selected_ref']);
450
452
451 };
453 };
452
454
453 var sourceRefSelect2 = Select2Box(
455 var sourceRefSelect2 = Select2Box(
454 $sourceRef, {
456 $sourceRef, {
455 placeholder: "${_('Select commit reference')}",
457 placeholder: "${_('Select commit reference')}",
456 query: function(query) {
458 query: function(query) {
457 var initialData = defaultSourceRepoData['refs']['select2_refs'];
459 var initialData = defaultSourceRepoData['refs']['select2_refs'];
458 queryTargetRefs(initialData, query)
460 queryTargetRefs(initialData, query)
459 },
461 },
460 initSelection: initRefSelection()
462 initSelection: initRefSelection()
461 }
463 }
462 );
464 );
463
465
464 var sourceRepoSelect2 = Select2Box($sourceRepo, {
466 var sourceRepoSelect2 = Select2Box($sourceRepo, {
465 query: function(query) {}
467 query: function(query) {}
466 });
468 });
467
469
468 var targetRepoSelect2 = Select2Box($targetRepo, {
470 var targetRepoSelect2 = Select2Box($targetRepo, {
469 cachedDataSource: {},
471 cachedDataSource: {},
470 query: $.debounce(250, function(query) {
472 query: $.debounce(250, function(query) {
471 queryTargetRepo(this, query);
473 queryTargetRepo(this, query);
472 }),
474 }),
473 formatResult: formatResult
475 formatResult: formatResult
474 });
476 });
475
477
476 sourceRefSelect2.initRef();
478 sourceRefSelect2.initRef();
477
479
478 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
480 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
479
481
480 targetRepoSelect2.initRepo(defaultTargetRepo, false);
482 targetRepoSelect2.initRepo(defaultTargetRepo, false);
481
483
482 $sourceRef.on('change', function(e){
484 $sourceRef.on('change', function(e){
483 loadRepoRefDiffPreview();
485 loadRepoRefDiffPreview();
484 loadDefaultReviewers();
486 loadDefaultReviewers();
485 });
487 });
486
488
487 $targetRef.on('change', function(e){
489 $targetRef.on('change', function(e){
488 loadRepoRefDiffPreview();
490 loadRepoRefDiffPreview();
489 loadDefaultReviewers();
491 loadDefaultReviewers();
490 });
492 });
491
493
492 $targetRepo.on('change', function(e){
494 $targetRepo.on('change', function(e){
493 var repoName = $(this).val();
495 var repoName = $(this).val();
494 calculateContainerWidth();
496 calculateContainerWidth();
495 $targetRef.select2('destroy');
497 $targetRef.select2('destroy');
496 $('#target_ref_loading').show();
498 $('#target_ref_loading').show();
497
499
498 $.ajax({
500 $.ajax({
499 url: pyroutes.url('pullrequest_repo_refs',
501 url: pyroutes.url('pullrequest_repo_refs',
500 {'repo_name': targetRepoName, 'target_repo_name':repoName}),
502 {'repo_name': targetRepoName, 'target_repo_name':repoName}),
501 data: {},
503 data: {},
502 dataType: 'json',
504 dataType: 'json',
503 type: 'GET',
505 type: 'GET',
504 success: function(data) {
506 success: function(data) {
505 $('#target_ref_loading').hide();
507 $('#target_ref_loading').hide();
506 targetRepoChanged(data);
508 targetRepoChanged(data);
507 loadRepoRefDiffPreview();
509 loadRepoRefDiffPreview();
508 },
510 },
509 error: function(data, textStatus, errorThrown) {
511 error: function(data, textStatus, errorThrown) {
510 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
512 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
511 }
513 }
512 })
514 })
513
515
514 });
516 });
515
517
516 var loadDefaultReviewers = function() {
518 var loadDefaultReviewers = function() {
517 if (loadDefaultReviewers._currentRequest) {
519 if (loadDefaultReviewers._currentRequest) {
518 loadDefaultReviewers._currentRequest.abort();
520 loadDefaultReviewers._currentRequest.abort();
519 }
521 }
520 var url = pyroutes.url('repo_default_reviewers_data', {'repo_name': targetRepoName});
522 var url = pyroutes.url('repo_default_reviewers_data', {'repo_name': targetRepoName});
521
523
522 var sourceRepo = $sourceRepo.eq(0).val();
524 var sourceRepo = $sourceRepo.eq(0).val();
523 var sourceRef = $sourceRef.eq(0).val().split(':');
525 var sourceRef = $sourceRef.eq(0).val().split(':');
524 var targetRepo = $targetRepo.eq(0).val();
526 var targetRepo = $targetRepo.eq(0).val();
525 var targetRef = $targetRef.eq(0).val().split(':');
527 var targetRef = $targetRef.eq(0).val().split(':');
526 url += '?source_repo=' + sourceRepo;
528 url += '?source_repo=' + sourceRepo;
527 url += '&source_ref=' + sourceRef[2];
529 url += '&source_ref=' + sourceRef[2];
528 url += '&target_repo=' + targetRepo;
530 url += '&target_repo=' + targetRepo;
529 url += '&target_ref=' + targetRef[2];
531 url += '&target_ref=' + targetRef[2];
530
532
531 loadDefaultReviewers._currentRequest = $.get(url)
533 loadDefaultReviewers._currentRequest = $.get(url)
532 .done(function(data) {
534 .done(function(data) {
533 loadDefaultReviewers._currentRequest = null;
535 loadDefaultReviewers._currentRequest = null;
534
536
535 // reset && add the reviewer based on selected repo
537 // reset && add the reviewer based on selected repo
536 $('#review_members').html('');
538 $('#review_members').html('');
537 for (var i = 0; i < data.reviewers.length; i++) {
539 for (var i = 0; i < data.reviewers.length; i++) {
538 var reviewer = data.reviewers[i];
540 var reviewer = data.reviewers[i];
539 addReviewMember(
541 addReviewMember(
540 reviewer.user_id, reviewer.firstname,
542 reviewer.user_id, reviewer.firstname,
541 reviewer.lastname, reviewer.username,
543 reviewer.lastname, reviewer.username,
542 reviewer.gravatar_link, reviewer.reasons);
544 reviewer.gravatar_link, reviewer.reasons);
543 }
545 }
544 });
546 });
545 };
547 };
546 prButtonLock(true, "${_('Please select origin and destination')}");
548 prButtonLock(true, "${_('Please select origin and destination')}");
547
549
548 // auto-load on init, the target refs select2
550 // auto-load on init, the target refs select2
549 calculateContainerWidth();
551 calculateContainerWidth();
550 targetRepoChanged(defaultTargetRepoData);
552 targetRepoChanged(defaultTargetRepoData);
551
553
552 $('#pullrequest_title').on('keyup', function(e){
554 $('#pullrequest_title').on('keyup', function(e){
553 $(this).removeClass('autogenerated-title');
555 $(this).removeClass('autogenerated-title');
554 });
556 });
555
557
556 %if c.default_source_ref:
558 %if c.default_source_ref:
557 // in case we have a pre-selected value, use it now
559 // in case we have a pre-selected value, use it now
558 $sourceRef.select2('val', '${c.default_source_ref}');
560 $sourceRef.select2('val', '${c.default_source_ref}');
559 loadRepoRefDiffPreview();
561 loadRepoRefDiffPreview();
560 loadDefaultReviewers();
562 loadDefaultReviewers();
561 %endif
563 %endif
562
564
563 ReviewerAutoComplete('user');
565 ReviewerAutoComplete('user');
564 });
566 });
565 </script>
567 </script>
566
568
567 </%def>
569 </%def>
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now