##// 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 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import mock
21 21 import pytest
22 22
23 23 import rhodecode
24 24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 25 from rhodecode.lib.vcs.nodes import FileNode
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 28 from rhodecode.model.db import (
29 29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
30 30 from rhodecode.model.meta import Session
31 31 from rhodecode.model.pull_request import PullRequestModel
32 32 from rhodecode.model.user import UserModel
33 33 from rhodecode.model.comment import CommentsModel
34 34 from rhodecode.tests import (
35 35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
36 36
37 37
38 38 def route_path(name, params=None, **kwargs):
39 39 import urllib
40 40
41 41 base_url = {
42 42 'repo_changelog': '/{repo_name}/changelog',
43 43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
44 44 'repo_commits': '/{repo_name}/commits',
45 45 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
46 46 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
47 47 'pullrequest_show_all': '/{repo_name}/pull-request',
48 48 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
49 49 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
50 50 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
51 51 'pullrequest_new': '/{repo_name}/pull-request/new',
52 52 'pullrequest_create': '/{repo_name}/pull-request/create',
53 53 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
54 54 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
55 55 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
56 56 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
57 57 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
58 58 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
59 59 }[name].format(**kwargs)
60 60
61 61 if params:
62 62 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
63 63 return base_url
64 64
65 65
66 66 @pytest.mark.usefixtures('app', 'autologin_user')
67 67 @pytest.mark.backends("git", "hg")
68 68 class TestPullrequestsView(object):
69 69
70 70 def test_index(self, backend):
71 71 self.app.get(route_path(
72 72 'pullrequest_new',
73 73 repo_name=backend.repo_name))
74 74
75 75 def test_option_menu_create_pull_request_exists(self, backend):
76 76 repo_name = backend.repo_name
77 77 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
78 78
79 79 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
80 80 'pullrequest_new', repo_name=repo_name)
81 81 response.mustcontain(create_pr_link)
82 82
83 83 def test_create_pr_form_with_raw_commit_id(self, backend):
84 84 repo = backend.repo
85 85
86 86 self.app.get(
87 87 route_path('pullrequest_new', repo_name=repo.repo_name,
88 88 commit=repo.get_commit().raw_id),
89 89 status=200)
90 90
91 91 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
92 92 @pytest.mark.parametrize('range_diff', ["0", "1"])
93 93 def test_show(self, pr_util, pr_merge_enabled, range_diff):
94 94 pull_request = pr_util.create_pull_request(
95 95 mergeable=pr_merge_enabled, enable_notifications=False)
96 96
97 97 response = self.app.get(route_path(
98 98 'pullrequest_show',
99 99 repo_name=pull_request.target_repo.scm_instance().name,
100 100 pull_request_id=pull_request.pull_request_id,
101 101 params={'range-diff': range_diff}))
102 102
103 103 for commit_id in pull_request.revisions:
104 104 response.mustcontain(commit_id)
105 105
106 106 response.mustcontain(pull_request.target_ref_parts.type)
107 107 response.mustcontain(pull_request.target_ref_parts.name)
108 108
109 109 response.mustcontain('class="pull-request-merge"')
110 110
111 111 if pr_merge_enabled:
112 112 response.mustcontain('Pull request reviewer approval is pending')
113 113 else:
114 114 response.mustcontain('Server-side pull request merging is disabled.')
115 115
116 116 if range_diff == "1":
117 117 response.mustcontain('Turn off: Show the diff as commit range')
118 118
119 119 def test_show_versions_of_pr(self, backend, csrf_token):
120 120 commits = [
121 121 {'message': 'initial-commit',
122 122 'added': [FileNode('test-file.txt', 'LINE1\n')]},
123 123
124 124 {'message': 'commit-1',
125 125 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
126 126 # Above is the initial version of PR that changes a single line
127 127
128 128 # from now on we'll add 3x commit adding a nother line on each step
129 129 {'message': 'commit-2',
130 130 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
131 131
132 132 {'message': 'commit-3',
133 133 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
134 134
135 135 {'message': 'commit-4',
136 136 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
137 137 ]
138 138
139 139 commit_ids = backend.create_master_repo(commits)
140 140 target = backend.create_repo(heads=['initial-commit'])
141 141 source = backend.create_repo(heads=['commit-1'])
142 142 source_repo_name = source.repo_name
143 143 target_repo_name = target.repo_name
144 144
145 145 target_ref = 'branch:{branch}:{commit_id}'.format(
146 146 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
147 147 source_ref = 'branch:{branch}:{commit_id}'.format(
148 148 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
149 149
150 150 response = self.app.post(
151 151 route_path('pullrequest_create', repo_name=source.repo_name),
152 152 [
153 153 ('source_repo', source_repo_name),
154 154 ('source_ref', source_ref),
155 155 ('target_repo', target_repo_name),
156 156 ('target_ref', target_ref),
157 157 ('common_ancestor', commit_ids['initial-commit']),
158 158 ('pullrequest_title', 'Title'),
159 159 ('pullrequest_desc', 'Description'),
160 160 ('description_renderer', 'markdown'),
161 161 ('__start__', 'review_members:sequence'),
162 162 ('__start__', 'reviewer:mapping'),
163 163 ('user_id', '1'),
164 164 ('__start__', 'reasons:sequence'),
165 165 ('reason', 'Some reason'),
166 166 ('__end__', 'reasons:sequence'),
167 167 ('__start__', 'rules:sequence'),
168 168 ('__end__', 'rules:sequence'),
169 169 ('mandatory', 'False'),
170 170 ('__end__', 'reviewer:mapping'),
171 171 ('__end__', 'review_members:sequence'),
172 172 ('__start__', 'revisions:sequence'),
173 173 ('revisions', commit_ids['commit-1']),
174 174 ('__end__', 'revisions:sequence'),
175 175 ('user', ''),
176 176 ('csrf_token', csrf_token),
177 177 ],
178 178 status=302)
179 179
180 180 location = response.headers['Location']
181 181
182 182 pull_request_id = location.rsplit('/', 1)[1]
183 183 assert pull_request_id != 'new'
184 184 pull_request = PullRequest.get(int(pull_request_id))
185 185
186 186 pull_request_id = pull_request.pull_request_id
187 187
188 188 # Show initial version of PR
189 189 response = self.app.get(
190 190 route_path('pullrequest_show',
191 191 repo_name=target_repo_name,
192 192 pull_request_id=pull_request_id))
193 193
194 194 response.mustcontain('commit-1')
195 195 response.mustcontain(no=['commit-2'])
196 196 response.mustcontain(no=['commit-3'])
197 197 response.mustcontain(no=['commit-4'])
198 198
199 199 response.mustcontain('cb-addition"></span><span>LINE2</span>')
200 200 response.mustcontain(no=['LINE3'])
201 201 response.mustcontain(no=['LINE4'])
202 202 response.mustcontain(no=['LINE5'])
203 203
204 204 # update PR #1
205 205 source_repo = Repository.get_by_repo_name(source_repo_name)
206 206 backend.pull_heads(source_repo, heads=['commit-2'])
207 207 response = self.app.post(
208 208 route_path('pullrequest_update',
209 209 repo_name=target_repo_name, pull_request_id=pull_request_id),
210 210 params={'update_commits': 'true', 'csrf_token': csrf_token})
211 211
212 212 # update PR #2
213 213 source_repo = Repository.get_by_repo_name(source_repo_name)
214 214 backend.pull_heads(source_repo, heads=['commit-3'])
215 215 response = self.app.post(
216 216 route_path('pullrequest_update',
217 217 repo_name=target_repo_name, pull_request_id=pull_request_id),
218 218 params={'update_commits': 'true', 'csrf_token': csrf_token})
219 219
220 220 # update PR #3
221 221 source_repo = Repository.get_by_repo_name(source_repo_name)
222 222 backend.pull_heads(source_repo, heads=['commit-4'])
223 223 response = self.app.post(
224 224 route_path('pullrequest_update',
225 225 repo_name=target_repo_name, pull_request_id=pull_request_id),
226 226 params={'update_commits': 'true', 'csrf_token': csrf_token})
227 227
228 228 # Show final version !
229 229 response = self.app.get(
230 230 route_path('pullrequest_show',
231 231 repo_name=target_repo_name,
232 232 pull_request_id=pull_request_id))
233 233
234 234 # 3 updates, and the latest == 4
235 235 response.mustcontain('4 versions available for this pull request')
236 236 response.mustcontain(no=['rhodecode diff rendering error'])
237 237
238 238 # initial show must have 3 commits, and 3 adds
239 239 response.mustcontain('commit-1')
240 240 response.mustcontain('commit-2')
241 241 response.mustcontain('commit-3')
242 242 response.mustcontain('commit-4')
243 243
244 244 response.mustcontain('cb-addition"></span><span>LINE2</span>')
245 245 response.mustcontain('cb-addition"></span><span>LINE3</span>')
246 246 response.mustcontain('cb-addition"></span><span>LINE4</span>')
247 247 response.mustcontain('cb-addition"></span><span>LINE5</span>')
248 248
249 249 # fetch versions
250 250 pr = PullRequest.get(pull_request_id)
251 251 versions = [x.pull_request_version_id for x in pr.versions.all()]
252 252 assert len(versions) == 3
253 253
254 254 # show v1,v2,v3,v4
255 255 def cb_line(text):
256 256 return 'cb-addition"></span><span>{}</span>'.format(text)
257 257
258 258 def cb_context(text):
259 259 return '<span class="cb-code"><span class="cb-action cb-context">' \
260 260 '</span><span>{}</span></span>'.format(text)
261 261
262 262 commit_tests = {
263 263 # in response, not in response
264 264 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
265 265 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
266 266 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
267 267 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
268 268 }
269 269 diff_tests = {
270 270 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
271 271 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
272 272 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
273 273 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
274 274 }
275 275 for idx, ver in enumerate(versions, 1):
276 276
277 277 response = self.app.get(
278 278 route_path('pullrequest_show',
279 279 repo_name=target_repo_name,
280 280 pull_request_id=pull_request_id,
281 281 params={'version': ver}))
282 282
283 283 response.mustcontain(no=['rhodecode diff rendering error'])
284 284 response.mustcontain('Showing changes at v{}'.format(idx))
285 285
286 286 yes, no = commit_tests[idx]
287 287 for y in yes:
288 288 response.mustcontain(y)
289 289 for n in no:
290 290 response.mustcontain(no=n)
291 291
292 292 yes, no = diff_tests[idx]
293 293 for y in yes:
294 294 response.mustcontain(cb_line(y))
295 295 for n in no:
296 296 response.mustcontain(no=n)
297 297
298 298 # show diff between versions
299 299 diff_compare_tests = {
300 300 1: (['LINE3'], ['LINE1', 'LINE2']),
301 301 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
302 302 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
303 303 }
304 304 for idx, ver in enumerate(versions, 1):
305 305 adds, context = diff_compare_tests[idx]
306 306
307 307 to_ver = ver+1
308 308 if idx == 3:
309 309 to_ver = 'latest'
310 310
311 311 response = self.app.get(
312 312 route_path('pullrequest_show',
313 313 repo_name=target_repo_name,
314 314 pull_request_id=pull_request_id,
315 315 params={'from_version': versions[0], 'version': to_ver}))
316 316
317 317 response.mustcontain(no=['rhodecode diff rendering error'])
318 318
319 319 for a in adds:
320 320 response.mustcontain(cb_line(a))
321 321 for c in context:
322 322 response.mustcontain(cb_context(c))
323 323
324 324 # test version v2 -> v3
325 325 response = self.app.get(
326 326 route_path('pullrequest_show',
327 327 repo_name=target_repo_name,
328 328 pull_request_id=pull_request_id,
329 329 params={'from_version': versions[1], 'version': versions[2]}))
330 330
331 331 response.mustcontain(cb_context('LINE1'))
332 332 response.mustcontain(cb_context('LINE2'))
333 333 response.mustcontain(cb_context('LINE3'))
334 334 response.mustcontain(cb_line('LINE4'))
335 335
336 336 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
337 337 # Logout
338 338 response = self.app.post(
339 339 h.route_path('logout'),
340 340 params={'csrf_token': csrf_token})
341 341 # Login as regular user
342 342 response = self.app.post(h.route_path('login'),
343 343 {'username': TEST_USER_REGULAR_LOGIN,
344 344 'password': 'test12'})
345 345
346 346 pull_request = pr_util.create_pull_request(
347 347 author=TEST_USER_REGULAR_LOGIN)
348 348
349 349 response = self.app.get(route_path(
350 350 'pullrequest_show',
351 351 repo_name=pull_request.target_repo.scm_instance().name,
352 352 pull_request_id=pull_request.pull_request_id))
353 353
354 354 response.mustcontain('Server-side pull request merging is disabled.')
355 355
356 356 assert_response = response.assert_response()
357 357 # for regular user without a merge permissions, we don't see it
358 358 assert_response.no_element_exists('#close-pull-request-action')
359 359
360 360 user_util.grant_user_permission_to_repo(
361 361 pull_request.target_repo,
362 362 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
363 363 'repository.write')
364 364 response = self.app.get(route_path(
365 365 'pullrequest_show',
366 366 repo_name=pull_request.target_repo.scm_instance().name,
367 367 pull_request_id=pull_request.pull_request_id))
368 368
369 369 response.mustcontain('Server-side pull request merging is disabled.')
370 370
371 371 assert_response = response.assert_response()
372 372 # now regular user has a merge permissions, we have CLOSE button
373 373 assert_response.one_element_exists('#close-pull-request-action')
374 374
375 375 def test_show_invalid_commit_id(self, pr_util):
376 376 # Simulating invalid revisions which will cause a lookup error
377 377 pull_request = pr_util.create_pull_request()
378 378 pull_request.revisions = ['invalid']
379 379 Session().add(pull_request)
380 380 Session().commit()
381 381
382 382 response = self.app.get(route_path(
383 383 'pullrequest_show',
384 384 repo_name=pull_request.target_repo.scm_instance().name,
385 385 pull_request_id=pull_request.pull_request_id))
386 386
387 387 for commit_id in pull_request.revisions:
388 388 response.mustcontain(commit_id)
389 389
390 390 def test_show_invalid_source_reference(self, pr_util):
391 391 pull_request = pr_util.create_pull_request()
392 392 pull_request.source_ref = 'branch:b:invalid'
393 393 Session().add(pull_request)
394 394 Session().commit()
395 395
396 396 self.app.get(route_path(
397 397 'pullrequest_show',
398 398 repo_name=pull_request.target_repo.scm_instance().name,
399 399 pull_request_id=pull_request.pull_request_id))
400 400
401 401 def test_edit_title_description(self, pr_util, csrf_token):
402 402 pull_request = pr_util.create_pull_request()
403 403 pull_request_id = pull_request.pull_request_id
404 404
405 405 response = self.app.post(
406 406 route_path('pullrequest_update',
407 407 repo_name=pull_request.target_repo.repo_name,
408 408 pull_request_id=pull_request_id),
409 409 params={
410 410 'edit_pull_request': 'true',
411 411 'title': 'New title',
412 412 'description': 'New description',
413 413 'csrf_token': csrf_token})
414 414
415 415 assert_session_flash(
416 416 response, u'Pull request title & description updated.',
417 417 category='success')
418 418
419 419 pull_request = PullRequest.get(pull_request_id)
420 420 assert pull_request.title == 'New title'
421 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 444 def test_edit_title_description_closed(self, pr_util, csrf_token):
424 445 pull_request = pr_util.create_pull_request()
425 446 pull_request_id = pull_request.pull_request_id
426 447 repo_name = pull_request.target_repo.repo_name
427 448 pr_util.close()
428 449
429 450 response = self.app.post(
430 451 route_path('pullrequest_update',
431 452 repo_name=repo_name, pull_request_id=pull_request_id),
432 453 params={
433 454 'edit_pull_request': 'true',
434 455 'title': 'New title',
435 456 'description': 'New description',
436 457 'csrf_token': csrf_token}, status=200)
437 458 assert_session_flash(
438 459 response, u'Cannot update closed pull requests.',
439 460 category='error')
440 461
441 462 def test_update_invalid_source_reference(self, pr_util, csrf_token):
442 463 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
443 464
444 465 pull_request = pr_util.create_pull_request()
445 466 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
446 467 Session().add(pull_request)
447 468 Session().commit()
448 469
449 470 pull_request_id = pull_request.pull_request_id
450 471
451 472 response = self.app.post(
452 473 route_path('pullrequest_update',
453 474 repo_name=pull_request.target_repo.repo_name,
454 475 pull_request_id=pull_request_id),
455 476 params={'update_commits': 'true', 'csrf_token': csrf_token})
456 477
457 478 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
458 479 UpdateFailureReason.MISSING_SOURCE_REF])
459 480 assert_session_flash(response, expected_msg, category='error')
460 481
461 482 def test_missing_target_reference(self, pr_util, csrf_token):
462 483 from rhodecode.lib.vcs.backends.base import MergeFailureReason
463 484 pull_request = pr_util.create_pull_request(
464 485 approved=True, mergeable=True)
465 486 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
466 487 pull_request.target_ref = unicode_reference
467 488 Session().add(pull_request)
468 489 Session().commit()
469 490
470 491 pull_request_id = pull_request.pull_request_id
471 492 pull_request_url = route_path(
472 493 'pullrequest_show',
473 494 repo_name=pull_request.target_repo.repo_name,
474 495 pull_request_id=pull_request_id)
475 496
476 497 response = self.app.get(pull_request_url)
477 498 target_ref_id = 'invalid-branch'
478 499 merge_resp = MergeResponse(
479 500 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
480 501 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
481 502 response.assert_response().element_contains(
482 503 'div[data-role="merge-message"]', merge_resp.merge_status_message)
483 504
484 505 def test_comment_and_close_pull_request_custom_message_approved(
485 506 self, pr_util, csrf_token, xhr_header):
486 507
487 508 pull_request = pr_util.create_pull_request(approved=True)
488 509 pull_request_id = pull_request.pull_request_id
489 510 author = pull_request.user_id
490 511 repo = pull_request.target_repo.repo_id
491 512
492 513 self.app.post(
493 514 route_path('pullrequest_comment_create',
494 515 repo_name=pull_request.target_repo.scm_instance().name,
495 516 pull_request_id=pull_request_id),
496 517 params={
497 518 'close_pull_request': '1',
498 519 'text': 'Closing a PR',
499 520 'csrf_token': csrf_token},
500 521 extra_environ=xhr_header,)
501 522
502 523 journal = UserLog.query()\
503 524 .filter(UserLog.user_id == author)\
504 525 .filter(UserLog.repository_id == repo) \
505 526 .order_by(UserLog.user_log_id.asc()) \
506 527 .all()
507 528 assert journal[-1].action == 'repo.pull_request.close'
508 529
509 530 pull_request = PullRequest.get(pull_request_id)
510 531 assert pull_request.is_closed()
511 532
512 533 status = ChangesetStatusModel().get_status(
513 534 pull_request.source_repo, pull_request=pull_request)
514 535 assert status == ChangesetStatus.STATUS_APPROVED
515 536 comments = ChangesetComment().query() \
516 537 .filter(ChangesetComment.pull_request == pull_request) \
517 538 .order_by(ChangesetComment.comment_id.asc())\
518 539 .all()
519 540 assert comments[-1].text == 'Closing a PR'
520 541
521 542 def test_comment_force_close_pull_request_rejected(
522 543 self, pr_util, csrf_token, xhr_header):
523 544 pull_request = pr_util.create_pull_request()
524 545 pull_request_id = pull_request.pull_request_id
525 546 PullRequestModel().update_reviewers(
526 547 pull_request_id, [
527 548 (1, ['reason'], False, 'reviewer', []),
528 549 (2, ['reason2'], False, 'reviewer', [])],
529 550 pull_request.author)
530 551 author = pull_request.user_id
531 552 repo = pull_request.target_repo.repo_id
532 553
533 554 self.app.post(
534 555 route_path('pullrequest_comment_create',
535 556 repo_name=pull_request.target_repo.scm_instance().name,
536 557 pull_request_id=pull_request_id),
537 558 params={
538 559 'close_pull_request': '1',
539 560 'csrf_token': csrf_token},
540 561 extra_environ=xhr_header)
541 562
542 563 pull_request = PullRequest.get(pull_request_id)
543 564
544 565 journal = UserLog.query()\
545 566 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
546 567 .order_by(UserLog.user_log_id.asc()) \
547 568 .all()
548 569 assert journal[-1].action == 'repo.pull_request.close'
549 570
550 571 # check only the latest status, not the review status
551 572 status = ChangesetStatusModel().get_status(
552 573 pull_request.source_repo, pull_request=pull_request)
553 574 assert status == ChangesetStatus.STATUS_REJECTED
554 575
555 576 def test_comment_and_close_pull_request(
556 577 self, pr_util, csrf_token, xhr_header):
557 578 pull_request = pr_util.create_pull_request()
558 579 pull_request_id = pull_request.pull_request_id
559 580
560 581 response = self.app.post(
561 582 route_path('pullrequest_comment_create',
562 583 repo_name=pull_request.target_repo.scm_instance().name,
563 584 pull_request_id=pull_request.pull_request_id),
564 585 params={
565 586 'close_pull_request': 'true',
566 587 'csrf_token': csrf_token},
567 588 extra_environ=xhr_header)
568 589
569 590 assert response.json
570 591
571 592 pull_request = PullRequest.get(pull_request_id)
572 593 assert pull_request.is_closed()
573 594
574 595 # check only the latest status, not the review status
575 596 status = ChangesetStatusModel().get_status(
576 597 pull_request.source_repo, pull_request=pull_request)
577 598 assert status == ChangesetStatus.STATUS_REJECTED
578 599
579 600 def test_comment_and_close_pull_request_try_edit_comment(
580 601 self, pr_util, csrf_token, xhr_header
581 602 ):
582 603 pull_request = pr_util.create_pull_request()
583 604 pull_request_id = pull_request.pull_request_id
584 605 target_scm = pull_request.target_repo.scm_instance()
585 606 target_scm_name = target_scm.name
586 607
587 608 response = self.app.post(
588 609 route_path(
589 610 'pullrequest_comment_create',
590 611 repo_name=target_scm_name,
591 612 pull_request_id=pull_request_id,
592 613 ),
593 614 params={
594 615 'close_pull_request': 'true',
595 616 'csrf_token': csrf_token,
596 617 },
597 618 extra_environ=xhr_header)
598 619
599 620 assert response.json
600 621
601 622 pull_request = PullRequest.get(pull_request_id)
602 623 target_scm = pull_request.target_repo.scm_instance()
603 624 target_scm_name = target_scm.name
604 625 assert pull_request.is_closed()
605 626
606 627 # check only the latest status, not the review status
607 628 status = ChangesetStatusModel().get_status(
608 629 pull_request.source_repo, pull_request=pull_request)
609 630 assert status == ChangesetStatus.STATUS_REJECTED
610 631
611 632 for comment_id in response.json.keys():
612 633 test_text = 'test'
613 634 response = self.app.post(
614 635 route_path(
615 636 'pullrequest_comment_edit',
616 637 repo_name=target_scm_name,
617 638 pull_request_id=pull_request_id,
618 639 comment_id=comment_id,
619 640 ),
620 641 extra_environ=xhr_header,
621 642 params={
622 643 'csrf_token': csrf_token,
623 644 'text': test_text,
624 645 },
625 646 status=403,
626 647 )
627 648 assert response.status_int == 403
628 649
629 650 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
630 651 pull_request = pr_util.create_pull_request()
631 652 target_scm = pull_request.target_repo.scm_instance()
632 653 target_scm_name = target_scm.name
633 654
634 655 response = self.app.post(
635 656 route_path(
636 657 'pullrequest_comment_create',
637 658 repo_name=target_scm_name,
638 659 pull_request_id=pull_request.pull_request_id),
639 660 params={
640 661 'csrf_token': csrf_token,
641 662 'text': 'init',
642 663 },
643 664 extra_environ=xhr_header,
644 665 )
645 666 assert response.json
646 667
647 668 for comment_id in response.json.keys():
648 669 assert comment_id
649 670 test_text = 'test'
650 671 self.app.post(
651 672 route_path(
652 673 'pullrequest_comment_edit',
653 674 repo_name=target_scm_name,
654 675 pull_request_id=pull_request.pull_request_id,
655 676 comment_id=comment_id,
656 677 ),
657 678 extra_environ=xhr_header,
658 679 params={
659 680 'csrf_token': csrf_token,
660 681 'text': test_text,
661 682 'version': '0',
662 683 },
663 684
664 685 )
665 686 text_form_db = ChangesetComment.query().filter(
666 687 ChangesetComment.comment_id == comment_id).first().text
667 688 assert test_text == text_form_db
668 689
669 690 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
670 691 pull_request = pr_util.create_pull_request()
671 692 target_scm = pull_request.target_repo.scm_instance()
672 693 target_scm_name = target_scm.name
673 694
674 695 response = self.app.post(
675 696 route_path(
676 697 'pullrequest_comment_create',
677 698 repo_name=target_scm_name,
678 699 pull_request_id=pull_request.pull_request_id),
679 700 params={
680 701 'csrf_token': csrf_token,
681 702 'text': 'init',
682 703 },
683 704 extra_environ=xhr_header,
684 705 )
685 706 assert response.json
686 707
687 708 for comment_id in response.json.keys():
688 709 test_text = 'init'
689 710 response = self.app.post(
690 711 route_path(
691 712 'pullrequest_comment_edit',
692 713 repo_name=target_scm_name,
693 714 pull_request_id=pull_request.pull_request_id,
694 715 comment_id=comment_id,
695 716 ),
696 717 extra_environ=xhr_header,
697 718 params={
698 719 'csrf_token': csrf_token,
699 720 'text': test_text,
700 721 'version': '0',
701 722 },
702 723 status=404,
703 724
704 725 )
705 726 assert response.status_int == 404
706 727
707 728 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
708 729 pull_request = pr_util.create_pull_request()
709 730 target_scm = pull_request.target_repo.scm_instance()
710 731 target_scm_name = target_scm.name
711 732
712 733 response = self.app.post(
713 734 route_path(
714 735 'pullrequest_comment_create',
715 736 repo_name=target_scm_name,
716 737 pull_request_id=pull_request.pull_request_id),
717 738 params={
718 739 'csrf_token': csrf_token,
719 740 'text': 'init',
720 741 },
721 742 extra_environ=xhr_header,
722 743 )
723 744 assert response.json
724 745 for comment_id in response.json.keys():
725 746 test_text = 'test'
726 747 self.app.post(
727 748 route_path(
728 749 'pullrequest_comment_edit',
729 750 repo_name=target_scm_name,
730 751 pull_request_id=pull_request.pull_request_id,
731 752 comment_id=comment_id,
732 753 ),
733 754 extra_environ=xhr_header,
734 755 params={
735 756 'csrf_token': csrf_token,
736 757 'text': test_text,
737 758 'version': '0',
738 759 },
739 760
740 761 )
741 762 test_text_v2 = 'test_v2'
742 763 response = self.app.post(
743 764 route_path(
744 765 'pullrequest_comment_edit',
745 766 repo_name=target_scm_name,
746 767 pull_request_id=pull_request.pull_request_id,
747 768 comment_id=comment_id,
748 769 ),
749 770 extra_environ=xhr_header,
750 771 params={
751 772 'csrf_token': csrf_token,
752 773 'text': test_text_v2,
753 774 'version': '0',
754 775 },
755 776 status=409,
756 777 )
757 778 assert response.status_int == 409
758 779
759 780 text_form_db = ChangesetComment.query().filter(
760 781 ChangesetComment.comment_id == comment_id).first().text
761 782
762 783 assert test_text == text_form_db
763 784 assert test_text_v2 != text_form_db
764 785
765 786 def test_comment_and_comment_edit_permissions_forbidden(
766 787 self, autologin_regular_user, user_regular, user_admin, pr_util,
767 788 csrf_token, xhr_header):
768 789 pull_request = pr_util.create_pull_request(
769 790 author=user_admin.username, enable_notifications=False)
770 791 comment = CommentsModel().create(
771 792 text='test',
772 793 repo=pull_request.target_repo.scm_instance().name,
773 794 user=user_admin,
774 795 pull_request=pull_request,
775 796 )
776 797 response = self.app.post(
777 798 route_path(
778 799 'pullrequest_comment_edit',
779 800 repo_name=pull_request.target_repo.scm_instance().name,
780 801 pull_request_id=pull_request.pull_request_id,
781 802 comment_id=comment.comment_id,
782 803 ),
783 804 extra_environ=xhr_header,
784 805 params={
785 806 'csrf_token': csrf_token,
786 807 'text': 'test_text',
787 808 },
788 809 status=403,
789 810 )
790 811 assert response.status_int == 403
791 812
792 813 def test_create_pull_request(self, backend, csrf_token):
793 814 commits = [
794 815 {'message': 'ancestor'},
795 816 {'message': 'change'},
796 817 {'message': 'change2'},
797 818 ]
798 819 commit_ids = backend.create_master_repo(commits)
799 820 target = backend.create_repo(heads=['ancestor'])
800 821 source = backend.create_repo(heads=['change2'])
801 822
802 823 response = self.app.post(
803 824 route_path('pullrequest_create', repo_name=source.repo_name),
804 825 [
805 826 ('source_repo', source.repo_name),
806 827 ('source_ref', 'branch:default:' + commit_ids['change2']),
807 828 ('target_repo', target.repo_name),
808 829 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
809 830 ('common_ancestor', commit_ids['ancestor']),
810 831 ('pullrequest_title', 'Title'),
811 832 ('pullrequest_desc', 'Description'),
812 833 ('description_renderer', 'markdown'),
813 834 ('__start__', 'review_members:sequence'),
814 835 ('__start__', 'reviewer:mapping'),
815 836 ('user_id', '1'),
816 837 ('__start__', 'reasons:sequence'),
817 838 ('reason', 'Some reason'),
818 839 ('__end__', 'reasons:sequence'),
819 840 ('__start__', 'rules:sequence'),
820 841 ('__end__', 'rules:sequence'),
821 842 ('mandatory', 'False'),
822 843 ('__end__', 'reviewer:mapping'),
823 844 ('__end__', 'review_members:sequence'),
824 845 ('__start__', 'revisions:sequence'),
825 846 ('revisions', commit_ids['change']),
826 847 ('revisions', commit_ids['change2']),
827 848 ('__end__', 'revisions:sequence'),
828 849 ('user', ''),
829 850 ('csrf_token', csrf_token),
830 851 ],
831 852 status=302)
832 853
833 854 location = response.headers['Location']
834 855 pull_request_id = location.rsplit('/', 1)[1]
835 856 assert pull_request_id != 'new'
836 857 pull_request = PullRequest.get(int(pull_request_id))
837 858
838 859 # check that we have now both revisions
839 860 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
840 861 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
841 862 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
842 863 assert pull_request.target_ref == expected_target_ref
843 864
844 865 def test_reviewer_notifications(self, backend, csrf_token):
845 866 # We have to use the app.post for this test so it will create the
846 867 # notifications properly with the new PR
847 868 commits = [
848 869 {'message': 'ancestor',
849 870 'added': [FileNode('file_A', content='content_of_ancestor')]},
850 871 {'message': 'change',
851 872 'added': [FileNode('file_a', content='content_of_change')]},
852 873 {'message': 'change-child'},
853 874 {'message': 'ancestor-child', 'parents': ['ancestor'],
854 875 'added': [
855 876 FileNode('file_B', content='content_of_ancestor_child')]},
856 877 {'message': 'ancestor-child-2'},
857 878 ]
858 879 commit_ids = backend.create_master_repo(commits)
859 880 target = backend.create_repo(heads=['ancestor-child'])
860 881 source = backend.create_repo(heads=['change'])
861 882
862 883 response = self.app.post(
863 884 route_path('pullrequest_create', repo_name=source.repo_name),
864 885 [
865 886 ('source_repo', source.repo_name),
866 887 ('source_ref', 'branch:default:' + commit_ids['change']),
867 888 ('target_repo', target.repo_name),
868 889 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
869 890 ('common_ancestor', commit_ids['ancestor']),
870 891 ('pullrequest_title', 'Title'),
871 892 ('pullrequest_desc', 'Description'),
872 893 ('description_renderer', 'markdown'),
873 894 ('__start__', 'review_members:sequence'),
874 895 ('__start__', 'reviewer:mapping'),
875 896 ('user_id', '2'),
876 897 ('__start__', 'reasons:sequence'),
877 898 ('reason', 'Some reason'),
878 899 ('__end__', 'reasons:sequence'),
879 900 ('__start__', 'rules:sequence'),
880 901 ('__end__', 'rules:sequence'),
881 902 ('mandatory', 'False'),
882 903 ('__end__', 'reviewer:mapping'),
883 904 ('__end__', 'review_members:sequence'),
884 905 ('__start__', 'revisions:sequence'),
885 906 ('revisions', commit_ids['change']),
886 907 ('__end__', 'revisions:sequence'),
887 908 ('user', ''),
888 909 ('csrf_token', csrf_token),
889 910 ],
890 911 status=302)
891 912
892 913 location = response.headers['Location']
893 914
894 915 pull_request_id = location.rsplit('/', 1)[1]
895 916 assert pull_request_id != 'new'
896 917 pull_request = PullRequest.get(int(pull_request_id))
897 918
898 919 # Check that a notification was made
899 920 notifications = Notification.query()\
900 921 .filter(Notification.created_by == pull_request.author.user_id,
901 922 Notification.type_ == Notification.TYPE_PULL_REQUEST,
902 923 Notification.subject.contains(
903 924 "requested a pull request review. !%s" % pull_request_id))
904 925 assert len(notifications.all()) == 1
905 926
906 927 # Change reviewers and check that a notification was made
907 928 PullRequestModel().update_reviewers(
908 929 pull_request.pull_request_id, [
909 930 (1, [], False, 'reviewer', [])
910 931 ],
911 932 pull_request.author)
912 933 assert len(notifications.all()) == 2
913 934
914 935 def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token):
915 936 commits = [
916 937 {'message': 'ancestor',
917 938 'added': [FileNode('file_A', content='content_of_ancestor')]},
918 939 {'message': 'change',
919 940 'added': [FileNode('file_a', content='content_of_change')]},
920 941 {'message': 'change-child'},
921 942 {'message': 'ancestor-child', 'parents': ['ancestor'],
922 943 'added': [
923 944 FileNode('file_B', content='content_of_ancestor_child')]},
924 945 {'message': 'ancestor-child-2'},
925 946 ]
926 947 commit_ids = backend.create_master_repo(commits)
927 948 target = backend.create_repo(heads=['ancestor-child'])
928 949 source = backend.create_repo(heads=['change'])
929 950
930 951 response = self.app.post(
931 952 route_path('pullrequest_create', repo_name=source.repo_name),
932 953 [
933 954 ('source_repo', source.repo_name),
934 955 ('source_ref', 'branch:default:' + commit_ids['change']),
935 956 ('target_repo', target.repo_name),
936 957 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
937 958 ('common_ancestor', commit_ids['ancestor']),
938 959 ('pullrequest_title', 'Title'),
939 960 ('pullrequest_desc', 'Description'),
940 961 ('description_renderer', 'markdown'),
941 962 ('__start__', 'review_members:sequence'),
942 963 ('__start__', 'reviewer:mapping'),
943 964 ('user_id', '1'),
944 965 ('__start__', 'reasons:sequence'),
945 966 ('reason', 'Some reason'),
946 967 ('__end__', 'reasons:sequence'),
947 968 ('__start__', 'rules:sequence'),
948 969 ('__end__', 'rules:sequence'),
949 970 ('mandatory', 'False'),
950 971 ('__end__', 'reviewer:mapping'),
951 972 ('__end__', 'review_members:sequence'),
952 973 ('__start__', 'revisions:sequence'),
953 974 ('revisions', commit_ids['change']),
954 975 ('__end__', 'revisions:sequence'),
955 976 ('user', ''),
956 977 ('csrf_token', csrf_token),
957 978 ],
958 979 status=302)
959 980
960 981 location = response.headers['Location']
961 982
962 983 pull_request_id = location.rsplit('/', 1)[1]
963 984 assert pull_request_id != 'new'
964 985 pull_request = PullRequest.get(int(pull_request_id))
965 986
966 987 # target_ref has to point to the ancestor's commit_id in order to
967 988 # show the correct diff
968 989 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
969 990 assert pull_request.target_ref == expected_target_ref
970 991
971 992 # Check generated diff contents
972 993 response = response.follow()
973 994 response.mustcontain(no=['content_of_ancestor'])
974 995 response.mustcontain(no=['content_of_ancestor-child'])
975 996 response.mustcontain('content_of_change')
976 997
977 998 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
978 999 # Clear any previous calls to rcextensions
979 1000 rhodecode.EXTENSIONS.calls.clear()
980 1001
981 1002 pull_request = pr_util.create_pull_request(
982 1003 approved=True, mergeable=True)
983 1004 pull_request_id = pull_request.pull_request_id
984 1005 repo_name = pull_request.target_repo.scm_instance().name,
985 1006
986 1007 url = route_path('pullrequest_merge',
987 1008 repo_name=str(repo_name[0]),
988 1009 pull_request_id=pull_request_id)
989 1010 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
990 1011
991 1012 pull_request = PullRequest.get(pull_request_id)
992 1013
993 1014 assert response.status_int == 200
994 1015 assert pull_request.is_closed()
995 1016 assert_pull_request_status(
996 1017 pull_request, ChangesetStatus.STATUS_APPROVED)
997 1018
998 1019 # Check the relevant log entries were added
999 1020 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
1000 1021 actions = [log.action for log in user_logs]
1001 1022 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
1002 1023 expected_actions = [
1003 1024 u'repo.pull_request.close',
1004 1025 u'repo.pull_request.merge',
1005 1026 u'repo.pull_request.comment.create'
1006 1027 ]
1007 1028 assert actions == expected_actions
1008 1029
1009 1030 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
1010 1031 actions = [log for log in user_logs]
1011 1032 assert actions[-1].action == 'user.push'
1012 1033 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
1013 1034
1014 1035 # Check post_push rcextension was really executed
1015 1036 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
1016 1037 assert len(push_calls) == 1
1017 1038 unused_last_call_args, last_call_kwargs = push_calls[0]
1018 1039 assert last_call_kwargs['action'] == 'push'
1019 1040 assert last_call_kwargs['commit_ids'] == pr_commit_ids
1020 1041
1021 1042 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
1022 1043 pull_request = pr_util.create_pull_request(mergeable=False)
1023 1044 pull_request_id = pull_request.pull_request_id
1024 1045 pull_request = PullRequest.get(pull_request_id)
1025 1046
1026 1047 response = self.app.post(
1027 1048 route_path('pullrequest_merge',
1028 1049 repo_name=pull_request.target_repo.scm_instance().name,
1029 1050 pull_request_id=pull_request.pull_request_id),
1030 1051 params={'csrf_token': csrf_token}).follow()
1031 1052
1032 1053 assert response.status_int == 200
1033 1054 response.mustcontain(
1034 1055 'Merge is not currently possible because of below failed checks.')
1035 1056 response.mustcontain('Server-side pull request merging is disabled.')
1036 1057
1037 1058 @pytest.mark.skip_backends('svn')
1038 1059 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
1039 1060 pull_request = pr_util.create_pull_request(mergeable=True)
1040 1061 pull_request_id = pull_request.pull_request_id
1041 1062 repo_name = pull_request.target_repo.scm_instance().name
1042 1063
1043 1064 response = self.app.post(
1044 1065 route_path('pullrequest_merge',
1045 1066 repo_name=repo_name, pull_request_id=pull_request_id),
1046 1067 params={'csrf_token': csrf_token}).follow()
1047 1068
1048 1069 assert response.status_int == 200
1049 1070
1050 1071 response.mustcontain(
1051 1072 'Merge is not currently possible because of below failed checks.')
1052 1073 response.mustcontain('Pull request reviewer approval is pending.')
1053 1074
1054 1075 def test_merge_pull_request_renders_failure_reason(
1055 1076 self, user_regular, csrf_token, pr_util):
1056 1077 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
1057 1078 pull_request_id = pull_request.pull_request_id
1058 1079 repo_name = pull_request.target_repo.scm_instance().name
1059 1080
1060 1081 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
1061 1082 MergeFailureReason.PUSH_FAILED,
1062 1083 metadata={'target': 'shadow repo',
1063 1084 'merge_commit': 'xxx'})
1064 1085 model_patcher = mock.patch.multiple(
1065 1086 PullRequestModel,
1066 1087 merge_repo=mock.Mock(return_value=merge_resp),
1067 1088 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
1068 1089
1069 1090 with model_patcher:
1070 1091 response = self.app.post(
1071 1092 route_path('pullrequest_merge',
1072 1093 repo_name=repo_name,
1073 1094 pull_request_id=pull_request_id),
1074 1095 params={'csrf_token': csrf_token}, status=302)
1075 1096
1076 1097 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
1077 1098 metadata={'target': 'shadow repo',
1078 1099 'merge_commit': 'xxx'})
1079 1100 assert_session_flash(response, merge_resp.merge_status_message)
1080 1101
1081 1102 def test_update_source_revision(self, backend, csrf_token):
1082 1103 commits = [
1083 1104 {'message': 'ancestor'},
1084 1105 {'message': 'change'},
1085 1106 {'message': 'change-2'},
1086 1107 ]
1087 1108 commit_ids = backend.create_master_repo(commits)
1088 1109 target = backend.create_repo(heads=['ancestor'])
1089 1110 source = backend.create_repo(heads=['change'])
1090 1111
1091 1112 # create pr from a in source to A in target
1092 1113 pull_request = PullRequest()
1093 1114
1094 1115 pull_request.source_repo = source
1095 1116 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1096 1117 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1097 1118
1098 1119 pull_request.target_repo = target
1099 1120 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1100 1121 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1101 1122
1102 1123 pull_request.revisions = [commit_ids['change']]
1103 1124 pull_request.title = u"Test"
1104 1125 pull_request.description = u"Description"
1105 1126 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1106 1127 pull_request.pull_request_state = PullRequest.STATE_CREATED
1107 1128 Session().add(pull_request)
1108 1129 Session().commit()
1109 1130 pull_request_id = pull_request.pull_request_id
1110 1131
1111 1132 # source has ancestor - change - change-2
1112 1133 backend.pull_heads(source, heads=['change-2'])
1113 1134 target_repo_name = target.repo_name
1114 1135
1115 1136 # update PR
1116 1137 self.app.post(
1117 1138 route_path('pullrequest_update',
1118 1139 repo_name=target_repo_name, pull_request_id=pull_request_id),
1119 1140 params={'update_commits': 'true', 'csrf_token': csrf_token})
1120 1141
1121 1142 response = self.app.get(
1122 1143 route_path('pullrequest_show',
1123 1144 repo_name=target_repo_name,
1124 1145 pull_request_id=pull_request.pull_request_id))
1125 1146
1126 1147 assert response.status_int == 200
1127 1148 response.mustcontain('Pull request updated to')
1128 1149 response.mustcontain('with 1 added, 0 removed commits.')
1129 1150
1130 1151 # check that we have now both revisions
1131 1152 pull_request = PullRequest.get(pull_request_id)
1132 1153 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
1133 1154
1134 1155 def test_update_target_revision(self, backend, csrf_token):
1135 1156 commits = [
1136 1157 {'message': 'ancestor'},
1137 1158 {'message': 'change'},
1138 1159 {'message': 'ancestor-new', 'parents': ['ancestor']},
1139 1160 {'message': 'change-rebased'},
1140 1161 ]
1141 1162 commit_ids = backend.create_master_repo(commits)
1142 1163 target = backend.create_repo(heads=['ancestor'])
1143 1164 source = backend.create_repo(heads=['change'])
1144 1165
1145 1166 # create pr from a in source to A in target
1146 1167 pull_request = PullRequest()
1147 1168
1148 1169 pull_request.source_repo = source
1149 1170 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1150 1171 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1151 1172
1152 1173 pull_request.target_repo = target
1153 1174 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1154 1175 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1155 1176
1156 1177 pull_request.revisions = [commit_ids['change']]
1157 1178 pull_request.title = u"Test"
1158 1179 pull_request.description = u"Description"
1159 1180 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1160 1181 pull_request.pull_request_state = PullRequest.STATE_CREATED
1161 1182
1162 1183 Session().add(pull_request)
1163 1184 Session().commit()
1164 1185 pull_request_id = pull_request.pull_request_id
1165 1186
1166 1187 # target has ancestor - ancestor-new
1167 1188 # source has ancestor - ancestor-new - change-rebased
1168 1189 backend.pull_heads(target, heads=['ancestor-new'])
1169 1190 backend.pull_heads(source, heads=['change-rebased'])
1170 1191 target_repo_name = target.repo_name
1171 1192
1172 1193 # update PR
1173 1194 url = route_path('pullrequest_update',
1174 1195 repo_name=target_repo_name,
1175 1196 pull_request_id=pull_request_id)
1176 1197 self.app.post(url,
1177 1198 params={'update_commits': 'true', 'csrf_token': csrf_token},
1178 1199 status=200)
1179 1200
1180 1201 # check that we have now both revisions
1181 1202 pull_request = PullRequest.get(pull_request_id)
1182 1203 assert pull_request.revisions == [commit_ids['change-rebased']]
1183 1204 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
1184 1205 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
1185 1206
1186 1207 response = self.app.get(
1187 1208 route_path('pullrequest_show',
1188 1209 repo_name=target_repo_name,
1189 1210 pull_request_id=pull_request.pull_request_id))
1190 1211 assert response.status_int == 200
1191 1212 response.mustcontain('Pull request updated to')
1192 1213 response.mustcontain('with 1 added, 1 removed commits.')
1193 1214
1194 1215 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
1195 1216 backend = backend_git
1196 1217 commits = [
1197 1218 {'message': 'master-commit-1'},
1198 1219 {'message': 'master-commit-2-change-1'},
1199 1220 {'message': 'master-commit-3-change-2'},
1200 1221
1201 1222 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
1202 1223 {'message': 'feat-commit-2'},
1203 1224 ]
1204 1225 commit_ids = backend.create_master_repo(commits)
1205 1226 target = backend.create_repo(heads=['master-commit-3-change-2'])
1206 1227 source = backend.create_repo(heads=['feat-commit-2'])
1207 1228
1208 1229 # create pr from a in source to A in target
1209 1230 pull_request = PullRequest()
1210 1231 pull_request.source_repo = source
1211 1232
1212 1233 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1213 1234 branch=backend.default_branch_name,
1214 1235 commit_id=commit_ids['master-commit-3-change-2'])
1215 1236
1216 1237 pull_request.target_repo = target
1217 1238 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1218 1239 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
1219 1240
1220 1241 pull_request.revisions = [
1221 1242 commit_ids['feat-commit-1'],
1222 1243 commit_ids['feat-commit-2']
1223 1244 ]
1224 1245 pull_request.title = u"Test"
1225 1246 pull_request.description = u"Description"
1226 1247 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1227 1248 pull_request.pull_request_state = PullRequest.STATE_CREATED
1228 1249 Session().add(pull_request)
1229 1250 Session().commit()
1230 1251 pull_request_id = pull_request.pull_request_id
1231 1252
1232 1253 # PR is created, now we simulate a force-push into target,
1233 1254 # that drops a 2 last commits
1234 1255 vcsrepo = target.scm_instance()
1235 1256 vcsrepo.config.clear_section('hooks')
1236 1257 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1237 1258 target_repo_name = target.repo_name
1238 1259
1239 1260 # update PR
1240 1261 url = route_path('pullrequest_update',
1241 1262 repo_name=target_repo_name,
1242 1263 pull_request_id=pull_request_id)
1243 1264 self.app.post(url,
1244 1265 params={'update_commits': 'true', 'csrf_token': csrf_token},
1245 1266 status=200)
1246 1267
1247 1268 response = self.app.get(route_path('pullrequest_new', repo_name=target_repo_name))
1248 1269 assert response.status_int == 200
1249 1270 response.mustcontain('Pull request updated to')
1250 1271 response.mustcontain('with 0 added, 0 removed commits.')
1251 1272
1252 1273 def test_update_of_ancestor_reference(self, backend, csrf_token):
1253 1274 commits = [
1254 1275 {'message': 'ancestor'},
1255 1276 {'message': 'change'},
1256 1277 {'message': 'change-2'},
1257 1278 {'message': 'ancestor-new', 'parents': ['ancestor']},
1258 1279 {'message': 'change-rebased'},
1259 1280 ]
1260 1281 commit_ids = backend.create_master_repo(commits)
1261 1282 target = backend.create_repo(heads=['ancestor'])
1262 1283 source = backend.create_repo(heads=['change'])
1263 1284
1264 1285 # create pr from a in source to A in target
1265 1286 pull_request = PullRequest()
1266 1287 pull_request.source_repo = source
1267 1288
1268 1289 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1269 1290 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1270 1291 pull_request.target_repo = target
1271 1292 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1272 1293 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1273 1294 pull_request.revisions = [commit_ids['change']]
1274 1295 pull_request.title = u"Test"
1275 1296 pull_request.description = u"Description"
1276 1297 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1277 1298 pull_request.pull_request_state = PullRequest.STATE_CREATED
1278 1299 Session().add(pull_request)
1279 1300 Session().commit()
1280 1301 pull_request_id = pull_request.pull_request_id
1281 1302
1282 1303 # target has ancestor - ancestor-new
1283 1304 # source has ancestor - ancestor-new - change-rebased
1284 1305 backend.pull_heads(target, heads=['ancestor-new'])
1285 1306 backend.pull_heads(source, heads=['change-rebased'])
1286 1307 target_repo_name = target.repo_name
1287 1308
1288 1309 # update PR
1289 1310 self.app.post(
1290 1311 route_path('pullrequest_update',
1291 1312 repo_name=target_repo_name, pull_request_id=pull_request_id),
1292 1313 params={'update_commits': 'true', 'csrf_token': csrf_token},
1293 1314 status=200)
1294 1315
1295 1316 # Expect the target reference to be updated correctly
1296 1317 pull_request = PullRequest.get(pull_request_id)
1297 1318 assert pull_request.revisions == [commit_ids['change-rebased']]
1298 1319 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1299 1320 branch=backend.default_branch_name,
1300 1321 commit_id=commit_ids['ancestor-new'])
1301 1322 assert pull_request.target_ref == expected_target_ref
1302 1323
1303 1324 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1304 1325 branch_name = 'development'
1305 1326 commits = [
1306 1327 {'message': 'initial-commit'},
1307 1328 {'message': 'old-feature'},
1308 1329 {'message': 'new-feature', 'branch': branch_name},
1309 1330 ]
1310 1331 repo = backend_git.create_repo(commits)
1311 1332 repo_name = repo.repo_name
1312 1333 commit_ids = backend_git.commit_ids
1313 1334
1314 1335 pull_request = PullRequest()
1315 1336 pull_request.source_repo = repo
1316 1337 pull_request.target_repo = repo
1317 1338 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1318 1339 branch=branch_name, commit_id=commit_ids['new-feature'])
1319 1340 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1320 1341 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1321 1342 pull_request.revisions = [commit_ids['new-feature']]
1322 1343 pull_request.title = u"Test"
1323 1344 pull_request.description = u"Description"
1324 1345 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1325 1346 pull_request.pull_request_state = PullRequest.STATE_CREATED
1326 1347 Session().add(pull_request)
1327 1348 Session().commit()
1328 1349
1329 1350 pull_request_id = pull_request.pull_request_id
1330 1351
1331 1352 vcs = repo.scm_instance()
1332 1353 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1333 1354 # NOTE(marcink): run GC to ensure the commits are gone
1334 1355 vcs.run_gc()
1335 1356
1336 1357 response = self.app.get(route_path(
1337 1358 'pullrequest_show',
1338 1359 repo_name=repo_name,
1339 1360 pull_request_id=pull_request_id))
1340 1361
1341 1362 assert response.status_int == 200
1342 1363
1343 1364 response.assert_response().element_contains(
1344 1365 '#changeset_compare_view_content .alert strong',
1345 1366 'Missing commits')
1346 1367 response.assert_response().element_contains(
1347 1368 '#changeset_compare_view_content .alert',
1348 1369 'This pull request cannot be displayed, because one or more'
1349 1370 ' commits no longer exist in the source repository.')
1350 1371
1351 1372 def test_strip_commits_from_pull_request(
1352 1373 self, backend, pr_util, csrf_token):
1353 1374 commits = [
1354 1375 {'message': 'initial-commit'},
1355 1376 {'message': 'old-feature'},
1356 1377 {'message': 'new-feature', 'parents': ['initial-commit']},
1357 1378 ]
1358 1379 pull_request = pr_util.create_pull_request(
1359 1380 commits, target_head='initial-commit', source_head='new-feature',
1360 1381 revisions=['new-feature'])
1361 1382
1362 1383 vcs = pr_util.source_repository.scm_instance()
1363 1384 if backend.alias == 'git':
1364 1385 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1365 1386 else:
1366 1387 vcs.strip(pr_util.commit_ids['new-feature'])
1367 1388
1368 1389 response = self.app.get(route_path(
1369 1390 'pullrequest_show',
1370 1391 repo_name=pr_util.target_repository.repo_name,
1371 1392 pull_request_id=pull_request.pull_request_id))
1372 1393
1373 1394 assert response.status_int == 200
1374 1395
1375 1396 response.assert_response().element_contains(
1376 1397 '#changeset_compare_view_content .alert strong',
1377 1398 'Missing commits')
1378 1399 response.assert_response().element_contains(
1379 1400 '#changeset_compare_view_content .alert',
1380 1401 'This pull request cannot be displayed, because one or more'
1381 1402 ' commits no longer exist in the source repository.')
1382 1403 response.assert_response().element_contains(
1383 1404 '#update_commits',
1384 1405 'Update commits')
1385 1406
1386 1407 def test_strip_commits_and_update(
1387 1408 self, backend, pr_util, csrf_token):
1388 1409 commits = [
1389 1410 {'message': 'initial-commit'},
1390 1411 {'message': 'old-feature'},
1391 1412 {'message': 'new-feature', 'parents': ['old-feature']},
1392 1413 ]
1393 1414 pull_request = pr_util.create_pull_request(
1394 1415 commits, target_head='old-feature', source_head='new-feature',
1395 1416 revisions=['new-feature'], mergeable=True)
1396 1417 pr_id = pull_request.pull_request_id
1397 1418 target_repo_name = pull_request.target_repo.repo_name
1398 1419
1399 1420 vcs = pr_util.source_repository.scm_instance()
1400 1421 if backend.alias == 'git':
1401 1422 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1402 1423 else:
1403 1424 vcs.strip(pr_util.commit_ids['new-feature'])
1404 1425
1405 1426 url = route_path('pullrequest_update',
1406 1427 repo_name=target_repo_name,
1407 1428 pull_request_id=pr_id)
1408 1429 response = self.app.post(url,
1409 1430 params={'update_commits': 'true',
1410 1431 'csrf_token': csrf_token})
1411 1432
1412 1433 assert response.status_int == 200
1413 1434 assert response.body == '{"response": true, "redirect_url": null}'
1414 1435
1415 1436 # Make sure that after update, it won't raise 500 errors
1416 1437 response = self.app.get(route_path(
1417 1438 'pullrequest_show',
1418 1439 repo_name=target_repo_name,
1419 1440 pull_request_id=pr_id))
1420 1441
1421 1442 assert response.status_int == 200
1422 1443 response.assert_response().element_contains(
1423 1444 '#changeset_compare_view_content .alert strong',
1424 1445 'Missing commits')
1425 1446
1426 1447 def test_branch_is_a_link(self, pr_util):
1427 1448 pull_request = pr_util.create_pull_request()
1428 1449 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1429 1450 pull_request.target_ref = 'branch:target:abcdef1234567890'
1430 1451 Session().add(pull_request)
1431 1452 Session().commit()
1432 1453
1433 1454 response = self.app.get(route_path(
1434 1455 'pullrequest_show',
1435 1456 repo_name=pull_request.target_repo.scm_instance().name,
1436 1457 pull_request_id=pull_request.pull_request_id))
1437 1458 assert response.status_int == 200
1438 1459
1439 1460 source = response.assert_response().get_element('.pr-source-info')
1440 1461 source_parent = source.getparent()
1441 1462 assert len(source_parent) == 1
1442 1463
1443 1464 target = response.assert_response().get_element('.pr-target-info')
1444 1465 target_parent = target.getparent()
1445 1466 assert len(target_parent) == 1
1446 1467
1447 1468 expected_origin_link = route_path(
1448 1469 'repo_commits',
1449 1470 repo_name=pull_request.source_repo.scm_instance().name,
1450 1471 params=dict(branch='origin'))
1451 1472 expected_target_link = route_path(
1452 1473 'repo_commits',
1453 1474 repo_name=pull_request.target_repo.scm_instance().name,
1454 1475 params=dict(branch='target'))
1455 1476 assert source_parent.attrib['href'] == expected_origin_link
1456 1477 assert target_parent.attrib['href'] == expected_target_link
1457 1478
1458 1479 def test_bookmark_is_not_a_link(self, pr_util):
1459 1480 pull_request = pr_util.create_pull_request()
1460 1481 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1461 1482 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1462 1483 Session().add(pull_request)
1463 1484 Session().commit()
1464 1485
1465 1486 response = self.app.get(route_path(
1466 1487 'pullrequest_show',
1467 1488 repo_name=pull_request.target_repo.scm_instance().name,
1468 1489 pull_request_id=pull_request.pull_request_id))
1469 1490 assert response.status_int == 200
1470 1491
1471 1492 source = response.assert_response().get_element('.pr-source-info')
1472 1493 assert source.text.strip() == 'bookmark:origin'
1473 1494 assert source.getparent().attrib.get('href') is None
1474 1495
1475 1496 target = response.assert_response().get_element('.pr-target-info')
1476 1497 assert target.text.strip() == 'bookmark:target'
1477 1498 assert target.getparent().attrib.get('href') is None
1478 1499
1479 1500 def test_tag_is_not_a_link(self, pr_util):
1480 1501 pull_request = pr_util.create_pull_request()
1481 1502 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1482 1503 pull_request.target_ref = 'tag:target:abcdef1234567890'
1483 1504 Session().add(pull_request)
1484 1505 Session().commit()
1485 1506
1486 1507 response = self.app.get(route_path(
1487 1508 'pullrequest_show',
1488 1509 repo_name=pull_request.target_repo.scm_instance().name,
1489 1510 pull_request_id=pull_request.pull_request_id))
1490 1511 assert response.status_int == 200
1491 1512
1492 1513 source = response.assert_response().get_element('.pr-source-info')
1493 1514 assert source.text.strip() == 'tag:origin'
1494 1515 assert source.getparent().attrib.get('href') is None
1495 1516
1496 1517 target = response.assert_response().get_element('.pr-target-info')
1497 1518 assert target.text.strip() == 'tag:target'
1498 1519 assert target.getparent().attrib.get('href') is None
1499 1520
1500 1521 @pytest.mark.parametrize('mergeable', [True, False])
1501 1522 def test_shadow_repository_link(
1502 1523 self, mergeable, pr_util, http_host_only_stub):
1503 1524 """
1504 1525 Check that the pull request summary page displays a link to the shadow
1505 1526 repository if the pull request is mergeable. If it is not mergeable
1506 1527 the link should not be displayed.
1507 1528 """
1508 1529 pull_request = pr_util.create_pull_request(
1509 1530 mergeable=mergeable, enable_notifications=False)
1510 1531 target_repo = pull_request.target_repo.scm_instance()
1511 1532 pr_id = pull_request.pull_request_id
1512 1533 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1513 1534 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1514 1535
1515 1536 response = self.app.get(route_path(
1516 1537 'pullrequest_show',
1517 1538 repo_name=target_repo.name,
1518 1539 pull_request_id=pr_id))
1519 1540
1520 1541 if mergeable:
1521 1542 response.assert_response().element_value_contains(
1522 1543 'input.pr-mergeinfo', shadow_url)
1523 1544 response.assert_response().element_value_contains(
1524 1545 'input.pr-mergeinfo ', 'pr-merge')
1525 1546 else:
1526 1547 response.assert_response().no_element_exists('.pr-mergeinfo')
1527 1548
1528 1549
1529 1550 @pytest.mark.usefixtures('app')
1530 1551 @pytest.mark.backends("git", "hg")
1531 1552 class TestPullrequestsControllerDelete(object):
1532 1553 def test_pull_request_delete_button_permissions_admin(
1533 1554 self, autologin_user, user_admin, pr_util):
1534 1555 pull_request = pr_util.create_pull_request(
1535 1556 author=user_admin.username, enable_notifications=False)
1536 1557
1537 1558 response = self.app.get(route_path(
1538 1559 'pullrequest_show',
1539 1560 repo_name=pull_request.target_repo.scm_instance().name,
1540 1561 pull_request_id=pull_request.pull_request_id))
1541 1562
1542 1563 response.mustcontain('id="delete_pullrequest"')
1543 1564 response.mustcontain('Confirm to delete this pull request')
1544 1565
1545 1566 def test_pull_request_delete_button_permissions_owner(
1546 1567 self, autologin_regular_user, user_regular, pr_util):
1547 1568 pull_request = pr_util.create_pull_request(
1548 1569 author=user_regular.username, enable_notifications=False)
1549 1570
1550 1571 response = self.app.get(route_path(
1551 1572 'pullrequest_show',
1552 1573 repo_name=pull_request.target_repo.scm_instance().name,
1553 1574 pull_request_id=pull_request.pull_request_id))
1554 1575
1555 1576 response.mustcontain('id="delete_pullrequest"')
1556 1577 response.mustcontain('Confirm to delete this pull request')
1557 1578
1558 1579 def test_pull_request_delete_button_permissions_forbidden(
1559 1580 self, autologin_regular_user, user_regular, user_admin, pr_util):
1560 1581 pull_request = pr_util.create_pull_request(
1561 1582 author=user_admin.username, enable_notifications=False)
1562 1583
1563 1584 response = self.app.get(route_path(
1564 1585 'pullrequest_show',
1565 1586 repo_name=pull_request.target_repo.scm_instance().name,
1566 1587 pull_request_id=pull_request.pull_request_id))
1567 1588 response.mustcontain(no=['id="delete_pullrequest"'])
1568 1589 response.mustcontain(no=['Confirm to delete this pull request'])
1569 1590
1570 1591 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1571 1592 self, autologin_regular_user, user_regular, user_admin, pr_util,
1572 1593 user_util):
1573 1594
1574 1595 pull_request = pr_util.create_pull_request(
1575 1596 author=user_admin.username, enable_notifications=False)
1576 1597
1577 1598 user_util.grant_user_permission_to_repo(
1578 1599 pull_request.target_repo, user_regular,
1579 1600 'repository.write')
1580 1601
1581 1602 response = self.app.get(route_path(
1582 1603 'pullrequest_show',
1583 1604 repo_name=pull_request.target_repo.scm_instance().name,
1584 1605 pull_request_id=pull_request.pull_request_id))
1585 1606
1586 1607 response.mustcontain('id="open_edit_pullrequest"')
1587 1608 response.mustcontain('id="delete_pullrequest"')
1588 1609 response.mustcontain(no=['Confirm to delete this pull request'])
1589 1610
1590 1611 def test_delete_comment_returns_404_if_comment_does_not_exist(
1591 1612 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1592 1613
1593 1614 pull_request = pr_util.create_pull_request(
1594 1615 author=user_admin.username, enable_notifications=False)
1595 1616
1596 1617 self.app.post(
1597 1618 route_path(
1598 1619 'pullrequest_comment_delete',
1599 1620 repo_name=pull_request.target_repo.scm_instance().name,
1600 1621 pull_request_id=pull_request.pull_request_id,
1601 1622 comment_id=1024404),
1602 1623 extra_environ=xhr_header,
1603 1624 params={'csrf_token': csrf_token},
1604 1625 status=404
1605 1626 )
1606 1627
1607 1628 def test_delete_comment(
1608 1629 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1609 1630
1610 1631 pull_request = pr_util.create_pull_request(
1611 1632 author=user_admin.username, enable_notifications=False)
1612 1633 comment = pr_util.create_comment()
1613 1634 comment_id = comment.comment_id
1614 1635
1615 1636 response = self.app.post(
1616 1637 route_path(
1617 1638 'pullrequest_comment_delete',
1618 1639 repo_name=pull_request.target_repo.scm_instance().name,
1619 1640 pull_request_id=pull_request.pull_request_id,
1620 1641 comment_id=comment_id),
1621 1642 extra_environ=xhr_header,
1622 1643 params={'csrf_token': csrf_token},
1623 1644 status=200
1624 1645 )
1625 1646 assert response.body == 'true'
1626 1647
1627 1648 @pytest.mark.parametrize('url_type', [
1628 1649 'pullrequest_new',
1629 1650 'pullrequest_create',
1630 1651 'pullrequest_update',
1631 1652 'pullrequest_merge',
1632 1653 ])
1633 1654 def test_pull_request_is_forbidden_on_archived_repo(
1634 1655 self, autologin_user, backend, xhr_header, user_util, url_type):
1635 1656
1636 1657 # create a temporary repo
1637 1658 source = user_util.create_repo(repo_type=backend.alias)
1638 1659 repo_name = source.repo_name
1639 1660 repo = Repository.get_by_repo_name(repo_name)
1640 1661 repo.archived = True
1641 1662 Session().commit()
1642 1663
1643 1664 response = self.app.get(
1644 1665 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1645 1666
1646 1667 msg = 'Action not supported for archived repository.'
1647 1668 assert_session_flash(response, msg)
1648 1669
1649 1670
1650 1671 def assert_pull_request_status(pull_request, expected_status):
1651 1672 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1652 1673 assert status == expected_status
1653 1674
1654 1675
1655 1676 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1656 1677 @pytest.mark.usefixtures("autologin_user")
1657 1678 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1658 1679 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
@@ -1,5830 +1,5836 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import time
28 28 import string
29 29 import hashlib
30 30 import logging
31 31 import datetime
32 32 import uuid
33 33 import warnings
34 34 import ipaddress
35 35 import functools
36 36 import traceback
37 37 import collections
38 38
39 39 from sqlalchemy import (
40 40 or_, and_, not_, func, cast, TypeDecorator, event,
41 41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
42 42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
43 43 Text, Float, PickleType, BigInteger)
44 44 from sqlalchemy.sql.expression import true, false, case
45 45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
46 46 from sqlalchemy.orm import (
47 47 relationship, joinedload, class_mapper, validates, aliased)
48 48 from sqlalchemy.ext.declarative import declared_attr
49 49 from sqlalchemy.ext.hybrid import hybrid_property
50 50 from sqlalchemy.exc import IntegrityError # pragma: no cover
51 51 from sqlalchemy.dialects.mysql import LONGTEXT
52 52 from zope.cachedescriptors.property import Lazy as LazyProperty
53 53 from pyramid import compat
54 54 from pyramid.threadlocal import get_current_request
55 55 from webhelpers2.text import remove_formatting
56 56
57 57 from rhodecode.translation import _
58 58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 59 from rhodecode.lib.vcs.backends.base import (
60 60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
61 61 from rhodecode.lib.utils2 import (
62 62 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
63 63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
64 64 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
65 65 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
66 66 JsonRaw
67 67 from rhodecode.lib.ext_json import json
68 68 from rhodecode.lib.caching_query import FromCache
69 69 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
70 70 from rhodecode.lib.encrypt2 import Encryptor
71 71 from rhodecode.lib.exceptions import (
72 72 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
73 73 from rhodecode.model.meta import Base, Session
74 74
75 75 URL_SEP = '/'
76 76 log = logging.getLogger(__name__)
77 77
78 78 # =============================================================================
79 79 # BASE CLASSES
80 80 # =============================================================================
81 81
82 82 # this is propagated from .ini file rhodecode.encrypted_values.secret or
83 83 # beaker.session.secret if first is not set.
84 84 # and initialized at environment.py
85 85 ENCRYPTION_KEY = None
86 86
87 87 # used to sort permissions by types, '#' used here is not allowed to be in
88 88 # usernames, and it's very early in sorted string.printable table.
89 89 PERMISSION_TYPE_SORT = {
90 90 'admin': '####',
91 91 'write': '###',
92 92 'read': '##',
93 93 'none': '#',
94 94 }
95 95
96 96
97 97 def display_user_sort(obj):
98 98 """
99 99 Sort function used to sort permissions in .permissions() function of
100 100 Repository, RepoGroup, UserGroup. Also it put the default user in front
101 101 of all other resources
102 102 """
103 103
104 104 if obj.username == User.DEFAULT_USER:
105 105 return '#####'
106 106 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
107 107 extra_sort_num = '1' # default
108 108
109 109 # NOTE(dan): inactive duplicates goes last
110 110 if getattr(obj, 'duplicate_perm', None):
111 111 extra_sort_num = '9'
112 112 return prefix + extra_sort_num + obj.username
113 113
114 114
115 115 def display_user_group_sort(obj):
116 116 """
117 117 Sort function used to sort permissions in .permissions() function of
118 118 Repository, RepoGroup, UserGroup. Also it put the default user in front
119 119 of all other resources
120 120 """
121 121
122 122 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
123 123 return prefix + obj.users_group_name
124 124
125 125
126 126 def _hash_key(k):
127 127 return sha1_safe(k)
128 128
129 129
130 130 def in_filter_generator(qry, items, limit=500):
131 131 """
132 132 Splits IN() into multiple with OR
133 133 e.g.::
134 134 cnt = Repository.query().filter(
135 135 or_(
136 136 *in_filter_generator(Repository.repo_id, range(100000))
137 137 )).count()
138 138 """
139 139 if not items:
140 140 # empty list will cause empty query which might cause security issues
141 141 # this can lead to hidden unpleasant results
142 142 items = [-1]
143 143
144 144 parts = []
145 145 for chunk in xrange(0, len(items), limit):
146 146 parts.append(
147 147 qry.in_(items[chunk: chunk + limit])
148 148 )
149 149
150 150 return parts
151 151
152 152
153 153 base_table_args = {
154 154 'extend_existing': True,
155 155 'mysql_engine': 'InnoDB',
156 156 'mysql_charset': 'utf8',
157 157 'sqlite_autoincrement': True
158 158 }
159 159
160 160
161 161 class EncryptedTextValue(TypeDecorator):
162 162 """
163 163 Special column for encrypted long text data, use like::
164 164
165 165 value = Column("encrypted_value", EncryptedValue(), nullable=False)
166 166
167 167 This column is intelligent so if value is in unencrypted form it return
168 168 unencrypted form, but on save it always encrypts
169 169 """
170 170 impl = Text
171 171
172 172 def process_bind_param(self, value, dialect):
173 173 """
174 174 Setter for storing value
175 175 """
176 176 import rhodecode
177 177 if not value:
178 178 return value
179 179
180 180 # protect against double encrypting if values is already encrypted
181 181 if value.startswith('enc$aes$') \
182 182 or value.startswith('enc$aes_hmac$') \
183 183 or value.startswith('enc2$'):
184 184 raise ValueError('value needs to be in unencrypted format, '
185 185 'ie. not starting with enc$ or enc2$')
186 186
187 187 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
188 188 if algo == 'aes':
189 189 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
190 190 elif algo == 'fernet':
191 191 return Encryptor(ENCRYPTION_KEY).encrypt(value)
192 192 else:
193 193 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
194 194
195 195 def process_result_value(self, value, dialect):
196 196 """
197 197 Getter for retrieving value
198 198 """
199 199
200 200 import rhodecode
201 201 if not value:
202 202 return value
203 203
204 204 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
205 205 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
206 206 if algo == 'aes':
207 207 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
208 208 elif algo == 'fernet':
209 209 return Encryptor(ENCRYPTION_KEY).decrypt(value)
210 210 else:
211 211 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
212 212 return decrypted_data
213 213
214 214
215 215 class BaseModel(object):
216 216 """
217 217 Base Model for all classes
218 218 """
219 219
220 220 @classmethod
221 221 def _get_keys(cls):
222 222 """return column names for this model """
223 223 return class_mapper(cls).c.keys()
224 224
225 225 def get_dict(self):
226 226 """
227 227 return dict with keys and values corresponding
228 228 to this model data """
229 229
230 230 d = {}
231 231 for k in self._get_keys():
232 232 d[k] = getattr(self, k)
233 233
234 234 # also use __json__() if present to get additional fields
235 235 _json_attr = getattr(self, '__json__', None)
236 236 if _json_attr:
237 237 # update with attributes from __json__
238 238 if callable(_json_attr):
239 239 _json_attr = _json_attr()
240 240 for k, val in _json_attr.iteritems():
241 241 d[k] = val
242 242 return d
243 243
244 244 def get_appstruct(self):
245 245 """return list with keys and values tuples corresponding
246 246 to this model data """
247 247
248 248 lst = []
249 249 for k in self._get_keys():
250 250 lst.append((k, getattr(self, k),))
251 251 return lst
252 252
253 253 def populate_obj(self, populate_dict):
254 254 """populate model with data from given populate_dict"""
255 255
256 256 for k in self._get_keys():
257 257 if k in populate_dict:
258 258 setattr(self, k, populate_dict[k])
259 259
260 260 @classmethod
261 261 def query(cls):
262 262 return Session().query(cls)
263 263
264 264 @classmethod
265 265 def get(cls, id_):
266 266 if id_:
267 267 return cls.query().get(id_)
268 268
269 269 @classmethod
270 270 def get_or_404(cls, id_):
271 271 from pyramid.httpexceptions import HTTPNotFound
272 272
273 273 try:
274 274 id_ = int(id_)
275 275 except (TypeError, ValueError):
276 276 raise HTTPNotFound()
277 277
278 278 res = cls.query().get(id_)
279 279 if not res:
280 280 raise HTTPNotFound()
281 281 return res
282 282
283 283 @classmethod
284 284 def getAll(cls):
285 285 # deprecated and left for backward compatibility
286 286 return cls.get_all()
287 287
288 288 @classmethod
289 289 def get_all(cls):
290 290 return cls.query().all()
291 291
292 292 @classmethod
293 293 def delete(cls, id_):
294 294 obj = cls.query().get(id_)
295 295 Session().delete(obj)
296 296
297 297 @classmethod
298 298 def identity_cache(cls, session, attr_name, value):
299 299 exist_in_session = []
300 300 for (item_cls, pkey), instance in session.identity_map.items():
301 301 if cls == item_cls and getattr(instance, attr_name) == value:
302 302 exist_in_session.append(instance)
303 303 if exist_in_session:
304 304 if len(exist_in_session) == 1:
305 305 return exist_in_session[0]
306 306 log.exception(
307 307 'multiple objects with attr %s and '
308 308 'value %s found with same name: %r',
309 309 attr_name, value, exist_in_session)
310 310
311 311 def __repr__(self):
312 312 if hasattr(self, '__unicode__'):
313 313 # python repr needs to return str
314 314 try:
315 315 return safe_str(self.__unicode__())
316 316 except UnicodeDecodeError:
317 317 pass
318 318 return '<DB:%s>' % (self.__class__.__name__)
319 319
320 320
321 321 class RhodeCodeSetting(Base, BaseModel):
322 322 __tablename__ = 'rhodecode_settings'
323 323 __table_args__ = (
324 324 UniqueConstraint('app_settings_name'),
325 325 base_table_args
326 326 )
327 327
328 328 SETTINGS_TYPES = {
329 329 'str': safe_str,
330 330 'int': safe_int,
331 331 'unicode': safe_unicode,
332 332 'bool': str2bool,
333 333 'list': functools.partial(aslist, sep=',')
334 334 }
335 335 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
336 336 GLOBAL_CONF_KEY = 'app_settings'
337 337
338 338 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
339 339 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
340 340 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
341 341 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
342 342
343 343 def __init__(self, key='', val='', type='unicode'):
344 344 self.app_settings_name = key
345 345 self.app_settings_type = type
346 346 self.app_settings_value = val
347 347
348 348 @validates('_app_settings_value')
349 349 def validate_settings_value(self, key, val):
350 350 assert type(val) == unicode
351 351 return val
352 352
353 353 @hybrid_property
354 354 def app_settings_value(self):
355 355 v = self._app_settings_value
356 356 _type = self.app_settings_type
357 357 if _type:
358 358 _type = self.app_settings_type.split('.')[0]
359 359 # decode the encrypted value
360 360 if 'encrypted' in self.app_settings_type:
361 361 cipher = EncryptedTextValue()
362 362 v = safe_unicode(cipher.process_result_value(v, None))
363 363
364 364 converter = self.SETTINGS_TYPES.get(_type) or \
365 365 self.SETTINGS_TYPES['unicode']
366 366 return converter(v)
367 367
368 368 @app_settings_value.setter
369 369 def app_settings_value(self, val):
370 370 """
371 371 Setter that will always make sure we use unicode in app_settings_value
372 372
373 373 :param val:
374 374 """
375 375 val = safe_unicode(val)
376 376 # encode the encrypted value
377 377 if 'encrypted' in self.app_settings_type:
378 378 cipher = EncryptedTextValue()
379 379 val = safe_unicode(cipher.process_bind_param(val, None))
380 380 self._app_settings_value = val
381 381
382 382 @hybrid_property
383 383 def app_settings_type(self):
384 384 return self._app_settings_type
385 385
386 386 @app_settings_type.setter
387 387 def app_settings_type(self, val):
388 388 if val.split('.')[0] not in self.SETTINGS_TYPES:
389 389 raise Exception('type must be one of %s got %s'
390 390 % (self.SETTINGS_TYPES.keys(), val))
391 391 self._app_settings_type = val
392 392
393 393 @classmethod
394 394 def get_by_prefix(cls, prefix):
395 395 return RhodeCodeSetting.query()\
396 396 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
397 397 .all()
398 398
399 399 def __unicode__(self):
400 400 return u"<%s('%s:%s[%s]')>" % (
401 401 self.__class__.__name__,
402 402 self.app_settings_name, self.app_settings_value,
403 403 self.app_settings_type
404 404 )
405 405
406 406
407 407 class RhodeCodeUi(Base, BaseModel):
408 408 __tablename__ = 'rhodecode_ui'
409 409 __table_args__ = (
410 410 UniqueConstraint('ui_key'),
411 411 base_table_args
412 412 )
413 413
414 414 HOOK_REPO_SIZE = 'changegroup.repo_size'
415 415 # HG
416 416 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
417 417 HOOK_PULL = 'outgoing.pull_logger'
418 418 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
419 419 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
420 420 HOOK_PUSH = 'changegroup.push_logger'
421 421 HOOK_PUSH_KEY = 'pushkey.key_push'
422 422
423 423 HOOKS_BUILTIN = [
424 424 HOOK_PRE_PULL,
425 425 HOOK_PULL,
426 426 HOOK_PRE_PUSH,
427 427 HOOK_PRETX_PUSH,
428 428 HOOK_PUSH,
429 429 HOOK_PUSH_KEY,
430 430 ]
431 431
432 432 # TODO: johbo: Unify way how hooks are configured for git and hg,
433 433 # git part is currently hardcoded.
434 434
435 435 # SVN PATTERNS
436 436 SVN_BRANCH_ID = 'vcs_svn_branch'
437 437 SVN_TAG_ID = 'vcs_svn_tag'
438 438
439 439 ui_id = Column(
440 440 "ui_id", Integer(), nullable=False, unique=True, default=None,
441 441 primary_key=True)
442 442 ui_section = Column(
443 443 "ui_section", String(255), nullable=True, unique=None, default=None)
444 444 ui_key = Column(
445 445 "ui_key", String(255), nullable=True, unique=None, default=None)
446 446 ui_value = Column(
447 447 "ui_value", String(255), nullable=True, unique=None, default=None)
448 448 ui_active = Column(
449 449 "ui_active", Boolean(), nullable=True, unique=None, default=True)
450 450
451 451 def __repr__(self):
452 452 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
453 453 self.ui_key, self.ui_value)
454 454
455 455
456 456 class RepoRhodeCodeSetting(Base, BaseModel):
457 457 __tablename__ = 'repo_rhodecode_settings'
458 458 __table_args__ = (
459 459 UniqueConstraint(
460 460 'app_settings_name', 'repository_id',
461 461 name='uq_repo_rhodecode_setting_name_repo_id'),
462 462 base_table_args
463 463 )
464 464
465 465 repository_id = Column(
466 466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
467 467 nullable=False)
468 468 app_settings_id = Column(
469 469 "app_settings_id", Integer(), nullable=False, unique=True,
470 470 default=None, primary_key=True)
471 471 app_settings_name = Column(
472 472 "app_settings_name", String(255), nullable=True, unique=None,
473 473 default=None)
474 474 _app_settings_value = Column(
475 475 "app_settings_value", String(4096), nullable=True, unique=None,
476 476 default=None)
477 477 _app_settings_type = Column(
478 478 "app_settings_type", String(255), nullable=True, unique=None,
479 479 default=None)
480 480
481 481 repository = relationship('Repository')
482 482
483 483 def __init__(self, repository_id, key='', val='', type='unicode'):
484 484 self.repository_id = repository_id
485 485 self.app_settings_name = key
486 486 self.app_settings_type = type
487 487 self.app_settings_value = val
488 488
489 489 @validates('_app_settings_value')
490 490 def validate_settings_value(self, key, val):
491 491 assert type(val) == unicode
492 492 return val
493 493
494 494 @hybrid_property
495 495 def app_settings_value(self):
496 496 v = self._app_settings_value
497 497 type_ = self.app_settings_type
498 498 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
499 499 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
500 500 return converter(v)
501 501
502 502 @app_settings_value.setter
503 503 def app_settings_value(self, val):
504 504 """
505 505 Setter that will always make sure we use unicode in app_settings_value
506 506
507 507 :param val:
508 508 """
509 509 self._app_settings_value = safe_unicode(val)
510 510
511 511 @hybrid_property
512 512 def app_settings_type(self):
513 513 return self._app_settings_type
514 514
515 515 @app_settings_type.setter
516 516 def app_settings_type(self, val):
517 517 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
518 518 if val not in SETTINGS_TYPES:
519 519 raise Exception('type must be one of %s got %s'
520 520 % (SETTINGS_TYPES.keys(), val))
521 521 self._app_settings_type = val
522 522
523 523 def __unicode__(self):
524 524 return u"<%s('%s:%s:%s[%s]')>" % (
525 525 self.__class__.__name__, self.repository.repo_name,
526 526 self.app_settings_name, self.app_settings_value,
527 527 self.app_settings_type
528 528 )
529 529
530 530
531 531 class RepoRhodeCodeUi(Base, BaseModel):
532 532 __tablename__ = 'repo_rhodecode_ui'
533 533 __table_args__ = (
534 534 UniqueConstraint(
535 535 'repository_id', 'ui_section', 'ui_key',
536 536 name='uq_repo_rhodecode_ui_repository_id_section_key'),
537 537 base_table_args
538 538 )
539 539
540 540 repository_id = Column(
541 541 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
542 542 nullable=False)
543 543 ui_id = Column(
544 544 "ui_id", Integer(), nullable=False, unique=True, default=None,
545 545 primary_key=True)
546 546 ui_section = Column(
547 547 "ui_section", String(255), nullable=True, unique=None, default=None)
548 548 ui_key = Column(
549 549 "ui_key", String(255), nullable=True, unique=None, default=None)
550 550 ui_value = Column(
551 551 "ui_value", String(255), nullable=True, unique=None, default=None)
552 552 ui_active = Column(
553 553 "ui_active", Boolean(), nullable=True, unique=None, default=True)
554 554
555 555 repository = relationship('Repository')
556 556
557 557 def __repr__(self):
558 558 return '<%s[%s:%s]%s=>%s]>' % (
559 559 self.__class__.__name__, self.repository.repo_name,
560 560 self.ui_section, self.ui_key, self.ui_value)
561 561
562 562
563 563 class User(Base, BaseModel):
564 564 __tablename__ = 'users'
565 565 __table_args__ = (
566 566 UniqueConstraint('username'), UniqueConstraint('email'),
567 567 Index('u_username_idx', 'username'),
568 568 Index('u_email_idx', 'email'),
569 569 base_table_args
570 570 )
571 571
572 572 DEFAULT_USER = 'default'
573 573 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
574 574 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
575 575
576 576 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
577 577 username = Column("username", String(255), nullable=True, unique=None, default=None)
578 578 password = Column("password", String(255), nullable=True, unique=None, default=None)
579 579 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
580 580 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
581 581 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
582 582 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
583 583 _email = Column("email", String(255), nullable=True, unique=None, default=None)
584 584 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
585 585 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
586 586 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
587 587
588 588 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
589 589 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
590 590 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
591 591 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
592 592 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
593 593 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
594 594
595 595 user_log = relationship('UserLog')
596 596 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
597 597
598 598 repositories = relationship('Repository')
599 599 repository_groups = relationship('RepoGroup')
600 600 user_groups = relationship('UserGroup')
601 601
602 602 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
603 603 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
604 604
605 605 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
606 606 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
607 607 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
608 608
609 609 group_member = relationship('UserGroupMember', cascade='all')
610 610
611 611 notifications = relationship('UserNotification', cascade='all')
612 612 # notifications assigned to this user
613 613 user_created_notifications = relationship('Notification', cascade='all')
614 614 # comments created by this user
615 615 user_comments = relationship('ChangesetComment', cascade='all')
616 616 # user profile extra info
617 617 user_emails = relationship('UserEmailMap', cascade='all')
618 618 user_ip_map = relationship('UserIpMap', cascade='all')
619 619 user_auth_tokens = relationship('UserApiKeys', cascade='all')
620 620 user_ssh_keys = relationship('UserSshKeys', cascade='all')
621 621
622 622 # gists
623 623 user_gists = relationship('Gist', cascade='all')
624 624 # user pull requests
625 625 user_pull_requests = relationship('PullRequest', cascade='all')
626 626
627 627 # external identities
628 628 external_identities = relationship(
629 629 'ExternalIdentity',
630 630 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
631 631 cascade='all')
632 632 # review rules
633 633 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
634 634
635 635 # artifacts owned
636 636 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
637 637
638 638 # no cascade, set NULL
639 639 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
640 640
641 641 def __unicode__(self):
642 642 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
643 643 self.user_id, self.username)
644 644
645 645 @hybrid_property
646 646 def email(self):
647 647 return self._email
648 648
649 649 @email.setter
650 650 def email(self, val):
651 651 self._email = val.lower() if val else None
652 652
653 653 @hybrid_property
654 654 def first_name(self):
655 655 from rhodecode.lib import helpers as h
656 656 if self.name:
657 657 return h.escape(self.name)
658 658 return self.name
659 659
660 660 @hybrid_property
661 661 def last_name(self):
662 662 from rhodecode.lib import helpers as h
663 663 if self.lastname:
664 664 return h.escape(self.lastname)
665 665 return self.lastname
666 666
667 667 @hybrid_property
668 668 def api_key(self):
669 669 """
670 670 Fetch if exist an auth-token with role ALL connected to this user
671 671 """
672 672 user_auth_token = UserApiKeys.query()\
673 673 .filter(UserApiKeys.user_id == self.user_id)\
674 674 .filter(or_(UserApiKeys.expires == -1,
675 675 UserApiKeys.expires >= time.time()))\
676 676 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
677 677 if user_auth_token:
678 678 user_auth_token = user_auth_token.api_key
679 679
680 680 return user_auth_token
681 681
682 682 @api_key.setter
683 683 def api_key(self, val):
684 684 # don't allow to set API key this is deprecated for now
685 685 self._api_key = None
686 686
687 687 @property
688 688 def reviewer_pull_requests(self):
689 689 return PullRequestReviewers.query() \
690 690 .options(joinedload(PullRequestReviewers.pull_request)) \
691 691 .filter(PullRequestReviewers.user_id == self.user_id) \
692 692 .all()
693 693
694 694 @property
695 695 def firstname(self):
696 696 # alias for future
697 697 return self.name
698 698
699 699 @property
700 700 def emails(self):
701 701 other = UserEmailMap.query()\
702 702 .filter(UserEmailMap.user == self) \
703 703 .order_by(UserEmailMap.email_id.asc()) \
704 704 .all()
705 705 return [self.email] + [x.email for x in other]
706 706
707 707 def emails_cached(self):
708 708 emails = UserEmailMap.query()\
709 709 .filter(UserEmailMap.user == self) \
710 710 .order_by(UserEmailMap.email_id.asc())
711 711
712 712 emails = emails.options(
713 713 FromCache("sql_cache_short", "get_user_{}_emails".format(self.user_id))
714 714 )
715 715
716 716 return [self.email] + [x.email for x in emails]
717 717
718 718 @property
719 719 def auth_tokens(self):
720 720 auth_tokens = self.get_auth_tokens()
721 721 return [x.api_key for x in auth_tokens]
722 722
723 723 def get_auth_tokens(self):
724 724 return UserApiKeys.query()\
725 725 .filter(UserApiKeys.user == self)\
726 726 .order_by(UserApiKeys.user_api_key_id.asc())\
727 727 .all()
728 728
729 729 @LazyProperty
730 730 def feed_token(self):
731 731 return self.get_feed_token()
732 732
733 733 def get_feed_token(self, cache=True):
734 734 feed_tokens = UserApiKeys.query()\
735 735 .filter(UserApiKeys.user == self)\
736 736 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
737 737 if cache:
738 738 feed_tokens = feed_tokens.options(
739 739 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
740 740
741 741 feed_tokens = feed_tokens.all()
742 742 if feed_tokens:
743 743 return feed_tokens[0].api_key
744 744 return 'NO_FEED_TOKEN_AVAILABLE'
745 745
746 746 @LazyProperty
747 747 def artifact_token(self):
748 748 return self.get_artifact_token()
749 749
750 750 def get_artifact_token(self, cache=True):
751 751 artifacts_tokens = UserApiKeys.query()\
752 752 .filter(UserApiKeys.user == self) \
753 753 .filter(or_(UserApiKeys.expires == -1,
754 754 UserApiKeys.expires >= time.time())) \
755 755 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
756 756
757 757 if cache:
758 758 artifacts_tokens = artifacts_tokens.options(
759 759 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
760 760
761 761 artifacts_tokens = artifacts_tokens.all()
762 762 if artifacts_tokens:
763 763 return artifacts_tokens[0].api_key
764 764 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
765 765
766 766 def get_or_create_artifact_token(self):
767 767 artifacts_tokens = UserApiKeys.query()\
768 768 .filter(UserApiKeys.user == self) \
769 769 .filter(or_(UserApiKeys.expires == -1,
770 770 UserApiKeys.expires >= time.time())) \
771 771 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
772 772
773 773 artifacts_tokens = artifacts_tokens.all()
774 774 if artifacts_tokens:
775 775 return artifacts_tokens[0].api_key
776 776 else:
777 777 from rhodecode.model.auth_token import AuthTokenModel
778 778 artifact_token = AuthTokenModel().create(
779 779 self, 'auto-generated-artifact-token',
780 780 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
781 781 Session.commit()
782 782 return artifact_token.api_key
783 783
784 784 @classmethod
785 785 def get(cls, user_id, cache=False):
786 786 if not user_id:
787 787 return
788 788
789 789 user = cls.query()
790 790 if cache:
791 791 user = user.options(
792 792 FromCache("sql_cache_short", "get_users_%s" % user_id))
793 793 return user.get(user_id)
794 794
795 795 @classmethod
796 796 def extra_valid_auth_tokens(cls, user, role=None):
797 797 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
798 798 .filter(or_(UserApiKeys.expires == -1,
799 799 UserApiKeys.expires >= time.time()))
800 800 if role:
801 801 tokens = tokens.filter(or_(UserApiKeys.role == role,
802 802 UserApiKeys.role == UserApiKeys.ROLE_ALL))
803 803 return tokens.all()
804 804
805 805 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
806 806 from rhodecode.lib import auth
807 807
808 808 log.debug('Trying to authenticate user: %s via auth-token, '
809 809 'and roles: %s', self, roles)
810 810
811 811 if not auth_token:
812 812 return False
813 813
814 814 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
815 815 tokens_q = UserApiKeys.query()\
816 816 .filter(UserApiKeys.user_id == self.user_id)\
817 817 .filter(or_(UserApiKeys.expires == -1,
818 818 UserApiKeys.expires >= time.time()))
819 819
820 820 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
821 821
822 822 crypto_backend = auth.crypto_backend()
823 823 enc_token_map = {}
824 824 plain_token_map = {}
825 825 for token in tokens_q:
826 826 if token.api_key.startswith(crypto_backend.ENC_PREF):
827 827 enc_token_map[token.api_key] = token
828 828 else:
829 829 plain_token_map[token.api_key] = token
830 830 log.debug(
831 831 'Found %s plain and %s encrypted tokens to check for authentication for this user',
832 832 len(plain_token_map), len(enc_token_map))
833 833
834 834 # plain token match comes first
835 835 match = plain_token_map.get(auth_token)
836 836
837 837 # check encrypted tokens now
838 838 if not match:
839 839 for token_hash, token in enc_token_map.items():
840 840 # NOTE(marcink): this is expensive to calculate, but most secure
841 841 if crypto_backend.hash_check(auth_token, token_hash):
842 842 match = token
843 843 break
844 844
845 845 if match:
846 846 log.debug('Found matching token %s', match)
847 847 if match.repo_id:
848 848 log.debug('Found scope, checking for scope match of token %s', match)
849 849 if match.repo_id == scope_repo_id:
850 850 return True
851 851 else:
852 852 log.debug(
853 853 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
854 854 'and calling scope is:%s, skipping further checks',
855 855 match.repo, scope_repo_id)
856 856 return False
857 857 else:
858 858 return True
859 859
860 860 return False
861 861
862 862 @property
863 863 def ip_addresses(self):
864 864 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
865 865 return [x.ip_addr for x in ret]
866 866
867 867 @property
868 868 def username_and_name(self):
869 869 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
870 870
871 871 @property
872 872 def username_or_name_or_email(self):
873 873 full_name = self.full_name if self.full_name is not ' ' else None
874 874 return self.username or full_name or self.email
875 875
876 876 @property
877 877 def full_name(self):
878 878 return '%s %s' % (self.first_name, self.last_name)
879 879
880 880 @property
881 881 def full_name_or_username(self):
882 882 return ('%s %s' % (self.first_name, self.last_name)
883 883 if (self.first_name and self.last_name) else self.username)
884 884
885 885 @property
886 886 def full_contact(self):
887 887 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
888 888
889 889 @property
890 890 def short_contact(self):
891 891 return '%s %s' % (self.first_name, self.last_name)
892 892
893 893 @property
894 894 def is_admin(self):
895 895 return self.admin
896 896
897 897 @property
898 898 def language(self):
899 899 return self.user_data.get('language')
900 900
901 901 def AuthUser(self, **kwargs):
902 902 """
903 903 Returns instance of AuthUser for this user
904 904 """
905 905 from rhodecode.lib.auth import AuthUser
906 906 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
907 907
908 908 @hybrid_property
909 909 def user_data(self):
910 910 if not self._user_data:
911 911 return {}
912 912
913 913 try:
914 914 return json.loads(self._user_data)
915 915 except TypeError:
916 916 return {}
917 917
918 918 @user_data.setter
919 919 def user_data(self, val):
920 920 if not isinstance(val, dict):
921 921 raise Exception('user_data must be dict, got %s' % type(val))
922 922 try:
923 923 self._user_data = json.dumps(val)
924 924 except Exception:
925 925 log.error(traceback.format_exc())
926 926
927 927 @classmethod
928 928 def get_by_username(cls, username, case_insensitive=False,
929 929 cache=False, identity_cache=False):
930 930 session = Session()
931 931
932 932 if case_insensitive:
933 933 q = cls.query().filter(
934 934 func.lower(cls.username) == func.lower(username))
935 935 else:
936 936 q = cls.query().filter(cls.username == username)
937 937
938 938 if cache:
939 939 if identity_cache:
940 940 val = cls.identity_cache(session, 'username', username)
941 941 if val:
942 942 return val
943 943 else:
944 944 cache_key = "get_user_by_name_%s" % _hash_key(username)
945 945 q = q.options(
946 946 FromCache("sql_cache_short", cache_key))
947 947
948 948 return q.scalar()
949 949
950 950 @classmethod
951 951 def get_by_auth_token(cls, auth_token, cache=False):
952 952 q = UserApiKeys.query()\
953 953 .filter(UserApiKeys.api_key == auth_token)\
954 954 .filter(or_(UserApiKeys.expires == -1,
955 955 UserApiKeys.expires >= time.time()))
956 956 if cache:
957 957 q = q.options(
958 958 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
959 959
960 960 match = q.first()
961 961 if match:
962 962 return match.user
963 963
964 964 @classmethod
965 965 def get_by_email(cls, email, case_insensitive=False, cache=False):
966 966
967 967 if case_insensitive:
968 968 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
969 969
970 970 else:
971 971 q = cls.query().filter(cls.email == email)
972 972
973 973 email_key = _hash_key(email)
974 974 if cache:
975 975 q = q.options(
976 976 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
977 977
978 978 ret = q.scalar()
979 979 if ret is None:
980 980 q = UserEmailMap.query()
981 981 # try fetching in alternate email map
982 982 if case_insensitive:
983 983 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
984 984 else:
985 985 q = q.filter(UserEmailMap.email == email)
986 986 q = q.options(joinedload(UserEmailMap.user))
987 987 if cache:
988 988 q = q.options(
989 989 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
990 990 ret = getattr(q.scalar(), 'user', None)
991 991
992 992 return ret
993 993
994 994 @classmethod
995 995 def get_from_cs_author(cls, author):
996 996 """
997 997 Tries to get User objects out of commit author string
998 998
999 999 :param author:
1000 1000 """
1001 1001 from rhodecode.lib.helpers import email, author_name
1002 1002 # Valid email in the attribute passed, see if they're in the system
1003 1003 _email = email(author)
1004 1004 if _email:
1005 1005 user = cls.get_by_email(_email, case_insensitive=True)
1006 1006 if user:
1007 1007 return user
1008 1008 # Maybe we can match by username?
1009 1009 _author = author_name(author)
1010 1010 user = cls.get_by_username(_author, case_insensitive=True)
1011 1011 if user:
1012 1012 return user
1013 1013
1014 1014 def update_userdata(self, **kwargs):
1015 1015 usr = self
1016 1016 old = usr.user_data
1017 1017 old.update(**kwargs)
1018 1018 usr.user_data = old
1019 1019 Session().add(usr)
1020 1020 log.debug('updated userdata with %s', kwargs)
1021 1021
1022 1022 def update_lastlogin(self):
1023 1023 """Update user lastlogin"""
1024 1024 self.last_login = datetime.datetime.now()
1025 1025 Session().add(self)
1026 1026 log.debug('updated user %s lastlogin', self.username)
1027 1027
1028 1028 def update_password(self, new_password):
1029 1029 from rhodecode.lib.auth import get_crypt_password
1030 1030
1031 1031 self.password = get_crypt_password(new_password)
1032 1032 Session().add(self)
1033 1033
1034 1034 @classmethod
1035 1035 def get_first_super_admin(cls):
1036 1036 user = User.query()\
1037 1037 .filter(User.admin == true()) \
1038 1038 .order_by(User.user_id.asc()) \
1039 1039 .first()
1040 1040
1041 1041 if user is None:
1042 1042 raise Exception('FATAL: Missing administrative account!')
1043 1043 return user
1044 1044
1045 1045 @classmethod
1046 1046 def get_all_super_admins(cls, only_active=False):
1047 1047 """
1048 1048 Returns all admin accounts sorted by username
1049 1049 """
1050 1050 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1051 1051 if only_active:
1052 1052 qry = qry.filter(User.active == true())
1053 1053 return qry.all()
1054 1054
1055 1055 @classmethod
1056 1056 def get_all_user_ids(cls, only_active=True):
1057 1057 """
1058 1058 Returns all users IDs
1059 1059 """
1060 1060 qry = Session().query(User.user_id)
1061 1061
1062 1062 if only_active:
1063 1063 qry = qry.filter(User.active == true())
1064 1064 return [x.user_id for x in qry]
1065 1065
1066 1066 @classmethod
1067 1067 def get_default_user(cls, cache=False, refresh=False):
1068 1068 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1069 1069 if user is None:
1070 1070 raise Exception('FATAL: Missing default account!')
1071 1071 if refresh:
1072 1072 # The default user might be based on outdated state which
1073 1073 # has been loaded from the cache.
1074 1074 # A call to refresh() ensures that the
1075 1075 # latest state from the database is used.
1076 1076 Session().refresh(user)
1077 1077 return user
1078 1078
1079 1079 @classmethod
1080 1080 def get_default_user_id(cls):
1081 1081 import rhodecode
1082 1082 return rhodecode.CONFIG['default_user_id']
1083 1083
1084 1084 def _get_default_perms(self, user, suffix=''):
1085 1085 from rhodecode.model.permission import PermissionModel
1086 1086 return PermissionModel().get_default_perms(user.user_perms, suffix)
1087 1087
1088 1088 def get_default_perms(self, suffix=''):
1089 1089 return self._get_default_perms(self, suffix)
1090 1090
1091 1091 def get_api_data(self, include_secrets=False, details='full'):
1092 1092 """
1093 1093 Common function for generating user related data for API
1094 1094
1095 1095 :param include_secrets: By default secrets in the API data will be replaced
1096 1096 by a placeholder value to prevent exposing this data by accident. In case
1097 1097 this data shall be exposed, set this flag to ``True``.
1098 1098
1099 1099 :param details: details can be 'basic|full' basic gives only a subset of
1100 1100 the available user information that includes user_id, name and emails.
1101 1101 """
1102 1102 user = self
1103 1103 user_data = self.user_data
1104 1104 data = {
1105 1105 'user_id': user.user_id,
1106 1106 'username': user.username,
1107 1107 'firstname': user.name,
1108 1108 'lastname': user.lastname,
1109 1109 'description': user.description,
1110 1110 'email': user.email,
1111 1111 'emails': user.emails,
1112 1112 }
1113 1113 if details == 'basic':
1114 1114 return data
1115 1115
1116 1116 auth_token_length = 40
1117 1117 auth_token_replacement = '*' * auth_token_length
1118 1118
1119 1119 extras = {
1120 1120 'auth_tokens': [auth_token_replacement],
1121 1121 'active': user.active,
1122 1122 'admin': user.admin,
1123 1123 'extern_type': user.extern_type,
1124 1124 'extern_name': user.extern_name,
1125 1125 'last_login': user.last_login,
1126 1126 'last_activity': user.last_activity,
1127 1127 'ip_addresses': user.ip_addresses,
1128 1128 'language': user_data.get('language')
1129 1129 }
1130 1130 data.update(extras)
1131 1131
1132 1132 if include_secrets:
1133 1133 data['auth_tokens'] = user.auth_tokens
1134 1134 return data
1135 1135
1136 1136 def __json__(self):
1137 1137 data = {
1138 1138 'full_name': self.full_name,
1139 1139 'full_name_or_username': self.full_name_or_username,
1140 1140 'short_contact': self.short_contact,
1141 1141 'full_contact': self.full_contact,
1142 1142 }
1143 1143 data.update(self.get_api_data())
1144 1144 return data
1145 1145
1146 1146
1147 1147 class UserApiKeys(Base, BaseModel):
1148 1148 __tablename__ = 'user_api_keys'
1149 1149 __table_args__ = (
1150 1150 Index('uak_api_key_idx', 'api_key'),
1151 1151 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1152 1152 base_table_args
1153 1153 )
1154 1154 __mapper_args__ = {}
1155 1155
1156 1156 # ApiKey role
1157 1157 ROLE_ALL = 'token_role_all'
1158 1158 ROLE_VCS = 'token_role_vcs'
1159 1159 ROLE_API = 'token_role_api'
1160 1160 ROLE_HTTP = 'token_role_http'
1161 1161 ROLE_FEED = 'token_role_feed'
1162 1162 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1163 1163 # The last one is ignored in the list as we only
1164 1164 # use it for one action, and cannot be created by users
1165 1165 ROLE_PASSWORD_RESET = 'token_password_reset'
1166 1166
1167 1167 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1168 1168
1169 1169 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1170 1170 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1171 1171 api_key = Column("api_key", String(255), nullable=False, unique=True)
1172 1172 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1173 1173 expires = Column('expires', Float(53), nullable=False)
1174 1174 role = Column('role', String(255), nullable=True)
1175 1175 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1176 1176
1177 1177 # scope columns
1178 1178 repo_id = Column(
1179 1179 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1180 1180 nullable=True, unique=None, default=None)
1181 1181 repo = relationship('Repository', lazy='joined')
1182 1182
1183 1183 repo_group_id = Column(
1184 1184 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1185 1185 nullable=True, unique=None, default=None)
1186 1186 repo_group = relationship('RepoGroup', lazy='joined')
1187 1187
1188 1188 user = relationship('User', lazy='joined')
1189 1189
1190 1190 def __unicode__(self):
1191 1191 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1192 1192
1193 1193 def __json__(self):
1194 1194 data = {
1195 1195 'auth_token': self.api_key,
1196 1196 'role': self.role,
1197 1197 'scope': self.scope_humanized,
1198 1198 'expired': self.expired
1199 1199 }
1200 1200 return data
1201 1201
1202 1202 def get_api_data(self, include_secrets=False):
1203 1203 data = self.__json__()
1204 1204 if include_secrets:
1205 1205 return data
1206 1206 else:
1207 1207 data['auth_token'] = self.token_obfuscated
1208 1208 return data
1209 1209
1210 1210 @hybrid_property
1211 1211 def description_safe(self):
1212 1212 from rhodecode.lib import helpers as h
1213 1213 return h.escape(self.description)
1214 1214
1215 1215 @property
1216 1216 def expired(self):
1217 1217 if self.expires == -1:
1218 1218 return False
1219 1219 return time.time() > self.expires
1220 1220
1221 1221 @classmethod
1222 1222 def _get_role_name(cls, role):
1223 1223 return {
1224 1224 cls.ROLE_ALL: _('all'),
1225 1225 cls.ROLE_HTTP: _('http/web interface'),
1226 1226 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1227 1227 cls.ROLE_API: _('api calls'),
1228 1228 cls.ROLE_FEED: _('feed access'),
1229 1229 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1230 1230 }.get(role, role)
1231 1231
1232 1232 @classmethod
1233 1233 def _get_role_description(cls, role):
1234 1234 return {
1235 1235 cls.ROLE_ALL: _('Token for all actions.'),
1236 1236 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1237 1237 'login using `api_access_controllers_whitelist` functionality.'),
1238 1238 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1239 1239 'Requires auth_token authentication plugin to be active. <br/>'
1240 1240 'Such Token should be used then instead of a password to '
1241 1241 'interact with a repository, and additionally can be '
1242 1242 'limited to single repository using repo scope.'),
1243 1243 cls.ROLE_API: _('Token limited to api calls.'),
1244 1244 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1245 1245 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1246 1246 }.get(role, role)
1247 1247
1248 1248 @property
1249 1249 def role_humanized(self):
1250 1250 return self._get_role_name(self.role)
1251 1251
1252 1252 def _get_scope(self):
1253 1253 if self.repo:
1254 1254 return 'Repository: {}'.format(self.repo.repo_name)
1255 1255 if self.repo_group:
1256 1256 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1257 1257 return 'Global'
1258 1258
1259 1259 @property
1260 1260 def scope_humanized(self):
1261 1261 return self._get_scope()
1262 1262
1263 1263 @property
1264 1264 def token_obfuscated(self):
1265 1265 if self.api_key:
1266 1266 return self.api_key[:4] + "****"
1267 1267
1268 1268
1269 1269 class UserEmailMap(Base, BaseModel):
1270 1270 __tablename__ = 'user_email_map'
1271 1271 __table_args__ = (
1272 1272 Index('uem_email_idx', 'email'),
1273 1273 UniqueConstraint('email'),
1274 1274 base_table_args
1275 1275 )
1276 1276 __mapper_args__ = {}
1277 1277
1278 1278 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1279 1279 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1280 1280 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1281 1281 user = relationship('User', lazy='joined')
1282 1282
1283 1283 @validates('_email')
1284 1284 def validate_email(self, key, email):
1285 1285 # check if this email is not main one
1286 1286 main_email = Session().query(User).filter(User.email == email).scalar()
1287 1287 if main_email is not None:
1288 1288 raise AttributeError('email %s is present is user table' % email)
1289 1289 return email
1290 1290
1291 1291 @hybrid_property
1292 1292 def email(self):
1293 1293 return self._email
1294 1294
1295 1295 @email.setter
1296 1296 def email(self, val):
1297 1297 self._email = val.lower() if val else None
1298 1298
1299 1299
1300 1300 class UserIpMap(Base, BaseModel):
1301 1301 __tablename__ = 'user_ip_map'
1302 1302 __table_args__ = (
1303 1303 UniqueConstraint('user_id', 'ip_addr'),
1304 1304 base_table_args
1305 1305 )
1306 1306 __mapper_args__ = {}
1307 1307
1308 1308 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1309 1309 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1310 1310 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1311 1311 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1312 1312 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1313 1313 user = relationship('User', lazy='joined')
1314 1314
1315 1315 @hybrid_property
1316 1316 def description_safe(self):
1317 1317 from rhodecode.lib import helpers as h
1318 1318 return h.escape(self.description)
1319 1319
1320 1320 @classmethod
1321 1321 def _get_ip_range(cls, ip_addr):
1322 1322 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1323 1323 return [str(net.network_address), str(net.broadcast_address)]
1324 1324
1325 1325 def __json__(self):
1326 1326 return {
1327 1327 'ip_addr': self.ip_addr,
1328 1328 'ip_range': self._get_ip_range(self.ip_addr),
1329 1329 }
1330 1330
1331 1331 def __unicode__(self):
1332 1332 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1333 1333 self.user_id, self.ip_addr)
1334 1334
1335 1335
1336 1336 class UserSshKeys(Base, BaseModel):
1337 1337 __tablename__ = 'user_ssh_keys'
1338 1338 __table_args__ = (
1339 1339 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1340 1340
1341 1341 UniqueConstraint('ssh_key_fingerprint'),
1342 1342
1343 1343 base_table_args
1344 1344 )
1345 1345 __mapper_args__ = {}
1346 1346
1347 1347 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1348 1348 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1349 1349 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1350 1350
1351 1351 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1352 1352
1353 1353 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1354 1354 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1355 1355 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1356 1356
1357 1357 user = relationship('User', lazy='joined')
1358 1358
1359 1359 def __json__(self):
1360 1360 data = {
1361 1361 'ssh_fingerprint': self.ssh_key_fingerprint,
1362 1362 'description': self.description,
1363 1363 'created_on': self.created_on
1364 1364 }
1365 1365 return data
1366 1366
1367 1367 def get_api_data(self):
1368 1368 data = self.__json__()
1369 1369 return data
1370 1370
1371 1371
1372 1372 class UserLog(Base, BaseModel):
1373 1373 __tablename__ = 'user_logs'
1374 1374 __table_args__ = (
1375 1375 base_table_args,
1376 1376 )
1377 1377
1378 1378 VERSION_1 = 'v1'
1379 1379 VERSION_2 = 'v2'
1380 1380 VERSIONS = [VERSION_1, VERSION_2]
1381 1381
1382 1382 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1383 1383 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1384 1384 username = Column("username", String(255), nullable=True, unique=None, default=None)
1385 1385 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1386 1386 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1387 1387 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1388 1388 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1389 1389 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1390 1390
1391 1391 version = Column("version", String(255), nullable=True, default=VERSION_1)
1392 1392 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1393 1393 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1394 1394
1395 1395 def __unicode__(self):
1396 1396 return u"<%s('id:%s:%s')>" % (
1397 1397 self.__class__.__name__, self.repository_name, self.action)
1398 1398
1399 1399 def __json__(self):
1400 1400 return {
1401 1401 'user_id': self.user_id,
1402 1402 'username': self.username,
1403 1403 'repository_id': self.repository_id,
1404 1404 'repository_name': self.repository_name,
1405 1405 'user_ip': self.user_ip,
1406 1406 'action_date': self.action_date,
1407 1407 'action': self.action,
1408 1408 }
1409 1409
1410 1410 @hybrid_property
1411 1411 def entry_id(self):
1412 1412 return self.user_log_id
1413 1413
1414 1414 @property
1415 1415 def action_as_day(self):
1416 1416 return datetime.date(*self.action_date.timetuple()[:3])
1417 1417
1418 1418 user = relationship('User')
1419 1419 repository = relationship('Repository', cascade='')
1420 1420
1421 1421
1422 1422 class UserGroup(Base, BaseModel):
1423 1423 __tablename__ = 'users_groups'
1424 1424 __table_args__ = (
1425 1425 base_table_args,
1426 1426 )
1427 1427
1428 1428 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1429 1429 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1430 1430 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1431 1431 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1432 1432 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1433 1433 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1434 1434 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1435 1435 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1436 1436
1437 1437 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1438 1438 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1439 1439 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1440 1440 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1441 1441 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1442 1442 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1443 1443
1444 1444 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1445 1445 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1446 1446
1447 1447 @classmethod
1448 1448 def _load_group_data(cls, column):
1449 1449 if not column:
1450 1450 return {}
1451 1451
1452 1452 try:
1453 1453 return json.loads(column) or {}
1454 1454 except TypeError:
1455 1455 return {}
1456 1456
1457 1457 @hybrid_property
1458 1458 def description_safe(self):
1459 1459 from rhodecode.lib import helpers as h
1460 1460 return h.escape(self.user_group_description)
1461 1461
1462 1462 @hybrid_property
1463 1463 def group_data(self):
1464 1464 return self._load_group_data(self._group_data)
1465 1465
1466 1466 @group_data.expression
1467 1467 def group_data(self, **kwargs):
1468 1468 return self._group_data
1469 1469
1470 1470 @group_data.setter
1471 1471 def group_data(self, val):
1472 1472 try:
1473 1473 self._group_data = json.dumps(val)
1474 1474 except Exception:
1475 1475 log.error(traceback.format_exc())
1476 1476
1477 1477 @classmethod
1478 1478 def _load_sync(cls, group_data):
1479 1479 if group_data:
1480 1480 return group_data.get('extern_type')
1481 1481
1482 1482 @property
1483 1483 def sync(self):
1484 1484 return self._load_sync(self.group_data)
1485 1485
1486 1486 def __unicode__(self):
1487 1487 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1488 1488 self.users_group_id,
1489 1489 self.users_group_name)
1490 1490
1491 1491 @classmethod
1492 1492 def get_by_group_name(cls, group_name, cache=False,
1493 1493 case_insensitive=False):
1494 1494 if case_insensitive:
1495 1495 q = cls.query().filter(func.lower(cls.users_group_name) ==
1496 1496 func.lower(group_name))
1497 1497
1498 1498 else:
1499 1499 q = cls.query().filter(cls.users_group_name == group_name)
1500 1500 if cache:
1501 1501 q = q.options(
1502 1502 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1503 1503 return q.scalar()
1504 1504
1505 1505 @classmethod
1506 1506 def get(cls, user_group_id, cache=False):
1507 1507 if not user_group_id:
1508 1508 return
1509 1509
1510 1510 user_group = cls.query()
1511 1511 if cache:
1512 1512 user_group = user_group.options(
1513 1513 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1514 1514 return user_group.get(user_group_id)
1515 1515
1516 1516 def permissions(self, with_admins=True, with_owner=True,
1517 1517 expand_from_user_groups=False):
1518 1518 """
1519 1519 Permissions for user groups
1520 1520 """
1521 1521 _admin_perm = 'usergroup.admin'
1522 1522
1523 1523 owner_row = []
1524 1524 if with_owner:
1525 1525 usr = AttributeDict(self.user.get_dict())
1526 1526 usr.owner_row = True
1527 1527 usr.permission = _admin_perm
1528 1528 owner_row.append(usr)
1529 1529
1530 1530 super_admin_ids = []
1531 1531 super_admin_rows = []
1532 1532 if with_admins:
1533 1533 for usr in User.get_all_super_admins():
1534 1534 super_admin_ids.append(usr.user_id)
1535 1535 # if this admin is also owner, don't double the record
1536 1536 if usr.user_id == owner_row[0].user_id:
1537 1537 owner_row[0].admin_row = True
1538 1538 else:
1539 1539 usr = AttributeDict(usr.get_dict())
1540 1540 usr.admin_row = True
1541 1541 usr.permission = _admin_perm
1542 1542 super_admin_rows.append(usr)
1543 1543
1544 1544 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1545 1545 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1546 1546 joinedload(UserUserGroupToPerm.user),
1547 1547 joinedload(UserUserGroupToPerm.permission),)
1548 1548
1549 1549 # get owners and admins and permissions. We do a trick of re-writing
1550 1550 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1551 1551 # has a global reference and changing one object propagates to all
1552 1552 # others. This means if admin is also an owner admin_row that change
1553 1553 # would propagate to both objects
1554 1554 perm_rows = []
1555 1555 for _usr in q.all():
1556 1556 usr = AttributeDict(_usr.user.get_dict())
1557 1557 # if this user is also owner/admin, mark as duplicate record
1558 1558 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1559 1559 usr.duplicate_perm = True
1560 1560 usr.permission = _usr.permission.permission_name
1561 1561 perm_rows.append(usr)
1562 1562
1563 1563 # filter the perm rows by 'default' first and then sort them by
1564 1564 # admin,write,read,none permissions sorted again alphabetically in
1565 1565 # each group
1566 1566 perm_rows = sorted(perm_rows, key=display_user_sort)
1567 1567
1568 1568 user_groups_rows = []
1569 1569 if expand_from_user_groups:
1570 1570 for ug in self.permission_user_groups(with_members=True):
1571 1571 for user_data in ug.members:
1572 1572 user_groups_rows.append(user_data)
1573 1573
1574 1574 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1575 1575
1576 1576 def permission_user_groups(self, with_members=False):
1577 1577 q = UserGroupUserGroupToPerm.query()\
1578 1578 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1579 1579 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1580 1580 joinedload(UserGroupUserGroupToPerm.target_user_group),
1581 1581 joinedload(UserGroupUserGroupToPerm.permission),)
1582 1582
1583 1583 perm_rows = []
1584 1584 for _user_group in q.all():
1585 1585 entry = AttributeDict(_user_group.user_group.get_dict())
1586 1586 entry.permission = _user_group.permission.permission_name
1587 1587 if with_members:
1588 1588 entry.members = [x.user.get_dict()
1589 1589 for x in _user_group.user_group.members]
1590 1590 perm_rows.append(entry)
1591 1591
1592 1592 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1593 1593 return perm_rows
1594 1594
1595 1595 def _get_default_perms(self, user_group, suffix=''):
1596 1596 from rhodecode.model.permission import PermissionModel
1597 1597 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1598 1598
1599 1599 def get_default_perms(self, suffix=''):
1600 1600 return self._get_default_perms(self, suffix)
1601 1601
1602 1602 def get_api_data(self, with_group_members=True, include_secrets=False):
1603 1603 """
1604 1604 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1605 1605 basically forwarded.
1606 1606
1607 1607 """
1608 1608 user_group = self
1609 1609 data = {
1610 1610 'users_group_id': user_group.users_group_id,
1611 1611 'group_name': user_group.users_group_name,
1612 1612 'group_description': user_group.user_group_description,
1613 1613 'active': user_group.users_group_active,
1614 1614 'owner': user_group.user.username,
1615 1615 'sync': user_group.sync,
1616 1616 'owner_email': user_group.user.email,
1617 1617 }
1618 1618
1619 1619 if with_group_members:
1620 1620 users = []
1621 1621 for user in user_group.members:
1622 1622 user = user.user
1623 1623 users.append(user.get_api_data(include_secrets=include_secrets))
1624 1624 data['users'] = users
1625 1625
1626 1626 return data
1627 1627
1628 1628
1629 1629 class UserGroupMember(Base, BaseModel):
1630 1630 __tablename__ = 'users_groups_members'
1631 1631 __table_args__ = (
1632 1632 base_table_args,
1633 1633 )
1634 1634
1635 1635 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1636 1636 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1637 1637 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1638 1638
1639 1639 user = relationship('User', lazy='joined')
1640 1640 users_group = relationship('UserGroup')
1641 1641
1642 1642 def __init__(self, gr_id='', u_id=''):
1643 1643 self.users_group_id = gr_id
1644 1644 self.user_id = u_id
1645 1645
1646 1646
1647 1647 class RepositoryField(Base, BaseModel):
1648 1648 __tablename__ = 'repositories_fields'
1649 1649 __table_args__ = (
1650 1650 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1651 1651 base_table_args,
1652 1652 )
1653 1653
1654 1654 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1655 1655
1656 1656 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1657 1657 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1658 1658 field_key = Column("field_key", String(250))
1659 1659 field_label = Column("field_label", String(1024), nullable=False)
1660 1660 field_value = Column("field_value", String(10000), nullable=False)
1661 1661 field_desc = Column("field_desc", String(1024), nullable=False)
1662 1662 field_type = Column("field_type", String(255), nullable=False, unique=None)
1663 1663 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1664 1664
1665 1665 repository = relationship('Repository')
1666 1666
1667 1667 @property
1668 1668 def field_key_prefixed(self):
1669 1669 return 'ex_%s' % self.field_key
1670 1670
1671 1671 @classmethod
1672 1672 def un_prefix_key(cls, key):
1673 1673 if key.startswith(cls.PREFIX):
1674 1674 return key[len(cls.PREFIX):]
1675 1675 return key
1676 1676
1677 1677 @classmethod
1678 1678 def get_by_key_name(cls, key, repo):
1679 1679 row = cls.query()\
1680 1680 .filter(cls.repository == repo)\
1681 1681 .filter(cls.field_key == key).scalar()
1682 1682 return row
1683 1683
1684 1684
1685 1685 class Repository(Base, BaseModel):
1686 1686 __tablename__ = 'repositories'
1687 1687 __table_args__ = (
1688 1688 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1689 1689 base_table_args,
1690 1690 )
1691 1691 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1692 1692 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1693 1693 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1694 1694
1695 1695 STATE_CREATED = 'repo_state_created'
1696 1696 STATE_PENDING = 'repo_state_pending'
1697 1697 STATE_ERROR = 'repo_state_error'
1698 1698
1699 1699 LOCK_AUTOMATIC = 'lock_auto'
1700 1700 LOCK_API = 'lock_api'
1701 1701 LOCK_WEB = 'lock_web'
1702 1702 LOCK_PULL = 'lock_pull'
1703 1703
1704 1704 NAME_SEP = URL_SEP
1705 1705
1706 1706 repo_id = Column(
1707 1707 "repo_id", Integer(), nullable=False, unique=True, default=None,
1708 1708 primary_key=True)
1709 1709 _repo_name = Column(
1710 1710 "repo_name", Text(), nullable=False, default=None)
1711 1711 repo_name_hash = Column(
1712 1712 "repo_name_hash", String(255), nullable=False, unique=True)
1713 1713 repo_state = Column("repo_state", String(255), nullable=True)
1714 1714
1715 1715 clone_uri = Column(
1716 1716 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1717 1717 default=None)
1718 1718 push_uri = Column(
1719 1719 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1720 1720 default=None)
1721 1721 repo_type = Column(
1722 1722 "repo_type", String(255), nullable=False, unique=False, default=None)
1723 1723 user_id = Column(
1724 1724 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1725 1725 unique=False, default=None)
1726 1726 private = Column(
1727 1727 "private", Boolean(), nullable=True, unique=None, default=None)
1728 1728 archived = Column(
1729 1729 "archived", Boolean(), nullable=True, unique=None, default=None)
1730 1730 enable_statistics = Column(
1731 1731 "statistics", Boolean(), nullable=True, unique=None, default=True)
1732 1732 enable_downloads = Column(
1733 1733 "downloads", Boolean(), nullable=True, unique=None, default=True)
1734 1734 description = Column(
1735 1735 "description", String(10000), nullable=True, unique=None, default=None)
1736 1736 created_on = Column(
1737 1737 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1738 1738 default=datetime.datetime.now)
1739 1739 updated_on = Column(
1740 1740 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1741 1741 default=datetime.datetime.now)
1742 1742 _landing_revision = Column(
1743 1743 "landing_revision", String(255), nullable=False, unique=False,
1744 1744 default=None)
1745 1745 enable_locking = Column(
1746 1746 "enable_locking", Boolean(), nullable=False, unique=None,
1747 1747 default=False)
1748 1748 _locked = Column(
1749 1749 "locked", String(255), nullable=True, unique=False, default=None)
1750 1750 _changeset_cache = Column(
1751 1751 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1752 1752
1753 1753 fork_id = Column(
1754 1754 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1755 1755 nullable=True, unique=False, default=None)
1756 1756 group_id = Column(
1757 1757 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1758 1758 unique=False, default=None)
1759 1759
1760 1760 user = relationship('User', lazy='joined')
1761 1761 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1762 1762 group = relationship('RepoGroup', lazy='joined')
1763 1763 repo_to_perm = relationship(
1764 1764 'UserRepoToPerm', cascade='all',
1765 1765 order_by='UserRepoToPerm.repo_to_perm_id')
1766 1766 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1767 1767 stats = relationship('Statistics', cascade='all', uselist=False)
1768 1768
1769 1769 followers = relationship(
1770 1770 'UserFollowing',
1771 1771 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1772 1772 cascade='all')
1773 1773 extra_fields = relationship(
1774 1774 'RepositoryField', cascade="all, delete-orphan")
1775 1775 logs = relationship('UserLog')
1776 1776 comments = relationship(
1777 1777 'ChangesetComment', cascade="all, delete-orphan")
1778 1778 pull_requests_source = relationship(
1779 1779 'PullRequest',
1780 1780 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1781 1781 cascade="all, delete-orphan")
1782 1782 pull_requests_target = relationship(
1783 1783 'PullRequest',
1784 1784 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1785 1785 cascade="all, delete-orphan")
1786 1786 ui = relationship('RepoRhodeCodeUi', cascade="all")
1787 1787 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1788 1788 integrations = relationship('Integration', cascade="all, delete-orphan")
1789 1789
1790 1790 scoped_tokens = relationship('UserApiKeys', cascade="all")
1791 1791
1792 1792 # no cascade, set NULL
1793 1793 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1794 1794
1795 1795 def __unicode__(self):
1796 1796 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1797 1797 safe_unicode(self.repo_name))
1798 1798
1799 1799 @hybrid_property
1800 1800 def description_safe(self):
1801 1801 from rhodecode.lib import helpers as h
1802 1802 return h.escape(self.description)
1803 1803
1804 1804 @hybrid_property
1805 1805 def landing_rev(self):
1806 1806 # always should return [rev_type, rev], e.g ['branch', 'master']
1807 1807 if self._landing_revision:
1808 1808 _rev_info = self._landing_revision.split(':')
1809 1809 if len(_rev_info) < 2:
1810 1810 _rev_info.insert(0, 'rev')
1811 1811 return [_rev_info[0], _rev_info[1]]
1812 1812 return [None, None]
1813 1813
1814 1814 @property
1815 1815 def landing_ref_type(self):
1816 1816 return self.landing_rev[0]
1817 1817
1818 1818 @property
1819 1819 def landing_ref_name(self):
1820 1820 return self.landing_rev[1]
1821 1821
1822 1822 @landing_rev.setter
1823 1823 def landing_rev(self, val):
1824 1824 if ':' not in val:
1825 1825 raise ValueError('value must be delimited with `:` and consist '
1826 1826 'of <rev_type>:<rev>, got %s instead' % val)
1827 1827 self._landing_revision = val
1828 1828
1829 1829 @hybrid_property
1830 1830 def locked(self):
1831 1831 if self._locked:
1832 1832 user_id, timelocked, reason = self._locked.split(':')
1833 1833 lock_values = int(user_id), timelocked, reason
1834 1834 else:
1835 1835 lock_values = [None, None, None]
1836 1836 return lock_values
1837 1837
1838 1838 @locked.setter
1839 1839 def locked(self, val):
1840 1840 if val and isinstance(val, (list, tuple)):
1841 1841 self._locked = ':'.join(map(str, val))
1842 1842 else:
1843 1843 self._locked = None
1844 1844
1845 1845 @classmethod
1846 1846 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1847 1847 from rhodecode.lib.vcs.backends.base import EmptyCommit
1848 1848 dummy = EmptyCommit().__json__()
1849 1849 if not changeset_cache_raw:
1850 1850 dummy['source_repo_id'] = repo_id
1851 1851 return json.loads(json.dumps(dummy))
1852 1852
1853 1853 try:
1854 1854 return json.loads(changeset_cache_raw)
1855 1855 except TypeError:
1856 1856 return dummy
1857 1857 except Exception:
1858 1858 log.error(traceback.format_exc())
1859 1859 return dummy
1860 1860
1861 1861 @hybrid_property
1862 1862 def changeset_cache(self):
1863 1863 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1864 1864
1865 1865 @changeset_cache.setter
1866 1866 def changeset_cache(self, val):
1867 1867 try:
1868 1868 self._changeset_cache = json.dumps(val)
1869 1869 except Exception:
1870 1870 log.error(traceback.format_exc())
1871 1871
1872 1872 @hybrid_property
1873 1873 def repo_name(self):
1874 1874 return self._repo_name
1875 1875
1876 1876 @repo_name.setter
1877 1877 def repo_name(self, value):
1878 1878 self._repo_name = value
1879 1879 self.repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1880 1880
1881 1881 @classmethod
1882 1882 def normalize_repo_name(cls, repo_name):
1883 1883 """
1884 1884 Normalizes os specific repo_name to the format internally stored inside
1885 1885 database using URL_SEP
1886 1886
1887 1887 :param cls:
1888 1888 :param repo_name:
1889 1889 """
1890 1890 return cls.NAME_SEP.join(repo_name.split(os.sep))
1891 1891
1892 1892 @classmethod
1893 1893 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1894 1894 session = Session()
1895 1895 q = session.query(cls).filter(cls.repo_name == repo_name)
1896 1896
1897 1897 if cache:
1898 1898 if identity_cache:
1899 1899 val = cls.identity_cache(session, 'repo_name', repo_name)
1900 1900 if val:
1901 1901 return val
1902 1902 else:
1903 1903 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1904 1904 q = q.options(
1905 1905 FromCache("sql_cache_short", cache_key))
1906 1906
1907 1907 return q.scalar()
1908 1908
1909 1909 @classmethod
1910 1910 def get_by_id_or_repo_name(cls, repoid):
1911 1911 if isinstance(repoid, (int, long)):
1912 1912 try:
1913 1913 repo = cls.get(repoid)
1914 1914 except ValueError:
1915 1915 repo = None
1916 1916 else:
1917 1917 repo = cls.get_by_repo_name(repoid)
1918 1918 return repo
1919 1919
1920 1920 @classmethod
1921 1921 def get_by_full_path(cls, repo_full_path):
1922 1922 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1923 1923 repo_name = cls.normalize_repo_name(repo_name)
1924 1924 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1925 1925
1926 1926 @classmethod
1927 1927 def get_repo_forks(cls, repo_id):
1928 1928 return cls.query().filter(Repository.fork_id == repo_id)
1929 1929
1930 1930 @classmethod
1931 1931 def base_path(cls):
1932 1932 """
1933 1933 Returns base path when all repos are stored
1934 1934
1935 1935 :param cls:
1936 1936 """
1937 1937 q = Session().query(RhodeCodeUi)\
1938 1938 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1939 1939 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1940 1940 return q.one().ui_value
1941 1941
1942 1942 @classmethod
1943 1943 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1944 1944 case_insensitive=True, archived=False):
1945 1945 q = Repository.query()
1946 1946
1947 1947 if not archived:
1948 1948 q = q.filter(Repository.archived.isnot(true()))
1949 1949
1950 1950 if not isinstance(user_id, Optional):
1951 1951 q = q.filter(Repository.user_id == user_id)
1952 1952
1953 1953 if not isinstance(group_id, Optional):
1954 1954 q = q.filter(Repository.group_id == group_id)
1955 1955
1956 1956 if case_insensitive:
1957 1957 q = q.order_by(func.lower(Repository.repo_name))
1958 1958 else:
1959 1959 q = q.order_by(Repository.repo_name)
1960 1960
1961 1961 return q.all()
1962 1962
1963 1963 @property
1964 1964 def repo_uid(self):
1965 1965 return '_{}'.format(self.repo_id)
1966 1966
1967 1967 @property
1968 1968 def forks(self):
1969 1969 """
1970 1970 Return forks of this repo
1971 1971 """
1972 1972 return Repository.get_repo_forks(self.repo_id)
1973 1973
1974 1974 @property
1975 1975 def parent(self):
1976 1976 """
1977 1977 Returns fork parent
1978 1978 """
1979 1979 return self.fork
1980 1980
1981 1981 @property
1982 1982 def just_name(self):
1983 1983 return self.repo_name.split(self.NAME_SEP)[-1]
1984 1984
1985 1985 @property
1986 1986 def groups_with_parents(self):
1987 1987 groups = []
1988 1988 if self.group is None:
1989 1989 return groups
1990 1990
1991 1991 cur_gr = self.group
1992 1992 groups.insert(0, cur_gr)
1993 1993 while 1:
1994 1994 gr = getattr(cur_gr, 'parent_group', None)
1995 1995 cur_gr = cur_gr.parent_group
1996 1996 if gr is None:
1997 1997 break
1998 1998 groups.insert(0, gr)
1999 1999
2000 2000 return groups
2001 2001
2002 2002 @property
2003 2003 def groups_and_repo(self):
2004 2004 return self.groups_with_parents, self
2005 2005
2006 2006 @LazyProperty
2007 2007 def repo_path(self):
2008 2008 """
2009 2009 Returns base full path for that repository means where it actually
2010 2010 exists on a filesystem
2011 2011 """
2012 2012 q = Session().query(RhodeCodeUi).filter(
2013 2013 RhodeCodeUi.ui_key == self.NAME_SEP)
2014 2014 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2015 2015 return q.one().ui_value
2016 2016
2017 2017 @property
2018 2018 def repo_full_path(self):
2019 2019 p = [self.repo_path]
2020 2020 # we need to split the name by / since this is how we store the
2021 2021 # names in the database, but that eventually needs to be converted
2022 2022 # into a valid system path
2023 2023 p += self.repo_name.split(self.NAME_SEP)
2024 2024 return os.path.join(*map(safe_unicode, p))
2025 2025
2026 2026 @property
2027 2027 def cache_keys(self):
2028 2028 """
2029 2029 Returns associated cache keys for that repo
2030 2030 """
2031 2031 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2032 2032 repo_id=self.repo_id)
2033 2033 return CacheKey.query()\
2034 2034 .filter(CacheKey.cache_args == invalidation_namespace)\
2035 2035 .order_by(CacheKey.cache_key)\
2036 2036 .all()
2037 2037
2038 2038 @property
2039 2039 def cached_diffs_relative_dir(self):
2040 2040 """
2041 2041 Return a relative to the repository store path of cached diffs
2042 2042 used for safe display for users, who shouldn't know the absolute store
2043 2043 path
2044 2044 """
2045 2045 return os.path.join(
2046 2046 os.path.dirname(self.repo_name),
2047 2047 self.cached_diffs_dir.split(os.path.sep)[-1])
2048 2048
2049 2049 @property
2050 2050 def cached_diffs_dir(self):
2051 2051 path = self.repo_full_path
2052 2052 return os.path.join(
2053 2053 os.path.dirname(path),
2054 2054 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
2055 2055
2056 2056 def cached_diffs(self):
2057 2057 diff_cache_dir = self.cached_diffs_dir
2058 2058 if os.path.isdir(diff_cache_dir):
2059 2059 return os.listdir(diff_cache_dir)
2060 2060 return []
2061 2061
2062 2062 def shadow_repos(self):
2063 2063 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
2064 2064 return [
2065 2065 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2066 2066 if x.startswith(shadow_repos_pattern)]
2067 2067
2068 2068 def get_new_name(self, repo_name):
2069 2069 """
2070 2070 returns new full repository name based on assigned group and new new
2071 2071
2072 2072 :param group_name:
2073 2073 """
2074 2074 path_prefix = self.group.full_path_splitted if self.group else []
2075 2075 return self.NAME_SEP.join(path_prefix + [repo_name])
2076 2076
2077 2077 @property
2078 2078 def _config(self):
2079 2079 """
2080 2080 Returns db based config object.
2081 2081 """
2082 2082 from rhodecode.lib.utils import make_db_config
2083 2083 return make_db_config(clear_session=False, repo=self)
2084 2084
2085 2085 def permissions(self, with_admins=True, with_owner=True,
2086 2086 expand_from_user_groups=False):
2087 2087 """
2088 2088 Permissions for repositories
2089 2089 """
2090 2090 _admin_perm = 'repository.admin'
2091 2091
2092 2092 owner_row = []
2093 2093 if with_owner:
2094 2094 usr = AttributeDict(self.user.get_dict())
2095 2095 usr.owner_row = True
2096 2096 usr.permission = _admin_perm
2097 2097 usr.permission_id = None
2098 2098 owner_row.append(usr)
2099 2099
2100 2100 super_admin_ids = []
2101 2101 super_admin_rows = []
2102 2102 if with_admins:
2103 2103 for usr in User.get_all_super_admins():
2104 2104 super_admin_ids.append(usr.user_id)
2105 2105 # if this admin is also owner, don't double the record
2106 2106 if usr.user_id == owner_row[0].user_id:
2107 2107 owner_row[0].admin_row = True
2108 2108 else:
2109 2109 usr = AttributeDict(usr.get_dict())
2110 2110 usr.admin_row = True
2111 2111 usr.permission = _admin_perm
2112 2112 usr.permission_id = None
2113 2113 super_admin_rows.append(usr)
2114 2114
2115 2115 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2116 2116 q = q.options(joinedload(UserRepoToPerm.repository),
2117 2117 joinedload(UserRepoToPerm.user),
2118 2118 joinedload(UserRepoToPerm.permission),)
2119 2119
2120 2120 # get owners and admins and permissions. We do a trick of re-writing
2121 2121 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2122 2122 # has a global reference and changing one object propagates to all
2123 2123 # others. This means if admin is also an owner admin_row that change
2124 2124 # would propagate to both objects
2125 2125 perm_rows = []
2126 2126 for _usr in q.all():
2127 2127 usr = AttributeDict(_usr.user.get_dict())
2128 2128 # if this user is also owner/admin, mark as duplicate record
2129 2129 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2130 2130 usr.duplicate_perm = True
2131 2131 # also check if this permission is maybe used by branch_permissions
2132 2132 if _usr.branch_perm_entry:
2133 2133 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2134 2134
2135 2135 usr.permission = _usr.permission.permission_name
2136 2136 usr.permission_id = _usr.repo_to_perm_id
2137 2137 perm_rows.append(usr)
2138 2138
2139 2139 # filter the perm rows by 'default' first and then sort them by
2140 2140 # admin,write,read,none permissions sorted again alphabetically in
2141 2141 # each group
2142 2142 perm_rows = sorted(perm_rows, key=display_user_sort)
2143 2143
2144 2144 user_groups_rows = []
2145 2145 if expand_from_user_groups:
2146 2146 for ug in self.permission_user_groups(with_members=True):
2147 2147 for user_data in ug.members:
2148 2148 user_groups_rows.append(user_data)
2149 2149
2150 2150 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2151 2151
2152 2152 def permission_user_groups(self, with_members=True):
2153 2153 q = UserGroupRepoToPerm.query()\
2154 2154 .filter(UserGroupRepoToPerm.repository == self)
2155 2155 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2156 2156 joinedload(UserGroupRepoToPerm.users_group),
2157 2157 joinedload(UserGroupRepoToPerm.permission),)
2158 2158
2159 2159 perm_rows = []
2160 2160 for _user_group in q.all():
2161 2161 entry = AttributeDict(_user_group.users_group.get_dict())
2162 2162 entry.permission = _user_group.permission.permission_name
2163 2163 if with_members:
2164 2164 entry.members = [x.user.get_dict()
2165 2165 for x in _user_group.users_group.members]
2166 2166 perm_rows.append(entry)
2167 2167
2168 2168 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2169 2169 return perm_rows
2170 2170
2171 2171 def get_api_data(self, include_secrets=False):
2172 2172 """
2173 2173 Common function for generating repo api data
2174 2174
2175 2175 :param include_secrets: See :meth:`User.get_api_data`.
2176 2176
2177 2177 """
2178 2178 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2179 2179 # move this methods on models level.
2180 2180 from rhodecode.model.settings import SettingsModel
2181 2181 from rhodecode.model.repo import RepoModel
2182 2182
2183 2183 repo = self
2184 2184 _user_id, _time, _reason = self.locked
2185 2185
2186 2186 data = {
2187 2187 'repo_id': repo.repo_id,
2188 2188 'repo_name': repo.repo_name,
2189 2189 'repo_type': repo.repo_type,
2190 2190 'clone_uri': repo.clone_uri or '',
2191 2191 'push_uri': repo.push_uri or '',
2192 2192 'url': RepoModel().get_url(self),
2193 2193 'private': repo.private,
2194 2194 'created_on': repo.created_on,
2195 2195 'description': repo.description_safe,
2196 2196 'landing_rev': repo.landing_rev,
2197 2197 'owner': repo.user.username,
2198 2198 'fork_of': repo.fork.repo_name if repo.fork else None,
2199 2199 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2200 2200 'enable_statistics': repo.enable_statistics,
2201 2201 'enable_locking': repo.enable_locking,
2202 2202 'enable_downloads': repo.enable_downloads,
2203 2203 'last_changeset': repo.changeset_cache,
2204 2204 'locked_by': User.get(_user_id).get_api_data(
2205 2205 include_secrets=include_secrets) if _user_id else None,
2206 2206 'locked_date': time_to_datetime(_time) if _time else None,
2207 2207 'lock_reason': _reason if _reason else None,
2208 2208 }
2209 2209
2210 2210 # TODO: mikhail: should be per-repo settings here
2211 2211 rc_config = SettingsModel().get_all_settings()
2212 2212 repository_fields = str2bool(
2213 2213 rc_config.get('rhodecode_repository_fields'))
2214 2214 if repository_fields:
2215 2215 for f in self.extra_fields:
2216 2216 data[f.field_key_prefixed] = f.field_value
2217 2217
2218 2218 return data
2219 2219
2220 2220 @classmethod
2221 2221 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2222 2222 if not lock_time:
2223 2223 lock_time = time.time()
2224 2224 if not lock_reason:
2225 2225 lock_reason = cls.LOCK_AUTOMATIC
2226 2226 repo.locked = [user_id, lock_time, lock_reason]
2227 2227 Session().add(repo)
2228 2228 Session().commit()
2229 2229
2230 2230 @classmethod
2231 2231 def unlock(cls, repo):
2232 2232 repo.locked = None
2233 2233 Session().add(repo)
2234 2234 Session().commit()
2235 2235
2236 2236 @classmethod
2237 2237 def getlock(cls, repo):
2238 2238 return repo.locked
2239 2239
2240 2240 def is_user_lock(self, user_id):
2241 2241 if self.lock[0]:
2242 2242 lock_user_id = safe_int(self.lock[0])
2243 2243 user_id = safe_int(user_id)
2244 2244 # both are ints, and they are equal
2245 2245 return all([lock_user_id, user_id]) and lock_user_id == user_id
2246 2246
2247 2247 return False
2248 2248
2249 2249 def get_locking_state(self, action, user_id, only_when_enabled=True):
2250 2250 """
2251 2251 Checks locking on this repository, if locking is enabled and lock is
2252 2252 present returns a tuple of make_lock, locked, locked_by.
2253 2253 make_lock can have 3 states None (do nothing) True, make lock
2254 2254 False release lock, This value is later propagated to hooks, which
2255 2255 do the locking. Think about this as signals passed to hooks what to do.
2256 2256
2257 2257 """
2258 2258 # TODO: johbo: This is part of the business logic and should be moved
2259 2259 # into the RepositoryModel.
2260 2260
2261 2261 if action not in ('push', 'pull'):
2262 2262 raise ValueError("Invalid action value: %s" % repr(action))
2263 2263
2264 2264 # defines if locked error should be thrown to user
2265 2265 currently_locked = False
2266 2266 # defines if new lock should be made, tri-state
2267 2267 make_lock = None
2268 2268 repo = self
2269 2269 user = User.get(user_id)
2270 2270
2271 2271 lock_info = repo.locked
2272 2272
2273 2273 if repo and (repo.enable_locking or not only_when_enabled):
2274 2274 if action == 'push':
2275 2275 # check if it's already locked !, if it is compare users
2276 2276 locked_by_user_id = lock_info[0]
2277 2277 if user.user_id == locked_by_user_id:
2278 2278 log.debug(
2279 2279 'Got `push` action from user %s, now unlocking', user)
2280 2280 # unlock if we have push from user who locked
2281 2281 make_lock = False
2282 2282 else:
2283 2283 # we're not the same user who locked, ban with
2284 2284 # code defined in settings (default is 423 HTTP Locked) !
2285 2285 log.debug('Repo %s is currently locked by %s', repo, user)
2286 2286 currently_locked = True
2287 2287 elif action == 'pull':
2288 2288 # [0] user [1] date
2289 2289 if lock_info[0] and lock_info[1]:
2290 2290 log.debug('Repo %s is currently locked by %s', repo, user)
2291 2291 currently_locked = True
2292 2292 else:
2293 2293 log.debug('Setting lock on repo %s by %s', repo, user)
2294 2294 make_lock = True
2295 2295
2296 2296 else:
2297 2297 log.debug('Repository %s do not have locking enabled', repo)
2298 2298
2299 2299 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2300 2300 make_lock, currently_locked, lock_info)
2301 2301
2302 2302 from rhodecode.lib.auth import HasRepoPermissionAny
2303 2303 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2304 2304 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2305 2305 # if we don't have at least write permission we cannot make a lock
2306 2306 log.debug('lock state reset back to FALSE due to lack '
2307 2307 'of at least read permission')
2308 2308 make_lock = False
2309 2309
2310 2310 return make_lock, currently_locked, lock_info
2311 2311
2312 2312 @property
2313 2313 def last_commit_cache_update_diff(self):
2314 2314 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2315 2315
2316 2316 @classmethod
2317 2317 def _load_commit_change(cls, last_commit_cache):
2318 2318 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2319 2319 empty_date = datetime.datetime.fromtimestamp(0)
2320 2320 date_latest = last_commit_cache.get('date', empty_date)
2321 2321 try:
2322 2322 return parse_datetime(date_latest)
2323 2323 except Exception:
2324 2324 return empty_date
2325 2325
2326 2326 @property
2327 2327 def last_commit_change(self):
2328 2328 return self._load_commit_change(self.changeset_cache)
2329 2329
2330 2330 @property
2331 2331 def last_db_change(self):
2332 2332 return self.updated_on
2333 2333
2334 2334 @property
2335 2335 def clone_uri_hidden(self):
2336 2336 clone_uri = self.clone_uri
2337 2337 if clone_uri:
2338 2338 import urlobject
2339 2339 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2340 2340 if url_obj.password:
2341 2341 clone_uri = url_obj.with_password('*****')
2342 2342 return clone_uri
2343 2343
2344 2344 @property
2345 2345 def push_uri_hidden(self):
2346 2346 push_uri = self.push_uri
2347 2347 if push_uri:
2348 2348 import urlobject
2349 2349 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2350 2350 if url_obj.password:
2351 2351 push_uri = url_obj.with_password('*****')
2352 2352 return push_uri
2353 2353
2354 2354 def clone_url(self, **override):
2355 2355 from rhodecode.model.settings import SettingsModel
2356 2356
2357 2357 uri_tmpl = None
2358 2358 if 'with_id' in override:
2359 2359 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2360 2360 del override['with_id']
2361 2361
2362 2362 if 'uri_tmpl' in override:
2363 2363 uri_tmpl = override['uri_tmpl']
2364 2364 del override['uri_tmpl']
2365 2365
2366 2366 ssh = False
2367 2367 if 'ssh' in override:
2368 2368 ssh = True
2369 2369 del override['ssh']
2370 2370
2371 2371 # we didn't override our tmpl from **overrides
2372 2372 request = get_current_request()
2373 2373 if not uri_tmpl:
2374 2374 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2375 2375 rc_config = request.call_context.rc_config
2376 2376 else:
2377 2377 rc_config = SettingsModel().get_all_settings(cache=True)
2378 2378
2379 2379 if ssh:
2380 2380 uri_tmpl = rc_config.get(
2381 2381 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2382 2382
2383 2383 else:
2384 2384 uri_tmpl = rc_config.get(
2385 2385 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2386 2386
2387 2387 return get_clone_url(request=request,
2388 2388 uri_tmpl=uri_tmpl,
2389 2389 repo_name=self.repo_name,
2390 2390 repo_id=self.repo_id,
2391 2391 repo_type=self.repo_type,
2392 2392 **override)
2393 2393
2394 2394 def set_state(self, state):
2395 2395 self.repo_state = state
2396 2396 Session().add(self)
2397 2397 #==========================================================================
2398 2398 # SCM PROPERTIES
2399 2399 #==========================================================================
2400 2400
2401 2401 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False):
2402 2402 return get_commit_safe(
2403 2403 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2404 2404 maybe_unreachable=maybe_unreachable)
2405 2405
2406 2406 def get_changeset(self, rev=None, pre_load=None):
2407 2407 warnings.warn("Use get_commit", DeprecationWarning)
2408 2408 commit_id = None
2409 2409 commit_idx = None
2410 2410 if isinstance(rev, compat.string_types):
2411 2411 commit_id = rev
2412 2412 else:
2413 2413 commit_idx = rev
2414 2414 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2415 2415 pre_load=pre_load)
2416 2416
2417 2417 def get_landing_commit(self):
2418 2418 """
2419 2419 Returns landing commit, or if that doesn't exist returns the tip
2420 2420 """
2421 2421 _rev_type, _rev = self.landing_rev
2422 2422 commit = self.get_commit(_rev)
2423 2423 if isinstance(commit, EmptyCommit):
2424 2424 return self.get_commit()
2425 2425 return commit
2426 2426
2427 2427 def flush_commit_cache(self):
2428 2428 self.update_commit_cache(cs_cache={'raw_id':'0'})
2429 2429 self.update_commit_cache()
2430 2430
2431 2431 def update_commit_cache(self, cs_cache=None, config=None):
2432 2432 """
2433 2433 Update cache of last commit for repository
2434 2434 cache_keys should be::
2435 2435
2436 2436 source_repo_id
2437 2437 short_id
2438 2438 raw_id
2439 2439 revision
2440 2440 parents
2441 2441 message
2442 2442 date
2443 2443 author
2444 2444 updated_on
2445 2445
2446 2446 """
2447 2447 from rhodecode.lib.vcs.backends.base import BaseChangeset
2448 2448 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2449 2449 empty_date = datetime.datetime.fromtimestamp(0)
2450 2450
2451 2451 if cs_cache is None:
2452 2452 # use no-cache version here
2453 2453 try:
2454 2454 scm_repo = self.scm_instance(cache=False, config=config)
2455 2455 except VCSError:
2456 2456 scm_repo = None
2457 2457 empty = scm_repo is None or scm_repo.is_empty()
2458 2458
2459 2459 if not empty:
2460 2460 cs_cache = scm_repo.get_commit(
2461 2461 pre_load=["author", "date", "message", "parents", "branch"])
2462 2462 else:
2463 2463 cs_cache = EmptyCommit()
2464 2464
2465 2465 if isinstance(cs_cache, BaseChangeset):
2466 2466 cs_cache = cs_cache.__json__()
2467 2467
2468 2468 def is_outdated(new_cs_cache):
2469 2469 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2470 2470 new_cs_cache['revision'] != self.changeset_cache['revision']):
2471 2471 return True
2472 2472 return False
2473 2473
2474 2474 # check if we have maybe already latest cached revision
2475 2475 if is_outdated(cs_cache) or not self.changeset_cache:
2476 2476 _current_datetime = datetime.datetime.utcnow()
2477 2477 last_change = cs_cache.get('date') or _current_datetime
2478 2478 # we check if last update is newer than the new value
2479 2479 # if yes, we use the current timestamp instead. Imagine you get
2480 2480 # old commit pushed 1y ago, we'd set last update 1y to ago.
2481 2481 last_change_timestamp = datetime_to_time(last_change)
2482 2482 current_timestamp = datetime_to_time(last_change)
2483 2483 if last_change_timestamp > current_timestamp and not empty:
2484 2484 cs_cache['date'] = _current_datetime
2485 2485
2486 2486 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2487 2487 cs_cache['updated_on'] = time.time()
2488 2488 self.changeset_cache = cs_cache
2489 2489 self.updated_on = last_change
2490 2490 Session().add(self)
2491 2491 Session().commit()
2492 2492
2493 2493 else:
2494 2494 if empty:
2495 2495 cs_cache = EmptyCommit().__json__()
2496 2496 else:
2497 2497 cs_cache = self.changeset_cache
2498 2498
2499 2499 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2500 2500
2501 2501 cs_cache['updated_on'] = time.time()
2502 2502 self.changeset_cache = cs_cache
2503 2503 self.updated_on = _date_latest
2504 2504 Session().add(self)
2505 2505 Session().commit()
2506 2506
2507 2507 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2508 2508 self.repo_name, cs_cache, _date_latest)
2509 2509
2510 2510 @property
2511 2511 def tip(self):
2512 2512 return self.get_commit('tip')
2513 2513
2514 2514 @property
2515 2515 def author(self):
2516 2516 return self.tip.author
2517 2517
2518 2518 @property
2519 2519 def last_change(self):
2520 2520 return self.scm_instance().last_change
2521 2521
2522 2522 def get_comments(self, revisions=None):
2523 2523 """
2524 2524 Returns comments for this repository grouped by revisions
2525 2525
2526 2526 :param revisions: filter query by revisions only
2527 2527 """
2528 2528 cmts = ChangesetComment.query()\
2529 2529 .filter(ChangesetComment.repo == self)
2530 2530 if revisions:
2531 2531 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2532 2532 grouped = collections.defaultdict(list)
2533 2533 for cmt in cmts.all():
2534 2534 grouped[cmt.revision].append(cmt)
2535 2535 return grouped
2536 2536
2537 2537 def statuses(self, revisions=None):
2538 2538 """
2539 2539 Returns statuses for this repository
2540 2540
2541 2541 :param revisions: list of revisions to get statuses for
2542 2542 """
2543 2543 statuses = ChangesetStatus.query()\
2544 2544 .filter(ChangesetStatus.repo == self)\
2545 2545 .filter(ChangesetStatus.version == 0)
2546 2546
2547 2547 if revisions:
2548 2548 # Try doing the filtering in chunks to avoid hitting limits
2549 2549 size = 500
2550 2550 status_results = []
2551 2551 for chunk in xrange(0, len(revisions), size):
2552 2552 status_results += statuses.filter(
2553 2553 ChangesetStatus.revision.in_(
2554 2554 revisions[chunk: chunk+size])
2555 2555 ).all()
2556 2556 else:
2557 2557 status_results = statuses.all()
2558 2558
2559 2559 grouped = {}
2560 2560
2561 2561 # maybe we have open new pullrequest without a status?
2562 2562 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2563 2563 status_lbl = ChangesetStatus.get_status_lbl(stat)
2564 2564 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2565 2565 for rev in pr.revisions:
2566 2566 pr_id = pr.pull_request_id
2567 2567 pr_repo = pr.target_repo.repo_name
2568 2568 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2569 2569
2570 2570 for stat in status_results:
2571 2571 pr_id = pr_repo = None
2572 2572 if stat.pull_request:
2573 2573 pr_id = stat.pull_request.pull_request_id
2574 2574 pr_repo = stat.pull_request.target_repo.repo_name
2575 2575 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2576 2576 pr_id, pr_repo]
2577 2577 return grouped
2578 2578
2579 2579 # ==========================================================================
2580 2580 # SCM CACHE INSTANCE
2581 2581 # ==========================================================================
2582 2582
2583 2583 def scm_instance(self, **kwargs):
2584 2584 import rhodecode
2585 2585
2586 2586 # Passing a config will not hit the cache currently only used
2587 2587 # for repo2dbmapper
2588 2588 config = kwargs.pop('config', None)
2589 2589 cache = kwargs.pop('cache', None)
2590 2590 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2591 2591 if vcs_full_cache is not None:
2592 2592 # allows override global config
2593 2593 full_cache = vcs_full_cache
2594 2594 else:
2595 2595 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2596 2596 # if cache is NOT defined use default global, else we have a full
2597 2597 # control over cache behaviour
2598 2598 if cache is None and full_cache and not config:
2599 2599 log.debug('Initializing pure cached instance for %s', self.repo_path)
2600 2600 return self._get_instance_cached()
2601 2601
2602 2602 # cache here is sent to the "vcs server"
2603 2603 return self._get_instance(cache=bool(cache), config=config)
2604 2604
2605 2605 def _get_instance_cached(self):
2606 2606 from rhodecode.lib import rc_cache
2607 2607
2608 2608 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2609 2609 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2610 2610 repo_id=self.repo_id)
2611 2611 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2612 2612
2613 2613 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2614 2614 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2615 2615 return self._get_instance(repo_state_uid=_cache_state_uid)
2616 2616
2617 2617 # we must use thread scoped cache here,
2618 2618 # because each thread of gevent needs it's own not shared connection and cache
2619 2619 # we also alter `args` so the cache key is individual for every green thread.
2620 2620 inv_context_manager = rc_cache.InvalidationContext(
2621 2621 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2622 2622 thread_scoped=True)
2623 2623 with inv_context_manager as invalidation_context:
2624 2624 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2625 2625 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2626 2626
2627 2627 # re-compute and store cache if we get invalidate signal
2628 2628 if invalidation_context.should_invalidate():
2629 2629 instance = get_instance_cached.refresh(*args)
2630 2630 else:
2631 2631 instance = get_instance_cached(*args)
2632 2632
2633 2633 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2634 2634 return instance
2635 2635
2636 2636 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2637 2637 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2638 2638 self.repo_type, self.repo_path, cache)
2639 2639 config = config or self._config
2640 2640 custom_wire = {
2641 2641 'cache': cache, # controls the vcs.remote cache
2642 2642 'repo_state_uid': repo_state_uid
2643 2643 }
2644 2644 repo = get_vcs_instance(
2645 2645 repo_path=safe_str(self.repo_full_path),
2646 2646 config=config,
2647 2647 with_wire=custom_wire,
2648 2648 create=False,
2649 2649 _vcs_alias=self.repo_type)
2650 2650 if repo is not None:
2651 2651 repo.count() # cache rebuild
2652 2652 return repo
2653 2653
2654 2654 def get_shadow_repository_path(self, workspace_id):
2655 2655 from rhodecode.lib.vcs.backends.base import BaseRepository
2656 2656 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2657 2657 self.repo_full_path, self.repo_id, workspace_id)
2658 2658 return shadow_repo_path
2659 2659
2660 2660 def __json__(self):
2661 2661 return {'landing_rev': self.landing_rev}
2662 2662
2663 2663 def get_dict(self):
2664 2664
2665 2665 # Since we transformed `repo_name` to a hybrid property, we need to
2666 2666 # keep compatibility with the code which uses `repo_name` field.
2667 2667
2668 2668 result = super(Repository, self).get_dict()
2669 2669 result['repo_name'] = result.pop('_repo_name', None)
2670 2670 return result
2671 2671
2672 2672
2673 2673 class RepoGroup(Base, BaseModel):
2674 2674 __tablename__ = 'groups'
2675 2675 __table_args__ = (
2676 2676 UniqueConstraint('group_name', 'group_parent_id'),
2677 2677 base_table_args,
2678 2678 )
2679 2679 __mapper_args__ = {'order_by': 'group_name'}
2680 2680
2681 2681 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2682 2682
2683 2683 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2684 2684 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2685 2685 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2686 2686 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2687 2687 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2688 2688 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2689 2689 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2690 2690 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2691 2691 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2692 2692 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2693 2693 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2694 2694
2695 2695 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2696 2696 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2697 2697 parent_group = relationship('RepoGroup', remote_side=group_id)
2698 2698 user = relationship('User')
2699 2699 integrations = relationship('Integration', cascade="all, delete-orphan")
2700 2700
2701 2701 # no cascade, set NULL
2702 2702 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2703 2703
2704 2704 def __init__(self, group_name='', parent_group=None):
2705 2705 self.group_name = group_name
2706 2706 self.parent_group = parent_group
2707 2707
2708 2708 def __unicode__(self):
2709 2709 return u"<%s('id:%s:%s')>" % (
2710 2710 self.__class__.__name__, self.group_id, self.group_name)
2711 2711
2712 2712 @hybrid_property
2713 2713 def group_name(self):
2714 2714 return self._group_name
2715 2715
2716 2716 @group_name.setter
2717 2717 def group_name(self, value):
2718 2718 self._group_name = value
2719 2719 self.group_name_hash = self.hash_repo_group_name(value)
2720 2720
2721 2721 @classmethod
2722 2722 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2723 2723 from rhodecode.lib.vcs.backends.base import EmptyCommit
2724 2724 dummy = EmptyCommit().__json__()
2725 2725 if not changeset_cache_raw:
2726 2726 dummy['source_repo_id'] = repo_id
2727 2727 return json.loads(json.dumps(dummy))
2728 2728
2729 2729 try:
2730 2730 return json.loads(changeset_cache_raw)
2731 2731 except TypeError:
2732 2732 return dummy
2733 2733 except Exception:
2734 2734 log.error(traceback.format_exc())
2735 2735 return dummy
2736 2736
2737 2737 @hybrid_property
2738 2738 def changeset_cache(self):
2739 2739 return self._load_changeset_cache('', self._changeset_cache)
2740 2740
2741 2741 @changeset_cache.setter
2742 2742 def changeset_cache(self, val):
2743 2743 try:
2744 2744 self._changeset_cache = json.dumps(val)
2745 2745 except Exception:
2746 2746 log.error(traceback.format_exc())
2747 2747
2748 2748 @validates('group_parent_id')
2749 2749 def validate_group_parent_id(self, key, val):
2750 2750 """
2751 2751 Check cycle references for a parent group to self
2752 2752 """
2753 2753 if self.group_id and val:
2754 2754 assert val != self.group_id
2755 2755
2756 2756 return val
2757 2757
2758 2758 @hybrid_property
2759 2759 def description_safe(self):
2760 2760 from rhodecode.lib import helpers as h
2761 2761 return h.escape(self.group_description)
2762 2762
2763 2763 @classmethod
2764 2764 def hash_repo_group_name(cls, repo_group_name):
2765 2765 val = remove_formatting(repo_group_name)
2766 2766 val = safe_str(val).lower()
2767 2767 chars = []
2768 2768 for c in val:
2769 2769 if c not in string.ascii_letters:
2770 2770 c = str(ord(c))
2771 2771 chars.append(c)
2772 2772
2773 2773 return ''.join(chars)
2774 2774
2775 2775 @classmethod
2776 2776 def _generate_choice(cls, repo_group):
2777 2777 from webhelpers2.html import literal as _literal
2778 2778 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2779 2779 return repo_group.group_id, _name(repo_group.full_path_splitted)
2780 2780
2781 2781 @classmethod
2782 2782 def groups_choices(cls, groups=None, show_empty_group=True):
2783 2783 if not groups:
2784 2784 groups = cls.query().all()
2785 2785
2786 2786 repo_groups = []
2787 2787 if show_empty_group:
2788 2788 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2789 2789
2790 2790 repo_groups.extend([cls._generate_choice(x) for x in groups])
2791 2791
2792 2792 repo_groups = sorted(
2793 2793 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2794 2794 return repo_groups
2795 2795
2796 2796 @classmethod
2797 2797 def url_sep(cls):
2798 2798 return URL_SEP
2799 2799
2800 2800 @classmethod
2801 2801 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2802 2802 if case_insensitive:
2803 2803 gr = cls.query().filter(func.lower(cls.group_name)
2804 2804 == func.lower(group_name))
2805 2805 else:
2806 2806 gr = cls.query().filter(cls.group_name == group_name)
2807 2807 if cache:
2808 2808 name_key = _hash_key(group_name)
2809 2809 gr = gr.options(
2810 2810 FromCache("sql_cache_short", "get_group_%s" % name_key))
2811 2811 return gr.scalar()
2812 2812
2813 2813 @classmethod
2814 2814 def get_user_personal_repo_group(cls, user_id):
2815 2815 user = User.get(user_id)
2816 2816 if user.username == User.DEFAULT_USER:
2817 2817 return None
2818 2818
2819 2819 return cls.query()\
2820 2820 .filter(cls.personal == true()) \
2821 2821 .filter(cls.user == user) \
2822 2822 .order_by(cls.group_id.asc()) \
2823 2823 .first()
2824 2824
2825 2825 @classmethod
2826 2826 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2827 2827 case_insensitive=True):
2828 2828 q = RepoGroup.query()
2829 2829
2830 2830 if not isinstance(user_id, Optional):
2831 2831 q = q.filter(RepoGroup.user_id == user_id)
2832 2832
2833 2833 if not isinstance(group_id, Optional):
2834 2834 q = q.filter(RepoGroup.group_parent_id == group_id)
2835 2835
2836 2836 if case_insensitive:
2837 2837 q = q.order_by(func.lower(RepoGroup.group_name))
2838 2838 else:
2839 2839 q = q.order_by(RepoGroup.group_name)
2840 2840 return q.all()
2841 2841
2842 2842 @property
2843 2843 def parents(self, parents_recursion_limit=10):
2844 2844 groups = []
2845 2845 if self.parent_group is None:
2846 2846 return groups
2847 2847 cur_gr = self.parent_group
2848 2848 groups.insert(0, cur_gr)
2849 2849 cnt = 0
2850 2850 while 1:
2851 2851 cnt += 1
2852 2852 gr = getattr(cur_gr, 'parent_group', None)
2853 2853 cur_gr = cur_gr.parent_group
2854 2854 if gr is None:
2855 2855 break
2856 2856 if cnt == parents_recursion_limit:
2857 2857 # this will prevent accidental infinit loops
2858 2858 log.error('more than %s parents found for group %s, stopping '
2859 2859 'recursive parent fetching', parents_recursion_limit, self)
2860 2860 break
2861 2861
2862 2862 groups.insert(0, gr)
2863 2863 return groups
2864 2864
2865 2865 @property
2866 2866 def last_commit_cache_update_diff(self):
2867 2867 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2868 2868
2869 2869 @classmethod
2870 2870 def _load_commit_change(cls, last_commit_cache):
2871 2871 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2872 2872 empty_date = datetime.datetime.fromtimestamp(0)
2873 2873 date_latest = last_commit_cache.get('date', empty_date)
2874 2874 try:
2875 2875 return parse_datetime(date_latest)
2876 2876 except Exception:
2877 2877 return empty_date
2878 2878
2879 2879 @property
2880 2880 def last_commit_change(self):
2881 2881 return self._load_commit_change(self.changeset_cache)
2882 2882
2883 2883 @property
2884 2884 def last_db_change(self):
2885 2885 return self.updated_on
2886 2886
2887 2887 @property
2888 2888 def children(self):
2889 2889 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2890 2890
2891 2891 @property
2892 2892 def name(self):
2893 2893 return self.group_name.split(RepoGroup.url_sep())[-1]
2894 2894
2895 2895 @property
2896 2896 def full_path(self):
2897 2897 return self.group_name
2898 2898
2899 2899 @property
2900 2900 def full_path_splitted(self):
2901 2901 return self.group_name.split(RepoGroup.url_sep())
2902 2902
2903 2903 @property
2904 2904 def repositories(self):
2905 2905 return Repository.query()\
2906 2906 .filter(Repository.group == self)\
2907 2907 .order_by(Repository.repo_name)
2908 2908
2909 2909 @property
2910 2910 def repositories_recursive_count(self):
2911 2911 cnt = self.repositories.count()
2912 2912
2913 2913 def children_count(group):
2914 2914 cnt = 0
2915 2915 for child in group.children:
2916 2916 cnt += child.repositories.count()
2917 2917 cnt += children_count(child)
2918 2918 return cnt
2919 2919
2920 2920 return cnt + children_count(self)
2921 2921
2922 2922 def _recursive_objects(self, include_repos=True, include_groups=True):
2923 2923 all_ = []
2924 2924
2925 2925 def _get_members(root_gr):
2926 2926 if include_repos:
2927 2927 for r in root_gr.repositories:
2928 2928 all_.append(r)
2929 2929 childs = root_gr.children.all()
2930 2930 if childs:
2931 2931 for gr in childs:
2932 2932 if include_groups:
2933 2933 all_.append(gr)
2934 2934 _get_members(gr)
2935 2935
2936 2936 root_group = []
2937 2937 if include_groups:
2938 2938 root_group = [self]
2939 2939
2940 2940 _get_members(self)
2941 2941 return root_group + all_
2942 2942
2943 2943 def recursive_groups_and_repos(self):
2944 2944 """
2945 2945 Recursive return all groups, with repositories in those groups
2946 2946 """
2947 2947 return self._recursive_objects()
2948 2948
2949 2949 def recursive_groups(self):
2950 2950 """
2951 2951 Returns all children groups for this group including children of children
2952 2952 """
2953 2953 return self._recursive_objects(include_repos=False)
2954 2954
2955 2955 def recursive_repos(self):
2956 2956 """
2957 2957 Returns all children repositories for this group
2958 2958 """
2959 2959 return self._recursive_objects(include_groups=False)
2960 2960
2961 2961 def get_new_name(self, group_name):
2962 2962 """
2963 2963 returns new full group name based on parent and new name
2964 2964
2965 2965 :param group_name:
2966 2966 """
2967 2967 path_prefix = (self.parent_group.full_path_splitted if
2968 2968 self.parent_group else [])
2969 2969 return RepoGroup.url_sep().join(path_prefix + [group_name])
2970 2970
2971 2971 def update_commit_cache(self, config=None):
2972 2972 """
2973 2973 Update cache of last commit for newest repository inside this repository group.
2974 2974 cache_keys should be::
2975 2975
2976 2976 source_repo_id
2977 2977 short_id
2978 2978 raw_id
2979 2979 revision
2980 2980 parents
2981 2981 message
2982 2982 date
2983 2983 author
2984 2984
2985 2985 """
2986 2986 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2987 2987 empty_date = datetime.datetime.fromtimestamp(0)
2988 2988
2989 2989 def repo_groups_and_repos(root_gr):
2990 2990 for _repo in root_gr.repositories:
2991 2991 yield _repo
2992 2992 for child_group in root_gr.children.all():
2993 2993 yield child_group
2994 2994
2995 2995 latest_repo_cs_cache = {}
2996 2996 for obj in repo_groups_and_repos(self):
2997 2997 repo_cs_cache = obj.changeset_cache
2998 2998 date_latest = latest_repo_cs_cache.get('date', empty_date)
2999 2999 date_current = repo_cs_cache.get('date', empty_date)
3000 3000 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3001 3001 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3002 3002 latest_repo_cs_cache = repo_cs_cache
3003 3003 if hasattr(obj, 'repo_id'):
3004 3004 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3005 3005 else:
3006 3006 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3007 3007
3008 3008 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3009 3009
3010 3010 latest_repo_cs_cache['updated_on'] = time.time()
3011 3011 self.changeset_cache = latest_repo_cs_cache
3012 3012 self.updated_on = _date_latest
3013 3013 Session().add(self)
3014 3014 Session().commit()
3015 3015
3016 3016 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3017 3017 self.group_name, latest_repo_cs_cache, _date_latest)
3018 3018
3019 3019 def permissions(self, with_admins=True, with_owner=True,
3020 3020 expand_from_user_groups=False):
3021 3021 """
3022 3022 Permissions for repository groups
3023 3023 """
3024 3024 _admin_perm = 'group.admin'
3025 3025
3026 3026 owner_row = []
3027 3027 if with_owner:
3028 3028 usr = AttributeDict(self.user.get_dict())
3029 3029 usr.owner_row = True
3030 3030 usr.permission = _admin_perm
3031 3031 owner_row.append(usr)
3032 3032
3033 3033 super_admin_ids = []
3034 3034 super_admin_rows = []
3035 3035 if with_admins:
3036 3036 for usr in User.get_all_super_admins():
3037 3037 super_admin_ids.append(usr.user_id)
3038 3038 # if this admin is also owner, don't double the record
3039 3039 if usr.user_id == owner_row[0].user_id:
3040 3040 owner_row[0].admin_row = True
3041 3041 else:
3042 3042 usr = AttributeDict(usr.get_dict())
3043 3043 usr.admin_row = True
3044 3044 usr.permission = _admin_perm
3045 3045 super_admin_rows.append(usr)
3046 3046
3047 3047 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3048 3048 q = q.options(joinedload(UserRepoGroupToPerm.group),
3049 3049 joinedload(UserRepoGroupToPerm.user),
3050 3050 joinedload(UserRepoGroupToPerm.permission),)
3051 3051
3052 3052 # get owners and admins and permissions. We do a trick of re-writing
3053 3053 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3054 3054 # has a global reference and changing one object propagates to all
3055 3055 # others. This means if admin is also an owner admin_row that change
3056 3056 # would propagate to both objects
3057 3057 perm_rows = []
3058 3058 for _usr in q.all():
3059 3059 usr = AttributeDict(_usr.user.get_dict())
3060 3060 # if this user is also owner/admin, mark as duplicate record
3061 3061 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3062 3062 usr.duplicate_perm = True
3063 3063 usr.permission = _usr.permission.permission_name
3064 3064 perm_rows.append(usr)
3065 3065
3066 3066 # filter the perm rows by 'default' first and then sort them by
3067 3067 # admin,write,read,none permissions sorted again alphabetically in
3068 3068 # each group
3069 3069 perm_rows = sorted(perm_rows, key=display_user_sort)
3070 3070
3071 3071 user_groups_rows = []
3072 3072 if expand_from_user_groups:
3073 3073 for ug in self.permission_user_groups(with_members=True):
3074 3074 for user_data in ug.members:
3075 3075 user_groups_rows.append(user_data)
3076 3076
3077 3077 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3078 3078
3079 3079 def permission_user_groups(self, with_members=False):
3080 3080 q = UserGroupRepoGroupToPerm.query()\
3081 3081 .filter(UserGroupRepoGroupToPerm.group == self)
3082 3082 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3083 3083 joinedload(UserGroupRepoGroupToPerm.users_group),
3084 3084 joinedload(UserGroupRepoGroupToPerm.permission),)
3085 3085
3086 3086 perm_rows = []
3087 3087 for _user_group in q.all():
3088 3088 entry = AttributeDict(_user_group.users_group.get_dict())
3089 3089 entry.permission = _user_group.permission.permission_name
3090 3090 if with_members:
3091 3091 entry.members = [x.user.get_dict()
3092 3092 for x in _user_group.users_group.members]
3093 3093 perm_rows.append(entry)
3094 3094
3095 3095 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3096 3096 return perm_rows
3097 3097
3098 3098 def get_api_data(self):
3099 3099 """
3100 3100 Common function for generating api data
3101 3101
3102 3102 """
3103 3103 group = self
3104 3104 data = {
3105 3105 'group_id': group.group_id,
3106 3106 'group_name': group.group_name,
3107 3107 'group_description': group.description_safe,
3108 3108 'parent_group': group.parent_group.group_name if group.parent_group else None,
3109 3109 'repositories': [x.repo_name for x in group.repositories],
3110 3110 'owner': group.user.username,
3111 3111 }
3112 3112 return data
3113 3113
3114 3114 def get_dict(self):
3115 3115 # Since we transformed `group_name` to a hybrid property, we need to
3116 3116 # keep compatibility with the code which uses `group_name` field.
3117 3117 result = super(RepoGroup, self).get_dict()
3118 3118 result['group_name'] = result.pop('_group_name', None)
3119 3119 return result
3120 3120
3121 3121
3122 3122 class Permission(Base, BaseModel):
3123 3123 __tablename__ = 'permissions'
3124 3124 __table_args__ = (
3125 3125 Index('p_perm_name_idx', 'permission_name'),
3126 3126 base_table_args,
3127 3127 )
3128 3128
3129 3129 PERMS = [
3130 3130 ('hg.admin', _('RhodeCode Super Administrator')),
3131 3131
3132 3132 ('repository.none', _('Repository no access')),
3133 3133 ('repository.read', _('Repository read access')),
3134 3134 ('repository.write', _('Repository write access')),
3135 3135 ('repository.admin', _('Repository admin access')),
3136 3136
3137 3137 ('group.none', _('Repository group no access')),
3138 3138 ('group.read', _('Repository group read access')),
3139 3139 ('group.write', _('Repository group write access')),
3140 3140 ('group.admin', _('Repository group admin access')),
3141 3141
3142 3142 ('usergroup.none', _('User group no access')),
3143 3143 ('usergroup.read', _('User group read access')),
3144 3144 ('usergroup.write', _('User group write access')),
3145 3145 ('usergroup.admin', _('User group admin access')),
3146 3146
3147 3147 ('branch.none', _('Branch no permissions')),
3148 3148 ('branch.merge', _('Branch access by web merge')),
3149 3149 ('branch.push', _('Branch access by push')),
3150 3150 ('branch.push_force', _('Branch access by push with force')),
3151 3151
3152 3152 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3153 3153 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3154 3154
3155 3155 ('hg.usergroup.create.false', _('User Group creation disabled')),
3156 3156 ('hg.usergroup.create.true', _('User Group creation enabled')),
3157 3157
3158 3158 ('hg.create.none', _('Repository creation disabled')),
3159 3159 ('hg.create.repository', _('Repository creation enabled')),
3160 3160 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3161 3161 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3162 3162
3163 3163 ('hg.fork.none', _('Repository forking disabled')),
3164 3164 ('hg.fork.repository', _('Repository forking enabled')),
3165 3165
3166 3166 ('hg.register.none', _('Registration disabled')),
3167 3167 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3168 3168 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3169 3169
3170 3170 ('hg.password_reset.enabled', _('Password reset enabled')),
3171 3171 ('hg.password_reset.hidden', _('Password reset hidden')),
3172 3172 ('hg.password_reset.disabled', _('Password reset disabled')),
3173 3173
3174 3174 ('hg.extern_activate.manual', _('Manual activation of external account')),
3175 3175 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3176 3176
3177 3177 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3178 3178 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3179 3179 ]
3180 3180
3181 3181 # definition of system default permissions for DEFAULT user, created on
3182 3182 # system setup
3183 3183 DEFAULT_USER_PERMISSIONS = [
3184 3184 # object perms
3185 3185 'repository.read',
3186 3186 'group.read',
3187 3187 'usergroup.read',
3188 3188 # branch, for backward compat we need same value as before so forced pushed
3189 3189 'branch.push_force',
3190 3190 # global
3191 3191 'hg.create.repository',
3192 3192 'hg.repogroup.create.false',
3193 3193 'hg.usergroup.create.false',
3194 3194 'hg.create.write_on_repogroup.true',
3195 3195 'hg.fork.repository',
3196 3196 'hg.register.manual_activate',
3197 3197 'hg.password_reset.enabled',
3198 3198 'hg.extern_activate.auto',
3199 3199 'hg.inherit_default_perms.true',
3200 3200 ]
3201 3201
3202 3202 # defines which permissions are more important higher the more important
3203 3203 # Weight defines which permissions are more important.
3204 3204 # The higher number the more important.
3205 3205 PERM_WEIGHTS = {
3206 3206 'repository.none': 0,
3207 3207 'repository.read': 1,
3208 3208 'repository.write': 3,
3209 3209 'repository.admin': 4,
3210 3210
3211 3211 'group.none': 0,
3212 3212 'group.read': 1,
3213 3213 'group.write': 3,
3214 3214 'group.admin': 4,
3215 3215
3216 3216 'usergroup.none': 0,
3217 3217 'usergroup.read': 1,
3218 3218 'usergroup.write': 3,
3219 3219 'usergroup.admin': 4,
3220 3220
3221 3221 'branch.none': 0,
3222 3222 'branch.merge': 1,
3223 3223 'branch.push': 3,
3224 3224 'branch.push_force': 4,
3225 3225
3226 3226 'hg.repogroup.create.false': 0,
3227 3227 'hg.repogroup.create.true': 1,
3228 3228
3229 3229 'hg.usergroup.create.false': 0,
3230 3230 'hg.usergroup.create.true': 1,
3231 3231
3232 3232 'hg.fork.none': 0,
3233 3233 'hg.fork.repository': 1,
3234 3234 'hg.create.none': 0,
3235 3235 'hg.create.repository': 1
3236 3236 }
3237 3237
3238 3238 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3239 3239 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3240 3240 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3241 3241
3242 3242 def __unicode__(self):
3243 3243 return u"<%s('%s:%s')>" % (
3244 3244 self.__class__.__name__, self.permission_id, self.permission_name
3245 3245 )
3246 3246
3247 3247 @classmethod
3248 3248 def get_by_key(cls, key):
3249 3249 return cls.query().filter(cls.permission_name == key).scalar()
3250 3250
3251 3251 @classmethod
3252 3252 def get_default_repo_perms(cls, user_id, repo_id=None):
3253 3253 q = Session().query(UserRepoToPerm, Repository, Permission)\
3254 3254 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3255 3255 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3256 3256 .filter(UserRepoToPerm.user_id == user_id)
3257 3257 if repo_id:
3258 3258 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3259 3259 return q.all()
3260 3260
3261 3261 @classmethod
3262 3262 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3263 3263 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3264 3264 .join(
3265 3265 Permission,
3266 3266 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3267 3267 .join(
3268 3268 UserRepoToPerm,
3269 3269 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3270 3270 .filter(UserRepoToPerm.user_id == user_id)
3271 3271
3272 3272 if repo_id:
3273 3273 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3274 3274 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3275 3275
3276 3276 @classmethod
3277 3277 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3278 3278 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3279 3279 .join(
3280 3280 Permission,
3281 3281 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3282 3282 .join(
3283 3283 Repository,
3284 3284 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3285 3285 .join(
3286 3286 UserGroup,
3287 3287 UserGroupRepoToPerm.users_group_id ==
3288 3288 UserGroup.users_group_id)\
3289 3289 .join(
3290 3290 UserGroupMember,
3291 3291 UserGroupRepoToPerm.users_group_id ==
3292 3292 UserGroupMember.users_group_id)\
3293 3293 .filter(
3294 3294 UserGroupMember.user_id == user_id,
3295 3295 UserGroup.users_group_active == true())
3296 3296 if repo_id:
3297 3297 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3298 3298 return q.all()
3299 3299
3300 3300 @classmethod
3301 3301 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3302 3302 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3303 3303 .join(
3304 3304 Permission,
3305 3305 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3306 3306 .join(
3307 3307 UserGroupRepoToPerm,
3308 3308 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3309 3309 .join(
3310 3310 UserGroup,
3311 3311 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3312 3312 .join(
3313 3313 UserGroupMember,
3314 3314 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3315 3315 .filter(
3316 3316 UserGroupMember.user_id == user_id,
3317 3317 UserGroup.users_group_active == true())
3318 3318
3319 3319 if repo_id:
3320 3320 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3321 3321 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3322 3322
3323 3323 @classmethod
3324 3324 def get_default_group_perms(cls, user_id, repo_group_id=None):
3325 3325 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3326 3326 .join(
3327 3327 Permission,
3328 3328 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3329 3329 .join(
3330 3330 RepoGroup,
3331 3331 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3332 3332 .filter(UserRepoGroupToPerm.user_id == user_id)
3333 3333 if repo_group_id:
3334 3334 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3335 3335 return q.all()
3336 3336
3337 3337 @classmethod
3338 3338 def get_default_group_perms_from_user_group(
3339 3339 cls, user_id, repo_group_id=None):
3340 3340 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3341 3341 .join(
3342 3342 Permission,
3343 3343 UserGroupRepoGroupToPerm.permission_id ==
3344 3344 Permission.permission_id)\
3345 3345 .join(
3346 3346 RepoGroup,
3347 3347 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3348 3348 .join(
3349 3349 UserGroup,
3350 3350 UserGroupRepoGroupToPerm.users_group_id ==
3351 3351 UserGroup.users_group_id)\
3352 3352 .join(
3353 3353 UserGroupMember,
3354 3354 UserGroupRepoGroupToPerm.users_group_id ==
3355 3355 UserGroupMember.users_group_id)\
3356 3356 .filter(
3357 3357 UserGroupMember.user_id == user_id,
3358 3358 UserGroup.users_group_active == true())
3359 3359 if repo_group_id:
3360 3360 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3361 3361 return q.all()
3362 3362
3363 3363 @classmethod
3364 3364 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3365 3365 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3366 3366 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3367 3367 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3368 3368 .filter(UserUserGroupToPerm.user_id == user_id)
3369 3369 if user_group_id:
3370 3370 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3371 3371 return q.all()
3372 3372
3373 3373 @classmethod
3374 3374 def get_default_user_group_perms_from_user_group(
3375 3375 cls, user_id, user_group_id=None):
3376 3376 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3377 3377 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3378 3378 .join(
3379 3379 Permission,
3380 3380 UserGroupUserGroupToPerm.permission_id ==
3381 3381 Permission.permission_id)\
3382 3382 .join(
3383 3383 TargetUserGroup,
3384 3384 UserGroupUserGroupToPerm.target_user_group_id ==
3385 3385 TargetUserGroup.users_group_id)\
3386 3386 .join(
3387 3387 UserGroup,
3388 3388 UserGroupUserGroupToPerm.user_group_id ==
3389 3389 UserGroup.users_group_id)\
3390 3390 .join(
3391 3391 UserGroupMember,
3392 3392 UserGroupUserGroupToPerm.user_group_id ==
3393 3393 UserGroupMember.users_group_id)\
3394 3394 .filter(
3395 3395 UserGroupMember.user_id == user_id,
3396 3396 UserGroup.users_group_active == true())
3397 3397 if user_group_id:
3398 3398 q = q.filter(
3399 3399 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3400 3400
3401 3401 return q.all()
3402 3402
3403 3403
3404 3404 class UserRepoToPerm(Base, BaseModel):
3405 3405 __tablename__ = 'repo_to_perm'
3406 3406 __table_args__ = (
3407 3407 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3408 3408 base_table_args
3409 3409 )
3410 3410
3411 3411 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3412 3412 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3413 3413 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3414 3414 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3415 3415
3416 3416 user = relationship('User')
3417 3417 repository = relationship('Repository')
3418 3418 permission = relationship('Permission')
3419 3419
3420 3420 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3421 3421
3422 3422 @classmethod
3423 3423 def create(cls, user, repository, permission):
3424 3424 n = cls()
3425 3425 n.user = user
3426 3426 n.repository = repository
3427 3427 n.permission = permission
3428 3428 Session().add(n)
3429 3429 return n
3430 3430
3431 3431 def __unicode__(self):
3432 3432 return u'<%s => %s >' % (self.user, self.repository)
3433 3433
3434 3434
3435 3435 class UserUserGroupToPerm(Base, BaseModel):
3436 3436 __tablename__ = 'user_user_group_to_perm'
3437 3437 __table_args__ = (
3438 3438 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3439 3439 base_table_args
3440 3440 )
3441 3441
3442 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 3443 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3444 3444 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3445 3445 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3446 3446
3447 3447 user = relationship('User')
3448 3448 user_group = relationship('UserGroup')
3449 3449 permission = relationship('Permission')
3450 3450
3451 3451 @classmethod
3452 3452 def create(cls, user, user_group, permission):
3453 3453 n = cls()
3454 3454 n.user = user
3455 3455 n.user_group = user_group
3456 3456 n.permission = permission
3457 3457 Session().add(n)
3458 3458 return n
3459 3459
3460 3460 def __unicode__(self):
3461 3461 return u'<%s => %s >' % (self.user, self.user_group)
3462 3462
3463 3463
3464 3464 class UserToPerm(Base, BaseModel):
3465 3465 __tablename__ = 'user_to_perm'
3466 3466 __table_args__ = (
3467 3467 UniqueConstraint('user_id', 'permission_id'),
3468 3468 base_table_args
3469 3469 )
3470 3470
3471 3471 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3472 3472 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3473 3473 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3474 3474
3475 3475 user = relationship('User')
3476 3476 permission = relationship('Permission', lazy='joined')
3477 3477
3478 3478 def __unicode__(self):
3479 3479 return u'<%s => %s >' % (self.user, self.permission)
3480 3480
3481 3481
3482 3482 class UserGroupRepoToPerm(Base, BaseModel):
3483 3483 __tablename__ = 'users_group_repo_to_perm'
3484 3484 __table_args__ = (
3485 3485 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3486 3486 base_table_args
3487 3487 )
3488 3488
3489 3489 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3490 3490 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3491 3491 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3492 3492 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3493 3493
3494 3494 users_group = relationship('UserGroup')
3495 3495 permission = relationship('Permission')
3496 3496 repository = relationship('Repository')
3497 3497 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3498 3498
3499 3499 @classmethod
3500 3500 def create(cls, users_group, repository, permission):
3501 3501 n = cls()
3502 3502 n.users_group = users_group
3503 3503 n.repository = repository
3504 3504 n.permission = permission
3505 3505 Session().add(n)
3506 3506 return n
3507 3507
3508 3508 def __unicode__(self):
3509 3509 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3510 3510
3511 3511
3512 3512 class UserGroupUserGroupToPerm(Base, BaseModel):
3513 3513 __tablename__ = 'user_group_user_group_to_perm'
3514 3514 __table_args__ = (
3515 3515 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3516 3516 CheckConstraint('target_user_group_id != user_group_id'),
3517 3517 base_table_args
3518 3518 )
3519 3519
3520 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 3521 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3522 3522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3523 3523 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3524 3524
3525 3525 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3526 3526 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3527 3527 permission = relationship('Permission')
3528 3528
3529 3529 @classmethod
3530 3530 def create(cls, target_user_group, user_group, permission):
3531 3531 n = cls()
3532 3532 n.target_user_group = target_user_group
3533 3533 n.user_group = user_group
3534 3534 n.permission = permission
3535 3535 Session().add(n)
3536 3536 return n
3537 3537
3538 3538 def __unicode__(self):
3539 3539 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3540 3540
3541 3541
3542 3542 class UserGroupToPerm(Base, BaseModel):
3543 3543 __tablename__ = 'users_group_to_perm'
3544 3544 __table_args__ = (
3545 3545 UniqueConstraint('users_group_id', 'permission_id',),
3546 3546 base_table_args
3547 3547 )
3548 3548
3549 3549 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3550 3550 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3551 3551 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3552 3552
3553 3553 users_group = relationship('UserGroup')
3554 3554 permission = relationship('Permission')
3555 3555
3556 3556
3557 3557 class UserRepoGroupToPerm(Base, BaseModel):
3558 3558 __tablename__ = 'user_repo_group_to_perm'
3559 3559 __table_args__ = (
3560 3560 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3561 3561 base_table_args
3562 3562 )
3563 3563
3564 3564 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3565 3565 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3566 3566 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3567 3567 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3568 3568
3569 3569 user = relationship('User')
3570 3570 group = relationship('RepoGroup')
3571 3571 permission = relationship('Permission')
3572 3572
3573 3573 @classmethod
3574 3574 def create(cls, user, repository_group, permission):
3575 3575 n = cls()
3576 3576 n.user = user
3577 3577 n.group = repository_group
3578 3578 n.permission = permission
3579 3579 Session().add(n)
3580 3580 return n
3581 3581
3582 3582
3583 3583 class UserGroupRepoGroupToPerm(Base, BaseModel):
3584 3584 __tablename__ = 'users_group_repo_group_to_perm'
3585 3585 __table_args__ = (
3586 3586 UniqueConstraint('users_group_id', 'group_id'),
3587 3587 base_table_args
3588 3588 )
3589 3589
3590 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 3591 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3592 3592 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3593 3593 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3594 3594
3595 3595 users_group = relationship('UserGroup')
3596 3596 permission = relationship('Permission')
3597 3597 group = relationship('RepoGroup')
3598 3598
3599 3599 @classmethod
3600 3600 def create(cls, user_group, repository_group, permission):
3601 3601 n = cls()
3602 3602 n.users_group = user_group
3603 3603 n.group = repository_group
3604 3604 n.permission = permission
3605 3605 Session().add(n)
3606 3606 return n
3607 3607
3608 3608 def __unicode__(self):
3609 3609 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3610 3610
3611 3611
3612 3612 class Statistics(Base, BaseModel):
3613 3613 __tablename__ = 'statistics'
3614 3614 __table_args__ = (
3615 3615 base_table_args
3616 3616 )
3617 3617
3618 3618 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3619 3619 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3620 3620 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3621 3621 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3622 3622 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3623 3623 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3624 3624
3625 3625 repository = relationship('Repository', single_parent=True)
3626 3626
3627 3627
3628 3628 class UserFollowing(Base, BaseModel):
3629 3629 __tablename__ = 'user_followings'
3630 3630 __table_args__ = (
3631 3631 UniqueConstraint('user_id', 'follows_repository_id'),
3632 3632 UniqueConstraint('user_id', 'follows_user_id'),
3633 3633 base_table_args
3634 3634 )
3635 3635
3636 3636 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3637 3637 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3638 3638 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3639 3639 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3640 3640 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3641 3641
3642 3642 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3643 3643
3644 3644 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3645 3645 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3646 3646
3647 3647 @classmethod
3648 3648 def get_repo_followers(cls, repo_id):
3649 3649 return cls.query().filter(cls.follows_repo_id == repo_id)
3650 3650
3651 3651
3652 3652 class CacheKey(Base, BaseModel):
3653 3653 __tablename__ = 'cache_invalidation'
3654 3654 __table_args__ = (
3655 3655 UniqueConstraint('cache_key'),
3656 3656 Index('key_idx', 'cache_key'),
3657 3657 base_table_args,
3658 3658 )
3659 3659
3660 3660 CACHE_TYPE_FEED = 'FEED'
3661 3661
3662 3662 # namespaces used to register process/thread aware caches
3663 3663 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3664 3664 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3665 3665
3666 3666 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3667 3667 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3668 3668 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3669 3669 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3670 3670 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3671 3671
3672 3672 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3673 3673 self.cache_key = cache_key
3674 3674 self.cache_args = cache_args
3675 3675 self.cache_active = False
3676 3676 # first key should be same for all entries, since all workers should share it
3677 3677 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3678 3678
3679 3679 def __unicode__(self):
3680 3680 return u"<%s('%s:%s[%s]')>" % (
3681 3681 self.__class__.__name__,
3682 3682 self.cache_id, self.cache_key, self.cache_active)
3683 3683
3684 3684 def _cache_key_partition(self):
3685 3685 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3686 3686 return prefix, repo_name, suffix
3687 3687
3688 3688 def get_prefix(self):
3689 3689 """
3690 3690 Try to extract prefix from existing cache key. The key could consist
3691 3691 of prefix, repo_name, suffix
3692 3692 """
3693 3693 # this returns prefix, repo_name, suffix
3694 3694 return self._cache_key_partition()[0]
3695 3695
3696 3696 def get_suffix(self):
3697 3697 """
3698 3698 get suffix that might have been used in _get_cache_key to
3699 3699 generate self.cache_key. Only used for informational purposes
3700 3700 in repo_edit.mako.
3701 3701 """
3702 3702 # prefix, repo_name, suffix
3703 3703 return self._cache_key_partition()[2]
3704 3704
3705 3705 @classmethod
3706 3706 def generate_new_state_uid(cls, based_on=None):
3707 3707 if based_on:
3708 3708 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3709 3709 else:
3710 3710 return str(uuid.uuid4())
3711 3711
3712 3712 @classmethod
3713 3713 def delete_all_cache(cls):
3714 3714 """
3715 3715 Delete all cache keys from database.
3716 3716 Should only be run when all instances are down and all entries
3717 3717 thus stale.
3718 3718 """
3719 3719 cls.query().delete()
3720 3720 Session().commit()
3721 3721
3722 3722 @classmethod
3723 3723 def set_invalidate(cls, cache_uid, delete=False):
3724 3724 """
3725 3725 Mark all caches of a repo as invalid in the database.
3726 3726 """
3727 3727
3728 3728 try:
3729 3729 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3730 3730 if delete:
3731 3731 qry.delete()
3732 3732 log.debug('cache objects deleted for cache args %s',
3733 3733 safe_str(cache_uid))
3734 3734 else:
3735 3735 qry.update({"cache_active": False,
3736 3736 "cache_state_uid": cls.generate_new_state_uid()})
3737 3737 log.debug('cache objects marked as invalid for cache args %s',
3738 3738 safe_str(cache_uid))
3739 3739
3740 3740 Session().commit()
3741 3741 except Exception:
3742 3742 log.exception(
3743 3743 'Cache key invalidation failed for cache args %s',
3744 3744 safe_str(cache_uid))
3745 3745 Session().rollback()
3746 3746
3747 3747 @classmethod
3748 3748 def get_active_cache(cls, cache_key):
3749 3749 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3750 3750 if inv_obj:
3751 3751 return inv_obj
3752 3752 return None
3753 3753
3754 3754 @classmethod
3755 3755 def get_namespace_map(cls, namespace):
3756 3756 return {
3757 3757 x.cache_key: x
3758 3758 for x in cls.query().filter(cls.cache_args == namespace)}
3759 3759
3760 3760
3761 3761 class ChangesetComment(Base, BaseModel):
3762 3762 __tablename__ = 'changeset_comments'
3763 3763 __table_args__ = (
3764 3764 Index('cc_revision_idx', 'revision'),
3765 3765 base_table_args,
3766 3766 )
3767 3767
3768 3768 COMMENT_OUTDATED = u'comment_outdated'
3769 3769 COMMENT_TYPE_NOTE = u'note'
3770 3770 COMMENT_TYPE_TODO = u'todo'
3771 3771 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3772 3772
3773 3773 OP_IMMUTABLE = u'immutable'
3774 3774 OP_CHANGEABLE = u'changeable'
3775 3775
3776 3776 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3777 3777 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3778 3778 revision = Column('revision', String(40), nullable=True)
3779 3779 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3780 3780 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3781 3781 line_no = Column('line_no', Unicode(10), nullable=True)
3782 3782 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3783 3783 f_path = Column('f_path', Unicode(1000), nullable=True)
3784 3784 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3785 3785 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3786 3786 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3787 3787 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3788 3788 renderer = Column('renderer', Unicode(64), nullable=True)
3789 3789 display_state = Column('display_state', Unicode(128), nullable=True)
3790 3790 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3791 3791 draft = Column('draft', Boolean(), nullable=True, default=False)
3792 3792
3793 3793 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3794 3794 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3795 3795
3796 3796 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3797 3797 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3798 3798
3799 3799 author = relationship('User', lazy='select')
3800 3800 repo = relationship('Repository')
3801 3801 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select')
3802 3802 pull_request = relationship('PullRequest', lazy='select')
3803 3803 pull_request_version = relationship('PullRequestVersion', lazy='select')
3804 3804 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version')
3805 3805
3806 3806 @classmethod
3807 3807 def get_users(cls, revision=None, pull_request_id=None):
3808 3808 """
3809 3809 Returns user associated with this ChangesetComment. ie those
3810 3810 who actually commented
3811 3811
3812 3812 :param cls:
3813 3813 :param revision:
3814 3814 """
3815 3815 q = Session().query(User)\
3816 3816 .join(ChangesetComment.author)
3817 3817 if revision:
3818 3818 q = q.filter(cls.revision == revision)
3819 3819 elif pull_request_id:
3820 3820 q = q.filter(cls.pull_request_id == pull_request_id)
3821 3821 return q.all()
3822 3822
3823 3823 @classmethod
3824 3824 def get_index_from_version(cls, pr_version, versions=None, num_versions=None):
3825 3825
3826 3826 if versions is not None:
3827 3827 num_versions = [x.pull_request_version_id for x in versions]
3828 3828
3829 3829 num_versions = num_versions or []
3830 3830 try:
3831 3831 return num_versions.index(pr_version) + 1
3832 3832 except (IndexError, ValueError):
3833 3833 return
3834 3834
3835 3835 @property
3836 3836 def outdated(self):
3837 3837 return self.display_state == self.COMMENT_OUTDATED
3838 3838
3839 3839 @property
3840 3840 def outdated_js(self):
3841 3841 return json.dumps(self.display_state == self.COMMENT_OUTDATED)
3842 3842
3843 3843 @property
3844 3844 def immutable(self):
3845 3845 return self.immutable_state == self.OP_IMMUTABLE
3846 3846
3847 3847 def outdated_at_version(self, version):
3848 3848 """
3849 3849 Checks if comment is outdated for given pull request version
3850 3850 """
3851 3851 def version_check():
3852 3852 return self.pull_request_version_id and self.pull_request_version_id != version
3853 3853
3854 3854 if self.is_inline:
3855 3855 return self.outdated and version_check()
3856 3856 else:
3857 3857 # general comments don't have .outdated set, also latest don't have a version
3858 3858 return version_check()
3859 3859
3860 3860 def outdated_at_version_js(self, version):
3861 3861 """
3862 3862 Checks if comment is outdated for given pull request version
3863 3863 """
3864 3864 return json.dumps(self.outdated_at_version(version))
3865 3865
3866 3866 def older_than_version(self, version):
3867 3867 """
3868 3868 Checks if comment is made from previous version than given
3869 3869 """
3870 3870 if version is None:
3871 3871 return self.pull_request_version != version
3872 3872
3873 3873 return self.pull_request_version < version
3874 3874
3875 3875 def older_than_version_js(self, version):
3876 3876 """
3877 3877 Checks if comment is made from previous version than given
3878 3878 """
3879 3879 return json.dumps(self.older_than_version(version))
3880 3880
3881 3881 @property
3882 3882 def commit_id(self):
3883 3883 """New style naming to stop using .revision"""
3884 3884 return self.revision
3885 3885
3886 3886 @property
3887 3887 def resolved(self):
3888 3888 return self.resolved_by[0] if self.resolved_by else None
3889 3889
3890 3890 @property
3891 3891 def is_todo(self):
3892 3892 return self.comment_type == self.COMMENT_TYPE_TODO
3893 3893
3894 3894 @property
3895 3895 def is_inline(self):
3896 3896 if self.line_no and self.f_path:
3897 3897 return True
3898 3898 return False
3899 3899
3900 3900 @property
3901 3901 def last_version(self):
3902 3902 version = 0
3903 3903 if self.history:
3904 3904 version = self.history[-1].version
3905 3905 return version
3906 3906
3907 3907 def get_index_version(self, versions):
3908 3908 return self.get_index_from_version(
3909 3909 self.pull_request_version_id, versions)
3910 3910
3911 3911 @property
3912 3912 def review_status(self):
3913 3913 if self.status_change:
3914 3914 return self.status_change[0].status
3915 3915
3916 3916 @property
3917 3917 def review_status_lbl(self):
3918 3918 if self.status_change:
3919 3919 return self.status_change[0].status_lbl
3920 3920
3921 3921 def __repr__(self):
3922 3922 if self.comment_id:
3923 3923 return '<DB:Comment #%s>' % self.comment_id
3924 3924 else:
3925 3925 return '<DB:Comment at %#x>' % id(self)
3926 3926
3927 3927 def get_api_data(self):
3928 3928 comment = self
3929 3929
3930 3930 data = {
3931 3931 'comment_id': comment.comment_id,
3932 3932 'comment_type': comment.comment_type,
3933 3933 'comment_text': comment.text,
3934 3934 'comment_status': comment.status_change,
3935 3935 'comment_f_path': comment.f_path,
3936 3936 'comment_lineno': comment.line_no,
3937 3937 'comment_author': comment.author,
3938 3938 'comment_created_on': comment.created_on,
3939 3939 'comment_resolved_by': self.resolved,
3940 3940 'comment_commit_id': comment.revision,
3941 3941 'comment_pull_request_id': comment.pull_request_id,
3942 3942 'comment_last_version': self.last_version
3943 3943 }
3944 3944 return data
3945 3945
3946 3946 def __json__(self):
3947 3947 data = dict()
3948 3948 data.update(self.get_api_data())
3949 3949 return data
3950 3950
3951 3951
3952 3952 class ChangesetCommentHistory(Base, BaseModel):
3953 3953 __tablename__ = 'changeset_comments_history'
3954 3954 __table_args__ = (
3955 3955 Index('cch_comment_id_idx', 'comment_id'),
3956 3956 base_table_args,
3957 3957 )
3958 3958
3959 3959 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3960 3960 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3961 3961 version = Column("version", Integer(), nullable=False, default=0)
3962 3962 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3963 3963 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3964 3964 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3965 3965 deleted = Column('deleted', Boolean(), default=False)
3966 3966
3967 3967 author = relationship('User', lazy='joined')
3968 3968 comment = relationship('ChangesetComment', cascade="all, delete")
3969 3969
3970 3970 @classmethod
3971 3971 def get_version(cls, comment_id):
3972 3972 q = Session().query(ChangesetCommentHistory).filter(
3973 3973 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
3974 3974 if q.count() == 0:
3975 3975 return 1
3976 3976 elif q.count() >= q[0].version:
3977 3977 return q.count() + 1
3978 3978 else:
3979 3979 return q[0].version + 1
3980 3980
3981 3981
3982 3982 class ChangesetStatus(Base, BaseModel):
3983 3983 __tablename__ = 'changeset_statuses'
3984 3984 __table_args__ = (
3985 3985 Index('cs_revision_idx', 'revision'),
3986 3986 Index('cs_version_idx', 'version'),
3987 3987 UniqueConstraint('repo_id', 'revision', 'version'),
3988 3988 base_table_args
3989 3989 )
3990 3990
3991 3991 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3992 3992 STATUS_APPROVED = 'approved'
3993 3993 STATUS_REJECTED = 'rejected'
3994 3994 STATUS_UNDER_REVIEW = 'under_review'
3995 3995 CheckConstraint,
3996 3996 STATUSES = [
3997 3997 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3998 3998 (STATUS_APPROVED, _("Approved")),
3999 3999 (STATUS_REJECTED, _("Rejected")),
4000 4000 (STATUS_UNDER_REVIEW, _("Under Review")),
4001 4001 ]
4002 4002
4003 4003 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4004 4004 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4005 4005 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4006 4006 revision = Column('revision', String(40), nullable=False)
4007 4007 status = Column('status', String(128), nullable=False, default=DEFAULT)
4008 4008 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4009 4009 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4010 4010 version = Column('version', Integer(), nullable=False, default=0)
4011 4011 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4012 4012
4013 4013 author = relationship('User', lazy='select')
4014 4014 repo = relationship('Repository', lazy='select')
4015 4015 comment = relationship('ChangesetComment', lazy='select')
4016 4016 pull_request = relationship('PullRequest', lazy='select')
4017 4017
4018 4018 def __unicode__(self):
4019 4019 return u"<%s('%s[v%s]:%s')>" % (
4020 4020 self.__class__.__name__,
4021 4021 self.status, self.version, self.author
4022 4022 )
4023 4023
4024 4024 @classmethod
4025 4025 def get_status_lbl(cls, value):
4026 4026 return dict(cls.STATUSES).get(value)
4027 4027
4028 4028 @property
4029 4029 def status_lbl(self):
4030 4030 return ChangesetStatus.get_status_lbl(self.status)
4031 4031
4032 4032 def get_api_data(self):
4033 4033 status = self
4034 4034 data = {
4035 4035 'status_id': status.changeset_status_id,
4036 4036 'status': status.status,
4037 4037 }
4038 4038 return data
4039 4039
4040 4040 def __json__(self):
4041 4041 data = dict()
4042 4042 data.update(self.get_api_data())
4043 4043 return data
4044 4044
4045 4045
4046 4046 class _SetState(object):
4047 4047 """
4048 4048 Context processor allowing changing state for sensitive operation such as
4049 4049 pull request update or merge
4050 4050 """
4051 4051
4052 4052 def __init__(self, pull_request, pr_state, back_state=None):
4053 4053 self._pr = pull_request
4054 4054 self._org_state = back_state or pull_request.pull_request_state
4055 4055 self._pr_state = pr_state
4056 4056 self._current_state = None
4057 4057
4058 4058 def __enter__(self):
4059 4059 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4060 4060 self._pr, self._pr_state)
4061 4061 self.set_pr_state(self._pr_state)
4062 4062 return self
4063 4063
4064 4064 def __exit__(self, exc_type, exc_val, exc_tb):
4065 4065 if exc_val is not None:
4066 4066 log.error(traceback.format_exc(exc_tb))
4067 4067 return None
4068 4068
4069 4069 self.set_pr_state(self._org_state)
4070 4070 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4071 4071 self._pr, self._org_state)
4072 4072
4073 4073 @property
4074 4074 def state(self):
4075 4075 return self._current_state
4076 4076
4077 4077 def set_pr_state(self, pr_state):
4078 4078 try:
4079 4079 self._pr.pull_request_state = pr_state
4080 4080 Session().add(self._pr)
4081 4081 Session().commit()
4082 4082 self._current_state = pr_state
4083 4083 except Exception:
4084 4084 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4085 4085 raise
4086 4086
4087 4087
4088 4088 class _PullRequestBase(BaseModel):
4089 4089 """
4090 4090 Common attributes of pull request and version entries.
4091 4091 """
4092 4092
4093 4093 # .status values
4094 4094 STATUS_NEW = u'new'
4095 4095 STATUS_OPEN = u'open'
4096 4096 STATUS_CLOSED = u'closed'
4097 4097
4098 4098 # available states
4099 4099 STATE_CREATING = u'creating'
4100 4100 STATE_UPDATING = u'updating'
4101 4101 STATE_MERGING = u'merging'
4102 4102 STATE_CREATED = u'created'
4103 4103
4104 4104 title = Column('title', Unicode(255), nullable=True)
4105 4105 description = Column(
4106 4106 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4107 4107 nullable=True)
4108 4108 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4109 4109
4110 4110 # new/open/closed status of pull request (not approve/reject/etc)
4111 4111 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4112 4112 created_on = Column(
4113 4113 'created_on', DateTime(timezone=False), nullable=False,
4114 4114 default=datetime.datetime.now)
4115 4115 updated_on = Column(
4116 4116 'updated_on', DateTime(timezone=False), nullable=False,
4117 4117 default=datetime.datetime.now)
4118 4118
4119 4119 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4120 4120
4121 4121 @declared_attr
4122 4122 def user_id(cls):
4123 4123 return Column(
4124 4124 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4125 4125 unique=None)
4126 4126
4127 4127 # 500 revisions max
4128 4128 _revisions = Column(
4129 4129 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4130 4130
4131 4131 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4132 4132
4133 4133 @declared_attr
4134 4134 def source_repo_id(cls):
4135 4135 # TODO: dan: rename column to source_repo_id
4136 4136 return Column(
4137 4137 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4138 4138 nullable=False)
4139 4139
4140 4140 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4141 4141
4142 4142 @hybrid_property
4143 4143 def source_ref(self):
4144 4144 return self._source_ref
4145 4145
4146 4146 @source_ref.setter
4147 4147 def source_ref(self, val):
4148 4148 parts = (val or '').split(':')
4149 4149 if len(parts) != 3:
4150 4150 raise ValueError(
4151 4151 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4152 4152 self._source_ref = safe_unicode(val)
4153 4153
4154 4154 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4155 4155
4156 4156 @hybrid_property
4157 4157 def target_ref(self):
4158 4158 return self._target_ref
4159 4159
4160 4160 @target_ref.setter
4161 4161 def target_ref(self, val):
4162 4162 parts = (val or '').split(':')
4163 4163 if len(parts) != 3:
4164 4164 raise ValueError(
4165 4165 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4166 4166 self._target_ref = safe_unicode(val)
4167 4167
4168 4168 @declared_attr
4169 4169 def target_repo_id(cls):
4170 4170 # TODO: dan: rename column to target_repo_id
4171 4171 return Column(
4172 4172 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4173 4173 nullable=False)
4174 4174
4175 4175 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4176 4176
4177 4177 # TODO: dan: rename column to last_merge_source_rev
4178 4178 _last_merge_source_rev = Column(
4179 4179 'last_merge_org_rev', String(40), nullable=True)
4180 4180 # TODO: dan: rename column to last_merge_target_rev
4181 4181 _last_merge_target_rev = Column(
4182 4182 'last_merge_other_rev', String(40), nullable=True)
4183 4183 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4184 4184 last_merge_metadata = Column(
4185 4185 'last_merge_metadata', MutationObj.as_mutable(
4186 4186 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4187 4187
4188 4188 merge_rev = Column('merge_rev', String(40), nullable=True)
4189 4189
4190 4190 reviewer_data = Column(
4191 4191 'reviewer_data_json', MutationObj.as_mutable(
4192 4192 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4193 4193
4194 4194 @property
4195 4195 def reviewer_data_json(self):
4196 4196 return json.dumps(self.reviewer_data)
4197 4197
4198 4198 @property
4199 4199 def last_merge_metadata_parsed(self):
4200 4200 metadata = {}
4201 4201 if not self.last_merge_metadata:
4202 4202 return metadata
4203 4203
4204 4204 if hasattr(self.last_merge_metadata, 'de_coerce'):
4205 4205 for k, v in self.last_merge_metadata.de_coerce().items():
4206 4206 if k in ['target_ref', 'source_ref']:
4207 4207 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4208 4208 else:
4209 4209 if hasattr(v, 'de_coerce'):
4210 4210 metadata[k] = v.de_coerce()
4211 4211 else:
4212 4212 metadata[k] = v
4213 4213 return metadata
4214 4214
4215 4215 @property
4216 4216 def work_in_progress(self):
4217 4217 """checks if pull request is work in progress by checking the title"""
4218 4218 title = self.title.upper()
4219 4219 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4220 4220 return True
4221 4221 return False
4222 4222
4223 @property
4224 def title_safe(self):
4225 return self.title\
4226 .replace('{', '{{')\
4227 .replace('}', '}}')
4228
4223 4229 @hybrid_property
4224 4230 def description_safe(self):
4225 4231 from rhodecode.lib import helpers as h
4226 4232 return h.escape(self.description)
4227 4233
4228 4234 @hybrid_property
4229 4235 def revisions(self):
4230 4236 return self._revisions.split(':') if self._revisions else []
4231 4237
4232 4238 @revisions.setter
4233 4239 def revisions(self, val):
4234 4240 self._revisions = u':'.join(val)
4235 4241
4236 4242 @hybrid_property
4237 4243 def last_merge_status(self):
4238 4244 return safe_int(self._last_merge_status)
4239 4245
4240 4246 @last_merge_status.setter
4241 4247 def last_merge_status(self, val):
4242 4248 self._last_merge_status = val
4243 4249
4244 4250 @declared_attr
4245 4251 def author(cls):
4246 4252 return relationship('User', lazy='joined')
4247 4253
4248 4254 @declared_attr
4249 4255 def source_repo(cls):
4250 4256 return relationship(
4251 4257 'Repository',
4252 4258 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4253 4259
4254 4260 @property
4255 4261 def source_ref_parts(self):
4256 4262 return self.unicode_to_reference(self.source_ref)
4257 4263
4258 4264 @declared_attr
4259 4265 def target_repo(cls):
4260 4266 return relationship(
4261 4267 'Repository',
4262 4268 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4263 4269
4264 4270 @property
4265 4271 def target_ref_parts(self):
4266 4272 return self.unicode_to_reference(self.target_ref)
4267 4273
4268 4274 @property
4269 4275 def shadow_merge_ref(self):
4270 4276 return self.unicode_to_reference(self._shadow_merge_ref)
4271 4277
4272 4278 @shadow_merge_ref.setter
4273 4279 def shadow_merge_ref(self, ref):
4274 4280 self._shadow_merge_ref = self.reference_to_unicode(ref)
4275 4281
4276 4282 @staticmethod
4277 4283 def unicode_to_reference(raw):
4278 4284 return unicode_to_reference(raw)
4279 4285
4280 4286 @staticmethod
4281 4287 def reference_to_unicode(ref):
4282 4288 return reference_to_unicode(ref)
4283 4289
4284 4290 def get_api_data(self, with_merge_state=True):
4285 4291 from rhodecode.model.pull_request import PullRequestModel
4286 4292
4287 4293 pull_request = self
4288 4294 if with_merge_state:
4289 4295 merge_response, merge_status, msg = \
4290 4296 PullRequestModel().merge_status(pull_request)
4291 4297 merge_state = {
4292 4298 'status': merge_status,
4293 4299 'message': safe_unicode(msg),
4294 4300 }
4295 4301 else:
4296 4302 merge_state = {'status': 'not_available',
4297 4303 'message': 'not_available'}
4298 4304
4299 4305 merge_data = {
4300 4306 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4301 4307 'reference': (
4302 4308 pull_request.shadow_merge_ref._asdict()
4303 4309 if pull_request.shadow_merge_ref else None),
4304 4310 }
4305 4311
4306 4312 data = {
4307 4313 'pull_request_id': pull_request.pull_request_id,
4308 4314 'url': PullRequestModel().get_url(pull_request),
4309 4315 'title': pull_request.title,
4310 4316 'description': pull_request.description,
4311 4317 'status': pull_request.status,
4312 4318 'state': pull_request.pull_request_state,
4313 4319 'created_on': pull_request.created_on,
4314 4320 'updated_on': pull_request.updated_on,
4315 4321 'commit_ids': pull_request.revisions,
4316 4322 'review_status': pull_request.calculated_review_status(),
4317 4323 'mergeable': merge_state,
4318 4324 'source': {
4319 4325 'clone_url': pull_request.source_repo.clone_url(),
4320 4326 'repository': pull_request.source_repo.repo_name,
4321 4327 'reference': {
4322 4328 'name': pull_request.source_ref_parts.name,
4323 4329 'type': pull_request.source_ref_parts.type,
4324 4330 'commit_id': pull_request.source_ref_parts.commit_id,
4325 4331 },
4326 4332 },
4327 4333 'target': {
4328 4334 'clone_url': pull_request.target_repo.clone_url(),
4329 4335 'repository': pull_request.target_repo.repo_name,
4330 4336 'reference': {
4331 4337 'name': pull_request.target_ref_parts.name,
4332 4338 'type': pull_request.target_ref_parts.type,
4333 4339 'commit_id': pull_request.target_ref_parts.commit_id,
4334 4340 },
4335 4341 },
4336 4342 'merge': merge_data,
4337 4343 'author': pull_request.author.get_api_data(include_secrets=False,
4338 4344 details='basic'),
4339 4345 'reviewers': [
4340 4346 {
4341 4347 'user': reviewer.get_api_data(include_secrets=False,
4342 4348 details='basic'),
4343 4349 'reasons': reasons,
4344 4350 'review_status': st[0][1].status if st else 'not_reviewed',
4345 4351 }
4346 4352 for obj, reviewer, reasons, mandatory, st in
4347 4353 pull_request.reviewers_statuses()
4348 4354 ]
4349 4355 }
4350 4356
4351 4357 return data
4352 4358
4353 4359 def set_state(self, pull_request_state, final_state=None):
4354 4360 """
4355 4361 # goes from initial state to updating to initial state.
4356 4362 # initial state can be changed by specifying back_state=
4357 4363 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4358 4364 pull_request.merge()
4359 4365
4360 4366 :param pull_request_state:
4361 4367 :param final_state:
4362 4368
4363 4369 """
4364 4370
4365 4371 return _SetState(self, pull_request_state, back_state=final_state)
4366 4372
4367 4373
4368 4374 class PullRequest(Base, _PullRequestBase):
4369 4375 __tablename__ = 'pull_requests'
4370 4376 __table_args__ = (
4371 4377 base_table_args,
4372 4378 )
4373 4379 LATEST_VER = 'latest'
4374 4380
4375 4381 pull_request_id = Column(
4376 4382 'pull_request_id', Integer(), nullable=False, primary_key=True)
4377 4383
4378 4384 def __repr__(self):
4379 4385 if self.pull_request_id:
4380 4386 return '<DB:PullRequest #%s>' % self.pull_request_id
4381 4387 else:
4382 4388 return '<DB:PullRequest at %#x>' % id(self)
4383 4389
4384 4390 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4385 4391 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4386 4392 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4387 4393 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4388 4394 lazy='dynamic')
4389 4395
4390 4396 @classmethod
4391 4397 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4392 4398 internal_methods=None):
4393 4399
4394 4400 class PullRequestDisplay(object):
4395 4401 """
4396 4402 Special object wrapper for showing PullRequest data via Versions
4397 4403 It mimics PR object as close as possible. This is read only object
4398 4404 just for display
4399 4405 """
4400 4406
4401 4407 def __init__(self, attrs, internal=None):
4402 4408 self.attrs = attrs
4403 4409 # internal have priority over the given ones via attrs
4404 4410 self.internal = internal or ['versions']
4405 4411
4406 4412 def __getattr__(self, item):
4407 4413 if item in self.internal:
4408 4414 return getattr(self, item)
4409 4415 try:
4410 4416 return self.attrs[item]
4411 4417 except KeyError:
4412 4418 raise AttributeError(
4413 4419 '%s object has no attribute %s' % (self, item))
4414 4420
4415 4421 def __repr__(self):
4416 4422 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4417 4423
4418 4424 def versions(self):
4419 4425 return pull_request_obj.versions.order_by(
4420 4426 PullRequestVersion.pull_request_version_id).all()
4421 4427
4422 4428 def is_closed(self):
4423 4429 return pull_request_obj.is_closed()
4424 4430
4425 4431 def is_state_changing(self):
4426 4432 return pull_request_obj.is_state_changing()
4427 4433
4428 4434 @property
4429 4435 def pull_request_version_id(self):
4430 4436 return getattr(pull_request_obj, 'pull_request_version_id', None)
4431 4437
4432 4438 @property
4433 4439 def pull_request_last_version(self):
4434 4440 return pull_request_obj.pull_request_last_version
4435 4441
4436 4442 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4437 4443
4438 4444 attrs.author = StrictAttributeDict(
4439 4445 pull_request_obj.author.get_api_data())
4440 4446 if pull_request_obj.target_repo:
4441 4447 attrs.target_repo = StrictAttributeDict(
4442 4448 pull_request_obj.target_repo.get_api_data())
4443 4449 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4444 4450
4445 4451 if pull_request_obj.source_repo:
4446 4452 attrs.source_repo = StrictAttributeDict(
4447 4453 pull_request_obj.source_repo.get_api_data())
4448 4454 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4449 4455
4450 4456 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4451 4457 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4452 4458 attrs.revisions = pull_request_obj.revisions
4453 4459 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4454 4460 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4455 4461 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4456 4462 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4457 4463
4458 4464 return PullRequestDisplay(attrs, internal=internal_methods)
4459 4465
4460 4466 def is_closed(self):
4461 4467 return self.status == self.STATUS_CLOSED
4462 4468
4463 4469 def is_state_changing(self):
4464 4470 return self.pull_request_state != PullRequest.STATE_CREATED
4465 4471
4466 4472 def __json__(self):
4467 4473 return {
4468 4474 'revisions': self.revisions,
4469 4475 'versions': self.versions_count
4470 4476 }
4471 4477
4472 4478 def calculated_review_status(self):
4473 4479 from rhodecode.model.changeset_status import ChangesetStatusModel
4474 4480 return ChangesetStatusModel().calculated_review_status(self)
4475 4481
4476 4482 def reviewers_statuses(self):
4477 4483 from rhodecode.model.changeset_status import ChangesetStatusModel
4478 4484 return ChangesetStatusModel().reviewers_statuses(self)
4479 4485
4480 4486 def get_pull_request_reviewers(self, role=None):
4481 4487 qry = PullRequestReviewers.query()\
4482 4488 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4483 4489 if role:
4484 4490 qry = qry.filter(PullRequestReviewers.role == role)
4485 4491
4486 4492 return qry.all()
4487 4493
4488 4494 @property
4489 4495 def reviewers_count(self):
4490 4496 qry = PullRequestReviewers.query()\
4491 4497 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4492 4498 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4493 4499 return qry.count()
4494 4500
4495 4501 @property
4496 4502 def observers_count(self):
4497 4503 qry = PullRequestReviewers.query()\
4498 4504 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4499 4505 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4500 4506 return qry.count()
4501 4507
4502 4508 def observers(self):
4503 4509 qry = PullRequestReviewers.query()\
4504 4510 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4505 4511 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4506 4512 .all()
4507 4513
4508 4514 for entry in qry:
4509 4515 yield entry, entry.user
4510 4516
4511 4517 @property
4512 4518 def workspace_id(self):
4513 4519 from rhodecode.model.pull_request import PullRequestModel
4514 4520 return PullRequestModel()._workspace_id(self)
4515 4521
4516 4522 def get_shadow_repo(self):
4517 4523 workspace_id = self.workspace_id
4518 4524 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4519 4525 if os.path.isdir(shadow_repository_path):
4520 4526 vcs_obj = self.target_repo.scm_instance()
4521 4527 return vcs_obj.get_shadow_instance(shadow_repository_path)
4522 4528
4523 4529 @property
4524 4530 def versions_count(self):
4525 4531 """
4526 4532 return number of versions this PR have, e.g a PR that once been
4527 4533 updated will have 2 versions
4528 4534 """
4529 4535 return self.versions.count() + 1
4530 4536
4531 4537 @property
4532 4538 def pull_request_last_version(self):
4533 4539 return self.versions_count
4534 4540
4535 4541
4536 4542 class PullRequestVersion(Base, _PullRequestBase):
4537 4543 __tablename__ = 'pull_request_versions'
4538 4544 __table_args__ = (
4539 4545 base_table_args,
4540 4546 )
4541 4547
4542 4548 pull_request_version_id = Column(
4543 4549 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4544 4550 pull_request_id = Column(
4545 4551 'pull_request_id', Integer(),
4546 4552 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4547 4553 pull_request = relationship('PullRequest')
4548 4554
4549 4555 def __repr__(self):
4550 4556 if self.pull_request_version_id:
4551 4557 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4552 4558 else:
4553 4559 return '<DB:PullRequestVersion at %#x>' % id(self)
4554 4560
4555 4561 @property
4556 4562 def reviewers(self):
4557 4563 return self.pull_request.reviewers
4558 4564 @property
4559 4565 def reviewers(self):
4560 4566 return self.pull_request.reviewers
4561 4567
4562 4568 @property
4563 4569 def versions(self):
4564 4570 return self.pull_request.versions
4565 4571
4566 4572 def is_closed(self):
4567 4573 # calculate from original
4568 4574 return self.pull_request.status == self.STATUS_CLOSED
4569 4575
4570 4576 def is_state_changing(self):
4571 4577 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4572 4578
4573 4579 def calculated_review_status(self):
4574 4580 return self.pull_request.calculated_review_status()
4575 4581
4576 4582 def reviewers_statuses(self):
4577 4583 return self.pull_request.reviewers_statuses()
4578 4584
4579 4585 def observers(self):
4580 4586 return self.pull_request.observers()
4581 4587
4582 4588
4583 4589 class PullRequestReviewers(Base, BaseModel):
4584 4590 __tablename__ = 'pull_request_reviewers'
4585 4591 __table_args__ = (
4586 4592 base_table_args,
4587 4593 )
4588 4594 ROLE_REVIEWER = u'reviewer'
4589 4595 ROLE_OBSERVER = u'observer'
4590 4596 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4591 4597
4592 4598 @hybrid_property
4593 4599 def reasons(self):
4594 4600 if not self._reasons:
4595 4601 return []
4596 4602 return self._reasons
4597 4603
4598 4604 @reasons.setter
4599 4605 def reasons(self, val):
4600 4606 val = val or []
4601 4607 if any(not isinstance(x, compat.string_types) for x in val):
4602 4608 raise Exception('invalid reasons type, must be list of strings')
4603 4609 self._reasons = val
4604 4610
4605 4611 pull_requests_reviewers_id = Column(
4606 4612 'pull_requests_reviewers_id', Integer(), nullable=False,
4607 4613 primary_key=True)
4608 4614 pull_request_id = Column(
4609 4615 "pull_request_id", Integer(),
4610 4616 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4611 4617 user_id = Column(
4612 4618 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4613 4619 _reasons = Column(
4614 4620 'reason', MutationList.as_mutable(
4615 4621 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4616 4622
4617 4623 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4618 4624 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4619 4625
4620 4626 user = relationship('User')
4621 4627 pull_request = relationship('PullRequest')
4622 4628
4623 4629 rule_data = Column(
4624 4630 'rule_data_json',
4625 4631 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4626 4632
4627 4633 def rule_user_group_data(self):
4628 4634 """
4629 4635 Returns the voting user group rule data for this reviewer
4630 4636 """
4631 4637
4632 4638 if self.rule_data and 'vote_rule' in self.rule_data:
4633 4639 user_group_data = {}
4634 4640 if 'rule_user_group_entry_id' in self.rule_data:
4635 4641 # means a group with voting rules !
4636 4642 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4637 4643 user_group_data['name'] = self.rule_data['rule_name']
4638 4644 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4639 4645
4640 4646 return user_group_data
4641 4647
4642 4648 @classmethod
4643 4649 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4644 4650 qry = PullRequestReviewers.query()\
4645 4651 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4646 4652 if role:
4647 4653 qry = qry.filter(PullRequestReviewers.role == role)
4648 4654
4649 4655 return qry.all()
4650 4656
4651 4657 def __unicode__(self):
4652 4658 return u"<%s('id:%s')>" % (self.__class__.__name__,
4653 4659 self.pull_requests_reviewers_id)
4654 4660
4655 4661
4656 4662 class Notification(Base, BaseModel):
4657 4663 __tablename__ = 'notifications'
4658 4664 __table_args__ = (
4659 4665 Index('notification_type_idx', 'type'),
4660 4666 base_table_args,
4661 4667 )
4662 4668
4663 4669 TYPE_CHANGESET_COMMENT = u'cs_comment'
4664 4670 TYPE_MESSAGE = u'message'
4665 4671 TYPE_MENTION = u'mention'
4666 4672 TYPE_REGISTRATION = u'registration'
4667 4673 TYPE_PULL_REQUEST = u'pull_request'
4668 4674 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4669 4675 TYPE_PULL_REQUEST_UPDATE = u'pull_request_update'
4670 4676
4671 4677 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4672 4678 subject = Column('subject', Unicode(512), nullable=True)
4673 4679 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4674 4680 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4675 4681 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4676 4682 type_ = Column('type', Unicode(255))
4677 4683
4678 4684 created_by_user = relationship('User')
4679 4685 notifications_to_users = relationship('UserNotification', lazy='joined',
4680 4686 cascade="all, delete-orphan")
4681 4687
4682 4688 @property
4683 4689 def recipients(self):
4684 4690 return [x.user for x in UserNotification.query()\
4685 4691 .filter(UserNotification.notification == self)\
4686 4692 .order_by(UserNotification.user_id.asc()).all()]
4687 4693
4688 4694 @classmethod
4689 4695 def create(cls, created_by, subject, body, recipients, type_=None):
4690 4696 if type_ is None:
4691 4697 type_ = Notification.TYPE_MESSAGE
4692 4698
4693 4699 notification = cls()
4694 4700 notification.created_by_user = created_by
4695 4701 notification.subject = subject
4696 4702 notification.body = body
4697 4703 notification.type_ = type_
4698 4704 notification.created_on = datetime.datetime.now()
4699 4705
4700 4706 # For each recipient link the created notification to his account
4701 4707 for u in recipients:
4702 4708 assoc = UserNotification()
4703 4709 assoc.user_id = u.user_id
4704 4710 assoc.notification = notification
4705 4711
4706 4712 # if created_by is inside recipients mark his notification
4707 4713 # as read
4708 4714 if u.user_id == created_by.user_id:
4709 4715 assoc.read = True
4710 4716 Session().add(assoc)
4711 4717
4712 4718 Session().add(notification)
4713 4719
4714 4720 return notification
4715 4721
4716 4722
4717 4723 class UserNotification(Base, BaseModel):
4718 4724 __tablename__ = 'user_to_notification'
4719 4725 __table_args__ = (
4720 4726 UniqueConstraint('user_id', 'notification_id'),
4721 4727 base_table_args
4722 4728 )
4723 4729
4724 4730 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4725 4731 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4726 4732 read = Column('read', Boolean, default=False)
4727 4733 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4728 4734
4729 4735 user = relationship('User', lazy="joined")
4730 4736 notification = relationship('Notification', lazy="joined",
4731 4737 order_by=lambda: Notification.created_on.desc(),)
4732 4738
4733 4739 def mark_as_read(self):
4734 4740 self.read = True
4735 4741 Session().add(self)
4736 4742
4737 4743
4738 4744 class UserNotice(Base, BaseModel):
4739 4745 __tablename__ = 'user_notices'
4740 4746 __table_args__ = (
4741 4747 base_table_args
4742 4748 )
4743 4749
4744 4750 NOTIFICATION_TYPE_MESSAGE = 'message'
4745 4751 NOTIFICATION_TYPE_NOTICE = 'notice'
4746 4752
4747 4753 NOTIFICATION_LEVEL_INFO = 'info'
4748 4754 NOTIFICATION_LEVEL_WARNING = 'warning'
4749 4755 NOTIFICATION_LEVEL_ERROR = 'error'
4750 4756
4751 4757 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4752 4758
4753 4759 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4754 4760 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4755 4761
4756 4762 notice_read = Column('notice_read', Boolean, default=False)
4757 4763
4758 4764 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4759 4765 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4760 4766
4761 4767 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4762 4768 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4763 4769
4764 4770 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4765 4771 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4766 4772
4767 4773 @classmethod
4768 4774 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4769 4775
4770 4776 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4771 4777 cls.NOTIFICATION_LEVEL_WARNING,
4772 4778 cls.NOTIFICATION_LEVEL_INFO]:
4773 4779 return
4774 4780
4775 4781 from rhodecode.model.user import UserModel
4776 4782 user = UserModel().get_user(user)
4777 4783
4778 4784 new_notice = UserNotice()
4779 4785 if not allow_duplicate:
4780 4786 existing_msg = UserNotice().query() \
4781 4787 .filter(UserNotice.user == user) \
4782 4788 .filter(UserNotice.notice_body == body) \
4783 4789 .filter(UserNotice.notice_read == false()) \
4784 4790 .scalar()
4785 4791 if existing_msg:
4786 4792 log.warning('Ignoring duplicate notice for user %s', user)
4787 4793 return
4788 4794
4789 4795 new_notice.user = user
4790 4796 new_notice.notice_subject = subject
4791 4797 new_notice.notice_body = body
4792 4798 new_notice.notification_level = notice_level
4793 4799 Session().add(new_notice)
4794 4800 Session().commit()
4795 4801
4796 4802
4797 4803 class Gist(Base, BaseModel):
4798 4804 __tablename__ = 'gists'
4799 4805 __table_args__ = (
4800 4806 Index('g_gist_access_id_idx', 'gist_access_id'),
4801 4807 Index('g_created_on_idx', 'created_on'),
4802 4808 base_table_args
4803 4809 )
4804 4810
4805 4811 GIST_PUBLIC = u'public'
4806 4812 GIST_PRIVATE = u'private'
4807 4813 DEFAULT_FILENAME = u'gistfile1.txt'
4808 4814
4809 4815 ACL_LEVEL_PUBLIC = u'acl_public'
4810 4816 ACL_LEVEL_PRIVATE = u'acl_private'
4811 4817
4812 4818 gist_id = Column('gist_id', Integer(), primary_key=True)
4813 4819 gist_access_id = Column('gist_access_id', Unicode(250))
4814 4820 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4815 4821 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4816 4822 gist_expires = Column('gist_expires', Float(53), nullable=False)
4817 4823 gist_type = Column('gist_type', Unicode(128), nullable=False)
4818 4824 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4819 4825 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4820 4826 acl_level = Column('acl_level', Unicode(128), nullable=True)
4821 4827
4822 4828 owner = relationship('User')
4823 4829
4824 4830 def __repr__(self):
4825 4831 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4826 4832
4827 4833 @hybrid_property
4828 4834 def description_safe(self):
4829 4835 from rhodecode.lib import helpers as h
4830 4836 return h.escape(self.gist_description)
4831 4837
4832 4838 @classmethod
4833 4839 def get_or_404(cls, id_):
4834 4840 from pyramid.httpexceptions import HTTPNotFound
4835 4841
4836 4842 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4837 4843 if not res:
4838 4844 log.debug('WARN: No DB entry with id %s', id_)
4839 4845 raise HTTPNotFound()
4840 4846 return res
4841 4847
4842 4848 @classmethod
4843 4849 def get_by_access_id(cls, gist_access_id):
4844 4850 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4845 4851
4846 4852 def gist_url(self):
4847 4853 from rhodecode.model.gist import GistModel
4848 4854 return GistModel().get_url(self)
4849 4855
4850 4856 @classmethod
4851 4857 def base_path(cls):
4852 4858 """
4853 4859 Returns base path when all gists are stored
4854 4860
4855 4861 :param cls:
4856 4862 """
4857 4863 from rhodecode.model.gist import GIST_STORE_LOC
4858 4864 q = Session().query(RhodeCodeUi)\
4859 4865 .filter(RhodeCodeUi.ui_key == URL_SEP)
4860 4866 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4861 4867 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4862 4868
4863 4869 def get_api_data(self):
4864 4870 """
4865 4871 Common function for generating gist related data for API
4866 4872 """
4867 4873 gist = self
4868 4874 data = {
4869 4875 'gist_id': gist.gist_id,
4870 4876 'type': gist.gist_type,
4871 4877 'access_id': gist.gist_access_id,
4872 4878 'description': gist.gist_description,
4873 4879 'url': gist.gist_url(),
4874 4880 'expires': gist.gist_expires,
4875 4881 'created_on': gist.created_on,
4876 4882 'modified_at': gist.modified_at,
4877 4883 'content': None,
4878 4884 'acl_level': gist.acl_level,
4879 4885 }
4880 4886 return data
4881 4887
4882 4888 def __json__(self):
4883 4889 data = dict(
4884 4890 )
4885 4891 data.update(self.get_api_data())
4886 4892 return data
4887 4893 # SCM functions
4888 4894
4889 4895 def scm_instance(self, **kwargs):
4890 4896 """
4891 4897 Get an instance of VCS Repository
4892 4898
4893 4899 :param kwargs:
4894 4900 """
4895 4901 from rhodecode.model.gist import GistModel
4896 4902 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4897 4903 return get_vcs_instance(
4898 4904 repo_path=safe_str(full_repo_path), create=False,
4899 4905 _vcs_alias=GistModel.vcs_backend)
4900 4906
4901 4907
4902 4908 class ExternalIdentity(Base, BaseModel):
4903 4909 __tablename__ = 'external_identities'
4904 4910 __table_args__ = (
4905 4911 Index('local_user_id_idx', 'local_user_id'),
4906 4912 Index('external_id_idx', 'external_id'),
4907 4913 base_table_args
4908 4914 )
4909 4915
4910 4916 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4911 4917 external_username = Column('external_username', Unicode(1024), default=u'')
4912 4918 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4913 4919 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4914 4920 access_token = Column('access_token', String(1024), default=u'')
4915 4921 alt_token = Column('alt_token', String(1024), default=u'')
4916 4922 token_secret = Column('token_secret', String(1024), default=u'')
4917 4923
4918 4924 @classmethod
4919 4925 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4920 4926 """
4921 4927 Returns ExternalIdentity instance based on search params
4922 4928
4923 4929 :param external_id:
4924 4930 :param provider_name:
4925 4931 :return: ExternalIdentity
4926 4932 """
4927 4933 query = cls.query()
4928 4934 query = query.filter(cls.external_id == external_id)
4929 4935 query = query.filter(cls.provider_name == provider_name)
4930 4936 if local_user_id:
4931 4937 query = query.filter(cls.local_user_id == local_user_id)
4932 4938 return query.first()
4933 4939
4934 4940 @classmethod
4935 4941 def user_by_external_id_and_provider(cls, external_id, provider_name):
4936 4942 """
4937 4943 Returns User instance based on search params
4938 4944
4939 4945 :param external_id:
4940 4946 :param provider_name:
4941 4947 :return: User
4942 4948 """
4943 4949 query = User.query()
4944 4950 query = query.filter(cls.external_id == external_id)
4945 4951 query = query.filter(cls.provider_name == provider_name)
4946 4952 query = query.filter(User.user_id == cls.local_user_id)
4947 4953 return query.first()
4948 4954
4949 4955 @classmethod
4950 4956 def by_local_user_id(cls, local_user_id):
4951 4957 """
4952 4958 Returns all tokens for user
4953 4959
4954 4960 :param local_user_id:
4955 4961 :return: ExternalIdentity
4956 4962 """
4957 4963 query = cls.query()
4958 4964 query = query.filter(cls.local_user_id == local_user_id)
4959 4965 return query
4960 4966
4961 4967 @classmethod
4962 4968 def load_provider_plugin(cls, plugin_id):
4963 4969 from rhodecode.authentication.base import loadplugin
4964 4970 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4965 4971 auth_plugin = loadplugin(_plugin_id)
4966 4972 return auth_plugin
4967 4973
4968 4974
4969 4975 class Integration(Base, BaseModel):
4970 4976 __tablename__ = 'integrations'
4971 4977 __table_args__ = (
4972 4978 base_table_args
4973 4979 )
4974 4980
4975 4981 integration_id = Column('integration_id', Integer(), primary_key=True)
4976 4982 integration_type = Column('integration_type', String(255))
4977 4983 enabled = Column('enabled', Boolean(), nullable=False)
4978 4984 name = Column('name', String(255), nullable=False)
4979 4985 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4980 4986 default=False)
4981 4987
4982 4988 settings = Column(
4983 4989 'settings_json', MutationObj.as_mutable(
4984 4990 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4985 4991 repo_id = Column(
4986 4992 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4987 4993 nullable=True, unique=None, default=None)
4988 4994 repo = relationship('Repository', lazy='joined')
4989 4995
4990 4996 repo_group_id = Column(
4991 4997 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4992 4998 nullable=True, unique=None, default=None)
4993 4999 repo_group = relationship('RepoGroup', lazy='joined')
4994 5000
4995 5001 @property
4996 5002 def scope(self):
4997 5003 if self.repo:
4998 5004 return repr(self.repo)
4999 5005 if self.repo_group:
5000 5006 if self.child_repos_only:
5001 5007 return repr(self.repo_group) + ' (child repos only)'
5002 5008 else:
5003 5009 return repr(self.repo_group) + ' (recursive)'
5004 5010 if self.child_repos_only:
5005 5011 return 'root_repos'
5006 5012 return 'global'
5007 5013
5008 5014 def __repr__(self):
5009 5015 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5010 5016
5011 5017
5012 5018 class RepoReviewRuleUser(Base, BaseModel):
5013 5019 __tablename__ = 'repo_review_rules_users'
5014 5020 __table_args__ = (
5015 5021 base_table_args
5016 5022 )
5017 5023 ROLE_REVIEWER = u'reviewer'
5018 5024 ROLE_OBSERVER = u'observer'
5019 5025 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5020 5026
5021 5027 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5022 5028 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5023 5029 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5024 5030 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5025 5031 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5026 5032 user = relationship('User')
5027 5033
5028 5034 def rule_data(self):
5029 5035 return {
5030 5036 'mandatory': self.mandatory,
5031 5037 'role': self.role,
5032 5038 }
5033 5039
5034 5040
5035 5041 class RepoReviewRuleUserGroup(Base, BaseModel):
5036 5042 __tablename__ = 'repo_review_rules_users_groups'
5037 5043 __table_args__ = (
5038 5044 base_table_args
5039 5045 )
5040 5046
5041 5047 VOTE_RULE_ALL = -1
5042 5048 ROLE_REVIEWER = u'reviewer'
5043 5049 ROLE_OBSERVER = u'observer'
5044 5050 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5045 5051
5046 5052 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5047 5053 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5048 5054 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5049 5055 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5050 5056 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5051 5057 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5052 5058 users_group = relationship('UserGroup')
5053 5059
5054 5060 def rule_data(self):
5055 5061 return {
5056 5062 'mandatory': self.mandatory,
5057 5063 'role': self.role,
5058 5064 'vote_rule': self.vote_rule
5059 5065 }
5060 5066
5061 5067 @property
5062 5068 def vote_rule_label(self):
5063 5069 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5064 5070 return 'all must vote'
5065 5071 else:
5066 5072 return 'min. vote {}'.format(self.vote_rule)
5067 5073
5068 5074
5069 5075 class RepoReviewRule(Base, BaseModel):
5070 5076 __tablename__ = 'repo_review_rules'
5071 5077 __table_args__ = (
5072 5078 base_table_args
5073 5079 )
5074 5080
5075 5081 repo_review_rule_id = Column(
5076 5082 'repo_review_rule_id', Integer(), primary_key=True)
5077 5083 repo_id = Column(
5078 5084 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5079 5085 repo = relationship('Repository', backref='review_rules')
5080 5086
5081 5087 review_rule_name = Column('review_rule_name', String(255))
5082 5088 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5083 5089 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5084 5090 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5085 5091
5086 5092 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5087 5093
5088 5094 # Legacy fields, just for backward compat
5089 5095 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5090 5096 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5091 5097
5092 5098 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5093 5099 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5094 5100
5095 5101 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5096 5102
5097 5103 rule_users = relationship('RepoReviewRuleUser')
5098 5104 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5099 5105
5100 5106 def _validate_pattern(self, value):
5101 5107 re.compile('^' + glob2re(value) + '$')
5102 5108
5103 5109 @hybrid_property
5104 5110 def source_branch_pattern(self):
5105 5111 return self._branch_pattern or '*'
5106 5112
5107 5113 @source_branch_pattern.setter
5108 5114 def source_branch_pattern(self, value):
5109 5115 self._validate_pattern(value)
5110 5116 self._branch_pattern = value or '*'
5111 5117
5112 5118 @hybrid_property
5113 5119 def target_branch_pattern(self):
5114 5120 return self._target_branch_pattern or '*'
5115 5121
5116 5122 @target_branch_pattern.setter
5117 5123 def target_branch_pattern(self, value):
5118 5124 self._validate_pattern(value)
5119 5125 self._target_branch_pattern = value or '*'
5120 5126
5121 5127 @hybrid_property
5122 5128 def file_pattern(self):
5123 5129 return self._file_pattern or '*'
5124 5130
5125 5131 @file_pattern.setter
5126 5132 def file_pattern(self, value):
5127 5133 self._validate_pattern(value)
5128 5134 self._file_pattern = value or '*'
5129 5135
5130 5136 @hybrid_property
5131 5137 def forbid_pr_author_to_review(self):
5132 5138 return self.pr_author == 'forbid_pr_author'
5133 5139
5134 5140 @hybrid_property
5135 5141 def include_pr_author_to_review(self):
5136 5142 return self.pr_author == 'include_pr_author'
5137 5143
5138 5144 @hybrid_property
5139 5145 def forbid_commit_author_to_review(self):
5140 5146 return self.commit_author == 'forbid_commit_author'
5141 5147
5142 5148 @hybrid_property
5143 5149 def include_commit_author_to_review(self):
5144 5150 return self.commit_author == 'include_commit_author'
5145 5151
5146 5152 def matches(self, source_branch, target_branch, files_changed):
5147 5153 """
5148 5154 Check if this review rule matches a branch/files in a pull request
5149 5155
5150 5156 :param source_branch: source branch name for the commit
5151 5157 :param target_branch: target branch name for the commit
5152 5158 :param files_changed: list of file paths changed in the pull request
5153 5159 """
5154 5160
5155 5161 source_branch = source_branch or ''
5156 5162 target_branch = target_branch or ''
5157 5163 files_changed = files_changed or []
5158 5164
5159 5165 branch_matches = True
5160 5166 if source_branch or target_branch:
5161 5167 if self.source_branch_pattern == '*':
5162 5168 source_branch_match = True
5163 5169 else:
5164 5170 if self.source_branch_pattern.startswith('re:'):
5165 5171 source_pattern = self.source_branch_pattern[3:]
5166 5172 else:
5167 5173 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5168 5174 source_branch_regex = re.compile(source_pattern)
5169 5175 source_branch_match = bool(source_branch_regex.search(source_branch))
5170 5176 if self.target_branch_pattern == '*':
5171 5177 target_branch_match = True
5172 5178 else:
5173 5179 if self.target_branch_pattern.startswith('re:'):
5174 5180 target_pattern = self.target_branch_pattern[3:]
5175 5181 else:
5176 5182 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5177 5183 target_branch_regex = re.compile(target_pattern)
5178 5184 target_branch_match = bool(target_branch_regex.search(target_branch))
5179 5185
5180 5186 branch_matches = source_branch_match and target_branch_match
5181 5187
5182 5188 files_matches = True
5183 5189 if self.file_pattern != '*':
5184 5190 files_matches = False
5185 5191 if self.file_pattern.startswith('re:'):
5186 5192 file_pattern = self.file_pattern[3:]
5187 5193 else:
5188 5194 file_pattern = glob2re(self.file_pattern)
5189 5195 file_regex = re.compile(file_pattern)
5190 5196 for file_data in files_changed:
5191 5197 filename = file_data.get('filename')
5192 5198
5193 5199 if file_regex.search(filename):
5194 5200 files_matches = True
5195 5201 break
5196 5202
5197 5203 return branch_matches and files_matches
5198 5204
5199 5205 @property
5200 5206 def review_users(self):
5201 5207 """ Returns the users which this rule applies to """
5202 5208
5203 5209 users = collections.OrderedDict()
5204 5210
5205 5211 for rule_user in self.rule_users:
5206 5212 if rule_user.user.active:
5207 5213 if rule_user.user not in users:
5208 5214 users[rule_user.user.username] = {
5209 5215 'user': rule_user.user,
5210 5216 'source': 'user',
5211 5217 'source_data': {},
5212 5218 'data': rule_user.rule_data()
5213 5219 }
5214 5220
5215 5221 for rule_user_group in self.rule_user_groups:
5216 5222 source_data = {
5217 5223 'user_group_id': rule_user_group.users_group.users_group_id,
5218 5224 'name': rule_user_group.users_group.users_group_name,
5219 5225 'members': len(rule_user_group.users_group.members)
5220 5226 }
5221 5227 for member in rule_user_group.users_group.members:
5222 5228 if member.user.active:
5223 5229 key = member.user.username
5224 5230 if key in users:
5225 5231 # skip this member as we have him already
5226 5232 # this prevents from override the "first" matched
5227 5233 # users with duplicates in multiple groups
5228 5234 continue
5229 5235
5230 5236 users[key] = {
5231 5237 'user': member.user,
5232 5238 'source': 'user_group',
5233 5239 'source_data': source_data,
5234 5240 'data': rule_user_group.rule_data()
5235 5241 }
5236 5242
5237 5243 return users
5238 5244
5239 5245 def user_group_vote_rule(self, user_id):
5240 5246
5241 5247 rules = []
5242 5248 if not self.rule_user_groups:
5243 5249 return rules
5244 5250
5245 5251 for user_group in self.rule_user_groups:
5246 5252 user_group_members = [x.user_id for x in user_group.users_group.members]
5247 5253 if user_id in user_group_members:
5248 5254 rules.append(user_group)
5249 5255 return rules
5250 5256
5251 5257 def __repr__(self):
5252 5258 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
5253 5259 self.repo_review_rule_id, self.repo)
5254 5260
5255 5261
5256 5262 class ScheduleEntry(Base, BaseModel):
5257 5263 __tablename__ = 'schedule_entries'
5258 5264 __table_args__ = (
5259 5265 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5260 5266 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5261 5267 base_table_args,
5262 5268 )
5263 5269
5264 5270 schedule_types = ['crontab', 'timedelta', 'integer']
5265 5271 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5266 5272
5267 5273 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5268 5274 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5269 5275 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5270 5276
5271 5277 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5272 5278 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5273 5279
5274 5280 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5275 5281 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5276 5282
5277 5283 # task
5278 5284 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5279 5285 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5280 5286 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5281 5287 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5282 5288
5283 5289 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5284 5290 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5285 5291
5286 5292 @hybrid_property
5287 5293 def schedule_type(self):
5288 5294 return self._schedule_type
5289 5295
5290 5296 @schedule_type.setter
5291 5297 def schedule_type(self, val):
5292 5298 if val not in self.schedule_types:
5293 5299 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5294 5300 val, self.schedule_type))
5295 5301
5296 5302 self._schedule_type = val
5297 5303
5298 5304 @classmethod
5299 5305 def get_uid(cls, obj):
5300 5306 args = obj.task_args
5301 5307 kwargs = obj.task_kwargs
5302 5308 if isinstance(args, JsonRaw):
5303 5309 try:
5304 5310 args = json.loads(args)
5305 5311 except ValueError:
5306 5312 args = tuple()
5307 5313
5308 5314 if isinstance(kwargs, JsonRaw):
5309 5315 try:
5310 5316 kwargs = json.loads(kwargs)
5311 5317 except ValueError:
5312 5318 kwargs = dict()
5313 5319
5314 5320 dot_notation = obj.task_dot_notation
5315 5321 val = '.'.join(map(safe_str, [
5316 5322 sorted(dot_notation), args, sorted(kwargs.items())]))
5317 5323 return hashlib.sha1(val).hexdigest()
5318 5324
5319 5325 @classmethod
5320 5326 def get_by_schedule_name(cls, schedule_name):
5321 5327 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5322 5328
5323 5329 @classmethod
5324 5330 def get_by_schedule_id(cls, schedule_id):
5325 5331 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5326 5332
5327 5333 @property
5328 5334 def task(self):
5329 5335 return self.task_dot_notation
5330 5336
5331 5337 @property
5332 5338 def schedule(self):
5333 5339 from rhodecode.lib.celerylib.utils import raw_2_schedule
5334 5340 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5335 5341 return schedule
5336 5342
5337 5343 @property
5338 5344 def args(self):
5339 5345 try:
5340 5346 return list(self.task_args or [])
5341 5347 except ValueError:
5342 5348 return list()
5343 5349
5344 5350 @property
5345 5351 def kwargs(self):
5346 5352 try:
5347 5353 return dict(self.task_kwargs or {})
5348 5354 except ValueError:
5349 5355 return dict()
5350 5356
5351 5357 def _as_raw(self, val, indent=None):
5352 5358 if hasattr(val, 'de_coerce'):
5353 5359 val = val.de_coerce()
5354 5360 if val:
5355 5361 val = json.dumps(val, indent=indent, sort_keys=True)
5356 5362
5357 5363 return val
5358 5364
5359 5365 @property
5360 5366 def schedule_definition_raw(self):
5361 5367 return self._as_raw(self.schedule_definition)
5362 5368
5363 5369 def args_raw(self, indent=None):
5364 5370 return self._as_raw(self.task_args, indent)
5365 5371
5366 5372 def kwargs_raw(self, indent=None):
5367 5373 return self._as_raw(self.task_kwargs, indent)
5368 5374
5369 5375 def __repr__(self):
5370 5376 return '<DB:ScheduleEntry({}:{})>'.format(
5371 5377 self.schedule_entry_id, self.schedule_name)
5372 5378
5373 5379
5374 5380 @event.listens_for(ScheduleEntry, 'before_update')
5375 5381 def update_task_uid(mapper, connection, target):
5376 5382 target.task_uid = ScheduleEntry.get_uid(target)
5377 5383
5378 5384
5379 5385 @event.listens_for(ScheduleEntry, 'before_insert')
5380 5386 def set_task_uid(mapper, connection, target):
5381 5387 target.task_uid = ScheduleEntry.get_uid(target)
5382 5388
5383 5389
5384 5390 class _BaseBranchPerms(BaseModel):
5385 5391 @classmethod
5386 5392 def compute_hash(cls, value):
5387 5393 return sha1_safe(value)
5388 5394
5389 5395 @hybrid_property
5390 5396 def branch_pattern(self):
5391 5397 return self._branch_pattern or '*'
5392 5398
5393 5399 @hybrid_property
5394 5400 def branch_hash(self):
5395 5401 return self._branch_hash
5396 5402
5397 5403 def _validate_glob(self, value):
5398 5404 re.compile('^' + glob2re(value) + '$')
5399 5405
5400 5406 @branch_pattern.setter
5401 5407 def branch_pattern(self, value):
5402 5408 self._validate_glob(value)
5403 5409 self._branch_pattern = value or '*'
5404 5410 # set the Hash when setting the branch pattern
5405 5411 self._branch_hash = self.compute_hash(self._branch_pattern)
5406 5412
5407 5413 def matches(self, branch):
5408 5414 """
5409 5415 Check if this the branch matches entry
5410 5416
5411 5417 :param branch: branch name for the commit
5412 5418 """
5413 5419
5414 5420 branch = branch or ''
5415 5421
5416 5422 branch_matches = True
5417 5423 if branch:
5418 5424 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5419 5425 branch_matches = bool(branch_regex.search(branch))
5420 5426
5421 5427 return branch_matches
5422 5428
5423 5429
5424 5430 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5425 5431 __tablename__ = 'user_to_repo_branch_permissions'
5426 5432 __table_args__ = (
5427 5433 base_table_args
5428 5434 )
5429 5435
5430 5436 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5431 5437
5432 5438 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5433 5439 repo = relationship('Repository', backref='user_branch_perms')
5434 5440
5435 5441 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5436 5442 permission = relationship('Permission')
5437 5443
5438 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 5445 user_repo_to_perm = relationship('UserRepoToPerm')
5440 5446
5441 5447 rule_order = Column('rule_order', Integer(), nullable=False)
5442 5448 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5443 5449 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5444 5450
5445 5451 def __unicode__(self):
5446 5452 return u'<UserBranchPermission(%s => %r)>' % (
5447 5453 self.user_repo_to_perm, self.branch_pattern)
5448 5454
5449 5455
5450 5456 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5451 5457 __tablename__ = 'user_group_to_repo_branch_permissions'
5452 5458 __table_args__ = (
5453 5459 base_table_args
5454 5460 )
5455 5461
5456 5462 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5457 5463
5458 5464 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5459 5465 repo = relationship('Repository', backref='user_group_branch_perms')
5460 5466
5461 5467 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5462 5468 permission = relationship('Permission')
5463 5469
5464 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 5471 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5466 5472
5467 5473 rule_order = Column('rule_order', Integer(), nullable=False)
5468 5474 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5469 5475 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5470 5476
5471 5477 def __unicode__(self):
5472 5478 return u'<UserBranchPermission(%s => %r)>' % (
5473 5479 self.user_group_repo_to_perm, self.branch_pattern)
5474 5480
5475 5481
5476 5482 class UserBookmark(Base, BaseModel):
5477 5483 __tablename__ = 'user_bookmarks'
5478 5484 __table_args__ = (
5479 5485 UniqueConstraint('user_id', 'bookmark_repo_id'),
5480 5486 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5481 5487 UniqueConstraint('user_id', 'bookmark_position'),
5482 5488 base_table_args
5483 5489 )
5484 5490
5485 5491 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5486 5492 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5487 5493 position = Column("bookmark_position", Integer(), nullable=False)
5488 5494 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5489 5495 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5490 5496 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5491 5497
5492 5498 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5493 5499 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5494 5500
5495 5501 user = relationship("User")
5496 5502
5497 5503 repository = relationship("Repository")
5498 5504 repository_group = relationship("RepoGroup")
5499 5505
5500 5506 @classmethod
5501 5507 def get_by_position_for_user(cls, position, user_id):
5502 5508 return cls.query() \
5503 5509 .filter(UserBookmark.user_id == user_id) \
5504 5510 .filter(UserBookmark.position == position).scalar()
5505 5511
5506 5512 @classmethod
5507 5513 def get_bookmarks_for_user(cls, user_id, cache=True):
5508 5514 bookmarks = cls.query() \
5509 5515 .filter(UserBookmark.user_id == user_id) \
5510 5516 .options(joinedload(UserBookmark.repository)) \
5511 5517 .options(joinedload(UserBookmark.repository_group)) \
5512 5518 .order_by(UserBookmark.position.asc())
5513 5519
5514 5520 if cache:
5515 5521 bookmarks = bookmarks.options(
5516 5522 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5517 5523 )
5518 5524
5519 5525 return bookmarks.all()
5520 5526
5521 5527 def __unicode__(self):
5522 5528 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5523 5529
5524 5530
5525 5531 class FileStore(Base, BaseModel):
5526 5532 __tablename__ = 'file_store'
5527 5533 __table_args__ = (
5528 5534 base_table_args
5529 5535 )
5530 5536
5531 5537 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5532 5538 file_uid = Column('file_uid', String(1024), nullable=False)
5533 5539 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5534 5540 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5535 5541 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5536 5542
5537 5543 # sha256 hash
5538 5544 file_hash = Column('file_hash', String(512), nullable=False)
5539 5545 file_size = Column('file_size', BigInteger(), nullable=False)
5540 5546
5541 5547 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5542 5548 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5543 5549 accessed_count = Column('accessed_count', Integer(), default=0)
5544 5550
5545 5551 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5546 5552
5547 5553 # if repo/repo_group reference is set, check for permissions
5548 5554 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5549 5555
5550 5556 # hidden defines an attachment that should be hidden from showing in artifact listing
5551 5557 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5552 5558
5553 5559 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5554 5560 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5555 5561
5556 5562 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5557 5563
5558 5564 # scope limited to user, which requester have access to
5559 5565 scope_user_id = Column(
5560 5566 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5561 5567 nullable=True, unique=None, default=None)
5562 5568 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5563 5569
5564 5570 # scope limited to user group, which requester have access to
5565 5571 scope_user_group_id = Column(
5566 5572 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5567 5573 nullable=True, unique=None, default=None)
5568 5574 user_group = relationship('UserGroup', lazy='joined')
5569 5575
5570 5576 # scope limited to repo, which requester have access to
5571 5577 scope_repo_id = Column(
5572 5578 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5573 5579 nullable=True, unique=None, default=None)
5574 5580 repo = relationship('Repository', lazy='joined')
5575 5581
5576 5582 # scope limited to repo group, which requester have access to
5577 5583 scope_repo_group_id = Column(
5578 5584 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5579 5585 nullable=True, unique=None, default=None)
5580 5586 repo_group = relationship('RepoGroup', lazy='joined')
5581 5587
5582 5588 @classmethod
5583 5589 def get_by_store_uid(cls, file_store_uid, safe=False):
5584 5590 if safe:
5585 5591 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5586 5592 else:
5587 5593 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5588 5594
5589 5595 @classmethod
5590 5596 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5591 5597 file_description='', enabled=True, hidden=False, check_acl=True,
5592 5598 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5593 5599
5594 5600 store_entry = FileStore()
5595 5601 store_entry.file_uid = file_uid
5596 5602 store_entry.file_display_name = file_display_name
5597 5603 store_entry.file_org_name = filename
5598 5604 store_entry.file_size = file_size
5599 5605 store_entry.file_hash = file_hash
5600 5606 store_entry.file_description = file_description
5601 5607
5602 5608 store_entry.check_acl = check_acl
5603 5609 store_entry.enabled = enabled
5604 5610 store_entry.hidden = hidden
5605 5611
5606 5612 store_entry.user_id = user_id
5607 5613 store_entry.scope_user_id = scope_user_id
5608 5614 store_entry.scope_repo_id = scope_repo_id
5609 5615 store_entry.scope_repo_group_id = scope_repo_group_id
5610 5616
5611 5617 return store_entry
5612 5618
5613 5619 @classmethod
5614 5620 def store_metadata(cls, file_store_id, args, commit=True):
5615 5621 file_store = FileStore.get(file_store_id)
5616 5622 if file_store is None:
5617 5623 return
5618 5624
5619 5625 for section, key, value, value_type in args:
5620 5626 has_key = FileStoreMetadata().query() \
5621 5627 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5622 5628 .filter(FileStoreMetadata.file_store_meta_section == section) \
5623 5629 .filter(FileStoreMetadata.file_store_meta_key == key) \
5624 5630 .scalar()
5625 5631 if has_key:
5626 5632 msg = 'key `{}` already defined under section `{}` for this file.'\
5627 5633 .format(key, section)
5628 5634 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5629 5635
5630 5636 # NOTE(marcink): raises ArtifactMetadataBadValueType
5631 5637 FileStoreMetadata.valid_value_type(value_type)
5632 5638
5633 5639 meta_entry = FileStoreMetadata()
5634 5640 meta_entry.file_store = file_store
5635 5641 meta_entry.file_store_meta_section = section
5636 5642 meta_entry.file_store_meta_key = key
5637 5643 meta_entry.file_store_meta_value_type = value_type
5638 5644 meta_entry.file_store_meta_value = value
5639 5645
5640 5646 Session().add(meta_entry)
5641 5647
5642 5648 try:
5643 5649 if commit:
5644 5650 Session().commit()
5645 5651 except IntegrityError:
5646 5652 Session().rollback()
5647 5653 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5648 5654
5649 5655 @classmethod
5650 5656 def bump_access_counter(cls, file_uid, commit=True):
5651 5657 FileStore().query()\
5652 5658 .filter(FileStore.file_uid == file_uid)\
5653 5659 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5654 5660 FileStore.accessed_on: datetime.datetime.now()})
5655 5661 if commit:
5656 5662 Session().commit()
5657 5663
5658 5664 def __json__(self):
5659 5665 data = {
5660 5666 'filename': self.file_display_name,
5661 5667 'filename_org': self.file_org_name,
5662 5668 'file_uid': self.file_uid,
5663 5669 'description': self.file_description,
5664 5670 'hidden': self.hidden,
5665 5671 'size': self.file_size,
5666 5672 'created_on': self.created_on,
5667 5673 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5668 5674 'downloaded_times': self.accessed_count,
5669 5675 'sha256': self.file_hash,
5670 5676 'metadata': self.file_metadata,
5671 5677 }
5672 5678
5673 5679 return data
5674 5680
5675 5681 def __repr__(self):
5676 5682 return '<FileStore({})>'.format(self.file_store_id)
5677 5683
5678 5684
5679 5685 class FileStoreMetadata(Base, BaseModel):
5680 5686 __tablename__ = 'file_store_metadata'
5681 5687 __table_args__ = (
5682 5688 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5683 5689 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5684 5690 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5685 5691 base_table_args
5686 5692 )
5687 5693 SETTINGS_TYPES = {
5688 5694 'str': safe_str,
5689 5695 'int': safe_int,
5690 5696 'unicode': safe_unicode,
5691 5697 'bool': str2bool,
5692 5698 'list': functools.partial(aslist, sep=',')
5693 5699 }
5694 5700
5695 5701 file_store_meta_id = Column(
5696 5702 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5697 5703 primary_key=True)
5698 5704 _file_store_meta_section = Column(
5699 5705 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5700 5706 nullable=True, unique=None, default=None)
5701 5707 _file_store_meta_section_hash = Column(
5702 5708 "file_store_meta_section_hash", String(255),
5703 5709 nullable=True, unique=None, default=None)
5704 5710 _file_store_meta_key = Column(
5705 5711 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5706 5712 nullable=True, unique=None, default=None)
5707 5713 _file_store_meta_key_hash = Column(
5708 5714 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5709 5715 _file_store_meta_value = Column(
5710 5716 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5711 5717 nullable=True, unique=None, default=None)
5712 5718 _file_store_meta_value_type = Column(
5713 5719 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5714 5720 default='unicode')
5715 5721
5716 5722 file_store_id = Column(
5717 5723 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5718 5724 nullable=True, unique=None, default=None)
5719 5725
5720 5726 file_store = relationship('FileStore', lazy='joined')
5721 5727
5722 5728 @classmethod
5723 5729 def valid_value_type(cls, value):
5724 5730 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5725 5731 raise ArtifactMetadataBadValueType(
5726 5732 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5727 5733
5728 5734 @hybrid_property
5729 5735 def file_store_meta_section(self):
5730 5736 return self._file_store_meta_section
5731 5737
5732 5738 @file_store_meta_section.setter
5733 5739 def file_store_meta_section(self, value):
5734 5740 self._file_store_meta_section = value
5735 5741 self._file_store_meta_section_hash = _hash_key(value)
5736 5742
5737 5743 @hybrid_property
5738 5744 def file_store_meta_key(self):
5739 5745 return self._file_store_meta_key
5740 5746
5741 5747 @file_store_meta_key.setter
5742 5748 def file_store_meta_key(self, value):
5743 5749 self._file_store_meta_key = value
5744 5750 self._file_store_meta_key_hash = _hash_key(value)
5745 5751
5746 5752 @hybrid_property
5747 5753 def file_store_meta_value(self):
5748 5754 val = self._file_store_meta_value
5749 5755
5750 5756 if self._file_store_meta_value_type:
5751 5757 # e.g unicode.encrypted == unicode
5752 5758 _type = self._file_store_meta_value_type.split('.')[0]
5753 5759 # decode the encrypted value if it's encrypted field type
5754 5760 if '.encrypted' in self._file_store_meta_value_type:
5755 5761 cipher = EncryptedTextValue()
5756 5762 val = safe_unicode(cipher.process_result_value(val, None))
5757 5763 # do final type conversion
5758 5764 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5759 5765 val = converter(val)
5760 5766
5761 5767 return val
5762 5768
5763 5769 @file_store_meta_value.setter
5764 5770 def file_store_meta_value(self, val):
5765 5771 val = safe_unicode(val)
5766 5772 # encode the encrypted value
5767 5773 if '.encrypted' in self.file_store_meta_value_type:
5768 5774 cipher = EncryptedTextValue()
5769 5775 val = safe_unicode(cipher.process_bind_param(val, None))
5770 5776 self._file_store_meta_value = val
5771 5777
5772 5778 @hybrid_property
5773 5779 def file_store_meta_value_type(self):
5774 5780 return self._file_store_meta_value_type
5775 5781
5776 5782 @file_store_meta_value_type.setter
5777 5783 def file_store_meta_value_type(self, val):
5778 5784 # e.g unicode.encrypted
5779 5785 self.valid_value_type(val)
5780 5786 self._file_store_meta_value_type = val
5781 5787
5782 5788 def __json__(self):
5783 5789 data = {
5784 5790 'artifact': self.file_store.file_uid,
5785 5791 'section': self.file_store_meta_section,
5786 5792 'key': self.file_store_meta_key,
5787 5793 'value': self.file_store_meta_value,
5788 5794 }
5789 5795
5790 5796 return data
5791 5797
5792 5798 def __repr__(self):
5793 5799 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5794 5800 self.file_store_meta_key, self.file_store_meta_value)
5795 5801
5796 5802
5797 5803 class DbMigrateVersion(Base, BaseModel):
5798 5804 __tablename__ = 'db_migrate_version'
5799 5805 __table_args__ = (
5800 5806 base_table_args,
5801 5807 )
5802 5808
5803 5809 repository_id = Column('repository_id', String(250), primary_key=True)
5804 5810 repository_path = Column('repository_path', Text)
5805 5811 version = Column('version', Integer)
5806 5812
5807 5813 @classmethod
5808 5814 def set_version(cls, version):
5809 5815 """
5810 5816 Helper for forcing a different version, usually for debugging purposes via ishell.
5811 5817 """
5812 5818 ver = DbMigrateVersion.query().first()
5813 5819 ver.version = version
5814 5820 Session().commit()
5815 5821
5816 5822
5817 5823 class DbSession(Base, BaseModel):
5818 5824 __tablename__ = 'db_session'
5819 5825 __table_args__ = (
5820 5826 base_table_args,
5821 5827 )
5822 5828
5823 5829 def __repr__(self):
5824 5830 return '<DB:DbSession({})>'.format(self.id)
5825 5831
5826 5832 id = Column('id', Integer())
5827 5833 namespace = Column('namespace', String(255), primary_key=True)
5828 5834 accessed = Column('accessed', DateTime, nullable=False)
5829 5835 created = Column('created', DateTime, nullable=False)
5830 5836 data = Column('data', PickleType, nullable=False)
@@ -1,1502 +1,1501 b''
1 1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 var firefoxAnchorFix = function() {
20 20 // hack to make anchor links behave properly on firefox, in our inline
21 21 // comments generation when comments are injected firefox is misbehaving
22 22 // when jumping to anchor links
23 23 if (location.href.indexOf('#') > -1) {
24 24 location.href += '';
25 25 }
26 26 };
27 27
28 28 var linkifyComments = function(comments) {
29 29 var firstCommentId = null;
30 30 if (comments) {
31 31 firstCommentId = $(comments[0]).data('comment-id');
32 32 }
33 33
34 34 if (firstCommentId){
35 35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 36 }
37 37 };
38 38
39 39 var bindToggleButtons = function() {
40 40 $('.comment-toggle').on('click', function() {
41 41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 42 });
43 43 };
44 44
45 45
46 46
47 47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
48 48 failHandler = failHandler || function() {};
49 49 postData = toQueryString(postData);
50 50 var request = $.ajax({
51 51 url: url,
52 52 type: 'POST',
53 53 data: postData,
54 54 headers: {'X-PARTIAL-XHR': true}
55 55 })
56 56 .done(function (data) {
57 57 successHandler(data);
58 58 })
59 59 .fail(function (data, textStatus, errorThrown) {
60 60 failHandler(data, textStatus, errorThrown)
61 61 });
62 62 return request;
63 63 };
64 64
65 65
66 66
67 67
68 68 /* Comment form for main and inline comments */
69 69 (function(mod) {
70 70
71 71 if (typeof exports == "object" && typeof module == "object") {
72 72 // CommonJS
73 73 module.exports = mod();
74 74 }
75 75 else {
76 76 // Plain browser env
77 77 (this || window).CommentForm = mod();
78 78 }
79 79
80 80 })(function() {
81 81 "use strict";
82 82
83 83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
84 84
85 85 if (!(this instanceof CommentForm)) {
86 86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
87 87 }
88 88
89 89 // bind the element instance to our Form
90 90 $(formElement).get(0).CommentForm = this;
91 91
92 92 this.withLineNo = function(selector) {
93 93 var lineNo = this.lineNo;
94 94 if (lineNo === undefined) {
95 95 return selector
96 96 } else {
97 97 return selector + '_' + lineNo;
98 98 }
99 99 };
100 100
101 101 this.commitId = commitId;
102 102 this.pullRequestId = pullRequestId;
103 103 this.lineNo = lineNo;
104 104 this.initAutocompleteActions = initAutocompleteActions;
105 105
106 106 this.previewButton = this.withLineNo('#preview-btn');
107 107 this.previewContainer = this.withLineNo('#preview-container');
108 108
109 109 this.previewBoxSelector = this.withLineNo('#preview-box');
110 110
111 111 this.editButton = this.withLineNo('#edit-btn');
112 112 this.editContainer = this.withLineNo('#edit-container');
113 113 this.cancelButton = this.withLineNo('#cancel-btn');
114 114 this.commentType = this.withLineNo('#comment_type');
115 115
116 116 this.resolvesId = null;
117 117 this.resolvesActionId = null;
118 118
119 119 this.closesPr = '#close_pull_request';
120 120
121 121 this.cmBox = this.withLineNo('#text');
122 122 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
123 123
124 124 this.statusChange = this.withLineNo('#change_status');
125 125
126 126 this.submitForm = formElement;
127 127
128 128 this.submitButton = $(this.submitForm).find('.submit-comment-action');
129 129 this.submitButtonText = this.submitButton.val();
130 130
131 131 this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
132 132 this.submitDraftButtonText = this.submitDraftButton.val();
133 133
134 134 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
135 135 {'repo_name': templateContext.repo_name,
136 136 'commit_id': templateContext.commit_data.commit_id});
137 137
138 138 if (edit){
139 139 this.submitDraftButton.hide();
140 140 this.submitButtonText = _gettext('Update Comment');
141 141 $(this.commentType).prop('disabled', true);
142 142 $(this.commentType).addClass('disabled');
143 143 var editInfo =
144 144 '';
145 145 $(editInfo).insertBefore($(this.editButton).parent());
146 146 }
147 147
148 148 if (resolvesCommentId){
149 149 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
150 150 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
151 151 $(this.commentType).prop('disabled', true);
152 152 $(this.commentType).addClass('disabled');
153 153
154 154 // disable select
155 155 setTimeout(function() {
156 156 $(self.statusChange).select2('readonly', true);
157 157 }, 10);
158 158
159 159 var resolvedInfo = (
160 160 '<li class="resolve-action">' +
161 161 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
162 162 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
163 163 '</li>'
164 164 ).format(resolvesCommentId, _gettext('resolve comment'));
165 165 $(resolvedInfo).insertAfter($(this.commentType).parent());
166 166 }
167 167
168 168 // based on commitId, or pullRequestId decide where do we submit
169 169 // out data
170 170 if (this.commitId){
171 171 var pyurl = 'repo_commit_comment_create';
172 172 if(edit){
173 173 pyurl = 'repo_commit_comment_edit';
174 174 }
175 175 this.submitUrl = pyroutes.url(pyurl,
176 176 {'repo_name': templateContext.repo_name,
177 177 'commit_id': this.commitId,
178 178 'comment_id': comment_id});
179 179 this.selfUrl = pyroutes.url('repo_commit',
180 180 {'repo_name': templateContext.repo_name,
181 181 'commit_id': this.commitId});
182 182
183 183 } else if (this.pullRequestId) {
184 184 var pyurl = 'pullrequest_comment_create';
185 185 if(edit){
186 186 pyurl = 'pullrequest_comment_edit';
187 187 }
188 188 this.submitUrl = pyroutes.url(pyurl,
189 189 {'repo_name': templateContext.repo_name,
190 190 'pull_request_id': this.pullRequestId,
191 191 'comment_id': comment_id});
192 192 this.selfUrl = pyroutes.url('pullrequest_show',
193 193 {'repo_name': templateContext.repo_name,
194 194 'pull_request_id': this.pullRequestId});
195 195
196 196 } else {
197 197 throw new Error(
198 198 'CommentForm requires pullRequestId, or commitId to be specified.')
199 199 }
200 200
201 201 // FUNCTIONS and helpers
202 202 var self = this;
203 203
204 204 this.isInline = function(){
205 205 return this.lineNo && this.lineNo != 'general';
206 206 };
207 207
208 208 this.getCmInstance = function(){
209 209 return this.cm
210 210 };
211 211
212 212 this.setPlaceholder = function(placeholder) {
213 213 var cm = this.getCmInstance();
214 214 if (cm){
215 215 cm.setOption('placeholder', placeholder);
216 216 }
217 217 };
218 218
219 219 this.getCommentStatus = function() {
220 220 return $(this.submitForm).find(this.statusChange).val();
221 221 };
222 222
223 223 this.getCommentType = function() {
224 224 return $(this.submitForm).find(this.commentType).val();
225 225 };
226 226
227 227 this.getDraftState = function () {
228 228 var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
229 229 var data = $(submitterElem).data('isDraft');
230 230 return data
231 231 }
232 232
233 233 this.getResolvesId = function() {
234 234 return $(this.submitForm).find(this.resolvesId).val() || null;
235 235 };
236 236
237 237 this.getClosePr = function() {
238 238 return $(this.submitForm).find(this.closesPr).val() || null;
239 239 };
240 240
241 241 this.markCommentResolved = function(resolvedCommentId){
242 242 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
243 243 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
244 244 };
245 245
246 246 this.isAllowedToSubmit = function() {
247 247 var commentDisabled = $(this.submitButton).prop('disabled');
248 248 var draftDisabled = $(this.submitDraftButton).prop('disabled');
249 249 return !commentDisabled && !draftDisabled;
250 250 };
251 251
252 252 this.initStatusChangeSelector = function(){
253 253 var formatChangeStatus = function(state, escapeMarkup) {
254 254 var originalOption = state.element;
255 255 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
256 256 return tmpl
257 257 };
258 258 var formatResult = function(result, container, query, escapeMarkup) {
259 259 return formatChangeStatus(result, escapeMarkup);
260 260 };
261 261
262 262 var formatSelection = function(data, container, escapeMarkup) {
263 263 return formatChangeStatus(data, escapeMarkup);
264 264 };
265 265
266 266 $(this.submitForm).find(this.statusChange).select2({
267 267 placeholder: _gettext('Status Review'),
268 268 formatResult: formatResult,
269 269 formatSelection: formatSelection,
270 270 containerCssClass: "drop-menu status_box_menu",
271 271 dropdownCssClass: "drop-menu-dropdown",
272 272 dropdownAutoWidth: true,
273 273 minimumResultsForSearch: -1
274 274 });
275 275
276 276 $(this.submitForm).find(this.statusChange).on('change', function() {
277 277 var status = self.getCommentStatus();
278 278
279 279 if (status && !self.isInline()) {
280 280 $(self.submitButton).prop('disabled', false);
281 281 $(self.submitDraftButton).prop('disabled', false);
282 282 }
283 283
284 284 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
285 285 self.setPlaceholder(placeholderText)
286 286 })
287 287 };
288 288
289 289 // reset the comment form into it's original state
290 290 this.resetCommentFormState = function(content) {
291 291 content = content || '';
292 292
293 293 $(this.editContainer).show();
294 294 $(this.editButton).parent().addClass('active');
295 295
296 296 $(this.previewContainer).hide();
297 297 $(this.previewButton).parent().removeClass('active');
298 298
299 299 this.setActionButtonsDisabled(true);
300 300 self.cm.setValue(content);
301 301 self.cm.setOption("readOnly", false);
302 302
303 303 if (this.resolvesId) {
304 304 // destroy the resolve action
305 305 $(this.resolvesId).parent().remove();
306 306 }
307 307 // reset closingPR flag
308 308 $('.close-pr-input').remove();
309 309
310 310 $(this.statusChange).select2('readonly', false);
311 311 };
312 312
313 313 this.globalSubmitSuccessCallback = function(comment){
314 314 // default behaviour is to call GLOBAL hook, if it's registered.
315 315 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
316 316 commentFormGlobalSubmitSuccessCallback(comment);
317 317 }
318 318 };
319 319
320 320 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
321 321 return _submitAjaxPOST(url, postData, successHandler, failHandler);
322 322 };
323 323
324 324 // overwrite a submitHandler, we need to do it for inline comments
325 325 this.setHandleFormSubmit = function(callback) {
326 326 this.handleFormSubmit = callback;
327 327 };
328 328
329 329 // overwrite a submitSuccessHandler
330 330 this.setGlobalSubmitSuccessCallback = function(callback) {
331 331 this.globalSubmitSuccessCallback = callback;
332 332 };
333 333
334 334 // default handler for for submit for main comments
335 335 this.handleFormSubmit = function() {
336 336 var text = self.cm.getValue();
337 337 var status = self.getCommentStatus();
338 338 var commentType = self.getCommentType();
339 339 var isDraft = self.getDraftState();
340 340 var resolvesCommentId = self.getResolvesId();
341 341 var closePullRequest = self.getClosePr();
342 342
343 343 if (text === "" && !status) {
344 344 return;
345 345 }
346 346
347 347 var excludeCancelBtn = false;
348 348 var submitEvent = true;
349 349 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
350 350 self.cm.setOption("readOnly", true);
351 351
352 352 var postData = {
353 353 'text': text,
354 354 'changeset_status': status,
355 355 'comment_type': commentType,
356 356 'csrf_token': CSRF_TOKEN
357 357 };
358 358
359 359 if (resolvesCommentId) {
360 360 postData['resolves_comment_id'] = resolvesCommentId;
361 361 }
362 362
363 363 if (closePullRequest) {
364 364 postData['close_pull_request'] = true;
365 365 }
366 366
367 367 // submitSuccess for general comments
368 368 var submitSuccessCallback = function(json_data) {
369 369 // reload page if we change status for single commit.
370 370 if (status && self.commitId) {
371 371 location.reload(true);
372 372 } else {
373 373 // inject newly created comments, json_data is {<comment_id>: {}}
374 374 Rhodecode.comments.attachGeneralComment(json_data)
375 375
376 376 self.resetCommentFormState();
377 377 timeagoActivate();
378 378 tooltipActivate();
379 379
380 380 // mark visually which comment was resolved
381 381 if (resolvesCommentId) {
382 382 self.markCommentResolved(resolvesCommentId);
383 383 }
384 384 }
385 385
386 386 // run global callback on submit
387 387 self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
388 388
389 389 };
390 390 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
391 391 var prefix = "Error while submitting comment.\n"
392 392 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
393 393 ajaxErrorSwal(message);
394 394 self.resetCommentFormState(text);
395 395 };
396 396 self.submitAjaxPOST(
397 397 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
398 398 };
399 399
400 400 this.previewSuccessCallback = function(o) {
401 401 $(self.previewBoxSelector).html(o);
402 402 $(self.previewBoxSelector).removeClass('unloaded');
403 403
404 404 // swap buttons, making preview active
405 405 $(self.previewButton).parent().addClass('active');
406 406 $(self.editButton).parent().removeClass('active');
407 407
408 408 // unlock buttons
409 409 self.setActionButtonsDisabled(false);
410 410 };
411 411
412 412 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
413 413 excludeCancelBtn = excludeCancelBtn || false;
414 414 submitEvent = submitEvent || false;
415 415
416 416 $(this.editButton).prop('disabled', state);
417 417 $(this.previewButton).prop('disabled', state);
418 418
419 419 if (!excludeCancelBtn) {
420 420 $(this.cancelButton).prop('disabled', state);
421 421 }
422 422
423 423 var submitState = state;
424 424 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
425 425 // if the value of commit review status is set, we allow
426 426 // submit button, but only on Main form, isInline means inline
427 427 submitState = false
428 428 }
429 429
430 430 $(this.submitButton).prop('disabled', submitState);
431 431 $(this.submitDraftButton).prop('disabled', submitState);
432 432
433 433 if (submitEvent) {
434 434 var isDraft = self.getDraftState();
435 435
436 436 if (isDraft) {
437 437 $(this.submitDraftButton).val(_gettext('Saving Draft...'));
438 438 } else {
439 439 $(this.submitButton).val(_gettext('Submitting...'));
440 440 }
441 441
442 442 } else {
443 443 $(this.submitButton).val(this.submitButtonText);
444 444 $(this.submitDraftButton).val(this.submitDraftButtonText);
445 445 }
446 446
447 447 };
448 448
449 449 // lock preview/edit/submit buttons on load, but exclude cancel button
450 450 var excludeCancelBtn = true;
451 451 this.setActionButtonsDisabled(true, excludeCancelBtn);
452 452
453 453 // anonymous users don't have access to initialized CM instance
454 454 if (this.cm !== undefined){
455 455 this.cm.on('change', function(cMirror) {
456 456 if (cMirror.getValue() === "") {
457 457 self.setActionButtonsDisabled(true, excludeCancelBtn)
458 458 } else {
459 459 self.setActionButtonsDisabled(false, excludeCancelBtn)
460 460 }
461 461 });
462 462 }
463 463
464 464 $(this.editButton).on('click', function(e) {
465 465 e.preventDefault();
466 466
467 467 $(self.previewButton).parent().removeClass('active');
468 468 $(self.previewContainer).hide();
469 469
470 470 $(self.editButton).parent().addClass('active');
471 471 $(self.editContainer).show();
472 472
473 473 });
474 474
475 475 $(this.previewButton).on('click', function(e) {
476 476 e.preventDefault();
477 477 var text = self.cm.getValue();
478 478
479 479 if (text === "") {
480 480 return;
481 481 }
482 482
483 483 var postData = {
484 484 'text': text,
485 485 'renderer': templateContext.visual.default_renderer,
486 486 'csrf_token': CSRF_TOKEN
487 487 };
488 488
489 489 // lock ALL buttons on preview
490 490 self.setActionButtonsDisabled(true);
491 491
492 492 $(self.previewBoxSelector).addClass('unloaded');
493 493 $(self.previewBoxSelector).html(_gettext('Loading ...'));
494 494
495 495 $(self.editContainer).hide();
496 496 $(self.previewContainer).show();
497 497
498 498 // by default we reset state of comment preserving the text
499 499 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
500 500 var prefix = "Error while preview of comment.\n"
501 501 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
502 502 ajaxErrorSwal(message);
503 503
504 504 self.resetCommentFormState(text)
505 505 };
506 506 self.submitAjaxPOST(
507 507 self.previewUrl, postData, self.previewSuccessCallback,
508 508 previewFailCallback);
509 509
510 510 $(self.previewButton).parent().addClass('active');
511 511 $(self.editButton).parent().removeClass('active');
512 512 });
513 513
514 514 $(this.submitForm).submit(function(e) {
515 515 e.preventDefault();
516 516 var allowedToSubmit = self.isAllowedToSubmit();
517 517 if (!allowedToSubmit){
518 518 return false;
519 519 }
520 520
521 521 self.handleFormSubmit();
522 522 });
523 523
524 524 }
525 525
526 526 return CommentForm;
527 527 });
528 528
529 529 /* selector for comment versions */
530 530 var initVersionSelector = function(selector, initialData) {
531 531
532 532 var formatResult = function(result, container, query, escapeMarkup) {
533 533
534 534 return renderTemplate('commentVersion', {
535 535 show_disabled: true,
536 536 version: result.comment_version,
537 537 user_name: result.comment_author_username,
538 538 gravatar_url: result.comment_author_gravatar,
539 539 size: 16,
540 540 timeago_component: result.comment_created_on,
541 541 })
542 542 };
543 543
544 544 $(selector).select2({
545 545 placeholder: "Edited",
546 546 containerCssClass: "drop-menu-comment-history",
547 547 dropdownCssClass: "drop-menu-dropdown",
548 548 dropdownAutoWidth: true,
549 549 minimumResultsForSearch: -1,
550 550 data: initialData,
551 551 formatResult: formatResult,
552 552 });
553 553
554 554 $(selector).on('select2-selecting', function (e) {
555 555 // hide the mast as we later do preventDefault()
556 556 $("#select2-drop-mask").click();
557 557 e.preventDefault();
558 558 e.choice.action();
559 559 });
560 560
561 561 $(selector).on("select2-open", function() {
562 562 timeagoActivate();
563 563 });
564 564 };
565 565
566 566 /* comments controller */
567 567 var CommentsController = function() {
568 568 var mainComment = '#text';
569 569 var self = this;
570 570
571 571 this.showVersion = function (comment_id, comment_history_id) {
572 572
573 573 var historyViewUrl = pyroutes.url(
574 574 'repo_commit_comment_history_view',
575 575 {
576 576 'repo_name': templateContext.repo_name,
577 577 'commit_id': comment_id,
578 578 'comment_history_id': comment_history_id,
579 579 }
580 580 );
581 581 successRenderCommit = function (data) {
582 582 SwalNoAnimation.fire({
583 583 html: data,
584 584 title: '',
585 585 });
586 586 };
587 587 failRenderCommit = function () {
588 588 SwalNoAnimation.fire({
589 589 html: 'Error while loading comment history',
590 590 title: '',
591 591 });
592 592 };
593 593 _submitAjaxPOST(
594 594 historyViewUrl, {'csrf_token': CSRF_TOKEN},
595 595 successRenderCommit,
596 596 failRenderCommit
597 597 );
598 598 };
599 599
600 600 this.getLineNumber = function(node) {
601 601 var $node = $(node);
602 602 var lineNo = $node.closest('td').attr('data-line-no');
603 603 if (lineNo === undefined && $node.data('commentInline')){
604 604 lineNo = $node.data('commentLineNo')
605 605 }
606 606
607 607 return lineNo
608 608 };
609 609
610 610 this.scrollToComment = function(node, offset, outdated) {
611 611 if (offset === undefined) {
612 612 offset = 0;
613 613 }
614 614 var outdated = outdated || false;
615 615 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
616 616
617 617 if (!node) {
618 618 node = $('.comment-selected');
619 619 if (!node.length) {
620 620 node = $('comment-current')
621 621 }
622 622 }
623 623
624 624 $wrapper = $(node).closest('div.comment');
625 625
626 626 // show hidden comment when referenced.
627 627 if (!$wrapper.is(':visible')){
628 628 $wrapper.show();
629 629 }
630 630
631 631 $comment = $(node).closest(klass);
632 632 $comments = $(klass);
633 633
634 634 $('.comment-selected').removeClass('comment-selected');
635 635
636 636 var nextIdx = $(klass).index($comment) + offset;
637 637 if (nextIdx >= $comments.length) {
638 638 nextIdx = 0;
639 639 }
640 640 var $next = $(klass).eq(nextIdx);
641 641
642 642 var $cb = $next.closest('.cb');
643 643 $cb.removeClass('cb-collapsed');
644 644
645 645 var $filediffCollapseState = $cb.closest('.filediff').prev();
646 646 $filediffCollapseState.prop('checked', false);
647 647 $next.addClass('comment-selected');
648 648 scrollToElement($next);
649 649 return false;
650 650 };
651 651
652 652 this.nextComment = function(node) {
653 653 return self.scrollToComment(node, 1);
654 654 };
655 655
656 656 this.prevComment = function(node) {
657 657 return self.scrollToComment(node, -1);
658 658 };
659 659
660 660 this.nextOutdatedComment = function(node) {
661 661 return self.scrollToComment(node, 1, true);
662 662 };
663 663
664 664 this.prevOutdatedComment = function(node) {
665 665 return self.scrollToComment(node, -1, true);
666 666 };
667 667
668 668 this.cancelComment = function (node) {
669 669 var $node = $(node);
670 670 var edit = $(this).attr('edit');
671 671 var $inlineComments = $node.closest('div.inline-comments');
672 672
673 673 if (edit) {
674 674 var $general_comments = null;
675 675 if (!$inlineComments.length) {
676 676 $general_comments = $('#comments');
677 677 var $comment = $general_comments.parent().find('div.comment:hidden');
678 678 // show hidden general comment form
679 679 $('#cb-comment-general-form-placeholder').show();
680 680 } else {
681 681 var $comment = $inlineComments.find('div.comment:hidden');
682 682 }
683 683 $comment.show();
684 684 }
685 685 var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper')
686 686 $replyWrapper.removeClass('comment-form-active');
687 687
688 688 var lastComment = $inlineComments.find('.comment-inline').last();
689 689 if ($(lastComment).hasClass('comment-outdated')) {
690 690 $replyWrapper.hide();
691 691 }
692 692
693 693 $node.closest('.comment-inline-form').remove();
694 694 return false;
695 695 };
696 696
697 697 this._deleteComment = function(node) {
698 698 var $node = $(node);
699 699 var $td = $node.closest('td');
700 700 var $comment = $node.closest('.comment');
701 701 var comment_id = $($comment).data('commentId');
702 702 var isDraft = $($comment).data('commentDraft');
703 703
704 704 var pullRequestId = templateContext.pull_request_data.pull_request_id;
705 705 var commitId = templateContext.commit_data.commit_id;
706 706
707 707 if (pullRequestId) {
708 708 var url = pyroutes.url('pullrequest_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
709 709 } else if (commitId) {
710 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 713 var postData = {
714 714 'csrf_token': CSRF_TOKEN
715 715 };
716 716
717 717 $comment.addClass('comment-deleting');
718 718 $comment.hide('fast');
719 719
720 720 var success = function(response) {
721 721 $comment.remove();
722 722
723 723 if (window.updateSticky !== undefined) {
724 724 // potentially our comments change the active window size, so we
725 725 // notify sticky elements
726 726 updateSticky()
727 727 }
728 728
729 729 if (window.refreshAllComments !== undefined && !isDraft) {
730 730 // if we have this handler, run it, and refresh all comments boxes
731 731 refreshAllComments()
732 732 }
733 733 else if (window.refreshDraftComments !== undefined && isDraft) {
734 734 // if we have this handler, run it, and refresh all comments boxes
735 735 refreshDraftComments();
736 736 }
737 737 return false;
738 738 };
739 739
740 740 var failure = function(jqXHR, textStatus, errorThrown) {
741 741 var prefix = "Error while deleting this comment.\n"
742 742 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
743 743 ajaxErrorSwal(message);
744 744
745 745 $comment.show('fast');
746 746 $comment.removeClass('comment-deleting');
747 747 return false;
748 748 };
749 749 ajaxPOST(url, postData, success, failure);
750 750
751 751 }
752 752
753 753 this.deleteComment = function(node) {
754 754 var $comment = $(node).closest('.comment');
755 755 var comment_id = $comment.attr('data-comment-id');
756 756
757 757 SwalNoAnimation.fire({
758 758 title: 'Delete this comment?',
759 759 icon: 'warning',
760 760 showCancelButton: true,
761 761 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
762 762
763 763 }).then(function(result) {
764 764 if (result.value) {
765 765 self._deleteComment(node);
766 766 }
767 767 })
768 768 };
769 769
770 770 this._finalizeDrafts = function(commentIds) {
771 771
772 772 var pullRequestId = templateContext.pull_request_data.pull_request_id;
773 773 var commitId = templateContext.commit_data.commit_id;
774 774
775 775 if (pullRequestId) {
776 776 var url = pyroutes.url('pullrequest_draft_comments_submit', {"repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
777 777 } else if (commitId) {
778 778 var url = pyroutes.url('commit_draft_comments_submit', {"repo_name": templateContext.repo_name, "commit_id": commitId})
779 779 }
780 780
781 781 // remove the drafts so we can lock them before submit.
782 782 $.each(commentIds, function(idx, val){
783 783 $('#comment-{0}'.format(val)).remove();
784 784 })
785 785
786 786 var postData = {'comments': commentIds, 'csrf_token': CSRF_TOKEN};
787 787
788 788 var submitSuccessCallback = function(json_data) {
789 789 self.attachInlineComment(json_data);
790 790
791 791 if (window.refreshDraftComments !== undefined) {
792 792 // if we have this handler, run it, and refresh all comments boxes
793 793 refreshDraftComments()
794 794 }
795 795
796 796 return false;
797 797 };
798 798
799 799 ajaxPOST(url, postData, submitSuccessCallback)
800 800
801 801 }
802 802
803 803 this.finalizeDrafts = function(commentIds, callback) {
804 804
805 805 SwalNoAnimation.fire({
806 806 title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
807 807 icon: 'warning',
808 808 showCancelButton: true,
809 809 confirmButtonText: _gettext('Yes'),
810 810
811 811 }).then(function(result) {
812 812 if (result.value) {
813 813 if (callback !== undefined) {
814 814 callback(result)
815 815 }
816 816 self._finalizeDrafts(commentIds);
817 817 }
818 818 })
819 819 };
820 820
821 821 this.toggleWideMode = function (node) {
822 822
823 823 if ($('#content').hasClass('wrapper')) {
824 824 $('#content').removeClass("wrapper");
825 825 $('#content').addClass("wide-mode-wrapper");
826 826 $(node).addClass('btn-success');
827 827 return true
828 828 } else {
829 829 $('#content').removeClass("wide-mode-wrapper");
830 830 $('#content').addClass("wrapper");
831 831 $(node).removeClass('btn-success');
832 832 return false
833 833 }
834 834
835 835 };
836 836
837 837 /**
838 838 * Turn off/on all comments in file diff
839 839 */
840 840 this.toggleDiffComments = function(node) {
841 841 // Find closes filediff container
842 842 var $filediff = $(node).closest('.filediff');
843 843 if ($(node).hasClass('toggle-on')) {
844 844 var show = false;
845 845 } else if ($(node).hasClass('toggle-off')) {
846 846 var show = true;
847 847 }
848 848
849 849 // Toggle each individual comment block, so we can un-toggle single ones
850 850 $.each($filediff.find('.toggle-comment-action'), function(idx, val) {
851 851 self.toggleLineComments($(val), show)
852 852 })
853 853
854 854 // since we change the height of the diff container that has anchor points for upper
855 855 // sticky header, we need to tell it to re-calculate those
856 856 if (window.updateSticky !== undefined) {
857 857 // potentially our comments change the active window size, so we
858 858 // notify sticky elements
859 859 updateSticky()
860 860 }
861 861
862 862 return false;
863 863 }
864 864
865 865 this.toggleLineComments = function(node, show) {
866 866
867 867 var trElem = $(node).closest('tr')
868 868
869 869 if (show === true) {
870 870 // mark outdated comments as visible before the toggle;
871 871 $(trElem).find('.comment-outdated').show();
872 872 $(trElem).removeClass('hide-line-comments');
873 873 } else if (show === false) {
874 874 $(trElem).find('.comment-outdated').hide();
875 875 $(trElem).addClass('hide-line-comments');
876 876 } else {
877 877 // mark outdated comments as visible before the toggle;
878 878 $(trElem).find('.comment-outdated').show();
879 879 $(trElem).toggleClass('hide-line-comments');
880 880 }
881 881
882 882 // since we change the height of the diff container that has anchor points for upper
883 883 // sticky header, we need to tell it to re-calculate those
884 884 if (window.updateSticky !== undefined) {
885 885 // potentially our comments change the active window size, so we
886 886 // notify sticky elements
887 887 updateSticky()
888 888 }
889 889
890 890 };
891 891
892 892 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
893 893 var pullRequestId = templateContext.pull_request_data.pull_request_id;
894 894 var commitId = templateContext.commit_data.commit_id;
895 895
896 896 var commentForm = new CommentForm(
897 897 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
898 898 var cm = commentForm.getCmInstance();
899 899
900 900 if (resolvesCommentId){
901 901 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
902 902 }
903 903
904 904 setTimeout(function() {
905 905 // callbacks
906 906 if (cm !== undefined) {
907 907 commentForm.setPlaceholder(placeholderText);
908 908 if (commentForm.isInline()) {
909 909 cm.focus();
910 910 cm.refresh();
911 911 }
912 912 }
913 913 }, 10);
914 914
915 915 // trigger scrolldown to the resolve comment, since it might be away
916 916 // from the clicked
917 917 if (resolvesCommentId){
918 918 var actionNode = $(commentForm.resolvesActionId).offset();
919 919
920 920 setTimeout(function() {
921 921 if (actionNode) {
922 922 $('body, html').animate({scrollTop: actionNode.top}, 10);
923 923 }
924 924 }, 100);
925 925 }
926 926
927 927 // add dropzone support
928 928 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
929 929 var renderer = templateContext.visual.default_renderer;
930 930 if (renderer == 'rst') {
931 931 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
932 932 if (isRendered){
933 933 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
934 934 }
935 935 } else if (renderer == 'markdown') {
936 936 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
937 937 if (isRendered){
938 938 attachmentUrl = '!' + attachmentUrl;
939 939 }
940 940 } else {
941 941 var attachmentUrl = '{}'.format(attachmentStoreUrl);
942 942 }
943 943 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
944 944
945 945 return false;
946 946 };
947 947
948 948 //see: https://www.dropzonejs.com/#configuration
949 949 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
950 950 {'repo_name': templateContext.repo_name,
951 951 'commit_id': templateContext.commit_data.commit_id})
952 952
953 953 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
954 954 if (previewTmpl !== undefined){
955 955 var selectLink = $(formElement).find('.pick-attachment').get(0);
956 956 $(formElement).find('.comment-attachment-uploader').dropzone({
957 957 url: storeUrl,
958 958 headers: {"X-CSRF-Token": CSRF_TOKEN},
959 959 paramName: function () {
960 960 return "attachment"
961 961 }, // The name that will be used to transfer the file
962 962 clickable: selectLink,
963 963 parallelUploads: 1,
964 964 maxFiles: 10,
965 965 maxFilesize: templateContext.attachment_store.max_file_size_mb,
966 966 uploadMultiple: false,
967 967 autoProcessQueue: true, // if false queue will not be processed automatically.
968 968 createImageThumbnails: false,
969 969 previewTemplate: previewTmpl.innerHTML,
970 970
971 971 accept: function (file, done) {
972 972 done();
973 973 },
974 974 init: function () {
975 975
976 976 this.on("sending", function (file, xhr, formData) {
977 977 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
978 978 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
979 979 });
980 980
981 981 this.on("success", function (file, response) {
982 982 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
983 983 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
984 984
985 985 var isRendered = false;
986 986 var ext = file.name.split('.').pop();
987 987 var imageExts = templateContext.attachment_store.image_ext;
988 988 if (imageExts.indexOf(ext) !== -1){
989 989 isRendered = true;
990 990 }
991 991
992 992 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
993 993 });
994 994
995 995 this.on("error", function (file, errorMessage, xhr) {
996 996 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
997 997
998 998 var error = null;
999 999
1000 1000 if (xhr !== undefined){
1001 1001 var httpStatus = xhr.status + " " + xhr.statusText;
1002 1002 if (xhr !== undefined && xhr.status >= 500) {
1003 1003 error = httpStatus;
1004 1004 }
1005 1005 }
1006 1006
1007 1007 if (error === null) {
1008 1008 error = errorMessage.error || errorMessage || httpStatus;
1009 1009 }
1010 1010 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
1011 1011
1012 1012 });
1013 1013 }
1014 1014 });
1015 1015 }
1016 1016 return commentForm;
1017 1017 };
1018 1018
1019 1019 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
1020 1020
1021 1021 var tmpl = $('#cb-comment-general-form-template').html();
1022 1022 tmpl = tmpl.format(null, 'general');
1023 1023 var $form = $(tmpl);
1024 1024
1025 1025 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
1026 1026 var curForm = $formPlaceholder.find('form');
1027 1027 if (curForm){
1028 1028 curForm.remove();
1029 1029 }
1030 1030 $formPlaceholder.append($form);
1031 1031
1032 1032 var _form = $($form[0]);
1033 1033 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
1034 1034 var edit = false;
1035 1035 var comment_id = null;
1036 1036 var commentForm = this.createCommentForm(
1037 1037 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
1038 1038 commentForm.initStatusChangeSelector();
1039 1039
1040 1040 return commentForm;
1041 1041 };
1042 1042
1043 1043 this.editComment = function(node, line_no, f_path) {
1044 1044 self.edit = true;
1045 1045 var $node = $(node);
1046 1046 var $td = $node.closest('td');
1047 1047
1048 1048 var $comment = $(node).closest('.comment');
1049 1049 var comment_id = $($comment).data('commentId');
1050 1050 var isDraft = $($comment).data('commentDraft');
1051 1051 var $editForm = null
1052 1052
1053 1053 var $comments = $node.closest('div.inline-comments');
1054 1054 var $general_comments = null;
1055 1055
1056 1056 if($comments.length){
1057 1057 // inline comments setup
1058 1058 $editForm = $comments.find('.comment-inline-form');
1059 1059 line_no = self.getLineNumber(node)
1060 1060 }
1061 1061 else{
1062 1062 // general comments setup
1063 1063 $comments = $('#comments');
1064 1064 $editForm = $comments.find('.comment-inline-form');
1065 1065 line_no = $comment[0].id
1066 1066 $('#cb-comment-general-form-placeholder').hide();
1067 1067 }
1068 1068
1069 1069 if ($editForm.length === 0) {
1070 1070
1071 1071 // unhide all comments if they are hidden for a proper REPLY mode
1072 1072 var $filediff = $node.closest('.filediff');
1073 1073 $filediff.removeClass('hide-comments');
1074 1074
1075 1075 $editForm = self.createNewFormWrapper(f_path, line_no);
1076 1076 if(f_path && line_no) {
1077 1077 $editForm.addClass('comment-inline-form-edit')
1078 1078 }
1079 1079
1080 1080 $comment.after($editForm)
1081 1081
1082 1082 var _form = $($editForm[0]).find('form');
1083 1083 var autocompleteActions = ['as_note',];
1084 1084 var commentForm = this.createCommentForm(
1085 1085 _form, line_no, '', autocompleteActions, resolvesCommentId,
1086 1086 this.edit, comment_id);
1087 1087 var old_comment_text_binary = $comment.attr('data-comment-text');
1088 1088 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
1089 1089 commentForm.cm.setValue(old_comment_text);
1090 1090 $comment.hide();
1091 1091 tooltipActivate();
1092 1092
1093 1093 // set a CUSTOM submit handler for inline comment edit action.
1094 1094 commentForm.setHandleFormSubmit(function(o) {
1095 1095 var text = commentForm.cm.getValue();
1096 1096 var commentType = commentForm.getCommentType();
1097 1097
1098 1098 if (text === "") {
1099 1099 return;
1100 1100 }
1101 1101
1102 1102 if (old_comment_text == text) {
1103 1103 SwalNoAnimation.fire({
1104 1104 title: 'Unable to edit comment',
1105 1105 html: _gettext('Comment body was not changed.'),
1106 1106 });
1107 1107 return;
1108 1108 }
1109 1109 var excludeCancelBtn = false;
1110 1110 var submitEvent = true;
1111 1111 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1112 1112 commentForm.cm.setOption("readOnly", true);
1113 1113
1114 1114 // Read last version known
1115 1115 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
1116 1116 var version = versionSelector.data('lastVersion');
1117 1117
1118 1118 if (!version) {
1119 1119 version = 0;
1120 1120 }
1121 1121
1122 1122 var postData = {
1123 1123 'text': text,
1124 1124 'f_path': f_path,
1125 1125 'line': line_no,
1126 1126 'comment_type': commentType,
1127 1127 'draft': isDraft,
1128 1128 'version': version,
1129 1129 'csrf_token': CSRF_TOKEN
1130 1130 };
1131 1131
1132 1132 var submitSuccessCallback = function(json_data) {
1133 1133 $editForm.remove();
1134 1134 $comment.show();
1135 1135 var postData = {
1136 1136 'text': text,
1137 1137 'renderer': $comment.attr('data-comment-renderer'),
1138 1138 'csrf_token': CSRF_TOKEN
1139 1139 };
1140 1140
1141 1141 /* Inject new edited version selector */
1142 1142 var updateCommentVersionDropDown = function () {
1143 1143 var versionSelectId = '#comment_versions_'+comment_id;
1144 1144 var preLoadVersionData = [
1145 1145 {
1146 1146 id: json_data['comment_version'],
1147 1147 text: "v{0}".format(json_data['comment_version']),
1148 1148 action: function () {
1149 1149 Rhodecode.comments.showVersion(
1150 1150 json_data['comment_id'],
1151 1151 json_data['comment_history_id']
1152 1152 )
1153 1153 },
1154 1154 comment_version: json_data['comment_version'],
1155 1155 comment_author_username: json_data['comment_author_username'],
1156 1156 comment_author_gravatar: json_data['comment_author_gravatar'],
1157 1157 comment_created_on: json_data['comment_created_on'],
1158 1158 },
1159 1159 ]
1160 1160
1161 1161
1162 1162 if ($(versionSelectId).data('select2')) {
1163 1163 var oldData = $(versionSelectId).data('select2').opts.data.results;
1164 1164 $(versionSelectId).select2("destroy");
1165 1165 preLoadVersionData = oldData.concat(preLoadVersionData)
1166 1166 }
1167 1167
1168 1168 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1169 1169
1170 1170 $comment.attr('data-comment-text', utf8ToB64(text));
1171 1171
1172 1172 var versionSelector = $('#comment_versions_'+comment_id);
1173 1173
1174 1174 // set lastVersion so we know our last edit version
1175 1175 versionSelector.data('lastVersion', json_data['comment_version'])
1176 1176 versionSelector.parent().show();
1177 1177 }
1178 1178 updateCommentVersionDropDown();
1179 1179
1180 1180 // by default we reset state of comment preserving the text
1181 1181 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1182 1182 var prefix = "Error while editing this comment.\n"
1183 1183 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1184 1184 ajaxErrorSwal(message);
1185 1185 };
1186 1186
1187 1187 var successRenderCommit = function(o){
1188 1188 $comment.show();
1189 1189 $comment[0].lastElementChild.innerHTML = o;
1190 1190 };
1191 1191
1192 1192 var previewUrl = pyroutes.url(
1193 1193 'repo_commit_comment_preview',
1194 1194 {'repo_name': templateContext.repo_name,
1195 1195 'commit_id': templateContext.commit_data.commit_id});
1196 1196
1197 1197 _submitAjaxPOST(
1198 1198 previewUrl, postData, successRenderCommit, failRenderCommit
1199 1199 );
1200 1200
1201 1201 try {
1202 1202 var html = json_data.rendered_text;
1203 1203 var lineno = json_data.line_no;
1204 1204 var target_id = json_data.target_id;
1205 1205
1206 1206 $comments.find('.cb-comment-add-button').before(html);
1207 1207
1208 1208 // run global callback on submit
1209 1209 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1210 1210
1211 1211 } catch (e) {
1212 1212 console.error(e);
1213 1213 }
1214 1214
1215 1215 // re trigger the linkification of next/prev navigation
1216 1216 linkifyComments($('.inline-comment-injected'));
1217 1217 timeagoActivate();
1218 1218 tooltipActivate();
1219 1219
1220 1220 if (window.updateSticky !== undefined) {
1221 1221 // potentially our comments change the active window size, so we
1222 1222 // notify sticky elements
1223 1223 updateSticky()
1224 1224 }
1225 1225
1226 1226 if (window.refreshAllComments !== undefined && !isDraft) {
1227 1227 // if we have this handler, run it, and refresh all comments boxes
1228 1228 refreshAllComments()
1229 1229 }
1230 1230 else if (window.refreshDraftComments !== undefined && isDraft) {
1231 1231 // if we have this handler, run it, and refresh all comments boxes
1232 1232 refreshDraftComments();
1233 1233 }
1234 1234
1235 1235 commentForm.setActionButtonsDisabled(false);
1236 1236
1237 1237 };
1238 1238
1239 1239 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1240 1240 var prefix = "Error while editing comment.\n"
1241 1241 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1242 1242 if (jqXHR.status == 409){
1243 1243 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1244 1244 ajaxErrorSwal(message, 'Comment version mismatch.');
1245 1245 } else {
1246 1246 ajaxErrorSwal(message);
1247 1247 }
1248 1248
1249 1249 commentForm.resetCommentFormState(text)
1250 1250 };
1251 1251 commentForm.submitAjaxPOST(
1252 1252 commentForm.submitUrl, postData,
1253 1253 submitSuccessCallback,
1254 1254 submitFailCallback);
1255 1255 });
1256 1256 }
1257 1257
1258 1258 $editForm.addClass('comment-inline-form-open');
1259 1259 };
1260 1260
1261 1261 this.attachComment = function(json_data) {
1262 1262 var self = this;
1263 1263 $.each(json_data, function(idx, val) {
1264 1264 var json_data_elem = [val]
1265 1265 var isInline = val.comment_f_path && val.comment_lineno
1266 1266
1267 1267 if (isInline) {
1268 1268 self.attachInlineComment(json_data_elem)
1269 1269 } else {
1270 1270 self.attachGeneralComment(json_data_elem)
1271 1271 }
1272 1272 })
1273 1273
1274 1274 }
1275 1275
1276 1276 this.attachGeneralComment = function(json_data) {
1277 1277 $.each(json_data, function(idx, val) {
1278 1278 $('#injected_page_comments').append(val.rendered_text);
1279 1279 })
1280 1280 }
1281 1281
1282 1282 this.attachInlineComment = function(json_data) {
1283 1283
1284 1284 $.each(json_data, function (idx, val) {
1285 1285 var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
1286 1286 var html = val.rendered_text;
1287 1287 var $inlineComments = $('#' + val.target_id)
1288 1288 .find(line_qry)
1289 1289 .find('.inline-comments');
1290 1290
1291 1291 var lastComment = $inlineComments.find('.comment-inline').last();
1292 1292
1293 1293 if (lastComment.length === 0) {
1294 1294 // first comment, we append simply
1295 1295 $inlineComments.find('.reply-thread-container-wrapper').before(html);
1296 1296 } else {
1297 1297 $(lastComment).after(html)
1298 1298 }
1299 1299
1300 1300 })
1301 1301
1302 1302 };
1303 1303
1304 1304 this.createNewFormWrapper = function(f_path, line_no) {
1305 1305 // create a new reply HTML form from template
1306 1306 var tmpl = $('#cb-comment-inline-form-template').html();
1307 1307 tmpl = tmpl.format(escapeHtml(f_path), line_no);
1308 1308 return $(tmpl);
1309 1309 }
1310 1310
1311 1311 this.createComment = function(node, f_path, line_no, resolutionComment) {
1312 1312 self.edit = false;
1313 1313 var $node = $(node);
1314 1314 var $td = $node.closest('td');
1315 1315 var resolvesCommentId = resolutionComment || null;
1316 1316
1317 1317 var $replyForm = $td.find('.comment-inline-form');
1318 1318
1319 1319 // if form isn't existing, we're generating a new one and injecting it.
1320 1320 if ($replyForm.length === 0) {
1321 1321
1322 1322 // unhide/expand all comments if they are hidden for a proper REPLY mode
1323 1323 self.toggleLineComments($node, true);
1324 1324
1325 1325 $replyForm = self.createNewFormWrapper(f_path, line_no);
1326 1326
1327 1327 var $comments = $td.find('.inline-comments');
1328 1328
1329 1329 // There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
1330 1330 if ($comments.length===0) {
1331 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 1332 var $reply_container = $('#cb-comments-inline-container-template')
1333 1333 $reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
1334 1334 $td.append($($reply_container).html());
1335 1335 }
1336 1336
1337 1337 // default comment button exists, so we prepend the form for leaving initial comment
1338 1338 $td.find('.cb-comment-add-button').before($replyForm);
1339 1339 // set marker, that we have a open form
1340 1340 var $replyWrapper = $td.find('.reply-thread-container-wrapper')
1341 1341 $replyWrapper.addClass('comment-form-active');
1342 1342
1343 1343 var lastComment = $comments.find('.comment-inline').last();
1344 1344 if ($(lastComment).hasClass('comment-outdated')) {
1345 1345 $replyWrapper.show();
1346 1346 }
1347 1347
1348 1348 var _form = $($replyForm[0]).find('form');
1349 1349 var autocompleteActions = ['as_note', 'as_todo'];
1350 1350 var comment_id=null;
1351 1351 var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
1352 1352 var commentForm = self.createCommentForm(
1353 1353 _form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
1354 1354 self.edit, comment_id);
1355 1355
1356 1356 // set a CUSTOM submit handler for inline comments.
1357 1357 commentForm.setHandleFormSubmit(function(o) {
1358 1358 var text = commentForm.cm.getValue();
1359 1359 var commentType = commentForm.getCommentType();
1360 1360 var resolvesCommentId = commentForm.getResolvesId();
1361 1361 var isDraft = commentForm.getDraftState();
1362 1362
1363 1363 if (text === "") {
1364 1364 return;
1365 1365 }
1366 1366
1367 1367 if (line_no === undefined) {
1368 1368 alert('Error: unable to fetch line number for this inline comment !');
1369 1369 return;
1370 1370 }
1371 1371
1372 1372 if (f_path === undefined) {
1373 1373 alert('Error: unable to fetch file path for this inline comment !');
1374 1374 return;
1375 1375 }
1376 1376
1377 1377 var excludeCancelBtn = false;
1378 1378 var submitEvent = true;
1379 1379 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1380 1380 commentForm.cm.setOption("readOnly", true);
1381 1381 var postData = {
1382 1382 'text': text,
1383 1383 'f_path': f_path,
1384 1384 'line': line_no,
1385 1385 'comment_type': commentType,
1386 1386 'draft': isDraft,
1387 1387 'csrf_token': CSRF_TOKEN
1388 1388 };
1389 1389 if (resolvesCommentId){
1390 1390 postData['resolves_comment_id'] = resolvesCommentId;
1391 1391 }
1392 1392
1393 1393 // submitSuccess for inline commits
1394 1394 var submitSuccessCallback = function(json_data) {
1395 1395
1396 1396 $replyForm.remove();
1397 1397 $td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
1398 1398
1399 1399 try {
1400 1400
1401 1401 // inject newly created comments, json_data is {<comment_id>: {}}
1402 1402 self.attachInlineComment(json_data)
1403 1403
1404 1404 //mark visually which comment was resolved
1405 1405 if (resolvesCommentId) {
1406 1406 commentForm.markCommentResolved(resolvesCommentId);
1407 1407 }
1408 1408
1409 1409 // run global callback on submit
1410 1410 commentForm.globalSubmitSuccessCallback({
1411 1411 draft: isDraft,
1412 1412 comment_id: comment_id
1413 1413 });
1414 1414
1415 1415 } catch (e) {
1416 1416 console.error(e);
1417 1417 }
1418 1418
1419 1419 if (window.updateSticky !== undefined) {
1420 1420 // potentially our comments change the active window size, so we
1421 1421 // notify sticky elements
1422 1422 updateSticky()
1423 1423 }
1424 1424
1425 1425 if (window.refreshAllComments !== undefined && !isDraft) {
1426 1426 // if we have this handler, run it, and refresh all comments boxes
1427 1427 refreshAllComments()
1428 1428 }
1429 1429 else if (window.refreshDraftComments !== undefined && isDraft) {
1430 1430 // if we have this handler, run it, and refresh all comments boxes
1431 1431 refreshDraftComments();
1432 1432 }
1433 1433
1434 1434 commentForm.setActionButtonsDisabled(false);
1435 1435
1436 1436 // re trigger the linkification of next/prev navigation
1437 1437 linkifyComments($('.inline-comment-injected'));
1438 1438 timeagoActivate();
1439 1439 tooltipActivate();
1440 1440 };
1441 1441
1442 1442 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1443 1443 var prefix = "Error while submitting comment.\n"
1444 1444 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1445 1445 ajaxErrorSwal(message);
1446 1446 commentForm.resetCommentFormState(text)
1447 1447 };
1448 1448
1449 1449 commentForm.submitAjaxPOST(
1450 1450 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1451 1451 });
1452 1452 }
1453 1453
1454 1454 // Finally "open" our reply form, since we know there are comments and we have the "attached" old form
1455 1455 $replyForm.addClass('comment-inline-form-open');
1456 1456 tooltipActivate();
1457 1457 };
1458 1458
1459 1459 this.createResolutionComment = function(commentId){
1460 1460 // hide the trigger text
1461 1461 $('#resolve-comment-{0}'.format(commentId)).hide();
1462 1462
1463 1463 var comment = $('#comment-'+commentId);
1464 1464 var commentData = comment.data();
1465 console.log(commentData);
1466 1465
1467 1466 if (commentData.commentInline) {
1468 1467 var f_path = commentData.commentFPath;
1469 1468 var line_no = commentData.commentLineNo;
1470 1469 this.createComment(comment, f_path, line_no, commentId)
1471 1470 } else {
1472 1471 this.createGeneralComment('general', "$placeholder", commentId)
1473 1472 }
1474 1473
1475 1474 return false;
1476 1475 };
1477 1476
1478 1477 this.submitResolution = function(commentId){
1479 1478 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1480 1479 var commentForm = form.get(0).CommentForm;
1481 1480
1482 1481 var cm = commentForm.getCmInstance();
1483 1482 var renderer = templateContext.visual.default_renderer;
1484 1483 if (renderer == 'rst'){
1485 1484 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1486 1485 } else if (renderer == 'markdown') {
1487 1486 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1488 1487 } else {
1489 1488 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1490 1489 }
1491 1490
1492 1491 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1493 1492 form.submit();
1494 1493 return false;
1495 1494 };
1496 1495
1497 1496 };
1498 1497
1499 1498 window.commentHelp = function(renderer) {
1500 1499 var funcData = {'renderer': renderer}
1501 1500 return renderTemplate('commentHelpHovercard', funcData)
1502 1501 } No newline at end of file
@@ -1,206 +1,205 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3 <%namespace name="base" file="base.mako"/>
4 4
5 5 ## EMAIL SUBJECT
6 6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 7 <%
8 8 data = {
9 9 'user': '@'+h.person(user),
10 10 'repo_name': repo_name,
11 11 'status': status_change,
12 12 'comment_file': comment_file,
13 13 'comment_line': comment_line,
14 14 'comment_type': comment_type,
15 15 'comment_id': comment_id,
16 16
17 'pr_title': pull_request.title,
17 'pr_title': pull_request.title_safe,
18 18 'pr_id': pull_request.pull_request_id,
19 19 'mention_prefix': '[mention] ' if mention else '',
20 20 }
21 21
22 22 if comment_file:
23 23 subject_template = email_pr_comment_file_subject_template or \
24 24 _('{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"').format(**data)
25 25 else:
26 26 if status_change:
27 27 subject_template = email_pr_comment_status_change_subject_template or \
28 28 _('{mention_prefix}[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
29 29 else:
30 30 subject_template = email_pr_comment_subject_template or \
31 31 _('{mention_prefix}{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
32 32 %>
33 33
34
35 34 ${subject_template.format(**data) |n}
36 35 </%def>
37 36
38 37 ## PLAINTEXT VERSION OF BODY
39 38 <%def name="body_plaintext()" filter="n,trim">
40 39 <%
41 40 data = {
42 41 'user': h.person(user),
43 42 'repo_name': repo_name,
44 43 'status': status_change,
45 44 'comment_file': comment_file,
46 45 'comment_line': comment_line,
47 46 'comment_type': comment_type,
48 47 'comment_id': comment_id,
49 48
50 'pr_title': pull_request.title,
49 'pr_title': pull_request.title_safe,
51 50 'pr_id': pull_request.pull_request_id,
52 51 'source_ref_type': pull_request.source_ref_parts.type,
53 52 'source_ref_name': pull_request.source_ref_parts.name,
54 53 'target_ref_type': pull_request.target_ref_parts.type,
55 54 'target_ref_name': pull_request.target_ref_parts.name,
56 55 'source_repo': pull_request_source_repo.repo_name,
57 56 'target_repo': pull_request_target_repo.repo_name,
58 57 'source_repo_url': pull_request_source_repo_url,
59 58 'target_repo_url': pull_request_target_repo_url,
60 59 }
61 60 %>
62 61
63 62 * ${_('Comment link')}: ${pr_comment_url}
64 63
65 64 * ${_('Pull Request')}: !${pull_request.pull_request_id}
66 65
67 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 68 %if status_change and not closing_pr:
70 69 * ${_('{user} submitted pull request !{pr_id} status: *{status}*').format(**data)}
71 70
72 71 %elif status_change and closing_pr:
73 72 * ${_('{user} submitted pull request !{pr_id} status: *{status} and closed*').format(**data)}
74 73
75 74 %endif
76 75 %if comment_file:
77 76 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
78 77
79 78 %endif
80 79 % if comment_type == 'todo':
81 80 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
82 81 % else:
83 82 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
84 83 % endif
85 84
86 85 ${comment_body |n, trim}
87 86
88 87 ---
89 88 ${self.plaintext_footer()}
90 89 </%def>
91 90
92 91
93 92 <%
94 93 data = {
95 94 'user': h.person(user),
96 95 'comment_file': comment_file,
97 96 'comment_line': comment_line,
98 97 'comment_type': comment_type,
99 98 'comment_id': comment_id,
100 99 'renderer_type': renderer_type or 'plain',
101 100
102 'pr_title': pull_request.title,
101 'pr_title': pull_request.title_safe,
103 102 'pr_id': pull_request.pull_request_id,
104 103 'status': status_change,
105 104 'source_ref_type': pull_request.source_ref_parts.type,
106 105 'source_ref_name': pull_request.source_ref_parts.name,
107 106 'target_ref_type': pull_request.target_ref_parts.type,
108 107 'target_ref_name': pull_request.target_ref_parts.name,
109 108 'source_repo': pull_request_source_repo.repo_name,
110 109 'target_repo': pull_request_target_repo.repo_name,
111 110 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
112 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 115 ## header
117 116 <table style="text-align:left;vertical-align:middle;width: 100%">
118 117 <tr>
119 118 <td style="width:100%;border-bottom:1px solid #dbd9da;">
120 119
121 120 <div style="margin: 0; font-weight: bold">
122 121 <div class="clear-both" style="margin-bottom: 4px">
123 122 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
124 123 ${_('left a')}
125 124 <a href="${pr_comment_url}" style="${base.link_css()}">
126 125 % if comment_file:
127 126 ${_('{comment_type} on file `{comment_file}` in pull request.').format(**data)}
128 127 % else:
129 128 ${_('{comment_type} on pull request.').format(**data) |n}
130 129 % endif
131 130 </a>
132 131 </div>
133 132 <div style="margin-top: 10px"></div>
134 133 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
135 134 </div>
136 135
137 136 </td>
138 137 </tr>
139 138
140 139 </table>
141 140 <div class="clear-both"></div>
142 141 ## main body
143 142 <table style="text-align:left;vertical-align:middle;width: 100%">
144 143
145 144 ## spacing def
146 145 <tr>
147 146 <td style="width: 130px"></td>
148 147 <td></td>
149 148 </tr>
150 149
151 150 % if status_change:
152 151 <tr>
153 152 <td style="padding-right:20px;">${_('Review Status')}:</td>
154 153 <td>
155 154 % if closing_pr:
156 155 ${_('Closed pull request with status')}: ${base.status_text(status_change, tag_type=status_change_type)}
157 156 % else:
158 157 ${_('Submitted review status')}: ${base.status_text(status_change, tag_type=status_change_type)}
159 158 % endif
160 159 </td>
161 160 </tr>
162 161 % endif
163 162 <tr>
164 163 <td style="padding-right:20px;">${_('Pull request')}:</td>
165 164 <td>
166 165 <a href="${pull_request_url}" style="${base.link_css()}">
167 166 !${pull_request.pull_request_id}
168 167 </a>
169 168 </td>
170 169 </tr>
171 170
172 171 <tr>
173 172 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
174 173 <td style="line-height:20px;">
175 174 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
176 175 &rarr;
177 176 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
178 177 </td>
179 178 </tr>
180 179
181 180 % if comment_file:
182 181 <tr>
183 182 <td style="padding-right:20px;">${_('File')}:</td>
184 183 <td><a href="${pr_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
185 184 </tr>
186 185 % endif
187 186
188 187 <tr style="border-bottom:1px solid #dbd9da;">
189 188 <td colspan="2" style="padding-right:20px;">
190 189 % if comment_type == 'todo':
191 190 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
192 191 % else:
193 192 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
194 193 % endif
195 194 </td>
196 195 </tr>
197 196
198 197 <tr>
199 198 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
200 199 </tr>
201 200
202 201 <tr>
203 202 <td><a href="${pr_comment_reply_url}">${_('Reply')}</a></td>
204 203 <td></td>
205 204 </tr>
206 205 </table>
@@ -1,154 +1,154 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3 <%namespace name="base" file="base.mako"/>
4 4
5 5 ## EMAIL SUBJECT
6 6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 7 <%
8 8 data = {
9 9 'user': '@'+h.person(user),
10 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 14 if user_role == 'observer':
15 15 subject_template = email_pr_review_subject_template or _('{user} added you as observer to pull request. !{pr_id}: "{pr_title}"')
16 16 else:
17 17 subject_template = email_pr_review_subject_template or _('{user} requested a pull request review. !{pr_id}: "{pr_title}"')
18 18 %>
19 19
20 20 ${subject_template.format(**data) |n}
21 21 </%def>
22 22
23 23 ## PLAINTEXT VERSION OF BODY
24 24 <%def name="body_plaintext()" filter="n,trim">
25 25 <%
26 26 data = {
27 27 'user': h.person(user),
28 28 'pr_id': pull_request.pull_request_id,
29 'pr_title': pull_request.title,
29 'pr_title': pull_request.title_safe,
30 30 'source_ref_type': pull_request.source_ref_parts.type,
31 31 'source_ref_name': pull_request.source_ref_parts.name,
32 32 'target_ref_type': pull_request.target_ref_parts.type,
33 33 'target_ref_name': pull_request.target_ref_parts.name,
34 34 'repo_url': pull_request_source_repo_url,
35 35 'source_repo': pull_request_source_repo.repo_name,
36 36 'target_repo': pull_request_target_repo.repo_name,
37 37 'source_repo_url': pull_request_source_repo_url,
38 38 'target_repo_url': pull_request_target_repo_url,
39 39 }
40 40
41 41 %>
42 42
43 43 * ${_('Pull Request link')}: ${pull_request_url}
44 44
45 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 47 * ${_('Title')}: ${pull_request.title}
48 48
49 49 * ${_('Description')}:
50 50
51 51 ${pull_request.description | trim}
52 52
53 53
54 54 * ${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}:
55 55
56 56 % for commit_id, message in pull_request_commits:
57 57 - ${h.short_id(commit_id)}
58 58 ${h.chop_at_smart(message.lstrip(), '\n', suffix_if_chopped='...')}
59 59
60 60 % endfor
61 61
62 62 ---
63 63 ${self.plaintext_footer()}
64 64 </%def>
65 65 <%
66 66 data = {
67 67 'user': h.person(user),
68 68 'pr_id': pull_request.pull_request_id,
69 'pr_title': pull_request.title,
69 'pr_title': pull_request.title_safe,
70 70 'source_ref_type': pull_request.source_ref_parts.type,
71 71 'source_ref_name': pull_request.source_ref_parts.name,
72 72 'target_ref_type': pull_request.target_ref_parts.type,
73 73 'target_ref_name': pull_request.target_ref_parts.name,
74 74 'repo_url': pull_request_source_repo_url,
75 75 'source_repo': pull_request_source_repo.repo_name,
76 76 'target_repo': pull_request_target_repo.repo_name,
77 77 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
78 78 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
79 79 }
80 80 %>
81 81 ## header
82 82 <table style="text-align:left;vertical-align:middle;width: 100%">
83 83 <tr>
84 84 <td style="width:100%;border-bottom:1px solid #dbd9da;">
85 85 <div style="margin: 0; font-weight: bold">
86 86 % if user_role == 'observer':
87 87 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
88 88 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
89 89 ${_('added you as observer to')}
90 90 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a>.
91 91 </div>
92 92 % else:
93 93 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
94 94 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
95 95 ${_('requested a')}
96 96 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a> review.
97 97 </div>
98 98 % endif
99 99 <div style="margin-top: 10px"></div>
100 100 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
101 101 </div>
102 102 </td>
103 103 </tr>
104 104
105 105 </table>
106 106 <div class="clear-both"></div>
107 107 ## main body
108 108 <table style="text-align:left;vertical-align:middle;width: 100%">
109 109 ## spacing def
110 110 <tr>
111 111 <td style="width: 130px"></td>
112 112 <td></td>
113 113 </tr>
114 114
115 115 <tr>
116 116 <td style="padding-right:20px;">${_('Pull request')}:</td>
117 117 <td>
118 118 <a href="${pull_request_url}" style="${base.link_css()}">
119 119 !${pull_request.pull_request_id}
120 120 </a>
121 121 </td>
122 122 </tr>
123 123
124 124 <tr>
125 125 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
126 126 <td style="line-height:20px;">
127 127 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
128 128 &rarr;
129 129 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
130 130 </td>
131 131 </tr>
132 132
133 133 <tr>
134 134 <td style="padding-right:20px;">${_('Description')}:</td>
135 135 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
136 136 </tr>
137 137 <tr>
138 138 <td style="padding-right:20px;">${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits)) % {'num': len(pull_request_commits)}}:</td>
139 139 <td></td>
140 140 </tr>
141 141
142 142 <tr>
143 143 <td colspan="2">
144 144 <ol style="margin:0 0 0 1em;padding:0;text-align:left;">
145 145 % for commit_id, message in pull_request_commits:
146 146 <li style="margin:0 0 1em;">
147 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 148 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
149 149 </li>
150 150 % endfor
151 151 </ol>
152 152 </td>
153 153 </tr>
154 154 </table>
@@ -1,172 +1,172 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3 <%namespace name="base" file="base.mako"/>
4 4
5 5 ## EMAIL SUBJECT
6 6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 7 <%
8 8 data = {
9 9 'updating_user': '@'+h.person(updating_user),
10 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 14 subject_template = email_pr_update_subject_template or _('{updating_user} updated pull request. !{pr_id}: "{pr_title}"')
15 15 %>
16 16
17 17 ${subject_template.format(**data) |n}
18 18 </%def>
19 19
20 20 ## PLAINTEXT VERSION OF BODY
21 21 <%def name="body_plaintext()" filter="n,trim">
22 22 <%
23 23 data = {
24 24 'updating_user': h.person(updating_user),
25 25 'pr_id': pull_request.pull_request_id,
26 'pr_title': pull_request.title,
26 'pr_title': pull_request.title_safe,
27 27 'source_ref_type': pull_request.source_ref_parts.type,
28 28 'source_ref_name': pull_request.source_ref_parts.name,
29 29 'target_ref_type': pull_request.target_ref_parts.type,
30 30 'target_ref_name': pull_request.target_ref_parts.name,
31 31 'repo_url': pull_request_source_repo_url,
32 32 'source_repo': pull_request_source_repo.repo_name,
33 33 'target_repo': pull_request_target_repo.repo_name,
34 34 'source_repo_url': pull_request_source_repo_url,
35 35 'target_repo_url': pull_request_target_repo_url,
36 36 }
37 37 %>
38 38
39 39 * ${_('Pull Request link')}: ${pull_request_url}
40 40
41 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 43 * ${_('Title')}: ${pull_request.title}
44 44
45 45 * ${_('Description')}:
46 46
47 47 ${pull_request.description | trim}
48 48
49 49 * Changed commits:
50 50
51 51 - Added: ${len(added_commits)}
52 52 - Removed: ${len(removed_commits)}
53 53
54 54 * Changed files:
55 55
56 56 %if not changed_files:
57 57 No file changes found
58 58 %else:
59 59 %for file_name in added_files:
60 60 - A `${file_name}`
61 61 %endfor
62 62 %for file_name in modified_files:
63 63 - M `${file_name}`
64 64 %endfor
65 65 %for file_name in removed_files:
66 66 - R `${file_name}`
67 67 %endfor
68 68 %endif
69 69
70 70 ---
71 71 ${self.plaintext_footer()}
72 72 </%def>
73 73 <%
74 74 data = {
75 75 'updating_user': h.person(updating_user),
76 76 'pr_id': pull_request.pull_request_id,
77 'pr_title': pull_request.title,
77 'pr_title': pull_request.title_safe,
78 78 'source_ref_type': pull_request.source_ref_parts.type,
79 79 'source_ref_name': pull_request.source_ref_parts.name,
80 80 'target_ref_type': pull_request.target_ref_parts.type,
81 81 'target_ref_name': pull_request.target_ref_parts.name,
82 82 'repo_url': pull_request_source_repo_url,
83 83 'source_repo': pull_request_source_repo.repo_name,
84 84 'target_repo': pull_request_target_repo.repo_name,
85 85 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
86 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 90 ## header
91 91 <table style="text-align:left;vertical-align:middle;width: 100%">
92 92 <tr>
93 93 <td style="width:100%;border-bottom:1px solid #dbd9da;">
94 94
95 95 <div style="margin: 0; font-weight: bold">
96 96 <div class="clear-both" style="margin-bottom: 4px">
97 97 <span style="color:#7E7F7F">@${h.person(updating_user.username)}</span>
98 98 ${_('updated')}
99 99 <a href="${pull_request_url}" style="${base.link_css()}">
100 100 ${_('pull request.').format(**data) }
101 101 </a>
102 102 </div>
103 103 <div style="margin-top: 10px"></div>
104 104 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
105 105 </div>
106 106
107 107 </td>
108 108 </tr>
109 109
110 110 </table>
111 111 <div class="clear-both"></div>
112 112 ## main body
113 113 <table style="text-align:left;vertical-align:middle;width: 100%">
114 114 ## spacing def
115 115 <tr>
116 116 <td style="width: 130px"></td>
117 117 <td></td>
118 118 </tr>
119 119
120 120 <tr>
121 121 <td style="padding-right:20px;">${_('Pull request')}:</td>
122 122 <td>
123 123 <a href="${pull_request_url}" style="${base.link_css()}">
124 124 !${pull_request.pull_request_id}
125 125 </a>
126 126 </td>
127 127 </tr>
128 128
129 129 <tr>
130 130 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
131 131 <td style="line-height:20px;">
132 132 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
133 133 &rarr;
134 134 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
135 135 </td>
136 136 </tr>
137 137
138 138 <tr>
139 139 <td style="padding-right:20px;">${_('Description')}:</td>
140 140 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
141 141 </tr>
142 142 <tr>
143 143 <td style="padding-right:20px;">${_('Changes')}:</td>
144 144 <td>
145 145 <strong>Changed commits:</strong>
146 146 <ul class="changes-ul">
147 147 <li>- Added: ${len(added_commits)}</li>
148 148 <li>- Removed: ${len(removed_commits)}</li>
149 149 </ul>
150 150
151 151 <strong>Changed files:</strong>
152 152 <ul class="changes-ul">
153 153
154 154 %if not changed_files:
155 155 <li>No file changes found</li>
156 156 %else:
157 157 %for file_name in added_files:
158 158 <li>- A <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
159 159 %endfor
160 160 %for file_name in modified_files:
161 161 <li>- M <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
162 162 %endfor
163 163 %for file_name in removed_files:
164 164 <li>- R <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
165 165 %endfor
166 166 %endif
167 167
168 168 </ul>
169 169 </td>
170 170 </tr>
171 171
172 172 </table>
@@ -1,195 +1,197 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22 import collections
23 23
24 24 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
25 25 from rhodecode.lib.utils2 import AttributeDict
26 26 from rhodecode.model.db import User, PullRequestReviewers
27 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 40 def test_get_template_obj(app, request_stub):
31 41 template = EmailNotificationModel().get_renderer(
32 42 EmailNotificationModel.TYPE_TEST, request_stub)
33 43 assert isinstance(template, PyramidPartialRenderer)
34 44
35 45
36 46 def test_render_email(app, http_host_only_stub):
37 47 kwargs = {}
38 48 subject, body, body_plaintext = EmailNotificationModel().render_email(
39 49 EmailNotificationModel.TYPE_TEST, **kwargs)
40 50
41 51 # subject
42 52 assert subject == 'Test "Subject" hello "world"'
43 53
44 54 # body plaintext
45 55 assert body_plaintext == 'Email Plaintext Body'
46 56
47 57 # body
48 58 notification_footer1 = 'This is a notification from RhodeCode.'
49 59 notification_footer2 = 'http://{}/'.format(http_host_only_stub)
50 60 assert notification_footer1 in body
51 61 assert notification_footer2 in body
52 62 assert 'Email Body' in body
53 63
54 64
55 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 67 ref = collections.namedtuple(
58 68 'Ref', 'name, type')('fxies123', 'book')
59
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
69 pr = pr(ref)
64 70 source_repo = target_repo = collections.namedtuple(
65 71 'Repo', 'type, repo_name')('hg', 'pull_request_1')
66 72
67 73 kwargs = {
68 74 'user': User.get_first_super_admin(),
69 75 'pull_request': pr,
70 76 'pull_request_commits': [],
71 77
72 78 'pull_request_target_repo': target_repo,
73 79 'pull_request_target_repo_url': 'x',
74 80
75 81 'pull_request_source_repo': source_repo,
76 82 'pull_request_source_repo_url': 'x',
77 83
78 84 'pull_request_url': 'http://localhost/pr1',
79 85 'user_role': role,
80 86 }
81 87
82 88 subject, body, body_plaintext = EmailNotificationModel().render_email(
83 89 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
84 90
85 91 # subject
86 92 if role == PullRequestReviewers.ROLE_REVIEWER:
87 93 assert subject == '@test_admin (RhodeCode Admin) requested a pull request review. !200: "Example Pull Request"'
88 94 elif role == PullRequestReviewers.ROLE_OBSERVER:
89 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 99 ref = collections.namedtuple(
94 100 'Ref', 'name, type')('fxies123', 'book')
95 101
96 pr = collections.namedtuple('PullRequest',
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')
102 pr = pr(ref)
99 103
100 104 source_repo = target_repo = collections.namedtuple(
101 105 'Repo', 'type, repo_name')('hg', 'pull_request_1')
102 106
103 107 commit_changes = AttributeDict({
104 108 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
105 109 'removed': ['eeeeeeeeeee'],
106 110 })
107 111 file_changes = AttributeDict({
108 112 'added': ['a/file1.md', 'file2.py'],
109 113 'modified': ['b/modified_file.rst'],
110 114 'removed': ['.idea'],
111 115 })
112 116
113 117 kwargs = {
114 118 'updating_user': User.get_first_super_admin(),
115 119
116 120 'pull_request': pr,
117 121 'pull_request_commits': [],
118 122
119 123 'pull_request_target_repo': target_repo,
120 124 'pull_request_target_repo_url': 'x',
121 125
122 126 'pull_request_source_repo': source_repo,
123 127 'pull_request_source_repo_url': 'x',
124 128
125 129 'pull_request_url': 'http://localhost/pr1',
126 130
127 131 'pr_comment_url': 'http://comment-url',
128 132 'pr_comment_reply_url': 'http://comment-url#reply',
129 133 'ancestor_commit_id': 'f39bd443',
130 134 'added_commits': commit_changes.added,
131 135 'removed_commits': commit_changes.removed,
132 136 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
133 137 'added_files': file_changes.added,
134 138 'modified_files': file_changes.modified,
135 139 'removed_files': file_changes.removed,
136 140 }
137 141
138 142 subject, body, body_plaintext = EmailNotificationModel().render_email(
139 143 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **kwargs)
140 144
141 145 # subject
142 146 assert subject == '@test_admin (RhodeCode Admin) updated pull request. !200: "Example Pull Request"'
143 147
144 148
145 149 @pytest.mark.parametrize('mention', [
146 150 True,
147 151 False
148 152 ])
149 153 @pytest.mark.parametrize('email_type', [
150 154 EmailNotificationModel.TYPE_COMMIT_COMMENT,
151 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 158 ref = collections.namedtuple(
155 159 'Ref', 'name, type')('fxies123', 'book')
156 160
157 pr = collections.namedtuple('PullRequest',
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')
161 pr = pr(ref)
160 162
161 163 source_repo = target_repo = collections.namedtuple(
162 164 'Repo', 'type, repo_name')('hg', 'pull_request_1')
163 165
164 166 kwargs = {
165 167 'user': User.get_first_super_admin(),
166 168 'commit': AttributeDict(raw_id='a'*40, message='Commit message'),
167 169 'status_change': 'approved',
168 170 'commit_target_repo_url': 'http://foo.example.com/#comment1',
169 171 'repo_name': 'test-repo',
170 172 'comment_file': 'test-file.py',
171 173 'comment_line': 'n100',
172 174 'comment_type': 'note',
173 175 'comment_id': 2048,
174 176 'commit_comment_url': 'http://comment-url',
175 177 'commit_comment_reply_url': 'http://comment-url/#Reply',
176 178 'instance_url': 'http://rc-instance',
177 179 'comment_body': 'hello world',
178 180 'mention': mention,
179 181
180 182 'pr_comment_url': 'http://comment-url',
181 183 'pr_comment_reply_url': 'http://comment-url/#Reply',
182 184 'pull_request': pr,
183 185 'pull_request_commits': [],
184 186
185 187 'pull_request_target_repo': target_repo,
186 188 'pull_request_target_repo_url': 'x',
187 189
188 190 'pull_request_source_repo': source_repo,
189 191 'pull_request_source_repo_url': 'x',
190 192
191 193 'pull_request_url': 'http://code.rc.com/_pr/123'
192 194 }
193 195 subject, body, body_plaintext = EmailNotificationModel().render_email(email_type, **kwargs)
194 196
195 197 assert '\n' not in subject
General Comments 0
You need to be logged in to leave comments. Login now