##// END OF EJS Templates
vcs: moved svn proxy settings into vcs related settings...
marcink -
r754:f39fd7f4 default
parent child Browse files
Show More
@@ -1,793 +1,796 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 """
23 23 settings controller for rhodecode admin
24 24 """
25 25
26 26 import collections
27 27 import logging
28 28 import urllib2
29 29
30 30 import datetime
31 31 import formencode
32 32 from formencode import htmlfill
33 33 import packaging.version
34 34 from pylons import request, tmpl_context as c, url, config
35 35 from pylons.controllers.util import redirect
36 36 from pylons.i18n.translation import _, lazy_ugettext
37 37 from webob.exc import HTTPBadRequest
38 38
39 39 import rhodecode
40 40 from rhodecode.admin.navigation import navigation_list
41 41 from rhodecode.lib import auth
42 42 from rhodecode.lib import helpers as h
43 43 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
44 44 from rhodecode.lib.base import BaseController, render
45 45 from rhodecode.lib.celerylib import tasks, run_task
46 46 from rhodecode.lib.utils import repo2db_mapper
47 47 from rhodecode.lib.utils2 import (
48 48 str2bool, safe_unicode, AttributeDict, safe_int)
49 49 from rhodecode.lib.compat import OrderedDict
50 50 from rhodecode.lib.ext_json import json
51 51 from rhodecode.lib.utils import jsonify
52 52
53 53 from rhodecode.model.db import RhodeCodeUi, Repository
54 54 from rhodecode.model.forms import ApplicationSettingsForm, \
55 55 ApplicationUiSettingsForm, ApplicationVisualisationForm, \
56 56 LabsSettingsForm, IssueTrackerPatternsForm
57 57
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.notification import EmailNotificationModel
60 60 from rhodecode.model.meta import Session
61 61 from rhodecode.model.settings import (
62 62 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
63 63 SettingsModel)
64 64
65 65 from rhodecode.model.supervisor import SupervisorModel, SUPERVISOR_MASTER
66 66
67 67
68 68 log = logging.getLogger(__name__)
69 69
70 70
71 71 class SettingsController(BaseController):
72 72 """REST Controller styled on the Atom Publishing Protocol"""
73 73 # To properly map this controller, ensure your config/routing.py
74 74 # file has a resource setup:
75 75 # map.resource('setting', 'settings', controller='admin/settings',
76 76 # path_prefix='/admin', name_prefix='admin_')
77 77
78 78 @LoginRequired()
79 79 def __before__(self):
80 80 super(SettingsController, self).__before__()
81 81 c.labs_active = str2bool(
82 82 rhodecode.CONFIG.get('labs_settings_active', 'true'))
83 83 c.navlist = navigation_list(request)
84 84
85 85 def _get_hg_ui_settings(self):
86 86 ret = RhodeCodeUi.query().all()
87 87
88 88 if not ret:
89 89 raise Exception('Could not get application ui settings !')
90 90 settings = {}
91 91 for each in ret:
92 92 k = each.ui_key
93 93 v = each.ui_value
94 94 if k == '/':
95 95 k = 'root_path'
96 96
97 97 if k in ['push_ssl', 'publish']:
98 98 v = str2bool(v)
99 99
100 100 if k.find('.') != -1:
101 101 k = k.replace('.', '_')
102 102
103 103 if each.ui_section in ['hooks', 'extensions']:
104 104 v = each.ui_active
105 105
106 106 settings[each.ui_section + '_' + k] = v
107 107 return settings
108 108
109 109 @HasPermissionAllDecorator('hg.admin')
110 110 @auth.CSRFRequired()
111 111 @jsonify
112 112 def delete_svn_pattern(self):
113 113 if not request.is_xhr:
114 114 raise HTTPBadRequest()
115 115
116 116 delete_pattern_id = request.POST.get('delete_svn_pattern')
117 117 model = VcsSettingsModel()
118 118 try:
119 119 model.delete_global_svn_pattern(delete_pattern_id)
120 120 except SettingNotFound:
121 121 raise HTTPBadRequest()
122 122
123 123 Session().commit()
124 124 return True
125 125
126 126 @HasPermissionAllDecorator('hg.admin')
127 127 @auth.CSRFRequired()
128 128 def settings_vcs_update(self):
129 129 """POST /admin/settings: All items in the collection"""
130 130 # url('admin_settings_vcs')
131 131 c.active = 'vcs'
132 132
133 133 model = VcsSettingsModel()
134 134 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
135 135 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
136 136
137 137 application_form = ApplicationUiSettingsForm()()
138
138 139 try:
139 140 form_result = application_form.to_python(dict(request.POST))
140 141 except formencode.Invalid as errors:
141 142 h.flash(
142 143 _("Some form inputs contain invalid data."),
143 144 category='error')
144 145 return htmlfill.render(
145 146 render('admin/settings/settings.html'),
146 147 defaults=errors.value,
147 148 errors=errors.error_dict or {},
148 149 prefix_error=False,
149 150 encoding="UTF-8",
150 151 force_defaults=False
151 152 )
152 153
153 154 try:
154 model.update_global_ssl_setting(form_result['web_push_ssl'])
155 155 if c.visual.allow_repo_location_change:
156 156 model.update_global_path_setting(
157 157 form_result['paths_root_path'])
158
159 model.update_global_ssl_setting(form_result['web_push_ssl'])
158 160 model.update_global_hook_settings(form_result)
159 model.create_global_svn_settings(form_result)
161
162 model.create_or_update_global_svn_settings(form_result)
160 163 model.create_or_update_global_hg_settings(form_result)
161 164 model.create_or_update_global_pr_settings(form_result)
162 165 except Exception:
163 166 log.exception("Exception while updating settings")
164 167 h.flash(_('Error occurred during updating '
165 168 'application settings'), category='error')
166 169 else:
167 170 Session().commit()
168 171 h.flash(_('Updated VCS settings'), category='success')
169 172 return redirect(url('admin_settings_vcs'))
170 173
171 174 return htmlfill.render(
172 175 render('admin/settings/settings.html'),
173 176 defaults=self._form_defaults(),
174 177 encoding="UTF-8",
175 178 force_defaults=False)
176 179
177 180 @HasPermissionAllDecorator('hg.admin')
178 181 def settings_vcs(self):
179 182 """GET /admin/settings: All items in the collection"""
180 183 # url('admin_settings_vcs')
181 184 c.active = 'vcs'
182 185 model = VcsSettingsModel()
183 186 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
184 187 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
185 188
186 189 return htmlfill.render(
187 190 render('admin/settings/settings.html'),
188 191 defaults=self._form_defaults(),
189 192 encoding="UTF-8",
190 193 force_defaults=False)
191 194
192 195 @HasPermissionAllDecorator('hg.admin')
193 196 @auth.CSRFRequired()
194 197 def settings_mapping_update(self):
195 198 """POST /admin/settings/mapping: All items in the collection"""
196 199 # url('admin_settings_mapping')
197 200 c.active = 'mapping'
198 201 rm_obsolete = request.POST.get('destroy', False)
199 202 invalidate_cache = request.POST.get('invalidate', False)
200 203 log.debug(
201 204 'rescanning repo location with destroy obsolete=%s', rm_obsolete)
202 205
203 206 if invalidate_cache:
204 207 log.debug('invalidating all repositories cache')
205 208 for repo in Repository.get_all():
206 209 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
207 210
208 211 filesystem_repos = ScmModel().repo_scan()
209 212 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
210 213 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
211 214 h.flash(_('Repositories successfully '
212 215 'rescanned added: %s ; removed: %s') %
213 216 (_repr(added), _repr(removed)),
214 217 category='success')
215 218 return redirect(url('admin_settings_mapping'))
216 219
217 220 @HasPermissionAllDecorator('hg.admin')
218 221 def settings_mapping(self):
219 222 """GET /admin/settings/mapping: All items in the collection"""
220 223 # url('admin_settings_mapping')
221 224 c.active = 'mapping'
222 225
223 226 return htmlfill.render(
224 227 render('admin/settings/settings.html'),
225 228 defaults=self._form_defaults(),
226 229 encoding="UTF-8",
227 230 force_defaults=False)
228 231
229 232 @HasPermissionAllDecorator('hg.admin')
230 233 @auth.CSRFRequired()
231 234 def settings_global_update(self):
232 235 """POST /admin/settings/global: All items in the collection"""
233 236 # url('admin_settings_global')
234 237 c.active = 'global'
235 238 application_form = ApplicationSettingsForm()()
236 239 try:
237 240 form_result = application_form.to_python(dict(request.POST))
238 241 except formencode.Invalid as errors:
239 242 return htmlfill.render(
240 243 render('admin/settings/settings.html'),
241 244 defaults=errors.value,
242 245 errors=errors.error_dict or {},
243 246 prefix_error=False,
244 247 encoding="UTF-8",
245 248 force_defaults=False)
246 249
247 250 try:
248 251 settings = [
249 252 ('title', 'rhodecode_title'),
250 253 ('realm', 'rhodecode_realm'),
251 254 ('pre_code', 'rhodecode_pre_code'),
252 255 ('post_code', 'rhodecode_post_code'),
253 256 ('captcha_public_key', 'rhodecode_captcha_public_key'),
254 257 ('captcha_private_key', 'rhodecode_captcha_private_key'),
255 258 ]
256 259 for setting, form_key in settings:
257 260 sett = SettingsModel().create_or_update_setting(
258 261 setting, form_result[form_key])
259 262 Session().add(sett)
260 263
261 264 Session().commit()
262 265 SettingsModel().invalidate_settings_cache()
263 266 h.flash(_('Updated application settings'), category='success')
264 267 except Exception:
265 268 log.exception("Exception while updating application settings")
266 269 h.flash(
267 270 _('Error occurred during updating application settings'),
268 271 category='error')
269 272
270 273 return redirect(url('admin_settings_global'))
271 274
272 275 @HasPermissionAllDecorator('hg.admin')
273 276 def settings_global(self):
274 277 """GET /admin/settings/global: All items in the collection"""
275 278 # url('admin_settings_global')
276 279 c.active = 'global'
277 280
278 281 return htmlfill.render(
279 282 render('admin/settings/settings.html'),
280 283 defaults=self._form_defaults(),
281 284 encoding="UTF-8",
282 285 force_defaults=False)
283 286
284 287 @HasPermissionAllDecorator('hg.admin')
285 288 @auth.CSRFRequired()
286 289 def settings_visual_update(self):
287 290 """POST /admin/settings/visual: All items in the collection"""
288 291 # url('admin_settings_visual')
289 292 c.active = 'visual'
290 293 application_form = ApplicationVisualisationForm()()
291 294 try:
292 295 form_result = application_form.to_python(dict(request.POST))
293 296 except formencode.Invalid as errors:
294 297 return htmlfill.render(
295 298 render('admin/settings/settings.html'),
296 299 defaults=errors.value,
297 300 errors=errors.error_dict or {},
298 301 prefix_error=False,
299 302 encoding="UTF-8",
300 303 force_defaults=False
301 304 )
302 305
303 306 try:
304 307 settings = [
305 308 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
306 309 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
307 310 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
308 311 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
309 312 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
310 313 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
311 314 ('show_version', 'rhodecode_show_version', 'bool'),
312 315 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
313 316 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
314 317 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
315 318 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
316 319 ('support_url', 'rhodecode_support_url', 'unicode'),
317 320 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
318 321 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
319 322 ]
320 323 for setting, form_key, type_ in settings:
321 324 sett = SettingsModel().create_or_update_setting(
322 325 setting, form_result[form_key], type_)
323 326 Session().add(sett)
324 327
325 328 Session().commit()
326 329 SettingsModel().invalidate_settings_cache()
327 330 h.flash(_('Updated visualisation settings'), category='success')
328 331 except Exception:
329 332 log.exception("Exception updating visualization settings")
330 333 h.flash(_('Error occurred during updating '
331 334 'visualisation settings'),
332 335 category='error')
333 336
334 337 return redirect(url('admin_settings_visual'))
335 338
336 339 @HasPermissionAllDecorator('hg.admin')
337 340 def settings_visual(self):
338 341 """GET /admin/settings/visual: All items in the collection"""
339 342 # url('admin_settings_visual')
340 343 c.active = 'visual'
341 344
342 345 return htmlfill.render(
343 346 render('admin/settings/settings.html'),
344 347 defaults=self._form_defaults(),
345 348 encoding="UTF-8",
346 349 force_defaults=False)
347 350
348 351 @HasPermissionAllDecorator('hg.admin')
349 352 @auth.CSRFRequired()
350 353 def settings_issuetracker_test(self):
351 354 if request.is_xhr:
352 355 return h.urlify_commit_message(
353 356 request.POST.get('test_text', ''),
354 357 'repo_group/test_repo1')
355 358 else:
356 359 raise HTTPBadRequest()
357 360
358 361 @HasPermissionAllDecorator('hg.admin')
359 362 @auth.CSRFRequired()
360 363 def settings_issuetracker_delete(self):
361 364 uid = request.POST.get('uid')
362 365 IssueTrackerSettingsModel().delete_entries(uid)
363 366 h.flash(_('Removed issue tracker entry'), category='success')
364 367 return redirect(url('admin_settings_issuetracker'))
365 368
366 369 @HasPermissionAllDecorator('hg.admin')
367 370 def settings_issuetracker(self):
368 371 """GET /admin/settings/issue-tracker: All items in the collection"""
369 372 # url('admin_settings_issuetracker')
370 373 c.active = 'issuetracker'
371 374 defaults = SettingsModel().get_all_settings()
372 375
373 376 entry_key = 'rhodecode_issuetracker_pat_'
374 377
375 378 c.issuetracker_entries = {}
376 379 for k, v in defaults.items():
377 380 if k.startswith(entry_key):
378 381 uid = k[len(entry_key):]
379 382 c.issuetracker_entries[uid] = None
380 383
381 384 for uid in c.issuetracker_entries:
382 385 c.issuetracker_entries[uid] = AttributeDict({
383 386 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
384 387 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
385 388 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
386 389 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
387 390 })
388 391
389 392 return render('admin/settings/settings.html')
390 393
391 394 @HasPermissionAllDecorator('hg.admin')
392 395 @auth.CSRFRequired()
393 396 def settings_issuetracker_save(self):
394 397 settings_model = IssueTrackerSettingsModel()
395 398
396 399 form = IssueTrackerPatternsForm()().to_python(request.POST)
397 400 for uid in form['delete_patterns']:
398 401 settings_model.delete_entries(uid)
399 402
400 403 for pattern in form['patterns']:
401 404 for setting, value, type_ in pattern:
402 405 sett = settings_model.create_or_update_setting(
403 406 setting, value, type_)
404 407 Session().add(sett)
405 408
406 409 Session().commit()
407 410
408 411 SettingsModel().invalidate_settings_cache()
409 412 h.flash(_('Updated issue tracker entries'), category='success')
410 413 return redirect(url('admin_settings_issuetracker'))
411 414
412 415 @HasPermissionAllDecorator('hg.admin')
413 416 @auth.CSRFRequired()
414 417 def settings_email_update(self):
415 418 """POST /admin/settings/email: All items in the collection"""
416 419 # url('admin_settings_email')
417 420 c.active = 'email'
418 421
419 422 test_email = request.POST.get('test_email')
420 423
421 424 if not test_email:
422 425 h.flash(_('Please enter email address'), category='error')
423 426 return redirect(url('admin_settings_email'))
424 427
425 428 email_kwargs = {
426 429 'date': datetime.datetime.now(),
427 430 'user': c.rhodecode_user,
428 431 'rhodecode_version': c.rhodecode_version
429 432 }
430 433
431 434 (subject, headers, email_body,
432 435 email_body_plaintext) = EmailNotificationModel().render_email(
433 436 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
434 437
435 438 recipients = [test_email] if test_email else None
436 439
437 440 run_task(tasks.send_email, recipients, subject,
438 441 email_body_plaintext, email_body)
439 442
440 443 h.flash(_('Send email task created'), category='success')
441 444 return redirect(url('admin_settings_email'))
442 445
443 446 @HasPermissionAllDecorator('hg.admin')
444 447 def settings_email(self):
445 448 """GET /admin/settings/email: All items in the collection"""
446 449 # url('admin_settings_email')
447 450 c.active = 'email'
448 451 c.rhodecode_ini = rhodecode.CONFIG
449 452
450 453 return htmlfill.render(
451 454 render('admin/settings/settings.html'),
452 455 defaults=self._form_defaults(),
453 456 encoding="UTF-8",
454 457 force_defaults=False)
455 458
456 459 @HasPermissionAllDecorator('hg.admin')
457 460 @auth.CSRFRequired()
458 461 def settings_hooks_update(self):
459 462 """POST or DELETE /admin/settings/hooks: All items in the collection"""
460 463 # url('admin_settings_hooks')
461 464 c.active = 'hooks'
462 465 if c.visual.allow_custom_hooks_settings:
463 466 ui_key = request.POST.get('new_hook_ui_key')
464 467 ui_value = request.POST.get('new_hook_ui_value')
465 468
466 469 hook_id = request.POST.get('hook_id')
467 470 new_hook = False
468 471
469 472 model = SettingsModel()
470 473 try:
471 474 if ui_value and ui_key:
472 475 model.create_or_update_hook(ui_key, ui_value)
473 476 h.flash(_('Added new hook'), category='success')
474 477 new_hook = True
475 478 elif hook_id:
476 479 RhodeCodeUi.delete(hook_id)
477 480 Session().commit()
478 481
479 482 # check for edits
480 483 update = False
481 484 _d = request.POST.dict_of_lists()
482 485 for k, v in zip(_d.get('hook_ui_key', []),
483 486 _d.get('hook_ui_value_new', [])):
484 487 model.create_or_update_hook(k, v)
485 488 update = True
486 489
487 490 if update and not new_hook:
488 491 h.flash(_('Updated hooks'), category='success')
489 492 Session().commit()
490 493 except Exception:
491 494 log.exception("Exception during hook creation")
492 495 h.flash(_('Error occurred during hook creation'),
493 496 category='error')
494 497
495 498 return redirect(url('admin_settings_hooks'))
496 499
497 500 @HasPermissionAllDecorator('hg.admin')
498 501 def settings_hooks(self):
499 502 """GET /admin/settings/hooks: All items in the collection"""
500 503 # url('admin_settings_hooks')
501 504 c.active = 'hooks'
502 505
503 506 model = SettingsModel()
504 507 c.hooks = model.get_builtin_hooks()
505 508 c.custom_hooks = model.get_custom_hooks()
506 509
507 510 return htmlfill.render(
508 511 render('admin/settings/settings.html'),
509 512 defaults=self._form_defaults(),
510 513 encoding="UTF-8",
511 514 force_defaults=False)
512 515
513 516 @HasPermissionAllDecorator('hg.admin')
514 517 def settings_search(self):
515 518 """GET /admin/settings/search: All items in the collection"""
516 519 # url('admin_settings_search')
517 520 c.active = 'search'
518 521
519 522 from rhodecode.lib.index import searcher_from_config
520 523 searcher = searcher_from_config(config)
521 524 c.statistics = searcher.statistics()
522 525
523 526 return render('admin/settings/settings.html')
524 527
525 528 @HasPermissionAllDecorator('hg.admin')
526 529 def settings_system(self):
527 530 """GET /admin/settings/system: All items in the collection"""
528 531 # url('admin_settings_system')
529 532 snapshot = str2bool(request.GET.get('snapshot'))
530 533 c.active = 'system'
531 534
532 535 defaults = self._form_defaults()
533 536 c.rhodecode_ini = rhodecode.CONFIG
534 537 c.rhodecode_update_url = defaults.get('rhodecode_update_url')
535 538 server_info = ScmModel().get_server_info(request.environ)
536 539 for key, val in server_info.iteritems():
537 540 setattr(c, key, val)
538 541
539 542 if c.disk['percent'] > 90:
540 543 h.flash(h.literal(_(
541 544 'Critical: your disk space is very low <b>%s%%</b> used' %
542 545 c.disk['percent'])), 'error')
543 546 elif c.disk['percent'] > 70:
544 547 h.flash(h.literal(_(
545 548 'Warning: your disk space is running low <b>%s%%</b> used' %
546 549 c.disk['percent'])), 'warning')
547 550
548 551 try:
549 552 c.uptime_age = h._age(
550 553 h.time_to_datetime(c.boot_time), False, show_suffix=False)
551 554 except TypeError:
552 555 c.uptime_age = c.boot_time
553 556
554 557 try:
555 558 c.system_memory = '%s/%s, %s%% (%s%%) used%s' % (
556 559 h.format_byte_size_binary(c.memory['used']),
557 560 h.format_byte_size_binary(c.memory['total']),
558 561 c.memory['percent2'],
559 562 c.memory['percent'],
560 563 ' %s' % c.memory['error'] if 'error' in c.memory else '')
561 564 except TypeError:
562 565 c.system_memory = 'NOT AVAILABLE'
563 566
564 567 rhodecode_ini_safe = rhodecode.CONFIG.copy()
565 568 blacklist = [
566 569 'rhodecode_license_key',
567 570 'routes.map',
568 571 'pylons.h',
569 572 'pylons.app_globals',
570 573 'pylons.environ_config',
571 574 'sqlalchemy.db1.url',
572 575 ('app_conf', 'sqlalchemy.db1.url')
573 576 ]
574 577 for k in blacklist:
575 578 if isinstance(k, tuple):
576 579 section, key = k
577 580 if section in rhodecode_ini_safe:
578 581 rhodecode_ini_safe[section].pop(key, None)
579 582 else:
580 583 rhodecode_ini_safe.pop(k, None)
581 584
582 585 c.rhodecode_ini_safe = rhodecode_ini_safe
583 586
584 587 # TODO: marcink, figure out how to allow only selected users to do this
585 588 c.allowed_to_snapshot = False
586 589
587 590 if snapshot:
588 591 if c.allowed_to_snapshot:
589 592 return render('admin/settings/settings_system_snapshot.html')
590 593 else:
591 594 h.flash('You are not allowed to do this', category='warning')
592 595
593 596 return htmlfill.render(
594 597 render('admin/settings/settings.html'),
595 598 defaults=defaults,
596 599 encoding="UTF-8",
597 600 force_defaults=False)
598 601
599 602 @staticmethod
600 603 def get_update_data(update_url):
601 604 """Return the JSON update data."""
602 605 ver = rhodecode.__version__
603 606 log.debug('Checking for upgrade on `%s` server', update_url)
604 607 opener = urllib2.build_opener()
605 608 opener.addheaders = [('User-agent', 'RhodeCode-SCM/%s' % ver)]
606 609 response = opener.open(update_url)
607 610 response_data = response.read()
608 611 data = json.loads(response_data)
609 612
610 613 return data
611 614
612 615 @HasPermissionAllDecorator('hg.admin')
613 616 def settings_system_update(self):
614 617 """GET /admin/settings/system/updates: All items in the collection"""
615 618 # url('admin_settings_system_update')
616 619 defaults = self._form_defaults()
617 620 update_url = defaults.get('rhodecode_update_url', '')
618 621
619 622 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">%s</div>' % (s)
620 623 try:
621 624 data = self.get_update_data(update_url)
622 625 except urllib2.URLError as e:
623 626 log.exception("Exception contacting upgrade server")
624 627 return _err('Failed to contact upgrade server: %r' % e)
625 628 except ValueError as e:
626 629 log.exception("Bad data sent from update server")
627 630 return _err('Bad data sent from update server')
628 631
629 632 latest = data['versions'][0]
630 633
631 634 c.update_url = update_url
632 635 c.latest_data = latest
633 636 c.latest_ver = latest['version']
634 637 c.cur_ver = rhodecode.__version__
635 638 c.should_upgrade = False
636 639
637 640 if (packaging.version.Version(c.latest_ver) >
638 641 packaging.version.Version(c.cur_ver)):
639 642 c.should_upgrade = True
640 643 c.important_notices = latest['general']
641 644
642 645 return render('admin/settings/settings_system_update.html')
643 646
644 647 @HasPermissionAllDecorator('hg.admin')
645 648 def settings_supervisor(self):
646 649 c.rhodecode_ini = rhodecode.CONFIG
647 650 c.active = 'supervisor'
648 651
649 652 c.supervisor_procs = OrderedDict([
650 653 (SUPERVISOR_MASTER, {}),
651 654 ])
652 655
653 656 c.log_size = 10240
654 657 supervisor = SupervisorModel()
655 658
656 659 _connection = supervisor.get_connection(
657 660 c.rhodecode_ini.get('supervisor.uri'))
658 661 c.connection_error = None
659 662 try:
660 663 _connection.supervisor.getAllProcessInfo()
661 664 except Exception as e:
662 665 c.connection_error = str(e)
663 666 log.exception("Exception reading supervisor data")
664 667 return render('admin/settings/settings.html')
665 668
666 669 groupid = c.rhodecode_ini.get('supervisor.group_id')
667 670
668 671 # feed our group processes to the main
669 672 for proc in supervisor.get_group_processes(_connection, groupid):
670 673 c.supervisor_procs[proc['name']] = {}
671 674
672 675 for k in c.supervisor_procs.keys():
673 676 try:
674 677 # master process info
675 678 if k == SUPERVISOR_MASTER:
676 679 _data = supervisor.get_master_state(_connection)
677 680 _data['name'] = 'supervisor master'
678 681 _data['description'] = 'pid %s, id: %s, ver: %s' % (
679 682 _data['pid'], _data['id'], _data['ver'])
680 683 c.supervisor_procs[k] = _data
681 684 else:
682 685 procid = groupid + ":" + k
683 686 c.supervisor_procs[k] = supervisor.get_process_info(_connection, procid)
684 687 except Exception as e:
685 688 log.exception("Exception reading supervisor data")
686 689 c.supervisor_procs[k] = {'_rhodecode_error': str(e)}
687 690
688 691 return render('admin/settings/settings.html')
689 692
690 693 @HasPermissionAllDecorator('hg.admin')
691 694 def settings_supervisor_log(self, procid):
692 695 import rhodecode
693 696 c.rhodecode_ini = rhodecode.CONFIG
694 697 c.active = 'supervisor_tail'
695 698
696 699 supervisor = SupervisorModel()
697 700 _connection = supervisor.get_connection(c.rhodecode_ini.get('supervisor.uri'))
698 701 groupid = c.rhodecode_ini.get('supervisor.group_id')
699 702 procid = groupid + ":" + procid if procid != SUPERVISOR_MASTER else procid
700 703
701 704 c.log_size = 10240
702 705 offset = abs(safe_int(request.GET.get('offset', c.log_size))) * -1
703 706 c.log = supervisor.read_process_log(_connection, procid, offset, 0)
704 707
705 708 return render('admin/settings/settings.html')
706 709
707 710 @HasPermissionAllDecorator('hg.admin')
708 711 @auth.CSRFRequired()
709 712 def settings_labs_update(self):
710 713 """POST /admin/settings/labs: All items in the collection"""
711 714 # url('admin_settings/labs', method={'POST'})
712 715 c.active = 'labs'
713 716
714 717 application_form = LabsSettingsForm()()
715 718 try:
716 719 form_result = application_form.to_python(dict(request.POST))
717 720 except formencode.Invalid as errors:
718 721 h.flash(
719 722 _('Some form inputs contain invalid data.'),
720 723 category='error')
721 724 return htmlfill.render(
722 725 render('admin/settings/settings.html'),
723 726 defaults=errors.value,
724 727 errors=errors.error_dict or {},
725 728 prefix_error=False,
726 729 encoding='UTF-8',
727 730 force_defaults=False
728 731 )
729 732
730 733 try:
731 734 session = Session()
732 735 for setting in _LAB_SETTINGS:
733 736 setting_name = setting.key[len('rhodecode_'):]
734 737 sett = SettingsModel().create_or_update_setting(
735 738 setting_name, form_result[setting.key], setting.type)
736 739 session.add(sett)
737 740
738 741 except Exception:
739 742 log.exception('Exception while updating lab settings')
740 743 h.flash(_('Error occurred during updating labs settings'),
741 744 category='error')
742 745 else:
743 746 Session().commit()
744 747 SettingsModel().invalidate_settings_cache()
745 748 h.flash(_('Updated Labs settings'), category='success')
746 749 return redirect(url('admin_settings_labs'))
747 750
748 751 return htmlfill.render(
749 752 render('admin/settings/settings.html'),
750 753 defaults=self._form_defaults(),
751 754 encoding='UTF-8',
752 755 force_defaults=False)
753 756
754 757 @HasPermissionAllDecorator('hg.admin')
755 758 def settings_labs(self):
756 759 """GET /admin/settings/labs: All items in the collection"""
757 760 # url('admin_settings_labs')
758 761 if not c.labs_active:
759 762 redirect(url('admin_settings'))
760 763
761 764 c.active = 'labs'
762 765 c.lab_settings = _LAB_SETTINGS
763 766
764 767 return htmlfill.render(
765 768 render('admin/settings/settings.html'),
766 769 defaults=self._form_defaults(),
767 770 encoding='UTF-8',
768 771 force_defaults=False)
769 772
770 773 def _form_defaults(self):
771 774 defaults = SettingsModel().get_all_settings()
772 775 defaults.update(self._get_hg_ui_settings())
773 776 defaults.update({
774 777 'new_svn_branch': '',
775 778 'new_svn_tag': '',
776 779 })
777 780 return defaults
778 781
779 782
780 783 # :param key: name of the setting including the 'rhodecode_' prefix
781 784 # :param type: the RhodeCodeSetting type to use.
782 785 # :param group: the i18ned group in which we should dispaly this setting
783 786 # :param label: the i18ned label we should display for this setting
784 787 # :param help: the i18ned help we should dispaly for this setting
785 788 LabSetting = collections.namedtuple(
786 789 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
787 790
788 791
789 792 # This list has to be kept in sync with the form
790 793 # rhodecode.model.forms.LabsSettingsForm.
791 794 _LAB_SETTINGS = [
792 795
793 796 ]
@@ -1,587 +1,587 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 The base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import ipaddress
31 31
32 32 from paste.auth.basic import AuthBasicAuthenticator
33 33 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
34 34 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
35 35 from pylons import config, tmpl_context as c, request, session, url
36 36 from pylons.controllers import WSGIController
37 37 from pylons.controllers.util import redirect
38 38 from pylons.i18n import translation
39 39 # marcink: don't remove this import
40 40 from pylons.templating import render_mako as render # noqa
41 41 from pylons.i18n.translation import _
42 42 from webob.exc import HTTPFound
43 43
44 44
45 45 import rhodecode
46 46 from rhodecode.authentication.base import VCS_TYPE
47 47 from rhodecode.lib import auth, utils2
48 48 from rhodecode.lib import helpers as h
49 49 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
50 50 from rhodecode.lib.exceptions import UserCreationError
51 51 from rhodecode.lib.utils import (
52 52 get_repo_slug, set_rhodecode_config, password_changed,
53 53 get_enabled_hook_classes)
54 54 from rhodecode.lib.utils2 import (
55 55 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
56 56 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
57 57 from rhodecode.model import meta
58 58 from rhodecode.model.db import Repository, User
59 59 from rhodecode.model.notification import NotificationModel
60 60 from rhodecode.model.scm import ScmModel
61 61 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
62 62
63 63
64 64 log = logging.getLogger(__name__)
65 65
66 66
67 67 def _filter_proxy(ip):
68 68 """
69 69 Passed in IP addresses in HEADERS can be in a special format of multiple
70 70 ips. Those comma separated IPs are passed from various proxies in the
71 71 chain of request processing. The left-most being the original client.
72 72 We only care about the first IP which came from the org. client.
73 73
74 74 :param ip: ip string from headers
75 75 """
76 76 if ',' in ip:
77 77 _ips = ip.split(',')
78 78 _first_ip = _ips[0].strip()
79 79 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
80 80 return _first_ip
81 81 return ip
82 82
83 83
84 84 def _filter_port(ip):
85 85 """
86 86 Removes a port from ip, there are 4 main cases to handle here.
87 87 - ipv4 eg. 127.0.0.1
88 88 - ipv6 eg. ::1
89 89 - ipv4+port eg. 127.0.0.1:8080
90 90 - ipv6+port eg. [::1]:8080
91 91
92 92 :param ip:
93 93 """
94 94 def is_ipv6(ip_addr):
95 95 if hasattr(socket, 'inet_pton'):
96 96 try:
97 97 socket.inet_pton(socket.AF_INET6, ip_addr)
98 98 except socket.error:
99 99 return False
100 100 else:
101 101 # fallback to ipaddress
102 102 try:
103 103 ipaddress.IPv6Address(ip_addr)
104 104 except Exception:
105 105 return False
106 106 return True
107 107
108 108 if ':' not in ip: # must be ipv4 pure ip
109 109 return ip
110 110
111 111 if '[' in ip and ']' in ip: # ipv6 with port
112 112 return ip.split(']')[0][1:].lower()
113 113
114 114 # must be ipv6 or ipv4 with port
115 115 if is_ipv6(ip):
116 116 return ip
117 117 else:
118 118 ip, _port = ip.split(':')[:2] # means ipv4+port
119 119 return ip
120 120
121 121
122 122 def get_ip_addr(environ):
123 123 proxy_key = 'HTTP_X_REAL_IP'
124 124 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
125 125 def_key = 'REMOTE_ADDR'
126 126 _filters = lambda x: _filter_port(_filter_proxy(x))
127 127
128 128 ip = environ.get(proxy_key)
129 129 if ip:
130 130 return _filters(ip)
131 131
132 132 ip = environ.get(proxy_key2)
133 133 if ip:
134 134 return _filters(ip)
135 135
136 136 ip = environ.get(def_key, '0.0.0.0')
137 137 return _filters(ip)
138 138
139 139
140 140 def get_server_ip_addr(environ, log_errors=True):
141 141 hostname = environ.get('SERVER_NAME')
142 142 try:
143 143 return socket.gethostbyname(hostname)
144 144 except Exception as e:
145 145 if log_errors:
146 146 # in some cases this lookup is not possible, and we don't want to
147 147 # make it an exception in logs
148 148 log.exception('Could not retrieve server ip address: %s', e)
149 149 return hostname
150 150
151 151
152 152 def get_server_port(environ):
153 153 return environ.get('SERVER_PORT')
154 154
155 155
156 156 def get_access_path(environ):
157 157 path = environ.get('PATH_INFO')
158 158 org_req = environ.get('pylons.original_request')
159 159 if org_req:
160 160 path = org_req.environ.get('PATH_INFO')
161 161 return path
162 162
163 163
164 164 def vcs_operation_context(
165 165 environ, repo_name, username, action, scm, check_locking=True):
166 166 """
167 167 Generate the context for a vcs operation, e.g. push or pull.
168 168
169 169 This context is passed over the layers so that hooks triggered by the
170 170 vcs operation know details like the user, the user's IP address etc.
171 171
172 172 :param check_locking: Allows to switch of the computation of the locking
173 173 data. This serves mainly the need of the simplevcs middleware to be
174 174 able to disable this for certain operations.
175 175
176 176 """
177 177 # Tri-state value: False: unlock, None: nothing, True: lock
178 178 make_lock = None
179 179 locked_by = [None, None, None]
180 180 is_anonymous = username == User.DEFAULT_USER
181 181 if not is_anonymous and check_locking:
182 182 log.debug('Checking locking on repository "%s"', repo_name)
183 183 user = User.get_by_username(username)
184 184 repo = Repository.get_by_repo_name(repo_name)
185 185 make_lock, __, locked_by = repo.get_locking_state(
186 186 action, user.user_id)
187 187
188 188 settings_model = VcsSettingsModel(repo=repo_name)
189 189 ui_settings = settings_model.get_ui_settings()
190 190
191 191 extras = {
192 192 'ip': get_ip_addr(environ),
193 193 'username': username,
194 194 'action': action,
195 195 'repository': repo_name,
196 196 'scm': scm,
197 197 'config': rhodecode.CONFIG['__file__'],
198 198 'make_lock': make_lock,
199 199 'locked_by': locked_by,
200 200 'server_url': utils2.get_server_url(environ),
201 201 'hooks': get_enabled_hook_classes(ui_settings),
202 202 }
203 203 return extras
204 204
205 205
206 206 class BasicAuth(AuthBasicAuthenticator):
207 207
208 208 def __init__(self, realm, authfunc, registry, auth_http_code=None,
209 209 initial_call_detection=False):
210 210 self.realm = realm
211 211 self.initial_call = initial_call_detection
212 212 self.authfunc = authfunc
213 213 self.registry = registry
214 214 self._rc_auth_http_code = auth_http_code
215 215
216 216 def _get_response_from_code(self, http_code):
217 217 try:
218 218 return get_exception(safe_int(http_code))
219 219 except Exception:
220 220 log.exception('Failed to fetch response for code %s' % http_code)
221 221 return HTTPForbidden
222 222
223 223 def build_authentication(self):
224 224 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
225 225 if self._rc_auth_http_code and not self.initial_call:
226 226 # return alternative HTTP code if alternative http return code
227 227 # is specified in RhodeCode config, but ONLY if it's not the
228 228 # FIRST call
229 229 custom_response_klass = self._get_response_from_code(
230 230 self._rc_auth_http_code)
231 231 return custom_response_klass(headers=head)
232 232 return HTTPUnauthorized(headers=head)
233 233
234 234 def authenticate(self, environ):
235 235 authorization = AUTHORIZATION(environ)
236 236 if not authorization:
237 237 return self.build_authentication()
238 238 (authmeth, auth) = authorization.split(' ', 1)
239 239 if 'basic' != authmeth.lower():
240 240 return self.build_authentication()
241 241 auth = auth.strip().decode('base64')
242 242 _parts = auth.split(':', 1)
243 243 if len(_parts) == 2:
244 244 username, password = _parts
245 245 if self.authfunc(
246 246 username, password, environ, VCS_TYPE,
247 247 registry=self.registry):
248 248 return username
249 249 if username and password:
250 250 # we mark that we actually executed authentication once, at
251 251 # that point we can use the alternative auth code
252 252 self.initial_call = False
253 253
254 254 return self.build_authentication()
255 255
256 256 __call__ = authenticate
257 257
258 258
259 259 def attach_context_attributes(context, request):
260 260 """
261 261 Attach variables into template context called `c`, please note that
262 262 request could be pylons or pyramid request in here.
263 263 """
264 264 rc_config = SettingsModel().get_all_settings(cache=True)
265 265
266 266 context.rhodecode_version = rhodecode.__version__
267 267 context.rhodecode_edition = config.get('rhodecode.edition')
268 268 # unique secret + version does not leak the version but keep consistency
269 269 context.rhodecode_version_hash = md5(
270 270 config.get('beaker.session.secret', '') +
271 271 rhodecode.__version__)[:8]
272 272
273 273 # Default language set for the incoming request
274 274 context.language = translation.get_lang()[0]
275 275
276 276 # Visual options
277 277 context.visual = AttributeDict({})
278 278
279 # DB store
279 # DB stored Visual Items
280 280 context.visual.show_public_icon = str2bool(
281 281 rc_config.get('rhodecode_show_public_icon'))
282 282 context.visual.show_private_icon = str2bool(
283 283 rc_config.get('rhodecode_show_private_icon'))
284 284 context.visual.stylify_metatags = str2bool(
285 285 rc_config.get('rhodecode_stylify_metatags'))
286 286 context.visual.dashboard_items = safe_int(
287 287 rc_config.get('rhodecode_dashboard_items', 100))
288 288 context.visual.admin_grid_items = safe_int(
289 289 rc_config.get('rhodecode_admin_grid_items', 100))
290 290 context.visual.repository_fields = str2bool(
291 291 rc_config.get('rhodecode_repository_fields'))
292 292 context.visual.show_version = str2bool(
293 293 rc_config.get('rhodecode_show_version'))
294 294 context.visual.use_gravatar = str2bool(
295 295 rc_config.get('rhodecode_use_gravatar'))
296 296 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
297 297 context.visual.default_renderer = rc_config.get(
298 298 'rhodecode_markup_renderer', 'rst')
299 299 context.visual.rhodecode_support_url = \
300 300 rc_config.get('rhodecode_support_url') or url('rhodecode_support')
301 301
302 302 context.pre_code = rc_config.get('rhodecode_pre_code')
303 303 context.post_code = rc_config.get('rhodecode_post_code')
304 304 context.rhodecode_name = rc_config.get('rhodecode_title')
305 305 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
306 306 # if we have specified default_encoding in the request, it has more
307 307 # priority
308 308 if request.GET.get('default_encoding'):
309 309 context.default_encodings.insert(0, request.GET.get('default_encoding'))
310 310 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
311 311
312 312 # INI stored
313 313 context.labs_active = str2bool(
314 314 config.get('labs_settings_active', 'false'))
315 315 context.visual.allow_repo_location_change = str2bool(
316 316 config.get('allow_repo_location_change', True))
317 317 context.visual.allow_custom_hooks_settings = str2bool(
318 318 config.get('allow_custom_hooks_settings', True))
319 319 context.debug_style = str2bool(config.get('debug_style', False))
320 320
321 321 context.rhodecode_instanceid = config.get('instance_id')
322 322
323 323 # AppEnlight
324 324 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
325 325 context.appenlight_api_public_key = config.get(
326 326 'appenlight.api_public_key', '')
327 327 context.appenlight_server_url = config.get('appenlight.server_url', '')
328 328
329 329 # JS template context
330 330 context.template_context = {
331 331 'repo_name': None,
332 332 'repo_type': None,
333 333 'repo_landing_commit': None,
334 334 'rhodecode_user': {
335 335 'username': None,
336 336 'email': None,
337 337 'notification_status': False
338 338 },
339 339 'visual': {
340 340 'default_renderer': None
341 341 },
342 342 'commit_data': {
343 343 'commit_id': None
344 344 },
345 345 'pull_request_data': {'pull_request_id': None},
346 346 'timeago': {
347 347 'refresh_time': 120 * 1000,
348 348 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
349 349 },
350 350 'pylons_dispatch': {
351 351 # 'controller': request.environ['pylons.routes_dict']['controller'],
352 352 # 'action': request.environ['pylons.routes_dict']['action'],
353 353 },
354 354 'pyramid_dispatch': {
355 355
356 356 },
357 357 'extra': {'plugins': {}}
358 358 }
359 359 # END CONFIG VARS
360 360
361 361 # TODO: This dosn't work when called from pylons compatibility tween.
362 362 # Fix this and remove it from base controller.
363 363 # context.repo_name = get_repo_slug(request) # can be empty
364 364
365 365 context.csrf_token = auth.get_csrf_token()
366 366 context.backends = rhodecode.BACKENDS.keys()
367 367 context.backends.sort()
368 368 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
369 369 context.rhodecode_user.user_id)
370 370
371 371
372 372 def get_auth_user(environ):
373 373 ip_addr = get_ip_addr(environ)
374 374 # make sure that we update permissions each time we call controller
375 375 _auth_token = (request.GET.get('auth_token', '') or
376 376 request.GET.get('api_key', ''))
377 377
378 378 if _auth_token:
379 379 # when using API_KEY we are sure user exists.
380 380 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
381 381 authenticated = False
382 382 else:
383 383 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
384 384 try:
385 385 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
386 386 ip_addr=ip_addr)
387 387 except UserCreationError as e:
388 388 h.flash(e, 'error')
389 389 # container auth or other auth functions that create users
390 390 # on the fly can throw this exception signaling that there's
391 391 # issue with user creation, explanation should be provided
392 392 # in Exception itself. We then create a simple blank
393 393 # AuthUser
394 394 auth_user = AuthUser(ip_addr=ip_addr)
395 395
396 396 if password_changed(auth_user, session):
397 397 session.invalidate()
398 398 cookie_store = CookieStoreWrapper(
399 399 session.get('rhodecode_user'))
400 400 auth_user = AuthUser(ip_addr=ip_addr)
401 401
402 402 authenticated = cookie_store.get('is_authenticated')
403 403
404 404 if not auth_user.is_authenticated and auth_user.is_user_object:
405 405 # user is not authenticated and not empty
406 406 auth_user.set_authenticated(authenticated)
407 407
408 408 return auth_user
409 409
410 410
411 411 class BaseController(WSGIController):
412 412
413 413 def __before__(self):
414 414 """
415 415 __before__ is called before controller methods and after __call__
416 416 """
417 417 # on each call propagate settings calls into global settings.
418 418 set_rhodecode_config(config)
419 419 attach_context_attributes(c, request)
420 420
421 421 # TODO: Remove this when fixed in attach_context_attributes()
422 422 c.repo_name = get_repo_slug(request) # can be empty
423 423
424 424 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
425 425 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
426 426 self.sa = meta.Session
427 427 self.scm_model = ScmModel(self.sa)
428 428
429 429 default_lang = c.language
430 430 user_lang = c.language
431 431 try:
432 432 user_obj = self._rhodecode_user.get_instance()
433 433 if user_obj:
434 434 user_lang = user_obj.user_data.get('language')
435 435 except Exception:
436 436 log.exception('Failed to fetch user language for user %s',
437 437 self._rhodecode_user)
438 438
439 439 if user_lang and user_lang != default_lang:
440 440 log.debug('set language to %s for user %s', user_lang,
441 441 self._rhodecode_user)
442 442 translation.set_lang(user_lang)
443 443
444 444 def _dispatch_redirect(self, with_url, environ, start_response):
445 445 resp = HTTPFound(with_url)
446 446 environ['SCRIPT_NAME'] = '' # handle prefix middleware
447 447 environ['PATH_INFO'] = with_url
448 448 return resp(environ, start_response)
449 449
450 450 def __call__(self, environ, start_response):
451 451 """Invoke the Controller"""
452 452 # WSGIController.__call__ dispatches to the Controller method
453 453 # the request is routed to. This routing information is
454 454 # available in environ['pylons.routes_dict']
455 455 from rhodecode.lib import helpers as h
456 456
457 457 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
458 458 if environ.get('debugtoolbar.wants_pylons_context', False):
459 459 environ['debugtoolbar.pylons_context'] = c._current_obj()
460 460
461 461 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
462 462 environ['pylons.routes_dict']['action']])
463 463
464 464 self.rc_config = SettingsModel().get_all_settings(cache=True)
465 465 self.ip_addr = get_ip_addr(environ)
466 466
467 467 # The rhodecode auth user is looked up and passed through the
468 468 # environ by the pylons compatibility tween in pyramid.
469 469 # So we can just grab it from there.
470 470 auth_user = environ['rc_auth_user']
471 471
472 472 # set globals for auth user
473 473 request.user = auth_user
474 474 c.rhodecode_user = self._rhodecode_user = auth_user
475 475
476 476 log.info('IP: %s User: %s accessed %s [%s]' % (
477 477 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
478 478 _route_name)
479 479 )
480 480
481 481 # TODO: Maybe this should be move to pyramid to cover all views.
482 482 # check user attributes for password change flag
483 483 user_obj = auth_user.get_instance()
484 484 if user_obj and user_obj.user_data.get('force_password_change'):
485 485 h.flash('You are required to change your password', 'warning',
486 486 ignore_duplicate=True)
487 487
488 488 skip_user_check_urls = [
489 489 'error.document', 'login.logout', 'login.index',
490 490 'admin/my_account.my_account_password',
491 491 'admin/my_account.my_account_password_update'
492 492 ]
493 493 if _route_name not in skip_user_check_urls:
494 494 return self._dispatch_redirect(
495 495 url('my_account_password'), environ, start_response)
496 496
497 497 return WSGIController.__call__(self, environ, start_response)
498 498
499 499
500 500 class BaseRepoController(BaseController):
501 501 """
502 502 Base class for controllers responsible for loading all needed data for
503 503 repository loaded items are
504 504
505 505 c.rhodecode_repo: instance of scm repository
506 506 c.rhodecode_db_repo: instance of db
507 507 c.repository_requirements_missing: shows that repository specific data
508 508 could not be displayed due to the missing requirements
509 509 c.repository_pull_requests: show number of open pull requests
510 510 """
511 511
512 512 def __before__(self):
513 513 super(BaseRepoController, self).__before__()
514 514 if c.repo_name: # extracted from routes
515 515 db_repo = Repository.get_by_repo_name(c.repo_name)
516 516 if not db_repo:
517 517 return
518 518
519 519 log.debug(
520 520 'Found repository in database %s with state `%s`',
521 521 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
522 522 route = getattr(request.environ.get('routes.route'), 'name', '')
523 523
524 524 # allow to delete repos that are somehow damages in filesystem
525 525 if route in ['delete_repo']:
526 526 return
527 527
528 528 if db_repo.repo_state in [Repository.STATE_PENDING]:
529 529 if route in ['repo_creating_home']:
530 530 return
531 531 check_url = url('repo_creating_home', repo_name=c.repo_name)
532 532 return redirect(check_url)
533 533
534 534 self.rhodecode_db_repo = db_repo
535 535
536 536 missing_requirements = False
537 537 try:
538 538 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
539 539 except RepositoryRequirementError as e:
540 540 missing_requirements = True
541 541 self._handle_missing_requirements(e)
542 542
543 543 if self.rhodecode_repo is None and not missing_requirements:
544 544 log.error('%s this repository is present in database but it '
545 545 'cannot be created as an scm instance', c.repo_name)
546 546
547 547 h.flash(_(
548 548 "The repository at %(repo_name)s cannot be located.") %
549 549 {'repo_name': c.repo_name},
550 550 category='error', ignore_duplicate=True)
551 551 redirect(url('home'))
552 552
553 553 # update last change according to VCS data
554 554 if not missing_requirements:
555 555 commit = db_repo.get_commit(
556 556 pre_load=["author", "date", "message", "parents"])
557 557 db_repo.update_commit_cache(commit)
558 558
559 559 # Prepare context
560 560 c.rhodecode_db_repo = db_repo
561 561 c.rhodecode_repo = self.rhodecode_repo
562 562 c.repository_requirements_missing = missing_requirements
563 563
564 564 self._update_global_counters(self.scm_model, db_repo)
565 565
566 566 def _update_global_counters(self, scm_model, db_repo):
567 567 """
568 568 Base variables that are exposed to every page of repository
569 569 """
570 570 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
571 571
572 572 def _handle_missing_requirements(self, error):
573 573 self.rhodecode_repo = None
574 574 log.error(
575 575 'Requirements are missing for repository %s: %s',
576 576 c.repo_name, error.message)
577 577
578 578 summary_url = url('summary_home', repo_name=c.repo_name)
579 579 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
580 580 settings_update_url = url('repo', repo_name=c.repo_name)
581 581 path = request.path
582 582 should_redirect = (
583 583 path not in (summary_url, settings_update_url)
584 584 and '/settings' not in path or path == statistics_url
585 585 )
586 586 if should_redirect:
587 587 redirect(summary_url)
@@ -1,1966 +1,1966 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 Helper functions
23 23
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 27
28 28 import random
29 29 import hashlib
30 30 import StringIO
31 31 import urllib
32 32 import math
33 33 import logging
34 34 import re
35 35 import urlparse
36 36 import time
37 37 import string
38 38 import hashlib
39 39 import pygments
40 40
41 41 from datetime import datetime
42 42 from functools import partial
43 43 from pygments.formatters.html import HtmlFormatter
44 44 from pygments import highlight as code_highlight
45 45 from pygments.lexers import (
46 46 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
47 47 from pylons import url as pylons_url
48 48 from pylons.i18n.translation import _, ungettext
49 49 from pyramid.threadlocal import get_current_request
50 50
51 51 from webhelpers.html import literal, HTML, escape
52 52 from webhelpers.html.tools import *
53 53 from webhelpers.html.builder import make_tag
54 54 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
55 55 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
56 56 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
57 57 submit, text, password, textarea, title, ul, xml_declaration, radio
58 58 from webhelpers.html.tools import auto_link, button_to, highlight, \
59 59 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
60 60 from webhelpers.pylonslib import Flash as _Flash
61 61 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
62 62 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
63 63 replace_whitespace, urlify, truncate, wrap_paragraphs
64 64 from webhelpers.date import time_ago_in_words
65 65 from webhelpers.paginate import Page as _Page
66 66 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
67 67 convert_boolean_attrs, NotGiven, _make_safe_id_component
68 68 from webhelpers2.number import format_byte_size
69 69
70 70 from rhodecode.lib.annotate import annotate_highlight
71 71 from rhodecode.lib.action_parser import action_parser
72 72 from rhodecode.lib.ext_json import json
73 73 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
74 74 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
75 75 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
76 76 AttributeDict, safe_int, md5, md5_safe
77 77 from rhodecode.lib.markup_renderer import MarkupRenderer
78 78 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
79 79 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
80 80 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
81 81 from rhodecode.model.changeset_status import ChangesetStatusModel
82 82 from rhodecode.model.db import Permission, User, Repository
83 83 from rhodecode.model.repo_group import RepoGroupModel
84 84 from rhodecode.model.settings import IssueTrackerSettingsModel
85 85
86 86 log = logging.getLogger(__name__)
87 87
88 88
89 89 DEFAULT_USER = User.DEFAULT_USER
90 90 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
91 91
92 92
93 93 def url(*args, **kw):
94 94 return pylons_url(*args, **kw)
95 95
96 96
97 97 def pylons_url_current(*args, **kw):
98 98 """
99 99 This function overrides pylons.url.current() which returns the current
100 100 path so that it will also work from a pyramid only context. This
101 101 should be removed once port to pyramid is complete.
102 102 """
103 103 if not args and not kw:
104 104 request = get_current_request()
105 105 return request.path
106 106 return pylons_url.current(*args, **kw)
107 107
108 108 url.current = pylons_url_current
109 109
110 110
111 111 def asset(path, ver=None):
112 112 """
113 113 Helper to generate a static asset file path for rhodecode assets
114 114
115 115 eg. h.asset('images/image.png', ver='3923')
116 116
117 117 :param path: path of asset
118 118 :param ver: optional version query param to append as ?ver=
119 119 """
120 120 request = get_current_request()
121 121 query = {}
122 122 if ver:
123 123 query = {'ver': ver}
124 124 return request.static_path(
125 125 'rhodecode:public/{}'.format(path), _query=query)
126 126
127 127
128 128 def html_escape(text, html_escape_table=None):
129 129 """Produce entities within text."""
130 130 if not html_escape_table:
131 131 html_escape_table = {
132 132 "&": "&amp;",
133 133 '"': "&quot;",
134 134 "'": "&apos;",
135 135 ">": "&gt;",
136 136 "<": "&lt;",
137 137 }
138 138 return "".join(html_escape_table.get(c, c) for c in text)
139 139
140 140
141 141 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
142 142 """
143 143 Truncate string ``s`` at the first occurrence of ``sub``.
144 144
145 145 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
146 146 """
147 147 suffix_if_chopped = suffix_if_chopped or ''
148 148 pos = s.find(sub)
149 149 if pos == -1:
150 150 return s
151 151
152 152 if inclusive:
153 153 pos += len(sub)
154 154
155 155 chopped = s[:pos]
156 156 left = s[pos:].strip()
157 157
158 158 if left and suffix_if_chopped:
159 159 chopped += suffix_if_chopped
160 160
161 161 return chopped
162 162
163 163
164 164 def shorter(text, size=20):
165 165 postfix = '...'
166 166 if len(text) > size:
167 167 return text[:size - len(postfix)] + postfix
168 168 return text
169 169
170 170
171 171 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
172 172 """
173 173 Reset button
174 174 """
175 175 _set_input_attrs(attrs, type, name, value)
176 176 _set_id_attr(attrs, id, name)
177 177 convert_boolean_attrs(attrs, ["disabled"])
178 178 return HTML.input(**attrs)
179 179
180 180 reset = _reset
181 181 safeid = _make_safe_id_component
182 182
183 183
184 184 def branding(name, length=40):
185 185 return truncate(name, length, indicator="")
186 186
187 187
188 188 def FID(raw_id, path):
189 189 """
190 190 Creates a unique ID for filenode based on it's hash of path and commit
191 191 it's safe to use in urls
192 192
193 193 :param raw_id:
194 194 :param path:
195 195 """
196 196
197 197 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
198 198
199 199
200 200 class _GetError(object):
201 201 """Get error from form_errors, and represent it as span wrapped error
202 202 message
203 203
204 204 :param field_name: field to fetch errors for
205 205 :param form_errors: form errors dict
206 206 """
207 207
208 208 def __call__(self, field_name, form_errors):
209 209 tmpl = """<span class="error_msg">%s</span>"""
210 210 if form_errors and field_name in form_errors:
211 211 return literal(tmpl % form_errors.get(field_name))
212 212
213 213 get_error = _GetError()
214 214
215 215
216 216 class _ToolTip(object):
217 217
218 218 def __call__(self, tooltip_title, trim_at=50):
219 219 """
220 220 Special function just to wrap our text into nice formatted
221 221 autowrapped text
222 222
223 223 :param tooltip_title:
224 224 """
225 225 tooltip_title = escape(tooltip_title)
226 226 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
227 227 return tooltip_title
228 228 tooltip = _ToolTip()
229 229
230 230
231 231 def files_breadcrumbs(repo_name, commit_id, file_path):
232 232 if isinstance(file_path, str):
233 233 file_path = safe_unicode(file_path)
234 234
235 235 # TODO: johbo: Is this always a url like path, or is this operating
236 236 # system dependent?
237 237 path_segments = file_path.split('/')
238 238
239 239 repo_name_html = escape(repo_name)
240 240 if len(path_segments) == 1 and path_segments[0] == '':
241 241 url_segments = [repo_name_html]
242 242 else:
243 243 url_segments = [
244 244 link_to(
245 245 repo_name_html,
246 246 url('files_home',
247 247 repo_name=repo_name,
248 248 revision=commit_id,
249 249 f_path=''),
250 250 class_='pjax-link')]
251 251
252 252 last_cnt = len(path_segments) - 1
253 253 for cnt, segment in enumerate(path_segments):
254 254 if not segment:
255 255 continue
256 256 segment_html = escape(segment)
257 257
258 258 if cnt != last_cnt:
259 259 url_segments.append(
260 260 link_to(
261 261 segment_html,
262 262 url('files_home',
263 263 repo_name=repo_name,
264 264 revision=commit_id,
265 265 f_path='/'.join(path_segments[:cnt + 1])),
266 266 class_='pjax-link'))
267 267 else:
268 268 url_segments.append(segment_html)
269 269
270 270 return literal('/'.join(url_segments))
271 271
272 272
273 273 class CodeHtmlFormatter(HtmlFormatter):
274 274 """
275 275 My code Html Formatter for source codes
276 276 """
277 277
278 278 def wrap(self, source, outfile):
279 279 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
280 280
281 281 def _wrap_code(self, source):
282 282 for cnt, it in enumerate(source):
283 283 i, t = it
284 284 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
285 285 yield i, t
286 286
287 287 def _wrap_tablelinenos(self, inner):
288 288 dummyoutfile = StringIO.StringIO()
289 289 lncount = 0
290 290 for t, line in inner:
291 291 if t:
292 292 lncount += 1
293 293 dummyoutfile.write(line)
294 294
295 295 fl = self.linenostart
296 296 mw = len(str(lncount + fl - 1))
297 297 sp = self.linenospecial
298 298 st = self.linenostep
299 299 la = self.lineanchors
300 300 aln = self.anchorlinenos
301 301 nocls = self.noclasses
302 302 if sp:
303 303 lines = []
304 304
305 305 for i in range(fl, fl + lncount):
306 306 if i % st == 0:
307 307 if i % sp == 0:
308 308 if aln:
309 309 lines.append('<a href="#%s%d" class="special">%*d</a>' %
310 310 (la, i, mw, i))
311 311 else:
312 312 lines.append('<span class="special">%*d</span>' % (mw, i))
313 313 else:
314 314 if aln:
315 315 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
316 316 else:
317 317 lines.append('%*d' % (mw, i))
318 318 else:
319 319 lines.append('')
320 320 ls = '\n'.join(lines)
321 321 else:
322 322 lines = []
323 323 for i in range(fl, fl + lncount):
324 324 if i % st == 0:
325 325 if aln:
326 326 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
327 327 else:
328 328 lines.append('%*d' % (mw, i))
329 329 else:
330 330 lines.append('')
331 331 ls = '\n'.join(lines)
332 332
333 333 # in case you wonder about the seemingly redundant <div> here: since the
334 334 # content in the other cell also is wrapped in a div, some browsers in
335 335 # some configurations seem to mess up the formatting...
336 336 if nocls:
337 337 yield 0, ('<table class="%stable">' % self.cssclass +
338 338 '<tr><td><div class="linenodiv" '
339 339 'style="background-color: #f0f0f0; padding-right: 10px">'
340 340 '<pre style="line-height: 125%">' +
341 341 ls + '</pre></div></td><td id="hlcode" class="code">')
342 342 else:
343 343 yield 0, ('<table class="%stable">' % self.cssclass +
344 344 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
345 345 ls + '</pre></div></td><td id="hlcode" class="code">')
346 346 yield 0, dummyoutfile.getvalue()
347 347 yield 0, '</td></tr></table>'
348 348
349 349
350 350 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
351 351 def __init__(self, **kw):
352 352 # only show these line numbers if set
353 353 self.only_lines = kw.pop('only_line_numbers', [])
354 354 self.query_terms = kw.pop('query_terms', [])
355 355 self.max_lines = kw.pop('max_lines', 5)
356 356 self.line_context = kw.pop('line_context', 3)
357 357 self.url = kw.pop('url', None)
358 358
359 359 super(CodeHtmlFormatter, self).__init__(**kw)
360 360
361 361 def _wrap_code(self, source):
362 362 for cnt, it in enumerate(source):
363 363 i, t = it
364 364 t = '<pre>%s</pre>' % t
365 365 yield i, t
366 366
367 367 def _wrap_tablelinenos(self, inner):
368 368 yield 0, '<table class="code-highlight %stable">' % self.cssclass
369 369
370 370 last_shown_line_number = 0
371 371 current_line_number = 1
372 372
373 373 for t, line in inner:
374 374 if not t:
375 375 yield t, line
376 376 continue
377 377
378 378 if current_line_number in self.only_lines:
379 379 if last_shown_line_number + 1 != current_line_number:
380 380 yield 0, '<tr>'
381 381 yield 0, '<td class="line">...</td>'
382 382 yield 0, '<td id="hlcode" class="code"></td>'
383 383 yield 0, '</tr>'
384 384
385 385 yield 0, '<tr>'
386 386 if self.url:
387 387 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
388 388 self.url, current_line_number, current_line_number)
389 389 else:
390 390 yield 0, '<td class="line"><a href="">%i</a></td>' % (
391 391 current_line_number)
392 392 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
393 393 yield 0, '</tr>'
394 394
395 395 last_shown_line_number = current_line_number
396 396
397 397 current_line_number += 1
398 398
399 399
400 400 yield 0, '</table>'
401 401
402 402
403 403 def extract_phrases(text_query):
404 404 """
405 405 Extracts phrases from search term string making sure phrases
406 406 contained in double quotes are kept together - and discarding empty values
407 407 or fully whitespace values eg.
408 408
409 409 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
410 410
411 411 """
412 412
413 413 in_phrase = False
414 414 buf = ''
415 415 phrases = []
416 416 for char in text_query:
417 417 if in_phrase:
418 418 if char == '"': # end phrase
419 419 phrases.append(buf)
420 420 buf = ''
421 421 in_phrase = False
422 422 continue
423 423 else:
424 424 buf += char
425 425 continue
426 426 else:
427 427 if char == '"': # start phrase
428 428 in_phrase = True
429 429 phrases.append(buf)
430 430 buf = ''
431 431 continue
432 432 elif char == ' ':
433 433 phrases.append(buf)
434 434 buf = ''
435 435 continue
436 436 else:
437 437 buf += char
438 438
439 439 phrases.append(buf)
440 440 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
441 441 return phrases
442 442
443 443
444 444 def get_matching_offsets(text, phrases):
445 445 """
446 446 Returns a list of string offsets in `text` that the list of `terms` match
447 447
448 448 >>> get_matching_offsets('some text here', ['some', 'here'])
449 449 [(0, 4), (10, 14)]
450 450
451 451 """
452 452 offsets = []
453 453 for phrase in phrases:
454 454 for match in re.finditer(phrase, text):
455 455 offsets.append((match.start(), match.end()))
456 456
457 457 return offsets
458 458
459 459
460 460 def normalize_text_for_matching(x):
461 461 """
462 462 Replaces all non alnum characters to spaces and lower cases the string,
463 463 useful for comparing two text strings without punctuation
464 464 """
465 465 return re.sub(r'[^\w]', ' ', x.lower())
466 466
467 467
468 468 def get_matching_line_offsets(lines, terms):
469 469 """ Return a set of `lines` indices (starting from 1) matching a
470 470 text search query, along with `context` lines above/below matching lines
471 471
472 472 :param lines: list of strings representing lines
473 473 :param terms: search term string to match in lines eg. 'some text'
474 474 :param context: number of lines above/below a matching line to add to result
475 475 :param max_lines: cut off for lines of interest
476 476 eg.
477 477
478 478 text = '''
479 479 words words words
480 480 words words words
481 481 some text some
482 482 words words words
483 483 words words words
484 484 text here what
485 485 '''
486 486 get_matching_line_offsets(text, 'text', context=1)
487 487 {3: [(5, 9)], 6: [(0, 4)]]
488 488
489 489 """
490 490 matching_lines = {}
491 491 phrases = [normalize_text_for_matching(phrase)
492 492 for phrase in extract_phrases(terms)]
493 493
494 494 for line_index, line in enumerate(lines, start=1):
495 495 match_offsets = get_matching_offsets(
496 496 normalize_text_for_matching(line), phrases)
497 497 if match_offsets:
498 498 matching_lines[line_index] = match_offsets
499 499
500 500 return matching_lines
501 501
502 502
503 503 def get_lexer_safe(mimetype=None, filepath=None):
504 504 """
505 505 Tries to return a relevant pygments lexer using mimetype/filepath name,
506 506 defaulting to plain text if none could be found
507 507 """
508 508 lexer = None
509 509 try:
510 510 if mimetype:
511 511 lexer = get_lexer_for_mimetype(mimetype)
512 512 if not lexer:
513 513 lexer = get_lexer_for_filename(filepath)
514 514 except pygments.util.ClassNotFound:
515 515 pass
516 516
517 517 if not lexer:
518 518 lexer = get_lexer_by_name('text')
519 519
520 520 return lexer
521 521
522 522
523 523 def pygmentize(filenode, **kwargs):
524 524 """
525 525 pygmentize function using pygments
526 526
527 527 :param filenode:
528 528 """
529 529 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
530 530 return literal(code_highlight(filenode.content, lexer,
531 531 CodeHtmlFormatter(**kwargs)))
532 532
533 533
534 534 def pygmentize_annotation(repo_name, filenode, **kwargs):
535 535 """
536 536 pygmentize function for annotation
537 537
538 538 :param filenode:
539 539 """
540 540
541 541 color_dict = {}
542 542
543 543 def gen_color(n=10000):
544 544 """generator for getting n of evenly distributed colors using
545 545 hsv color and golden ratio. It always return same order of colors
546 546
547 547 :returns: RGB tuple
548 548 """
549 549
550 550 def hsv_to_rgb(h, s, v):
551 551 if s == 0.0:
552 552 return v, v, v
553 553 i = int(h * 6.0) # XXX assume int() truncates!
554 554 f = (h * 6.0) - i
555 555 p = v * (1.0 - s)
556 556 q = v * (1.0 - s * f)
557 557 t = v * (1.0 - s * (1.0 - f))
558 558 i = i % 6
559 559 if i == 0:
560 560 return v, t, p
561 561 if i == 1:
562 562 return q, v, p
563 563 if i == 2:
564 564 return p, v, t
565 565 if i == 3:
566 566 return p, q, v
567 567 if i == 4:
568 568 return t, p, v
569 569 if i == 5:
570 570 return v, p, q
571 571
572 572 golden_ratio = 0.618033988749895
573 573 h = 0.22717784590367374
574 574
575 575 for _ in xrange(n):
576 576 h += golden_ratio
577 577 h %= 1
578 578 HSV_tuple = [h, 0.95, 0.95]
579 579 RGB_tuple = hsv_to_rgb(*HSV_tuple)
580 580 yield map(lambda x: str(int(x * 256)), RGB_tuple)
581 581
582 582 cgenerator = gen_color()
583 583
584 584 def get_color_string(commit_id):
585 585 if commit_id in color_dict:
586 586 col = color_dict[commit_id]
587 587 else:
588 588 col = color_dict[commit_id] = cgenerator.next()
589 589 return "color: rgb(%s)! important;" % (', '.join(col))
590 590
591 591 def url_func(repo_name):
592 592
593 593 def _url_func(commit):
594 594 author = commit.author
595 595 date = commit.date
596 596 message = tooltip(commit.message)
597 597
598 598 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
599 599 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
600 600 "</b> %s<br/></div>")
601 601
602 602 tooltip_html = tooltip_html % (author, date, message)
603 603 lnk_format = '%5s:%s' % ('r%s' % commit.idx, commit.short_id)
604 604 uri = link_to(
605 605 lnk_format,
606 606 url('changeset_home', repo_name=repo_name,
607 607 revision=commit.raw_id),
608 608 style=get_color_string(commit.raw_id),
609 609 class_='tooltip',
610 610 title=tooltip_html
611 611 )
612 612
613 613 uri += '\n'
614 614 return uri
615 615 return _url_func
616 616
617 617 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
618 618
619 619
620 620 def is_following_repo(repo_name, user_id):
621 621 from rhodecode.model.scm import ScmModel
622 622 return ScmModel().is_following_repo(repo_name, user_id)
623 623
624 624
625 625 class _Message(object):
626 626 """A message returned by ``Flash.pop_messages()``.
627 627
628 628 Converting the message to a string returns the message text. Instances
629 629 also have the following attributes:
630 630
631 631 * ``message``: the message text.
632 632 * ``category``: the category specified when the message was created.
633 633 """
634 634
635 635 def __init__(self, category, message):
636 636 self.category = category
637 637 self.message = message
638 638
639 639 def __str__(self):
640 640 return self.message
641 641
642 642 __unicode__ = __str__
643 643
644 644 def __html__(self):
645 645 return escape(safe_unicode(self.message))
646 646
647 647
648 648 class Flash(_Flash):
649 649
650 650 def pop_messages(self):
651 651 """Return all accumulated messages and delete them from the session.
652 652
653 653 The return value is a list of ``Message`` objects.
654 654 """
655 655 from pylons import session
656 656
657 657 messages = []
658 658
659 659 # Pop the 'old' pylons flash messages. They are tuples of the form
660 660 # (category, message)
661 661 for cat, msg in session.pop(self.session_key, []):
662 662 messages.append(_Message(cat, msg))
663 663
664 664 # Pop the 'new' pyramid flash messages for each category as list
665 665 # of strings.
666 666 for cat in self.categories:
667 667 for msg in session.pop_flash(queue=cat):
668 668 messages.append(_Message(cat, msg))
669 669 # Map messages from the default queue to the 'notice' category.
670 670 for msg in session.pop_flash():
671 671 messages.append(_Message('notice', msg))
672 672
673 673 session.save()
674 674 return messages
675 675
676 676 flash = Flash()
677 677
678 678 #==============================================================================
679 679 # SCM FILTERS available via h.
680 680 #==============================================================================
681 681 from rhodecode.lib.vcs.utils import author_name, author_email
682 682 from rhodecode.lib.utils2 import credentials_filter, age as _age
683 683 from rhodecode.model.db import User, ChangesetStatus
684 684
685 685 age = _age
686 686 capitalize = lambda x: x.capitalize()
687 687 email = author_email
688 688 short_id = lambda x: x[:12]
689 689 hide_credentials = lambda x: ''.join(credentials_filter(x))
690 690
691 691
692 692 def age_component(datetime_iso, value=None, time_is_local=False):
693 693 title = value or format_date(datetime_iso)
694 694
695 695 # detect if we have a timezone info, otherwise, add it
696 696 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
697 697 tzinfo = '+00:00'
698 698
699 699 if time_is_local:
700 700 tzinfo = time.strftime("+%H:%M",
701 701 time.gmtime(
702 702 (datetime.now() - datetime.utcnow()).seconds + 1
703 703 )
704 704 )
705 705
706 706 return literal(
707 707 '<time class="timeago tooltip" '
708 708 'title="{1}" datetime="{0}{2}">{1}</time>'.format(
709 709 datetime_iso, title, tzinfo))
710 710
711 711
712 712 def _shorten_commit_id(commit_id):
713 713 from rhodecode import CONFIG
714 714 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
715 715 return commit_id[:def_len]
716 716
717 717
718 718 def show_id(commit):
719 719 """
720 720 Configurable function that shows ID
721 721 by default it's r123:fffeeefffeee
722 722
723 723 :param commit: commit instance
724 724 """
725 725 from rhodecode import CONFIG
726 726 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
727 727
728 728 raw_id = _shorten_commit_id(commit.raw_id)
729 729 if show_idx:
730 730 return 'r%s:%s' % (commit.idx, raw_id)
731 731 else:
732 732 return '%s' % (raw_id, )
733 733
734 734
735 735 def format_date(date):
736 736 """
737 737 use a standardized formatting for dates used in RhodeCode
738 738
739 739 :param date: date/datetime object
740 740 :return: formatted date
741 741 """
742 742
743 743 if date:
744 744 _fmt = "%a, %d %b %Y %H:%M:%S"
745 745 return safe_unicode(date.strftime(_fmt))
746 746
747 747 return u""
748 748
749 749
750 750 class _RepoChecker(object):
751 751
752 752 def __init__(self, backend_alias):
753 753 self._backend_alias = backend_alias
754 754
755 755 def __call__(self, repository):
756 756 if hasattr(repository, 'alias'):
757 757 _type = repository.alias
758 758 elif hasattr(repository, 'repo_type'):
759 759 _type = repository.repo_type
760 760 else:
761 761 _type = repository
762 762 return _type == self._backend_alias
763 763
764 764 is_git = _RepoChecker('git')
765 765 is_hg = _RepoChecker('hg')
766 766 is_svn = _RepoChecker('svn')
767 767
768 768
769 769 def get_repo_type_by_name(repo_name):
770 770 repo = Repository.get_by_repo_name(repo_name)
771 771 return repo.repo_type
772 772
773 773
774 774 def is_svn_without_proxy(repository):
775 from rhodecode import CONFIG
776 775 if is_svn(repository):
777 if not CONFIG.get('rhodecode_proxy_subversion_http_requests', False):
778 return True
776 from rhodecode.model.settings import VcsSettingsModel
777 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
778 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
779 779 return False
780 780
781 781
782 782 def discover_user(author):
783 783 """
784 784 Tries to discover RhodeCode User based on the autho string. Author string
785 785 is typically `FirstName LastName <email@address.com>`
786 786 """
787 787
788 788 # if author is already an instance use it for extraction
789 789 if isinstance(author, User):
790 790 return author
791 791
792 792 # Valid email in the attribute passed, see if they're in the system
793 793 _email = author_email(author)
794 794 if _email != '':
795 795 user = User.get_by_email(_email, case_insensitive=True, cache=True)
796 796 if user is not None:
797 797 return user
798 798
799 799 # Maybe it's a username, we try to extract it and fetch by username ?
800 800 _author = author_name(author)
801 801 user = User.get_by_username(_author, case_insensitive=True, cache=True)
802 802 if user is not None:
803 803 return user
804 804
805 805 return None
806 806
807 807
808 808 def email_or_none(author):
809 809 # extract email from the commit string
810 810 _email = author_email(author)
811 811
812 812 # If we have an email, use it, otherwise
813 813 # see if it contains a username we can get an email from
814 814 if _email != '':
815 815 return _email
816 816 else:
817 817 user = User.get_by_username(
818 818 author_name(author), case_insensitive=True, cache=True)
819 819
820 820 if user is not None:
821 821 return user.email
822 822
823 823 # No valid email, not a valid user in the system, none!
824 824 return None
825 825
826 826
827 827 def link_to_user(author, length=0, **kwargs):
828 828 user = discover_user(author)
829 829 # user can be None, but if we have it already it means we can re-use it
830 830 # in the person() function, so we save 1 intensive-query
831 831 if user:
832 832 author = user
833 833
834 834 display_person = person(author, 'username_or_name_or_email')
835 835 if length:
836 836 display_person = shorter(display_person, length)
837 837
838 838 if user:
839 839 return link_to(
840 840 escape(display_person),
841 841 url('user_profile', username=user.username),
842 842 **kwargs)
843 843 else:
844 844 return escape(display_person)
845 845
846 846
847 847 def person(author, show_attr="username_and_name"):
848 848 user = discover_user(author)
849 849 if user:
850 850 return getattr(user, show_attr)
851 851 else:
852 852 _author = author_name(author)
853 853 _email = email(author)
854 854 return _author or _email
855 855
856 856
857 857 def author_string(email):
858 858 if email:
859 859 user = User.get_by_email(email, case_insensitive=True, cache=True)
860 860 if user:
861 861 if user.firstname or user.lastname:
862 862 return '%s %s &lt;%s&gt;' % (user.firstname, user.lastname, email)
863 863 else:
864 864 return email
865 865 else:
866 866 return email
867 867 else:
868 868 return None
869 869
870 870
871 871 def person_by_id(id_, show_attr="username_and_name"):
872 872 # attr to return from fetched user
873 873 person_getter = lambda usr: getattr(usr, show_attr)
874 874
875 875 #maybe it's an ID ?
876 876 if str(id_).isdigit() or isinstance(id_, int):
877 877 id_ = int(id_)
878 878 user = User.get(id_)
879 879 if user is not None:
880 880 return person_getter(user)
881 881 return id_
882 882
883 883
884 884 def gravatar_with_user(author, show_disabled=False):
885 885 from rhodecode.lib.utils import PartialRenderer
886 886 _render = PartialRenderer('base/base.html')
887 887 return _render('gravatar_with_user', author, show_disabled=show_disabled)
888 888
889 889
890 890 def desc_stylize(value):
891 891 """
892 892 converts tags from value into html equivalent
893 893
894 894 :param value:
895 895 """
896 896 if not value:
897 897 return ''
898 898
899 899 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
900 900 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
901 901 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
902 902 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
903 903 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
904 904 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
905 905 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
906 906 '<div class="metatag" tag="lang">\\2</div>', value)
907 907 value = re.sub(r'\[([a-z]+)\]',
908 908 '<div class="metatag" tag="\\1">\\1</div>', value)
909 909
910 910 return value
911 911
912 912
913 913 def escaped_stylize(value):
914 914 """
915 915 converts tags from value into html equivalent, but escaping its value first
916 916 """
917 917 if not value:
918 918 return ''
919 919
920 920 # Using default webhelper escape method, but has to force it as a
921 921 # plain unicode instead of a markup tag to be used in regex expressions
922 922 value = unicode(escape(safe_unicode(value)))
923 923
924 924 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
925 925 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
926 926 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
927 927 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
928 928 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
929 929 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
930 930 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
931 931 '<div class="metatag" tag="lang">\\2</div>', value)
932 932 value = re.sub(r'\[([a-z]+)\]',
933 933 '<div class="metatag" tag="\\1">\\1</div>', value)
934 934
935 935 return value
936 936
937 937
938 938 def bool2icon(value):
939 939 """
940 940 Returns boolean value of a given value, represented as html element with
941 941 classes that will represent icons
942 942
943 943 :param value: given value to convert to html node
944 944 """
945 945
946 946 if value: # does bool conversion
947 947 return HTML.tag('i', class_="icon-true")
948 948 else: # not true as bool
949 949 return HTML.tag('i', class_="icon-false")
950 950
951 951
952 952 #==============================================================================
953 953 # PERMS
954 954 #==============================================================================
955 955 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
956 956 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
957 957 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
958 958 csrf_token_key
959 959
960 960
961 961 #==============================================================================
962 962 # GRAVATAR URL
963 963 #==============================================================================
964 964 class InitialsGravatar(object):
965 965 def __init__(self, email_address, first_name, last_name, size=30,
966 966 background=None, text_color='#fff'):
967 967 self.size = size
968 968 self.first_name = first_name
969 969 self.last_name = last_name
970 970 self.email_address = email_address
971 971 self.background = background or self.str2color(email_address)
972 972 self.text_color = text_color
973 973
974 974 def get_color_bank(self):
975 975 """
976 976 returns a predefined list of colors that gravatars can use.
977 977 Those are randomized distinct colors that guarantee readability and
978 978 uniqueness.
979 979
980 980 generated with: http://phrogz.net/css/distinct-colors.html
981 981 """
982 982 return [
983 983 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
984 984 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
985 985 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
986 986 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
987 987 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
988 988 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
989 989 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
990 990 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
991 991 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
992 992 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
993 993 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
994 994 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
995 995 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
996 996 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
997 997 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
998 998 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
999 999 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1000 1000 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1001 1001 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1002 1002 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1003 1003 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1004 1004 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1005 1005 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1006 1006 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1007 1007 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1008 1008 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1009 1009 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1010 1010 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1011 1011 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1012 1012 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1013 1013 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1014 1014 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1015 1015 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1016 1016 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1017 1017 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1018 1018 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1019 1019 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1020 1020 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1021 1021 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1022 1022 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1023 1023 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1024 1024 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1025 1025 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1026 1026 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1027 1027 '#4f8c46', '#368dd9', '#5c0073'
1028 1028 ]
1029 1029
1030 1030 def rgb_to_hex_color(self, rgb_tuple):
1031 1031 """
1032 1032 Converts an rgb_tuple passed to an hex color.
1033 1033
1034 1034 :param rgb_tuple: tuple with 3 ints represents rgb color space
1035 1035 """
1036 1036 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1037 1037
1038 1038 def email_to_int_list(self, email_str):
1039 1039 """
1040 1040 Get every byte of the hex digest value of email and turn it to integer.
1041 1041 It's going to be always between 0-255
1042 1042 """
1043 1043 digest = md5_safe(email_str.lower())
1044 1044 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1045 1045
1046 1046 def pick_color_bank_index(self, email_str, color_bank):
1047 1047 return self.email_to_int_list(email_str)[0] % len(color_bank)
1048 1048
1049 1049 def str2color(self, email_str):
1050 1050 """
1051 1051 Tries to map in a stable algorithm an email to color
1052 1052
1053 1053 :param email_str:
1054 1054 """
1055 1055 color_bank = self.get_color_bank()
1056 1056 # pick position (module it's length so we always find it in the
1057 1057 # bank even if it's smaller than 256 values
1058 1058 pos = self.pick_color_bank_index(email_str, color_bank)
1059 1059 return color_bank[pos]
1060 1060
1061 1061 def normalize_email(self, email_address):
1062 1062 import unicodedata
1063 1063 # default host used to fill in the fake/missing email
1064 1064 default_host = u'localhost'
1065 1065
1066 1066 if not email_address:
1067 1067 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1068 1068
1069 1069 email_address = safe_unicode(email_address)
1070 1070
1071 1071 if u'@' not in email_address:
1072 1072 email_address = u'%s@%s' % (email_address, default_host)
1073 1073
1074 1074 if email_address.endswith(u'@'):
1075 1075 email_address = u'%s%s' % (email_address, default_host)
1076 1076
1077 1077 email_address = unicodedata.normalize('NFKD', email_address)\
1078 1078 .encode('ascii', 'ignore')
1079 1079 return email_address
1080 1080
1081 1081 def get_initials(self):
1082 1082 """
1083 1083 Returns 2 letter initials calculated based on the input.
1084 1084 The algorithm picks first given email address, and takes first letter
1085 1085 of part before @, and then the first letter of server name. In case
1086 1086 the part before @ is in a format of `somestring.somestring2` it replaces
1087 1087 the server letter with first letter of somestring2
1088 1088
1089 1089 In case function was initialized with both first and lastname, this
1090 1090 overrides the extraction from email by first letter of the first and
1091 1091 last name. We add special logic to that functionality, In case Full name
1092 1092 is compound, like Guido Von Rossum, we use last part of the last name
1093 1093 (Von Rossum) picking `R`.
1094 1094
1095 1095 Function also normalizes the non-ascii characters to they ascii
1096 1096 representation, eg Δ„ => A
1097 1097 """
1098 1098 import unicodedata
1099 1099 # replace non-ascii to ascii
1100 1100 first_name = unicodedata.normalize(
1101 1101 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1102 1102 last_name = unicodedata.normalize(
1103 1103 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1104 1104
1105 1105 # do NFKD encoding, and also make sure email has proper format
1106 1106 email_address = self.normalize_email(self.email_address)
1107 1107
1108 1108 # first push the email initials
1109 1109 prefix, server = email_address.split('@', 1)
1110 1110
1111 1111 # check if prefix is maybe a 'firstname.lastname' syntax
1112 1112 _dot_split = prefix.rsplit('.', 1)
1113 1113 if len(_dot_split) == 2:
1114 1114 initials = [_dot_split[0][0], _dot_split[1][0]]
1115 1115 else:
1116 1116 initials = [prefix[0], server[0]]
1117 1117
1118 1118 # then try to replace either firtname or lastname
1119 1119 fn_letter = (first_name or " ")[0].strip()
1120 1120 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1121 1121
1122 1122 if fn_letter:
1123 1123 initials[0] = fn_letter
1124 1124
1125 1125 if ln_letter:
1126 1126 initials[1] = ln_letter
1127 1127
1128 1128 return ''.join(initials).upper()
1129 1129
1130 1130 def get_img_data_by_type(self, font_family, img_type):
1131 1131 default_user = """
1132 1132 <svg xmlns="http://www.w3.org/2000/svg"
1133 1133 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1134 1134 viewBox="-15 -10 439.165 429.164"
1135 1135
1136 1136 xml:space="preserve"
1137 1137 style="background:{background};" >
1138 1138
1139 1139 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1140 1140 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1141 1141 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1142 1142 168.596,153.916,216.671,
1143 1143 204.583,216.671z" fill="{text_color}"/>
1144 1144 <path d="M407.164,374.717L360.88,
1145 1145 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1146 1146 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1147 1147 15.366-44.203,23.488-69.076,23.488c-24.877,
1148 1148 0-48.762-8.122-69.078-23.488
1149 1149 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1150 1150 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1151 1151 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1152 1152 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1153 1153 19.402-10.527 C409.699,390.129,
1154 1154 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1155 1155 </svg>""".format(
1156 1156 size=self.size,
1157 1157 background='#979797', # @grey4
1158 1158 text_color=self.text_color,
1159 1159 font_family=font_family)
1160 1160
1161 1161 return {
1162 1162 "default_user": default_user
1163 1163 }[img_type]
1164 1164
1165 1165 def get_img_data(self, svg_type=None):
1166 1166 """
1167 1167 generates the svg metadata for image
1168 1168 """
1169 1169
1170 1170 font_family = ','.join([
1171 1171 'proximanovaregular',
1172 1172 'Proxima Nova Regular',
1173 1173 'Proxima Nova',
1174 1174 'Arial',
1175 1175 'Lucida Grande',
1176 1176 'sans-serif'
1177 1177 ])
1178 1178 if svg_type:
1179 1179 return self.get_img_data_by_type(font_family, svg_type)
1180 1180
1181 1181 initials = self.get_initials()
1182 1182 img_data = """
1183 1183 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1184 1184 width="{size}" height="{size}"
1185 1185 style="width: 100%; height: 100%; background-color: {background}"
1186 1186 viewBox="0 0 {size} {size}">
1187 1187 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1188 1188 pointer-events="auto" fill="{text_color}"
1189 1189 font-family="{font_family}"
1190 1190 style="font-weight: 400; font-size: {f_size}px;">{text}
1191 1191 </text>
1192 1192 </svg>""".format(
1193 1193 size=self.size,
1194 1194 f_size=self.size/1.85, # scale the text inside the box nicely
1195 1195 background=self.background,
1196 1196 text_color=self.text_color,
1197 1197 text=initials.upper(),
1198 1198 font_family=font_family)
1199 1199
1200 1200 return img_data
1201 1201
1202 1202 def generate_svg(self, svg_type=None):
1203 1203 img_data = self.get_img_data(svg_type)
1204 1204 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1205 1205
1206 1206
1207 1207 def initials_gravatar(email_address, first_name, last_name, size=30):
1208 1208 svg_type = None
1209 1209 if email_address == User.DEFAULT_USER_EMAIL:
1210 1210 svg_type = 'default_user'
1211 1211 klass = InitialsGravatar(email_address, first_name, last_name, size)
1212 1212 return klass.generate_svg(svg_type=svg_type)
1213 1213
1214 1214
1215 1215 def gravatar_url(email_address, size=30):
1216 1216 # doh, we need to re-import those to mock it later
1217 1217 from pylons import tmpl_context as c
1218 1218
1219 1219 _use_gravatar = c.visual.use_gravatar
1220 1220 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
1221 1221
1222 1222 email_address = email_address or User.DEFAULT_USER_EMAIL
1223 1223 if isinstance(email_address, unicode):
1224 1224 # hashlib crashes on unicode items
1225 1225 email_address = safe_str(email_address)
1226 1226
1227 1227 # empty email or default user
1228 1228 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1229 1229 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1230 1230
1231 1231 if _use_gravatar:
1232 1232 # TODO: Disuse pyramid thread locals. Think about another solution to
1233 1233 # get the host and schema here.
1234 1234 request = get_current_request()
1235 1235 tmpl = safe_str(_gravatar_url)
1236 1236 tmpl = tmpl.replace('{email}', email_address)\
1237 1237 .replace('{md5email}', md5_safe(email_address.lower())) \
1238 1238 .replace('{netloc}', request.host)\
1239 1239 .replace('{scheme}', request.scheme)\
1240 1240 .replace('{size}', safe_str(size))
1241 1241 return tmpl
1242 1242 else:
1243 1243 return initials_gravatar(email_address, '', '', size=size)
1244 1244
1245 1245
1246 1246 class Page(_Page):
1247 1247 """
1248 1248 Custom pager to match rendering style with paginator
1249 1249 """
1250 1250
1251 1251 def _get_pos(self, cur_page, max_page, items):
1252 1252 edge = (items / 2) + 1
1253 1253 if (cur_page <= edge):
1254 1254 radius = max(items / 2, items - cur_page)
1255 1255 elif (max_page - cur_page) < edge:
1256 1256 radius = (items - 1) - (max_page - cur_page)
1257 1257 else:
1258 1258 radius = items / 2
1259 1259
1260 1260 left = max(1, (cur_page - (radius)))
1261 1261 right = min(max_page, cur_page + (radius))
1262 1262 return left, cur_page, right
1263 1263
1264 1264 def _range(self, regexp_match):
1265 1265 """
1266 1266 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1267 1267
1268 1268 Arguments:
1269 1269
1270 1270 regexp_match
1271 1271 A "re" (regular expressions) match object containing the
1272 1272 radius of linked pages around the current page in
1273 1273 regexp_match.group(1) as a string
1274 1274
1275 1275 This function is supposed to be called as a callable in
1276 1276 re.sub.
1277 1277
1278 1278 """
1279 1279 radius = int(regexp_match.group(1))
1280 1280
1281 1281 # Compute the first and last page number within the radius
1282 1282 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1283 1283 # -> leftmost_page = 5
1284 1284 # -> rightmost_page = 9
1285 1285 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1286 1286 self.last_page,
1287 1287 (radius * 2) + 1)
1288 1288 nav_items = []
1289 1289
1290 1290 # Create a link to the first page (unless we are on the first page
1291 1291 # or there would be no need to insert '..' spacers)
1292 1292 if self.page != self.first_page and self.first_page < leftmost_page:
1293 1293 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1294 1294
1295 1295 # Insert dots if there are pages between the first page
1296 1296 # and the currently displayed page range
1297 1297 if leftmost_page - self.first_page > 1:
1298 1298 # Wrap in a SPAN tag if nolink_attr is set
1299 1299 text = '..'
1300 1300 if self.dotdot_attr:
1301 1301 text = HTML.span(c=text, **self.dotdot_attr)
1302 1302 nav_items.append(text)
1303 1303
1304 1304 for thispage in xrange(leftmost_page, rightmost_page + 1):
1305 1305 # Hilight the current page number and do not use a link
1306 1306 if thispage == self.page:
1307 1307 text = '%s' % (thispage,)
1308 1308 # Wrap in a SPAN tag if nolink_attr is set
1309 1309 if self.curpage_attr:
1310 1310 text = HTML.span(c=text, **self.curpage_attr)
1311 1311 nav_items.append(text)
1312 1312 # Otherwise create just a link to that page
1313 1313 else:
1314 1314 text = '%s' % (thispage,)
1315 1315 nav_items.append(self._pagerlink(thispage, text))
1316 1316
1317 1317 # Insert dots if there are pages between the displayed
1318 1318 # page numbers and the end of the page range
1319 1319 if self.last_page - rightmost_page > 1:
1320 1320 text = '..'
1321 1321 # Wrap in a SPAN tag if nolink_attr is set
1322 1322 if self.dotdot_attr:
1323 1323 text = HTML.span(c=text, **self.dotdot_attr)
1324 1324 nav_items.append(text)
1325 1325
1326 1326 # Create a link to the very last page (unless we are on the last
1327 1327 # page or there would be no need to insert '..' spacers)
1328 1328 if self.page != self.last_page and rightmost_page < self.last_page:
1329 1329 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1330 1330
1331 1331 ## prerender links
1332 1332 #_page_link = url.current()
1333 1333 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1334 1334 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1335 1335 return self.separator.join(nav_items)
1336 1336
1337 1337 def pager(self, format='~2~', page_param='page', partial_param='partial',
1338 1338 show_if_single_page=False, separator=' ', onclick=None,
1339 1339 symbol_first='<<', symbol_last='>>',
1340 1340 symbol_previous='<', symbol_next='>',
1341 1341 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1342 1342 curpage_attr={'class': 'pager_curpage'},
1343 1343 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1344 1344
1345 1345 self.curpage_attr = curpage_attr
1346 1346 self.separator = separator
1347 1347 self.pager_kwargs = kwargs
1348 1348 self.page_param = page_param
1349 1349 self.partial_param = partial_param
1350 1350 self.onclick = onclick
1351 1351 self.link_attr = link_attr
1352 1352 self.dotdot_attr = dotdot_attr
1353 1353
1354 1354 # Don't show navigator if there is no more than one page
1355 1355 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1356 1356 return ''
1357 1357
1358 1358 from string import Template
1359 1359 # Replace ~...~ in token format by range of pages
1360 1360 result = re.sub(r'~(\d+)~', self._range, format)
1361 1361
1362 1362 # Interpolate '%' variables
1363 1363 result = Template(result).safe_substitute({
1364 1364 'first_page': self.first_page,
1365 1365 'last_page': self.last_page,
1366 1366 'page': self.page,
1367 1367 'page_count': self.page_count,
1368 1368 'items_per_page': self.items_per_page,
1369 1369 'first_item': self.first_item,
1370 1370 'last_item': self.last_item,
1371 1371 'item_count': self.item_count,
1372 1372 'link_first': self.page > self.first_page and \
1373 1373 self._pagerlink(self.first_page, symbol_first) or '',
1374 1374 'link_last': self.page < self.last_page and \
1375 1375 self._pagerlink(self.last_page, symbol_last) or '',
1376 1376 'link_previous': self.previous_page and \
1377 1377 self._pagerlink(self.previous_page, symbol_previous) \
1378 1378 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1379 1379 'link_next': self.next_page and \
1380 1380 self._pagerlink(self.next_page, symbol_next) \
1381 1381 or HTML.span(symbol_next, class_="pg-next disabled")
1382 1382 })
1383 1383
1384 1384 return literal(result)
1385 1385
1386 1386
1387 1387 #==============================================================================
1388 1388 # REPO PAGER, PAGER FOR REPOSITORY
1389 1389 #==============================================================================
1390 1390 class RepoPage(Page):
1391 1391
1392 1392 def __init__(self, collection, page=1, items_per_page=20,
1393 1393 item_count=None, url=None, **kwargs):
1394 1394
1395 1395 """Create a "RepoPage" instance. special pager for paging
1396 1396 repository
1397 1397 """
1398 1398 self._url_generator = url
1399 1399
1400 1400 # Safe the kwargs class-wide so they can be used in the pager() method
1401 1401 self.kwargs = kwargs
1402 1402
1403 1403 # Save a reference to the collection
1404 1404 self.original_collection = collection
1405 1405
1406 1406 self.collection = collection
1407 1407
1408 1408 # The self.page is the number of the current page.
1409 1409 # The first page has the number 1!
1410 1410 try:
1411 1411 self.page = int(page) # make it int() if we get it as a string
1412 1412 except (ValueError, TypeError):
1413 1413 self.page = 1
1414 1414
1415 1415 self.items_per_page = items_per_page
1416 1416
1417 1417 # Unless the user tells us how many items the collections has
1418 1418 # we calculate that ourselves.
1419 1419 if item_count is not None:
1420 1420 self.item_count = item_count
1421 1421 else:
1422 1422 self.item_count = len(self.collection)
1423 1423
1424 1424 # Compute the number of the first and last available page
1425 1425 if self.item_count > 0:
1426 1426 self.first_page = 1
1427 1427 self.page_count = int(math.ceil(float(self.item_count) /
1428 1428 self.items_per_page))
1429 1429 self.last_page = self.first_page + self.page_count - 1
1430 1430
1431 1431 # Make sure that the requested page number is the range of
1432 1432 # valid pages
1433 1433 if self.page > self.last_page:
1434 1434 self.page = self.last_page
1435 1435 elif self.page < self.first_page:
1436 1436 self.page = self.first_page
1437 1437
1438 1438 # Note: the number of items on this page can be less than
1439 1439 # items_per_page if the last page is not full
1440 1440 self.first_item = max(0, (self.item_count) - (self.page *
1441 1441 items_per_page))
1442 1442 self.last_item = ((self.item_count - 1) - items_per_page *
1443 1443 (self.page - 1))
1444 1444
1445 1445 self.items = list(self.collection[self.first_item:self.last_item + 1])
1446 1446
1447 1447 # Links to previous and next page
1448 1448 if self.page > self.first_page:
1449 1449 self.previous_page = self.page - 1
1450 1450 else:
1451 1451 self.previous_page = None
1452 1452
1453 1453 if self.page < self.last_page:
1454 1454 self.next_page = self.page + 1
1455 1455 else:
1456 1456 self.next_page = None
1457 1457
1458 1458 # No items available
1459 1459 else:
1460 1460 self.first_page = None
1461 1461 self.page_count = 0
1462 1462 self.last_page = None
1463 1463 self.first_item = None
1464 1464 self.last_item = None
1465 1465 self.previous_page = None
1466 1466 self.next_page = None
1467 1467 self.items = []
1468 1468
1469 1469 # This is a subclass of the 'list' type. Initialise the list now.
1470 1470 list.__init__(self, reversed(self.items))
1471 1471
1472 1472
1473 1473 def changed_tooltip(nodes):
1474 1474 """
1475 1475 Generates a html string for changed nodes in commit page.
1476 1476 It limits the output to 30 entries
1477 1477
1478 1478 :param nodes: LazyNodesGenerator
1479 1479 """
1480 1480 if nodes:
1481 1481 pref = ': <br/> '
1482 1482 suf = ''
1483 1483 if len(nodes) > 30:
1484 1484 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1485 1485 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1486 1486 for x in nodes[:30]]) + suf)
1487 1487 else:
1488 1488 return ': ' + _('No Files')
1489 1489
1490 1490
1491 1491 def breadcrumb_repo_link(repo):
1492 1492 """
1493 1493 Makes a breadcrumbs path link to repo
1494 1494
1495 1495 ex::
1496 1496 group >> subgroup >> repo
1497 1497
1498 1498 :param repo: a Repository instance
1499 1499 """
1500 1500
1501 1501 path = [
1502 1502 link_to(group.name, url('repo_group_home', group_name=group.group_name))
1503 1503 for group in repo.groups_with_parents
1504 1504 ] + [
1505 1505 link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name))
1506 1506 ]
1507 1507
1508 1508 return literal(' &raquo; '.join(path))
1509 1509
1510 1510
1511 1511 def format_byte_size_binary(file_size):
1512 1512 """
1513 1513 Formats file/folder sizes to standard.
1514 1514 """
1515 1515 formatted_size = format_byte_size(file_size, binary=True)
1516 1516 return formatted_size
1517 1517
1518 1518
1519 1519 def fancy_file_stats(stats):
1520 1520 """
1521 1521 Displays a fancy two colored bar for number of added/deleted
1522 1522 lines of code on file
1523 1523
1524 1524 :param stats: two element list of added/deleted lines of code
1525 1525 """
1526 1526 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1527 1527 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1528 1528
1529 1529 def cgen(l_type, a_v, d_v):
1530 1530 mapping = {'tr': 'top-right-rounded-corner-mid',
1531 1531 'tl': 'top-left-rounded-corner-mid',
1532 1532 'br': 'bottom-right-rounded-corner-mid',
1533 1533 'bl': 'bottom-left-rounded-corner-mid'}
1534 1534 map_getter = lambda x: mapping[x]
1535 1535
1536 1536 if l_type == 'a' and d_v:
1537 1537 #case when added and deleted are present
1538 1538 return ' '.join(map(map_getter, ['tl', 'bl']))
1539 1539
1540 1540 if l_type == 'a' and not d_v:
1541 1541 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1542 1542
1543 1543 if l_type == 'd' and a_v:
1544 1544 return ' '.join(map(map_getter, ['tr', 'br']))
1545 1545
1546 1546 if l_type == 'd' and not a_v:
1547 1547 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1548 1548
1549 1549 a, d = stats['added'], stats['deleted']
1550 1550 width = 100
1551 1551
1552 1552 if stats['binary']: # binary operations like chmod/rename etc
1553 1553 lbl = []
1554 1554 bin_op = 0 # undefined
1555 1555
1556 1556 # prefix with bin for binary files
1557 1557 if BIN_FILENODE in stats['ops']:
1558 1558 lbl += ['bin']
1559 1559
1560 1560 if NEW_FILENODE in stats['ops']:
1561 1561 lbl += [_('new file')]
1562 1562 bin_op = NEW_FILENODE
1563 1563 elif MOD_FILENODE in stats['ops']:
1564 1564 lbl += [_('mod')]
1565 1565 bin_op = MOD_FILENODE
1566 1566 elif DEL_FILENODE in stats['ops']:
1567 1567 lbl += [_('del')]
1568 1568 bin_op = DEL_FILENODE
1569 1569 elif RENAMED_FILENODE in stats['ops']:
1570 1570 lbl += [_('rename')]
1571 1571 bin_op = RENAMED_FILENODE
1572 1572
1573 1573 # chmod can go with other operations, so we add a + to lbl if needed
1574 1574 if CHMOD_FILENODE in stats['ops']:
1575 1575 lbl += [_('chmod')]
1576 1576 if bin_op == 0:
1577 1577 bin_op = CHMOD_FILENODE
1578 1578
1579 1579 lbl = '+'.join(lbl)
1580 1580 b_a = '<div class="bin bin%s %s" style="width:100%%">%s</div>' \
1581 1581 % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1582 1582 b_d = '<div class="bin bin1" style="width:0%%"></div>'
1583 1583 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1584 1584
1585 1585 t = stats['added'] + stats['deleted']
1586 1586 unit = float(width) / (t or 1)
1587 1587
1588 1588 # needs > 9% of width to be visible or 0 to be hidden
1589 1589 a_p = max(9, unit * a) if a > 0 else 0
1590 1590 d_p = max(9, unit * d) if d > 0 else 0
1591 1591 p_sum = a_p + d_p
1592 1592
1593 1593 if p_sum > width:
1594 1594 #adjust the percentage to be == 100% since we adjusted to 9
1595 1595 if a_p > d_p:
1596 1596 a_p = a_p - (p_sum - width)
1597 1597 else:
1598 1598 d_p = d_p - (p_sum - width)
1599 1599
1600 1600 a_v = a if a > 0 else ''
1601 1601 d_v = d if d > 0 else ''
1602 1602
1603 1603 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1604 1604 cgen('a', a_v, d_v), a_p, a_v
1605 1605 )
1606 1606 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1607 1607 cgen('d', a_v, d_v), d_p, d_v
1608 1608 )
1609 1609 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1610 1610
1611 1611
1612 1612 def urlify_text(text_, safe=True):
1613 1613 """
1614 1614 Extrac urls from text and make html links out of them
1615 1615
1616 1616 :param text_:
1617 1617 """
1618 1618
1619 1619 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1620 1620 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1621 1621
1622 1622 def url_func(match_obj):
1623 1623 url_full = match_obj.groups()[0]
1624 1624 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1625 1625 _newtext = url_pat.sub(url_func, text_)
1626 1626 if safe:
1627 1627 return literal(_newtext)
1628 1628 return _newtext
1629 1629
1630 1630
1631 1631 def urlify_commits(text_, repository):
1632 1632 """
1633 1633 Extract commit ids from text and make link from them
1634 1634
1635 1635 :param text_:
1636 1636 :param repository: repo name to build the URL with
1637 1637 """
1638 1638 from pylons import url # doh, we need to re-import url to mock it later
1639 1639 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1640 1640
1641 1641 def url_func(match_obj):
1642 1642 commit_id = match_obj.groups()[1]
1643 1643 pref = match_obj.groups()[0]
1644 1644 suf = match_obj.groups()[2]
1645 1645
1646 1646 tmpl = (
1647 1647 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1648 1648 '%(commit_id)s</a>%(suf)s'
1649 1649 )
1650 1650 return tmpl % {
1651 1651 'pref': pref,
1652 1652 'cls': 'revision-link',
1653 1653 'url': url('changeset_home', repo_name=repository,
1654 1654 revision=commit_id, qualified=True),
1655 1655 'commit_id': commit_id,
1656 1656 'suf': suf
1657 1657 }
1658 1658
1659 1659 newtext = URL_PAT.sub(url_func, text_)
1660 1660
1661 1661 return newtext
1662 1662
1663 1663
1664 1664 def _process_url_func(match_obj, repo_name, uid, entry,
1665 1665 return_raw_data=False):
1666 1666 pref = ''
1667 1667 if match_obj.group().startswith(' '):
1668 1668 pref = ' '
1669 1669
1670 1670 issue_id = ''.join(match_obj.groups())
1671 1671 tmpl = (
1672 1672 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1673 1673 '%(issue-prefix)s%(id-repr)s'
1674 1674 '</a>')
1675 1675
1676 1676 (repo_name_cleaned,
1677 1677 parent_group_name) = RepoGroupModel().\
1678 1678 _get_group_name_and_parent(repo_name)
1679 1679
1680 1680 # variables replacement
1681 1681 named_vars = {
1682 1682 'id': issue_id,
1683 1683 'repo': repo_name,
1684 1684 'repo_name': repo_name_cleaned,
1685 1685 'group_name': parent_group_name
1686 1686 }
1687 1687 # named regex variables
1688 1688 named_vars.update(match_obj.groupdict())
1689 1689 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1690 1690
1691 1691 data = {
1692 1692 'pref': pref,
1693 1693 'cls': 'issue-tracker-link',
1694 1694 'url': _url,
1695 1695 'id-repr': issue_id,
1696 1696 'issue-prefix': entry['pref'],
1697 1697 'serv': entry['url'],
1698 1698 }
1699 1699 if return_raw_data:
1700 1700 return {
1701 1701 'id': issue_id,
1702 1702 'url': _url
1703 1703 }
1704 1704 return tmpl % data
1705 1705
1706 1706
1707 1707 def process_patterns(text_string, repo_name, config=None):
1708 1708 repo = None
1709 1709 if repo_name:
1710 1710 # Retrieving repo_name to avoid invalid repo_name to explode on
1711 1711 # IssueTrackerSettingsModel but still passing invalid name further down
1712 1712 repo = Repository.get_by_repo_name(repo_name, cache=True)
1713 1713
1714 1714 settings_model = IssueTrackerSettingsModel(repo=repo)
1715 1715 active_entries = settings_model.get_settings(cache=True)
1716 1716
1717 1717 issues_data = []
1718 1718 newtext = text_string
1719 1719 for uid, entry in active_entries.items():
1720 1720 log.debug('found issue tracker entry with uid %s' % (uid,))
1721 1721
1722 1722 if not (entry['pat'] and entry['url']):
1723 1723 log.debug('skipping due to missing data')
1724 1724 continue
1725 1725
1726 1726 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1727 1727 % (uid, entry['pat'], entry['url'], entry['pref']))
1728 1728
1729 1729 try:
1730 1730 pattern = re.compile(r'%s' % entry['pat'])
1731 1731 except re.error:
1732 1732 log.exception(
1733 1733 'issue tracker pattern: `%s` failed to compile',
1734 1734 entry['pat'])
1735 1735 continue
1736 1736
1737 1737 data_func = partial(
1738 1738 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1739 1739 return_raw_data=True)
1740 1740
1741 1741 for match_obj in pattern.finditer(text_string):
1742 1742 issues_data.append(data_func(match_obj))
1743 1743
1744 1744 url_func = partial(
1745 1745 _process_url_func, repo_name=repo_name, entry=entry, uid=uid)
1746 1746
1747 1747 newtext = pattern.sub(url_func, newtext)
1748 1748 log.debug('processed prefix:uid `%s`' % (uid,))
1749 1749
1750 1750 return newtext, issues_data
1751 1751
1752 1752
1753 1753 def urlify_commit_message(commit_text, repository=None):
1754 1754 """
1755 1755 Parses given text message and makes proper links.
1756 1756 issues are linked to given issue-server, and rest is a commit link
1757 1757
1758 1758 :param commit_text:
1759 1759 :param repository:
1760 1760 """
1761 1761 from pylons import url # doh, we need to re-import url to mock it later
1762 1762
1763 1763 def escaper(string):
1764 1764 return string.replace('<', '&lt;').replace('>', '&gt;')
1765 1765
1766 1766 newtext = escaper(commit_text)
1767 1767
1768 1768 # extract http/https links and make them real urls
1769 1769 newtext = urlify_text(newtext, safe=False)
1770 1770
1771 1771 # urlify commits - extract commit ids and make link out of them, if we have
1772 1772 # the scope of repository present.
1773 1773 if repository:
1774 1774 newtext = urlify_commits(newtext, repository)
1775 1775
1776 1776 # process issue tracker patterns
1777 1777 newtext, issues = process_patterns(newtext, repository or '')
1778 1778
1779 1779 return literal(newtext)
1780 1780
1781 1781
1782 1782 def rst(source, mentions=False):
1783 1783 return literal('<div class="rst-block">%s</div>' %
1784 1784 MarkupRenderer.rst(source, mentions=mentions))
1785 1785
1786 1786
1787 1787 def markdown(source, mentions=False):
1788 1788 return literal('<div class="markdown-block">%s</div>' %
1789 1789 MarkupRenderer.markdown(source, flavored=True,
1790 1790 mentions=mentions))
1791 1791
1792 1792 def renderer_from_filename(filename, exclude=None):
1793 1793 return MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1794 1794
1795 1795
1796 1796 def render(source, renderer='rst', mentions=False):
1797 1797 if renderer == 'rst':
1798 1798 return rst(source, mentions=mentions)
1799 1799 if renderer == 'markdown':
1800 1800 return markdown(source, mentions=mentions)
1801 1801
1802 1802
1803 1803 def commit_status(repo, commit_id):
1804 1804 return ChangesetStatusModel().get_status(repo, commit_id)
1805 1805
1806 1806
1807 1807 def commit_status_lbl(commit_status):
1808 1808 return dict(ChangesetStatus.STATUSES).get(commit_status)
1809 1809
1810 1810
1811 1811 def commit_time(repo_name, commit_id):
1812 1812 repo = Repository.get_by_repo_name(repo_name)
1813 1813 commit = repo.get_commit(commit_id=commit_id)
1814 1814 return commit.date
1815 1815
1816 1816
1817 1817 def get_permission_name(key):
1818 1818 return dict(Permission.PERMS).get(key)
1819 1819
1820 1820
1821 1821 def journal_filter_help():
1822 1822 return _(
1823 1823 'Example filter terms:\n' +
1824 1824 ' repository:vcs\n' +
1825 1825 ' username:marcin\n' +
1826 1826 ' action:*push*\n' +
1827 1827 ' ip:127.0.0.1\n' +
1828 1828 ' date:20120101\n' +
1829 1829 ' date:[20120101100000 TO 20120102]\n' +
1830 1830 '\n' +
1831 1831 'Generate wildcards using \'*\' character:\n' +
1832 1832 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1833 1833 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1834 1834 '\n' +
1835 1835 'Optional AND / OR operators in queries\n' +
1836 1836 ' "repository:vcs OR repository:test"\n' +
1837 1837 ' "username:test AND repository:test*"\n'
1838 1838 )
1839 1839
1840 1840
1841 1841 def not_mapped_error(repo_name):
1842 1842 flash(_('%s repository is not mapped to db perhaps'
1843 1843 ' it was created or renamed from the filesystem'
1844 1844 ' please run the application again'
1845 1845 ' in order to rescan repositories') % repo_name, category='error')
1846 1846
1847 1847
1848 1848 def ip_range(ip_addr):
1849 1849 from rhodecode.model.db import UserIpMap
1850 1850 s, e = UserIpMap._get_ip_range(ip_addr)
1851 1851 return '%s - %s' % (s, e)
1852 1852
1853 1853
1854 1854 def form(url, method='post', needs_csrf_token=True, **attrs):
1855 1855 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1856 1856 if method.lower() != 'get' and needs_csrf_token:
1857 1857 raise Exception(
1858 1858 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1859 1859 'CSRF token. If the endpoint does not require such token you can ' +
1860 1860 'explicitly set the parameter needs_csrf_token to false.')
1861 1861
1862 1862 return wh_form(url, method=method, **attrs)
1863 1863
1864 1864
1865 1865 def secure_form(url, method="POST", multipart=False, **attrs):
1866 1866 """Start a form tag that points the action to an url. This
1867 1867 form tag will also include the hidden field containing
1868 1868 the auth token.
1869 1869
1870 1870 The url options should be given either as a string, or as a
1871 1871 ``url()`` function. The method for the form defaults to POST.
1872 1872
1873 1873 Options:
1874 1874
1875 1875 ``multipart``
1876 1876 If set to True, the enctype is set to "multipart/form-data".
1877 1877 ``method``
1878 1878 The method to use when submitting the form, usually either
1879 1879 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1880 1880 hidden input with name _method is added to simulate the verb
1881 1881 over POST.
1882 1882
1883 1883 """
1884 1884 from webhelpers.pylonslib.secure_form import insecure_form
1885 1885 form = insecure_form(url, method, multipart, **attrs)
1886 1886 token = csrf_input()
1887 1887 return literal("%s\n%s" % (form, token))
1888 1888
1889 1889 def csrf_input():
1890 1890 return literal(
1891 1891 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1892 1892 csrf_token_key, csrf_token_key, get_csrf_token()))
1893 1893
1894 1894 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1895 1895 select_html = select(name, selected, options, **attrs)
1896 1896 select2 = """
1897 1897 <script>
1898 1898 $(document).ready(function() {
1899 1899 $('#%s').select2({
1900 1900 containerCssClass: 'drop-menu',
1901 1901 dropdownCssClass: 'drop-menu-dropdown',
1902 1902 dropdownAutoWidth: true%s
1903 1903 });
1904 1904 });
1905 1905 </script>
1906 1906 """
1907 1907 filter_option = """,
1908 1908 minimumResultsForSearch: -1
1909 1909 """
1910 1910 input_id = attrs.get('id') or name
1911 1911 filter_enabled = "" if enable_filter else filter_option
1912 1912 select_script = literal(select2 % (input_id, filter_enabled))
1913 1913
1914 1914 return literal(select_html+select_script)
1915 1915
1916 1916
1917 1917 def get_visual_attr(tmpl_context_var, attr_name):
1918 1918 """
1919 1919 A safe way to get a variable from visual variable of template context
1920 1920
1921 1921 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1922 1922 :param attr_name: name of the attribute we fetch from the c.visual
1923 1923 """
1924 1924 visual = getattr(tmpl_context_var, 'visual', None)
1925 1925 if not visual:
1926 1926 return
1927 1927 else:
1928 1928 return getattr(visual, attr_name, None)
1929 1929
1930 1930
1931 1931 def get_last_path_part(file_node):
1932 1932 if not file_node.path:
1933 1933 return u''
1934 1934
1935 1935 path = safe_unicode(file_node.path.split('/')[-1])
1936 1936 return u'../' + path
1937 1937
1938 1938
1939 1939 def route_path(*args, **kwds):
1940 1940 """
1941 1941 Wrapper around pyramids `route_path` function. It is used to generate
1942 1942 URLs from within pylons views or templates. This will be removed when
1943 1943 pyramid migration if finished.
1944 1944 """
1945 1945 req = get_current_request()
1946 1946 return req.route_path(*args, **kwds)
1947 1947
1948 1948
1949 1949 def static_url(*args, **kwds):
1950 1950 """
1951 1951 Wrapper around pyramids `route_path` function. It is used to generate
1952 1952 URLs from within pylons views or templates. This will be removed when
1953 1953 pyramid migration if finished.
1954 1954 """
1955 1955 req = get_current_request()
1956 1956 return req.static_url(*args, **kwds)
1957 1957
1958 1958
1959 1959 def resource_path(*args, **kwds):
1960 1960 """
1961 1961 Wrapper around pyramids `route_path` function. It is used to generate
1962 1962 URLs from within pylons views or templates. This will be removed when
1963 1963 pyramid migration if finished.
1964 1964 """
1965 1965 req = get_current_request()
1966 1966 return req.resource_path(*args, **kwds)
@@ -1,139 +1,159 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 logging
22 22 from urlparse import urljoin
23 23
24 24 import requests
25 from webob.exc import HTTPNotAcceptable
25 26
26 import rhodecode
27 27 from rhodecode.lib.middleware import simplevcs
28 28 from rhodecode.lib.utils import is_valid_repo
29 from rhodecode.lib.utils2 import str2bool
29 30
30 31 log = logging.getLogger(__name__)
31 32
32 33
33 34 class SimpleSvnApp(object):
34 35 IGNORED_HEADERS = [
35 36 'connection', 'keep-alive', 'content-encoding',
36 37 'transfer-encoding', 'content-length']
37 38
38 39 def __init__(self, config):
39 40 self.config = config
40 41
41 42 def __call__(self, environ, start_response):
42 43 request_headers = self._get_request_headers(environ)
43 44
44 45 data = environ['wsgi.input']
45 46 # johbo: Avoid that we end up with sending the request in chunked
46 47 # transfer encoding (mainly on Gunicorn). If we know the content
47 48 # length, then we should transfer the payload in one request.
48 49 if environ['REQUEST_METHOD'] == 'MKCOL' or 'CONTENT_LENGTH' in environ:
49 50 data = data.read()
50 51
51 52 response = requests.request(
52 53 environ['REQUEST_METHOD'], self._get_url(environ['PATH_INFO']),
53 54 data=data, headers=request_headers)
54 55
55 56 response_headers = self._get_response_headers(response.headers)
56 57 start_response(
57 58 '{} {}'.format(response.status_code, response.reason),
58 59 response_headers)
59 60 return response.iter_content(chunk_size=1024)
60 61
61 62 def _get_url(self, path):
62 63 return urljoin(
63 64 self.config.get('subversion_http_server_url', ''), path)
64 65
65 66 def _get_request_headers(self, environ):
66 67 headers = {}
67 68
68 69 for key in environ:
69 70 if not key.startswith('HTTP_'):
70 71 continue
71 72 new_key = key.split('_')
72 73 new_key = [k.capitalize() for k in new_key[1:]]
73 74 new_key = '-'.join(new_key)
74 75 headers[new_key] = environ[key]
75 76
76 77 if 'CONTENT_TYPE' in environ:
77 78 headers['Content-Type'] = environ['CONTENT_TYPE']
78 79
79 80 if 'CONTENT_LENGTH' in environ:
80 81 headers['Content-Length'] = environ['CONTENT_LENGTH']
81 82
82 83 return headers
83 84
84 85 def _get_response_headers(self, headers):
85 86 headers = [
86 87 (h, headers[h])
87 88 for h in headers
88 89 if h.lower() not in self.IGNORED_HEADERS
89 90 ]
90 91
91 92 # Add custom response header to indicate that this is a VCS response
92 93 # and which backend is used.
93 94 headers.append(('X-RhodeCode-Backend', 'svn'))
94 95
95 96 return headers
96 97
97 98
99 class DisabledSimpleSvnApp(object):
100 def __init__(self, config):
101 self.config = config
102
103 def __call__(self, environ, start_response):
104 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
105 log.warning(reason)
106 return HTTPNotAcceptable(reason)(environ, start_response)
107
108
98 109 class SimpleSvn(simplevcs.SimpleVCS):
99 110
100 111 SCM = 'svn'
101 112 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
113 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
102 114
103 115 def _get_repository_name(self, environ):
104 116 """
105 117 Gets repository name out of PATH_INFO header
106 118
107 119 :param environ: environ where PATH_INFO is stored
108 120 """
109 121 path = environ['PATH_INFO'].split('!')
110 122 repo_name = path[0].strip('/')
111 123
112 124 # SVN includes the whole path in it's requests, including
113 125 # subdirectories inside the repo. Therefore we have to search for
114 126 # the repo root directory.
115 127 if not is_valid_repo(repo_name, self.basepath, self.SCM):
116 128 current_path = ''
117 129 for component in repo_name.split('/'):
118 130 current_path += component
119 131 if is_valid_repo(current_path, self.basepath, self.SCM):
120 132 return current_path
121 133 current_path += '/'
122 134
123 135 return repo_name
124 136
125 137 def _get_action(self, environ):
126 138 return (
127 139 'pull'
128 140 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
129 141 else 'push')
130 142
131 143 def _create_wsgi_app(self, repo_path, repo_name, config):
144 if self._is_svn_enabled():
132 145 return SimpleSvnApp(config)
146 # we don't have http proxy enabled return dummy request handler
147 return DisabledSimpleSvnApp(config)
148
149 def _is_svn_enabled(self):
150 conf = self.repo_vcs_config
151 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
133 152
134 153 def _create_config(self, extras, repo_name):
135 server_url = rhodecode.CONFIG.get(
136 'rhodecode_subversion_http_server_url', '')
137 extras['subversion_http_server_url'] = (
138 server_url or 'http://localhost/')
154 conf = self.repo_vcs_config
155 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
156 server_url = server_url or self.DEFAULT_HTTP_SERVER
157
158 extras['subversion_http_server_url'] = server_url
139 159 return extras
@@ -1,448 +1,452 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-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 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 23 It's implemented with basic auth function
24 24 """
25 25
26 26 import os
27 27 import logging
28 28 import importlib
29 29 from functools import wraps
30 30
31 31 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
32 32 from webob.exc import (
33 33 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
34 34
35 35 import rhodecode
36 36 from rhodecode.authentication.base import authenticate, VCS_TYPE
37 37 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
38 38 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
39 39 from rhodecode.lib.exceptions import (
40 40 HTTPLockedRC, HTTPRequirementError, UserCreationError,
41 41 NotAllowedToCreateUserError)
42 42 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 43 from rhodecode.lib.middleware import appenlight
44 44 from rhodecode.lib.middleware.utils import scm_app
45 45 from rhodecode.lib.utils import (
46 46 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path)
47 47 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
48 48 from rhodecode.lib.vcs.conf import settings as vcs_settings
49 from rhodecode.lib.vcs.backends import base
49 50 from rhodecode.model import meta
50 51 from rhodecode.model.db import User, Repository
51 52 from rhodecode.model.scm import ScmModel
52 53 from rhodecode.model.settings import SettingsModel
53 54
55
54 56 log = logging.getLogger(__name__)
55 57
56 58
57 59 def initialize_generator(factory):
58 60 """
59 61 Initializes the returned generator by draining its first element.
60 62
61 63 This can be used to give a generator an initializer, which is the code
62 64 up to the first yield statement. This decorator enforces that the first
63 65 produced element has the value ``"__init__"`` to make its special
64 66 purpose very explicit in the using code.
65 67 """
66 68
67 69 @wraps(factory)
68 70 def wrapper(*args, **kwargs):
69 71 gen = factory(*args, **kwargs)
70 72 try:
71 73 init = gen.next()
72 74 except StopIteration:
73 75 raise ValueError('Generator must yield at least one element.')
74 76 if init != "__init__":
75 77 raise ValueError('First yielded element must be "__init__".')
76 78 return gen
77 79 return wrapper
78 80
79 81
80 82 class SimpleVCS(object):
81 83 """Common functionality for SCM HTTP handlers."""
82 84
83 85 SCM = 'unknown'
84 86
85 87 def __init__(self, application, config, registry):
86 88 self.registry = registry
87 89 self.application = application
88 90 self.config = config
91 # re-populated by specialized middlewares
92 self.repo_vcs_config = base.Config()
89 93 # base path of repo locations
90 94 self.basepath = get_rhodecode_base_path()
91 95 # authenticate this VCS request using authfunc
92 96 auth_ret_code_detection = \
93 97 str2bool(self.config.get('auth_ret_code_detection', False))
94 98 self.authenticate = BasicAuth(
95 99 '', authenticate, registry, config.get('auth_ret_code'),
96 100 auth_ret_code_detection)
97 101 self.ip_addr = '0.0.0.0'
98 102
99 103 @property
100 104 def scm_app(self):
101 105 custom_implementation = self.config.get('vcs.scm_app_implementation')
102 106 if custom_implementation and custom_implementation != 'pyro4':
103 107 log.info(
104 108 "Using custom implementation of scm_app: %s",
105 109 custom_implementation)
106 110 scm_app_impl = importlib.import_module(custom_implementation)
107 111 else:
108 112 scm_app_impl = scm_app
109 113 return scm_app_impl
110 114
111 115 def _get_by_id(self, repo_name):
112 116 """
113 117 Gets a special pattern _<ID> from clone url and tries to replace it
114 118 with a repository_name for support of _<ID> non changable urls
115 119
116 120 :param repo_name:
117 121 """
118 122
119 123 data = repo_name.split('/')
120 124 if len(data) >= 2:
121 125 from rhodecode.model.repo import RepoModel
122 126 by_id_match = RepoModel().get_repo_by_id(repo_name)
123 127 if by_id_match:
124 128 data[1] = by_id_match.repo_name
125 129
126 130 return safe_str('/'.join(data))
127 131
128 132 def _invalidate_cache(self, repo_name):
129 133 """
130 134 Set's cache for this repository for invalidation on next access
131 135
132 136 :param repo_name: full repo name, also a cache key
133 137 """
134 138 ScmModel().mark_for_invalidation(repo_name)
135 139
136 140 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
137 141 db_repo = Repository.get_by_repo_name(repo_name)
138 142 if not db_repo:
139 143 log.debug('Repository `%s` not found inside the database.',
140 144 repo_name)
141 145 return False
142 146
143 147 if db_repo.repo_type != scm_type:
144 148 log.warning(
145 149 'Repository `%s` have incorrect scm_type, expected %s got %s',
146 150 repo_name, db_repo.repo_type, scm_type)
147 151 return False
148 152
149 153 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
150 154
151 155 def valid_and_active_user(self, user):
152 156 """
153 157 Checks if that user is not empty, and if it's actually object it checks
154 158 if he's active.
155 159
156 160 :param user: user object or None
157 161 :return: boolean
158 162 """
159 163 if user is None:
160 164 return False
161 165
162 166 elif user.active:
163 167 return True
164 168
165 169 return False
166 170
167 171 def _check_permission(self, action, user, repo_name, ip_addr=None):
168 172 """
169 173 Checks permissions using action (push/pull) user and repository
170 174 name
171 175
172 176 :param action: push or pull action
173 177 :param user: user instance
174 178 :param repo_name: repository name
175 179 """
176 180 # check IP
177 181 inherit = user.inherit_default_permissions
178 182 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
179 183 inherit_from_default=inherit)
180 184 if ip_allowed:
181 185 log.info('Access for IP:%s allowed', ip_addr)
182 186 else:
183 187 return False
184 188
185 189 if action == 'push':
186 190 if not HasPermissionAnyMiddleware('repository.write',
187 191 'repository.admin')(user,
188 192 repo_name):
189 193 return False
190 194
191 195 else:
192 196 # any other action need at least read permission
193 197 if not HasPermissionAnyMiddleware('repository.read',
194 198 'repository.write',
195 199 'repository.admin')(user,
196 200 repo_name):
197 201 return False
198 202
199 203 return True
200 204
201 205 def _check_ssl(self, environ, start_response):
202 206 """
203 207 Checks the SSL check flag and returns False if SSL is not present
204 208 and required True otherwise
205 209 """
206 210 org_proto = environ['wsgi._org_proto']
207 211 # check if we have SSL required ! if not it's a bad request !
208 212 require_ssl = str2bool(
209 213 SettingsModel().get_ui_by_key('push_ssl').ui_value)
210 214 if require_ssl and org_proto == 'http':
211 215 log.debug('proto is %s and SSL is required BAD REQUEST !',
212 216 org_proto)
213 217 return False
214 218 return True
215 219
216 220 def __call__(self, environ, start_response):
217 221 try:
218 222 return self._handle_request(environ, start_response)
219 223 except Exception:
220 224 log.exception("Exception while handling request")
221 225 appenlight.track_exception(environ)
222 226 return HTTPInternalServerError()(environ, start_response)
223 227 finally:
224 228 meta.Session.remove()
225 229
226 230 def _handle_request(self, environ, start_response):
227 231
228 232 if not self._check_ssl(environ, start_response):
229 233 reason = ('SSL required, while RhodeCode was unable '
230 234 'to detect this as SSL request')
231 235 log.debug('User not allowed to proceed, %s', reason)
232 236 return HTTPNotAcceptable(reason)(environ, start_response)
233 237
234 238 ip_addr = get_ip_addr(environ)
235 239 username = None
236 240
237 241 # skip passing error to error controller
238 242 environ['pylons.status_code_redirect'] = True
239 243
240 244 # ======================================================================
241 245 # EXTRACT REPOSITORY NAME FROM ENV
242 246 # ======================================================================
243 247 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
244 248 repo_name = self._get_repository_name(environ)
245 249 environ['REPO_NAME'] = repo_name
246 250 log.debug('Extracted repo name is %s', repo_name)
247 251
248 252 # check for type, presence in database and on filesystem
249 253 if not self.is_valid_and_existing_repo(
250 254 repo_name, self.basepath, self.SCM):
251 255 return HTTPNotFound()(environ, start_response)
252 256
253 257 # ======================================================================
254 258 # GET ACTION PULL or PUSH
255 259 # ======================================================================
256 260 action = self._get_action(environ)
257 261
258 262 # ======================================================================
259 263 # CHECK ANONYMOUS PERMISSION
260 264 # ======================================================================
261 265 if action in ['pull', 'push']:
262 266 anonymous_user = User.get_default_user()
263 267 username = anonymous_user.username
264 268 if anonymous_user.active:
265 269 # ONLY check permissions if the user is activated
266 270 anonymous_perm = self._check_permission(
267 271 action, anonymous_user, repo_name, ip_addr)
268 272 else:
269 273 anonymous_perm = False
270 274
271 275 if not anonymous_user.active or not anonymous_perm:
272 276 if not anonymous_user.active:
273 277 log.debug('Anonymous access is disabled, running '
274 278 'authentication')
275 279
276 280 if not anonymous_perm:
277 281 log.debug('Not enough credentials to access this '
278 282 'repository as anonymous user')
279 283
280 284 username = None
281 285 # ==============================================================
282 286 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
283 287 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
284 288 # ==============================================================
285 289
286 290 # try to auth based on environ, container auth methods
287 291 log.debug('Running PRE-AUTH for container based authentication')
288 292 pre_auth = authenticate(
289 293 '', '', environ, VCS_TYPE, registry=self.registry)
290 294 if pre_auth and pre_auth.get('username'):
291 295 username = pre_auth['username']
292 296 log.debug('PRE-AUTH got %s as username', username)
293 297
294 298 # If not authenticated by the container, running basic auth
295 299 if not username:
296 300 self.authenticate.realm = get_rhodecode_realm()
297 301
298 302 try:
299 303 result = self.authenticate(environ)
300 304 except (UserCreationError, NotAllowedToCreateUserError) as e:
301 305 log.error(e)
302 306 reason = safe_str(e)
303 307 return HTTPNotAcceptable(reason)(environ, start_response)
304 308
305 309 if isinstance(result, str):
306 310 AUTH_TYPE.update(environ, 'basic')
307 311 REMOTE_USER.update(environ, result)
308 312 username = result
309 313 else:
310 314 return result.wsgi_application(environ, start_response)
311 315
312 316 # ==============================================================
313 317 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
314 318 # ==============================================================
315 319 user = User.get_by_username(username)
316 320 if not self.valid_and_active_user(user):
317 321 return HTTPForbidden()(environ, start_response)
318 322 username = user.username
319 323 user.update_lastactivity()
320 324 meta.Session().commit()
321 325
322 326 # check user attributes for password change flag
323 327 user_obj = user
324 328 if user_obj and user_obj.username != User.DEFAULT_USER and \
325 329 user_obj.user_data.get('force_password_change'):
326 330 reason = 'password change required'
327 331 log.debug('User not allowed to authenticate, %s', reason)
328 332 return HTTPNotAcceptable(reason)(environ, start_response)
329 333
330 334 # check permissions for this repository
331 335 perm = self._check_permission(action, user, repo_name, ip_addr)
332 336 if not perm:
333 337 return HTTPForbidden()(environ, start_response)
334 338
335 339 # extras are injected into UI object and later available
336 340 # in hooks executed by rhodecode
337 341 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
338 342 extras = vcs_operation_context(
339 343 environ, repo_name=repo_name, username=username,
340 344 action=action, scm=self.SCM,
341 345 check_locking=check_locking)
342 346
343 347 # ======================================================================
344 348 # REQUEST HANDLING
345 349 # ======================================================================
346 350 str_repo_name = safe_str(repo_name)
347 351 repo_path = os.path.join(safe_str(self.basepath), str_repo_name)
348 352 log.debug('Repository path is %s', repo_path)
349 353
350 354 fix_PATH()
351 355
352 356 log.info(
353 357 '%s action on %s repo "%s" by "%s" from %s',
354 358 action, self.SCM, str_repo_name, safe_str(username), ip_addr)
355 359
356 360 return self._generate_vcs_response(
357 361 environ, start_response, repo_path, repo_name, extras, action)
358 362
359 363 @initialize_generator
360 364 def _generate_vcs_response(
361 365 self, environ, start_response, repo_path, repo_name, extras,
362 366 action):
363 367 """
364 368 Returns a generator for the response content.
365 369
366 370 This method is implemented as a generator, so that it can trigger
367 371 the cache validation after all content sent back to the client. It
368 372 also handles the locking exceptions which will be triggered when
369 373 the first chunk is produced by the underlying WSGI application.
370 374 """
371 375 callback_daemon, extras = self._prepare_callback_daemon(extras)
372 376 config = self._create_config(extras, repo_name)
373 377 log.debug('HOOKS extras is %s', extras)
374 378 app = self._create_wsgi_app(repo_path, repo_name, config)
375 379
376 380 try:
377 381 with callback_daemon:
378 382 try:
379 383 response = app(environ, start_response)
380 384 finally:
381 385 # This statement works together with the decorator
382 386 # "initialize_generator" above. The decorator ensures that
383 387 # we hit the first yield statement before the generator is
384 388 # returned back to the WSGI server. This is needed to
385 389 # ensure that the call to "app" above triggers the
386 390 # needed callback to "start_response" before the
387 391 # generator is actually used.
388 392 yield "__init__"
389 393
390 394 for chunk in response:
391 395 yield chunk
392 396 except Exception as exc:
393 397 # TODO: johbo: Improve "translating" back the exception.
394 398 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
395 399 exc = HTTPLockedRC(*exc.args)
396 400 _code = rhodecode.CONFIG.get('lock_ret_code')
397 401 log.debug('Repository LOCKED ret code %s!', (_code,))
398 402 elif getattr(exc, '_vcs_kind', None) == 'requirement':
399 403 log.debug(
400 404 'Repository requires features unknown to this Mercurial')
401 405 exc = HTTPRequirementError(*exc.args)
402 406 else:
403 407 raise
404 408
405 409 for chunk in exc(environ, start_response):
406 410 yield chunk
407 411 finally:
408 412 # invalidate cache on push
409 413 try:
410 414 if action == 'push':
411 415 self._invalidate_cache(repo_name)
412 416 finally:
413 417 meta.Session.remove()
414 418
415 419 def _get_repository_name(self, environ):
416 420 """Get repository name out of the environmnent
417 421
418 422 :param environ: WSGI environment
419 423 """
420 424 raise NotImplementedError()
421 425
422 426 def _get_action(self, environ):
423 427 """Map request commands into a pull or push command.
424 428
425 429 :param environ: WSGI environment
426 430 """
427 431 raise NotImplementedError()
428 432
429 433 def _create_wsgi_app(self, repo_path, repo_name, config):
430 434 """Return the WSGI app that will finally handle the request."""
431 435 raise NotImplementedError()
432 436
433 437 def _create_config(self, extras, repo_name):
434 438 """Create a Pyro safe config representation."""
435 439 raise NotImplementedError()
436 440
437 441 def _prepare_callback_daemon(self, extras):
438 442 return prepare_callback_daemon(
439 443 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
440 444 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
441 445
442 446
443 447 def _should_check_locking(query_string):
444 448 # this is kind of hacky, but due to how mercurial handles client-server
445 449 # server see all operation on commit; bookmarks, phases and
446 450 # obsolescence marker in different transaction, we don't want to check
447 451 # locking on those
448 452 return query_string not in ['cmd=listkeys']
@@ -1,161 +1,178 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 gzip
22 22 import shutil
23 23 import logging
24 24 import tempfile
25 25 import urlparse
26 26
27 27 import rhodecode
28 28 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
29 29 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
30 30 from rhodecode.lib.middleware.simplehg import SimpleHg
31 31 from rhodecode.lib.middleware.simplesvn import SimpleSvn
32
32 from rhodecode.lib.vcs.backends import base
33 from rhodecode.model.settings import VcsSettingsModel
33 34
34 35 log = logging.getLogger(__name__)
35 36
36 37
37 38 def is_git(environ):
38 39 """
39 40 Returns True if requests should be handled by GIT wsgi middleware
40 41 """
41 42 is_git_path = GIT_PROTO_PAT.match(environ['PATH_INFO'])
42 43 log.debug(
43 44 'request path: `%s` detected as GIT PROTOCOL %s', environ['PATH_INFO'],
44 45 is_git_path is not None)
45 46
46 47 return is_git_path
47 48
48 49
49 50 def is_hg(environ):
50 51 """
51 52 Returns True if requests target is mercurial server - header
52 53 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
53 54 """
54 55 is_hg_path = False
55 56
56 57 http_accept = environ.get('HTTP_ACCEPT')
57 58
58 59 if http_accept and http_accept.startswith('application/mercurial'):
59 60 query = urlparse.parse_qs(environ['QUERY_STRING'])
60 61 if 'cmd' in query:
61 62 is_hg_path = True
62 63
63 64 log.debug(
64 65 'request path: `%s` detected as HG PROTOCOL %s', environ['PATH_INFO'],
65 66 is_hg_path)
66 67
67 68 return is_hg_path
68 69
69 70
70 71 def is_svn(environ):
71 72 """
72 73 Returns True if requests target is Subversion server
73 74 """
74 75 http_dav = environ.get('HTTP_DAV', '')
75 76 magic_path_segment = rhodecode.CONFIG.get(
76 77 'rhodecode_subversion_magic_path', '/!svn')
77 78 is_svn_path = (
78 79 'subversion' in http_dav or
79 80 magic_path_segment in environ['PATH_INFO'])
80 81 log.debug(
81 82 'request path: `%s` detected as SVN PROTOCOL %s', environ['PATH_INFO'],
82 83 is_svn_path)
83 84
84 85 return is_svn_path
85 86
86 87
87 88 class GunzipMiddleware(object):
88 89 """
89 90 WSGI middleware that unzips gzip-encoded requests before
90 91 passing on to the underlying application.
91 92 """
92 93
93 94 def __init__(self, application):
94 95 self.app = application
95 96
96 97 def __call__(self, environ, start_response):
97 98 accepts_encoding_header = environ.get('HTTP_CONTENT_ENCODING', b'')
98 99
99 100 if b'gzip' in accepts_encoding_header:
100 101 log.debug('gzip detected, now running gunzip wrapper')
101 102 wsgi_input = environ['wsgi.input']
102 103
103 104 if not hasattr(environ['wsgi.input'], 'seek'):
104 105 # The gzip implementation in the standard library of Python 2.x
105 106 # requires the '.seek()' and '.tell()' methods to be available
106 107 # on the input stream. Read the data into a temporary file to
107 108 # work around this limitation.
108 109
109 110 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
110 111 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
111 112 wsgi_input.seek(0)
112 113
113 114 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
114 115 # since we "Ungzipped" the content we say now it's no longer gzip
115 116 # content encoding
116 117 del environ['HTTP_CONTENT_ENCODING']
117 118
118 119 # content length has changes ? or i'm not sure
119 120 if 'CONTENT_LENGTH' in environ:
120 121 del environ['CONTENT_LENGTH']
121 122 else:
122 123 log.debug('content not gzipped, gzipMiddleware passing '
123 124 'request further')
124 125 return self.app(environ, start_response)
125 126
126 127
127 128 class VCSMiddleware(object):
128 129
129 130 def __init__(self, app, config, appenlight_client, registry):
130 131 self.application = app
131 132 self.config = config
132 133 self.appenlight_client = appenlight_client
133 134 self.registry = registry
135 self.repo_vcs_config = base.Config()
136 self.use_gzip = True
137
138 def vcs_config(self, repo_name=None):
139 """
140 returns serialized VcsSettings
141 """
142 return VcsSettingsModel(repo=repo_name).get_ui_settings_as_config_obj()
143
144 def wrap_in_gzip_if_enabled(self, app):
145 if self.use_gzip:
146 app = GunzipMiddleware(app)
147 return app
134 148
135 149 def _get_handler_app(self, environ):
136 150 app = None
151
137 152 if is_hg(environ):
138 153 app = SimpleHg(self.application, self.config, self.registry)
139 154
140 155 if is_git(environ):
141 156 app = SimpleGit(self.application, self.config, self.registry)
142 157
143 proxy_svn = rhodecode.CONFIG.get(
144 'rhodecode_proxy_subversion_http_requests', False)
145 if proxy_svn and is_svn(environ):
158 if is_svn(environ):
146 159 app = SimpleSvn(self.application, self.config, self.registry)
147 160
148 161 if app:
149 app = GunzipMiddleware(app)
162 repo_name = app._get_repository_name(environ)
163 self.repo_vcs_config = self.vcs_config(repo_name)
164 app.repo_vcs_config = self.repo_vcs_config
165
166 app = self.wrap_in_gzip_if_enabled(app)
150 167 app, _ = wrap_in_appenlight_if_enabled(
151 168 app, self.config, self.appenlight_client)
152 169
153 170 return app
154 171
155 172 def __call__(self, environ, start_response):
156 173 # check if we handle one of interesting protocols ?
157 174 vcs_handler = self._get_handler_app(environ)
158 175 if vcs_handler:
159 176 return vcs_handler(environ, start_response)
160 177
161 178 return self.application(environ, start_response)
@@ -1,547 +1,547 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 this is forms validation classes
23 23 http://formencode.org/module-formencode.validators.html
24 24 for list off all availible validators
25 25
26 26 we can create our own validators
27 27
28 28 The table below outlines the options which can be used in a schema in addition to the validators themselves
29 29 pre_validators [] These validators will be applied before the schema
30 30 chained_validators [] These validators will be applied after the schema
31 31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
32 32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
33 33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
34 34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
35 35
36 36
37 37 <name> = formencode.validators.<name of validator>
38 38 <name> must equal form name
39 39 list=[1,2,3,4,5]
40 40 for SELECT use formencode.All(OneOf(list), Int())
41 41
42 42 """
43 43
44 44 import deform
45 45 import logging
46 46 import formencode
47 47
48 48 from pkg_resources import resource_filename
49 49 from formencode import All, Pipe
50 50
51 51 from pylons.i18n.translation import _
52 52
53 53 from rhodecode import BACKENDS
54 54 from rhodecode.lib import helpers
55 55 from rhodecode.model import validators as v
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 deform_templates = resource_filename('deform', 'templates')
61 61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
62 62 search_path = (rhodecode_templates, deform_templates)
63 63
64 64
65 65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
66 66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
67 67 def __call__(self, template_name, **kw):
68 68 kw['h'] = helpers
69 69 return self.load(template_name)(**kw)
70 70
71 71
72 72 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
73 73 deform.Form.set_default_renderer(form_renderer)
74 74
75 75
76 76 def LoginForm():
77 77 class _LoginForm(formencode.Schema):
78 78 allow_extra_fields = True
79 79 filter_extra_fields = True
80 80 username = v.UnicodeString(
81 81 strip=True,
82 82 min=1,
83 83 not_empty=True,
84 84 messages={
85 85 'empty': _(u'Please enter a login'),
86 86 'tooShort': _(u'Enter a value %(min)i characters long or more')
87 87 }
88 88 )
89 89
90 90 password = v.UnicodeString(
91 91 strip=False,
92 92 min=3,
93 93 not_empty=True,
94 94 messages={
95 95 'empty': _(u'Please enter a password'),
96 96 'tooShort': _(u'Enter %(min)i characters or more')}
97 97 )
98 98
99 99 remember = v.StringBoolean(if_missing=False)
100 100
101 101 chained_validators = [v.ValidAuth()]
102 102 return _LoginForm
103 103
104 104
105 105 def UserForm(edit=False, available_languages=[], old_data={}):
106 106 class _UserForm(formencode.Schema):
107 107 allow_extra_fields = True
108 108 filter_extra_fields = True
109 109 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
110 110 v.ValidUsername(edit, old_data))
111 111 if edit:
112 112 new_password = All(
113 113 v.ValidPassword(),
114 114 v.UnicodeString(strip=False, min=6, not_empty=False)
115 115 )
116 116 password_confirmation = All(
117 117 v.ValidPassword(),
118 118 v.UnicodeString(strip=False, min=6, not_empty=False),
119 119 )
120 120 admin = v.StringBoolean(if_missing=False)
121 121 else:
122 122 password = All(
123 123 v.ValidPassword(),
124 124 v.UnicodeString(strip=False, min=6, not_empty=True)
125 125 )
126 126 password_confirmation = All(
127 127 v.ValidPassword(),
128 128 v.UnicodeString(strip=False, min=6, not_empty=False)
129 129 )
130 130
131 131 password_change = v.StringBoolean(if_missing=False)
132 132 create_repo_group = v.StringBoolean(if_missing=False)
133 133
134 134 active = v.StringBoolean(if_missing=False)
135 135 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
136 136 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
137 137 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
138 138 extern_name = v.UnicodeString(strip=True)
139 139 extern_type = v.UnicodeString(strip=True)
140 140 language = v.OneOf(available_languages, hideList=False,
141 141 testValueList=True, if_missing=None)
142 142 chained_validators = [v.ValidPasswordsMatch()]
143 143 return _UserForm
144 144
145 145
146 146 def UserGroupForm(edit=False, old_data=None, available_members=None,
147 147 allow_disabled=False):
148 148 old_data = old_data or {}
149 149 available_members = available_members or []
150 150
151 151 class _UserGroupForm(formencode.Schema):
152 152 allow_extra_fields = True
153 153 filter_extra_fields = True
154 154
155 155 users_group_name = All(
156 156 v.UnicodeString(strip=True, min=1, not_empty=True),
157 157 v.ValidUserGroup(edit, old_data)
158 158 )
159 159 user_group_description = v.UnicodeString(strip=True, min=1,
160 160 not_empty=False)
161 161
162 162 users_group_active = v.StringBoolean(if_missing=False)
163 163
164 164 if edit:
165 165 users_group_members = v.OneOf(
166 166 available_members, hideList=False, testValueList=True,
167 167 if_missing=None, not_empty=False
168 168 )
169 169 # this is user group owner
170 170 user = All(
171 171 v.UnicodeString(not_empty=True),
172 172 v.ValidRepoUser(allow_disabled))
173 173 return _UserGroupForm
174 174
175 175
176 176 def RepoGroupForm(edit=False, old_data=None, available_groups=None,
177 177 can_create_in_root=False, allow_disabled=False):
178 178 old_data = old_data or {}
179 179 available_groups = available_groups or []
180 180
181 181 class _RepoGroupForm(formencode.Schema):
182 182 allow_extra_fields = True
183 183 filter_extra_fields = False
184 184
185 185 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
186 186 v.SlugifyName(),)
187 187 group_description = v.UnicodeString(strip=True, min=1,
188 188 not_empty=False)
189 189 group_copy_permissions = v.StringBoolean(if_missing=False)
190 190
191 191 group_parent_id = v.OneOf(available_groups, hideList=False,
192 192 testValueList=True, not_empty=True)
193 193 enable_locking = v.StringBoolean(if_missing=False)
194 194 chained_validators = [
195 195 v.ValidRepoGroup(edit, old_data, can_create_in_root)]
196 196
197 197 if edit:
198 198 # this is repo group owner
199 199 user = All(
200 200 v.UnicodeString(not_empty=True),
201 201 v.ValidRepoUser(allow_disabled))
202 202
203 203 return _RepoGroupForm
204 204
205 205
206 206 def RegisterForm(edit=False, old_data={}):
207 207 class _RegisterForm(formencode.Schema):
208 208 allow_extra_fields = True
209 209 filter_extra_fields = True
210 210 username = All(
211 211 v.ValidUsername(edit, old_data),
212 212 v.UnicodeString(strip=True, min=1, not_empty=True)
213 213 )
214 214 password = All(
215 215 v.ValidPassword(),
216 216 v.UnicodeString(strip=False, min=6, not_empty=True)
217 217 )
218 218 password_confirmation = All(
219 219 v.ValidPassword(),
220 220 v.UnicodeString(strip=False, min=6, not_empty=True)
221 221 )
222 222 active = v.StringBoolean(if_missing=False)
223 223 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
224 224 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
225 225 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
226 226
227 227 chained_validators = [v.ValidPasswordsMatch()]
228 228
229 229 return _RegisterForm
230 230
231 231
232 232 def PasswordResetForm():
233 233 class _PasswordResetForm(formencode.Schema):
234 234 allow_extra_fields = True
235 235 filter_extra_fields = True
236 236 email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
237 237 return _PasswordResetForm
238 238
239 239
240 240 def RepoForm(edit=False, old_data=None, repo_groups=None, landing_revs=None,
241 241 allow_disabled=False):
242 242 old_data = old_data or {}
243 243 repo_groups = repo_groups or []
244 244 landing_revs = landing_revs or []
245 245 supported_backends = BACKENDS.keys()
246 246
247 247 class _RepoForm(formencode.Schema):
248 248 allow_extra_fields = True
249 249 filter_extra_fields = False
250 250 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
251 251 v.SlugifyName())
252 252 repo_group = All(v.CanWriteGroup(old_data),
253 253 v.OneOf(repo_groups, hideList=True))
254 254 repo_type = v.OneOf(supported_backends, required=False,
255 255 if_missing=old_data.get('repo_type'))
256 256 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
257 257 repo_private = v.StringBoolean(if_missing=False)
258 258 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
259 259 repo_copy_permissions = v.StringBoolean(if_missing=False)
260 260 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
261 261
262 262 repo_enable_statistics = v.StringBoolean(if_missing=False)
263 263 repo_enable_downloads = v.StringBoolean(if_missing=False)
264 264 repo_enable_locking = v.StringBoolean(if_missing=False)
265 265
266 266 if edit:
267 267 # this is repo owner
268 268 user = All(
269 269 v.UnicodeString(not_empty=True),
270 270 v.ValidRepoUser(allow_disabled))
271 271 clone_uri_change = v.UnicodeString(
272 272 not_empty=False, if_missing=v.Missing)
273 273
274 274 chained_validators = [v.ValidCloneUri(),
275 275 v.ValidRepoName(edit, old_data)]
276 276 return _RepoForm
277 277
278 278
279 279 def RepoPermsForm():
280 280 class _RepoPermsForm(formencode.Schema):
281 281 allow_extra_fields = True
282 282 filter_extra_fields = False
283 283 chained_validators = [v.ValidPerms(type_='repo')]
284 284 return _RepoPermsForm
285 285
286 286
287 287 def RepoGroupPermsForm(valid_recursive_choices):
288 288 class _RepoGroupPermsForm(formencode.Schema):
289 289 allow_extra_fields = True
290 290 filter_extra_fields = False
291 291 recursive = v.OneOf(valid_recursive_choices)
292 292 chained_validators = [v.ValidPerms(type_='repo_group')]
293 293 return _RepoGroupPermsForm
294 294
295 295
296 296 def UserGroupPermsForm():
297 297 class _UserPermsForm(formencode.Schema):
298 298 allow_extra_fields = True
299 299 filter_extra_fields = False
300 300 chained_validators = [v.ValidPerms(type_='user_group')]
301 301 return _UserPermsForm
302 302
303 303
304 304 def RepoFieldForm():
305 305 class _RepoFieldForm(formencode.Schema):
306 306 filter_extra_fields = True
307 307 allow_extra_fields = True
308 308
309 309 new_field_key = All(v.FieldKey(),
310 310 v.UnicodeString(strip=True, min=3, not_empty=True))
311 311 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
312 312 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
313 313 if_missing='str')
314 314 new_field_label = v.UnicodeString(not_empty=False)
315 315 new_field_desc = v.UnicodeString(not_empty=False)
316 316
317 317 return _RepoFieldForm
318 318
319 319
320 320 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
321 321 repo_groups=[], landing_revs=[]):
322 322 class _RepoForkForm(formencode.Schema):
323 323 allow_extra_fields = True
324 324 filter_extra_fields = False
325 325 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
326 326 v.SlugifyName())
327 327 repo_group = All(v.CanWriteGroup(),
328 328 v.OneOf(repo_groups, hideList=True))
329 329 repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
330 330 description = v.UnicodeString(strip=True, min=1, not_empty=True)
331 331 private = v.StringBoolean(if_missing=False)
332 332 copy_permissions = v.StringBoolean(if_missing=False)
333 333 fork_parent_id = v.UnicodeString()
334 334 chained_validators = [v.ValidForkName(edit, old_data)]
335 335 landing_rev = v.OneOf(landing_revs, hideList=True)
336 336
337 337 return _RepoForkForm
338 338
339 339
340 340 def ApplicationSettingsForm():
341 341 class _ApplicationSettingsForm(formencode.Schema):
342 342 allow_extra_fields = True
343 343 filter_extra_fields = False
344 344 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
345 345 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
346 346 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
347 347 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
348 348 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
349 349 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
350 350
351 351 return _ApplicationSettingsForm
352 352
353 353
354 354 def ApplicationVisualisationForm():
355 355 class _ApplicationVisualisationForm(formencode.Schema):
356 356 allow_extra_fields = True
357 357 filter_extra_fields = False
358 358 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
359 359 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
360 360 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
361 361
362 362 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
363 363 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
364 364 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
365 365 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
366 366 rhodecode_show_version = v.StringBoolean(if_missing=False)
367 367 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
368 368 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
369 369 rhodecode_gravatar_url = v.UnicodeString(min=3)
370 370 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
371 371 rhodecode_support_url = v.UnicodeString()
372 372 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
373 373 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
374 374
375 375 return _ApplicationVisualisationForm
376 376
377 377
378 378 class _BaseVcsSettingsForm(formencode.Schema):
379 379 allow_extra_fields = True
380 380 filter_extra_fields = False
381 381 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
382 382 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
383 383 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
384 384
385 385 extensions_largefiles = v.StringBoolean(if_missing=False)
386 386 phases_publish = v.StringBoolean(if_missing=False)
387 387
388 388 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
389 389 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
390 390 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
391 391
392 rhodecode_proxy_subversion_http_requests = v.StringBoolean(if_missing=False)
393 rhodecode_subversion_http_server_url = v.UnicodeString(
394 strip=True, if_missing=None)
392 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
393 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
394
395 395
396 396 def ApplicationUiSettingsForm():
397 397 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
398 398 web_push_ssl = v.StringBoolean(if_missing=False)
399 399 paths_root_path = All(
400 400 v.ValidPath(),
401 401 v.UnicodeString(strip=True, min=1, not_empty=True)
402 402 )
403 403 extensions_hgsubversion = v.StringBoolean(if_missing=False)
404 404 extensions_hggit = v.StringBoolean(if_missing=False)
405 405 new_svn_branch = v.ValidSvnPattern(section='vcs_svn_branch')
406 406 new_svn_tag = v.ValidSvnPattern(section='vcs_svn_tag')
407 407
408 408 return _ApplicationUiSettingsForm
409 409
410 410
411 411 def RepoVcsSettingsForm(repo_name):
412 412 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
413 413 inherit_global_settings = v.StringBoolean(if_missing=False)
414 414 new_svn_branch = v.ValidSvnPattern(
415 415 section='vcs_svn_branch', repo_name=repo_name)
416 416 new_svn_tag = v.ValidSvnPattern(
417 417 section='vcs_svn_tag', repo_name=repo_name)
418 418
419 419 return _RepoVcsSettingsForm
420 420
421 421
422 422 def LabsSettingsForm():
423 423 class _LabSettingsForm(formencode.Schema):
424 424 allow_extra_fields = True
425 425 filter_extra_fields = False
426 426
427 427 return _LabSettingsForm
428 428
429 429
430 430 def ApplicationPermissionsForm(register_choices, extern_activate_choices):
431 431 class _DefaultPermissionsForm(formencode.Schema):
432 432 allow_extra_fields = True
433 433 filter_extra_fields = True
434 434
435 435 anonymous = v.StringBoolean(if_missing=False)
436 436 default_register = v.OneOf(register_choices)
437 437 default_register_message = v.UnicodeString()
438 438 default_extern_activate = v.OneOf(extern_activate_choices)
439 439
440 440 return _DefaultPermissionsForm
441 441
442 442
443 443 def ObjectPermissionsForm(repo_perms_choices, group_perms_choices,
444 444 user_group_perms_choices):
445 445 class _ObjectPermissionsForm(formencode.Schema):
446 446 allow_extra_fields = True
447 447 filter_extra_fields = True
448 448 overwrite_default_repo = v.StringBoolean(if_missing=False)
449 449 overwrite_default_group = v.StringBoolean(if_missing=False)
450 450 overwrite_default_user_group = v.StringBoolean(if_missing=False)
451 451 default_repo_perm = v.OneOf(repo_perms_choices)
452 452 default_group_perm = v.OneOf(group_perms_choices)
453 453 default_user_group_perm = v.OneOf(user_group_perms_choices)
454 454
455 455 return _ObjectPermissionsForm
456 456
457 457
458 458 def UserPermissionsForm(create_choices, create_on_write_choices,
459 459 repo_group_create_choices, user_group_create_choices,
460 460 fork_choices, inherit_default_permissions_choices):
461 461 class _DefaultPermissionsForm(formencode.Schema):
462 462 allow_extra_fields = True
463 463 filter_extra_fields = True
464 464
465 465 anonymous = v.StringBoolean(if_missing=False)
466 466
467 467 default_repo_create = v.OneOf(create_choices)
468 468 default_repo_create_on_write = v.OneOf(create_on_write_choices)
469 469 default_user_group_create = v.OneOf(user_group_create_choices)
470 470 default_repo_group_create = v.OneOf(repo_group_create_choices)
471 471 default_fork_create = v.OneOf(fork_choices)
472 472 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
473 473
474 474 return _DefaultPermissionsForm
475 475
476 476
477 477 def UserIndividualPermissionsForm():
478 478 class _DefaultPermissionsForm(formencode.Schema):
479 479 allow_extra_fields = True
480 480 filter_extra_fields = True
481 481
482 482 inherit_default_permissions = v.StringBoolean(if_missing=False)
483 483
484 484 return _DefaultPermissionsForm
485 485
486 486
487 487 def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
488 488 class _DefaultsForm(formencode.Schema):
489 489 allow_extra_fields = True
490 490 filter_extra_fields = True
491 491 default_repo_type = v.OneOf(supported_backends)
492 492 default_repo_private = v.StringBoolean(if_missing=False)
493 493 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
494 494 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
495 495 default_repo_enable_locking = v.StringBoolean(if_missing=False)
496 496
497 497 return _DefaultsForm
498 498
499 499
500 500 def AuthSettingsForm():
501 501 class _AuthSettingsForm(formencode.Schema):
502 502 allow_extra_fields = True
503 503 filter_extra_fields = True
504 504 auth_plugins = All(v.ValidAuthPlugins(),
505 505 v.UniqueListFromString()(not_empty=True))
506 506
507 507 return _AuthSettingsForm
508 508
509 509
510 510 def UserExtraEmailForm():
511 511 class _UserExtraEmailForm(formencode.Schema):
512 512 email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
513 513 return _UserExtraEmailForm
514 514
515 515
516 516 def UserExtraIpForm():
517 517 class _UserExtraIpForm(formencode.Schema):
518 518 ip = v.ValidIp()(not_empty=True)
519 519 return _UserExtraIpForm
520 520
521 521
522 522 def PullRequestForm(repo_id):
523 523 class _PullRequestForm(formencode.Schema):
524 524 allow_extra_fields = True
525 525 filter_extra_fields = True
526 526
527 527 user = v.UnicodeString(strip=True, required=True)
528 528 source_repo = v.UnicodeString(strip=True, required=True)
529 529 source_ref = v.UnicodeString(strip=True, required=True)
530 530 target_repo = v.UnicodeString(strip=True, required=True)
531 531 target_ref = v.UnicodeString(strip=True, required=True)
532 532 revisions = All(#v.NotReviewedRevisions(repo_id)(),
533 533 v.UniqueList()(not_empty=True))
534 534 review_members = v.UniqueList(convert=int)(not_empty=True)
535 535
536 536 pullrequest_title = v.UnicodeString(strip=True, required=True)
537 537 pullrequest_desc = v.UnicodeString(strip=True, required=False)
538 538
539 539 return _PullRequestForm
540 540
541 541
542 542 def IssueTrackerPatternsForm():
543 543 class _IssueTrackerPatternsForm(formencode.Schema):
544 544 allow_extra_fields = True
545 545 filter_extra_fields = False
546 546 chained_validators = [v.ValidPattern()]
547 547 return _IssueTrackerPatternsForm
@@ -1,716 +1,730 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 from rhodecode.lib.caching_query import FromCache
28 27 from rhodecode.lib.utils2 import (
29 28 Optional, AttributeDict, safe_str, remove_prefix, str2bool)
29 from rhodecode.lib.vcs.backends import base
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 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 'use_outdated_comments', 'pr_merge_enabled',
405 'use_outdated_comments',
406 'pr_merge_enabled',
406 407 'hg_use_rebase_for_merging')
408
407 409 HOOKS_SETTINGS = (
408 410 ('hooks', 'changegroup.repo_size'),
409 411 ('hooks', 'changegroup.push_logger'),
410 412 ('hooks', 'outgoing.pull_logger'))
411 413 HG_SETTINGS = (
412 ('extensions', 'largefiles'), ('phases', 'publish'))
413 GLOBAL_HG_SETTINGS = HG_SETTINGS + (('extensions', 'hgsubversion'), )
414 ('extensions', 'largefiles'),
415 ('phases', 'publish'))
416 GLOBAL_HG_SETTINGS = (
417 ('extensions', 'largefiles'),
418 ('phases', 'publish'),
419 ('extensions', 'hgsubversion'))
414 420 GLOBAL_SVN_SETTINGS = (
415 'rhodecode_proxy_subversion_http_requests',
416 'rhodecode_subversion_http_server_url')
421 ('vcs_svn_proxy', 'http_requests_enabled'),
422 ('vcs_svn_proxy', 'http_server_url'))
423
417 424 SVN_BRANCH_SECTION = 'vcs_svn_branch'
418 425 SVN_TAG_SECTION = 'vcs_svn_tag'
419 426 SSL_SETTING = ('web', 'push_ssl')
420 427 PATH_SETTING = ('paths', '/')
421 428
422 429 def __init__(self, sa=None, repo=None):
423 430 self.global_settings = SettingsModel(sa=sa)
424 431 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
425 432 self._ui_settings = self.HG_SETTINGS + self.HOOKS_SETTINGS
426 433 self._svn_sections = (self.SVN_BRANCH_SECTION, self.SVN_TAG_SECTION)
427 434
428 435 @property
429 436 @assert_repo_settings
430 437 def inherit_global_settings(self):
431 438 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
432 439 return setting.app_settings_value if setting else True
433 440
434 441 @inherit_global_settings.setter
435 442 @assert_repo_settings
436 443 def inherit_global_settings(self, value):
437 444 self.repo_settings.create_or_update_setting(
438 445 self.INHERIT_SETTINGS, value, type_='bool')
439 446
440 447 def get_global_svn_branch_patterns(self):
441 448 return self.global_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
442 449
443 450 @assert_repo_settings
444 451 def get_repo_svn_branch_patterns(self):
445 452 return self.repo_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
446 453
447 454 def get_global_svn_tag_patterns(self):
448 455 return self.global_settings.get_ui_by_section(self.SVN_TAG_SECTION)
449 456
450 457 @assert_repo_settings
451 458 def get_repo_svn_tag_patterns(self):
452 459 return self.repo_settings.get_ui_by_section(self.SVN_TAG_SECTION)
453 460
454 461 def get_global_settings(self):
455 462 return self._collect_all_settings(global_=True)
456 463
457 464 @assert_repo_settings
458 465 def get_repo_settings(self):
459 466 return self._collect_all_settings(global_=False)
460 467
461 468 @assert_repo_settings
462 469 def create_or_update_repo_settings(
463 470 self, data, inherit_global_settings=False):
464 471 from rhodecode.model.scm import ScmModel
465 472
466 473 self.inherit_global_settings = inherit_global_settings
467 474
468 475 repo = self.repo_settings.get_repo()
469 476 if not inherit_global_settings:
470 477 if repo.repo_type == 'svn':
471 478 self.create_repo_svn_settings(data)
472 479 else:
473 480 self.create_or_update_repo_hook_settings(data)
474 481 self.create_or_update_repo_pr_settings(data)
475 482
476 483 if repo.repo_type == 'hg':
477 484 self.create_or_update_repo_hg_settings(data)
478 485
479 486 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
480 487
481 488 @assert_repo_settings
482 489 def create_or_update_repo_hook_settings(self, data):
483 490 for section, key in self.HOOKS_SETTINGS:
484 491 data_key = self._get_form_ui_key(section, key)
485 492 if data_key not in data:
486 493 raise ValueError(
487 494 'The given data does not contain {} key'.format(data_key))
488 495
489 496 active = data.get(data_key)
490 497 repo_setting = self.repo_settings.get_ui_by_section_and_key(
491 498 section, key)
492 499 if not repo_setting:
493 500 global_setting = self.global_settings.\
494 501 get_ui_by_section_and_key(section, key)
495 502 self.repo_settings.create_ui_section_value(
496 503 section, global_setting.ui_value, key=key, active=active)
497 504 else:
498 505 repo_setting.ui_active = active
499 506 Session().add(repo_setting)
500 507
501 508 def update_global_hook_settings(self, data):
502 509 for section, key in self.HOOKS_SETTINGS:
503 510 data_key = self._get_form_ui_key(section, key)
504 511 if data_key not in data:
505 512 raise ValueError(
506 513 'The given data does not contain {} key'.format(data_key))
507 514 active = data.get(data_key)
508 515 repo_setting = self.global_settings.get_ui_by_section_and_key(
509 516 section, key)
510 517 repo_setting.ui_active = active
511 518 Session().add(repo_setting)
512 519
513 520 @assert_repo_settings
514 521 def create_or_update_repo_pr_settings(self, data):
515 522 return self._create_or_update_general_settings(
516 523 self.repo_settings, data)
517 524
518 525 def create_or_update_global_pr_settings(self, data):
519 526 return self._create_or_update_general_settings(
520 527 self.global_settings, data)
521 528
522 529 @assert_repo_settings
523 530 def create_repo_svn_settings(self, data):
524 531 return self._create_svn_settings(self.repo_settings, data)
525 532
526 def create_global_svn_settings(self, data):
527 return self._create_svn_settings(self.global_settings, data)
528
529 533 @assert_repo_settings
530 534 def create_or_update_repo_hg_settings(self, data):
531 535 largefiles, phases = self.HG_SETTINGS
532 largefiles_key, phases_key = self._get_hg_settings(
536 largefiles_key, phases_key = self._get_settings_keys(
533 537 self.HG_SETTINGS, data)
534 538 self._create_or_update_ui(
535 539 self.repo_settings, *largefiles, value='',
536 540 active=data[largefiles_key])
537 541 self._create_or_update_ui(
538 542 self.repo_settings, *phases, value=safe_str(data[phases_key]))
539 543
540 544 def create_or_update_global_hg_settings(self, data):
541 largefiles, phases, subversion = self.GLOBAL_HG_SETTINGS
542 largefiles_key, phases_key, subversion_key = self._get_hg_settings(
545 largefiles, phases, hgsubversion = self.GLOBAL_HG_SETTINGS
546 largefiles_key, phases_key, subversion_key = self._get_settings_keys(
543 547 self.GLOBAL_HG_SETTINGS, data)
544 548 self._create_or_update_ui(
545 549 self.global_settings, *largefiles, value='',
546 550 active=data[largefiles_key])
547 551 self._create_or_update_ui(
548 552 self.global_settings, *phases, value=safe_str(data[phases_key]))
549 553 self._create_or_update_ui(
550 self.global_settings, *subversion, active=data[subversion_key])
554 self.global_settings, *hgsubversion, active=data[subversion_key])
551 555
552 556 def create_or_update_global_svn_settings(self, data):
553 rhodecode_proxy_subversion_http_requests,
554 rhodecode_subversion_http_server_url = self.GLOBAL_SVN_SETTINGS
555 rhodecode_proxy_subversion_http_requests_key,
556 rhodecode_subversion_http_server_url_key = self._get_svn_settings(
557 # branch/tags patterns
558 self._create_svn_settings(self.global_settings, data)
559
560 http_requests_enabled, http_server_url = self.GLOBAL_SVN_SETTINGS
561 http_requests_enabled_key, http_server_url_key = self._get_settings_keys(
557 562 self.GLOBAL_SVN_SETTINGS, data)
563
558 564 self._create_or_update_ui(
559 self.global_settings,
560 *rhodecode_proxy_subversion_http_requests,
561 value=safe_str(data[rhodecode_proxy_subversion_http_requests_key]))
565 self.global_settings, *http_requests_enabled,
566 value=safe_str(data[http_requests_enabled_key]))
562 567 self._create_or_update_ui(
563 self.global_settings,
564 *rhodecode_subversion_http_server_url,
565 active=data[rhodecode_subversion_http_server_url_key])
568 self.global_settings, *http_server_url,
569 value=data[http_server_url_key])
566 570
567 571 def update_global_ssl_setting(self, value):
568 572 self._create_or_update_ui(
569 573 self.global_settings, *self.SSL_SETTING, value=value)
570 574
571 575 def update_global_path_setting(self, value):
572 576 self._create_or_update_ui(
573 577 self.global_settings, *self.PATH_SETTING, value=value)
574 578
575 579 @assert_repo_settings
576 580 def delete_repo_svn_pattern(self, id_):
577 581 self.repo_settings.delete_ui(id_)
578 582
579 583 def delete_global_svn_pattern(self, id_):
580 584 self.global_settings.delete_ui(id_)
581 585
582 586 @assert_repo_settings
583 587 def get_repo_ui_settings(self, section=None, key=None):
584 588 global_uis = self.global_settings.get_ui(section, key)
585 589 repo_uis = self.repo_settings.get_ui(section, key)
586 590 filtered_repo_uis = self._filter_ui_settings(repo_uis)
587 591 filtered_repo_uis_keys = [
588 592 (s.section, s.key) for s in filtered_repo_uis]
589 593
590 594 def _is_global_ui_filtered(ui):
591 595 return (
592 596 (ui.section, ui.key) in filtered_repo_uis_keys
593 597 or ui.section in self._svn_sections)
594 598
595 599 filtered_global_uis = [
596 600 ui for ui in global_uis if not _is_global_ui_filtered(ui)]
597 601
598 602 return filtered_global_uis + filtered_repo_uis
599 603
600 604 def get_global_ui_settings(self, section=None, key=None):
601 605 return self.global_settings.get_ui(section, key)
602 606
607 def get_ui_settings_as_config_obj(self, section=None, key=None):
608 config = base.Config()
609
610 ui_settings = self.get_ui_settings(section=section, key=key)
611
612 for entry in ui_settings:
613 config.set(entry.section, entry.key, entry.value)
614
615 return config
616
603 617 def get_ui_settings(self, section=None, key=None):
604 618 if not self.repo_settings or self.inherit_global_settings:
605 619 return self.get_global_ui_settings(section, key)
606 620 else:
607 621 return self.get_repo_ui_settings(section, key)
608 622
609 623 def get_svn_patterns(self, section=None):
610 624 if not self.repo_settings:
611 625 return self.get_global_ui_settings(section)
612 626 else:
613 627 return self.get_repo_ui_settings(section)
614 628
615 629 @assert_repo_settings
616 630 def get_repo_general_settings(self):
617 631 global_settings = self.global_settings.get_all_settings()
618 632 repo_settings = self.repo_settings.get_all_settings()
619 633 filtered_repo_settings = self._filter_general_settings(repo_settings)
620 634 global_settings.update(filtered_repo_settings)
621 635 return global_settings
622 636
623 637 def get_global_general_settings(self):
624 638 return self.global_settings.get_all_settings()
625 639
626 640 def get_general_settings(self):
627 641 if not self.repo_settings or self.inherit_global_settings:
628 642 return self.get_global_general_settings()
629 643 else:
630 644 return self.get_repo_general_settings()
631 645
632 646 def get_repos_location(self):
633 647 return self.global_settings.get_ui_by_key('/').ui_value
634 648
635 649 def _filter_ui_settings(self, settings):
636 650 filtered_settings = [
637 651 s for s in settings if self._should_keep_setting(s)]
638 652 return filtered_settings
639 653
640 654 def _should_keep_setting(self, setting):
641 655 keep = (
642 656 (setting.section, setting.key) in self._ui_settings or
643 657 setting.section in self._svn_sections)
644 658 return keep
645 659
646 660 def _filter_general_settings(self, settings):
647 661 keys = ['rhodecode_{}'.format(key) for key in self.GENERAL_SETTINGS]
648 662 return {
649 663 k: settings[k]
650 664 for k in settings if k in keys}
651 665
652 666 def _collect_all_settings(self, global_=False):
653 667 settings = self.global_settings if global_ else self.repo_settings
654 668 result = {}
655 669
656 670 for section, key in self._ui_settings:
657 671 ui = settings.get_ui_by_section_and_key(section, key)
658 672 result_key = self._get_form_ui_key(section, key)
659 673 if ui:
660 674 if section in ('hooks', 'extensions'):
661 675 result[result_key] = ui.ui_active
662 676 else:
663 677 result[result_key] = ui.ui_value
664 678
665 679 for name in self.GENERAL_SETTINGS:
666 680 setting = settings.get_setting_by_name(name)
667 681 if setting:
668 682 result_key = 'rhodecode_{}'.format(name)
669 683 result[result_key] = setting.app_settings_value
670 684
671 685 return result
672 686
673 687 def _get_form_ui_key(self, section, key):
674 688 return '{section}_{key}'.format(
675 689 section=section, key=key.replace('.', '_'))
676 690
677 691 def _create_or_update_ui(
678 692 self, settings, section, key, value=None, active=None):
679 693 ui = settings.get_ui_by_section_and_key(section, key)
680 694 if not ui:
681 695 active = True if active is None else active
682 696 settings.create_ui_section_value(
683 697 section, value, key=key, active=active)
684 698 else:
685 699 if active is not None:
686 700 ui.ui_active = active
687 701 if value is not None:
688 702 ui.ui_value = value
689 703 Session().add(ui)
690 704
691 705 def _create_svn_settings(self, settings, data):
692 706 svn_settings = {
693 707 'new_svn_branch': self.SVN_BRANCH_SECTION,
694 708 'new_svn_tag': self.SVN_TAG_SECTION
695 709 }
696 710 for key in svn_settings:
697 711 if data.get(key):
698 712 settings.create_ui_section_value(svn_settings[key], data[key])
699 713
700 714 def _create_or_update_general_settings(self, settings, data):
701 715 for name in self.GENERAL_SETTINGS:
702 716 data_key = 'rhodecode_{}'.format(name)
703 717 if data_key not in data:
704 718 raise ValueError(
705 719 'The given data does not contain {} key'.format(data_key))
706 720 setting = settings.create_or_update_setting(
707 721 name, data[data_key], 'bool')
708 722 Session().add(setting)
709 723
710 def _get_hg_settings(self, settings, data):
724 def _get_settings_keys(self, settings, data):
711 725 data_keys = [self._get_form_ui_key(*s) for s in settings]
712 726 for data_key in data_keys:
713 727 if data_key not in data:
714 728 raise ValueError(
715 729 'The given data does not contain {} key'.format(data_key))
716 730 return data_keys
@@ -1,259 +1,267 b''
1 1 ## snippet for displaying vcs settings
2 2 ## usage:
3 3 ## <%namespace name="vcss" file="/base/vcssettings.html"/>
4 4 ## ${vcss.vcs_settings_fields()}
5 5
6 6 <%def name="vcs_settings_fields(suffix='', svn_branch_patterns=None, svn_tag_patterns=None, repo_type=None, display_globals=False, allow_repo_location_change=False, **kwargs)">
7 7 % if display_globals:
8 8 <div class="panel panel-default">
9 9 <div class="panel-heading" id="general">
10 10 <h3 class="panel-title">${_('General')}</h3>
11 11 </div>
12 12 <div class="panel-body">
13 13 <div class="field">
14 14 <div class="checkbox">
15 15 ${h.checkbox('web_push_ssl' + suffix, 'True')}
16 16 <label for="web_push_ssl${suffix}">${_('Require SSL for vcs operations')}</label>
17 17 </div>
18 18 <div class="label">
19 19 <span class="help-block">${_('Activate to set RhodeCode to require SSL for pushing or pulling. If SSL certificate is missing it will return a HTTP Error 406: Not Acceptable.')}</span>
20 20 </div>
21 21 </div>
22 22 </div>
23 23 </div>
24 24 % endif
25 25
26 26 % if display_globals:
27 27 <div class="panel panel-default">
28 28 <div class="panel-heading">
29 29 <h3 class="panel-title">${_('Main Storage Location')}</h3>
30 30 </div>
31 31 <div class="panel-body">
32 32 <div class="field">
33 33 <div class="inputx locked_input">
34 34 %if allow_repo_location_change:
35 35 ${h.text('paths_root_path',size=59,readonly="readonly", class_="disabled")}
36 36 <span id="path_unlock" class="tooltip"
37 37 title="${h.tooltip(_('Click to unlock. You must restart RhodeCode in order to make this setting take effect.'))}">
38 38 <div class="btn btn-default lock_input_button"><i id="path_unlock_icon" class="icon-lock"></i></div>
39 39 </span>
40 40 %else:
41 41 ${_('Repository location change is disabled. You can enable this by changing the `allow_repo_location_change` inside .ini file.')}
42 42 ## form still requires this but we cannot internally change it anyway
43 43 ${h.hidden('paths_root_path',size=30,readonly="readonly", class_="disabled")}
44 44 %endif
45 45 </div>
46 46 </div>
47 47 <div class="label">
48 48 <span class="help-block">${_('Filesystem location where repositories should be stored. After changing this value a restart and rescan of the repository folder are required.')}</span>
49 49 </div>
50 50 </div>
51 51 </div>
52 52 % endif
53 53
54 54 % if display_globals or repo_type in ['git', 'hg']:
55 55 <div class="panel panel-default">
56 56 <div class="panel-heading" id="general">
57 57 <h3 class="panel-title">${_('Internal Hooks')}</h3>
58 58 </div>
59 59 <div class="panel-body">
60 60 <div class="field">
61 61 <div class="checkbox">
62 62 ${h.checkbox('hooks_changegroup_repo_size' + suffix, 'True', **kwargs)}
63 63 <label for="hooks_changegroup_repo_size${suffix}">${_('Show repository size after push')}</label>
64 64 </div>
65 65
66 66 <div class="label">
67 67 <span class="help-block">${_('Trigger a hook that calculates repository size after each push.')}</span>
68 68 </div>
69 69 <div class="checkbox">
70 70 ${h.checkbox('hooks_changegroup_push_logger' + suffix, 'True', **kwargs)}
71 71 <label for="hooks_changegroup_push_logger${suffix}">${_('Execute pre/post push hooks')}</label>
72 72 </div>
73 73 <div class="label">
74 74 <span class="help-block">${_('Execute Built in pre/post push hooks. This also executes rcextensions hooks.')}</span>
75 75 </div>
76 76 <div class="checkbox">
77 77 ${h.checkbox('hooks_outgoing_pull_logger' + suffix, 'True', **kwargs)}
78 78 <label for="hooks_outgoing_pull_logger${suffix}">${_('Execute pre/post pull hooks')}</label>
79 79 </div>
80 80 <div class="label">
81 81 <span class="help-block">${_('Execute Built in pre/post pull hooks. This also executes rcextensions hooks.')}</span>
82 82 </div>
83 83 </div>
84 84 </div>
85 85 </div>
86 86 % endif
87 87
88 88 % if display_globals or repo_type in ['hg']:
89 89 <div class="panel panel-default">
90 90 <div class="panel-heading">
91 91 <h3 class="panel-title">${_('Mercurial Settings')}</h3>
92 92 </div>
93 93 <div class="panel-body">
94 94 <div class="checkbox">
95 95 ${h.checkbox('extensions_largefiles' + suffix, 'True', **kwargs)}
96 96 <label for="extensions_largefiles${suffix}">${_('Enable largefiles extension')}</label>
97 97 </div>
98 98 <div class="label">
99 99 <span class="help-block">${_('Enable Largefiles extensions for all repositories.')}</span>
100 100 </div>
101 101 <div class="checkbox">
102 102 ${h.checkbox('phases_publish' + suffix, 'True', **kwargs)}
103 103 <label for="phases_publish${suffix}">${_('Set repositories as publishing') if display_globals else _('Set repository as publishing')}</label>
104 104 </div>
105 105 <div class="label">
106 106 <span class="help-block">${_('When this is enabled all commits in the repository are seen as public commits by clients.')}</span>
107 107 </div>
108 108 % if display_globals:
109 109 <div class="checkbox">
110 110 ${h.checkbox('extensions_hgsubversion' + suffix,'True')}
111 111 <label for="extensions_hgsubversion${suffix}">${_('Enable hgsubversion extension')}</label>
112 112 </div>
113 113 <div class="label">
114 114 <span class="help-block">${_('Requires hgsubversion library to be installed. Allows cloning remote SVN repositories and migrates them to Mercurial type.')}</span>
115 115 </div>
116 116 % endif
117 117 </div>
118 118 </div>
119 119 ## LABS for HG
120 120 % if c.labs_active:
121 121 <div class="panel panel-danger">
122 122 <div class="panel-heading">
123 123 <h3 class="panel-title">${_('Mercurial Labs Settings')} (${_('These features are considered experimental and may not work as expected.')})</h3>
124 124 </div>
125 125 <div class="panel-body">
126 126
127 127 <div class="checkbox">
128 128 ${h.checkbox('rhodecode_hg_use_rebase_for_merging' + suffix, 'True', **kwargs)}
129 129 <label for="rhodecode_hg_use_rebase_for_merging{suffix}">${_('Use rebase as merge strategy')}</label>
130 130 </div>
131 131 <div class="label">
132 132 <span class="help-block">${_('Use rebase instead of creating a merge commit when merging via web interface.')}</span>
133 133 </div>
134 134
135 135 </div>
136 136 </div>
137 137 % endif
138 138
139 139 % endif
140 140
141 % if display_globals or repo_type in ['svn']:
141 % if display_globals:
142 142 <div class="panel panel-default">
143 143 <div class="panel-heading">
144 <h3 class="panel-title">${_('Subversion Settings')}</h3>
144 <h3 class="panel-title">${_('Global Subversion Settings')}</h3>
145 145 </div>
146 146 <div class="panel-body">
147 147 <div class="field">
148 148 <div class="checkbox">
149 ${h.checkbox('rhodecode_proxy_subversion_http_requests' + suffix, 'True', **kwargs)}
150 <label for="rhodecode_proxy_subversion_http_requests${suffix}">${_('Proxy subversion HTTP requests')}</label>
149 ${h.checkbox('vcs_svn_proxy_http_requests_enabled' + suffix, 'True', **kwargs)}
150 <label for="vcs_svn_proxy_http_requests_enabled{suffix}">${_('Proxy subversion HTTP requests')}</label>
151 151 </div>
152 152 <div class="label">
153 153 <span class="help-block">${_('Subversion HTTP Support. Enables communication with SVN over HTTP protocol.')}</span>
154 154 </div>
155 155 </div>
156 156 <div class="field">
157 157 <div class="label">
158 <label for="rhodecode_subversion_http_server_url">${_('Subversion HTTP Server URL')}</label><br/>
158 <label for="vcs_svn_proxy_http_server_url">${_('Subversion HTTP Server URL')}</label><br/>
159 159 </div>
160 160 <div class="input">
161 ${h.text('rhodecode_subversion_http_server_url',size=59)}
161 ${h.text('vcs_svn_proxy_http_server_url',size=59)}
162 </div>
163 </div>
162 164 </div>
163 165 </div>
164 <div class="field">
165 <div class="label">
166 <span class="help-block">${_('Url to Apache Proxy, e.g. http://localhost:8080/')}</span>
166 % endif
167
168 % if display_globals or repo_type in ['svn']:
169 <div class="panel panel-default">
170 <div class="panel-heading">
171 <h3 class="panel-title">${_('Subversion Settings')}</h3>
167 172 </div>
168 </div>
173 <div class="panel-body">
169 174 <div class="field">
170 175 <div class="content" >
171 176 <label>${_('Repository patterns')}</label><br/>
172 177 </div>
173 178 </div>
174 179 <div class="label">
175 180 <span class="help-block">${_('Patterns for identifying SVN branches and tags. For recursive search, use "*". Eg.: "/branches/*"')}</span>
176 181 </div>
177 182
178 183 <div class="field branch_patterns">
179 184 <div class="input" >
180 185 <label>${_('Branches')}:</label><br/>
181 186 </div>
182 187 % if svn_branch_patterns:
183 188 % for branch in svn_branch_patterns:
184 189 <div class="input adjacent" id="${'id%s' % branch.ui_id}">
185 190 ${h.hidden('branch_ui_key' + suffix, branch.ui_key)}
186 191 ${h.text('branch_value_%d' % branch.ui_id + suffix, branch.ui_value, size=59, readonly="readonly", class_='disabled')}
187 192 % if kwargs.get('disabled') != 'disabled':
188 193 <span class="btn btn-x" onclick="ajaxDeletePattern(${branch.ui_id},'${'id%s' % branch.ui_id}')">
189 194 ${_('Delete')}
190 195 </span>
191 196 % endif
192 197 </div>
193 198 % endfor
194 199 %endif
195 200 </div>
196 201 % if kwargs.get('disabled') != 'disabled':
197 202 <div class="field branch_patterns">
198 203 <div class="input" >
199 204 ${h.text('new_svn_branch',size=59,placeholder='New branch pattern')}
200 205 </div>
201 206 </div>
202 207 % endif
203 208 <div class="field tag_patterns">
204 209 <div class="input" >
205 210 <label>${_('Tags')}:</label><br/>
206 211 </div>
207 212 % if svn_tag_patterns:
208 213 % for tag in svn_tag_patterns:
209 214 <div class="input" id="${'id%s' % tag.ui_id + suffix}">
210 215 ${h.hidden('tag_ui_key' + suffix, tag.ui_key)}
211 216 ${h.text('tag_ui_value_new_%d' % tag.ui_id + suffix, tag.ui_value, size=59, readonly="readonly", class_='disabled tag_input')}
212 217 % if kwargs.get('disabled') != 'disabled':
213 218 <span class="btn btn-x" onclick="ajaxDeletePattern(${tag.ui_id},'${'id%s' % tag.ui_id}')">
214 219 ${_('Delete')}
215 220 </span>
216 221 %endif
217 222 </div>
218 223 % endfor
219 224 % endif
220 225 </div>
221 226 % if kwargs.get('disabled') != 'disabled':
222 227 <div class="field tag_patterns">
223 228 <div class="input" >
224 229 ${h.text('new_svn_tag' + suffix, size=59, placeholder='New tag pattern')}
225 230 </div>
226 231 </div>
227 232 %endif
228 233 </div>
229 234 </div>
230 235 % else:
231 236 ${h.hidden('new_svn_branch' + suffix, '')}
232 237 ${h.hidden('new_svn_tag' + suffix, '')}
233 238 % endif
234 239
240
241
242
235 243 % if display_globals or repo_type in ['hg', 'git']:
236 244 <div class="panel panel-default">
237 245 <div class="panel-heading">
238 246 <h3 class="panel-title">${_('Pull Request Settings')}</h3>
239 247 </div>
240 248 <div class="panel-body">
241 249 <div class="checkbox">
242 250 ${h.checkbox('rhodecode_pr_merge_enabled' + suffix, 'True', **kwargs)}
243 251 <label for="rhodecode_pr_merge_enabled${suffix}">${_('Enable server-side merge for pull requests')}</label>
244 252 </div>
245 253 <div class="label">
246 254 <span class="help-block">${_('Note: when this feature is enabled, it only runs hooks defined in the rcextension package. Custom hooks added on the Admin -> Settings -> Hooks page will not be run when pull requests are automatically merged from the web interface.')}</span>
247 255 </div>
248 256 <div class="checkbox">
249 257 ${h.checkbox('rhodecode_use_outdated_comments' + suffix, 'True', **kwargs)}
250 258 <label for="rhodecode_use_outdated_comments${suffix}">${_('Invalidate and relocate inline comments during update')}</label>
251 259 </div>
252 260 <div class="label">
253 261 <span class="help-block">${_('During the update of a pull request, the position of inline comments will be updated and outdated inline comments will be hidden.')}</span>
254 262 </div>
255 263 </div>
256 264 </div>
257 265 % endif
258 266
259 267 </%def>
@@ -1,655 +1,611 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 mock
22 22 import pytest
23 23
24 24 import rhodecode
25 25 from rhodecode.config.routing import ADMIN_PREFIX
26 26 from rhodecode.lib.utils2 import md5
27 27 from rhodecode.model.db import RhodeCodeUi
28 28 from rhodecode.model.meta import Session
29 29 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
30 30 from rhodecode.tests import url, assert_session_flash
31 31 from rhodecode.tests.utils import AssertResponse
32 32
33 33
34 34 UPDATE_DATA_QUALNAME = (
35 35 'rhodecode.controllers.admin.settings.SettingsController.get_update_data')
36 36
37 37
38 38 @pytest.mark.usefixtures('autologin_user', 'app')
39 39 class TestAdminSettingsController:
40 40
41 41 @pytest.mark.parametrize('urlname', [
42 42 'admin_settings_vcs',
43 43 'admin_settings_mapping',
44 44 'admin_settings_global',
45 45 'admin_settings_visual',
46 46 'admin_settings_email',
47 47 'admin_settings_hooks',
48 48 'admin_settings_search',
49 49 'admin_settings_system',
50 50 ])
51 51 def test_simple_get(self, urlname, app):
52 52 app.get(url(urlname))
53 53
54 54 def test_create_custom_hook(self, csrf_token):
55 55 response = self.app.post(
56 56 url('admin_settings_hooks'),
57 57 params={
58 58 'new_hook_ui_key': 'test_hooks_1',
59 59 'new_hook_ui_value': 'cd /tmp',
60 60 'csrf_token': csrf_token})
61 61
62 62 response = response.follow()
63 63 response.mustcontain('test_hooks_1')
64 64 response.mustcontain('cd /tmp')
65 65
66 66 def test_create_custom_hook_delete(self, csrf_token):
67 67 response = self.app.post(
68 68 url('admin_settings_hooks'),
69 69 params={
70 70 'new_hook_ui_key': 'test_hooks_2',
71 71 'new_hook_ui_value': 'cd /tmp2',
72 72 'csrf_token': csrf_token})
73 73
74 74 response = response.follow()
75 75 response.mustcontain('test_hooks_2')
76 76 response.mustcontain('cd /tmp2')
77 77
78 78 hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
79 79
80 80 # delete
81 81 self.app.post(
82 82 url('admin_settings_hooks'),
83 83 params={'hook_id': hook_id, 'csrf_token': csrf_token})
84 84 response = self.app.get(url('admin_settings_hooks'))
85 85 response.mustcontain(no=['test_hooks_2'])
86 86 response.mustcontain(no=['cd /tmp2'])
87 87
88 88 def test_system_update_new_version(self):
89 89 update_data = {
90 90 'versions': [
91 91 {
92 92 'version': '100.3.1415926535',
93 93 'general': 'The latest version we are ever going to ship'
94 94 },
95 95 {
96 96 'version': '0.0.0',
97 97 'general': 'The first version we ever shipped'
98 98 }
99 99 ]
100 100 }
101 101 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
102 102 response = self.app.get(url('admin_settings_system_update'))
103 103 response.mustcontain('A <b>new version</b> is available')
104 104
105 105 def test_system_update_nothing_new(self):
106 106 update_data = {
107 107 'versions': [
108 108 {
109 109 'version': '0.0.0',
110 110 'general': 'The first version we ever shipped'
111 111 }
112 112 ]
113 113 }
114 114 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
115 115 response = self.app.get(url('admin_settings_system_update'))
116 116 response.mustcontain(
117 117 'You already have the <b>latest</b> stable version.')
118 118
119 119 def test_system_update_bad_response(self):
120 120 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
121 121 response = self.app.get(url('admin_settings_system_update'))
122 122 response.mustcontain(
123 123 'Bad data sent from update server')
124 124
125 125
126 126 @pytest.mark.usefixtures('autologin_user', 'app')
127 127 class TestAdminSettingsGlobal:
128 128
129 129 def test_pre_post_code_code_active(self, csrf_token):
130 130 pre_code = 'rc-pre-code-187652122'
131 131 post_code = 'rc-postcode-98165231'
132 132
133 133 response = self.post_and_verify_settings({
134 134 'rhodecode_pre_code': pre_code,
135 135 'rhodecode_post_code': post_code,
136 136 'csrf_token': csrf_token,
137 137 })
138 138
139 139 response = response.follow()
140 140 response.mustcontain(pre_code, post_code)
141 141
142 142 def test_pre_post_code_code_inactive(self, csrf_token):
143 143 pre_code = 'rc-pre-code-187652122'
144 144 post_code = 'rc-postcode-98165231'
145 145 response = self.post_and_verify_settings({
146 146 'rhodecode_pre_code': '',
147 147 'rhodecode_post_code': '',
148 148 'csrf_token': csrf_token,
149 149 })
150 150
151 151 response = response.follow()
152 152 response.mustcontain(no=[pre_code, post_code])
153 153
154 154 def test_captcha_activate(self, csrf_token):
155 155 self.post_and_verify_settings({
156 156 'rhodecode_captcha_private_key': '1234567890',
157 157 'rhodecode_captcha_public_key': '1234567890',
158 158 'csrf_token': csrf_token,
159 159 })
160 160
161 161 response = self.app.get(ADMIN_PREFIX + '/register')
162 162 response.mustcontain('captcha')
163 163
164 164 def test_captcha_deactivate(self, csrf_token):
165 165 self.post_and_verify_settings({
166 166 'rhodecode_captcha_private_key': '',
167 167 'rhodecode_captcha_public_key': '1234567890',
168 168 'csrf_token': csrf_token,
169 169 })
170 170
171 171 response = self.app.get(ADMIN_PREFIX + '/register')
172 172 response.mustcontain(no=['captcha'])
173 173
174 174 def test_title_change(self, csrf_token):
175 175 old_title = 'RhodeCode'
176 176 new_title = old_title + '_changed'
177 177
178 178 for new_title in ['Changed', 'Ε»Γ³Ε‚wik', old_title]:
179 179 response = self.post_and_verify_settings({
180 180 'rhodecode_title': new_title,
181 181 'csrf_token': csrf_token,
182 182 })
183 183
184 184 response = response.follow()
185 185 response.mustcontain(
186 186 """<div class="branding">- %s</div>""" % new_title)
187 187
188 188 def post_and_verify_settings(self, settings):
189 189 old_title = 'RhodeCode'
190 190 old_realm = 'RhodeCode authentication'
191 191 params = {
192 192 'rhodecode_title': old_title,
193 193 'rhodecode_realm': old_realm,
194 194 'rhodecode_pre_code': '',
195 195 'rhodecode_post_code': '',
196 196 'rhodecode_captcha_private_key': '',
197 197 'rhodecode_captcha_public_key': '',
198 198 }
199 199 params.update(settings)
200 200 response = self.app.post(url('admin_settings_global'), params=params)
201 201
202 202 assert_session_flash(response, 'Updated application settings')
203 203 app_settings = SettingsModel().get_all_settings()
204 204 del settings['csrf_token']
205 205 for key, value in settings.iteritems():
206 206 assert app_settings[key] == value.decode('utf-8')
207 207
208 208 return response
209 209
210 210
211 211 @pytest.mark.usefixtures('autologin_user', 'app')
212 212 class TestAdminSettingsVcs:
213 213
214 214 def test_contains_svn_default_patterns(self, app):
215 215 response = app.get(url('admin_settings_vcs'))
216 216 expected_patterns = [
217 217 '/trunk',
218 218 '/branches/*',
219 219 '/tags/*',
220 220 ]
221 221 for pattern in expected_patterns:
222 222 response.mustcontain(pattern)
223 223
224 224 def test_add_new_svn_branch_and_tag_pattern(
225 225 self, app, backend_svn, form_defaults, disable_sql_cache,
226 226 csrf_token):
227 227 form_defaults.update({
228 228 'new_svn_branch': '/exp/branches/*',
229 229 'new_svn_tag': '/important_tags/*',
230 230 'csrf_token': csrf_token,
231 231 })
232 232
233 233 response = app.post(
234 234 url('admin_settings_vcs'), params=form_defaults, status=302)
235 235 response = response.follow()
236 236
237 237 # Expect to find the new values on the page
238 238 response.mustcontain('/exp/branches/*')
239 239 response.mustcontain('/important_tags/*')
240 240
241 241 # Expect that those patterns are used to match branches and tags now
242 242 repo = backend_svn['svn-simple-layout'].scm_instance()
243 243 assert 'exp/branches/exp-sphinx-docs' in repo.branches
244 244 assert 'important_tags/v0.5' in repo.tags
245 245
246 246 def test_add_same_svn_value_twice_shows_an_error_message(
247 247 self, app, form_defaults, csrf_token, settings_util):
248 248 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
249 249 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
250 250
251 251 response = app.post(
252 252 url('admin_settings_vcs'),
253 253 params={
254 254 'paths_root_path': form_defaults['paths_root_path'],
255 255 'new_svn_branch': '/test',
256 256 'new_svn_tag': '/test',
257 257 'csrf_token': csrf_token,
258 258 },
259 259 status=200)
260 260
261 261 response.mustcontain("Pattern already exists")
262 262 response.mustcontain("Some form inputs contain invalid data.")
263 263
264 264 @pytest.mark.parametrize('section', [
265 265 'vcs_svn_branch',
266 266 'vcs_svn_tag',
267 267 ])
268 268 def test_delete_svn_patterns(
269 269 self, section, app, csrf_token, settings_util):
270 270 setting = settings_util.create_rhodecode_ui(
271 271 section, '/test_delete', cleanup=False)
272 272
273 273 app.post(
274 274 url('admin_settings_vcs'),
275 275 params={
276 276 '_method': 'delete',
277 277 'delete_svn_pattern': setting.ui_id,
278 278 'csrf_token': csrf_token},
279 279 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
280 280
281 281 @pytest.mark.parametrize('section', [
282 282 'vcs_svn_branch',
283 283 'vcs_svn_tag',
284 284 ])
285 285 def test_delete_svn_patterns_raises_400_when_no_xhr(
286 286 self, section, app, csrf_token, settings_util):
287 287 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
288 288
289 289 app.post(
290 290 url('admin_settings_vcs'),
291 291 params={
292 292 '_method': 'delete',
293 293 'delete_svn_pattern': setting.ui_id,
294 294 'csrf_token': csrf_token},
295 295 status=400)
296 296
297 297 def test_extensions_hgsubversion(self, app, form_defaults, csrf_token):
298 298 form_defaults.update({
299 299 'csrf_token': csrf_token,
300 300 'extensions_hgsubversion': 'True',
301 301 })
302 302 response = app.post(
303 303 url('admin_settings_vcs'),
304 304 params=form_defaults,
305 305 status=302)
306 306
307 307 response = response.follow()
308 308 extensions_input = (
309 309 '<input id="extensions_hgsubversion" '
310 310 'name="extensions_hgsubversion" type="checkbox" '
311 311 'value="True" checked="checked" />')
312 312 response.mustcontain(extensions_input)
313 313
314 314 def test_has_a_section_for_pull_request_settings(self, app):
315 315 response = app.get(url('admin_settings_vcs'))
316 316 response.mustcontain('Pull Request Settings')
317 317
318 318 def test_has_an_input_for_invalidation_of_inline_comments(
319 319 self, app):
320 320 response = app.get(url('admin_settings_vcs'))
321 321 assert_response = AssertResponse(response)
322 322 assert_response.one_element_exists(
323 323 '[name=rhodecode_use_outdated_comments]')
324 324
325 325 @pytest.mark.parametrize('new_value', [True, False])
326 326 def test_allows_to_change_invalidation_of_inline_comments(
327 327 self, app, form_defaults, csrf_token, new_value):
328 328 setting_key = 'use_outdated_comments'
329 329 setting = SettingsModel().create_or_update_setting(
330 330 setting_key, not new_value, 'bool')
331 331 Session().add(setting)
332 332 Session().commit()
333 333
334 334 form_defaults.update({
335 335 'csrf_token': csrf_token,
336 336 'rhodecode_use_outdated_comments': str(new_value),
337 337 })
338 338 response = app.post(
339 339 url('admin_settings_vcs'),
340 340 params=form_defaults,
341 341 status=302)
342 342 response = response.follow()
343 343 setting = SettingsModel().get_setting_by_name(setting_key)
344 344 assert setting.app_settings_value is new_value
345 345
346 346 def test_has_a_section_for_labs_settings_if_enabled(self, app):
347 347 with mock.patch.dict(
348 348 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
349 349 response = self.app.get(url('admin_settings_vcs'))
350 response.mustcontain('Labs settings:')
350 response.mustcontain('Labs Settings')
351 351
352 352 def test_has_not_a_section_for_labs_settings_if_disables(self, app):
353 353 with mock.patch.dict(
354 354 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
355 355 response = self.app.get(url('admin_settings_vcs'))
356 response.mustcontain(no='Labs settings:')
356 response.mustcontain(no='Labs Settings')
357 357
358 358 @pytest.mark.parametrize('new_value', [True, False])
359 359 def test_allows_to_change_hg_rebase_merge_strategy(
360 360 self, app, form_defaults, csrf_token, new_value):
361 361 setting_key = 'hg_use_rebase_for_merging'
362 362
363 363 form_defaults.update({
364 364 'csrf_token': csrf_token,
365 365 'rhodecode_' + setting_key: str(new_value),
366 366 })
367 367
368 368 with mock.patch.dict(
369 369 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
370 370 app.post(
371 371 url('admin_settings_vcs'),
372 372 params=form_defaults,
373 373 status=302)
374 374
375 375 setting = SettingsModel().get_setting_by_name(setting_key)
376 376 assert setting.app_settings_value is new_value
377 377
378 378 @pytest.fixture
379 379 def disable_sql_cache(self, request):
380 380 patcher = mock.patch(
381 381 'rhodecode.lib.caching_query.FromCache.process_query')
382 382 request.addfinalizer(patcher.stop)
383 383 patcher.start()
384 384
385 385 @pytest.fixture
386 386 def form_defaults(self):
387 387 from rhodecode.controllers.admin.settings import SettingsController
388 388 controller = SettingsController()
389 389 return controller._form_defaults()
390 390
391 391 # TODO: johbo: What we really want is to checkpoint before a test run and
392 392 # reset the session afterwards.
393 393 @pytest.fixture(scope='class', autouse=True)
394 394 def cleanup_settings(self, request, pylonsapp):
395 395 ui_id = RhodeCodeUi.ui_id
396 396 original_ids = list(
397 397 r.ui_id for r in RhodeCodeUi.query().values(ui_id))
398 398
399 399 @request.addfinalizer
400 400 def cleanup():
401 401 RhodeCodeUi.query().filter(
402 402 ui_id.notin_(original_ids)).delete(False)
403 403
404 404
405 405 @pytest.mark.usefixtures('autologin_user', 'app')
406 406 class TestLabsSettings(object):
407 407 def test_get_settings_page_disabled(self):
408 408 with mock.patch.dict(rhodecode.CONFIG,
409 409 {'labs_settings_active': 'false'}):
410 410 response = self.app.get(url('admin_settings_labs'), status=302)
411 411
412 412 assert response.location.endswith(url('admin_settings'))
413 413
414 414 def test_get_settings_page_enabled(self):
415 415 from rhodecode.controllers.admin import settings
416 416 lab_settings = [
417 417 settings.LabSetting(
418 418 key='rhodecode_bool',
419 419 type='bool',
420 420 group='bool group',
421 421 label='bool label',
422 422 help='bool help'
423 423 ),
424 424 settings.LabSetting(
425 425 key='rhodecode_text',
426 426 type='unicode',
427 427 group='text group',
428 428 label='text label',
429 429 help='text help'
430 430 ),
431 431 ]
432 432 with mock.patch.dict(rhodecode.CONFIG,
433 433 {'labs_settings_active': 'true'}):
434 434 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
435 435 response = self.app.get(url('admin_settings_labs'))
436 436
437 437 assert '<label>bool group:</label>' in response
438 438 assert '<label for="rhodecode_bool">bool label</label>' in response
439 439 assert '<p class="help-block">bool help</p>' in response
440 440 assert 'name="rhodecode_bool" type="checkbox"' in response
441 441
442 442 assert '<label>text group:</label>' in response
443 443 assert '<label for="rhodecode_text">text label</label>' in response
444 444 assert '<p class="help-block">text help</p>' in response
445 445 assert 'name="rhodecode_text" size="60" type="text"' in response
446 446
447 @pytest.mark.parametrize('setting_name', [
448 'proxy_subversion_http_requests',
449 ])
450 def test_update_boolean_settings(self, csrf_token, setting_name):
451 self.app.post(
452 url('admin_settings_labs'),
453 params={
454 'rhodecode_{}'.format(setting_name): 'true',
455 'csrf_token': csrf_token,
456 })
457 setting = SettingsModel().get_setting_by_name(setting_name)
458 assert setting.app_settings_value
459
460 self.app.post(
461 url('admin_settings_labs'),
462 params={
463 'rhodecode_{}'.format(setting_name): 'false',
464 'csrf_token': csrf_token,
465 })
466 setting = SettingsModel().get_setting_by_name(setting_name)
467 assert not setting.app_settings_value
468
469 @pytest.mark.parametrize('setting_name', [
470 'subversion_http_server_url',
471 ])
472 def test_update_string_settings(self, csrf_token, setting_name):
473 self.app.post(
474 url('admin_settings_labs'),
475 params={
476 'rhodecode_{}'.format(setting_name): 'Test 1',
477 'csrf_token': csrf_token,
478 })
479 setting = SettingsModel().get_setting_by_name(setting_name)
480 assert setting.app_settings_value == 'Test 1'
481
482 self.app.post(
483 url('admin_settings_labs'),
484 params={
485 'rhodecode_{}'.format(setting_name): ' Test 2 ',
486 'csrf_token': csrf_token,
487 })
488 setting = SettingsModel().get_setting_by_name(setting_name)
489 assert setting.app_settings_value == 'Test 2'
490
491 447
492 448 @pytest.mark.usefixtures('app')
493 449 class TestOpenSourceLicenses(object):
494 450
495 451 def _get_url(self):
496 452 return ADMIN_PREFIX + '/settings/open_source'
497 453
498 454 def test_records_are_displayed(self, autologin_user):
499 455 sample_licenses = {
500 456 "python2.7-pytest-2.7.1": {
501 457 "UNKNOWN": None
502 458 },
503 459 "python2.7-Markdown-2.6.2": {
504 460 "BSD-3-Clause": "http://spdx.org/licenses/BSD-3-Clause"
505 461 }
506 462 }
507 463 read_licenses_patch = mock.patch(
508 464 'rhodecode.admin.views.read_opensource_licenses',
509 465 return_value=sample_licenses)
510 466 with read_licenses_patch:
511 467 response = self.app.get(self._get_url(), status=200)
512 468
513 469 assert_response = AssertResponse(response)
514 470 assert_response.element_contains(
515 471 '.panel-heading', 'Licenses of Third Party Packages')
516 472 for name in sample_licenses:
517 473 response.mustcontain(name)
518 474 for license in sample_licenses[name]:
519 475 assert_response.element_contains('.panel-body', license)
520 476
521 477 def test_records_can_be_read(self, autologin_user):
522 478 response = self.app.get(self._get_url(), status=200)
523 479 assert_response = AssertResponse(response)
524 480 assert_response.element_contains(
525 481 '.panel-heading', 'Licenses of Third Party Packages')
526 482
527 483 def test_forbidden_when_normal_user(self, autologin_regular_user):
528 484 self.app.get(self._get_url(), status=403)
529 485
530 486
531 487 @pytest.mark.usefixtures("app")
532 488 class TestAdminSettingsIssueTracker:
533 489 RC_PREFIX = 'rhodecode_'
534 490 SHORT_PATTERN_KEY = 'issuetracker_pat_'
535 491 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
536 492
537 493 def test_issuetracker_index(self, autologin_user):
538 494 response = self.app.get(url('admin_settings_issuetracker'))
539 495 assert response.status_code == 200
540 496
541 497 def test_add_issuetracker_pattern(
542 498 self, request, autologin_user, csrf_token):
543 499 pattern = 'issuetracker_pat'
544 500 another_pattern = pattern+'1'
545 501 post_url = url('admin_settings_issuetracker_save')
546 502 post_data = {
547 503 'new_pattern_pattern_0': pattern,
548 504 'new_pattern_url_0': 'url',
549 505 'new_pattern_prefix_0': 'prefix',
550 506 'new_pattern_description_0': 'description',
551 507 'new_pattern_pattern_1': another_pattern,
552 508 'new_pattern_url_1': 'url1',
553 509 'new_pattern_prefix_1': 'prefix1',
554 510 'new_pattern_description_1': 'description1',
555 511 'csrf_token': csrf_token
556 512 }
557 513 self.app.post(post_url, post_data, status=302)
558 514 settings = SettingsModel().get_all_settings()
559 515 self.uid = md5(pattern)
560 516 assert settings[self.PATTERN_KEY+self.uid] == pattern
561 517 self.another_uid = md5(another_pattern)
562 518 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
563 519
564 520 @request.addfinalizer
565 521 def cleanup():
566 522 defaults = SettingsModel().get_all_settings()
567 523
568 524 entries = [name for name in defaults if (
569 525 (self.uid in name) or (self.another_uid) in name)]
570 526 start = len(self.RC_PREFIX)
571 527 for del_key in entries:
572 528 # TODO: anderson: get_by_name needs name without prefix
573 529 entry = SettingsModel().get_setting_by_name(del_key[start:])
574 530 Session().delete(entry)
575 531
576 532 Session().commit()
577 533
578 534 def test_edit_issuetracker_pattern(
579 535 self, autologin_user, backend, csrf_token, request):
580 536 old_pattern = 'issuetracker_pat'
581 537 old_uid = md5(old_pattern)
582 538 pattern = 'issuetracker_pat_new'
583 539 self.new_uid = md5(pattern)
584 540
585 541 SettingsModel().create_or_update_setting(
586 542 self.SHORT_PATTERN_KEY+old_uid, old_pattern, 'unicode')
587 543
588 544 post_url = url('admin_settings_issuetracker_save')
589 545 post_data = {
590 546 'new_pattern_pattern_0': pattern,
591 547 'new_pattern_url_0': 'url',
592 548 'new_pattern_prefix_0': 'prefix',
593 549 'new_pattern_description_0': 'description',
594 550 'uid': old_uid,
595 551 'csrf_token': csrf_token
596 552 }
597 553 self.app.post(post_url, post_data, status=302)
598 554 settings = SettingsModel().get_all_settings()
599 555 assert settings[self.PATTERN_KEY+self.new_uid] == pattern
600 556 assert self.PATTERN_KEY+old_uid not in settings
601 557
602 558 @request.addfinalizer
603 559 def cleanup():
604 560 IssueTrackerSettingsModel().delete_entries(self.new_uid)
605 561
606 562 def test_replace_issuetracker_pattern_description(
607 563 self, autologin_user, csrf_token, request, settings_util):
608 564 prefix = 'issuetracker'
609 565 pattern = 'issuetracker_pat'
610 566 self.uid = md5(pattern)
611 567 pattern_key = '_'.join([prefix, 'pat', self.uid])
612 568 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
613 569 desc_key = '_'.join([prefix, 'desc', self.uid])
614 570 rc_desc_key = '_'.join(['rhodecode', desc_key])
615 571 new_description = 'new_description'
616 572
617 573 settings_util.create_rhodecode_setting(
618 574 pattern_key, pattern, 'unicode', cleanup=False)
619 575 settings_util.create_rhodecode_setting(
620 576 desc_key, 'old description', 'unicode', cleanup=False)
621 577
622 578 post_url = url('admin_settings_issuetracker_save')
623 579 post_data = {
624 580 'new_pattern_pattern_0': pattern,
625 581 'new_pattern_url_0': 'url',
626 582 'new_pattern_prefix_0': 'prefix',
627 583 'new_pattern_description_0': new_description,
628 584 'uid': self.uid,
629 585 'csrf_token': csrf_token
630 586 }
631 587 self.app.post(post_url, post_data, status=302)
632 588 settings = SettingsModel().get_all_settings()
633 589 assert settings[rc_pattern_key] == pattern
634 590 assert settings[rc_desc_key] == new_description
635 591
636 592 @request.addfinalizer
637 593 def cleanup():
638 594 IssueTrackerSettingsModel().delete_entries(self.uid)
639 595
640 596 def test_delete_issuetracker_pattern(
641 597 self, autologin_user, backend, csrf_token, settings_util):
642 598 pattern = 'issuetracker_pat'
643 599 uid = md5(pattern)
644 600 settings_util.create_rhodecode_setting(
645 601 self.SHORT_PATTERN_KEY+uid, pattern, 'unicode', cleanup=False)
646 602
647 603 post_url = url('admin_issuetracker_delete')
648 604 post_data = {
649 605 '_method': 'delete',
650 606 'uid': uid,
651 607 'csrf_token': csrf_token
652 608 }
653 609 self.app.post(post_url, post_data, status=302)
654 610 settings = SettingsModel().get_all_settings()
655 611 assert 'rhodecode_%s%s' % (self.SHORT_PATTERN_KEY, uid) not in settings
@@ -1,188 +1,203 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 from StringIO import StringIO
22 22
23 23 import pytest
24 24 from mock import patch, Mock
25 25
26 26 import rhodecode
27 27 from rhodecode.lib.middleware.simplesvn import SimpleSvn, SimpleSvnApp
28 28
29 29
30 30 class TestSimpleSvn(object):
31 31 @pytest.fixture(autouse=True)
32 32 def simple_svn(self, pylonsapp):
33 33 self.app = SimpleSvn(
34 34 application='None',
35 35 config={'auth_ret_code': '',
36 36 'base_path': rhodecode.CONFIG['base_path']},
37 37 registry=None)
38 38
39 39 def test_get_config(self):
40 40 extras = {'foo': 'FOO', 'bar': 'BAR'}
41 41 config = self.app._create_config(extras, repo_name='test-repo')
42 42 assert config == extras
43 43
44 44 @pytest.mark.parametrize(
45 45 'method', ['OPTIONS', 'PROPFIND', 'GET', 'REPORT'])
46 46 def test_get_action_returns_pull(self, method):
47 47 environment = {'REQUEST_METHOD': method}
48 48 action = self.app._get_action(environment)
49 49 assert action == 'pull'
50 50
51 51 @pytest.mark.parametrize(
52 52 'method', [
53 53 'MKACTIVITY', 'PROPPATCH', 'PUT', 'CHECKOUT', 'MKCOL', 'MOVE',
54 54 'COPY', 'DELETE', 'LOCK', 'UNLOCK', 'MERGE'
55 55 ])
56 56 def test_get_action_returns_push(self, method):
57 57 environment = {'REQUEST_METHOD': method}
58 58 action = self.app._get_action(environment)
59 59 assert action == 'push'
60 60
61 61 @pytest.mark.parametrize(
62 62 'path, expected_name', [
63 63 ('/hello-svn', 'hello-svn'),
64 64 ('/hello-svn/', 'hello-svn'),
65 65 ('/group/hello-svn/', 'group/hello-svn'),
66 66 ('/group/hello-svn/!svn/vcc/default', 'group/hello-svn'),
67 67 ])
68 68 def test_get_repository_name(self, path, expected_name):
69 69 environment = {'PATH_INFO': path}
70 70 name = self.app._get_repository_name(environment)
71 71 assert name == expected_name
72 72
73 73 def test_get_repository_name_subfolder(self, backend_svn):
74 74 repo = backend_svn.repo
75 75 environment = {
76 76 'PATH_INFO': '/{}/path/with/subfolders'.format(repo.repo_name)}
77 77 name = self.app._get_repository_name(environment)
78 78 assert name == repo.repo_name
79 79
80 80 def test_create_wsgi_app(self):
81 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
82 mock_method.return_value = False
83 with patch('rhodecode.lib.middleware.simplesvn.DisabledSimpleSvnApp') as (
84 wsgi_app_mock):
85 config = Mock()
86 wsgi_app = self.app._create_wsgi_app(
87 repo_path='', repo_name='', config=config)
88
89 wsgi_app_mock.assert_called_once_with(config)
90 assert wsgi_app == wsgi_app_mock()
91
92 def test_create_wsgi_app_when_enabled(self):
93 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
94 mock_method.return_value = True
81 95 with patch('rhodecode.lib.middleware.simplesvn.SimpleSvnApp') as (
82 96 wsgi_app_mock):
83 97 config = Mock()
84 98 wsgi_app = self.app._create_wsgi_app(
85 99 repo_path='', repo_name='', config=config)
86 100
87 101 wsgi_app_mock.assert_called_once_with(config)
88 102 assert wsgi_app == wsgi_app_mock()
89 103
90 104
105
91 106 class TestSimpleSvnApp(object):
92 107 data = '<xml></xml>'
93 108 path = '/group/my-repo'
94 109 wsgi_input = StringIO(data)
95 110 environment = {
96 111 'HTTP_DAV': (
97 112 'http://subversion.tigris.org/xmlns/dav/svn/depth,'
98 113 ' http://subversion.tigris.org/xmlns/dav/svn/mergeinfo'),
99 114 'HTTP_USER_AGENT': 'SVN/1.8.11 (x86_64-linux) serf/1.3.8',
100 115 'REQUEST_METHOD': 'OPTIONS',
101 116 'PATH_INFO': path,
102 117 'wsgi.input': wsgi_input,
103 118 'CONTENT_TYPE': 'text/xml',
104 119 'CONTENT_LENGTH': '130'
105 120 }
106 121
107 122 def setup_method(self, method):
108 123 self.host = 'http://localhost/'
109 124 self.app = SimpleSvnApp(
110 125 config={'subversion_http_server_url': self.host})
111 126
112 127 def test_get_request_headers_with_content_type(self):
113 128 expected_headers = {
114 129 'Dav': self.environment['HTTP_DAV'],
115 130 'User-Agent': self.environment['HTTP_USER_AGENT'],
116 131 'Content-Type': self.environment['CONTENT_TYPE'],
117 132 'Content-Length': self.environment['CONTENT_LENGTH']
118 133 }
119 134 headers = self.app._get_request_headers(self.environment)
120 135 assert headers == expected_headers
121 136
122 137 def test_get_request_headers_without_content_type(self):
123 138 environment = self.environment.copy()
124 139 environment.pop('CONTENT_TYPE')
125 140 expected_headers = {
126 141 'Dav': environment['HTTP_DAV'],
127 142 'Content-Length': self.environment['CONTENT_LENGTH'],
128 143 'User-Agent': environment['HTTP_USER_AGENT'],
129 144 }
130 145 request_headers = self.app._get_request_headers(environment)
131 146 assert request_headers == expected_headers
132 147
133 148 def test_get_response_headers(self):
134 149 headers = {
135 150 'Connection': 'keep-alive',
136 151 'Keep-Alive': 'timeout=5, max=100',
137 152 'Transfer-Encoding': 'chunked',
138 153 'Content-Encoding': 'gzip',
139 154 'MS-Author-Via': 'DAV',
140 155 'SVN-Supported-Posts': 'create-txn-with-props'
141 156 }
142 157 expected_headers = [
143 158 ('MS-Author-Via', 'DAV'),
144 159 ('SVN-Supported-Posts', 'create-txn-with-props'),
145 160 ('X-RhodeCode-Backend', 'svn'),
146 161 ]
147 162 response_headers = self.app._get_response_headers(headers)
148 163 assert sorted(response_headers) == sorted(expected_headers)
149 164
150 165 def test_get_url(self):
151 166 url = self.app._get_url(self.path)
152 167 expected_url = '{}{}'.format(self.host.strip('/'), self.path)
153 168 assert url == expected_url
154 169
155 170 def test_call(self):
156 171 start_response = Mock()
157 172 response_mock = Mock()
158 173 response_mock.headers = {
159 174 'Content-Encoding': 'gzip',
160 175 'MS-Author-Via': 'DAV',
161 176 'SVN-Supported-Posts': 'create-txn-with-props'
162 177 }
163 178 response_mock.status_code = 200
164 179 response_mock.reason = 'OK'
165 180 with patch('rhodecode.lib.middleware.simplesvn.requests.request') as (
166 181 request_mock):
167 182 request_mock.return_value = response_mock
168 183 self.app(self.environment, start_response)
169 184
170 185 expected_url = '{}{}'.format(self.host.strip('/'), self.path)
171 186 expected_request_headers = {
172 187 'Dav': self.environment['HTTP_DAV'],
173 188 'User-Agent': self.environment['HTTP_USER_AGENT'],
174 189 'Content-Type': self.environment['CONTENT_TYPE'],
175 190 'Content-Length': self.environment['CONTENT_LENGTH']
176 191 }
177 192 expected_response_headers = [
178 193 ('SVN-Supported-Posts', 'create-txn-with-props'),
179 194 ('MS-Author-Via', 'DAV'),
180 195 ('X-RhodeCode-Backend', 'svn'),
181 196 ]
182 197 request_mock.assert_called_once_with(
183 198 self.environment['REQUEST_METHOD'], expected_url,
184 199 data=self.data, headers=expected_request_headers)
185 200 response_mock.iter_content.assert_called_once_with(chunk_size=1024)
186 201 args, _ = start_response.call_args
187 202 assert args[0] == '200 OK'
188 203 assert sorted(args[1]) == sorted(expected_response_headers)
@@ -1,136 +1,140 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 from mock import patch, Mock
22 22
23 23 import rhodecode
24 24 from rhodecode.lib.middleware import vcs
25 from rhodecode.lib.middleware.simplesvn import (
26 SimpleSvn, DisabledSimpleSvnApp, SimpleSvnApp)
27 from rhodecode.tests import SVN_REPO
25 28
29 svn_repo_path = '/'+ SVN_REPO
26 30
27 31 def test_is_hg():
28 32 environ = {
29 'PATH_INFO': '/rhodecode-dev',
33 'PATH_INFO': svn_repo_path,
30 34 'QUERY_STRING': 'cmd=changegroup',
31 35 'HTTP_ACCEPT': 'application/mercurial'
32 36 }
33 37 assert vcs.is_hg(environ)
34 38
35 39
36 40 def test_is_hg_no_cmd():
37 41 environ = {
38 'PATH_INFO': '/rhodecode-dev',
42 'PATH_INFO': svn_repo_path,
39 43 'QUERY_STRING': '',
40 44 'HTTP_ACCEPT': 'application/mercurial'
41 45 }
42 46 assert not vcs.is_hg(environ)
43 47
44 48
45 49 def test_is_hg_empty_cmd():
46 50 environ = {
47 'PATH_INFO': '/rhodecode-dev',
51 'PATH_INFO': svn_repo_path,
48 52 'QUERY_STRING': 'cmd=',
49 53 'HTTP_ACCEPT': 'application/mercurial'
50 54 }
51 55 assert not vcs.is_hg(environ)
52 56
53 57
54 58 def test_is_svn_returns_true_if_subversion_is_in_a_dav_header():
55 59 environ = {
56 'PATH_INFO': '/rhodecode-dev',
60 'PATH_INFO': svn_repo_path,
57 61 'HTTP_DAV': 'http://subversion.tigris.org/xmlns/dav/svn/log-revprops'
58 62 }
59 63 assert vcs.is_svn(environ) is True
60 64
61 65
62 66 def test_is_svn_returns_false_if_subversion_is_not_in_a_dav_header():
63 67 environ = {
64 'PATH_INFO': '/rhodecode-dev',
68 'PATH_INFO': svn_repo_path,
65 69 'HTTP_DAV': 'http://stuff.tigris.org/xmlns/dav/svn/log-revprops'
66 70 }
67 71 assert vcs.is_svn(environ) is False
68 72
69 73
70 74 def test_is_svn_returns_false_if_no_dav_header():
71 75 environ = {
72 'PATH_INFO': '/rhodecode-dev',
76 'PATH_INFO': svn_repo_path,
73 77 }
74 78 assert vcs.is_svn(environ) is False
75 79
76 80
77 81 def test_is_svn_returns_true_if_magic_path_segment():
78 82 environ = {
79 83 'PATH_INFO': '/stub-repository/!svn/rev/4',
80 84 }
81 85 assert vcs.is_svn(environ)
82 86
83 87
84 88 def test_is_svn_allows_to_configure_the_magic_path(monkeypatch):
85 89 """
86 90 This is intended as a fallback in case someone has configured his
87 91 Subversion server with a different magic path segment.
88 92 """
89 93 monkeypatch.setitem(
90 94 rhodecode.CONFIG, 'rhodecode_subversion_magic_path', '/!my-magic')
91 95 environ = {
92 96 'PATH_INFO': '/stub-repository/!my-magic/rev/4',
93 97 }
94 98 assert vcs.is_svn(environ)
95 99
96 100
97 101 class TestVCSMiddleware(object):
98 def test_get_handler_app_retuns_svn_app_when_proxy_enabled(self):
102 def test_get_handler_app_retuns_svn_app_when_proxy_enabled(self, app):
99 103 environ = {
100 'PATH_INFO': 'rhodecode-dev',
104 'PATH_INFO': SVN_REPO,
101 105 'HTTP_DAV': 'http://subversion.tigris.org/xmlns/dav/svn/log'
102 106 }
103 app = Mock()
104 config = Mock()
107 application = Mock()
108 config = {'appenlight': False}
105 109 registry = Mock()
106 110 middleware = vcs.VCSMiddleware(
107 app, config=config, appenlight_client=None, registry=registry)
108 snv_patch = patch('rhodecode.lib.middleware.vcs.SimpleSvn')
109 settings_patch = patch.dict(
110 rhodecode.CONFIG,
111 {'rhodecode_proxy_subversion_http_requests': True})
112 with snv_patch as svn_mock, settings_patch:
113 svn_mock.return_value = None
114 middleware._get_handler_app(environ)
111 application, config=config,
112 appenlight_client=None, registry=registry)
113 middleware.use_gzip = False
115 114
116 svn_mock.assert_called_once_with(app, config, registry)
115 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
116 mock_method.return_value = True
117 application = middleware._get_handler_app(environ)
118 assert isinstance(application, SimpleSvn)
119 assert isinstance(application._create_wsgi_app(
120 Mock(), Mock(), Mock()), SimpleSvnApp)
117 121
118 def test_get_handler_app_retuns_no_svn_app_when_proxy_disabled(self):
122 def test_get_handler_app_retuns_dummy_svn_app_when_proxy_disabled(self, app):
119 123 environ = {
120 'PATH_INFO': 'rhodecode-dev',
124 'PATH_INFO': SVN_REPO,
121 125 'HTTP_DAV': 'http://subversion.tigris.org/xmlns/dav/svn/log'
122 126 }
123 app = Mock()
124 config = Mock()
127 application = Mock()
128 config = {'appenlight': False}
125 129 registry = Mock()
126 130 middleware = vcs.VCSMiddleware(
127 app, config=config, appenlight_client=None, registry=registry)
128 snv_patch = patch('rhodecode.lib.middleware.vcs.SimpleSvn')
129 settings_patch = patch.dict(
130 rhodecode.CONFIG,
131 {'rhodecode_proxy_subversion_http_requests': False})
132 with snv_patch as svn_mock, settings_patch:
133 app = middleware._get_handler_app(environ)
131 application, config=config,
132 appenlight_client=None, registry=registry)
133 middleware.use_gzip = False
134 134
135 assert svn_mock.call_count == 0
136 assert app is None
135 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
136 mock_method.return_value = False
137 application = middleware._get_handler_app(environ)
138 assert isinstance(application, SimpleSvn)
139 assert isinstance(application._create_wsgi_app(
140 Mock(), Mock(), Mock()), DisabledSimpleSvnApp)
@@ -1,1038 +1,1029 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 mock
22 22 import pytest
23 23
24 24 from rhodecode.lib.utils2 import str2bool
25 25 from rhodecode.model.meta import Session
26 26 from rhodecode.model.settings import VcsSettingsModel, UiSetting
27 27
28 28
29 29 HOOKS_FORM_DATA = {
30 30 'hooks_changegroup_repo_size': True,
31 31 'hooks_changegroup_push_logger': True,
32 32 'hooks_outgoing_pull_logger': True
33 33 }
34 34
35 35 SVN_FORM_DATA = {
36 36 'new_svn_branch': 'test-branch',
37 37 'new_svn_tag': 'test-tag'
38 38 }
39 39
40 40 GENERAL_FORM_DATA = {
41 41 'rhodecode_pr_merge_enabled': True,
42 42 'rhodecode_use_outdated_comments': True,
43 43 'rhodecode_hg_use_rebase_for_merging': True,
44 44 }
45 45
46 46
47 47 class TestInheritGlobalSettingsProperty(object):
48 48 def test_get_raises_exception_when_repository_not_specified(self):
49 49 model = VcsSettingsModel()
50 50 with pytest.raises(Exception) as exc_info:
51 51 model.inherit_global_settings
52 52 assert exc_info.value.message == 'Repository is not specified'
53 53
54 54 def test_true_is_returned_when_value_is_not_found(self, repo_stub):
55 55 model = VcsSettingsModel(repo=repo_stub.repo_name)
56 56 assert model.inherit_global_settings is True
57 57
58 58 def test_value_is_returned(self, repo_stub, settings_util):
59 59 model = VcsSettingsModel(repo=repo_stub.repo_name)
60 60 settings_util.create_repo_rhodecode_setting(
61 61 repo_stub, VcsSettingsModel.INHERIT_SETTINGS, False, 'bool')
62 62 assert model.inherit_global_settings is False
63 63
64 64 def test_value_is_set(self, repo_stub):
65 65 model = VcsSettingsModel(repo=repo_stub.repo_name)
66 66 model.inherit_global_settings = False
67 67 setting = model.repo_settings.get_setting_by_name(
68 68 VcsSettingsModel.INHERIT_SETTINGS)
69 69 try:
70 70 assert setting.app_settings_type == 'bool'
71 71 assert setting.app_settings_value is False
72 72 finally:
73 73 Session().delete(setting)
74 74 Session().commit()
75 75
76 76 def test_set_raises_exception_when_repository_not_specified(self):
77 77 model = VcsSettingsModel()
78 78 with pytest.raises(Exception) as exc_info:
79 79 model.inherit_global_settings = False
80 80 assert exc_info.value.message == 'Repository is not specified'
81 81
82 82
83 83 class TestVcsSettingsModel(object):
84 84 def test_global_svn_branch_patterns(self):
85 85 model = VcsSettingsModel()
86 86 expected_result = {'test': 'test'}
87 87 with mock.patch.object(model, 'global_settings') as settings_mock:
88 88 get_settings = settings_mock.get_ui_by_section
89 89 get_settings.return_value = expected_result
90 90 settings_mock.return_value = expected_result
91 91 result = model.get_global_svn_branch_patterns()
92 92
93 93 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
94 94 assert expected_result == result
95 95
96 96 def test_repo_svn_branch_patterns(self):
97 97 model = VcsSettingsModel()
98 98 expected_result = {'test': 'test'}
99 99 with mock.patch.object(model, 'repo_settings') as settings_mock:
100 100 get_settings = settings_mock.get_ui_by_section
101 101 get_settings.return_value = expected_result
102 102 settings_mock.return_value = expected_result
103 103 result = model.get_repo_svn_branch_patterns()
104 104
105 105 get_settings.assert_called_once_with(model.SVN_BRANCH_SECTION)
106 106 assert expected_result == result
107 107
108 108 def test_repo_svn_branch_patterns_raises_exception_when_repo_is_not_set(
109 109 self):
110 110 model = VcsSettingsModel()
111 111 with pytest.raises(Exception) as exc_info:
112 112 model.get_repo_svn_branch_patterns()
113 113 assert exc_info.value.message == 'Repository is not specified'
114 114
115 115 def test_global_svn_tag_patterns(self):
116 116 model = VcsSettingsModel()
117 117 expected_result = {'test': 'test'}
118 118 with mock.patch.object(model, 'global_settings') as settings_mock:
119 119 get_settings = settings_mock.get_ui_by_section
120 120 get_settings.return_value = expected_result
121 121 settings_mock.return_value = expected_result
122 122 result = model.get_global_svn_tag_patterns()
123 123
124 124 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
125 125 assert expected_result == result
126 126
127 127 def test_repo_svn_tag_patterns(self):
128 128 model = VcsSettingsModel()
129 129 expected_result = {'test': 'test'}
130 130 with mock.patch.object(model, 'repo_settings') as settings_mock:
131 131 get_settings = settings_mock.get_ui_by_section
132 132 get_settings.return_value = expected_result
133 133 settings_mock.return_value = expected_result
134 134 result = model.get_repo_svn_tag_patterns()
135 135
136 136 get_settings.assert_called_once_with(model.SVN_TAG_SECTION)
137 137 assert expected_result == result
138 138
139 139 def test_repo_svn_tag_patterns_raises_exception_when_repo_is_not_set(self):
140 140 model = VcsSettingsModel()
141 141 with pytest.raises(Exception) as exc_info:
142 142 model.get_repo_svn_tag_patterns()
143 143 assert exc_info.value.message == 'Repository is not specified'
144 144
145 145 def test_get_global_settings(self):
146 146 expected_result = {'test': 'test'}
147 147 model = VcsSettingsModel()
148 148 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
149 149 collect_mock.return_value = expected_result
150 150 result = model.get_global_settings()
151 151
152 152 collect_mock.assert_called_once_with(global_=True)
153 153 assert result == expected_result
154 154
155 155 def test_get_repo_settings(self, repo_stub):
156 156 model = VcsSettingsModel(repo=repo_stub.repo_name)
157 157 expected_result = {'test': 'test'}
158 158 with mock.patch.object(model, '_collect_all_settings') as collect_mock:
159 159 collect_mock.return_value = expected_result
160 160 result = model.get_repo_settings()
161 161
162 162 collect_mock.assert_called_once_with(global_=False)
163 163 assert result == expected_result
164 164
165 165 @pytest.mark.parametrize('settings, global_', [
166 166 ('global_settings', True),
167 167 ('repo_settings', False)
168 168 ])
169 169 def test_collect_all_settings(self, settings, global_):
170 170 model = VcsSettingsModel()
171 171 result_mock = self._mock_result()
172 172
173 173 settings_patch = mock.patch.object(model, settings)
174 174 with settings_patch as settings_mock:
175 175 settings_mock.get_ui_by_section_and_key.return_value = result_mock
176 176 settings_mock.get_setting_by_name.return_value = result_mock
177 177 result = model._collect_all_settings(global_=global_)
178 178
179 179 ui_settings = model.HG_SETTINGS + model.HOOKS_SETTINGS
180 180 self._assert_get_settings_calls(
181 181 settings_mock, ui_settings, model.GENERAL_SETTINGS)
182 182 self._assert_collect_all_settings_result(
183 183 ui_settings, model.GENERAL_SETTINGS, result)
184 184
185 185 @pytest.mark.parametrize('settings, global_', [
186 186 ('global_settings', True),
187 187 ('repo_settings', False)
188 188 ])
189 189 def test_collect_all_settings_without_empty_value(self, settings, global_):
190 190 model = VcsSettingsModel()
191 191
192 192 settings_patch = mock.patch.object(model, settings)
193 193 with settings_patch as settings_mock:
194 194 settings_mock.get_ui_by_section_and_key.return_value = None
195 195 settings_mock.get_setting_by_name.return_value = None
196 196 result = model._collect_all_settings(global_=global_)
197 197
198 198 assert result == {}
199 199
200 200 def _mock_result(self):
201 201 result_mock = mock.Mock()
202 202 result_mock.ui_value = 'ui_value'
203 203 result_mock.ui_active = True
204 204 result_mock.app_settings_value = 'setting_value'
205 205 return result_mock
206 206
207 207 def _assert_get_settings_calls(
208 208 self, settings_mock, ui_settings, general_settings):
209 209 assert (
210 210 settings_mock.get_ui_by_section_and_key.call_count ==
211 211 len(ui_settings))
212 212 assert (
213 213 settings_mock.get_setting_by_name.call_count ==
214 214 len(general_settings))
215 215
216 216 for section, key in ui_settings:
217 217 expected_call = mock.call(section, key)
218 218 assert (
219 219 expected_call in
220 220 settings_mock.get_ui_by_section_and_key.call_args_list)
221 221
222 222 for name in general_settings:
223 223 expected_call = mock.call(name)
224 224 assert (
225 225 expected_call in
226 226 settings_mock.get_setting_by_name.call_args_list)
227 227
228 228 def _assert_collect_all_settings_result(
229 229 self, ui_settings, general_settings, result):
230 230 expected_result = {}
231 231 for section, key in ui_settings:
232 232 key = '{}_{}'.format(section, key.replace('.', '_'))
233 233 value = True if section in ('extensions', 'hooks') else 'ui_value'
234 234 expected_result[key] = value
235 235
236 236 for name in general_settings:
237 237 key = 'rhodecode_' + name
238 238 expected_result[key] = 'setting_value'
239 239
240 240 assert expected_result == result
241 241
242 242
243 243 class TestCreateOrUpdateRepoHookSettings(object):
244 244 def test_create_when_no_repo_object_found(self, repo_stub):
245 245 model = VcsSettingsModel(repo=repo_stub.repo_name)
246 246
247 247 self._create_settings(model, HOOKS_FORM_DATA)
248 248
249 249 cleanup = []
250 250 try:
251 251 for section, key in model.HOOKS_SETTINGS:
252 252 ui = model.repo_settings.get_ui_by_section_and_key(
253 253 section, key)
254 254 assert ui.ui_active is True
255 255 cleanup.append(ui)
256 256 finally:
257 257 for ui in cleanup:
258 258 Session().delete(ui)
259 259 Session().commit()
260 260
261 261 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
262 262 model = VcsSettingsModel(repo=repo_stub.repo_name)
263 263
264 264 deleted_key = 'hooks_changegroup_repo_size'
265 265 data = HOOKS_FORM_DATA.copy()
266 266 data.pop(deleted_key)
267 267
268 268 with pytest.raises(ValueError) as exc_info:
269 269 model.create_or_update_repo_hook_settings(data)
270 270 assert (
271 271 exc_info.value.message ==
272 272 'The given data does not contain {} key'.format(deleted_key))
273 273
274 274 def test_update_when_repo_object_found(self, repo_stub, settings_util):
275 275 model = VcsSettingsModel(repo=repo_stub.repo_name)
276 276 for section, key in model.HOOKS_SETTINGS:
277 277 settings_util.create_repo_rhodecode_ui(
278 278 repo_stub, section, None, key=key, active=False)
279 279 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
280 280 for section, key in model.HOOKS_SETTINGS:
281 281 ui = model.repo_settings.get_ui_by_section_and_key(section, key)
282 282 assert ui.ui_active is True
283 283
284 284 def _create_settings(self, model, data):
285 285 global_patch = mock.patch.object(model, 'global_settings')
286 286 global_setting = mock.Mock()
287 287 global_setting.ui_value = 'Test value'
288 288 with global_patch as global_mock:
289 289 global_mock.get_ui_by_section_and_key.return_value = global_setting
290 290 model.create_or_update_repo_hook_settings(HOOKS_FORM_DATA)
291 291
292 292
293 293 class TestUpdateGlobalHookSettings(object):
294 294 def test_update_raises_exception_when_data_incomplete(self):
295 295 model = VcsSettingsModel()
296 296
297 297 deleted_key = 'hooks_changegroup_repo_size'
298 298 data = HOOKS_FORM_DATA.copy()
299 299 data.pop(deleted_key)
300 300
301 301 with pytest.raises(ValueError) as exc_info:
302 302 model.update_global_hook_settings(data)
303 303 assert (
304 304 exc_info.value.message ==
305 305 'The given data does not contain {} key'.format(deleted_key))
306 306
307 307 def test_update_global_hook_settings(self, settings_util):
308 308 model = VcsSettingsModel()
309 309 setting_mock = mock.MagicMock()
310 310 setting_mock.ui_active = False
311 311 get_settings_patcher = mock.patch.object(
312 312 model.global_settings, 'get_ui_by_section_and_key',
313 313 return_value=setting_mock)
314 314 session_patcher = mock.patch('rhodecode.model.settings.Session')
315 315 with get_settings_patcher as get_settings_mock, session_patcher:
316 316 model.update_global_hook_settings(HOOKS_FORM_DATA)
317 317 assert setting_mock.ui_active is True
318 318 assert get_settings_mock.call_count == 3
319 319
320 320
321 321 class TestCreateOrUpdateRepoGeneralSettings(object):
322 322 def test_calls_create_or_update_general_settings(self, repo_stub):
323 323 model = VcsSettingsModel(repo=repo_stub.repo_name)
324 324 create_patch = mock.patch.object(
325 325 model, '_create_or_update_general_settings')
326 326 with create_patch as create_mock:
327 327 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
328 328 create_mock.assert_called_once_with(
329 329 model.repo_settings, GENERAL_FORM_DATA)
330 330
331 331 def test_raises_exception_when_repository_is_not_specified(self):
332 332 model = VcsSettingsModel()
333 333 with pytest.raises(Exception) as exc_info:
334 334 model.create_or_update_repo_pr_settings(GENERAL_FORM_DATA)
335 335 assert exc_info.value.message == 'Repository is not specified'
336 336
337 337
338 338 class TestCreateOrUpdatGlobalGeneralSettings(object):
339 339 def test_calls_create_or_update_general_settings(self):
340 340 model = VcsSettingsModel()
341 341 create_patch = mock.patch.object(
342 342 model, '_create_or_update_general_settings')
343 343 with create_patch as create_mock:
344 344 model.create_or_update_global_pr_settings(GENERAL_FORM_DATA)
345 345 create_mock.assert_called_once_with(
346 346 model.global_settings, GENERAL_FORM_DATA)
347 347
348 348
349 349 class TestCreateOrUpdateGeneralSettings(object):
350 350 def test_create_when_no_repo_settings_found(self, repo_stub):
351 351 model = VcsSettingsModel(repo=repo_stub.repo_name)
352 352 model._create_or_update_general_settings(
353 353 model.repo_settings, GENERAL_FORM_DATA)
354 354
355 355 cleanup = []
356 356 try:
357 357 for name in model.GENERAL_SETTINGS:
358 358 setting = model.repo_settings.get_setting_by_name(name)
359 359 assert setting.app_settings_value is True
360 360 cleanup.append(setting)
361 361 finally:
362 362 for setting in cleanup:
363 363 Session().delete(setting)
364 364 Session().commit()
365 365
366 366 def test_create_raises_exception_when_data_incomplete(self, repo_stub):
367 367 model = VcsSettingsModel(repo=repo_stub.repo_name)
368 368
369 369 deleted_key = 'rhodecode_pr_merge_enabled'
370 370 data = GENERAL_FORM_DATA.copy()
371 371 data.pop(deleted_key)
372 372
373 373 with pytest.raises(ValueError) as exc_info:
374 374 model._create_or_update_general_settings(model.repo_settings, data)
375 375 assert (
376 376 exc_info.value.message ==
377 377 'The given data does not contain {} key'.format(deleted_key))
378 378
379 379 def test_update_when_repo_setting_found(self, repo_stub, settings_util):
380 380 model = VcsSettingsModel(repo=repo_stub.repo_name)
381 381 for name in model.GENERAL_SETTINGS:
382 382 settings_util.create_repo_rhodecode_setting(
383 383 repo_stub, name, False, 'bool')
384 384
385 385 model._create_or_update_general_settings(
386 386 model.repo_settings, GENERAL_FORM_DATA)
387 387
388 388 for name in model.GENERAL_SETTINGS:
389 389 setting = model.repo_settings.get_setting_by_name(name)
390 390 assert setting.app_settings_value is True
391 391
392 392
393 393 class TestCreateRepoSvnSettings(object):
394 394 def test_calls_create_svn_settings(self, repo_stub):
395 395 model = VcsSettingsModel(repo=repo_stub.repo_name)
396 396 with mock.patch.object(model, '_create_svn_settings') as create_mock:
397 397 model.create_repo_svn_settings(SVN_FORM_DATA)
398 398 create_mock.assert_called_once_with(model.repo_settings, SVN_FORM_DATA)
399 399
400 400 def test_raises_exception_when_repository_is_not_specified(self):
401 401 model = VcsSettingsModel()
402 402 with pytest.raises(Exception) as exc_info:
403 403 model.create_repo_svn_settings(SVN_FORM_DATA)
404 404 assert exc_info.value.message == 'Repository is not specified'
405 405
406 406
407 class TestCreateGlobalSvnSettings(object):
408 def test_calls_create_svn_settings(self):
409 model = VcsSettingsModel()
410 with mock.patch.object(model, '_create_svn_settings') as create_mock:
411 model.create_global_svn_settings(SVN_FORM_DATA)
412 create_mock.assert_called_once_with(
413 model.global_settings, SVN_FORM_DATA)
414
415
416 407 class TestCreateSvnSettings(object):
417 408 def test_create(self, repo_stub):
418 409 model = VcsSettingsModel(repo=repo_stub.repo_name)
419 410 model._create_svn_settings(model.repo_settings, SVN_FORM_DATA)
420 411 Session().commit()
421 412
422 413 branch_ui = model.repo_settings.get_ui_by_section(
423 414 model.SVN_BRANCH_SECTION)
424 415 tag_ui = model.repo_settings.get_ui_by_section(
425 416 model.SVN_TAG_SECTION)
426 417
427 418 try:
428 419 assert len(branch_ui) == 1
429 420 assert len(tag_ui) == 1
430 421 finally:
431 422 Session().delete(branch_ui[0])
432 423 Session().delete(tag_ui[0])
433 424 Session().commit()
434 425
435 426 def test_create_tag(self, repo_stub):
436 427 model = VcsSettingsModel(repo=repo_stub.repo_name)
437 428 data = SVN_FORM_DATA.copy()
438 429 data.pop('new_svn_branch')
439 430 model._create_svn_settings(model.repo_settings, data)
440 431 Session().commit()
441 432
442 433 branch_ui = model.repo_settings.get_ui_by_section(
443 434 model.SVN_BRANCH_SECTION)
444 435 tag_ui = model.repo_settings.get_ui_by_section(
445 436 model.SVN_TAG_SECTION)
446 437
447 438 try:
448 439 assert len(branch_ui) == 0
449 440 assert len(tag_ui) == 1
450 441 finally:
451 442 Session().delete(tag_ui[0])
452 443 Session().commit()
453 444
454 445 def test_create_nothing_when_no_svn_settings_specified(self, repo_stub):
455 446 model = VcsSettingsModel(repo=repo_stub.repo_name)
456 447 model._create_svn_settings(model.repo_settings, {})
457 448 Session().commit()
458 449
459 450 branch_ui = model.repo_settings.get_ui_by_section(
460 451 model.SVN_BRANCH_SECTION)
461 452 tag_ui = model.repo_settings.get_ui_by_section(
462 453 model.SVN_TAG_SECTION)
463 454
464 455 assert len(branch_ui) == 0
465 456 assert len(tag_ui) == 0
466 457
467 458 def test_create_nothing_when_empty_settings_specified(self, repo_stub):
468 459 model = VcsSettingsModel(repo=repo_stub.repo_name)
469 460 data = {
470 461 'new_svn_branch': '',
471 462 'new_svn_tag': ''
472 463 }
473 464 model._create_svn_settings(model.repo_settings, data)
474 465 Session().commit()
475 466
476 467 branch_ui = model.repo_settings.get_ui_by_section(
477 468 model.SVN_BRANCH_SECTION)
478 469 tag_ui = model.repo_settings.get_ui_by_section(
479 470 model.SVN_TAG_SECTION)
480 471
481 472 assert len(branch_ui) == 0
482 473 assert len(tag_ui) == 0
483 474
484 475
485 476 class TestCreateOrUpdateUi(object):
486 477 def test_create(self, repo_stub):
487 478 model = VcsSettingsModel(repo=repo_stub.repo_name)
488 479 model._create_or_update_ui(
489 480 model.repo_settings, 'test-section', 'test-key', active=False,
490 481 value='False')
491 482 Session().commit()
492 483
493 484 created_ui = model.repo_settings.get_ui_by_section_and_key(
494 485 'test-section', 'test-key')
495 486
496 487 try:
497 488 assert created_ui.ui_active is False
498 489 assert str2bool(created_ui.ui_value) is False
499 490 finally:
500 491 Session().delete(created_ui)
501 492 Session().commit()
502 493
503 494 def test_update(self, repo_stub, settings_util):
504 495 model = VcsSettingsModel(repo=repo_stub.repo_name)
505 496
506 497 largefiles, phases = model.HG_SETTINGS
507 498 section = 'test-section'
508 499 key = 'test-key'
509 500 settings_util.create_repo_rhodecode_ui(
510 501 repo_stub, section, 'True', key=key, active=True)
511 502
512 503 model._create_or_update_ui(
513 504 model.repo_settings, section, key, active=False, value='False')
514 505 Session().commit()
515 506
516 507 created_ui = model.repo_settings.get_ui_by_section_and_key(
517 508 section, key)
518 509 assert created_ui.ui_active is False
519 510 assert str2bool(created_ui.ui_value) is False
520 511
521 512
522 513 class TestCreateOrUpdateRepoHgSettings(object):
523 514 FORM_DATA = {
524 515 'extensions_largefiles': False,
525 516 'phases_publish': False
526 517 }
527 518
528 519 def test_creates_repo_hg_settings_when_data_is_correct(self, repo_stub):
529 520 model = VcsSettingsModel(repo=repo_stub.repo_name)
530 521 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
531 522 model.create_or_update_repo_hg_settings(self.FORM_DATA)
532 523 expected_calls = [
533 524 mock.call(model.repo_settings, 'extensions', 'largefiles',
534 525 active=False, value=''),
535 526 mock.call(model.repo_settings, 'phases', 'publish', value='False'),
536 527 ]
537 528 assert expected_calls == create_mock.call_args_list
538 529
539 530 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
540 531 def test_key_is_not_found(self, repo_stub, field_to_remove):
541 532 model = VcsSettingsModel(repo=repo_stub.repo_name)
542 533 data = self.FORM_DATA.copy()
543 534 data.pop(field_to_remove)
544 535 with pytest.raises(ValueError) as exc_info:
545 536 model.create_or_update_repo_hg_settings(data)
546 537 expected_message = 'The given data does not contain {} key'.format(
547 538 field_to_remove)
548 539 assert exc_info.value.message == expected_message
549 540
550 541 def test_create_raises_exception_when_repository_not_specified(self):
551 542 model = VcsSettingsModel()
552 543 with pytest.raises(Exception) as exc_info:
553 544 model.create_or_update_repo_hg_settings(self.FORM_DATA)
554 545 assert exc_info.value.message == 'Repository is not specified'
555 546
556 547
557 548 class TestUpdateGlobalSslSetting(object):
558 549 def test_updates_global_hg_settings(self):
559 550 model = VcsSettingsModel()
560 551 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
561 552 model.update_global_ssl_setting('False')
562 553 create_mock.assert_called_once_with(
563 554 model.global_settings, 'web', 'push_ssl', value='False')
564 555
565 556
566 557 class TestUpdateGlobalPathSetting(object):
567 558 def test_updates_global_path_settings(self):
568 559 model = VcsSettingsModel()
569 560 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
570 561 model.update_global_path_setting('False')
571 562 create_mock.assert_called_once_with(
572 563 model.global_settings, 'paths', '/', value='False')
573 564
574 565
575 566 class TestCreateOrUpdateGlobalHgSettings(object):
576 567 FORM_DATA = {
577 568 'extensions_largefiles': False,
578 569 'phases_publish': False,
579 570 'extensions_hgsubversion': False
580 571 }
581 572
582 573 def test_creates_repo_hg_settings_when_data_is_correct(self):
583 574 model = VcsSettingsModel()
584 575 with mock.patch.object(model, '_create_or_update_ui') as create_mock:
585 576 model.create_or_update_global_hg_settings(self.FORM_DATA)
586 577 expected_calls = [
587 578 mock.call(model.global_settings, 'extensions', 'largefiles',
588 579 active=False, value=''),
589 580 mock.call(model.global_settings, 'phases', 'publish',
590 581 value='False'),
591 582 mock.call(model.global_settings, 'extensions', 'hgsubversion',
592 583 active=False)
593 584 ]
594 585 assert expected_calls == create_mock.call_args_list
595 586
596 587 @pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
597 588 def test_key_is_not_found(self, repo_stub, field_to_remove):
598 589 model = VcsSettingsModel(repo=repo_stub.repo_name)
599 590 data = self.FORM_DATA.copy()
600 591 data.pop(field_to_remove)
601 592 with pytest.raises(Exception) as exc_info:
602 593 model.create_or_update_global_hg_settings(data)
603 594 expected_message = 'The given data does not contain {} key'.format(
604 595 field_to_remove)
605 596 assert exc_info.value.message == expected_message
606 597
607 598
608 599 class TestDeleteRepoSvnPattern(object):
609 600 def test_success_when_repo_is_set(self, backend_svn):
610 601 repo_name = backend_svn.repo_name
611 602 model = VcsSettingsModel(repo=repo_name)
612 603 delete_ui_patch = mock.patch.object(model.repo_settings, 'delete_ui')
613 604 with delete_ui_patch as delete_ui_mock:
614 605 model.delete_repo_svn_pattern(123)
615 606 delete_ui_mock.assert_called_once_with(123)
616 607
617 608 def test_raises_exception_when_repository_is_not_specified(self):
618 609 model = VcsSettingsModel()
619 610 with pytest.raises(Exception) as exc_info:
620 611 model.delete_repo_svn_pattern(123)
621 612 assert exc_info.value.message == 'Repository is not specified'
622 613
623 614
624 615 class TestDeleteGlobalSvnPattern(object):
625 616 def test_delete_global_svn_pattern_calls_delete_ui(self):
626 617 model = VcsSettingsModel()
627 618 delete_ui_patch = mock.patch.object(model.global_settings, 'delete_ui')
628 619 with delete_ui_patch as delete_ui_mock:
629 620 model.delete_global_svn_pattern(123)
630 621 delete_ui_mock.assert_called_once_with(123)
631 622
632 623
633 624 class TestFilterUiSettings(object):
634 625 def test_settings_are_filtered(self):
635 626 model = VcsSettingsModel()
636 627 repo_settings = [
637 628 UiSetting('extensions', 'largefiles', '', True),
638 629 UiSetting('phases', 'publish', 'True', True),
639 630 UiSetting('hooks', 'changegroup.repo_size', 'hook', True),
640 631 UiSetting('hooks', 'changegroup.push_logger', 'hook', True),
641 632 UiSetting('hooks', 'outgoing.pull_logger', 'hook', True),
642 633 UiSetting(
643 634 'vcs_svn_branch', '84223c972204fa545ca1b22dac7bef5b68d7442d',
644 635 'test_branch', True),
645 636 UiSetting(
646 637 'vcs_svn_tag', '84229c972204fa545ca1b22dac7bef5b68d7442d',
647 638 'test_tag', True),
648 639 ]
649 640 non_repo_settings = [
650 641 UiSetting('test', 'outgoing.pull_logger', 'hook', True),
651 642 UiSetting('hooks', 'test2', 'hook', True),
652 643 UiSetting(
653 644 'vcs_svn_repo', '84229c972204fa545ca1b22dac7bef5b68d7442d',
654 645 'test_tag', True),
655 646 ]
656 647 settings = repo_settings + non_repo_settings
657 648 filtered_settings = model._filter_ui_settings(settings)
658 649 assert sorted(filtered_settings) == sorted(repo_settings)
659 650
660 651
661 652 class TestFilterGeneralSettings(object):
662 653 def test_settings_are_filtered(self):
663 654 model = VcsSettingsModel()
664 655 settings = {
665 656 'rhodecode_abcde': 'value1',
666 657 'rhodecode_vwxyz': 'value2',
667 658 }
668 659 general_settings = {
669 660 'rhodecode_{}'.format(key): 'value'
670 661 for key in VcsSettingsModel.GENERAL_SETTINGS
671 662 }
672 663 settings.update(general_settings)
673 664
674 665 filtered_settings = model._filter_general_settings(general_settings)
675 666 assert sorted(filtered_settings) == sorted(general_settings)
676 667
677 668
678 669 class TestGetRepoUiSettings(object):
679 670 def test_global_uis_are_returned_when_no_repo_uis_found(
680 671 self, repo_stub):
681 672 model = VcsSettingsModel(repo=repo_stub.repo_name)
682 673 result = model.get_repo_ui_settings()
683 674 svn_sections = (
684 675 VcsSettingsModel.SVN_TAG_SECTION,
685 676 VcsSettingsModel.SVN_BRANCH_SECTION)
686 677 expected_result = [
687 678 s for s in model.global_settings.get_ui()
688 679 if s.section not in svn_sections]
689 680 assert sorted(result) == sorted(expected_result)
690 681
691 682 def test_repo_uis_are_overriding_global_uis(
692 683 self, repo_stub, settings_util):
693 684 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
694 685 settings_util.create_repo_rhodecode_ui(
695 686 repo_stub, section, 'repo', key=key, active=False)
696 687 model = VcsSettingsModel(repo=repo_stub.repo_name)
697 688 result = model.get_repo_ui_settings()
698 689 for setting in result:
699 690 locator = (setting.section, setting.key)
700 691 if locator in VcsSettingsModel.HOOKS_SETTINGS:
701 692 assert setting.value == 'repo'
702 693
703 694 assert setting.active is False
704 695
705 696 def test_global_svn_patterns_are_not_in_list(
706 697 self, repo_stub, settings_util):
707 698 svn_sections = (
708 699 VcsSettingsModel.SVN_TAG_SECTION,
709 700 VcsSettingsModel.SVN_BRANCH_SECTION)
710 701 for section in svn_sections:
711 702 settings_util.create_rhodecode_ui(
712 703 section, 'repo', key='deadbeef' + section, active=False)
713 704 model = VcsSettingsModel(repo=repo_stub.repo_name)
714 705 result = model.get_repo_ui_settings()
715 706 for setting in result:
716 707 assert setting.section not in svn_sections
717 708
718 709 def test_repo_uis_filtered_by_section_are_returned(
719 710 self, repo_stub, settings_util):
720 711 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
721 712 settings_util.create_repo_rhodecode_ui(
722 713 repo_stub, section, 'repo', key=key, active=False)
723 714 model = VcsSettingsModel(repo=repo_stub.repo_name)
724 715 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
725 716 result = model.get_repo_ui_settings(section=section)
726 717 for setting in result:
727 718 assert setting.section == section
728 719
729 720 def test_repo_uis_filtered_by_key_are_returned(
730 721 self, repo_stub, settings_util):
731 722 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
732 723 settings_util.create_repo_rhodecode_ui(
733 724 repo_stub, section, 'repo', key=key, active=False)
734 725 model = VcsSettingsModel(repo=repo_stub.repo_name)
735 726 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
736 727 result = model.get_repo_ui_settings(key=key)
737 728 for setting in result:
738 729 assert setting.key == key
739 730
740 731 def test_raises_exception_when_repository_is_not_specified(self):
741 732 model = VcsSettingsModel()
742 733 with pytest.raises(Exception) as exc_info:
743 734 model.get_repo_ui_settings()
744 735 assert exc_info.value.message == 'Repository is not specified'
745 736
746 737
747 738 class TestGetRepoGeneralSettings(object):
748 739 def test_global_settings_are_returned_when_no_repo_settings_found(
749 740 self, repo_stub):
750 741 model = VcsSettingsModel(repo=repo_stub.repo_name)
751 742 result = model.get_repo_general_settings()
752 743 expected_result = model.global_settings.get_all_settings()
753 744 assert sorted(result) == sorted(expected_result)
754 745
755 746 def test_repo_uis_are_overriding_global_uis(
756 747 self, repo_stub, settings_util):
757 748 for key in VcsSettingsModel.GENERAL_SETTINGS:
758 749 settings_util.create_repo_rhodecode_setting(
759 750 repo_stub, key, 'abcde', type_='unicode')
760 751 model = VcsSettingsModel(repo=repo_stub.repo_name)
761 752 result = model.get_repo_ui_settings()
762 753 for key in result:
763 754 if key in VcsSettingsModel.GENERAL_SETTINGS:
764 755 assert result[key] == 'abcde'
765 756
766 757 def test_raises_exception_when_repository_is_not_specified(self):
767 758 model = VcsSettingsModel()
768 759 with pytest.raises(Exception) as exc_info:
769 760 model.get_repo_general_settings()
770 761 assert exc_info.value.message == 'Repository is not specified'
771 762
772 763
773 764 class TestGetGlobalGeneralSettings(object):
774 765 def test_global_settings_are_returned(self, repo_stub):
775 766 model = VcsSettingsModel()
776 767 result = model.get_global_general_settings()
777 768 expected_result = model.global_settings.get_all_settings()
778 769 assert sorted(result) == sorted(expected_result)
779 770
780 771 def test_repo_uis_are_not_overriding_global_uis(
781 772 self, repo_stub, settings_util):
782 773 for key in VcsSettingsModel.GENERAL_SETTINGS:
783 774 settings_util.create_repo_rhodecode_setting(
784 775 repo_stub, key, 'abcde', type_='unicode')
785 776 model = VcsSettingsModel(repo=repo_stub.repo_name)
786 777 result = model.get_global_general_settings()
787 778 expected_result = model.global_settings.get_all_settings()
788 779 assert sorted(result) == sorted(expected_result)
789 780
790 781
791 782 class TestGetGlobalUiSettings(object):
792 783 def test_global_uis_are_returned(self, repo_stub):
793 784 model = VcsSettingsModel()
794 785 result = model.get_global_ui_settings()
795 786 expected_result = model.global_settings.get_ui()
796 787 assert sorted(result) == sorted(expected_result)
797 788
798 789 def test_repo_uis_are_not_overriding_global_uis(
799 790 self, repo_stub, settings_util):
800 791 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
801 792 settings_util.create_repo_rhodecode_ui(
802 793 repo_stub, section, 'repo', key=key, active=False)
803 794 model = VcsSettingsModel(repo=repo_stub.repo_name)
804 795 result = model.get_global_ui_settings()
805 796 expected_result = model.global_settings.get_ui()
806 797 assert sorted(result) == sorted(expected_result)
807 798
808 799 def test_ui_settings_filtered_by_section(
809 800 self, repo_stub, settings_util):
810 801 model = VcsSettingsModel(repo=repo_stub.repo_name)
811 802 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
812 803 result = model.get_global_ui_settings(section=section)
813 804 expected_result = model.global_settings.get_ui(section=section)
814 805 assert sorted(result) == sorted(expected_result)
815 806
816 807 def test_ui_settings_filtered_by_key(
817 808 self, repo_stub, settings_util):
818 809 model = VcsSettingsModel(repo=repo_stub.repo_name)
819 810 section, key = VcsSettingsModel.HOOKS_SETTINGS[0]
820 811 result = model.get_global_ui_settings(key=key)
821 812 expected_result = model.global_settings.get_ui(key=key)
822 813 assert sorted(result) == sorted(expected_result)
823 814
824 815
825 816 class TestGetGeneralSettings(object):
826 817 def test_global_settings_are_returned_when_inherited_is_true(
827 818 self, repo_stub, settings_util):
828 819 model = VcsSettingsModel(repo=repo_stub.repo_name)
829 820 model.inherit_global_settings = True
830 821 for key in VcsSettingsModel.GENERAL_SETTINGS:
831 822 settings_util.create_repo_rhodecode_setting(
832 823 repo_stub, key, 'abcde', type_='unicode')
833 824 result = model.get_general_settings()
834 825 expected_result = model.get_global_general_settings()
835 826 assert sorted(result) == sorted(expected_result)
836 827
837 828 def test_repo_settings_are_returned_when_inherited_is_false(
838 829 self, repo_stub, settings_util):
839 830 model = VcsSettingsModel(repo=repo_stub.repo_name)
840 831 model.inherit_global_settings = False
841 832 for key in VcsSettingsModel.GENERAL_SETTINGS:
842 833 settings_util.create_repo_rhodecode_setting(
843 834 repo_stub, key, 'abcde', type_='unicode')
844 835 result = model.get_general_settings()
845 836 expected_result = model.get_repo_general_settings()
846 837 assert sorted(result) == sorted(expected_result)
847 838
848 839 def test_global_settings_are_returned_when_no_repository_specified(self):
849 840 model = VcsSettingsModel()
850 841 result = model.get_general_settings()
851 842 expected_result = model.get_global_general_settings()
852 843 assert sorted(result) == sorted(expected_result)
853 844
854 845
855 846 class TestGetUiSettings(object):
856 847 def test_global_settings_are_returned_when_inherited_is_true(
857 848 self, repo_stub, settings_util):
858 849 model = VcsSettingsModel(repo=repo_stub.repo_name)
859 850 model.inherit_global_settings = True
860 851 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
861 852 settings_util.create_repo_rhodecode_ui(
862 853 repo_stub, section, 'repo', key=key, active=True)
863 854 result = model.get_ui_settings()
864 855 expected_result = model.get_global_ui_settings()
865 856 assert sorted(result) == sorted(expected_result)
866 857
867 858 def test_repo_settings_are_returned_when_inherited_is_false(
868 859 self, repo_stub, settings_util):
869 860 model = VcsSettingsModel(repo=repo_stub.repo_name)
870 861 model.inherit_global_settings = False
871 862 for section, key in VcsSettingsModel.HOOKS_SETTINGS:
872 863 settings_util.create_repo_rhodecode_ui(
873 864 repo_stub, section, 'repo', key=key, active=True)
874 865 result = model.get_ui_settings()
875 866 expected_result = model.get_repo_ui_settings()
876 867 assert sorted(result) == sorted(expected_result)
877 868
878 869 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
879 870 model = VcsSettingsModel(repo=repo_stub.repo_name)
880 871 model.inherit_global_settings = False
881 872 args = ('section', 'key')
882 873 with mock.patch.object(model, 'get_repo_ui_settings') as settings_mock:
883 874 model.get_ui_settings(*args)
884 875 settings_mock.assert_called_once_with(*args)
885 876
886 877 def test_global_settings_filtered_by_section_and_key(self):
887 878 model = VcsSettingsModel()
888 879 args = ('section', 'key')
889 880 with mock.patch.object(model, 'get_global_ui_settings') as (
890 881 settings_mock):
891 882 model.get_ui_settings(*args)
892 883 settings_mock.assert_called_once_with(*args)
893 884
894 885 def test_global_settings_are_returned_when_no_repository_specified(self):
895 886 model = VcsSettingsModel()
896 887 result = model.get_ui_settings()
897 888 expected_result = model.get_global_ui_settings()
898 889 assert sorted(result) == sorted(expected_result)
899 890
900 891
901 892 class TestGetSvnPatterns(object):
902 893 def test_repo_settings_filtered_by_section_and_key(self, repo_stub):
903 894 model = VcsSettingsModel(repo=repo_stub.repo_name)
904 895 args = ('section', )
905 896 with mock.patch.object(model, 'get_repo_ui_settings') as settings_mock:
906 897 model.get_svn_patterns(*args)
907 898 settings_mock.assert_called_once_with(*args)
908 899
909 900 def test_global_settings_filtered_by_section_and_key(self):
910 901 model = VcsSettingsModel()
911 902 args = ('section', )
912 903 with mock.patch.object(model, 'get_global_ui_settings') as (
913 904 settings_mock):
914 905 model.get_svn_patterns(*args)
915 906 settings_mock.assert_called_once_with(*args)
916 907
917 908
918 909 class TestGetReposLocation(object):
919 910 def test_returns_repos_location(self, repo_stub):
920 911 model = VcsSettingsModel()
921 912
922 913 result_mock = mock.Mock()
923 914 result_mock.ui_value = '/tmp'
924 915
925 916 with mock.patch.object(model, 'global_settings') as settings_mock:
926 917 settings_mock.get_ui_by_key.return_value = result_mock
927 918 result = model.get_repos_location()
928 919
929 920 settings_mock.get_ui_by_key.assert_called_once_with('/')
930 921 assert result == '/tmp'
931 922
932 923
933 924 class TestCreateOrUpdateRepoSettings(object):
934 925 FORM_DATA = {
935 926 'inherit_global_settings': False,
936 927 'hooks_changegroup_repo_size': False,
937 928 'hooks_changegroup_push_logger': False,
938 929 'hooks_outgoing_pull_logger': False,
939 930 'extensions_largefiles': False,
940 931 'phases_publish': 'false',
941 932 'rhodecode_pr_merge_enabled': False,
942 933 'rhodecode_use_outdated_comments': False,
943 934 'new_svn_branch': '',
944 935 'new_svn_tag': ''
945 936 }
946 937
947 938 def test_get_raises_exception_when_repository_not_specified(self):
948 939 model = VcsSettingsModel()
949 940 with pytest.raises(Exception) as exc_info:
950 941 model.create_or_update_repo_settings(data=self.FORM_DATA)
951 942 assert exc_info.value.message == 'Repository is not specified'
952 943
953 944 def test_only_svn_settings_are_updated_when_type_is_svn(self, backend_svn):
954 945 repo = backend_svn.create_repo()
955 946 model = VcsSettingsModel(repo=repo)
956 947 with self._patch_model(model) as mocks:
957 948 model.create_or_update_repo_settings(
958 949 data=self.FORM_DATA, inherit_global_settings=False)
959 950 mocks['create_repo_svn_settings'].assert_called_once_with(
960 951 self.FORM_DATA)
961 952 non_called_methods = (
962 953 'create_or_update_repo_hook_settings',
963 954 'create_or_update_repo_pr_settings',
964 955 'create_or_update_repo_hg_settings')
965 956 for method in non_called_methods:
966 957 assert mocks[method].call_count == 0
967 958
968 959 def test_non_svn_settings_are_updated_when_type_is_hg(self, backend_hg):
969 960 repo = backend_hg.create_repo()
970 961 model = VcsSettingsModel(repo=repo)
971 962 with self._patch_model(model) as mocks:
972 963 model.create_or_update_repo_settings(
973 964 data=self.FORM_DATA, inherit_global_settings=False)
974 965
975 966 assert mocks['create_repo_svn_settings'].call_count == 0
976 967 called_methods = (
977 968 'create_or_update_repo_hook_settings',
978 969 'create_or_update_repo_pr_settings',
979 970 'create_or_update_repo_hg_settings')
980 971 for method in called_methods:
981 972 mocks[method].assert_called_once_with(self.FORM_DATA)
982 973
983 974 def test_non_svn_and_hg_settings_are_updated_when_type_is_git(
984 975 self, backend_git):
985 976 repo = backend_git.create_repo()
986 977 model = VcsSettingsModel(repo=repo)
987 978 with self._patch_model(model) as mocks:
988 979 model.create_or_update_repo_settings(
989 980 data=self.FORM_DATA, inherit_global_settings=False)
990 981
991 982 assert mocks['create_repo_svn_settings'].call_count == 0
992 983 called_methods = (
993 984 'create_or_update_repo_hook_settings',
994 985 'create_or_update_repo_pr_settings')
995 986 non_called_methods = (
996 987 'create_repo_svn_settings',
997 988 'create_or_update_repo_hg_settings'
998 989 )
999 990 for method in called_methods:
1000 991 mocks[method].assert_called_once_with(self.FORM_DATA)
1001 992 for method in non_called_methods:
1002 993 assert mocks[method].call_count == 0
1003 994
1004 995 def test_no_methods_are_called_when_settings_are_inherited(
1005 996 self, backend):
1006 997 repo = backend.create_repo()
1007 998 model = VcsSettingsModel(repo=repo)
1008 999 with self._patch_model(model) as mocks:
1009 1000 model.create_or_update_repo_settings(
1010 1001 data=self.FORM_DATA, inherit_global_settings=True)
1011 1002 for method_name in mocks:
1012 1003 assert mocks[method_name].call_count == 0
1013 1004
1014 1005 def test_cache_is_marked_for_invalidation(self, repo_stub):
1015 1006 model = VcsSettingsModel(repo=repo_stub)
1016 1007 invalidation_patcher = mock.patch(
1017 1008 'rhodecode.controllers.admin.repos.ScmModel.mark_for_invalidation')
1018 1009 with invalidation_patcher as invalidation_mock:
1019 1010 model.create_or_update_repo_settings(
1020 1011 data=self.FORM_DATA, inherit_global_settings=True)
1021 1012 invalidation_mock.assert_called_once_with(
1022 1013 repo_stub.repo_name, delete=True)
1023 1014
1024 1015 def test_inherit_flag_is_saved(self, repo_stub):
1025 1016 model = VcsSettingsModel(repo=repo_stub)
1026 1017 model.inherit_global_settings = True
1027 1018 with self._patch_model(model):
1028 1019 model.create_or_update_repo_settings(
1029 1020 data=self.FORM_DATA, inherit_global_settings=False)
1030 1021 assert model.inherit_global_settings is False
1031 1022
1032 1023 def _patch_model(self, model):
1033 1024 return mock.patch.multiple(
1034 1025 model,
1035 1026 create_repo_svn_settings=mock.DEFAULT,
1036 1027 create_or_update_repo_hook_settings=mock.DEFAULT,
1037 1028 create_or_update_repo_pr_settings=mock.DEFAULT,
1038 1029 create_or_update_repo_hg_settings=mock.DEFAULT)
General Comments 0
You need to be logged in to leave comments. Login now