##// END OF EJS Templates
pull-requests: fixed a case when template marker was used in description field....
milka -
r4631:21211d2f stable
parent child Browse files
Show More
@@ -1,1658 +1,1679 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 import rhodecode
23 import rhodecode
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 from rhodecode.lib.vcs.nodes import FileNode
25 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.comment import CommentsModel
34 from rhodecode.tests import (
34 from rhodecode.tests import (
35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
36
36
37
37
38 def route_path(name, params=None, **kwargs):
38 def route_path(name, params=None, **kwargs):
39 import urllib
39 import urllib
40
40
41 base_url = {
41 base_url = {
42 'repo_changelog': '/{repo_name}/changelog',
42 'repo_changelog': '/{repo_name}/changelog',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
44 'repo_commits': '/{repo_name}/commits',
44 'repo_commits': '/{repo_name}/commits',
45 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
45 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
46 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
46 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
47 'pullrequest_show_all': '/{repo_name}/pull-request',
47 'pullrequest_show_all': '/{repo_name}/pull-request',
48 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
48 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
49 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
49 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
50 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
50 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
51 'pullrequest_new': '/{repo_name}/pull-request/new',
51 'pullrequest_new': '/{repo_name}/pull-request/new',
52 'pullrequest_create': '/{repo_name}/pull-request/create',
52 'pullrequest_create': '/{repo_name}/pull-request/create',
53 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
53 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
54 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
54 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
55 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
55 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
56 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
56 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
57 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
57 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
58 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
58 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
59 }[name].format(**kwargs)
59 }[name].format(**kwargs)
60
60
61 if params:
61 if params:
62 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
62 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
63 return base_url
63 return base_url
64
64
65
65
66 @pytest.mark.usefixtures('app', 'autologin_user')
66 @pytest.mark.usefixtures('app', 'autologin_user')
67 @pytest.mark.backends("git", "hg")
67 @pytest.mark.backends("git", "hg")
68 class TestPullrequestsView(object):
68 class TestPullrequestsView(object):
69
69
70 def test_index(self, backend):
70 def test_index(self, backend):
71 self.app.get(route_path(
71 self.app.get(route_path(
72 'pullrequest_new',
72 'pullrequest_new',
73 repo_name=backend.repo_name))
73 repo_name=backend.repo_name))
74
74
75 def test_option_menu_create_pull_request_exists(self, backend):
75 def test_option_menu_create_pull_request_exists(self, backend):
76 repo_name = backend.repo_name
76 repo_name = backend.repo_name
77 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
77 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
78
78
79 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
79 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
80 'pullrequest_new', repo_name=repo_name)
80 'pullrequest_new', repo_name=repo_name)
81 response.mustcontain(create_pr_link)
81 response.mustcontain(create_pr_link)
82
82
83 def test_create_pr_form_with_raw_commit_id(self, backend):
83 def test_create_pr_form_with_raw_commit_id(self, backend):
84 repo = backend.repo
84 repo = backend.repo
85
85
86 self.app.get(
86 self.app.get(
87 route_path('pullrequest_new', repo_name=repo.repo_name,
87 route_path('pullrequest_new', repo_name=repo.repo_name,
88 commit=repo.get_commit().raw_id),
88 commit=repo.get_commit().raw_id),
89 status=200)
89 status=200)
90
90
91 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
91 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
92 @pytest.mark.parametrize('range_diff', ["0", "1"])
92 @pytest.mark.parametrize('range_diff', ["0", "1"])
93 def test_show(self, pr_util, pr_merge_enabled, range_diff):
93 def test_show(self, pr_util, pr_merge_enabled, range_diff):
94 pull_request = pr_util.create_pull_request(
94 pull_request = pr_util.create_pull_request(
95 mergeable=pr_merge_enabled, enable_notifications=False)
95 mergeable=pr_merge_enabled, enable_notifications=False)
96
96
97 response = self.app.get(route_path(
97 response = self.app.get(route_path(
98 'pullrequest_show',
98 'pullrequest_show',
99 repo_name=pull_request.target_repo.scm_instance().name,
99 repo_name=pull_request.target_repo.scm_instance().name,
100 pull_request_id=pull_request.pull_request_id,
100 pull_request_id=pull_request.pull_request_id,
101 params={'range-diff': range_diff}))
101 params={'range-diff': range_diff}))
102
102
103 for commit_id in pull_request.revisions:
103 for commit_id in pull_request.revisions:
104 response.mustcontain(commit_id)
104 response.mustcontain(commit_id)
105
105
106 response.mustcontain(pull_request.target_ref_parts.type)
106 response.mustcontain(pull_request.target_ref_parts.type)
107 response.mustcontain(pull_request.target_ref_parts.name)
107 response.mustcontain(pull_request.target_ref_parts.name)
108
108
109 response.mustcontain('class="pull-request-merge"')
109 response.mustcontain('class="pull-request-merge"')
110
110
111 if pr_merge_enabled:
111 if pr_merge_enabled:
112 response.mustcontain('Pull request reviewer approval is pending')
112 response.mustcontain('Pull request reviewer approval is pending')
113 else:
113 else:
114 response.mustcontain('Server-side pull request merging is disabled.')
114 response.mustcontain('Server-side pull request merging is disabled.')
115
115
116 if range_diff == "1":
116 if range_diff == "1":
117 response.mustcontain('Turn off: Show the diff as commit range')
117 response.mustcontain('Turn off: Show the diff as commit range')
118
118
119 def test_show_versions_of_pr(self, backend, csrf_token):
119 def test_show_versions_of_pr(self, backend, csrf_token):
120 commits = [
120 commits = [
121 {'message': 'initial-commit',
121 {'message': 'initial-commit',
122 'added': [FileNode('test-file.txt', 'LINE1\n')]},
122 'added': [FileNode('test-file.txt', 'LINE1\n')]},
123
123
124 {'message': 'commit-1',
124 {'message': 'commit-1',
125 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
125 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
126 # Above is the initial version of PR that changes a single line
126 # Above is the initial version of PR that changes a single line
127
127
128 # from now on we'll add 3x commit adding a nother line on each step
128 # from now on we'll add 3x commit adding a nother line on each step
129 {'message': 'commit-2',
129 {'message': 'commit-2',
130 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
130 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
131
131
132 {'message': 'commit-3',
132 {'message': 'commit-3',
133 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
133 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
134
134
135 {'message': 'commit-4',
135 {'message': 'commit-4',
136 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
136 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
137 ]
137 ]
138
138
139 commit_ids = backend.create_master_repo(commits)
139 commit_ids = backend.create_master_repo(commits)
140 target = backend.create_repo(heads=['initial-commit'])
140 target = backend.create_repo(heads=['initial-commit'])
141 source = backend.create_repo(heads=['commit-1'])
141 source = backend.create_repo(heads=['commit-1'])
142 source_repo_name = source.repo_name
142 source_repo_name = source.repo_name
143 target_repo_name = target.repo_name
143 target_repo_name = target.repo_name
144
144
145 target_ref = 'branch:{branch}:{commit_id}'.format(
145 target_ref = 'branch:{branch}:{commit_id}'.format(
146 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
146 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
147 source_ref = 'branch:{branch}:{commit_id}'.format(
147 source_ref = 'branch:{branch}:{commit_id}'.format(
148 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
148 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
149
149
150 response = self.app.post(
150 response = self.app.post(
151 route_path('pullrequest_create', repo_name=source.repo_name),
151 route_path('pullrequest_create', repo_name=source.repo_name),
152 [
152 [
153 ('source_repo', source_repo_name),
153 ('source_repo', source_repo_name),
154 ('source_ref', source_ref),
154 ('source_ref', source_ref),
155 ('target_repo', target_repo_name),
155 ('target_repo', target_repo_name),
156 ('target_ref', target_ref),
156 ('target_ref', target_ref),
157 ('common_ancestor', commit_ids['initial-commit']),
157 ('common_ancestor', commit_ids['initial-commit']),
158 ('pullrequest_title', 'Title'),
158 ('pullrequest_title', 'Title'),
159 ('pullrequest_desc', 'Description'),
159 ('pullrequest_desc', 'Description'),
160 ('description_renderer', 'markdown'),
160 ('description_renderer', 'markdown'),
161 ('__start__', 'review_members:sequence'),
161 ('__start__', 'review_members:sequence'),
162 ('__start__', 'reviewer:mapping'),
162 ('__start__', 'reviewer:mapping'),
163 ('user_id', '1'),
163 ('user_id', '1'),
164 ('__start__', 'reasons:sequence'),
164 ('__start__', 'reasons:sequence'),
165 ('reason', 'Some reason'),
165 ('reason', 'Some reason'),
166 ('__end__', 'reasons:sequence'),
166 ('__end__', 'reasons:sequence'),
167 ('__start__', 'rules:sequence'),
167 ('__start__', 'rules:sequence'),
168 ('__end__', 'rules:sequence'),
168 ('__end__', 'rules:sequence'),
169 ('mandatory', 'False'),
169 ('mandatory', 'False'),
170 ('__end__', 'reviewer:mapping'),
170 ('__end__', 'reviewer:mapping'),
171 ('__end__', 'review_members:sequence'),
171 ('__end__', 'review_members:sequence'),
172 ('__start__', 'revisions:sequence'),
172 ('__start__', 'revisions:sequence'),
173 ('revisions', commit_ids['commit-1']),
173 ('revisions', commit_ids['commit-1']),
174 ('__end__', 'revisions:sequence'),
174 ('__end__', 'revisions:sequence'),
175 ('user', ''),
175 ('user', ''),
176 ('csrf_token', csrf_token),
176 ('csrf_token', csrf_token),
177 ],
177 ],
178 status=302)
178 status=302)
179
179
180 location = response.headers['Location']
180 location = response.headers['Location']
181
181
182 pull_request_id = location.rsplit('/', 1)[1]
182 pull_request_id = location.rsplit('/', 1)[1]
183 assert pull_request_id != 'new'
183 assert pull_request_id != 'new'
184 pull_request = PullRequest.get(int(pull_request_id))
184 pull_request = PullRequest.get(int(pull_request_id))
185
185
186 pull_request_id = pull_request.pull_request_id
186 pull_request_id = pull_request.pull_request_id
187
187
188 # Show initial version of PR
188 # Show initial version of PR
189 response = self.app.get(
189 response = self.app.get(
190 route_path('pullrequest_show',
190 route_path('pullrequest_show',
191 repo_name=target_repo_name,
191 repo_name=target_repo_name,
192 pull_request_id=pull_request_id))
192 pull_request_id=pull_request_id))
193
193
194 response.mustcontain('commit-1')
194 response.mustcontain('commit-1')
195 response.mustcontain(no=['commit-2'])
195 response.mustcontain(no=['commit-2'])
196 response.mustcontain(no=['commit-3'])
196 response.mustcontain(no=['commit-3'])
197 response.mustcontain(no=['commit-4'])
197 response.mustcontain(no=['commit-4'])
198
198
199 response.mustcontain('cb-addition"></span><span>LINE2</span>')
199 response.mustcontain('cb-addition"></span><span>LINE2</span>')
200 response.mustcontain(no=['LINE3'])
200 response.mustcontain(no=['LINE3'])
201 response.mustcontain(no=['LINE4'])
201 response.mustcontain(no=['LINE4'])
202 response.mustcontain(no=['LINE5'])
202 response.mustcontain(no=['LINE5'])
203
203
204 # update PR #1
204 # update PR #1
205 source_repo = Repository.get_by_repo_name(source_repo_name)
205 source_repo = Repository.get_by_repo_name(source_repo_name)
206 backend.pull_heads(source_repo, heads=['commit-2'])
206 backend.pull_heads(source_repo, heads=['commit-2'])
207 response = self.app.post(
207 response = self.app.post(
208 route_path('pullrequest_update',
208 route_path('pullrequest_update',
209 repo_name=target_repo_name, pull_request_id=pull_request_id),
209 repo_name=target_repo_name, pull_request_id=pull_request_id),
210 params={'update_commits': 'true', 'csrf_token': csrf_token})
210 params={'update_commits': 'true', 'csrf_token': csrf_token})
211
211
212 # update PR #2
212 # update PR #2
213 source_repo = Repository.get_by_repo_name(source_repo_name)
213 source_repo = Repository.get_by_repo_name(source_repo_name)
214 backend.pull_heads(source_repo, heads=['commit-3'])
214 backend.pull_heads(source_repo, heads=['commit-3'])
215 response = self.app.post(
215 response = self.app.post(
216 route_path('pullrequest_update',
216 route_path('pullrequest_update',
217 repo_name=target_repo_name, pull_request_id=pull_request_id),
217 repo_name=target_repo_name, pull_request_id=pull_request_id),
218 params={'update_commits': 'true', 'csrf_token': csrf_token})
218 params={'update_commits': 'true', 'csrf_token': csrf_token})
219
219
220 # update PR #3
220 # update PR #3
221 source_repo = Repository.get_by_repo_name(source_repo_name)
221 source_repo = Repository.get_by_repo_name(source_repo_name)
222 backend.pull_heads(source_repo, heads=['commit-4'])
222 backend.pull_heads(source_repo, heads=['commit-4'])
223 response = self.app.post(
223 response = self.app.post(
224 route_path('pullrequest_update',
224 route_path('pullrequest_update',
225 repo_name=target_repo_name, pull_request_id=pull_request_id),
225 repo_name=target_repo_name, pull_request_id=pull_request_id),
226 params={'update_commits': 'true', 'csrf_token': csrf_token})
226 params={'update_commits': 'true', 'csrf_token': csrf_token})
227
227
228 # Show final version !
228 # Show final version !
229 response = self.app.get(
229 response = self.app.get(
230 route_path('pullrequest_show',
230 route_path('pullrequest_show',
231 repo_name=target_repo_name,
231 repo_name=target_repo_name,
232 pull_request_id=pull_request_id))
232 pull_request_id=pull_request_id))
233
233
234 # 3 updates, and the latest == 4
234 # 3 updates, and the latest == 4
235 response.mustcontain('4 versions available for this pull request')
235 response.mustcontain('4 versions available for this pull request')
236 response.mustcontain(no=['rhodecode diff rendering error'])
236 response.mustcontain(no=['rhodecode diff rendering error'])
237
237
238 # initial show must have 3 commits, and 3 adds
238 # initial show must have 3 commits, and 3 adds
239 response.mustcontain('commit-1')
239 response.mustcontain('commit-1')
240 response.mustcontain('commit-2')
240 response.mustcontain('commit-2')
241 response.mustcontain('commit-3')
241 response.mustcontain('commit-3')
242 response.mustcontain('commit-4')
242 response.mustcontain('commit-4')
243
243
244 response.mustcontain('cb-addition"></span><span>LINE2</span>')
244 response.mustcontain('cb-addition"></span><span>LINE2</span>')
245 response.mustcontain('cb-addition"></span><span>LINE3</span>')
245 response.mustcontain('cb-addition"></span><span>LINE3</span>')
246 response.mustcontain('cb-addition"></span><span>LINE4</span>')
246 response.mustcontain('cb-addition"></span><span>LINE4</span>')
247 response.mustcontain('cb-addition"></span><span>LINE5</span>')
247 response.mustcontain('cb-addition"></span><span>LINE5</span>')
248
248
249 # fetch versions
249 # fetch versions
250 pr = PullRequest.get(pull_request_id)
250 pr = PullRequest.get(pull_request_id)
251 versions = [x.pull_request_version_id for x in pr.versions.all()]
251 versions = [x.pull_request_version_id for x in pr.versions.all()]
252 assert len(versions) == 3
252 assert len(versions) == 3
253
253
254 # show v1,v2,v3,v4
254 # show v1,v2,v3,v4
255 def cb_line(text):
255 def cb_line(text):
256 return 'cb-addition"></span><span>{}</span>'.format(text)
256 return 'cb-addition"></span><span>{}</span>'.format(text)
257
257
258 def cb_context(text):
258 def cb_context(text):
259 return '<span class="cb-code"><span class="cb-action cb-context">' \
259 return '<span class="cb-code"><span class="cb-action cb-context">' \
260 '</span><span>{}</span></span>'.format(text)
260 '</span><span>{}</span></span>'.format(text)
261
261
262 commit_tests = {
262 commit_tests = {
263 # in response, not in response
263 # in response, not in response
264 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
264 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
265 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
265 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
266 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
266 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
267 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
267 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
268 }
268 }
269 diff_tests = {
269 diff_tests = {
270 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
270 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
271 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
271 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
272 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
272 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
273 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
273 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
274 }
274 }
275 for idx, ver in enumerate(versions, 1):
275 for idx, ver in enumerate(versions, 1):
276
276
277 response = self.app.get(
277 response = self.app.get(
278 route_path('pullrequest_show',
278 route_path('pullrequest_show',
279 repo_name=target_repo_name,
279 repo_name=target_repo_name,
280 pull_request_id=pull_request_id,
280 pull_request_id=pull_request_id,
281 params={'version': ver}))
281 params={'version': ver}))
282
282
283 response.mustcontain(no=['rhodecode diff rendering error'])
283 response.mustcontain(no=['rhodecode diff rendering error'])
284 response.mustcontain('Showing changes at v{}'.format(idx))
284 response.mustcontain('Showing changes at v{}'.format(idx))
285
285
286 yes, no = commit_tests[idx]
286 yes, no = commit_tests[idx]
287 for y in yes:
287 for y in yes:
288 response.mustcontain(y)
288 response.mustcontain(y)
289 for n in no:
289 for n in no:
290 response.mustcontain(no=n)
290 response.mustcontain(no=n)
291
291
292 yes, no = diff_tests[idx]
292 yes, no = diff_tests[idx]
293 for y in yes:
293 for y in yes:
294 response.mustcontain(cb_line(y))
294 response.mustcontain(cb_line(y))
295 for n in no:
295 for n in no:
296 response.mustcontain(no=n)
296 response.mustcontain(no=n)
297
297
298 # show diff between versions
298 # show diff between versions
299 diff_compare_tests = {
299 diff_compare_tests = {
300 1: (['LINE3'], ['LINE1', 'LINE2']),
300 1: (['LINE3'], ['LINE1', 'LINE2']),
301 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
301 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
302 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
302 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
303 }
303 }
304 for idx, ver in enumerate(versions, 1):
304 for idx, ver in enumerate(versions, 1):
305 adds, context = diff_compare_tests[idx]
305 adds, context = diff_compare_tests[idx]
306
306
307 to_ver = ver+1
307 to_ver = ver+1
308 if idx == 3:
308 if idx == 3:
309 to_ver = 'latest'
309 to_ver = 'latest'
310
310
311 response = self.app.get(
311 response = self.app.get(
312 route_path('pullrequest_show',
312 route_path('pullrequest_show',
313 repo_name=target_repo_name,
313 repo_name=target_repo_name,
314 pull_request_id=pull_request_id,
314 pull_request_id=pull_request_id,
315 params={'from_version': versions[0], 'version': to_ver}))
315 params={'from_version': versions[0], 'version': to_ver}))
316
316
317 response.mustcontain(no=['rhodecode diff rendering error'])
317 response.mustcontain(no=['rhodecode diff rendering error'])
318
318
319 for a in adds:
319 for a in adds:
320 response.mustcontain(cb_line(a))
320 response.mustcontain(cb_line(a))
321 for c in context:
321 for c in context:
322 response.mustcontain(cb_context(c))
322 response.mustcontain(cb_context(c))
323
323
324 # test version v2 -> v3
324 # test version v2 -> v3
325 response = self.app.get(
325 response = self.app.get(
326 route_path('pullrequest_show',
326 route_path('pullrequest_show',
327 repo_name=target_repo_name,
327 repo_name=target_repo_name,
328 pull_request_id=pull_request_id,
328 pull_request_id=pull_request_id,
329 params={'from_version': versions[1], 'version': versions[2]}))
329 params={'from_version': versions[1], 'version': versions[2]}))
330
330
331 response.mustcontain(cb_context('LINE1'))
331 response.mustcontain(cb_context('LINE1'))
332 response.mustcontain(cb_context('LINE2'))
332 response.mustcontain(cb_context('LINE2'))
333 response.mustcontain(cb_context('LINE3'))
333 response.mustcontain(cb_context('LINE3'))
334 response.mustcontain(cb_line('LINE4'))
334 response.mustcontain(cb_line('LINE4'))
335
335
336 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
336 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
337 # Logout
337 # Logout
338 response = self.app.post(
338 response = self.app.post(
339 h.route_path('logout'),
339 h.route_path('logout'),
340 params={'csrf_token': csrf_token})
340 params={'csrf_token': csrf_token})
341 # Login as regular user
341 # Login as regular user
342 response = self.app.post(h.route_path('login'),
342 response = self.app.post(h.route_path('login'),
343 {'username': TEST_USER_REGULAR_LOGIN,
343 {'username': TEST_USER_REGULAR_LOGIN,
344 'password': 'test12'})
344 'password': 'test12'})
345
345
346 pull_request = pr_util.create_pull_request(
346 pull_request = pr_util.create_pull_request(
347 author=TEST_USER_REGULAR_LOGIN)
347 author=TEST_USER_REGULAR_LOGIN)
348
348
349 response = self.app.get(route_path(
349 response = self.app.get(route_path(
350 'pullrequest_show',
350 'pullrequest_show',
351 repo_name=pull_request.target_repo.scm_instance().name,
351 repo_name=pull_request.target_repo.scm_instance().name,
352 pull_request_id=pull_request.pull_request_id))
352 pull_request_id=pull_request.pull_request_id))
353
353
354 response.mustcontain('Server-side pull request merging is disabled.')
354 response.mustcontain('Server-side pull request merging is disabled.')
355
355
356 assert_response = response.assert_response()
356 assert_response = response.assert_response()
357 # for regular user without a merge permissions, we don't see it
357 # for regular user without a merge permissions, we don't see it
358 assert_response.no_element_exists('#close-pull-request-action')
358 assert_response.no_element_exists('#close-pull-request-action')
359
359
360 user_util.grant_user_permission_to_repo(
360 user_util.grant_user_permission_to_repo(
361 pull_request.target_repo,
361 pull_request.target_repo,
362 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
362 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
363 'repository.write')
363 'repository.write')
364 response = self.app.get(route_path(
364 response = self.app.get(route_path(
365 'pullrequest_show',
365 'pullrequest_show',
366 repo_name=pull_request.target_repo.scm_instance().name,
366 repo_name=pull_request.target_repo.scm_instance().name,
367 pull_request_id=pull_request.pull_request_id))
367 pull_request_id=pull_request.pull_request_id))
368
368
369 response.mustcontain('Server-side pull request merging is disabled.')
369 response.mustcontain('Server-side pull request merging is disabled.')
370
370
371 assert_response = response.assert_response()
371 assert_response = response.assert_response()
372 # now regular user has a merge permissions, we have CLOSE button
372 # now regular user has a merge permissions, we have CLOSE button
373 assert_response.one_element_exists('#close-pull-request-action')
373 assert_response.one_element_exists('#close-pull-request-action')
374
374
375 def test_show_invalid_commit_id(self, pr_util):
375 def test_show_invalid_commit_id(self, pr_util):
376 # Simulating invalid revisions which will cause a lookup error
376 # Simulating invalid revisions which will cause a lookup error
377 pull_request = pr_util.create_pull_request()
377 pull_request = pr_util.create_pull_request()
378 pull_request.revisions = ['invalid']
378 pull_request.revisions = ['invalid']
379 Session().add(pull_request)
379 Session().add(pull_request)
380 Session().commit()
380 Session().commit()
381
381
382 response = self.app.get(route_path(
382 response = self.app.get(route_path(
383 'pullrequest_show',
383 'pullrequest_show',
384 repo_name=pull_request.target_repo.scm_instance().name,
384 repo_name=pull_request.target_repo.scm_instance().name,
385 pull_request_id=pull_request.pull_request_id))
385 pull_request_id=pull_request.pull_request_id))
386
386
387 for commit_id in pull_request.revisions:
387 for commit_id in pull_request.revisions:
388 response.mustcontain(commit_id)
388 response.mustcontain(commit_id)
389
389
390 def test_show_invalid_source_reference(self, pr_util):
390 def test_show_invalid_source_reference(self, pr_util):
391 pull_request = pr_util.create_pull_request()
391 pull_request = pr_util.create_pull_request()
392 pull_request.source_ref = 'branch:b:invalid'
392 pull_request.source_ref = 'branch:b:invalid'
393 Session().add(pull_request)
393 Session().add(pull_request)
394 Session().commit()
394 Session().commit()
395
395
396 self.app.get(route_path(
396 self.app.get(route_path(
397 'pullrequest_show',
397 'pullrequest_show',
398 repo_name=pull_request.target_repo.scm_instance().name,
398 repo_name=pull_request.target_repo.scm_instance().name,
399 pull_request_id=pull_request.pull_request_id))
399 pull_request_id=pull_request.pull_request_id))
400
400
401 def test_edit_title_description(self, pr_util, csrf_token):
401 def test_edit_title_description(self, pr_util, csrf_token):
402 pull_request = pr_util.create_pull_request()
402 pull_request = pr_util.create_pull_request()
403 pull_request_id = pull_request.pull_request_id
403 pull_request_id = pull_request.pull_request_id
404
404
405 response = self.app.post(
405 response = self.app.post(
406 route_path('pullrequest_update',
406 route_path('pullrequest_update',
407 repo_name=pull_request.target_repo.repo_name,
407 repo_name=pull_request.target_repo.repo_name,
408 pull_request_id=pull_request_id),
408 pull_request_id=pull_request_id),
409 params={
409 params={
410 'edit_pull_request': 'true',
410 'edit_pull_request': 'true',
411 'title': 'New title',
411 'title': 'New title',
412 'description': 'New description',
412 'description': 'New description',
413 'csrf_token': csrf_token})
413 'csrf_token': csrf_token})
414
414
415 assert_session_flash(
415 assert_session_flash(
416 response, u'Pull request title & description updated.',
416 response, u'Pull request title & description updated.',
417 category='success')
417 category='success')
418
418
419 pull_request = PullRequest.get(pull_request_id)
419 pull_request = PullRequest.get(pull_request_id)
420 assert pull_request.title == 'New title'
420 assert pull_request.title == 'New title'
421 assert pull_request.description == 'New description'
421 assert pull_request.description == 'New description'
422
422
423 def test_edit_title_description(self, pr_util, csrf_token):
424 pull_request = pr_util.create_pull_request()
425 pull_request_id = pull_request.pull_request_id
426
427 response = self.app.post(
428 route_path('pullrequest_update',
429 repo_name=pull_request.target_repo.repo_name,
430 pull_request_id=pull_request_id),
431 params={
432 'edit_pull_request': 'true',
433 'title': 'New title {} {2} {foo}',
434 'description': 'New description',
435 'csrf_token': csrf_token})
436
437 assert_session_flash(
438 response, u'Pull request title & description updated.',
439 category='success')
440
441 pull_request = PullRequest.get(pull_request_id)
442 assert pull_request.title_safe == 'New title {{}} {{2}} {{foo}}'
443
423 def test_edit_title_description_closed(self, pr_util, csrf_token):
444 def test_edit_title_description_closed(self, pr_util, csrf_token):
424 pull_request = pr_util.create_pull_request()
445 pull_request = pr_util.create_pull_request()
425 pull_request_id = pull_request.pull_request_id
446 pull_request_id = pull_request.pull_request_id
426 repo_name = pull_request.target_repo.repo_name
447 repo_name = pull_request.target_repo.repo_name
427 pr_util.close()
448 pr_util.close()
428
449
429 response = self.app.post(
450 response = self.app.post(
430 route_path('pullrequest_update',
451 route_path('pullrequest_update',
431 repo_name=repo_name, pull_request_id=pull_request_id),
452 repo_name=repo_name, pull_request_id=pull_request_id),
432 params={
453 params={
433 'edit_pull_request': 'true',
454 'edit_pull_request': 'true',
434 'title': 'New title',
455 'title': 'New title',
435 'description': 'New description',
456 'description': 'New description',
436 'csrf_token': csrf_token}, status=200)
457 'csrf_token': csrf_token}, status=200)
437 assert_session_flash(
458 assert_session_flash(
438 response, u'Cannot update closed pull requests.',
459 response, u'Cannot update closed pull requests.',
439 category='error')
460 category='error')
440
461
441 def test_update_invalid_source_reference(self, pr_util, csrf_token):
462 def test_update_invalid_source_reference(self, pr_util, csrf_token):
442 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
463 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
443
464
444 pull_request = pr_util.create_pull_request()
465 pull_request = pr_util.create_pull_request()
445 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
466 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
446 Session().add(pull_request)
467 Session().add(pull_request)
447 Session().commit()
468 Session().commit()
448
469
449 pull_request_id = pull_request.pull_request_id
470 pull_request_id = pull_request.pull_request_id
450
471
451 response = self.app.post(
472 response = self.app.post(
452 route_path('pullrequest_update',
473 route_path('pullrequest_update',
453 repo_name=pull_request.target_repo.repo_name,
474 repo_name=pull_request.target_repo.repo_name,
454 pull_request_id=pull_request_id),
475 pull_request_id=pull_request_id),
455 params={'update_commits': 'true', 'csrf_token': csrf_token})
476 params={'update_commits': 'true', 'csrf_token': csrf_token})
456
477
457 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
478 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
458 UpdateFailureReason.MISSING_SOURCE_REF])
479 UpdateFailureReason.MISSING_SOURCE_REF])
459 assert_session_flash(response, expected_msg, category='error')
480 assert_session_flash(response, expected_msg, category='error')
460
481
461 def test_missing_target_reference(self, pr_util, csrf_token):
482 def test_missing_target_reference(self, pr_util, csrf_token):
462 from rhodecode.lib.vcs.backends.base import MergeFailureReason
483 from rhodecode.lib.vcs.backends.base import MergeFailureReason
463 pull_request = pr_util.create_pull_request(
484 pull_request = pr_util.create_pull_request(
464 approved=True, mergeable=True)
485 approved=True, mergeable=True)
465 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
486 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
466 pull_request.target_ref = unicode_reference
487 pull_request.target_ref = unicode_reference
467 Session().add(pull_request)
488 Session().add(pull_request)
468 Session().commit()
489 Session().commit()
469
490
470 pull_request_id = pull_request.pull_request_id
491 pull_request_id = pull_request.pull_request_id
471 pull_request_url = route_path(
492 pull_request_url = route_path(
472 'pullrequest_show',
493 'pullrequest_show',
473 repo_name=pull_request.target_repo.repo_name,
494 repo_name=pull_request.target_repo.repo_name,
474 pull_request_id=pull_request_id)
495 pull_request_id=pull_request_id)
475
496
476 response = self.app.get(pull_request_url)
497 response = self.app.get(pull_request_url)
477 target_ref_id = 'invalid-branch'
498 target_ref_id = 'invalid-branch'
478 merge_resp = MergeResponse(
499 merge_resp = MergeResponse(
479 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
500 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
480 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
501 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
481 response.assert_response().element_contains(
502 response.assert_response().element_contains(
482 'div[data-role="merge-message"]', merge_resp.merge_status_message)
503 'div[data-role="merge-message"]', merge_resp.merge_status_message)
483
504
484 def test_comment_and_close_pull_request_custom_message_approved(
505 def test_comment_and_close_pull_request_custom_message_approved(
485 self, pr_util, csrf_token, xhr_header):
506 self, pr_util, csrf_token, xhr_header):
486
507
487 pull_request = pr_util.create_pull_request(approved=True)
508 pull_request = pr_util.create_pull_request(approved=True)
488 pull_request_id = pull_request.pull_request_id
509 pull_request_id = pull_request.pull_request_id
489 author = pull_request.user_id
510 author = pull_request.user_id
490 repo = pull_request.target_repo.repo_id
511 repo = pull_request.target_repo.repo_id
491
512
492 self.app.post(
513 self.app.post(
493 route_path('pullrequest_comment_create',
514 route_path('pullrequest_comment_create',
494 repo_name=pull_request.target_repo.scm_instance().name,
515 repo_name=pull_request.target_repo.scm_instance().name,
495 pull_request_id=pull_request_id),
516 pull_request_id=pull_request_id),
496 params={
517 params={
497 'close_pull_request': '1',
518 'close_pull_request': '1',
498 'text': 'Closing a PR',
519 'text': 'Closing a PR',
499 'csrf_token': csrf_token},
520 'csrf_token': csrf_token},
500 extra_environ=xhr_header,)
521 extra_environ=xhr_header,)
501
522
502 journal = UserLog.query()\
523 journal = UserLog.query()\
503 .filter(UserLog.user_id == author)\
524 .filter(UserLog.user_id == author)\
504 .filter(UserLog.repository_id == repo) \
525 .filter(UserLog.repository_id == repo) \
505 .order_by(UserLog.user_log_id.asc()) \
526 .order_by(UserLog.user_log_id.asc()) \
506 .all()
527 .all()
507 assert journal[-1].action == 'repo.pull_request.close'
528 assert journal[-1].action == 'repo.pull_request.close'
508
529
509 pull_request = PullRequest.get(pull_request_id)
530 pull_request = PullRequest.get(pull_request_id)
510 assert pull_request.is_closed()
531 assert pull_request.is_closed()
511
532
512 status = ChangesetStatusModel().get_status(
533 status = ChangesetStatusModel().get_status(
513 pull_request.source_repo, pull_request=pull_request)
534 pull_request.source_repo, pull_request=pull_request)
514 assert status == ChangesetStatus.STATUS_APPROVED
535 assert status == ChangesetStatus.STATUS_APPROVED
515 comments = ChangesetComment().query() \
536 comments = ChangesetComment().query() \
516 .filter(ChangesetComment.pull_request == pull_request) \
537 .filter(ChangesetComment.pull_request == pull_request) \
517 .order_by(ChangesetComment.comment_id.asc())\
538 .order_by(ChangesetComment.comment_id.asc())\
518 .all()
539 .all()
519 assert comments[-1].text == 'Closing a PR'
540 assert comments[-1].text == 'Closing a PR'
520
541
521 def test_comment_force_close_pull_request_rejected(
542 def test_comment_force_close_pull_request_rejected(
522 self, pr_util, csrf_token, xhr_header):
543 self, pr_util, csrf_token, xhr_header):
523 pull_request = pr_util.create_pull_request()
544 pull_request = pr_util.create_pull_request()
524 pull_request_id = pull_request.pull_request_id
545 pull_request_id = pull_request.pull_request_id
525 PullRequestModel().update_reviewers(
546 PullRequestModel().update_reviewers(
526 pull_request_id, [
547 pull_request_id, [
527 (1, ['reason'], False, 'reviewer', []),
548 (1, ['reason'], False, 'reviewer', []),
528 (2, ['reason2'], False, 'reviewer', [])],
549 (2, ['reason2'], False, 'reviewer', [])],
529 pull_request.author)
550 pull_request.author)
530 author = pull_request.user_id
551 author = pull_request.user_id
531 repo = pull_request.target_repo.repo_id
552 repo = pull_request.target_repo.repo_id
532
553
533 self.app.post(
554 self.app.post(
534 route_path('pullrequest_comment_create',
555 route_path('pullrequest_comment_create',
535 repo_name=pull_request.target_repo.scm_instance().name,
556 repo_name=pull_request.target_repo.scm_instance().name,
536 pull_request_id=pull_request_id),
557 pull_request_id=pull_request_id),
537 params={
558 params={
538 'close_pull_request': '1',
559 'close_pull_request': '1',
539 'csrf_token': csrf_token},
560 'csrf_token': csrf_token},
540 extra_environ=xhr_header)
561 extra_environ=xhr_header)
541
562
542 pull_request = PullRequest.get(pull_request_id)
563 pull_request = PullRequest.get(pull_request_id)
543
564
544 journal = UserLog.query()\
565 journal = UserLog.query()\
545 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
566 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
546 .order_by(UserLog.user_log_id.asc()) \
567 .order_by(UserLog.user_log_id.asc()) \
547 .all()
568 .all()
548 assert journal[-1].action == 'repo.pull_request.close'
569 assert journal[-1].action == 'repo.pull_request.close'
549
570
550 # check only the latest status, not the review status
571 # check only the latest status, not the review status
551 status = ChangesetStatusModel().get_status(
572 status = ChangesetStatusModel().get_status(
552 pull_request.source_repo, pull_request=pull_request)
573 pull_request.source_repo, pull_request=pull_request)
553 assert status == ChangesetStatus.STATUS_REJECTED
574 assert status == ChangesetStatus.STATUS_REJECTED
554
575
555 def test_comment_and_close_pull_request(
576 def test_comment_and_close_pull_request(
556 self, pr_util, csrf_token, xhr_header):
577 self, pr_util, csrf_token, xhr_header):
557 pull_request = pr_util.create_pull_request()
578 pull_request = pr_util.create_pull_request()
558 pull_request_id = pull_request.pull_request_id
579 pull_request_id = pull_request.pull_request_id
559
580
560 response = self.app.post(
581 response = self.app.post(
561 route_path('pullrequest_comment_create',
582 route_path('pullrequest_comment_create',
562 repo_name=pull_request.target_repo.scm_instance().name,
583 repo_name=pull_request.target_repo.scm_instance().name,
563 pull_request_id=pull_request.pull_request_id),
584 pull_request_id=pull_request.pull_request_id),
564 params={
585 params={
565 'close_pull_request': 'true',
586 'close_pull_request': 'true',
566 'csrf_token': csrf_token},
587 'csrf_token': csrf_token},
567 extra_environ=xhr_header)
588 extra_environ=xhr_header)
568
589
569 assert response.json
590 assert response.json
570
591
571 pull_request = PullRequest.get(pull_request_id)
592 pull_request = PullRequest.get(pull_request_id)
572 assert pull_request.is_closed()
593 assert pull_request.is_closed()
573
594
574 # check only the latest status, not the review status
595 # check only the latest status, not the review status
575 status = ChangesetStatusModel().get_status(
596 status = ChangesetStatusModel().get_status(
576 pull_request.source_repo, pull_request=pull_request)
597 pull_request.source_repo, pull_request=pull_request)
577 assert status == ChangesetStatus.STATUS_REJECTED
598 assert status == ChangesetStatus.STATUS_REJECTED
578
599
579 def test_comment_and_close_pull_request_try_edit_comment(
600 def test_comment_and_close_pull_request_try_edit_comment(
580 self, pr_util, csrf_token, xhr_header
601 self, pr_util, csrf_token, xhr_header
581 ):
602 ):
582 pull_request = pr_util.create_pull_request()
603 pull_request = pr_util.create_pull_request()
583 pull_request_id = pull_request.pull_request_id
604 pull_request_id = pull_request.pull_request_id
584 target_scm = pull_request.target_repo.scm_instance()
605 target_scm = pull_request.target_repo.scm_instance()
585 target_scm_name = target_scm.name
606 target_scm_name = target_scm.name
586
607
587 response = self.app.post(
608 response = self.app.post(
588 route_path(
609 route_path(
589 'pullrequest_comment_create',
610 'pullrequest_comment_create',
590 repo_name=target_scm_name,
611 repo_name=target_scm_name,
591 pull_request_id=pull_request_id,
612 pull_request_id=pull_request_id,
592 ),
613 ),
593 params={
614 params={
594 'close_pull_request': 'true',
615 'close_pull_request': 'true',
595 'csrf_token': csrf_token,
616 'csrf_token': csrf_token,
596 },
617 },
597 extra_environ=xhr_header)
618 extra_environ=xhr_header)
598
619
599 assert response.json
620 assert response.json
600
621
601 pull_request = PullRequest.get(pull_request_id)
622 pull_request = PullRequest.get(pull_request_id)
602 target_scm = pull_request.target_repo.scm_instance()
623 target_scm = pull_request.target_repo.scm_instance()
603 target_scm_name = target_scm.name
624 target_scm_name = target_scm.name
604 assert pull_request.is_closed()
625 assert pull_request.is_closed()
605
626
606 # check only the latest status, not the review status
627 # check only the latest status, not the review status
607 status = ChangesetStatusModel().get_status(
628 status = ChangesetStatusModel().get_status(
608 pull_request.source_repo, pull_request=pull_request)
629 pull_request.source_repo, pull_request=pull_request)
609 assert status == ChangesetStatus.STATUS_REJECTED
630 assert status == ChangesetStatus.STATUS_REJECTED
610
631
611 for comment_id in response.json.keys():
632 for comment_id in response.json.keys():
612 test_text = 'test'
633 test_text = 'test'
613 response = self.app.post(
634 response = self.app.post(
614 route_path(
635 route_path(
615 'pullrequest_comment_edit',
636 'pullrequest_comment_edit',
616 repo_name=target_scm_name,
637 repo_name=target_scm_name,
617 pull_request_id=pull_request_id,
638 pull_request_id=pull_request_id,
618 comment_id=comment_id,
639 comment_id=comment_id,
619 ),
640 ),
620 extra_environ=xhr_header,
641 extra_environ=xhr_header,
621 params={
642 params={
622 'csrf_token': csrf_token,
643 'csrf_token': csrf_token,
623 'text': test_text,
644 'text': test_text,
624 },
645 },
625 status=403,
646 status=403,
626 )
647 )
627 assert response.status_int == 403
648 assert response.status_int == 403
628
649
629 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
650 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
630 pull_request = pr_util.create_pull_request()
651 pull_request = pr_util.create_pull_request()
631 target_scm = pull_request.target_repo.scm_instance()
652 target_scm = pull_request.target_repo.scm_instance()
632 target_scm_name = target_scm.name
653 target_scm_name = target_scm.name
633
654
634 response = self.app.post(
655 response = self.app.post(
635 route_path(
656 route_path(
636 'pullrequest_comment_create',
657 'pullrequest_comment_create',
637 repo_name=target_scm_name,
658 repo_name=target_scm_name,
638 pull_request_id=pull_request.pull_request_id),
659 pull_request_id=pull_request.pull_request_id),
639 params={
660 params={
640 'csrf_token': csrf_token,
661 'csrf_token': csrf_token,
641 'text': 'init',
662 'text': 'init',
642 },
663 },
643 extra_environ=xhr_header,
664 extra_environ=xhr_header,
644 )
665 )
645 assert response.json
666 assert response.json
646
667
647 for comment_id in response.json.keys():
668 for comment_id in response.json.keys():
648 assert comment_id
669 assert comment_id
649 test_text = 'test'
670 test_text = 'test'
650 self.app.post(
671 self.app.post(
651 route_path(
672 route_path(
652 'pullrequest_comment_edit',
673 'pullrequest_comment_edit',
653 repo_name=target_scm_name,
674 repo_name=target_scm_name,
654 pull_request_id=pull_request.pull_request_id,
675 pull_request_id=pull_request.pull_request_id,
655 comment_id=comment_id,
676 comment_id=comment_id,
656 ),
677 ),
657 extra_environ=xhr_header,
678 extra_environ=xhr_header,
658 params={
679 params={
659 'csrf_token': csrf_token,
680 'csrf_token': csrf_token,
660 'text': test_text,
681 'text': test_text,
661 'version': '0',
682 'version': '0',
662 },
683 },
663
684
664 )
685 )
665 text_form_db = ChangesetComment.query().filter(
686 text_form_db = ChangesetComment.query().filter(
666 ChangesetComment.comment_id == comment_id).first().text
687 ChangesetComment.comment_id == comment_id).first().text
667 assert test_text == text_form_db
688 assert test_text == text_form_db
668
689
669 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
690 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
670 pull_request = pr_util.create_pull_request()
691 pull_request = pr_util.create_pull_request()
671 target_scm = pull_request.target_repo.scm_instance()
692 target_scm = pull_request.target_repo.scm_instance()
672 target_scm_name = target_scm.name
693 target_scm_name = target_scm.name
673
694
674 response = self.app.post(
695 response = self.app.post(
675 route_path(
696 route_path(
676 'pullrequest_comment_create',
697 'pullrequest_comment_create',
677 repo_name=target_scm_name,
698 repo_name=target_scm_name,
678 pull_request_id=pull_request.pull_request_id),
699 pull_request_id=pull_request.pull_request_id),
679 params={
700 params={
680 'csrf_token': csrf_token,
701 'csrf_token': csrf_token,
681 'text': 'init',
702 'text': 'init',
682 },
703 },
683 extra_environ=xhr_header,
704 extra_environ=xhr_header,
684 )
705 )
685 assert response.json
706 assert response.json
686
707
687 for comment_id in response.json.keys():
708 for comment_id in response.json.keys():
688 test_text = 'init'
709 test_text = 'init'
689 response = self.app.post(
710 response = self.app.post(
690 route_path(
711 route_path(
691 'pullrequest_comment_edit',
712 'pullrequest_comment_edit',
692 repo_name=target_scm_name,
713 repo_name=target_scm_name,
693 pull_request_id=pull_request.pull_request_id,
714 pull_request_id=pull_request.pull_request_id,
694 comment_id=comment_id,
715 comment_id=comment_id,
695 ),
716 ),
696 extra_environ=xhr_header,
717 extra_environ=xhr_header,
697 params={
718 params={
698 'csrf_token': csrf_token,
719 'csrf_token': csrf_token,
699 'text': test_text,
720 'text': test_text,
700 'version': '0',
721 'version': '0',
701 },
722 },
702 status=404,
723 status=404,
703
724
704 )
725 )
705 assert response.status_int == 404
726 assert response.status_int == 404
706
727
707 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
728 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
708 pull_request = pr_util.create_pull_request()
729 pull_request = pr_util.create_pull_request()
709 target_scm = pull_request.target_repo.scm_instance()
730 target_scm = pull_request.target_repo.scm_instance()
710 target_scm_name = target_scm.name
731 target_scm_name = target_scm.name
711
732
712 response = self.app.post(
733 response = self.app.post(
713 route_path(
734 route_path(
714 'pullrequest_comment_create',
735 'pullrequest_comment_create',
715 repo_name=target_scm_name,
736 repo_name=target_scm_name,
716 pull_request_id=pull_request.pull_request_id),
737 pull_request_id=pull_request.pull_request_id),
717 params={
738 params={
718 'csrf_token': csrf_token,
739 'csrf_token': csrf_token,
719 'text': 'init',
740 'text': 'init',
720 },
741 },
721 extra_environ=xhr_header,
742 extra_environ=xhr_header,
722 )
743 )
723 assert response.json
744 assert response.json
724 for comment_id in response.json.keys():
745 for comment_id in response.json.keys():
725 test_text = 'test'
746 test_text = 'test'
726 self.app.post(
747 self.app.post(
727 route_path(
748 route_path(
728 'pullrequest_comment_edit',
749 'pullrequest_comment_edit',
729 repo_name=target_scm_name,
750 repo_name=target_scm_name,
730 pull_request_id=pull_request.pull_request_id,
751 pull_request_id=pull_request.pull_request_id,
731 comment_id=comment_id,
752 comment_id=comment_id,
732 ),
753 ),
733 extra_environ=xhr_header,
754 extra_environ=xhr_header,
734 params={
755 params={
735 'csrf_token': csrf_token,
756 'csrf_token': csrf_token,
736 'text': test_text,
757 'text': test_text,
737 'version': '0',
758 'version': '0',
738 },
759 },
739
760
740 )
761 )
741 test_text_v2 = 'test_v2'
762 test_text_v2 = 'test_v2'
742 response = self.app.post(
763 response = self.app.post(
743 route_path(
764 route_path(
744 'pullrequest_comment_edit',
765 'pullrequest_comment_edit',
745 repo_name=target_scm_name,
766 repo_name=target_scm_name,
746 pull_request_id=pull_request.pull_request_id,
767 pull_request_id=pull_request.pull_request_id,
747 comment_id=comment_id,
768 comment_id=comment_id,
748 ),
769 ),
749 extra_environ=xhr_header,
770 extra_environ=xhr_header,
750 params={
771 params={
751 'csrf_token': csrf_token,
772 'csrf_token': csrf_token,
752 'text': test_text_v2,
773 'text': test_text_v2,
753 'version': '0',
774 'version': '0',
754 },
775 },
755 status=409,
776 status=409,
756 )
777 )
757 assert response.status_int == 409
778 assert response.status_int == 409
758
779
759 text_form_db = ChangesetComment.query().filter(
780 text_form_db = ChangesetComment.query().filter(
760 ChangesetComment.comment_id == comment_id).first().text
781 ChangesetComment.comment_id == comment_id).first().text
761
782
762 assert test_text == text_form_db
783 assert test_text == text_form_db
763 assert test_text_v2 != text_form_db
784 assert test_text_v2 != text_form_db
764
785
765 def test_comment_and_comment_edit_permissions_forbidden(
786 def test_comment_and_comment_edit_permissions_forbidden(
766 self, autologin_regular_user, user_regular, user_admin, pr_util,
787 self, autologin_regular_user, user_regular, user_admin, pr_util,
767 csrf_token, xhr_header):
788 csrf_token, xhr_header):
768 pull_request = pr_util.create_pull_request(
789 pull_request = pr_util.create_pull_request(
769 author=user_admin.username, enable_notifications=False)
790 author=user_admin.username, enable_notifications=False)
770 comment = CommentsModel().create(
791 comment = CommentsModel().create(
771 text='test',
792 text='test',
772 repo=pull_request.target_repo.scm_instance().name,
793 repo=pull_request.target_repo.scm_instance().name,
773 user=user_admin,
794 user=user_admin,
774 pull_request=pull_request,
795 pull_request=pull_request,
775 )
796 )
776 response = self.app.post(
797 response = self.app.post(
777 route_path(
798 route_path(
778 'pullrequest_comment_edit',
799 'pullrequest_comment_edit',
779 repo_name=pull_request.target_repo.scm_instance().name,
800 repo_name=pull_request.target_repo.scm_instance().name,
780 pull_request_id=pull_request.pull_request_id,
801 pull_request_id=pull_request.pull_request_id,
781 comment_id=comment.comment_id,
802 comment_id=comment.comment_id,
782 ),
803 ),
783 extra_environ=xhr_header,
804 extra_environ=xhr_header,
784 params={
805 params={
785 'csrf_token': csrf_token,
806 'csrf_token': csrf_token,
786 'text': 'test_text',
807 'text': 'test_text',
787 },
808 },
788 status=403,
809 status=403,
789 )
810 )
790 assert response.status_int == 403
811 assert response.status_int == 403
791
812
792 def test_create_pull_request(self, backend, csrf_token):
813 def test_create_pull_request(self, backend, csrf_token):
793 commits = [
814 commits = [
794 {'message': 'ancestor'},
815 {'message': 'ancestor'},
795 {'message': 'change'},
816 {'message': 'change'},
796 {'message': 'change2'},
817 {'message': 'change2'},
797 ]
818 ]
798 commit_ids = backend.create_master_repo(commits)
819 commit_ids = backend.create_master_repo(commits)
799 target = backend.create_repo(heads=['ancestor'])
820 target = backend.create_repo(heads=['ancestor'])
800 source = backend.create_repo(heads=['change2'])
821 source = backend.create_repo(heads=['change2'])
801
822
802 response = self.app.post(
823 response = self.app.post(
803 route_path('pullrequest_create', repo_name=source.repo_name),
824 route_path('pullrequest_create', repo_name=source.repo_name),
804 [
825 [
805 ('source_repo', source.repo_name),
826 ('source_repo', source.repo_name),
806 ('source_ref', 'branch:default:' + commit_ids['change2']),
827 ('source_ref', 'branch:default:' + commit_ids['change2']),
807 ('target_repo', target.repo_name),
828 ('target_repo', target.repo_name),
808 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
829 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
809 ('common_ancestor', commit_ids['ancestor']),
830 ('common_ancestor', commit_ids['ancestor']),
810 ('pullrequest_title', 'Title'),
831 ('pullrequest_title', 'Title'),
811 ('pullrequest_desc', 'Description'),
832 ('pullrequest_desc', 'Description'),
812 ('description_renderer', 'markdown'),
833 ('description_renderer', 'markdown'),
813 ('__start__', 'review_members:sequence'),
834 ('__start__', 'review_members:sequence'),
814 ('__start__', 'reviewer:mapping'),
835 ('__start__', 'reviewer:mapping'),
815 ('user_id', '1'),
836 ('user_id', '1'),
816 ('__start__', 'reasons:sequence'),
837 ('__start__', 'reasons:sequence'),
817 ('reason', 'Some reason'),
838 ('reason', 'Some reason'),
818 ('__end__', 'reasons:sequence'),
839 ('__end__', 'reasons:sequence'),
819 ('__start__', 'rules:sequence'),
840 ('__start__', 'rules:sequence'),
820 ('__end__', 'rules:sequence'),
841 ('__end__', 'rules:sequence'),
821 ('mandatory', 'False'),
842 ('mandatory', 'False'),
822 ('__end__', 'reviewer:mapping'),
843 ('__end__', 'reviewer:mapping'),
823 ('__end__', 'review_members:sequence'),
844 ('__end__', 'review_members:sequence'),
824 ('__start__', 'revisions:sequence'),
845 ('__start__', 'revisions:sequence'),
825 ('revisions', commit_ids['change']),
846 ('revisions', commit_ids['change']),
826 ('revisions', commit_ids['change2']),
847 ('revisions', commit_ids['change2']),
827 ('__end__', 'revisions:sequence'),
848 ('__end__', 'revisions:sequence'),
828 ('user', ''),
849 ('user', ''),
829 ('csrf_token', csrf_token),
850 ('csrf_token', csrf_token),
830 ],
851 ],
831 status=302)
852 status=302)
832
853
833 location = response.headers['Location']
854 location = response.headers['Location']
834 pull_request_id = location.rsplit('/', 1)[1]
855 pull_request_id = location.rsplit('/', 1)[1]
835 assert pull_request_id != 'new'
856 assert pull_request_id != 'new'
836 pull_request = PullRequest.get(int(pull_request_id))
857 pull_request = PullRequest.get(int(pull_request_id))
837
858
838 # check that we have now both revisions
859 # check that we have now both revisions
839 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
860 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
840 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
861 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
841 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
862 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
842 assert pull_request.target_ref == expected_target_ref
863 assert pull_request.target_ref == expected_target_ref
843
864
844 def test_reviewer_notifications(self, backend, csrf_token):
865 def test_reviewer_notifications(self, backend, csrf_token):
845 # We have to use the app.post for this test so it will create the
866 # We have to use the app.post for this test so it will create the
846 # notifications properly with the new PR
867 # notifications properly with the new PR
847 commits = [
868 commits = [
848 {'message': 'ancestor',
869 {'message': 'ancestor',
849 'added': [FileNode('file_A', content='content_of_ancestor')]},
870 'added': [FileNode('file_A', content='content_of_ancestor')]},
850 {'message': 'change',
871 {'message': 'change',
851 'added': [FileNode('file_a', content='content_of_change')]},
872 'added': [FileNode('file_a', content='content_of_change')]},
852 {'message': 'change-child'},
873 {'message': 'change-child'},
853 {'message': 'ancestor-child', 'parents': ['ancestor'],
874 {'message': 'ancestor-child', 'parents': ['ancestor'],
854 'added': [
875 'added': [
855 FileNode('file_B', content='content_of_ancestor_child')]},
876 FileNode('file_B', content='content_of_ancestor_child')]},
856 {'message': 'ancestor-child-2'},
877 {'message': 'ancestor-child-2'},
857 ]
878 ]
858 commit_ids = backend.create_master_repo(commits)
879 commit_ids = backend.create_master_repo(commits)
859 target = backend.create_repo(heads=['ancestor-child'])
880 target = backend.create_repo(heads=['ancestor-child'])
860 source = backend.create_repo(heads=['change'])
881 source = backend.create_repo(heads=['change'])
861
882
862 response = self.app.post(
883 response = self.app.post(
863 route_path('pullrequest_create', repo_name=source.repo_name),
884 route_path('pullrequest_create', repo_name=source.repo_name),
864 [
885 [
865 ('source_repo', source.repo_name),
886 ('source_repo', source.repo_name),
866 ('source_ref', 'branch:default:' + commit_ids['change']),
887 ('source_ref', 'branch:default:' + commit_ids['change']),
867 ('target_repo', target.repo_name),
888 ('target_repo', target.repo_name),
868 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
889 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
869 ('common_ancestor', commit_ids['ancestor']),
890 ('common_ancestor', commit_ids['ancestor']),
870 ('pullrequest_title', 'Title'),
891 ('pullrequest_title', 'Title'),
871 ('pullrequest_desc', 'Description'),
892 ('pullrequest_desc', 'Description'),
872 ('description_renderer', 'markdown'),
893 ('description_renderer', 'markdown'),
873 ('__start__', 'review_members:sequence'),
894 ('__start__', 'review_members:sequence'),
874 ('__start__', 'reviewer:mapping'),
895 ('__start__', 'reviewer:mapping'),
875 ('user_id', '2'),
896 ('user_id', '2'),
876 ('__start__', 'reasons:sequence'),
897 ('__start__', 'reasons:sequence'),
877 ('reason', 'Some reason'),
898 ('reason', 'Some reason'),
878 ('__end__', 'reasons:sequence'),
899 ('__end__', 'reasons:sequence'),
879 ('__start__', 'rules:sequence'),
900 ('__start__', 'rules:sequence'),
880 ('__end__', 'rules:sequence'),
901 ('__end__', 'rules:sequence'),
881 ('mandatory', 'False'),
902 ('mandatory', 'False'),
882 ('__end__', 'reviewer:mapping'),
903 ('__end__', 'reviewer:mapping'),
883 ('__end__', 'review_members:sequence'),
904 ('__end__', 'review_members:sequence'),
884 ('__start__', 'revisions:sequence'),
905 ('__start__', 'revisions:sequence'),
885 ('revisions', commit_ids['change']),
906 ('revisions', commit_ids['change']),
886 ('__end__', 'revisions:sequence'),
907 ('__end__', 'revisions:sequence'),
887 ('user', ''),
908 ('user', ''),
888 ('csrf_token', csrf_token),
909 ('csrf_token', csrf_token),
889 ],
910 ],
890 status=302)
911 status=302)
891
912
892 location = response.headers['Location']
913 location = response.headers['Location']
893
914
894 pull_request_id = location.rsplit('/', 1)[1]
915 pull_request_id = location.rsplit('/', 1)[1]
895 assert pull_request_id != 'new'
916 assert pull_request_id != 'new'
896 pull_request = PullRequest.get(int(pull_request_id))
917 pull_request = PullRequest.get(int(pull_request_id))
897
918
898 # Check that a notification was made
919 # Check that a notification was made
899 notifications = Notification.query()\
920 notifications = Notification.query()\
900 .filter(Notification.created_by == pull_request.author.user_id,
921 .filter(Notification.created_by == pull_request.author.user_id,
901 Notification.type_ == Notification.TYPE_PULL_REQUEST,
922 Notification.type_ == Notification.TYPE_PULL_REQUEST,
902 Notification.subject.contains(
923 Notification.subject.contains(
903 "requested a pull request review. !%s" % pull_request_id))
924 "requested a pull request review. !%s" % pull_request_id))
904 assert len(notifications.all()) == 1
925 assert len(notifications.all()) == 1
905
926
906 # Change reviewers and check that a notification was made
927 # Change reviewers and check that a notification was made
907 PullRequestModel().update_reviewers(
928 PullRequestModel().update_reviewers(
908 pull_request.pull_request_id, [
929 pull_request.pull_request_id, [
909 (1, [], False, 'reviewer', [])
930 (1, [], False, 'reviewer', [])
910 ],
931 ],
911 pull_request.author)
932 pull_request.author)
912 assert len(notifications.all()) == 2
933 assert len(notifications.all()) == 2
913
934
914 def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token):
935 def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token):
915 commits = [
936 commits = [
916 {'message': 'ancestor',
937 {'message': 'ancestor',
917 'added': [FileNode('file_A', content='content_of_ancestor')]},
938 'added': [FileNode('file_A', content='content_of_ancestor')]},
918 {'message': 'change',
939 {'message': 'change',
919 'added': [FileNode('file_a', content='content_of_change')]},
940 'added': [FileNode('file_a', content='content_of_change')]},
920 {'message': 'change-child'},
941 {'message': 'change-child'},
921 {'message': 'ancestor-child', 'parents': ['ancestor'],
942 {'message': 'ancestor-child', 'parents': ['ancestor'],
922 'added': [
943 'added': [
923 FileNode('file_B', content='content_of_ancestor_child')]},
944 FileNode('file_B', content='content_of_ancestor_child')]},
924 {'message': 'ancestor-child-2'},
945 {'message': 'ancestor-child-2'},
925 ]
946 ]
926 commit_ids = backend.create_master_repo(commits)
947 commit_ids = backend.create_master_repo(commits)
927 target = backend.create_repo(heads=['ancestor-child'])
948 target = backend.create_repo(heads=['ancestor-child'])
928 source = backend.create_repo(heads=['change'])
949 source = backend.create_repo(heads=['change'])
929
950
930 response = self.app.post(
951 response = self.app.post(
931 route_path('pullrequest_create', repo_name=source.repo_name),
952 route_path('pullrequest_create', repo_name=source.repo_name),
932 [
953 [
933 ('source_repo', source.repo_name),
954 ('source_repo', source.repo_name),
934 ('source_ref', 'branch:default:' + commit_ids['change']),
955 ('source_ref', 'branch:default:' + commit_ids['change']),
935 ('target_repo', target.repo_name),
956 ('target_repo', target.repo_name),
936 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
957 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
937 ('common_ancestor', commit_ids['ancestor']),
958 ('common_ancestor', commit_ids['ancestor']),
938 ('pullrequest_title', 'Title'),
959 ('pullrequest_title', 'Title'),
939 ('pullrequest_desc', 'Description'),
960 ('pullrequest_desc', 'Description'),
940 ('description_renderer', 'markdown'),
961 ('description_renderer', 'markdown'),
941 ('__start__', 'review_members:sequence'),
962 ('__start__', 'review_members:sequence'),
942 ('__start__', 'reviewer:mapping'),
963 ('__start__', 'reviewer:mapping'),
943 ('user_id', '1'),
964 ('user_id', '1'),
944 ('__start__', 'reasons:sequence'),
965 ('__start__', 'reasons:sequence'),
945 ('reason', 'Some reason'),
966 ('reason', 'Some reason'),
946 ('__end__', 'reasons:sequence'),
967 ('__end__', 'reasons:sequence'),
947 ('__start__', 'rules:sequence'),
968 ('__start__', 'rules:sequence'),
948 ('__end__', 'rules:sequence'),
969 ('__end__', 'rules:sequence'),
949 ('mandatory', 'False'),
970 ('mandatory', 'False'),
950 ('__end__', 'reviewer:mapping'),
971 ('__end__', 'reviewer:mapping'),
951 ('__end__', 'review_members:sequence'),
972 ('__end__', 'review_members:sequence'),
952 ('__start__', 'revisions:sequence'),
973 ('__start__', 'revisions:sequence'),
953 ('revisions', commit_ids['change']),
974 ('revisions', commit_ids['change']),
954 ('__end__', 'revisions:sequence'),
975 ('__end__', 'revisions:sequence'),
955 ('user', ''),
976 ('user', ''),
956 ('csrf_token', csrf_token),
977 ('csrf_token', csrf_token),
957 ],
978 ],
958 status=302)
979 status=302)
959
980
960 location = response.headers['Location']
981 location = response.headers['Location']
961
982
962 pull_request_id = location.rsplit('/', 1)[1]
983 pull_request_id = location.rsplit('/', 1)[1]
963 assert pull_request_id != 'new'
984 assert pull_request_id != 'new'
964 pull_request = PullRequest.get(int(pull_request_id))
985 pull_request = PullRequest.get(int(pull_request_id))
965
986
966 # target_ref has to point to the ancestor's commit_id in order to
987 # target_ref has to point to the ancestor's commit_id in order to
967 # show the correct diff
988 # show the correct diff
968 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
989 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
969 assert pull_request.target_ref == expected_target_ref
990 assert pull_request.target_ref == expected_target_ref
970
991
971 # Check generated diff contents
992 # Check generated diff contents
972 response = response.follow()
993 response = response.follow()
973 response.mustcontain(no=['content_of_ancestor'])
994 response.mustcontain(no=['content_of_ancestor'])
974 response.mustcontain(no=['content_of_ancestor-child'])
995 response.mustcontain(no=['content_of_ancestor-child'])
975 response.mustcontain('content_of_change')
996 response.mustcontain('content_of_change')
976
997
977 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
998 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
978 # Clear any previous calls to rcextensions
999 # Clear any previous calls to rcextensions
979 rhodecode.EXTENSIONS.calls.clear()
1000 rhodecode.EXTENSIONS.calls.clear()
980
1001
981 pull_request = pr_util.create_pull_request(
1002 pull_request = pr_util.create_pull_request(
982 approved=True, mergeable=True)
1003 approved=True, mergeable=True)
983 pull_request_id = pull_request.pull_request_id
1004 pull_request_id = pull_request.pull_request_id
984 repo_name = pull_request.target_repo.scm_instance().name,
1005 repo_name = pull_request.target_repo.scm_instance().name,
985
1006
986 url = route_path('pullrequest_merge',
1007 url = route_path('pullrequest_merge',
987 repo_name=str(repo_name[0]),
1008 repo_name=str(repo_name[0]),
988 pull_request_id=pull_request_id)
1009 pull_request_id=pull_request_id)
989 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
1010 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
990
1011
991 pull_request = PullRequest.get(pull_request_id)
1012 pull_request = PullRequest.get(pull_request_id)
992
1013
993 assert response.status_int == 200
1014 assert response.status_int == 200
994 assert pull_request.is_closed()
1015 assert pull_request.is_closed()
995 assert_pull_request_status(
1016 assert_pull_request_status(
996 pull_request, ChangesetStatus.STATUS_APPROVED)
1017 pull_request, ChangesetStatus.STATUS_APPROVED)
997
1018
998 # Check the relevant log entries were added
1019 # Check the relevant log entries were added
999 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
1020 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
1000 actions = [log.action for log in user_logs]
1021 actions = [log.action for log in user_logs]
1001 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
1022 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
1002 expected_actions = [
1023 expected_actions = [
1003 u'repo.pull_request.close',
1024 u'repo.pull_request.close',
1004 u'repo.pull_request.merge',
1025 u'repo.pull_request.merge',
1005 u'repo.pull_request.comment.create'
1026 u'repo.pull_request.comment.create'
1006 ]
1027 ]
1007 assert actions == expected_actions
1028 assert actions == expected_actions
1008
1029
1009 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
1030 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
1010 actions = [log for log in user_logs]
1031 actions = [log for log in user_logs]
1011 assert actions[-1].action == 'user.push'
1032 assert actions[-1].action == 'user.push'
1012 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
1033 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
1013
1034
1014 # Check post_push rcextension was really executed
1035 # Check post_push rcextension was really executed
1015 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
1036 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
1016 assert len(push_calls) == 1
1037 assert len(push_calls) == 1
1017 unused_last_call_args, last_call_kwargs = push_calls[0]
1038 unused_last_call_args, last_call_kwargs = push_calls[0]
1018 assert last_call_kwargs['action'] == 'push'
1039 assert last_call_kwargs['action'] == 'push'
1019 assert last_call_kwargs['commit_ids'] == pr_commit_ids
1040 assert last_call_kwargs['commit_ids'] == pr_commit_ids
1020
1041
1021 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
1042 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
1022 pull_request = pr_util.create_pull_request(mergeable=False)
1043 pull_request = pr_util.create_pull_request(mergeable=False)
1023 pull_request_id = pull_request.pull_request_id
1044 pull_request_id = pull_request.pull_request_id
1024 pull_request = PullRequest.get(pull_request_id)
1045 pull_request = PullRequest.get(pull_request_id)
1025
1046
1026 response = self.app.post(
1047 response = self.app.post(
1027 route_path('pullrequest_merge',
1048 route_path('pullrequest_merge',
1028 repo_name=pull_request.target_repo.scm_instance().name,
1049 repo_name=pull_request.target_repo.scm_instance().name,
1029 pull_request_id=pull_request.pull_request_id),
1050 pull_request_id=pull_request.pull_request_id),
1030 params={'csrf_token': csrf_token}).follow()
1051 params={'csrf_token': csrf_token}).follow()
1031
1052
1032 assert response.status_int == 200
1053 assert response.status_int == 200
1033 response.mustcontain(
1054 response.mustcontain(
1034 'Merge is not currently possible because of below failed checks.')
1055 'Merge is not currently possible because of below failed checks.')
1035 response.mustcontain('Server-side pull request merging is disabled.')
1056 response.mustcontain('Server-side pull request merging is disabled.')
1036
1057
1037 @pytest.mark.skip_backends('svn')
1058 @pytest.mark.skip_backends('svn')
1038 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
1059 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
1039 pull_request = pr_util.create_pull_request(mergeable=True)
1060 pull_request = pr_util.create_pull_request(mergeable=True)
1040 pull_request_id = pull_request.pull_request_id
1061 pull_request_id = pull_request.pull_request_id
1041 repo_name = pull_request.target_repo.scm_instance().name
1062 repo_name = pull_request.target_repo.scm_instance().name
1042
1063
1043 response = self.app.post(
1064 response = self.app.post(
1044 route_path('pullrequest_merge',
1065 route_path('pullrequest_merge',
1045 repo_name=repo_name, pull_request_id=pull_request_id),
1066 repo_name=repo_name, pull_request_id=pull_request_id),
1046 params={'csrf_token': csrf_token}).follow()
1067 params={'csrf_token': csrf_token}).follow()
1047
1068
1048 assert response.status_int == 200
1069 assert response.status_int == 200
1049
1070
1050 response.mustcontain(
1071 response.mustcontain(
1051 'Merge is not currently possible because of below failed checks.')
1072 'Merge is not currently possible because of below failed checks.')
1052 response.mustcontain('Pull request reviewer approval is pending.')
1073 response.mustcontain('Pull request reviewer approval is pending.')
1053
1074
1054 def test_merge_pull_request_renders_failure_reason(
1075 def test_merge_pull_request_renders_failure_reason(
1055 self, user_regular, csrf_token, pr_util):
1076 self, user_regular, csrf_token, pr_util):
1056 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
1077 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
1057 pull_request_id = pull_request.pull_request_id
1078 pull_request_id = pull_request.pull_request_id
1058 repo_name = pull_request.target_repo.scm_instance().name
1079 repo_name = pull_request.target_repo.scm_instance().name
1059
1080
1060 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
1081 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
1061 MergeFailureReason.PUSH_FAILED,
1082 MergeFailureReason.PUSH_FAILED,
1062 metadata={'target': 'shadow repo',
1083 metadata={'target': 'shadow repo',
1063 'merge_commit': 'xxx'})
1084 'merge_commit': 'xxx'})
1064 model_patcher = mock.patch.multiple(
1085 model_patcher = mock.patch.multiple(
1065 PullRequestModel,
1086 PullRequestModel,
1066 merge_repo=mock.Mock(return_value=merge_resp),
1087 merge_repo=mock.Mock(return_value=merge_resp),
1067 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
1088 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
1068
1089
1069 with model_patcher:
1090 with model_patcher:
1070 response = self.app.post(
1091 response = self.app.post(
1071 route_path('pullrequest_merge',
1092 route_path('pullrequest_merge',
1072 repo_name=repo_name,
1093 repo_name=repo_name,
1073 pull_request_id=pull_request_id),
1094 pull_request_id=pull_request_id),
1074 params={'csrf_token': csrf_token}, status=302)
1095 params={'csrf_token': csrf_token}, status=302)
1075
1096
1076 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
1097 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
1077 metadata={'target': 'shadow repo',
1098 metadata={'target': 'shadow repo',
1078 'merge_commit': 'xxx'})
1099 'merge_commit': 'xxx'})
1079 assert_session_flash(response, merge_resp.merge_status_message)
1100 assert_session_flash(response, merge_resp.merge_status_message)
1080
1101
1081 def test_update_source_revision(self, backend, csrf_token):
1102 def test_update_source_revision(self, backend, csrf_token):
1082 commits = [
1103 commits = [
1083 {'message': 'ancestor'},
1104 {'message': 'ancestor'},
1084 {'message': 'change'},
1105 {'message': 'change'},
1085 {'message': 'change-2'},
1106 {'message': 'change-2'},
1086 ]
1107 ]
1087 commit_ids = backend.create_master_repo(commits)
1108 commit_ids = backend.create_master_repo(commits)
1088 target = backend.create_repo(heads=['ancestor'])
1109 target = backend.create_repo(heads=['ancestor'])
1089 source = backend.create_repo(heads=['change'])
1110 source = backend.create_repo(heads=['change'])
1090
1111
1091 # create pr from a in source to A in target
1112 # create pr from a in source to A in target
1092 pull_request = PullRequest()
1113 pull_request = PullRequest()
1093
1114
1094 pull_request.source_repo = source
1115 pull_request.source_repo = source
1095 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1116 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1096 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1117 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1097
1118
1098 pull_request.target_repo = target
1119 pull_request.target_repo = target
1099 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1120 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1100 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1121 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1101
1122
1102 pull_request.revisions = [commit_ids['change']]
1123 pull_request.revisions = [commit_ids['change']]
1103 pull_request.title = u"Test"
1124 pull_request.title = u"Test"
1104 pull_request.description = u"Description"
1125 pull_request.description = u"Description"
1105 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1126 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1106 pull_request.pull_request_state = PullRequest.STATE_CREATED
1127 pull_request.pull_request_state = PullRequest.STATE_CREATED
1107 Session().add(pull_request)
1128 Session().add(pull_request)
1108 Session().commit()
1129 Session().commit()
1109 pull_request_id = pull_request.pull_request_id
1130 pull_request_id = pull_request.pull_request_id
1110
1131
1111 # source has ancestor - change - change-2
1132 # source has ancestor - change - change-2
1112 backend.pull_heads(source, heads=['change-2'])
1133 backend.pull_heads(source, heads=['change-2'])
1113 target_repo_name = target.repo_name
1134 target_repo_name = target.repo_name
1114
1135
1115 # update PR
1136 # update PR
1116 self.app.post(
1137 self.app.post(
1117 route_path('pullrequest_update',
1138 route_path('pullrequest_update',
1118 repo_name=target_repo_name, pull_request_id=pull_request_id),
1139 repo_name=target_repo_name, pull_request_id=pull_request_id),
1119 params={'update_commits': 'true', 'csrf_token': csrf_token})
1140 params={'update_commits': 'true', 'csrf_token': csrf_token})
1120
1141
1121 response = self.app.get(
1142 response = self.app.get(
1122 route_path('pullrequest_show',
1143 route_path('pullrequest_show',
1123 repo_name=target_repo_name,
1144 repo_name=target_repo_name,
1124 pull_request_id=pull_request.pull_request_id))
1145 pull_request_id=pull_request.pull_request_id))
1125
1146
1126 assert response.status_int == 200
1147 assert response.status_int == 200
1127 response.mustcontain('Pull request updated to')
1148 response.mustcontain('Pull request updated to')
1128 response.mustcontain('with 1 added, 0 removed commits.')
1149 response.mustcontain('with 1 added, 0 removed commits.')
1129
1150
1130 # check that we have now both revisions
1151 # check that we have now both revisions
1131 pull_request = PullRequest.get(pull_request_id)
1152 pull_request = PullRequest.get(pull_request_id)
1132 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
1153 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
1133
1154
1134 def test_update_target_revision(self, backend, csrf_token):
1155 def test_update_target_revision(self, backend, csrf_token):
1135 commits = [
1156 commits = [
1136 {'message': 'ancestor'},
1157 {'message': 'ancestor'},
1137 {'message': 'change'},
1158 {'message': 'change'},
1138 {'message': 'ancestor-new', 'parents': ['ancestor']},
1159 {'message': 'ancestor-new', 'parents': ['ancestor']},
1139 {'message': 'change-rebased'},
1160 {'message': 'change-rebased'},
1140 ]
1161 ]
1141 commit_ids = backend.create_master_repo(commits)
1162 commit_ids = backend.create_master_repo(commits)
1142 target = backend.create_repo(heads=['ancestor'])
1163 target = backend.create_repo(heads=['ancestor'])
1143 source = backend.create_repo(heads=['change'])
1164 source = backend.create_repo(heads=['change'])
1144
1165
1145 # create pr from a in source to A in target
1166 # create pr from a in source to A in target
1146 pull_request = PullRequest()
1167 pull_request = PullRequest()
1147
1168
1148 pull_request.source_repo = source
1169 pull_request.source_repo = source
1149 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1170 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1150 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1171 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1151
1172
1152 pull_request.target_repo = target
1173 pull_request.target_repo = target
1153 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1174 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1154 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1175 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1155
1176
1156 pull_request.revisions = [commit_ids['change']]
1177 pull_request.revisions = [commit_ids['change']]
1157 pull_request.title = u"Test"
1178 pull_request.title = u"Test"
1158 pull_request.description = u"Description"
1179 pull_request.description = u"Description"
1159 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1180 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1160 pull_request.pull_request_state = PullRequest.STATE_CREATED
1181 pull_request.pull_request_state = PullRequest.STATE_CREATED
1161
1182
1162 Session().add(pull_request)
1183 Session().add(pull_request)
1163 Session().commit()
1184 Session().commit()
1164 pull_request_id = pull_request.pull_request_id
1185 pull_request_id = pull_request.pull_request_id
1165
1186
1166 # target has ancestor - ancestor-new
1187 # target has ancestor - ancestor-new
1167 # source has ancestor - ancestor-new - change-rebased
1188 # source has ancestor - ancestor-new - change-rebased
1168 backend.pull_heads(target, heads=['ancestor-new'])
1189 backend.pull_heads(target, heads=['ancestor-new'])
1169 backend.pull_heads(source, heads=['change-rebased'])
1190 backend.pull_heads(source, heads=['change-rebased'])
1170 target_repo_name = target.repo_name
1191 target_repo_name = target.repo_name
1171
1192
1172 # update PR
1193 # update PR
1173 url = route_path('pullrequest_update',
1194 url = route_path('pullrequest_update',
1174 repo_name=target_repo_name,
1195 repo_name=target_repo_name,
1175 pull_request_id=pull_request_id)
1196 pull_request_id=pull_request_id)
1176 self.app.post(url,
1197 self.app.post(url,
1177 params={'update_commits': 'true', 'csrf_token': csrf_token},
1198 params={'update_commits': 'true', 'csrf_token': csrf_token},
1178 status=200)
1199 status=200)
1179
1200
1180 # check that we have now both revisions
1201 # check that we have now both revisions
1181 pull_request = PullRequest.get(pull_request_id)
1202 pull_request = PullRequest.get(pull_request_id)
1182 assert pull_request.revisions == [commit_ids['change-rebased']]
1203 assert pull_request.revisions == [commit_ids['change-rebased']]
1183 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
1204 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
1184 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
1205 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
1185
1206
1186 response = self.app.get(
1207 response = self.app.get(
1187 route_path('pullrequest_show',
1208 route_path('pullrequest_show',
1188 repo_name=target_repo_name,
1209 repo_name=target_repo_name,
1189 pull_request_id=pull_request.pull_request_id))
1210 pull_request_id=pull_request.pull_request_id))
1190 assert response.status_int == 200
1211 assert response.status_int == 200
1191 response.mustcontain('Pull request updated to')
1212 response.mustcontain('Pull request updated to')
1192 response.mustcontain('with 1 added, 1 removed commits.')
1213 response.mustcontain('with 1 added, 1 removed commits.')
1193
1214
1194 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
1215 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
1195 backend = backend_git
1216 backend = backend_git
1196 commits = [
1217 commits = [
1197 {'message': 'master-commit-1'},
1218 {'message': 'master-commit-1'},
1198 {'message': 'master-commit-2-change-1'},
1219 {'message': 'master-commit-2-change-1'},
1199 {'message': 'master-commit-3-change-2'},
1220 {'message': 'master-commit-3-change-2'},
1200
1221
1201 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
1222 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
1202 {'message': 'feat-commit-2'},
1223 {'message': 'feat-commit-2'},
1203 ]
1224 ]
1204 commit_ids = backend.create_master_repo(commits)
1225 commit_ids = backend.create_master_repo(commits)
1205 target = backend.create_repo(heads=['master-commit-3-change-2'])
1226 target = backend.create_repo(heads=['master-commit-3-change-2'])
1206 source = backend.create_repo(heads=['feat-commit-2'])
1227 source = backend.create_repo(heads=['feat-commit-2'])
1207
1228
1208 # create pr from a in source to A in target
1229 # create pr from a in source to A in target
1209 pull_request = PullRequest()
1230 pull_request = PullRequest()
1210 pull_request.source_repo = source
1231 pull_request.source_repo = source
1211
1232
1212 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1233 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1213 branch=backend.default_branch_name,
1234 branch=backend.default_branch_name,
1214 commit_id=commit_ids['master-commit-3-change-2'])
1235 commit_id=commit_ids['master-commit-3-change-2'])
1215
1236
1216 pull_request.target_repo = target
1237 pull_request.target_repo = target
1217 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1238 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1218 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
1239 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
1219
1240
1220 pull_request.revisions = [
1241 pull_request.revisions = [
1221 commit_ids['feat-commit-1'],
1242 commit_ids['feat-commit-1'],
1222 commit_ids['feat-commit-2']
1243 commit_ids['feat-commit-2']
1223 ]
1244 ]
1224 pull_request.title = u"Test"
1245 pull_request.title = u"Test"
1225 pull_request.description = u"Description"
1246 pull_request.description = u"Description"
1226 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1247 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1227 pull_request.pull_request_state = PullRequest.STATE_CREATED
1248 pull_request.pull_request_state = PullRequest.STATE_CREATED
1228 Session().add(pull_request)
1249 Session().add(pull_request)
1229 Session().commit()
1250 Session().commit()
1230 pull_request_id = pull_request.pull_request_id
1251 pull_request_id = pull_request.pull_request_id
1231
1252
1232 # PR is created, now we simulate a force-push into target,
1253 # PR is created, now we simulate a force-push into target,
1233 # that drops a 2 last commits
1254 # that drops a 2 last commits
1234 vcsrepo = target.scm_instance()
1255 vcsrepo = target.scm_instance()
1235 vcsrepo.config.clear_section('hooks')
1256 vcsrepo.config.clear_section('hooks')
1236 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1257 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1237 target_repo_name = target.repo_name
1258 target_repo_name = target.repo_name
1238
1259
1239 # update PR
1260 # update PR
1240 url = route_path('pullrequest_update',
1261 url = route_path('pullrequest_update',
1241 repo_name=target_repo_name,
1262 repo_name=target_repo_name,
1242 pull_request_id=pull_request_id)
1263 pull_request_id=pull_request_id)
1243 self.app.post(url,
1264 self.app.post(url,
1244 params={'update_commits': 'true', 'csrf_token': csrf_token},
1265 params={'update_commits': 'true', 'csrf_token': csrf_token},
1245 status=200)
1266 status=200)
1246
1267
1247 response = self.app.get(route_path('pullrequest_new', repo_name=target_repo_name))
1268 response = self.app.get(route_path('pullrequest_new', repo_name=target_repo_name))
1248 assert response.status_int == 200
1269 assert response.status_int == 200
1249 response.mustcontain('Pull request updated to')
1270 response.mustcontain('Pull request updated to')
1250 response.mustcontain('with 0 added, 0 removed commits.')
1271 response.mustcontain('with 0 added, 0 removed commits.')
1251
1272
1252 def test_update_of_ancestor_reference(self, backend, csrf_token):
1273 def test_update_of_ancestor_reference(self, backend, csrf_token):
1253 commits = [
1274 commits = [
1254 {'message': 'ancestor'},
1275 {'message': 'ancestor'},
1255 {'message': 'change'},
1276 {'message': 'change'},
1256 {'message': 'change-2'},
1277 {'message': 'change-2'},
1257 {'message': 'ancestor-new', 'parents': ['ancestor']},
1278 {'message': 'ancestor-new', 'parents': ['ancestor']},
1258 {'message': 'change-rebased'},
1279 {'message': 'change-rebased'},
1259 ]
1280 ]
1260 commit_ids = backend.create_master_repo(commits)
1281 commit_ids = backend.create_master_repo(commits)
1261 target = backend.create_repo(heads=['ancestor'])
1282 target = backend.create_repo(heads=['ancestor'])
1262 source = backend.create_repo(heads=['change'])
1283 source = backend.create_repo(heads=['change'])
1263
1284
1264 # create pr from a in source to A in target
1285 # create pr from a in source to A in target
1265 pull_request = PullRequest()
1286 pull_request = PullRequest()
1266 pull_request.source_repo = source
1287 pull_request.source_repo = source
1267
1288
1268 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1289 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1269 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1290 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1270 pull_request.target_repo = target
1291 pull_request.target_repo = target
1271 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1292 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1272 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1293 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1273 pull_request.revisions = [commit_ids['change']]
1294 pull_request.revisions = [commit_ids['change']]
1274 pull_request.title = u"Test"
1295 pull_request.title = u"Test"
1275 pull_request.description = u"Description"
1296 pull_request.description = u"Description"
1276 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1297 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1277 pull_request.pull_request_state = PullRequest.STATE_CREATED
1298 pull_request.pull_request_state = PullRequest.STATE_CREATED
1278 Session().add(pull_request)
1299 Session().add(pull_request)
1279 Session().commit()
1300 Session().commit()
1280 pull_request_id = pull_request.pull_request_id
1301 pull_request_id = pull_request.pull_request_id
1281
1302
1282 # target has ancestor - ancestor-new
1303 # target has ancestor - ancestor-new
1283 # source has ancestor - ancestor-new - change-rebased
1304 # source has ancestor - ancestor-new - change-rebased
1284 backend.pull_heads(target, heads=['ancestor-new'])
1305 backend.pull_heads(target, heads=['ancestor-new'])
1285 backend.pull_heads(source, heads=['change-rebased'])
1306 backend.pull_heads(source, heads=['change-rebased'])
1286 target_repo_name = target.repo_name
1307 target_repo_name = target.repo_name
1287
1308
1288 # update PR
1309 # update PR
1289 self.app.post(
1310 self.app.post(
1290 route_path('pullrequest_update',
1311 route_path('pullrequest_update',
1291 repo_name=target_repo_name, pull_request_id=pull_request_id),
1312 repo_name=target_repo_name, pull_request_id=pull_request_id),
1292 params={'update_commits': 'true', 'csrf_token': csrf_token},
1313 params={'update_commits': 'true', 'csrf_token': csrf_token},
1293 status=200)
1314 status=200)
1294
1315
1295 # Expect the target reference to be updated correctly
1316 # Expect the target reference to be updated correctly
1296 pull_request = PullRequest.get(pull_request_id)
1317 pull_request = PullRequest.get(pull_request_id)
1297 assert pull_request.revisions == [commit_ids['change-rebased']]
1318 assert pull_request.revisions == [commit_ids['change-rebased']]
1298 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1319 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1299 branch=backend.default_branch_name,
1320 branch=backend.default_branch_name,
1300 commit_id=commit_ids['ancestor-new'])
1321 commit_id=commit_ids['ancestor-new'])
1301 assert pull_request.target_ref == expected_target_ref
1322 assert pull_request.target_ref == expected_target_ref
1302
1323
1303 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1324 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1304 branch_name = 'development'
1325 branch_name = 'development'
1305 commits = [
1326 commits = [
1306 {'message': 'initial-commit'},
1327 {'message': 'initial-commit'},
1307 {'message': 'old-feature'},
1328 {'message': 'old-feature'},
1308 {'message': 'new-feature', 'branch': branch_name},
1329 {'message': 'new-feature', 'branch': branch_name},
1309 ]
1330 ]
1310 repo = backend_git.create_repo(commits)
1331 repo = backend_git.create_repo(commits)
1311 repo_name = repo.repo_name
1332 repo_name = repo.repo_name
1312 commit_ids = backend_git.commit_ids
1333 commit_ids = backend_git.commit_ids
1313
1334
1314 pull_request = PullRequest()
1335 pull_request = PullRequest()
1315 pull_request.source_repo = repo
1336 pull_request.source_repo = repo
1316 pull_request.target_repo = repo
1337 pull_request.target_repo = repo
1317 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1338 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1318 branch=branch_name, commit_id=commit_ids['new-feature'])
1339 branch=branch_name, commit_id=commit_ids['new-feature'])
1319 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1340 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1320 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1341 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1321 pull_request.revisions = [commit_ids['new-feature']]
1342 pull_request.revisions = [commit_ids['new-feature']]
1322 pull_request.title = u"Test"
1343 pull_request.title = u"Test"
1323 pull_request.description = u"Description"
1344 pull_request.description = u"Description"
1324 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1345 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1325 pull_request.pull_request_state = PullRequest.STATE_CREATED
1346 pull_request.pull_request_state = PullRequest.STATE_CREATED
1326 Session().add(pull_request)
1347 Session().add(pull_request)
1327 Session().commit()
1348 Session().commit()
1328
1349
1329 pull_request_id = pull_request.pull_request_id
1350 pull_request_id = pull_request.pull_request_id
1330
1351
1331 vcs = repo.scm_instance()
1352 vcs = repo.scm_instance()
1332 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1353 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1333 # NOTE(marcink): run GC to ensure the commits are gone
1354 # NOTE(marcink): run GC to ensure the commits are gone
1334 vcs.run_gc()
1355 vcs.run_gc()
1335
1356
1336 response = self.app.get(route_path(
1357 response = self.app.get(route_path(
1337 'pullrequest_show',
1358 'pullrequest_show',
1338 repo_name=repo_name,
1359 repo_name=repo_name,
1339 pull_request_id=pull_request_id))
1360 pull_request_id=pull_request_id))
1340
1361
1341 assert response.status_int == 200
1362 assert response.status_int == 200
1342
1363
1343 response.assert_response().element_contains(
1364 response.assert_response().element_contains(
1344 '#changeset_compare_view_content .alert strong',
1365 '#changeset_compare_view_content .alert strong',
1345 'Missing commits')
1366 'Missing commits')
1346 response.assert_response().element_contains(
1367 response.assert_response().element_contains(
1347 '#changeset_compare_view_content .alert',
1368 '#changeset_compare_view_content .alert',
1348 'This pull request cannot be displayed, because one or more'
1369 'This pull request cannot be displayed, because one or more'
1349 ' commits no longer exist in the source repository.')
1370 ' commits no longer exist in the source repository.')
1350
1371
1351 def test_strip_commits_from_pull_request(
1372 def test_strip_commits_from_pull_request(
1352 self, backend, pr_util, csrf_token):
1373 self, backend, pr_util, csrf_token):
1353 commits = [
1374 commits = [
1354 {'message': 'initial-commit'},
1375 {'message': 'initial-commit'},
1355 {'message': 'old-feature'},
1376 {'message': 'old-feature'},
1356 {'message': 'new-feature', 'parents': ['initial-commit']},
1377 {'message': 'new-feature', 'parents': ['initial-commit']},
1357 ]
1378 ]
1358 pull_request = pr_util.create_pull_request(
1379 pull_request = pr_util.create_pull_request(
1359 commits, target_head='initial-commit', source_head='new-feature',
1380 commits, target_head='initial-commit', source_head='new-feature',
1360 revisions=['new-feature'])
1381 revisions=['new-feature'])
1361
1382
1362 vcs = pr_util.source_repository.scm_instance()
1383 vcs = pr_util.source_repository.scm_instance()
1363 if backend.alias == 'git':
1384 if backend.alias == 'git':
1364 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1385 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1365 else:
1386 else:
1366 vcs.strip(pr_util.commit_ids['new-feature'])
1387 vcs.strip(pr_util.commit_ids['new-feature'])
1367
1388
1368 response = self.app.get(route_path(
1389 response = self.app.get(route_path(
1369 'pullrequest_show',
1390 'pullrequest_show',
1370 repo_name=pr_util.target_repository.repo_name,
1391 repo_name=pr_util.target_repository.repo_name,
1371 pull_request_id=pull_request.pull_request_id))
1392 pull_request_id=pull_request.pull_request_id))
1372
1393
1373 assert response.status_int == 200
1394 assert response.status_int == 200
1374
1395
1375 response.assert_response().element_contains(
1396 response.assert_response().element_contains(
1376 '#changeset_compare_view_content .alert strong',
1397 '#changeset_compare_view_content .alert strong',
1377 'Missing commits')
1398 'Missing commits')
1378 response.assert_response().element_contains(
1399 response.assert_response().element_contains(
1379 '#changeset_compare_view_content .alert',
1400 '#changeset_compare_view_content .alert',
1380 'This pull request cannot be displayed, because one or more'
1401 'This pull request cannot be displayed, because one or more'
1381 ' commits no longer exist in the source repository.')
1402 ' commits no longer exist in the source repository.')
1382 response.assert_response().element_contains(
1403 response.assert_response().element_contains(
1383 '#update_commits',
1404 '#update_commits',
1384 'Update commits')
1405 'Update commits')
1385
1406
1386 def test_strip_commits_and_update(
1407 def test_strip_commits_and_update(
1387 self, backend, pr_util, csrf_token):
1408 self, backend, pr_util, csrf_token):
1388 commits = [
1409 commits = [
1389 {'message': 'initial-commit'},
1410 {'message': 'initial-commit'},
1390 {'message': 'old-feature'},
1411 {'message': 'old-feature'},
1391 {'message': 'new-feature', 'parents': ['old-feature']},
1412 {'message': 'new-feature', 'parents': ['old-feature']},
1392 ]
1413 ]
1393 pull_request = pr_util.create_pull_request(
1414 pull_request = pr_util.create_pull_request(
1394 commits, target_head='old-feature', source_head='new-feature',
1415 commits, target_head='old-feature', source_head='new-feature',
1395 revisions=['new-feature'], mergeable=True)
1416 revisions=['new-feature'], mergeable=True)
1396 pr_id = pull_request.pull_request_id
1417 pr_id = pull_request.pull_request_id
1397 target_repo_name = pull_request.target_repo.repo_name
1418 target_repo_name = pull_request.target_repo.repo_name
1398
1419
1399 vcs = pr_util.source_repository.scm_instance()
1420 vcs = pr_util.source_repository.scm_instance()
1400 if backend.alias == 'git':
1421 if backend.alias == 'git':
1401 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1422 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1402 else:
1423 else:
1403 vcs.strip(pr_util.commit_ids['new-feature'])
1424 vcs.strip(pr_util.commit_ids['new-feature'])
1404
1425
1405 url = route_path('pullrequest_update',
1426 url = route_path('pullrequest_update',
1406 repo_name=target_repo_name,
1427 repo_name=target_repo_name,
1407 pull_request_id=pr_id)
1428 pull_request_id=pr_id)
1408 response = self.app.post(url,
1429 response = self.app.post(url,
1409 params={'update_commits': 'true',
1430 params={'update_commits': 'true',
1410 'csrf_token': csrf_token})
1431 'csrf_token': csrf_token})
1411
1432
1412 assert response.status_int == 200
1433 assert response.status_int == 200
1413 assert response.body == '{"response": true, "redirect_url": null}'
1434 assert response.body == '{"response": true, "redirect_url": null}'
1414
1435
1415 # Make sure that after update, it won't raise 500 errors
1436 # Make sure that after update, it won't raise 500 errors
1416 response = self.app.get(route_path(
1437 response = self.app.get(route_path(
1417 'pullrequest_show',
1438 'pullrequest_show',
1418 repo_name=target_repo_name,
1439 repo_name=target_repo_name,
1419 pull_request_id=pr_id))
1440 pull_request_id=pr_id))
1420
1441
1421 assert response.status_int == 200
1442 assert response.status_int == 200
1422 response.assert_response().element_contains(
1443 response.assert_response().element_contains(
1423 '#changeset_compare_view_content .alert strong',
1444 '#changeset_compare_view_content .alert strong',
1424 'Missing commits')
1445 'Missing commits')
1425
1446
1426 def test_branch_is_a_link(self, pr_util):
1447 def test_branch_is_a_link(self, pr_util):
1427 pull_request = pr_util.create_pull_request()
1448 pull_request = pr_util.create_pull_request()
1428 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1449 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1429 pull_request.target_ref = 'branch:target:abcdef1234567890'
1450 pull_request.target_ref = 'branch:target:abcdef1234567890'
1430 Session().add(pull_request)
1451 Session().add(pull_request)
1431 Session().commit()
1452 Session().commit()
1432
1453
1433 response = self.app.get(route_path(
1454 response = self.app.get(route_path(
1434 'pullrequest_show',
1455 'pullrequest_show',
1435 repo_name=pull_request.target_repo.scm_instance().name,
1456 repo_name=pull_request.target_repo.scm_instance().name,
1436 pull_request_id=pull_request.pull_request_id))
1457 pull_request_id=pull_request.pull_request_id))
1437 assert response.status_int == 200
1458 assert response.status_int == 200
1438
1459
1439 source = response.assert_response().get_element('.pr-source-info')
1460 source = response.assert_response().get_element('.pr-source-info')
1440 source_parent = source.getparent()
1461 source_parent = source.getparent()
1441 assert len(source_parent) == 1
1462 assert len(source_parent) == 1
1442
1463
1443 target = response.assert_response().get_element('.pr-target-info')
1464 target = response.assert_response().get_element('.pr-target-info')
1444 target_parent = target.getparent()
1465 target_parent = target.getparent()
1445 assert len(target_parent) == 1
1466 assert len(target_parent) == 1
1446
1467
1447 expected_origin_link = route_path(
1468 expected_origin_link = route_path(
1448 'repo_commits',
1469 'repo_commits',
1449 repo_name=pull_request.source_repo.scm_instance().name,
1470 repo_name=pull_request.source_repo.scm_instance().name,
1450 params=dict(branch='origin'))
1471 params=dict(branch='origin'))
1451 expected_target_link = route_path(
1472 expected_target_link = route_path(
1452 'repo_commits',
1473 'repo_commits',
1453 repo_name=pull_request.target_repo.scm_instance().name,
1474 repo_name=pull_request.target_repo.scm_instance().name,
1454 params=dict(branch='target'))
1475 params=dict(branch='target'))
1455 assert source_parent.attrib['href'] == expected_origin_link
1476 assert source_parent.attrib['href'] == expected_origin_link
1456 assert target_parent.attrib['href'] == expected_target_link
1477 assert target_parent.attrib['href'] == expected_target_link
1457
1478
1458 def test_bookmark_is_not_a_link(self, pr_util):
1479 def test_bookmark_is_not_a_link(self, pr_util):
1459 pull_request = pr_util.create_pull_request()
1480 pull_request = pr_util.create_pull_request()
1460 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1481 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1461 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1482 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1462 Session().add(pull_request)
1483 Session().add(pull_request)
1463 Session().commit()
1484 Session().commit()
1464
1485
1465 response = self.app.get(route_path(
1486 response = self.app.get(route_path(
1466 'pullrequest_show',
1487 'pullrequest_show',
1467 repo_name=pull_request.target_repo.scm_instance().name,
1488 repo_name=pull_request.target_repo.scm_instance().name,
1468 pull_request_id=pull_request.pull_request_id))
1489 pull_request_id=pull_request.pull_request_id))
1469 assert response.status_int == 200
1490 assert response.status_int == 200
1470
1491
1471 source = response.assert_response().get_element('.pr-source-info')
1492 source = response.assert_response().get_element('.pr-source-info')
1472 assert source.text.strip() == 'bookmark:origin'
1493 assert source.text.strip() == 'bookmark:origin'
1473 assert source.getparent().attrib.get('href') is None
1494 assert source.getparent().attrib.get('href') is None
1474
1495
1475 target = response.assert_response().get_element('.pr-target-info')
1496 target = response.assert_response().get_element('.pr-target-info')
1476 assert target.text.strip() == 'bookmark:target'
1497 assert target.text.strip() == 'bookmark:target'
1477 assert target.getparent().attrib.get('href') is None
1498 assert target.getparent().attrib.get('href') is None
1478
1499
1479 def test_tag_is_not_a_link(self, pr_util):
1500 def test_tag_is_not_a_link(self, pr_util):
1480 pull_request = pr_util.create_pull_request()
1501 pull_request = pr_util.create_pull_request()
1481 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1502 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1482 pull_request.target_ref = 'tag:target:abcdef1234567890'
1503 pull_request.target_ref = 'tag:target:abcdef1234567890'
1483 Session().add(pull_request)
1504 Session().add(pull_request)
1484 Session().commit()
1505 Session().commit()
1485
1506
1486 response = self.app.get(route_path(
1507 response = self.app.get(route_path(
1487 'pullrequest_show',
1508 'pullrequest_show',
1488 repo_name=pull_request.target_repo.scm_instance().name,
1509 repo_name=pull_request.target_repo.scm_instance().name,
1489 pull_request_id=pull_request.pull_request_id))
1510 pull_request_id=pull_request.pull_request_id))
1490 assert response.status_int == 200
1511 assert response.status_int == 200
1491
1512
1492 source = response.assert_response().get_element('.pr-source-info')
1513 source = response.assert_response().get_element('.pr-source-info')
1493 assert source.text.strip() == 'tag:origin'
1514 assert source.text.strip() == 'tag:origin'
1494 assert source.getparent().attrib.get('href') is None
1515 assert source.getparent().attrib.get('href') is None
1495
1516
1496 target = response.assert_response().get_element('.pr-target-info')
1517 target = response.assert_response().get_element('.pr-target-info')
1497 assert target.text.strip() == 'tag:target'
1518 assert target.text.strip() == 'tag:target'
1498 assert target.getparent().attrib.get('href') is None
1519 assert target.getparent().attrib.get('href') is None
1499
1520
1500 @pytest.mark.parametrize('mergeable', [True, False])
1521 @pytest.mark.parametrize('mergeable', [True, False])
1501 def test_shadow_repository_link(
1522 def test_shadow_repository_link(
1502 self, mergeable, pr_util, http_host_only_stub):
1523 self, mergeable, pr_util, http_host_only_stub):
1503 """
1524 """
1504 Check that the pull request summary page displays a link to the shadow
1525 Check that the pull request summary page displays a link to the shadow
1505 repository if the pull request is mergeable. If it is not mergeable
1526 repository if the pull request is mergeable. If it is not mergeable
1506 the link should not be displayed.
1527 the link should not be displayed.
1507 """
1528 """
1508 pull_request = pr_util.create_pull_request(
1529 pull_request = pr_util.create_pull_request(
1509 mergeable=mergeable, enable_notifications=False)
1530 mergeable=mergeable, enable_notifications=False)
1510 target_repo = pull_request.target_repo.scm_instance()
1531 target_repo = pull_request.target_repo.scm_instance()
1511 pr_id = pull_request.pull_request_id
1532 pr_id = pull_request.pull_request_id
1512 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1533 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1513 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1534 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1514
1535
1515 response = self.app.get(route_path(
1536 response = self.app.get(route_path(
1516 'pullrequest_show',
1537 'pullrequest_show',
1517 repo_name=target_repo.name,
1538 repo_name=target_repo.name,
1518 pull_request_id=pr_id))
1539 pull_request_id=pr_id))
1519
1540
1520 if mergeable:
1541 if mergeable:
1521 response.assert_response().element_value_contains(
1542 response.assert_response().element_value_contains(
1522 'input.pr-mergeinfo', shadow_url)
1543 'input.pr-mergeinfo', shadow_url)
1523 response.assert_response().element_value_contains(
1544 response.assert_response().element_value_contains(
1524 'input.pr-mergeinfo ', 'pr-merge')
1545 'input.pr-mergeinfo ', 'pr-merge')
1525 else:
1546 else:
1526 response.assert_response().no_element_exists('.pr-mergeinfo')
1547 response.assert_response().no_element_exists('.pr-mergeinfo')
1527
1548
1528
1549
1529 @pytest.mark.usefixtures('app')
1550 @pytest.mark.usefixtures('app')
1530 @pytest.mark.backends("git", "hg")
1551 @pytest.mark.backends("git", "hg")
1531 class TestPullrequestsControllerDelete(object):
1552 class TestPullrequestsControllerDelete(object):
1532 def test_pull_request_delete_button_permissions_admin(
1553 def test_pull_request_delete_button_permissions_admin(
1533 self, autologin_user, user_admin, pr_util):
1554 self, autologin_user, user_admin, pr_util):
1534 pull_request = pr_util.create_pull_request(
1555 pull_request = pr_util.create_pull_request(
1535 author=user_admin.username, enable_notifications=False)
1556 author=user_admin.username, enable_notifications=False)
1536
1557
1537 response = self.app.get(route_path(
1558 response = self.app.get(route_path(
1538 'pullrequest_show',
1559 'pullrequest_show',
1539 repo_name=pull_request.target_repo.scm_instance().name,
1560 repo_name=pull_request.target_repo.scm_instance().name,
1540 pull_request_id=pull_request.pull_request_id))
1561 pull_request_id=pull_request.pull_request_id))
1541
1562
1542 response.mustcontain('id="delete_pullrequest"')
1563 response.mustcontain('id="delete_pullrequest"')
1543 response.mustcontain('Confirm to delete this pull request')
1564 response.mustcontain('Confirm to delete this pull request')
1544
1565
1545 def test_pull_request_delete_button_permissions_owner(
1566 def test_pull_request_delete_button_permissions_owner(
1546 self, autologin_regular_user, user_regular, pr_util):
1567 self, autologin_regular_user, user_regular, pr_util):
1547 pull_request = pr_util.create_pull_request(
1568 pull_request = pr_util.create_pull_request(
1548 author=user_regular.username, enable_notifications=False)
1569 author=user_regular.username, enable_notifications=False)
1549
1570
1550 response = self.app.get(route_path(
1571 response = self.app.get(route_path(
1551 'pullrequest_show',
1572 'pullrequest_show',
1552 repo_name=pull_request.target_repo.scm_instance().name,
1573 repo_name=pull_request.target_repo.scm_instance().name,
1553 pull_request_id=pull_request.pull_request_id))
1574 pull_request_id=pull_request.pull_request_id))
1554
1575
1555 response.mustcontain('id="delete_pullrequest"')
1576 response.mustcontain('id="delete_pullrequest"')
1556 response.mustcontain('Confirm to delete this pull request')
1577 response.mustcontain('Confirm to delete this pull request')
1557
1578
1558 def test_pull_request_delete_button_permissions_forbidden(
1579 def test_pull_request_delete_button_permissions_forbidden(
1559 self, autologin_regular_user, user_regular, user_admin, pr_util):
1580 self, autologin_regular_user, user_regular, user_admin, pr_util):
1560 pull_request = pr_util.create_pull_request(
1581 pull_request = pr_util.create_pull_request(
1561 author=user_admin.username, enable_notifications=False)
1582 author=user_admin.username, enable_notifications=False)
1562
1583
1563 response = self.app.get(route_path(
1584 response = self.app.get(route_path(
1564 'pullrequest_show',
1585 'pullrequest_show',
1565 repo_name=pull_request.target_repo.scm_instance().name,
1586 repo_name=pull_request.target_repo.scm_instance().name,
1566 pull_request_id=pull_request.pull_request_id))
1587 pull_request_id=pull_request.pull_request_id))
1567 response.mustcontain(no=['id="delete_pullrequest"'])
1588 response.mustcontain(no=['id="delete_pullrequest"'])
1568 response.mustcontain(no=['Confirm to delete this pull request'])
1589 response.mustcontain(no=['Confirm to delete this pull request'])
1569
1590
1570 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1591 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1571 self, autologin_regular_user, user_regular, user_admin, pr_util,
1592 self, autologin_regular_user, user_regular, user_admin, pr_util,
1572 user_util):
1593 user_util):
1573
1594
1574 pull_request = pr_util.create_pull_request(
1595 pull_request = pr_util.create_pull_request(
1575 author=user_admin.username, enable_notifications=False)
1596 author=user_admin.username, enable_notifications=False)
1576
1597
1577 user_util.grant_user_permission_to_repo(
1598 user_util.grant_user_permission_to_repo(
1578 pull_request.target_repo, user_regular,
1599 pull_request.target_repo, user_regular,
1579 'repository.write')
1600 'repository.write')
1580
1601
1581 response = self.app.get(route_path(
1602 response = self.app.get(route_path(
1582 'pullrequest_show',
1603 'pullrequest_show',
1583 repo_name=pull_request.target_repo.scm_instance().name,
1604 repo_name=pull_request.target_repo.scm_instance().name,
1584 pull_request_id=pull_request.pull_request_id))
1605 pull_request_id=pull_request.pull_request_id))
1585
1606
1586 response.mustcontain('id="open_edit_pullrequest"')
1607 response.mustcontain('id="open_edit_pullrequest"')
1587 response.mustcontain('id="delete_pullrequest"')
1608 response.mustcontain('id="delete_pullrequest"')
1588 response.mustcontain(no=['Confirm to delete this pull request'])
1609 response.mustcontain(no=['Confirm to delete this pull request'])
1589
1610
1590 def test_delete_comment_returns_404_if_comment_does_not_exist(
1611 def test_delete_comment_returns_404_if_comment_does_not_exist(
1591 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1612 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1592
1613
1593 pull_request = pr_util.create_pull_request(
1614 pull_request = pr_util.create_pull_request(
1594 author=user_admin.username, enable_notifications=False)
1615 author=user_admin.username, enable_notifications=False)
1595
1616
1596 self.app.post(
1617 self.app.post(
1597 route_path(
1618 route_path(
1598 'pullrequest_comment_delete',
1619 'pullrequest_comment_delete',
1599 repo_name=pull_request.target_repo.scm_instance().name,
1620 repo_name=pull_request.target_repo.scm_instance().name,
1600 pull_request_id=pull_request.pull_request_id,
1621 pull_request_id=pull_request.pull_request_id,
1601 comment_id=1024404),
1622 comment_id=1024404),
1602 extra_environ=xhr_header,
1623 extra_environ=xhr_header,
1603 params={'csrf_token': csrf_token},
1624 params={'csrf_token': csrf_token},
1604 status=404
1625 status=404
1605 )
1626 )
1606
1627
1607 def test_delete_comment(
1628 def test_delete_comment(
1608 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1629 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1609
1630
1610 pull_request = pr_util.create_pull_request(
1631 pull_request = pr_util.create_pull_request(
1611 author=user_admin.username, enable_notifications=False)
1632 author=user_admin.username, enable_notifications=False)
1612 comment = pr_util.create_comment()
1633 comment = pr_util.create_comment()
1613 comment_id = comment.comment_id
1634 comment_id = comment.comment_id
1614
1635
1615 response = self.app.post(
1636 response = self.app.post(
1616 route_path(
1637 route_path(
1617 'pullrequest_comment_delete',
1638 'pullrequest_comment_delete',
1618 repo_name=pull_request.target_repo.scm_instance().name,
1639 repo_name=pull_request.target_repo.scm_instance().name,
1619 pull_request_id=pull_request.pull_request_id,
1640 pull_request_id=pull_request.pull_request_id,
1620 comment_id=comment_id),
1641 comment_id=comment_id),
1621 extra_environ=xhr_header,
1642 extra_environ=xhr_header,
1622 params={'csrf_token': csrf_token},
1643 params={'csrf_token': csrf_token},
1623 status=200
1644 status=200
1624 )
1645 )
1625 assert response.body == 'true'
1646 assert response.body == 'true'
1626
1647
1627 @pytest.mark.parametrize('url_type', [
1648 @pytest.mark.parametrize('url_type', [
1628 'pullrequest_new',
1649 'pullrequest_new',
1629 'pullrequest_create',
1650 'pullrequest_create',
1630 'pullrequest_update',
1651 'pullrequest_update',
1631 'pullrequest_merge',
1652 'pullrequest_merge',
1632 ])
1653 ])
1633 def test_pull_request_is_forbidden_on_archived_repo(
1654 def test_pull_request_is_forbidden_on_archived_repo(
1634 self, autologin_user, backend, xhr_header, user_util, url_type):
1655 self, autologin_user, backend, xhr_header, user_util, url_type):
1635
1656
1636 # create a temporary repo
1657 # create a temporary repo
1637 source = user_util.create_repo(repo_type=backend.alias)
1658 source = user_util.create_repo(repo_type=backend.alias)
1638 repo_name = source.repo_name
1659 repo_name = source.repo_name
1639 repo = Repository.get_by_repo_name(repo_name)
1660 repo = Repository.get_by_repo_name(repo_name)
1640 repo.archived = True
1661 repo.archived = True
1641 Session().commit()
1662 Session().commit()
1642
1663
1643 response = self.app.get(
1664 response = self.app.get(
1644 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1665 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1645
1666
1646 msg = 'Action not supported for archived repository.'
1667 msg = 'Action not supported for archived repository.'
1647 assert_session_flash(response, msg)
1668 assert_session_flash(response, msg)
1648
1669
1649
1670
1650 def assert_pull_request_status(pull_request, expected_status):
1671 def assert_pull_request_status(pull_request, expected_status):
1651 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1672 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1652 assert status == expected_status
1673 assert status == expected_status
1653
1674
1654
1675
1655 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1676 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1656 @pytest.mark.usefixtures("autologin_user")
1677 @pytest.mark.usefixtures("autologin_user")
1657 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1678 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1658 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
1679 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
@@ -1,5830 +1,5836 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 time
27 import time
28 import string
28 import string
29 import hashlib
29 import hashlib
30 import logging
30 import logging
31 import datetime
31 import datetime
32 import uuid
32 import uuid
33 import warnings
33 import warnings
34 import ipaddress
34 import ipaddress
35 import functools
35 import functools
36 import traceback
36 import traceback
37 import collections
37 import collections
38
38
39 from sqlalchemy import (
39 from sqlalchemy import (
40 or_, and_, not_, func, cast, TypeDecorator, event,
40 or_, and_, not_, func, cast, TypeDecorator, event,
41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
43 Text, Float, PickleType, BigInteger)
43 Text, Float, PickleType, BigInteger)
44 from sqlalchemy.sql.expression import true, false, case
44 from sqlalchemy.sql.expression import true, false, case
45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
46 from sqlalchemy.orm import (
46 from sqlalchemy.orm import (
47 relationship, joinedload, class_mapper, validates, aliased)
47 relationship, joinedload, class_mapper, validates, aliased)
48 from sqlalchemy.ext.declarative import declared_attr
48 from sqlalchemy.ext.declarative import declared_attr
49 from sqlalchemy.ext.hybrid import hybrid_property
49 from sqlalchemy.ext.hybrid import hybrid_property
50 from sqlalchemy.exc import IntegrityError # pragma: no cover
50 from sqlalchemy.exc import IntegrityError # pragma: no cover
51 from sqlalchemy.dialects.mysql import LONGTEXT
51 from sqlalchemy.dialects.mysql import LONGTEXT
52 from zope.cachedescriptors.property import Lazy as LazyProperty
52 from zope.cachedescriptors.property import Lazy as LazyProperty
53 from pyramid import compat
53 from pyramid import compat
54 from pyramid.threadlocal import get_current_request
54 from pyramid.threadlocal import get_current_request
55 from webhelpers2.text import remove_formatting
55 from webhelpers2.text import remove_formatting
56
56
57 from rhodecode.translation import _
57 from rhodecode.translation import _
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 from rhodecode.lib.vcs.backends.base import (
59 from rhodecode.lib.vcs.backends.base import (
60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
61 from rhodecode.lib.utils2 import (
61 from rhodecode.lib.utils2 import (
62 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
62 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
64 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
64 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
65 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
65 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
66 JsonRaw
66 JsonRaw
67 from rhodecode.lib.ext_json import json
67 from rhodecode.lib.ext_json import json
68 from rhodecode.lib.caching_query import FromCache
68 from rhodecode.lib.caching_query import FromCache
69 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
69 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
70 from rhodecode.lib.encrypt2 import Encryptor
70 from rhodecode.lib.encrypt2 import Encryptor
71 from rhodecode.lib.exceptions import (
71 from rhodecode.lib.exceptions import (
72 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
73 from rhodecode.model.meta import Base, Session
73 from rhodecode.model.meta import Base, Session
74
74
75 URL_SEP = '/'
75 URL_SEP = '/'
76 log = logging.getLogger(__name__)
76 log = logging.getLogger(__name__)
77
77
78 # =============================================================================
78 # =============================================================================
79 # BASE CLASSES
79 # BASE CLASSES
80 # =============================================================================
80 # =============================================================================
81
81
82 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 # this is propagated from .ini file rhodecode.encrypted_values.secret or
83 # beaker.session.secret if first is not set.
83 # beaker.session.secret if first is not set.
84 # and initialized at environment.py
84 # and initialized at environment.py
85 ENCRYPTION_KEY = None
85 ENCRYPTION_KEY = None
86
86
87 # used to sort permissions by types, '#' used here is not allowed to be in
87 # used to sort permissions by types, '#' used here is not allowed to be in
88 # usernames, and it's very early in sorted string.printable table.
88 # usernames, and it's very early in sorted string.printable table.
89 PERMISSION_TYPE_SORT = {
89 PERMISSION_TYPE_SORT = {
90 'admin': '####',
90 'admin': '####',
91 'write': '###',
91 'write': '###',
92 'read': '##',
92 'read': '##',
93 'none': '#',
93 'none': '#',
94 }
94 }
95
95
96
96
97 def display_user_sort(obj):
97 def display_user_sort(obj):
98 """
98 """
99 Sort function used to sort permissions in .permissions() function of
99 Sort function used to sort permissions in .permissions() function of
100 Repository, RepoGroup, UserGroup. Also it put the default user in front
100 Repository, RepoGroup, UserGroup. Also it put the default user in front
101 of all other resources
101 of all other resources
102 """
102 """
103
103
104 if obj.username == User.DEFAULT_USER:
104 if obj.username == User.DEFAULT_USER:
105 return '#####'
105 return '#####'
106 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
107 extra_sort_num = '1' # default
107 extra_sort_num = '1' # default
108
108
109 # NOTE(dan): inactive duplicates goes last
109 # NOTE(dan): inactive duplicates goes last
110 if getattr(obj, 'duplicate_perm', None):
110 if getattr(obj, 'duplicate_perm', None):
111 extra_sort_num = '9'
111 extra_sort_num = '9'
112 return prefix + extra_sort_num + obj.username
112 return prefix + extra_sort_num + obj.username
113
113
114
114
115 def display_user_group_sort(obj):
115 def display_user_group_sort(obj):
116 """
116 """
117 Sort function used to sort permissions in .permissions() function of
117 Sort function used to sort permissions in .permissions() function of
118 Repository, RepoGroup, UserGroup. Also it put the default user in front
118 Repository, RepoGroup, UserGroup. Also it put the default user in front
119 of all other resources
119 of all other resources
120 """
120 """
121
121
122 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
122 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
123 return prefix + obj.users_group_name
123 return prefix + obj.users_group_name
124
124
125
125
126 def _hash_key(k):
126 def _hash_key(k):
127 return sha1_safe(k)
127 return sha1_safe(k)
128
128
129
129
130 def in_filter_generator(qry, items, limit=500):
130 def in_filter_generator(qry, items, limit=500):
131 """
131 """
132 Splits IN() into multiple with OR
132 Splits IN() into multiple with OR
133 e.g.::
133 e.g.::
134 cnt = Repository.query().filter(
134 cnt = Repository.query().filter(
135 or_(
135 or_(
136 *in_filter_generator(Repository.repo_id, range(100000))
136 *in_filter_generator(Repository.repo_id, range(100000))
137 )).count()
137 )).count()
138 """
138 """
139 if not items:
139 if not items:
140 # empty list will cause empty query which might cause security issues
140 # empty list will cause empty query which might cause security issues
141 # this can lead to hidden unpleasant results
141 # this can lead to hidden unpleasant results
142 items = [-1]
142 items = [-1]
143
143
144 parts = []
144 parts = []
145 for chunk in xrange(0, len(items), limit):
145 for chunk in xrange(0, len(items), limit):
146 parts.append(
146 parts.append(
147 qry.in_(items[chunk: chunk + limit])
147 qry.in_(items[chunk: chunk + limit])
148 )
148 )
149
149
150 return parts
150 return parts
151
151
152
152
153 base_table_args = {
153 base_table_args = {
154 'extend_existing': True,
154 'extend_existing': True,
155 'mysql_engine': 'InnoDB',
155 'mysql_engine': 'InnoDB',
156 'mysql_charset': 'utf8',
156 'mysql_charset': 'utf8',
157 'sqlite_autoincrement': True
157 'sqlite_autoincrement': True
158 }
158 }
159
159
160
160
161 class EncryptedTextValue(TypeDecorator):
161 class EncryptedTextValue(TypeDecorator):
162 """
162 """
163 Special column for encrypted long text data, use like::
163 Special column for encrypted long text data, use like::
164
164
165 value = Column("encrypted_value", EncryptedValue(), nullable=False)
165 value = Column("encrypted_value", EncryptedValue(), nullable=False)
166
166
167 This column is intelligent so if value is in unencrypted form it return
167 This column is intelligent so if value is in unencrypted form it return
168 unencrypted form, but on save it always encrypts
168 unencrypted form, but on save it always encrypts
169 """
169 """
170 impl = Text
170 impl = Text
171
171
172 def process_bind_param(self, value, dialect):
172 def process_bind_param(self, value, dialect):
173 """
173 """
174 Setter for storing value
174 Setter for storing value
175 """
175 """
176 import rhodecode
176 import rhodecode
177 if not value:
177 if not value:
178 return value
178 return value
179
179
180 # protect against double encrypting if values is already encrypted
180 # protect against double encrypting if values is already encrypted
181 if value.startswith('enc$aes$') \
181 if value.startswith('enc$aes$') \
182 or value.startswith('enc$aes_hmac$') \
182 or value.startswith('enc$aes_hmac$') \
183 or value.startswith('enc2$'):
183 or value.startswith('enc2$'):
184 raise ValueError('value needs to be in unencrypted format, '
184 raise ValueError('value needs to be in unencrypted format, '
185 'ie. not starting with enc$ or enc2$')
185 'ie. not starting with enc$ or enc2$')
186
186
187 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
187 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
188 if algo == 'aes':
188 if algo == 'aes':
189 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
189 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
190 elif algo == 'fernet':
190 elif algo == 'fernet':
191 return Encryptor(ENCRYPTION_KEY).encrypt(value)
191 return Encryptor(ENCRYPTION_KEY).encrypt(value)
192 else:
192 else:
193 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
193 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
194
194
195 def process_result_value(self, value, dialect):
195 def process_result_value(self, value, dialect):
196 """
196 """
197 Getter for retrieving value
197 Getter for retrieving value
198 """
198 """
199
199
200 import rhodecode
200 import rhodecode
201 if not value:
201 if not value:
202 return value
202 return value
203
203
204 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
204 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
205 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
205 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
206 if algo == 'aes':
206 if algo == 'aes':
207 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
207 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
208 elif algo == 'fernet':
208 elif algo == 'fernet':
209 return Encryptor(ENCRYPTION_KEY).decrypt(value)
209 return Encryptor(ENCRYPTION_KEY).decrypt(value)
210 else:
210 else:
211 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
211 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
212 return decrypted_data
212 return decrypted_data
213
213
214
214
215 class BaseModel(object):
215 class BaseModel(object):
216 """
216 """
217 Base Model for all classes
217 Base Model for all classes
218 """
218 """
219
219
220 @classmethod
220 @classmethod
221 def _get_keys(cls):
221 def _get_keys(cls):
222 """return column names for this model """
222 """return column names for this model """
223 return class_mapper(cls).c.keys()
223 return class_mapper(cls).c.keys()
224
224
225 def get_dict(self):
225 def get_dict(self):
226 """
226 """
227 return dict with keys and values corresponding
227 return dict with keys and values corresponding
228 to this model data """
228 to this model data """
229
229
230 d = {}
230 d = {}
231 for k in self._get_keys():
231 for k in self._get_keys():
232 d[k] = getattr(self, k)
232 d[k] = getattr(self, k)
233
233
234 # also use __json__() if present to get additional fields
234 # also use __json__() if present to get additional fields
235 _json_attr = getattr(self, '__json__', None)
235 _json_attr = getattr(self, '__json__', None)
236 if _json_attr:
236 if _json_attr:
237 # update with attributes from __json__
237 # update with attributes from __json__
238 if callable(_json_attr):
238 if callable(_json_attr):
239 _json_attr = _json_attr()
239 _json_attr = _json_attr()
240 for k, val in _json_attr.iteritems():
240 for k, val in _json_attr.iteritems():
241 d[k] = val
241 d[k] = val
242 return d
242 return d
243
243
244 def get_appstruct(self):
244 def get_appstruct(self):
245 """return list with keys and values tuples corresponding
245 """return list with keys and values tuples corresponding
246 to this model data """
246 to this model data """
247
247
248 lst = []
248 lst = []
249 for k in self._get_keys():
249 for k in self._get_keys():
250 lst.append((k, getattr(self, k),))
250 lst.append((k, getattr(self, k),))
251 return lst
251 return lst
252
252
253 def populate_obj(self, populate_dict):
253 def populate_obj(self, populate_dict):
254 """populate model with data from given populate_dict"""
254 """populate model with data from given populate_dict"""
255
255
256 for k in self._get_keys():
256 for k in self._get_keys():
257 if k in populate_dict:
257 if k in populate_dict:
258 setattr(self, k, populate_dict[k])
258 setattr(self, k, populate_dict[k])
259
259
260 @classmethod
260 @classmethod
261 def query(cls):
261 def query(cls):
262 return Session().query(cls)
262 return Session().query(cls)
263
263
264 @classmethod
264 @classmethod
265 def get(cls, id_):
265 def get(cls, id_):
266 if id_:
266 if id_:
267 return cls.query().get(id_)
267 return cls.query().get(id_)
268
268
269 @classmethod
269 @classmethod
270 def get_or_404(cls, id_):
270 def get_or_404(cls, id_):
271 from pyramid.httpexceptions import HTTPNotFound
271 from pyramid.httpexceptions import HTTPNotFound
272
272
273 try:
273 try:
274 id_ = int(id_)
274 id_ = int(id_)
275 except (TypeError, ValueError):
275 except (TypeError, ValueError):
276 raise HTTPNotFound()
276 raise HTTPNotFound()
277
277
278 res = cls.query().get(id_)
278 res = cls.query().get(id_)
279 if not res:
279 if not res:
280 raise HTTPNotFound()
280 raise HTTPNotFound()
281 return res
281 return res
282
282
283 @classmethod
283 @classmethod
284 def getAll(cls):
284 def getAll(cls):
285 # deprecated and left for backward compatibility
285 # deprecated and left for backward compatibility
286 return cls.get_all()
286 return cls.get_all()
287
287
288 @classmethod
288 @classmethod
289 def get_all(cls):
289 def get_all(cls):
290 return cls.query().all()
290 return cls.query().all()
291
291
292 @classmethod
292 @classmethod
293 def delete(cls, id_):
293 def delete(cls, id_):
294 obj = cls.query().get(id_)
294 obj = cls.query().get(id_)
295 Session().delete(obj)
295 Session().delete(obj)
296
296
297 @classmethod
297 @classmethod
298 def identity_cache(cls, session, attr_name, value):
298 def identity_cache(cls, session, attr_name, value):
299 exist_in_session = []
299 exist_in_session = []
300 for (item_cls, pkey), instance in session.identity_map.items():
300 for (item_cls, pkey), instance in session.identity_map.items():
301 if cls == item_cls and getattr(instance, attr_name) == value:
301 if cls == item_cls and getattr(instance, attr_name) == value:
302 exist_in_session.append(instance)
302 exist_in_session.append(instance)
303 if exist_in_session:
303 if exist_in_session:
304 if len(exist_in_session) == 1:
304 if len(exist_in_session) == 1:
305 return exist_in_session[0]
305 return exist_in_session[0]
306 log.exception(
306 log.exception(
307 'multiple objects with attr %s and '
307 'multiple objects with attr %s and '
308 'value %s found with same name: %r',
308 'value %s found with same name: %r',
309 attr_name, value, exist_in_session)
309 attr_name, value, exist_in_session)
310
310
311 def __repr__(self):
311 def __repr__(self):
312 if hasattr(self, '__unicode__'):
312 if hasattr(self, '__unicode__'):
313 # python repr needs to return str
313 # python repr needs to return str
314 try:
314 try:
315 return safe_str(self.__unicode__())
315 return safe_str(self.__unicode__())
316 except UnicodeDecodeError:
316 except UnicodeDecodeError:
317 pass
317 pass
318 return '<DB:%s>' % (self.__class__.__name__)
318 return '<DB:%s>' % (self.__class__.__name__)
319
319
320
320
321 class RhodeCodeSetting(Base, BaseModel):
321 class RhodeCodeSetting(Base, BaseModel):
322 __tablename__ = 'rhodecode_settings'
322 __tablename__ = 'rhodecode_settings'
323 __table_args__ = (
323 __table_args__ = (
324 UniqueConstraint('app_settings_name'),
324 UniqueConstraint('app_settings_name'),
325 base_table_args
325 base_table_args
326 )
326 )
327
327
328 SETTINGS_TYPES = {
328 SETTINGS_TYPES = {
329 'str': safe_str,
329 'str': safe_str,
330 'int': safe_int,
330 'int': safe_int,
331 'unicode': safe_unicode,
331 'unicode': safe_unicode,
332 'bool': str2bool,
332 'bool': str2bool,
333 'list': functools.partial(aslist, sep=',')
333 'list': functools.partial(aslist, sep=',')
334 }
334 }
335 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
335 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
336 GLOBAL_CONF_KEY = 'app_settings'
336 GLOBAL_CONF_KEY = 'app_settings'
337
337
338 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
338 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
339 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
339 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
340 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
340 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
341 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
341 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
342
342
343 def __init__(self, key='', val='', type='unicode'):
343 def __init__(self, key='', val='', type='unicode'):
344 self.app_settings_name = key
344 self.app_settings_name = key
345 self.app_settings_type = type
345 self.app_settings_type = type
346 self.app_settings_value = val
346 self.app_settings_value = val
347
347
348 @validates('_app_settings_value')
348 @validates('_app_settings_value')
349 def validate_settings_value(self, key, val):
349 def validate_settings_value(self, key, val):
350 assert type(val) == unicode
350 assert type(val) == unicode
351 return val
351 return val
352
352
353 @hybrid_property
353 @hybrid_property
354 def app_settings_value(self):
354 def app_settings_value(self):
355 v = self._app_settings_value
355 v = self._app_settings_value
356 _type = self.app_settings_type
356 _type = self.app_settings_type
357 if _type:
357 if _type:
358 _type = self.app_settings_type.split('.')[0]
358 _type = self.app_settings_type.split('.')[0]
359 # decode the encrypted value
359 # decode the encrypted value
360 if 'encrypted' in self.app_settings_type:
360 if 'encrypted' in self.app_settings_type:
361 cipher = EncryptedTextValue()
361 cipher = EncryptedTextValue()
362 v = safe_unicode(cipher.process_result_value(v, None))
362 v = safe_unicode(cipher.process_result_value(v, None))
363
363
364 converter = self.SETTINGS_TYPES.get(_type) or \
364 converter = self.SETTINGS_TYPES.get(_type) or \
365 self.SETTINGS_TYPES['unicode']
365 self.SETTINGS_TYPES['unicode']
366 return converter(v)
366 return converter(v)
367
367
368 @app_settings_value.setter
368 @app_settings_value.setter
369 def app_settings_value(self, val):
369 def app_settings_value(self, val):
370 """
370 """
371 Setter that will always make sure we use unicode in app_settings_value
371 Setter that will always make sure we use unicode in app_settings_value
372
372
373 :param val:
373 :param val:
374 """
374 """
375 val = safe_unicode(val)
375 val = safe_unicode(val)
376 # encode the encrypted value
376 # encode the encrypted value
377 if 'encrypted' in self.app_settings_type:
377 if 'encrypted' in self.app_settings_type:
378 cipher = EncryptedTextValue()
378 cipher = EncryptedTextValue()
379 val = safe_unicode(cipher.process_bind_param(val, None))
379 val = safe_unicode(cipher.process_bind_param(val, None))
380 self._app_settings_value = val
380 self._app_settings_value = val
381
381
382 @hybrid_property
382 @hybrid_property
383 def app_settings_type(self):
383 def app_settings_type(self):
384 return self._app_settings_type
384 return self._app_settings_type
385
385
386 @app_settings_type.setter
386 @app_settings_type.setter
387 def app_settings_type(self, val):
387 def app_settings_type(self, val):
388 if val.split('.')[0] not in self.SETTINGS_TYPES:
388 if val.split('.')[0] not in self.SETTINGS_TYPES:
389 raise Exception('type must be one of %s got %s'
389 raise Exception('type must be one of %s got %s'
390 % (self.SETTINGS_TYPES.keys(), val))
390 % (self.SETTINGS_TYPES.keys(), val))
391 self._app_settings_type = val
391 self._app_settings_type = val
392
392
393 @classmethod
393 @classmethod
394 def get_by_prefix(cls, prefix):
394 def get_by_prefix(cls, prefix):
395 return RhodeCodeSetting.query()\
395 return RhodeCodeSetting.query()\
396 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
396 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
397 .all()
397 .all()
398
398
399 def __unicode__(self):
399 def __unicode__(self):
400 return u"<%s('%s:%s[%s]')>" % (
400 return u"<%s('%s:%s[%s]')>" % (
401 self.__class__.__name__,
401 self.__class__.__name__,
402 self.app_settings_name, self.app_settings_value,
402 self.app_settings_name, self.app_settings_value,
403 self.app_settings_type
403 self.app_settings_type
404 )
404 )
405
405
406
406
407 class RhodeCodeUi(Base, BaseModel):
407 class RhodeCodeUi(Base, BaseModel):
408 __tablename__ = 'rhodecode_ui'
408 __tablename__ = 'rhodecode_ui'
409 __table_args__ = (
409 __table_args__ = (
410 UniqueConstraint('ui_key'),
410 UniqueConstraint('ui_key'),
411 base_table_args
411 base_table_args
412 )
412 )
413
413
414 HOOK_REPO_SIZE = 'changegroup.repo_size'
414 HOOK_REPO_SIZE = 'changegroup.repo_size'
415 # HG
415 # HG
416 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
416 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
417 HOOK_PULL = 'outgoing.pull_logger'
417 HOOK_PULL = 'outgoing.pull_logger'
418 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
418 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
419 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
419 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
420 HOOK_PUSH = 'changegroup.push_logger'
420 HOOK_PUSH = 'changegroup.push_logger'
421 HOOK_PUSH_KEY = 'pushkey.key_push'
421 HOOK_PUSH_KEY = 'pushkey.key_push'
422
422
423 HOOKS_BUILTIN = [
423 HOOKS_BUILTIN = [
424 HOOK_PRE_PULL,
424 HOOK_PRE_PULL,
425 HOOK_PULL,
425 HOOK_PULL,
426 HOOK_PRE_PUSH,
426 HOOK_PRE_PUSH,
427 HOOK_PRETX_PUSH,
427 HOOK_PRETX_PUSH,
428 HOOK_PUSH,
428 HOOK_PUSH,
429 HOOK_PUSH_KEY,
429 HOOK_PUSH_KEY,
430 ]
430 ]
431
431
432 # TODO: johbo: Unify way how hooks are configured for git and hg,
432 # TODO: johbo: Unify way how hooks are configured for git and hg,
433 # git part is currently hardcoded.
433 # git part is currently hardcoded.
434
434
435 # SVN PATTERNS
435 # SVN PATTERNS
436 SVN_BRANCH_ID = 'vcs_svn_branch'
436 SVN_BRANCH_ID = 'vcs_svn_branch'
437 SVN_TAG_ID = 'vcs_svn_tag'
437 SVN_TAG_ID = 'vcs_svn_tag'
438
438
439 ui_id = Column(
439 ui_id = Column(
440 "ui_id", Integer(), nullable=False, unique=True, default=None,
440 "ui_id", Integer(), nullable=False, unique=True, default=None,
441 primary_key=True)
441 primary_key=True)
442 ui_section = Column(
442 ui_section = Column(
443 "ui_section", String(255), nullable=True, unique=None, default=None)
443 "ui_section", String(255), nullable=True, unique=None, default=None)
444 ui_key = Column(
444 ui_key = Column(
445 "ui_key", String(255), nullable=True, unique=None, default=None)
445 "ui_key", String(255), nullable=True, unique=None, default=None)
446 ui_value = Column(
446 ui_value = Column(
447 "ui_value", String(255), nullable=True, unique=None, default=None)
447 "ui_value", String(255), nullable=True, unique=None, default=None)
448 ui_active = Column(
448 ui_active = Column(
449 "ui_active", Boolean(), nullable=True, unique=None, default=True)
449 "ui_active", Boolean(), nullable=True, unique=None, default=True)
450
450
451 def __repr__(self):
451 def __repr__(self):
452 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
452 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
453 self.ui_key, self.ui_value)
453 self.ui_key, self.ui_value)
454
454
455
455
456 class RepoRhodeCodeSetting(Base, BaseModel):
456 class RepoRhodeCodeSetting(Base, BaseModel):
457 __tablename__ = 'repo_rhodecode_settings'
457 __tablename__ = 'repo_rhodecode_settings'
458 __table_args__ = (
458 __table_args__ = (
459 UniqueConstraint(
459 UniqueConstraint(
460 'app_settings_name', 'repository_id',
460 'app_settings_name', 'repository_id',
461 name='uq_repo_rhodecode_setting_name_repo_id'),
461 name='uq_repo_rhodecode_setting_name_repo_id'),
462 base_table_args
462 base_table_args
463 )
463 )
464
464
465 repository_id = Column(
465 repository_id = Column(
466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
467 nullable=False)
467 nullable=False)
468 app_settings_id = Column(
468 app_settings_id = Column(
469 "app_settings_id", Integer(), nullable=False, unique=True,
469 "app_settings_id", Integer(), nullable=False, unique=True,
470 default=None, primary_key=True)
470 default=None, primary_key=True)
471 app_settings_name = Column(
471 app_settings_name = Column(
472 "app_settings_name", String(255), nullable=True, unique=None,
472 "app_settings_name", String(255), nullable=True, unique=None,
473 default=None)
473 default=None)
474 _app_settings_value = Column(
474 _app_settings_value = Column(
475 "app_settings_value", String(4096), nullable=True, unique=None,
475 "app_settings_value", String(4096), nullable=True, unique=None,
476 default=None)
476 default=None)
477 _app_settings_type = Column(
477 _app_settings_type = Column(
478 "app_settings_type", String(255), nullable=True, unique=None,
478 "app_settings_type", String(255), nullable=True, unique=None,
479 default=None)
479 default=None)
480
480
481 repository = relationship('Repository')
481 repository = relationship('Repository')
482
482
483 def __init__(self, repository_id, key='', val='', type='unicode'):
483 def __init__(self, repository_id, key='', val='', type='unicode'):
484 self.repository_id = repository_id
484 self.repository_id = repository_id
485 self.app_settings_name = key
485 self.app_settings_name = key
486 self.app_settings_type = type
486 self.app_settings_type = type
487 self.app_settings_value = val
487 self.app_settings_value = val
488
488
489 @validates('_app_settings_value')
489 @validates('_app_settings_value')
490 def validate_settings_value(self, key, val):
490 def validate_settings_value(self, key, val):
491 assert type(val) == unicode
491 assert type(val) == unicode
492 return val
492 return val
493
493
494 @hybrid_property
494 @hybrid_property
495 def app_settings_value(self):
495 def app_settings_value(self):
496 v = self._app_settings_value
496 v = self._app_settings_value
497 type_ = self.app_settings_type
497 type_ = self.app_settings_type
498 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
498 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
499 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
499 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
500 return converter(v)
500 return converter(v)
501
501
502 @app_settings_value.setter
502 @app_settings_value.setter
503 def app_settings_value(self, val):
503 def app_settings_value(self, val):
504 """
504 """
505 Setter that will always make sure we use unicode in app_settings_value
505 Setter that will always make sure we use unicode in app_settings_value
506
506
507 :param val:
507 :param val:
508 """
508 """
509 self._app_settings_value = safe_unicode(val)
509 self._app_settings_value = safe_unicode(val)
510
510
511 @hybrid_property
511 @hybrid_property
512 def app_settings_type(self):
512 def app_settings_type(self):
513 return self._app_settings_type
513 return self._app_settings_type
514
514
515 @app_settings_type.setter
515 @app_settings_type.setter
516 def app_settings_type(self, val):
516 def app_settings_type(self, val):
517 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
517 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
518 if val not in SETTINGS_TYPES:
518 if val not in SETTINGS_TYPES:
519 raise Exception('type must be one of %s got %s'
519 raise Exception('type must be one of %s got %s'
520 % (SETTINGS_TYPES.keys(), val))
520 % (SETTINGS_TYPES.keys(), val))
521 self._app_settings_type = val
521 self._app_settings_type = val
522
522
523 def __unicode__(self):
523 def __unicode__(self):
524 return u"<%s('%s:%s:%s[%s]')>" % (
524 return u"<%s('%s:%s:%s[%s]')>" % (
525 self.__class__.__name__, self.repository.repo_name,
525 self.__class__.__name__, self.repository.repo_name,
526 self.app_settings_name, self.app_settings_value,
526 self.app_settings_name, self.app_settings_value,
527 self.app_settings_type
527 self.app_settings_type
528 )
528 )
529
529
530
530
531 class RepoRhodeCodeUi(Base, BaseModel):
531 class RepoRhodeCodeUi(Base, BaseModel):
532 __tablename__ = 'repo_rhodecode_ui'
532 __tablename__ = 'repo_rhodecode_ui'
533 __table_args__ = (
533 __table_args__ = (
534 UniqueConstraint(
534 UniqueConstraint(
535 'repository_id', 'ui_section', 'ui_key',
535 'repository_id', 'ui_section', 'ui_key',
536 name='uq_repo_rhodecode_ui_repository_id_section_key'),
536 name='uq_repo_rhodecode_ui_repository_id_section_key'),
537 base_table_args
537 base_table_args
538 )
538 )
539
539
540 repository_id = Column(
540 repository_id = Column(
541 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
541 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
542 nullable=False)
542 nullable=False)
543 ui_id = Column(
543 ui_id = Column(
544 "ui_id", Integer(), nullable=False, unique=True, default=None,
544 "ui_id", Integer(), nullable=False, unique=True, default=None,
545 primary_key=True)
545 primary_key=True)
546 ui_section = Column(
546 ui_section = Column(
547 "ui_section", String(255), nullable=True, unique=None, default=None)
547 "ui_section", String(255), nullable=True, unique=None, default=None)
548 ui_key = Column(
548 ui_key = Column(
549 "ui_key", String(255), nullable=True, unique=None, default=None)
549 "ui_key", String(255), nullable=True, unique=None, default=None)
550 ui_value = Column(
550 ui_value = Column(
551 "ui_value", String(255), nullable=True, unique=None, default=None)
551 "ui_value", String(255), nullable=True, unique=None, default=None)
552 ui_active = Column(
552 ui_active = Column(
553 "ui_active", Boolean(), nullable=True, unique=None, default=True)
553 "ui_active", Boolean(), nullable=True, unique=None, default=True)
554
554
555 repository = relationship('Repository')
555 repository = relationship('Repository')
556
556
557 def __repr__(self):
557 def __repr__(self):
558 return '<%s[%s:%s]%s=>%s]>' % (
558 return '<%s[%s:%s]%s=>%s]>' % (
559 self.__class__.__name__, self.repository.repo_name,
559 self.__class__.__name__, self.repository.repo_name,
560 self.ui_section, self.ui_key, self.ui_value)
560 self.ui_section, self.ui_key, self.ui_value)
561
561
562
562
563 class User(Base, BaseModel):
563 class User(Base, BaseModel):
564 __tablename__ = 'users'
564 __tablename__ = 'users'
565 __table_args__ = (
565 __table_args__ = (
566 UniqueConstraint('username'), UniqueConstraint('email'),
566 UniqueConstraint('username'), UniqueConstraint('email'),
567 Index('u_username_idx', 'username'),
567 Index('u_username_idx', 'username'),
568 Index('u_email_idx', 'email'),
568 Index('u_email_idx', 'email'),
569 base_table_args
569 base_table_args
570 )
570 )
571
571
572 DEFAULT_USER = 'default'
572 DEFAULT_USER = 'default'
573 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
573 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
574 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
574 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
575
575
576 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
576 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
577 username = Column("username", String(255), nullable=True, unique=None, default=None)
577 username = Column("username", String(255), nullable=True, unique=None, default=None)
578 password = Column("password", String(255), nullable=True, unique=None, default=None)
578 password = Column("password", String(255), nullable=True, unique=None, default=None)
579 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
579 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
580 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
580 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
581 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
581 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
582 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
582 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
583 _email = Column("email", String(255), nullable=True, unique=None, default=None)
583 _email = Column("email", String(255), nullable=True, unique=None, default=None)
584 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
584 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
585 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
585 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
586 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
586 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
587
587
588 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
588 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
589 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
589 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
590 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
590 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
591 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
591 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
592 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
592 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
593 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
593 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
594
594
595 user_log = relationship('UserLog')
595 user_log = relationship('UserLog')
596 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
596 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
597
597
598 repositories = relationship('Repository')
598 repositories = relationship('Repository')
599 repository_groups = relationship('RepoGroup')
599 repository_groups = relationship('RepoGroup')
600 user_groups = relationship('UserGroup')
600 user_groups = relationship('UserGroup')
601
601
602 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
602 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
603 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
603 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
604
604
605 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
605 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
606 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
606 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
607 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
607 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
608
608
609 group_member = relationship('UserGroupMember', cascade='all')
609 group_member = relationship('UserGroupMember', cascade='all')
610
610
611 notifications = relationship('UserNotification', cascade='all')
611 notifications = relationship('UserNotification', cascade='all')
612 # notifications assigned to this user
612 # notifications assigned to this user
613 user_created_notifications = relationship('Notification', cascade='all')
613 user_created_notifications = relationship('Notification', cascade='all')
614 # comments created by this user
614 # comments created by this user
615 user_comments = relationship('ChangesetComment', cascade='all')
615 user_comments = relationship('ChangesetComment', cascade='all')
616 # user profile extra info
616 # user profile extra info
617 user_emails = relationship('UserEmailMap', cascade='all')
617 user_emails = relationship('UserEmailMap', cascade='all')
618 user_ip_map = relationship('UserIpMap', cascade='all')
618 user_ip_map = relationship('UserIpMap', cascade='all')
619 user_auth_tokens = relationship('UserApiKeys', cascade='all')
619 user_auth_tokens = relationship('UserApiKeys', cascade='all')
620 user_ssh_keys = relationship('UserSshKeys', cascade='all')
620 user_ssh_keys = relationship('UserSshKeys', cascade='all')
621
621
622 # gists
622 # gists
623 user_gists = relationship('Gist', cascade='all')
623 user_gists = relationship('Gist', cascade='all')
624 # user pull requests
624 # user pull requests
625 user_pull_requests = relationship('PullRequest', cascade='all')
625 user_pull_requests = relationship('PullRequest', cascade='all')
626
626
627 # external identities
627 # external identities
628 external_identities = relationship(
628 external_identities = relationship(
629 'ExternalIdentity',
629 'ExternalIdentity',
630 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
630 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
631 cascade='all')
631 cascade='all')
632 # review rules
632 # review rules
633 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
633 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
634
634
635 # artifacts owned
635 # artifacts owned
636 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
636 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
637
637
638 # no cascade, set NULL
638 # no cascade, set NULL
639 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
639 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
640
640
641 def __unicode__(self):
641 def __unicode__(self):
642 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
642 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
643 self.user_id, self.username)
643 self.user_id, self.username)
644
644
645 @hybrid_property
645 @hybrid_property
646 def email(self):
646 def email(self):
647 return self._email
647 return self._email
648
648
649 @email.setter
649 @email.setter
650 def email(self, val):
650 def email(self, val):
651 self._email = val.lower() if val else None
651 self._email = val.lower() if val else None
652
652
653 @hybrid_property
653 @hybrid_property
654 def first_name(self):
654 def first_name(self):
655 from rhodecode.lib import helpers as h
655 from rhodecode.lib import helpers as h
656 if self.name:
656 if self.name:
657 return h.escape(self.name)
657 return h.escape(self.name)
658 return self.name
658 return self.name
659
659
660 @hybrid_property
660 @hybrid_property
661 def last_name(self):
661 def last_name(self):
662 from rhodecode.lib import helpers as h
662 from rhodecode.lib import helpers as h
663 if self.lastname:
663 if self.lastname:
664 return h.escape(self.lastname)
664 return h.escape(self.lastname)
665 return self.lastname
665 return self.lastname
666
666
667 @hybrid_property
667 @hybrid_property
668 def api_key(self):
668 def api_key(self):
669 """
669 """
670 Fetch if exist an auth-token with role ALL connected to this user
670 Fetch if exist an auth-token with role ALL connected to this user
671 """
671 """
672 user_auth_token = UserApiKeys.query()\
672 user_auth_token = UserApiKeys.query()\
673 .filter(UserApiKeys.user_id == self.user_id)\
673 .filter(UserApiKeys.user_id == self.user_id)\
674 .filter(or_(UserApiKeys.expires == -1,
674 .filter(or_(UserApiKeys.expires == -1,
675 UserApiKeys.expires >= time.time()))\
675 UserApiKeys.expires >= time.time()))\
676 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
676 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
677 if user_auth_token:
677 if user_auth_token:
678 user_auth_token = user_auth_token.api_key
678 user_auth_token = user_auth_token.api_key
679
679
680 return user_auth_token
680 return user_auth_token
681
681
682 @api_key.setter
682 @api_key.setter
683 def api_key(self, val):
683 def api_key(self, val):
684 # don't allow to set API key this is deprecated for now
684 # don't allow to set API key this is deprecated for now
685 self._api_key = None
685 self._api_key = None
686
686
687 @property
687 @property
688 def reviewer_pull_requests(self):
688 def reviewer_pull_requests(self):
689 return PullRequestReviewers.query() \
689 return PullRequestReviewers.query() \
690 .options(joinedload(PullRequestReviewers.pull_request)) \
690 .options(joinedload(PullRequestReviewers.pull_request)) \
691 .filter(PullRequestReviewers.user_id == self.user_id) \
691 .filter(PullRequestReviewers.user_id == self.user_id) \
692 .all()
692 .all()
693
693
694 @property
694 @property
695 def firstname(self):
695 def firstname(self):
696 # alias for future
696 # alias for future
697 return self.name
697 return self.name
698
698
699 @property
699 @property
700 def emails(self):
700 def emails(self):
701 other = UserEmailMap.query()\
701 other = UserEmailMap.query()\
702 .filter(UserEmailMap.user == self) \
702 .filter(UserEmailMap.user == self) \
703 .order_by(UserEmailMap.email_id.asc()) \
703 .order_by(UserEmailMap.email_id.asc()) \
704 .all()
704 .all()
705 return [self.email] + [x.email for x in other]
705 return [self.email] + [x.email for x in other]
706
706
707 def emails_cached(self):
707 def emails_cached(self):
708 emails = UserEmailMap.query()\
708 emails = UserEmailMap.query()\
709 .filter(UserEmailMap.user == self) \
709 .filter(UserEmailMap.user == self) \
710 .order_by(UserEmailMap.email_id.asc())
710 .order_by(UserEmailMap.email_id.asc())
711
711
712 emails = emails.options(
712 emails = emails.options(
713 FromCache("sql_cache_short", "get_user_{}_emails".format(self.user_id))
713 FromCache("sql_cache_short", "get_user_{}_emails".format(self.user_id))
714 )
714 )
715
715
716 return [self.email] + [x.email for x in emails]
716 return [self.email] + [x.email for x in emails]
717
717
718 @property
718 @property
719 def auth_tokens(self):
719 def auth_tokens(self):
720 auth_tokens = self.get_auth_tokens()
720 auth_tokens = self.get_auth_tokens()
721 return [x.api_key for x in auth_tokens]
721 return [x.api_key for x in auth_tokens]
722
722
723 def get_auth_tokens(self):
723 def get_auth_tokens(self):
724 return UserApiKeys.query()\
724 return UserApiKeys.query()\
725 .filter(UserApiKeys.user == self)\
725 .filter(UserApiKeys.user == self)\
726 .order_by(UserApiKeys.user_api_key_id.asc())\
726 .order_by(UserApiKeys.user_api_key_id.asc())\
727 .all()
727 .all()
728
728
729 @LazyProperty
729 @LazyProperty
730 def feed_token(self):
730 def feed_token(self):
731 return self.get_feed_token()
731 return self.get_feed_token()
732
732
733 def get_feed_token(self, cache=True):
733 def get_feed_token(self, cache=True):
734 feed_tokens = UserApiKeys.query()\
734 feed_tokens = UserApiKeys.query()\
735 .filter(UserApiKeys.user == self)\
735 .filter(UserApiKeys.user == self)\
736 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
736 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
737 if cache:
737 if cache:
738 feed_tokens = feed_tokens.options(
738 feed_tokens = feed_tokens.options(
739 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
739 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
740
740
741 feed_tokens = feed_tokens.all()
741 feed_tokens = feed_tokens.all()
742 if feed_tokens:
742 if feed_tokens:
743 return feed_tokens[0].api_key
743 return feed_tokens[0].api_key
744 return 'NO_FEED_TOKEN_AVAILABLE'
744 return 'NO_FEED_TOKEN_AVAILABLE'
745
745
746 @LazyProperty
746 @LazyProperty
747 def artifact_token(self):
747 def artifact_token(self):
748 return self.get_artifact_token()
748 return self.get_artifact_token()
749
749
750 def get_artifact_token(self, cache=True):
750 def get_artifact_token(self, cache=True):
751 artifacts_tokens = UserApiKeys.query()\
751 artifacts_tokens = UserApiKeys.query()\
752 .filter(UserApiKeys.user == self) \
752 .filter(UserApiKeys.user == self) \
753 .filter(or_(UserApiKeys.expires == -1,
753 .filter(or_(UserApiKeys.expires == -1,
754 UserApiKeys.expires >= time.time())) \
754 UserApiKeys.expires >= time.time())) \
755 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
755 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
756
756
757 if cache:
757 if cache:
758 artifacts_tokens = artifacts_tokens.options(
758 artifacts_tokens = artifacts_tokens.options(
759 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
759 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
760
760
761 artifacts_tokens = artifacts_tokens.all()
761 artifacts_tokens = artifacts_tokens.all()
762 if artifacts_tokens:
762 if artifacts_tokens:
763 return artifacts_tokens[0].api_key
763 return artifacts_tokens[0].api_key
764 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
764 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
765
765
766 def get_or_create_artifact_token(self):
766 def get_or_create_artifact_token(self):
767 artifacts_tokens = UserApiKeys.query()\
767 artifacts_tokens = UserApiKeys.query()\
768 .filter(UserApiKeys.user == self) \
768 .filter(UserApiKeys.user == self) \
769 .filter(or_(UserApiKeys.expires == -1,
769 .filter(or_(UserApiKeys.expires == -1,
770 UserApiKeys.expires >= time.time())) \
770 UserApiKeys.expires >= time.time())) \
771 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
771 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
772
772
773 artifacts_tokens = artifacts_tokens.all()
773 artifacts_tokens = artifacts_tokens.all()
774 if artifacts_tokens:
774 if artifacts_tokens:
775 return artifacts_tokens[0].api_key
775 return artifacts_tokens[0].api_key
776 else:
776 else:
777 from rhodecode.model.auth_token import AuthTokenModel
777 from rhodecode.model.auth_token import AuthTokenModel
778 artifact_token = AuthTokenModel().create(
778 artifact_token = AuthTokenModel().create(
779 self, 'auto-generated-artifact-token',
779 self, 'auto-generated-artifact-token',
780 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
780 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
781 Session.commit()
781 Session.commit()
782 return artifact_token.api_key
782 return artifact_token.api_key
783
783
784 @classmethod
784 @classmethod
785 def get(cls, user_id, cache=False):
785 def get(cls, user_id, cache=False):
786 if not user_id:
786 if not user_id:
787 return
787 return
788
788
789 user = cls.query()
789 user = cls.query()
790 if cache:
790 if cache:
791 user = user.options(
791 user = user.options(
792 FromCache("sql_cache_short", "get_users_%s" % user_id))
792 FromCache("sql_cache_short", "get_users_%s" % user_id))
793 return user.get(user_id)
793 return user.get(user_id)
794
794
795 @classmethod
795 @classmethod
796 def extra_valid_auth_tokens(cls, user, role=None):
796 def extra_valid_auth_tokens(cls, user, role=None):
797 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
797 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
798 .filter(or_(UserApiKeys.expires == -1,
798 .filter(or_(UserApiKeys.expires == -1,
799 UserApiKeys.expires >= time.time()))
799 UserApiKeys.expires >= time.time()))
800 if role:
800 if role:
801 tokens = tokens.filter(or_(UserApiKeys.role == role,
801 tokens = tokens.filter(or_(UserApiKeys.role == role,
802 UserApiKeys.role == UserApiKeys.ROLE_ALL))
802 UserApiKeys.role == UserApiKeys.ROLE_ALL))
803 return tokens.all()
803 return tokens.all()
804
804
805 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
805 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
806 from rhodecode.lib import auth
806 from rhodecode.lib import auth
807
807
808 log.debug('Trying to authenticate user: %s via auth-token, '
808 log.debug('Trying to authenticate user: %s via auth-token, '
809 'and roles: %s', self, roles)
809 'and roles: %s', self, roles)
810
810
811 if not auth_token:
811 if not auth_token:
812 return False
812 return False
813
813
814 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
814 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
815 tokens_q = UserApiKeys.query()\
815 tokens_q = UserApiKeys.query()\
816 .filter(UserApiKeys.user_id == self.user_id)\
816 .filter(UserApiKeys.user_id == self.user_id)\
817 .filter(or_(UserApiKeys.expires == -1,
817 .filter(or_(UserApiKeys.expires == -1,
818 UserApiKeys.expires >= time.time()))
818 UserApiKeys.expires >= time.time()))
819
819
820 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
820 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
821
821
822 crypto_backend = auth.crypto_backend()
822 crypto_backend = auth.crypto_backend()
823 enc_token_map = {}
823 enc_token_map = {}
824 plain_token_map = {}
824 plain_token_map = {}
825 for token in tokens_q:
825 for token in tokens_q:
826 if token.api_key.startswith(crypto_backend.ENC_PREF):
826 if token.api_key.startswith(crypto_backend.ENC_PREF):
827 enc_token_map[token.api_key] = token
827 enc_token_map[token.api_key] = token
828 else:
828 else:
829 plain_token_map[token.api_key] = token
829 plain_token_map[token.api_key] = token
830 log.debug(
830 log.debug(
831 'Found %s plain and %s encrypted tokens to check for authentication for this user',
831 'Found %s plain and %s encrypted tokens to check for authentication for this user',
832 len(plain_token_map), len(enc_token_map))
832 len(plain_token_map), len(enc_token_map))
833
833
834 # plain token match comes first
834 # plain token match comes first
835 match = plain_token_map.get(auth_token)
835 match = plain_token_map.get(auth_token)
836
836
837 # check encrypted tokens now
837 # check encrypted tokens now
838 if not match:
838 if not match:
839 for token_hash, token in enc_token_map.items():
839 for token_hash, token in enc_token_map.items():
840 # NOTE(marcink): this is expensive to calculate, but most secure
840 # NOTE(marcink): this is expensive to calculate, but most secure
841 if crypto_backend.hash_check(auth_token, token_hash):
841 if crypto_backend.hash_check(auth_token, token_hash):
842 match = token
842 match = token
843 break
843 break
844
844
845 if match:
845 if match:
846 log.debug('Found matching token %s', match)
846 log.debug('Found matching token %s', match)
847 if match.repo_id:
847 if match.repo_id:
848 log.debug('Found scope, checking for scope match of token %s', match)
848 log.debug('Found scope, checking for scope match of token %s', match)
849 if match.repo_id == scope_repo_id:
849 if match.repo_id == scope_repo_id:
850 return True
850 return True
851 else:
851 else:
852 log.debug(
852 log.debug(
853 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
853 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
854 'and calling scope is:%s, skipping further checks',
854 'and calling scope is:%s, skipping further checks',
855 match.repo, scope_repo_id)
855 match.repo, scope_repo_id)
856 return False
856 return False
857 else:
857 else:
858 return True
858 return True
859
859
860 return False
860 return False
861
861
862 @property
862 @property
863 def ip_addresses(self):
863 def ip_addresses(self):
864 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
864 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
865 return [x.ip_addr for x in ret]
865 return [x.ip_addr for x in ret]
866
866
867 @property
867 @property
868 def username_and_name(self):
868 def username_and_name(self):
869 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
869 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
870
870
871 @property
871 @property
872 def username_or_name_or_email(self):
872 def username_or_name_or_email(self):
873 full_name = self.full_name if self.full_name is not ' ' else None
873 full_name = self.full_name if self.full_name is not ' ' else None
874 return self.username or full_name or self.email
874 return self.username or full_name or self.email
875
875
876 @property
876 @property
877 def full_name(self):
877 def full_name(self):
878 return '%s %s' % (self.first_name, self.last_name)
878 return '%s %s' % (self.first_name, self.last_name)
879
879
880 @property
880 @property
881 def full_name_or_username(self):
881 def full_name_or_username(self):
882 return ('%s %s' % (self.first_name, self.last_name)
882 return ('%s %s' % (self.first_name, self.last_name)
883 if (self.first_name and self.last_name) else self.username)
883 if (self.first_name and self.last_name) else self.username)
884
884
885 @property
885 @property
886 def full_contact(self):
886 def full_contact(self):
887 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
887 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
888
888
889 @property
889 @property
890 def short_contact(self):
890 def short_contact(self):
891 return '%s %s' % (self.first_name, self.last_name)
891 return '%s %s' % (self.first_name, self.last_name)
892
892
893 @property
893 @property
894 def is_admin(self):
894 def is_admin(self):
895 return self.admin
895 return self.admin
896
896
897 @property
897 @property
898 def language(self):
898 def language(self):
899 return self.user_data.get('language')
899 return self.user_data.get('language')
900
900
901 def AuthUser(self, **kwargs):
901 def AuthUser(self, **kwargs):
902 """
902 """
903 Returns instance of AuthUser for this user
903 Returns instance of AuthUser for this user
904 """
904 """
905 from rhodecode.lib.auth import AuthUser
905 from rhodecode.lib.auth import AuthUser
906 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
906 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
907
907
908 @hybrid_property
908 @hybrid_property
909 def user_data(self):
909 def user_data(self):
910 if not self._user_data:
910 if not self._user_data:
911 return {}
911 return {}
912
912
913 try:
913 try:
914 return json.loads(self._user_data)
914 return json.loads(self._user_data)
915 except TypeError:
915 except TypeError:
916 return {}
916 return {}
917
917
918 @user_data.setter
918 @user_data.setter
919 def user_data(self, val):
919 def user_data(self, val):
920 if not isinstance(val, dict):
920 if not isinstance(val, dict):
921 raise Exception('user_data must be dict, got %s' % type(val))
921 raise Exception('user_data must be dict, got %s' % type(val))
922 try:
922 try:
923 self._user_data = json.dumps(val)
923 self._user_data = json.dumps(val)
924 except Exception:
924 except Exception:
925 log.error(traceback.format_exc())
925 log.error(traceback.format_exc())
926
926
927 @classmethod
927 @classmethod
928 def get_by_username(cls, username, case_insensitive=False,
928 def get_by_username(cls, username, case_insensitive=False,
929 cache=False, identity_cache=False):
929 cache=False, identity_cache=False):
930 session = Session()
930 session = Session()
931
931
932 if case_insensitive:
932 if case_insensitive:
933 q = cls.query().filter(
933 q = cls.query().filter(
934 func.lower(cls.username) == func.lower(username))
934 func.lower(cls.username) == func.lower(username))
935 else:
935 else:
936 q = cls.query().filter(cls.username == username)
936 q = cls.query().filter(cls.username == username)
937
937
938 if cache:
938 if cache:
939 if identity_cache:
939 if identity_cache:
940 val = cls.identity_cache(session, 'username', username)
940 val = cls.identity_cache(session, 'username', username)
941 if val:
941 if val:
942 return val
942 return val
943 else:
943 else:
944 cache_key = "get_user_by_name_%s" % _hash_key(username)
944 cache_key = "get_user_by_name_%s" % _hash_key(username)
945 q = q.options(
945 q = q.options(
946 FromCache("sql_cache_short", cache_key))
946 FromCache("sql_cache_short", cache_key))
947
947
948 return q.scalar()
948 return q.scalar()
949
949
950 @classmethod
950 @classmethod
951 def get_by_auth_token(cls, auth_token, cache=False):
951 def get_by_auth_token(cls, auth_token, cache=False):
952 q = UserApiKeys.query()\
952 q = UserApiKeys.query()\
953 .filter(UserApiKeys.api_key == auth_token)\
953 .filter(UserApiKeys.api_key == auth_token)\
954 .filter(or_(UserApiKeys.expires == -1,
954 .filter(or_(UserApiKeys.expires == -1,
955 UserApiKeys.expires >= time.time()))
955 UserApiKeys.expires >= time.time()))
956 if cache:
956 if cache:
957 q = q.options(
957 q = q.options(
958 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
958 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
959
959
960 match = q.first()
960 match = q.first()
961 if match:
961 if match:
962 return match.user
962 return match.user
963
963
964 @classmethod
964 @classmethod
965 def get_by_email(cls, email, case_insensitive=False, cache=False):
965 def get_by_email(cls, email, case_insensitive=False, cache=False):
966
966
967 if case_insensitive:
967 if case_insensitive:
968 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
968 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
969
969
970 else:
970 else:
971 q = cls.query().filter(cls.email == email)
971 q = cls.query().filter(cls.email == email)
972
972
973 email_key = _hash_key(email)
973 email_key = _hash_key(email)
974 if cache:
974 if cache:
975 q = q.options(
975 q = q.options(
976 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
976 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
977
977
978 ret = q.scalar()
978 ret = q.scalar()
979 if ret is None:
979 if ret is None:
980 q = UserEmailMap.query()
980 q = UserEmailMap.query()
981 # try fetching in alternate email map
981 # try fetching in alternate email map
982 if case_insensitive:
982 if case_insensitive:
983 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
983 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
984 else:
984 else:
985 q = q.filter(UserEmailMap.email == email)
985 q = q.filter(UserEmailMap.email == email)
986 q = q.options(joinedload(UserEmailMap.user))
986 q = q.options(joinedload(UserEmailMap.user))
987 if cache:
987 if cache:
988 q = q.options(
988 q = q.options(
989 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
989 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
990 ret = getattr(q.scalar(), 'user', None)
990 ret = getattr(q.scalar(), 'user', None)
991
991
992 return ret
992 return ret
993
993
994 @classmethod
994 @classmethod
995 def get_from_cs_author(cls, author):
995 def get_from_cs_author(cls, author):
996 """
996 """
997 Tries to get User objects out of commit author string
997 Tries to get User objects out of commit author string
998
998
999 :param author:
999 :param author:
1000 """
1000 """
1001 from rhodecode.lib.helpers import email, author_name
1001 from rhodecode.lib.helpers import email, author_name
1002 # Valid email in the attribute passed, see if they're in the system
1002 # Valid email in the attribute passed, see if they're in the system
1003 _email = email(author)
1003 _email = email(author)
1004 if _email:
1004 if _email:
1005 user = cls.get_by_email(_email, case_insensitive=True)
1005 user = cls.get_by_email(_email, case_insensitive=True)
1006 if user:
1006 if user:
1007 return user
1007 return user
1008 # Maybe we can match by username?
1008 # Maybe we can match by username?
1009 _author = author_name(author)
1009 _author = author_name(author)
1010 user = cls.get_by_username(_author, case_insensitive=True)
1010 user = cls.get_by_username(_author, case_insensitive=True)
1011 if user:
1011 if user:
1012 return user
1012 return user
1013
1013
1014 def update_userdata(self, **kwargs):
1014 def update_userdata(self, **kwargs):
1015 usr = self
1015 usr = self
1016 old = usr.user_data
1016 old = usr.user_data
1017 old.update(**kwargs)
1017 old.update(**kwargs)
1018 usr.user_data = old
1018 usr.user_data = old
1019 Session().add(usr)
1019 Session().add(usr)
1020 log.debug('updated userdata with %s', kwargs)
1020 log.debug('updated userdata with %s', kwargs)
1021
1021
1022 def update_lastlogin(self):
1022 def update_lastlogin(self):
1023 """Update user lastlogin"""
1023 """Update user lastlogin"""
1024 self.last_login = datetime.datetime.now()
1024 self.last_login = datetime.datetime.now()
1025 Session().add(self)
1025 Session().add(self)
1026 log.debug('updated user %s lastlogin', self.username)
1026 log.debug('updated user %s lastlogin', self.username)
1027
1027
1028 def update_password(self, new_password):
1028 def update_password(self, new_password):
1029 from rhodecode.lib.auth import get_crypt_password
1029 from rhodecode.lib.auth import get_crypt_password
1030
1030
1031 self.password = get_crypt_password(new_password)
1031 self.password = get_crypt_password(new_password)
1032 Session().add(self)
1032 Session().add(self)
1033
1033
1034 @classmethod
1034 @classmethod
1035 def get_first_super_admin(cls):
1035 def get_first_super_admin(cls):
1036 user = User.query()\
1036 user = User.query()\
1037 .filter(User.admin == true()) \
1037 .filter(User.admin == true()) \
1038 .order_by(User.user_id.asc()) \
1038 .order_by(User.user_id.asc()) \
1039 .first()
1039 .first()
1040
1040
1041 if user is None:
1041 if user is None:
1042 raise Exception('FATAL: Missing administrative account!')
1042 raise Exception('FATAL: Missing administrative account!')
1043 return user
1043 return user
1044
1044
1045 @classmethod
1045 @classmethod
1046 def get_all_super_admins(cls, only_active=False):
1046 def get_all_super_admins(cls, only_active=False):
1047 """
1047 """
1048 Returns all admin accounts sorted by username
1048 Returns all admin accounts sorted by username
1049 """
1049 """
1050 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1050 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1051 if only_active:
1051 if only_active:
1052 qry = qry.filter(User.active == true())
1052 qry = qry.filter(User.active == true())
1053 return qry.all()
1053 return qry.all()
1054
1054
1055 @classmethod
1055 @classmethod
1056 def get_all_user_ids(cls, only_active=True):
1056 def get_all_user_ids(cls, only_active=True):
1057 """
1057 """
1058 Returns all users IDs
1058 Returns all users IDs
1059 """
1059 """
1060 qry = Session().query(User.user_id)
1060 qry = Session().query(User.user_id)
1061
1061
1062 if only_active:
1062 if only_active:
1063 qry = qry.filter(User.active == true())
1063 qry = qry.filter(User.active == true())
1064 return [x.user_id for x in qry]
1064 return [x.user_id for x in qry]
1065
1065
1066 @classmethod
1066 @classmethod
1067 def get_default_user(cls, cache=False, refresh=False):
1067 def get_default_user(cls, cache=False, refresh=False):
1068 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1068 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1069 if user is None:
1069 if user is None:
1070 raise Exception('FATAL: Missing default account!')
1070 raise Exception('FATAL: Missing default account!')
1071 if refresh:
1071 if refresh:
1072 # The default user might be based on outdated state which
1072 # The default user might be based on outdated state which
1073 # has been loaded from the cache.
1073 # has been loaded from the cache.
1074 # A call to refresh() ensures that the
1074 # A call to refresh() ensures that the
1075 # latest state from the database is used.
1075 # latest state from the database is used.
1076 Session().refresh(user)
1076 Session().refresh(user)
1077 return user
1077 return user
1078
1078
1079 @classmethod
1079 @classmethod
1080 def get_default_user_id(cls):
1080 def get_default_user_id(cls):
1081 import rhodecode
1081 import rhodecode
1082 return rhodecode.CONFIG['default_user_id']
1082 return rhodecode.CONFIG['default_user_id']
1083
1083
1084 def _get_default_perms(self, user, suffix=''):
1084 def _get_default_perms(self, user, suffix=''):
1085 from rhodecode.model.permission import PermissionModel
1085 from rhodecode.model.permission import PermissionModel
1086 return PermissionModel().get_default_perms(user.user_perms, suffix)
1086 return PermissionModel().get_default_perms(user.user_perms, suffix)
1087
1087
1088 def get_default_perms(self, suffix=''):
1088 def get_default_perms(self, suffix=''):
1089 return self._get_default_perms(self, suffix)
1089 return self._get_default_perms(self, suffix)
1090
1090
1091 def get_api_data(self, include_secrets=False, details='full'):
1091 def get_api_data(self, include_secrets=False, details='full'):
1092 """
1092 """
1093 Common function for generating user related data for API
1093 Common function for generating user related data for API
1094
1094
1095 :param include_secrets: By default secrets in the API data will be replaced
1095 :param include_secrets: By default secrets in the API data will be replaced
1096 by a placeholder value to prevent exposing this data by accident. In case
1096 by a placeholder value to prevent exposing this data by accident. In case
1097 this data shall be exposed, set this flag to ``True``.
1097 this data shall be exposed, set this flag to ``True``.
1098
1098
1099 :param details: details can be 'basic|full' basic gives only a subset of
1099 :param details: details can be 'basic|full' basic gives only a subset of
1100 the available user information that includes user_id, name and emails.
1100 the available user information that includes user_id, name and emails.
1101 """
1101 """
1102 user = self
1102 user = self
1103 user_data = self.user_data
1103 user_data = self.user_data
1104 data = {
1104 data = {
1105 'user_id': user.user_id,
1105 'user_id': user.user_id,
1106 'username': user.username,
1106 'username': user.username,
1107 'firstname': user.name,
1107 'firstname': user.name,
1108 'lastname': user.lastname,
1108 'lastname': user.lastname,
1109 'description': user.description,
1109 'description': user.description,
1110 'email': user.email,
1110 'email': user.email,
1111 'emails': user.emails,
1111 'emails': user.emails,
1112 }
1112 }
1113 if details == 'basic':
1113 if details == 'basic':
1114 return data
1114 return data
1115
1115
1116 auth_token_length = 40
1116 auth_token_length = 40
1117 auth_token_replacement = '*' * auth_token_length
1117 auth_token_replacement = '*' * auth_token_length
1118
1118
1119 extras = {
1119 extras = {
1120 'auth_tokens': [auth_token_replacement],
1120 'auth_tokens': [auth_token_replacement],
1121 'active': user.active,
1121 'active': user.active,
1122 'admin': user.admin,
1122 'admin': user.admin,
1123 'extern_type': user.extern_type,
1123 'extern_type': user.extern_type,
1124 'extern_name': user.extern_name,
1124 'extern_name': user.extern_name,
1125 'last_login': user.last_login,
1125 'last_login': user.last_login,
1126 'last_activity': user.last_activity,
1126 'last_activity': user.last_activity,
1127 'ip_addresses': user.ip_addresses,
1127 'ip_addresses': user.ip_addresses,
1128 'language': user_data.get('language')
1128 'language': user_data.get('language')
1129 }
1129 }
1130 data.update(extras)
1130 data.update(extras)
1131
1131
1132 if include_secrets:
1132 if include_secrets:
1133 data['auth_tokens'] = user.auth_tokens
1133 data['auth_tokens'] = user.auth_tokens
1134 return data
1134 return data
1135
1135
1136 def __json__(self):
1136 def __json__(self):
1137 data = {
1137 data = {
1138 'full_name': self.full_name,
1138 'full_name': self.full_name,
1139 'full_name_or_username': self.full_name_or_username,
1139 'full_name_or_username': self.full_name_or_username,
1140 'short_contact': self.short_contact,
1140 'short_contact': self.short_contact,
1141 'full_contact': self.full_contact,
1141 'full_contact': self.full_contact,
1142 }
1142 }
1143 data.update(self.get_api_data())
1143 data.update(self.get_api_data())
1144 return data
1144 return data
1145
1145
1146
1146
1147 class UserApiKeys(Base, BaseModel):
1147 class UserApiKeys(Base, BaseModel):
1148 __tablename__ = 'user_api_keys'
1148 __tablename__ = 'user_api_keys'
1149 __table_args__ = (
1149 __table_args__ = (
1150 Index('uak_api_key_idx', 'api_key'),
1150 Index('uak_api_key_idx', 'api_key'),
1151 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1151 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1152 base_table_args
1152 base_table_args
1153 )
1153 )
1154 __mapper_args__ = {}
1154 __mapper_args__ = {}
1155
1155
1156 # ApiKey role
1156 # ApiKey role
1157 ROLE_ALL = 'token_role_all'
1157 ROLE_ALL = 'token_role_all'
1158 ROLE_VCS = 'token_role_vcs'
1158 ROLE_VCS = 'token_role_vcs'
1159 ROLE_API = 'token_role_api'
1159 ROLE_API = 'token_role_api'
1160 ROLE_HTTP = 'token_role_http'
1160 ROLE_HTTP = 'token_role_http'
1161 ROLE_FEED = 'token_role_feed'
1161 ROLE_FEED = 'token_role_feed'
1162 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1162 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1163 # The last one is ignored in the list as we only
1163 # The last one is ignored in the list as we only
1164 # use it for one action, and cannot be created by users
1164 # use it for one action, and cannot be created by users
1165 ROLE_PASSWORD_RESET = 'token_password_reset'
1165 ROLE_PASSWORD_RESET = 'token_password_reset'
1166
1166
1167 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1167 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1168
1168
1169 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1169 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1170 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1170 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1171 api_key = Column("api_key", String(255), nullable=False, unique=True)
1171 api_key = Column("api_key", String(255), nullable=False, unique=True)
1172 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1172 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1173 expires = Column('expires', Float(53), nullable=False)
1173 expires = Column('expires', Float(53), nullable=False)
1174 role = Column('role', String(255), nullable=True)
1174 role = Column('role', String(255), nullable=True)
1175 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1175 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1176
1176
1177 # scope columns
1177 # scope columns
1178 repo_id = Column(
1178 repo_id = Column(
1179 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1179 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1180 nullable=True, unique=None, default=None)
1180 nullable=True, unique=None, default=None)
1181 repo = relationship('Repository', lazy='joined')
1181 repo = relationship('Repository', lazy='joined')
1182
1182
1183 repo_group_id = Column(
1183 repo_group_id = Column(
1184 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1184 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1185 nullable=True, unique=None, default=None)
1185 nullable=True, unique=None, default=None)
1186 repo_group = relationship('RepoGroup', lazy='joined')
1186 repo_group = relationship('RepoGroup', lazy='joined')
1187
1187
1188 user = relationship('User', lazy='joined')
1188 user = relationship('User', lazy='joined')
1189
1189
1190 def __unicode__(self):
1190 def __unicode__(self):
1191 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1191 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1192
1192
1193 def __json__(self):
1193 def __json__(self):
1194 data = {
1194 data = {
1195 'auth_token': self.api_key,
1195 'auth_token': self.api_key,
1196 'role': self.role,
1196 'role': self.role,
1197 'scope': self.scope_humanized,
1197 'scope': self.scope_humanized,
1198 'expired': self.expired
1198 'expired': self.expired
1199 }
1199 }
1200 return data
1200 return data
1201
1201
1202 def get_api_data(self, include_secrets=False):
1202 def get_api_data(self, include_secrets=False):
1203 data = self.__json__()
1203 data = self.__json__()
1204 if include_secrets:
1204 if include_secrets:
1205 return data
1205 return data
1206 else:
1206 else:
1207 data['auth_token'] = self.token_obfuscated
1207 data['auth_token'] = self.token_obfuscated
1208 return data
1208 return data
1209
1209
1210 @hybrid_property
1210 @hybrid_property
1211 def description_safe(self):
1211 def description_safe(self):
1212 from rhodecode.lib import helpers as h
1212 from rhodecode.lib import helpers as h
1213 return h.escape(self.description)
1213 return h.escape(self.description)
1214
1214
1215 @property
1215 @property
1216 def expired(self):
1216 def expired(self):
1217 if self.expires == -1:
1217 if self.expires == -1:
1218 return False
1218 return False
1219 return time.time() > self.expires
1219 return time.time() > self.expires
1220
1220
1221 @classmethod
1221 @classmethod
1222 def _get_role_name(cls, role):
1222 def _get_role_name(cls, role):
1223 return {
1223 return {
1224 cls.ROLE_ALL: _('all'),
1224 cls.ROLE_ALL: _('all'),
1225 cls.ROLE_HTTP: _('http/web interface'),
1225 cls.ROLE_HTTP: _('http/web interface'),
1226 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1226 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1227 cls.ROLE_API: _('api calls'),
1227 cls.ROLE_API: _('api calls'),
1228 cls.ROLE_FEED: _('feed access'),
1228 cls.ROLE_FEED: _('feed access'),
1229 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1229 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1230 }.get(role, role)
1230 }.get(role, role)
1231
1231
1232 @classmethod
1232 @classmethod
1233 def _get_role_description(cls, role):
1233 def _get_role_description(cls, role):
1234 return {
1234 return {
1235 cls.ROLE_ALL: _('Token for all actions.'),
1235 cls.ROLE_ALL: _('Token for all actions.'),
1236 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1236 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1237 'login using `api_access_controllers_whitelist` functionality.'),
1237 'login using `api_access_controllers_whitelist` functionality.'),
1238 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1238 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1239 'Requires auth_token authentication plugin to be active. <br/>'
1239 'Requires auth_token authentication plugin to be active. <br/>'
1240 'Such Token should be used then instead of a password to '
1240 'Such Token should be used then instead of a password to '
1241 'interact with a repository, and additionally can be '
1241 'interact with a repository, and additionally can be '
1242 'limited to single repository using repo scope.'),
1242 'limited to single repository using repo scope.'),
1243 cls.ROLE_API: _('Token limited to api calls.'),
1243 cls.ROLE_API: _('Token limited to api calls.'),
1244 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1244 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1245 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1245 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1246 }.get(role, role)
1246 }.get(role, role)
1247
1247
1248 @property
1248 @property
1249 def role_humanized(self):
1249 def role_humanized(self):
1250 return self._get_role_name(self.role)
1250 return self._get_role_name(self.role)
1251
1251
1252 def _get_scope(self):
1252 def _get_scope(self):
1253 if self.repo:
1253 if self.repo:
1254 return 'Repository: {}'.format(self.repo.repo_name)
1254 return 'Repository: {}'.format(self.repo.repo_name)
1255 if self.repo_group:
1255 if self.repo_group:
1256 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1256 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1257 return 'Global'
1257 return 'Global'
1258
1258
1259 @property
1259 @property
1260 def scope_humanized(self):
1260 def scope_humanized(self):
1261 return self._get_scope()
1261 return self._get_scope()
1262
1262
1263 @property
1263 @property
1264 def token_obfuscated(self):
1264 def token_obfuscated(self):
1265 if self.api_key:
1265 if self.api_key:
1266 return self.api_key[:4] + "****"
1266 return self.api_key[:4] + "****"
1267
1267
1268
1268
1269 class UserEmailMap(Base, BaseModel):
1269 class UserEmailMap(Base, BaseModel):
1270 __tablename__ = 'user_email_map'
1270 __tablename__ = 'user_email_map'
1271 __table_args__ = (
1271 __table_args__ = (
1272 Index('uem_email_idx', 'email'),
1272 Index('uem_email_idx', 'email'),
1273 UniqueConstraint('email'),
1273 UniqueConstraint('email'),
1274 base_table_args
1274 base_table_args
1275 )
1275 )
1276 __mapper_args__ = {}
1276 __mapper_args__ = {}
1277
1277
1278 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1278 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1279 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1279 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1280 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1280 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1281 user = relationship('User', lazy='joined')
1281 user = relationship('User', lazy='joined')
1282
1282
1283 @validates('_email')
1283 @validates('_email')
1284 def validate_email(self, key, email):
1284 def validate_email(self, key, email):
1285 # check if this email is not main one
1285 # check if this email is not main one
1286 main_email = Session().query(User).filter(User.email == email).scalar()
1286 main_email = Session().query(User).filter(User.email == email).scalar()
1287 if main_email is not None:
1287 if main_email is not None:
1288 raise AttributeError('email %s is present is user table' % email)
1288 raise AttributeError('email %s is present is user table' % email)
1289 return email
1289 return email
1290
1290
1291 @hybrid_property
1291 @hybrid_property
1292 def email(self):
1292 def email(self):
1293 return self._email
1293 return self._email
1294
1294
1295 @email.setter
1295 @email.setter
1296 def email(self, val):
1296 def email(self, val):
1297 self._email = val.lower() if val else None
1297 self._email = val.lower() if val else None
1298
1298
1299
1299
1300 class UserIpMap(Base, BaseModel):
1300 class UserIpMap(Base, BaseModel):
1301 __tablename__ = 'user_ip_map'
1301 __tablename__ = 'user_ip_map'
1302 __table_args__ = (
1302 __table_args__ = (
1303 UniqueConstraint('user_id', 'ip_addr'),
1303 UniqueConstraint('user_id', 'ip_addr'),
1304 base_table_args
1304 base_table_args
1305 )
1305 )
1306 __mapper_args__ = {}
1306 __mapper_args__ = {}
1307
1307
1308 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1308 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1309 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1309 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1310 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1310 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1311 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1311 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1312 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1312 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1313 user = relationship('User', lazy='joined')
1313 user = relationship('User', lazy='joined')
1314
1314
1315 @hybrid_property
1315 @hybrid_property
1316 def description_safe(self):
1316 def description_safe(self):
1317 from rhodecode.lib import helpers as h
1317 from rhodecode.lib import helpers as h
1318 return h.escape(self.description)
1318 return h.escape(self.description)
1319
1319
1320 @classmethod
1320 @classmethod
1321 def _get_ip_range(cls, ip_addr):
1321 def _get_ip_range(cls, ip_addr):
1322 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1322 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1323 return [str(net.network_address), str(net.broadcast_address)]
1323 return [str(net.network_address), str(net.broadcast_address)]
1324
1324
1325 def __json__(self):
1325 def __json__(self):
1326 return {
1326 return {
1327 'ip_addr': self.ip_addr,
1327 'ip_addr': self.ip_addr,
1328 'ip_range': self._get_ip_range(self.ip_addr),
1328 'ip_range': self._get_ip_range(self.ip_addr),
1329 }
1329 }
1330
1330
1331 def __unicode__(self):
1331 def __unicode__(self):
1332 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1332 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1333 self.user_id, self.ip_addr)
1333 self.user_id, self.ip_addr)
1334
1334
1335
1335
1336 class UserSshKeys(Base, BaseModel):
1336 class UserSshKeys(Base, BaseModel):
1337 __tablename__ = 'user_ssh_keys'
1337 __tablename__ = 'user_ssh_keys'
1338 __table_args__ = (
1338 __table_args__ = (
1339 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1339 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1340
1340
1341 UniqueConstraint('ssh_key_fingerprint'),
1341 UniqueConstraint('ssh_key_fingerprint'),
1342
1342
1343 base_table_args
1343 base_table_args
1344 )
1344 )
1345 __mapper_args__ = {}
1345 __mapper_args__ = {}
1346
1346
1347 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1347 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1348 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1348 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1349 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1349 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1350
1350
1351 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1351 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1352
1352
1353 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1353 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1354 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1354 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1355 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1355 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1356
1356
1357 user = relationship('User', lazy='joined')
1357 user = relationship('User', lazy='joined')
1358
1358
1359 def __json__(self):
1359 def __json__(self):
1360 data = {
1360 data = {
1361 'ssh_fingerprint': self.ssh_key_fingerprint,
1361 'ssh_fingerprint': self.ssh_key_fingerprint,
1362 'description': self.description,
1362 'description': self.description,
1363 'created_on': self.created_on
1363 'created_on': self.created_on
1364 }
1364 }
1365 return data
1365 return data
1366
1366
1367 def get_api_data(self):
1367 def get_api_data(self):
1368 data = self.__json__()
1368 data = self.__json__()
1369 return data
1369 return data
1370
1370
1371
1371
1372 class UserLog(Base, BaseModel):
1372 class UserLog(Base, BaseModel):
1373 __tablename__ = 'user_logs'
1373 __tablename__ = 'user_logs'
1374 __table_args__ = (
1374 __table_args__ = (
1375 base_table_args,
1375 base_table_args,
1376 )
1376 )
1377
1377
1378 VERSION_1 = 'v1'
1378 VERSION_1 = 'v1'
1379 VERSION_2 = 'v2'
1379 VERSION_2 = 'v2'
1380 VERSIONS = [VERSION_1, VERSION_2]
1380 VERSIONS = [VERSION_1, VERSION_2]
1381
1381
1382 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1382 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1383 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1383 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1384 username = Column("username", String(255), nullable=True, unique=None, default=None)
1384 username = Column("username", String(255), nullable=True, unique=None, default=None)
1385 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1385 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1386 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1386 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1387 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1387 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1388 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1388 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1389 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1389 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1390
1390
1391 version = Column("version", String(255), nullable=True, default=VERSION_1)
1391 version = Column("version", String(255), nullable=True, default=VERSION_1)
1392 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1392 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1393 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1393 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1394
1394
1395 def __unicode__(self):
1395 def __unicode__(self):
1396 return u"<%s('id:%s:%s')>" % (
1396 return u"<%s('id:%s:%s')>" % (
1397 self.__class__.__name__, self.repository_name, self.action)
1397 self.__class__.__name__, self.repository_name, self.action)
1398
1398
1399 def __json__(self):
1399 def __json__(self):
1400 return {
1400 return {
1401 'user_id': self.user_id,
1401 'user_id': self.user_id,
1402 'username': self.username,
1402 'username': self.username,
1403 'repository_id': self.repository_id,
1403 'repository_id': self.repository_id,
1404 'repository_name': self.repository_name,
1404 'repository_name': self.repository_name,
1405 'user_ip': self.user_ip,
1405 'user_ip': self.user_ip,
1406 'action_date': self.action_date,
1406 'action_date': self.action_date,
1407 'action': self.action,
1407 'action': self.action,
1408 }
1408 }
1409
1409
1410 @hybrid_property
1410 @hybrid_property
1411 def entry_id(self):
1411 def entry_id(self):
1412 return self.user_log_id
1412 return self.user_log_id
1413
1413
1414 @property
1414 @property
1415 def action_as_day(self):
1415 def action_as_day(self):
1416 return datetime.date(*self.action_date.timetuple()[:3])
1416 return datetime.date(*self.action_date.timetuple()[:3])
1417
1417
1418 user = relationship('User')
1418 user = relationship('User')
1419 repository = relationship('Repository', cascade='')
1419 repository = relationship('Repository', cascade='')
1420
1420
1421
1421
1422 class UserGroup(Base, BaseModel):
1422 class UserGroup(Base, BaseModel):
1423 __tablename__ = 'users_groups'
1423 __tablename__ = 'users_groups'
1424 __table_args__ = (
1424 __table_args__ = (
1425 base_table_args,
1425 base_table_args,
1426 )
1426 )
1427
1427
1428 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1428 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1429 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1429 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1430 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1430 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1431 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1431 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1432 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1432 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1433 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1433 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1434 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1434 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1435 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1435 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1436
1436
1437 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1437 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1438 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1438 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1439 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1439 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1440 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1440 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1441 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1441 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1442 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1442 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1443
1443
1444 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1444 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1445 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1445 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1446
1446
1447 @classmethod
1447 @classmethod
1448 def _load_group_data(cls, column):
1448 def _load_group_data(cls, column):
1449 if not column:
1449 if not column:
1450 return {}
1450 return {}
1451
1451
1452 try:
1452 try:
1453 return json.loads(column) or {}
1453 return json.loads(column) or {}
1454 except TypeError:
1454 except TypeError:
1455 return {}
1455 return {}
1456
1456
1457 @hybrid_property
1457 @hybrid_property
1458 def description_safe(self):
1458 def description_safe(self):
1459 from rhodecode.lib import helpers as h
1459 from rhodecode.lib import helpers as h
1460 return h.escape(self.user_group_description)
1460 return h.escape(self.user_group_description)
1461
1461
1462 @hybrid_property
1462 @hybrid_property
1463 def group_data(self):
1463 def group_data(self):
1464 return self._load_group_data(self._group_data)
1464 return self._load_group_data(self._group_data)
1465
1465
1466 @group_data.expression
1466 @group_data.expression
1467 def group_data(self, **kwargs):
1467 def group_data(self, **kwargs):
1468 return self._group_data
1468 return self._group_data
1469
1469
1470 @group_data.setter
1470 @group_data.setter
1471 def group_data(self, val):
1471 def group_data(self, val):
1472 try:
1472 try:
1473 self._group_data = json.dumps(val)
1473 self._group_data = json.dumps(val)
1474 except Exception:
1474 except Exception:
1475 log.error(traceback.format_exc())
1475 log.error(traceback.format_exc())
1476
1476
1477 @classmethod
1477 @classmethod
1478 def _load_sync(cls, group_data):
1478 def _load_sync(cls, group_data):
1479 if group_data:
1479 if group_data:
1480 return group_data.get('extern_type')
1480 return group_data.get('extern_type')
1481
1481
1482 @property
1482 @property
1483 def sync(self):
1483 def sync(self):
1484 return self._load_sync(self.group_data)
1484 return self._load_sync(self.group_data)
1485
1485
1486 def __unicode__(self):
1486 def __unicode__(self):
1487 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1487 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1488 self.users_group_id,
1488 self.users_group_id,
1489 self.users_group_name)
1489 self.users_group_name)
1490
1490
1491 @classmethod
1491 @classmethod
1492 def get_by_group_name(cls, group_name, cache=False,
1492 def get_by_group_name(cls, group_name, cache=False,
1493 case_insensitive=False):
1493 case_insensitive=False):
1494 if case_insensitive:
1494 if case_insensitive:
1495 q = cls.query().filter(func.lower(cls.users_group_name) ==
1495 q = cls.query().filter(func.lower(cls.users_group_name) ==
1496 func.lower(group_name))
1496 func.lower(group_name))
1497
1497
1498 else:
1498 else:
1499 q = cls.query().filter(cls.users_group_name == group_name)
1499 q = cls.query().filter(cls.users_group_name == group_name)
1500 if cache:
1500 if cache:
1501 q = q.options(
1501 q = q.options(
1502 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1502 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1503 return q.scalar()
1503 return q.scalar()
1504
1504
1505 @classmethod
1505 @classmethod
1506 def get(cls, user_group_id, cache=False):
1506 def get(cls, user_group_id, cache=False):
1507 if not user_group_id:
1507 if not user_group_id:
1508 return
1508 return
1509
1509
1510 user_group = cls.query()
1510 user_group = cls.query()
1511 if cache:
1511 if cache:
1512 user_group = user_group.options(
1512 user_group = user_group.options(
1513 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1513 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1514 return user_group.get(user_group_id)
1514 return user_group.get(user_group_id)
1515
1515
1516 def permissions(self, with_admins=True, with_owner=True,
1516 def permissions(self, with_admins=True, with_owner=True,
1517 expand_from_user_groups=False):
1517 expand_from_user_groups=False):
1518 """
1518 """
1519 Permissions for user groups
1519 Permissions for user groups
1520 """
1520 """
1521 _admin_perm = 'usergroup.admin'
1521 _admin_perm = 'usergroup.admin'
1522
1522
1523 owner_row = []
1523 owner_row = []
1524 if with_owner:
1524 if with_owner:
1525 usr = AttributeDict(self.user.get_dict())
1525 usr = AttributeDict(self.user.get_dict())
1526 usr.owner_row = True
1526 usr.owner_row = True
1527 usr.permission = _admin_perm
1527 usr.permission = _admin_perm
1528 owner_row.append(usr)
1528 owner_row.append(usr)
1529
1529
1530 super_admin_ids = []
1530 super_admin_ids = []
1531 super_admin_rows = []
1531 super_admin_rows = []
1532 if with_admins:
1532 if with_admins:
1533 for usr in User.get_all_super_admins():
1533 for usr in User.get_all_super_admins():
1534 super_admin_ids.append(usr.user_id)
1534 super_admin_ids.append(usr.user_id)
1535 # if this admin is also owner, don't double the record
1535 # if this admin is also owner, don't double the record
1536 if usr.user_id == owner_row[0].user_id:
1536 if usr.user_id == owner_row[0].user_id:
1537 owner_row[0].admin_row = True
1537 owner_row[0].admin_row = True
1538 else:
1538 else:
1539 usr = AttributeDict(usr.get_dict())
1539 usr = AttributeDict(usr.get_dict())
1540 usr.admin_row = True
1540 usr.admin_row = True
1541 usr.permission = _admin_perm
1541 usr.permission = _admin_perm
1542 super_admin_rows.append(usr)
1542 super_admin_rows.append(usr)
1543
1543
1544 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1544 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1545 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1545 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1546 joinedload(UserUserGroupToPerm.user),
1546 joinedload(UserUserGroupToPerm.user),
1547 joinedload(UserUserGroupToPerm.permission),)
1547 joinedload(UserUserGroupToPerm.permission),)
1548
1548
1549 # get owners and admins and permissions. We do a trick of re-writing
1549 # get owners and admins and permissions. We do a trick of re-writing
1550 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1550 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1551 # has a global reference and changing one object propagates to all
1551 # has a global reference and changing one object propagates to all
1552 # others. This means if admin is also an owner admin_row that change
1552 # others. This means if admin is also an owner admin_row that change
1553 # would propagate to both objects
1553 # would propagate to both objects
1554 perm_rows = []
1554 perm_rows = []
1555 for _usr in q.all():
1555 for _usr in q.all():
1556 usr = AttributeDict(_usr.user.get_dict())
1556 usr = AttributeDict(_usr.user.get_dict())
1557 # if this user is also owner/admin, mark as duplicate record
1557 # if this user is also owner/admin, mark as duplicate record
1558 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1558 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1559 usr.duplicate_perm = True
1559 usr.duplicate_perm = True
1560 usr.permission = _usr.permission.permission_name
1560 usr.permission = _usr.permission.permission_name
1561 perm_rows.append(usr)
1561 perm_rows.append(usr)
1562
1562
1563 # filter the perm rows by 'default' first and then sort them by
1563 # filter the perm rows by 'default' first and then sort them by
1564 # admin,write,read,none permissions sorted again alphabetically in
1564 # admin,write,read,none permissions sorted again alphabetically in
1565 # each group
1565 # each group
1566 perm_rows = sorted(perm_rows, key=display_user_sort)
1566 perm_rows = sorted(perm_rows, key=display_user_sort)
1567
1567
1568 user_groups_rows = []
1568 user_groups_rows = []
1569 if expand_from_user_groups:
1569 if expand_from_user_groups:
1570 for ug in self.permission_user_groups(with_members=True):
1570 for ug in self.permission_user_groups(with_members=True):
1571 for user_data in ug.members:
1571 for user_data in ug.members:
1572 user_groups_rows.append(user_data)
1572 user_groups_rows.append(user_data)
1573
1573
1574 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1574 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1575
1575
1576 def permission_user_groups(self, with_members=False):
1576 def permission_user_groups(self, with_members=False):
1577 q = UserGroupUserGroupToPerm.query()\
1577 q = UserGroupUserGroupToPerm.query()\
1578 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1578 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1579 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1579 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1580 joinedload(UserGroupUserGroupToPerm.target_user_group),
1580 joinedload(UserGroupUserGroupToPerm.target_user_group),
1581 joinedload(UserGroupUserGroupToPerm.permission),)
1581 joinedload(UserGroupUserGroupToPerm.permission),)
1582
1582
1583 perm_rows = []
1583 perm_rows = []
1584 for _user_group in q.all():
1584 for _user_group in q.all():
1585 entry = AttributeDict(_user_group.user_group.get_dict())
1585 entry = AttributeDict(_user_group.user_group.get_dict())
1586 entry.permission = _user_group.permission.permission_name
1586 entry.permission = _user_group.permission.permission_name
1587 if with_members:
1587 if with_members:
1588 entry.members = [x.user.get_dict()
1588 entry.members = [x.user.get_dict()
1589 for x in _user_group.user_group.members]
1589 for x in _user_group.user_group.members]
1590 perm_rows.append(entry)
1590 perm_rows.append(entry)
1591
1591
1592 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1592 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1593 return perm_rows
1593 return perm_rows
1594
1594
1595 def _get_default_perms(self, user_group, suffix=''):
1595 def _get_default_perms(self, user_group, suffix=''):
1596 from rhodecode.model.permission import PermissionModel
1596 from rhodecode.model.permission import PermissionModel
1597 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1597 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1598
1598
1599 def get_default_perms(self, suffix=''):
1599 def get_default_perms(self, suffix=''):
1600 return self._get_default_perms(self, suffix)
1600 return self._get_default_perms(self, suffix)
1601
1601
1602 def get_api_data(self, with_group_members=True, include_secrets=False):
1602 def get_api_data(self, with_group_members=True, include_secrets=False):
1603 """
1603 """
1604 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1604 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1605 basically forwarded.
1605 basically forwarded.
1606
1606
1607 """
1607 """
1608 user_group = self
1608 user_group = self
1609 data = {
1609 data = {
1610 'users_group_id': user_group.users_group_id,
1610 'users_group_id': user_group.users_group_id,
1611 'group_name': user_group.users_group_name,
1611 'group_name': user_group.users_group_name,
1612 'group_description': user_group.user_group_description,
1612 'group_description': user_group.user_group_description,
1613 'active': user_group.users_group_active,
1613 'active': user_group.users_group_active,
1614 'owner': user_group.user.username,
1614 'owner': user_group.user.username,
1615 'sync': user_group.sync,
1615 'sync': user_group.sync,
1616 'owner_email': user_group.user.email,
1616 'owner_email': user_group.user.email,
1617 }
1617 }
1618
1618
1619 if with_group_members:
1619 if with_group_members:
1620 users = []
1620 users = []
1621 for user in user_group.members:
1621 for user in user_group.members:
1622 user = user.user
1622 user = user.user
1623 users.append(user.get_api_data(include_secrets=include_secrets))
1623 users.append(user.get_api_data(include_secrets=include_secrets))
1624 data['users'] = users
1624 data['users'] = users
1625
1625
1626 return data
1626 return data
1627
1627
1628
1628
1629 class UserGroupMember(Base, BaseModel):
1629 class UserGroupMember(Base, BaseModel):
1630 __tablename__ = 'users_groups_members'
1630 __tablename__ = 'users_groups_members'
1631 __table_args__ = (
1631 __table_args__ = (
1632 base_table_args,
1632 base_table_args,
1633 )
1633 )
1634
1634
1635 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1635 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1636 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1636 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1637 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1637 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1638
1638
1639 user = relationship('User', lazy='joined')
1639 user = relationship('User', lazy='joined')
1640 users_group = relationship('UserGroup')
1640 users_group = relationship('UserGroup')
1641
1641
1642 def __init__(self, gr_id='', u_id=''):
1642 def __init__(self, gr_id='', u_id=''):
1643 self.users_group_id = gr_id
1643 self.users_group_id = gr_id
1644 self.user_id = u_id
1644 self.user_id = u_id
1645
1645
1646
1646
1647 class RepositoryField(Base, BaseModel):
1647 class RepositoryField(Base, BaseModel):
1648 __tablename__ = 'repositories_fields'
1648 __tablename__ = 'repositories_fields'
1649 __table_args__ = (
1649 __table_args__ = (
1650 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1650 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1651 base_table_args,
1651 base_table_args,
1652 )
1652 )
1653
1653
1654 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1654 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1655
1655
1656 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1656 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1657 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1657 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1658 field_key = Column("field_key", String(250))
1658 field_key = Column("field_key", String(250))
1659 field_label = Column("field_label", String(1024), nullable=False)
1659 field_label = Column("field_label", String(1024), nullable=False)
1660 field_value = Column("field_value", String(10000), nullable=False)
1660 field_value = Column("field_value", String(10000), nullable=False)
1661 field_desc = Column("field_desc", String(1024), nullable=False)
1661 field_desc = Column("field_desc", String(1024), nullable=False)
1662 field_type = Column("field_type", String(255), nullable=False, unique=None)
1662 field_type = Column("field_type", String(255), nullable=False, unique=None)
1663 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1663 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1664
1664
1665 repository = relationship('Repository')
1665 repository = relationship('Repository')
1666
1666
1667 @property
1667 @property
1668 def field_key_prefixed(self):
1668 def field_key_prefixed(self):
1669 return 'ex_%s' % self.field_key
1669 return 'ex_%s' % self.field_key
1670
1670
1671 @classmethod
1671 @classmethod
1672 def un_prefix_key(cls, key):
1672 def un_prefix_key(cls, key):
1673 if key.startswith(cls.PREFIX):
1673 if key.startswith(cls.PREFIX):
1674 return key[len(cls.PREFIX):]
1674 return key[len(cls.PREFIX):]
1675 return key
1675 return key
1676
1676
1677 @classmethod
1677 @classmethod
1678 def get_by_key_name(cls, key, repo):
1678 def get_by_key_name(cls, key, repo):
1679 row = cls.query()\
1679 row = cls.query()\
1680 .filter(cls.repository == repo)\
1680 .filter(cls.repository == repo)\
1681 .filter(cls.field_key == key).scalar()
1681 .filter(cls.field_key == key).scalar()
1682 return row
1682 return row
1683
1683
1684
1684
1685 class Repository(Base, BaseModel):
1685 class Repository(Base, BaseModel):
1686 __tablename__ = 'repositories'
1686 __tablename__ = 'repositories'
1687 __table_args__ = (
1687 __table_args__ = (
1688 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1688 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1689 base_table_args,
1689 base_table_args,
1690 )
1690 )
1691 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1691 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1692 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1692 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1693 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1693 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1694
1694
1695 STATE_CREATED = 'repo_state_created'
1695 STATE_CREATED = 'repo_state_created'
1696 STATE_PENDING = 'repo_state_pending'
1696 STATE_PENDING = 'repo_state_pending'
1697 STATE_ERROR = 'repo_state_error'
1697 STATE_ERROR = 'repo_state_error'
1698
1698
1699 LOCK_AUTOMATIC = 'lock_auto'
1699 LOCK_AUTOMATIC = 'lock_auto'
1700 LOCK_API = 'lock_api'
1700 LOCK_API = 'lock_api'
1701 LOCK_WEB = 'lock_web'
1701 LOCK_WEB = 'lock_web'
1702 LOCK_PULL = 'lock_pull'
1702 LOCK_PULL = 'lock_pull'
1703
1703
1704 NAME_SEP = URL_SEP
1704 NAME_SEP = URL_SEP
1705
1705
1706 repo_id = Column(
1706 repo_id = Column(
1707 "repo_id", Integer(), nullable=False, unique=True, default=None,
1707 "repo_id", Integer(), nullable=False, unique=True, default=None,
1708 primary_key=True)
1708 primary_key=True)
1709 _repo_name = Column(
1709 _repo_name = Column(
1710 "repo_name", Text(), nullable=False, default=None)
1710 "repo_name", Text(), nullable=False, default=None)
1711 repo_name_hash = Column(
1711 repo_name_hash = Column(
1712 "repo_name_hash", String(255), nullable=False, unique=True)
1712 "repo_name_hash", String(255), nullable=False, unique=True)
1713 repo_state = Column("repo_state", String(255), nullable=True)
1713 repo_state = Column("repo_state", String(255), nullable=True)
1714
1714
1715 clone_uri = Column(
1715 clone_uri = Column(
1716 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1716 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1717 default=None)
1717 default=None)
1718 push_uri = Column(
1718 push_uri = Column(
1719 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1719 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1720 default=None)
1720 default=None)
1721 repo_type = Column(
1721 repo_type = Column(
1722 "repo_type", String(255), nullable=False, unique=False, default=None)
1722 "repo_type", String(255), nullable=False, unique=False, default=None)
1723 user_id = Column(
1723 user_id = Column(
1724 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1724 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1725 unique=False, default=None)
1725 unique=False, default=None)
1726 private = Column(
1726 private = Column(
1727 "private", Boolean(), nullable=True, unique=None, default=None)
1727 "private", Boolean(), nullable=True, unique=None, default=None)
1728 archived = Column(
1728 archived = Column(
1729 "archived", Boolean(), nullable=True, unique=None, default=None)
1729 "archived", Boolean(), nullable=True, unique=None, default=None)
1730 enable_statistics = Column(
1730 enable_statistics = Column(
1731 "statistics", Boolean(), nullable=True, unique=None, default=True)
1731 "statistics", Boolean(), nullable=True, unique=None, default=True)
1732 enable_downloads = Column(
1732 enable_downloads = Column(
1733 "downloads", Boolean(), nullable=True, unique=None, default=True)
1733 "downloads", Boolean(), nullable=True, unique=None, default=True)
1734 description = Column(
1734 description = Column(
1735 "description", String(10000), nullable=True, unique=None, default=None)
1735 "description", String(10000), nullable=True, unique=None, default=None)
1736 created_on = Column(
1736 created_on = Column(
1737 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1737 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1738 default=datetime.datetime.now)
1738 default=datetime.datetime.now)
1739 updated_on = Column(
1739 updated_on = Column(
1740 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1740 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1741 default=datetime.datetime.now)
1741 default=datetime.datetime.now)
1742 _landing_revision = Column(
1742 _landing_revision = Column(
1743 "landing_revision", String(255), nullable=False, unique=False,
1743 "landing_revision", String(255), nullable=False, unique=False,
1744 default=None)
1744 default=None)
1745 enable_locking = Column(
1745 enable_locking = Column(
1746 "enable_locking", Boolean(), nullable=False, unique=None,
1746 "enable_locking", Boolean(), nullable=False, unique=None,
1747 default=False)
1747 default=False)
1748 _locked = Column(
1748 _locked = Column(
1749 "locked", String(255), nullable=True, unique=False, default=None)
1749 "locked", String(255), nullable=True, unique=False, default=None)
1750 _changeset_cache = Column(
1750 _changeset_cache = Column(
1751 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1751 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1752
1752
1753 fork_id = Column(
1753 fork_id = Column(
1754 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1754 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1755 nullable=True, unique=False, default=None)
1755 nullable=True, unique=False, default=None)
1756 group_id = Column(
1756 group_id = Column(
1757 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1757 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1758 unique=False, default=None)
1758 unique=False, default=None)
1759
1759
1760 user = relationship('User', lazy='joined')
1760 user = relationship('User', lazy='joined')
1761 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1761 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1762 group = relationship('RepoGroup', lazy='joined')
1762 group = relationship('RepoGroup', lazy='joined')
1763 repo_to_perm = relationship(
1763 repo_to_perm = relationship(
1764 'UserRepoToPerm', cascade='all',
1764 'UserRepoToPerm', cascade='all',
1765 order_by='UserRepoToPerm.repo_to_perm_id')
1765 order_by='UserRepoToPerm.repo_to_perm_id')
1766 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1766 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1767 stats = relationship('Statistics', cascade='all', uselist=False)
1767 stats = relationship('Statistics', cascade='all', uselist=False)
1768
1768
1769 followers = relationship(
1769 followers = relationship(
1770 'UserFollowing',
1770 'UserFollowing',
1771 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1771 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1772 cascade='all')
1772 cascade='all')
1773 extra_fields = relationship(
1773 extra_fields = relationship(
1774 'RepositoryField', cascade="all, delete-orphan")
1774 'RepositoryField', cascade="all, delete-orphan")
1775 logs = relationship('UserLog')
1775 logs = relationship('UserLog')
1776 comments = relationship(
1776 comments = relationship(
1777 'ChangesetComment', cascade="all, delete-orphan")
1777 'ChangesetComment', cascade="all, delete-orphan")
1778 pull_requests_source = relationship(
1778 pull_requests_source = relationship(
1779 'PullRequest',
1779 'PullRequest',
1780 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1780 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1781 cascade="all, delete-orphan")
1781 cascade="all, delete-orphan")
1782 pull_requests_target = relationship(
1782 pull_requests_target = relationship(
1783 'PullRequest',
1783 'PullRequest',
1784 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1784 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1785 cascade="all, delete-orphan")
1785 cascade="all, delete-orphan")
1786 ui = relationship('RepoRhodeCodeUi', cascade="all")
1786 ui = relationship('RepoRhodeCodeUi', cascade="all")
1787 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1787 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1788 integrations = relationship('Integration', cascade="all, delete-orphan")
1788 integrations = relationship('Integration', cascade="all, delete-orphan")
1789
1789
1790 scoped_tokens = relationship('UserApiKeys', cascade="all")
1790 scoped_tokens = relationship('UserApiKeys', cascade="all")
1791
1791
1792 # no cascade, set NULL
1792 # no cascade, set NULL
1793 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1793 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1794
1794
1795 def __unicode__(self):
1795 def __unicode__(self):
1796 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1796 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1797 safe_unicode(self.repo_name))
1797 safe_unicode(self.repo_name))
1798
1798
1799 @hybrid_property
1799 @hybrid_property
1800 def description_safe(self):
1800 def description_safe(self):
1801 from rhodecode.lib import helpers as h
1801 from rhodecode.lib import helpers as h
1802 return h.escape(self.description)
1802 return h.escape(self.description)
1803
1803
1804 @hybrid_property
1804 @hybrid_property
1805 def landing_rev(self):
1805 def landing_rev(self):
1806 # always should return [rev_type, rev], e.g ['branch', 'master']
1806 # always should return [rev_type, rev], e.g ['branch', 'master']
1807 if self._landing_revision:
1807 if self._landing_revision:
1808 _rev_info = self._landing_revision.split(':')
1808 _rev_info = self._landing_revision.split(':')
1809 if len(_rev_info) < 2:
1809 if len(_rev_info) < 2:
1810 _rev_info.insert(0, 'rev')
1810 _rev_info.insert(0, 'rev')
1811 return [_rev_info[0], _rev_info[1]]
1811 return [_rev_info[0], _rev_info[1]]
1812 return [None, None]
1812 return [None, None]
1813
1813
1814 @property
1814 @property
1815 def landing_ref_type(self):
1815 def landing_ref_type(self):
1816 return self.landing_rev[0]
1816 return self.landing_rev[0]
1817
1817
1818 @property
1818 @property
1819 def landing_ref_name(self):
1819 def landing_ref_name(self):
1820 return self.landing_rev[1]
1820 return self.landing_rev[1]
1821
1821
1822 @landing_rev.setter
1822 @landing_rev.setter
1823 def landing_rev(self, val):
1823 def landing_rev(self, val):
1824 if ':' not in val:
1824 if ':' not in val:
1825 raise ValueError('value must be delimited with `:` and consist '
1825 raise ValueError('value must be delimited with `:` and consist '
1826 'of <rev_type>:<rev>, got %s instead' % val)
1826 'of <rev_type>:<rev>, got %s instead' % val)
1827 self._landing_revision = val
1827 self._landing_revision = val
1828
1828
1829 @hybrid_property
1829 @hybrid_property
1830 def locked(self):
1830 def locked(self):
1831 if self._locked:
1831 if self._locked:
1832 user_id, timelocked, reason = self._locked.split(':')
1832 user_id, timelocked, reason = self._locked.split(':')
1833 lock_values = int(user_id), timelocked, reason
1833 lock_values = int(user_id), timelocked, reason
1834 else:
1834 else:
1835 lock_values = [None, None, None]
1835 lock_values = [None, None, None]
1836 return lock_values
1836 return lock_values
1837
1837
1838 @locked.setter
1838 @locked.setter
1839 def locked(self, val):
1839 def locked(self, val):
1840 if val and isinstance(val, (list, tuple)):
1840 if val and isinstance(val, (list, tuple)):
1841 self._locked = ':'.join(map(str, val))
1841 self._locked = ':'.join(map(str, val))
1842 else:
1842 else:
1843 self._locked = None
1843 self._locked = None
1844
1844
1845 @classmethod
1845 @classmethod
1846 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1846 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1847 from rhodecode.lib.vcs.backends.base import EmptyCommit
1847 from rhodecode.lib.vcs.backends.base import EmptyCommit
1848 dummy = EmptyCommit().__json__()
1848 dummy = EmptyCommit().__json__()
1849 if not changeset_cache_raw:
1849 if not changeset_cache_raw:
1850 dummy['source_repo_id'] = repo_id
1850 dummy['source_repo_id'] = repo_id
1851 return json.loads(json.dumps(dummy))
1851 return json.loads(json.dumps(dummy))
1852
1852
1853 try:
1853 try:
1854 return json.loads(changeset_cache_raw)
1854 return json.loads(changeset_cache_raw)
1855 except TypeError:
1855 except TypeError:
1856 return dummy
1856 return dummy
1857 except Exception:
1857 except Exception:
1858 log.error(traceback.format_exc())
1858 log.error(traceback.format_exc())
1859 return dummy
1859 return dummy
1860
1860
1861 @hybrid_property
1861 @hybrid_property
1862 def changeset_cache(self):
1862 def changeset_cache(self):
1863 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1863 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1864
1864
1865 @changeset_cache.setter
1865 @changeset_cache.setter
1866 def changeset_cache(self, val):
1866 def changeset_cache(self, val):
1867 try:
1867 try:
1868 self._changeset_cache = json.dumps(val)
1868 self._changeset_cache = json.dumps(val)
1869 except Exception:
1869 except Exception:
1870 log.error(traceback.format_exc())
1870 log.error(traceback.format_exc())
1871
1871
1872 @hybrid_property
1872 @hybrid_property
1873 def repo_name(self):
1873 def repo_name(self):
1874 return self._repo_name
1874 return self._repo_name
1875
1875
1876 @repo_name.setter
1876 @repo_name.setter
1877 def repo_name(self, value):
1877 def repo_name(self, value):
1878 self._repo_name = value
1878 self._repo_name = value
1879 self.repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1879 self.repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1880
1880
1881 @classmethod
1881 @classmethod
1882 def normalize_repo_name(cls, repo_name):
1882 def normalize_repo_name(cls, repo_name):
1883 """
1883 """
1884 Normalizes os specific repo_name to the format internally stored inside
1884 Normalizes os specific repo_name to the format internally stored inside
1885 database using URL_SEP
1885 database using URL_SEP
1886
1886
1887 :param cls:
1887 :param cls:
1888 :param repo_name:
1888 :param repo_name:
1889 """
1889 """
1890 return cls.NAME_SEP.join(repo_name.split(os.sep))
1890 return cls.NAME_SEP.join(repo_name.split(os.sep))
1891
1891
1892 @classmethod
1892 @classmethod
1893 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1893 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1894 session = Session()
1894 session = Session()
1895 q = session.query(cls).filter(cls.repo_name == repo_name)
1895 q = session.query(cls).filter(cls.repo_name == repo_name)
1896
1896
1897 if cache:
1897 if cache:
1898 if identity_cache:
1898 if identity_cache:
1899 val = cls.identity_cache(session, 'repo_name', repo_name)
1899 val = cls.identity_cache(session, 'repo_name', repo_name)
1900 if val:
1900 if val:
1901 return val
1901 return val
1902 else:
1902 else:
1903 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1903 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1904 q = q.options(
1904 q = q.options(
1905 FromCache("sql_cache_short", cache_key))
1905 FromCache("sql_cache_short", cache_key))
1906
1906
1907 return q.scalar()
1907 return q.scalar()
1908
1908
1909 @classmethod
1909 @classmethod
1910 def get_by_id_or_repo_name(cls, repoid):
1910 def get_by_id_or_repo_name(cls, repoid):
1911 if isinstance(repoid, (int, long)):
1911 if isinstance(repoid, (int, long)):
1912 try:
1912 try:
1913 repo = cls.get(repoid)
1913 repo = cls.get(repoid)
1914 except ValueError:
1914 except ValueError:
1915 repo = None
1915 repo = None
1916 else:
1916 else:
1917 repo = cls.get_by_repo_name(repoid)
1917 repo = cls.get_by_repo_name(repoid)
1918 return repo
1918 return repo
1919
1919
1920 @classmethod
1920 @classmethod
1921 def get_by_full_path(cls, repo_full_path):
1921 def get_by_full_path(cls, repo_full_path):
1922 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1922 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1923 repo_name = cls.normalize_repo_name(repo_name)
1923 repo_name = cls.normalize_repo_name(repo_name)
1924 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1924 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1925
1925
1926 @classmethod
1926 @classmethod
1927 def get_repo_forks(cls, repo_id):
1927 def get_repo_forks(cls, repo_id):
1928 return cls.query().filter(Repository.fork_id == repo_id)
1928 return cls.query().filter(Repository.fork_id == repo_id)
1929
1929
1930 @classmethod
1930 @classmethod
1931 def base_path(cls):
1931 def base_path(cls):
1932 """
1932 """
1933 Returns base path when all repos are stored
1933 Returns base path when all repos are stored
1934
1934
1935 :param cls:
1935 :param cls:
1936 """
1936 """
1937 q = Session().query(RhodeCodeUi)\
1937 q = Session().query(RhodeCodeUi)\
1938 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1938 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1939 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1939 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1940 return q.one().ui_value
1940 return q.one().ui_value
1941
1941
1942 @classmethod
1942 @classmethod
1943 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1943 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1944 case_insensitive=True, archived=False):
1944 case_insensitive=True, archived=False):
1945 q = Repository.query()
1945 q = Repository.query()
1946
1946
1947 if not archived:
1947 if not archived:
1948 q = q.filter(Repository.archived.isnot(true()))
1948 q = q.filter(Repository.archived.isnot(true()))
1949
1949
1950 if not isinstance(user_id, Optional):
1950 if not isinstance(user_id, Optional):
1951 q = q.filter(Repository.user_id == user_id)
1951 q = q.filter(Repository.user_id == user_id)
1952
1952
1953 if not isinstance(group_id, Optional):
1953 if not isinstance(group_id, Optional):
1954 q = q.filter(Repository.group_id == group_id)
1954 q = q.filter(Repository.group_id == group_id)
1955
1955
1956 if case_insensitive:
1956 if case_insensitive:
1957 q = q.order_by(func.lower(Repository.repo_name))
1957 q = q.order_by(func.lower(Repository.repo_name))
1958 else:
1958 else:
1959 q = q.order_by(Repository.repo_name)
1959 q = q.order_by(Repository.repo_name)
1960
1960
1961 return q.all()
1961 return q.all()
1962
1962
1963 @property
1963 @property
1964 def repo_uid(self):
1964 def repo_uid(self):
1965 return '_{}'.format(self.repo_id)
1965 return '_{}'.format(self.repo_id)
1966
1966
1967 @property
1967 @property
1968 def forks(self):
1968 def forks(self):
1969 """
1969 """
1970 Return forks of this repo
1970 Return forks of this repo
1971 """
1971 """
1972 return Repository.get_repo_forks(self.repo_id)
1972 return Repository.get_repo_forks(self.repo_id)
1973
1973
1974 @property
1974 @property
1975 def parent(self):
1975 def parent(self):
1976 """
1976 """
1977 Returns fork parent
1977 Returns fork parent
1978 """
1978 """
1979 return self.fork
1979 return self.fork
1980
1980
1981 @property
1981 @property
1982 def just_name(self):
1982 def just_name(self):
1983 return self.repo_name.split(self.NAME_SEP)[-1]
1983 return self.repo_name.split(self.NAME_SEP)[-1]
1984
1984
1985 @property
1985 @property
1986 def groups_with_parents(self):
1986 def groups_with_parents(self):
1987 groups = []
1987 groups = []
1988 if self.group is None:
1988 if self.group is None:
1989 return groups
1989 return groups
1990
1990
1991 cur_gr = self.group
1991 cur_gr = self.group
1992 groups.insert(0, cur_gr)
1992 groups.insert(0, cur_gr)
1993 while 1:
1993 while 1:
1994 gr = getattr(cur_gr, 'parent_group', None)
1994 gr = getattr(cur_gr, 'parent_group', None)
1995 cur_gr = cur_gr.parent_group
1995 cur_gr = cur_gr.parent_group
1996 if gr is None:
1996 if gr is None:
1997 break
1997 break
1998 groups.insert(0, gr)
1998 groups.insert(0, gr)
1999
1999
2000 return groups
2000 return groups
2001
2001
2002 @property
2002 @property
2003 def groups_and_repo(self):
2003 def groups_and_repo(self):
2004 return self.groups_with_parents, self
2004 return self.groups_with_parents, self
2005
2005
2006 @LazyProperty
2006 @LazyProperty
2007 def repo_path(self):
2007 def repo_path(self):
2008 """
2008 """
2009 Returns base full path for that repository means where it actually
2009 Returns base full path for that repository means where it actually
2010 exists on a filesystem
2010 exists on a filesystem
2011 """
2011 """
2012 q = Session().query(RhodeCodeUi).filter(
2012 q = Session().query(RhodeCodeUi).filter(
2013 RhodeCodeUi.ui_key == self.NAME_SEP)
2013 RhodeCodeUi.ui_key == self.NAME_SEP)
2014 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2014 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2015 return q.one().ui_value
2015 return q.one().ui_value
2016
2016
2017 @property
2017 @property
2018 def repo_full_path(self):
2018 def repo_full_path(self):
2019 p = [self.repo_path]
2019 p = [self.repo_path]
2020 # we need to split the name by / since this is how we store the
2020 # we need to split the name by / since this is how we store the
2021 # names in the database, but that eventually needs to be converted
2021 # names in the database, but that eventually needs to be converted
2022 # into a valid system path
2022 # into a valid system path
2023 p += self.repo_name.split(self.NAME_SEP)
2023 p += self.repo_name.split(self.NAME_SEP)
2024 return os.path.join(*map(safe_unicode, p))
2024 return os.path.join(*map(safe_unicode, p))
2025
2025
2026 @property
2026 @property
2027 def cache_keys(self):
2027 def cache_keys(self):
2028 """
2028 """
2029 Returns associated cache keys for that repo
2029 Returns associated cache keys for that repo
2030 """
2030 """
2031 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2031 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2032 repo_id=self.repo_id)
2032 repo_id=self.repo_id)
2033 return CacheKey.query()\
2033 return CacheKey.query()\
2034 .filter(CacheKey.cache_args == invalidation_namespace)\
2034 .filter(CacheKey.cache_args == invalidation_namespace)\
2035 .order_by(CacheKey.cache_key)\
2035 .order_by(CacheKey.cache_key)\
2036 .all()
2036 .all()
2037
2037
2038 @property
2038 @property
2039 def cached_diffs_relative_dir(self):
2039 def cached_diffs_relative_dir(self):
2040 """
2040 """
2041 Return a relative to the repository store path of cached diffs
2041 Return a relative to the repository store path of cached diffs
2042 used for safe display for users, who shouldn't know the absolute store
2042 used for safe display for users, who shouldn't know the absolute store
2043 path
2043 path
2044 """
2044 """
2045 return os.path.join(
2045 return os.path.join(
2046 os.path.dirname(self.repo_name),
2046 os.path.dirname(self.repo_name),
2047 self.cached_diffs_dir.split(os.path.sep)[-1])
2047 self.cached_diffs_dir.split(os.path.sep)[-1])
2048
2048
2049 @property
2049 @property
2050 def cached_diffs_dir(self):
2050 def cached_diffs_dir(self):
2051 path = self.repo_full_path
2051 path = self.repo_full_path
2052 return os.path.join(
2052 return os.path.join(
2053 os.path.dirname(path),
2053 os.path.dirname(path),
2054 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
2054 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
2055
2055
2056 def cached_diffs(self):
2056 def cached_diffs(self):
2057 diff_cache_dir = self.cached_diffs_dir
2057 diff_cache_dir = self.cached_diffs_dir
2058 if os.path.isdir(diff_cache_dir):
2058 if os.path.isdir(diff_cache_dir):
2059 return os.listdir(diff_cache_dir)
2059 return os.listdir(diff_cache_dir)
2060 return []
2060 return []
2061
2061
2062 def shadow_repos(self):
2062 def shadow_repos(self):
2063 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
2063 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
2064 return [
2064 return [
2065 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2065 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2066 if x.startswith(shadow_repos_pattern)]
2066 if x.startswith(shadow_repos_pattern)]
2067
2067
2068 def get_new_name(self, repo_name):
2068 def get_new_name(self, repo_name):
2069 """
2069 """
2070 returns new full repository name based on assigned group and new new
2070 returns new full repository name based on assigned group and new new
2071
2071
2072 :param group_name:
2072 :param group_name:
2073 """
2073 """
2074 path_prefix = self.group.full_path_splitted if self.group else []
2074 path_prefix = self.group.full_path_splitted if self.group else []
2075 return self.NAME_SEP.join(path_prefix + [repo_name])
2075 return self.NAME_SEP.join(path_prefix + [repo_name])
2076
2076
2077 @property
2077 @property
2078 def _config(self):
2078 def _config(self):
2079 """
2079 """
2080 Returns db based config object.
2080 Returns db based config object.
2081 """
2081 """
2082 from rhodecode.lib.utils import make_db_config
2082 from rhodecode.lib.utils import make_db_config
2083 return make_db_config(clear_session=False, repo=self)
2083 return make_db_config(clear_session=False, repo=self)
2084
2084
2085 def permissions(self, with_admins=True, with_owner=True,
2085 def permissions(self, with_admins=True, with_owner=True,
2086 expand_from_user_groups=False):
2086 expand_from_user_groups=False):
2087 """
2087 """
2088 Permissions for repositories
2088 Permissions for repositories
2089 """
2089 """
2090 _admin_perm = 'repository.admin'
2090 _admin_perm = 'repository.admin'
2091
2091
2092 owner_row = []
2092 owner_row = []
2093 if with_owner:
2093 if with_owner:
2094 usr = AttributeDict(self.user.get_dict())
2094 usr = AttributeDict(self.user.get_dict())
2095 usr.owner_row = True
2095 usr.owner_row = True
2096 usr.permission = _admin_perm
2096 usr.permission = _admin_perm
2097 usr.permission_id = None
2097 usr.permission_id = None
2098 owner_row.append(usr)
2098 owner_row.append(usr)
2099
2099
2100 super_admin_ids = []
2100 super_admin_ids = []
2101 super_admin_rows = []
2101 super_admin_rows = []
2102 if with_admins:
2102 if with_admins:
2103 for usr in User.get_all_super_admins():
2103 for usr in User.get_all_super_admins():
2104 super_admin_ids.append(usr.user_id)
2104 super_admin_ids.append(usr.user_id)
2105 # if this admin is also owner, don't double the record
2105 # if this admin is also owner, don't double the record
2106 if usr.user_id == owner_row[0].user_id:
2106 if usr.user_id == owner_row[0].user_id:
2107 owner_row[0].admin_row = True
2107 owner_row[0].admin_row = True
2108 else:
2108 else:
2109 usr = AttributeDict(usr.get_dict())
2109 usr = AttributeDict(usr.get_dict())
2110 usr.admin_row = True
2110 usr.admin_row = True
2111 usr.permission = _admin_perm
2111 usr.permission = _admin_perm
2112 usr.permission_id = None
2112 usr.permission_id = None
2113 super_admin_rows.append(usr)
2113 super_admin_rows.append(usr)
2114
2114
2115 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2115 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2116 q = q.options(joinedload(UserRepoToPerm.repository),
2116 q = q.options(joinedload(UserRepoToPerm.repository),
2117 joinedload(UserRepoToPerm.user),
2117 joinedload(UserRepoToPerm.user),
2118 joinedload(UserRepoToPerm.permission),)
2118 joinedload(UserRepoToPerm.permission),)
2119
2119
2120 # get owners and admins and permissions. We do a trick of re-writing
2120 # get owners and admins and permissions. We do a trick of re-writing
2121 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2121 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2122 # has a global reference and changing one object propagates to all
2122 # has a global reference and changing one object propagates to all
2123 # others. This means if admin is also an owner admin_row that change
2123 # others. This means if admin is also an owner admin_row that change
2124 # would propagate to both objects
2124 # would propagate to both objects
2125 perm_rows = []
2125 perm_rows = []
2126 for _usr in q.all():
2126 for _usr in q.all():
2127 usr = AttributeDict(_usr.user.get_dict())
2127 usr = AttributeDict(_usr.user.get_dict())
2128 # if this user is also owner/admin, mark as duplicate record
2128 # if this user is also owner/admin, mark as duplicate record
2129 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2129 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2130 usr.duplicate_perm = True
2130 usr.duplicate_perm = True
2131 # also check if this permission is maybe used by branch_permissions
2131 # also check if this permission is maybe used by branch_permissions
2132 if _usr.branch_perm_entry:
2132 if _usr.branch_perm_entry:
2133 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2133 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2134
2134
2135 usr.permission = _usr.permission.permission_name
2135 usr.permission = _usr.permission.permission_name
2136 usr.permission_id = _usr.repo_to_perm_id
2136 usr.permission_id = _usr.repo_to_perm_id
2137 perm_rows.append(usr)
2137 perm_rows.append(usr)
2138
2138
2139 # filter the perm rows by 'default' first and then sort them by
2139 # filter the perm rows by 'default' first and then sort them by
2140 # admin,write,read,none permissions sorted again alphabetically in
2140 # admin,write,read,none permissions sorted again alphabetically in
2141 # each group
2141 # each group
2142 perm_rows = sorted(perm_rows, key=display_user_sort)
2142 perm_rows = sorted(perm_rows, key=display_user_sort)
2143
2143
2144 user_groups_rows = []
2144 user_groups_rows = []
2145 if expand_from_user_groups:
2145 if expand_from_user_groups:
2146 for ug in self.permission_user_groups(with_members=True):
2146 for ug in self.permission_user_groups(with_members=True):
2147 for user_data in ug.members:
2147 for user_data in ug.members:
2148 user_groups_rows.append(user_data)
2148 user_groups_rows.append(user_data)
2149
2149
2150 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2150 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2151
2151
2152 def permission_user_groups(self, with_members=True):
2152 def permission_user_groups(self, with_members=True):
2153 q = UserGroupRepoToPerm.query()\
2153 q = UserGroupRepoToPerm.query()\
2154 .filter(UserGroupRepoToPerm.repository == self)
2154 .filter(UserGroupRepoToPerm.repository == self)
2155 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2155 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2156 joinedload(UserGroupRepoToPerm.users_group),
2156 joinedload(UserGroupRepoToPerm.users_group),
2157 joinedload(UserGroupRepoToPerm.permission),)
2157 joinedload(UserGroupRepoToPerm.permission),)
2158
2158
2159 perm_rows = []
2159 perm_rows = []
2160 for _user_group in q.all():
2160 for _user_group in q.all():
2161 entry = AttributeDict(_user_group.users_group.get_dict())
2161 entry = AttributeDict(_user_group.users_group.get_dict())
2162 entry.permission = _user_group.permission.permission_name
2162 entry.permission = _user_group.permission.permission_name
2163 if with_members:
2163 if with_members:
2164 entry.members = [x.user.get_dict()
2164 entry.members = [x.user.get_dict()
2165 for x in _user_group.users_group.members]
2165 for x in _user_group.users_group.members]
2166 perm_rows.append(entry)
2166 perm_rows.append(entry)
2167
2167
2168 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2168 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2169 return perm_rows
2169 return perm_rows
2170
2170
2171 def get_api_data(self, include_secrets=False):
2171 def get_api_data(self, include_secrets=False):
2172 """
2172 """
2173 Common function for generating repo api data
2173 Common function for generating repo api data
2174
2174
2175 :param include_secrets: See :meth:`User.get_api_data`.
2175 :param include_secrets: See :meth:`User.get_api_data`.
2176
2176
2177 """
2177 """
2178 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2178 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2179 # move this methods on models level.
2179 # move this methods on models level.
2180 from rhodecode.model.settings import SettingsModel
2180 from rhodecode.model.settings import SettingsModel
2181 from rhodecode.model.repo import RepoModel
2181 from rhodecode.model.repo import RepoModel
2182
2182
2183 repo = self
2183 repo = self
2184 _user_id, _time, _reason = self.locked
2184 _user_id, _time, _reason = self.locked
2185
2185
2186 data = {
2186 data = {
2187 'repo_id': repo.repo_id,
2187 'repo_id': repo.repo_id,
2188 'repo_name': repo.repo_name,
2188 'repo_name': repo.repo_name,
2189 'repo_type': repo.repo_type,
2189 'repo_type': repo.repo_type,
2190 'clone_uri': repo.clone_uri or '',
2190 'clone_uri': repo.clone_uri or '',
2191 'push_uri': repo.push_uri or '',
2191 'push_uri': repo.push_uri or '',
2192 'url': RepoModel().get_url(self),
2192 'url': RepoModel().get_url(self),
2193 'private': repo.private,
2193 'private': repo.private,
2194 'created_on': repo.created_on,
2194 'created_on': repo.created_on,
2195 'description': repo.description_safe,
2195 'description': repo.description_safe,
2196 'landing_rev': repo.landing_rev,
2196 'landing_rev': repo.landing_rev,
2197 'owner': repo.user.username,
2197 'owner': repo.user.username,
2198 'fork_of': repo.fork.repo_name if repo.fork else None,
2198 'fork_of': repo.fork.repo_name if repo.fork else None,
2199 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2199 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2200 'enable_statistics': repo.enable_statistics,
2200 'enable_statistics': repo.enable_statistics,
2201 'enable_locking': repo.enable_locking,
2201 'enable_locking': repo.enable_locking,
2202 'enable_downloads': repo.enable_downloads,
2202 'enable_downloads': repo.enable_downloads,
2203 'last_changeset': repo.changeset_cache,
2203 'last_changeset': repo.changeset_cache,
2204 'locked_by': User.get(_user_id).get_api_data(
2204 'locked_by': User.get(_user_id).get_api_data(
2205 include_secrets=include_secrets) if _user_id else None,
2205 include_secrets=include_secrets) if _user_id else None,
2206 'locked_date': time_to_datetime(_time) if _time else None,
2206 'locked_date': time_to_datetime(_time) if _time else None,
2207 'lock_reason': _reason if _reason else None,
2207 'lock_reason': _reason if _reason else None,
2208 }
2208 }
2209
2209
2210 # TODO: mikhail: should be per-repo settings here
2210 # TODO: mikhail: should be per-repo settings here
2211 rc_config = SettingsModel().get_all_settings()
2211 rc_config = SettingsModel().get_all_settings()
2212 repository_fields = str2bool(
2212 repository_fields = str2bool(
2213 rc_config.get('rhodecode_repository_fields'))
2213 rc_config.get('rhodecode_repository_fields'))
2214 if repository_fields:
2214 if repository_fields:
2215 for f in self.extra_fields:
2215 for f in self.extra_fields:
2216 data[f.field_key_prefixed] = f.field_value
2216 data[f.field_key_prefixed] = f.field_value
2217
2217
2218 return data
2218 return data
2219
2219
2220 @classmethod
2220 @classmethod
2221 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2221 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2222 if not lock_time:
2222 if not lock_time:
2223 lock_time = time.time()
2223 lock_time = time.time()
2224 if not lock_reason:
2224 if not lock_reason:
2225 lock_reason = cls.LOCK_AUTOMATIC
2225 lock_reason = cls.LOCK_AUTOMATIC
2226 repo.locked = [user_id, lock_time, lock_reason]
2226 repo.locked = [user_id, lock_time, lock_reason]
2227 Session().add(repo)
2227 Session().add(repo)
2228 Session().commit()
2228 Session().commit()
2229
2229
2230 @classmethod
2230 @classmethod
2231 def unlock(cls, repo):
2231 def unlock(cls, repo):
2232 repo.locked = None
2232 repo.locked = None
2233 Session().add(repo)
2233 Session().add(repo)
2234 Session().commit()
2234 Session().commit()
2235
2235
2236 @classmethod
2236 @classmethod
2237 def getlock(cls, repo):
2237 def getlock(cls, repo):
2238 return repo.locked
2238 return repo.locked
2239
2239
2240 def is_user_lock(self, user_id):
2240 def is_user_lock(self, user_id):
2241 if self.lock[0]:
2241 if self.lock[0]:
2242 lock_user_id = safe_int(self.lock[0])
2242 lock_user_id = safe_int(self.lock[0])
2243 user_id = safe_int(user_id)
2243 user_id = safe_int(user_id)
2244 # both are ints, and they are equal
2244 # both are ints, and they are equal
2245 return all([lock_user_id, user_id]) and lock_user_id == user_id
2245 return all([lock_user_id, user_id]) and lock_user_id == user_id
2246
2246
2247 return False
2247 return False
2248
2248
2249 def get_locking_state(self, action, user_id, only_when_enabled=True):
2249 def get_locking_state(self, action, user_id, only_when_enabled=True):
2250 """
2250 """
2251 Checks locking on this repository, if locking is enabled and lock is
2251 Checks locking on this repository, if locking is enabled and lock is
2252 present returns a tuple of make_lock, locked, locked_by.
2252 present returns a tuple of make_lock, locked, locked_by.
2253 make_lock can have 3 states None (do nothing) True, make lock
2253 make_lock can have 3 states None (do nothing) True, make lock
2254 False release lock, This value is later propagated to hooks, which
2254 False release lock, This value is later propagated to hooks, which
2255 do the locking. Think about this as signals passed to hooks what to do.
2255 do the locking. Think about this as signals passed to hooks what to do.
2256
2256
2257 """
2257 """
2258 # TODO: johbo: This is part of the business logic and should be moved
2258 # TODO: johbo: This is part of the business logic and should be moved
2259 # into the RepositoryModel.
2259 # into the RepositoryModel.
2260
2260
2261 if action not in ('push', 'pull'):
2261 if action not in ('push', 'pull'):
2262 raise ValueError("Invalid action value: %s" % repr(action))
2262 raise ValueError("Invalid action value: %s" % repr(action))
2263
2263
2264 # defines if locked error should be thrown to user
2264 # defines if locked error should be thrown to user
2265 currently_locked = False
2265 currently_locked = False
2266 # defines if new lock should be made, tri-state
2266 # defines if new lock should be made, tri-state
2267 make_lock = None
2267 make_lock = None
2268 repo = self
2268 repo = self
2269 user = User.get(user_id)
2269 user = User.get(user_id)
2270
2270
2271 lock_info = repo.locked
2271 lock_info = repo.locked
2272
2272
2273 if repo and (repo.enable_locking or not only_when_enabled):
2273 if repo and (repo.enable_locking or not only_when_enabled):
2274 if action == 'push':
2274 if action == 'push':
2275 # check if it's already locked !, if it is compare users
2275 # check if it's already locked !, if it is compare users
2276 locked_by_user_id = lock_info[0]
2276 locked_by_user_id = lock_info[0]
2277 if user.user_id == locked_by_user_id:
2277 if user.user_id == locked_by_user_id:
2278 log.debug(
2278 log.debug(
2279 'Got `push` action from user %s, now unlocking', user)
2279 'Got `push` action from user %s, now unlocking', user)
2280 # unlock if we have push from user who locked
2280 # unlock if we have push from user who locked
2281 make_lock = False
2281 make_lock = False
2282 else:
2282 else:
2283 # we're not the same user who locked, ban with
2283 # we're not the same user who locked, ban with
2284 # code defined in settings (default is 423 HTTP Locked) !
2284 # code defined in settings (default is 423 HTTP Locked) !
2285 log.debug('Repo %s is currently locked by %s', repo, user)
2285 log.debug('Repo %s is currently locked by %s', repo, user)
2286 currently_locked = True
2286 currently_locked = True
2287 elif action == 'pull':
2287 elif action == 'pull':
2288 # [0] user [1] date
2288 # [0] user [1] date
2289 if lock_info[0] and lock_info[1]:
2289 if lock_info[0] and lock_info[1]:
2290 log.debug('Repo %s is currently locked by %s', repo, user)
2290 log.debug('Repo %s is currently locked by %s', repo, user)
2291 currently_locked = True
2291 currently_locked = True
2292 else:
2292 else:
2293 log.debug('Setting lock on repo %s by %s', repo, user)
2293 log.debug('Setting lock on repo %s by %s', repo, user)
2294 make_lock = True
2294 make_lock = True
2295
2295
2296 else:
2296 else:
2297 log.debug('Repository %s do not have locking enabled', repo)
2297 log.debug('Repository %s do not have locking enabled', repo)
2298
2298
2299 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2299 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2300 make_lock, currently_locked, lock_info)
2300 make_lock, currently_locked, lock_info)
2301
2301
2302 from rhodecode.lib.auth import HasRepoPermissionAny
2302 from rhodecode.lib.auth import HasRepoPermissionAny
2303 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2303 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2304 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2304 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2305 # if we don't have at least write permission we cannot make a lock
2305 # if we don't have at least write permission we cannot make a lock
2306 log.debug('lock state reset back to FALSE due to lack '
2306 log.debug('lock state reset back to FALSE due to lack '
2307 'of at least read permission')
2307 'of at least read permission')
2308 make_lock = False
2308 make_lock = False
2309
2309
2310 return make_lock, currently_locked, lock_info
2310 return make_lock, currently_locked, lock_info
2311
2311
2312 @property
2312 @property
2313 def last_commit_cache_update_diff(self):
2313 def last_commit_cache_update_diff(self):
2314 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2314 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2315
2315
2316 @classmethod
2316 @classmethod
2317 def _load_commit_change(cls, last_commit_cache):
2317 def _load_commit_change(cls, last_commit_cache):
2318 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2318 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2319 empty_date = datetime.datetime.fromtimestamp(0)
2319 empty_date = datetime.datetime.fromtimestamp(0)
2320 date_latest = last_commit_cache.get('date', empty_date)
2320 date_latest = last_commit_cache.get('date', empty_date)
2321 try:
2321 try:
2322 return parse_datetime(date_latest)
2322 return parse_datetime(date_latest)
2323 except Exception:
2323 except Exception:
2324 return empty_date
2324 return empty_date
2325
2325
2326 @property
2326 @property
2327 def last_commit_change(self):
2327 def last_commit_change(self):
2328 return self._load_commit_change(self.changeset_cache)
2328 return self._load_commit_change(self.changeset_cache)
2329
2329
2330 @property
2330 @property
2331 def last_db_change(self):
2331 def last_db_change(self):
2332 return self.updated_on
2332 return self.updated_on
2333
2333
2334 @property
2334 @property
2335 def clone_uri_hidden(self):
2335 def clone_uri_hidden(self):
2336 clone_uri = self.clone_uri
2336 clone_uri = self.clone_uri
2337 if clone_uri:
2337 if clone_uri:
2338 import urlobject
2338 import urlobject
2339 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2339 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2340 if url_obj.password:
2340 if url_obj.password:
2341 clone_uri = url_obj.with_password('*****')
2341 clone_uri = url_obj.with_password('*****')
2342 return clone_uri
2342 return clone_uri
2343
2343
2344 @property
2344 @property
2345 def push_uri_hidden(self):
2345 def push_uri_hidden(self):
2346 push_uri = self.push_uri
2346 push_uri = self.push_uri
2347 if push_uri:
2347 if push_uri:
2348 import urlobject
2348 import urlobject
2349 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2349 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2350 if url_obj.password:
2350 if url_obj.password:
2351 push_uri = url_obj.with_password('*****')
2351 push_uri = url_obj.with_password('*****')
2352 return push_uri
2352 return push_uri
2353
2353
2354 def clone_url(self, **override):
2354 def clone_url(self, **override):
2355 from rhodecode.model.settings import SettingsModel
2355 from rhodecode.model.settings import SettingsModel
2356
2356
2357 uri_tmpl = None
2357 uri_tmpl = None
2358 if 'with_id' in override:
2358 if 'with_id' in override:
2359 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2359 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2360 del override['with_id']
2360 del override['with_id']
2361
2361
2362 if 'uri_tmpl' in override:
2362 if 'uri_tmpl' in override:
2363 uri_tmpl = override['uri_tmpl']
2363 uri_tmpl = override['uri_tmpl']
2364 del override['uri_tmpl']
2364 del override['uri_tmpl']
2365
2365
2366 ssh = False
2366 ssh = False
2367 if 'ssh' in override:
2367 if 'ssh' in override:
2368 ssh = True
2368 ssh = True
2369 del override['ssh']
2369 del override['ssh']
2370
2370
2371 # we didn't override our tmpl from **overrides
2371 # we didn't override our tmpl from **overrides
2372 request = get_current_request()
2372 request = get_current_request()
2373 if not uri_tmpl:
2373 if not uri_tmpl:
2374 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2374 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2375 rc_config = request.call_context.rc_config
2375 rc_config = request.call_context.rc_config
2376 else:
2376 else:
2377 rc_config = SettingsModel().get_all_settings(cache=True)
2377 rc_config = SettingsModel().get_all_settings(cache=True)
2378
2378
2379 if ssh:
2379 if ssh:
2380 uri_tmpl = rc_config.get(
2380 uri_tmpl = rc_config.get(
2381 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2381 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2382
2382
2383 else:
2383 else:
2384 uri_tmpl = rc_config.get(
2384 uri_tmpl = rc_config.get(
2385 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2385 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2386
2386
2387 return get_clone_url(request=request,
2387 return get_clone_url(request=request,
2388 uri_tmpl=uri_tmpl,
2388 uri_tmpl=uri_tmpl,
2389 repo_name=self.repo_name,
2389 repo_name=self.repo_name,
2390 repo_id=self.repo_id,
2390 repo_id=self.repo_id,
2391 repo_type=self.repo_type,
2391 repo_type=self.repo_type,
2392 **override)
2392 **override)
2393
2393
2394 def set_state(self, state):
2394 def set_state(self, state):
2395 self.repo_state = state
2395 self.repo_state = state
2396 Session().add(self)
2396 Session().add(self)
2397 #==========================================================================
2397 #==========================================================================
2398 # SCM PROPERTIES
2398 # SCM PROPERTIES
2399 #==========================================================================
2399 #==========================================================================
2400
2400
2401 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False):
2401 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False):
2402 return get_commit_safe(
2402 return get_commit_safe(
2403 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2403 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2404 maybe_unreachable=maybe_unreachable)
2404 maybe_unreachable=maybe_unreachable)
2405
2405
2406 def get_changeset(self, rev=None, pre_load=None):
2406 def get_changeset(self, rev=None, pre_load=None):
2407 warnings.warn("Use get_commit", DeprecationWarning)
2407 warnings.warn("Use get_commit", DeprecationWarning)
2408 commit_id = None
2408 commit_id = None
2409 commit_idx = None
2409 commit_idx = None
2410 if isinstance(rev, compat.string_types):
2410 if isinstance(rev, compat.string_types):
2411 commit_id = rev
2411 commit_id = rev
2412 else:
2412 else:
2413 commit_idx = rev
2413 commit_idx = rev
2414 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2414 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2415 pre_load=pre_load)
2415 pre_load=pre_load)
2416
2416
2417 def get_landing_commit(self):
2417 def get_landing_commit(self):
2418 """
2418 """
2419 Returns landing commit, or if that doesn't exist returns the tip
2419 Returns landing commit, or if that doesn't exist returns the tip
2420 """
2420 """
2421 _rev_type, _rev = self.landing_rev
2421 _rev_type, _rev = self.landing_rev
2422 commit = self.get_commit(_rev)
2422 commit = self.get_commit(_rev)
2423 if isinstance(commit, EmptyCommit):
2423 if isinstance(commit, EmptyCommit):
2424 return self.get_commit()
2424 return self.get_commit()
2425 return commit
2425 return commit
2426
2426
2427 def flush_commit_cache(self):
2427 def flush_commit_cache(self):
2428 self.update_commit_cache(cs_cache={'raw_id':'0'})
2428 self.update_commit_cache(cs_cache={'raw_id':'0'})
2429 self.update_commit_cache()
2429 self.update_commit_cache()
2430
2430
2431 def update_commit_cache(self, cs_cache=None, config=None):
2431 def update_commit_cache(self, cs_cache=None, config=None):
2432 """
2432 """
2433 Update cache of last commit for repository
2433 Update cache of last commit for repository
2434 cache_keys should be::
2434 cache_keys should be::
2435
2435
2436 source_repo_id
2436 source_repo_id
2437 short_id
2437 short_id
2438 raw_id
2438 raw_id
2439 revision
2439 revision
2440 parents
2440 parents
2441 message
2441 message
2442 date
2442 date
2443 author
2443 author
2444 updated_on
2444 updated_on
2445
2445
2446 """
2446 """
2447 from rhodecode.lib.vcs.backends.base import BaseChangeset
2447 from rhodecode.lib.vcs.backends.base import BaseChangeset
2448 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2448 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2449 empty_date = datetime.datetime.fromtimestamp(0)
2449 empty_date = datetime.datetime.fromtimestamp(0)
2450
2450
2451 if cs_cache is None:
2451 if cs_cache is None:
2452 # use no-cache version here
2452 # use no-cache version here
2453 try:
2453 try:
2454 scm_repo = self.scm_instance(cache=False, config=config)
2454 scm_repo = self.scm_instance(cache=False, config=config)
2455 except VCSError:
2455 except VCSError:
2456 scm_repo = None
2456 scm_repo = None
2457 empty = scm_repo is None or scm_repo.is_empty()
2457 empty = scm_repo is None or scm_repo.is_empty()
2458
2458
2459 if not empty:
2459 if not empty:
2460 cs_cache = scm_repo.get_commit(
2460 cs_cache = scm_repo.get_commit(
2461 pre_load=["author", "date", "message", "parents", "branch"])
2461 pre_load=["author", "date", "message", "parents", "branch"])
2462 else:
2462 else:
2463 cs_cache = EmptyCommit()
2463 cs_cache = EmptyCommit()
2464
2464
2465 if isinstance(cs_cache, BaseChangeset):
2465 if isinstance(cs_cache, BaseChangeset):
2466 cs_cache = cs_cache.__json__()
2466 cs_cache = cs_cache.__json__()
2467
2467
2468 def is_outdated(new_cs_cache):
2468 def is_outdated(new_cs_cache):
2469 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2469 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2470 new_cs_cache['revision'] != self.changeset_cache['revision']):
2470 new_cs_cache['revision'] != self.changeset_cache['revision']):
2471 return True
2471 return True
2472 return False
2472 return False
2473
2473
2474 # check if we have maybe already latest cached revision
2474 # check if we have maybe already latest cached revision
2475 if is_outdated(cs_cache) or not self.changeset_cache:
2475 if is_outdated(cs_cache) or not self.changeset_cache:
2476 _current_datetime = datetime.datetime.utcnow()
2476 _current_datetime = datetime.datetime.utcnow()
2477 last_change = cs_cache.get('date') or _current_datetime
2477 last_change = cs_cache.get('date') or _current_datetime
2478 # we check if last update is newer than the new value
2478 # we check if last update is newer than the new value
2479 # if yes, we use the current timestamp instead. Imagine you get
2479 # if yes, we use the current timestamp instead. Imagine you get
2480 # old commit pushed 1y ago, we'd set last update 1y to ago.
2480 # old commit pushed 1y ago, we'd set last update 1y to ago.
2481 last_change_timestamp = datetime_to_time(last_change)
2481 last_change_timestamp = datetime_to_time(last_change)
2482 current_timestamp = datetime_to_time(last_change)
2482 current_timestamp = datetime_to_time(last_change)
2483 if last_change_timestamp > current_timestamp and not empty:
2483 if last_change_timestamp > current_timestamp and not empty:
2484 cs_cache['date'] = _current_datetime
2484 cs_cache['date'] = _current_datetime
2485
2485
2486 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2486 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2487 cs_cache['updated_on'] = time.time()
2487 cs_cache['updated_on'] = time.time()
2488 self.changeset_cache = cs_cache
2488 self.changeset_cache = cs_cache
2489 self.updated_on = last_change
2489 self.updated_on = last_change
2490 Session().add(self)
2490 Session().add(self)
2491 Session().commit()
2491 Session().commit()
2492
2492
2493 else:
2493 else:
2494 if empty:
2494 if empty:
2495 cs_cache = EmptyCommit().__json__()
2495 cs_cache = EmptyCommit().__json__()
2496 else:
2496 else:
2497 cs_cache = self.changeset_cache
2497 cs_cache = self.changeset_cache
2498
2498
2499 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2499 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2500
2500
2501 cs_cache['updated_on'] = time.time()
2501 cs_cache['updated_on'] = time.time()
2502 self.changeset_cache = cs_cache
2502 self.changeset_cache = cs_cache
2503 self.updated_on = _date_latest
2503 self.updated_on = _date_latest
2504 Session().add(self)
2504 Session().add(self)
2505 Session().commit()
2505 Session().commit()
2506
2506
2507 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2507 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2508 self.repo_name, cs_cache, _date_latest)
2508 self.repo_name, cs_cache, _date_latest)
2509
2509
2510 @property
2510 @property
2511 def tip(self):
2511 def tip(self):
2512 return self.get_commit('tip')
2512 return self.get_commit('tip')
2513
2513
2514 @property
2514 @property
2515 def author(self):
2515 def author(self):
2516 return self.tip.author
2516 return self.tip.author
2517
2517
2518 @property
2518 @property
2519 def last_change(self):
2519 def last_change(self):
2520 return self.scm_instance().last_change
2520 return self.scm_instance().last_change
2521
2521
2522 def get_comments(self, revisions=None):
2522 def get_comments(self, revisions=None):
2523 """
2523 """
2524 Returns comments for this repository grouped by revisions
2524 Returns comments for this repository grouped by revisions
2525
2525
2526 :param revisions: filter query by revisions only
2526 :param revisions: filter query by revisions only
2527 """
2527 """
2528 cmts = ChangesetComment.query()\
2528 cmts = ChangesetComment.query()\
2529 .filter(ChangesetComment.repo == self)
2529 .filter(ChangesetComment.repo == self)
2530 if revisions:
2530 if revisions:
2531 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2531 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2532 grouped = collections.defaultdict(list)
2532 grouped = collections.defaultdict(list)
2533 for cmt in cmts.all():
2533 for cmt in cmts.all():
2534 grouped[cmt.revision].append(cmt)
2534 grouped[cmt.revision].append(cmt)
2535 return grouped
2535 return grouped
2536
2536
2537 def statuses(self, revisions=None):
2537 def statuses(self, revisions=None):
2538 """
2538 """
2539 Returns statuses for this repository
2539 Returns statuses for this repository
2540
2540
2541 :param revisions: list of revisions to get statuses for
2541 :param revisions: list of revisions to get statuses for
2542 """
2542 """
2543 statuses = ChangesetStatus.query()\
2543 statuses = ChangesetStatus.query()\
2544 .filter(ChangesetStatus.repo == self)\
2544 .filter(ChangesetStatus.repo == self)\
2545 .filter(ChangesetStatus.version == 0)
2545 .filter(ChangesetStatus.version == 0)
2546
2546
2547 if revisions:
2547 if revisions:
2548 # Try doing the filtering in chunks to avoid hitting limits
2548 # Try doing the filtering in chunks to avoid hitting limits
2549 size = 500
2549 size = 500
2550 status_results = []
2550 status_results = []
2551 for chunk in xrange(0, len(revisions), size):
2551 for chunk in xrange(0, len(revisions), size):
2552 status_results += statuses.filter(
2552 status_results += statuses.filter(
2553 ChangesetStatus.revision.in_(
2553 ChangesetStatus.revision.in_(
2554 revisions[chunk: chunk+size])
2554 revisions[chunk: chunk+size])
2555 ).all()
2555 ).all()
2556 else:
2556 else:
2557 status_results = statuses.all()
2557 status_results = statuses.all()
2558
2558
2559 grouped = {}
2559 grouped = {}
2560
2560
2561 # maybe we have open new pullrequest without a status?
2561 # maybe we have open new pullrequest without a status?
2562 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2562 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2563 status_lbl = ChangesetStatus.get_status_lbl(stat)
2563 status_lbl = ChangesetStatus.get_status_lbl(stat)
2564 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2564 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2565 for rev in pr.revisions:
2565 for rev in pr.revisions:
2566 pr_id = pr.pull_request_id
2566 pr_id = pr.pull_request_id
2567 pr_repo = pr.target_repo.repo_name
2567 pr_repo = pr.target_repo.repo_name
2568 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2568 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2569
2569
2570 for stat in status_results:
2570 for stat in status_results:
2571 pr_id = pr_repo = None
2571 pr_id = pr_repo = None
2572 if stat.pull_request:
2572 if stat.pull_request:
2573 pr_id = stat.pull_request.pull_request_id
2573 pr_id = stat.pull_request.pull_request_id
2574 pr_repo = stat.pull_request.target_repo.repo_name
2574 pr_repo = stat.pull_request.target_repo.repo_name
2575 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2575 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2576 pr_id, pr_repo]
2576 pr_id, pr_repo]
2577 return grouped
2577 return grouped
2578
2578
2579 # ==========================================================================
2579 # ==========================================================================
2580 # SCM CACHE INSTANCE
2580 # SCM CACHE INSTANCE
2581 # ==========================================================================
2581 # ==========================================================================
2582
2582
2583 def scm_instance(self, **kwargs):
2583 def scm_instance(self, **kwargs):
2584 import rhodecode
2584 import rhodecode
2585
2585
2586 # Passing a config will not hit the cache currently only used
2586 # Passing a config will not hit the cache currently only used
2587 # for repo2dbmapper
2587 # for repo2dbmapper
2588 config = kwargs.pop('config', None)
2588 config = kwargs.pop('config', None)
2589 cache = kwargs.pop('cache', None)
2589 cache = kwargs.pop('cache', None)
2590 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2590 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2591 if vcs_full_cache is not None:
2591 if vcs_full_cache is not None:
2592 # allows override global config
2592 # allows override global config
2593 full_cache = vcs_full_cache
2593 full_cache = vcs_full_cache
2594 else:
2594 else:
2595 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2595 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2596 # if cache is NOT defined use default global, else we have a full
2596 # if cache is NOT defined use default global, else we have a full
2597 # control over cache behaviour
2597 # control over cache behaviour
2598 if cache is None and full_cache and not config:
2598 if cache is None and full_cache and not config:
2599 log.debug('Initializing pure cached instance for %s', self.repo_path)
2599 log.debug('Initializing pure cached instance for %s', self.repo_path)
2600 return self._get_instance_cached()
2600 return self._get_instance_cached()
2601
2601
2602 # cache here is sent to the "vcs server"
2602 # cache here is sent to the "vcs server"
2603 return self._get_instance(cache=bool(cache), config=config)
2603 return self._get_instance(cache=bool(cache), config=config)
2604
2604
2605 def _get_instance_cached(self):
2605 def _get_instance_cached(self):
2606 from rhodecode.lib import rc_cache
2606 from rhodecode.lib import rc_cache
2607
2607
2608 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2608 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2609 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2609 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2610 repo_id=self.repo_id)
2610 repo_id=self.repo_id)
2611 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2611 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2612
2612
2613 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2613 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2614 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2614 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2615 return self._get_instance(repo_state_uid=_cache_state_uid)
2615 return self._get_instance(repo_state_uid=_cache_state_uid)
2616
2616
2617 # we must use thread scoped cache here,
2617 # we must use thread scoped cache here,
2618 # because each thread of gevent needs it's own not shared connection and cache
2618 # because each thread of gevent needs it's own not shared connection and cache
2619 # we also alter `args` so the cache key is individual for every green thread.
2619 # we also alter `args` so the cache key is individual for every green thread.
2620 inv_context_manager = rc_cache.InvalidationContext(
2620 inv_context_manager = rc_cache.InvalidationContext(
2621 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2621 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2622 thread_scoped=True)
2622 thread_scoped=True)
2623 with inv_context_manager as invalidation_context:
2623 with inv_context_manager as invalidation_context:
2624 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2624 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2625 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2625 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2626
2626
2627 # re-compute and store cache if we get invalidate signal
2627 # re-compute and store cache if we get invalidate signal
2628 if invalidation_context.should_invalidate():
2628 if invalidation_context.should_invalidate():
2629 instance = get_instance_cached.refresh(*args)
2629 instance = get_instance_cached.refresh(*args)
2630 else:
2630 else:
2631 instance = get_instance_cached(*args)
2631 instance = get_instance_cached(*args)
2632
2632
2633 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2633 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2634 return instance
2634 return instance
2635
2635
2636 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2636 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2637 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2637 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2638 self.repo_type, self.repo_path, cache)
2638 self.repo_type, self.repo_path, cache)
2639 config = config or self._config
2639 config = config or self._config
2640 custom_wire = {
2640 custom_wire = {
2641 'cache': cache, # controls the vcs.remote cache
2641 'cache': cache, # controls the vcs.remote cache
2642 'repo_state_uid': repo_state_uid
2642 'repo_state_uid': repo_state_uid
2643 }
2643 }
2644 repo = get_vcs_instance(
2644 repo = get_vcs_instance(
2645 repo_path=safe_str(self.repo_full_path),
2645 repo_path=safe_str(self.repo_full_path),
2646 config=config,
2646 config=config,
2647 with_wire=custom_wire,
2647 with_wire=custom_wire,
2648 create=False,
2648 create=False,
2649 _vcs_alias=self.repo_type)
2649 _vcs_alias=self.repo_type)
2650 if repo is not None:
2650 if repo is not None:
2651 repo.count() # cache rebuild
2651 repo.count() # cache rebuild
2652 return repo
2652 return repo
2653
2653
2654 def get_shadow_repository_path(self, workspace_id):
2654 def get_shadow_repository_path(self, workspace_id):
2655 from rhodecode.lib.vcs.backends.base import BaseRepository
2655 from rhodecode.lib.vcs.backends.base import BaseRepository
2656 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2656 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2657 self.repo_full_path, self.repo_id, workspace_id)
2657 self.repo_full_path, self.repo_id, workspace_id)
2658 return shadow_repo_path
2658 return shadow_repo_path
2659
2659
2660 def __json__(self):
2660 def __json__(self):
2661 return {'landing_rev': self.landing_rev}
2661 return {'landing_rev': self.landing_rev}
2662
2662
2663 def get_dict(self):
2663 def get_dict(self):
2664
2664
2665 # Since we transformed `repo_name` to a hybrid property, we need to
2665 # Since we transformed `repo_name` to a hybrid property, we need to
2666 # keep compatibility with the code which uses `repo_name` field.
2666 # keep compatibility with the code which uses `repo_name` field.
2667
2667
2668 result = super(Repository, self).get_dict()
2668 result = super(Repository, self).get_dict()
2669 result['repo_name'] = result.pop('_repo_name', None)
2669 result['repo_name'] = result.pop('_repo_name', None)
2670 return result
2670 return result
2671
2671
2672
2672
2673 class RepoGroup(Base, BaseModel):
2673 class RepoGroup(Base, BaseModel):
2674 __tablename__ = 'groups'
2674 __tablename__ = 'groups'
2675 __table_args__ = (
2675 __table_args__ = (
2676 UniqueConstraint('group_name', 'group_parent_id'),
2676 UniqueConstraint('group_name', 'group_parent_id'),
2677 base_table_args,
2677 base_table_args,
2678 )
2678 )
2679 __mapper_args__ = {'order_by': 'group_name'}
2679 __mapper_args__ = {'order_by': 'group_name'}
2680
2680
2681 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2681 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2682
2682
2683 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2683 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2684 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2684 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2685 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2685 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2686 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2686 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2687 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2687 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2688 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2688 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2689 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2689 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2690 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2690 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2691 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2691 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2692 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2692 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2693 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2693 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2694
2694
2695 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2695 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2696 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2696 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2697 parent_group = relationship('RepoGroup', remote_side=group_id)
2697 parent_group = relationship('RepoGroup', remote_side=group_id)
2698 user = relationship('User')
2698 user = relationship('User')
2699 integrations = relationship('Integration', cascade="all, delete-orphan")
2699 integrations = relationship('Integration', cascade="all, delete-orphan")
2700
2700
2701 # no cascade, set NULL
2701 # no cascade, set NULL
2702 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2702 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2703
2703
2704 def __init__(self, group_name='', parent_group=None):
2704 def __init__(self, group_name='', parent_group=None):
2705 self.group_name = group_name
2705 self.group_name = group_name
2706 self.parent_group = parent_group
2706 self.parent_group = parent_group
2707
2707
2708 def __unicode__(self):
2708 def __unicode__(self):
2709 return u"<%s('id:%s:%s')>" % (
2709 return u"<%s('id:%s:%s')>" % (
2710 self.__class__.__name__, self.group_id, self.group_name)
2710 self.__class__.__name__, self.group_id, self.group_name)
2711
2711
2712 @hybrid_property
2712 @hybrid_property
2713 def group_name(self):
2713 def group_name(self):
2714 return self._group_name
2714 return self._group_name
2715
2715
2716 @group_name.setter
2716 @group_name.setter
2717 def group_name(self, value):
2717 def group_name(self, value):
2718 self._group_name = value
2718 self._group_name = value
2719 self.group_name_hash = self.hash_repo_group_name(value)
2719 self.group_name_hash = self.hash_repo_group_name(value)
2720
2720
2721 @classmethod
2721 @classmethod
2722 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2722 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2723 from rhodecode.lib.vcs.backends.base import EmptyCommit
2723 from rhodecode.lib.vcs.backends.base import EmptyCommit
2724 dummy = EmptyCommit().__json__()
2724 dummy = EmptyCommit().__json__()
2725 if not changeset_cache_raw:
2725 if not changeset_cache_raw:
2726 dummy['source_repo_id'] = repo_id
2726 dummy['source_repo_id'] = repo_id
2727 return json.loads(json.dumps(dummy))
2727 return json.loads(json.dumps(dummy))
2728
2728
2729 try:
2729 try:
2730 return json.loads(changeset_cache_raw)
2730 return json.loads(changeset_cache_raw)
2731 except TypeError:
2731 except TypeError:
2732 return dummy
2732 return dummy
2733 except Exception:
2733 except Exception:
2734 log.error(traceback.format_exc())
2734 log.error(traceback.format_exc())
2735 return dummy
2735 return dummy
2736
2736
2737 @hybrid_property
2737 @hybrid_property
2738 def changeset_cache(self):
2738 def changeset_cache(self):
2739 return self._load_changeset_cache('', self._changeset_cache)
2739 return self._load_changeset_cache('', self._changeset_cache)
2740
2740
2741 @changeset_cache.setter
2741 @changeset_cache.setter
2742 def changeset_cache(self, val):
2742 def changeset_cache(self, val):
2743 try:
2743 try:
2744 self._changeset_cache = json.dumps(val)
2744 self._changeset_cache = json.dumps(val)
2745 except Exception:
2745 except Exception:
2746 log.error(traceback.format_exc())
2746 log.error(traceback.format_exc())
2747
2747
2748 @validates('group_parent_id')
2748 @validates('group_parent_id')
2749 def validate_group_parent_id(self, key, val):
2749 def validate_group_parent_id(self, key, val):
2750 """
2750 """
2751 Check cycle references for a parent group to self
2751 Check cycle references for a parent group to self
2752 """
2752 """
2753 if self.group_id and val:
2753 if self.group_id and val:
2754 assert val != self.group_id
2754 assert val != self.group_id
2755
2755
2756 return val
2756 return val
2757
2757
2758 @hybrid_property
2758 @hybrid_property
2759 def description_safe(self):
2759 def description_safe(self):
2760 from rhodecode.lib import helpers as h
2760 from rhodecode.lib import helpers as h
2761 return h.escape(self.group_description)
2761 return h.escape(self.group_description)
2762
2762
2763 @classmethod
2763 @classmethod
2764 def hash_repo_group_name(cls, repo_group_name):
2764 def hash_repo_group_name(cls, repo_group_name):
2765 val = remove_formatting(repo_group_name)
2765 val = remove_formatting(repo_group_name)
2766 val = safe_str(val).lower()
2766 val = safe_str(val).lower()
2767 chars = []
2767 chars = []
2768 for c in val:
2768 for c in val:
2769 if c not in string.ascii_letters:
2769 if c not in string.ascii_letters:
2770 c = str(ord(c))
2770 c = str(ord(c))
2771 chars.append(c)
2771 chars.append(c)
2772
2772
2773 return ''.join(chars)
2773 return ''.join(chars)
2774
2774
2775 @classmethod
2775 @classmethod
2776 def _generate_choice(cls, repo_group):
2776 def _generate_choice(cls, repo_group):
2777 from webhelpers2.html import literal as _literal
2777 from webhelpers2.html import literal as _literal
2778 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2778 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2779 return repo_group.group_id, _name(repo_group.full_path_splitted)
2779 return repo_group.group_id, _name(repo_group.full_path_splitted)
2780
2780
2781 @classmethod
2781 @classmethod
2782 def groups_choices(cls, groups=None, show_empty_group=True):
2782 def groups_choices(cls, groups=None, show_empty_group=True):
2783 if not groups:
2783 if not groups:
2784 groups = cls.query().all()
2784 groups = cls.query().all()
2785
2785
2786 repo_groups = []
2786 repo_groups = []
2787 if show_empty_group:
2787 if show_empty_group:
2788 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2788 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2789
2789
2790 repo_groups.extend([cls._generate_choice(x) for x in groups])
2790 repo_groups.extend([cls._generate_choice(x) for x in groups])
2791
2791
2792 repo_groups = sorted(
2792 repo_groups = sorted(
2793 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2793 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2794 return repo_groups
2794 return repo_groups
2795
2795
2796 @classmethod
2796 @classmethod
2797 def url_sep(cls):
2797 def url_sep(cls):
2798 return URL_SEP
2798 return URL_SEP
2799
2799
2800 @classmethod
2800 @classmethod
2801 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2801 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2802 if case_insensitive:
2802 if case_insensitive:
2803 gr = cls.query().filter(func.lower(cls.group_name)
2803 gr = cls.query().filter(func.lower(cls.group_name)
2804 == func.lower(group_name))
2804 == func.lower(group_name))
2805 else:
2805 else:
2806 gr = cls.query().filter(cls.group_name == group_name)
2806 gr = cls.query().filter(cls.group_name == group_name)
2807 if cache:
2807 if cache:
2808 name_key = _hash_key(group_name)
2808 name_key = _hash_key(group_name)
2809 gr = gr.options(
2809 gr = gr.options(
2810 FromCache("sql_cache_short", "get_group_%s" % name_key))
2810 FromCache("sql_cache_short", "get_group_%s" % name_key))
2811 return gr.scalar()
2811 return gr.scalar()
2812
2812
2813 @classmethod
2813 @classmethod
2814 def get_user_personal_repo_group(cls, user_id):
2814 def get_user_personal_repo_group(cls, user_id):
2815 user = User.get(user_id)
2815 user = User.get(user_id)
2816 if user.username == User.DEFAULT_USER:
2816 if user.username == User.DEFAULT_USER:
2817 return None
2817 return None
2818
2818
2819 return cls.query()\
2819 return cls.query()\
2820 .filter(cls.personal == true()) \
2820 .filter(cls.personal == true()) \
2821 .filter(cls.user == user) \
2821 .filter(cls.user == user) \
2822 .order_by(cls.group_id.asc()) \
2822 .order_by(cls.group_id.asc()) \
2823 .first()
2823 .first()
2824
2824
2825 @classmethod
2825 @classmethod
2826 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2826 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2827 case_insensitive=True):
2827 case_insensitive=True):
2828 q = RepoGroup.query()
2828 q = RepoGroup.query()
2829
2829
2830 if not isinstance(user_id, Optional):
2830 if not isinstance(user_id, Optional):
2831 q = q.filter(RepoGroup.user_id == user_id)
2831 q = q.filter(RepoGroup.user_id == user_id)
2832
2832
2833 if not isinstance(group_id, Optional):
2833 if not isinstance(group_id, Optional):
2834 q = q.filter(RepoGroup.group_parent_id == group_id)
2834 q = q.filter(RepoGroup.group_parent_id == group_id)
2835
2835
2836 if case_insensitive:
2836 if case_insensitive:
2837 q = q.order_by(func.lower(RepoGroup.group_name))
2837 q = q.order_by(func.lower(RepoGroup.group_name))
2838 else:
2838 else:
2839 q = q.order_by(RepoGroup.group_name)
2839 q = q.order_by(RepoGroup.group_name)
2840 return q.all()
2840 return q.all()
2841
2841
2842 @property
2842 @property
2843 def parents(self, parents_recursion_limit=10):
2843 def parents(self, parents_recursion_limit=10):
2844 groups = []
2844 groups = []
2845 if self.parent_group is None:
2845 if self.parent_group is None:
2846 return groups
2846 return groups
2847 cur_gr = self.parent_group
2847 cur_gr = self.parent_group
2848 groups.insert(0, cur_gr)
2848 groups.insert(0, cur_gr)
2849 cnt = 0
2849 cnt = 0
2850 while 1:
2850 while 1:
2851 cnt += 1
2851 cnt += 1
2852 gr = getattr(cur_gr, 'parent_group', None)
2852 gr = getattr(cur_gr, 'parent_group', None)
2853 cur_gr = cur_gr.parent_group
2853 cur_gr = cur_gr.parent_group
2854 if gr is None:
2854 if gr is None:
2855 break
2855 break
2856 if cnt == parents_recursion_limit:
2856 if cnt == parents_recursion_limit:
2857 # this will prevent accidental infinit loops
2857 # this will prevent accidental infinit loops
2858 log.error('more than %s parents found for group %s, stopping '
2858 log.error('more than %s parents found for group %s, stopping '
2859 'recursive parent fetching', parents_recursion_limit, self)
2859 'recursive parent fetching', parents_recursion_limit, self)
2860 break
2860 break
2861
2861
2862 groups.insert(0, gr)
2862 groups.insert(0, gr)
2863 return groups
2863 return groups
2864
2864
2865 @property
2865 @property
2866 def last_commit_cache_update_diff(self):
2866 def last_commit_cache_update_diff(self):
2867 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2867 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2868
2868
2869 @classmethod
2869 @classmethod
2870 def _load_commit_change(cls, last_commit_cache):
2870 def _load_commit_change(cls, last_commit_cache):
2871 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2871 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2872 empty_date = datetime.datetime.fromtimestamp(0)
2872 empty_date = datetime.datetime.fromtimestamp(0)
2873 date_latest = last_commit_cache.get('date', empty_date)
2873 date_latest = last_commit_cache.get('date', empty_date)
2874 try:
2874 try:
2875 return parse_datetime(date_latest)
2875 return parse_datetime(date_latest)
2876 except Exception:
2876 except Exception:
2877 return empty_date
2877 return empty_date
2878
2878
2879 @property
2879 @property
2880 def last_commit_change(self):
2880 def last_commit_change(self):
2881 return self._load_commit_change(self.changeset_cache)
2881 return self._load_commit_change(self.changeset_cache)
2882
2882
2883 @property
2883 @property
2884 def last_db_change(self):
2884 def last_db_change(self):
2885 return self.updated_on
2885 return self.updated_on
2886
2886
2887 @property
2887 @property
2888 def children(self):
2888 def children(self):
2889 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2889 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2890
2890
2891 @property
2891 @property
2892 def name(self):
2892 def name(self):
2893 return self.group_name.split(RepoGroup.url_sep())[-1]
2893 return self.group_name.split(RepoGroup.url_sep())[-1]
2894
2894
2895 @property
2895 @property
2896 def full_path(self):
2896 def full_path(self):
2897 return self.group_name
2897 return self.group_name
2898
2898
2899 @property
2899 @property
2900 def full_path_splitted(self):
2900 def full_path_splitted(self):
2901 return self.group_name.split(RepoGroup.url_sep())
2901 return self.group_name.split(RepoGroup.url_sep())
2902
2902
2903 @property
2903 @property
2904 def repositories(self):
2904 def repositories(self):
2905 return Repository.query()\
2905 return Repository.query()\
2906 .filter(Repository.group == self)\
2906 .filter(Repository.group == self)\
2907 .order_by(Repository.repo_name)
2907 .order_by(Repository.repo_name)
2908
2908
2909 @property
2909 @property
2910 def repositories_recursive_count(self):
2910 def repositories_recursive_count(self):
2911 cnt = self.repositories.count()
2911 cnt = self.repositories.count()
2912
2912
2913 def children_count(group):
2913 def children_count(group):
2914 cnt = 0
2914 cnt = 0
2915 for child in group.children:
2915 for child in group.children:
2916 cnt += child.repositories.count()
2916 cnt += child.repositories.count()
2917 cnt += children_count(child)
2917 cnt += children_count(child)
2918 return cnt
2918 return cnt
2919
2919
2920 return cnt + children_count(self)
2920 return cnt + children_count(self)
2921
2921
2922 def _recursive_objects(self, include_repos=True, include_groups=True):
2922 def _recursive_objects(self, include_repos=True, include_groups=True):
2923 all_ = []
2923 all_ = []
2924
2924
2925 def _get_members(root_gr):
2925 def _get_members(root_gr):
2926 if include_repos:
2926 if include_repos:
2927 for r in root_gr.repositories:
2927 for r in root_gr.repositories:
2928 all_.append(r)
2928 all_.append(r)
2929 childs = root_gr.children.all()
2929 childs = root_gr.children.all()
2930 if childs:
2930 if childs:
2931 for gr in childs:
2931 for gr in childs:
2932 if include_groups:
2932 if include_groups:
2933 all_.append(gr)
2933 all_.append(gr)
2934 _get_members(gr)
2934 _get_members(gr)
2935
2935
2936 root_group = []
2936 root_group = []
2937 if include_groups:
2937 if include_groups:
2938 root_group = [self]
2938 root_group = [self]
2939
2939
2940 _get_members(self)
2940 _get_members(self)
2941 return root_group + all_
2941 return root_group + all_
2942
2942
2943 def recursive_groups_and_repos(self):
2943 def recursive_groups_and_repos(self):
2944 """
2944 """
2945 Recursive return all groups, with repositories in those groups
2945 Recursive return all groups, with repositories in those groups
2946 """
2946 """
2947 return self._recursive_objects()
2947 return self._recursive_objects()
2948
2948
2949 def recursive_groups(self):
2949 def recursive_groups(self):
2950 """
2950 """
2951 Returns all children groups for this group including children of children
2951 Returns all children groups for this group including children of children
2952 """
2952 """
2953 return self._recursive_objects(include_repos=False)
2953 return self._recursive_objects(include_repos=False)
2954
2954
2955 def recursive_repos(self):
2955 def recursive_repos(self):
2956 """
2956 """
2957 Returns all children repositories for this group
2957 Returns all children repositories for this group
2958 """
2958 """
2959 return self._recursive_objects(include_groups=False)
2959 return self._recursive_objects(include_groups=False)
2960
2960
2961 def get_new_name(self, group_name):
2961 def get_new_name(self, group_name):
2962 """
2962 """
2963 returns new full group name based on parent and new name
2963 returns new full group name based on parent and new name
2964
2964
2965 :param group_name:
2965 :param group_name:
2966 """
2966 """
2967 path_prefix = (self.parent_group.full_path_splitted if
2967 path_prefix = (self.parent_group.full_path_splitted if
2968 self.parent_group else [])
2968 self.parent_group else [])
2969 return RepoGroup.url_sep().join(path_prefix + [group_name])
2969 return RepoGroup.url_sep().join(path_prefix + [group_name])
2970
2970
2971 def update_commit_cache(self, config=None):
2971 def update_commit_cache(self, config=None):
2972 """
2972 """
2973 Update cache of last commit for newest repository inside this repository group.
2973 Update cache of last commit for newest repository inside this repository group.
2974 cache_keys should be::
2974 cache_keys should be::
2975
2975
2976 source_repo_id
2976 source_repo_id
2977 short_id
2977 short_id
2978 raw_id
2978 raw_id
2979 revision
2979 revision
2980 parents
2980 parents
2981 message
2981 message
2982 date
2982 date
2983 author
2983 author
2984
2984
2985 """
2985 """
2986 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2986 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2987 empty_date = datetime.datetime.fromtimestamp(0)
2987 empty_date = datetime.datetime.fromtimestamp(0)
2988
2988
2989 def repo_groups_and_repos(root_gr):
2989 def repo_groups_and_repos(root_gr):
2990 for _repo in root_gr.repositories:
2990 for _repo in root_gr.repositories:
2991 yield _repo
2991 yield _repo
2992 for child_group in root_gr.children.all():
2992 for child_group in root_gr.children.all():
2993 yield child_group
2993 yield child_group
2994
2994
2995 latest_repo_cs_cache = {}
2995 latest_repo_cs_cache = {}
2996 for obj in repo_groups_and_repos(self):
2996 for obj in repo_groups_and_repos(self):
2997 repo_cs_cache = obj.changeset_cache
2997 repo_cs_cache = obj.changeset_cache
2998 date_latest = latest_repo_cs_cache.get('date', empty_date)
2998 date_latest = latest_repo_cs_cache.get('date', empty_date)
2999 date_current = repo_cs_cache.get('date', empty_date)
2999 date_current = repo_cs_cache.get('date', empty_date)
3000 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3000 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3001 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3001 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3002 latest_repo_cs_cache = repo_cs_cache
3002 latest_repo_cs_cache = repo_cs_cache
3003 if hasattr(obj, 'repo_id'):
3003 if hasattr(obj, 'repo_id'):
3004 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3004 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3005 else:
3005 else:
3006 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3006 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3007
3007
3008 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3008 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3009
3009
3010 latest_repo_cs_cache['updated_on'] = time.time()
3010 latest_repo_cs_cache['updated_on'] = time.time()
3011 self.changeset_cache = latest_repo_cs_cache
3011 self.changeset_cache = latest_repo_cs_cache
3012 self.updated_on = _date_latest
3012 self.updated_on = _date_latest
3013 Session().add(self)
3013 Session().add(self)
3014 Session().commit()
3014 Session().commit()
3015
3015
3016 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3016 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3017 self.group_name, latest_repo_cs_cache, _date_latest)
3017 self.group_name, latest_repo_cs_cache, _date_latest)
3018
3018
3019 def permissions(self, with_admins=True, with_owner=True,
3019 def permissions(self, with_admins=True, with_owner=True,
3020 expand_from_user_groups=False):
3020 expand_from_user_groups=False):
3021 """
3021 """
3022 Permissions for repository groups
3022 Permissions for repository groups
3023 """
3023 """
3024 _admin_perm = 'group.admin'
3024 _admin_perm = 'group.admin'
3025
3025
3026 owner_row = []
3026 owner_row = []
3027 if with_owner:
3027 if with_owner:
3028 usr = AttributeDict(self.user.get_dict())
3028 usr = AttributeDict(self.user.get_dict())
3029 usr.owner_row = True
3029 usr.owner_row = True
3030 usr.permission = _admin_perm
3030 usr.permission = _admin_perm
3031 owner_row.append(usr)
3031 owner_row.append(usr)
3032
3032
3033 super_admin_ids = []
3033 super_admin_ids = []
3034 super_admin_rows = []
3034 super_admin_rows = []
3035 if with_admins:
3035 if with_admins:
3036 for usr in User.get_all_super_admins():
3036 for usr in User.get_all_super_admins():
3037 super_admin_ids.append(usr.user_id)
3037 super_admin_ids.append(usr.user_id)
3038 # if this admin is also owner, don't double the record
3038 # if this admin is also owner, don't double the record
3039 if usr.user_id == owner_row[0].user_id:
3039 if usr.user_id == owner_row[0].user_id:
3040 owner_row[0].admin_row = True
3040 owner_row[0].admin_row = True
3041 else:
3041 else:
3042 usr = AttributeDict(usr.get_dict())
3042 usr = AttributeDict(usr.get_dict())
3043 usr.admin_row = True
3043 usr.admin_row = True
3044 usr.permission = _admin_perm
3044 usr.permission = _admin_perm
3045 super_admin_rows.append(usr)
3045 super_admin_rows.append(usr)
3046
3046
3047 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3047 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3048 q = q.options(joinedload(UserRepoGroupToPerm.group),
3048 q = q.options(joinedload(UserRepoGroupToPerm.group),
3049 joinedload(UserRepoGroupToPerm.user),
3049 joinedload(UserRepoGroupToPerm.user),
3050 joinedload(UserRepoGroupToPerm.permission),)
3050 joinedload(UserRepoGroupToPerm.permission),)
3051
3051
3052 # get owners and admins and permissions. We do a trick of re-writing
3052 # get owners and admins and permissions. We do a trick of re-writing
3053 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3053 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3054 # has a global reference and changing one object propagates to all
3054 # has a global reference and changing one object propagates to all
3055 # others. This means if admin is also an owner admin_row that change
3055 # others. This means if admin is also an owner admin_row that change
3056 # would propagate to both objects
3056 # would propagate to both objects
3057 perm_rows = []
3057 perm_rows = []
3058 for _usr in q.all():
3058 for _usr in q.all():
3059 usr = AttributeDict(_usr.user.get_dict())
3059 usr = AttributeDict(_usr.user.get_dict())
3060 # if this user is also owner/admin, mark as duplicate record
3060 # if this user is also owner/admin, mark as duplicate record
3061 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3061 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3062 usr.duplicate_perm = True
3062 usr.duplicate_perm = True
3063 usr.permission = _usr.permission.permission_name
3063 usr.permission = _usr.permission.permission_name
3064 perm_rows.append(usr)
3064 perm_rows.append(usr)
3065
3065
3066 # filter the perm rows by 'default' first and then sort them by
3066 # filter the perm rows by 'default' first and then sort them by
3067 # admin,write,read,none permissions sorted again alphabetically in
3067 # admin,write,read,none permissions sorted again alphabetically in
3068 # each group
3068 # each group
3069 perm_rows = sorted(perm_rows, key=display_user_sort)
3069 perm_rows = sorted(perm_rows, key=display_user_sort)
3070
3070
3071 user_groups_rows = []
3071 user_groups_rows = []
3072 if expand_from_user_groups:
3072 if expand_from_user_groups:
3073 for ug in self.permission_user_groups(with_members=True):
3073 for ug in self.permission_user_groups(with_members=True):
3074 for user_data in ug.members:
3074 for user_data in ug.members:
3075 user_groups_rows.append(user_data)
3075 user_groups_rows.append(user_data)
3076
3076
3077 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3077 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3078
3078
3079 def permission_user_groups(self, with_members=False):
3079 def permission_user_groups(self, with_members=False):
3080 q = UserGroupRepoGroupToPerm.query()\
3080 q = UserGroupRepoGroupToPerm.query()\
3081 .filter(UserGroupRepoGroupToPerm.group == self)
3081 .filter(UserGroupRepoGroupToPerm.group == self)
3082 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3082 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3083 joinedload(UserGroupRepoGroupToPerm.users_group),
3083 joinedload(UserGroupRepoGroupToPerm.users_group),
3084 joinedload(UserGroupRepoGroupToPerm.permission),)
3084 joinedload(UserGroupRepoGroupToPerm.permission),)
3085
3085
3086 perm_rows = []
3086 perm_rows = []
3087 for _user_group in q.all():
3087 for _user_group in q.all():
3088 entry = AttributeDict(_user_group.users_group.get_dict())
3088 entry = AttributeDict(_user_group.users_group.get_dict())
3089 entry.permission = _user_group.permission.permission_name
3089 entry.permission = _user_group.permission.permission_name
3090 if with_members:
3090 if with_members:
3091 entry.members = [x.user.get_dict()
3091 entry.members = [x.user.get_dict()
3092 for x in _user_group.users_group.members]
3092 for x in _user_group.users_group.members]
3093 perm_rows.append(entry)
3093 perm_rows.append(entry)
3094
3094
3095 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3095 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3096 return perm_rows
3096 return perm_rows
3097
3097
3098 def get_api_data(self):
3098 def get_api_data(self):
3099 """
3099 """
3100 Common function for generating api data
3100 Common function for generating api data
3101
3101
3102 """
3102 """
3103 group = self
3103 group = self
3104 data = {
3104 data = {
3105 'group_id': group.group_id,
3105 'group_id': group.group_id,
3106 'group_name': group.group_name,
3106 'group_name': group.group_name,
3107 'group_description': group.description_safe,
3107 'group_description': group.description_safe,
3108 'parent_group': group.parent_group.group_name if group.parent_group else None,
3108 'parent_group': group.parent_group.group_name if group.parent_group else None,
3109 'repositories': [x.repo_name for x in group.repositories],
3109 'repositories': [x.repo_name for x in group.repositories],
3110 'owner': group.user.username,
3110 'owner': group.user.username,
3111 }
3111 }
3112 return data
3112 return data
3113
3113
3114 def get_dict(self):
3114 def get_dict(self):
3115 # Since we transformed `group_name` to a hybrid property, we need to
3115 # Since we transformed `group_name` to a hybrid property, we need to
3116 # keep compatibility with the code which uses `group_name` field.
3116 # keep compatibility with the code which uses `group_name` field.
3117 result = super(RepoGroup, self).get_dict()
3117 result = super(RepoGroup, self).get_dict()
3118 result['group_name'] = result.pop('_group_name', None)
3118 result['group_name'] = result.pop('_group_name', None)
3119 return result
3119 return result
3120
3120
3121
3121
3122 class Permission(Base, BaseModel):
3122 class Permission(Base, BaseModel):
3123 __tablename__ = 'permissions'
3123 __tablename__ = 'permissions'
3124 __table_args__ = (
3124 __table_args__ = (
3125 Index('p_perm_name_idx', 'permission_name'),
3125 Index('p_perm_name_idx', 'permission_name'),
3126 base_table_args,
3126 base_table_args,
3127 )
3127 )
3128
3128
3129 PERMS = [
3129 PERMS = [
3130 ('hg.admin', _('RhodeCode Super Administrator')),
3130 ('hg.admin', _('RhodeCode Super Administrator')),
3131
3131
3132 ('repository.none', _('Repository no access')),
3132 ('repository.none', _('Repository no access')),
3133 ('repository.read', _('Repository read access')),
3133 ('repository.read', _('Repository read access')),
3134 ('repository.write', _('Repository write access')),
3134 ('repository.write', _('Repository write access')),
3135 ('repository.admin', _('Repository admin access')),
3135 ('repository.admin', _('Repository admin access')),
3136
3136
3137 ('group.none', _('Repository group no access')),
3137 ('group.none', _('Repository group no access')),
3138 ('group.read', _('Repository group read access')),
3138 ('group.read', _('Repository group read access')),
3139 ('group.write', _('Repository group write access')),
3139 ('group.write', _('Repository group write access')),
3140 ('group.admin', _('Repository group admin access')),
3140 ('group.admin', _('Repository group admin access')),
3141
3141
3142 ('usergroup.none', _('User group no access')),
3142 ('usergroup.none', _('User group no access')),
3143 ('usergroup.read', _('User group read access')),
3143 ('usergroup.read', _('User group read access')),
3144 ('usergroup.write', _('User group write access')),
3144 ('usergroup.write', _('User group write access')),
3145 ('usergroup.admin', _('User group admin access')),
3145 ('usergroup.admin', _('User group admin access')),
3146
3146
3147 ('branch.none', _('Branch no permissions')),
3147 ('branch.none', _('Branch no permissions')),
3148 ('branch.merge', _('Branch access by web merge')),
3148 ('branch.merge', _('Branch access by web merge')),
3149 ('branch.push', _('Branch access by push')),
3149 ('branch.push', _('Branch access by push')),
3150 ('branch.push_force', _('Branch access by push with force')),
3150 ('branch.push_force', _('Branch access by push with force')),
3151
3151
3152 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3152 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3153 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3153 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3154
3154
3155 ('hg.usergroup.create.false', _('User Group creation disabled')),
3155 ('hg.usergroup.create.false', _('User Group creation disabled')),
3156 ('hg.usergroup.create.true', _('User Group creation enabled')),
3156 ('hg.usergroup.create.true', _('User Group creation enabled')),
3157
3157
3158 ('hg.create.none', _('Repository creation disabled')),
3158 ('hg.create.none', _('Repository creation disabled')),
3159 ('hg.create.repository', _('Repository creation enabled')),
3159 ('hg.create.repository', _('Repository creation enabled')),
3160 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3160 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3161 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3161 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3162
3162
3163 ('hg.fork.none', _('Repository forking disabled')),
3163 ('hg.fork.none', _('Repository forking disabled')),
3164 ('hg.fork.repository', _('Repository forking enabled')),
3164 ('hg.fork.repository', _('Repository forking enabled')),
3165
3165
3166 ('hg.register.none', _('Registration disabled')),
3166 ('hg.register.none', _('Registration disabled')),
3167 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3167 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3168 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3168 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3169
3169
3170 ('hg.password_reset.enabled', _('Password reset enabled')),
3170 ('hg.password_reset.enabled', _('Password reset enabled')),
3171 ('hg.password_reset.hidden', _('Password reset hidden')),
3171 ('hg.password_reset.hidden', _('Password reset hidden')),
3172 ('hg.password_reset.disabled', _('Password reset disabled')),
3172 ('hg.password_reset.disabled', _('Password reset disabled')),
3173
3173
3174 ('hg.extern_activate.manual', _('Manual activation of external account')),
3174 ('hg.extern_activate.manual', _('Manual activation of external account')),
3175 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3175 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3176
3176
3177 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3177 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3178 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3178 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3179 ]
3179 ]
3180
3180
3181 # definition of system default permissions for DEFAULT user, created on
3181 # definition of system default permissions for DEFAULT user, created on
3182 # system setup
3182 # system setup
3183 DEFAULT_USER_PERMISSIONS = [
3183 DEFAULT_USER_PERMISSIONS = [
3184 # object perms
3184 # object perms
3185 'repository.read',
3185 'repository.read',
3186 'group.read',
3186 'group.read',
3187 'usergroup.read',
3187 'usergroup.read',
3188 # branch, for backward compat we need same value as before so forced pushed
3188 # branch, for backward compat we need same value as before so forced pushed
3189 'branch.push_force',
3189 'branch.push_force',
3190 # global
3190 # global
3191 'hg.create.repository',
3191 'hg.create.repository',
3192 'hg.repogroup.create.false',
3192 'hg.repogroup.create.false',
3193 'hg.usergroup.create.false',
3193 'hg.usergroup.create.false',
3194 'hg.create.write_on_repogroup.true',
3194 'hg.create.write_on_repogroup.true',
3195 'hg.fork.repository',
3195 'hg.fork.repository',
3196 'hg.register.manual_activate',
3196 'hg.register.manual_activate',
3197 'hg.password_reset.enabled',
3197 'hg.password_reset.enabled',
3198 'hg.extern_activate.auto',
3198 'hg.extern_activate.auto',
3199 'hg.inherit_default_perms.true',
3199 'hg.inherit_default_perms.true',
3200 ]
3200 ]
3201
3201
3202 # defines which permissions are more important higher the more important
3202 # defines which permissions are more important higher the more important
3203 # Weight defines which permissions are more important.
3203 # Weight defines which permissions are more important.
3204 # The higher number the more important.
3204 # The higher number the more important.
3205 PERM_WEIGHTS = {
3205 PERM_WEIGHTS = {
3206 'repository.none': 0,
3206 'repository.none': 0,
3207 'repository.read': 1,
3207 'repository.read': 1,
3208 'repository.write': 3,
3208 'repository.write': 3,
3209 'repository.admin': 4,
3209 'repository.admin': 4,
3210
3210
3211 'group.none': 0,
3211 'group.none': 0,
3212 'group.read': 1,
3212 'group.read': 1,
3213 'group.write': 3,
3213 'group.write': 3,
3214 'group.admin': 4,
3214 'group.admin': 4,
3215
3215
3216 'usergroup.none': 0,
3216 'usergroup.none': 0,
3217 'usergroup.read': 1,
3217 'usergroup.read': 1,
3218 'usergroup.write': 3,
3218 'usergroup.write': 3,
3219 'usergroup.admin': 4,
3219 'usergroup.admin': 4,
3220
3220
3221 'branch.none': 0,
3221 'branch.none': 0,
3222 'branch.merge': 1,
3222 'branch.merge': 1,
3223 'branch.push': 3,
3223 'branch.push': 3,
3224 'branch.push_force': 4,
3224 'branch.push_force': 4,
3225
3225
3226 'hg.repogroup.create.false': 0,
3226 'hg.repogroup.create.false': 0,
3227 'hg.repogroup.create.true': 1,
3227 'hg.repogroup.create.true': 1,
3228
3228
3229 'hg.usergroup.create.false': 0,
3229 'hg.usergroup.create.false': 0,
3230 'hg.usergroup.create.true': 1,
3230 'hg.usergroup.create.true': 1,
3231
3231
3232 'hg.fork.none': 0,
3232 'hg.fork.none': 0,
3233 'hg.fork.repository': 1,
3233 'hg.fork.repository': 1,
3234 'hg.create.none': 0,
3234 'hg.create.none': 0,
3235 'hg.create.repository': 1
3235 'hg.create.repository': 1
3236 }
3236 }
3237
3237
3238 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3238 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3239 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3239 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3240 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3240 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3241
3241
3242 def __unicode__(self):
3242 def __unicode__(self):
3243 return u"<%s('%s:%s')>" % (
3243 return u"<%s('%s:%s')>" % (
3244 self.__class__.__name__, self.permission_id, self.permission_name
3244 self.__class__.__name__, self.permission_id, self.permission_name
3245 )
3245 )
3246
3246
3247 @classmethod
3247 @classmethod
3248 def get_by_key(cls, key):
3248 def get_by_key(cls, key):
3249 return cls.query().filter(cls.permission_name == key).scalar()
3249 return cls.query().filter(cls.permission_name == key).scalar()
3250
3250
3251 @classmethod
3251 @classmethod
3252 def get_default_repo_perms(cls, user_id, repo_id=None):
3252 def get_default_repo_perms(cls, user_id, repo_id=None):
3253 q = Session().query(UserRepoToPerm, Repository, Permission)\
3253 q = Session().query(UserRepoToPerm, Repository, Permission)\
3254 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3254 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3255 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3255 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3256 .filter(UserRepoToPerm.user_id == user_id)
3256 .filter(UserRepoToPerm.user_id == user_id)
3257 if repo_id:
3257 if repo_id:
3258 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3258 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3259 return q.all()
3259 return q.all()
3260
3260
3261 @classmethod
3261 @classmethod
3262 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3262 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3263 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3263 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3264 .join(
3264 .join(
3265 Permission,
3265 Permission,
3266 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3266 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3267 .join(
3267 .join(
3268 UserRepoToPerm,
3268 UserRepoToPerm,
3269 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3269 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3270 .filter(UserRepoToPerm.user_id == user_id)
3270 .filter(UserRepoToPerm.user_id == user_id)
3271
3271
3272 if repo_id:
3272 if repo_id:
3273 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3273 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3274 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3274 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3275
3275
3276 @classmethod
3276 @classmethod
3277 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3277 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3278 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3278 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3279 .join(
3279 .join(
3280 Permission,
3280 Permission,
3281 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3281 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3282 .join(
3282 .join(
3283 Repository,
3283 Repository,
3284 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3284 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3285 .join(
3285 .join(
3286 UserGroup,
3286 UserGroup,
3287 UserGroupRepoToPerm.users_group_id ==
3287 UserGroupRepoToPerm.users_group_id ==
3288 UserGroup.users_group_id)\
3288 UserGroup.users_group_id)\
3289 .join(
3289 .join(
3290 UserGroupMember,
3290 UserGroupMember,
3291 UserGroupRepoToPerm.users_group_id ==
3291 UserGroupRepoToPerm.users_group_id ==
3292 UserGroupMember.users_group_id)\
3292 UserGroupMember.users_group_id)\
3293 .filter(
3293 .filter(
3294 UserGroupMember.user_id == user_id,
3294 UserGroupMember.user_id == user_id,
3295 UserGroup.users_group_active == true())
3295 UserGroup.users_group_active == true())
3296 if repo_id:
3296 if repo_id:
3297 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3297 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3298 return q.all()
3298 return q.all()
3299
3299
3300 @classmethod
3300 @classmethod
3301 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3301 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3302 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3302 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3303 .join(
3303 .join(
3304 Permission,
3304 Permission,
3305 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3305 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3306 .join(
3306 .join(
3307 UserGroupRepoToPerm,
3307 UserGroupRepoToPerm,
3308 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3308 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3309 .join(
3309 .join(
3310 UserGroup,
3310 UserGroup,
3311 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3311 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3312 .join(
3312 .join(
3313 UserGroupMember,
3313 UserGroupMember,
3314 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3314 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3315 .filter(
3315 .filter(
3316 UserGroupMember.user_id == user_id,
3316 UserGroupMember.user_id == user_id,
3317 UserGroup.users_group_active == true())
3317 UserGroup.users_group_active == true())
3318
3318
3319 if repo_id:
3319 if repo_id:
3320 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3320 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3321 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3321 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3322
3322
3323 @classmethod
3323 @classmethod
3324 def get_default_group_perms(cls, user_id, repo_group_id=None):
3324 def get_default_group_perms(cls, user_id, repo_group_id=None):
3325 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3325 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3326 .join(
3326 .join(
3327 Permission,
3327 Permission,
3328 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3328 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3329 .join(
3329 .join(
3330 RepoGroup,
3330 RepoGroup,
3331 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3331 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3332 .filter(UserRepoGroupToPerm.user_id == user_id)
3332 .filter(UserRepoGroupToPerm.user_id == user_id)
3333 if repo_group_id:
3333 if repo_group_id:
3334 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3334 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3335 return q.all()
3335 return q.all()
3336
3336
3337 @classmethod
3337 @classmethod
3338 def get_default_group_perms_from_user_group(
3338 def get_default_group_perms_from_user_group(
3339 cls, user_id, repo_group_id=None):
3339 cls, user_id, repo_group_id=None):
3340 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3340 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3341 .join(
3341 .join(
3342 Permission,
3342 Permission,
3343 UserGroupRepoGroupToPerm.permission_id ==
3343 UserGroupRepoGroupToPerm.permission_id ==
3344 Permission.permission_id)\
3344 Permission.permission_id)\
3345 .join(
3345 .join(
3346 RepoGroup,
3346 RepoGroup,
3347 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3347 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3348 .join(
3348 .join(
3349 UserGroup,
3349 UserGroup,
3350 UserGroupRepoGroupToPerm.users_group_id ==
3350 UserGroupRepoGroupToPerm.users_group_id ==
3351 UserGroup.users_group_id)\
3351 UserGroup.users_group_id)\
3352 .join(
3352 .join(
3353 UserGroupMember,
3353 UserGroupMember,
3354 UserGroupRepoGroupToPerm.users_group_id ==
3354 UserGroupRepoGroupToPerm.users_group_id ==
3355 UserGroupMember.users_group_id)\
3355 UserGroupMember.users_group_id)\
3356 .filter(
3356 .filter(
3357 UserGroupMember.user_id == user_id,
3357 UserGroupMember.user_id == user_id,
3358 UserGroup.users_group_active == true())
3358 UserGroup.users_group_active == true())
3359 if repo_group_id:
3359 if repo_group_id:
3360 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3360 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3361 return q.all()
3361 return q.all()
3362
3362
3363 @classmethod
3363 @classmethod
3364 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3364 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3365 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3365 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3366 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3366 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3367 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3367 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3368 .filter(UserUserGroupToPerm.user_id == user_id)
3368 .filter(UserUserGroupToPerm.user_id == user_id)
3369 if user_group_id:
3369 if user_group_id:
3370 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3370 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3371 return q.all()
3371 return q.all()
3372
3372
3373 @classmethod
3373 @classmethod
3374 def get_default_user_group_perms_from_user_group(
3374 def get_default_user_group_perms_from_user_group(
3375 cls, user_id, user_group_id=None):
3375 cls, user_id, user_group_id=None):
3376 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3376 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3377 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3377 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3378 .join(
3378 .join(
3379 Permission,
3379 Permission,
3380 UserGroupUserGroupToPerm.permission_id ==
3380 UserGroupUserGroupToPerm.permission_id ==
3381 Permission.permission_id)\
3381 Permission.permission_id)\
3382 .join(
3382 .join(
3383 TargetUserGroup,
3383 TargetUserGroup,
3384 UserGroupUserGroupToPerm.target_user_group_id ==
3384 UserGroupUserGroupToPerm.target_user_group_id ==
3385 TargetUserGroup.users_group_id)\
3385 TargetUserGroup.users_group_id)\
3386 .join(
3386 .join(
3387 UserGroup,
3387 UserGroup,
3388 UserGroupUserGroupToPerm.user_group_id ==
3388 UserGroupUserGroupToPerm.user_group_id ==
3389 UserGroup.users_group_id)\
3389 UserGroup.users_group_id)\
3390 .join(
3390 .join(
3391 UserGroupMember,
3391 UserGroupMember,
3392 UserGroupUserGroupToPerm.user_group_id ==
3392 UserGroupUserGroupToPerm.user_group_id ==
3393 UserGroupMember.users_group_id)\
3393 UserGroupMember.users_group_id)\
3394 .filter(
3394 .filter(
3395 UserGroupMember.user_id == user_id,
3395 UserGroupMember.user_id == user_id,
3396 UserGroup.users_group_active == true())
3396 UserGroup.users_group_active == true())
3397 if user_group_id:
3397 if user_group_id:
3398 q = q.filter(
3398 q = q.filter(
3399 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3399 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3400
3400
3401 return q.all()
3401 return q.all()
3402
3402
3403
3403
3404 class UserRepoToPerm(Base, BaseModel):
3404 class UserRepoToPerm(Base, BaseModel):
3405 __tablename__ = 'repo_to_perm'
3405 __tablename__ = 'repo_to_perm'
3406 __table_args__ = (
3406 __table_args__ = (
3407 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3407 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3408 base_table_args
3408 base_table_args
3409 )
3409 )
3410
3410
3411 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3411 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3412 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3412 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3413 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3413 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3414 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3414 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3415
3415
3416 user = relationship('User')
3416 user = relationship('User')
3417 repository = relationship('Repository')
3417 repository = relationship('Repository')
3418 permission = relationship('Permission')
3418 permission = relationship('Permission')
3419
3419
3420 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3420 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3421
3421
3422 @classmethod
3422 @classmethod
3423 def create(cls, user, repository, permission):
3423 def create(cls, user, repository, permission):
3424 n = cls()
3424 n = cls()
3425 n.user = user
3425 n.user = user
3426 n.repository = repository
3426 n.repository = repository
3427 n.permission = permission
3427 n.permission = permission
3428 Session().add(n)
3428 Session().add(n)
3429 return n
3429 return n
3430
3430
3431 def __unicode__(self):
3431 def __unicode__(self):
3432 return u'<%s => %s >' % (self.user, self.repository)
3432 return u'<%s => %s >' % (self.user, self.repository)
3433
3433
3434
3434
3435 class UserUserGroupToPerm(Base, BaseModel):
3435 class UserUserGroupToPerm(Base, BaseModel):
3436 __tablename__ = 'user_user_group_to_perm'
3436 __tablename__ = 'user_user_group_to_perm'
3437 __table_args__ = (
3437 __table_args__ = (
3438 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3438 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3439 base_table_args
3439 base_table_args
3440 )
3440 )
3441
3441
3442 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3442 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3443 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3443 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3444 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3444 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3445 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3445 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3446
3446
3447 user = relationship('User')
3447 user = relationship('User')
3448 user_group = relationship('UserGroup')
3448 user_group = relationship('UserGroup')
3449 permission = relationship('Permission')
3449 permission = relationship('Permission')
3450
3450
3451 @classmethod
3451 @classmethod
3452 def create(cls, user, user_group, permission):
3452 def create(cls, user, user_group, permission):
3453 n = cls()
3453 n = cls()
3454 n.user = user
3454 n.user = user
3455 n.user_group = user_group
3455 n.user_group = user_group
3456 n.permission = permission
3456 n.permission = permission
3457 Session().add(n)
3457 Session().add(n)
3458 return n
3458 return n
3459
3459
3460 def __unicode__(self):
3460 def __unicode__(self):
3461 return u'<%s => %s >' % (self.user, self.user_group)
3461 return u'<%s => %s >' % (self.user, self.user_group)
3462
3462
3463
3463
3464 class UserToPerm(Base, BaseModel):
3464 class UserToPerm(Base, BaseModel):
3465 __tablename__ = 'user_to_perm'
3465 __tablename__ = 'user_to_perm'
3466 __table_args__ = (
3466 __table_args__ = (
3467 UniqueConstraint('user_id', 'permission_id'),
3467 UniqueConstraint('user_id', 'permission_id'),
3468 base_table_args
3468 base_table_args
3469 )
3469 )
3470
3470
3471 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3471 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3472 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3472 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3473 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3473 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3474
3474
3475 user = relationship('User')
3475 user = relationship('User')
3476 permission = relationship('Permission', lazy='joined')
3476 permission = relationship('Permission', lazy='joined')
3477
3477
3478 def __unicode__(self):
3478 def __unicode__(self):
3479 return u'<%s => %s >' % (self.user, self.permission)
3479 return u'<%s => %s >' % (self.user, self.permission)
3480
3480
3481
3481
3482 class UserGroupRepoToPerm(Base, BaseModel):
3482 class UserGroupRepoToPerm(Base, BaseModel):
3483 __tablename__ = 'users_group_repo_to_perm'
3483 __tablename__ = 'users_group_repo_to_perm'
3484 __table_args__ = (
3484 __table_args__ = (
3485 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3485 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3486 base_table_args
3486 base_table_args
3487 )
3487 )
3488
3488
3489 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3489 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3490 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3490 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3491 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3491 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3492 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3492 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3493
3493
3494 users_group = relationship('UserGroup')
3494 users_group = relationship('UserGroup')
3495 permission = relationship('Permission')
3495 permission = relationship('Permission')
3496 repository = relationship('Repository')
3496 repository = relationship('Repository')
3497 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3497 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3498
3498
3499 @classmethod
3499 @classmethod
3500 def create(cls, users_group, repository, permission):
3500 def create(cls, users_group, repository, permission):
3501 n = cls()
3501 n = cls()
3502 n.users_group = users_group
3502 n.users_group = users_group
3503 n.repository = repository
3503 n.repository = repository
3504 n.permission = permission
3504 n.permission = permission
3505 Session().add(n)
3505 Session().add(n)
3506 return n
3506 return n
3507
3507
3508 def __unicode__(self):
3508 def __unicode__(self):
3509 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3509 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3510
3510
3511
3511
3512 class UserGroupUserGroupToPerm(Base, BaseModel):
3512 class UserGroupUserGroupToPerm(Base, BaseModel):
3513 __tablename__ = 'user_group_user_group_to_perm'
3513 __tablename__ = 'user_group_user_group_to_perm'
3514 __table_args__ = (
3514 __table_args__ = (
3515 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3515 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3516 CheckConstraint('target_user_group_id != user_group_id'),
3516 CheckConstraint('target_user_group_id != user_group_id'),
3517 base_table_args
3517 base_table_args
3518 )
3518 )
3519
3519
3520 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)
3520 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)
3521 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3521 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3523 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3523 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3524
3524
3525 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3525 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3526 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3526 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3527 permission = relationship('Permission')
3527 permission = relationship('Permission')
3528
3528
3529 @classmethod
3529 @classmethod
3530 def create(cls, target_user_group, user_group, permission):
3530 def create(cls, target_user_group, user_group, permission):
3531 n = cls()
3531 n = cls()
3532 n.target_user_group = target_user_group
3532 n.target_user_group = target_user_group
3533 n.user_group = user_group
3533 n.user_group = user_group
3534 n.permission = permission
3534 n.permission = permission
3535 Session().add(n)
3535 Session().add(n)
3536 return n
3536 return n
3537
3537
3538 def __unicode__(self):
3538 def __unicode__(self):
3539 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3539 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3540
3540
3541
3541
3542 class UserGroupToPerm(Base, BaseModel):
3542 class UserGroupToPerm(Base, BaseModel):
3543 __tablename__ = 'users_group_to_perm'
3543 __tablename__ = 'users_group_to_perm'
3544 __table_args__ = (
3544 __table_args__ = (
3545 UniqueConstraint('users_group_id', 'permission_id',),
3545 UniqueConstraint('users_group_id', 'permission_id',),
3546 base_table_args
3546 base_table_args
3547 )
3547 )
3548
3548
3549 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3549 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3550 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3550 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3551 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3551 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3552
3552
3553 users_group = relationship('UserGroup')
3553 users_group = relationship('UserGroup')
3554 permission = relationship('Permission')
3554 permission = relationship('Permission')
3555
3555
3556
3556
3557 class UserRepoGroupToPerm(Base, BaseModel):
3557 class UserRepoGroupToPerm(Base, BaseModel):
3558 __tablename__ = 'user_repo_group_to_perm'
3558 __tablename__ = 'user_repo_group_to_perm'
3559 __table_args__ = (
3559 __table_args__ = (
3560 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3560 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3561 base_table_args
3561 base_table_args
3562 )
3562 )
3563
3563
3564 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3564 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3565 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3565 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3566 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3566 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3567 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3567 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3568
3568
3569 user = relationship('User')
3569 user = relationship('User')
3570 group = relationship('RepoGroup')
3570 group = relationship('RepoGroup')
3571 permission = relationship('Permission')
3571 permission = relationship('Permission')
3572
3572
3573 @classmethod
3573 @classmethod
3574 def create(cls, user, repository_group, permission):
3574 def create(cls, user, repository_group, permission):
3575 n = cls()
3575 n = cls()
3576 n.user = user
3576 n.user = user
3577 n.group = repository_group
3577 n.group = repository_group
3578 n.permission = permission
3578 n.permission = permission
3579 Session().add(n)
3579 Session().add(n)
3580 return n
3580 return n
3581
3581
3582
3582
3583 class UserGroupRepoGroupToPerm(Base, BaseModel):
3583 class UserGroupRepoGroupToPerm(Base, BaseModel):
3584 __tablename__ = 'users_group_repo_group_to_perm'
3584 __tablename__ = 'users_group_repo_group_to_perm'
3585 __table_args__ = (
3585 __table_args__ = (
3586 UniqueConstraint('users_group_id', 'group_id'),
3586 UniqueConstraint('users_group_id', 'group_id'),
3587 base_table_args
3587 base_table_args
3588 )
3588 )
3589
3589
3590 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)
3590 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)
3591 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3591 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3592 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3592 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3593 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3593 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3594
3594
3595 users_group = relationship('UserGroup')
3595 users_group = relationship('UserGroup')
3596 permission = relationship('Permission')
3596 permission = relationship('Permission')
3597 group = relationship('RepoGroup')
3597 group = relationship('RepoGroup')
3598
3598
3599 @classmethod
3599 @classmethod
3600 def create(cls, user_group, repository_group, permission):
3600 def create(cls, user_group, repository_group, permission):
3601 n = cls()
3601 n = cls()
3602 n.users_group = user_group
3602 n.users_group = user_group
3603 n.group = repository_group
3603 n.group = repository_group
3604 n.permission = permission
3604 n.permission = permission
3605 Session().add(n)
3605 Session().add(n)
3606 return n
3606 return n
3607
3607
3608 def __unicode__(self):
3608 def __unicode__(self):
3609 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3609 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3610
3610
3611
3611
3612 class Statistics(Base, BaseModel):
3612 class Statistics(Base, BaseModel):
3613 __tablename__ = 'statistics'
3613 __tablename__ = 'statistics'
3614 __table_args__ = (
3614 __table_args__ = (
3615 base_table_args
3615 base_table_args
3616 )
3616 )
3617
3617
3618 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3618 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3619 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3619 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3620 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3620 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3621 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3621 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3622 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3622 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3623 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3623 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3624
3624
3625 repository = relationship('Repository', single_parent=True)
3625 repository = relationship('Repository', single_parent=True)
3626
3626
3627
3627
3628 class UserFollowing(Base, BaseModel):
3628 class UserFollowing(Base, BaseModel):
3629 __tablename__ = 'user_followings'
3629 __tablename__ = 'user_followings'
3630 __table_args__ = (
3630 __table_args__ = (
3631 UniqueConstraint('user_id', 'follows_repository_id'),
3631 UniqueConstraint('user_id', 'follows_repository_id'),
3632 UniqueConstraint('user_id', 'follows_user_id'),
3632 UniqueConstraint('user_id', 'follows_user_id'),
3633 base_table_args
3633 base_table_args
3634 )
3634 )
3635
3635
3636 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3636 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3637 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3637 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3638 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3638 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3639 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3639 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3640 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3640 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3641
3641
3642 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3642 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3643
3643
3644 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3644 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3645 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3645 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3646
3646
3647 @classmethod
3647 @classmethod
3648 def get_repo_followers(cls, repo_id):
3648 def get_repo_followers(cls, repo_id):
3649 return cls.query().filter(cls.follows_repo_id == repo_id)
3649 return cls.query().filter(cls.follows_repo_id == repo_id)
3650
3650
3651
3651
3652 class CacheKey(Base, BaseModel):
3652 class CacheKey(Base, BaseModel):
3653 __tablename__ = 'cache_invalidation'
3653 __tablename__ = 'cache_invalidation'
3654 __table_args__ = (
3654 __table_args__ = (
3655 UniqueConstraint('cache_key'),
3655 UniqueConstraint('cache_key'),
3656 Index('key_idx', 'cache_key'),
3656 Index('key_idx', 'cache_key'),
3657 base_table_args,
3657 base_table_args,
3658 )
3658 )
3659
3659
3660 CACHE_TYPE_FEED = 'FEED'
3660 CACHE_TYPE_FEED = 'FEED'
3661
3661
3662 # namespaces used to register process/thread aware caches
3662 # namespaces used to register process/thread aware caches
3663 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3663 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3664 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3664 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3665
3665
3666 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3666 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3667 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3667 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3668 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3668 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3669 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3669 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3670 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3670 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3671
3671
3672 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3672 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3673 self.cache_key = cache_key
3673 self.cache_key = cache_key
3674 self.cache_args = cache_args
3674 self.cache_args = cache_args
3675 self.cache_active = False
3675 self.cache_active = False
3676 # first key should be same for all entries, since all workers should share it
3676 # first key should be same for all entries, since all workers should share it
3677 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3677 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3678
3678
3679 def __unicode__(self):
3679 def __unicode__(self):
3680 return u"<%s('%s:%s[%s]')>" % (
3680 return u"<%s('%s:%s[%s]')>" % (
3681 self.__class__.__name__,
3681 self.__class__.__name__,
3682 self.cache_id, self.cache_key, self.cache_active)
3682 self.cache_id, self.cache_key, self.cache_active)
3683
3683
3684 def _cache_key_partition(self):
3684 def _cache_key_partition(self):
3685 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3685 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3686 return prefix, repo_name, suffix
3686 return prefix, repo_name, suffix
3687
3687
3688 def get_prefix(self):
3688 def get_prefix(self):
3689 """
3689 """
3690 Try to extract prefix from existing cache key. The key could consist
3690 Try to extract prefix from existing cache key. The key could consist
3691 of prefix, repo_name, suffix
3691 of prefix, repo_name, suffix
3692 """
3692 """
3693 # this returns prefix, repo_name, suffix
3693 # this returns prefix, repo_name, suffix
3694 return self._cache_key_partition()[0]
3694 return self._cache_key_partition()[0]
3695
3695
3696 def get_suffix(self):
3696 def get_suffix(self):
3697 """
3697 """
3698 get suffix that might have been used in _get_cache_key to
3698 get suffix that might have been used in _get_cache_key to
3699 generate self.cache_key. Only used for informational purposes
3699 generate self.cache_key. Only used for informational purposes
3700 in repo_edit.mako.
3700 in repo_edit.mako.
3701 """
3701 """
3702 # prefix, repo_name, suffix
3702 # prefix, repo_name, suffix
3703 return self._cache_key_partition()[2]
3703 return self._cache_key_partition()[2]
3704
3704
3705 @classmethod
3705 @classmethod
3706 def generate_new_state_uid(cls, based_on=None):
3706 def generate_new_state_uid(cls, based_on=None):
3707 if based_on:
3707 if based_on:
3708 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3708 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3709 else:
3709 else:
3710 return str(uuid.uuid4())
3710 return str(uuid.uuid4())
3711
3711
3712 @classmethod
3712 @classmethod
3713 def delete_all_cache(cls):
3713 def delete_all_cache(cls):
3714 """
3714 """
3715 Delete all cache keys from database.
3715 Delete all cache keys from database.
3716 Should only be run when all instances are down and all entries
3716 Should only be run when all instances are down and all entries
3717 thus stale.
3717 thus stale.
3718 """
3718 """
3719 cls.query().delete()
3719 cls.query().delete()
3720 Session().commit()
3720 Session().commit()
3721
3721
3722 @classmethod
3722 @classmethod
3723 def set_invalidate(cls, cache_uid, delete=False):
3723 def set_invalidate(cls, cache_uid, delete=False):
3724 """
3724 """
3725 Mark all caches of a repo as invalid in the database.
3725 Mark all caches of a repo as invalid in the database.
3726 """
3726 """
3727
3727
3728 try:
3728 try:
3729 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3729 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3730 if delete:
3730 if delete:
3731 qry.delete()
3731 qry.delete()
3732 log.debug('cache objects deleted for cache args %s',
3732 log.debug('cache objects deleted for cache args %s',
3733 safe_str(cache_uid))
3733 safe_str(cache_uid))
3734 else:
3734 else:
3735 qry.update({"cache_active": False,
3735 qry.update({"cache_active": False,
3736 "cache_state_uid": cls.generate_new_state_uid()})
3736 "cache_state_uid": cls.generate_new_state_uid()})
3737 log.debug('cache objects marked as invalid for cache args %s',
3737 log.debug('cache objects marked as invalid for cache args %s',
3738 safe_str(cache_uid))
3738 safe_str(cache_uid))
3739
3739
3740 Session().commit()
3740 Session().commit()
3741 except Exception:
3741 except Exception:
3742 log.exception(
3742 log.exception(
3743 'Cache key invalidation failed for cache args %s',
3743 'Cache key invalidation failed for cache args %s',
3744 safe_str(cache_uid))
3744 safe_str(cache_uid))
3745 Session().rollback()
3745 Session().rollback()
3746
3746
3747 @classmethod
3747 @classmethod
3748 def get_active_cache(cls, cache_key):
3748 def get_active_cache(cls, cache_key):
3749 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3749 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3750 if inv_obj:
3750 if inv_obj:
3751 return inv_obj
3751 return inv_obj
3752 return None
3752 return None
3753
3753
3754 @classmethod
3754 @classmethod
3755 def get_namespace_map(cls, namespace):
3755 def get_namespace_map(cls, namespace):
3756 return {
3756 return {
3757 x.cache_key: x
3757 x.cache_key: x
3758 for x in cls.query().filter(cls.cache_args == namespace)}
3758 for x in cls.query().filter(cls.cache_args == namespace)}
3759
3759
3760
3760
3761 class ChangesetComment(Base, BaseModel):
3761 class ChangesetComment(Base, BaseModel):
3762 __tablename__ = 'changeset_comments'
3762 __tablename__ = 'changeset_comments'
3763 __table_args__ = (
3763 __table_args__ = (
3764 Index('cc_revision_idx', 'revision'),
3764 Index('cc_revision_idx', 'revision'),
3765 base_table_args,
3765 base_table_args,
3766 )
3766 )
3767
3767
3768 COMMENT_OUTDATED = u'comment_outdated'
3768 COMMENT_OUTDATED = u'comment_outdated'
3769 COMMENT_TYPE_NOTE = u'note'
3769 COMMENT_TYPE_NOTE = u'note'
3770 COMMENT_TYPE_TODO = u'todo'
3770 COMMENT_TYPE_TODO = u'todo'
3771 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3771 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3772
3772
3773 OP_IMMUTABLE = u'immutable'
3773 OP_IMMUTABLE = u'immutable'
3774 OP_CHANGEABLE = u'changeable'
3774 OP_CHANGEABLE = u'changeable'
3775
3775
3776 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3776 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3777 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3777 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3778 revision = Column('revision', String(40), nullable=True)
3778 revision = Column('revision', String(40), nullable=True)
3779 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3779 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3780 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3780 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3781 line_no = Column('line_no', Unicode(10), nullable=True)
3781 line_no = Column('line_no', Unicode(10), nullable=True)
3782 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3782 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3783 f_path = Column('f_path', Unicode(1000), nullable=True)
3783 f_path = Column('f_path', Unicode(1000), nullable=True)
3784 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3784 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3785 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3785 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3786 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3786 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3787 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3787 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3788 renderer = Column('renderer', Unicode(64), nullable=True)
3788 renderer = Column('renderer', Unicode(64), nullable=True)
3789 display_state = Column('display_state', Unicode(128), nullable=True)
3789 display_state = Column('display_state', Unicode(128), nullable=True)
3790 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3790 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3791 draft = Column('draft', Boolean(), nullable=True, default=False)
3791 draft = Column('draft', Boolean(), nullable=True, default=False)
3792
3792
3793 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3793 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3794 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3794 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3795
3795
3796 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3796 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3797 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3797 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3798
3798
3799 author = relationship('User', lazy='select')
3799 author = relationship('User', lazy='select')
3800 repo = relationship('Repository')
3800 repo = relationship('Repository')
3801 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select')
3801 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select')
3802 pull_request = relationship('PullRequest', lazy='select')
3802 pull_request = relationship('PullRequest', lazy='select')
3803 pull_request_version = relationship('PullRequestVersion', lazy='select')
3803 pull_request_version = relationship('PullRequestVersion', lazy='select')
3804 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version')
3804 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version')
3805
3805
3806 @classmethod
3806 @classmethod
3807 def get_users(cls, revision=None, pull_request_id=None):
3807 def get_users(cls, revision=None, pull_request_id=None):
3808 """
3808 """
3809 Returns user associated with this ChangesetComment. ie those
3809 Returns user associated with this ChangesetComment. ie those
3810 who actually commented
3810 who actually commented
3811
3811
3812 :param cls:
3812 :param cls:
3813 :param revision:
3813 :param revision:
3814 """
3814 """
3815 q = Session().query(User)\
3815 q = Session().query(User)\
3816 .join(ChangesetComment.author)
3816 .join(ChangesetComment.author)
3817 if revision:
3817 if revision:
3818 q = q.filter(cls.revision == revision)
3818 q = q.filter(cls.revision == revision)
3819 elif pull_request_id:
3819 elif pull_request_id:
3820 q = q.filter(cls.pull_request_id == pull_request_id)
3820 q = q.filter(cls.pull_request_id == pull_request_id)
3821 return q.all()
3821 return q.all()
3822
3822
3823 @classmethod
3823 @classmethod
3824 def get_index_from_version(cls, pr_version, versions=None, num_versions=None):
3824 def get_index_from_version(cls, pr_version, versions=None, num_versions=None):
3825
3825
3826 if versions is not None:
3826 if versions is not None:
3827 num_versions = [x.pull_request_version_id for x in versions]
3827 num_versions = [x.pull_request_version_id for x in versions]
3828
3828
3829 num_versions = num_versions or []
3829 num_versions = num_versions or []
3830 try:
3830 try:
3831 return num_versions.index(pr_version) + 1
3831 return num_versions.index(pr_version) + 1
3832 except (IndexError, ValueError):
3832 except (IndexError, ValueError):
3833 return
3833 return
3834
3834
3835 @property
3835 @property
3836 def outdated(self):
3836 def outdated(self):
3837 return self.display_state == self.COMMENT_OUTDATED
3837 return self.display_state == self.COMMENT_OUTDATED
3838
3838
3839 @property
3839 @property
3840 def outdated_js(self):
3840 def outdated_js(self):
3841 return json.dumps(self.display_state == self.COMMENT_OUTDATED)
3841 return json.dumps(self.display_state == self.COMMENT_OUTDATED)
3842
3842
3843 @property
3843 @property
3844 def immutable(self):
3844 def immutable(self):
3845 return self.immutable_state == self.OP_IMMUTABLE
3845 return self.immutable_state == self.OP_IMMUTABLE
3846
3846
3847 def outdated_at_version(self, version):
3847 def outdated_at_version(self, version):
3848 """
3848 """
3849 Checks if comment is outdated for given pull request version
3849 Checks if comment is outdated for given pull request version
3850 """
3850 """
3851 def version_check():
3851 def version_check():
3852 return self.pull_request_version_id and self.pull_request_version_id != version
3852 return self.pull_request_version_id and self.pull_request_version_id != version
3853
3853
3854 if self.is_inline:
3854 if self.is_inline:
3855 return self.outdated and version_check()
3855 return self.outdated and version_check()
3856 else:
3856 else:
3857 # general comments don't have .outdated set, also latest don't have a version
3857 # general comments don't have .outdated set, also latest don't have a version
3858 return version_check()
3858 return version_check()
3859
3859
3860 def outdated_at_version_js(self, version):
3860 def outdated_at_version_js(self, version):
3861 """
3861 """
3862 Checks if comment is outdated for given pull request version
3862 Checks if comment is outdated for given pull request version
3863 """
3863 """
3864 return json.dumps(self.outdated_at_version(version))
3864 return json.dumps(self.outdated_at_version(version))
3865
3865
3866 def older_than_version(self, version):
3866 def older_than_version(self, version):
3867 """
3867 """
3868 Checks if comment is made from previous version than given
3868 Checks if comment is made from previous version than given
3869 """
3869 """
3870 if version is None:
3870 if version is None:
3871 return self.pull_request_version != version
3871 return self.pull_request_version != version
3872
3872
3873 return self.pull_request_version < version
3873 return self.pull_request_version < version
3874
3874
3875 def older_than_version_js(self, version):
3875 def older_than_version_js(self, version):
3876 """
3876 """
3877 Checks if comment is made from previous version than given
3877 Checks if comment is made from previous version than given
3878 """
3878 """
3879 return json.dumps(self.older_than_version(version))
3879 return json.dumps(self.older_than_version(version))
3880
3880
3881 @property
3881 @property
3882 def commit_id(self):
3882 def commit_id(self):
3883 """New style naming to stop using .revision"""
3883 """New style naming to stop using .revision"""
3884 return self.revision
3884 return self.revision
3885
3885
3886 @property
3886 @property
3887 def resolved(self):
3887 def resolved(self):
3888 return self.resolved_by[0] if self.resolved_by else None
3888 return self.resolved_by[0] if self.resolved_by else None
3889
3889
3890 @property
3890 @property
3891 def is_todo(self):
3891 def is_todo(self):
3892 return self.comment_type == self.COMMENT_TYPE_TODO
3892 return self.comment_type == self.COMMENT_TYPE_TODO
3893
3893
3894 @property
3894 @property
3895 def is_inline(self):
3895 def is_inline(self):
3896 if self.line_no and self.f_path:
3896 if self.line_no and self.f_path:
3897 return True
3897 return True
3898 return False
3898 return False
3899
3899
3900 @property
3900 @property
3901 def last_version(self):
3901 def last_version(self):
3902 version = 0
3902 version = 0
3903 if self.history:
3903 if self.history:
3904 version = self.history[-1].version
3904 version = self.history[-1].version
3905 return version
3905 return version
3906
3906
3907 def get_index_version(self, versions):
3907 def get_index_version(self, versions):
3908 return self.get_index_from_version(
3908 return self.get_index_from_version(
3909 self.pull_request_version_id, versions)
3909 self.pull_request_version_id, versions)
3910
3910
3911 @property
3911 @property
3912 def review_status(self):
3912 def review_status(self):
3913 if self.status_change:
3913 if self.status_change:
3914 return self.status_change[0].status
3914 return self.status_change[0].status
3915
3915
3916 @property
3916 @property
3917 def review_status_lbl(self):
3917 def review_status_lbl(self):
3918 if self.status_change:
3918 if self.status_change:
3919 return self.status_change[0].status_lbl
3919 return self.status_change[0].status_lbl
3920
3920
3921 def __repr__(self):
3921 def __repr__(self):
3922 if self.comment_id:
3922 if self.comment_id:
3923 return '<DB:Comment #%s>' % self.comment_id
3923 return '<DB:Comment #%s>' % self.comment_id
3924 else:
3924 else:
3925 return '<DB:Comment at %#x>' % id(self)
3925 return '<DB:Comment at %#x>' % id(self)
3926
3926
3927 def get_api_data(self):
3927 def get_api_data(self):
3928 comment = self
3928 comment = self
3929
3929
3930 data = {
3930 data = {
3931 'comment_id': comment.comment_id,
3931 'comment_id': comment.comment_id,
3932 'comment_type': comment.comment_type,
3932 'comment_type': comment.comment_type,
3933 'comment_text': comment.text,
3933 'comment_text': comment.text,
3934 'comment_status': comment.status_change,
3934 'comment_status': comment.status_change,
3935 'comment_f_path': comment.f_path,
3935 'comment_f_path': comment.f_path,
3936 'comment_lineno': comment.line_no,
3936 'comment_lineno': comment.line_no,
3937 'comment_author': comment.author,
3937 'comment_author': comment.author,
3938 'comment_created_on': comment.created_on,
3938 'comment_created_on': comment.created_on,
3939 'comment_resolved_by': self.resolved,
3939 'comment_resolved_by': self.resolved,
3940 'comment_commit_id': comment.revision,
3940 'comment_commit_id': comment.revision,
3941 'comment_pull_request_id': comment.pull_request_id,
3941 'comment_pull_request_id': comment.pull_request_id,
3942 'comment_last_version': self.last_version
3942 'comment_last_version': self.last_version
3943 }
3943 }
3944 return data
3944 return data
3945
3945
3946 def __json__(self):
3946 def __json__(self):
3947 data = dict()
3947 data = dict()
3948 data.update(self.get_api_data())
3948 data.update(self.get_api_data())
3949 return data
3949 return data
3950
3950
3951
3951
3952 class ChangesetCommentHistory(Base, BaseModel):
3952 class ChangesetCommentHistory(Base, BaseModel):
3953 __tablename__ = 'changeset_comments_history'
3953 __tablename__ = 'changeset_comments_history'
3954 __table_args__ = (
3954 __table_args__ = (
3955 Index('cch_comment_id_idx', 'comment_id'),
3955 Index('cch_comment_id_idx', 'comment_id'),
3956 base_table_args,
3956 base_table_args,
3957 )
3957 )
3958
3958
3959 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3959 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3960 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3960 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3961 version = Column("version", Integer(), nullable=False, default=0)
3961 version = Column("version", Integer(), nullable=False, default=0)
3962 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3962 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3963 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3963 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3964 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3964 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3965 deleted = Column('deleted', Boolean(), default=False)
3965 deleted = Column('deleted', Boolean(), default=False)
3966
3966
3967 author = relationship('User', lazy='joined')
3967 author = relationship('User', lazy='joined')
3968 comment = relationship('ChangesetComment', cascade="all, delete")
3968 comment = relationship('ChangesetComment', cascade="all, delete")
3969
3969
3970 @classmethod
3970 @classmethod
3971 def get_version(cls, comment_id):
3971 def get_version(cls, comment_id):
3972 q = Session().query(ChangesetCommentHistory).filter(
3972 q = Session().query(ChangesetCommentHistory).filter(
3973 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
3973 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
3974 if q.count() == 0:
3974 if q.count() == 0:
3975 return 1
3975 return 1
3976 elif q.count() >= q[0].version:
3976 elif q.count() >= q[0].version:
3977 return q.count() + 1
3977 return q.count() + 1
3978 else:
3978 else:
3979 return q[0].version + 1
3979 return q[0].version + 1
3980
3980
3981
3981
3982 class ChangesetStatus(Base, BaseModel):
3982 class ChangesetStatus(Base, BaseModel):
3983 __tablename__ = 'changeset_statuses'
3983 __tablename__ = 'changeset_statuses'
3984 __table_args__ = (
3984 __table_args__ = (
3985 Index('cs_revision_idx', 'revision'),
3985 Index('cs_revision_idx', 'revision'),
3986 Index('cs_version_idx', 'version'),
3986 Index('cs_version_idx', 'version'),
3987 UniqueConstraint('repo_id', 'revision', 'version'),
3987 UniqueConstraint('repo_id', 'revision', 'version'),
3988 base_table_args
3988 base_table_args
3989 )
3989 )
3990
3990
3991 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3991 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3992 STATUS_APPROVED = 'approved'
3992 STATUS_APPROVED = 'approved'
3993 STATUS_REJECTED = 'rejected'
3993 STATUS_REJECTED = 'rejected'
3994 STATUS_UNDER_REVIEW = 'under_review'
3994 STATUS_UNDER_REVIEW = 'under_review'
3995 CheckConstraint,
3995 CheckConstraint,
3996 STATUSES = [
3996 STATUSES = [
3997 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3997 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3998 (STATUS_APPROVED, _("Approved")),
3998 (STATUS_APPROVED, _("Approved")),
3999 (STATUS_REJECTED, _("Rejected")),
3999 (STATUS_REJECTED, _("Rejected")),
4000 (STATUS_UNDER_REVIEW, _("Under Review")),
4000 (STATUS_UNDER_REVIEW, _("Under Review")),
4001 ]
4001 ]
4002
4002
4003 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4003 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4004 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4004 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4005 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4005 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4006 revision = Column('revision', String(40), nullable=False)
4006 revision = Column('revision', String(40), nullable=False)
4007 status = Column('status', String(128), nullable=False, default=DEFAULT)
4007 status = Column('status', String(128), nullable=False, default=DEFAULT)
4008 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4008 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4009 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4009 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4010 version = Column('version', Integer(), nullable=False, default=0)
4010 version = Column('version', Integer(), nullable=False, default=0)
4011 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4011 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4012
4012
4013 author = relationship('User', lazy='select')
4013 author = relationship('User', lazy='select')
4014 repo = relationship('Repository', lazy='select')
4014 repo = relationship('Repository', lazy='select')
4015 comment = relationship('ChangesetComment', lazy='select')
4015 comment = relationship('ChangesetComment', lazy='select')
4016 pull_request = relationship('PullRequest', lazy='select')
4016 pull_request = relationship('PullRequest', lazy='select')
4017
4017
4018 def __unicode__(self):
4018 def __unicode__(self):
4019 return u"<%s('%s[v%s]:%s')>" % (
4019 return u"<%s('%s[v%s]:%s')>" % (
4020 self.__class__.__name__,
4020 self.__class__.__name__,
4021 self.status, self.version, self.author
4021 self.status, self.version, self.author
4022 )
4022 )
4023
4023
4024 @classmethod
4024 @classmethod
4025 def get_status_lbl(cls, value):
4025 def get_status_lbl(cls, value):
4026 return dict(cls.STATUSES).get(value)
4026 return dict(cls.STATUSES).get(value)
4027
4027
4028 @property
4028 @property
4029 def status_lbl(self):
4029 def status_lbl(self):
4030 return ChangesetStatus.get_status_lbl(self.status)
4030 return ChangesetStatus.get_status_lbl(self.status)
4031
4031
4032 def get_api_data(self):
4032 def get_api_data(self):
4033 status = self
4033 status = self
4034 data = {
4034 data = {
4035 'status_id': status.changeset_status_id,
4035 'status_id': status.changeset_status_id,
4036 'status': status.status,
4036 'status': status.status,
4037 }
4037 }
4038 return data
4038 return data
4039
4039
4040 def __json__(self):
4040 def __json__(self):
4041 data = dict()
4041 data = dict()
4042 data.update(self.get_api_data())
4042 data.update(self.get_api_data())
4043 return data
4043 return data
4044
4044
4045
4045
4046 class _SetState(object):
4046 class _SetState(object):
4047 """
4047 """
4048 Context processor allowing changing state for sensitive operation such as
4048 Context processor allowing changing state for sensitive operation such as
4049 pull request update or merge
4049 pull request update or merge
4050 """
4050 """
4051
4051
4052 def __init__(self, pull_request, pr_state, back_state=None):
4052 def __init__(self, pull_request, pr_state, back_state=None):
4053 self._pr = pull_request
4053 self._pr = pull_request
4054 self._org_state = back_state or pull_request.pull_request_state
4054 self._org_state = back_state or pull_request.pull_request_state
4055 self._pr_state = pr_state
4055 self._pr_state = pr_state
4056 self._current_state = None
4056 self._current_state = None
4057
4057
4058 def __enter__(self):
4058 def __enter__(self):
4059 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4059 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4060 self._pr, self._pr_state)
4060 self._pr, self._pr_state)
4061 self.set_pr_state(self._pr_state)
4061 self.set_pr_state(self._pr_state)
4062 return self
4062 return self
4063
4063
4064 def __exit__(self, exc_type, exc_val, exc_tb):
4064 def __exit__(self, exc_type, exc_val, exc_tb):
4065 if exc_val is not None:
4065 if exc_val is not None:
4066 log.error(traceback.format_exc(exc_tb))
4066 log.error(traceback.format_exc(exc_tb))
4067 return None
4067 return None
4068
4068
4069 self.set_pr_state(self._org_state)
4069 self.set_pr_state(self._org_state)
4070 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4070 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4071 self._pr, self._org_state)
4071 self._pr, self._org_state)
4072
4072
4073 @property
4073 @property
4074 def state(self):
4074 def state(self):
4075 return self._current_state
4075 return self._current_state
4076
4076
4077 def set_pr_state(self, pr_state):
4077 def set_pr_state(self, pr_state):
4078 try:
4078 try:
4079 self._pr.pull_request_state = pr_state
4079 self._pr.pull_request_state = pr_state
4080 Session().add(self._pr)
4080 Session().add(self._pr)
4081 Session().commit()
4081 Session().commit()
4082 self._current_state = pr_state
4082 self._current_state = pr_state
4083 except Exception:
4083 except Exception:
4084 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4084 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4085 raise
4085 raise
4086
4086
4087
4087
4088 class _PullRequestBase(BaseModel):
4088 class _PullRequestBase(BaseModel):
4089 """
4089 """
4090 Common attributes of pull request and version entries.
4090 Common attributes of pull request and version entries.
4091 """
4091 """
4092
4092
4093 # .status values
4093 # .status values
4094 STATUS_NEW = u'new'
4094 STATUS_NEW = u'new'
4095 STATUS_OPEN = u'open'
4095 STATUS_OPEN = u'open'
4096 STATUS_CLOSED = u'closed'
4096 STATUS_CLOSED = u'closed'
4097
4097
4098 # available states
4098 # available states
4099 STATE_CREATING = u'creating'
4099 STATE_CREATING = u'creating'
4100 STATE_UPDATING = u'updating'
4100 STATE_UPDATING = u'updating'
4101 STATE_MERGING = u'merging'
4101 STATE_MERGING = u'merging'
4102 STATE_CREATED = u'created'
4102 STATE_CREATED = u'created'
4103
4103
4104 title = Column('title', Unicode(255), nullable=True)
4104 title = Column('title', Unicode(255), nullable=True)
4105 description = Column(
4105 description = Column(
4106 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4106 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4107 nullable=True)
4107 nullable=True)
4108 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4108 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4109
4109
4110 # new/open/closed status of pull request (not approve/reject/etc)
4110 # new/open/closed status of pull request (not approve/reject/etc)
4111 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4111 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4112 created_on = Column(
4112 created_on = Column(
4113 'created_on', DateTime(timezone=False), nullable=False,
4113 'created_on', DateTime(timezone=False), nullable=False,
4114 default=datetime.datetime.now)
4114 default=datetime.datetime.now)
4115 updated_on = Column(
4115 updated_on = Column(
4116 'updated_on', DateTime(timezone=False), nullable=False,
4116 'updated_on', DateTime(timezone=False), nullable=False,
4117 default=datetime.datetime.now)
4117 default=datetime.datetime.now)
4118
4118
4119 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4119 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4120
4120
4121 @declared_attr
4121 @declared_attr
4122 def user_id(cls):
4122 def user_id(cls):
4123 return Column(
4123 return Column(
4124 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4124 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4125 unique=None)
4125 unique=None)
4126
4126
4127 # 500 revisions max
4127 # 500 revisions max
4128 _revisions = Column(
4128 _revisions = Column(
4129 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4129 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4130
4130
4131 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4131 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4132
4132
4133 @declared_attr
4133 @declared_attr
4134 def source_repo_id(cls):
4134 def source_repo_id(cls):
4135 # TODO: dan: rename column to source_repo_id
4135 # TODO: dan: rename column to source_repo_id
4136 return Column(
4136 return Column(
4137 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4137 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4138 nullable=False)
4138 nullable=False)
4139
4139
4140 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4140 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4141
4141
4142 @hybrid_property
4142 @hybrid_property
4143 def source_ref(self):
4143 def source_ref(self):
4144 return self._source_ref
4144 return self._source_ref
4145
4145
4146 @source_ref.setter
4146 @source_ref.setter
4147 def source_ref(self, val):
4147 def source_ref(self, val):
4148 parts = (val or '').split(':')
4148 parts = (val or '').split(':')
4149 if len(parts) != 3:
4149 if len(parts) != 3:
4150 raise ValueError(
4150 raise ValueError(
4151 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4151 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4152 self._source_ref = safe_unicode(val)
4152 self._source_ref = safe_unicode(val)
4153
4153
4154 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4154 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4155
4155
4156 @hybrid_property
4156 @hybrid_property
4157 def target_ref(self):
4157 def target_ref(self):
4158 return self._target_ref
4158 return self._target_ref
4159
4159
4160 @target_ref.setter
4160 @target_ref.setter
4161 def target_ref(self, val):
4161 def target_ref(self, val):
4162 parts = (val or '').split(':')
4162 parts = (val or '').split(':')
4163 if len(parts) != 3:
4163 if len(parts) != 3:
4164 raise ValueError(
4164 raise ValueError(
4165 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4165 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4166 self._target_ref = safe_unicode(val)
4166 self._target_ref = safe_unicode(val)
4167
4167
4168 @declared_attr
4168 @declared_attr
4169 def target_repo_id(cls):
4169 def target_repo_id(cls):
4170 # TODO: dan: rename column to target_repo_id
4170 # TODO: dan: rename column to target_repo_id
4171 return Column(
4171 return Column(
4172 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4172 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4173 nullable=False)
4173 nullable=False)
4174
4174
4175 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4175 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4176
4176
4177 # TODO: dan: rename column to last_merge_source_rev
4177 # TODO: dan: rename column to last_merge_source_rev
4178 _last_merge_source_rev = Column(
4178 _last_merge_source_rev = Column(
4179 'last_merge_org_rev', String(40), nullable=True)
4179 'last_merge_org_rev', String(40), nullable=True)
4180 # TODO: dan: rename column to last_merge_target_rev
4180 # TODO: dan: rename column to last_merge_target_rev
4181 _last_merge_target_rev = Column(
4181 _last_merge_target_rev = Column(
4182 'last_merge_other_rev', String(40), nullable=True)
4182 'last_merge_other_rev', String(40), nullable=True)
4183 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4183 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4184 last_merge_metadata = Column(
4184 last_merge_metadata = Column(
4185 'last_merge_metadata', MutationObj.as_mutable(
4185 'last_merge_metadata', MutationObj.as_mutable(
4186 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4186 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4187
4187
4188 merge_rev = Column('merge_rev', String(40), nullable=True)
4188 merge_rev = Column('merge_rev', String(40), nullable=True)
4189
4189
4190 reviewer_data = Column(
4190 reviewer_data = Column(
4191 'reviewer_data_json', MutationObj.as_mutable(
4191 'reviewer_data_json', MutationObj.as_mutable(
4192 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4192 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4193
4193
4194 @property
4194 @property
4195 def reviewer_data_json(self):
4195 def reviewer_data_json(self):
4196 return json.dumps(self.reviewer_data)
4196 return json.dumps(self.reviewer_data)
4197
4197
4198 @property
4198 @property
4199 def last_merge_metadata_parsed(self):
4199 def last_merge_metadata_parsed(self):
4200 metadata = {}
4200 metadata = {}
4201 if not self.last_merge_metadata:
4201 if not self.last_merge_metadata:
4202 return metadata
4202 return metadata
4203
4203
4204 if hasattr(self.last_merge_metadata, 'de_coerce'):
4204 if hasattr(self.last_merge_metadata, 'de_coerce'):
4205 for k, v in self.last_merge_metadata.de_coerce().items():
4205 for k, v in self.last_merge_metadata.de_coerce().items():
4206 if k in ['target_ref', 'source_ref']:
4206 if k in ['target_ref', 'source_ref']:
4207 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4207 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4208 else:
4208 else:
4209 if hasattr(v, 'de_coerce'):
4209 if hasattr(v, 'de_coerce'):
4210 metadata[k] = v.de_coerce()
4210 metadata[k] = v.de_coerce()
4211 else:
4211 else:
4212 metadata[k] = v
4212 metadata[k] = v
4213 return metadata
4213 return metadata
4214
4214
4215 @property
4215 @property
4216 def work_in_progress(self):
4216 def work_in_progress(self):
4217 """checks if pull request is work in progress by checking the title"""
4217 """checks if pull request is work in progress by checking the title"""
4218 title = self.title.upper()
4218 title = self.title.upper()
4219 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4219 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4220 return True
4220 return True
4221 return False
4221 return False
4222
4222
4223 @property
4224 def title_safe(self):
4225 return self.title\
4226 .replace('{', '{{')\
4227 .replace('}', '}}')
4228
4223 @hybrid_property
4229 @hybrid_property
4224 def description_safe(self):
4230 def description_safe(self):
4225 from rhodecode.lib import helpers as h
4231 from rhodecode.lib import helpers as h
4226 return h.escape(self.description)
4232 return h.escape(self.description)
4227
4233
4228 @hybrid_property
4234 @hybrid_property
4229 def revisions(self):
4235 def revisions(self):
4230 return self._revisions.split(':') if self._revisions else []
4236 return self._revisions.split(':') if self._revisions else []
4231
4237
4232 @revisions.setter
4238 @revisions.setter
4233 def revisions(self, val):
4239 def revisions(self, val):
4234 self._revisions = u':'.join(val)
4240 self._revisions = u':'.join(val)
4235
4241
4236 @hybrid_property
4242 @hybrid_property
4237 def last_merge_status(self):
4243 def last_merge_status(self):
4238 return safe_int(self._last_merge_status)
4244 return safe_int(self._last_merge_status)
4239
4245
4240 @last_merge_status.setter
4246 @last_merge_status.setter
4241 def last_merge_status(self, val):
4247 def last_merge_status(self, val):
4242 self._last_merge_status = val
4248 self._last_merge_status = val
4243
4249
4244 @declared_attr
4250 @declared_attr
4245 def author(cls):
4251 def author(cls):
4246 return relationship('User', lazy='joined')
4252 return relationship('User', lazy='joined')
4247
4253
4248 @declared_attr
4254 @declared_attr
4249 def source_repo(cls):
4255 def source_repo(cls):
4250 return relationship(
4256 return relationship(
4251 'Repository',
4257 'Repository',
4252 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4258 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4253
4259
4254 @property
4260 @property
4255 def source_ref_parts(self):
4261 def source_ref_parts(self):
4256 return self.unicode_to_reference(self.source_ref)
4262 return self.unicode_to_reference(self.source_ref)
4257
4263
4258 @declared_attr
4264 @declared_attr
4259 def target_repo(cls):
4265 def target_repo(cls):
4260 return relationship(
4266 return relationship(
4261 'Repository',
4267 'Repository',
4262 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4268 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4263
4269
4264 @property
4270 @property
4265 def target_ref_parts(self):
4271 def target_ref_parts(self):
4266 return self.unicode_to_reference(self.target_ref)
4272 return self.unicode_to_reference(self.target_ref)
4267
4273
4268 @property
4274 @property
4269 def shadow_merge_ref(self):
4275 def shadow_merge_ref(self):
4270 return self.unicode_to_reference(self._shadow_merge_ref)
4276 return self.unicode_to_reference(self._shadow_merge_ref)
4271
4277
4272 @shadow_merge_ref.setter
4278 @shadow_merge_ref.setter
4273 def shadow_merge_ref(self, ref):
4279 def shadow_merge_ref(self, ref):
4274 self._shadow_merge_ref = self.reference_to_unicode(ref)
4280 self._shadow_merge_ref = self.reference_to_unicode(ref)
4275
4281
4276 @staticmethod
4282 @staticmethod
4277 def unicode_to_reference(raw):
4283 def unicode_to_reference(raw):
4278 return unicode_to_reference(raw)
4284 return unicode_to_reference(raw)
4279
4285
4280 @staticmethod
4286 @staticmethod
4281 def reference_to_unicode(ref):
4287 def reference_to_unicode(ref):
4282 return reference_to_unicode(ref)
4288 return reference_to_unicode(ref)
4283
4289
4284 def get_api_data(self, with_merge_state=True):
4290 def get_api_data(self, with_merge_state=True):
4285 from rhodecode.model.pull_request import PullRequestModel
4291 from rhodecode.model.pull_request import PullRequestModel
4286
4292
4287 pull_request = self
4293 pull_request = self
4288 if with_merge_state:
4294 if with_merge_state:
4289 merge_response, merge_status, msg = \
4295 merge_response, merge_status, msg = \
4290 PullRequestModel().merge_status(pull_request)
4296 PullRequestModel().merge_status(pull_request)
4291 merge_state = {
4297 merge_state = {
4292 'status': merge_status,
4298 'status': merge_status,
4293 'message': safe_unicode(msg),
4299 'message': safe_unicode(msg),
4294 }
4300 }
4295 else:
4301 else:
4296 merge_state = {'status': 'not_available',
4302 merge_state = {'status': 'not_available',
4297 'message': 'not_available'}
4303 'message': 'not_available'}
4298
4304
4299 merge_data = {
4305 merge_data = {
4300 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4306 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4301 'reference': (
4307 'reference': (
4302 pull_request.shadow_merge_ref._asdict()
4308 pull_request.shadow_merge_ref._asdict()
4303 if pull_request.shadow_merge_ref else None),
4309 if pull_request.shadow_merge_ref else None),
4304 }
4310 }
4305
4311
4306 data = {
4312 data = {
4307 'pull_request_id': pull_request.pull_request_id,
4313 'pull_request_id': pull_request.pull_request_id,
4308 'url': PullRequestModel().get_url(pull_request),
4314 'url': PullRequestModel().get_url(pull_request),
4309 'title': pull_request.title,
4315 'title': pull_request.title,
4310 'description': pull_request.description,
4316 'description': pull_request.description,
4311 'status': pull_request.status,
4317 'status': pull_request.status,
4312 'state': pull_request.pull_request_state,
4318 'state': pull_request.pull_request_state,
4313 'created_on': pull_request.created_on,
4319 'created_on': pull_request.created_on,
4314 'updated_on': pull_request.updated_on,
4320 'updated_on': pull_request.updated_on,
4315 'commit_ids': pull_request.revisions,
4321 'commit_ids': pull_request.revisions,
4316 'review_status': pull_request.calculated_review_status(),
4322 'review_status': pull_request.calculated_review_status(),
4317 'mergeable': merge_state,
4323 'mergeable': merge_state,
4318 'source': {
4324 'source': {
4319 'clone_url': pull_request.source_repo.clone_url(),
4325 'clone_url': pull_request.source_repo.clone_url(),
4320 'repository': pull_request.source_repo.repo_name,
4326 'repository': pull_request.source_repo.repo_name,
4321 'reference': {
4327 'reference': {
4322 'name': pull_request.source_ref_parts.name,
4328 'name': pull_request.source_ref_parts.name,
4323 'type': pull_request.source_ref_parts.type,
4329 'type': pull_request.source_ref_parts.type,
4324 'commit_id': pull_request.source_ref_parts.commit_id,
4330 'commit_id': pull_request.source_ref_parts.commit_id,
4325 },
4331 },
4326 },
4332 },
4327 'target': {
4333 'target': {
4328 'clone_url': pull_request.target_repo.clone_url(),
4334 'clone_url': pull_request.target_repo.clone_url(),
4329 'repository': pull_request.target_repo.repo_name,
4335 'repository': pull_request.target_repo.repo_name,
4330 'reference': {
4336 'reference': {
4331 'name': pull_request.target_ref_parts.name,
4337 'name': pull_request.target_ref_parts.name,
4332 'type': pull_request.target_ref_parts.type,
4338 'type': pull_request.target_ref_parts.type,
4333 'commit_id': pull_request.target_ref_parts.commit_id,
4339 'commit_id': pull_request.target_ref_parts.commit_id,
4334 },
4340 },
4335 },
4341 },
4336 'merge': merge_data,
4342 'merge': merge_data,
4337 'author': pull_request.author.get_api_data(include_secrets=False,
4343 'author': pull_request.author.get_api_data(include_secrets=False,
4338 details='basic'),
4344 details='basic'),
4339 'reviewers': [
4345 'reviewers': [
4340 {
4346 {
4341 'user': reviewer.get_api_data(include_secrets=False,
4347 'user': reviewer.get_api_data(include_secrets=False,
4342 details='basic'),
4348 details='basic'),
4343 'reasons': reasons,
4349 'reasons': reasons,
4344 'review_status': st[0][1].status if st else 'not_reviewed',
4350 'review_status': st[0][1].status if st else 'not_reviewed',
4345 }
4351 }
4346 for obj, reviewer, reasons, mandatory, st in
4352 for obj, reviewer, reasons, mandatory, st in
4347 pull_request.reviewers_statuses()
4353 pull_request.reviewers_statuses()
4348 ]
4354 ]
4349 }
4355 }
4350
4356
4351 return data
4357 return data
4352
4358
4353 def set_state(self, pull_request_state, final_state=None):
4359 def set_state(self, pull_request_state, final_state=None):
4354 """
4360 """
4355 # goes from initial state to updating to initial state.
4361 # goes from initial state to updating to initial state.
4356 # initial state can be changed by specifying back_state=
4362 # initial state can be changed by specifying back_state=
4357 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4363 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4358 pull_request.merge()
4364 pull_request.merge()
4359
4365
4360 :param pull_request_state:
4366 :param pull_request_state:
4361 :param final_state:
4367 :param final_state:
4362
4368
4363 """
4369 """
4364
4370
4365 return _SetState(self, pull_request_state, back_state=final_state)
4371 return _SetState(self, pull_request_state, back_state=final_state)
4366
4372
4367
4373
4368 class PullRequest(Base, _PullRequestBase):
4374 class PullRequest(Base, _PullRequestBase):
4369 __tablename__ = 'pull_requests'
4375 __tablename__ = 'pull_requests'
4370 __table_args__ = (
4376 __table_args__ = (
4371 base_table_args,
4377 base_table_args,
4372 )
4378 )
4373 LATEST_VER = 'latest'
4379 LATEST_VER = 'latest'
4374
4380
4375 pull_request_id = Column(
4381 pull_request_id = Column(
4376 'pull_request_id', Integer(), nullable=False, primary_key=True)
4382 'pull_request_id', Integer(), nullable=False, primary_key=True)
4377
4383
4378 def __repr__(self):
4384 def __repr__(self):
4379 if self.pull_request_id:
4385 if self.pull_request_id:
4380 return '<DB:PullRequest #%s>' % self.pull_request_id
4386 return '<DB:PullRequest #%s>' % self.pull_request_id
4381 else:
4387 else:
4382 return '<DB:PullRequest at %#x>' % id(self)
4388 return '<DB:PullRequest at %#x>' % id(self)
4383
4389
4384 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4390 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4385 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4391 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4386 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4392 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4387 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4393 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4388 lazy='dynamic')
4394 lazy='dynamic')
4389
4395
4390 @classmethod
4396 @classmethod
4391 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4397 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4392 internal_methods=None):
4398 internal_methods=None):
4393
4399
4394 class PullRequestDisplay(object):
4400 class PullRequestDisplay(object):
4395 """
4401 """
4396 Special object wrapper for showing PullRequest data via Versions
4402 Special object wrapper for showing PullRequest data via Versions
4397 It mimics PR object as close as possible. This is read only object
4403 It mimics PR object as close as possible. This is read only object
4398 just for display
4404 just for display
4399 """
4405 """
4400
4406
4401 def __init__(self, attrs, internal=None):
4407 def __init__(self, attrs, internal=None):
4402 self.attrs = attrs
4408 self.attrs = attrs
4403 # internal have priority over the given ones via attrs
4409 # internal have priority over the given ones via attrs
4404 self.internal = internal or ['versions']
4410 self.internal = internal or ['versions']
4405
4411
4406 def __getattr__(self, item):
4412 def __getattr__(self, item):
4407 if item in self.internal:
4413 if item in self.internal:
4408 return getattr(self, item)
4414 return getattr(self, item)
4409 try:
4415 try:
4410 return self.attrs[item]
4416 return self.attrs[item]
4411 except KeyError:
4417 except KeyError:
4412 raise AttributeError(
4418 raise AttributeError(
4413 '%s object has no attribute %s' % (self, item))
4419 '%s object has no attribute %s' % (self, item))
4414
4420
4415 def __repr__(self):
4421 def __repr__(self):
4416 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4422 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4417
4423
4418 def versions(self):
4424 def versions(self):
4419 return pull_request_obj.versions.order_by(
4425 return pull_request_obj.versions.order_by(
4420 PullRequestVersion.pull_request_version_id).all()
4426 PullRequestVersion.pull_request_version_id).all()
4421
4427
4422 def is_closed(self):
4428 def is_closed(self):
4423 return pull_request_obj.is_closed()
4429 return pull_request_obj.is_closed()
4424
4430
4425 def is_state_changing(self):
4431 def is_state_changing(self):
4426 return pull_request_obj.is_state_changing()
4432 return pull_request_obj.is_state_changing()
4427
4433
4428 @property
4434 @property
4429 def pull_request_version_id(self):
4435 def pull_request_version_id(self):
4430 return getattr(pull_request_obj, 'pull_request_version_id', None)
4436 return getattr(pull_request_obj, 'pull_request_version_id', None)
4431
4437
4432 @property
4438 @property
4433 def pull_request_last_version(self):
4439 def pull_request_last_version(self):
4434 return pull_request_obj.pull_request_last_version
4440 return pull_request_obj.pull_request_last_version
4435
4441
4436 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4442 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4437
4443
4438 attrs.author = StrictAttributeDict(
4444 attrs.author = StrictAttributeDict(
4439 pull_request_obj.author.get_api_data())
4445 pull_request_obj.author.get_api_data())
4440 if pull_request_obj.target_repo:
4446 if pull_request_obj.target_repo:
4441 attrs.target_repo = StrictAttributeDict(
4447 attrs.target_repo = StrictAttributeDict(
4442 pull_request_obj.target_repo.get_api_data())
4448 pull_request_obj.target_repo.get_api_data())
4443 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4449 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4444
4450
4445 if pull_request_obj.source_repo:
4451 if pull_request_obj.source_repo:
4446 attrs.source_repo = StrictAttributeDict(
4452 attrs.source_repo = StrictAttributeDict(
4447 pull_request_obj.source_repo.get_api_data())
4453 pull_request_obj.source_repo.get_api_data())
4448 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4454 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4449
4455
4450 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4456 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4451 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4457 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4452 attrs.revisions = pull_request_obj.revisions
4458 attrs.revisions = pull_request_obj.revisions
4453 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4459 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4454 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4460 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4455 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4461 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4456 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4462 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4457
4463
4458 return PullRequestDisplay(attrs, internal=internal_methods)
4464 return PullRequestDisplay(attrs, internal=internal_methods)
4459
4465
4460 def is_closed(self):
4466 def is_closed(self):
4461 return self.status == self.STATUS_CLOSED
4467 return self.status == self.STATUS_CLOSED
4462
4468
4463 def is_state_changing(self):
4469 def is_state_changing(self):
4464 return self.pull_request_state != PullRequest.STATE_CREATED
4470 return self.pull_request_state != PullRequest.STATE_CREATED
4465
4471
4466 def __json__(self):
4472 def __json__(self):
4467 return {
4473 return {
4468 'revisions': self.revisions,
4474 'revisions': self.revisions,
4469 'versions': self.versions_count
4475 'versions': self.versions_count
4470 }
4476 }
4471
4477
4472 def calculated_review_status(self):
4478 def calculated_review_status(self):
4473 from rhodecode.model.changeset_status import ChangesetStatusModel
4479 from rhodecode.model.changeset_status import ChangesetStatusModel
4474 return ChangesetStatusModel().calculated_review_status(self)
4480 return ChangesetStatusModel().calculated_review_status(self)
4475
4481
4476 def reviewers_statuses(self):
4482 def reviewers_statuses(self):
4477 from rhodecode.model.changeset_status import ChangesetStatusModel
4483 from rhodecode.model.changeset_status import ChangesetStatusModel
4478 return ChangesetStatusModel().reviewers_statuses(self)
4484 return ChangesetStatusModel().reviewers_statuses(self)
4479
4485
4480 def get_pull_request_reviewers(self, role=None):
4486 def get_pull_request_reviewers(self, role=None):
4481 qry = PullRequestReviewers.query()\
4487 qry = PullRequestReviewers.query()\
4482 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4488 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4483 if role:
4489 if role:
4484 qry = qry.filter(PullRequestReviewers.role == role)
4490 qry = qry.filter(PullRequestReviewers.role == role)
4485
4491
4486 return qry.all()
4492 return qry.all()
4487
4493
4488 @property
4494 @property
4489 def reviewers_count(self):
4495 def reviewers_count(self):
4490 qry = PullRequestReviewers.query()\
4496 qry = PullRequestReviewers.query()\
4491 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4497 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4492 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4498 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4493 return qry.count()
4499 return qry.count()
4494
4500
4495 @property
4501 @property
4496 def observers_count(self):
4502 def observers_count(self):
4497 qry = PullRequestReviewers.query()\
4503 qry = PullRequestReviewers.query()\
4498 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4504 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4499 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4505 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4500 return qry.count()
4506 return qry.count()
4501
4507
4502 def observers(self):
4508 def observers(self):
4503 qry = PullRequestReviewers.query()\
4509 qry = PullRequestReviewers.query()\
4504 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4510 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4505 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4511 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4506 .all()
4512 .all()
4507
4513
4508 for entry in qry:
4514 for entry in qry:
4509 yield entry, entry.user
4515 yield entry, entry.user
4510
4516
4511 @property
4517 @property
4512 def workspace_id(self):
4518 def workspace_id(self):
4513 from rhodecode.model.pull_request import PullRequestModel
4519 from rhodecode.model.pull_request import PullRequestModel
4514 return PullRequestModel()._workspace_id(self)
4520 return PullRequestModel()._workspace_id(self)
4515
4521
4516 def get_shadow_repo(self):
4522 def get_shadow_repo(self):
4517 workspace_id = self.workspace_id
4523 workspace_id = self.workspace_id
4518 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4524 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4519 if os.path.isdir(shadow_repository_path):
4525 if os.path.isdir(shadow_repository_path):
4520 vcs_obj = self.target_repo.scm_instance()
4526 vcs_obj = self.target_repo.scm_instance()
4521 return vcs_obj.get_shadow_instance(shadow_repository_path)
4527 return vcs_obj.get_shadow_instance(shadow_repository_path)
4522
4528
4523 @property
4529 @property
4524 def versions_count(self):
4530 def versions_count(self):
4525 """
4531 """
4526 return number of versions this PR have, e.g a PR that once been
4532 return number of versions this PR have, e.g a PR that once been
4527 updated will have 2 versions
4533 updated will have 2 versions
4528 """
4534 """
4529 return self.versions.count() + 1
4535 return self.versions.count() + 1
4530
4536
4531 @property
4537 @property
4532 def pull_request_last_version(self):
4538 def pull_request_last_version(self):
4533 return self.versions_count
4539 return self.versions_count
4534
4540
4535
4541
4536 class PullRequestVersion(Base, _PullRequestBase):
4542 class PullRequestVersion(Base, _PullRequestBase):
4537 __tablename__ = 'pull_request_versions'
4543 __tablename__ = 'pull_request_versions'
4538 __table_args__ = (
4544 __table_args__ = (
4539 base_table_args,
4545 base_table_args,
4540 )
4546 )
4541
4547
4542 pull_request_version_id = Column(
4548 pull_request_version_id = Column(
4543 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4549 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4544 pull_request_id = Column(
4550 pull_request_id = Column(
4545 'pull_request_id', Integer(),
4551 'pull_request_id', Integer(),
4546 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4552 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4547 pull_request = relationship('PullRequest')
4553 pull_request = relationship('PullRequest')
4548
4554
4549 def __repr__(self):
4555 def __repr__(self):
4550 if self.pull_request_version_id:
4556 if self.pull_request_version_id:
4551 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4557 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4552 else:
4558 else:
4553 return '<DB:PullRequestVersion at %#x>' % id(self)
4559 return '<DB:PullRequestVersion at %#x>' % id(self)
4554
4560
4555 @property
4561 @property
4556 def reviewers(self):
4562 def reviewers(self):
4557 return self.pull_request.reviewers
4563 return self.pull_request.reviewers
4558 @property
4564 @property
4559 def reviewers(self):
4565 def reviewers(self):
4560 return self.pull_request.reviewers
4566 return self.pull_request.reviewers
4561
4567
4562 @property
4568 @property
4563 def versions(self):
4569 def versions(self):
4564 return self.pull_request.versions
4570 return self.pull_request.versions
4565
4571
4566 def is_closed(self):
4572 def is_closed(self):
4567 # calculate from original
4573 # calculate from original
4568 return self.pull_request.status == self.STATUS_CLOSED
4574 return self.pull_request.status == self.STATUS_CLOSED
4569
4575
4570 def is_state_changing(self):
4576 def is_state_changing(self):
4571 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4577 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4572
4578
4573 def calculated_review_status(self):
4579 def calculated_review_status(self):
4574 return self.pull_request.calculated_review_status()
4580 return self.pull_request.calculated_review_status()
4575
4581
4576 def reviewers_statuses(self):
4582 def reviewers_statuses(self):
4577 return self.pull_request.reviewers_statuses()
4583 return self.pull_request.reviewers_statuses()
4578
4584
4579 def observers(self):
4585 def observers(self):
4580 return self.pull_request.observers()
4586 return self.pull_request.observers()
4581
4587
4582
4588
4583 class PullRequestReviewers(Base, BaseModel):
4589 class PullRequestReviewers(Base, BaseModel):
4584 __tablename__ = 'pull_request_reviewers'
4590 __tablename__ = 'pull_request_reviewers'
4585 __table_args__ = (
4591 __table_args__ = (
4586 base_table_args,
4592 base_table_args,
4587 )
4593 )
4588 ROLE_REVIEWER = u'reviewer'
4594 ROLE_REVIEWER = u'reviewer'
4589 ROLE_OBSERVER = u'observer'
4595 ROLE_OBSERVER = u'observer'
4590 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4596 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4591
4597
4592 @hybrid_property
4598 @hybrid_property
4593 def reasons(self):
4599 def reasons(self):
4594 if not self._reasons:
4600 if not self._reasons:
4595 return []
4601 return []
4596 return self._reasons
4602 return self._reasons
4597
4603
4598 @reasons.setter
4604 @reasons.setter
4599 def reasons(self, val):
4605 def reasons(self, val):
4600 val = val or []
4606 val = val or []
4601 if any(not isinstance(x, compat.string_types) for x in val):
4607 if any(not isinstance(x, compat.string_types) for x in val):
4602 raise Exception('invalid reasons type, must be list of strings')
4608 raise Exception('invalid reasons type, must be list of strings')
4603 self._reasons = val
4609 self._reasons = val
4604
4610
4605 pull_requests_reviewers_id = Column(
4611 pull_requests_reviewers_id = Column(
4606 'pull_requests_reviewers_id', Integer(), nullable=False,
4612 'pull_requests_reviewers_id', Integer(), nullable=False,
4607 primary_key=True)
4613 primary_key=True)
4608 pull_request_id = Column(
4614 pull_request_id = Column(
4609 "pull_request_id", Integer(),
4615 "pull_request_id", Integer(),
4610 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4616 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4611 user_id = Column(
4617 user_id = Column(
4612 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4618 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4613 _reasons = Column(
4619 _reasons = Column(
4614 'reason', MutationList.as_mutable(
4620 'reason', MutationList.as_mutable(
4615 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4621 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4616
4622
4617 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4623 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4618 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4624 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4619
4625
4620 user = relationship('User')
4626 user = relationship('User')
4621 pull_request = relationship('PullRequest')
4627 pull_request = relationship('PullRequest')
4622
4628
4623 rule_data = Column(
4629 rule_data = Column(
4624 'rule_data_json',
4630 'rule_data_json',
4625 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4631 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4626
4632
4627 def rule_user_group_data(self):
4633 def rule_user_group_data(self):
4628 """
4634 """
4629 Returns the voting user group rule data for this reviewer
4635 Returns the voting user group rule data for this reviewer
4630 """
4636 """
4631
4637
4632 if self.rule_data and 'vote_rule' in self.rule_data:
4638 if self.rule_data and 'vote_rule' in self.rule_data:
4633 user_group_data = {}
4639 user_group_data = {}
4634 if 'rule_user_group_entry_id' in self.rule_data:
4640 if 'rule_user_group_entry_id' in self.rule_data:
4635 # means a group with voting rules !
4641 # means a group with voting rules !
4636 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4642 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4637 user_group_data['name'] = self.rule_data['rule_name']
4643 user_group_data['name'] = self.rule_data['rule_name']
4638 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4644 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4639
4645
4640 return user_group_data
4646 return user_group_data
4641
4647
4642 @classmethod
4648 @classmethod
4643 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4649 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4644 qry = PullRequestReviewers.query()\
4650 qry = PullRequestReviewers.query()\
4645 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4651 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4646 if role:
4652 if role:
4647 qry = qry.filter(PullRequestReviewers.role == role)
4653 qry = qry.filter(PullRequestReviewers.role == role)
4648
4654
4649 return qry.all()
4655 return qry.all()
4650
4656
4651 def __unicode__(self):
4657 def __unicode__(self):
4652 return u"<%s('id:%s')>" % (self.__class__.__name__,
4658 return u"<%s('id:%s')>" % (self.__class__.__name__,
4653 self.pull_requests_reviewers_id)
4659 self.pull_requests_reviewers_id)
4654
4660
4655
4661
4656 class Notification(Base, BaseModel):
4662 class Notification(Base, BaseModel):
4657 __tablename__ = 'notifications'
4663 __tablename__ = 'notifications'
4658 __table_args__ = (
4664 __table_args__ = (
4659 Index('notification_type_idx', 'type'),
4665 Index('notification_type_idx', 'type'),
4660 base_table_args,
4666 base_table_args,
4661 )
4667 )
4662
4668
4663 TYPE_CHANGESET_COMMENT = u'cs_comment'
4669 TYPE_CHANGESET_COMMENT = u'cs_comment'
4664 TYPE_MESSAGE = u'message'
4670 TYPE_MESSAGE = u'message'
4665 TYPE_MENTION = u'mention'
4671 TYPE_MENTION = u'mention'
4666 TYPE_REGISTRATION = u'registration'
4672 TYPE_REGISTRATION = u'registration'
4667 TYPE_PULL_REQUEST = u'pull_request'
4673 TYPE_PULL_REQUEST = u'pull_request'
4668 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4674 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4669 TYPE_PULL_REQUEST_UPDATE = u'pull_request_update'
4675 TYPE_PULL_REQUEST_UPDATE = u'pull_request_update'
4670
4676
4671 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4677 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4672 subject = Column('subject', Unicode(512), nullable=True)
4678 subject = Column('subject', Unicode(512), nullable=True)
4673 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4679 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4674 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4680 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4675 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4681 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4676 type_ = Column('type', Unicode(255))
4682 type_ = Column('type', Unicode(255))
4677
4683
4678 created_by_user = relationship('User')
4684 created_by_user = relationship('User')
4679 notifications_to_users = relationship('UserNotification', lazy='joined',
4685 notifications_to_users = relationship('UserNotification', lazy='joined',
4680 cascade="all, delete-orphan")
4686 cascade="all, delete-orphan")
4681
4687
4682 @property
4688 @property
4683 def recipients(self):
4689 def recipients(self):
4684 return [x.user for x in UserNotification.query()\
4690 return [x.user for x in UserNotification.query()\
4685 .filter(UserNotification.notification == self)\
4691 .filter(UserNotification.notification == self)\
4686 .order_by(UserNotification.user_id.asc()).all()]
4692 .order_by(UserNotification.user_id.asc()).all()]
4687
4693
4688 @classmethod
4694 @classmethod
4689 def create(cls, created_by, subject, body, recipients, type_=None):
4695 def create(cls, created_by, subject, body, recipients, type_=None):
4690 if type_ is None:
4696 if type_ is None:
4691 type_ = Notification.TYPE_MESSAGE
4697 type_ = Notification.TYPE_MESSAGE
4692
4698
4693 notification = cls()
4699 notification = cls()
4694 notification.created_by_user = created_by
4700 notification.created_by_user = created_by
4695 notification.subject = subject
4701 notification.subject = subject
4696 notification.body = body
4702 notification.body = body
4697 notification.type_ = type_
4703 notification.type_ = type_
4698 notification.created_on = datetime.datetime.now()
4704 notification.created_on = datetime.datetime.now()
4699
4705
4700 # For each recipient link the created notification to his account
4706 # For each recipient link the created notification to his account
4701 for u in recipients:
4707 for u in recipients:
4702 assoc = UserNotification()
4708 assoc = UserNotification()
4703 assoc.user_id = u.user_id
4709 assoc.user_id = u.user_id
4704 assoc.notification = notification
4710 assoc.notification = notification
4705
4711
4706 # if created_by is inside recipients mark his notification
4712 # if created_by is inside recipients mark his notification
4707 # as read
4713 # as read
4708 if u.user_id == created_by.user_id:
4714 if u.user_id == created_by.user_id:
4709 assoc.read = True
4715 assoc.read = True
4710 Session().add(assoc)
4716 Session().add(assoc)
4711
4717
4712 Session().add(notification)
4718 Session().add(notification)
4713
4719
4714 return notification
4720 return notification
4715
4721
4716
4722
4717 class UserNotification(Base, BaseModel):
4723 class UserNotification(Base, BaseModel):
4718 __tablename__ = 'user_to_notification'
4724 __tablename__ = 'user_to_notification'
4719 __table_args__ = (
4725 __table_args__ = (
4720 UniqueConstraint('user_id', 'notification_id'),
4726 UniqueConstraint('user_id', 'notification_id'),
4721 base_table_args
4727 base_table_args
4722 )
4728 )
4723
4729
4724 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4730 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4725 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4731 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4726 read = Column('read', Boolean, default=False)
4732 read = Column('read', Boolean, default=False)
4727 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4733 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4728
4734
4729 user = relationship('User', lazy="joined")
4735 user = relationship('User', lazy="joined")
4730 notification = relationship('Notification', lazy="joined",
4736 notification = relationship('Notification', lazy="joined",
4731 order_by=lambda: Notification.created_on.desc(),)
4737 order_by=lambda: Notification.created_on.desc(),)
4732
4738
4733 def mark_as_read(self):
4739 def mark_as_read(self):
4734 self.read = True
4740 self.read = True
4735 Session().add(self)
4741 Session().add(self)
4736
4742
4737
4743
4738 class UserNotice(Base, BaseModel):
4744 class UserNotice(Base, BaseModel):
4739 __tablename__ = 'user_notices'
4745 __tablename__ = 'user_notices'
4740 __table_args__ = (
4746 __table_args__ = (
4741 base_table_args
4747 base_table_args
4742 )
4748 )
4743
4749
4744 NOTIFICATION_TYPE_MESSAGE = 'message'
4750 NOTIFICATION_TYPE_MESSAGE = 'message'
4745 NOTIFICATION_TYPE_NOTICE = 'notice'
4751 NOTIFICATION_TYPE_NOTICE = 'notice'
4746
4752
4747 NOTIFICATION_LEVEL_INFO = 'info'
4753 NOTIFICATION_LEVEL_INFO = 'info'
4748 NOTIFICATION_LEVEL_WARNING = 'warning'
4754 NOTIFICATION_LEVEL_WARNING = 'warning'
4749 NOTIFICATION_LEVEL_ERROR = 'error'
4755 NOTIFICATION_LEVEL_ERROR = 'error'
4750
4756
4751 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4757 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4752
4758
4753 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4759 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4754 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4760 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4755
4761
4756 notice_read = Column('notice_read', Boolean, default=False)
4762 notice_read = Column('notice_read', Boolean, default=False)
4757
4763
4758 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4764 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4759 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4765 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4760
4766
4761 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4767 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4762 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4768 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4763
4769
4764 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4770 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4765 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4771 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4766
4772
4767 @classmethod
4773 @classmethod
4768 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4774 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4769
4775
4770 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4776 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4771 cls.NOTIFICATION_LEVEL_WARNING,
4777 cls.NOTIFICATION_LEVEL_WARNING,
4772 cls.NOTIFICATION_LEVEL_INFO]:
4778 cls.NOTIFICATION_LEVEL_INFO]:
4773 return
4779 return
4774
4780
4775 from rhodecode.model.user import UserModel
4781 from rhodecode.model.user import UserModel
4776 user = UserModel().get_user(user)
4782 user = UserModel().get_user(user)
4777
4783
4778 new_notice = UserNotice()
4784 new_notice = UserNotice()
4779 if not allow_duplicate:
4785 if not allow_duplicate:
4780 existing_msg = UserNotice().query() \
4786 existing_msg = UserNotice().query() \
4781 .filter(UserNotice.user == user) \
4787 .filter(UserNotice.user == user) \
4782 .filter(UserNotice.notice_body == body) \
4788 .filter(UserNotice.notice_body == body) \
4783 .filter(UserNotice.notice_read == false()) \
4789 .filter(UserNotice.notice_read == false()) \
4784 .scalar()
4790 .scalar()
4785 if existing_msg:
4791 if existing_msg:
4786 log.warning('Ignoring duplicate notice for user %s', user)
4792 log.warning('Ignoring duplicate notice for user %s', user)
4787 return
4793 return
4788
4794
4789 new_notice.user = user
4795 new_notice.user = user
4790 new_notice.notice_subject = subject
4796 new_notice.notice_subject = subject
4791 new_notice.notice_body = body
4797 new_notice.notice_body = body
4792 new_notice.notification_level = notice_level
4798 new_notice.notification_level = notice_level
4793 Session().add(new_notice)
4799 Session().add(new_notice)
4794 Session().commit()
4800 Session().commit()
4795
4801
4796
4802
4797 class Gist(Base, BaseModel):
4803 class Gist(Base, BaseModel):
4798 __tablename__ = 'gists'
4804 __tablename__ = 'gists'
4799 __table_args__ = (
4805 __table_args__ = (
4800 Index('g_gist_access_id_idx', 'gist_access_id'),
4806 Index('g_gist_access_id_idx', 'gist_access_id'),
4801 Index('g_created_on_idx', 'created_on'),
4807 Index('g_created_on_idx', 'created_on'),
4802 base_table_args
4808 base_table_args
4803 )
4809 )
4804
4810
4805 GIST_PUBLIC = u'public'
4811 GIST_PUBLIC = u'public'
4806 GIST_PRIVATE = u'private'
4812 GIST_PRIVATE = u'private'
4807 DEFAULT_FILENAME = u'gistfile1.txt'
4813 DEFAULT_FILENAME = u'gistfile1.txt'
4808
4814
4809 ACL_LEVEL_PUBLIC = u'acl_public'
4815 ACL_LEVEL_PUBLIC = u'acl_public'
4810 ACL_LEVEL_PRIVATE = u'acl_private'
4816 ACL_LEVEL_PRIVATE = u'acl_private'
4811
4817
4812 gist_id = Column('gist_id', Integer(), primary_key=True)
4818 gist_id = Column('gist_id', Integer(), primary_key=True)
4813 gist_access_id = Column('gist_access_id', Unicode(250))
4819 gist_access_id = Column('gist_access_id', Unicode(250))
4814 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4820 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4815 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4821 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4816 gist_expires = Column('gist_expires', Float(53), nullable=False)
4822 gist_expires = Column('gist_expires', Float(53), nullable=False)
4817 gist_type = Column('gist_type', Unicode(128), nullable=False)
4823 gist_type = Column('gist_type', Unicode(128), nullable=False)
4818 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4824 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4819 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4825 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4820 acl_level = Column('acl_level', Unicode(128), nullable=True)
4826 acl_level = Column('acl_level', Unicode(128), nullable=True)
4821
4827
4822 owner = relationship('User')
4828 owner = relationship('User')
4823
4829
4824 def __repr__(self):
4830 def __repr__(self):
4825 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4831 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4826
4832
4827 @hybrid_property
4833 @hybrid_property
4828 def description_safe(self):
4834 def description_safe(self):
4829 from rhodecode.lib import helpers as h
4835 from rhodecode.lib import helpers as h
4830 return h.escape(self.gist_description)
4836 return h.escape(self.gist_description)
4831
4837
4832 @classmethod
4838 @classmethod
4833 def get_or_404(cls, id_):
4839 def get_or_404(cls, id_):
4834 from pyramid.httpexceptions import HTTPNotFound
4840 from pyramid.httpexceptions import HTTPNotFound
4835
4841
4836 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4842 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4837 if not res:
4843 if not res:
4838 log.debug('WARN: No DB entry with id %s', id_)
4844 log.debug('WARN: No DB entry with id %s', id_)
4839 raise HTTPNotFound()
4845 raise HTTPNotFound()
4840 return res
4846 return res
4841
4847
4842 @classmethod
4848 @classmethod
4843 def get_by_access_id(cls, gist_access_id):
4849 def get_by_access_id(cls, gist_access_id):
4844 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4850 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4845
4851
4846 def gist_url(self):
4852 def gist_url(self):
4847 from rhodecode.model.gist import GistModel
4853 from rhodecode.model.gist import GistModel
4848 return GistModel().get_url(self)
4854 return GistModel().get_url(self)
4849
4855
4850 @classmethod
4856 @classmethod
4851 def base_path(cls):
4857 def base_path(cls):
4852 """
4858 """
4853 Returns base path when all gists are stored
4859 Returns base path when all gists are stored
4854
4860
4855 :param cls:
4861 :param cls:
4856 """
4862 """
4857 from rhodecode.model.gist import GIST_STORE_LOC
4863 from rhodecode.model.gist import GIST_STORE_LOC
4858 q = Session().query(RhodeCodeUi)\
4864 q = Session().query(RhodeCodeUi)\
4859 .filter(RhodeCodeUi.ui_key == URL_SEP)
4865 .filter(RhodeCodeUi.ui_key == URL_SEP)
4860 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4866 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4861 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4867 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4862
4868
4863 def get_api_data(self):
4869 def get_api_data(self):
4864 """
4870 """
4865 Common function for generating gist related data for API
4871 Common function for generating gist related data for API
4866 """
4872 """
4867 gist = self
4873 gist = self
4868 data = {
4874 data = {
4869 'gist_id': gist.gist_id,
4875 'gist_id': gist.gist_id,
4870 'type': gist.gist_type,
4876 'type': gist.gist_type,
4871 'access_id': gist.gist_access_id,
4877 'access_id': gist.gist_access_id,
4872 'description': gist.gist_description,
4878 'description': gist.gist_description,
4873 'url': gist.gist_url(),
4879 'url': gist.gist_url(),
4874 'expires': gist.gist_expires,
4880 'expires': gist.gist_expires,
4875 'created_on': gist.created_on,
4881 'created_on': gist.created_on,
4876 'modified_at': gist.modified_at,
4882 'modified_at': gist.modified_at,
4877 'content': None,
4883 'content': None,
4878 'acl_level': gist.acl_level,
4884 'acl_level': gist.acl_level,
4879 }
4885 }
4880 return data
4886 return data
4881
4887
4882 def __json__(self):
4888 def __json__(self):
4883 data = dict(
4889 data = dict(
4884 )
4890 )
4885 data.update(self.get_api_data())
4891 data.update(self.get_api_data())
4886 return data
4892 return data
4887 # SCM functions
4893 # SCM functions
4888
4894
4889 def scm_instance(self, **kwargs):
4895 def scm_instance(self, **kwargs):
4890 """
4896 """
4891 Get an instance of VCS Repository
4897 Get an instance of VCS Repository
4892
4898
4893 :param kwargs:
4899 :param kwargs:
4894 """
4900 """
4895 from rhodecode.model.gist import GistModel
4901 from rhodecode.model.gist import GistModel
4896 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4902 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4897 return get_vcs_instance(
4903 return get_vcs_instance(
4898 repo_path=safe_str(full_repo_path), create=False,
4904 repo_path=safe_str(full_repo_path), create=False,
4899 _vcs_alias=GistModel.vcs_backend)
4905 _vcs_alias=GistModel.vcs_backend)
4900
4906
4901
4907
4902 class ExternalIdentity(Base, BaseModel):
4908 class ExternalIdentity(Base, BaseModel):
4903 __tablename__ = 'external_identities'
4909 __tablename__ = 'external_identities'
4904 __table_args__ = (
4910 __table_args__ = (
4905 Index('local_user_id_idx', 'local_user_id'),
4911 Index('local_user_id_idx', 'local_user_id'),
4906 Index('external_id_idx', 'external_id'),
4912 Index('external_id_idx', 'external_id'),
4907 base_table_args
4913 base_table_args
4908 )
4914 )
4909
4915
4910 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4916 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4911 external_username = Column('external_username', Unicode(1024), default=u'')
4917 external_username = Column('external_username', Unicode(1024), default=u'')
4912 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4918 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4913 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4919 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4914 access_token = Column('access_token', String(1024), default=u'')
4920 access_token = Column('access_token', String(1024), default=u'')
4915 alt_token = Column('alt_token', String(1024), default=u'')
4921 alt_token = Column('alt_token', String(1024), default=u'')
4916 token_secret = Column('token_secret', String(1024), default=u'')
4922 token_secret = Column('token_secret', String(1024), default=u'')
4917
4923
4918 @classmethod
4924 @classmethod
4919 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4925 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4920 """
4926 """
4921 Returns ExternalIdentity instance based on search params
4927 Returns ExternalIdentity instance based on search params
4922
4928
4923 :param external_id:
4929 :param external_id:
4924 :param provider_name:
4930 :param provider_name:
4925 :return: ExternalIdentity
4931 :return: ExternalIdentity
4926 """
4932 """
4927 query = cls.query()
4933 query = cls.query()
4928 query = query.filter(cls.external_id == external_id)
4934 query = query.filter(cls.external_id == external_id)
4929 query = query.filter(cls.provider_name == provider_name)
4935 query = query.filter(cls.provider_name == provider_name)
4930 if local_user_id:
4936 if local_user_id:
4931 query = query.filter(cls.local_user_id == local_user_id)
4937 query = query.filter(cls.local_user_id == local_user_id)
4932 return query.first()
4938 return query.first()
4933
4939
4934 @classmethod
4940 @classmethod
4935 def user_by_external_id_and_provider(cls, external_id, provider_name):
4941 def user_by_external_id_and_provider(cls, external_id, provider_name):
4936 """
4942 """
4937 Returns User instance based on search params
4943 Returns User instance based on search params
4938
4944
4939 :param external_id:
4945 :param external_id:
4940 :param provider_name:
4946 :param provider_name:
4941 :return: User
4947 :return: User
4942 """
4948 """
4943 query = User.query()
4949 query = User.query()
4944 query = query.filter(cls.external_id == external_id)
4950 query = query.filter(cls.external_id == external_id)
4945 query = query.filter(cls.provider_name == provider_name)
4951 query = query.filter(cls.provider_name == provider_name)
4946 query = query.filter(User.user_id == cls.local_user_id)
4952 query = query.filter(User.user_id == cls.local_user_id)
4947 return query.first()
4953 return query.first()
4948
4954
4949 @classmethod
4955 @classmethod
4950 def by_local_user_id(cls, local_user_id):
4956 def by_local_user_id(cls, local_user_id):
4951 """
4957 """
4952 Returns all tokens for user
4958 Returns all tokens for user
4953
4959
4954 :param local_user_id:
4960 :param local_user_id:
4955 :return: ExternalIdentity
4961 :return: ExternalIdentity
4956 """
4962 """
4957 query = cls.query()
4963 query = cls.query()
4958 query = query.filter(cls.local_user_id == local_user_id)
4964 query = query.filter(cls.local_user_id == local_user_id)
4959 return query
4965 return query
4960
4966
4961 @classmethod
4967 @classmethod
4962 def load_provider_plugin(cls, plugin_id):
4968 def load_provider_plugin(cls, plugin_id):
4963 from rhodecode.authentication.base import loadplugin
4969 from rhodecode.authentication.base import loadplugin
4964 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4970 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4965 auth_plugin = loadplugin(_plugin_id)
4971 auth_plugin = loadplugin(_plugin_id)
4966 return auth_plugin
4972 return auth_plugin
4967
4973
4968
4974
4969 class Integration(Base, BaseModel):
4975 class Integration(Base, BaseModel):
4970 __tablename__ = 'integrations'
4976 __tablename__ = 'integrations'
4971 __table_args__ = (
4977 __table_args__ = (
4972 base_table_args
4978 base_table_args
4973 )
4979 )
4974
4980
4975 integration_id = Column('integration_id', Integer(), primary_key=True)
4981 integration_id = Column('integration_id', Integer(), primary_key=True)
4976 integration_type = Column('integration_type', String(255))
4982 integration_type = Column('integration_type', String(255))
4977 enabled = Column('enabled', Boolean(), nullable=False)
4983 enabled = Column('enabled', Boolean(), nullable=False)
4978 name = Column('name', String(255), nullable=False)
4984 name = Column('name', String(255), nullable=False)
4979 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4985 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4980 default=False)
4986 default=False)
4981
4987
4982 settings = Column(
4988 settings = Column(
4983 'settings_json', MutationObj.as_mutable(
4989 'settings_json', MutationObj.as_mutable(
4984 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4990 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4985 repo_id = Column(
4991 repo_id = Column(
4986 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4992 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4987 nullable=True, unique=None, default=None)
4993 nullable=True, unique=None, default=None)
4988 repo = relationship('Repository', lazy='joined')
4994 repo = relationship('Repository', lazy='joined')
4989
4995
4990 repo_group_id = Column(
4996 repo_group_id = Column(
4991 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4997 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4992 nullable=True, unique=None, default=None)
4998 nullable=True, unique=None, default=None)
4993 repo_group = relationship('RepoGroup', lazy='joined')
4999 repo_group = relationship('RepoGroup', lazy='joined')
4994
5000
4995 @property
5001 @property
4996 def scope(self):
5002 def scope(self):
4997 if self.repo:
5003 if self.repo:
4998 return repr(self.repo)
5004 return repr(self.repo)
4999 if self.repo_group:
5005 if self.repo_group:
5000 if self.child_repos_only:
5006 if self.child_repos_only:
5001 return repr(self.repo_group) + ' (child repos only)'
5007 return repr(self.repo_group) + ' (child repos only)'
5002 else:
5008 else:
5003 return repr(self.repo_group) + ' (recursive)'
5009 return repr(self.repo_group) + ' (recursive)'
5004 if self.child_repos_only:
5010 if self.child_repos_only:
5005 return 'root_repos'
5011 return 'root_repos'
5006 return 'global'
5012 return 'global'
5007
5013
5008 def __repr__(self):
5014 def __repr__(self):
5009 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5015 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5010
5016
5011
5017
5012 class RepoReviewRuleUser(Base, BaseModel):
5018 class RepoReviewRuleUser(Base, BaseModel):
5013 __tablename__ = 'repo_review_rules_users'
5019 __tablename__ = 'repo_review_rules_users'
5014 __table_args__ = (
5020 __table_args__ = (
5015 base_table_args
5021 base_table_args
5016 )
5022 )
5017 ROLE_REVIEWER = u'reviewer'
5023 ROLE_REVIEWER = u'reviewer'
5018 ROLE_OBSERVER = u'observer'
5024 ROLE_OBSERVER = u'observer'
5019 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5025 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5020
5026
5021 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5027 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5022 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5028 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5023 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5029 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5024 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5030 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5025 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5031 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5026 user = relationship('User')
5032 user = relationship('User')
5027
5033
5028 def rule_data(self):
5034 def rule_data(self):
5029 return {
5035 return {
5030 'mandatory': self.mandatory,
5036 'mandatory': self.mandatory,
5031 'role': self.role,
5037 'role': self.role,
5032 }
5038 }
5033
5039
5034
5040
5035 class RepoReviewRuleUserGroup(Base, BaseModel):
5041 class RepoReviewRuleUserGroup(Base, BaseModel):
5036 __tablename__ = 'repo_review_rules_users_groups'
5042 __tablename__ = 'repo_review_rules_users_groups'
5037 __table_args__ = (
5043 __table_args__ = (
5038 base_table_args
5044 base_table_args
5039 )
5045 )
5040
5046
5041 VOTE_RULE_ALL = -1
5047 VOTE_RULE_ALL = -1
5042 ROLE_REVIEWER = u'reviewer'
5048 ROLE_REVIEWER = u'reviewer'
5043 ROLE_OBSERVER = u'observer'
5049 ROLE_OBSERVER = u'observer'
5044 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5050 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5045
5051
5046 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5052 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5047 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5053 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5048 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5054 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5049 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5055 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5050 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5056 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5051 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5057 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5052 users_group = relationship('UserGroup')
5058 users_group = relationship('UserGroup')
5053
5059
5054 def rule_data(self):
5060 def rule_data(self):
5055 return {
5061 return {
5056 'mandatory': self.mandatory,
5062 'mandatory': self.mandatory,
5057 'role': self.role,
5063 'role': self.role,
5058 'vote_rule': self.vote_rule
5064 'vote_rule': self.vote_rule
5059 }
5065 }
5060
5066
5061 @property
5067 @property
5062 def vote_rule_label(self):
5068 def vote_rule_label(self):
5063 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5069 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5064 return 'all must vote'
5070 return 'all must vote'
5065 else:
5071 else:
5066 return 'min. vote {}'.format(self.vote_rule)
5072 return 'min. vote {}'.format(self.vote_rule)
5067
5073
5068
5074
5069 class RepoReviewRule(Base, BaseModel):
5075 class RepoReviewRule(Base, BaseModel):
5070 __tablename__ = 'repo_review_rules'
5076 __tablename__ = 'repo_review_rules'
5071 __table_args__ = (
5077 __table_args__ = (
5072 base_table_args
5078 base_table_args
5073 )
5079 )
5074
5080
5075 repo_review_rule_id = Column(
5081 repo_review_rule_id = Column(
5076 'repo_review_rule_id', Integer(), primary_key=True)
5082 'repo_review_rule_id', Integer(), primary_key=True)
5077 repo_id = Column(
5083 repo_id = Column(
5078 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5084 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5079 repo = relationship('Repository', backref='review_rules')
5085 repo = relationship('Repository', backref='review_rules')
5080
5086
5081 review_rule_name = Column('review_rule_name', String(255))
5087 review_rule_name = Column('review_rule_name', String(255))
5082 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5088 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5083 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5089 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5084 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5090 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5085
5091
5086 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5092 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5087
5093
5088 # Legacy fields, just for backward compat
5094 # Legacy fields, just for backward compat
5089 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5095 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5090 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5096 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5091
5097
5092 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5098 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5093 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5099 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5094
5100
5095 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5101 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5096
5102
5097 rule_users = relationship('RepoReviewRuleUser')
5103 rule_users = relationship('RepoReviewRuleUser')
5098 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5104 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5099
5105
5100 def _validate_pattern(self, value):
5106 def _validate_pattern(self, value):
5101 re.compile('^' + glob2re(value) + '$')
5107 re.compile('^' + glob2re(value) + '$')
5102
5108
5103 @hybrid_property
5109 @hybrid_property
5104 def source_branch_pattern(self):
5110 def source_branch_pattern(self):
5105 return self._branch_pattern or '*'
5111 return self._branch_pattern or '*'
5106
5112
5107 @source_branch_pattern.setter
5113 @source_branch_pattern.setter
5108 def source_branch_pattern(self, value):
5114 def source_branch_pattern(self, value):
5109 self._validate_pattern(value)
5115 self._validate_pattern(value)
5110 self._branch_pattern = value or '*'
5116 self._branch_pattern = value or '*'
5111
5117
5112 @hybrid_property
5118 @hybrid_property
5113 def target_branch_pattern(self):
5119 def target_branch_pattern(self):
5114 return self._target_branch_pattern or '*'
5120 return self._target_branch_pattern or '*'
5115
5121
5116 @target_branch_pattern.setter
5122 @target_branch_pattern.setter
5117 def target_branch_pattern(self, value):
5123 def target_branch_pattern(self, value):
5118 self._validate_pattern(value)
5124 self._validate_pattern(value)
5119 self._target_branch_pattern = value or '*'
5125 self._target_branch_pattern = value or '*'
5120
5126
5121 @hybrid_property
5127 @hybrid_property
5122 def file_pattern(self):
5128 def file_pattern(self):
5123 return self._file_pattern or '*'
5129 return self._file_pattern or '*'
5124
5130
5125 @file_pattern.setter
5131 @file_pattern.setter
5126 def file_pattern(self, value):
5132 def file_pattern(self, value):
5127 self._validate_pattern(value)
5133 self._validate_pattern(value)
5128 self._file_pattern = value or '*'
5134 self._file_pattern = value or '*'
5129
5135
5130 @hybrid_property
5136 @hybrid_property
5131 def forbid_pr_author_to_review(self):
5137 def forbid_pr_author_to_review(self):
5132 return self.pr_author == 'forbid_pr_author'
5138 return self.pr_author == 'forbid_pr_author'
5133
5139
5134 @hybrid_property
5140 @hybrid_property
5135 def include_pr_author_to_review(self):
5141 def include_pr_author_to_review(self):
5136 return self.pr_author == 'include_pr_author'
5142 return self.pr_author == 'include_pr_author'
5137
5143
5138 @hybrid_property
5144 @hybrid_property
5139 def forbid_commit_author_to_review(self):
5145 def forbid_commit_author_to_review(self):
5140 return self.commit_author == 'forbid_commit_author'
5146 return self.commit_author == 'forbid_commit_author'
5141
5147
5142 @hybrid_property
5148 @hybrid_property
5143 def include_commit_author_to_review(self):
5149 def include_commit_author_to_review(self):
5144 return self.commit_author == 'include_commit_author'
5150 return self.commit_author == 'include_commit_author'
5145
5151
5146 def matches(self, source_branch, target_branch, files_changed):
5152 def matches(self, source_branch, target_branch, files_changed):
5147 """
5153 """
5148 Check if this review rule matches a branch/files in a pull request
5154 Check if this review rule matches a branch/files in a pull request
5149
5155
5150 :param source_branch: source branch name for the commit
5156 :param source_branch: source branch name for the commit
5151 :param target_branch: target branch name for the commit
5157 :param target_branch: target branch name for the commit
5152 :param files_changed: list of file paths changed in the pull request
5158 :param files_changed: list of file paths changed in the pull request
5153 """
5159 """
5154
5160
5155 source_branch = source_branch or ''
5161 source_branch = source_branch or ''
5156 target_branch = target_branch or ''
5162 target_branch = target_branch or ''
5157 files_changed = files_changed or []
5163 files_changed = files_changed or []
5158
5164
5159 branch_matches = True
5165 branch_matches = True
5160 if source_branch or target_branch:
5166 if source_branch or target_branch:
5161 if self.source_branch_pattern == '*':
5167 if self.source_branch_pattern == '*':
5162 source_branch_match = True
5168 source_branch_match = True
5163 else:
5169 else:
5164 if self.source_branch_pattern.startswith('re:'):
5170 if self.source_branch_pattern.startswith('re:'):
5165 source_pattern = self.source_branch_pattern[3:]
5171 source_pattern = self.source_branch_pattern[3:]
5166 else:
5172 else:
5167 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5173 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5168 source_branch_regex = re.compile(source_pattern)
5174 source_branch_regex = re.compile(source_pattern)
5169 source_branch_match = bool(source_branch_regex.search(source_branch))
5175 source_branch_match = bool(source_branch_regex.search(source_branch))
5170 if self.target_branch_pattern == '*':
5176 if self.target_branch_pattern == '*':
5171 target_branch_match = True
5177 target_branch_match = True
5172 else:
5178 else:
5173 if self.target_branch_pattern.startswith('re:'):
5179 if self.target_branch_pattern.startswith('re:'):
5174 target_pattern = self.target_branch_pattern[3:]
5180 target_pattern = self.target_branch_pattern[3:]
5175 else:
5181 else:
5176 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5182 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5177 target_branch_regex = re.compile(target_pattern)
5183 target_branch_regex = re.compile(target_pattern)
5178 target_branch_match = bool(target_branch_regex.search(target_branch))
5184 target_branch_match = bool(target_branch_regex.search(target_branch))
5179
5185
5180 branch_matches = source_branch_match and target_branch_match
5186 branch_matches = source_branch_match and target_branch_match
5181
5187
5182 files_matches = True
5188 files_matches = True
5183 if self.file_pattern != '*':
5189 if self.file_pattern != '*':
5184 files_matches = False
5190 files_matches = False
5185 if self.file_pattern.startswith('re:'):
5191 if self.file_pattern.startswith('re:'):
5186 file_pattern = self.file_pattern[3:]
5192 file_pattern = self.file_pattern[3:]
5187 else:
5193 else:
5188 file_pattern = glob2re(self.file_pattern)
5194 file_pattern = glob2re(self.file_pattern)
5189 file_regex = re.compile(file_pattern)
5195 file_regex = re.compile(file_pattern)
5190 for file_data in files_changed:
5196 for file_data in files_changed:
5191 filename = file_data.get('filename')
5197 filename = file_data.get('filename')
5192
5198
5193 if file_regex.search(filename):
5199 if file_regex.search(filename):
5194 files_matches = True
5200 files_matches = True
5195 break
5201 break
5196
5202
5197 return branch_matches and files_matches
5203 return branch_matches and files_matches
5198
5204
5199 @property
5205 @property
5200 def review_users(self):
5206 def review_users(self):
5201 """ Returns the users which this rule applies to """
5207 """ Returns the users which this rule applies to """
5202
5208
5203 users = collections.OrderedDict()
5209 users = collections.OrderedDict()
5204
5210
5205 for rule_user in self.rule_users:
5211 for rule_user in self.rule_users:
5206 if rule_user.user.active:
5212 if rule_user.user.active:
5207 if rule_user.user not in users:
5213 if rule_user.user not in users:
5208 users[rule_user.user.username] = {
5214 users[rule_user.user.username] = {
5209 'user': rule_user.user,
5215 'user': rule_user.user,
5210 'source': 'user',
5216 'source': 'user',
5211 'source_data': {},
5217 'source_data': {},
5212 'data': rule_user.rule_data()
5218 'data': rule_user.rule_data()
5213 }
5219 }
5214
5220
5215 for rule_user_group in self.rule_user_groups:
5221 for rule_user_group in self.rule_user_groups:
5216 source_data = {
5222 source_data = {
5217 'user_group_id': rule_user_group.users_group.users_group_id,
5223 'user_group_id': rule_user_group.users_group.users_group_id,
5218 'name': rule_user_group.users_group.users_group_name,
5224 'name': rule_user_group.users_group.users_group_name,
5219 'members': len(rule_user_group.users_group.members)
5225 'members': len(rule_user_group.users_group.members)
5220 }
5226 }
5221 for member in rule_user_group.users_group.members:
5227 for member in rule_user_group.users_group.members:
5222 if member.user.active:
5228 if member.user.active:
5223 key = member.user.username
5229 key = member.user.username
5224 if key in users:
5230 if key in users:
5225 # skip this member as we have him already
5231 # skip this member as we have him already
5226 # this prevents from override the "first" matched
5232 # this prevents from override the "first" matched
5227 # users with duplicates in multiple groups
5233 # users with duplicates in multiple groups
5228 continue
5234 continue
5229
5235
5230 users[key] = {
5236 users[key] = {
5231 'user': member.user,
5237 'user': member.user,
5232 'source': 'user_group',
5238 'source': 'user_group',
5233 'source_data': source_data,
5239 'source_data': source_data,
5234 'data': rule_user_group.rule_data()
5240 'data': rule_user_group.rule_data()
5235 }
5241 }
5236
5242
5237 return users
5243 return users
5238
5244
5239 def user_group_vote_rule(self, user_id):
5245 def user_group_vote_rule(self, user_id):
5240
5246
5241 rules = []
5247 rules = []
5242 if not self.rule_user_groups:
5248 if not self.rule_user_groups:
5243 return rules
5249 return rules
5244
5250
5245 for user_group in self.rule_user_groups:
5251 for user_group in self.rule_user_groups:
5246 user_group_members = [x.user_id for x in user_group.users_group.members]
5252 user_group_members = [x.user_id for x in user_group.users_group.members]
5247 if user_id in user_group_members:
5253 if user_id in user_group_members:
5248 rules.append(user_group)
5254 rules.append(user_group)
5249 return rules
5255 return rules
5250
5256
5251 def __repr__(self):
5257 def __repr__(self):
5252 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
5258 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
5253 self.repo_review_rule_id, self.repo)
5259 self.repo_review_rule_id, self.repo)
5254
5260
5255
5261
5256 class ScheduleEntry(Base, BaseModel):
5262 class ScheduleEntry(Base, BaseModel):
5257 __tablename__ = 'schedule_entries'
5263 __tablename__ = 'schedule_entries'
5258 __table_args__ = (
5264 __table_args__ = (
5259 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5265 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5260 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5266 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5261 base_table_args,
5267 base_table_args,
5262 )
5268 )
5263
5269
5264 schedule_types = ['crontab', 'timedelta', 'integer']
5270 schedule_types = ['crontab', 'timedelta', 'integer']
5265 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5271 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5266
5272
5267 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5273 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5268 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5274 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5269 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5275 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5270
5276
5271 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5277 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5272 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5278 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5273
5279
5274 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5280 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5275 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5281 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5276
5282
5277 # task
5283 # task
5278 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5284 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5279 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5285 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5280 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5286 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5281 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5287 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5282
5288
5283 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5289 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5284 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5290 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5285
5291
5286 @hybrid_property
5292 @hybrid_property
5287 def schedule_type(self):
5293 def schedule_type(self):
5288 return self._schedule_type
5294 return self._schedule_type
5289
5295
5290 @schedule_type.setter
5296 @schedule_type.setter
5291 def schedule_type(self, val):
5297 def schedule_type(self, val):
5292 if val not in self.schedule_types:
5298 if val not in self.schedule_types:
5293 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5299 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5294 val, self.schedule_type))
5300 val, self.schedule_type))
5295
5301
5296 self._schedule_type = val
5302 self._schedule_type = val
5297
5303
5298 @classmethod
5304 @classmethod
5299 def get_uid(cls, obj):
5305 def get_uid(cls, obj):
5300 args = obj.task_args
5306 args = obj.task_args
5301 kwargs = obj.task_kwargs
5307 kwargs = obj.task_kwargs
5302 if isinstance(args, JsonRaw):
5308 if isinstance(args, JsonRaw):
5303 try:
5309 try:
5304 args = json.loads(args)
5310 args = json.loads(args)
5305 except ValueError:
5311 except ValueError:
5306 args = tuple()
5312 args = tuple()
5307
5313
5308 if isinstance(kwargs, JsonRaw):
5314 if isinstance(kwargs, JsonRaw):
5309 try:
5315 try:
5310 kwargs = json.loads(kwargs)
5316 kwargs = json.loads(kwargs)
5311 except ValueError:
5317 except ValueError:
5312 kwargs = dict()
5318 kwargs = dict()
5313
5319
5314 dot_notation = obj.task_dot_notation
5320 dot_notation = obj.task_dot_notation
5315 val = '.'.join(map(safe_str, [
5321 val = '.'.join(map(safe_str, [
5316 sorted(dot_notation), args, sorted(kwargs.items())]))
5322 sorted(dot_notation), args, sorted(kwargs.items())]))
5317 return hashlib.sha1(val).hexdigest()
5323 return hashlib.sha1(val).hexdigest()
5318
5324
5319 @classmethod
5325 @classmethod
5320 def get_by_schedule_name(cls, schedule_name):
5326 def get_by_schedule_name(cls, schedule_name):
5321 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5327 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5322
5328
5323 @classmethod
5329 @classmethod
5324 def get_by_schedule_id(cls, schedule_id):
5330 def get_by_schedule_id(cls, schedule_id):
5325 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5331 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5326
5332
5327 @property
5333 @property
5328 def task(self):
5334 def task(self):
5329 return self.task_dot_notation
5335 return self.task_dot_notation
5330
5336
5331 @property
5337 @property
5332 def schedule(self):
5338 def schedule(self):
5333 from rhodecode.lib.celerylib.utils import raw_2_schedule
5339 from rhodecode.lib.celerylib.utils import raw_2_schedule
5334 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5340 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5335 return schedule
5341 return schedule
5336
5342
5337 @property
5343 @property
5338 def args(self):
5344 def args(self):
5339 try:
5345 try:
5340 return list(self.task_args or [])
5346 return list(self.task_args or [])
5341 except ValueError:
5347 except ValueError:
5342 return list()
5348 return list()
5343
5349
5344 @property
5350 @property
5345 def kwargs(self):
5351 def kwargs(self):
5346 try:
5352 try:
5347 return dict(self.task_kwargs or {})
5353 return dict(self.task_kwargs or {})
5348 except ValueError:
5354 except ValueError:
5349 return dict()
5355 return dict()
5350
5356
5351 def _as_raw(self, val, indent=None):
5357 def _as_raw(self, val, indent=None):
5352 if hasattr(val, 'de_coerce'):
5358 if hasattr(val, 'de_coerce'):
5353 val = val.de_coerce()
5359 val = val.de_coerce()
5354 if val:
5360 if val:
5355 val = json.dumps(val, indent=indent, sort_keys=True)
5361 val = json.dumps(val, indent=indent, sort_keys=True)
5356
5362
5357 return val
5363 return val
5358
5364
5359 @property
5365 @property
5360 def schedule_definition_raw(self):
5366 def schedule_definition_raw(self):
5361 return self._as_raw(self.schedule_definition)
5367 return self._as_raw(self.schedule_definition)
5362
5368
5363 def args_raw(self, indent=None):
5369 def args_raw(self, indent=None):
5364 return self._as_raw(self.task_args, indent)
5370 return self._as_raw(self.task_args, indent)
5365
5371
5366 def kwargs_raw(self, indent=None):
5372 def kwargs_raw(self, indent=None):
5367 return self._as_raw(self.task_kwargs, indent)
5373 return self._as_raw(self.task_kwargs, indent)
5368
5374
5369 def __repr__(self):
5375 def __repr__(self):
5370 return '<DB:ScheduleEntry({}:{})>'.format(
5376 return '<DB:ScheduleEntry({}:{})>'.format(
5371 self.schedule_entry_id, self.schedule_name)
5377 self.schedule_entry_id, self.schedule_name)
5372
5378
5373
5379
5374 @event.listens_for(ScheduleEntry, 'before_update')
5380 @event.listens_for(ScheduleEntry, 'before_update')
5375 def update_task_uid(mapper, connection, target):
5381 def update_task_uid(mapper, connection, target):
5376 target.task_uid = ScheduleEntry.get_uid(target)
5382 target.task_uid = ScheduleEntry.get_uid(target)
5377
5383
5378
5384
5379 @event.listens_for(ScheduleEntry, 'before_insert')
5385 @event.listens_for(ScheduleEntry, 'before_insert')
5380 def set_task_uid(mapper, connection, target):
5386 def set_task_uid(mapper, connection, target):
5381 target.task_uid = ScheduleEntry.get_uid(target)
5387 target.task_uid = ScheduleEntry.get_uid(target)
5382
5388
5383
5389
5384 class _BaseBranchPerms(BaseModel):
5390 class _BaseBranchPerms(BaseModel):
5385 @classmethod
5391 @classmethod
5386 def compute_hash(cls, value):
5392 def compute_hash(cls, value):
5387 return sha1_safe(value)
5393 return sha1_safe(value)
5388
5394
5389 @hybrid_property
5395 @hybrid_property
5390 def branch_pattern(self):
5396 def branch_pattern(self):
5391 return self._branch_pattern or '*'
5397 return self._branch_pattern or '*'
5392
5398
5393 @hybrid_property
5399 @hybrid_property
5394 def branch_hash(self):
5400 def branch_hash(self):
5395 return self._branch_hash
5401 return self._branch_hash
5396
5402
5397 def _validate_glob(self, value):
5403 def _validate_glob(self, value):
5398 re.compile('^' + glob2re(value) + '$')
5404 re.compile('^' + glob2re(value) + '$')
5399
5405
5400 @branch_pattern.setter
5406 @branch_pattern.setter
5401 def branch_pattern(self, value):
5407 def branch_pattern(self, value):
5402 self._validate_glob(value)
5408 self._validate_glob(value)
5403 self._branch_pattern = value or '*'
5409 self._branch_pattern = value or '*'
5404 # set the Hash when setting the branch pattern
5410 # set the Hash when setting the branch pattern
5405 self._branch_hash = self.compute_hash(self._branch_pattern)
5411 self._branch_hash = self.compute_hash(self._branch_pattern)
5406
5412
5407 def matches(self, branch):
5413 def matches(self, branch):
5408 """
5414 """
5409 Check if this the branch matches entry
5415 Check if this the branch matches entry
5410
5416
5411 :param branch: branch name for the commit
5417 :param branch: branch name for the commit
5412 """
5418 """
5413
5419
5414 branch = branch or ''
5420 branch = branch or ''
5415
5421
5416 branch_matches = True
5422 branch_matches = True
5417 if branch:
5423 if branch:
5418 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5424 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5419 branch_matches = bool(branch_regex.search(branch))
5425 branch_matches = bool(branch_regex.search(branch))
5420
5426
5421 return branch_matches
5427 return branch_matches
5422
5428
5423
5429
5424 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5430 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5425 __tablename__ = 'user_to_repo_branch_permissions'
5431 __tablename__ = 'user_to_repo_branch_permissions'
5426 __table_args__ = (
5432 __table_args__ = (
5427 base_table_args
5433 base_table_args
5428 )
5434 )
5429
5435
5430 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5436 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5431
5437
5432 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5438 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5433 repo = relationship('Repository', backref='user_branch_perms')
5439 repo = relationship('Repository', backref='user_branch_perms')
5434
5440
5435 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5441 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5436 permission = relationship('Permission')
5442 permission = relationship('Permission')
5437
5443
5438 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5444 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5439 user_repo_to_perm = relationship('UserRepoToPerm')
5445 user_repo_to_perm = relationship('UserRepoToPerm')
5440
5446
5441 rule_order = Column('rule_order', Integer(), nullable=False)
5447 rule_order = Column('rule_order', Integer(), nullable=False)
5442 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5448 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5443 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5449 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5444
5450
5445 def __unicode__(self):
5451 def __unicode__(self):
5446 return u'<UserBranchPermission(%s => %r)>' % (
5452 return u'<UserBranchPermission(%s => %r)>' % (
5447 self.user_repo_to_perm, self.branch_pattern)
5453 self.user_repo_to_perm, self.branch_pattern)
5448
5454
5449
5455
5450 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5456 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5451 __tablename__ = 'user_group_to_repo_branch_permissions'
5457 __tablename__ = 'user_group_to_repo_branch_permissions'
5452 __table_args__ = (
5458 __table_args__ = (
5453 base_table_args
5459 base_table_args
5454 )
5460 )
5455
5461
5456 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5462 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5457
5463
5458 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5464 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5459 repo = relationship('Repository', backref='user_group_branch_perms')
5465 repo = relationship('Repository', backref='user_group_branch_perms')
5460
5466
5461 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5467 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5462 permission = relationship('Permission')
5468 permission = relationship('Permission')
5463
5469
5464 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5470 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5465 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5471 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5466
5472
5467 rule_order = Column('rule_order', Integer(), nullable=False)
5473 rule_order = Column('rule_order', Integer(), nullable=False)
5468 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5474 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5469 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5475 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5470
5476
5471 def __unicode__(self):
5477 def __unicode__(self):
5472 return u'<UserBranchPermission(%s => %r)>' % (
5478 return u'<UserBranchPermission(%s => %r)>' % (
5473 self.user_group_repo_to_perm, self.branch_pattern)
5479 self.user_group_repo_to_perm, self.branch_pattern)
5474
5480
5475
5481
5476 class UserBookmark(Base, BaseModel):
5482 class UserBookmark(Base, BaseModel):
5477 __tablename__ = 'user_bookmarks'
5483 __tablename__ = 'user_bookmarks'
5478 __table_args__ = (
5484 __table_args__ = (
5479 UniqueConstraint('user_id', 'bookmark_repo_id'),
5485 UniqueConstraint('user_id', 'bookmark_repo_id'),
5480 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5486 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5481 UniqueConstraint('user_id', 'bookmark_position'),
5487 UniqueConstraint('user_id', 'bookmark_position'),
5482 base_table_args
5488 base_table_args
5483 )
5489 )
5484
5490
5485 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5491 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5486 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5492 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5487 position = Column("bookmark_position", Integer(), nullable=False)
5493 position = Column("bookmark_position", Integer(), nullable=False)
5488 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5494 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5489 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5495 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5490 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5496 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5491
5497
5492 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5498 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5493 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5499 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5494
5500
5495 user = relationship("User")
5501 user = relationship("User")
5496
5502
5497 repository = relationship("Repository")
5503 repository = relationship("Repository")
5498 repository_group = relationship("RepoGroup")
5504 repository_group = relationship("RepoGroup")
5499
5505
5500 @classmethod
5506 @classmethod
5501 def get_by_position_for_user(cls, position, user_id):
5507 def get_by_position_for_user(cls, position, user_id):
5502 return cls.query() \
5508 return cls.query() \
5503 .filter(UserBookmark.user_id == user_id) \
5509 .filter(UserBookmark.user_id == user_id) \
5504 .filter(UserBookmark.position == position).scalar()
5510 .filter(UserBookmark.position == position).scalar()
5505
5511
5506 @classmethod
5512 @classmethod
5507 def get_bookmarks_for_user(cls, user_id, cache=True):
5513 def get_bookmarks_for_user(cls, user_id, cache=True):
5508 bookmarks = cls.query() \
5514 bookmarks = cls.query() \
5509 .filter(UserBookmark.user_id == user_id) \
5515 .filter(UserBookmark.user_id == user_id) \
5510 .options(joinedload(UserBookmark.repository)) \
5516 .options(joinedload(UserBookmark.repository)) \
5511 .options(joinedload(UserBookmark.repository_group)) \
5517 .options(joinedload(UserBookmark.repository_group)) \
5512 .order_by(UserBookmark.position.asc())
5518 .order_by(UserBookmark.position.asc())
5513
5519
5514 if cache:
5520 if cache:
5515 bookmarks = bookmarks.options(
5521 bookmarks = bookmarks.options(
5516 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5522 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5517 )
5523 )
5518
5524
5519 return bookmarks.all()
5525 return bookmarks.all()
5520
5526
5521 def __unicode__(self):
5527 def __unicode__(self):
5522 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5528 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5523
5529
5524
5530
5525 class FileStore(Base, BaseModel):
5531 class FileStore(Base, BaseModel):
5526 __tablename__ = 'file_store'
5532 __tablename__ = 'file_store'
5527 __table_args__ = (
5533 __table_args__ = (
5528 base_table_args
5534 base_table_args
5529 )
5535 )
5530
5536
5531 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5537 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5532 file_uid = Column('file_uid', String(1024), nullable=False)
5538 file_uid = Column('file_uid', String(1024), nullable=False)
5533 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5539 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5534 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5540 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5535 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5541 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5536
5542
5537 # sha256 hash
5543 # sha256 hash
5538 file_hash = Column('file_hash', String(512), nullable=False)
5544 file_hash = Column('file_hash', String(512), nullable=False)
5539 file_size = Column('file_size', BigInteger(), nullable=False)
5545 file_size = Column('file_size', BigInteger(), nullable=False)
5540
5546
5541 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5547 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5542 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5548 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5543 accessed_count = Column('accessed_count', Integer(), default=0)
5549 accessed_count = Column('accessed_count', Integer(), default=0)
5544
5550
5545 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5551 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5546
5552
5547 # if repo/repo_group reference is set, check for permissions
5553 # if repo/repo_group reference is set, check for permissions
5548 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5554 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5549
5555
5550 # hidden defines an attachment that should be hidden from showing in artifact listing
5556 # hidden defines an attachment that should be hidden from showing in artifact listing
5551 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5557 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5552
5558
5553 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5559 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5554 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5560 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5555
5561
5556 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5562 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5557
5563
5558 # scope limited to user, which requester have access to
5564 # scope limited to user, which requester have access to
5559 scope_user_id = Column(
5565 scope_user_id = Column(
5560 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5566 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5561 nullable=True, unique=None, default=None)
5567 nullable=True, unique=None, default=None)
5562 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5568 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5563
5569
5564 # scope limited to user group, which requester have access to
5570 # scope limited to user group, which requester have access to
5565 scope_user_group_id = Column(
5571 scope_user_group_id = Column(
5566 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5572 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5567 nullable=True, unique=None, default=None)
5573 nullable=True, unique=None, default=None)
5568 user_group = relationship('UserGroup', lazy='joined')
5574 user_group = relationship('UserGroup', lazy='joined')
5569
5575
5570 # scope limited to repo, which requester have access to
5576 # scope limited to repo, which requester have access to
5571 scope_repo_id = Column(
5577 scope_repo_id = Column(
5572 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5578 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5573 nullable=True, unique=None, default=None)
5579 nullable=True, unique=None, default=None)
5574 repo = relationship('Repository', lazy='joined')
5580 repo = relationship('Repository', lazy='joined')
5575
5581
5576 # scope limited to repo group, which requester have access to
5582 # scope limited to repo group, which requester have access to
5577 scope_repo_group_id = Column(
5583 scope_repo_group_id = Column(
5578 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5584 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5579 nullable=True, unique=None, default=None)
5585 nullable=True, unique=None, default=None)
5580 repo_group = relationship('RepoGroup', lazy='joined')
5586 repo_group = relationship('RepoGroup', lazy='joined')
5581
5587
5582 @classmethod
5588 @classmethod
5583 def get_by_store_uid(cls, file_store_uid, safe=False):
5589 def get_by_store_uid(cls, file_store_uid, safe=False):
5584 if safe:
5590 if safe:
5585 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5591 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5586 else:
5592 else:
5587 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5593 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5588
5594
5589 @classmethod
5595 @classmethod
5590 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5596 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5591 file_description='', enabled=True, hidden=False, check_acl=True,
5597 file_description='', enabled=True, hidden=False, check_acl=True,
5592 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5598 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5593
5599
5594 store_entry = FileStore()
5600 store_entry = FileStore()
5595 store_entry.file_uid = file_uid
5601 store_entry.file_uid = file_uid
5596 store_entry.file_display_name = file_display_name
5602 store_entry.file_display_name = file_display_name
5597 store_entry.file_org_name = filename
5603 store_entry.file_org_name = filename
5598 store_entry.file_size = file_size
5604 store_entry.file_size = file_size
5599 store_entry.file_hash = file_hash
5605 store_entry.file_hash = file_hash
5600 store_entry.file_description = file_description
5606 store_entry.file_description = file_description
5601
5607
5602 store_entry.check_acl = check_acl
5608 store_entry.check_acl = check_acl
5603 store_entry.enabled = enabled
5609 store_entry.enabled = enabled
5604 store_entry.hidden = hidden
5610 store_entry.hidden = hidden
5605
5611
5606 store_entry.user_id = user_id
5612 store_entry.user_id = user_id
5607 store_entry.scope_user_id = scope_user_id
5613 store_entry.scope_user_id = scope_user_id
5608 store_entry.scope_repo_id = scope_repo_id
5614 store_entry.scope_repo_id = scope_repo_id
5609 store_entry.scope_repo_group_id = scope_repo_group_id
5615 store_entry.scope_repo_group_id = scope_repo_group_id
5610
5616
5611 return store_entry
5617 return store_entry
5612
5618
5613 @classmethod
5619 @classmethod
5614 def store_metadata(cls, file_store_id, args, commit=True):
5620 def store_metadata(cls, file_store_id, args, commit=True):
5615 file_store = FileStore.get(file_store_id)
5621 file_store = FileStore.get(file_store_id)
5616 if file_store is None:
5622 if file_store is None:
5617 return
5623 return
5618
5624
5619 for section, key, value, value_type in args:
5625 for section, key, value, value_type in args:
5620 has_key = FileStoreMetadata().query() \
5626 has_key = FileStoreMetadata().query() \
5621 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5627 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5622 .filter(FileStoreMetadata.file_store_meta_section == section) \
5628 .filter(FileStoreMetadata.file_store_meta_section == section) \
5623 .filter(FileStoreMetadata.file_store_meta_key == key) \
5629 .filter(FileStoreMetadata.file_store_meta_key == key) \
5624 .scalar()
5630 .scalar()
5625 if has_key:
5631 if has_key:
5626 msg = 'key `{}` already defined under section `{}` for this file.'\
5632 msg = 'key `{}` already defined under section `{}` for this file.'\
5627 .format(key, section)
5633 .format(key, section)
5628 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5634 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5629
5635
5630 # NOTE(marcink): raises ArtifactMetadataBadValueType
5636 # NOTE(marcink): raises ArtifactMetadataBadValueType
5631 FileStoreMetadata.valid_value_type(value_type)
5637 FileStoreMetadata.valid_value_type(value_type)
5632
5638
5633 meta_entry = FileStoreMetadata()
5639 meta_entry = FileStoreMetadata()
5634 meta_entry.file_store = file_store
5640 meta_entry.file_store = file_store
5635 meta_entry.file_store_meta_section = section
5641 meta_entry.file_store_meta_section = section
5636 meta_entry.file_store_meta_key = key
5642 meta_entry.file_store_meta_key = key
5637 meta_entry.file_store_meta_value_type = value_type
5643 meta_entry.file_store_meta_value_type = value_type
5638 meta_entry.file_store_meta_value = value
5644 meta_entry.file_store_meta_value = value
5639
5645
5640 Session().add(meta_entry)
5646 Session().add(meta_entry)
5641
5647
5642 try:
5648 try:
5643 if commit:
5649 if commit:
5644 Session().commit()
5650 Session().commit()
5645 except IntegrityError:
5651 except IntegrityError:
5646 Session().rollback()
5652 Session().rollback()
5647 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5653 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5648
5654
5649 @classmethod
5655 @classmethod
5650 def bump_access_counter(cls, file_uid, commit=True):
5656 def bump_access_counter(cls, file_uid, commit=True):
5651 FileStore().query()\
5657 FileStore().query()\
5652 .filter(FileStore.file_uid == file_uid)\
5658 .filter(FileStore.file_uid == file_uid)\
5653 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5659 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5654 FileStore.accessed_on: datetime.datetime.now()})
5660 FileStore.accessed_on: datetime.datetime.now()})
5655 if commit:
5661 if commit:
5656 Session().commit()
5662 Session().commit()
5657
5663
5658 def __json__(self):
5664 def __json__(self):
5659 data = {
5665 data = {
5660 'filename': self.file_display_name,
5666 'filename': self.file_display_name,
5661 'filename_org': self.file_org_name,
5667 'filename_org': self.file_org_name,
5662 'file_uid': self.file_uid,
5668 'file_uid': self.file_uid,
5663 'description': self.file_description,
5669 'description': self.file_description,
5664 'hidden': self.hidden,
5670 'hidden': self.hidden,
5665 'size': self.file_size,
5671 'size': self.file_size,
5666 'created_on': self.created_on,
5672 'created_on': self.created_on,
5667 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5673 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5668 'downloaded_times': self.accessed_count,
5674 'downloaded_times': self.accessed_count,
5669 'sha256': self.file_hash,
5675 'sha256': self.file_hash,
5670 'metadata': self.file_metadata,
5676 'metadata': self.file_metadata,
5671 }
5677 }
5672
5678
5673 return data
5679 return data
5674
5680
5675 def __repr__(self):
5681 def __repr__(self):
5676 return '<FileStore({})>'.format(self.file_store_id)
5682 return '<FileStore({})>'.format(self.file_store_id)
5677
5683
5678
5684
5679 class FileStoreMetadata(Base, BaseModel):
5685 class FileStoreMetadata(Base, BaseModel):
5680 __tablename__ = 'file_store_metadata'
5686 __tablename__ = 'file_store_metadata'
5681 __table_args__ = (
5687 __table_args__ = (
5682 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5688 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5683 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5689 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5684 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5690 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5685 base_table_args
5691 base_table_args
5686 )
5692 )
5687 SETTINGS_TYPES = {
5693 SETTINGS_TYPES = {
5688 'str': safe_str,
5694 'str': safe_str,
5689 'int': safe_int,
5695 'int': safe_int,
5690 'unicode': safe_unicode,
5696 'unicode': safe_unicode,
5691 'bool': str2bool,
5697 'bool': str2bool,
5692 'list': functools.partial(aslist, sep=',')
5698 'list': functools.partial(aslist, sep=',')
5693 }
5699 }
5694
5700
5695 file_store_meta_id = Column(
5701 file_store_meta_id = Column(
5696 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5702 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5697 primary_key=True)
5703 primary_key=True)
5698 _file_store_meta_section = Column(
5704 _file_store_meta_section = Column(
5699 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5705 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5700 nullable=True, unique=None, default=None)
5706 nullable=True, unique=None, default=None)
5701 _file_store_meta_section_hash = Column(
5707 _file_store_meta_section_hash = Column(
5702 "file_store_meta_section_hash", String(255),
5708 "file_store_meta_section_hash", String(255),
5703 nullable=True, unique=None, default=None)
5709 nullable=True, unique=None, default=None)
5704 _file_store_meta_key = Column(
5710 _file_store_meta_key = Column(
5705 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5711 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5706 nullable=True, unique=None, default=None)
5712 nullable=True, unique=None, default=None)
5707 _file_store_meta_key_hash = Column(
5713 _file_store_meta_key_hash = Column(
5708 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5714 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5709 _file_store_meta_value = Column(
5715 _file_store_meta_value = Column(
5710 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5716 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5711 nullable=True, unique=None, default=None)
5717 nullable=True, unique=None, default=None)
5712 _file_store_meta_value_type = Column(
5718 _file_store_meta_value_type = Column(
5713 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5719 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5714 default='unicode')
5720 default='unicode')
5715
5721
5716 file_store_id = Column(
5722 file_store_id = Column(
5717 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5723 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5718 nullable=True, unique=None, default=None)
5724 nullable=True, unique=None, default=None)
5719
5725
5720 file_store = relationship('FileStore', lazy='joined')
5726 file_store = relationship('FileStore', lazy='joined')
5721
5727
5722 @classmethod
5728 @classmethod
5723 def valid_value_type(cls, value):
5729 def valid_value_type(cls, value):
5724 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5730 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5725 raise ArtifactMetadataBadValueType(
5731 raise ArtifactMetadataBadValueType(
5726 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5732 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5727
5733
5728 @hybrid_property
5734 @hybrid_property
5729 def file_store_meta_section(self):
5735 def file_store_meta_section(self):
5730 return self._file_store_meta_section
5736 return self._file_store_meta_section
5731
5737
5732 @file_store_meta_section.setter
5738 @file_store_meta_section.setter
5733 def file_store_meta_section(self, value):
5739 def file_store_meta_section(self, value):
5734 self._file_store_meta_section = value
5740 self._file_store_meta_section = value
5735 self._file_store_meta_section_hash = _hash_key(value)
5741 self._file_store_meta_section_hash = _hash_key(value)
5736
5742
5737 @hybrid_property
5743 @hybrid_property
5738 def file_store_meta_key(self):
5744 def file_store_meta_key(self):
5739 return self._file_store_meta_key
5745 return self._file_store_meta_key
5740
5746
5741 @file_store_meta_key.setter
5747 @file_store_meta_key.setter
5742 def file_store_meta_key(self, value):
5748 def file_store_meta_key(self, value):
5743 self._file_store_meta_key = value
5749 self._file_store_meta_key = value
5744 self._file_store_meta_key_hash = _hash_key(value)
5750 self._file_store_meta_key_hash = _hash_key(value)
5745
5751
5746 @hybrid_property
5752 @hybrid_property
5747 def file_store_meta_value(self):
5753 def file_store_meta_value(self):
5748 val = self._file_store_meta_value
5754 val = self._file_store_meta_value
5749
5755
5750 if self._file_store_meta_value_type:
5756 if self._file_store_meta_value_type:
5751 # e.g unicode.encrypted == unicode
5757 # e.g unicode.encrypted == unicode
5752 _type = self._file_store_meta_value_type.split('.')[0]
5758 _type = self._file_store_meta_value_type.split('.')[0]
5753 # decode the encrypted value if it's encrypted field type
5759 # decode the encrypted value if it's encrypted field type
5754 if '.encrypted' in self._file_store_meta_value_type:
5760 if '.encrypted' in self._file_store_meta_value_type:
5755 cipher = EncryptedTextValue()
5761 cipher = EncryptedTextValue()
5756 val = safe_unicode(cipher.process_result_value(val, None))
5762 val = safe_unicode(cipher.process_result_value(val, None))
5757 # do final type conversion
5763 # do final type conversion
5758 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5764 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5759 val = converter(val)
5765 val = converter(val)
5760
5766
5761 return val
5767 return val
5762
5768
5763 @file_store_meta_value.setter
5769 @file_store_meta_value.setter
5764 def file_store_meta_value(self, val):
5770 def file_store_meta_value(self, val):
5765 val = safe_unicode(val)
5771 val = safe_unicode(val)
5766 # encode the encrypted value
5772 # encode the encrypted value
5767 if '.encrypted' in self.file_store_meta_value_type:
5773 if '.encrypted' in self.file_store_meta_value_type:
5768 cipher = EncryptedTextValue()
5774 cipher = EncryptedTextValue()
5769 val = safe_unicode(cipher.process_bind_param(val, None))
5775 val = safe_unicode(cipher.process_bind_param(val, None))
5770 self._file_store_meta_value = val
5776 self._file_store_meta_value = val
5771
5777
5772 @hybrid_property
5778 @hybrid_property
5773 def file_store_meta_value_type(self):
5779 def file_store_meta_value_type(self):
5774 return self._file_store_meta_value_type
5780 return self._file_store_meta_value_type
5775
5781
5776 @file_store_meta_value_type.setter
5782 @file_store_meta_value_type.setter
5777 def file_store_meta_value_type(self, val):
5783 def file_store_meta_value_type(self, val):
5778 # e.g unicode.encrypted
5784 # e.g unicode.encrypted
5779 self.valid_value_type(val)
5785 self.valid_value_type(val)
5780 self._file_store_meta_value_type = val
5786 self._file_store_meta_value_type = val
5781
5787
5782 def __json__(self):
5788 def __json__(self):
5783 data = {
5789 data = {
5784 'artifact': self.file_store.file_uid,
5790 'artifact': self.file_store.file_uid,
5785 'section': self.file_store_meta_section,
5791 'section': self.file_store_meta_section,
5786 'key': self.file_store_meta_key,
5792 'key': self.file_store_meta_key,
5787 'value': self.file_store_meta_value,
5793 'value': self.file_store_meta_value,
5788 }
5794 }
5789
5795
5790 return data
5796 return data
5791
5797
5792 def __repr__(self):
5798 def __repr__(self):
5793 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5799 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5794 self.file_store_meta_key, self.file_store_meta_value)
5800 self.file_store_meta_key, self.file_store_meta_value)
5795
5801
5796
5802
5797 class DbMigrateVersion(Base, BaseModel):
5803 class DbMigrateVersion(Base, BaseModel):
5798 __tablename__ = 'db_migrate_version'
5804 __tablename__ = 'db_migrate_version'
5799 __table_args__ = (
5805 __table_args__ = (
5800 base_table_args,
5806 base_table_args,
5801 )
5807 )
5802
5808
5803 repository_id = Column('repository_id', String(250), primary_key=True)
5809 repository_id = Column('repository_id', String(250), primary_key=True)
5804 repository_path = Column('repository_path', Text)
5810 repository_path = Column('repository_path', Text)
5805 version = Column('version', Integer)
5811 version = Column('version', Integer)
5806
5812
5807 @classmethod
5813 @classmethod
5808 def set_version(cls, version):
5814 def set_version(cls, version):
5809 """
5815 """
5810 Helper for forcing a different version, usually for debugging purposes via ishell.
5816 Helper for forcing a different version, usually for debugging purposes via ishell.
5811 """
5817 """
5812 ver = DbMigrateVersion.query().first()
5818 ver = DbMigrateVersion.query().first()
5813 ver.version = version
5819 ver.version = version
5814 Session().commit()
5820 Session().commit()
5815
5821
5816
5822
5817 class DbSession(Base, BaseModel):
5823 class DbSession(Base, BaseModel):
5818 __tablename__ = 'db_session'
5824 __tablename__ = 'db_session'
5819 __table_args__ = (
5825 __table_args__ = (
5820 base_table_args,
5826 base_table_args,
5821 )
5827 )
5822
5828
5823 def __repr__(self):
5829 def __repr__(self):
5824 return '<DB:DbSession({})>'.format(self.id)
5830 return '<DB:DbSession({})>'.format(self.id)
5825
5831
5826 id = Column('id', Integer())
5832 id = Column('id', Integer())
5827 namespace = Column('namespace', String(255), primary_key=True)
5833 namespace = Column('namespace', String(255), primary_key=True)
5828 accessed = Column('accessed', DateTime, nullable=False)
5834 accessed = Column('accessed', DateTime, nullable=False)
5829 created = Column('created', DateTime, nullable=False)
5835 created = Column('created', DateTime, nullable=False)
5830 data = Column('data', PickleType, nullable=False)
5836 data = Column('data', PickleType, nullable=False)
@@ -1,1502 +1,1501 b''
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
1 // # Copyright (C) 2010-2020 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 var firefoxAnchorFix = function() {
19 var firefoxAnchorFix = function() {
20 // hack to make anchor links behave properly on firefox, in our inline
20 // hack to make anchor links behave properly on firefox, in our inline
21 // comments generation when comments are injected firefox is misbehaving
21 // comments generation when comments are injected firefox is misbehaving
22 // when jumping to anchor links
22 // when jumping to anchor links
23 if (location.href.indexOf('#') > -1) {
23 if (location.href.indexOf('#') > -1) {
24 location.href += '';
24 location.href += '';
25 }
25 }
26 };
26 };
27
27
28 var linkifyComments = function(comments) {
28 var linkifyComments = function(comments) {
29 var firstCommentId = null;
29 var firstCommentId = null;
30 if (comments) {
30 if (comments) {
31 firstCommentId = $(comments[0]).data('comment-id');
31 firstCommentId = $(comments[0]).data('comment-id');
32 }
32 }
33
33
34 if (firstCommentId){
34 if (firstCommentId){
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 }
36 }
37 };
37 };
38
38
39 var bindToggleButtons = function() {
39 var bindToggleButtons = function() {
40 $('.comment-toggle').on('click', function() {
40 $('.comment-toggle').on('click', function() {
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 });
42 });
43 };
43 };
44
44
45
45
46
46
47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
48 failHandler = failHandler || function() {};
48 failHandler = failHandler || function() {};
49 postData = toQueryString(postData);
49 postData = toQueryString(postData);
50 var request = $.ajax({
50 var request = $.ajax({
51 url: url,
51 url: url,
52 type: 'POST',
52 type: 'POST',
53 data: postData,
53 data: postData,
54 headers: {'X-PARTIAL-XHR': true}
54 headers: {'X-PARTIAL-XHR': true}
55 })
55 })
56 .done(function (data) {
56 .done(function (data) {
57 successHandler(data);
57 successHandler(data);
58 })
58 })
59 .fail(function (data, textStatus, errorThrown) {
59 .fail(function (data, textStatus, errorThrown) {
60 failHandler(data, textStatus, errorThrown)
60 failHandler(data, textStatus, errorThrown)
61 });
61 });
62 return request;
62 return request;
63 };
63 };
64
64
65
65
66
66
67
67
68 /* Comment form for main and inline comments */
68 /* Comment form for main and inline comments */
69 (function(mod) {
69 (function(mod) {
70
70
71 if (typeof exports == "object" && typeof module == "object") {
71 if (typeof exports == "object" && typeof module == "object") {
72 // CommonJS
72 // CommonJS
73 module.exports = mod();
73 module.exports = mod();
74 }
74 }
75 else {
75 else {
76 // Plain browser env
76 // Plain browser env
77 (this || window).CommentForm = mod();
77 (this || window).CommentForm = mod();
78 }
78 }
79
79
80 })(function() {
80 })(function() {
81 "use strict";
81 "use strict";
82
82
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
84
84
85 if (!(this instanceof CommentForm)) {
85 if (!(this instanceof CommentForm)) {
86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
87 }
87 }
88
88
89 // bind the element instance to our Form
89 // bind the element instance to our Form
90 $(formElement).get(0).CommentForm = this;
90 $(formElement).get(0).CommentForm = this;
91
91
92 this.withLineNo = function(selector) {
92 this.withLineNo = function(selector) {
93 var lineNo = this.lineNo;
93 var lineNo = this.lineNo;
94 if (lineNo === undefined) {
94 if (lineNo === undefined) {
95 return selector
95 return selector
96 } else {
96 } else {
97 return selector + '_' + lineNo;
97 return selector + '_' + lineNo;
98 }
98 }
99 };
99 };
100
100
101 this.commitId = commitId;
101 this.commitId = commitId;
102 this.pullRequestId = pullRequestId;
102 this.pullRequestId = pullRequestId;
103 this.lineNo = lineNo;
103 this.lineNo = lineNo;
104 this.initAutocompleteActions = initAutocompleteActions;
104 this.initAutocompleteActions = initAutocompleteActions;
105
105
106 this.previewButton = this.withLineNo('#preview-btn');
106 this.previewButton = this.withLineNo('#preview-btn');
107 this.previewContainer = this.withLineNo('#preview-container');
107 this.previewContainer = this.withLineNo('#preview-container');
108
108
109 this.previewBoxSelector = this.withLineNo('#preview-box');
109 this.previewBoxSelector = this.withLineNo('#preview-box');
110
110
111 this.editButton = this.withLineNo('#edit-btn');
111 this.editButton = this.withLineNo('#edit-btn');
112 this.editContainer = this.withLineNo('#edit-container');
112 this.editContainer = this.withLineNo('#edit-container');
113 this.cancelButton = this.withLineNo('#cancel-btn');
113 this.cancelButton = this.withLineNo('#cancel-btn');
114 this.commentType = this.withLineNo('#comment_type');
114 this.commentType = this.withLineNo('#comment_type');
115
115
116 this.resolvesId = null;
116 this.resolvesId = null;
117 this.resolvesActionId = null;
117 this.resolvesActionId = null;
118
118
119 this.closesPr = '#close_pull_request';
119 this.closesPr = '#close_pull_request';
120
120
121 this.cmBox = this.withLineNo('#text');
121 this.cmBox = this.withLineNo('#text');
122 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
122 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
123
123
124 this.statusChange = this.withLineNo('#change_status');
124 this.statusChange = this.withLineNo('#change_status');
125
125
126 this.submitForm = formElement;
126 this.submitForm = formElement;
127
127
128 this.submitButton = $(this.submitForm).find('.submit-comment-action');
128 this.submitButton = $(this.submitForm).find('.submit-comment-action');
129 this.submitButtonText = this.submitButton.val();
129 this.submitButtonText = this.submitButton.val();
130
130
131 this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
131 this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
132 this.submitDraftButtonText = this.submitDraftButton.val();
132 this.submitDraftButtonText = this.submitDraftButton.val();
133
133
134 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
134 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
135 {'repo_name': templateContext.repo_name,
135 {'repo_name': templateContext.repo_name,
136 'commit_id': templateContext.commit_data.commit_id});
136 'commit_id': templateContext.commit_data.commit_id});
137
137
138 if (edit){
138 if (edit){
139 this.submitDraftButton.hide();
139 this.submitDraftButton.hide();
140 this.submitButtonText = _gettext('Update Comment');
140 this.submitButtonText = _gettext('Update Comment');
141 $(this.commentType).prop('disabled', true);
141 $(this.commentType).prop('disabled', true);
142 $(this.commentType).addClass('disabled');
142 $(this.commentType).addClass('disabled');
143 var editInfo =
143 var editInfo =
144 '';
144 '';
145 $(editInfo).insertBefore($(this.editButton).parent());
145 $(editInfo).insertBefore($(this.editButton).parent());
146 }
146 }
147
147
148 if (resolvesCommentId){
148 if (resolvesCommentId){
149 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
149 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
150 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
150 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
151 $(this.commentType).prop('disabled', true);
151 $(this.commentType).prop('disabled', true);
152 $(this.commentType).addClass('disabled');
152 $(this.commentType).addClass('disabled');
153
153
154 // disable select
154 // disable select
155 setTimeout(function() {
155 setTimeout(function() {
156 $(self.statusChange).select2('readonly', true);
156 $(self.statusChange).select2('readonly', true);
157 }, 10);
157 }, 10);
158
158
159 var resolvedInfo = (
159 var resolvedInfo = (
160 '<li class="resolve-action">' +
160 '<li class="resolve-action">' +
161 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
161 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
162 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
162 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
163 '</li>'
163 '</li>'
164 ).format(resolvesCommentId, _gettext('resolve comment'));
164 ).format(resolvesCommentId, _gettext('resolve comment'));
165 $(resolvedInfo).insertAfter($(this.commentType).parent());
165 $(resolvedInfo).insertAfter($(this.commentType).parent());
166 }
166 }
167
167
168 // based on commitId, or pullRequestId decide where do we submit
168 // based on commitId, or pullRequestId decide where do we submit
169 // out data
169 // out data
170 if (this.commitId){
170 if (this.commitId){
171 var pyurl = 'repo_commit_comment_create';
171 var pyurl = 'repo_commit_comment_create';
172 if(edit){
172 if(edit){
173 pyurl = 'repo_commit_comment_edit';
173 pyurl = 'repo_commit_comment_edit';
174 }
174 }
175 this.submitUrl = pyroutes.url(pyurl,
175 this.submitUrl = pyroutes.url(pyurl,
176 {'repo_name': templateContext.repo_name,
176 {'repo_name': templateContext.repo_name,
177 'commit_id': this.commitId,
177 'commit_id': this.commitId,
178 'comment_id': comment_id});
178 'comment_id': comment_id});
179 this.selfUrl = pyroutes.url('repo_commit',
179 this.selfUrl = pyroutes.url('repo_commit',
180 {'repo_name': templateContext.repo_name,
180 {'repo_name': templateContext.repo_name,
181 'commit_id': this.commitId});
181 'commit_id': this.commitId});
182
182
183 } else if (this.pullRequestId) {
183 } else if (this.pullRequestId) {
184 var pyurl = 'pullrequest_comment_create';
184 var pyurl = 'pullrequest_comment_create';
185 if(edit){
185 if(edit){
186 pyurl = 'pullrequest_comment_edit';
186 pyurl = 'pullrequest_comment_edit';
187 }
187 }
188 this.submitUrl = pyroutes.url(pyurl,
188 this.submitUrl = pyroutes.url(pyurl,
189 {'repo_name': templateContext.repo_name,
189 {'repo_name': templateContext.repo_name,
190 'pull_request_id': this.pullRequestId,
190 'pull_request_id': this.pullRequestId,
191 'comment_id': comment_id});
191 'comment_id': comment_id});
192 this.selfUrl = pyroutes.url('pullrequest_show',
192 this.selfUrl = pyroutes.url('pullrequest_show',
193 {'repo_name': templateContext.repo_name,
193 {'repo_name': templateContext.repo_name,
194 'pull_request_id': this.pullRequestId});
194 'pull_request_id': this.pullRequestId});
195
195
196 } else {
196 } else {
197 throw new Error(
197 throw new Error(
198 'CommentForm requires pullRequestId, or commitId to be specified.')
198 'CommentForm requires pullRequestId, or commitId to be specified.')
199 }
199 }
200
200
201 // FUNCTIONS and helpers
201 // FUNCTIONS and helpers
202 var self = this;
202 var self = this;
203
203
204 this.isInline = function(){
204 this.isInline = function(){
205 return this.lineNo && this.lineNo != 'general';
205 return this.lineNo && this.lineNo != 'general';
206 };
206 };
207
207
208 this.getCmInstance = function(){
208 this.getCmInstance = function(){
209 return this.cm
209 return this.cm
210 };
210 };
211
211
212 this.setPlaceholder = function(placeholder) {
212 this.setPlaceholder = function(placeholder) {
213 var cm = this.getCmInstance();
213 var cm = this.getCmInstance();
214 if (cm){
214 if (cm){
215 cm.setOption('placeholder', placeholder);
215 cm.setOption('placeholder', placeholder);
216 }
216 }
217 };
217 };
218
218
219 this.getCommentStatus = function() {
219 this.getCommentStatus = function() {
220 return $(this.submitForm).find(this.statusChange).val();
220 return $(this.submitForm).find(this.statusChange).val();
221 };
221 };
222
222
223 this.getCommentType = function() {
223 this.getCommentType = function() {
224 return $(this.submitForm).find(this.commentType).val();
224 return $(this.submitForm).find(this.commentType).val();
225 };
225 };
226
226
227 this.getDraftState = function () {
227 this.getDraftState = function () {
228 var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
228 var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
229 var data = $(submitterElem).data('isDraft');
229 var data = $(submitterElem).data('isDraft');
230 return data
230 return data
231 }
231 }
232
232
233 this.getResolvesId = function() {
233 this.getResolvesId = function() {
234 return $(this.submitForm).find(this.resolvesId).val() || null;
234 return $(this.submitForm).find(this.resolvesId).val() || null;
235 };
235 };
236
236
237 this.getClosePr = function() {
237 this.getClosePr = function() {
238 return $(this.submitForm).find(this.closesPr).val() || null;
238 return $(this.submitForm).find(this.closesPr).val() || null;
239 };
239 };
240
240
241 this.markCommentResolved = function(resolvedCommentId){
241 this.markCommentResolved = function(resolvedCommentId){
242 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
242 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
243 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
243 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
244 };
244 };
245
245
246 this.isAllowedToSubmit = function() {
246 this.isAllowedToSubmit = function() {
247 var commentDisabled = $(this.submitButton).prop('disabled');
247 var commentDisabled = $(this.submitButton).prop('disabled');
248 var draftDisabled = $(this.submitDraftButton).prop('disabled');
248 var draftDisabled = $(this.submitDraftButton).prop('disabled');
249 return !commentDisabled && !draftDisabled;
249 return !commentDisabled && !draftDisabled;
250 };
250 };
251
251
252 this.initStatusChangeSelector = function(){
252 this.initStatusChangeSelector = function(){
253 var formatChangeStatus = function(state, escapeMarkup) {
253 var formatChangeStatus = function(state, escapeMarkup) {
254 var originalOption = state.element;
254 var originalOption = state.element;
255 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
255 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
256 return tmpl
256 return tmpl
257 };
257 };
258 var formatResult = function(result, container, query, escapeMarkup) {
258 var formatResult = function(result, container, query, escapeMarkup) {
259 return formatChangeStatus(result, escapeMarkup);
259 return formatChangeStatus(result, escapeMarkup);
260 };
260 };
261
261
262 var formatSelection = function(data, container, escapeMarkup) {
262 var formatSelection = function(data, container, escapeMarkup) {
263 return formatChangeStatus(data, escapeMarkup);
263 return formatChangeStatus(data, escapeMarkup);
264 };
264 };
265
265
266 $(this.submitForm).find(this.statusChange).select2({
266 $(this.submitForm).find(this.statusChange).select2({
267 placeholder: _gettext('Status Review'),
267 placeholder: _gettext('Status Review'),
268 formatResult: formatResult,
268 formatResult: formatResult,
269 formatSelection: formatSelection,
269 formatSelection: formatSelection,
270 containerCssClass: "drop-menu status_box_menu",
270 containerCssClass: "drop-menu status_box_menu",
271 dropdownCssClass: "drop-menu-dropdown",
271 dropdownCssClass: "drop-menu-dropdown",
272 dropdownAutoWidth: true,
272 dropdownAutoWidth: true,
273 minimumResultsForSearch: -1
273 minimumResultsForSearch: -1
274 });
274 });
275
275
276 $(this.submitForm).find(this.statusChange).on('change', function() {
276 $(this.submitForm).find(this.statusChange).on('change', function() {
277 var status = self.getCommentStatus();
277 var status = self.getCommentStatus();
278
278
279 if (status && !self.isInline()) {
279 if (status && !self.isInline()) {
280 $(self.submitButton).prop('disabled', false);
280 $(self.submitButton).prop('disabled', false);
281 $(self.submitDraftButton).prop('disabled', false);
281 $(self.submitDraftButton).prop('disabled', false);
282 }
282 }
283
283
284 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
284 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
285 self.setPlaceholder(placeholderText)
285 self.setPlaceholder(placeholderText)
286 })
286 })
287 };
287 };
288
288
289 // reset the comment form into it's original state
289 // reset the comment form into it's original state
290 this.resetCommentFormState = function(content) {
290 this.resetCommentFormState = function(content) {
291 content = content || '';
291 content = content || '';
292
292
293 $(this.editContainer).show();
293 $(this.editContainer).show();
294 $(this.editButton).parent().addClass('active');
294 $(this.editButton).parent().addClass('active');
295
295
296 $(this.previewContainer).hide();
296 $(this.previewContainer).hide();
297 $(this.previewButton).parent().removeClass('active');
297 $(this.previewButton).parent().removeClass('active');
298
298
299 this.setActionButtonsDisabled(true);
299 this.setActionButtonsDisabled(true);
300 self.cm.setValue(content);
300 self.cm.setValue(content);
301 self.cm.setOption("readOnly", false);
301 self.cm.setOption("readOnly", false);
302
302
303 if (this.resolvesId) {
303 if (this.resolvesId) {
304 // destroy the resolve action
304 // destroy the resolve action
305 $(this.resolvesId).parent().remove();
305 $(this.resolvesId).parent().remove();
306 }
306 }
307 // reset closingPR flag
307 // reset closingPR flag
308 $('.close-pr-input').remove();
308 $('.close-pr-input').remove();
309
309
310 $(this.statusChange).select2('readonly', false);
310 $(this.statusChange).select2('readonly', false);
311 };
311 };
312
312
313 this.globalSubmitSuccessCallback = function(comment){
313 this.globalSubmitSuccessCallback = function(comment){
314 // default behaviour is to call GLOBAL hook, if it's registered.
314 // default behaviour is to call GLOBAL hook, if it's registered.
315 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
315 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
316 commentFormGlobalSubmitSuccessCallback(comment);
316 commentFormGlobalSubmitSuccessCallback(comment);
317 }
317 }
318 };
318 };
319
319
320 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
320 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
321 return _submitAjaxPOST(url, postData, successHandler, failHandler);
321 return _submitAjaxPOST(url, postData, successHandler, failHandler);
322 };
322 };
323
323
324 // overwrite a submitHandler, we need to do it for inline comments
324 // overwrite a submitHandler, we need to do it for inline comments
325 this.setHandleFormSubmit = function(callback) {
325 this.setHandleFormSubmit = function(callback) {
326 this.handleFormSubmit = callback;
326 this.handleFormSubmit = callback;
327 };
327 };
328
328
329 // overwrite a submitSuccessHandler
329 // overwrite a submitSuccessHandler
330 this.setGlobalSubmitSuccessCallback = function(callback) {
330 this.setGlobalSubmitSuccessCallback = function(callback) {
331 this.globalSubmitSuccessCallback = callback;
331 this.globalSubmitSuccessCallback = callback;
332 };
332 };
333
333
334 // default handler for for submit for main comments
334 // default handler for for submit for main comments
335 this.handleFormSubmit = function() {
335 this.handleFormSubmit = function() {
336 var text = self.cm.getValue();
336 var text = self.cm.getValue();
337 var status = self.getCommentStatus();
337 var status = self.getCommentStatus();
338 var commentType = self.getCommentType();
338 var commentType = self.getCommentType();
339 var isDraft = self.getDraftState();
339 var isDraft = self.getDraftState();
340 var resolvesCommentId = self.getResolvesId();
340 var resolvesCommentId = self.getResolvesId();
341 var closePullRequest = self.getClosePr();
341 var closePullRequest = self.getClosePr();
342
342
343 if (text === "" && !status) {
343 if (text === "" && !status) {
344 return;
344 return;
345 }
345 }
346
346
347 var excludeCancelBtn = false;
347 var excludeCancelBtn = false;
348 var submitEvent = true;
348 var submitEvent = true;
349 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
349 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
350 self.cm.setOption("readOnly", true);
350 self.cm.setOption("readOnly", true);
351
351
352 var postData = {
352 var postData = {
353 'text': text,
353 'text': text,
354 'changeset_status': status,
354 'changeset_status': status,
355 'comment_type': commentType,
355 'comment_type': commentType,
356 'csrf_token': CSRF_TOKEN
356 'csrf_token': CSRF_TOKEN
357 };
357 };
358
358
359 if (resolvesCommentId) {
359 if (resolvesCommentId) {
360 postData['resolves_comment_id'] = resolvesCommentId;
360 postData['resolves_comment_id'] = resolvesCommentId;
361 }
361 }
362
362
363 if (closePullRequest) {
363 if (closePullRequest) {
364 postData['close_pull_request'] = true;
364 postData['close_pull_request'] = true;
365 }
365 }
366
366
367 // submitSuccess for general comments
367 // submitSuccess for general comments
368 var submitSuccessCallback = function(json_data) {
368 var submitSuccessCallback = function(json_data) {
369 // reload page if we change status for single commit.
369 // reload page if we change status for single commit.
370 if (status && self.commitId) {
370 if (status && self.commitId) {
371 location.reload(true);
371 location.reload(true);
372 } else {
372 } else {
373 // inject newly created comments, json_data is {<comment_id>: {}}
373 // inject newly created comments, json_data is {<comment_id>: {}}
374 Rhodecode.comments.attachGeneralComment(json_data)
374 Rhodecode.comments.attachGeneralComment(json_data)
375
375
376 self.resetCommentFormState();
376 self.resetCommentFormState();
377 timeagoActivate();
377 timeagoActivate();
378 tooltipActivate();
378 tooltipActivate();
379
379
380 // mark visually which comment was resolved
380 // mark visually which comment was resolved
381 if (resolvesCommentId) {
381 if (resolvesCommentId) {
382 self.markCommentResolved(resolvesCommentId);
382 self.markCommentResolved(resolvesCommentId);
383 }
383 }
384 }
384 }
385
385
386 // run global callback on submit
386 // run global callback on submit
387 self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
387 self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
388
388
389 };
389 };
390 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
390 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
391 var prefix = "Error while submitting comment.\n"
391 var prefix = "Error while submitting comment.\n"
392 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
392 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
393 ajaxErrorSwal(message);
393 ajaxErrorSwal(message);
394 self.resetCommentFormState(text);
394 self.resetCommentFormState(text);
395 };
395 };
396 self.submitAjaxPOST(
396 self.submitAjaxPOST(
397 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
397 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
398 };
398 };
399
399
400 this.previewSuccessCallback = function(o) {
400 this.previewSuccessCallback = function(o) {
401 $(self.previewBoxSelector).html(o);
401 $(self.previewBoxSelector).html(o);
402 $(self.previewBoxSelector).removeClass('unloaded');
402 $(self.previewBoxSelector).removeClass('unloaded');
403
403
404 // swap buttons, making preview active
404 // swap buttons, making preview active
405 $(self.previewButton).parent().addClass('active');
405 $(self.previewButton).parent().addClass('active');
406 $(self.editButton).parent().removeClass('active');
406 $(self.editButton).parent().removeClass('active');
407
407
408 // unlock buttons
408 // unlock buttons
409 self.setActionButtonsDisabled(false);
409 self.setActionButtonsDisabled(false);
410 };
410 };
411
411
412 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
412 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
413 excludeCancelBtn = excludeCancelBtn || false;
413 excludeCancelBtn = excludeCancelBtn || false;
414 submitEvent = submitEvent || false;
414 submitEvent = submitEvent || false;
415
415
416 $(this.editButton).prop('disabled', state);
416 $(this.editButton).prop('disabled', state);
417 $(this.previewButton).prop('disabled', state);
417 $(this.previewButton).prop('disabled', state);
418
418
419 if (!excludeCancelBtn) {
419 if (!excludeCancelBtn) {
420 $(this.cancelButton).prop('disabled', state);
420 $(this.cancelButton).prop('disabled', state);
421 }
421 }
422
422
423 var submitState = state;
423 var submitState = state;
424 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
424 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
425 // if the value of commit review status is set, we allow
425 // if the value of commit review status is set, we allow
426 // submit button, but only on Main form, isInline means inline
426 // submit button, but only on Main form, isInline means inline
427 submitState = false
427 submitState = false
428 }
428 }
429
429
430 $(this.submitButton).prop('disabled', submitState);
430 $(this.submitButton).prop('disabled', submitState);
431 $(this.submitDraftButton).prop('disabled', submitState);
431 $(this.submitDraftButton).prop('disabled', submitState);
432
432
433 if (submitEvent) {
433 if (submitEvent) {
434 var isDraft = self.getDraftState();
434 var isDraft = self.getDraftState();
435
435
436 if (isDraft) {
436 if (isDraft) {
437 $(this.submitDraftButton).val(_gettext('Saving Draft...'));
437 $(this.submitDraftButton).val(_gettext('Saving Draft...'));
438 } else {
438 } else {
439 $(this.submitButton).val(_gettext('Submitting...'));
439 $(this.submitButton).val(_gettext('Submitting...'));
440 }
440 }
441
441
442 } else {
442 } else {
443 $(this.submitButton).val(this.submitButtonText);
443 $(this.submitButton).val(this.submitButtonText);
444 $(this.submitDraftButton).val(this.submitDraftButtonText);
444 $(this.submitDraftButton).val(this.submitDraftButtonText);
445 }
445 }
446
446
447 };
447 };
448
448
449 // lock preview/edit/submit buttons on load, but exclude cancel button
449 // lock preview/edit/submit buttons on load, but exclude cancel button
450 var excludeCancelBtn = true;
450 var excludeCancelBtn = true;
451 this.setActionButtonsDisabled(true, excludeCancelBtn);
451 this.setActionButtonsDisabled(true, excludeCancelBtn);
452
452
453 // anonymous users don't have access to initialized CM instance
453 // anonymous users don't have access to initialized CM instance
454 if (this.cm !== undefined){
454 if (this.cm !== undefined){
455 this.cm.on('change', function(cMirror) {
455 this.cm.on('change', function(cMirror) {
456 if (cMirror.getValue() === "") {
456 if (cMirror.getValue() === "") {
457 self.setActionButtonsDisabled(true, excludeCancelBtn)
457 self.setActionButtonsDisabled(true, excludeCancelBtn)
458 } else {
458 } else {
459 self.setActionButtonsDisabled(false, excludeCancelBtn)
459 self.setActionButtonsDisabled(false, excludeCancelBtn)
460 }
460 }
461 });
461 });
462 }
462 }
463
463
464 $(this.editButton).on('click', function(e) {
464 $(this.editButton).on('click', function(e) {
465 e.preventDefault();
465 e.preventDefault();
466
466
467 $(self.previewButton).parent().removeClass('active');
467 $(self.previewButton).parent().removeClass('active');
468 $(self.previewContainer).hide();
468 $(self.previewContainer).hide();
469
469
470 $(self.editButton).parent().addClass('active');
470 $(self.editButton).parent().addClass('active');
471 $(self.editContainer).show();
471 $(self.editContainer).show();
472
472
473 });
473 });
474
474
475 $(this.previewButton).on('click', function(e) {
475 $(this.previewButton).on('click', function(e) {
476 e.preventDefault();
476 e.preventDefault();
477 var text = self.cm.getValue();
477 var text = self.cm.getValue();
478
478
479 if (text === "") {
479 if (text === "") {
480 return;
480 return;
481 }
481 }
482
482
483 var postData = {
483 var postData = {
484 'text': text,
484 'text': text,
485 'renderer': templateContext.visual.default_renderer,
485 'renderer': templateContext.visual.default_renderer,
486 'csrf_token': CSRF_TOKEN
486 'csrf_token': CSRF_TOKEN
487 };
487 };
488
488
489 // lock ALL buttons on preview
489 // lock ALL buttons on preview
490 self.setActionButtonsDisabled(true);
490 self.setActionButtonsDisabled(true);
491
491
492 $(self.previewBoxSelector).addClass('unloaded');
492 $(self.previewBoxSelector).addClass('unloaded');
493 $(self.previewBoxSelector).html(_gettext('Loading ...'));
493 $(self.previewBoxSelector).html(_gettext('Loading ...'));
494
494
495 $(self.editContainer).hide();
495 $(self.editContainer).hide();
496 $(self.previewContainer).show();
496 $(self.previewContainer).show();
497
497
498 // by default we reset state of comment preserving the text
498 // by default we reset state of comment preserving the text
499 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
499 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
500 var prefix = "Error while preview of comment.\n"
500 var prefix = "Error while preview of comment.\n"
501 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
501 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
502 ajaxErrorSwal(message);
502 ajaxErrorSwal(message);
503
503
504 self.resetCommentFormState(text)
504 self.resetCommentFormState(text)
505 };
505 };
506 self.submitAjaxPOST(
506 self.submitAjaxPOST(
507 self.previewUrl, postData, self.previewSuccessCallback,
507 self.previewUrl, postData, self.previewSuccessCallback,
508 previewFailCallback);
508 previewFailCallback);
509
509
510 $(self.previewButton).parent().addClass('active');
510 $(self.previewButton).parent().addClass('active');
511 $(self.editButton).parent().removeClass('active');
511 $(self.editButton).parent().removeClass('active');
512 });
512 });
513
513
514 $(this.submitForm).submit(function(e) {
514 $(this.submitForm).submit(function(e) {
515 e.preventDefault();
515 e.preventDefault();
516 var allowedToSubmit = self.isAllowedToSubmit();
516 var allowedToSubmit = self.isAllowedToSubmit();
517 if (!allowedToSubmit){
517 if (!allowedToSubmit){
518 return false;
518 return false;
519 }
519 }
520
520
521 self.handleFormSubmit();
521 self.handleFormSubmit();
522 });
522 });
523
523
524 }
524 }
525
525
526 return CommentForm;
526 return CommentForm;
527 });
527 });
528
528
529 /* selector for comment versions */
529 /* selector for comment versions */
530 var initVersionSelector = function(selector, initialData) {
530 var initVersionSelector = function(selector, initialData) {
531
531
532 var formatResult = function(result, container, query, escapeMarkup) {
532 var formatResult = function(result, container, query, escapeMarkup) {
533
533
534 return renderTemplate('commentVersion', {
534 return renderTemplate('commentVersion', {
535 show_disabled: true,
535 show_disabled: true,
536 version: result.comment_version,
536 version: result.comment_version,
537 user_name: result.comment_author_username,
537 user_name: result.comment_author_username,
538 gravatar_url: result.comment_author_gravatar,
538 gravatar_url: result.comment_author_gravatar,
539 size: 16,
539 size: 16,
540 timeago_component: result.comment_created_on,
540 timeago_component: result.comment_created_on,
541 })
541 })
542 };
542 };
543
543
544 $(selector).select2({
544 $(selector).select2({
545 placeholder: "Edited",
545 placeholder: "Edited",
546 containerCssClass: "drop-menu-comment-history",
546 containerCssClass: "drop-menu-comment-history",
547 dropdownCssClass: "drop-menu-dropdown",
547 dropdownCssClass: "drop-menu-dropdown",
548 dropdownAutoWidth: true,
548 dropdownAutoWidth: true,
549 minimumResultsForSearch: -1,
549 minimumResultsForSearch: -1,
550 data: initialData,
550 data: initialData,
551 formatResult: formatResult,
551 formatResult: formatResult,
552 });
552 });
553
553
554 $(selector).on('select2-selecting', function (e) {
554 $(selector).on('select2-selecting', function (e) {
555 // hide the mast as we later do preventDefault()
555 // hide the mast as we later do preventDefault()
556 $("#select2-drop-mask").click();
556 $("#select2-drop-mask").click();
557 e.preventDefault();
557 e.preventDefault();
558 e.choice.action();
558 e.choice.action();
559 });
559 });
560
560
561 $(selector).on("select2-open", function() {
561 $(selector).on("select2-open", function() {
562 timeagoActivate();
562 timeagoActivate();
563 });
563 });
564 };
564 };
565
565
566 /* comments controller */
566 /* comments controller */
567 var CommentsController = function() {
567 var CommentsController = function() {
568 var mainComment = '#text';
568 var mainComment = '#text';
569 var self = this;
569 var self = this;
570
570
571 this.showVersion = function (comment_id, comment_history_id) {
571 this.showVersion = function (comment_id, comment_history_id) {
572
572
573 var historyViewUrl = pyroutes.url(
573 var historyViewUrl = pyroutes.url(
574 'repo_commit_comment_history_view',
574 'repo_commit_comment_history_view',
575 {
575 {
576 'repo_name': templateContext.repo_name,
576 'repo_name': templateContext.repo_name,
577 'commit_id': comment_id,
577 'commit_id': comment_id,
578 'comment_history_id': comment_history_id,
578 'comment_history_id': comment_history_id,
579 }
579 }
580 );
580 );
581 successRenderCommit = function (data) {
581 successRenderCommit = function (data) {
582 SwalNoAnimation.fire({
582 SwalNoAnimation.fire({
583 html: data,
583 html: data,
584 title: '',
584 title: '',
585 });
585 });
586 };
586 };
587 failRenderCommit = function () {
587 failRenderCommit = function () {
588 SwalNoAnimation.fire({
588 SwalNoAnimation.fire({
589 html: 'Error while loading comment history',
589 html: 'Error while loading comment history',
590 title: '',
590 title: '',
591 });
591 });
592 };
592 };
593 _submitAjaxPOST(
593 _submitAjaxPOST(
594 historyViewUrl, {'csrf_token': CSRF_TOKEN},
594 historyViewUrl, {'csrf_token': CSRF_TOKEN},
595 successRenderCommit,
595 successRenderCommit,
596 failRenderCommit
596 failRenderCommit
597 );
597 );
598 };
598 };
599
599
600 this.getLineNumber = function(node) {
600 this.getLineNumber = function(node) {
601 var $node = $(node);
601 var $node = $(node);
602 var lineNo = $node.closest('td').attr('data-line-no');
602 var lineNo = $node.closest('td').attr('data-line-no');
603 if (lineNo === undefined && $node.data('commentInline')){
603 if (lineNo === undefined && $node.data('commentInline')){
604 lineNo = $node.data('commentLineNo')
604 lineNo = $node.data('commentLineNo')
605 }
605 }
606
606
607 return lineNo
607 return lineNo
608 };
608 };
609
609
610 this.scrollToComment = function(node, offset, outdated) {
610 this.scrollToComment = function(node, offset, outdated) {
611 if (offset === undefined) {
611 if (offset === undefined) {
612 offset = 0;
612 offset = 0;
613 }
613 }
614 var outdated = outdated || false;
614 var outdated = outdated || false;
615 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
615 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
616
616
617 if (!node) {
617 if (!node) {
618 node = $('.comment-selected');
618 node = $('.comment-selected');
619 if (!node.length) {
619 if (!node.length) {
620 node = $('comment-current')
620 node = $('comment-current')
621 }
621 }
622 }
622 }
623
623
624 $wrapper = $(node).closest('div.comment');
624 $wrapper = $(node).closest('div.comment');
625
625
626 // show hidden comment when referenced.
626 // show hidden comment when referenced.
627 if (!$wrapper.is(':visible')){
627 if (!$wrapper.is(':visible')){
628 $wrapper.show();
628 $wrapper.show();
629 }
629 }
630
630
631 $comment = $(node).closest(klass);
631 $comment = $(node).closest(klass);
632 $comments = $(klass);
632 $comments = $(klass);
633
633
634 $('.comment-selected').removeClass('comment-selected');
634 $('.comment-selected').removeClass('comment-selected');
635
635
636 var nextIdx = $(klass).index($comment) + offset;
636 var nextIdx = $(klass).index($comment) + offset;
637 if (nextIdx >= $comments.length) {
637 if (nextIdx >= $comments.length) {
638 nextIdx = 0;
638 nextIdx = 0;
639 }
639 }
640 var $next = $(klass).eq(nextIdx);
640 var $next = $(klass).eq(nextIdx);
641
641
642 var $cb = $next.closest('.cb');
642 var $cb = $next.closest('.cb');
643 $cb.removeClass('cb-collapsed');
643 $cb.removeClass('cb-collapsed');
644
644
645 var $filediffCollapseState = $cb.closest('.filediff').prev();
645 var $filediffCollapseState = $cb.closest('.filediff').prev();
646 $filediffCollapseState.prop('checked', false);
646 $filediffCollapseState.prop('checked', false);
647 $next.addClass('comment-selected');
647 $next.addClass('comment-selected');
648 scrollToElement($next);
648 scrollToElement($next);
649 return false;
649 return false;
650 };
650 };
651
651
652 this.nextComment = function(node) {
652 this.nextComment = function(node) {
653 return self.scrollToComment(node, 1);
653 return self.scrollToComment(node, 1);
654 };
654 };
655
655
656 this.prevComment = function(node) {
656 this.prevComment = function(node) {
657 return self.scrollToComment(node, -1);
657 return self.scrollToComment(node, -1);
658 };
658 };
659
659
660 this.nextOutdatedComment = function(node) {
660 this.nextOutdatedComment = function(node) {
661 return self.scrollToComment(node, 1, true);
661 return self.scrollToComment(node, 1, true);
662 };
662 };
663
663
664 this.prevOutdatedComment = function(node) {
664 this.prevOutdatedComment = function(node) {
665 return self.scrollToComment(node, -1, true);
665 return self.scrollToComment(node, -1, true);
666 };
666 };
667
667
668 this.cancelComment = function (node) {
668 this.cancelComment = function (node) {
669 var $node = $(node);
669 var $node = $(node);
670 var edit = $(this).attr('edit');
670 var edit = $(this).attr('edit');
671 var $inlineComments = $node.closest('div.inline-comments');
671 var $inlineComments = $node.closest('div.inline-comments');
672
672
673 if (edit) {
673 if (edit) {
674 var $general_comments = null;
674 var $general_comments = null;
675 if (!$inlineComments.length) {
675 if (!$inlineComments.length) {
676 $general_comments = $('#comments');
676 $general_comments = $('#comments');
677 var $comment = $general_comments.parent().find('div.comment:hidden');
677 var $comment = $general_comments.parent().find('div.comment:hidden');
678 // show hidden general comment form
678 // show hidden general comment form
679 $('#cb-comment-general-form-placeholder').show();
679 $('#cb-comment-general-form-placeholder').show();
680 } else {
680 } else {
681 var $comment = $inlineComments.find('div.comment:hidden');
681 var $comment = $inlineComments.find('div.comment:hidden');
682 }
682 }
683 $comment.show();
683 $comment.show();
684 }
684 }
685 var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper')
685 var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper')
686 $replyWrapper.removeClass('comment-form-active');
686 $replyWrapper.removeClass('comment-form-active');
687
687
688 var lastComment = $inlineComments.find('.comment-inline').last();
688 var lastComment = $inlineComments.find('.comment-inline').last();
689 if ($(lastComment).hasClass('comment-outdated')) {
689 if ($(lastComment).hasClass('comment-outdated')) {
690 $replyWrapper.hide();
690 $replyWrapper.hide();
691 }
691 }
692
692
693 $node.closest('.comment-inline-form').remove();
693 $node.closest('.comment-inline-form').remove();
694 return false;
694 return false;
695 };
695 };
696
696
697 this._deleteComment = function(node) {
697 this._deleteComment = function(node) {
698 var $node = $(node);
698 var $node = $(node);
699 var $td = $node.closest('td');
699 var $td = $node.closest('td');
700 var $comment = $node.closest('.comment');
700 var $comment = $node.closest('.comment');
701 var comment_id = $($comment).data('commentId');
701 var comment_id = $($comment).data('commentId');
702 var isDraft = $($comment).data('commentDraft');
702 var isDraft = $($comment).data('commentDraft');
703
703
704 var pullRequestId = templateContext.pull_request_data.pull_request_id;
704 var pullRequestId = templateContext.pull_request_data.pull_request_id;
705 var commitId = templateContext.commit_data.commit_id;
705 var commitId = templateContext.commit_data.commit_id;
706
706
707 if (pullRequestId) {
707 if (pullRequestId) {
708 var url = pyroutes.url('pullrequest_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
708 var url = pyroutes.url('pullrequest_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
709 } else if (commitId) {
709 } else if (commitId) {
710 var url = pyroutes.url('repo_commit_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "commit_id": commitId})
710 var url = pyroutes.url('repo_commit_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "commit_id": commitId})
711 }
711 }
712
712
713 var postData = {
713 var postData = {
714 'csrf_token': CSRF_TOKEN
714 'csrf_token': CSRF_TOKEN
715 };
715 };
716
716
717 $comment.addClass('comment-deleting');
717 $comment.addClass('comment-deleting');
718 $comment.hide('fast');
718 $comment.hide('fast');
719
719
720 var success = function(response) {
720 var success = function(response) {
721 $comment.remove();
721 $comment.remove();
722
722
723 if (window.updateSticky !== undefined) {
723 if (window.updateSticky !== undefined) {
724 // potentially our comments change the active window size, so we
724 // potentially our comments change the active window size, so we
725 // notify sticky elements
725 // notify sticky elements
726 updateSticky()
726 updateSticky()
727 }
727 }
728
728
729 if (window.refreshAllComments !== undefined && !isDraft) {
729 if (window.refreshAllComments !== undefined && !isDraft) {
730 // if we have this handler, run it, and refresh all comments boxes
730 // if we have this handler, run it, and refresh all comments boxes
731 refreshAllComments()
731 refreshAllComments()
732 }
732 }
733 else if (window.refreshDraftComments !== undefined && isDraft) {
733 else if (window.refreshDraftComments !== undefined && isDraft) {
734 // if we have this handler, run it, and refresh all comments boxes
734 // if we have this handler, run it, and refresh all comments boxes
735 refreshDraftComments();
735 refreshDraftComments();
736 }
736 }
737 return false;
737 return false;
738 };
738 };
739
739
740 var failure = function(jqXHR, textStatus, errorThrown) {
740 var failure = function(jqXHR, textStatus, errorThrown) {
741 var prefix = "Error while deleting this comment.\n"
741 var prefix = "Error while deleting this comment.\n"
742 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
742 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
743 ajaxErrorSwal(message);
743 ajaxErrorSwal(message);
744
744
745 $comment.show('fast');
745 $comment.show('fast');
746 $comment.removeClass('comment-deleting');
746 $comment.removeClass('comment-deleting');
747 return false;
747 return false;
748 };
748 };
749 ajaxPOST(url, postData, success, failure);
749 ajaxPOST(url, postData, success, failure);
750
750
751 }
751 }
752
752
753 this.deleteComment = function(node) {
753 this.deleteComment = function(node) {
754 var $comment = $(node).closest('.comment');
754 var $comment = $(node).closest('.comment');
755 var comment_id = $comment.attr('data-comment-id');
755 var comment_id = $comment.attr('data-comment-id');
756
756
757 SwalNoAnimation.fire({
757 SwalNoAnimation.fire({
758 title: 'Delete this comment?',
758 title: 'Delete this comment?',
759 icon: 'warning',
759 icon: 'warning',
760 showCancelButton: true,
760 showCancelButton: true,
761 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
761 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
762
762
763 }).then(function(result) {
763 }).then(function(result) {
764 if (result.value) {
764 if (result.value) {
765 self._deleteComment(node);
765 self._deleteComment(node);
766 }
766 }
767 })
767 })
768 };
768 };
769
769
770 this._finalizeDrafts = function(commentIds) {
770 this._finalizeDrafts = function(commentIds) {
771
771
772 var pullRequestId = templateContext.pull_request_data.pull_request_id;
772 var pullRequestId = templateContext.pull_request_data.pull_request_id;
773 var commitId = templateContext.commit_data.commit_id;
773 var commitId = templateContext.commit_data.commit_id;
774
774
775 if (pullRequestId) {
775 if (pullRequestId) {
776 var url = pyroutes.url('pullrequest_draft_comments_submit', {"repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
776 var url = pyroutes.url('pullrequest_draft_comments_submit', {"repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
777 } else if (commitId) {
777 } else if (commitId) {
778 var url = pyroutes.url('commit_draft_comments_submit', {"repo_name": templateContext.repo_name, "commit_id": commitId})
778 var url = pyroutes.url('commit_draft_comments_submit', {"repo_name": templateContext.repo_name, "commit_id": commitId})
779 }
779 }
780
780
781 // remove the drafts so we can lock them before submit.
781 // remove the drafts so we can lock them before submit.
782 $.each(commentIds, function(idx, val){
782 $.each(commentIds, function(idx, val){
783 $('#comment-{0}'.format(val)).remove();
783 $('#comment-{0}'.format(val)).remove();
784 })
784 })
785
785
786 var postData = {'comments': commentIds, 'csrf_token': CSRF_TOKEN};
786 var postData = {'comments': commentIds, 'csrf_token': CSRF_TOKEN};
787
787
788 var submitSuccessCallback = function(json_data) {
788 var submitSuccessCallback = function(json_data) {
789 self.attachInlineComment(json_data);
789 self.attachInlineComment(json_data);
790
790
791 if (window.refreshDraftComments !== undefined) {
791 if (window.refreshDraftComments !== undefined) {
792 // if we have this handler, run it, and refresh all comments boxes
792 // if we have this handler, run it, and refresh all comments boxes
793 refreshDraftComments()
793 refreshDraftComments()
794 }
794 }
795
795
796 return false;
796 return false;
797 };
797 };
798
798
799 ajaxPOST(url, postData, submitSuccessCallback)
799 ajaxPOST(url, postData, submitSuccessCallback)
800
800
801 }
801 }
802
802
803 this.finalizeDrafts = function(commentIds, callback) {
803 this.finalizeDrafts = function(commentIds, callback) {
804
804
805 SwalNoAnimation.fire({
805 SwalNoAnimation.fire({
806 title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
806 title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
807 icon: 'warning',
807 icon: 'warning',
808 showCancelButton: true,
808 showCancelButton: true,
809 confirmButtonText: _gettext('Yes'),
809 confirmButtonText: _gettext('Yes'),
810
810
811 }).then(function(result) {
811 }).then(function(result) {
812 if (result.value) {
812 if (result.value) {
813 if (callback !== undefined) {
813 if (callback !== undefined) {
814 callback(result)
814 callback(result)
815 }
815 }
816 self._finalizeDrafts(commentIds);
816 self._finalizeDrafts(commentIds);
817 }
817 }
818 })
818 })
819 };
819 };
820
820
821 this.toggleWideMode = function (node) {
821 this.toggleWideMode = function (node) {
822
822
823 if ($('#content').hasClass('wrapper')) {
823 if ($('#content').hasClass('wrapper')) {
824 $('#content').removeClass("wrapper");
824 $('#content').removeClass("wrapper");
825 $('#content').addClass("wide-mode-wrapper");
825 $('#content').addClass("wide-mode-wrapper");
826 $(node).addClass('btn-success');
826 $(node).addClass('btn-success');
827 return true
827 return true
828 } else {
828 } else {
829 $('#content').removeClass("wide-mode-wrapper");
829 $('#content').removeClass("wide-mode-wrapper");
830 $('#content').addClass("wrapper");
830 $('#content').addClass("wrapper");
831 $(node).removeClass('btn-success');
831 $(node).removeClass('btn-success');
832 return false
832 return false
833 }
833 }
834
834
835 };
835 };
836
836
837 /**
837 /**
838 * Turn off/on all comments in file diff
838 * Turn off/on all comments in file diff
839 */
839 */
840 this.toggleDiffComments = function(node) {
840 this.toggleDiffComments = function(node) {
841 // Find closes filediff container
841 // Find closes filediff container
842 var $filediff = $(node).closest('.filediff');
842 var $filediff = $(node).closest('.filediff');
843 if ($(node).hasClass('toggle-on')) {
843 if ($(node).hasClass('toggle-on')) {
844 var show = false;
844 var show = false;
845 } else if ($(node).hasClass('toggle-off')) {
845 } else if ($(node).hasClass('toggle-off')) {
846 var show = true;
846 var show = true;
847 }
847 }
848
848
849 // Toggle each individual comment block, so we can un-toggle single ones
849 // Toggle each individual comment block, so we can un-toggle single ones
850 $.each($filediff.find('.toggle-comment-action'), function(idx, val) {
850 $.each($filediff.find('.toggle-comment-action'), function(idx, val) {
851 self.toggleLineComments($(val), show)
851 self.toggleLineComments($(val), show)
852 })
852 })
853
853
854 // since we change the height of the diff container that has anchor points for upper
854 // since we change the height of the diff container that has anchor points for upper
855 // sticky header, we need to tell it to re-calculate those
855 // sticky header, we need to tell it to re-calculate those
856 if (window.updateSticky !== undefined) {
856 if (window.updateSticky !== undefined) {
857 // potentially our comments change the active window size, so we
857 // potentially our comments change the active window size, so we
858 // notify sticky elements
858 // notify sticky elements
859 updateSticky()
859 updateSticky()
860 }
860 }
861
861
862 return false;
862 return false;
863 }
863 }
864
864
865 this.toggleLineComments = function(node, show) {
865 this.toggleLineComments = function(node, show) {
866
866
867 var trElem = $(node).closest('tr')
867 var trElem = $(node).closest('tr')
868
868
869 if (show === true) {
869 if (show === true) {
870 // mark outdated comments as visible before the toggle;
870 // mark outdated comments as visible before the toggle;
871 $(trElem).find('.comment-outdated').show();
871 $(trElem).find('.comment-outdated').show();
872 $(trElem).removeClass('hide-line-comments');
872 $(trElem).removeClass('hide-line-comments');
873 } else if (show === false) {
873 } else if (show === false) {
874 $(trElem).find('.comment-outdated').hide();
874 $(trElem).find('.comment-outdated').hide();
875 $(trElem).addClass('hide-line-comments');
875 $(trElem).addClass('hide-line-comments');
876 } else {
876 } else {
877 // mark outdated comments as visible before the toggle;
877 // mark outdated comments as visible before the toggle;
878 $(trElem).find('.comment-outdated').show();
878 $(trElem).find('.comment-outdated').show();
879 $(trElem).toggleClass('hide-line-comments');
879 $(trElem).toggleClass('hide-line-comments');
880 }
880 }
881
881
882 // since we change the height of the diff container that has anchor points for upper
882 // since we change the height of the diff container that has anchor points for upper
883 // sticky header, we need to tell it to re-calculate those
883 // sticky header, we need to tell it to re-calculate those
884 if (window.updateSticky !== undefined) {
884 if (window.updateSticky !== undefined) {
885 // potentially our comments change the active window size, so we
885 // potentially our comments change the active window size, so we
886 // notify sticky elements
886 // notify sticky elements
887 updateSticky()
887 updateSticky()
888 }
888 }
889
889
890 };
890 };
891
891
892 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
892 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
893 var pullRequestId = templateContext.pull_request_data.pull_request_id;
893 var pullRequestId = templateContext.pull_request_data.pull_request_id;
894 var commitId = templateContext.commit_data.commit_id;
894 var commitId = templateContext.commit_data.commit_id;
895
895
896 var commentForm = new CommentForm(
896 var commentForm = new CommentForm(
897 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
897 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
898 var cm = commentForm.getCmInstance();
898 var cm = commentForm.getCmInstance();
899
899
900 if (resolvesCommentId){
900 if (resolvesCommentId){
901 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
901 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
902 }
902 }
903
903
904 setTimeout(function() {
904 setTimeout(function() {
905 // callbacks
905 // callbacks
906 if (cm !== undefined) {
906 if (cm !== undefined) {
907 commentForm.setPlaceholder(placeholderText);
907 commentForm.setPlaceholder(placeholderText);
908 if (commentForm.isInline()) {
908 if (commentForm.isInline()) {
909 cm.focus();
909 cm.focus();
910 cm.refresh();
910 cm.refresh();
911 }
911 }
912 }
912 }
913 }, 10);
913 }, 10);
914
914
915 // trigger scrolldown to the resolve comment, since it might be away
915 // trigger scrolldown to the resolve comment, since it might be away
916 // from the clicked
916 // from the clicked
917 if (resolvesCommentId){
917 if (resolvesCommentId){
918 var actionNode = $(commentForm.resolvesActionId).offset();
918 var actionNode = $(commentForm.resolvesActionId).offset();
919
919
920 setTimeout(function() {
920 setTimeout(function() {
921 if (actionNode) {
921 if (actionNode) {
922 $('body, html').animate({scrollTop: actionNode.top}, 10);
922 $('body, html').animate({scrollTop: actionNode.top}, 10);
923 }
923 }
924 }, 100);
924 }, 100);
925 }
925 }
926
926
927 // add dropzone support
927 // add dropzone support
928 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
928 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
929 var renderer = templateContext.visual.default_renderer;
929 var renderer = templateContext.visual.default_renderer;
930 if (renderer == 'rst') {
930 if (renderer == 'rst') {
931 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
931 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
932 if (isRendered){
932 if (isRendered){
933 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
933 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
934 }
934 }
935 } else if (renderer == 'markdown') {
935 } else if (renderer == 'markdown') {
936 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
936 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
937 if (isRendered){
937 if (isRendered){
938 attachmentUrl = '!' + attachmentUrl;
938 attachmentUrl = '!' + attachmentUrl;
939 }
939 }
940 } else {
940 } else {
941 var attachmentUrl = '{}'.format(attachmentStoreUrl);
941 var attachmentUrl = '{}'.format(attachmentStoreUrl);
942 }
942 }
943 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
943 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
944
944
945 return false;
945 return false;
946 };
946 };
947
947
948 //see: https://www.dropzonejs.com/#configuration
948 //see: https://www.dropzonejs.com/#configuration
949 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
949 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
950 {'repo_name': templateContext.repo_name,
950 {'repo_name': templateContext.repo_name,
951 'commit_id': templateContext.commit_data.commit_id})
951 'commit_id': templateContext.commit_data.commit_id})
952
952
953 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
953 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
954 if (previewTmpl !== undefined){
954 if (previewTmpl !== undefined){
955 var selectLink = $(formElement).find('.pick-attachment').get(0);
955 var selectLink = $(formElement).find('.pick-attachment').get(0);
956 $(formElement).find('.comment-attachment-uploader').dropzone({
956 $(formElement).find('.comment-attachment-uploader').dropzone({
957 url: storeUrl,
957 url: storeUrl,
958 headers: {"X-CSRF-Token": CSRF_TOKEN},
958 headers: {"X-CSRF-Token": CSRF_TOKEN},
959 paramName: function () {
959 paramName: function () {
960 return "attachment"
960 return "attachment"
961 }, // The name that will be used to transfer the file
961 }, // The name that will be used to transfer the file
962 clickable: selectLink,
962 clickable: selectLink,
963 parallelUploads: 1,
963 parallelUploads: 1,
964 maxFiles: 10,
964 maxFiles: 10,
965 maxFilesize: templateContext.attachment_store.max_file_size_mb,
965 maxFilesize: templateContext.attachment_store.max_file_size_mb,
966 uploadMultiple: false,
966 uploadMultiple: false,
967 autoProcessQueue: true, // if false queue will not be processed automatically.
967 autoProcessQueue: true, // if false queue will not be processed automatically.
968 createImageThumbnails: false,
968 createImageThumbnails: false,
969 previewTemplate: previewTmpl.innerHTML,
969 previewTemplate: previewTmpl.innerHTML,
970
970
971 accept: function (file, done) {
971 accept: function (file, done) {
972 done();
972 done();
973 },
973 },
974 init: function () {
974 init: function () {
975
975
976 this.on("sending", function (file, xhr, formData) {
976 this.on("sending", function (file, xhr, formData) {
977 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
977 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
978 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
978 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
979 });
979 });
980
980
981 this.on("success", function (file, response) {
981 this.on("success", function (file, response) {
982 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
982 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
983 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
983 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
984
984
985 var isRendered = false;
985 var isRendered = false;
986 var ext = file.name.split('.').pop();
986 var ext = file.name.split('.').pop();
987 var imageExts = templateContext.attachment_store.image_ext;
987 var imageExts = templateContext.attachment_store.image_ext;
988 if (imageExts.indexOf(ext) !== -1){
988 if (imageExts.indexOf(ext) !== -1){
989 isRendered = true;
989 isRendered = true;
990 }
990 }
991
991
992 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
992 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
993 });
993 });
994
994
995 this.on("error", function (file, errorMessage, xhr) {
995 this.on("error", function (file, errorMessage, xhr) {
996 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
996 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
997
997
998 var error = null;
998 var error = null;
999
999
1000 if (xhr !== undefined){
1000 if (xhr !== undefined){
1001 var httpStatus = xhr.status + " " + xhr.statusText;
1001 var httpStatus = xhr.status + " " + xhr.statusText;
1002 if (xhr !== undefined && xhr.status >= 500) {
1002 if (xhr !== undefined && xhr.status >= 500) {
1003 error = httpStatus;
1003 error = httpStatus;
1004 }
1004 }
1005 }
1005 }
1006
1006
1007 if (error === null) {
1007 if (error === null) {
1008 error = errorMessage.error || errorMessage || httpStatus;
1008 error = errorMessage.error || errorMessage || httpStatus;
1009 }
1009 }
1010 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
1010 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
1011
1011
1012 });
1012 });
1013 }
1013 }
1014 });
1014 });
1015 }
1015 }
1016 return commentForm;
1016 return commentForm;
1017 };
1017 };
1018
1018
1019 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
1019 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
1020
1020
1021 var tmpl = $('#cb-comment-general-form-template').html();
1021 var tmpl = $('#cb-comment-general-form-template').html();
1022 tmpl = tmpl.format(null, 'general');
1022 tmpl = tmpl.format(null, 'general');
1023 var $form = $(tmpl);
1023 var $form = $(tmpl);
1024
1024
1025 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
1025 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
1026 var curForm = $formPlaceholder.find('form');
1026 var curForm = $formPlaceholder.find('form');
1027 if (curForm){
1027 if (curForm){
1028 curForm.remove();
1028 curForm.remove();
1029 }
1029 }
1030 $formPlaceholder.append($form);
1030 $formPlaceholder.append($form);
1031
1031
1032 var _form = $($form[0]);
1032 var _form = $($form[0]);
1033 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
1033 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
1034 var edit = false;
1034 var edit = false;
1035 var comment_id = null;
1035 var comment_id = null;
1036 var commentForm = this.createCommentForm(
1036 var commentForm = this.createCommentForm(
1037 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
1037 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
1038 commentForm.initStatusChangeSelector();
1038 commentForm.initStatusChangeSelector();
1039
1039
1040 return commentForm;
1040 return commentForm;
1041 };
1041 };
1042
1042
1043 this.editComment = function(node, line_no, f_path) {
1043 this.editComment = function(node, line_no, f_path) {
1044 self.edit = true;
1044 self.edit = true;
1045 var $node = $(node);
1045 var $node = $(node);
1046 var $td = $node.closest('td');
1046 var $td = $node.closest('td');
1047
1047
1048 var $comment = $(node).closest('.comment');
1048 var $comment = $(node).closest('.comment');
1049 var comment_id = $($comment).data('commentId');
1049 var comment_id = $($comment).data('commentId');
1050 var isDraft = $($comment).data('commentDraft');
1050 var isDraft = $($comment).data('commentDraft');
1051 var $editForm = null
1051 var $editForm = null
1052
1052
1053 var $comments = $node.closest('div.inline-comments');
1053 var $comments = $node.closest('div.inline-comments');
1054 var $general_comments = null;
1054 var $general_comments = null;
1055
1055
1056 if($comments.length){
1056 if($comments.length){
1057 // inline comments setup
1057 // inline comments setup
1058 $editForm = $comments.find('.comment-inline-form');
1058 $editForm = $comments.find('.comment-inline-form');
1059 line_no = self.getLineNumber(node)
1059 line_no = self.getLineNumber(node)
1060 }
1060 }
1061 else{
1061 else{
1062 // general comments setup
1062 // general comments setup
1063 $comments = $('#comments');
1063 $comments = $('#comments');
1064 $editForm = $comments.find('.comment-inline-form');
1064 $editForm = $comments.find('.comment-inline-form');
1065 line_no = $comment[0].id
1065 line_no = $comment[0].id
1066 $('#cb-comment-general-form-placeholder').hide();
1066 $('#cb-comment-general-form-placeholder').hide();
1067 }
1067 }
1068
1068
1069 if ($editForm.length === 0) {
1069 if ($editForm.length === 0) {
1070
1070
1071 // unhide all comments if they are hidden for a proper REPLY mode
1071 // unhide all comments if they are hidden for a proper REPLY mode
1072 var $filediff = $node.closest('.filediff');
1072 var $filediff = $node.closest('.filediff');
1073 $filediff.removeClass('hide-comments');
1073 $filediff.removeClass('hide-comments');
1074
1074
1075 $editForm = self.createNewFormWrapper(f_path, line_no);
1075 $editForm = self.createNewFormWrapper(f_path, line_no);
1076 if(f_path && line_no) {
1076 if(f_path && line_no) {
1077 $editForm.addClass('comment-inline-form-edit')
1077 $editForm.addClass('comment-inline-form-edit')
1078 }
1078 }
1079
1079
1080 $comment.after($editForm)
1080 $comment.after($editForm)
1081
1081
1082 var _form = $($editForm[0]).find('form');
1082 var _form = $($editForm[0]).find('form');
1083 var autocompleteActions = ['as_note',];
1083 var autocompleteActions = ['as_note',];
1084 var commentForm = this.createCommentForm(
1084 var commentForm = this.createCommentForm(
1085 _form, line_no, '', autocompleteActions, resolvesCommentId,
1085 _form, line_no, '', autocompleteActions, resolvesCommentId,
1086 this.edit, comment_id);
1086 this.edit, comment_id);
1087 var old_comment_text_binary = $comment.attr('data-comment-text');
1087 var old_comment_text_binary = $comment.attr('data-comment-text');
1088 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
1088 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
1089 commentForm.cm.setValue(old_comment_text);
1089 commentForm.cm.setValue(old_comment_text);
1090 $comment.hide();
1090 $comment.hide();
1091 tooltipActivate();
1091 tooltipActivate();
1092
1092
1093 // set a CUSTOM submit handler for inline comment edit action.
1093 // set a CUSTOM submit handler for inline comment edit action.
1094 commentForm.setHandleFormSubmit(function(o) {
1094 commentForm.setHandleFormSubmit(function(o) {
1095 var text = commentForm.cm.getValue();
1095 var text = commentForm.cm.getValue();
1096 var commentType = commentForm.getCommentType();
1096 var commentType = commentForm.getCommentType();
1097
1097
1098 if (text === "") {
1098 if (text === "") {
1099 return;
1099 return;
1100 }
1100 }
1101
1101
1102 if (old_comment_text == text) {
1102 if (old_comment_text == text) {
1103 SwalNoAnimation.fire({
1103 SwalNoAnimation.fire({
1104 title: 'Unable to edit comment',
1104 title: 'Unable to edit comment',
1105 html: _gettext('Comment body was not changed.'),
1105 html: _gettext('Comment body was not changed.'),
1106 });
1106 });
1107 return;
1107 return;
1108 }
1108 }
1109 var excludeCancelBtn = false;
1109 var excludeCancelBtn = false;
1110 var submitEvent = true;
1110 var submitEvent = true;
1111 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1111 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1112 commentForm.cm.setOption("readOnly", true);
1112 commentForm.cm.setOption("readOnly", true);
1113
1113
1114 // Read last version known
1114 // Read last version known
1115 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
1115 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
1116 var version = versionSelector.data('lastVersion');
1116 var version = versionSelector.data('lastVersion');
1117
1117
1118 if (!version) {
1118 if (!version) {
1119 version = 0;
1119 version = 0;
1120 }
1120 }
1121
1121
1122 var postData = {
1122 var postData = {
1123 'text': text,
1123 'text': text,
1124 'f_path': f_path,
1124 'f_path': f_path,
1125 'line': line_no,
1125 'line': line_no,
1126 'comment_type': commentType,
1126 'comment_type': commentType,
1127 'draft': isDraft,
1127 'draft': isDraft,
1128 'version': version,
1128 'version': version,
1129 'csrf_token': CSRF_TOKEN
1129 'csrf_token': CSRF_TOKEN
1130 };
1130 };
1131
1131
1132 var submitSuccessCallback = function(json_data) {
1132 var submitSuccessCallback = function(json_data) {
1133 $editForm.remove();
1133 $editForm.remove();
1134 $comment.show();
1134 $comment.show();
1135 var postData = {
1135 var postData = {
1136 'text': text,
1136 'text': text,
1137 'renderer': $comment.attr('data-comment-renderer'),
1137 'renderer': $comment.attr('data-comment-renderer'),
1138 'csrf_token': CSRF_TOKEN
1138 'csrf_token': CSRF_TOKEN
1139 };
1139 };
1140
1140
1141 /* Inject new edited version selector */
1141 /* Inject new edited version selector */
1142 var updateCommentVersionDropDown = function () {
1142 var updateCommentVersionDropDown = function () {
1143 var versionSelectId = '#comment_versions_'+comment_id;
1143 var versionSelectId = '#comment_versions_'+comment_id;
1144 var preLoadVersionData = [
1144 var preLoadVersionData = [
1145 {
1145 {
1146 id: json_data['comment_version'],
1146 id: json_data['comment_version'],
1147 text: "v{0}".format(json_data['comment_version']),
1147 text: "v{0}".format(json_data['comment_version']),
1148 action: function () {
1148 action: function () {
1149 Rhodecode.comments.showVersion(
1149 Rhodecode.comments.showVersion(
1150 json_data['comment_id'],
1150 json_data['comment_id'],
1151 json_data['comment_history_id']
1151 json_data['comment_history_id']
1152 )
1152 )
1153 },
1153 },
1154 comment_version: json_data['comment_version'],
1154 comment_version: json_data['comment_version'],
1155 comment_author_username: json_data['comment_author_username'],
1155 comment_author_username: json_data['comment_author_username'],
1156 comment_author_gravatar: json_data['comment_author_gravatar'],
1156 comment_author_gravatar: json_data['comment_author_gravatar'],
1157 comment_created_on: json_data['comment_created_on'],
1157 comment_created_on: json_data['comment_created_on'],
1158 },
1158 },
1159 ]
1159 ]
1160
1160
1161
1161
1162 if ($(versionSelectId).data('select2')) {
1162 if ($(versionSelectId).data('select2')) {
1163 var oldData = $(versionSelectId).data('select2').opts.data.results;
1163 var oldData = $(versionSelectId).data('select2').opts.data.results;
1164 $(versionSelectId).select2("destroy");
1164 $(versionSelectId).select2("destroy");
1165 preLoadVersionData = oldData.concat(preLoadVersionData)
1165 preLoadVersionData = oldData.concat(preLoadVersionData)
1166 }
1166 }
1167
1167
1168 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1168 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1169
1169
1170 $comment.attr('data-comment-text', utf8ToB64(text));
1170 $comment.attr('data-comment-text', utf8ToB64(text));
1171
1171
1172 var versionSelector = $('#comment_versions_'+comment_id);
1172 var versionSelector = $('#comment_versions_'+comment_id);
1173
1173
1174 // set lastVersion so we know our last edit version
1174 // set lastVersion so we know our last edit version
1175 versionSelector.data('lastVersion', json_data['comment_version'])
1175 versionSelector.data('lastVersion', json_data['comment_version'])
1176 versionSelector.parent().show();
1176 versionSelector.parent().show();
1177 }
1177 }
1178 updateCommentVersionDropDown();
1178 updateCommentVersionDropDown();
1179
1179
1180 // by default we reset state of comment preserving the text
1180 // by default we reset state of comment preserving the text
1181 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1181 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1182 var prefix = "Error while editing this comment.\n"
1182 var prefix = "Error while editing this comment.\n"
1183 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1183 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1184 ajaxErrorSwal(message);
1184 ajaxErrorSwal(message);
1185 };
1185 };
1186
1186
1187 var successRenderCommit = function(o){
1187 var successRenderCommit = function(o){
1188 $comment.show();
1188 $comment.show();
1189 $comment[0].lastElementChild.innerHTML = o;
1189 $comment[0].lastElementChild.innerHTML = o;
1190 };
1190 };
1191
1191
1192 var previewUrl = pyroutes.url(
1192 var previewUrl = pyroutes.url(
1193 'repo_commit_comment_preview',
1193 'repo_commit_comment_preview',
1194 {'repo_name': templateContext.repo_name,
1194 {'repo_name': templateContext.repo_name,
1195 'commit_id': templateContext.commit_data.commit_id});
1195 'commit_id': templateContext.commit_data.commit_id});
1196
1196
1197 _submitAjaxPOST(
1197 _submitAjaxPOST(
1198 previewUrl, postData, successRenderCommit, failRenderCommit
1198 previewUrl, postData, successRenderCommit, failRenderCommit
1199 );
1199 );
1200
1200
1201 try {
1201 try {
1202 var html = json_data.rendered_text;
1202 var html = json_data.rendered_text;
1203 var lineno = json_data.line_no;
1203 var lineno = json_data.line_no;
1204 var target_id = json_data.target_id;
1204 var target_id = json_data.target_id;
1205
1205
1206 $comments.find('.cb-comment-add-button').before(html);
1206 $comments.find('.cb-comment-add-button').before(html);
1207
1207
1208 // run global callback on submit
1208 // run global callback on submit
1209 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1209 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1210
1210
1211 } catch (e) {
1211 } catch (e) {
1212 console.error(e);
1212 console.error(e);
1213 }
1213 }
1214
1214
1215 // re trigger the linkification of next/prev navigation
1215 // re trigger the linkification of next/prev navigation
1216 linkifyComments($('.inline-comment-injected'));
1216 linkifyComments($('.inline-comment-injected'));
1217 timeagoActivate();
1217 timeagoActivate();
1218 tooltipActivate();
1218 tooltipActivate();
1219
1219
1220 if (window.updateSticky !== undefined) {
1220 if (window.updateSticky !== undefined) {
1221 // potentially our comments change the active window size, so we
1221 // potentially our comments change the active window size, so we
1222 // notify sticky elements
1222 // notify sticky elements
1223 updateSticky()
1223 updateSticky()
1224 }
1224 }
1225
1225
1226 if (window.refreshAllComments !== undefined && !isDraft) {
1226 if (window.refreshAllComments !== undefined && !isDraft) {
1227 // if we have this handler, run it, and refresh all comments boxes
1227 // if we have this handler, run it, and refresh all comments boxes
1228 refreshAllComments()
1228 refreshAllComments()
1229 }
1229 }
1230 else if (window.refreshDraftComments !== undefined && isDraft) {
1230 else if (window.refreshDraftComments !== undefined && isDraft) {
1231 // if we have this handler, run it, and refresh all comments boxes
1231 // if we have this handler, run it, and refresh all comments boxes
1232 refreshDraftComments();
1232 refreshDraftComments();
1233 }
1233 }
1234
1234
1235 commentForm.setActionButtonsDisabled(false);
1235 commentForm.setActionButtonsDisabled(false);
1236
1236
1237 };
1237 };
1238
1238
1239 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1239 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1240 var prefix = "Error while editing comment.\n"
1240 var prefix = "Error while editing comment.\n"
1241 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1241 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1242 if (jqXHR.status == 409){
1242 if (jqXHR.status == 409){
1243 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1243 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1244 ajaxErrorSwal(message, 'Comment version mismatch.');
1244 ajaxErrorSwal(message, 'Comment version mismatch.');
1245 } else {
1245 } else {
1246 ajaxErrorSwal(message);
1246 ajaxErrorSwal(message);
1247 }
1247 }
1248
1248
1249 commentForm.resetCommentFormState(text)
1249 commentForm.resetCommentFormState(text)
1250 };
1250 };
1251 commentForm.submitAjaxPOST(
1251 commentForm.submitAjaxPOST(
1252 commentForm.submitUrl, postData,
1252 commentForm.submitUrl, postData,
1253 submitSuccessCallback,
1253 submitSuccessCallback,
1254 submitFailCallback);
1254 submitFailCallback);
1255 });
1255 });
1256 }
1256 }
1257
1257
1258 $editForm.addClass('comment-inline-form-open');
1258 $editForm.addClass('comment-inline-form-open');
1259 };
1259 };
1260
1260
1261 this.attachComment = function(json_data) {
1261 this.attachComment = function(json_data) {
1262 var self = this;
1262 var self = this;
1263 $.each(json_data, function(idx, val) {
1263 $.each(json_data, function(idx, val) {
1264 var json_data_elem = [val]
1264 var json_data_elem = [val]
1265 var isInline = val.comment_f_path && val.comment_lineno
1265 var isInline = val.comment_f_path && val.comment_lineno
1266
1266
1267 if (isInline) {
1267 if (isInline) {
1268 self.attachInlineComment(json_data_elem)
1268 self.attachInlineComment(json_data_elem)
1269 } else {
1269 } else {
1270 self.attachGeneralComment(json_data_elem)
1270 self.attachGeneralComment(json_data_elem)
1271 }
1271 }
1272 })
1272 })
1273
1273
1274 }
1274 }
1275
1275
1276 this.attachGeneralComment = function(json_data) {
1276 this.attachGeneralComment = function(json_data) {
1277 $.each(json_data, function(idx, val) {
1277 $.each(json_data, function(idx, val) {
1278 $('#injected_page_comments').append(val.rendered_text);
1278 $('#injected_page_comments').append(val.rendered_text);
1279 })
1279 })
1280 }
1280 }
1281
1281
1282 this.attachInlineComment = function(json_data) {
1282 this.attachInlineComment = function(json_data) {
1283
1283
1284 $.each(json_data, function (idx, val) {
1284 $.each(json_data, function (idx, val) {
1285 var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
1285 var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
1286 var html = val.rendered_text;
1286 var html = val.rendered_text;
1287 var $inlineComments = $('#' + val.target_id)
1287 var $inlineComments = $('#' + val.target_id)
1288 .find(line_qry)
1288 .find(line_qry)
1289 .find('.inline-comments');
1289 .find('.inline-comments');
1290
1290
1291 var lastComment = $inlineComments.find('.comment-inline').last();
1291 var lastComment = $inlineComments.find('.comment-inline').last();
1292
1292
1293 if (lastComment.length === 0) {
1293 if (lastComment.length === 0) {
1294 // first comment, we append simply
1294 // first comment, we append simply
1295 $inlineComments.find('.reply-thread-container-wrapper').before(html);
1295 $inlineComments.find('.reply-thread-container-wrapper').before(html);
1296 } else {
1296 } else {
1297 $(lastComment).after(html)
1297 $(lastComment).after(html)
1298 }
1298 }
1299
1299
1300 })
1300 })
1301
1301
1302 };
1302 };
1303
1303
1304 this.createNewFormWrapper = function(f_path, line_no) {
1304 this.createNewFormWrapper = function(f_path, line_no) {
1305 // create a new reply HTML form from template
1305 // create a new reply HTML form from template
1306 var tmpl = $('#cb-comment-inline-form-template').html();
1306 var tmpl = $('#cb-comment-inline-form-template').html();
1307 tmpl = tmpl.format(escapeHtml(f_path), line_no);
1307 tmpl = tmpl.format(escapeHtml(f_path), line_no);
1308 return $(tmpl);
1308 return $(tmpl);
1309 }
1309 }
1310
1310
1311 this.createComment = function(node, f_path, line_no, resolutionComment) {
1311 this.createComment = function(node, f_path, line_no, resolutionComment) {
1312 self.edit = false;
1312 self.edit = false;
1313 var $node = $(node);
1313 var $node = $(node);
1314 var $td = $node.closest('td');
1314 var $td = $node.closest('td');
1315 var resolvesCommentId = resolutionComment || null;
1315 var resolvesCommentId = resolutionComment || null;
1316
1316
1317 var $replyForm = $td.find('.comment-inline-form');
1317 var $replyForm = $td.find('.comment-inline-form');
1318
1318
1319 // if form isn't existing, we're generating a new one and injecting it.
1319 // if form isn't existing, we're generating a new one and injecting it.
1320 if ($replyForm.length === 0) {
1320 if ($replyForm.length === 0) {
1321
1321
1322 // unhide/expand all comments if they are hidden for a proper REPLY mode
1322 // unhide/expand all comments if they are hidden for a proper REPLY mode
1323 self.toggleLineComments($node, true);
1323 self.toggleLineComments($node, true);
1324
1324
1325 $replyForm = self.createNewFormWrapper(f_path, line_no);
1325 $replyForm = self.createNewFormWrapper(f_path, line_no);
1326
1326
1327 var $comments = $td.find('.inline-comments');
1327 var $comments = $td.find('.inline-comments');
1328
1328
1329 // There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
1329 // There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
1330 if ($comments.length===0) {
1330 if ($comments.length===0) {
1331 var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(f_path, line_no)
1331 var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(f_path, line_no)
1332 var $reply_container = $('#cb-comments-inline-container-template')
1332 var $reply_container = $('#cb-comments-inline-container-template')
1333 $reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
1333 $reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
1334 $td.append($($reply_container).html());
1334 $td.append($($reply_container).html());
1335 }
1335 }
1336
1336
1337 // default comment button exists, so we prepend the form for leaving initial comment
1337 // default comment button exists, so we prepend the form for leaving initial comment
1338 $td.find('.cb-comment-add-button').before($replyForm);
1338 $td.find('.cb-comment-add-button').before($replyForm);
1339 // set marker, that we have a open form
1339 // set marker, that we have a open form
1340 var $replyWrapper = $td.find('.reply-thread-container-wrapper')
1340 var $replyWrapper = $td.find('.reply-thread-container-wrapper')
1341 $replyWrapper.addClass('comment-form-active');
1341 $replyWrapper.addClass('comment-form-active');
1342
1342
1343 var lastComment = $comments.find('.comment-inline').last();
1343 var lastComment = $comments.find('.comment-inline').last();
1344 if ($(lastComment).hasClass('comment-outdated')) {
1344 if ($(lastComment).hasClass('comment-outdated')) {
1345 $replyWrapper.show();
1345 $replyWrapper.show();
1346 }
1346 }
1347
1347
1348 var _form = $($replyForm[0]).find('form');
1348 var _form = $($replyForm[0]).find('form');
1349 var autocompleteActions = ['as_note', 'as_todo'];
1349 var autocompleteActions = ['as_note', 'as_todo'];
1350 var comment_id=null;
1350 var comment_id=null;
1351 var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
1351 var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
1352 var commentForm = self.createCommentForm(
1352 var commentForm = self.createCommentForm(
1353 _form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
1353 _form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
1354 self.edit, comment_id);
1354 self.edit, comment_id);
1355
1355
1356 // set a CUSTOM submit handler for inline comments.
1356 // set a CUSTOM submit handler for inline comments.
1357 commentForm.setHandleFormSubmit(function(o) {
1357 commentForm.setHandleFormSubmit(function(o) {
1358 var text = commentForm.cm.getValue();
1358 var text = commentForm.cm.getValue();
1359 var commentType = commentForm.getCommentType();
1359 var commentType = commentForm.getCommentType();
1360 var resolvesCommentId = commentForm.getResolvesId();
1360 var resolvesCommentId = commentForm.getResolvesId();
1361 var isDraft = commentForm.getDraftState();
1361 var isDraft = commentForm.getDraftState();
1362
1362
1363 if (text === "") {
1363 if (text === "") {
1364 return;
1364 return;
1365 }
1365 }
1366
1366
1367 if (line_no === undefined) {
1367 if (line_no === undefined) {
1368 alert('Error: unable to fetch line number for this inline comment !');
1368 alert('Error: unable to fetch line number for this inline comment !');
1369 return;
1369 return;
1370 }
1370 }
1371
1371
1372 if (f_path === undefined) {
1372 if (f_path === undefined) {
1373 alert('Error: unable to fetch file path for this inline comment !');
1373 alert('Error: unable to fetch file path for this inline comment !');
1374 return;
1374 return;
1375 }
1375 }
1376
1376
1377 var excludeCancelBtn = false;
1377 var excludeCancelBtn = false;
1378 var submitEvent = true;
1378 var submitEvent = true;
1379 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1379 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1380 commentForm.cm.setOption("readOnly", true);
1380 commentForm.cm.setOption("readOnly", true);
1381 var postData = {
1381 var postData = {
1382 'text': text,
1382 'text': text,
1383 'f_path': f_path,
1383 'f_path': f_path,
1384 'line': line_no,
1384 'line': line_no,
1385 'comment_type': commentType,
1385 'comment_type': commentType,
1386 'draft': isDraft,
1386 'draft': isDraft,
1387 'csrf_token': CSRF_TOKEN
1387 'csrf_token': CSRF_TOKEN
1388 };
1388 };
1389 if (resolvesCommentId){
1389 if (resolvesCommentId){
1390 postData['resolves_comment_id'] = resolvesCommentId;
1390 postData['resolves_comment_id'] = resolvesCommentId;
1391 }
1391 }
1392
1392
1393 // submitSuccess for inline commits
1393 // submitSuccess for inline commits
1394 var submitSuccessCallback = function(json_data) {
1394 var submitSuccessCallback = function(json_data) {
1395
1395
1396 $replyForm.remove();
1396 $replyForm.remove();
1397 $td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
1397 $td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
1398
1398
1399 try {
1399 try {
1400
1400
1401 // inject newly created comments, json_data is {<comment_id>: {}}
1401 // inject newly created comments, json_data is {<comment_id>: {}}
1402 self.attachInlineComment(json_data)
1402 self.attachInlineComment(json_data)
1403
1403
1404 //mark visually which comment was resolved
1404 //mark visually which comment was resolved
1405 if (resolvesCommentId) {
1405 if (resolvesCommentId) {
1406 commentForm.markCommentResolved(resolvesCommentId);
1406 commentForm.markCommentResolved(resolvesCommentId);
1407 }
1407 }
1408
1408
1409 // run global callback on submit
1409 // run global callback on submit
1410 commentForm.globalSubmitSuccessCallback({
1410 commentForm.globalSubmitSuccessCallback({
1411 draft: isDraft,
1411 draft: isDraft,
1412 comment_id: comment_id
1412 comment_id: comment_id
1413 });
1413 });
1414
1414
1415 } catch (e) {
1415 } catch (e) {
1416 console.error(e);
1416 console.error(e);
1417 }
1417 }
1418
1418
1419 if (window.updateSticky !== undefined) {
1419 if (window.updateSticky !== undefined) {
1420 // potentially our comments change the active window size, so we
1420 // potentially our comments change the active window size, so we
1421 // notify sticky elements
1421 // notify sticky elements
1422 updateSticky()
1422 updateSticky()
1423 }
1423 }
1424
1424
1425 if (window.refreshAllComments !== undefined && !isDraft) {
1425 if (window.refreshAllComments !== undefined && !isDraft) {
1426 // if we have this handler, run it, and refresh all comments boxes
1426 // if we have this handler, run it, and refresh all comments boxes
1427 refreshAllComments()
1427 refreshAllComments()
1428 }
1428 }
1429 else if (window.refreshDraftComments !== undefined && isDraft) {
1429 else if (window.refreshDraftComments !== undefined && isDraft) {
1430 // if we have this handler, run it, and refresh all comments boxes
1430 // if we have this handler, run it, and refresh all comments boxes
1431 refreshDraftComments();
1431 refreshDraftComments();
1432 }
1432 }
1433
1433
1434 commentForm.setActionButtonsDisabled(false);
1434 commentForm.setActionButtonsDisabled(false);
1435
1435
1436 // re trigger the linkification of next/prev navigation
1436 // re trigger the linkification of next/prev navigation
1437 linkifyComments($('.inline-comment-injected'));
1437 linkifyComments($('.inline-comment-injected'));
1438 timeagoActivate();
1438 timeagoActivate();
1439 tooltipActivate();
1439 tooltipActivate();
1440 };
1440 };
1441
1441
1442 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1442 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1443 var prefix = "Error while submitting comment.\n"
1443 var prefix = "Error while submitting comment.\n"
1444 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1444 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1445 ajaxErrorSwal(message);
1445 ajaxErrorSwal(message);
1446 commentForm.resetCommentFormState(text)
1446 commentForm.resetCommentFormState(text)
1447 };
1447 };
1448
1448
1449 commentForm.submitAjaxPOST(
1449 commentForm.submitAjaxPOST(
1450 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1450 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1451 });
1451 });
1452 }
1452 }
1453
1453
1454 // Finally "open" our reply form, since we know there are comments and we have the "attached" old form
1454 // Finally "open" our reply form, since we know there are comments and we have the "attached" old form
1455 $replyForm.addClass('comment-inline-form-open');
1455 $replyForm.addClass('comment-inline-form-open');
1456 tooltipActivate();
1456 tooltipActivate();
1457 };
1457 };
1458
1458
1459 this.createResolutionComment = function(commentId){
1459 this.createResolutionComment = function(commentId){
1460 // hide the trigger text
1460 // hide the trigger text
1461 $('#resolve-comment-{0}'.format(commentId)).hide();
1461 $('#resolve-comment-{0}'.format(commentId)).hide();
1462
1462
1463 var comment = $('#comment-'+commentId);
1463 var comment = $('#comment-'+commentId);
1464 var commentData = comment.data();
1464 var commentData = comment.data();
1465 console.log(commentData);
1466
1465
1467 if (commentData.commentInline) {
1466 if (commentData.commentInline) {
1468 var f_path = commentData.commentFPath;
1467 var f_path = commentData.commentFPath;
1469 var line_no = commentData.commentLineNo;
1468 var line_no = commentData.commentLineNo;
1470 this.createComment(comment, f_path, line_no, commentId)
1469 this.createComment(comment, f_path, line_no, commentId)
1471 } else {
1470 } else {
1472 this.createGeneralComment('general', "$placeholder", commentId)
1471 this.createGeneralComment('general', "$placeholder", commentId)
1473 }
1472 }
1474
1473
1475 return false;
1474 return false;
1476 };
1475 };
1477
1476
1478 this.submitResolution = function(commentId){
1477 this.submitResolution = function(commentId){
1479 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1478 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1480 var commentForm = form.get(0).CommentForm;
1479 var commentForm = form.get(0).CommentForm;
1481
1480
1482 var cm = commentForm.getCmInstance();
1481 var cm = commentForm.getCmInstance();
1483 var renderer = templateContext.visual.default_renderer;
1482 var renderer = templateContext.visual.default_renderer;
1484 if (renderer == 'rst'){
1483 if (renderer == 'rst'){
1485 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1484 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1486 } else if (renderer == 'markdown') {
1485 } else if (renderer == 'markdown') {
1487 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1486 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1488 } else {
1487 } else {
1489 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1488 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1490 }
1489 }
1491
1490
1492 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1491 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1493 form.submit();
1492 form.submit();
1494 return false;
1493 return false;
1495 };
1494 };
1496
1495
1497 };
1496 };
1498
1497
1499 window.commentHelp = function(renderer) {
1498 window.commentHelp = function(renderer) {
1500 var funcData = {'renderer': renderer}
1499 var funcData = {'renderer': renderer}
1501 return renderTemplate('commentHelpHovercard', funcData)
1500 return renderTemplate('commentHelpHovercard', funcData)
1502 } No newline at end of file
1501 }
@@ -1,206 +1,205 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
4
4
5 ## EMAIL SUBJECT
5 ## EMAIL SUBJECT
6 <%def name="subject()" filter="n,trim,whitespace_filter">
6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 <%
7 <%
8 data = {
8 data = {
9 'user': '@'+h.person(user),
9 'user': '@'+h.person(user),
10 'repo_name': repo_name,
10 'repo_name': repo_name,
11 'status': status_change,
11 'status': status_change,
12 'comment_file': comment_file,
12 'comment_file': comment_file,
13 'comment_line': comment_line,
13 'comment_line': comment_line,
14 'comment_type': comment_type,
14 'comment_type': comment_type,
15 'comment_id': comment_id,
15 'comment_id': comment_id,
16
16
17 'pr_title': pull_request.title,
17 'pr_title': pull_request.title_safe,
18 'pr_id': pull_request.pull_request_id,
18 'pr_id': pull_request.pull_request_id,
19 'mention_prefix': '[mention] ' if mention else '',
19 'mention_prefix': '[mention] ' if mention else '',
20 }
20 }
21
21
22 if comment_file:
22 if comment_file:
23 subject_template = email_pr_comment_file_subject_template or \
23 subject_template = email_pr_comment_file_subject_template or \
24 _('{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"').format(**data)
24 _('{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"').format(**data)
25 else:
25 else:
26 if status_change:
26 if status_change:
27 subject_template = email_pr_comment_status_change_subject_template or \
27 subject_template = email_pr_comment_status_change_subject_template or \
28 _('{mention_prefix}[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
28 _('{mention_prefix}[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
29 else:
29 else:
30 subject_template = email_pr_comment_subject_template or \
30 subject_template = email_pr_comment_subject_template or \
31 _('{mention_prefix}{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
31 _('{mention_prefix}{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
32 %>
32 %>
33
33
34
35 ${subject_template.format(**data) |n}
34 ${subject_template.format(**data) |n}
36 </%def>
35 </%def>
37
36
38 ## PLAINTEXT VERSION OF BODY
37 ## PLAINTEXT VERSION OF BODY
39 <%def name="body_plaintext()" filter="n,trim">
38 <%def name="body_plaintext()" filter="n,trim">
40 <%
39 <%
41 data = {
40 data = {
42 'user': h.person(user),
41 'user': h.person(user),
43 'repo_name': repo_name,
42 'repo_name': repo_name,
44 'status': status_change,
43 'status': status_change,
45 'comment_file': comment_file,
44 'comment_file': comment_file,
46 'comment_line': comment_line,
45 'comment_line': comment_line,
47 'comment_type': comment_type,
46 'comment_type': comment_type,
48 'comment_id': comment_id,
47 'comment_id': comment_id,
49
48
50 'pr_title': pull_request.title,
49 'pr_title': pull_request.title_safe,
51 'pr_id': pull_request.pull_request_id,
50 'pr_id': pull_request.pull_request_id,
52 'source_ref_type': pull_request.source_ref_parts.type,
51 'source_ref_type': pull_request.source_ref_parts.type,
53 'source_ref_name': pull_request.source_ref_parts.name,
52 'source_ref_name': pull_request.source_ref_parts.name,
54 'target_ref_type': pull_request.target_ref_parts.type,
53 'target_ref_type': pull_request.target_ref_parts.type,
55 'target_ref_name': pull_request.target_ref_parts.name,
54 'target_ref_name': pull_request.target_ref_parts.name,
56 'source_repo': pull_request_source_repo.repo_name,
55 'source_repo': pull_request_source_repo.repo_name,
57 'target_repo': pull_request_target_repo.repo_name,
56 'target_repo': pull_request_target_repo.repo_name,
58 'source_repo_url': pull_request_source_repo_url,
57 'source_repo_url': pull_request_source_repo_url,
59 'target_repo_url': pull_request_target_repo_url,
58 'target_repo_url': pull_request_target_repo_url,
60 }
59 }
61 %>
60 %>
62
61
63 * ${_('Comment link')}: ${pr_comment_url}
62 * ${_('Comment link')}: ${pr_comment_url}
64
63
65 * ${_('Pull Request')}: !${pull_request.pull_request_id}
64 * ${_('Pull Request')}: !${pull_request.pull_request_id}
66
65
67 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
66 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
68
67
69 %if status_change and not closing_pr:
68 %if status_change and not closing_pr:
70 * ${_('{user} submitted pull request !{pr_id} status: *{status}*').format(**data)}
69 * ${_('{user} submitted pull request !{pr_id} status: *{status}*').format(**data)}
71
70
72 %elif status_change and closing_pr:
71 %elif status_change and closing_pr:
73 * ${_('{user} submitted pull request !{pr_id} status: *{status} and closed*').format(**data)}
72 * ${_('{user} submitted pull request !{pr_id} status: *{status} and closed*').format(**data)}
74
73
75 %endif
74 %endif
76 %if comment_file:
75 %if comment_file:
77 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
76 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
78
77
79 %endif
78 %endif
80 % if comment_type == 'todo':
79 % if comment_type == 'todo':
81 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
80 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
82 % else:
81 % else:
83 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
82 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
84 % endif
83 % endif
85
84
86 ${comment_body |n, trim}
85 ${comment_body |n, trim}
87
86
88 ---
87 ---
89 ${self.plaintext_footer()}
88 ${self.plaintext_footer()}
90 </%def>
89 </%def>
91
90
92
91
93 <%
92 <%
94 data = {
93 data = {
95 'user': h.person(user),
94 'user': h.person(user),
96 'comment_file': comment_file,
95 'comment_file': comment_file,
97 'comment_line': comment_line,
96 'comment_line': comment_line,
98 'comment_type': comment_type,
97 'comment_type': comment_type,
99 'comment_id': comment_id,
98 'comment_id': comment_id,
100 'renderer_type': renderer_type or 'plain',
99 'renderer_type': renderer_type or 'plain',
101
100
102 'pr_title': pull_request.title,
101 'pr_title': pull_request.title_safe,
103 'pr_id': pull_request.pull_request_id,
102 'pr_id': pull_request.pull_request_id,
104 'status': status_change,
103 'status': status_change,
105 'source_ref_type': pull_request.source_ref_parts.type,
104 'source_ref_type': pull_request.source_ref_parts.type,
106 'source_ref_name': pull_request.source_ref_parts.name,
105 'source_ref_name': pull_request.source_ref_parts.name,
107 'target_ref_type': pull_request.target_ref_parts.type,
106 'target_ref_type': pull_request.target_ref_parts.type,
108 'target_ref_name': pull_request.target_ref_parts.name,
107 'target_ref_name': pull_request.target_ref_parts.name,
109 'source_repo': pull_request_source_repo.repo_name,
108 'source_repo': pull_request_source_repo.repo_name,
110 'target_repo': pull_request_target_repo.repo_name,
109 'target_repo': pull_request_target_repo.repo_name,
111 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
110 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
112 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
111 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
113 }
112 }
114 %>
113 %>
115
114
116 ## header
115 ## header
117 <table style="text-align:left;vertical-align:middle;width: 100%">
116 <table style="text-align:left;vertical-align:middle;width: 100%">
118 <tr>
117 <tr>
119 <td style="width:100%;border-bottom:1px solid #dbd9da;">
118 <td style="width:100%;border-bottom:1px solid #dbd9da;">
120
119
121 <div style="margin: 0; font-weight: bold">
120 <div style="margin: 0; font-weight: bold">
122 <div class="clear-both" style="margin-bottom: 4px">
121 <div class="clear-both" style="margin-bottom: 4px">
123 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
122 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
124 ${_('left a')}
123 ${_('left a')}
125 <a href="${pr_comment_url}" style="${base.link_css()}">
124 <a href="${pr_comment_url}" style="${base.link_css()}">
126 % if comment_file:
125 % if comment_file:
127 ${_('{comment_type} on file `{comment_file}` in pull request.').format(**data)}
126 ${_('{comment_type} on file `{comment_file}` in pull request.').format(**data)}
128 % else:
127 % else:
129 ${_('{comment_type} on pull request.').format(**data) |n}
128 ${_('{comment_type} on pull request.').format(**data) |n}
130 % endif
129 % endif
131 </a>
130 </a>
132 </div>
131 </div>
133 <div style="margin-top: 10px"></div>
132 <div style="margin-top: 10px"></div>
134 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
133 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
135 </div>
134 </div>
136
135
137 </td>
136 </td>
138 </tr>
137 </tr>
139
138
140 </table>
139 </table>
141 <div class="clear-both"></div>
140 <div class="clear-both"></div>
142 ## main body
141 ## main body
143 <table style="text-align:left;vertical-align:middle;width: 100%">
142 <table style="text-align:left;vertical-align:middle;width: 100%">
144
143
145 ## spacing def
144 ## spacing def
146 <tr>
145 <tr>
147 <td style="width: 130px"></td>
146 <td style="width: 130px"></td>
148 <td></td>
147 <td></td>
149 </tr>
148 </tr>
150
149
151 % if status_change:
150 % if status_change:
152 <tr>
151 <tr>
153 <td style="padding-right:20px;">${_('Review Status')}:</td>
152 <td style="padding-right:20px;">${_('Review Status')}:</td>
154 <td>
153 <td>
155 % if closing_pr:
154 % if closing_pr:
156 ${_('Closed pull request with status')}: ${base.status_text(status_change, tag_type=status_change_type)}
155 ${_('Closed pull request with status')}: ${base.status_text(status_change, tag_type=status_change_type)}
157 % else:
156 % else:
158 ${_('Submitted review status')}: ${base.status_text(status_change, tag_type=status_change_type)}
157 ${_('Submitted review status')}: ${base.status_text(status_change, tag_type=status_change_type)}
159 % endif
158 % endif
160 </td>
159 </td>
161 </tr>
160 </tr>
162 % endif
161 % endif
163 <tr>
162 <tr>
164 <td style="padding-right:20px;">${_('Pull request')}:</td>
163 <td style="padding-right:20px;">${_('Pull request')}:</td>
165 <td>
164 <td>
166 <a href="${pull_request_url}" style="${base.link_css()}">
165 <a href="${pull_request_url}" style="${base.link_css()}">
167 !${pull_request.pull_request_id}
166 !${pull_request.pull_request_id}
168 </a>
167 </a>
169 </td>
168 </td>
170 </tr>
169 </tr>
171
170
172 <tr>
171 <tr>
173 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
172 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
174 <td style="line-height:20px;">
173 <td style="line-height:20px;">
175 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
174 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
176 &rarr;
175 &rarr;
177 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
176 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
178 </td>
177 </td>
179 </tr>
178 </tr>
180
179
181 % if comment_file:
180 % if comment_file:
182 <tr>
181 <tr>
183 <td style="padding-right:20px;">${_('File')}:</td>
182 <td style="padding-right:20px;">${_('File')}:</td>
184 <td><a href="${pr_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
183 <td><a href="${pr_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
185 </tr>
184 </tr>
186 % endif
185 % endif
187
186
188 <tr style="border-bottom:1px solid #dbd9da;">
187 <tr style="border-bottom:1px solid #dbd9da;">
189 <td colspan="2" style="padding-right:20px;">
188 <td colspan="2" style="padding-right:20px;">
190 % if comment_type == 'todo':
189 % if comment_type == 'todo':
191 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
190 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
192 % else:
191 % else:
193 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
192 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
194 % endif
193 % endif
195 </td>
194 </td>
196 </tr>
195 </tr>
197
196
198 <tr>
197 <tr>
199 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
198 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
200 </tr>
199 </tr>
201
200
202 <tr>
201 <tr>
203 <td><a href="${pr_comment_reply_url}">${_('Reply')}</a></td>
202 <td><a href="${pr_comment_reply_url}">${_('Reply')}</a></td>
204 <td></td>
203 <td></td>
205 </tr>
204 </tr>
206 </table>
205 </table>
@@ -1,154 +1,154 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
4
4
5 ## EMAIL SUBJECT
5 ## EMAIL SUBJECT
6 <%def name="subject()" filter="n,trim,whitespace_filter">
6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 <%
7 <%
8 data = {
8 data = {
9 'user': '@'+h.person(user),
9 'user': '@'+h.person(user),
10 'pr_id': pull_request.pull_request_id,
10 'pr_id': pull_request.pull_request_id,
11 'pr_title': pull_request.title,
11 'pr_title': pull_request.title_safe,
12 }
12 }
13
13
14 if user_role == 'observer':
14 if user_role == 'observer':
15 subject_template = email_pr_review_subject_template or _('{user} added you as observer to pull request. !{pr_id}: "{pr_title}"')
15 subject_template = email_pr_review_subject_template or _('{user} added you as observer to pull request. !{pr_id}: "{pr_title}"')
16 else:
16 else:
17 subject_template = email_pr_review_subject_template or _('{user} requested a pull request review. !{pr_id}: "{pr_title}"')
17 subject_template = email_pr_review_subject_template or _('{user} requested a pull request review. !{pr_id}: "{pr_title}"')
18 %>
18 %>
19
19
20 ${subject_template.format(**data) |n}
20 ${subject_template.format(**data) |n}
21 </%def>
21 </%def>
22
22
23 ## PLAINTEXT VERSION OF BODY
23 ## PLAINTEXT VERSION OF BODY
24 <%def name="body_plaintext()" filter="n,trim">
24 <%def name="body_plaintext()" filter="n,trim">
25 <%
25 <%
26 data = {
26 data = {
27 'user': h.person(user),
27 'user': h.person(user),
28 'pr_id': pull_request.pull_request_id,
28 'pr_id': pull_request.pull_request_id,
29 'pr_title': pull_request.title,
29 'pr_title': pull_request.title_safe,
30 'source_ref_type': pull_request.source_ref_parts.type,
30 'source_ref_type': pull_request.source_ref_parts.type,
31 'source_ref_name': pull_request.source_ref_parts.name,
31 'source_ref_name': pull_request.source_ref_parts.name,
32 'target_ref_type': pull_request.target_ref_parts.type,
32 'target_ref_type': pull_request.target_ref_parts.type,
33 'target_ref_name': pull_request.target_ref_parts.name,
33 'target_ref_name': pull_request.target_ref_parts.name,
34 'repo_url': pull_request_source_repo_url,
34 'repo_url': pull_request_source_repo_url,
35 'source_repo': pull_request_source_repo.repo_name,
35 'source_repo': pull_request_source_repo.repo_name,
36 'target_repo': pull_request_target_repo.repo_name,
36 'target_repo': pull_request_target_repo.repo_name,
37 'source_repo_url': pull_request_source_repo_url,
37 'source_repo_url': pull_request_source_repo_url,
38 'target_repo_url': pull_request_target_repo_url,
38 'target_repo_url': pull_request_target_repo_url,
39 }
39 }
40
40
41 %>
41 %>
42
42
43 * ${_('Pull Request link')}: ${pull_request_url}
43 * ${_('Pull Request link')}: ${pull_request_url}
44
44
45 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
45 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
46
46
47 * ${_('Title')}: ${pull_request.title}
47 * ${_('Title')}: ${pull_request.title}
48
48
49 * ${_('Description')}:
49 * ${_('Description')}:
50
50
51 ${pull_request.description | trim}
51 ${pull_request.description | trim}
52
52
53
53
54 * ${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}:
54 * ${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}:
55
55
56 % for commit_id, message in pull_request_commits:
56 % for commit_id, message in pull_request_commits:
57 - ${h.short_id(commit_id)}
57 - ${h.short_id(commit_id)}
58 ${h.chop_at_smart(message.lstrip(), '\n', suffix_if_chopped='...')}
58 ${h.chop_at_smart(message.lstrip(), '\n', suffix_if_chopped='...')}
59
59
60 % endfor
60 % endfor
61
61
62 ---
62 ---
63 ${self.plaintext_footer()}
63 ${self.plaintext_footer()}
64 </%def>
64 </%def>
65 <%
65 <%
66 data = {
66 data = {
67 'user': h.person(user),
67 'user': h.person(user),
68 'pr_id': pull_request.pull_request_id,
68 'pr_id': pull_request.pull_request_id,
69 'pr_title': pull_request.title,
69 'pr_title': pull_request.title_safe,
70 'source_ref_type': pull_request.source_ref_parts.type,
70 'source_ref_type': pull_request.source_ref_parts.type,
71 'source_ref_name': pull_request.source_ref_parts.name,
71 'source_ref_name': pull_request.source_ref_parts.name,
72 'target_ref_type': pull_request.target_ref_parts.type,
72 'target_ref_type': pull_request.target_ref_parts.type,
73 'target_ref_name': pull_request.target_ref_parts.name,
73 'target_ref_name': pull_request.target_ref_parts.name,
74 'repo_url': pull_request_source_repo_url,
74 'repo_url': pull_request_source_repo_url,
75 'source_repo': pull_request_source_repo.repo_name,
75 'source_repo': pull_request_source_repo.repo_name,
76 'target_repo': pull_request_target_repo.repo_name,
76 'target_repo': pull_request_target_repo.repo_name,
77 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
77 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
78 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
78 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
79 }
79 }
80 %>
80 %>
81 ## header
81 ## header
82 <table style="text-align:left;vertical-align:middle;width: 100%">
82 <table style="text-align:left;vertical-align:middle;width: 100%">
83 <tr>
83 <tr>
84 <td style="width:100%;border-bottom:1px solid #dbd9da;">
84 <td style="width:100%;border-bottom:1px solid #dbd9da;">
85 <div style="margin: 0; font-weight: bold">
85 <div style="margin: 0; font-weight: bold">
86 % if user_role == 'observer':
86 % if user_role == 'observer':
87 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
87 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
88 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
88 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
89 ${_('added you as observer to')}
89 ${_('added you as observer to')}
90 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a>.
90 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a>.
91 </div>
91 </div>
92 % else:
92 % else:
93 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
93 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
94 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
94 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
95 ${_('requested a')}
95 ${_('requested a')}
96 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a> review.
96 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a> review.
97 </div>
97 </div>
98 % endif
98 % endif
99 <div style="margin-top: 10px"></div>
99 <div style="margin-top: 10px"></div>
100 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
100 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
101 </div>
101 </div>
102 </td>
102 </td>
103 </tr>
103 </tr>
104
104
105 </table>
105 </table>
106 <div class="clear-both"></div>
106 <div class="clear-both"></div>
107 ## main body
107 ## main body
108 <table style="text-align:left;vertical-align:middle;width: 100%">
108 <table style="text-align:left;vertical-align:middle;width: 100%">
109 ## spacing def
109 ## spacing def
110 <tr>
110 <tr>
111 <td style="width: 130px"></td>
111 <td style="width: 130px"></td>
112 <td></td>
112 <td></td>
113 </tr>
113 </tr>
114
114
115 <tr>
115 <tr>
116 <td style="padding-right:20px;">${_('Pull request')}:</td>
116 <td style="padding-right:20px;">${_('Pull request')}:</td>
117 <td>
117 <td>
118 <a href="${pull_request_url}" style="${base.link_css()}">
118 <a href="${pull_request_url}" style="${base.link_css()}">
119 !${pull_request.pull_request_id}
119 !${pull_request.pull_request_id}
120 </a>
120 </a>
121 </td>
121 </td>
122 </tr>
122 </tr>
123
123
124 <tr>
124 <tr>
125 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
125 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
126 <td style="line-height:20px;">
126 <td style="line-height:20px;">
127 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
127 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
128 &rarr;
128 &rarr;
129 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
129 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
130 </td>
130 </td>
131 </tr>
131 </tr>
132
132
133 <tr>
133 <tr>
134 <td style="padding-right:20px;">${_('Description')}:</td>
134 <td style="padding-right:20px;">${_('Description')}:</td>
135 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
135 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
136 </tr>
136 </tr>
137 <tr>
137 <tr>
138 <td style="padding-right:20px;">${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits)) % {'num': len(pull_request_commits)}}:</td>
138 <td style="padding-right:20px;">${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits)) % {'num': len(pull_request_commits)}}:</td>
139 <td></td>
139 <td></td>
140 </tr>
140 </tr>
141
141
142 <tr>
142 <tr>
143 <td colspan="2">
143 <td colspan="2">
144 <ol style="margin:0 0 0 1em;padding:0;text-align:left;">
144 <ol style="margin:0 0 0 1em;padding:0;text-align:left;">
145 % for commit_id, message in pull_request_commits:
145 % for commit_id, message in pull_request_commits:
146 <li style="margin:0 0 1em;">
146 <li style="margin:0 0 1em;">
147 <pre style="margin:0 0 .5em"><a href="${h.route_path('repo_commit', repo_name=pull_request_source_repo.repo_name, commit_id=commit_id)}" style="${base.link_css()}">${h.short_id(commit_id)}</a></pre>
147 <pre style="margin:0 0 .5em"><a href="${h.route_path('repo_commit', repo_name=pull_request_source_repo.repo_name, commit_id=commit_id)}" style="${base.link_css()}">${h.short_id(commit_id)}</a></pre>
148 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
148 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
149 </li>
149 </li>
150 % endfor
150 % endfor
151 </ol>
151 </ol>
152 </td>
152 </td>
153 </tr>
153 </tr>
154 </table>
154 </table>
@@ -1,172 +1,172 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
4
4
5 ## EMAIL SUBJECT
5 ## EMAIL SUBJECT
6 <%def name="subject()" filter="n,trim,whitespace_filter">
6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 <%
7 <%
8 data = {
8 data = {
9 'updating_user': '@'+h.person(updating_user),
9 'updating_user': '@'+h.person(updating_user),
10 'pr_id': pull_request.pull_request_id,
10 'pr_id': pull_request.pull_request_id,
11 'pr_title': pull_request.title,
11 'pr_title': pull_request.title_safe,
12 }
12 }
13
13
14 subject_template = email_pr_update_subject_template or _('{updating_user} updated pull request. !{pr_id}: "{pr_title}"')
14 subject_template = email_pr_update_subject_template or _('{updating_user} updated pull request. !{pr_id}: "{pr_title}"')
15 %>
15 %>
16
16
17 ${subject_template.format(**data) |n}
17 ${subject_template.format(**data) |n}
18 </%def>
18 </%def>
19
19
20 ## PLAINTEXT VERSION OF BODY
20 ## PLAINTEXT VERSION OF BODY
21 <%def name="body_plaintext()" filter="n,trim">
21 <%def name="body_plaintext()" filter="n,trim">
22 <%
22 <%
23 data = {
23 data = {
24 'updating_user': h.person(updating_user),
24 'updating_user': h.person(updating_user),
25 'pr_id': pull_request.pull_request_id,
25 'pr_id': pull_request.pull_request_id,
26 'pr_title': pull_request.title,
26 'pr_title': pull_request.title_safe,
27 'source_ref_type': pull_request.source_ref_parts.type,
27 'source_ref_type': pull_request.source_ref_parts.type,
28 'source_ref_name': pull_request.source_ref_parts.name,
28 'source_ref_name': pull_request.source_ref_parts.name,
29 'target_ref_type': pull_request.target_ref_parts.type,
29 'target_ref_type': pull_request.target_ref_parts.type,
30 'target_ref_name': pull_request.target_ref_parts.name,
30 'target_ref_name': pull_request.target_ref_parts.name,
31 'repo_url': pull_request_source_repo_url,
31 'repo_url': pull_request_source_repo_url,
32 'source_repo': pull_request_source_repo.repo_name,
32 'source_repo': pull_request_source_repo.repo_name,
33 'target_repo': pull_request_target_repo.repo_name,
33 'target_repo': pull_request_target_repo.repo_name,
34 'source_repo_url': pull_request_source_repo_url,
34 'source_repo_url': pull_request_source_repo_url,
35 'target_repo_url': pull_request_target_repo_url,
35 'target_repo_url': pull_request_target_repo_url,
36 }
36 }
37 %>
37 %>
38
38
39 * ${_('Pull Request link')}: ${pull_request_url}
39 * ${_('Pull Request link')}: ${pull_request_url}
40
40
41 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
41 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
42
42
43 * ${_('Title')}: ${pull_request.title}
43 * ${_('Title')}: ${pull_request.title}
44
44
45 * ${_('Description')}:
45 * ${_('Description')}:
46
46
47 ${pull_request.description | trim}
47 ${pull_request.description | trim}
48
48
49 * Changed commits:
49 * Changed commits:
50
50
51 - Added: ${len(added_commits)}
51 - Added: ${len(added_commits)}
52 - Removed: ${len(removed_commits)}
52 - Removed: ${len(removed_commits)}
53
53
54 * Changed files:
54 * Changed files:
55
55
56 %if not changed_files:
56 %if not changed_files:
57 No file changes found
57 No file changes found
58 %else:
58 %else:
59 %for file_name in added_files:
59 %for file_name in added_files:
60 - A `${file_name}`
60 - A `${file_name}`
61 %endfor
61 %endfor
62 %for file_name in modified_files:
62 %for file_name in modified_files:
63 - M `${file_name}`
63 - M `${file_name}`
64 %endfor
64 %endfor
65 %for file_name in removed_files:
65 %for file_name in removed_files:
66 - R `${file_name}`
66 - R `${file_name}`
67 %endfor
67 %endfor
68 %endif
68 %endif
69
69
70 ---
70 ---
71 ${self.plaintext_footer()}
71 ${self.plaintext_footer()}
72 </%def>
72 </%def>
73 <%
73 <%
74 data = {
74 data = {
75 'updating_user': h.person(updating_user),
75 'updating_user': h.person(updating_user),
76 'pr_id': pull_request.pull_request_id,
76 'pr_id': pull_request.pull_request_id,
77 'pr_title': pull_request.title,
77 'pr_title': pull_request.title_safe,
78 'source_ref_type': pull_request.source_ref_parts.type,
78 'source_ref_type': pull_request.source_ref_parts.type,
79 'source_ref_name': pull_request.source_ref_parts.name,
79 'source_ref_name': pull_request.source_ref_parts.name,
80 'target_ref_type': pull_request.target_ref_parts.type,
80 'target_ref_type': pull_request.target_ref_parts.type,
81 'target_ref_name': pull_request.target_ref_parts.name,
81 'target_ref_name': pull_request.target_ref_parts.name,
82 'repo_url': pull_request_source_repo_url,
82 'repo_url': pull_request_source_repo_url,
83 'source_repo': pull_request_source_repo.repo_name,
83 'source_repo': pull_request_source_repo.repo_name,
84 'target_repo': pull_request_target_repo.repo_name,
84 'target_repo': pull_request_target_repo.repo_name,
85 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
85 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
86 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
86 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
87 }
87 }
88 %>
88 %>
89
89
90 ## header
90 ## header
91 <table style="text-align:left;vertical-align:middle;width: 100%">
91 <table style="text-align:left;vertical-align:middle;width: 100%">
92 <tr>
92 <tr>
93 <td style="width:100%;border-bottom:1px solid #dbd9da;">
93 <td style="width:100%;border-bottom:1px solid #dbd9da;">
94
94
95 <div style="margin: 0; font-weight: bold">
95 <div style="margin: 0; font-weight: bold">
96 <div class="clear-both" style="margin-bottom: 4px">
96 <div class="clear-both" style="margin-bottom: 4px">
97 <span style="color:#7E7F7F">@${h.person(updating_user.username)}</span>
97 <span style="color:#7E7F7F">@${h.person(updating_user.username)}</span>
98 ${_('updated')}
98 ${_('updated')}
99 <a href="${pull_request_url}" style="${base.link_css()}">
99 <a href="${pull_request_url}" style="${base.link_css()}">
100 ${_('pull request.').format(**data) }
100 ${_('pull request.').format(**data) }
101 </a>
101 </a>
102 </div>
102 </div>
103 <div style="margin-top: 10px"></div>
103 <div style="margin-top: 10px"></div>
104 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
104 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
105 </div>
105 </div>
106
106
107 </td>
107 </td>
108 </tr>
108 </tr>
109
109
110 </table>
110 </table>
111 <div class="clear-both"></div>
111 <div class="clear-both"></div>
112 ## main body
112 ## main body
113 <table style="text-align:left;vertical-align:middle;width: 100%">
113 <table style="text-align:left;vertical-align:middle;width: 100%">
114 ## spacing def
114 ## spacing def
115 <tr>
115 <tr>
116 <td style="width: 130px"></td>
116 <td style="width: 130px"></td>
117 <td></td>
117 <td></td>
118 </tr>
118 </tr>
119
119
120 <tr>
120 <tr>
121 <td style="padding-right:20px;">${_('Pull request')}:</td>
121 <td style="padding-right:20px;">${_('Pull request')}:</td>
122 <td>
122 <td>
123 <a href="${pull_request_url}" style="${base.link_css()}">
123 <a href="${pull_request_url}" style="${base.link_css()}">
124 !${pull_request.pull_request_id}
124 !${pull_request.pull_request_id}
125 </a>
125 </a>
126 </td>
126 </td>
127 </tr>
127 </tr>
128
128
129 <tr>
129 <tr>
130 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
130 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
131 <td style="line-height:20px;">
131 <td style="line-height:20px;">
132 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
132 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
133 &rarr;
133 &rarr;
134 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
134 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
135 </td>
135 </td>
136 </tr>
136 </tr>
137
137
138 <tr>
138 <tr>
139 <td style="padding-right:20px;">${_('Description')}:</td>
139 <td style="padding-right:20px;">${_('Description')}:</td>
140 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
140 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
141 </tr>
141 </tr>
142 <tr>
142 <tr>
143 <td style="padding-right:20px;">${_('Changes')}:</td>
143 <td style="padding-right:20px;">${_('Changes')}:</td>
144 <td>
144 <td>
145 <strong>Changed commits:</strong>
145 <strong>Changed commits:</strong>
146 <ul class="changes-ul">
146 <ul class="changes-ul">
147 <li>- Added: ${len(added_commits)}</li>
147 <li>- Added: ${len(added_commits)}</li>
148 <li>- Removed: ${len(removed_commits)}</li>
148 <li>- Removed: ${len(removed_commits)}</li>
149 </ul>
149 </ul>
150
150
151 <strong>Changed files:</strong>
151 <strong>Changed files:</strong>
152 <ul class="changes-ul">
152 <ul class="changes-ul">
153
153
154 %if not changed_files:
154 %if not changed_files:
155 <li>No file changes found</li>
155 <li>No file changes found</li>
156 %else:
156 %else:
157 %for file_name in added_files:
157 %for file_name in added_files:
158 <li>- A <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
158 <li>- A <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
159 %endfor
159 %endfor
160 %for file_name in modified_files:
160 %for file_name in modified_files:
161 <li>- M <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
161 <li>- M <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
162 %endfor
162 %endfor
163 %for file_name in removed_files:
163 %for file_name in removed_files:
164 <li>- R <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
164 <li>- R <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
165 %endfor
165 %endfor
166 %endif
166 %endif
167
167
168 </ul>
168 </ul>
169 </td>
169 </td>
170 </tr>
170 </tr>
171
171
172 </table>
172 </table>
@@ -1,195 +1,197 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 import pytest
21 import pytest
22 import collections
22 import collections
23
23
24 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
24 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
25 from rhodecode.lib.utils2 import AttributeDict
25 from rhodecode.lib.utils2 import AttributeDict
26 from rhodecode.model.db import User, PullRequestReviewers
26 from rhodecode.model.db import User, PullRequestReviewers
27 from rhodecode.model.notification import EmailNotificationModel
27 from rhodecode.model.notification import EmailNotificationModel
28
28
29
29
30 @pytest.fixture()
31 def pr():
32 def factory(ref):
33 return collections.namedtuple(
34 'PullRequest',
35 'pull_request_id, title, title_safe, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')\
36 (200, 'Example Pull Request', 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
37 return factory
38
39
30 def test_get_template_obj(app, request_stub):
40 def test_get_template_obj(app, request_stub):
31 template = EmailNotificationModel().get_renderer(
41 template = EmailNotificationModel().get_renderer(
32 EmailNotificationModel.TYPE_TEST, request_stub)
42 EmailNotificationModel.TYPE_TEST, request_stub)
33 assert isinstance(template, PyramidPartialRenderer)
43 assert isinstance(template, PyramidPartialRenderer)
34
44
35
45
36 def test_render_email(app, http_host_only_stub):
46 def test_render_email(app, http_host_only_stub):
37 kwargs = {}
47 kwargs = {}
38 subject, body, body_plaintext = EmailNotificationModel().render_email(
48 subject, body, body_plaintext = EmailNotificationModel().render_email(
39 EmailNotificationModel.TYPE_TEST, **kwargs)
49 EmailNotificationModel.TYPE_TEST, **kwargs)
40
50
41 # subject
51 # subject
42 assert subject == 'Test "Subject" hello "world"'
52 assert subject == 'Test "Subject" hello "world"'
43
53
44 # body plaintext
54 # body plaintext
45 assert body_plaintext == 'Email Plaintext Body'
55 assert body_plaintext == 'Email Plaintext Body'
46
56
47 # body
57 # body
48 notification_footer1 = 'This is a notification from RhodeCode.'
58 notification_footer1 = 'This is a notification from RhodeCode.'
49 notification_footer2 = 'http://{}/'.format(http_host_only_stub)
59 notification_footer2 = 'http://{}/'.format(http_host_only_stub)
50 assert notification_footer1 in body
60 assert notification_footer1 in body
51 assert notification_footer2 in body
61 assert notification_footer2 in body
52 assert 'Email Body' in body
62 assert 'Email Body' in body
53
63
54
64
55 @pytest.mark.parametrize('role', PullRequestReviewers.ROLES)
65 @pytest.mark.parametrize('role', PullRequestReviewers.ROLES)
56 def test_render_pr_email(app, user_admin, role):
66 def test_render_pr_email(app, user_admin, role, pr):
57 ref = collections.namedtuple(
67 ref = collections.namedtuple(
58 'Ref', 'name, type')('fxies123', 'book')
68 'Ref', 'name, type')('fxies123', 'book')
59
69 pr = pr(ref)
60 pr = collections.namedtuple('PullRequest',
61 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
62 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
63
64 source_repo = target_repo = collections.namedtuple(
70 source_repo = target_repo = collections.namedtuple(
65 'Repo', 'type, repo_name')('hg', 'pull_request_1')
71 'Repo', 'type, repo_name')('hg', 'pull_request_1')
66
72
67 kwargs = {
73 kwargs = {
68 'user': User.get_first_super_admin(),
74 'user': User.get_first_super_admin(),
69 'pull_request': pr,
75 'pull_request': pr,
70 'pull_request_commits': [],
76 'pull_request_commits': [],
71
77
72 'pull_request_target_repo': target_repo,
78 'pull_request_target_repo': target_repo,
73 'pull_request_target_repo_url': 'x',
79 'pull_request_target_repo_url': 'x',
74
80
75 'pull_request_source_repo': source_repo,
81 'pull_request_source_repo': source_repo,
76 'pull_request_source_repo_url': 'x',
82 'pull_request_source_repo_url': 'x',
77
83
78 'pull_request_url': 'http://localhost/pr1',
84 'pull_request_url': 'http://localhost/pr1',
79 'user_role': role,
85 'user_role': role,
80 }
86 }
81
87
82 subject, body, body_plaintext = EmailNotificationModel().render_email(
88 subject, body, body_plaintext = EmailNotificationModel().render_email(
83 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
89 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
84
90
85 # subject
91 # subject
86 if role == PullRequestReviewers.ROLE_REVIEWER:
92 if role == PullRequestReviewers.ROLE_REVIEWER:
87 assert subject == '@test_admin (RhodeCode Admin) requested a pull request review. !200: "Example Pull Request"'
93 assert subject == '@test_admin (RhodeCode Admin) requested a pull request review. !200: "Example Pull Request"'
88 elif role == PullRequestReviewers.ROLE_OBSERVER:
94 elif role == PullRequestReviewers.ROLE_OBSERVER:
89 assert subject == '@test_admin (RhodeCode Admin) added you as observer to pull request. !200: "Example Pull Request"'
95 assert subject == '@test_admin (RhodeCode Admin) added you as observer to pull request. !200: "Example Pull Request"'
90
96
91
97
92 def test_render_pr_update_email(app, user_admin):
98 def test_render_pr_update_email(app, user_admin, pr):
93 ref = collections.namedtuple(
99 ref = collections.namedtuple(
94 'Ref', 'name, type')('fxies123', 'book')
100 'Ref', 'name, type')('fxies123', 'book')
95
101
96 pr = collections.namedtuple('PullRequest',
102 pr = pr(ref)
97 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
98 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
99
103
100 source_repo = target_repo = collections.namedtuple(
104 source_repo = target_repo = collections.namedtuple(
101 'Repo', 'type, repo_name')('hg', 'pull_request_1')
105 'Repo', 'type, repo_name')('hg', 'pull_request_1')
102
106
103 commit_changes = AttributeDict({
107 commit_changes = AttributeDict({
104 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
108 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
105 'removed': ['eeeeeeeeeee'],
109 'removed': ['eeeeeeeeeee'],
106 })
110 })
107 file_changes = AttributeDict({
111 file_changes = AttributeDict({
108 'added': ['a/file1.md', 'file2.py'],
112 'added': ['a/file1.md', 'file2.py'],
109 'modified': ['b/modified_file.rst'],
113 'modified': ['b/modified_file.rst'],
110 'removed': ['.idea'],
114 'removed': ['.idea'],
111 })
115 })
112
116
113 kwargs = {
117 kwargs = {
114 'updating_user': User.get_first_super_admin(),
118 'updating_user': User.get_first_super_admin(),
115
119
116 'pull_request': pr,
120 'pull_request': pr,
117 'pull_request_commits': [],
121 'pull_request_commits': [],
118
122
119 'pull_request_target_repo': target_repo,
123 'pull_request_target_repo': target_repo,
120 'pull_request_target_repo_url': 'x',
124 'pull_request_target_repo_url': 'x',
121
125
122 'pull_request_source_repo': source_repo,
126 'pull_request_source_repo': source_repo,
123 'pull_request_source_repo_url': 'x',
127 'pull_request_source_repo_url': 'x',
124
128
125 'pull_request_url': 'http://localhost/pr1',
129 'pull_request_url': 'http://localhost/pr1',
126
130
127 'pr_comment_url': 'http://comment-url',
131 'pr_comment_url': 'http://comment-url',
128 'pr_comment_reply_url': 'http://comment-url#reply',
132 'pr_comment_reply_url': 'http://comment-url#reply',
129 'ancestor_commit_id': 'f39bd443',
133 'ancestor_commit_id': 'f39bd443',
130 'added_commits': commit_changes.added,
134 'added_commits': commit_changes.added,
131 'removed_commits': commit_changes.removed,
135 'removed_commits': commit_changes.removed,
132 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
136 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
133 'added_files': file_changes.added,
137 'added_files': file_changes.added,
134 'modified_files': file_changes.modified,
138 'modified_files': file_changes.modified,
135 'removed_files': file_changes.removed,
139 'removed_files': file_changes.removed,
136 }
140 }
137
141
138 subject, body, body_plaintext = EmailNotificationModel().render_email(
142 subject, body, body_plaintext = EmailNotificationModel().render_email(
139 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **kwargs)
143 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **kwargs)
140
144
141 # subject
145 # subject
142 assert subject == '@test_admin (RhodeCode Admin) updated pull request. !200: "Example Pull Request"'
146 assert subject == '@test_admin (RhodeCode Admin) updated pull request. !200: "Example Pull Request"'
143
147
144
148
145 @pytest.mark.parametrize('mention', [
149 @pytest.mark.parametrize('mention', [
146 True,
150 True,
147 False
151 False
148 ])
152 ])
149 @pytest.mark.parametrize('email_type', [
153 @pytest.mark.parametrize('email_type', [
150 EmailNotificationModel.TYPE_COMMIT_COMMENT,
154 EmailNotificationModel.TYPE_COMMIT_COMMENT,
151 EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
155 EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
152 ])
156 ])
153 def test_render_comment_subject_no_newlines(app, mention, email_type):
157 def test_render_comment_subject_no_newlines(app, mention, email_type, pr):
154 ref = collections.namedtuple(
158 ref = collections.namedtuple(
155 'Ref', 'name, type')('fxies123', 'book')
159 'Ref', 'name, type')('fxies123', 'book')
156
160
157 pr = collections.namedtuple('PullRequest',
161 pr = pr(ref)
158 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
159 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
160
162
161 source_repo = target_repo = collections.namedtuple(
163 source_repo = target_repo = collections.namedtuple(
162 'Repo', 'type, repo_name')('hg', 'pull_request_1')
164 'Repo', 'type, repo_name')('hg', 'pull_request_1')
163
165
164 kwargs = {
166 kwargs = {
165 'user': User.get_first_super_admin(),
167 'user': User.get_first_super_admin(),
166 'commit': AttributeDict(raw_id='a'*40, message='Commit message'),
168 'commit': AttributeDict(raw_id='a'*40, message='Commit message'),
167 'status_change': 'approved',
169 'status_change': 'approved',
168 'commit_target_repo_url': 'http://foo.example.com/#comment1',
170 'commit_target_repo_url': 'http://foo.example.com/#comment1',
169 'repo_name': 'test-repo',
171 'repo_name': 'test-repo',
170 'comment_file': 'test-file.py',
172 'comment_file': 'test-file.py',
171 'comment_line': 'n100',
173 'comment_line': 'n100',
172 'comment_type': 'note',
174 'comment_type': 'note',
173 'comment_id': 2048,
175 'comment_id': 2048,
174 'commit_comment_url': 'http://comment-url',
176 'commit_comment_url': 'http://comment-url',
175 'commit_comment_reply_url': 'http://comment-url/#Reply',
177 'commit_comment_reply_url': 'http://comment-url/#Reply',
176 'instance_url': 'http://rc-instance',
178 'instance_url': 'http://rc-instance',
177 'comment_body': 'hello world',
179 'comment_body': 'hello world',
178 'mention': mention,
180 'mention': mention,
179
181
180 'pr_comment_url': 'http://comment-url',
182 'pr_comment_url': 'http://comment-url',
181 'pr_comment_reply_url': 'http://comment-url/#Reply',
183 'pr_comment_reply_url': 'http://comment-url/#Reply',
182 'pull_request': pr,
184 'pull_request': pr,
183 'pull_request_commits': [],
185 'pull_request_commits': [],
184
186
185 'pull_request_target_repo': target_repo,
187 'pull_request_target_repo': target_repo,
186 'pull_request_target_repo_url': 'x',
188 'pull_request_target_repo_url': 'x',
187
189
188 'pull_request_source_repo': source_repo,
190 'pull_request_source_repo': source_repo,
189 'pull_request_source_repo_url': 'x',
191 'pull_request_source_repo_url': 'x',
190
192
191 'pull_request_url': 'http://code.rc.com/_pr/123'
193 'pull_request_url': 'http://code.rc.com/_pr/123'
192 }
194 }
193 subject, body, body_plaintext = EmailNotificationModel().render_email(email_type, **kwargs)
195 subject, body, body_plaintext = EmailNotificationModel().render_email(email_type, **kwargs)
194
196
195 assert '\n' not in subject
197 assert '\n' not in subject
General Comments 0
You need to be logged in to leave comments. Login now