##// END OF EJS Templates
db: always use Session() for compatibility, Using Session is actually the...
marcink -
r506:4c6b9282 default
parent child Browse files
Show More
@@ -1,95 +1,95 b''
1 1 .. _rhodecode-reset-ref:
2 2
3 3 Settings Management
4 4 -------------------
5 5
6 6 All |RCE| settings can be set from the user interface, but in the event that
7 7 it somehow becomes unavailable you can use ``ishell`` inside your |RCE|
8 8 ``virtualenv`` to carry out emergency measures.
9 9
10 10 .. warning::
11 11
12 12 Logging into the |RCE| database with ``iShell`` should only be done by an
13 13 experienced and knowledgeable database administrator.
14 14
15 15 Reset Admin Account Privileges
16 16 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
17 17
18 18 If you accidentally remove your admin privileges from the admin account you
19 19 can restore them using ``ishell``. Use the following example to reset your
20 20 account permissions.
21 21
22 22 .. code-block:: bash
23 23
24 24 # Open iShell from the terminal
25 25 $ .rccontrol/enterprise-5/profile/bin/paster \
26 26 ishell .rccontrol/enterprise-5/rhodecode.ini
27 27
28 28 .. code-block:: mysql
29 29
30 30 # Use this example to change user permissions
31 31 In [1]: adminuser = User.get_by_username('username')
32 32 In [2]: adminuser.admin = True
33 In [3]: Session.add(adminuser);Session().commit()
33 In [3]: Session().add(adminuser);Session().commit()
34 34 In [4]: exit()
35 35
36 36 Set to read global ``.hgrc`` file
37 37 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
38 38
39 39 By default, |RCE| does not read global ``hgrc`` files in
40 40 ``/etc/mercurial/hgrc`` or ``/etc/mercurial/hgrc.d`` because it
41 41 can lead to issues. This is set in the ``rhodecode_ui`` table for which
42 42 there is no UI. If you need to edit this you can
43 43 manually change the settings using SQL statements with ``ishell``. Use the
44 44 following example to make changes to this table.
45 45
46 46 .. code-block:: bash
47 47
48 48 # Open iShell from the terminal
49 49 $ .rccontrol/enterprise-5/profile/bin/paster \
50 50 ishell.rccontrol/enterprise-5/rhodecode.ini
51 51
52 52 .. code-block:: mysql
53 53
54 54 # Use this example to enable global .hgrc access
55 55 In [4]: new_option = RhodeCodeUi()
56 56 In [5]: new_option.ui_section='web'
57 57 In [6]: new_option.ui_key='allow_push'
58 58 In [7]: new_option.ui_value='*'
59 59 In [8]: Session().add(new_option);Session().commit()
60 60
61 61 Manually Reset Password
62 62 ^^^^^^^^^^^^^^^^^^^^^^^
63 63
64 64 If you need to manually reset a user password, use the following steps.
65 65
66 66 1. Navigate to your |RCE| install location.
67 67 2. Run the interactive ``ishell`` prompt.
68 68 3. Set a new password.
69 69
70 70 Use the following code example to carry out these steps.
71 71
72 72 .. code-block:: bash
73 73
74 74 # starts the ishell interactive prompt
75 75 $ .rccontrol/enterprise-5/profile/bin/paster \
76 76 ishell .rccontrol/enterprise-5/rhodecode.ini
77 77
78 78 .. code-block:: mysql
79 79
80 80 from rhodecode.lib.auth import generate_auth_token
81 81 from rhodecode.lib.auth import get_crypt_password
82 82
83 83 # Enter the user name whose password you wish to change
84 84 my_user = 'USERNAME'
85 85 u = User.get_by_username(my_user)
86 86
87 87 # If this fails then the user does not exist
88 88 u.auth_token = generate_auth_token(my_user)
89 89
90 90 # Set the new password
91 91 u.password = get_crypt_password('PASSWORD')
92 92
93 93 Session().add(u)
94 94 Session().commit()
95 95 exit
@@ -1,633 +1,633 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 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 import logging
23 23
24 24 from rhodecode.api import jsonrpc_method, JSONRPCError
25 25 from rhodecode.api.utils import (
26 26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 28 has_repo_permissions, resolve_ref_or_error)
29 29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 30 from rhodecode.lib.base import vcs_operation_context
31 31 from rhodecode.lib.utils2 import str2bool
32 32 from rhodecode.model.changeset_status import ChangesetStatusModel
33 33 from rhodecode.model.comment import ChangesetCommentsModel
34 34 from rhodecode.model.db import Session, ChangesetStatus
35 35 from rhodecode.model.pull_request import PullRequestModel
36 36 from rhodecode.model.settings import SettingsModel
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 @jsonrpc_method()
42 42 def get_pull_request(request, apiuser, repoid, pullrequestid):
43 43 """
44 44 Get a pull request based on the given ID.
45 45
46 46 :param apiuser: This is filled automatically from the |authtoken|.
47 47 :type apiuser: AuthUser
48 48 :param repoid: Repository name or repository ID from where the pull
49 49 request was opened.
50 50 :type repoid: str or int
51 51 :param pullrequestid: ID of the requested pull request.
52 52 :type pullrequestid: int
53 53
54 54 Example output:
55 55
56 56 .. code-block:: bash
57 57
58 58 "id": <id_given_in_input>,
59 59 "result":
60 60 {
61 61 "pull_request_id": "<pull_request_id>",
62 62 "url": "<url>",
63 63 "title": "<title>",
64 64 "description": "<description>",
65 65 "status" : "<status>",
66 66 "created_on": "<date_time_created>",
67 67 "updated_on": "<date_time_updated>",
68 68 "commit_ids": [
69 69 ...
70 70 "<commit_id>",
71 71 "<commit_id>",
72 72 ...
73 73 ],
74 74 "review_status": "<review_status>",
75 75 "mergeable": {
76 76 "status": "<bool>",
77 77 "message": "<message>",
78 78 },
79 79 "source": {
80 80 "clone_url": "<clone_url>",
81 81 "repository": "<repository_name>",
82 82 "reference":
83 83 {
84 84 "name": "<name>",
85 85 "type": "<type>",
86 86 "commit_id": "<commit_id>",
87 87 }
88 88 },
89 89 "target": {
90 90 "clone_url": "<clone_url>",
91 91 "repository": "<repository_name>",
92 92 "reference":
93 93 {
94 94 "name": "<name>",
95 95 "type": "<type>",
96 96 "commit_id": "<commit_id>",
97 97 }
98 98 },
99 99 "author": <user_obj>,
100 100 "reviewers": [
101 101 ...
102 102 {
103 103 "user": "<user_obj>",
104 104 "review_status": "<review_status>",
105 105 }
106 106 ...
107 107 ]
108 108 },
109 109 "error": null
110 110 """
111 111 get_repo_or_error(repoid)
112 112 pull_request = get_pull_request_or_error(pullrequestid)
113 113 if not PullRequestModel().check_user_read(
114 114 pull_request, apiuser, api=True):
115 115 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
116 116 data = pull_request.get_api_data()
117 117 return data
118 118
119 119
120 120 @jsonrpc_method()
121 121 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
122 122 """
123 123 Get all pull requests from the repository specified in `repoid`.
124 124
125 125 :param apiuser: This is filled automatically from the |authtoken|.
126 126 :type apiuser: AuthUser
127 127 :param repoid: Repository name or repository ID.
128 128 :type repoid: str or int
129 129 :param status: Only return pull requests with the specified status.
130 130 Valid options are.
131 131 * ``new`` (default)
132 132 * ``open``
133 133 * ``closed``
134 134 :type status: str
135 135
136 136 Example output:
137 137
138 138 .. code-block:: bash
139 139
140 140 "id": <id_given_in_input>,
141 141 "result":
142 142 [
143 143 ...
144 144 {
145 145 "pull_request_id": "<pull_request_id>",
146 146 "url": "<url>",
147 147 "title" : "<title>",
148 148 "description": "<description>",
149 149 "status": "<status>",
150 150 "created_on": "<date_time_created>",
151 151 "updated_on": "<date_time_updated>",
152 152 "commit_ids": [
153 153 ...
154 154 "<commit_id>",
155 155 "<commit_id>",
156 156 ...
157 157 ],
158 158 "review_status": "<review_status>",
159 159 "mergeable": {
160 160 "status": "<bool>",
161 161 "message: "<message>",
162 162 },
163 163 "source": {
164 164 "clone_url": "<clone_url>",
165 165 "reference":
166 166 {
167 167 "name": "<name>",
168 168 "type": "<type>",
169 169 "commit_id": "<commit_id>",
170 170 }
171 171 },
172 172 "target": {
173 173 "clone_url": "<clone_url>",
174 174 "reference":
175 175 {
176 176 "name": "<name>",
177 177 "type": "<type>",
178 178 "commit_id": "<commit_id>",
179 179 }
180 180 },
181 181 "author": <user_obj>,
182 182 "reviewers": [
183 183 ...
184 184 {
185 185 "user": "<user_obj>",
186 186 "review_status": "<review_status>",
187 187 }
188 188 ...
189 189 ]
190 190 }
191 191 ...
192 192 ],
193 193 "error": null
194 194
195 195 """
196 196 repo = get_repo_or_error(repoid)
197 197 if not has_superadmin_permission(apiuser):
198 198 _perms = (
199 199 'repository.admin', 'repository.write', 'repository.read',)
200 200 has_repo_permissions(apiuser, repoid, repo, _perms)
201 201
202 202 status = Optional.extract(status)
203 203 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
204 204 data = [pr.get_api_data() for pr in pull_requests]
205 205 return data
206 206
207 207
208 208 @jsonrpc_method()
209 209 def merge_pull_request(request, apiuser, repoid, pullrequestid,
210 210 userid=Optional(OAttr('apiuser'))):
211 211 """
212 212 Merge the pull request specified by `pullrequestid` into its target
213 213 repository.
214 214
215 215 :param apiuser: This is filled automatically from the |authtoken|.
216 216 :type apiuser: AuthUser
217 217 :param repoid: The Repository name or repository ID of the
218 218 target repository to which the |pr| is to be merged.
219 219 :type repoid: str or int
220 220 :param pullrequestid: ID of the pull request which shall be merged.
221 221 :type pullrequestid: int
222 222 :param userid: Merge the pull request as this user.
223 223 :type userid: Optional(str or int)
224 224
225 225 Example output:
226 226
227 227 .. code-block:: bash
228 228
229 229 "id": <id_given_in_input>,
230 230 "result":
231 231 {
232 232 "executed": "<bool>",
233 233 "failure_reason": "<int>",
234 234 "merge_commit_id": "<merge_commit_id>",
235 235 "possible": "<bool>"
236 236 },
237 237 "error": null
238 238
239 239 """
240 240 repo = get_repo_or_error(repoid)
241 241 if not isinstance(userid, Optional):
242 242 if (has_superadmin_permission(apiuser) or
243 243 HasRepoPermissionAnyApi('repository.admin')(
244 244 user=apiuser, repo_name=repo.repo_name)):
245 245 apiuser = get_user_or_error(userid)
246 246 else:
247 247 raise JSONRPCError('userid is not the same as your user')
248 248
249 249 pull_request = get_pull_request_or_error(pullrequestid)
250 250 if not PullRequestModel().check_user_merge(
251 251 pull_request, apiuser, api=True):
252 252 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
253 253 if pull_request.is_closed():
254 254 raise JSONRPCError(
255 255 'pull request `%s` merge failed, pull request is closed' % (
256 256 pullrequestid,))
257 257
258 258 target_repo = pull_request.target_repo
259 259 extras = vcs_operation_context(
260 260 request.environ, repo_name=target_repo.repo_name,
261 261 username=apiuser.username, action='push',
262 262 scm=target_repo.repo_type)
263 263 data = PullRequestModel().merge(pull_request, apiuser, extras=extras)
264 264 if data.executed:
265 265 PullRequestModel().close_pull_request(
266 266 pull_request.pull_request_id, apiuser)
267 267
268 Session.commit()
268 Session().commit()
269 269 return data
270 270
271 271
272 272 @jsonrpc_method()
273 273 def close_pull_request(request, apiuser, repoid, pullrequestid,
274 274 userid=Optional(OAttr('apiuser'))):
275 275 """
276 276 Close the pull request specified by `pullrequestid`.
277 277
278 278 :param apiuser: This is filled automatically from the |authtoken|.
279 279 :type apiuser: AuthUser
280 280 :param repoid: Repository name or repository ID to which the pull
281 281 request belongs.
282 282 :type repoid: str or int
283 283 :param pullrequestid: ID of the pull request to be closed.
284 284 :type pullrequestid: int
285 285 :param userid: Close the pull request as this user.
286 286 :type userid: Optional(str or int)
287 287
288 288 Example output:
289 289
290 290 .. code-block:: bash
291 291
292 292 "id": <id_given_in_input>,
293 293 "result":
294 294 {
295 295 "pull_request_id": "<int>",
296 296 "closed": "<bool>"
297 297 },
298 298 "error": null
299 299
300 300 """
301 301 repo = get_repo_or_error(repoid)
302 302 if not isinstance(userid, Optional):
303 303 if (has_superadmin_permission(apiuser) or
304 304 HasRepoPermissionAnyApi('repository.admin')(
305 305 user=apiuser, repo_name=repo.repo_name)):
306 306 apiuser = get_user_or_error(userid)
307 307 else:
308 308 raise JSONRPCError('userid is not the same as your user')
309 309
310 310 pull_request = get_pull_request_or_error(pullrequestid)
311 311 if not PullRequestModel().check_user_update(
312 312 pull_request, apiuser, api=True):
313 313 raise JSONRPCError(
314 314 'pull request `%s` close failed, no permission to close.' % (
315 315 pullrequestid,))
316 316 if pull_request.is_closed():
317 317 raise JSONRPCError(
318 318 'pull request `%s` is already closed' % (pullrequestid,))
319 319
320 320 PullRequestModel().close_pull_request(
321 321 pull_request.pull_request_id, apiuser)
322 Session.commit()
322 Session().commit()
323 323 data = {
324 324 'pull_request_id': pull_request.pull_request_id,
325 325 'closed': True,
326 326 }
327 327 return data
328 328
329 329
330 330 @jsonrpc_method()
331 331 def comment_pull_request(request, apiuser, repoid, pullrequestid,
332 332 message=Optional(None), status=Optional(None),
333 333 userid=Optional(OAttr('apiuser'))):
334 334 """
335 335 Comment on the pull request specified with the `pullrequestid`,
336 336 in the |repo| specified by the `repoid`, and optionally change the
337 337 review status.
338 338
339 339 :param apiuser: This is filled automatically from the |authtoken|.
340 340 :type apiuser: AuthUser
341 341 :param repoid: The repository name or repository ID.
342 342 :type repoid: str or int
343 343 :param pullrequestid: The pull request ID.
344 344 :type pullrequestid: int
345 345 :param message: The text content of the comment.
346 346 :type message: str
347 347 :param status: (**Optional**) Set the approval status of the pull
348 348 request. Valid options are:
349 349 * not_reviewed
350 350 * approved
351 351 * rejected
352 352 * under_review
353 353 :type status: str
354 354 :param userid: Comment on the pull request as this user
355 355 :type userid: Optional(str or int)
356 356
357 357 Example output:
358 358
359 359 .. code-block:: bash
360 360
361 361 id : <id_given_in_input>
362 362 result :
363 363 {
364 364 "pull_request_id": "<Integer>",
365 365 "comment_id": "<Integer>"
366 366 }
367 367 error : null
368 368 """
369 369 repo = get_repo_or_error(repoid)
370 370 if not isinstance(userid, Optional):
371 371 if (has_superadmin_permission(apiuser) or
372 372 HasRepoPermissionAnyApi('repository.admin')(
373 373 user=apiuser, repo_name=repo.repo_name)):
374 374 apiuser = get_user_or_error(userid)
375 375 else:
376 376 raise JSONRPCError('userid is not the same as your user')
377 377
378 378 pull_request = get_pull_request_or_error(pullrequestid)
379 379 if not PullRequestModel().check_user_read(
380 380 pull_request, apiuser, api=True):
381 381 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
382 382 message = Optional.extract(message)
383 383 status = Optional.extract(status)
384 384 if not message and not status:
385 385 raise JSONRPCError('message and status parameter missing')
386 386
387 387 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
388 388 status is not None):
389 389 raise JSONRPCError('unknown comment status`%s`' % status)
390 390
391 391 allowed_to_change_status = PullRequestModel().check_user_change_status(
392 392 pull_request, apiuser)
393 393 text = message
394 394 if status and allowed_to_change_status:
395 395 st_message = (('Status change %(transition_icon)s %(status)s')
396 396 % {'transition_icon': '>',
397 397 'status': ChangesetStatus.get_status_lbl(status)})
398 398 text = message or st_message
399 399
400 400 rc_config = SettingsModel().get_all_settings()
401 401 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
402 402 comment = ChangesetCommentsModel().create(
403 403 text=text,
404 404 repo=pull_request.target_repo.repo_id,
405 405 user=apiuser.user_id,
406 406 pull_request=pull_request.pull_request_id,
407 407 f_path=None,
408 408 line_no=None,
409 409 status_change=(ChangesetStatus.get_status_lbl(status)
410 410 if status and allowed_to_change_status else None),
411 411 closing_pr=False,
412 412 renderer=renderer
413 413 )
414 414
415 415 if allowed_to_change_status and status:
416 416 ChangesetStatusModel().set_status(
417 417 pull_request.target_repo.repo_id,
418 418 status,
419 419 apiuser.user_id,
420 420 comment,
421 421 pull_request=pull_request.pull_request_id
422 422 )
423 423 Session().flush()
424 424
425 425 Session().commit()
426 426 data = {
427 427 'pull_request_id': pull_request.pull_request_id,
428 428 'comment_id': comment.comment_id,
429 429 'status': status
430 430 }
431 431 return data
432 432
433 433
434 434 @jsonrpc_method()
435 435 def create_pull_request(
436 436 request, apiuser, source_repo, target_repo, source_ref, target_ref,
437 437 title, description=Optional(''), reviewers=Optional(None)):
438 438 """
439 439 Creates a new pull request.
440 440
441 441 Accepts refs in the following formats:
442 442
443 443 * branch:<branch_name>:<sha>
444 444 * branch:<branch_name>
445 445 * bookmark:<bookmark_name>:<sha> (Mercurial only)
446 446 * bookmark:<bookmark_name> (Mercurial only)
447 447
448 448 :param apiuser: This is filled automatically from the |authtoken|.
449 449 :type apiuser: AuthUser
450 450 :param source_repo: Set the source repository name.
451 451 :type source_repo: str
452 452 :param target_repo: Set the target repository name.
453 453 :type target_repo: str
454 454 :param source_ref: Set the source ref name.
455 455 :type source_ref: str
456 456 :param target_ref: Set the target ref name.
457 457 :type target_ref: str
458 458 :param title: Set the pull request title.
459 459 :type title: str
460 460 :param description: Set the pull request description.
461 461 :type description: Optional(str)
462 462 :param reviewers: Set the new pull request reviewers list.
463 463 :type reviewers: Optional(list)
464 464 """
465 465 source = get_repo_or_error(source_repo)
466 466 target = get_repo_or_error(target_repo)
467 467 if not has_superadmin_permission(apiuser):
468 468 _perms = ('repository.admin', 'repository.write', 'repository.read',)
469 469 has_repo_permissions(apiuser, source_repo, source, _perms)
470 470
471 471 full_source_ref = resolve_ref_or_error(source_ref, source)
472 472 full_target_ref = resolve_ref_or_error(target_ref, target)
473 473 source_commit = get_commit_or_error(full_source_ref, source)
474 474 target_commit = get_commit_or_error(full_target_ref, target)
475 475 source_scm = source.scm_instance()
476 476 target_scm = target.scm_instance()
477 477
478 478 commit_ranges = target_scm.compare(
479 479 target_commit.raw_id, source_commit.raw_id, source_scm,
480 480 merge=True, pre_load=[])
481 481
482 482 ancestor = target_scm.get_common_ancestor(
483 483 target_commit.raw_id, source_commit.raw_id, source_scm)
484 484
485 485 if not commit_ranges:
486 486 raise JSONRPCError('no commits found')
487 487
488 488 if not ancestor:
489 489 raise JSONRPCError('no common ancestor found')
490 490
491 491 reviewer_names = Optional.extract(reviewers) or []
492 492 if not isinstance(reviewer_names, list):
493 493 raise JSONRPCError('reviewers should be specified as a list')
494 494
495 495 reviewer_users = [get_user_or_error(n) for n in reviewer_names]
496 496 reviewer_ids = [u.user_id for u in reviewer_users]
497 497
498 498 pull_request_model = PullRequestModel()
499 499 pull_request = pull_request_model.create(
500 500 created_by=apiuser.user_id,
501 501 source_repo=source_repo,
502 502 source_ref=full_source_ref,
503 503 target_repo=target_repo,
504 504 target_ref=full_target_ref,
505 505 revisions=reversed(
506 506 [commit.raw_id for commit in reversed(commit_ranges)]),
507 507 reviewers=reviewer_ids,
508 508 title=title,
509 509 description=Optional.extract(description)
510 510 )
511 511
512 512 Session().commit()
513 513 data = {
514 514 'msg': 'Created new pull request `{}`'.format(title),
515 515 'pull_request_id': pull_request.pull_request_id,
516 516 }
517 517 return data
518 518
519 519
520 520 @jsonrpc_method()
521 521 def update_pull_request(
522 522 request, apiuser, repoid, pullrequestid, title=Optional(''),
523 523 description=Optional(''), reviewers=Optional(None),
524 524 update_commits=Optional(None), close_pull_request=Optional(None)):
525 525 """
526 526 Updates a pull request.
527 527
528 528 :param apiuser: This is filled automatically from the |authtoken|.
529 529 :type apiuser: AuthUser
530 530 :param repoid: The repository name or repository ID.
531 531 :type repoid: str or int
532 532 :param pullrequestid: The pull request ID.
533 533 :type pullrequestid: int
534 534 :param title: Set the pull request title.
535 535 :type title: str
536 536 :param description: Update pull request description.
537 537 :type description: Optional(str)
538 538 :param reviewers: Update pull request reviewers list with new value.
539 539 :type reviewers: Optional(list)
540 540 :param update_commits: Trigger update of commits for this pull request
541 541 :type: update_commits: Optional(bool)
542 542 :param close_pull_request: Close this pull request with rejected state
543 543 :type: close_pull_request: Optional(bool)
544 544
545 545 Example output:
546 546
547 547 .. code-block:: bash
548 548
549 549 id : <id_given_in_input>
550 550 result :
551 551 {
552 552 "msg": "Updated pull request `63`",
553 553 "pull_request": <pull_request_object>,
554 554 "updated_reviewers": {
555 555 "added": [
556 556 "username"
557 557 ],
558 558 "removed": []
559 559 },
560 560 "updated_commits": {
561 561 "added": [
562 562 "<sha1_hash>"
563 563 ],
564 564 "common": [
565 565 "<sha1_hash>",
566 566 "<sha1_hash>",
567 567 ],
568 568 "removed": []
569 569 }
570 570 }
571 571 error : null
572 572 """
573 573
574 574 repo = get_repo_or_error(repoid)
575 575 pull_request = get_pull_request_or_error(pullrequestid)
576 576 if not PullRequestModel().check_user_update(
577 577 pull_request, apiuser, api=True):
578 578 raise JSONRPCError(
579 579 'pull request `%s` update failed, no permission to update.' % (
580 580 pullrequestid,))
581 581 if pull_request.is_closed():
582 582 raise JSONRPCError(
583 583 'pull request `%s` update failed, pull request is closed' % (
584 584 pullrequestid,))
585 585
586 586 reviewer_names = Optional.extract(reviewers) or []
587 587 if not isinstance(reviewer_names, list):
588 588 raise JSONRPCError('reviewers should be specified as a list')
589 589
590 590 reviewer_users = [get_user_or_error(n) for n in reviewer_names]
591 591 reviewer_ids = [u.user_id for u in reviewer_users]
592 592
593 593 title = Optional.extract(title)
594 594 description = Optional.extract(description)
595 595 if title or description:
596 596 PullRequestModel().edit(
597 597 pull_request, title or pull_request.title,
598 598 description or pull_request.description)
599 599 Session().commit()
600 600
601 601 commit_changes = {"added": [], "common": [], "removed": []}
602 602 if str2bool(Optional.extract(update_commits)):
603 603 if PullRequestModel().has_valid_update_type(pull_request):
604 604 _version, _commit_changes = PullRequestModel().update_commits(
605 605 pull_request)
606 606 commit_changes = _commit_changes or commit_changes
607 607 Session().commit()
608 608
609 609 reviewers_changes = {"added": [], "removed": []}
610 610 if reviewer_ids:
611 611 added_reviewers, removed_reviewers = \
612 612 PullRequestModel().update_reviewers(pull_request, reviewer_ids)
613 613
614 614 reviewers_changes['added'] = sorted(
615 615 [get_user_or_error(n).username for n in added_reviewers])
616 616 reviewers_changes['removed'] = sorted(
617 617 [get_user_or_error(n).username for n in removed_reviewers])
618 618 Session().commit()
619 619
620 620 if str2bool(Optional.extract(close_pull_request)):
621 621 PullRequestModel().close_pull_request_with_comment(
622 622 pull_request, apiuser, repo)
623 623 Session().commit()
624 624
625 625 data = {
626 626 'msg': 'Updated pull request `{}`'.format(
627 627 pull_request.pull_request_id),
628 628 'pull_request': pull_request.get_api_data(),
629 629 'updated_commits': commit_changes,
630 630 'updated_reviewers': reviewers_changes
631 631 }
632 632 return data
633 633
@@ -1,192 +1,192 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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 colander
22 22 import formencode.htmlfill
23 23 import logging
24 24
25 25 from pyramid.httpexceptions import HTTPFound
26 26 from pyramid.renderers import render
27 27 from pyramid.response import Response
28 28
29 29 from rhodecode.authentication.base import (
30 30 get_auth_cache_manager, get_authn_registry)
31 31 from rhodecode.lib import auth
32 32 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
33 33 from rhodecode.model.forms import AuthSettingsForm
34 34 from rhodecode.model.meta import Session
35 35 from rhodecode.model.settings import SettingsModel
36 36 from rhodecode.translation import _
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class AuthnPluginViewBase(object):
42 42
43 43 def __init__(self, context, request):
44 44 self.request = request
45 45 self.context = context
46 46 self.plugin = context.plugin
47 47 self._rhodecode_user = request.user
48 48
49 49 @LoginRequired()
50 50 @HasPermissionAllDecorator('hg.admin')
51 51 def settings_get(self, defaults=None, errors=None):
52 52 """
53 53 View that displays the plugin settings as a form.
54 54 """
55 55 defaults = defaults or {}
56 56 errors = errors or {}
57 57 schema = self.plugin.get_settings_schema()
58 58
59 59 # Compute default values for the form. Priority is:
60 60 # 1. Passed to this method 2. DB value 3. Schema default
61 61 for node in schema:
62 62 if node.name not in defaults:
63 63 defaults[node.name] = self.plugin.get_setting_by_name(
64 64 node.name, node.default)
65 65
66 66 template_context = {
67 67 'defaults': defaults,
68 68 'errors': errors,
69 69 'plugin': self.context.plugin,
70 70 'resource': self.context,
71 71 }
72 72
73 73 return template_context
74 74
75 75 @LoginRequired()
76 76 @HasPermissionAllDecorator('hg.admin')
77 77 @auth.CSRFRequired()
78 78 def settings_post(self):
79 79 """
80 80 View that validates and stores the plugin settings.
81 81 """
82 82 schema = self.plugin.get_settings_schema()
83 83 data = self.request.params
84 84
85 85 try:
86 86 valid_data = schema.deserialize(data)
87 87 except colander.Invalid, e:
88 88 # Display error message and display form again.
89 89 self.request.session.flash(
90 90 _('Errors exist when saving plugin settings. '
91 91 'Please check the form inputs.'),
92 92 queue='error')
93 93 defaults = {key: data[key] for key in data if key in schema}
94 94 return self.settings_get(errors=e.asdict(), defaults=defaults)
95 95
96 96 # Store validated data.
97 97 for name, value in valid_data.items():
98 98 self.plugin.create_or_update_setting(name, value)
99 Session.commit()
99 Session().commit()
100 100
101 101 # Display success message and redirect.
102 102 self.request.session.flash(
103 103 _('Auth settings updated successfully.'),
104 104 queue='success')
105 105 redirect_to = self.request.resource_path(
106 106 self.context, route_name='auth_home')
107 107 return HTTPFound(redirect_to)
108 108
109 109
110 110 # TODO: Ongoing migration in these views.
111 111 # - Maybe we should also use a colander schema for these views.
112 112 class AuthSettingsView(object):
113 113 def __init__(self, context, request):
114 114 self.context = context
115 115 self.request = request
116 116
117 117 # TODO: Move this into a utility function. It is needed in all view
118 118 # classes during migration. Maybe a mixin?
119 119
120 120 # Some of the decorators rely on this attribute to be present on the
121 121 # class of the decorated method.
122 122 self._rhodecode_user = request.user
123 123
124 124 @LoginRequired()
125 125 @HasPermissionAllDecorator('hg.admin')
126 126 def index(self, defaults=None, errors=None, prefix_error=False):
127 127 defaults = defaults or {}
128 128 authn_registry = get_authn_registry(self.request.registry)
129 129 enabled_plugins = SettingsModel().get_auth_plugins()
130 130
131 131 # Create template context and render it.
132 132 template_context = {
133 133 'resource': self.context,
134 134 'available_plugins': authn_registry.get_plugins(),
135 135 'enabled_plugins': enabled_plugins,
136 136 }
137 137 html = render('rhodecode:templates/admin/auth/auth_settings.html',
138 138 template_context,
139 139 request=self.request)
140 140
141 141 # Create form default values and fill the form.
142 142 form_defaults = {
143 143 'auth_plugins': ','.join(enabled_plugins)
144 144 }
145 145 form_defaults.update(defaults)
146 146 html = formencode.htmlfill.render(
147 147 html,
148 148 defaults=form_defaults,
149 149 errors=errors,
150 150 prefix_error=prefix_error,
151 151 encoding="UTF-8",
152 152 force_defaults=False)
153 153
154 154 return Response(html)
155 155
156 156 @LoginRequired()
157 157 @HasPermissionAllDecorator('hg.admin')
158 158 @auth.CSRFRequired()
159 159 def auth_settings(self):
160 160 try:
161 161 form = AuthSettingsForm()()
162 162 form_result = form.to_python(self.request.params)
163 163 plugins = ','.join(form_result['auth_plugins'])
164 164 setting = SettingsModel().create_or_update_setting(
165 165 'auth_plugins', plugins)
166 166 Session().add(setting)
167 167 Session().commit()
168 168
169 169 cache_manager = get_auth_cache_manager()
170 170 cache_manager.clear()
171 171 self.request.session.flash(
172 172 _('Auth settings updated successfully.'),
173 173 queue='success')
174 174 except formencode.Invalid as errors:
175 175 e = errors.error_dict or {}
176 176 self.request.session.flash(
177 177 _('Errors exist when saving plugin setting. '
178 178 'Please check the form inputs.'),
179 179 queue='error')
180 180 return self.index(
181 181 defaults=errors.value,
182 182 errors=e,
183 183 prefix_error=False)
184 184 except Exception:
185 185 log.exception('Exception in auth_settings')
186 186 self.request.session.flash(
187 187 _('Error occurred during update of auth settings.'),
188 188 queue='error')
189 189
190 190 redirect_to = self.request.resource_path(
191 191 self.context, route_name='auth_home')
192 192 return HTTPFound(redirect_to)
@@ -1,306 +1,306 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 Journal / user event log controller for rhodecode
23 23 """
24 24
25 25 import logging
26 26 from itertools import groupby
27 27
28 28 from sqlalchemy import or_
29 29 from sqlalchemy.orm import joinedload
30 30
31 31 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
32 32
33 33 from webob.exc import HTTPBadRequest
34 34 from pylons import request, tmpl_context as c, response, url
35 35 from pylons.i18n.translation import _
36 36
37 37 from rhodecode.controllers.admin.admin import _journal_filter
38 38 from rhodecode.model.db import UserLog, UserFollowing, User
39 39 from rhodecode.model.meta import Session
40 40 import rhodecode.lib.helpers as h
41 41 from rhodecode.lib.helpers import Page
42 42 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
43 43 from rhodecode.lib.base import BaseController, render
44 44 from rhodecode.lib.utils2 import safe_int, AttributeDict
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class JournalController(BaseController):
50 50
51 51 def __before__(self):
52 52 super(JournalController, self).__before__()
53 53 self.language = 'en-us'
54 54 self.ttl = "5"
55 55 self.feed_nr = 20
56 56 c.search_term = request.GET.get('filter')
57 57
58 58 def _get_daily_aggregate(self, journal):
59 59 groups = []
60 60 for k, g in groupby(journal, lambda x: x.action_as_day):
61 61 user_group = []
62 62 #groupby username if it's a present value, else fallback to journal username
63 63 for _, g2 in groupby(list(g), lambda x: x.user.username if x.user else x.username):
64 64 l = list(g2)
65 65 user_group.append((l[0].user, l))
66 66
67 67 groups.append((k, user_group,))
68 68
69 69 return groups
70 70
71 71 def _get_journal_data(self, following_repos):
72 72 repo_ids = [x.follows_repository.repo_id for x in following_repos
73 73 if x.follows_repository is not None]
74 74 user_ids = [x.follows_user.user_id for x in following_repos
75 75 if x.follows_user is not None]
76 76
77 77 filtering_criterion = None
78 78
79 79 if repo_ids and user_ids:
80 80 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
81 81 UserLog.user_id.in_(user_ids))
82 82 if repo_ids and not user_ids:
83 83 filtering_criterion = UserLog.repository_id.in_(repo_ids)
84 84 if not repo_ids and user_ids:
85 85 filtering_criterion = UserLog.user_id.in_(user_ids)
86 86 if filtering_criterion is not None:
87 87 journal = self.sa.query(UserLog)\
88 88 .options(joinedload(UserLog.user))\
89 89 .options(joinedload(UserLog.repository))
90 90 #filter
91 91 try:
92 92 journal = _journal_filter(journal, c.search_term)
93 93 except Exception:
94 94 # we want this to crash for now
95 95 raise
96 96 journal = journal.filter(filtering_criterion)\
97 97 .order_by(UserLog.action_date.desc())
98 98 else:
99 99 journal = []
100 100
101 101 return journal
102 102
103 103 def _atom_feed(self, repos, public=True):
104 104 journal = self._get_journal_data(repos)
105 105 if public:
106 106 _link = url('public_journal_atom', qualified=True)
107 107 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
108 108 'atom feed')
109 109 else:
110 110 _link = url('journal_atom', qualified=True)
111 111 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'atom feed')
112 112
113 113 feed = Atom1Feed(title=_desc,
114 114 link=_link,
115 115 description=_desc,
116 116 language=self.language,
117 117 ttl=self.ttl)
118 118
119 119 for entry in journal[:self.feed_nr]:
120 120 user = entry.user
121 121 if user is None:
122 122 #fix deleted users
123 123 user = AttributeDict({'short_contact': entry.username,
124 124 'email': '',
125 125 'full_contact': ''})
126 126 action, action_extra, ico = h.action_parser(entry, feed=True)
127 127 title = "%s - %s %s" % (user.short_contact, action(),
128 128 entry.repository.repo_name)
129 129 desc = action_extra()
130 130 _url = None
131 131 if entry.repository is not None:
132 132 _url = url('changelog_home',
133 133 repo_name=entry.repository.repo_name,
134 134 qualified=True)
135 135
136 136 feed.add_item(title=title,
137 137 pubdate=entry.action_date,
138 138 link=_url or url('', qualified=True),
139 139 author_email=user.email,
140 140 author_name=user.full_contact,
141 141 description=desc)
142 142
143 143 response.content_type = feed.mime_type
144 144 return feed.writeString('utf-8')
145 145
146 146 def _rss_feed(self, repos, public=True):
147 147 journal = self._get_journal_data(repos)
148 148 if public:
149 149 _link = url('public_journal_atom', qualified=True)
150 150 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
151 151 'rss feed')
152 152 else:
153 153 _link = url('journal_atom', qualified=True)
154 154 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'rss feed')
155 155
156 156 feed = Rss201rev2Feed(title=_desc,
157 157 link=_link,
158 158 description=_desc,
159 159 language=self.language,
160 160 ttl=self.ttl)
161 161
162 162 for entry in journal[:self.feed_nr]:
163 163 user = entry.user
164 164 if user is None:
165 165 #fix deleted users
166 166 user = AttributeDict({'short_contact': entry.username,
167 167 'email': '',
168 168 'full_contact': ''})
169 169 action, action_extra, ico = h.action_parser(entry, feed=True)
170 170 title = "%s - %s %s" % (user.short_contact, action(),
171 171 entry.repository.repo_name)
172 172 desc = action_extra()
173 173 _url = None
174 174 if entry.repository is not None:
175 175 _url = url('changelog_home',
176 176 repo_name=entry.repository.repo_name,
177 177 qualified=True)
178 178
179 179 feed.add_item(title=title,
180 180 pubdate=entry.action_date,
181 181 link=_url or url('', qualified=True),
182 182 author_email=user.email,
183 183 author_name=user.full_contact,
184 184 description=desc)
185 185
186 186 response.content_type = feed.mime_type
187 187 return feed.writeString('utf-8')
188 188
189 189 @LoginRequired()
190 190 @NotAnonymous()
191 191 def index(self):
192 192 # Return a rendered template
193 193 p = safe_int(request.GET.get('page', 1), 1)
194 194 c.user = User.get(c.rhodecode_user.user_id)
195 195 following = self.sa.query(UserFollowing)\
196 196 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
197 197 .options(joinedload(UserFollowing.follows_repository))\
198 198 .all()
199 199
200 200 journal = self._get_journal_data(following)
201 201
202 202 def url_generator(**kw):
203 203 return url.current(filter=c.search_term, **kw)
204 204
205 205 c.journal_pager = Page(journal, page=p, items_per_page=20, url=url_generator)
206 206 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
207 207
208 208 c.journal_data = render('journal/journal_data.html')
209 209 if request.is_xhr:
210 210 return c.journal_data
211 211
212 212 return render('journal/journal.html')
213 213
214 214 @LoginRequired(auth_token_access=True)
215 215 @NotAnonymous()
216 216 def journal_atom(self):
217 217 """
218 218 Produce an atom-1.0 feed via feedgenerator module
219 219 """
220 220 following = self.sa.query(UserFollowing)\
221 221 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
222 222 .options(joinedload(UserFollowing.follows_repository))\
223 223 .all()
224 224 return self._atom_feed(following, public=False)
225 225
226 226 @LoginRequired(auth_token_access=True)
227 227 @NotAnonymous()
228 228 def journal_rss(self):
229 229 """
230 230 Produce an rss feed via feedgenerator module
231 231 """
232 232 following = self.sa.query(UserFollowing)\
233 233 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
234 234 .options(joinedload(UserFollowing.follows_repository))\
235 235 .all()
236 236 return self._rss_feed(following, public=False)
237 237
238 238 @CSRFRequired()
239 239 @LoginRequired()
240 240 @NotAnonymous()
241 241 def toggle_following(self):
242 242 user_id = request.POST.get('follows_user_id')
243 243 if user_id:
244 244 try:
245 245 self.scm_model.toggle_following_user(
246 246 user_id, c.rhodecode_user.user_id)
247 Session.commit()
247 Session().commit()
248 248 return 'ok'
249 249 except Exception:
250 250 raise HTTPBadRequest()
251 251
252 252 repo_id = request.POST.get('follows_repo_id')
253 253 if repo_id:
254 254 try:
255 255 self.scm_model.toggle_following_repo(
256 256 repo_id, c.rhodecode_user.user_id)
257 Session.commit()
257 Session().commit()
258 258 return 'ok'
259 259 except Exception:
260 260 raise HTTPBadRequest()
261 261
262 262
263 263 @LoginRequired()
264 264 def public_journal(self):
265 265 # Return a rendered template
266 266 p = safe_int(request.GET.get('page', 1), 1)
267 267
268 268 c.following = self.sa.query(UserFollowing)\
269 269 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
270 270 .options(joinedload(UserFollowing.follows_repository))\
271 271 .all()
272 272
273 273 journal = self._get_journal_data(c.following)
274 274
275 275 c.journal_pager = Page(journal, page=p, items_per_page=20)
276 276
277 277 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
278 278
279 279 c.journal_data = render('journal/journal_data.html')
280 280 if request.is_xhr:
281 281 return c.journal_data
282 282 return render('journal/public_journal.html')
283 283
284 284 @LoginRequired(auth_token_access=True)
285 285 def public_journal_atom(self):
286 286 """
287 287 Produce an atom-1.0 feed via feedgenerator module
288 288 """
289 289 c.following = self.sa.query(UserFollowing)\
290 290 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
291 291 .options(joinedload(UserFollowing.follows_repository))\
292 292 .all()
293 293
294 294 return self._atom_feed(c.following)
295 295
296 296 @LoginRequired(auth_token_access=True)
297 297 def public_journal_rss(self):
298 298 """
299 299 Produce an rss2 feed via feedgenerator module
300 300 """
301 301 c.following = self.sa.query(UserFollowing)\
302 302 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
303 303 .options(joinedload(UserFollowing.follows_repository))\
304 304 .all()
305 305
306 306 return self._rss_feed(c.following)
@@ -1,698 +1,698 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 hashlib
22 22 import logging
23 23 from collections import namedtuple
24 24 from functools import wraps
25 25
26 26 from rhodecode.lib import caches
27 27 from rhodecode.lib.caching_query import FromCache
28 28 from rhodecode.lib.utils2 import (
29 29 Optional, AttributeDict, safe_str, remove_prefix, str2bool)
30 30 from rhodecode.model import BaseModel
31 31 from rhodecode.model.db import (
32 32 RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi, RhodeCodeSetting)
33 33 from rhodecode.model.meta import Session
34 34
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 UiSetting = namedtuple(
40 40 'UiSetting', ['section', 'key', 'value', 'active'])
41 41
42 42 SOCIAL_PLUGINS_LIST = ['github', 'bitbucket', 'twitter', 'google']
43 43
44 44
45 45 class SettingNotFound(Exception):
46 46 def __init__(self):
47 47 super(SettingNotFound, self).__init__('Setting is not found')
48 48
49 49
50 50 class SettingsModel(BaseModel):
51 51 BUILTIN_HOOKS = (
52 52 RhodeCodeUi.HOOK_REPO_SIZE, RhodeCodeUi.HOOK_PUSH,
53 53 RhodeCodeUi.HOOK_PRE_PUSH, RhodeCodeUi.HOOK_PULL,
54 54 RhodeCodeUi.HOOK_PRE_PULL)
55 55 HOOKS_SECTION = 'hooks'
56 56
57 57 def __init__(self, sa=None, repo=None):
58 58 self.repo = repo
59 59 self.UiDbModel = RepoRhodeCodeUi if repo else RhodeCodeUi
60 60 self.SettingsDbModel = (
61 61 RepoRhodeCodeSetting if repo else RhodeCodeSetting)
62 62 super(SettingsModel, self).__init__(sa)
63 63
64 64 def get_ui_by_key(self, key):
65 65 q = self.UiDbModel.query()
66 66 q = q.filter(self.UiDbModel.ui_key == key)
67 67 q = self._filter_by_repo(RepoRhodeCodeUi, q)
68 68 return q.scalar()
69 69
70 70 def get_ui_by_section(self, section):
71 71 q = self.UiDbModel.query()
72 72 q = q.filter(self.UiDbModel.ui_section == section)
73 73 q = self._filter_by_repo(RepoRhodeCodeUi, q)
74 74 return q.all()
75 75
76 76 def get_ui_by_section_and_key(self, section, key):
77 77 q = self.UiDbModel.query()
78 78 q = q.filter(self.UiDbModel.ui_section == section)
79 79 q = q.filter(self.UiDbModel.ui_key == key)
80 80 q = self._filter_by_repo(RepoRhodeCodeUi, q)
81 81 return q.scalar()
82 82
83 83 def get_ui(self, section=None, key=None):
84 84 q = self.UiDbModel.query()
85 85 q = self._filter_by_repo(RepoRhodeCodeUi, q)
86 86
87 87 if section:
88 88 q = q.filter(self.UiDbModel.ui_section == section)
89 89 if key:
90 90 q = q.filter(self.UiDbModel.ui_key == key)
91 91
92 92 # TODO: mikhail: add caching
93 93 result = [
94 94 UiSetting(
95 95 section=safe_str(r.ui_section), key=safe_str(r.ui_key),
96 96 value=safe_str(r.ui_value), active=r.ui_active
97 97 )
98 98 for r in q.all()
99 99 ]
100 100 return result
101 101
102 102 def get_builtin_hooks(self):
103 103 q = self.UiDbModel.query()
104 104 q = q.filter(self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
105 105 return self._get_hooks(q)
106 106
107 107 def get_custom_hooks(self):
108 108 q = self.UiDbModel.query()
109 109 q = q.filter(~self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
110 110 return self._get_hooks(q)
111 111
112 112 def create_ui_section_value(self, section, val, key=None, active=True):
113 113 new_ui = self.UiDbModel()
114 114 new_ui.ui_section = section
115 115 new_ui.ui_value = val
116 116 new_ui.ui_active = active
117 117
118 118 if self.repo:
119 119 repo = self._get_repo(self.repo)
120 120 repository_id = repo.repo_id
121 121 new_ui.repository_id = repository_id
122 122
123 123 if not key:
124 124 # keys are unique so they need appended info
125 125 if self.repo:
126 126 key = hashlib.sha1(
127 127 '{}{}{}'.format(section, val, repository_id)).hexdigest()
128 128 else:
129 129 key = hashlib.sha1('{}{}'.format(section, val)).hexdigest()
130 130
131 131 new_ui.ui_key = key
132 132
133 133 Session().add(new_ui)
134 134 return new_ui
135 135
136 136 def create_or_update_hook(self, key, value):
137 137 ui = (
138 138 self.get_ui_by_section_and_key(self.HOOKS_SECTION, key) or
139 139 self.UiDbModel())
140 140 ui.ui_section = self.HOOKS_SECTION
141 141 ui.ui_active = True
142 142 ui.ui_key = key
143 143 ui.ui_value = value
144 144
145 145 if self.repo:
146 146 repo = self._get_repo(self.repo)
147 147 repository_id = repo.repo_id
148 148 ui.repository_id = repository_id
149 149
150 150 Session().add(ui)
151 151 return ui
152 152
153 153 def delete_ui(self, id_):
154 154 ui = self.UiDbModel.get(id_)
155 155 if not ui:
156 156 raise SettingNotFound()
157 157 Session().delete(ui)
158 158
159 159 def get_setting_by_name(self, name):
160 160 q = self._get_settings_query()
161 161 q = q.filter(self.SettingsDbModel.app_settings_name == name)
162 162 return q.scalar()
163 163
164 164 def create_or_update_setting(
165 165 self, name, val=Optional(''), type_=Optional('unicode')):
166 166 """
167 167 Creates or updates RhodeCode setting. If updates is triggered it will
168 168 only update parameters that are explicityl set Optional instance will
169 169 be skipped
170 170
171 171 :param name:
172 172 :param val:
173 173 :param type_:
174 174 :return:
175 175 """
176 176
177 177 res = self.get_setting_by_name(name)
178 178 repo = self._get_repo(self.repo) if self.repo else None
179 179
180 180 if not res:
181 181 val = Optional.extract(val)
182 182 type_ = Optional.extract(type_)
183 183
184 184 args = (
185 185 (repo.repo_id, name, val, type_)
186 186 if repo else (name, val, type_))
187 187 res = self.SettingsDbModel(*args)
188 188
189 189 else:
190 190 if self.repo:
191 191 res.repository_id = repo.repo_id
192 192
193 193 res.app_settings_name = name
194 194 if not isinstance(type_, Optional):
195 195 # update if set
196 196 res.app_settings_type = type_
197 197 if not isinstance(val, Optional):
198 198 # update if set
199 199 res.app_settings_value = val
200 200
201 Session.add(res)
201 Session().add(res)
202 202 return res
203 203
204 204 def invalidate_settings_cache(self):
205 205 namespace = 'rhodecode_settings'
206 206 cache_manager = caches.get_cache_manager('sql_cache_short', namespace)
207 207 caches.clear_cache_manager(cache_manager)
208 208
209 209 def get_all_settings(self, cache=False):
210 210 def _compute():
211 211 q = self._get_settings_query()
212 212 if not q:
213 213 raise Exception('Could not get application settings !')
214 214
215 215 settings = {
216 216 'rhodecode_' + result.app_settings_name: result.app_settings_value
217 217 for result in q
218 218 }
219 219 return settings
220 220
221 221 if cache:
222 222 log.debug('Fetching app settings using cache')
223 223 repo = self._get_repo(self.repo) if self.repo else None
224 224 namespace = 'rhodecode_settings'
225 225 cache_manager = caches.get_cache_manager(
226 226 'sql_cache_short', namespace)
227 227 _cache_key = (
228 228 "get_repo_{}_settings".format(repo.repo_id)
229 229 if repo else "get_app_settings")
230 230
231 231 return cache_manager.get(_cache_key, createfunc=_compute)
232 232
233 233 else:
234 234 return _compute()
235 235
236 236 def get_auth_settings(self):
237 237 q = self._get_settings_query()
238 238 q = q.filter(
239 239 self.SettingsDbModel.app_settings_name.startswith('auth_'))
240 240 rows = q.all()
241 241 auth_settings = {
242 242 row.app_settings_name: row.app_settings_value for row in rows}
243 243 return auth_settings
244 244
245 245 def get_auth_plugins(self):
246 246 auth_plugins = self.get_setting_by_name("auth_plugins")
247 247 return auth_plugins.app_settings_value
248 248
249 249 def get_default_repo_settings(self, strip_prefix=False):
250 250 q = self._get_settings_query()
251 251 q = q.filter(
252 252 self.SettingsDbModel.app_settings_name.startswith('default_'))
253 253 rows = q.all()
254 254
255 255 result = {}
256 256 for row in rows:
257 257 key = row.app_settings_name
258 258 if strip_prefix:
259 259 key = remove_prefix(key, prefix='default_')
260 260 result.update({key: row.app_settings_value})
261 261 return result
262 262
263 263 def get_repo(self):
264 264 repo = self._get_repo(self.repo)
265 265 if not repo:
266 266 raise Exception(
267 267 'Repository {} cannot be found'.format(self.repo))
268 268 return repo
269 269
270 270 def _filter_by_repo(self, model, query):
271 271 if self.repo:
272 272 repo = self.get_repo()
273 273 query = query.filter(model.repository_id == repo.repo_id)
274 274 return query
275 275
276 276 def _get_hooks(self, query):
277 277 query = query.filter(self.UiDbModel.ui_section == self.HOOKS_SECTION)
278 278 query = self._filter_by_repo(RepoRhodeCodeUi, query)
279 279 return query.all()
280 280
281 281 def _get_settings_query(self):
282 282 q = self.SettingsDbModel.query()
283 283 return self._filter_by_repo(RepoRhodeCodeSetting, q)
284 284
285 285 def list_enabled_social_plugins(self, settings):
286 286 enabled = []
287 287 for plug in SOCIAL_PLUGINS_LIST:
288 288 if str2bool(settings.get('rhodecode_auth_{}_enabled'.format(plug)
289 289 )):
290 290 enabled.append(plug)
291 291 return enabled
292 292
293 293
294 294 def assert_repo_settings(func):
295 295 @wraps(func)
296 296 def _wrapper(self, *args, **kwargs):
297 297 if not self.repo_settings:
298 298 raise Exception('Repository is not specified')
299 299 return func(self, *args, **kwargs)
300 300 return _wrapper
301 301
302 302
303 303 class IssueTrackerSettingsModel(object):
304 304 INHERIT_SETTINGS = 'inherit_issue_tracker_settings'
305 305 SETTINGS_PREFIX = 'issuetracker_'
306 306
307 307 def __init__(self, sa=None, repo=None):
308 308 self.global_settings = SettingsModel(sa=sa)
309 309 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
310 310
311 311 @property
312 312 def inherit_global_settings(self):
313 313 if not self.repo_settings:
314 314 return True
315 315 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
316 316 return setting.app_settings_value if setting else True
317 317
318 318 @inherit_global_settings.setter
319 319 def inherit_global_settings(self, value):
320 320 if self.repo_settings:
321 321 settings = self.repo_settings.create_or_update_setting(
322 322 self.INHERIT_SETTINGS, value, type_='bool')
323 323 Session().add(settings)
324 324
325 325 def _get_keyname(self, key, uid, prefix=''):
326 326 return '{0}{1}{2}_{3}'.format(
327 327 prefix, self.SETTINGS_PREFIX, key, uid)
328 328
329 329 def _make_dict_for_settings(self, qs):
330 330 prefix_match = self._get_keyname('pat', '', 'rhodecode_')
331 331
332 332 issuetracker_entries = {}
333 333 # create keys
334 334 for k, v in qs.items():
335 335 if k.startswith(prefix_match):
336 336 uid = k[len(prefix_match):]
337 337 issuetracker_entries[uid] = None
338 338
339 339 # populate
340 340 for uid in issuetracker_entries:
341 341 issuetracker_entries[uid] = AttributeDict({
342 342 'pat': qs.get(self._get_keyname('pat', uid, 'rhodecode_')),
343 343 'url': qs.get(self._get_keyname('url', uid, 'rhodecode_')),
344 344 'pref': qs.get(self._get_keyname('pref', uid, 'rhodecode_')),
345 345 'desc': qs.get(self._get_keyname('desc', uid, 'rhodecode_')),
346 346 })
347 347 return issuetracker_entries
348 348
349 349 def get_global_settings(self, cache=False):
350 350 """
351 351 Returns list of global issue tracker settings
352 352 """
353 353 defaults = self.global_settings.get_all_settings(cache=cache)
354 354 settings = self._make_dict_for_settings(defaults)
355 355 return settings
356 356
357 357 def get_repo_settings(self, cache=False):
358 358 """
359 359 Returns list of issue tracker settings per repository
360 360 """
361 361 if not self.repo_settings:
362 362 raise Exception('Repository is not specified')
363 363 all_settings = self.repo_settings.get_all_settings(cache=cache)
364 364 settings = self._make_dict_for_settings(all_settings)
365 365 return settings
366 366
367 367 def get_settings(self, cache=False):
368 368 if self.inherit_global_settings:
369 369 return self.get_global_settings(cache=cache)
370 370 else:
371 371 return self.get_repo_settings(cache=cache)
372 372
373 373 def delete_entries(self, uid):
374 374 if self.repo_settings:
375 375 all_patterns = self.get_repo_settings()
376 376 settings_model = self.repo_settings
377 377 else:
378 378 all_patterns = self.get_global_settings()
379 379 settings_model = self.global_settings
380 380 entries = all_patterns.get(uid)
381 381
382 382 for del_key in entries:
383 383 setting_name = self._get_keyname(del_key, uid)
384 384 entry = settings_model.get_setting_by_name(setting_name)
385 385 if entry:
386 386 Session().delete(entry)
387 387
388 388 Session().commit()
389 389
390 390 def create_or_update_setting(
391 391 self, name, val=Optional(''), type_=Optional('unicode')):
392 392 if self.repo_settings:
393 393 setting = self.repo_settings.create_or_update_setting(
394 394 name, val, type_)
395 395 else:
396 396 setting = self.global_settings.create_or_update_setting(
397 397 name, val, type_)
398 398 return setting
399 399
400 400
401 401 class VcsSettingsModel(object):
402 402
403 403 INHERIT_SETTINGS = 'inherit_vcs_settings'
404 404 GENERAL_SETTINGS = (
405 405 'use_outdated_comments', 'pr_merge_enabled',
406 406 'hg_use_rebase_for_merging')
407 407 HOOKS_SETTINGS = (
408 408 ('hooks', 'changegroup.repo_size'),
409 409 ('hooks', 'changegroup.push_logger'),
410 410 ('hooks', 'outgoing.pull_logger'))
411 411 HG_SETTINGS = (
412 412 ('extensions', 'largefiles'), ('phases', 'publish'))
413 413 GLOBAL_HG_SETTINGS = HG_SETTINGS + (('extensions', 'hgsubversion'), )
414 414 SVN_BRANCH_SECTION = 'vcs_svn_branch'
415 415 SVN_TAG_SECTION = 'vcs_svn_tag'
416 416 SSL_SETTING = ('web', 'push_ssl')
417 417 PATH_SETTING = ('paths', '/')
418 418
419 419 def __init__(self, sa=None, repo=None):
420 420 self.global_settings = SettingsModel(sa=sa)
421 421 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
422 422 self._ui_settings = self.HG_SETTINGS + self.HOOKS_SETTINGS
423 423 self._svn_sections = (self.SVN_BRANCH_SECTION, self.SVN_TAG_SECTION)
424 424
425 425 @property
426 426 @assert_repo_settings
427 427 def inherit_global_settings(self):
428 428 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
429 429 return setting.app_settings_value if setting else True
430 430
431 431 @inherit_global_settings.setter
432 432 @assert_repo_settings
433 433 def inherit_global_settings(self, value):
434 434 self.repo_settings.create_or_update_setting(
435 435 self.INHERIT_SETTINGS, value, type_='bool')
436 436
437 437 def get_global_svn_branch_patterns(self):
438 438 return self.global_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
439 439
440 440 @assert_repo_settings
441 441 def get_repo_svn_branch_patterns(self):
442 442 return self.repo_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
443 443
444 444 def get_global_svn_tag_patterns(self):
445 445 return self.global_settings.get_ui_by_section(self.SVN_TAG_SECTION)
446 446
447 447 @assert_repo_settings
448 448 def get_repo_svn_tag_patterns(self):
449 449 return self.repo_settings.get_ui_by_section(self.SVN_TAG_SECTION)
450 450
451 451 def get_global_settings(self):
452 452 return self._collect_all_settings(global_=True)
453 453
454 454 @assert_repo_settings
455 455 def get_repo_settings(self):
456 456 return self._collect_all_settings(global_=False)
457 457
458 458 @assert_repo_settings
459 459 def create_or_update_repo_settings(
460 460 self, data, inherit_global_settings=False):
461 461 from rhodecode.model.scm import ScmModel
462 462
463 463 self.inherit_global_settings = inherit_global_settings
464 464
465 465 repo = self.repo_settings.get_repo()
466 466 if not inherit_global_settings:
467 467 if repo.repo_type == 'svn':
468 468 self.create_repo_svn_settings(data)
469 469 else:
470 470 self.create_or_update_repo_hook_settings(data)
471 471 self.create_or_update_repo_pr_settings(data)
472 472
473 473 if repo.repo_type == 'hg':
474 474 self.create_or_update_repo_hg_settings(data)
475 475
476 476 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
477 477
478 478 @assert_repo_settings
479 479 def create_or_update_repo_hook_settings(self, data):
480 480 for section, key in self.HOOKS_SETTINGS:
481 481 data_key = self._get_form_ui_key(section, key)
482 482 if data_key not in data:
483 483 raise ValueError(
484 484 'The given data does not contain {} key'.format(data_key))
485 485
486 486 active = data.get(data_key)
487 487 repo_setting = self.repo_settings.get_ui_by_section_and_key(
488 488 section, key)
489 489 if not repo_setting:
490 490 global_setting = self.global_settings.\
491 491 get_ui_by_section_and_key(section, key)
492 492 self.repo_settings.create_ui_section_value(
493 493 section, global_setting.ui_value, key=key, active=active)
494 494 else:
495 495 repo_setting.ui_active = active
496 496 Session().add(repo_setting)
497 497
498 498 def update_global_hook_settings(self, data):
499 499 for section, key in self.HOOKS_SETTINGS:
500 500 data_key = self._get_form_ui_key(section, key)
501 501 if data_key not in data:
502 502 raise ValueError(
503 503 'The given data does not contain {} key'.format(data_key))
504 504 active = data.get(data_key)
505 505 repo_setting = self.global_settings.get_ui_by_section_and_key(
506 506 section, key)
507 507 repo_setting.ui_active = active
508 508 Session().add(repo_setting)
509 509
510 510 @assert_repo_settings
511 511 def create_or_update_repo_pr_settings(self, data):
512 512 return self._create_or_update_general_settings(
513 513 self.repo_settings, data)
514 514
515 515 def create_or_update_global_pr_settings(self, data):
516 516 return self._create_or_update_general_settings(
517 517 self.global_settings, data)
518 518
519 519 @assert_repo_settings
520 520 def create_repo_svn_settings(self, data):
521 521 return self._create_svn_settings(self.repo_settings, data)
522 522
523 523 def create_global_svn_settings(self, data):
524 524 return self._create_svn_settings(self.global_settings, data)
525 525
526 526 @assert_repo_settings
527 527 def create_or_update_repo_hg_settings(self, data):
528 528 largefiles, phases = self.HG_SETTINGS
529 529 largefiles_key, phases_key = self._get_hg_settings(
530 530 self.HG_SETTINGS, data)
531 531 self._create_or_update_ui(
532 532 self.repo_settings, *largefiles, value='',
533 533 active=data[largefiles_key])
534 534 self._create_or_update_ui(
535 535 self.repo_settings, *phases, value=safe_str(data[phases_key]))
536 536
537 537 def create_or_update_global_hg_settings(self, data):
538 538 largefiles, phases, subversion = self.GLOBAL_HG_SETTINGS
539 539 largefiles_key, phases_key, subversion_key = self._get_hg_settings(
540 540 self.GLOBAL_HG_SETTINGS, data)
541 541 self._create_or_update_ui(
542 542 self.global_settings, *largefiles, value='',
543 543 active=data[largefiles_key])
544 544 self._create_or_update_ui(
545 545 self.global_settings, *phases, value=safe_str(data[phases_key]))
546 546 self._create_or_update_ui(
547 547 self.global_settings, *subversion, active=data[subversion_key])
548 548
549 549 def update_global_ssl_setting(self, value):
550 550 self._create_or_update_ui(
551 551 self.global_settings, *self.SSL_SETTING, value=value)
552 552
553 553 def update_global_path_setting(self, value):
554 554 self._create_or_update_ui(
555 555 self.global_settings, *self.PATH_SETTING, value=value)
556 556
557 557 @assert_repo_settings
558 558 def delete_repo_svn_pattern(self, id_):
559 559 self.repo_settings.delete_ui(id_)
560 560
561 561 def delete_global_svn_pattern(self, id_):
562 562 self.global_settings.delete_ui(id_)
563 563
564 564 @assert_repo_settings
565 565 def get_repo_ui_settings(self, section=None, key=None):
566 566 global_uis = self.global_settings.get_ui(section, key)
567 567 repo_uis = self.repo_settings.get_ui(section, key)
568 568 filtered_repo_uis = self._filter_ui_settings(repo_uis)
569 569 filtered_repo_uis_keys = [
570 570 (s.section, s.key) for s in filtered_repo_uis]
571 571
572 572 def _is_global_ui_filtered(ui):
573 573 return (
574 574 (ui.section, ui.key) in filtered_repo_uis_keys
575 575 or ui.section in self._svn_sections)
576 576
577 577 filtered_global_uis = [
578 578 ui for ui in global_uis if not _is_global_ui_filtered(ui)]
579 579
580 580 return filtered_global_uis + filtered_repo_uis
581 581
582 582 def get_global_ui_settings(self, section=None, key=None):
583 583 return self.global_settings.get_ui(section, key)
584 584
585 585 def get_ui_settings(self, section=None, key=None):
586 586 if not self.repo_settings or self.inherit_global_settings:
587 587 return self.get_global_ui_settings(section, key)
588 588 else:
589 589 return self.get_repo_ui_settings(section, key)
590 590
591 591 def get_svn_patterns(self, section=None):
592 592 if not self.repo_settings:
593 593 return self.get_global_ui_settings(section)
594 594 else:
595 595 return self.get_repo_ui_settings(section)
596 596
597 597 @assert_repo_settings
598 598 def get_repo_general_settings(self):
599 599 global_settings = self.global_settings.get_all_settings()
600 600 repo_settings = self.repo_settings.get_all_settings()
601 601 filtered_repo_settings = self._filter_general_settings(repo_settings)
602 602 global_settings.update(filtered_repo_settings)
603 603 return global_settings
604 604
605 605 def get_global_general_settings(self):
606 606 return self.global_settings.get_all_settings()
607 607
608 608 def get_general_settings(self):
609 609 if not self.repo_settings or self.inherit_global_settings:
610 610 return self.get_global_general_settings()
611 611 else:
612 612 return self.get_repo_general_settings()
613 613
614 614 def get_repos_location(self):
615 615 return self.global_settings.get_ui_by_key('/').ui_value
616 616
617 617 def _filter_ui_settings(self, settings):
618 618 filtered_settings = [
619 619 s for s in settings if self._should_keep_setting(s)]
620 620 return filtered_settings
621 621
622 622 def _should_keep_setting(self, setting):
623 623 keep = (
624 624 (setting.section, setting.key) in self._ui_settings or
625 625 setting.section in self._svn_sections)
626 626 return keep
627 627
628 628 def _filter_general_settings(self, settings):
629 629 keys = ['rhodecode_{}'.format(key) for key in self.GENERAL_SETTINGS]
630 630 return {
631 631 k: settings[k]
632 632 for k in settings if k in keys}
633 633
634 634 def _collect_all_settings(self, global_=False):
635 635 settings = self.global_settings if global_ else self.repo_settings
636 636 result = {}
637 637
638 638 for section, key in self._ui_settings:
639 639 ui = settings.get_ui_by_section_and_key(section, key)
640 640 result_key = self._get_form_ui_key(section, key)
641 641 if ui:
642 642 if section in ('hooks', 'extensions'):
643 643 result[result_key] = ui.ui_active
644 644 else:
645 645 result[result_key] = ui.ui_value
646 646
647 647 for name in self.GENERAL_SETTINGS:
648 648 setting = settings.get_setting_by_name(name)
649 649 if setting:
650 650 result_key = 'rhodecode_{}'.format(name)
651 651 result[result_key] = setting.app_settings_value
652 652
653 653 return result
654 654
655 655 def _get_form_ui_key(self, section, key):
656 656 return '{section}_{key}'.format(
657 657 section=section, key=key.replace('.', '_'))
658 658
659 659 def _create_or_update_ui(
660 660 self, settings, section, key, value=None, active=None):
661 661 ui = settings.get_ui_by_section_and_key(section, key)
662 662 if not ui:
663 663 active = True if active is None else active
664 664 settings.create_ui_section_value(
665 665 section, value, key=key, active=active)
666 666 else:
667 667 if active is not None:
668 668 ui.ui_active = active
669 669 if value is not None:
670 670 ui.ui_value = value
671 671 Session().add(ui)
672 672
673 673 def _create_svn_settings(self, settings, data):
674 674 svn_settings = {
675 675 'new_svn_branch': self.SVN_BRANCH_SECTION,
676 676 'new_svn_tag': self.SVN_TAG_SECTION
677 677 }
678 678 for key in svn_settings:
679 679 if data.get(key):
680 680 settings.create_ui_section_value(svn_settings[key], data[key])
681 681
682 682 def _create_or_update_general_settings(self, settings, data):
683 683 for name in self.GENERAL_SETTINGS:
684 684 data_key = 'rhodecode_{}'.format(name)
685 685 if data_key not in data:
686 686 raise ValueError(
687 687 'The given data does not contain {} key'.format(data_key))
688 688 setting = settings.create_or_update_setting(
689 689 name, data[data_key], 'bool')
690 690 Session().add(setting)
691 691
692 692 def _get_hg_settings(self, settings, data):
693 693 data_keys = [self._get_form_ui_key(*s) for s in settings]
694 694 for data_key in data_keys:
695 695 if data_key not in data:
696 696 raise ValueError(
697 697 'The given data does not contain {} key'.format(data_key))
698 698 return data_keys
@@ -1,201 +1,201 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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
23 23 from rhodecode.tests import assert_session_flash
24 24 from rhodecode.tests.utils import AssertResponse
25 25 from rhodecode.model.db import Session
26 26 from rhodecode.model.settings import SettingsModel
27 27
28 28
29 29 def assert_auth_settings_updated(response):
30 30 assert response.status_int == 302, 'Expected response HTTP Found 302'
31 31 assert_session_flash(response, 'Auth settings updated successfully')
32 32
33 33
34 34 @pytest.mark.usefixtures("autologin_user", "app")
35 35 class TestAuthSettingsController(object):
36 36
37 37 def _enable_plugins(self, plugins_list, csrf_token, override=None,
38 38 verify_response=False):
39 39 test_url = '/_admin/auth'
40 40 params = {
41 41 'auth_plugins': plugins_list,
42 42 'csrf_token': csrf_token,
43 43 }
44 44 if override:
45 45 params.update(override)
46 46 _enabled_plugins = []
47 47 for plugin in plugins_list.split(','):
48 48 plugin_name = plugin.partition('#')[-1]
49 49 enabled_plugin = '%s_enabled' % plugin_name
50 50 cache_ttl = '%s_cache_ttl' % plugin_name
51 51
52 52 # default params that are needed for each plugin,
53 53 # `enabled` and `cache_ttl`
54 54 params.update({
55 55 enabled_plugin: True,
56 56 cache_ttl: 0
57 57 })
58 58 _enabled_plugins.append(enabled_plugin)
59 59
60 60 # we need to clean any enabled plugin before, since they require
61 61 # form params to be present
62 62 db_plugin = SettingsModel().get_setting_by_name('auth_plugins')
63 63 db_plugin.app_settings_value = \
64 64 'egg:rhodecode-enterprise-ce#rhodecode'
65 65 Session().add(db_plugin)
66 66 Session().commit()
67 67 for _plugin in _enabled_plugins:
68 68 db_plugin = SettingsModel().get_setting_by_name(_plugin)
69 69 if db_plugin:
70 Session.delete(db_plugin)
70 Session().delete(db_plugin)
71 71 Session().commit()
72 72
73 73 response = self.app.post(url=test_url, params=params)
74 74
75 75 if verify_response:
76 76 assert_auth_settings_updated(response)
77 77 return params
78 78
79 79 def _post_ldap_settings(self, params, override=None, force=False):
80 80
81 81 params.update({
82 82 'filter': 'user',
83 83 'user_member_of': '',
84 84 'user_search_base': '',
85 85 'user_search_filter': 'test_filter',
86 86
87 87 'host': 'dc.example.com',
88 88 'port': '999',
89 89 'tls_kind': 'PLAIN',
90 90 'tls_reqcert': 'NEVER',
91 91
92 92 'dn_user': 'test_user',
93 93 'dn_pass': 'test_pass',
94 94 'base_dn': 'test_base_dn',
95 95 'search_scope': 'BASE',
96 96 'attr_login': 'test_attr_login',
97 97 'attr_firstname': 'ima',
98 98 'attr_lastname': 'tester',
99 99 'attr_email': 'test@example.com',
100 100 'cache_ttl': '0',
101 101 })
102 102 if force:
103 103 params = {}
104 104 params.update(override or {})
105 105
106 106 test_url = '/_admin/auth/ldap/'
107 107
108 108 response = self.app.post(url=test_url, params=params)
109 109 return response
110 110
111 111 def test_index(self):
112 112 response = self.app.get('/_admin/auth')
113 113 response.mustcontain('Authentication Plugins')
114 114
115 115 @pytest.mark.parametrize("disable_plugin, needs_import", [
116 116 ('egg:rhodecode-enterprise-ce#headers', None),
117 117 ('egg:rhodecode-enterprise-ce#crowd', None),
118 118 ('egg:rhodecode-enterprise-ce#jasig_cas', None),
119 119 ('egg:rhodecode-enterprise-ce#ldap', None),
120 120 ('egg:rhodecode-enterprise-ce#pam', "pam"),
121 121 ])
122 122 def test_disable_plugin(self, csrf_token, disable_plugin, needs_import):
123 123 # TODO: johbo: "pam" is currently not available on darwin,
124 124 # although the docs state that it should work on darwin.
125 125 if needs_import:
126 126 pytest.importorskip(needs_import)
127 127
128 128 self._enable_plugins(
129 129 'egg:rhodecode-enterprise-ce#rhodecode,' + disable_plugin,
130 130 csrf_token, verify_response=True)
131 131
132 132 self._enable_plugins(
133 133 'egg:rhodecode-enterprise-ce#rhodecode', csrf_token,
134 134 verify_response=True)
135 135
136 136 def test_ldap_save_settings(self, csrf_token):
137 137 params = self._enable_plugins(
138 138 'egg:rhodecode-enterprise-ce#rhodecode,'
139 139 'egg:rhodecode-enterprise-ce#ldap',
140 140 csrf_token)
141 141 response = self._post_ldap_settings(params)
142 142 assert_auth_settings_updated(response)
143 143
144 144 new_settings = SettingsModel().get_auth_settings()
145 145 assert new_settings['auth_ldap_host'] == u'dc.example.com', \
146 146 'fail db write compare'
147 147
148 148 def test_ldap_error_form_wrong_port_number(self, csrf_token):
149 149 params = self._enable_plugins(
150 150 'egg:rhodecode-enterprise-ce#rhodecode,'
151 151 'egg:rhodecode-enterprise-ce#ldap',
152 152 csrf_token)
153 153 invalid_port_value = 'invalid-port-number'
154 154 response = self._post_ldap_settings(params, override={
155 155 'port': invalid_port_value,
156 156 })
157 157 assertr = AssertResponse(response)
158 158 assertr.element_contains(
159 159 '.form .field #port ~ .error-message',
160 160 invalid_port_value)
161 161
162 162 def test_ldap_error_form(self, csrf_token):
163 163 params = self._enable_plugins(
164 164 'egg:rhodecode-enterprise-ce#rhodecode,'
165 165 'egg:rhodecode-enterprise-ce#ldap',
166 166 csrf_token)
167 167 response = self._post_ldap_settings(params, override={
168 168 'attr_login': '',
169 169 })
170 170 response.mustcontain("""<span class="error-message">The LDAP Login"""
171 171 """ attribute of the CN must be specified""")
172 172
173 173 def test_post_ldap_group_settings(self, csrf_token):
174 174 params = self._enable_plugins(
175 175 'egg:rhodecode-enterprise-ce#rhodecode,'
176 176 'egg:rhodecode-enterprise-ce#ldap',
177 177 csrf_token)
178 178
179 179 response = self._post_ldap_settings(params, override={
180 180 'host': 'dc-legacy.example.com',
181 181 'port': '999',
182 182 'tls_kind': 'PLAIN',
183 183 'tls_reqcert': 'NEVER',
184 184 'dn_user': 'test_user',
185 185 'dn_pass': 'test_pass',
186 186 'base_dn': 'test_base_dn',
187 187 'filter': 'test_filter',
188 188 'search_scope': 'BASE',
189 189 'attr_login': 'test_attr_login',
190 190 'attr_firstname': 'ima',
191 191 'attr_lastname': 'tester',
192 192 'attr_email': 'test@example.com',
193 193 'cache_ttl': '60',
194 194 'csrf_token': csrf_token,
195 195 }
196 196 )
197 197 assert_auth_settings_updated(response)
198 198
199 199 new_settings = SettingsModel().get_auth_settings()
200 200 assert new_settings['auth_ldap_host'] == u'dc-legacy.example.com', \
201 201 'fail db write compare'
@@ -1,1622 +1,1622 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 collections
22 22 import datetime
23 23 import hashlib
24 24 import os
25 25 import re
26 26 import pprint
27 27 import shutil
28 28 import socket
29 29 import subprocess
30 30 import time
31 31 import uuid
32 32
33 33 import mock
34 34 import pyramid.testing
35 35 import pytest
36 36 import requests
37 37 from webtest.app import TestApp
38 38
39 39 import rhodecode
40 40 from rhodecode.model.changeset_status import ChangesetStatusModel
41 41 from rhodecode.model.comment import ChangesetCommentsModel
42 42 from rhodecode.model.db import (
43 43 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
44 44 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
45 45 from rhodecode.model.meta import Session
46 46 from rhodecode.model.pull_request import PullRequestModel
47 47 from rhodecode.model.repo import RepoModel
48 48 from rhodecode.model.repo_group import RepoGroupModel
49 49 from rhodecode.model.user import UserModel
50 50 from rhodecode.model.settings import VcsSettingsModel
51 51 from rhodecode.model.user_group import UserGroupModel
52 52 from rhodecode.lib.utils import repo2db_mapper
53 53 from rhodecode.lib.vcs import create_vcsserver_proxy
54 54 from rhodecode.lib.vcs.backends import get_backend
55 55 from rhodecode.lib.vcs.nodes import FileNode
56 56 from rhodecode.tests import (
57 57 login_user_session, get_new_dir, utils, TESTS_TMP_PATH,
58 58 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR2_LOGIN,
59 59 TEST_USER_REGULAR_PASS)
60 60 from rhodecode.tests.fixture import Fixture
61 61
62 62
63 63 def _split_comma(value):
64 64 return value.split(',')
65 65
66 66
67 67 def pytest_addoption(parser):
68 68 parser.addoption(
69 69 '--keep-tmp-path', action='store_true',
70 70 help="Keep the test temporary directories")
71 71 parser.addoption(
72 72 '--backends', action='store', type=_split_comma,
73 73 default=['git', 'hg', 'svn'],
74 74 help="Select which backends to test for backend specific tests.")
75 75 parser.addoption(
76 76 '--dbs', action='store', type=_split_comma,
77 77 default=['sqlite'],
78 78 help="Select which database to test for database specific tests. "
79 79 "Possible options are sqlite,postgres,mysql")
80 80 parser.addoption(
81 81 '--appenlight', '--ae', action='store_true',
82 82 help="Track statistics in appenlight.")
83 83 parser.addoption(
84 84 '--appenlight-api-key', '--ae-key',
85 85 help="API key for Appenlight.")
86 86 parser.addoption(
87 87 '--appenlight-url', '--ae-url',
88 88 default="https://ae.rhodecode.com",
89 89 help="Appenlight service URL, defaults to https://ae.rhodecode.com")
90 90 parser.addoption(
91 91 '--sqlite-connection-string', action='store',
92 92 default='', help="Connection string for the dbs tests with SQLite")
93 93 parser.addoption(
94 94 '--postgres-connection-string', action='store',
95 95 default='', help="Connection string for the dbs tests with Postgres")
96 96 parser.addoption(
97 97 '--mysql-connection-string', action='store',
98 98 default='', help="Connection string for the dbs tests with MySQL")
99 99 parser.addoption(
100 100 '--repeat', type=int, default=100,
101 101 help="Number of repetitions in performance tests.")
102 102
103 103
104 104 def pytest_configure(config):
105 105 # Appy the kombu patch early on, needed for test discovery on Python 2.7.11
106 106 from rhodecode.config import patches
107 107 patches.kombu_1_5_1_python_2_7_11()
108 108
109 109
110 110 def pytest_collection_modifyitems(session, config, items):
111 111 # nottest marked, compare nose, used for transition from nose to pytest
112 112 remaining = [
113 113 i for i in items if getattr(i.obj, '__test__', True)]
114 114 items[:] = remaining
115 115
116 116
117 117 def pytest_generate_tests(metafunc):
118 118 # Support test generation based on --backend parameter
119 119 if 'backend_alias' in metafunc.fixturenames:
120 120 backends = get_backends_from_metafunc(metafunc)
121 121 scope = None
122 122 if not backends:
123 123 pytest.skip("Not enabled for any of selected backends")
124 124 metafunc.parametrize('backend_alias', backends, scope=scope)
125 125 elif hasattr(metafunc.function, 'backends'):
126 126 backends = get_backends_from_metafunc(metafunc)
127 127 if not backends:
128 128 pytest.skip("Not enabled for any of selected backends")
129 129
130 130
131 131 def get_backends_from_metafunc(metafunc):
132 132 requested_backends = set(metafunc.config.getoption('--backends'))
133 133 if hasattr(metafunc.function, 'backends'):
134 134 # Supported backends by this test function, created from
135 135 # pytest.mark.backends
136 136 backends = metafunc.function.backends.args
137 137 elif hasattr(metafunc.cls, 'backend_alias'):
138 138 # Support class attribute "backend_alias", this is mainly
139 139 # for legacy reasons for tests not yet using pytest.mark.backends
140 140 backends = [metafunc.cls.backend_alias]
141 141 else:
142 142 backends = metafunc.config.getoption('--backends')
143 143 return requested_backends.intersection(backends)
144 144
145 145
146 146 @pytest.fixture(scope='session', autouse=True)
147 147 def activate_example_rcextensions(request):
148 148 """
149 149 Patch in an example rcextensions module which verifies passed in kwargs.
150 150 """
151 151 from rhodecode.tests.other import example_rcextensions
152 152
153 153 old_extensions = rhodecode.EXTENSIONS
154 154 rhodecode.EXTENSIONS = example_rcextensions
155 155
156 156 @request.addfinalizer
157 157 def cleanup():
158 158 rhodecode.EXTENSIONS = old_extensions
159 159
160 160
161 161 @pytest.fixture
162 162 def capture_rcextensions():
163 163 """
164 164 Returns the recorded calls to entry points in rcextensions.
165 165 """
166 166 calls = rhodecode.EXTENSIONS.calls
167 167 calls.clear()
168 168 # Note: At this moment, it is still the empty dict, but that will
169 169 # be filled during the test run and since it is a reference this
170 170 # is enough to make it work.
171 171 return calls
172 172
173 173
174 174 @pytest.fixture(scope='session')
175 175 def http_environ_session():
176 176 """
177 177 Allow to use "http_environ" in session scope.
178 178 """
179 179 return http_environ(
180 180 http_host_stub=http_host_stub())
181 181
182 182
183 183 @pytest.fixture
184 184 def http_host_stub():
185 185 """
186 186 Value of HTTP_HOST in the test run.
187 187 """
188 188 return 'test.example.com:80'
189 189
190 190
191 191 @pytest.fixture
192 192 def http_environ(http_host_stub):
193 193 """
194 194 HTTP extra environ keys.
195 195
196 196 User by the test application and as well for setting up the pylons
197 197 environment. In the case of the fixture "app" it should be possible
198 198 to override this for a specific test case.
199 199 """
200 200 return {
201 201 'SERVER_NAME': http_host_stub.split(':')[0],
202 202 'SERVER_PORT': http_host_stub.split(':')[1],
203 203 'HTTP_HOST': http_host_stub,
204 204 }
205 205
206 206
207 207 @pytest.fixture(scope='function')
208 208 def app(request, pylonsapp, http_environ):
209 209 app = TestApp(
210 210 pylonsapp,
211 211 extra_environ=http_environ)
212 212 if request.cls:
213 213 request.cls.app = app
214 214 return app
215 215
216 216
217 217 LoginData = collections.namedtuple('LoginData', ('csrf_token', 'user'))
218 218
219 219
220 220 def _autologin_user(app, *args):
221 221 session = login_user_session(app, *args)
222 222 csrf_token = rhodecode.lib.auth.get_csrf_token(session)
223 223 return LoginData(csrf_token, session['rhodecode_user'])
224 224
225 225
226 226 @pytest.fixture
227 227 def autologin_user(app):
228 228 """
229 229 Utility fixture which makes sure that the admin user is logged in
230 230 """
231 231 return _autologin_user(app)
232 232
233 233
234 234 @pytest.fixture
235 235 def autologin_regular_user(app):
236 236 """
237 237 Utility fixture which makes sure that the regular user is logged in
238 238 """
239 239 return _autologin_user(
240 240 app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
241 241
242 242
243 243 @pytest.fixture(scope='function')
244 244 def csrf_token(request, autologin_user):
245 245 return autologin_user.csrf_token
246 246
247 247
248 248 @pytest.fixture(scope='function')
249 249 def xhr_header(request):
250 250 return {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
251 251
252 252
253 253 @pytest.fixture
254 254 def real_crypto_backend(monkeypatch):
255 255 """
256 256 Switch the production crypto backend on for this test.
257 257
258 258 During the test run the crypto backend is replaced with a faster
259 259 implementation based on the MD5 algorithm.
260 260 """
261 261 monkeypatch.setattr(rhodecode, 'is_test', False)
262 262
263 263
264 264 @pytest.fixture(scope='class')
265 265 def index_location(request, pylonsapp):
266 266 index_location = pylonsapp.config['app_conf']['search.location']
267 267 if request.cls:
268 268 request.cls.index_location = index_location
269 269 return index_location
270 270
271 271
272 272 @pytest.fixture(scope='session', autouse=True)
273 273 def tests_tmp_path(request):
274 274 """
275 275 Create temporary directory to be used during the test session.
276 276 """
277 277 if not os.path.exists(TESTS_TMP_PATH):
278 278 os.makedirs(TESTS_TMP_PATH)
279 279
280 280 if not request.config.getoption('--keep-tmp-path'):
281 281 @request.addfinalizer
282 282 def remove_tmp_path():
283 283 shutil.rmtree(TESTS_TMP_PATH)
284 284
285 285 return TESTS_TMP_PATH
286 286
287 287
288 288 @pytest.fixture(scope='session', autouse=True)
289 289 def patch_pyro_request_scope_proxy_factory(request):
290 290 """
291 291 Patch the pyro proxy factory to always use the same dummy request object
292 292 when under test. This will return the same pyro proxy on every call.
293 293 """
294 294 dummy_request = pyramid.testing.DummyRequest()
295 295
296 296 def mocked_call(self, request=None):
297 297 return self.getProxy(request=dummy_request)
298 298
299 299 patcher = mock.patch(
300 300 'rhodecode.lib.vcs.client.RequestScopeProxyFactory.__call__',
301 301 new=mocked_call)
302 302 patcher.start()
303 303
304 304 @request.addfinalizer
305 305 def undo_patching():
306 306 patcher.stop()
307 307
308 308
309 309 @pytest.fixture
310 310 def test_repo_group(request):
311 311 """
312 312 Create a temporary repository group, and destroy it after
313 313 usage automatically
314 314 """
315 315 fixture = Fixture()
316 316 repogroupid = 'test_repo_group_%s' % int(time.time())
317 317 repo_group = fixture.create_repo_group(repogroupid)
318 318
319 319 def _cleanup():
320 320 fixture.destroy_repo_group(repogroupid)
321 321
322 322 request.addfinalizer(_cleanup)
323 323 return repo_group
324 324
325 325
326 326 @pytest.fixture
327 327 def test_user_group(request):
328 328 """
329 329 Create a temporary user group, and destroy it after
330 330 usage automatically
331 331 """
332 332 fixture = Fixture()
333 333 usergroupid = 'test_user_group_%s' % int(time.time())
334 334 user_group = fixture.create_user_group(usergroupid)
335 335
336 336 def _cleanup():
337 337 fixture.destroy_user_group(user_group)
338 338
339 339 request.addfinalizer(_cleanup)
340 340 return user_group
341 341
342 342
343 343 @pytest.fixture(scope='session')
344 344 def test_repo(request):
345 345 container = TestRepoContainer()
346 346 request.addfinalizer(container._cleanup)
347 347 return container
348 348
349 349
350 350 class TestRepoContainer(object):
351 351 """
352 352 Container for test repositories which are used read only.
353 353
354 354 Repositories will be created on demand and re-used during the lifetime
355 355 of this object.
356 356
357 357 Usage to get the svn test repository "minimal"::
358 358
359 359 test_repo = TestContainer()
360 360 repo = test_repo('minimal', 'svn')
361 361
362 362 """
363 363
364 364 dump_extractors = {
365 365 'git': utils.extract_git_repo_from_dump,
366 366 'hg': utils.extract_hg_repo_from_dump,
367 367 'svn': utils.extract_svn_repo_from_dump,
368 368 }
369 369
370 370 def __init__(self):
371 371 self._cleanup_repos = []
372 372 self._fixture = Fixture()
373 373 self._repos = {}
374 374
375 375 def __call__(self, dump_name, backend_alias):
376 376 key = (dump_name, backend_alias)
377 377 if key not in self._repos:
378 378 repo = self._create_repo(dump_name, backend_alias)
379 379 self._repos[key] = repo.repo_id
380 380 return Repository.get(self._repos[key])
381 381
382 382 def _create_repo(self, dump_name, backend_alias):
383 383 repo_name = '%s-%s' % (backend_alias, dump_name)
384 384 backend_class = get_backend(backend_alias)
385 385 dump_extractor = self.dump_extractors[backend_alias]
386 386 repo_path = dump_extractor(dump_name, repo_name)
387 387 vcs_repo = backend_class(repo_path)
388 388 repo2db_mapper({repo_name: vcs_repo})
389 389 repo = RepoModel().get_by_repo_name(repo_name)
390 390 self._cleanup_repos.append(repo_name)
391 391 return repo
392 392
393 393 def _cleanup(self):
394 394 for repo_name in reversed(self._cleanup_repos):
395 395 self._fixture.destroy_repo(repo_name)
396 396
397 397
398 398 @pytest.fixture
399 399 def backend(request, backend_alias, pylonsapp, test_repo):
400 400 """
401 401 Parametrized fixture which represents a single backend implementation.
402 402
403 403 It respects the option `--backends` to focus the test run on specific
404 404 backend implementations.
405 405
406 406 It also supports `pytest.mark.xfail_backends` to mark tests as failing
407 407 for specific backends. This is intended as a utility for incremental
408 408 development of a new backend implementation.
409 409 """
410 410 if backend_alias not in request.config.getoption('--backends'):
411 411 pytest.skip("Backend %s not selected." % (backend_alias, ))
412 412
413 413 utils.check_xfail_backends(request.node, backend_alias)
414 414 utils.check_skip_backends(request.node, backend_alias)
415 415
416 416 repo_name = 'vcs_test_%s' % (backend_alias, )
417 417 backend = Backend(
418 418 alias=backend_alias,
419 419 repo_name=repo_name,
420 420 test_name=request.node.name,
421 421 test_repo_container=test_repo)
422 422 request.addfinalizer(backend.cleanup)
423 423 return backend
424 424
425 425
426 426 @pytest.fixture
427 427 def backend_git(request, pylonsapp, test_repo):
428 428 return backend(request, 'git', pylonsapp, test_repo)
429 429
430 430
431 431 @pytest.fixture
432 432 def backend_hg(request, pylonsapp, test_repo):
433 433 return backend(request, 'hg', pylonsapp, test_repo)
434 434
435 435
436 436 @pytest.fixture
437 437 def backend_svn(request, pylonsapp, test_repo):
438 438 return backend(request, 'svn', pylonsapp, test_repo)
439 439
440 440
441 441 @pytest.fixture
442 442 def backend_random(backend_git):
443 443 """
444 444 Use this to express that your tests need "a backend.
445 445
446 446 A few of our tests need a backend, so that we can run the code. This
447 447 fixture is intended to be used for such cases. It will pick one of the
448 448 backends and run the tests.
449 449
450 450 The fixture `backend` would run the test multiple times for each
451 451 available backend which is a pure waste of time if the test is
452 452 independent of the backend type.
453 453 """
454 454 # TODO: johbo: Change this to pick a random backend
455 455 return backend_git
456 456
457 457
458 458 @pytest.fixture
459 459 def backend_stub(backend_git):
460 460 """
461 461 Use this to express that your tests need a backend stub
462 462
463 463 TODO: mikhail: Implement a real stub logic instead of returning
464 464 a git backend
465 465 """
466 466 return backend_git
467 467
468 468
469 469 @pytest.fixture
470 470 def repo_stub(backend_stub):
471 471 """
472 472 Use this to express that your tests need a repository stub
473 473 """
474 474 return backend_stub.create_repo()
475 475
476 476
477 477 class Backend(object):
478 478 """
479 479 Represents the test configuration for one supported backend
480 480
481 481 Provides easy access to different test repositories based on
482 482 `__getitem__`. Such repositories will only be created once per test
483 483 session.
484 484 """
485 485
486 486 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
487 487 _master_repo = None
488 488 _commit_ids = {}
489 489
490 490 def __init__(self, alias, repo_name, test_name, test_repo_container):
491 491 self.alias = alias
492 492 self.repo_name = repo_name
493 493 self._cleanup_repos = []
494 494 self._test_name = test_name
495 495 self._test_repo_container = test_repo_container
496 496 # TODO: johbo: Used as a delegate interim. Not yet sure if Backend or
497 497 # Fixture will survive in the end.
498 498 self._fixture = Fixture()
499 499
500 500 def __getitem__(self, key):
501 501 return self._test_repo_container(key, self.alias)
502 502
503 503 @property
504 504 def repo(self):
505 505 """
506 506 Returns the "current" repository. This is the vcs_test repo or the
507 507 last repo which has been created with `create_repo`.
508 508 """
509 509 from rhodecode.model.db import Repository
510 510 return Repository.get_by_repo_name(self.repo_name)
511 511
512 512 @property
513 513 def default_branch_name(self):
514 514 VcsRepository = get_backend(self.alias)
515 515 return VcsRepository.DEFAULT_BRANCH_NAME
516 516
517 517 @property
518 518 def default_head_id(self):
519 519 """
520 520 Returns the default head id of the underlying backend.
521 521
522 522 This will be the default branch name in case the backend does have a
523 523 default branch. In the other cases it will point to a valid head
524 524 which can serve as the base to create a new commit on top of it.
525 525 """
526 526 vcsrepo = self.repo.scm_instance()
527 527 head_id = (
528 528 vcsrepo.DEFAULT_BRANCH_NAME or
529 529 vcsrepo.commit_ids[-1])
530 530 return head_id
531 531
532 532 @property
533 533 def commit_ids(self):
534 534 """
535 535 Returns the list of commits for the last created repository
536 536 """
537 537 return self._commit_ids
538 538
539 539 def create_master_repo(self, commits):
540 540 """
541 541 Create a repository and remember it as a template.
542 542
543 543 This allows to easily create derived repositories to construct
544 544 more complex scenarios for diff, compare and pull requests.
545 545
546 546 Returns a commit map which maps from commit message to raw_id.
547 547 """
548 548 self._master_repo = self.create_repo(commits=commits)
549 549 return self._commit_ids
550 550
551 551 def create_repo(
552 552 self, commits=None, number_of_commits=0, heads=None,
553 553 name_suffix=u'', **kwargs):
554 554 """
555 555 Create a repository and record it for later cleanup.
556 556
557 557 :param commits: Optional. A sequence of dict instances.
558 558 Will add a commit per entry to the new repository.
559 559 :param number_of_commits: Optional. If set to a number, this number of
560 560 commits will be added to the new repository.
561 561 :param heads: Optional. Can be set to a sequence of of commit
562 562 names which shall be pulled in from the master repository.
563 563
564 564 """
565 565 self.repo_name = self._next_repo_name() + name_suffix
566 566 repo = self._fixture.create_repo(
567 567 self.repo_name, repo_type=self.alias, **kwargs)
568 568 self._cleanup_repos.append(repo.repo_name)
569 569
570 570 commits = commits or [
571 571 {'message': 'Commit %s of %s' % (x, self.repo_name)}
572 572 for x in xrange(number_of_commits)]
573 573 self._add_commits_to_repo(repo.scm_instance(), commits)
574 574 if heads:
575 575 self.pull_heads(repo, heads)
576 576
577 577 return repo
578 578
579 579 def pull_heads(self, repo, heads):
580 580 """
581 581 Make sure that repo contains all commits mentioned in `heads`
582 582 """
583 583 vcsmaster = self._master_repo.scm_instance()
584 584 vcsrepo = repo.scm_instance()
585 585 vcsrepo.config.clear_section('hooks')
586 586 commit_ids = [self._commit_ids[h] for h in heads]
587 587 vcsrepo.pull(vcsmaster.path, commit_ids=commit_ids)
588 588
589 589 def create_fork(self):
590 590 repo_to_fork = self.repo_name
591 591 self.repo_name = self._next_repo_name()
592 592 repo = self._fixture.create_fork(repo_to_fork, self.repo_name)
593 593 self._cleanup_repos.append(self.repo_name)
594 594 return repo
595 595
596 596 def new_repo_name(self, suffix=u''):
597 597 self.repo_name = self._next_repo_name() + suffix
598 598 self._cleanup_repos.append(self.repo_name)
599 599 return self.repo_name
600 600
601 601 def _next_repo_name(self):
602 602 return u"%s_%s" % (
603 603 self.invalid_repo_name.sub(u'_', self._test_name),
604 604 len(self._cleanup_repos))
605 605
606 606 def ensure_file(self, filename, content='Test content\n'):
607 607 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
608 608 commits = [
609 609 {'added': [
610 610 FileNode(filename, content=content),
611 611 ]},
612 612 ]
613 613 self._add_commits_to_repo(self.repo.scm_instance(), commits)
614 614
615 615 def enable_downloads(self):
616 616 repo = self.repo
617 617 repo.enable_downloads = True
618 618 Session().add(repo)
619 619 Session().commit()
620 620
621 621 def cleanup(self):
622 622 for repo_name in reversed(self._cleanup_repos):
623 623 self._fixture.destroy_repo(repo_name)
624 624
625 625 def _add_commits_to_repo(self, repo, commits):
626 626 if not commits:
627 627 return
628 628
629 629 imc = repo.in_memory_commit
630 630 commit = None
631 631 self._commit_ids = {}
632 632
633 633 for idx, commit in enumerate(commits):
634 634 message = unicode(commit.get('message', 'Commit %s' % idx))
635 635
636 636 for node in commit.get('added', []):
637 637 imc.add(FileNode(node.path, content=node.content))
638 638 for node in commit.get('changed', []):
639 639 imc.change(FileNode(node.path, content=node.content))
640 640 for node in commit.get('removed', []):
641 641 imc.remove(FileNode(node.path))
642 642
643 643 parents = [
644 644 repo.get_commit(commit_id=self._commit_ids[p])
645 645 for p in commit.get('parents', [])]
646 646
647 647 operations = ('added', 'changed', 'removed')
648 648 if not any((commit.get(o) for o in operations)):
649 649 imc.add(FileNode('file_%s' % idx, content=message))
650 650
651 651 commit = imc.commit(
652 652 message=message,
653 653 author=unicode(commit.get('author', 'Automatic')),
654 654 date=commit.get('date'),
655 655 branch=commit.get('branch'),
656 656 parents=parents)
657 657
658 658 self._commit_ids[commit.message] = commit.raw_id
659 659
660 660 # Creating refs for Git to allow fetching them from remote repository
661 661 if self.alias == 'git':
662 662 refs = {}
663 663 for message in self._commit_ids:
664 664 # TODO: mikhail: do more special chars replacements
665 665 ref_name = 'refs/test-refs/{}'.format(
666 666 message.replace(' ', ''))
667 667 refs[ref_name] = self._commit_ids[message]
668 668 self._create_refs(repo, refs)
669 669
670 670 return commit
671 671
672 672 def _create_refs(self, repo, refs):
673 673 for ref_name in refs:
674 674 repo.set_refs(ref_name, refs[ref_name])
675 675
676 676
677 677 @pytest.fixture
678 678 def vcsbackend(request, backend_alias, tests_tmp_path, pylonsapp, test_repo):
679 679 """
680 680 Parametrized fixture which represents a single vcs backend implementation.
681 681
682 682 See the fixture `backend` for more details. This one implements the same
683 683 concept, but on vcs level. So it does not provide model instances etc.
684 684
685 685 Parameters are generated dynamically, see :func:`pytest_generate_tests`
686 686 for how this works.
687 687 """
688 688 if backend_alias not in request.config.getoption('--backends'):
689 689 pytest.skip("Backend %s not selected." % (backend_alias, ))
690 690
691 691 utils.check_xfail_backends(request.node, backend_alias)
692 692 utils.check_skip_backends(request.node, backend_alias)
693 693
694 694 repo_name = 'vcs_test_%s' % (backend_alias, )
695 695 repo_path = os.path.join(tests_tmp_path, repo_name)
696 696 backend = VcsBackend(
697 697 alias=backend_alias,
698 698 repo_path=repo_path,
699 699 test_name=request.node.name,
700 700 test_repo_container=test_repo)
701 701 request.addfinalizer(backend.cleanup)
702 702 return backend
703 703
704 704
705 705 @pytest.fixture
706 706 def vcsbackend_git(request, tests_tmp_path, pylonsapp, test_repo):
707 707 return vcsbackend(request, 'git', tests_tmp_path, pylonsapp, test_repo)
708 708
709 709
710 710 @pytest.fixture
711 711 def vcsbackend_hg(request, tests_tmp_path, pylonsapp, test_repo):
712 712 return vcsbackend(request, 'hg', tests_tmp_path, pylonsapp, test_repo)
713 713
714 714
715 715 @pytest.fixture
716 716 def vcsbackend_svn(request, tests_tmp_path, pylonsapp, test_repo):
717 717 return vcsbackend(request, 'svn', tests_tmp_path, pylonsapp, test_repo)
718 718
719 719
720 720 @pytest.fixture
721 721 def vcsbackend_random(vcsbackend_git):
722 722 """
723 723 Use this to express that your tests need "a vcsbackend".
724 724
725 725 The fixture `vcsbackend` would run the test multiple times for each
726 726 available vcs backend which is a pure waste of time if the test is
727 727 independent of the vcs backend type.
728 728 """
729 729 # TODO: johbo: Change this to pick a random backend
730 730 return vcsbackend_git
731 731
732 732
733 733 class VcsBackend(object):
734 734 """
735 735 Represents the test configuration for one supported vcs backend.
736 736 """
737 737
738 738 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
739 739
740 740 def __init__(self, alias, repo_path, test_name, test_repo_container):
741 741 self.alias = alias
742 742 self._repo_path = repo_path
743 743 self._cleanup_repos = []
744 744 self._test_name = test_name
745 745 self._test_repo_container = test_repo_container
746 746
747 747 def __getitem__(self, key):
748 748 return self._test_repo_container(key, self.alias).scm_instance()
749 749
750 750 @property
751 751 def repo(self):
752 752 """
753 753 Returns the "current" repository. This is the vcs_test repo of the last
754 754 repo which has been created.
755 755 """
756 756 Repository = get_backend(self.alias)
757 757 return Repository(self._repo_path)
758 758
759 759 @property
760 760 def backend(self):
761 761 """
762 762 Returns the backend implementation class.
763 763 """
764 764 return get_backend(self.alias)
765 765
766 766 def create_repo(self, number_of_commits=0, _clone_repo=None):
767 767 repo_name = self._next_repo_name()
768 768 self._repo_path = get_new_dir(repo_name)
769 769 Repository = get_backend(self.alias)
770 770 src_url = None
771 771 if _clone_repo:
772 772 src_url = _clone_repo.path
773 773 repo = Repository(self._repo_path, create=True, src_url=src_url)
774 774 self._cleanup_repos.append(repo)
775 775 for idx in xrange(number_of_commits):
776 776 self.ensure_file(filename='file_%s' % idx, content=repo.name)
777 777 return repo
778 778
779 779 def clone_repo(self, repo):
780 780 return self.create_repo(_clone_repo=repo)
781 781
782 782 def cleanup(self):
783 783 for repo in self._cleanup_repos:
784 784 shutil.rmtree(repo.path)
785 785
786 786 def new_repo_path(self):
787 787 repo_name = self._next_repo_name()
788 788 self._repo_path = get_new_dir(repo_name)
789 789 return self._repo_path
790 790
791 791 def _next_repo_name(self):
792 792 return "%s_%s" % (
793 793 self.invalid_repo_name.sub('_', self._test_name),
794 794 len(self._cleanup_repos))
795 795
796 796 def add_file(self, repo, filename, content='Test content\n'):
797 797 imc = repo.in_memory_commit
798 798 imc.add(FileNode(filename, content=content))
799 799 imc.commit(
800 800 message=u'Automatic commit from vcsbackend fixture',
801 801 author=u'Automatic')
802 802
803 803 def ensure_file(self, filename, content='Test content\n'):
804 804 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
805 805 self.add_file(self.repo, filename, content)
806 806
807 807
808 808 @pytest.fixture
809 809 def reposerver(request):
810 810 """
811 811 Allows to serve a backend repository
812 812 """
813 813
814 814 repo_server = RepoServer()
815 815 request.addfinalizer(repo_server.cleanup)
816 816 return repo_server
817 817
818 818
819 819 class RepoServer(object):
820 820 """
821 821 Utility to serve a local repository for the duration of a test case.
822 822
823 823 Supports only Subversion so far.
824 824 """
825 825
826 826 url = None
827 827
828 828 def __init__(self):
829 829 self._cleanup_servers = []
830 830
831 831 def serve(self, vcsrepo):
832 832 if vcsrepo.alias != 'svn':
833 833 raise TypeError("Backend %s not supported" % vcsrepo.alias)
834 834
835 835 proc = subprocess.Popen(
836 836 ['svnserve', '-d', '--foreground', '--listen-host', 'localhost',
837 837 '--root', vcsrepo.path])
838 838 self._cleanup_servers.append(proc)
839 839 self.url = 'svn://localhost'
840 840
841 841 def cleanup(self):
842 842 for proc in self._cleanup_servers:
843 843 proc.terminate()
844 844
845 845
846 846 @pytest.fixture
847 847 def pr_util(backend, request):
848 848 """
849 849 Utility for tests of models and for functional tests around pull requests.
850 850
851 851 It gives an instance of :class:`PRTestUtility` which provides various
852 852 utility methods around one pull request.
853 853
854 854 This fixture uses `backend` and inherits its parameterization.
855 855 """
856 856
857 857 util = PRTestUtility(backend)
858 858
859 859 @request.addfinalizer
860 860 def cleanup():
861 861 util.cleanup()
862 862
863 863 return util
864 864
865 865
866 866 class PRTestUtility(object):
867 867
868 868 pull_request = None
869 869 pull_request_id = None
870 870 mergeable_patcher = None
871 871 mergeable_mock = None
872 872 notification_patcher = None
873 873
874 874 def __init__(self, backend):
875 875 self.backend = backend
876 876
877 877 def create_pull_request(
878 878 self, commits=None, target_head=None, source_head=None,
879 879 revisions=None, approved=False, author=None, mergeable=False,
880 880 enable_notifications=True, name_suffix=u'', reviewers=None,
881 881 title=u"Test", description=u"Description"):
882 882 self.set_mergeable(mergeable)
883 883 if not enable_notifications:
884 884 # mock notification side effect
885 885 self.notification_patcher = mock.patch(
886 886 'rhodecode.model.notification.NotificationModel.create')
887 887 self.notification_patcher.start()
888 888
889 889 if not self.pull_request:
890 890 if not commits:
891 891 commits = [
892 892 {'message': 'c1'},
893 893 {'message': 'c2'},
894 894 {'message': 'c3'},
895 895 ]
896 896 target_head = 'c1'
897 897 source_head = 'c2'
898 898 revisions = ['c2']
899 899
900 900 self.commit_ids = self.backend.create_master_repo(commits)
901 901 self.target_repository = self.backend.create_repo(
902 902 heads=[target_head], name_suffix=name_suffix)
903 903 self.source_repository = self.backend.create_repo(
904 904 heads=[source_head], name_suffix=name_suffix)
905 905 self.author = author or UserModel().get_by_username(
906 906 TEST_USER_ADMIN_LOGIN)
907 907
908 908 model = PullRequestModel()
909 909 self.create_parameters = {
910 910 'created_by': self.author,
911 911 'source_repo': self.source_repository.repo_name,
912 912 'source_ref': self._default_branch_reference(source_head),
913 913 'target_repo': self.target_repository.repo_name,
914 914 'target_ref': self._default_branch_reference(target_head),
915 915 'revisions': [self.commit_ids[r] for r in revisions],
916 916 'reviewers': reviewers or self._get_reviewers(),
917 917 'title': title,
918 918 'description': description,
919 919 }
920 920 self.pull_request = model.create(**self.create_parameters)
921 921 assert model.get_versions(self.pull_request) == []
922 922
923 923 self.pull_request_id = self.pull_request.pull_request_id
924 924
925 925 if approved:
926 926 self.approve()
927 927
928 928 Session().add(self.pull_request)
929 929 Session().commit()
930 930
931 931 return self.pull_request
932 932
933 933 def approve(self):
934 934 self.create_status_votes(
935 935 ChangesetStatus.STATUS_APPROVED,
936 936 *self.pull_request.reviewers)
937 937
938 938 def close(self):
939 939 PullRequestModel().close_pull_request(self.pull_request, self.author)
940 940
941 941 def _default_branch_reference(self, commit_message):
942 942 reference = '%s:%s:%s' % (
943 943 'branch',
944 944 self.backend.default_branch_name,
945 945 self.commit_ids[commit_message])
946 946 return reference
947 947
948 948 def _get_reviewers(self):
949 949 model = UserModel()
950 950 return [
951 951 model.get_by_username(TEST_USER_REGULAR_LOGIN),
952 952 model.get_by_username(TEST_USER_REGULAR2_LOGIN),
953 953 ]
954 954
955 955 def update_source_repository(self, head=None):
956 956 heads = [head or 'c3']
957 957 self.backend.pull_heads(self.source_repository, heads=heads)
958 958
959 959 def add_one_commit(self, head=None):
960 960 self.update_source_repository(head=head)
961 961 old_commit_ids = set(self.pull_request.revisions)
962 962 PullRequestModel().update_commits(self.pull_request)
963 963 commit_ids = set(self.pull_request.revisions)
964 964 new_commit_ids = commit_ids - old_commit_ids
965 965 assert len(new_commit_ids) == 1
966 966 return new_commit_ids.pop()
967 967
968 968 def remove_one_commit(self):
969 969 assert len(self.pull_request.revisions) == 2
970 970 source_vcs = self.source_repository.scm_instance()
971 971 removed_commit_id = source_vcs.commit_ids[-1]
972 972
973 973 # TODO: johbo: Git and Mercurial have an inconsistent vcs api here,
974 974 # remove the if once that's sorted out.
975 975 if self.backend.alias == "git":
976 976 kwargs = {'branch_name': self.backend.default_branch_name}
977 977 else:
978 978 kwargs = {}
979 979 source_vcs.strip(removed_commit_id, **kwargs)
980 980
981 981 PullRequestModel().update_commits(self.pull_request)
982 982 assert len(self.pull_request.revisions) == 1
983 983 return removed_commit_id
984 984
985 985 def create_comment(self, linked_to=None):
986 986 comment = ChangesetCommentsModel().create(
987 987 text=u"Test comment",
988 988 repo=self.target_repository.repo_name,
989 989 user=self.author,
990 990 pull_request=self.pull_request)
991 991 assert comment.pull_request_version_id is None
992 992
993 993 if linked_to:
994 994 PullRequestModel()._link_comments_to_version(linked_to)
995 995
996 996 return comment
997 997
998 998 def create_inline_comment(
999 999 self, linked_to=None, line_no=u'n1', file_path='file_1'):
1000 1000 comment = ChangesetCommentsModel().create(
1001 1001 text=u"Test comment",
1002 1002 repo=self.target_repository.repo_name,
1003 1003 user=self.author,
1004 1004 line_no=line_no,
1005 1005 f_path=file_path,
1006 1006 pull_request=self.pull_request)
1007 1007 assert comment.pull_request_version_id is None
1008 1008
1009 1009 if linked_to:
1010 1010 PullRequestModel()._link_comments_to_version(linked_to)
1011 1011
1012 1012 return comment
1013 1013
1014 1014 def create_version_of_pull_request(self):
1015 1015 pull_request = self.create_pull_request()
1016 1016 version = PullRequestModel()._create_version_from_snapshot(
1017 1017 pull_request)
1018 1018 return version
1019 1019
1020 1020 def create_status_votes(self, status, *reviewers):
1021 1021 for reviewer in reviewers:
1022 1022 ChangesetStatusModel().set_status(
1023 1023 repo=self.pull_request.target_repo,
1024 1024 status=status,
1025 1025 user=reviewer.user_id,
1026 1026 pull_request=self.pull_request)
1027 1027
1028 1028 def set_mergeable(self, value):
1029 1029 if not self.mergeable_patcher:
1030 1030 self.mergeable_patcher = mock.patch.object(
1031 1031 VcsSettingsModel, 'get_general_settings')
1032 1032 self.mergeable_mock = self.mergeable_patcher.start()
1033 1033 self.mergeable_mock.return_value = {
1034 1034 'rhodecode_pr_merge_enabled': value}
1035 1035
1036 1036 def cleanup(self):
1037 1037 # In case the source repository is already cleaned up, the pull
1038 1038 # request will already be deleted.
1039 1039 pull_request = PullRequest().get(self.pull_request_id)
1040 1040 if pull_request:
1041 1041 PullRequestModel().delete(pull_request)
1042 1042 Session().commit()
1043 1043
1044 1044 if self.notification_patcher:
1045 1045 self.notification_patcher.stop()
1046 1046
1047 1047 if self.mergeable_patcher:
1048 1048 self.mergeable_patcher.stop()
1049 1049
1050 1050
1051 1051 @pytest.fixture
1052 1052 def user_admin(pylonsapp):
1053 1053 """
1054 1054 Provides the default admin test user as an instance of `db.User`.
1055 1055 """
1056 1056 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1057 1057 return user
1058 1058
1059 1059
1060 1060 @pytest.fixture
1061 1061 def user_regular(pylonsapp):
1062 1062 """
1063 1063 Provides the default regular test user as an instance of `db.User`.
1064 1064 """
1065 1065 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
1066 1066 return user
1067 1067
1068 1068
1069 1069 @pytest.fixture
1070 1070 def user_util(request, pylonsapp):
1071 1071 """
1072 1072 Provides a wired instance of `UserUtility` with integrated cleanup.
1073 1073 """
1074 1074 utility = UserUtility(test_name=request.node.name)
1075 1075 request.addfinalizer(utility.cleanup)
1076 1076 return utility
1077 1077
1078 1078
1079 1079 # TODO: johbo: Split this up into utilities per domain or something similar
1080 1080 class UserUtility(object):
1081 1081
1082 1082 def __init__(self, test_name="test"):
1083 1083 self._test_name = test_name
1084 1084 self.fixture = Fixture()
1085 1085 self.repo_group_ids = []
1086 1086 self.user_ids = []
1087 1087 self.user_group_ids = []
1088 1088 self.user_repo_permission_ids = []
1089 1089 self.user_group_repo_permission_ids = []
1090 1090 self.user_repo_group_permission_ids = []
1091 1091 self.user_group_repo_group_permission_ids = []
1092 1092 self.user_user_group_permission_ids = []
1093 1093 self.user_group_user_group_permission_ids = []
1094 1094 self.user_permissions = []
1095 1095
1096 1096 def create_repo_group(
1097 1097 self, owner=TEST_USER_ADMIN_LOGIN, auto_cleanup=True):
1098 1098 group_name = "{prefix}_repogroup_{count}".format(
1099 1099 prefix=self._test_name,
1100 1100 count=len(self.repo_group_ids))
1101 1101 repo_group = self.fixture.create_repo_group(
1102 1102 group_name, cur_user=owner)
1103 1103 if auto_cleanup:
1104 1104 self.repo_group_ids.append(repo_group.group_id)
1105 1105 return repo_group
1106 1106
1107 1107 def create_user(self, auto_cleanup=True, **kwargs):
1108 1108 user_name = "{prefix}_user_{count}".format(
1109 1109 prefix=self._test_name,
1110 1110 count=len(self.user_ids))
1111 1111 user = self.fixture.create_user(user_name, **kwargs)
1112 1112 if auto_cleanup:
1113 1113 self.user_ids.append(user.user_id)
1114 1114 return user
1115 1115
1116 1116 def create_user_with_group(self):
1117 1117 user = self.create_user()
1118 1118 user_group = self.create_user_group(members=[user])
1119 1119 return user, user_group
1120 1120
1121 1121 def create_user_group(self, members=None, auto_cleanup=True, **kwargs):
1122 1122 group_name = "{prefix}_usergroup_{count}".format(
1123 1123 prefix=self._test_name,
1124 1124 count=len(self.user_group_ids))
1125 1125 user_group = self.fixture.create_user_group(group_name, **kwargs)
1126 1126 if auto_cleanup:
1127 1127 self.user_group_ids.append(user_group.users_group_id)
1128 1128 if members:
1129 1129 for user in members:
1130 1130 UserGroupModel().add_user_to_group(user_group, user)
1131 1131 return user_group
1132 1132
1133 1133 def grant_user_permission(self, user_name, permission_name):
1134 1134 self._inherit_default_user_permissions(user_name, False)
1135 1135 self.user_permissions.append((user_name, permission_name))
1136 1136
1137 1137 def grant_user_permission_to_repo_group(
1138 1138 self, repo_group, user, permission_name):
1139 1139 permission = RepoGroupModel().grant_user_permission(
1140 1140 repo_group, user, permission_name)
1141 1141 self.user_repo_group_permission_ids.append(
1142 1142 (repo_group.group_id, user.user_id))
1143 1143 return permission
1144 1144
1145 1145 def grant_user_group_permission_to_repo_group(
1146 1146 self, repo_group, user_group, permission_name):
1147 1147 permission = RepoGroupModel().grant_user_group_permission(
1148 1148 repo_group, user_group, permission_name)
1149 1149 self.user_group_repo_group_permission_ids.append(
1150 1150 (repo_group.group_id, user_group.users_group_id))
1151 1151 return permission
1152 1152
1153 1153 def grant_user_permission_to_repo(
1154 1154 self, repo, user, permission_name):
1155 1155 permission = RepoModel().grant_user_permission(
1156 1156 repo, user, permission_name)
1157 1157 self.user_repo_permission_ids.append(
1158 1158 (repo.repo_id, user.user_id))
1159 1159 return permission
1160 1160
1161 1161 def grant_user_group_permission_to_repo(
1162 1162 self, repo, user_group, permission_name):
1163 1163 permission = RepoModel().grant_user_group_permission(
1164 1164 repo, user_group, permission_name)
1165 1165 self.user_group_repo_permission_ids.append(
1166 1166 (repo.repo_id, user_group.users_group_id))
1167 1167 return permission
1168 1168
1169 1169 def grant_user_permission_to_user_group(
1170 1170 self, target_user_group, user, permission_name):
1171 1171 permission = UserGroupModel().grant_user_permission(
1172 1172 target_user_group, user, permission_name)
1173 1173 self.user_user_group_permission_ids.append(
1174 1174 (target_user_group.users_group_id, user.user_id))
1175 1175 return permission
1176 1176
1177 1177 def grant_user_group_permission_to_user_group(
1178 1178 self, target_user_group, user_group, permission_name):
1179 1179 permission = UserGroupModel().grant_user_group_permission(
1180 1180 target_user_group, user_group, permission_name)
1181 1181 self.user_group_user_group_permission_ids.append(
1182 1182 (target_user_group.users_group_id, user_group.users_group_id))
1183 1183 return permission
1184 1184
1185 1185 def revoke_user_permission(self, user_name, permission_name):
1186 1186 self._inherit_default_user_permissions(user_name, True)
1187 1187 UserModel().revoke_perm(user_name, permission_name)
1188 1188
1189 1189 def _inherit_default_user_permissions(self, user_name, value):
1190 1190 user = UserModel().get_by_username(user_name)
1191 1191 user.inherit_default_permissions = value
1192 Session.add(user)
1193 Session.commit()
1192 Session().add(user)
1193 Session().commit()
1194 1194
1195 1195 def cleanup(self):
1196 1196 self._cleanup_permissions()
1197 1197 self._cleanup_repo_groups()
1198 1198 self._cleanup_user_groups()
1199 1199 self._cleanup_users()
1200 1200
1201 1201 def _cleanup_permissions(self):
1202 1202 if self.user_permissions:
1203 1203 for user_name, permission_name in self.user_permissions:
1204 1204 self.revoke_user_permission(user_name, permission_name)
1205 1205
1206 1206 for permission in self.user_repo_permission_ids:
1207 1207 RepoModel().revoke_user_permission(*permission)
1208 1208
1209 1209 for permission in self.user_group_repo_permission_ids:
1210 1210 RepoModel().revoke_user_group_permission(*permission)
1211 1211
1212 1212 for permission in self.user_repo_group_permission_ids:
1213 1213 RepoGroupModel().revoke_user_permission(*permission)
1214 1214
1215 1215 for permission in self.user_group_repo_group_permission_ids:
1216 1216 RepoGroupModel().revoke_user_group_permission(*permission)
1217 1217
1218 1218 for permission in self.user_user_group_permission_ids:
1219 1219 UserGroupModel().revoke_user_permission(*permission)
1220 1220
1221 1221 for permission in self.user_group_user_group_permission_ids:
1222 1222 UserGroupModel().revoke_user_group_permission(*permission)
1223 1223
1224 1224 def _cleanup_repo_groups(self):
1225 1225 def _repo_group_compare(first_group_id, second_group_id):
1226 1226 """
1227 1227 Gives higher priority to the groups with the most complex paths
1228 1228 """
1229 1229 first_group = RepoGroup.get(first_group_id)
1230 1230 second_group = RepoGroup.get(second_group_id)
1231 1231 first_group_parts = (
1232 1232 len(first_group.group_name.split('/')) if first_group else 0)
1233 1233 second_group_parts = (
1234 1234 len(second_group.group_name.split('/')) if second_group else 0)
1235 1235 return cmp(second_group_parts, first_group_parts)
1236 1236
1237 1237 sorted_repo_group_ids = sorted(
1238 1238 self.repo_group_ids, cmp=_repo_group_compare)
1239 1239 for repo_group_id in sorted_repo_group_ids:
1240 1240 self.fixture.destroy_repo_group(repo_group_id)
1241 1241
1242 1242 def _cleanup_user_groups(self):
1243 1243 def _user_group_compare(first_group_id, second_group_id):
1244 1244 """
1245 1245 Gives higher priority to the groups with the most complex paths
1246 1246 """
1247 1247 first_group = UserGroup.get(first_group_id)
1248 1248 second_group = UserGroup.get(second_group_id)
1249 1249 first_group_parts = (
1250 1250 len(first_group.users_group_name.split('/'))
1251 1251 if first_group else 0)
1252 1252 second_group_parts = (
1253 1253 len(second_group.users_group_name.split('/'))
1254 1254 if second_group else 0)
1255 1255 return cmp(second_group_parts, first_group_parts)
1256 1256
1257 1257 sorted_user_group_ids = sorted(
1258 1258 self.user_group_ids, cmp=_user_group_compare)
1259 1259 for user_group_id in sorted_user_group_ids:
1260 1260 self.fixture.destroy_user_group(user_group_id)
1261 1261
1262 1262 def _cleanup_users(self):
1263 1263 for user_id in self.user_ids:
1264 1264 self.fixture.destroy_user(user_id)
1265 1265
1266 1266
1267 1267 # TODO: Think about moving this into a pytest-pyro package and make it a
1268 1268 # pytest plugin
1269 1269 @pytest.hookimpl(tryfirst=True, hookwrapper=True)
1270 1270 def pytest_runtest_makereport(item, call):
1271 1271 """
1272 1272 Adding the remote traceback if the exception has this information.
1273 1273
1274 1274 Pyro4 attaches this information as the attribute `_pyroTraceback`
1275 1275 to the exception instance.
1276 1276 """
1277 1277 outcome = yield
1278 1278 report = outcome.get_result()
1279 1279 if call.excinfo:
1280 1280 _add_pyro_remote_traceback(report, call.excinfo.value)
1281 1281
1282 1282
1283 1283 def _add_pyro_remote_traceback(report, exc):
1284 1284 pyro_traceback = getattr(exc, '_pyroTraceback', None)
1285 1285
1286 1286 if pyro_traceback:
1287 1287 traceback = ''.join(pyro_traceback)
1288 1288 section = 'Pyro4 remote traceback ' + report.when
1289 1289 report.sections.append((section, traceback))
1290 1290
1291 1291
1292 1292 @pytest.fixture(scope='session')
1293 1293 def testrun():
1294 1294 return {
1295 1295 'uuid': uuid.uuid4(),
1296 1296 'start': datetime.datetime.utcnow().isoformat(),
1297 1297 'timestamp': int(time.time()),
1298 1298 }
1299 1299
1300 1300
1301 1301 @pytest.fixture(autouse=True)
1302 1302 def collect_appenlight_stats(request, testrun):
1303 1303 """
1304 1304 This fixture reports memory consumtion of single tests.
1305 1305
1306 1306 It gathers data based on `psutil` and sends them to Appenlight. The option
1307 1307 ``--ae`` has te be used to enable this fixture and the API key for your
1308 1308 application has to be provided in ``--ae-key``.
1309 1309 """
1310 1310 try:
1311 1311 # cygwin cannot have yet psutil support.
1312 1312 import psutil
1313 1313 except ImportError:
1314 1314 return
1315 1315
1316 1316 if not request.config.getoption('--appenlight'):
1317 1317 return
1318 1318 else:
1319 1319 # Only request the pylonsapp fixture if appenlight tracking is
1320 1320 # enabled. This will speed up a test run of unit tests by 2 to 3
1321 1321 # seconds if appenlight is not enabled.
1322 1322 pylonsapp = request.getfuncargvalue("pylonsapp")
1323 1323 url = '{}/api/logs'.format(request.config.getoption('--appenlight-url'))
1324 1324 client = AppenlightClient(
1325 1325 url=url,
1326 1326 api_key=request.config.getoption('--appenlight-api-key'),
1327 1327 namespace=request.node.nodeid,
1328 1328 request=str(testrun['uuid']),
1329 1329 testrun=testrun)
1330 1330
1331 1331 client.collect({
1332 1332 'message': "Starting",
1333 1333 })
1334 1334
1335 1335 server_and_port = pylonsapp.config['vcs.server']
1336 1336 server = create_vcsserver_proxy(server_and_port)
1337 1337 with server:
1338 1338 vcs_pid = server.get_pid()
1339 1339 server.run_gc()
1340 1340 vcs_process = psutil.Process(vcs_pid)
1341 1341 mem = vcs_process.memory_info()
1342 1342 client.tag_before('vcsserver.rss', mem.rss)
1343 1343 client.tag_before('vcsserver.vms', mem.vms)
1344 1344
1345 1345 test_process = psutil.Process()
1346 1346 mem = test_process.memory_info()
1347 1347 client.tag_before('test.rss', mem.rss)
1348 1348 client.tag_before('test.vms', mem.vms)
1349 1349
1350 1350 client.tag_before('time', time.time())
1351 1351
1352 1352 @request.addfinalizer
1353 1353 def send_stats():
1354 1354 client.tag_after('time', time.time())
1355 1355 with server:
1356 1356 gc_stats = server.run_gc()
1357 1357 for tag, value in gc_stats.items():
1358 1358 client.tag_after(tag, value)
1359 1359 mem = vcs_process.memory_info()
1360 1360 client.tag_after('vcsserver.rss', mem.rss)
1361 1361 client.tag_after('vcsserver.vms', mem.vms)
1362 1362
1363 1363 mem = test_process.memory_info()
1364 1364 client.tag_after('test.rss', mem.rss)
1365 1365 client.tag_after('test.vms', mem.vms)
1366 1366
1367 1367 client.collect({
1368 1368 'message': "Finished",
1369 1369 })
1370 1370 client.send_stats()
1371 1371
1372 1372 return client
1373 1373
1374 1374
1375 1375 class AppenlightClient():
1376 1376
1377 1377 url_template = '{url}?protocol_version=0.5'
1378 1378
1379 1379 def __init__(
1380 1380 self, url, api_key, add_server=True, add_timestamp=True,
1381 1381 namespace=None, request=None, testrun=None):
1382 1382 self.url = self.url_template.format(url=url)
1383 1383 self.api_key = api_key
1384 1384 self.add_server = add_server
1385 1385 self.add_timestamp = add_timestamp
1386 1386 self.namespace = namespace
1387 1387 self.request = request
1388 1388 self.server = socket.getfqdn(socket.gethostname())
1389 1389 self.tags_before = {}
1390 1390 self.tags_after = {}
1391 1391 self.stats = []
1392 1392 self.testrun = testrun or {}
1393 1393
1394 1394 def tag_before(self, tag, value):
1395 1395 self.tags_before[tag] = value
1396 1396
1397 1397 def tag_after(self, tag, value):
1398 1398 self.tags_after[tag] = value
1399 1399
1400 1400 def collect(self, data):
1401 1401 if self.add_server:
1402 1402 data.setdefault('server', self.server)
1403 1403 if self.add_timestamp:
1404 1404 data.setdefault('date', datetime.datetime.utcnow().isoformat())
1405 1405 if self.namespace:
1406 1406 data.setdefault('namespace', self.namespace)
1407 1407 if self.request:
1408 1408 data.setdefault('request', self.request)
1409 1409 self.stats.append(data)
1410 1410
1411 1411 def send_stats(self):
1412 1412 tags = [
1413 1413 ('testrun', self.request),
1414 1414 ('testrun.start', self.testrun['start']),
1415 1415 ('testrun.timestamp', self.testrun['timestamp']),
1416 1416 ('test', self.namespace),
1417 1417 ]
1418 1418 for key, value in self.tags_before.items():
1419 1419 tags.append((key + '.before', value))
1420 1420 try:
1421 1421 delta = self.tags_after[key] - value
1422 1422 tags.append((key + '.delta', delta))
1423 1423 except Exception:
1424 1424 pass
1425 1425 for key, value in self.tags_after.items():
1426 1426 tags.append((key + '.after', value))
1427 1427 self.collect({
1428 1428 'message': "Collected tags",
1429 1429 'tags': tags,
1430 1430 })
1431 1431
1432 1432 response = requests.post(
1433 1433 self.url,
1434 1434 headers={
1435 1435 'X-appenlight-api-key': self.api_key},
1436 1436 json=self.stats,
1437 1437 )
1438 1438
1439 1439 if not response.status_code == 200:
1440 1440 pprint.pprint(self.stats)
1441 1441 print response.headers
1442 1442 print response.text
1443 1443 raise Exception('Sending to appenlight failed')
1444 1444
1445 1445
1446 1446 @pytest.fixture
1447 1447 def gist_util(request, pylonsapp):
1448 1448 """
1449 1449 Provides a wired instance of `GistUtility` with integrated cleanup.
1450 1450 """
1451 1451 utility = GistUtility()
1452 1452 request.addfinalizer(utility.cleanup)
1453 1453 return utility
1454 1454
1455 1455
1456 1456 class GistUtility(object):
1457 1457 def __init__(self):
1458 1458 self.fixture = Fixture()
1459 1459 self.gist_ids = []
1460 1460
1461 1461 def create_gist(self, **kwargs):
1462 1462 gist = self.fixture.create_gist(**kwargs)
1463 1463 self.gist_ids.append(gist.gist_id)
1464 1464 return gist
1465 1465
1466 1466 def cleanup(self):
1467 1467 for id_ in self.gist_ids:
1468 1468 self.fixture.destroy_gists(str(id_))
1469 1469
1470 1470
1471 1471 @pytest.fixture
1472 1472 def enabled_backends(request):
1473 1473 backends = request.config.option.backends
1474 1474 return backends[:]
1475 1475
1476 1476
1477 1477 @pytest.fixture
1478 1478 def settings_util(request):
1479 1479 """
1480 1480 Provides a wired instance of `SettingsUtility` with integrated cleanup.
1481 1481 """
1482 1482 utility = SettingsUtility()
1483 1483 request.addfinalizer(utility.cleanup)
1484 1484 return utility
1485 1485
1486 1486
1487 1487 class SettingsUtility(object):
1488 1488 def __init__(self):
1489 1489 self.rhodecode_ui_ids = []
1490 1490 self.rhodecode_setting_ids = []
1491 1491 self.repo_rhodecode_ui_ids = []
1492 1492 self.repo_rhodecode_setting_ids = []
1493 1493
1494 1494 def create_repo_rhodecode_ui(
1495 1495 self, repo, section, value, key=None, active=True, cleanup=True):
1496 1496 key = key or hashlib.sha1(
1497 1497 '{}{}{}'.format(section, value, repo.repo_id)).hexdigest()
1498 1498
1499 1499 setting = RepoRhodeCodeUi()
1500 1500 setting.repository_id = repo.repo_id
1501 1501 setting.ui_section = section
1502 1502 setting.ui_value = value
1503 1503 setting.ui_key = key
1504 1504 setting.ui_active = active
1505 1505 Session().add(setting)
1506 1506 Session().commit()
1507 1507
1508 1508 if cleanup:
1509 1509 self.repo_rhodecode_ui_ids.append(setting.ui_id)
1510 1510 return setting
1511 1511
1512 1512 def create_rhodecode_ui(
1513 1513 self, section, value, key=None, active=True, cleanup=True):
1514 1514 key = key or hashlib.sha1('{}{}'.format(section, value)).hexdigest()
1515 1515
1516 1516 setting = RhodeCodeUi()
1517 1517 setting.ui_section = section
1518 1518 setting.ui_value = value
1519 1519 setting.ui_key = key
1520 1520 setting.ui_active = active
1521 1521 Session().add(setting)
1522 1522 Session().commit()
1523 1523
1524 1524 if cleanup:
1525 1525 self.rhodecode_ui_ids.append(setting.ui_id)
1526 1526 return setting
1527 1527
1528 1528 def create_repo_rhodecode_setting(
1529 1529 self, repo, name, value, type_, cleanup=True):
1530 1530 setting = RepoRhodeCodeSetting(
1531 1531 repo.repo_id, key=name, val=value, type=type_)
1532 1532 Session().add(setting)
1533 1533 Session().commit()
1534 1534
1535 1535 if cleanup:
1536 1536 self.repo_rhodecode_setting_ids.append(setting.app_settings_id)
1537 1537 return setting
1538 1538
1539 1539 def create_rhodecode_setting(self, name, value, type_, cleanup=True):
1540 1540 setting = RhodeCodeSetting(key=name, val=value, type=type_)
1541 1541 Session().add(setting)
1542 1542 Session().commit()
1543 1543
1544 1544 if cleanup:
1545 1545 self.rhodecode_setting_ids.append(setting.app_settings_id)
1546 1546
1547 1547 return setting
1548 1548
1549 1549 def cleanup(self):
1550 1550 for id_ in self.rhodecode_ui_ids:
1551 1551 setting = RhodeCodeUi.get(id_)
1552 1552 Session().delete(setting)
1553 1553
1554 1554 for id_ in self.rhodecode_setting_ids:
1555 1555 setting = RhodeCodeSetting.get(id_)
1556 1556 Session().delete(setting)
1557 1557
1558 1558 for id_ in self.repo_rhodecode_ui_ids:
1559 1559 setting = RepoRhodeCodeUi.get(id_)
1560 1560 Session().delete(setting)
1561 1561
1562 1562 for id_ in self.repo_rhodecode_setting_ids:
1563 1563 setting = RepoRhodeCodeSetting.get(id_)
1564 1564 Session().delete(setting)
1565 1565
1566 1566 Session().commit()
1567 1567
1568 1568
1569 1569 @pytest.fixture
1570 1570 def no_notifications(request):
1571 1571 notification_patcher = mock.patch(
1572 1572 'rhodecode.model.notification.NotificationModel.create')
1573 1573 notification_patcher.start()
1574 1574 request.addfinalizer(notification_patcher.stop)
1575 1575
1576 1576
1577 1577 @pytest.fixture
1578 1578 def silence_action_logger(request):
1579 1579 notification_patcher = mock.patch(
1580 1580 'rhodecode.lib.utils.action_logger')
1581 1581 notification_patcher.start()
1582 1582 request.addfinalizer(notification_patcher.stop)
1583 1583
1584 1584
1585 1585 @pytest.fixture(scope='session')
1586 1586 def repeat(request):
1587 1587 """
1588 1588 The number of repetitions is based on this fixture.
1589 1589
1590 1590 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
1591 1591 tests are not too slow in our default test suite.
1592 1592 """
1593 1593 return request.config.getoption('--repeat')
1594 1594
1595 1595
1596 1596 @pytest.fixture
1597 1597 def rhodecode_fixtures():
1598 1598 return Fixture()
1599 1599
1600 1600
1601 1601 @pytest.fixture
1602 1602 def request_stub():
1603 1603 """
1604 1604 Stub request object.
1605 1605 """
1606 1606 request = pyramid.testing.DummyRequest()
1607 1607 request.scheme = 'https'
1608 1608 return request
1609 1609
1610 1610
1611 1611 @pytest.fixture
1612 1612 def config_stub(request, request_stub):
1613 1613 """
1614 1614 Set up pyramid.testing and return the Configurator.
1615 1615 """
1616 1616 config = pyramid.testing.setUp(request=request_stub)
1617 1617
1618 1618 @request.addfinalizer
1619 1619 def cleanup():
1620 1620 pyramid.testing.tearDown()
1621 1621
1622 1622 return config
General Comments 0
You need to be logged in to leave comments. Login now