##// END OF EJS Templates
ui: allow selecting and specifing ssh clone url....
marcink -
r2497:9a1b0044 default
parent child Browse files
Show More

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

@@ -1,763 +1,764 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23 import collections
24 24
25 25 import datetime
26 26 import formencode
27 27 import formencode.htmlfill
28 28
29 29 import rhodecode
30 30 from pyramid.view import view_config
31 31 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34
35 35 from rhodecode.apps._base import BaseAppView
36 36 from rhodecode.apps.admin.navigation import navigation_list
37 37 from rhodecode.apps.svn_support.config_keys import generate_config
38 38 from rhodecode.lib import helpers as h
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
41 41 from rhodecode.lib.celerylib import tasks, run_task
42 42 from rhodecode.lib.utils import repo2db_mapper
43 43 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict
44 44 from rhodecode.lib.index import searcher_from_config
45 45
46 46 from rhodecode.model.db import RhodeCodeUi, Repository
47 47 from rhodecode.model.forms import (ApplicationSettingsForm,
48 48 ApplicationUiSettingsForm, ApplicationVisualisationForm,
49 49 LabsSettingsForm, IssueTrackerPatternsForm)
50 50 from rhodecode.model.repo_group import RepoGroupModel
51 51
52 52 from rhodecode.model.scm import ScmModel
53 53 from rhodecode.model.notification import EmailNotificationModel
54 54 from rhodecode.model.meta import Session
55 55 from rhodecode.model.settings import (
56 56 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
57 57 SettingsModel)
58 58
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class AdminSettingsView(BaseAppView):
64 64
65 65 def load_default_context(self):
66 66 c = self._get_local_tmpl_context()
67 67 c.labs_active = str2bool(
68 68 rhodecode.CONFIG.get('labs_settings_active', 'true'))
69 69 c.navlist = navigation_list(self.request)
70 70
71 71 return c
72 72
73 73 @classmethod
74 74 def _get_ui_settings(cls):
75 75 ret = RhodeCodeUi.query().all()
76 76
77 77 if not ret:
78 78 raise Exception('Could not get application ui settings !')
79 79 settings = {}
80 80 for each in ret:
81 81 k = each.ui_key
82 82 v = each.ui_value
83 83 if k == '/':
84 84 k = 'root_path'
85 85
86 86 if k in ['push_ssl', 'publish', 'enabled']:
87 87 v = str2bool(v)
88 88
89 89 if k.find('.') != -1:
90 90 k = k.replace('.', '_')
91 91
92 92 if each.ui_section in ['hooks', 'extensions']:
93 93 v = each.ui_active
94 94
95 95 settings[each.ui_section + '_' + k] = v
96 96 return settings
97 97
98 98 @classmethod
99 99 def _form_defaults(cls):
100 100 defaults = SettingsModel().get_all_settings()
101 101 defaults.update(cls._get_ui_settings())
102 102
103 103 defaults.update({
104 104 'new_svn_branch': '',
105 105 'new_svn_tag': '',
106 106 })
107 107 return defaults
108 108
109 109 @LoginRequired()
110 110 @HasPermissionAllDecorator('hg.admin')
111 111 @view_config(
112 112 route_name='admin_settings_vcs', request_method='GET',
113 113 renderer='rhodecode:templates/admin/settings/settings.mako')
114 114 def settings_vcs(self):
115 115 c = self.load_default_context()
116 116 c.active = 'vcs'
117 117 model = VcsSettingsModel()
118 118 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
119 119 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
120 120
121 121 settings = self.request.registry.settings
122 122 c.svn_proxy_generate_config = settings[generate_config]
123 123
124 124 defaults = self._form_defaults()
125 125
126 126 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
127 127
128 128 data = render('rhodecode:templates/admin/settings/settings.mako',
129 129 self._get_template_context(c), self.request)
130 130 html = formencode.htmlfill.render(
131 131 data,
132 132 defaults=defaults,
133 133 encoding="UTF-8",
134 134 force_defaults=False
135 135 )
136 136 return Response(html)
137 137
138 138 @LoginRequired()
139 139 @HasPermissionAllDecorator('hg.admin')
140 140 @CSRFRequired()
141 141 @view_config(
142 142 route_name='admin_settings_vcs_update', request_method='POST',
143 143 renderer='rhodecode:templates/admin/settings/settings.mako')
144 144 def settings_vcs_update(self):
145 145 _ = self.request.translate
146 146 c = self.load_default_context()
147 147 c.active = 'vcs'
148 148
149 149 model = VcsSettingsModel()
150 150 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
151 151 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
152 152
153 153 settings = self.request.registry.settings
154 154 c.svn_proxy_generate_config = settings[generate_config]
155 155
156 156 application_form = ApplicationUiSettingsForm(self.request.translate)()
157 157
158 158 try:
159 159 form_result = application_form.to_python(dict(self.request.POST))
160 160 except formencode.Invalid as errors:
161 161 h.flash(
162 162 _("Some form inputs contain invalid data."),
163 163 category='error')
164 164 data = render('rhodecode:templates/admin/settings/settings.mako',
165 165 self._get_template_context(c), self.request)
166 166 html = formencode.htmlfill.render(
167 167 data,
168 168 defaults=errors.value,
169 169 errors=errors.error_dict or {},
170 170 prefix_error=False,
171 171 encoding="UTF-8",
172 172 force_defaults=False
173 173 )
174 174 return Response(html)
175 175
176 176 try:
177 177 if c.visual.allow_repo_location_change:
178 178 model.update_global_path_setting(
179 179 form_result['paths_root_path'])
180 180
181 181 model.update_global_ssl_setting(form_result['web_push_ssl'])
182 182 model.update_global_hook_settings(form_result)
183 183
184 184 model.create_or_update_global_svn_settings(form_result)
185 185 model.create_or_update_global_hg_settings(form_result)
186 186 model.create_or_update_global_git_settings(form_result)
187 187 model.create_or_update_global_pr_settings(form_result)
188 188 except Exception:
189 189 log.exception("Exception while updating settings")
190 190 h.flash(_('Error occurred during updating '
191 191 'application settings'), category='error')
192 192 else:
193 193 Session().commit()
194 194 h.flash(_('Updated VCS settings'), category='success')
195 195 raise HTTPFound(h.route_path('admin_settings_vcs'))
196 196
197 197 data = render('rhodecode:templates/admin/settings/settings.mako',
198 198 self._get_template_context(c), self.request)
199 199 html = formencode.htmlfill.render(
200 200 data,
201 201 defaults=self._form_defaults(),
202 202 encoding="UTF-8",
203 203 force_defaults=False
204 204 )
205 205 return Response(html)
206 206
207 207 @LoginRequired()
208 208 @HasPermissionAllDecorator('hg.admin')
209 209 @CSRFRequired()
210 210 @view_config(
211 211 route_name='admin_settings_vcs_svn_pattern_delete', request_method='POST',
212 212 renderer='json_ext', xhr=True)
213 213 def settings_vcs_delete_svn_pattern(self):
214 214 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
215 215 model = VcsSettingsModel()
216 216 try:
217 217 model.delete_global_svn_pattern(delete_pattern_id)
218 218 except SettingNotFound:
219 219 log.exception(
220 220 'Failed to delete svn_pattern with id %s', delete_pattern_id)
221 221 raise HTTPNotFound()
222 222
223 223 Session().commit()
224 224 return True
225 225
226 226 @LoginRequired()
227 227 @HasPermissionAllDecorator('hg.admin')
228 228 @view_config(
229 229 route_name='admin_settings_mapping', request_method='GET',
230 230 renderer='rhodecode:templates/admin/settings/settings.mako')
231 231 def settings_mapping(self):
232 232 c = self.load_default_context()
233 233 c.active = 'mapping'
234 234
235 235 data = render('rhodecode:templates/admin/settings/settings.mako',
236 236 self._get_template_context(c), self.request)
237 237 html = formencode.htmlfill.render(
238 238 data,
239 239 defaults=self._form_defaults(),
240 240 encoding="UTF-8",
241 241 force_defaults=False
242 242 )
243 243 return Response(html)
244 244
245 245 @LoginRequired()
246 246 @HasPermissionAllDecorator('hg.admin')
247 247 @CSRFRequired()
248 248 @view_config(
249 249 route_name='admin_settings_mapping_update', request_method='POST',
250 250 renderer='rhodecode:templates/admin/settings/settings.mako')
251 251 def settings_mapping_update(self):
252 252 _ = self.request.translate
253 253 c = self.load_default_context()
254 254 c.active = 'mapping'
255 255 rm_obsolete = self.request.POST.get('destroy', False)
256 256 invalidate_cache = self.request.POST.get('invalidate', False)
257 257 log.debug(
258 258 'rescanning repo location with destroy obsolete=%s', rm_obsolete)
259 259
260 260 if invalidate_cache:
261 261 log.debug('invalidating all repositories cache')
262 262 for repo in Repository.get_all():
263 263 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
264 264
265 265 filesystem_repos = ScmModel().repo_scan()
266 266 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
267 267 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
268 268 h.flash(_('Repositories successfully '
269 269 'rescanned added: %s ; removed: %s') %
270 270 (_repr(added), _repr(removed)),
271 271 category='success')
272 272 raise HTTPFound(h.route_path('admin_settings_mapping'))
273 273
274 274 @LoginRequired()
275 275 @HasPermissionAllDecorator('hg.admin')
276 276 @view_config(
277 277 route_name='admin_settings', request_method='GET',
278 278 renderer='rhodecode:templates/admin/settings/settings.mako')
279 279 @view_config(
280 280 route_name='admin_settings_global', request_method='GET',
281 281 renderer='rhodecode:templates/admin/settings/settings.mako')
282 282 def settings_global(self):
283 283 c = self.load_default_context()
284 284 c.active = 'global'
285 285 c.personal_repo_group_default_pattern = RepoGroupModel()\
286 286 .get_personal_group_name_pattern()
287 287
288 288 data = render('rhodecode:templates/admin/settings/settings.mako',
289 289 self._get_template_context(c), self.request)
290 290 html = formencode.htmlfill.render(
291 291 data,
292 292 defaults=self._form_defaults(),
293 293 encoding="UTF-8",
294 294 force_defaults=False
295 295 )
296 296 return Response(html)
297 297
298 298 @LoginRequired()
299 299 @HasPermissionAllDecorator('hg.admin')
300 300 @CSRFRequired()
301 301 @view_config(
302 302 route_name='admin_settings_update', request_method='POST',
303 303 renderer='rhodecode:templates/admin/settings/settings.mako')
304 304 @view_config(
305 305 route_name='admin_settings_global_update', request_method='POST',
306 306 renderer='rhodecode:templates/admin/settings/settings.mako')
307 307 def settings_global_update(self):
308 308 _ = self.request.translate
309 309 c = self.load_default_context()
310 310 c.active = 'global'
311 311 c.personal_repo_group_default_pattern = RepoGroupModel()\
312 312 .get_personal_group_name_pattern()
313 313 application_form = ApplicationSettingsForm(self.request.translate)()
314 314 try:
315 315 form_result = application_form.to_python(dict(self.request.POST))
316 316 except formencode.Invalid as errors:
317 317 data = render('rhodecode:templates/admin/settings/settings.mako',
318 318 self._get_template_context(c), self.request)
319 319 html = formencode.htmlfill.render(
320 320 data,
321 321 defaults=errors.value,
322 322 errors=errors.error_dict or {},
323 323 prefix_error=False,
324 324 encoding="UTF-8",
325 325 force_defaults=False
326 326 )
327 327 return Response(html)
328 328
329 329 settings = [
330 330 ('title', 'rhodecode_title', 'unicode'),
331 331 ('realm', 'rhodecode_realm', 'unicode'),
332 332 ('pre_code', 'rhodecode_pre_code', 'unicode'),
333 333 ('post_code', 'rhodecode_post_code', 'unicode'),
334 334 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
335 335 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
336 336 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
337 337 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
338 338 ]
339 339 try:
340 340 for setting, form_key, type_ in settings:
341 341 sett = SettingsModel().create_or_update_setting(
342 342 setting, form_result[form_key], type_)
343 343 Session().add(sett)
344 344
345 345 Session().commit()
346 346 SettingsModel().invalidate_settings_cache()
347 347 h.flash(_('Updated application settings'), category='success')
348 348 except Exception:
349 349 log.exception("Exception while updating application settings")
350 350 h.flash(
351 351 _('Error occurred during updating application settings'),
352 352 category='error')
353 353
354 354 raise HTTPFound(h.route_path('admin_settings_global'))
355 355
356 356 @LoginRequired()
357 357 @HasPermissionAllDecorator('hg.admin')
358 358 @view_config(
359 359 route_name='admin_settings_visual', request_method='GET',
360 360 renderer='rhodecode:templates/admin/settings/settings.mako')
361 361 def settings_visual(self):
362 362 c = self.load_default_context()
363 363 c.active = 'visual'
364 364
365 365 data = render('rhodecode:templates/admin/settings/settings.mako',
366 366 self._get_template_context(c), self.request)
367 367 html = formencode.htmlfill.render(
368 368 data,
369 369 defaults=self._form_defaults(),
370 370 encoding="UTF-8",
371 371 force_defaults=False
372 372 )
373 373 return Response(html)
374 374
375 375 @LoginRequired()
376 376 @HasPermissionAllDecorator('hg.admin')
377 377 @CSRFRequired()
378 378 @view_config(
379 379 route_name='admin_settings_visual_update', request_method='POST',
380 380 renderer='rhodecode:templates/admin/settings/settings.mako')
381 381 def settings_visual_update(self):
382 382 _ = self.request.translate
383 383 c = self.load_default_context()
384 384 c.active = 'visual'
385 385 application_form = ApplicationVisualisationForm(self.request.translate)()
386 386 try:
387 387 form_result = application_form.to_python(dict(self.request.POST))
388 388 except formencode.Invalid as errors:
389 389 data = render('rhodecode:templates/admin/settings/settings.mako',
390 390 self._get_template_context(c), self.request)
391 391 html = formencode.htmlfill.render(
392 392 data,
393 393 defaults=errors.value,
394 394 errors=errors.error_dict or {},
395 395 prefix_error=False,
396 396 encoding="UTF-8",
397 397 force_defaults=False
398 398 )
399 399 return Response(html)
400 400
401 401 try:
402 402 settings = [
403 403 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
404 404 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
405 405 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
406 406 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
407 407 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
408 408 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
409 409 ('show_version', 'rhodecode_show_version', 'bool'),
410 410 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
411 411 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
412 412 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
413 413 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
414 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
414 415 ('support_url', 'rhodecode_support_url', 'unicode'),
415 416 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
416 417 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
417 418 ]
418 419 for setting, form_key, type_ in settings:
419 420 sett = SettingsModel().create_or_update_setting(
420 421 setting, form_result[form_key], type_)
421 422 Session().add(sett)
422 423
423 424 Session().commit()
424 425 SettingsModel().invalidate_settings_cache()
425 426 h.flash(_('Updated visualisation settings'), category='success')
426 427 except Exception:
427 428 log.exception("Exception updating visualization settings")
428 429 h.flash(_('Error occurred during updating '
429 430 'visualisation settings'),
430 431 category='error')
431 432
432 433 raise HTTPFound(h.route_path('admin_settings_visual'))
433 434
434 435 @LoginRequired()
435 436 @HasPermissionAllDecorator('hg.admin')
436 437 @view_config(
437 438 route_name='admin_settings_issuetracker', request_method='GET',
438 439 renderer='rhodecode:templates/admin/settings/settings.mako')
439 440 def settings_issuetracker(self):
440 441 c = self.load_default_context()
441 442 c.active = 'issuetracker'
442 443 defaults = SettingsModel().get_all_settings()
443 444
444 445 entry_key = 'rhodecode_issuetracker_pat_'
445 446
446 447 c.issuetracker_entries = {}
447 448 for k, v in defaults.items():
448 449 if k.startswith(entry_key):
449 450 uid = k[len(entry_key):]
450 451 c.issuetracker_entries[uid] = None
451 452
452 453 for uid in c.issuetracker_entries:
453 454 c.issuetracker_entries[uid] = AttributeDict({
454 455 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
455 456 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
456 457 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
457 458 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
458 459 })
459 460
460 461 return self._get_template_context(c)
461 462
462 463 @LoginRequired()
463 464 @HasPermissionAllDecorator('hg.admin')
464 465 @CSRFRequired()
465 466 @view_config(
466 467 route_name='admin_settings_issuetracker_test', request_method='POST',
467 468 renderer='string', xhr=True)
468 469 def settings_issuetracker_test(self):
469 470 return h.urlify_commit_message(
470 471 self.request.POST.get('test_text', ''),
471 472 'repo_group/test_repo1')
472 473
473 474 @LoginRequired()
474 475 @HasPermissionAllDecorator('hg.admin')
475 476 @CSRFRequired()
476 477 @view_config(
477 478 route_name='admin_settings_issuetracker_update', request_method='POST',
478 479 renderer='rhodecode:templates/admin/settings/settings.mako')
479 480 def settings_issuetracker_update(self):
480 481 _ = self.request.translate
481 482 self.load_default_context()
482 483 settings_model = IssueTrackerSettingsModel()
483 484
484 485 try:
485 486 form = IssueTrackerPatternsForm(self.request.translate)()
486 487 data = form.to_python(self.request.POST)
487 488 except formencode.Invalid as errors:
488 489 log.exception('Failed to add new pattern')
489 490 error = errors
490 491 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
491 492 category='error')
492 493 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
493 494
494 495 if data:
495 496 for uid in data.get('delete_patterns', []):
496 497 settings_model.delete_entries(uid)
497 498
498 499 for pattern in data.get('patterns', []):
499 500 for setting, value, type_ in pattern:
500 501 sett = settings_model.create_or_update_setting(
501 502 setting, value, type_)
502 503 Session().add(sett)
503 504
504 505 Session().commit()
505 506
506 507 SettingsModel().invalidate_settings_cache()
507 508 h.flash(_('Updated issue tracker entries'), category='success')
508 509 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
509 510
510 511 @LoginRequired()
511 512 @HasPermissionAllDecorator('hg.admin')
512 513 @CSRFRequired()
513 514 @view_config(
514 515 route_name='admin_settings_issuetracker_delete', request_method='POST',
515 516 renderer='rhodecode:templates/admin/settings/settings.mako')
516 517 def settings_issuetracker_delete(self):
517 518 _ = self.request.translate
518 519 self.load_default_context()
519 520 uid = self.request.POST.get('uid')
520 521 try:
521 522 IssueTrackerSettingsModel().delete_entries(uid)
522 523 except Exception:
523 524 log.exception('Failed to delete issue tracker setting %s', uid)
524 525 raise HTTPNotFound()
525 526 h.flash(_('Removed issue tracker entry'), category='success')
526 527 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
527 528
528 529 @LoginRequired()
529 530 @HasPermissionAllDecorator('hg.admin')
530 531 @view_config(
531 532 route_name='admin_settings_email', request_method='GET',
532 533 renderer='rhodecode:templates/admin/settings/settings.mako')
533 534 def settings_email(self):
534 535 c = self.load_default_context()
535 536 c.active = 'email'
536 537 c.rhodecode_ini = rhodecode.CONFIG
537 538
538 539 data = render('rhodecode:templates/admin/settings/settings.mako',
539 540 self._get_template_context(c), self.request)
540 541 html = formencode.htmlfill.render(
541 542 data,
542 543 defaults=self._form_defaults(),
543 544 encoding="UTF-8",
544 545 force_defaults=False
545 546 )
546 547 return Response(html)
547 548
548 549 @LoginRequired()
549 550 @HasPermissionAllDecorator('hg.admin')
550 551 @CSRFRequired()
551 552 @view_config(
552 553 route_name='admin_settings_email_update', request_method='POST',
553 554 renderer='rhodecode:templates/admin/settings/settings.mako')
554 555 def settings_email_update(self):
555 556 _ = self.request.translate
556 557 c = self.load_default_context()
557 558 c.active = 'email'
558 559
559 560 test_email = self.request.POST.get('test_email')
560 561
561 562 if not test_email:
562 563 h.flash(_('Please enter email address'), category='error')
563 564 raise HTTPFound(h.route_path('admin_settings_email'))
564 565
565 566 email_kwargs = {
566 567 'date': datetime.datetime.now(),
567 568 'user': c.rhodecode_user,
568 569 'rhodecode_version': c.rhodecode_version
569 570 }
570 571
571 572 (subject, headers, email_body,
572 573 email_body_plaintext) = EmailNotificationModel().render_email(
573 574 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
574 575
575 576 recipients = [test_email] if test_email else None
576 577
577 578 run_task(tasks.send_email, recipients, subject,
578 579 email_body_plaintext, email_body)
579 580
580 581 h.flash(_('Send email task created'), category='success')
581 582 raise HTTPFound(h.route_path('admin_settings_email'))
582 583
583 584 @LoginRequired()
584 585 @HasPermissionAllDecorator('hg.admin')
585 586 @view_config(
586 587 route_name='admin_settings_hooks', request_method='GET',
587 588 renderer='rhodecode:templates/admin/settings/settings.mako')
588 589 def settings_hooks(self):
589 590 c = self.load_default_context()
590 591 c.active = 'hooks'
591 592
592 593 model = SettingsModel()
593 594 c.hooks = model.get_builtin_hooks()
594 595 c.custom_hooks = model.get_custom_hooks()
595 596
596 597 data = render('rhodecode:templates/admin/settings/settings.mako',
597 598 self._get_template_context(c), self.request)
598 599 html = formencode.htmlfill.render(
599 600 data,
600 601 defaults=self._form_defaults(),
601 602 encoding="UTF-8",
602 603 force_defaults=False
603 604 )
604 605 return Response(html)
605 606
606 607 @LoginRequired()
607 608 @HasPermissionAllDecorator('hg.admin')
608 609 @CSRFRequired()
609 610 @view_config(
610 611 route_name='admin_settings_hooks_update', request_method='POST',
611 612 renderer='rhodecode:templates/admin/settings/settings.mako')
612 613 @view_config(
613 614 route_name='admin_settings_hooks_delete', request_method='POST',
614 615 renderer='rhodecode:templates/admin/settings/settings.mako')
615 616 def settings_hooks_update(self):
616 617 _ = self.request.translate
617 618 c = self.load_default_context()
618 619 c.active = 'hooks'
619 620 if c.visual.allow_custom_hooks_settings:
620 621 ui_key = self.request.POST.get('new_hook_ui_key')
621 622 ui_value = self.request.POST.get('new_hook_ui_value')
622 623
623 624 hook_id = self.request.POST.get('hook_id')
624 625 new_hook = False
625 626
626 627 model = SettingsModel()
627 628 try:
628 629 if ui_value and ui_key:
629 630 model.create_or_update_hook(ui_key, ui_value)
630 631 h.flash(_('Added new hook'), category='success')
631 632 new_hook = True
632 633 elif hook_id:
633 634 RhodeCodeUi.delete(hook_id)
634 635 Session().commit()
635 636
636 637 # check for edits
637 638 update = False
638 639 _d = self.request.POST.dict_of_lists()
639 640 for k, v in zip(_d.get('hook_ui_key', []),
640 641 _d.get('hook_ui_value_new', [])):
641 642 model.create_or_update_hook(k, v)
642 643 update = True
643 644
644 645 if update and not new_hook:
645 646 h.flash(_('Updated hooks'), category='success')
646 647 Session().commit()
647 648 except Exception:
648 649 log.exception("Exception during hook creation")
649 650 h.flash(_('Error occurred during hook creation'),
650 651 category='error')
651 652
652 653 raise HTTPFound(h.route_path('admin_settings_hooks'))
653 654
654 655 @LoginRequired()
655 656 @HasPermissionAllDecorator('hg.admin')
656 657 @view_config(
657 658 route_name='admin_settings_search', request_method='GET',
658 659 renderer='rhodecode:templates/admin/settings/settings.mako')
659 660 def settings_search(self):
660 661 c = self.load_default_context()
661 662 c.active = 'search'
662 663
663 664 searcher = searcher_from_config(self.request.registry.settings)
664 665 c.statistics = searcher.statistics(self.request.translate)
665 666
666 667 return self._get_template_context(c)
667 668
668 669 @LoginRequired()
669 670 @HasPermissionAllDecorator('hg.admin')
670 671 @view_config(
671 672 route_name='admin_settings_labs', request_method='GET',
672 673 renderer='rhodecode:templates/admin/settings/settings.mako')
673 674 def settings_labs(self):
674 675 c = self.load_default_context()
675 676 if not c.labs_active:
676 677 raise HTTPFound(h.route_path('admin_settings'))
677 678
678 679 c.active = 'labs'
679 680 c.lab_settings = _LAB_SETTINGS
680 681
681 682 data = render('rhodecode:templates/admin/settings/settings.mako',
682 683 self._get_template_context(c), self.request)
683 684 html = formencode.htmlfill.render(
684 685 data,
685 686 defaults=self._form_defaults(),
686 687 encoding="UTF-8",
687 688 force_defaults=False
688 689 )
689 690 return Response(html)
690 691
691 692 @LoginRequired()
692 693 @HasPermissionAllDecorator('hg.admin')
693 694 @CSRFRequired()
694 695 @view_config(
695 696 route_name='admin_settings_labs_update', request_method='POST',
696 697 renderer='rhodecode:templates/admin/settings/settings.mako')
697 698 def settings_labs_update(self):
698 699 _ = self.request.translate
699 700 c = self.load_default_context()
700 701 c.active = 'labs'
701 702
702 703 application_form = LabsSettingsForm(self.request.translate)()
703 704 try:
704 705 form_result = application_form.to_python(dict(self.request.POST))
705 706 except formencode.Invalid as errors:
706 707 h.flash(
707 708 _('Some form inputs contain invalid data.'),
708 709 category='error')
709 710 data = render('rhodecode:templates/admin/settings/settings.mako',
710 711 self._get_template_context(c), self.request)
711 712 html = formencode.htmlfill.render(
712 713 data,
713 714 defaults=errors.value,
714 715 errors=errors.error_dict or {},
715 716 prefix_error=False,
716 717 encoding="UTF-8",
717 718 force_defaults=False
718 719 )
719 720 return Response(html)
720 721
721 722 try:
722 723 session = Session()
723 724 for setting in _LAB_SETTINGS:
724 725 setting_name = setting.key[len('rhodecode_'):]
725 726 sett = SettingsModel().create_or_update_setting(
726 727 setting_name, form_result[setting.key], setting.type)
727 728 session.add(sett)
728 729
729 730 except Exception:
730 731 log.exception('Exception while updating lab settings')
731 732 h.flash(_('Error occurred during updating labs settings'),
732 733 category='error')
733 734 else:
734 735 Session().commit()
735 736 SettingsModel().invalidate_settings_cache()
736 737 h.flash(_('Updated Labs settings'), category='success')
737 738 raise HTTPFound(h.route_path('admin_settings_labs'))
738 739
739 740 data = render('rhodecode:templates/admin/settings/settings.mako',
740 741 self._get_template_context(c), self.request)
741 742 html = formencode.htmlfill.render(
742 743 data,
743 744 defaults=self._form_defaults(),
744 745 encoding="UTF-8",
745 746 force_defaults=False
746 747 )
747 748 return Response(html)
748 749
749 750
750 751 # :param key: name of the setting including the 'rhodecode_' prefix
751 752 # :param type: the RhodeCodeSetting type to use.
752 753 # :param group: the i18ned group in which we should dispaly this setting
753 754 # :param label: the i18ned label we should display for this setting
754 755 # :param help: the i18ned help we should dispaly for this setting
755 756 LabSetting = collections.namedtuple(
756 757 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
757 758
758 759
759 760 # This list has to be kept in sync with the form
760 761 # rhodecode.model.forms.LabsSettingsForm.
761 762 _LAB_SETTINGS = [
762 763
763 764 ]
@@ -1,371 +1,375 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 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 import string
23 23
24 24 from pyramid.view import view_config
25 25 from beaker.cache import cache_region
26 26
27 27 from rhodecode.controllers import utils
28 28 from rhodecode.apps._base import RepoAppView
29 29 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
30 30 from rhodecode.lib import caches, helpers as h
31 31 from rhodecode.lib.helpers import RepoPage
32 32 from rhodecode.lib.utils2 import safe_str, safe_int
33 33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 34 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
35 35 from rhodecode.lib.ext_json import json
36 36 from rhodecode.lib.vcs.backends.base import EmptyCommit
37 37 from rhodecode.lib.vcs.exceptions import CommitError, EmptyRepositoryError, \
38 38 CommitDoesNotExistError
39 39 from rhodecode.model.db import Statistics, CacheKey, User
40 40 from rhodecode.model.meta import Session
41 41 from rhodecode.model.repo import ReadmeFinder
42 42 from rhodecode.model.scm import ScmModel
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 class RepoSummaryView(RepoAppView):
48 48
49 49 def load_default_context(self):
50 50 c = self._get_local_tmpl_context(include_app_defaults=True)
51 51
52 52 c.rhodecode_repo = None
53 53 if not c.repository_requirements_missing:
54 54 c.rhodecode_repo = self.rhodecode_vcs_repo
55 55
56 56
57 57 return c
58 58
59 59 def _get_readme_data(self, db_repo, default_renderer):
60 60 repo_name = db_repo.repo_name
61 61 log.debug('Looking for README file')
62 62
63 63 @cache_region('long_term')
64 64 def _generate_readme(cache_key):
65 65 readme_data = None
66 66 readme_node = None
67 67 readme_filename = None
68 68 commit = self._get_landing_commit_or_none(db_repo)
69 69 if commit:
70 70 log.debug("Searching for a README file.")
71 71 readme_node = ReadmeFinder(default_renderer).search(commit)
72 72 if readme_node:
73 73 relative_urls = {
74 74 'raw': h.route_path(
75 75 'repo_file_raw', repo_name=repo_name,
76 76 commit_id=commit.raw_id, f_path=readme_node.path),
77 77 'standard': h.route_path(
78 78 'repo_files', repo_name=repo_name,
79 79 commit_id=commit.raw_id, f_path=readme_node.path),
80 80 }
81 81 readme_data = self._render_readme_or_none(
82 82 commit, readme_node, relative_urls)
83 83 readme_filename = readme_node.path
84 84 return readme_data, readme_filename
85 85
86 86 invalidator_context = CacheKey.repo_context_cache(
87 87 _generate_readme, repo_name, CacheKey.CACHE_TYPE_README)
88 88
89 89 with invalidator_context as context:
90 90 context.invalidate()
91 91 computed = context.compute()
92 92
93 93 return computed
94 94
95 95 def _get_landing_commit_or_none(self, db_repo):
96 96 log.debug("Getting the landing commit.")
97 97 try:
98 98 commit = db_repo.get_landing_commit()
99 99 if not isinstance(commit, EmptyCommit):
100 100 return commit
101 101 else:
102 102 log.debug("Repository is empty, no README to render.")
103 103 except CommitError:
104 104 log.exception(
105 105 "Problem getting commit when trying to render the README.")
106 106
107 107 def _render_readme_or_none(self, commit, readme_node, relative_urls):
108 108 log.debug(
109 109 'Found README file `%s` rendering...', readme_node.path)
110 110 renderer = MarkupRenderer()
111 111 try:
112 112 html_source = renderer.render(
113 113 readme_node.content, filename=readme_node.path)
114 114 if relative_urls:
115 115 return relative_links(html_source, relative_urls)
116 116 return html_source
117 117 except Exception:
118 118 log.exception(
119 119 "Exception while trying to render the README")
120 120
121 121 def _load_commits_context(self, c):
122 122 p = safe_int(self.request.GET.get('page'), 1)
123 123 size = safe_int(self.request.GET.get('size'), 10)
124 124
125 125 def url_generator(**kw):
126 126 query_params = {
127 127 'size': size
128 128 }
129 129 query_params.update(kw)
130 130 return h.route_path(
131 131 'repo_summary_commits',
132 132 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
133 133
134 134 pre_load = ['author', 'branch', 'date', 'message']
135 135 try:
136 136 collection = self.rhodecode_vcs_repo.get_commits(pre_load=pre_load)
137 137 except EmptyRepositoryError:
138 138 collection = self.rhodecode_vcs_repo
139 139
140 140 c.repo_commits = RepoPage(
141 141 collection, page=p, items_per_page=size, url=url_generator)
142 142 page_ids = [x.raw_id for x in c.repo_commits]
143 143 c.comments = self.db_repo.get_comments(page_ids)
144 144 c.statuses = self.db_repo.statuses(page_ids)
145 145
146 146 @LoginRequired()
147 147 @HasRepoPermissionAnyDecorator(
148 148 'repository.read', 'repository.write', 'repository.admin')
149 149 @view_config(
150 150 route_name='repo_summary_commits', request_method='GET',
151 151 renderer='rhodecode:templates/summary/summary_commits.mako')
152 152 def summary_commits(self):
153 153 c = self.load_default_context()
154 154 self._load_commits_context(c)
155 155 return self._get_template_context(c)
156 156
157 157 @LoginRequired()
158 158 @HasRepoPermissionAnyDecorator(
159 159 'repository.read', 'repository.write', 'repository.admin')
160 160 @view_config(
161 161 route_name='repo_summary', request_method='GET',
162 162 renderer='rhodecode:templates/summary/summary.mako')
163 163 @view_config(
164 164 route_name='repo_summary_slash', request_method='GET',
165 165 renderer='rhodecode:templates/summary/summary.mako')
166 166 @view_config(
167 167 route_name='repo_summary_explicit', request_method='GET',
168 168 renderer='rhodecode:templates/summary/summary.mako')
169 169 def summary(self):
170 170 c = self.load_default_context()
171 171
172 172 # Prepare the clone URL
173 173 username = ''
174 174 if self._rhodecode_user.username != User.DEFAULT_USER:
175 175 username = safe_str(self._rhodecode_user.username)
176 176
177 _def_clone_uri = _def_clone_uri_by_id = c.clone_uri_tmpl
177 _def_clone_uri = _def_clone_uri_id = c.clone_uri_tmpl
178 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
179
178 180 if '{repo}' in _def_clone_uri:
179 _def_clone_uri_by_id = _def_clone_uri.replace(
181 _def_clone_uri_id = _def_clone_uri.replace(
180 182 '{repo}', '_{repoid}')
181 183 elif '{repoid}' in _def_clone_uri:
182 _def_clone_uri_by_id = _def_clone_uri.replace(
184 _def_clone_uri_id = _def_clone_uri.replace(
183 185 '_{repoid}', '{repo}')
184 186
185 187 c.clone_repo_url = self.db_repo.clone_url(
186 188 user=username, uri_tmpl=_def_clone_uri)
187 189 c.clone_repo_url_id = self.db_repo.clone_url(
188 user=username, uri_tmpl=_def_clone_uri_by_id)
190 user=username, uri_tmpl=_def_clone_uri_id)
191 c.clone_repo_url_ssh = self.db_repo.clone_url(
192 uri_tmpl=_def_clone_uri_ssh, ssh=True)
189 193
190 194 # If enabled, get statistics data
191 195
192 196 c.show_stats = bool(self.db_repo.enable_statistics)
193 197
194 198 stats = Session().query(Statistics) \
195 199 .filter(Statistics.repository == self.db_repo) \
196 200 .scalar()
197 201
198 202 c.stats_percentage = 0
199 203
200 204 if stats and stats.languages:
201 205 c.no_data = False is self.db_repo.enable_statistics
202 206 lang_stats_d = json.loads(stats.languages)
203 207
204 208 # Sort first by decreasing count and second by the file extension,
205 209 # so we have a consistent output.
206 210 lang_stats_items = sorted(lang_stats_d.iteritems(),
207 211 key=lambda k: (-k[1], k[0]))[:10]
208 212 lang_stats = [(x, {"count": y,
209 213 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
210 214 for x, y in lang_stats_items]
211 215
212 216 c.trending_languages = json.dumps(lang_stats)
213 217 else:
214 218 c.no_data = True
215 219 c.trending_languages = json.dumps({})
216 220
217 221 scm_model = ScmModel()
218 222 c.enable_downloads = self.db_repo.enable_downloads
219 223 c.repository_followers = scm_model.get_followers(self.db_repo)
220 224 c.repository_forks = scm_model.get_forks(self.db_repo)
221 225 c.repository_is_user_following = scm_model.is_following_repo(
222 226 self.db_repo_name, self._rhodecode_user.user_id)
223 227
224 228 # first interaction with the VCS instance after here...
225 229 if c.repository_requirements_missing:
226 230 self.request.override_renderer = \
227 231 'rhodecode:templates/summary/missing_requirements.mako'
228 232 return self._get_template_context(c)
229 233
230 234 c.readme_data, c.readme_file = \
231 235 self._get_readme_data(self.db_repo, c.visual.default_renderer)
232 236
233 237 # loads the summary commits template context
234 238 self._load_commits_context(c)
235 239
236 240 return self._get_template_context(c)
237 241
238 242 def get_request_commit_id(self):
239 243 return self.request.matchdict['commit_id']
240 244
241 245 @LoginRequired()
242 246 @HasRepoPermissionAnyDecorator(
243 247 'repository.read', 'repository.write', 'repository.admin')
244 248 @view_config(
245 249 route_name='repo_stats', request_method='GET',
246 250 renderer='json_ext')
247 251 def repo_stats(self):
248 252 commit_id = self.get_request_commit_id()
249 253
250 254 _namespace = caches.get_repo_namespace_key(
251 255 caches.SUMMARY_STATS, self.db_repo_name)
252 256 show_stats = bool(self.db_repo.enable_statistics)
253 257 cache_manager = caches.get_cache_manager(
254 258 'repo_cache_long', _namespace)
255 259 _cache_key = caches.compute_key_from_params(
256 260 self.db_repo_name, commit_id, show_stats)
257 261
258 262 def compute_stats():
259 263 code_stats = {}
260 264 size = 0
261 265 try:
262 266 scm_instance = self.db_repo.scm_instance()
263 267 commit = scm_instance.get_commit(commit_id)
264 268
265 269 for node in commit.get_filenodes_generator():
266 270 size += node.size
267 271 if not show_stats:
268 272 continue
269 273 ext = string.lower(node.extension)
270 274 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
271 275 if ext_info:
272 276 if ext in code_stats:
273 277 code_stats[ext]['count'] += 1
274 278 else:
275 279 code_stats[ext] = {"count": 1, "desc": ext_info}
276 280 except (EmptyRepositoryError, CommitDoesNotExistError):
277 281 pass
278 282 return {'size': h.format_byte_size_binary(size),
279 283 'code_stats': code_stats}
280 284
281 285 stats = cache_manager.get(_cache_key, createfunc=compute_stats)
282 286 return stats
283 287
284 288 @LoginRequired()
285 289 @HasRepoPermissionAnyDecorator(
286 290 'repository.read', 'repository.write', 'repository.admin')
287 291 @view_config(
288 292 route_name='repo_refs_data', request_method='GET',
289 293 renderer='json_ext')
290 294 def repo_refs_data(self):
291 295 _ = self.request.translate
292 296 self.load_default_context()
293 297
294 298 repo = self.rhodecode_vcs_repo
295 299 refs_to_create = [
296 300 (_("Branch"), repo.branches, 'branch'),
297 301 (_("Tag"), repo.tags, 'tag'),
298 302 (_("Bookmark"), repo.bookmarks, 'book'),
299 303 ]
300 304 res = self._create_reference_data(
301 305 repo, self.db_repo_name, refs_to_create)
302 306 data = {
303 307 'more': False,
304 308 'results': res
305 309 }
306 310 return data
307 311
308 312 @LoginRequired()
309 313 @HasRepoPermissionAnyDecorator(
310 314 'repository.read', 'repository.write', 'repository.admin')
311 315 @view_config(
312 316 route_name='repo_refs_changelog_data', request_method='GET',
313 317 renderer='json_ext')
314 318 def repo_refs_changelog_data(self):
315 319 _ = self.request.translate
316 320 self.load_default_context()
317 321
318 322 repo = self.rhodecode_vcs_repo
319 323
320 324 refs_to_create = [
321 325 (_("Branches"), repo.branches, 'branch'),
322 326 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
323 327 # TODO: enable when vcs can handle bookmarks filters
324 328 # (_("Bookmarks"), repo.bookmarks, "book"),
325 329 ]
326 330 res = self._create_reference_data(
327 331 repo, self.db_repo_name, refs_to_create)
328 332 data = {
329 333 'more': False,
330 334 'results': res
331 335 }
332 336 return data
333 337
334 338 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
335 339 format_ref_id = utils.get_format_ref_id(repo)
336 340
337 341 result = []
338 342 for title, refs, ref_type in refs_to_create:
339 343 if refs:
340 344 result.append({
341 345 'text': title,
342 346 'children': self._create_reference_items(
343 347 repo, full_repo_name, refs, ref_type,
344 348 format_ref_id),
345 349 })
346 350 return result
347 351
348 352 def _create_reference_items(self, repo, full_repo_name, refs, ref_type,
349 353 format_ref_id):
350 354 result = []
351 355 is_svn = h.is_svn(repo)
352 356 for ref_name, raw_id in refs.iteritems():
353 357 files_url = self._create_files_url(
354 358 repo, full_repo_name, ref_name, raw_id, is_svn)
355 359 result.append({
356 360 'text': ref_name,
357 361 'id': format_ref_id(ref_name, raw_id),
358 362 'raw_id': raw_id,
359 363 'type': ref_type,
360 364 'files_url': files_url,
361 365 })
362 366 return result
363 367
364 368 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
365 369 use_commit_id = '/' in ref_name or is_svn
366 370 return h.route_path(
367 371 'repo_files',
368 372 repo_name=full_repo_name,
369 373 f_path=ref_name if is_svn else '',
370 374 commit_id=raw_id if use_commit_id else ref_name,
371 375 _query=dict(at=ref_name))
@@ -1,542 +1,543 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 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 markupsafe
31 31 import ipaddress
32 32
33 33 from paste.auth.basic import AuthBasicAuthenticator
34 34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 36
37 37 import rhodecode
38 38 from rhodecode.authentication.base import VCS_TYPE
39 39 from rhodecode.lib import auth, utils2
40 40 from rhodecode.lib import helpers as h
41 41 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
42 42 from rhodecode.lib.exceptions import UserCreationError
43 43 from rhodecode.lib.utils import (password_changed, get_enabled_hook_classes)
44 44 from rhodecode.lib.utils2 import (
45 45 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist, safe_str)
46 46 from rhodecode.model.db import Repository, User, ChangesetComment
47 47 from rhodecode.model.notification import NotificationModel
48 48 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 def _filter_proxy(ip):
54 54 """
55 55 Passed in IP addresses in HEADERS can be in a special format of multiple
56 56 ips. Those comma separated IPs are passed from various proxies in the
57 57 chain of request processing. The left-most being the original client.
58 58 We only care about the first IP which came from the org. client.
59 59
60 60 :param ip: ip string from headers
61 61 """
62 62 if ',' in ip:
63 63 _ips = ip.split(',')
64 64 _first_ip = _ips[0].strip()
65 65 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
66 66 return _first_ip
67 67 return ip
68 68
69 69
70 70 def _filter_port(ip):
71 71 """
72 72 Removes a port from ip, there are 4 main cases to handle here.
73 73 - ipv4 eg. 127.0.0.1
74 74 - ipv6 eg. ::1
75 75 - ipv4+port eg. 127.0.0.1:8080
76 76 - ipv6+port eg. [::1]:8080
77 77
78 78 :param ip:
79 79 """
80 80 def is_ipv6(ip_addr):
81 81 if hasattr(socket, 'inet_pton'):
82 82 try:
83 83 socket.inet_pton(socket.AF_INET6, ip_addr)
84 84 except socket.error:
85 85 return False
86 86 else:
87 87 # fallback to ipaddress
88 88 try:
89 89 ipaddress.IPv6Address(safe_unicode(ip_addr))
90 90 except Exception:
91 91 return False
92 92 return True
93 93
94 94 if ':' not in ip: # must be ipv4 pure ip
95 95 return ip
96 96
97 97 if '[' in ip and ']' in ip: # ipv6 with port
98 98 return ip.split(']')[0][1:].lower()
99 99
100 100 # must be ipv6 or ipv4 with port
101 101 if is_ipv6(ip):
102 102 return ip
103 103 else:
104 104 ip, _port = ip.split(':')[:2] # means ipv4+port
105 105 return ip
106 106
107 107
108 108 def get_ip_addr(environ):
109 109 proxy_key = 'HTTP_X_REAL_IP'
110 110 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
111 111 def_key = 'REMOTE_ADDR'
112 112 _filters = lambda x: _filter_port(_filter_proxy(x))
113 113
114 114 ip = environ.get(proxy_key)
115 115 if ip:
116 116 return _filters(ip)
117 117
118 118 ip = environ.get(proxy_key2)
119 119 if ip:
120 120 return _filters(ip)
121 121
122 122 ip = environ.get(def_key, '0.0.0.0')
123 123 return _filters(ip)
124 124
125 125
126 126 def get_server_ip_addr(environ, log_errors=True):
127 127 hostname = environ.get('SERVER_NAME')
128 128 try:
129 129 return socket.gethostbyname(hostname)
130 130 except Exception as e:
131 131 if log_errors:
132 132 # in some cases this lookup is not possible, and we don't want to
133 133 # make it an exception in logs
134 134 log.exception('Could not retrieve server ip address: %s', e)
135 135 return hostname
136 136
137 137
138 138 def get_server_port(environ):
139 139 return environ.get('SERVER_PORT')
140 140
141 141
142 142 def get_access_path(environ):
143 143 path = environ.get('PATH_INFO')
144 144 org_req = environ.get('pylons.original_request')
145 145 if org_req:
146 146 path = org_req.environ.get('PATH_INFO')
147 147 return path
148 148
149 149
150 150 def get_user_agent(environ):
151 151 return environ.get('HTTP_USER_AGENT')
152 152
153 153
154 154 def vcs_operation_context(
155 155 environ, repo_name, username, action, scm, check_locking=True,
156 156 is_shadow_repo=False):
157 157 """
158 158 Generate the context for a vcs operation, e.g. push or pull.
159 159
160 160 This context is passed over the layers so that hooks triggered by the
161 161 vcs operation know details like the user, the user's IP address etc.
162 162
163 163 :param check_locking: Allows to switch of the computation of the locking
164 164 data. This serves mainly the need of the simplevcs middleware to be
165 165 able to disable this for certain operations.
166 166
167 167 """
168 168 # Tri-state value: False: unlock, None: nothing, True: lock
169 169 make_lock = None
170 170 locked_by = [None, None, None]
171 171 is_anonymous = username == User.DEFAULT_USER
172 172 user = User.get_by_username(username)
173 173 if not is_anonymous and check_locking:
174 174 log.debug('Checking locking on repository "%s"', repo_name)
175 175 repo = Repository.get_by_repo_name(repo_name)
176 176 make_lock, __, locked_by = repo.get_locking_state(
177 177 action, user.user_id)
178 178 user_id = user.user_id
179 179 settings_model = VcsSettingsModel(repo=repo_name)
180 180 ui_settings = settings_model.get_ui_settings()
181 181
182 182 extras = {
183 183 'ip': get_ip_addr(environ),
184 184 'username': username,
185 185 'user_id': user_id,
186 186 'action': action,
187 187 'repository': repo_name,
188 188 'scm': scm,
189 189 'config': rhodecode.CONFIG['__file__'],
190 190 'make_lock': make_lock,
191 191 'locked_by': locked_by,
192 192 'server_url': utils2.get_server_url(environ),
193 193 'user_agent': get_user_agent(environ),
194 194 'hooks': get_enabled_hook_classes(ui_settings),
195 195 'is_shadow_repo': is_shadow_repo,
196 196 }
197 197 return extras
198 198
199 199
200 200 class BasicAuth(AuthBasicAuthenticator):
201 201
202 202 def __init__(self, realm, authfunc, registry, auth_http_code=None,
203 203 initial_call_detection=False, acl_repo_name=None):
204 204 self.realm = realm
205 205 self.initial_call = initial_call_detection
206 206 self.authfunc = authfunc
207 207 self.registry = registry
208 208 self.acl_repo_name = acl_repo_name
209 209 self._rc_auth_http_code = auth_http_code
210 210
211 211 def _get_response_from_code(self, http_code):
212 212 try:
213 213 return get_exception(safe_int(http_code))
214 214 except Exception:
215 215 log.exception('Failed to fetch response for code %s' % http_code)
216 216 return HTTPForbidden
217 217
218 218 def get_rc_realm(self):
219 219 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
220 220
221 221 def build_authentication(self):
222 222 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
223 223 if self._rc_auth_http_code and not self.initial_call:
224 224 # return alternative HTTP code if alternative http return code
225 225 # is specified in RhodeCode config, but ONLY if it's not the
226 226 # FIRST call
227 227 custom_response_klass = self._get_response_from_code(
228 228 self._rc_auth_http_code)
229 229 return custom_response_klass(headers=head)
230 230 return HTTPUnauthorized(headers=head)
231 231
232 232 def authenticate(self, environ):
233 233 authorization = AUTHORIZATION(environ)
234 234 if not authorization:
235 235 return self.build_authentication()
236 236 (authmeth, auth) = authorization.split(' ', 1)
237 237 if 'basic' != authmeth.lower():
238 238 return self.build_authentication()
239 239 auth = auth.strip().decode('base64')
240 240 _parts = auth.split(':', 1)
241 241 if len(_parts) == 2:
242 242 username, password = _parts
243 243 auth_data = self.authfunc(
244 244 username, password, environ, VCS_TYPE,
245 245 registry=self.registry, acl_repo_name=self.acl_repo_name)
246 246 if auth_data:
247 247 return {'username': username, 'auth_data': auth_data}
248 248 if username and password:
249 249 # we mark that we actually executed authentication once, at
250 250 # that point we can use the alternative auth code
251 251 self.initial_call = False
252 252
253 253 return self.build_authentication()
254 254
255 255 __call__ = authenticate
256 256
257 257
258 258 def calculate_version_hash(config):
259 259 return md5(
260 260 config.get('beaker.session.secret', '') +
261 261 rhodecode.__version__)[:8]
262 262
263 263
264 264 def get_current_lang(request):
265 265 # NOTE(marcink): remove after pyramid move
266 266 try:
267 267 return translation.get_lang()[0]
268 268 except:
269 269 pass
270 270
271 271 return getattr(request, '_LOCALE_', request.locale_name)
272 272
273 273
274 274 def attach_context_attributes(context, request, user_id):
275 275 """
276 276 Attach variables into template context called `c`.
277 277 """
278 278 config = request.registry.settings
279 279
280 280
281 281 rc_config = SettingsModel().get_all_settings(cache=True)
282 282
283 283 context.rhodecode_version = rhodecode.__version__
284 284 context.rhodecode_edition = config.get('rhodecode.edition')
285 285 # unique secret + version does not leak the version but keep consistency
286 286 context.rhodecode_version_hash = calculate_version_hash(config)
287 287
288 288 # Default language set for the incoming request
289 289 context.language = get_current_lang(request)
290 290
291 291 # Visual options
292 292 context.visual = AttributeDict({})
293 293
294 294 # DB stored Visual Items
295 295 context.visual.show_public_icon = str2bool(
296 296 rc_config.get('rhodecode_show_public_icon'))
297 297 context.visual.show_private_icon = str2bool(
298 298 rc_config.get('rhodecode_show_private_icon'))
299 299 context.visual.stylify_metatags = str2bool(
300 300 rc_config.get('rhodecode_stylify_metatags'))
301 301 context.visual.dashboard_items = safe_int(
302 302 rc_config.get('rhodecode_dashboard_items', 100))
303 303 context.visual.admin_grid_items = safe_int(
304 304 rc_config.get('rhodecode_admin_grid_items', 100))
305 305 context.visual.repository_fields = str2bool(
306 306 rc_config.get('rhodecode_repository_fields'))
307 307 context.visual.show_version = str2bool(
308 308 rc_config.get('rhodecode_show_version'))
309 309 context.visual.use_gravatar = str2bool(
310 310 rc_config.get('rhodecode_use_gravatar'))
311 311 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
312 312 context.visual.default_renderer = rc_config.get(
313 313 'rhodecode_markup_renderer', 'rst')
314 314 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
315 315 context.visual.rhodecode_support_url = \
316 316 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
317 317
318 318 context.visual.affected_files_cut_off = 60
319 319
320 320 context.pre_code = rc_config.get('rhodecode_pre_code')
321 321 context.post_code = rc_config.get('rhodecode_post_code')
322 322 context.rhodecode_name = rc_config.get('rhodecode_title')
323 323 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
324 324 # if we have specified default_encoding in the request, it has more
325 325 # priority
326 326 if request.GET.get('default_encoding'):
327 327 context.default_encodings.insert(0, request.GET.get('default_encoding'))
328 328 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
329 context.clone_uri_ssh_tmpl = rc_config.get('rhodecode_clone_uri_ssh_tmpl')
329 330
330 331 # INI stored
331 332 context.labs_active = str2bool(
332 333 config.get('labs_settings_active', 'false'))
333 334 context.visual.allow_repo_location_change = str2bool(
334 335 config.get('allow_repo_location_change', True))
335 336 context.visual.allow_custom_hooks_settings = str2bool(
336 337 config.get('allow_custom_hooks_settings', True))
337 338 context.debug_style = str2bool(config.get('debug_style', False))
338 339
339 340 context.rhodecode_instanceid = config.get('instance_id')
340 341
341 342 context.visual.cut_off_limit_diff = safe_int(
342 343 config.get('cut_off_limit_diff'))
343 344 context.visual.cut_off_limit_file = safe_int(
344 345 config.get('cut_off_limit_file'))
345 346
346 347 # AppEnlight
347 348 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
348 349 context.appenlight_api_public_key = config.get(
349 350 'appenlight.api_public_key', '')
350 351 context.appenlight_server_url = config.get('appenlight.server_url', '')
351 352
352 353 # JS template context
353 354 context.template_context = {
354 355 'repo_name': None,
355 356 'repo_type': None,
356 357 'repo_landing_commit': None,
357 358 'rhodecode_user': {
358 359 'username': None,
359 360 'email': None,
360 361 'notification_status': False
361 362 },
362 363 'visual': {
363 364 'default_renderer': None
364 365 },
365 366 'commit_data': {
366 367 'commit_id': None
367 368 },
368 369 'pull_request_data': {'pull_request_id': None},
369 370 'timeago': {
370 371 'refresh_time': 120 * 1000,
371 372 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
372 373 },
373 374 'pyramid_dispatch': {
374 375
375 376 },
376 377 'extra': {'plugins': {}}
377 378 }
378 379 # END CONFIG VARS
379 380
380 381 diffmode = 'sideside'
381 382 if request.GET.get('diffmode'):
382 383 if request.GET['diffmode'] == 'unified':
383 384 diffmode = 'unified'
384 385 elif request.session.get('diffmode'):
385 386 diffmode = request.session['diffmode']
386 387
387 388 context.diffmode = diffmode
388 389
389 390 if request.session.get('diffmode') != diffmode:
390 391 request.session['diffmode'] = diffmode
391 392
392 393 context.csrf_token = auth.get_csrf_token(session=request.session)
393 394 context.backends = rhodecode.BACKENDS.keys()
394 395 context.backends.sort()
395 396 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
396 397
397 398 # web case
398 399 if hasattr(request, 'user'):
399 400 context.auth_user = request.user
400 401 context.rhodecode_user = request.user
401 402
402 403 # api case
403 404 if hasattr(request, 'rpc_user'):
404 405 context.auth_user = request.rpc_user
405 406 context.rhodecode_user = request.rpc_user
406 407
407 408 # attach the whole call context to the request
408 409 request.call_context = context
409 410
410 411
411 412 def get_auth_user(request):
412 413 environ = request.environ
413 414 session = request.session
414 415
415 416 ip_addr = get_ip_addr(environ)
416 417 # make sure that we update permissions each time we call controller
417 418 _auth_token = (request.GET.get('auth_token', '') or
418 419 request.GET.get('api_key', ''))
419 420
420 421 if _auth_token:
421 422 # when using API_KEY we assume user exists, and
422 423 # doesn't need auth based on cookies.
423 424 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
424 425 authenticated = False
425 426 else:
426 427 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
427 428 try:
428 429 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
429 430 ip_addr=ip_addr)
430 431 except UserCreationError as e:
431 432 h.flash(e, 'error')
432 433 # container auth or other auth functions that create users
433 434 # on the fly can throw this exception signaling that there's
434 435 # issue with user creation, explanation should be provided
435 436 # in Exception itself. We then create a simple blank
436 437 # AuthUser
437 438 auth_user = AuthUser(ip_addr=ip_addr)
438 439
439 440 # in case someone changes a password for user it triggers session
440 441 # flush and forces a re-login
441 442 if password_changed(auth_user, session):
442 443 session.invalidate()
443 444 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
444 445 auth_user = AuthUser(ip_addr=ip_addr)
445 446
446 447 authenticated = cookie_store.get('is_authenticated')
447 448
448 449 if not auth_user.is_authenticated and auth_user.is_user_object:
449 450 # user is not authenticated and not empty
450 451 auth_user.set_authenticated(authenticated)
451 452
452 453 return auth_user
453 454
454 455
455 456 def h_filter(s):
456 457 """
457 458 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
458 459 we wrap this with additional functionality that converts None to empty
459 460 strings
460 461 """
461 462 if s is None:
462 463 return markupsafe.Markup()
463 464 return markupsafe.escape(s)
464 465
465 466
466 467 def add_events_routes(config):
467 468 """
468 469 Adds routing that can be used in events. Because some events are triggered
469 470 outside of pyramid context, we need to bootstrap request with some
470 471 routing registered
471 472 """
472 473
473 474 from rhodecode.apps._base import ADMIN_PREFIX
474 475
475 476 config.add_route(name='home', pattern='/')
476 477
477 478 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
478 479 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
479 480 config.add_route(name='repo_summary', pattern='/{repo_name}')
480 481 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
481 482 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
482 483
483 484 config.add_route(name='pullrequest_show',
484 485 pattern='/{repo_name}/pull-request/{pull_request_id}')
485 486 config.add_route(name='pull_requests_global',
486 487 pattern='/pull-request/{pull_request_id}')
487 488 config.add_route(name='repo_commit',
488 489 pattern='/{repo_name}/changeset/{commit_id}')
489 490
490 491 config.add_route(name='repo_files',
491 492 pattern='/{repo_name}/files/{commit_id}/{f_path}')
492 493
493 494
494 495 def bootstrap_config(request):
495 496 import pyramid.testing
496 497 registry = pyramid.testing.Registry('RcTestRegistry')
497 498
498 499 config = pyramid.testing.setUp(registry=registry, request=request)
499 500
500 501 # allow pyramid lookup in testing
501 502 config.include('pyramid_mako')
502 503 config.include('pyramid_beaker')
503 504 config.include('rhodecode.lib.caches')
504 505
505 506 add_events_routes(config)
506 507
507 508 return config
508 509
509 510
510 511 def bootstrap_request(**kwargs):
511 512 import pyramid.testing
512 513
513 514 class TestRequest(pyramid.testing.DummyRequest):
514 515 application_url = kwargs.pop('application_url', 'http://example.com')
515 516 host = kwargs.pop('host', 'example.com:80')
516 517 domain = kwargs.pop('domain', 'example.com')
517 518
518 519 def translate(self, msg):
519 520 return msg
520 521
521 522 def plularize(self, singular, plural, n):
522 523 return singular
523 524
524 525 def get_partial_renderer(self, tmpl_name):
525 526
526 527 from rhodecode.lib.partial_renderer import get_partial_renderer
527 528 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
528 529
529 530 _call_context = {}
530 531 @property
531 532 def call_context(self):
532 533 return self._call_context
533 534
534 535 class TestDummySession(pyramid.testing.DummySession):
535 536 def save(*arg, **kw):
536 537 pass
537 538
538 539 request = TestRequest(**kwargs)
539 540 request.session = TestDummySession()
540 541
541 542 return request
542 543
@@ -1,980 +1,984 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 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 Some simple helper functions
24 24 """
25 25
26 26 import collections
27 27 import datetime
28 28 import dateutil.relativedelta
29 29 import hashlib
30 30 import logging
31 31 import re
32 32 import sys
33 33 import time
34 34 import urllib
35 35 import urlobject
36 36 import uuid
37 import getpass
37 38
38 39 import pygments.lexers
39 40 import sqlalchemy
40 41 import sqlalchemy.engine.url
41 42 import sqlalchemy.exc
42 43 import sqlalchemy.sql
43 44 import webob
44 45 import pyramid.threadlocal
45 46
46 47 import rhodecode
47 48 from rhodecode.translation import _, _pluralize
48 49
49 50
50 51 def md5(s):
51 52 return hashlib.md5(s).hexdigest()
52 53
53 54
54 55 def md5_safe(s):
55 56 return md5(safe_str(s))
56 57
57 58
58 59 def __get_lem(extra_mapping=None):
59 60 """
60 61 Get language extension map based on what's inside pygments lexers
61 62 """
62 63 d = collections.defaultdict(lambda: [])
63 64
64 65 def __clean(s):
65 66 s = s.lstrip('*')
66 67 s = s.lstrip('.')
67 68
68 69 if s.find('[') != -1:
69 70 exts = []
70 71 start, stop = s.find('['), s.find(']')
71 72
72 73 for suffix in s[start + 1:stop]:
73 74 exts.append(s[:s.find('[')] + suffix)
74 75 return [e.lower() for e in exts]
75 76 else:
76 77 return [s.lower()]
77 78
78 79 for lx, t in sorted(pygments.lexers.LEXERS.items()):
79 80 m = map(__clean, t[-2])
80 81 if m:
81 82 m = reduce(lambda x, y: x + y, m)
82 83 for ext in m:
83 84 desc = lx.replace('Lexer', '')
84 85 d[ext].append(desc)
85 86
86 87 data = dict(d)
87 88
88 89 extra_mapping = extra_mapping or {}
89 90 if extra_mapping:
90 91 for k, v in extra_mapping.items():
91 92 if k not in data:
92 93 # register new mapping2lexer
93 94 data[k] = [v]
94 95
95 96 return data
96 97
97 98
98 99 def str2bool(_str):
99 100 """
100 101 returns True/False value from given string, it tries to translate the
101 102 string into boolean
102 103
103 104 :param _str: string value to translate into boolean
104 105 :rtype: boolean
105 106 :returns: boolean from given string
106 107 """
107 108 if _str is None:
108 109 return False
109 110 if _str in (True, False):
110 111 return _str
111 112 _str = str(_str).strip().lower()
112 113 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
113 114
114 115
115 116 def aslist(obj, sep=None, strip=True):
116 117 """
117 118 Returns given string separated by sep as list
118 119
119 120 :param obj:
120 121 :param sep:
121 122 :param strip:
122 123 """
123 124 if isinstance(obj, (basestring,)):
124 125 lst = obj.split(sep)
125 126 if strip:
126 127 lst = [v.strip() for v in lst]
127 128 return lst
128 129 elif isinstance(obj, (list, tuple)):
129 130 return obj
130 131 elif obj is None:
131 132 return []
132 133 else:
133 134 return [obj]
134 135
135 136
136 137 def convert_line_endings(line, mode):
137 138 """
138 139 Converts a given line "line end" accordingly to given mode
139 140
140 141 Available modes are::
141 142 0 - Unix
142 143 1 - Mac
143 144 2 - DOS
144 145
145 146 :param line: given line to convert
146 147 :param mode: mode to convert to
147 148 :rtype: str
148 149 :return: converted line according to mode
149 150 """
150 151 if mode == 0:
151 152 line = line.replace('\r\n', '\n')
152 153 line = line.replace('\r', '\n')
153 154 elif mode == 1:
154 155 line = line.replace('\r\n', '\r')
155 156 line = line.replace('\n', '\r')
156 157 elif mode == 2:
157 158 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
158 159 return line
159 160
160 161
161 162 def detect_mode(line, default):
162 163 """
163 164 Detects line break for given line, if line break couldn't be found
164 165 given default value is returned
165 166
166 167 :param line: str line
167 168 :param default: default
168 169 :rtype: int
169 170 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
170 171 """
171 172 if line.endswith('\r\n'):
172 173 return 2
173 174 elif line.endswith('\n'):
174 175 return 0
175 176 elif line.endswith('\r'):
176 177 return 1
177 178 else:
178 179 return default
179 180
180 181
181 182 def safe_int(val, default=None):
182 183 """
183 184 Returns int() of val if val is not convertable to int use default
184 185 instead
185 186
186 187 :param val:
187 188 :param default:
188 189 """
189 190
190 191 try:
191 192 val = int(val)
192 193 except (ValueError, TypeError):
193 194 val = default
194 195
195 196 return val
196 197
197 198
198 199 def safe_unicode(str_, from_encoding=None):
199 200 """
200 201 safe unicode function. Does few trick to turn str_ into unicode
201 202
202 203 In case of UnicodeDecode error, we try to return it with encoding detected
203 204 by chardet library if it fails fallback to unicode with errors replaced
204 205
205 206 :param str_: string to decode
206 207 :rtype: unicode
207 208 :returns: unicode object
208 209 """
209 210 if isinstance(str_, unicode):
210 211 return str_
211 212
212 213 if not from_encoding:
213 214 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
214 215 'utf8'), sep=',')
215 216 from_encoding = DEFAULT_ENCODINGS
216 217
217 218 if not isinstance(from_encoding, (list, tuple)):
218 219 from_encoding = [from_encoding]
219 220
220 221 try:
221 222 return unicode(str_)
222 223 except UnicodeDecodeError:
223 224 pass
224 225
225 226 for enc in from_encoding:
226 227 try:
227 228 return unicode(str_, enc)
228 229 except UnicodeDecodeError:
229 230 pass
230 231
231 232 try:
232 233 import chardet
233 234 encoding = chardet.detect(str_)['encoding']
234 235 if encoding is None:
235 236 raise Exception()
236 237 return str_.decode(encoding)
237 238 except (ImportError, UnicodeDecodeError, Exception):
238 239 return unicode(str_, from_encoding[0], 'replace')
239 240
240 241
241 242 def safe_str(unicode_, to_encoding=None):
242 243 """
243 244 safe str function. Does few trick to turn unicode_ into string
244 245
245 246 In case of UnicodeEncodeError, we try to return it with encoding detected
246 247 by chardet library if it fails fallback to string with errors replaced
247 248
248 249 :param unicode_: unicode to encode
249 250 :rtype: str
250 251 :returns: str object
251 252 """
252 253
253 254 # if it's not basestr cast to str
254 255 if not isinstance(unicode_, basestring):
255 256 return str(unicode_)
256 257
257 258 if isinstance(unicode_, str):
258 259 return unicode_
259 260
260 261 if not to_encoding:
261 262 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
262 263 'utf8'), sep=',')
263 264 to_encoding = DEFAULT_ENCODINGS
264 265
265 266 if not isinstance(to_encoding, (list, tuple)):
266 267 to_encoding = [to_encoding]
267 268
268 269 for enc in to_encoding:
269 270 try:
270 271 return unicode_.encode(enc)
271 272 except UnicodeEncodeError:
272 273 pass
273 274
274 275 try:
275 276 import chardet
276 277 encoding = chardet.detect(unicode_)['encoding']
277 278 if encoding is None:
278 279 raise UnicodeEncodeError()
279 280
280 281 return unicode_.encode(encoding)
281 282 except (ImportError, UnicodeEncodeError):
282 283 return unicode_.encode(to_encoding[0], 'replace')
283 284
284 285
285 286 def remove_suffix(s, suffix):
286 287 if s.endswith(suffix):
287 288 s = s[:-1 * len(suffix)]
288 289 return s
289 290
290 291
291 292 def remove_prefix(s, prefix):
292 293 if s.startswith(prefix):
293 294 s = s[len(prefix):]
294 295 return s
295 296
296 297
297 298 def find_calling_context(ignore_modules=None):
298 299 """
299 300 Look through the calling stack and return the frame which called
300 301 this function and is part of core module ( ie. rhodecode.* )
301 302
302 303 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
303 304 """
304 305
305 306 ignore_modules = ignore_modules or []
306 307
307 308 f = sys._getframe(2)
308 309 while f.f_back is not None:
309 310 name = f.f_globals.get('__name__')
310 311 if name and name.startswith(__name__.split('.')[0]):
311 312 if name not in ignore_modules:
312 313 return f
313 314 f = f.f_back
314 315 return None
315 316
316 317
317 318 def ping_connection(connection, branch):
318 319 if branch:
319 320 # "branch" refers to a sub-connection of a connection,
320 321 # we don't want to bother pinging on these.
321 322 return
322 323
323 324 # turn off "close with result". This flag is only used with
324 325 # "connectionless" execution, otherwise will be False in any case
325 326 save_should_close_with_result = connection.should_close_with_result
326 327 connection.should_close_with_result = False
327 328
328 329 try:
329 330 # run a SELECT 1. use a core select() so that
330 331 # the SELECT of a scalar value without a table is
331 332 # appropriately formatted for the backend
332 333 connection.scalar(sqlalchemy.sql.select([1]))
333 334 except sqlalchemy.exc.DBAPIError as err:
334 335 # catch SQLAlchemy's DBAPIError, which is a wrapper
335 336 # for the DBAPI's exception. It includes a .connection_invalidated
336 337 # attribute which specifies if this connection is a "disconnect"
337 338 # condition, which is based on inspection of the original exception
338 339 # by the dialect in use.
339 340 if err.connection_invalidated:
340 341 # run the same SELECT again - the connection will re-validate
341 342 # itself and establish a new connection. The disconnect detection
342 343 # here also causes the whole connection pool to be invalidated
343 344 # so that all stale connections are discarded.
344 345 connection.scalar(sqlalchemy.sql.select([1]))
345 346 else:
346 347 raise
347 348 finally:
348 349 # restore "close with result"
349 350 connection.should_close_with_result = save_should_close_with_result
350 351
351 352
352 353 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
353 354 """Custom engine_from_config functions."""
354 355 log = logging.getLogger('sqlalchemy.engine')
355 356 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
356 357
357 358 def color_sql(sql):
358 359 color_seq = '\033[1;33m' # This is yellow: code 33
359 360 normal = '\x1b[0m'
360 361 return ''.join([color_seq, sql, normal])
361 362
362 363 if configuration['debug']:
363 364 # attach events only for debug configuration
364 365
365 366 def before_cursor_execute(conn, cursor, statement,
366 367 parameters, context, executemany):
367 368 setattr(conn, 'query_start_time', time.time())
368 369 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
369 370 calling_context = find_calling_context(ignore_modules=[
370 371 'rhodecode.lib.caching_query',
371 372 'rhodecode.model.settings',
372 373 ])
373 374 if calling_context:
374 375 log.info(color_sql('call context %s:%s' % (
375 376 calling_context.f_code.co_filename,
376 377 calling_context.f_lineno,
377 378 )))
378 379
379 380 def after_cursor_execute(conn, cursor, statement,
380 381 parameters, context, executemany):
381 382 delattr(conn, 'query_start_time')
382 383
383 384 sqlalchemy.event.listen(engine, "engine_connect",
384 385 ping_connection)
385 386 sqlalchemy.event.listen(engine, "before_cursor_execute",
386 387 before_cursor_execute)
387 388 sqlalchemy.event.listen(engine, "after_cursor_execute",
388 389 after_cursor_execute)
389 390
390 391 return engine
391 392
392 393
393 394 def get_encryption_key(config):
394 395 secret = config.get('rhodecode.encrypted_values.secret')
395 396 default = config['beaker.session.secret']
396 397 return secret or default
397 398
398 399
399 400 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
400 401 short_format=False):
401 402 """
402 403 Turns a datetime into an age string.
403 404 If show_short_version is True, this generates a shorter string with
404 405 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
405 406
406 407 * IMPORTANT*
407 408 Code of this function is written in special way so it's easier to
408 409 backport it to javascript. If you mean to update it, please also update
409 410 `jquery.timeago-extension.js` file
410 411
411 412 :param prevdate: datetime object
412 413 :param now: get current time, if not define we use
413 414 `datetime.datetime.now()`
414 415 :param show_short_version: if it should approximate the date and
415 416 return a shorter string
416 417 :param show_suffix:
417 418 :param short_format: show short format, eg 2D instead of 2 days
418 419 :rtype: unicode
419 420 :returns: unicode words describing age
420 421 """
421 422
422 423 def _get_relative_delta(now, prevdate):
423 424 base = dateutil.relativedelta.relativedelta(now, prevdate)
424 425 return {
425 426 'year': base.years,
426 427 'month': base.months,
427 428 'day': base.days,
428 429 'hour': base.hours,
429 430 'minute': base.minutes,
430 431 'second': base.seconds,
431 432 }
432 433
433 434 def _is_leap_year(year):
434 435 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
435 436
436 437 def get_month(prevdate):
437 438 return prevdate.month
438 439
439 440 def get_year(prevdate):
440 441 return prevdate.year
441 442
442 443 now = now or datetime.datetime.now()
443 444 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
444 445 deltas = {}
445 446 future = False
446 447
447 448 if prevdate > now:
448 449 now_old = now
449 450 now = prevdate
450 451 prevdate = now_old
451 452 future = True
452 453 if future:
453 454 prevdate = prevdate.replace(microsecond=0)
454 455 # Get date parts deltas
455 456 for part in order:
456 457 rel_delta = _get_relative_delta(now, prevdate)
457 458 deltas[part] = rel_delta[part]
458 459
459 460 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
460 461 # not 1 hour, -59 minutes and -59 seconds)
461 462 offsets = [[5, 60], [4, 60], [3, 24]]
462 463 for element in offsets: # seconds, minutes, hours
463 464 num = element[0]
464 465 length = element[1]
465 466
466 467 part = order[num]
467 468 carry_part = order[num - 1]
468 469
469 470 if deltas[part] < 0:
470 471 deltas[part] += length
471 472 deltas[carry_part] -= 1
472 473
473 474 # Same thing for days except that the increment depends on the (variable)
474 475 # number of days in the month
475 476 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
476 477 if deltas['day'] < 0:
477 478 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
478 479 deltas['day'] += 29
479 480 else:
480 481 deltas['day'] += month_lengths[get_month(prevdate) - 1]
481 482
482 483 deltas['month'] -= 1
483 484
484 485 if deltas['month'] < 0:
485 486 deltas['month'] += 12
486 487 deltas['year'] -= 1
487 488
488 489 # Format the result
489 490 if short_format:
490 491 fmt_funcs = {
491 492 'year': lambda d: u'%dy' % d,
492 493 'month': lambda d: u'%dm' % d,
493 494 'day': lambda d: u'%dd' % d,
494 495 'hour': lambda d: u'%dh' % d,
495 496 'minute': lambda d: u'%dmin' % d,
496 497 'second': lambda d: u'%dsec' % d,
497 498 }
498 499 else:
499 500 fmt_funcs = {
500 501 'year': lambda d: _pluralize(u'${num} year', u'${num} years', d, mapping={'num': d}).interpolate(),
501 502 'month': lambda d: _pluralize(u'${num} month', u'${num} months', d, mapping={'num': d}).interpolate(),
502 503 'day': lambda d: _pluralize(u'${num} day', u'${num} days', d, mapping={'num': d}).interpolate(),
503 504 'hour': lambda d: _pluralize(u'${num} hour', u'${num} hours', d, mapping={'num': d}).interpolate(),
504 505 'minute': lambda d: _pluralize(u'${num} minute', u'${num} minutes', d, mapping={'num': d}).interpolate(),
505 506 'second': lambda d: _pluralize(u'${num} second', u'${num} seconds', d, mapping={'num': d}).interpolate(),
506 507 }
507 508
508 509 i = 0
509 510 for part in order:
510 511 value = deltas[part]
511 512 if value != 0:
512 513
513 514 if i < 5:
514 515 sub_part = order[i + 1]
515 516 sub_value = deltas[sub_part]
516 517 else:
517 518 sub_value = 0
518 519
519 520 if sub_value == 0 or show_short_version:
520 521 _val = fmt_funcs[part](value)
521 522 if future:
522 523 if show_suffix:
523 524 return _(u'in ${ago}', mapping={'ago': _val})
524 525 else:
525 526 return _(_val)
526 527
527 528 else:
528 529 if show_suffix:
529 530 return _(u'${ago} ago', mapping={'ago': _val})
530 531 else:
531 532 return _(_val)
532 533
533 534 val = fmt_funcs[part](value)
534 535 val_detail = fmt_funcs[sub_part](sub_value)
535 536 mapping = {'val': val, 'detail': val_detail}
536 537
537 538 if short_format:
538 539 datetime_tmpl = _(u'${val}, ${detail}', mapping=mapping)
539 540 if show_suffix:
540 541 datetime_tmpl = _(u'${val}, ${detail} ago', mapping=mapping)
541 542 if future:
542 543 datetime_tmpl = _(u'in ${val}, ${detail}', mapping=mapping)
543 544 else:
544 545 datetime_tmpl = _(u'${val} and ${detail}', mapping=mapping)
545 546 if show_suffix:
546 547 datetime_tmpl = _(u'${val} and ${detail} ago', mapping=mapping)
547 548 if future:
548 549 datetime_tmpl = _(u'in ${val} and ${detail}', mapping=mapping)
549 550
550 551 return datetime_tmpl
551 552 i += 1
552 553 return _(u'just now')
553 554
554 555
555 556 def cleaned_uri(uri):
556 557 """
557 558 Quotes '[' and ']' from uri if there is only one of them.
558 559 according to RFC3986 we cannot use such chars in uri
559 560 :param uri:
560 561 :return: uri without this chars
561 562 """
562 563 return urllib.quote(uri, safe='@$:/')
563 564
564 565
565 566 def uri_filter(uri):
566 567 """
567 568 Removes user:password from given url string
568 569
569 570 :param uri:
570 571 :rtype: unicode
571 572 :returns: filtered list of strings
572 573 """
573 574 if not uri:
574 575 return ''
575 576
576 577 proto = ''
577 578
578 579 for pat in ('https://', 'http://'):
579 580 if uri.startswith(pat):
580 581 uri = uri[len(pat):]
581 582 proto = pat
582 583 break
583 584
584 585 # remove passwords and username
585 586 uri = uri[uri.find('@') + 1:]
586 587
587 588 # get the port
588 589 cred_pos = uri.find(':')
589 590 if cred_pos == -1:
590 591 host, port = uri, None
591 592 else:
592 593 host, port = uri[:cred_pos], uri[cred_pos + 1:]
593 594
594 595 return filter(None, [proto, host, port])
595 596
596 597
597 598 def credentials_filter(uri):
598 599 """
599 600 Returns a url with removed credentials
600 601
601 602 :param uri:
602 603 """
603 604
604 605 uri = uri_filter(uri)
605 606 # check if we have port
606 607 if len(uri) > 2 and uri[2]:
607 608 uri[2] = ':' + uri[2]
608 609
609 610 return ''.join(uri)
610 611
611 612
612 613 def get_clone_url(request, uri_tmpl, repo_name, repo_id, **override):
613 614 qualifed_home_url = request.route_url('home')
614 615 parsed_url = urlobject.URLObject(qualifed_home_url)
615 616 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
617
616 618 args = {
617 619 'scheme': parsed_url.scheme,
618 620 'user': '',
621 'sys_user': getpass.getuser(),
619 622 # path if we use proxy-prefix
620 623 'netloc': parsed_url.netloc+decoded_path,
624 'hostname': parsed_url.hostname,
621 625 'prefix': decoded_path,
622 626 'repo': repo_name,
623 627 'repoid': str(repo_id)
624 628 }
625 629 args.update(override)
626 630 args['user'] = urllib.quote(safe_str(args['user']))
627 631
628 632 for k, v in args.items():
629 633 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
630 634
631 635 # remove leading @ sign if it's present. Case of empty user
632 636 url_obj = urlobject.URLObject(uri_tmpl)
633 637 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
634 638
635 639 return safe_unicode(url)
636 640
637 641
638 642 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None):
639 643 """
640 644 Safe version of get_commit if this commit doesn't exists for a
641 645 repository it returns a Dummy one instead
642 646
643 647 :param repo: repository instance
644 648 :param commit_id: commit id as str
645 649 :param pre_load: optional list of commit attributes to load
646 650 """
647 651 # TODO(skreft): remove these circular imports
648 652 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
649 653 from rhodecode.lib.vcs.exceptions import RepositoryError
650 654 if not isinstance(repo, BaseRepository):
651 655 raise Exception('You must pass an Repository '
652 656 'object as first argument got %s', type(repo))
653 657
654 658 try:
655 659 commit = repo.get_commit(
656 660 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
657 661 except (RepositoryError, LookupError):
658 662 commit = EmptyCommit()
659 663 return commit
660 664
661 665
662 666 def datetime_to_time(dt):
663 667 if dt:
664 668 return time.mktime(dt.timetuple())
665 669
666 670
667 671 def time_to_datetime(tm):
668 672 if tm:
669 673 if isinstance(tm, basestring):
670 674 try:
671 675 tm = float(tm)
672 676 except ValueError:
673 677 return
674 678 return datetime.datetime.fromtimestamp(tm)
675 679
676 680
677 681 def time_to_utcdatetime(tm):
678 682 if tm:
679 683 if isinstance(tm, basestring):
680 684 try:
681 685 tm = float(tm)
682 686 except ValueError:
683 687 return
684 688 return datetime.datetime.utcfromtimestamp(tm)
685 689
686 690
687 691 MENTIONS_REGEX = re.compile(
688 692 # ^@ or @ without any special chars in front
689 693 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
690 694 # main body starts with letter, then can be . - _
691 695 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
692 696 re.VERBOSE | re.MULTILINE)
693 697
694 698
695 699 def extract_mentioned_users(s):
696 700 """
697 701 Returns unique usernames from given string s that have @mention
698 702
699 703 :param s: string to get mentions
700 704 """
701 705 usrs = set()
702 706 for username in MENTIONS_REGEX.findall(s):
703 707 usrs.add(username)
704 708
705 709 return sorted(list(usrs), key=lambda k: k.lower())
706 710
707 711
708 712 class StrictAttributeDict(dict):
709 713 """
710 714 Strict Version of Attribute dict which raises an Attribute error when
711 715 requested attribute is not set
712 716 """
713 717 def __getattr__(self, attr):
714 718 try:
715 719 return self[attr]
716 720 except KeyError:
717 721 raise AttributeError('%s object has no attribute %s' % (
718 722 self.__class__, attr))
719 723 __setattr__ = dict.__setitem__
720 724 __delattr__ = dict.__delitem__
721 725
722 726
723 727 class AttributeDict(dict):
724 728 def __getattr__(self, attr):
725 729 return self.get(attr, None)
726 730 __setattr__ = dict.__setitem__
727 731 __delattr__ = dict.__delitem__
728 732
729 733
730 734 def fix_PATH(os_=None):
731 735 """
732 736 Get current active python path, and append it to PATH variable to fix
733 737 issues of subprocess calls and different python versions
734 738 """
735 739 if os_ is None:
736 740 import os
737 741 else:
738 742 os = os_
739 743
740 744 cur_path = os.path.split(sys.executable)[0]
741 745 if not os.environ['PATH'].startswith(cur_path):
742 746 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
743 747
744 748
745 749 def obfuscate_url_pw(engine):
746 750 _url = engine or ''
747 751 try:
748 752 _url = sqlalchemy.engine.url.make_url(engine)
749 753 if _url.password:
750 754 _url.password = 'XXXXX'
751 755 except Exception:
752 756 pass
753 757 return unicode(_url)
754 758
755 759
756 760 def get_server_url(environ):
757 761 req = webob.Request(environ)
758 762 return req.host_url + req.script_name
759 763
760 764
761 765 def unique_id(hexlen=32):
762 766 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
763 767 return suuid(truncate_to=hexlen, alphabet=alphabet)
764 768
765 769
766 770 def suuid(url=None, truncate_to=22, alphabet=None):
767 771 """
768 772 Generate and return a short URL safe UUID.
769 773
770 774 If the url parameter is provided, set the namespace to the provided
771 775 URL and generate a UUID.
772 776
773 777 :param url to get the uuid for
774 778 :truncate_to: truncate the basic 22 UUID to shorter version
775 779
776 780 The IDs won't be universally unique any longer, but the probability of
777 781 a collision will still be very low.
778 782 """
779 783 # Define our alphabet.
780 784 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
781 785
782 786 # If no URL is given, generate a random UUID.
783 787 if url is None:
784 788 unique_id = uuid.uuid4().int
785 789 else:
786 790 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
787 791
788 792 alphabet_length = len(_ALPHABET)
789 793 output = []
790 794 while unique_id > 0:
791 795 digit = unique_id % alphabet_length
792 796 output.append(_ALPHABET[digit])
793 797 unique_id = int(unique_id / alphabet_length)
794 798 return "".join(output)[:truncate_to]
795 799
796 800
797 801 def get_current_rhodecode_user(request=None):
798 802 """
799 803 Gets rhodecode user from request
800 804 """
801 805 pyramid_request = request or pyramid.threadlocal.get_current_request()
802 806
803 807 # web case
804 808 if pyramid_request and hasattr(pyramid_request, 'user'):
805 809 return pyramid_request.user
806 810
807 811 # api case
808 812 if pyramid_request and hasattr(pyramid_request, 'rpc_user'):
809 813 return pyramid_request.rpc_user
810 814
811 815 return None
812 816
813 817
814 818 def action_logger_generic(action, namespace=''):
815 819 """
816 820 A generic logger for actions useful to the system overview, tries to find
817 821 an acting user for the context of the call otherwise reports unknown user
818 822
819 823 :param action: logging message eg 'comment 5 deleted'
820 824 :param type: string
821 825
822 826 :param namespace: namespace of the logging message eg. 'repo.comments'
823 827 :param type: string
824 828
825 829 """
826 830
827 831 logger_name = 'rhodecode.actions'
828 832
829 833 if namespace:
830 834 logger_name += '.' + namespace
831 835
832 836 log = logging.getLogger(logger_name)
833 837
834 838 # get a user if we can
835 839 user = get_current_rhodecode_user()
836 840
837 841 logfunc = log.info
838 842
839 843 if not user:
840 844 user = '<unknown user>'
841 845 logfunc = log.warning
842 846
843 847 logfunc('Logging action by {}: {}'.format(user, action))
844 848
845 849
846 850 def escape_split(text, sep=',', maxsplit=-1):
847 851 r"""
848 852 Allows for escaping of the separator: e.g. arg='foo\, bar'
849 853
850 854 It should be noted that the way bash et. al. do command line parsing, those
851 855 single quotes are required.
852 856 """
853 857 escaped_sep = r'\%s' % sep
854 858
855 859 if escaped_sep not in text:
856 860 return text.split(sep, maxsplit)
857 861
858 862 before, _mid, after = text.partition(escaped_sep)
859 863 startlist = before.split(sep, maxsplit) # a regular split is fine here
860 864 unfinished = startlist[-1]
861 865 startlist = startlist[:-1]
862 866
863 867 # recurse because there may be more escaped separators
864 868 endlist = escape_split(after, sep, maxsplit)
865 869
866 870 # finish building the escaped value. we use endlist[0] becaue the first
867 871 # part of the string sent in recursion is the rest of the escaped value.
868 872 unfinished += sep + endlist[0]
869 873
870 874 return startlist + [unfinished] + endlist[1:] # put together all the parts
871 875
872 876
873 877 class OptionalAttr(object):
874 878 """
875 879 Special Optional Option that defines other attribute. Example::
876 880
877 881 def test(apiuser, userid=Optional(OAttr('apiuser')):
878 882 user = Optional.extract(userid)
879 883 # calls
880 884
881 885 """
882 886
883 887 def __init__(self, attr_name):
884 888 self.attr_name = attr_name
885 889
886 890 def __repr__(self):
887 891 return '<OptionalAttr:%s>' % self.attr_name
888 892
889 893 def __call__(self):
890 894 return self
891 895
892 896
893 897 # alias
894 898 OAttr = OptionalAttr
895 899
896 900
897 901 class Optional(object):
898 902 """
899 903 Defines an optional parameter::
900 904
901 905 param = param.getval() if isinstance(param, Optional) else param
902 906 param = param() if isinstance(param, Optional) else param
903 907
904 908 is equivalent of::
905 909
906 910 param = Optional.extract(param)
907 911
908 912 """
909 913
910 914 def __init__(self, type_):
911 915 self.type_ = type_
912 916
913 917 def __repr__(self):
914 918 return '<Optional:%s>' % self.type_.__repr__()
915 919
916 920 def __call__(self):
917 921 return self.getval()
918 922
919 923 def getval(self):
920 924 """
921 925 returns value from this Optional instance
922 926 """
923 927 if isinstance(self.type_, OAttr):
924 928 # use params name
925 929 return self.type_.attr_name
926 930 return self.type_
927 931
928 932 @classmethod
929 933 def extract(cls, val):
930 934 """
931 935 Extracts value from Optional() instance
932 936
933 937 :param val:
934 938 :return: original value if it's not Optional instance else
935 939 value of instance
936 940 """
937 941 if isinstance(val, cls):
938 942 return val.getval()
939 943 return val
940 944
941 945
942 946 def glob2re(pat):
943 947 """
944 948 Translate a shell PATTERN to a regular expression.
945 949
946 950 There is no way to quote meta-characters.
947 951 """
948 952
949 953 i, n = 0, len(pat)
950 954 res = ''
951 955 while i < n:
952 956 c = pat[i]
953 957 i = i+1
954 958 if c == '*':
955 959 #res = res + '.*'
956 960 res = res + '[^/]*'
957 961 elif c == '?':
958 962 #res = res + '.'
959 963 res = res + '[^/]'
960 964 elif c == '[':
961 965 j = i
962 966 if j < n and pat[j] == '!':
963 967 j = j+1
964 968 if j < n and pat[j] == ']':
965 969 j = j+1
966 970 while j < n and pat[j] != ']':
967 971 j = j+1
968 972 if j >= n:
969 973 res = res + '\\['
970 974 else:
971 975 stuff = pat[i:j].replace('\\','\\\\')
972 976 i = j+1
973 977 if stuff[0] == '!':
974 978 stuff = '^' + stuff[1:]
975 979 elif stuff[0] == '^':
976 980 stuff = '\\' + stuff
977 981 res = '%s[%s]' % (res, stuff)
978 982 else:
979 983 res = res + re.escape(c)
980 984 return res + '\Z(?ms)'
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,615 +1,616 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 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 pyramid.threadlocal import get_current_request
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 kw['request'] = get_current_request()
70 70 return self.load(template_name)(**kw)
71 71
72 72
73 73 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
74 74 deform.Form.set_default_renderer(form_renderer)
75 75
76 76
77 77 def LoginForm(localizer):
78 78 _ = localizer
79 79
80 80 class _LoginForm(formencode.Schema):
81 81 allow_extra_fields = True
82 82 filter_extra_fields = True
83 83 username = v.UnicodeString(
84 84 strip=True,
85 85 min=1,
86 86 not_empty=True,
87 87 messages={
88 88 'empty': _(u'Please enter a login'),
89 89 'tooShort': _(u'Enter a value %(min)i characters long or more')
90 90 }
91 91 )
92 92
93 93 password = v.UnicodeString(
94 94 strip=False,
95 95 min=3,
96 96 max=72,
97 97 not_empty=True,
98 98 messages={
99 99 'empty': _(u'Please enter a password'),
100 100 'tooShort': _(u'Enter %(min)i characters or more')}
101 101 )
102 102
103 103 remember = v.StringBoolean(if_missing=False)
104 104
105 105 chained_validators = [v.ValidAuth(localizer)]
106 106 return _LoginForm
107 107
108 108
109 109 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
110 110 old_data = old_data or {}
111 111 available_languages = available_languages or []
112 112 _ = localizer
113 113
114 114 class _UserForm(formencode.Schema):
115 115 allow_extra_fields = True
116 116 filter_extra_fields = True
117 117 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
118 118 v.ValidUsername(localizer, edit, old_data))
119 119 if edit:
120 120 new_password = All(
121 121 v.ValidPassword(localizer),
122 122 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
123 123 )
124 124 password_confirmation = All(
125 125 v.ValidPassword(localizer),
126 126 v.UnicodeString(strip=False, min=6, max=72, not_empty=False),
127 127 )
128 128 admin = v.StringBoolean(if_missing=False)
129 129 else:
130 130 password = All(
131 131 v.ValidPassword(localizer),
132 132 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
133 133 )
134 134 password_confirmation = All(
135 135 v.ValidPassword(localizer),
136 136 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
137 137 )
138 138
139 139 password_change = v.StringBoolean(if_missing=False)
140 140 create_repo_group = v.StringBoolean(if_missing=False)
141 141
142 142 active = v.StringBoolean(if_missing=False)
143 143 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
144 144 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
145 145 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
146 146 extern_name = v.UnicodeString(strip=True)
147 147 extern_type = v.UnicodeString(strip=True)
148 148 language = v.OneOf(available_languages, hideList=False,
149 149 testValueList=True, if_missing=None)
150 150 chained_validators = [v.ValidPasswordsMatch(localizer)]
151 151 return _UserForm
152 152
153 153
154 154 def UserGroupForm(localizer, edit=False, old_data=None, allow_disabled=False):
155 155 old_data = old_data or {}
156 156 _ = localizer
157 157
158 158 class _UserGroupForm(formencode.Schema):
159 159 allow_extra_fields = True
160 160 filter_extra_fields = True
161 161
162 162 users_group_name = All(
163 163 v.UnicodeString(strip=True, min=1, not_empty=True),
164 164 v.ValidUserGroup(localizer, edit, old_data)
165 165 )
166 166 user_group_description = v.UnicodeString(strip=True, min=1,
167 167 not_empty=False)
168 168
169 169 users_group_active = v.StringBoolean(if_missing=False)
170 170
171 171 if edit:
172 172 # this is user group owner
173 173 user = All(
174 174 v.UnicodeString(not_empty=True),
175 175 v.ValidRepoUser(localizer, allow_disabled))
176 176 return _UserGroupForm
177 177
178 178
179 179 def RepoGroupForm(localizer, edit=False, old_data=None, available_groups=None,
180 180 can_create_in_root=False, allow_disabled=False):
181 181 _ = localizer
182 182 old_data = old_data or {}
183 183 available_groups = available_groups or []
184 184
185 185 class _RepoGroupForm(formencode.Schema):
186 186 allow_extra_fields = True
187 187 filter_extra_fields = False
188 188
189 189 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
190 190 v.SlugifyName(localizer),)
191 191 group_description = v.UnicodeString(strip=True, min=1,
192 192 not_empty=False)
193 193 group_copy_permissions = v.StringBoolean(if_missing=False)
194 194
195 195 group_parent_id = v.OneOf(available_groups, hideList=False,
196 196 testValueList=True, not_empty=True)
197 197 enable_locking = v.StringBoolean(if_missing=False)
198 198 chained_validators = [
199 199 v.ValidRepoGroup(localizer, edit, old_data, can_create_in_root)]
200 200
201 201 if edit:
202 202 # this is repo group owner
203 203 user = All(
204 204 v.UnicodeString(not_empty=True),
205 205 v.ValidRepoUser(localizer, allow_disabled))
206 206 return _RepoGroupForm
207 207
208 208
209 209 def RegisterForm(localizer, edit=False, old_data=None):
210 210 _ = localizer
211 211 old_data = old_data or {}
212 212
213 213 class _RegisterForm(formencode.Schema):
214 214 allow_extra_fields = True
215 215 filter_extra_fields = True
216 216 username = All(
217 217 v.ValidUsername(localizer, edit, old_data),
218 218 v.UnicodeString(strip=True, min=1, not_empty=True)
219 219 )
220 220 password = All(
221 221 v.ValidPassword(localizer),
222 222 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
223 223 )
224 224 password_confirmation = All(
225 225 v.ValidPassword(localizer),
226 226 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
227 227 )
228 228 active = v.StringBoolean(if_missing=False)
229 229 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
230 230 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
231 231 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
232 232
233 233 chained_validators = [v.ValidPasswordsMatch(localizer)]
234 234 return _RegisterForm
235 235
236 236
237 237 def PasswordResetForm(localizer):
238 238 _ = localizer
239 239
240 240 class _PasswordResetForm(formencode.Schema):
241 241 allow_extra_fields = True
242 242 filter_extra_fields = True
243 243 email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
244 244 return _PasswordResetForm
245 245
246 246
247 247 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None,
248 248 landing_revs=None, allow_disabled=False):
249 249 _ = localizer
250 250 old_data = old_data or {}
251 251 repo_groups = repo_groups or []
252 252 landing_revs = landing_revs or []
253 253 supported_backends = BACKENDS.keys()
254 254
255 255 class _RepoForm(formencode.Schema):
256 256 allow_extra_fields = True
257 257 filter_extra_fields = False
258 258 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
259 259 v.SlugifyName(localizer), v.CannotHaveGitSuffix(localizer))
260 260 repo_group = All(v.CanWriteGroup(localizer, old_data),
261 261 v.OneOf(repo_groups, hideList=True))
262 262 repo_type = v.OneOf(supported_backends, required=False,
263 263 if_missing=old_data.get('repo_type'))
264 264 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
265 265 repo_private = v.StringBoolean(if_missing=False)
266 266 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
267 267 repo_copy_permissions = v.StringBoolean(if_missing=False)
268 268 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
269 269
270 270 repo_enable_statistics = v.StringBoolean(if_missing=False)
271 271 repo_enable_downloads = v.StringBoolean(if_missing=False)
272 272 repo_enable_locking = v.StringBoolean(if_missing=False)
273 273
274 274 if edit:
275 275 # this is repo owner
276 276 user = All(
277 277 v.UnicodeString(not_empty=True),
278 278 v.ValidRepoUser(localizer, allow_disabled))
279 279 clone_uri_change = v.UnicodeString(
280 280 not_empty=False, if_missing=v.Missing)
281 281
282 282 chained_validators = [v.ValidCloneUri(localizer),
283 283 v.ValidRepoName(localizer, edit, old_data)]
284 284 return _RepoForm
285 285
286 286
287 287 def RepoPermsForm(localizer):
288 288 _ = localizer
289 289
290 290 class _RepoPermsForm(formencode.Schema):
291 291 allow_extra_fields = True
292 292 filter_extra_fields = False
293 293 chained_validators = [v.ValidPerms(localizer, type_='repo')]
294 294 return _RepoPermsForm
295 295
296 296
297 297 def RepoGroupPermsForm(localizer, valid_recursive_choices):
298 298 _ = localizer
299 299
300 300 class _RepoGroupPermsForm(formencode.Schema):
301 301 allow_extra_fields = True
302 302 filter_extra_fields = False
303 303 recursive = v.OneOf(valid_recursive_choices)
304 304 chained_validators = [v.ValidPerms(localizer, type_='repo_group')]
305 305 return _RepoGroupPermsForm
306 306
307 307
308 308 def UserGroupPermsForm(localizer):
309 309 _ = localizer
310 310
311 311 class _UserPermsForm(formencode.Schema):
312 312 allow_extra_fields = True
313 313 filter_extra_fields = False
314 314 chained_validators = [v.ValidPerms(localizer, type_='user_group')]
315 315 return _UserPermsForm
316 316
317 317
318 318 def RepoFieldForm(localizer):
319 319 _ = localizer
320 320
321 321 class _RepoFieldForm(formencode.Schema):
322 322 filter_extra_fields = True
323 323 allow_extra_fields = True
324 324
325 325 new_field_key = All(v.FieldKey(localizer),
326 326 v.UnicodeString(strip=True, min=3, not_empty=True))
327 327 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
328 328 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
329 329 if_missing='str')
330 330 new_field_label = v.UnicodeString(not_empty=False)
331 331 new_field_desc = v.UnicodeString(not_empty=False)
332 332 return _RepoFieldForm
333 333
334 334
335 335 def RepoForkForm(localizer, edit=False, old_data=None,
336 336 supported_backends=BACKENDS.keys(), repo_groups=None,
337 337 landing_revs=None):
338 338 _ = localizer
339 339 old_data = old_data or {}
340 340 repo_groups = repo_groups or []
341 341 landing_revs = landing_revs or []
342 342
343 343 class _RepoForkForm(formencode.Schema):
344 344 allow_extra_fields = True
345 345 filter_extra_fields = False
346 346 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
347 347 v.SlugifyName(localizer))
348 348 repo_group = All(v.CanWriteGroup(localizer, ),
349 349 v.OneOf(repo_groups, hideList=True))
350 350 repo_type = All(v.ValidForkType(localizer, old_data), v.OneOf(supported_backends))
351 351 description = v.UnicodeString(strip=True, min=1, not_empty=True)
352 352 private = v.StringBoolean(if_missing=False)
353 353 copy_permissions = v.StringBoolean(if_missing=False)
354 354 fork_parent_id = v.UnicodeString()
355 355 chained_validators = [v.ValidForkName(localizer, edit, old_data)]
356 356 landing_rev = v.OneOf(landing_revs, hideList=True)
357 357 return _RepoForkForm
358 358
359 359
360 360 def ApplicationSettingsForm(localizer):
361 361 _ = localizer
362 362
363 363 class _ApplicationSettingsForm(formencode.Schema):
364 364 allow_extra_fields = True
365 365 filter_extra_fields = False
366 366 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
367 367 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
368 368 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
369 369 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
370 370 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
371 371 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
372 372 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
373 373 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
374 374 return _ApplicationSettingsForm
375 375
376 376
377 377 def ApplicationVisualisationForm(localizer):
378 378 _ = localizer
379 379
380 380 class _ApplicationVisualisationForm(formencode.Schema):
381 381 allow_extra_fields = True
382 382 filter_extra_fields = False
383 383 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
384 384 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
385 385 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
386 386
387 387 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
388 388 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
389 389 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
390 390 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
391 391 rhodecode_show_version = v.StringBoolean(if_missing=False)
392 392 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
393 393 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
394 394 rhodecode_gravatar_url = v.UnicodeString(min=3)
395 395 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
396 rhodecode_clone_uri_ssh_tmpl = v.UnicodeString(min=3)
396 397 rhodecode_support_url = v.UnicodeString()
397 398 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
398 399 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
399 400 return _ApplicationVisualisationForm
400 401
401 402
402 403 class _BaseVcsSettingsForm(formencode.Schema):
403 404
404 405 allow_extra_fields = True
405 406 filter_extra_fields = False
406 407 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
407 408 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
408 409 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
409 410
410 411 # PR/Code-review
411 412 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
412 413 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
413 414
414 415 # hg
415 416 extensions_largefiles = v.StringBoolean(if_missing=False)
416 417 extensions_evolve = v.StringBoolean(if_missing=False)
417 418 phases_publish = v.StringBoolean(if_missing=False)
418 419
419 420 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
420 421 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
421 422
422 423 # git
423 424 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
424 425 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
425 426 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
426 427
427 428 # svn
428 429 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
429 430 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
430 431
431 432
432 433 def ApplicationUiSettingsForm(localizer):
433 434 _ = localizer
434 435
435 436 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
436 437 web_push_ssl = v.StringBoolean(if_missing=False)
437 438 paths_root_path = All(
438 439 v.ValidPath(localizer),
439 440 v.UnicodeString(strip=True, min=1, not_empty=True)
440 441 )
441 442 largefiles_usercache = All(
442 443 v.ValidPath(localizer),
443 444 v.UnicodeString(strip=True, min=2, not_empty=True))
444 445 vcs_git_lfs_store_location = All(
445 446 v.ValidPath(localizer),
446 447 v.UnicodeString(strip=True, min=2, not_empty=True))
447 448 extensions_hgsubversion = v.StringBoolean(if_missing=False)
448 449 extensions_hggit = v.StringBoolean(if_missing=False)
449 450 new_svn_branch = v.ValidSvnPattern(localizer, section='vcs_svn_branch')
450 451 new_svn_tag = v.ValidSvnPattern(localizer, section='vcs_svn_tag')
451 452 return _ApplicationUiSettingsForm
452 453
453 454
454 455 def RepoVcsSettingsForm(localizer, repo_name):
455 456 _ = localizer
456 457
457 458 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
458 459 inherit_global_settings = v.StringBoolean(if_missing=False)
459 460 new_svn_branch = v.ValidSvnPattern(localizer,
460 461 section='vcs_svn_branch', repo_name=repo_name)
461 462 new_svn_tag = v.ValidSvnPattern(localizer,
462 463 section='vcs_svn_tag', repo_name=repo_name)
463 464 return _RepoVcsSettingsForm
464 465
465 466
466 467 def LabsSettingsForm(localizer):
467 468 _ = localizer
468 469
469 470 class _LabSettingsForm(formencode.Schema):
470 471 allow_extra_fields = True
471 472 filter_extra_fields = False
472 473 return _LabSettingsForm
473 474
474 475
475 476 def ApplicationPermissionsForm(
476 477 localizer, register_choices, password_reset_choices,
477 478 extern_activate_choices):
478 479 _ = localizer
479 480
480 481 class _DefaultPermissionsForm(formencode.Schema):
481 482 allow_extra_fields = True
482 483 filter_extra_fields = True
483 484
484 485 anonymous = v.StringBoolean(if_missing=False)
485 486 default_register = v.OneOf(register_choices)
486 487 default_register_message = v.UnicodeString()
487 488 default_password_reset = v.OneOf(password_reset_choices)
488 489 default_extern_activate = v.OneOf(extern_activate_choices)
489 490 return _DefaultPermissionsForm
490 491
491 492
492 493 def ObjectPermissionsForm(localizer, repo_perms_choices, group_perms_choices,
493 494 user_group_perms_choices):
494 495 _ = localizer
495 496
496 497 class _ObjectPermissionsForm(formencode.Schema):
497 498 allow_extra_fields = True
498 499 filter_extra_fields = True
499 500 overwrite_default_repo = v.StringBoolean(if_missing=False)
500 501 overwrite_default_group = v.StringBoolean(if_missing=False)
501 502 overwrite_default_user_group = v.StringBoolean(if_missing=False)
502 503 default_repo_perm = v.OneOf(repo_perms_choices)
503 504 default_group_perm = v.OneOf(group_perms_choices)
504 505 default_user_group_perm = v.OneOf(user_group_perms_choices)
505 506 return _ObjectPermissionsForm
506 507
507 508
508 509 def UserPermissionsForm(localizer, create_choices, create_on_write_choices,
509 510 repo_group_create_choices, user_group_create_choices,
510 511 fork_choices, inherit_default_permissions_choices):
511 512 _ = localizer
512 513
513 514 class _DefaultPermissionsForm(formencode.Schema):
514 515 allow_extra_fields = True
515 516 filter_extra_fields = True
516 517
517 518 anonymous = v.StringBoolean(if_missing=False)
518 519
519 520 default_repo_create = v.OneOf(create_choices)
520 521 default_repo_create_on_write = v.OneOf(create_on_write_choices)
521 522 default_user_group_create = v.OneOf(user_group_create_choices)
522 523 default_repo_group_create = v.OneOf(repo_group_create_choices)
523 524 default_fork_create = v.OneOf(fork_choices)
524 525 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
525 526 return _DefaultPermissionsForm
526 527
527 528
528 529 def UserIndividualPermissionsForm(localizer):
529 530 _ = localizer
530 531
531 532 class _DefaultPermissionsForm(formencode.Schema):
532 533 allow_extra_fields = True
533 534 filter_extra_fields = True
534 535
535 536 inherit_default_permissions = v.StringBoolean(if_missing=False)
536 537 return _DefaultPermissionsForm
537 538
538 539
539 540 def DefaultsForm(localizer, edit=False, old_data=None, supported_backends=BACKENDS.keys()):
540 541 _ = localizer
541 542 old_data = old_data or {}
542 543
543 544 class _DefaultsForm(formencode.Schema):
544 545 allow_extra_fields = True
545 546 filter_extra_fields = True
546 547 default_repo_type = v.OneOf(supported_backends)
547 548 default_repo_private = v.StringBoolean(if_missing=False)
548 549 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
549 550 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
550 551 default_repo_enable_locking = v.StringBoolean(if_missing=False)
551 552 return _DefaultsForm
552 553
553 554
554 555 def AuthSettingsForm(localizer):
555 556 _ = localizer
556 557
557 558 class _AuthSettingsForm(formencode.Schema):
558 559 allow_extra_fields = True
559 560 filter_extra_fields = True
560 561 auth_plugins = All(v.ValidAuthPlugins(localizer),
561 562 v.UniqueListFromString(localizer)(not_empty=True))
562 563 return _AuthSettingsForm
563 564
564 565
565 566 def UserExtraEmailForm(localizer):
566 567 _ = localizer
567 568
568 569 class _UserExtraEmailForm(formencode.Schema):
569 570 email = All(v.UniqSystemEmail(localizer), v.Email(not_empty=True))
570 571 return _UserExtraEmailForm
571 572
572 573
573 574 def UserExtraIpForm(localizer):
574 575 _ = localizer
575 576
576 577 class _UserExtraIpForm(formencode.Schema):
577 578 ip = v.ValidIp(localizer)(not_empty=True)
578 579 return _UserExtraIpForm
579 580
580 581
581 582 def PullRequestForm(localizer, repo_id):
582 583 _ = localizer
583 584
584 585 class ReviewerForm(formencode.Schema):
585 586 user_id = v.Int(not_empty=True)
586 587 reasons = All()
587 588 rules = All(v.UniqueList(localizer, convert=int)())
588 589 mandatory = v.StringBoolean()
589 590
590 591 class _PullRequestForm(formencode.Schema):
591 592 allow_extra_fields = True
592 593 filter_extra_fields = True
593 594
594 595 common_ancestor = v.UnicodeString(strip=True, required=True)
595 596 source_repo = v.UnicodeString(strip=True, required=True)
596 597 source_ref = v.UnicodeString(strip=True, required=True)
597 598 target_repo = v.UnicodeString(strip=True, required=True)
598 599 target_ref = v.UnicodeString(strip=True, required=True)
599 600 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
600 601 v.UniqueList(localizer)(not_empty=True))
601 602 review_members = formencode.ForEach(ReviewerForm())
602 603 pullrequest_title = v.UnicodeString(strip=True, required=True, min=3, max=255)
603 604 pullrequest_desc = v.UnicodeString(strip=True, required=False)
604 605
605 606 return _PullRequestForm
606 607
607 608
608 609 def IssueTrackerPatternsForm(localizer):
609 610 _ = localizer
610 611
611 612 class _IssueTrackerPatternsForm(formencode.Schema):
612 613 allow_extra_fields = True
613 614 filter_extra_fields = False
614 615 chained_validators = [v.ValidPattern(localizer)]
615 616 return _IssueTrackerPatternsForm
@@ -1,270 +1,278 b''
1 1 // summary.less
2 2 // For use in RhodeCode applications;
3 3 // Used for headers and file detail summary screens.
4 4
5 5 .summary {
6 6 float: left;
7 7 position: relative;
8 8 width: 100%;
9 9 margin: 0;
10 10 padding: 0;
11 11
12 12 .summary-detail-header {
13 13 float: left;
14 14 display: block;
15 15 width: 100%;
16 16 margin-bottom: @textmargin;
17 17 padding: 0 0 .5em 0;
18 18 border-bottom: @border-thickness solid @border-default-color;
19 19
20 20 .breadcrumbs {
21 21 float: left;
22 22 display: inline;
23 23 margin: 0;
24 24 padding: 0;
25 25 }
26 26 h4 {
27 27 float: left;
28 28 margin: 0 1em 0 0;
29 29 padding: 0;
30 30 line-height: 1.2em;
31 31 font-size: @basefontsize;
32 32 }
33 33
34 34 .action_link {
35 35 float: right;
36 36 }
37 37
38 38 .new-file {
39 39 float: right;
40 40 margin-top: -1.5em;
41 41 }
42 42 }
43 43
44 44 .summary-detail {
45 45 float: left;
46 46 position: relative;
47 47 width: 73%;
48 48 margin: 0 3% @space 0;
49 49 padding: 0;
50 50
51 51 .file_diff_buttons {
52 52 margin-top: @space;
53 53 }
54 54
55 55 // commit message
56 56 .commit {
57 57 white-space: pre-wrap;
58 58 }
59 59
60 #clone_url {
61 width: ~"calc(100% - 103px)";
62 padding: @padding/4;
60 .left-clone {
61 float: left;
62 height: 30px;
63 margin: 0;
64 padding: 0;
65 font-family: @text-semibold;
63 66 }
64 67
65 #clone_url_id {
66 width: ~"calc(100% - 125px)";
67 padding: @padding/4;
68 .right-clone {
69 float: right;
70 width: 83%;
71 }
72
73 .clone_url_input {
74 width: ~"calc(100% - 35px)";
75 padding: 5px;
68 76 }
69 77
70 78 &.directory {
71 79 margin-bottom: 0;
72 80 }
73 81
74 82 .desc {
75 83 white-space: pre-wrap;
76 84 }
77 85 .disabled {
78 86 opacity: .5;
79 87 cursor: inherit;
80 88 }
81 89 .help-block {
82 90 color: inherit;
83 91 margin: 0;
84 92 }
85 93 }
86 94
87 95 .sidebar-right {
88 96 float: left;
89 97 width: 24%;
90 98 margin: 0;
91 99 padding: 0;
92 100
93 101 ul {
94 102 margin-left: 0;
95 103 padding-left: 0;
96 104
97 105 li {
98 106
99 107 &:before {
100 108 content: none;
101 109 width: 0;
102 110 }
103 111 }
104 112 }
105 113 }
106 114
107 115 #clone_by_name, #clone_by_id{
108 116 display: inline-block;
109 117 margin-left: 0px;
110 118 }
111 119
112 120 .codeblock {
113 121 border: none;
114 122 background-color: transparent;
115 123 }
116 124
117 125 .code-body {
118 126 border: @border-thickness solid @border-default-color;
119 127 .border-radius(@border-radius);
120 128 }
121 129 }
122 130
123 131 // this is used outside of just the summary
124 132 .fieldset, // similar to form fieldset
125 133 .summary .sidebar-right-content { // these have to match
126 134 clear: both;
127 135 float: left;
128 136 position: relative;
129 137 display:block;
130 138 width: 100%;
131 139 min-height: 1em;
132 140 margin-bottom: @textmargin;
133 141 padding: 0;
134 142 line-height: 1.2em;
135 143
136 144 &:after { // clearfix
137 145 content: "";
138 146 clear: both;
139 147 width: 100%;
140 148 height: 1em;
141 149 }
142 150 }
143 151
144 152 .summary .sidebar-right-content {
145 153 margin-bottom: @space;
146 154
147 155 .rc-user {
148 156 min-width: 0;
149 157 }
150 158 }
151 159
152 160 .fieldset {
153 161
154 162 .left-label { // similar to form legend
155 163 float: left;
156 164 display: block;
157 165 width: 25%;
158 166 margin: 0;
159 167 padding: 0;
160 168 font-family: @text-semibold;
161 169 }
162 170
163 171 .right-content { // similar to form fields
164 172 float: left;
165 173 display: block;
166 174 width: 75%;
167 175 margin: 0 0 0 -15%;
168 176 padding: 0 0 0 15%;
169 177
170 178 .truncate-wrap,
171 179 .truncate {
172 180 max-width: 100%;
173 181 width: 100%;
174 182 }
175 183
176 184 .commit-long {
177 185 overflow-x: auto;
178 186 }
179 187 }
180 188 .commit.truncate-wrap {
181 189 overflow:hidden;
182 190 text-overflow: ellipsis;
183 191 }
184 192 }
185 193
186 194 // expand commit message
187 195 #message_expand {
188 196 clear: both;
189 197 display: block;
190 198 color: @rcblue;
191 199 cursor: pointer;
192 200 }
193 201
194 202 #trimmed_message_box {
195 203 max-height: floor(2 * @basefontsize * 1.2); // 2 lines * line-height
196 204 overflow: hidden;
197 205 }
198 206
199 207 // show/hide comments button
200 208 .show-inline-comments {
201 209 display: inline;
202 210 cursor: pointer;
203 211
204 212 .comments-show { display: inline; }
205 213 .comments-hide { display: none; }
206 214
207 215 &.comments-visible {
208 216 .comments-show { display: none; }
209 217 .comments-hide { display: inline; }
210 218 }
211 219 }
212 220
213 221 // Quick Start section
214 222 .quick_start {
215 223 float: left;
216 224 display: block;
217 225 position: relative;
218 226 width: 100%;
219 227
220 228 // adds some space to make copy and paste easier
221 229 .left-label,
222 230 .right-content {
223 231 line-height: 1.6em;
224 232 }
225 233 }
226 234
227 235 .submodule {
228 236 .summary-detail {
229 237 width: 100%;
230 238
231 239 .btn-collapse {
232 240 display: none;
233 241 }
234 242 }
235 243 }
236 244
237 245 .codeblock-header {
238 246 float: left;
239 247 display: block;
240 248 width: 100%;
241 249 margin: 0;
242 250 padding: @space 0 @padding 0;
243 251 border-top: @border-thickness solid @border-default-color;
244 252
245 253 .stats {
246 254 float: left;
247 255 width: 50%;
248 256 }
249 257
250 258 .buttons {
251 259 float: right;
252 260 width: 50%;
253 261 text-align: right;
254 262 color: @grey4;
255 263 }
256 264 }
257 265
258 266 #summary-menu-stats {
259 267
260 268 .stats-bullet {
261 269 color: @grey3;
262 270 min-width: 3em;
263 271 }
264 272
265 273 .repo-size {
266 274 margin-bottom: .5em;
267 275 }
268 276
269 277 }
270 278
@@ -1,226 +1,230 b''
1 1 ${h.secure_form(h.route_path('admin_settings_visual_update'), request=request)}
2 2
3 3 <div class="panel panel-default">
4 4 <div class="panel-heading" id="general">
5 5 <h3 class="panel-title">${_('General')}</h3>
6 6 </div>
7 7 <div class="panel-body">
8 8 <div class="checkbox">
9 9 ${h.checkbox('rhodecode_repository_fields','True')}
10 10 <label for="rhodecode_repository_fields">${_('Use repository extra fields')}</label>
11 11 </div>
12 12 <span class="help-block">${_('Allows storing additional customized fields per repository.')}</span>
13 13
14 14 <div></div>
15 15 <div class="checkbox">
16 16 ${h.checkbox('rhodecode_show_version','True')}
17 17 <label for="rhodecode_show_version">${_('Show RhodeCode version')}</label>
18 18 </div>
19 19 <span class="help-block">${_('Shows or hides a version number of RhodeCode displayed in the footer.')}</span>
20 20 </div>
21 21 </div>
22 22
23 23
24 24 <div class="panel panel-default">
25 25 <div class="panel-heading" id="gravatars">
26 26 <h3 class="panel-title">${_('Gravatars')}</h3>
27 27 </div>
28 28 <div class="panel-body">
29 29 <div class="checkbox">
30 30 ${h.checkbox('rhodecode_use_gravatar','True')}
31 31 <label for="rhodecode_use_gravatar">${_('Use Gravatars based avatars')}</label>
32 32 </div>
33 33 <span class="help-block">${_('Use gravatar.com as avatar system for RhodeCode accounts. If this is disabled avatars are generated based on initials and email.')}</span>
34 34
35 35 <div class="label">
36 36 <label for="rhodecode_gravatar_url">${_('Gravatar URL')}</label>
37 37 </div>
38 38 <div class="input">
39 39 <div class="field">
40 40 ${h.text('rhodecode_gravatar_url', size='100%')}
41 41 </div>
42 42
43 43 <div class="field">
44 44 <span class="help-block">${_('''Gravatar url allows you to use other avatar server application.
45 45 Following variables of the URL will be replaced accordingly.
46 46 {scheme} 'http' or 'https' sent from running RhodeCode server,
47 47 {email} user email,
48 48 {md5email} md5 hash of the user email (like at gravatar.com),
49 49 {size} size of the image that is expected from the server application,
50 50 {netloc} network location/server host of running RhodeCode server''')}</span>
51 51 </div>
52 52 </div>
53 53 </div>
54 54 </div>
55 55
56 56
57 57 <div class="panel panel-default">
58 58 <div class="panel-heading" id="meta-tagging">
59 59 <h3 class="panel-title">${_('Meta-Tagging')}</h3>
60 60 </div>
61 61 <div class="panel-body">
62 62 <div class="checkbox">
63 63 ${h.checkbox('rhodecode_stylify_metatags','True')}
64 64 <label for="rhodecode_stylify_metatags">${_('Stylify recognised meta tags')}</label>
65 65 </div>
66 66 <span class="help-block">${_('Parses meta tags from repository or repository group description fields and turns them into colored tags.')}</span>
67 67 <div>
68 68 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
69 69 ${dt.metatags_help()}
70 70 </div>
71 71 </div>
72 72 </div>
73 73
74 74
75 75 <div class="panel panel-default">
76 76 <div class="panel-heading">
77 77 <h3 class="panel-title">${_('Dashboard Items')}</h3>
78 78 </div>
79 79 <div class="panel-body">
80 80 <div class="label">
81 81 <label for="rhodecode_dashboard_items">${_('Main page dashboard items')}</label>
82 82 </div>
83 83 <div class="field input">
84 84 ${h.text('rhodecode_dashboard_items',size=5)}
85 85 </div>
86 86 <div class="field">
87 87 <span class="help-block">${_('Number of items displayed in the main page dashboard before pagination is shown.')}</span>
88 88 </div>
89 89
90 90 <div class="label">
91 91 <label for="rhodecode_admin_grid_items">${_('Admin pages items')}</label>
92 92 </div>
93 93 <div class="field input">
94 94 ${h.text('rhodecode_admin_grid_items',size=5)}
95 95 </div>
96 96 <div class="field">
97 97 <span class="help-block">${_('Number of items displayed in the admin pages grids before pagination is shown.')}</span>
98 98 </div>
99 99 </div>
100 100 </div>
101 101
102 102
103 103
104 104 <div class="panel panel-default">
105 105 <div class="panel-heading" id="commit-id">
106 106 <h3 class="panel-title">${_('Commit ID Style')}</h3>
107 107 </div>
108 108 <div class="panel-body">
109 109 <div class="label">
110 110 <label for="rhodecode_show_sha_length">${_('Commit sha length')}</label>
111 111 </div>
112 112 <div class="input">
113 113 <div class="field">
114 114 ${h.text('rhodecode_show_sha_length',size=5)}
115 115 </div>
116 116 <div class="field">
117 117 <span class="help-block">${_('''Number of chars to show in commit sha displayed in web interface.
118 118 By default it's shown as r123:9043a6a4c226 this value defines the
119 119 length of the sha after the `r123:` part.''')}</span>
120 120 </div>
121 121 </div>
122 122
123 123 <div class="checkbox">
124 124 ${h.checkbox('rhodecode_show_revision_number','True')}
125 125 <label for="rhodecode_show_revision_number">${_('Show commit ID numeric reference')} / ${_('Commit show revision number')}</label>
126 126 </div>
127 127 <span class="help-block">${_('''Show revision number in commit sha displayed in web interface.
128 128 By default it's shown as r123:9043a6a4c226 this value defines the
129 129 if the `r123:` part is shown.''')}</span>
130 130 </div>
131 131 </div>
132 132
133 133
134 134 <div class="panel panel-default">
135 135 <div class="panel-heading" id="icons">
136 136 <h3 class="panel-title">${_('Icons')}</h3>
137 137 </div>
138 138 <div class="panel-body">
139 139 <div class="checkbox">
140 140 ${h.checkbox('rhodecode_show_public_icon','True')}
141 141 <label for="rhodecode_show_public_icon">${_('Show public repo icon on repositories')}</label>
142 142 </div>
143 143 <div></div>
144 144
145 145 <div class="checkbox">
146 146 ${h.checkbox('rhodecode_show_private_icon','True')}
147 147 <label for="rhodecode_show_private_icon">${_('Show private repo icon on repositories')}</label>
148 148 </div>
149 149 <span class="help-block">${_('Show public/private icons next to repositories names.')}</span>
150 150 </div>
151 151 </div>
152 152
153 153
154 154 <div class="panel panel-default">
155 155 <div class="panel-heading">
156 156 <h3 class="panel-title">${_('Markup Renderer')}</h3>
157 157 </div>
158 158 <div class="panel-body">
159 159 <div class="field select">
160 160 ${h.select('rhodecode_markup_renderer', '', ['rst', 'markdown'])}
161 161 </div>
162 162 <div class="field">
163 163 <span class="help-block">${_('Default renderer used to render comments, pull request descriptions and other description elements. After change old entries will still work correctly.')}</span>
164 164 </div>
165 165 </div>
166 166 </div>
167 167
168 168 <div class="panel panel-default">
169 169 <div class="panel-heading">
170 <h3 class="panel-title">${_('Clone URL')}</h3>
170 <h3 class="panel-title">${_('Clone URL templates')}</h3>
171 171 </div>
172 172 <div class="panel-body">
173 173 <div class="field">
174 ${h.text('rhodecode_clone_uri_tmpl', size=60)}
174 ${h.text('rhodecode_clone_uri_tmpl', size=60)} HTTP[S]
175 175 </div>
176
176 <div class="field">
177 ${h.text('rhodecode_clone_uri_ssh_tmpl', size=60)} SSH
178 </div>
177 179 <div class="field">
178 180 <span class="help-block">
179 181 ${_('''Schema of clone url construction eg. '{scheme}://{user}@{netloc}/{repo}', available vars:
180 182 {scheme} 'http' or 'https' sent from running RhodeCode server,
181 183 {user} current user username,
184 {sys_user} current system user running this process, usefull for ssh,
185 {hostname} hostname of this server running RhodeCode,
182 186 {netloc} network location/server host of running RhodeCode server,
183 187 {repo} full repository name,
184 188 {repoid} ID of repository, can be used to contruct clone-by-id''')}
185 189 </span>
186 190 </div>
187 191 </div>
188 192 </div>
189 193
190 194 <div class="panel panel-default">
191 195 <div class="panel-heading">
192 196 <h3 class="panel-title">${_('Custom Support Link')}</h3>
193 197 </div>
194 198 <div class="panel-body">
195 199 <div class="field">
196 200 ${h.text('rhodecode_support_url', size=60)}
197 201 </div>
198 202 <div class="field">
199 203 <span class="help-block">
200 204 ${_('''Custom url for the support link located at the bottom.
201 205 The default is set to %(default_url)s. In case there's a need
202 206 to change the support link to internal issue tracker, it should be done here.
203 207 ''') % {'default_url': h.route_url('rhodecode_support')}}
204 208 </span>
205 209 </div>
206 210 </div>
207 211 </div>
208 212
209 213 <div class="buttons">
210 214 ${h.submit('save',_('Save settings'),class_="btn")}
211 215 ${h.reset('reset',_('Reset'),class_="btn")}
212 216 </div>
213 217
214 218
215 219 ${h.end_form()}
216 220
217 221 <script>
218 222 $(document).ready(function() {
219 223 $('#rhodecode_markup_renderer').select2({
220 224 containerCssClass: 'drop-menu',
221 225 dropdownCssClass: 'drop-menu-dropdown',
222 226 dropdownAutoWidth: true,
223 227 minimumResultsForSearch: -1
224 228 });
225 229 });
226 230 </script>
@@ -1,210 +1,214 b''
1 1 <%def name="refs_counters(branches, closed_branches, tags, bookmarks)">
2 2 <span class="branchtag tag">
3 3 <a href="${h.route_path('branches_home',repo_name=c.repo_name)}" class="childs">
4 4 <i class="icon-branch"></i>${_ungettext(
5 5 '%(num)s Branch','%(num)s Branches', len(branches)) % {'num': len(branches)}}</a>
6 6 </span>
7 7
8 8 %if closed_branches:
9 9 <span class="branchtag tag">
10 10 <a href="${h.route_path('branches_home',repo_name=c.repo_name)}" class="childs">
11 11 <i class="icon-branch"></i>${_ungettext(
12 12 '%(num)s Closed Branch', '%(num)s Closed Branches', len(closed_branches)) % {'num': len(closed_branches)}}</a>
13 13 </span>
14 14 %endif
15 15
16 16 <span class="tagtag tag">
17 17 <a href="${h.route_path('tags_home',repo_name=c.repo_name)}" class="childs">
18 18 <i class="icon-tag"></i>${_ungettext(
19 19 '%(num)s Tag', '%(num)s Tags', len(tags)) % {'num': len(tags)}}</a>
20 20 </span>
21 21
22 22 %if bookmarks:
23 23 <span class="booktag tag">
24 24 <a href="${h.route_path('bookmarks_home',repo_name=c.repo_name)}" class="childs">
25 25 <i class="icon-bookmark"></i>${_ungettext(
26 26 '%(num)s Bookmark', '%(num)s Bookmarks', len(bookmarks)) % {'num': len(bookmarks)}}</a>
27 27 </span>
28 28 %endif
29 29 </%def>
30 30
31 31 <%def name="summary_detail(breadcrumbs_links, show_downloads=True)">
32 32 <% summary = lambda n:{False:'summary-short'}.get(n) %>
33 33
34 34 <div id="summary-menu-stats" class="summary-detail">
35 35 <div class="summary-detail-header">
36 36 <div class="breadcrumbs files_location">
37 37 <h4>
38 38 ${breadcrumbs_links}
39 39 </h4>
40 40 </div>
41 41 <div id="summary_details_expand" class="btn-collapse" data-toggle="summary-details">
42 42 ${_('Show More')}
43 43 </div>
44 44 </div>
45 45
46 46 <div class="fieldset">
47 %if h.is_svn_without_proxy(c.rhodecode_db_repo):
48 <div class="left-label disabled">
49 ${_('Read-only url')}:
50 </div>
51 <div class="right-content disabled">
52 <input type="text" class="input-monospace" id="clone_url" disabled value="${c.clone_repo_url}"/>
53 <i id="clone_by_name_copy" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url}" title="${_('Copy the clone url')}"></i>
54 47
55 <input type="text" class="input-monospace" id="clone_url_id" disabled value="${c.clone_repo_url_id}" style="display: none;"/>
56 <i id="clone_by_id_copy" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url_id}" title="${_('Copy the clone by id url')}" style="display: none"></i>
57
58 <a id="clone_by_name" class="clone" style="display: none;">${_('Show by Name')}</a>
59 <a id="clone_by_id" class="clone">${_('Show by ID')}</a>
60
61 <p class="help-block">${_('SVN Protocol is disabled. To enable it, see the')} <a href="${h.route_url('enterprise_svn_setup')}" target="_blank">${_('documentation here')}</a>.</p>
48 <div class="left-clone">
49 <select id="clone_option" name="clone_option">
50 <option value="http" selected="selected">HTTP</option>
51 <option value="http_id">HTTP UID</option>
52 <option value="ssh">SSH</option>
53 </select>
62 54 </div>
63 %else:
64 <div class="left-label">
65 ${_('Clone url')}:
66 </div>
67 <div class="right-content">
68 <input type="text" class="input-monospace" id="clone_url" readonly="readonly" value="${c.clone_repo_url}"/>
69 <i id="clone_by_name_copy" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url}" title="${_('Copy the clone url')}"></i>
55 <div class="right-clone">
56 <%
57 maybe_disabled = ''
58 if h.is_svn_without_proxy(c.rhodecode_db_repo):
59 maybe_disabled = 'disabled'
60 %>
61
62 <span id="clone_option_http">
63 <input type="text" class="input-monospace clone_url_input" ${maybe_disabled} readonly="readonly" value="${c.clone_repo_url}"/>
64 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url}" title="${_('Copy the clone url')}"></i>
65 </span>
70 66
71 <input type="text" class="input-monospace" id="clone_url_id" readonly="readonly" value="${c.clone_repo_url_id}" style="display: none;"/>
72 <i id="clone_by_id_copy" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url_id}" title="${_('Copy the clone by id url')}" style="display: none"></i>
67 <span style="display: none;" id="clone_option_http_id">
68 <input type="text" class="input-monospace clone_url_input" ${maybe_disabled} readonly="readonly" value="${c.clone_repo_url_id}"/>
69 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url_id}" title="${_('Copy the clone by id url')}"></i>
70 </span>
73 71
74 <a id="clone_by_name" class="clone" style="display: none;">${_('Show by Name')}</a>
75 <a id="clone_by_id" class="clone">${_('Show by ID')}</a>
72 <span style="display: none;" id="clone_option_ssh">
73 <input type="text" class="input-monospace clone_url_input" ${maybe_disabled} readonly="readonly" value="${c.clone_repo_url_ssh}"/>
74 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url_ssh}" title="${_('Copy the clone by ssh url')}"></i>
75 </span>
76
77 % if maybe_disabled:
78 <p class="help-block">${_('SVN Protocol is disabled. To enable it, see the')} <a href="${h.route_url('enterprise_svn_setup')}" target="_blank">${_('documentation here')}</a>.</p>
79 % endif
80
76 81 </div>
77 %endif
78 82 </div>
79 83
80 84 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
81 85 <div class="left-label">
82 86 ${_('Description')}:
83 87 </div>
84 88 <div class="right-content">
85 89 <div class="input ${summary(c.show_stats)}">
86 90 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
87 91 ${dt.repo_desc(c.rhodecode_db_repo.description_safe, c.visual.stylify_metatags)}
88 92 </div>
89 93 </div>
90 94 </div>
91 95
92 96 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
93 97 <div class="left-label">
94 98 ${_('Information')}:
95 99 </div>
96 100 <div class="right-content">
97 101
98 102 <div class="repo-size">
99 103 <% commit_rev = c.rhodecode_db_repo.changeset_cache.get('revision') %>
100 104
101 105 ## commits
102 106 % if commit_rev == -1:
103 107 ${_ungettext('%(num)s Commit', '%(num)s Commits', 0) % {'num': 0}},
104 108 % else:
105 109 <a href="${h.route_path('repo_changelog', repo_name=c.repo_name)}">
106 110 ${_ungettext('%(num)s Commit', '%(num)s Commits', commit_rev) % {'num': commit_rev}}</a>,
107 111 % endif
108 112
109 113 ## forks
110 114 <a title="${_('Number of Repository Forks')}" href="${h.route_path('repo_forks_show_all', repo_name=c.repo_name)}">
111 115 ${c.repository_forks} ${_ungettext('Fork', 'Forks', c.repository_forks)}</a>,
112 116
113 117 ## repo size
114 118 % if commit_rev == -1:
115 119 <span class="stats-bullet">0 B</span>
116 120 % else:
117 121 <span class="stats-bullet" id="repo_size_container">
118 122 ${_('Calculating Repository Size...')}
119 123 </span>
120 124 % endif
121 125 </div>
122 126
123 127 <div class="commit-info">
124 128 <div class="tags">
125 129 % if c.rhodecode_repo:
126 130 ${refs_counters(
127 131 c.rhodecode_repo.branches,
128 132 c.rhodecode_repo.branches_closed,
129 133 c.rhodecode_repo.tags,
130 134 c.rhodecode_repo.bookmarks)}
131 135 % else:
132 136 ## missing requirements can make c.rhodecode_repo None
133 137 ${refs_counters([], [], [], [])}
134 138 % endif
135 139 </div>
136 140 </div>
137 141
138 142 </div>
139 143 </div>
140 144
141 145 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
142 146 <div class="left-label">
143 147 ${_('Statistics')}:
144 148 </div>
145 149 <div class="right-content">
146 150 <div class="input ${summary(c.show_stats)} statistics">
147 151 % if c.show_stats:
148 152 <div id="lang_stats" class="enabled">
149 153 ${_('Calculating Code Statistics...')}
150 154 </div>
151 155 % else:
152 156 <span class="disabled">
153 157 ${_('Statistics are disabled for this repository')}
154 158 </span>
155 159 % if h.HasPermissionAll('hg.admin')('enable stats on from summary'):
156 160 , ${h.link_to(_('enable statistics'),h.route_path('edit_repo',repo_name=c.repo_name, _anchor='repo_enable_statistics'))}
157 161 % endif
158 162 % endif
159 163 </div>
160 164
161 165 </div>
162 166 </div>
163 167
164 168 % if show_downloads:
165 169 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
166 170 <div class="left-label">
167 171 ${_('Downloads')}:
168 172 </div>
169 173 <div class="right-content">
170 174 <div class="input ${summary(c.show_stats)} downloads">
171 175 % if c.rhodecode_repo and len(c.rhodecode_repo.revisions) == 0:
172 176 <span class="disabled">
173 177 ${_('There are no downloads yet')}
174 178 </span>
175 179 % elif not c.enable_downloads:
176 180 <span class="disabled">
177 181 ${_('Downloads are disabled for this repository')}
178 182 </span>
179 183 % if h.HasPermissionAll('hg.admin')('enable downloads on from summary'):
180 184 , ${h.link_to(_('enable downloads'),h.route_path('edit_repo',repo_name=c.repo_name, _anchor='repo_enable_downloads'))}
181 185 % endif
182 186 % else:
183 187 <span class="enabled">
184 188 <a id="archive_link" class="btn btn-small" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name,fname='tip.zip')}">
185 189 <i class="icon-archive"></i> tip.zip
186 190 ## replaced by some JS on select
187 191 </a>
188 192 </span>
189 193 ${h.hidden('download_options')}
190 194 % endif
191 195 </div>
192 196 </div>
193 197 </div>
194 198 % endif
195 199
196 200 </div><!--end summary-detail-->
197 201 </%def>
198 202
199 203 <%def name="summary_stats(gravatar_function)">
200 204 <div class="sidebar-right">
201 205 <div class="summary-detail-header">
202 206 <h4 class="item">
203 207 ${_('Owner')}
204 208 </h4>
205 209 </div>
206 210 <div class="sidebar-right-content">
207 211 ${gravatar_function(c.rhodecode_db_repo.user.email, 16)}
208 212 </div>
209 213 </div><!--end sidebar-right-->
210 214 </%def>
@@ -1,137 +1,119 b''
1 1 <%inherit file="/summary/summary_base.mako"/>
2 2
3 3 <%namespace name="components" file="/summary/components.mako"/>
4 4
5 5
6 6 <%def name="menu_bar_subnav()">
7 7 ${self.repo_menu(active='summary')}
8 8 </%def>
9 9
10 10 <%def name="main()">
11 11
12 12 <div class="title">
13 13 ${self.repo_page_title(c.rhodecode_db_repo)}
14 14 <ul class="links icon-only-links block-right">
15 15 <li>
16 16 %if c.rhodecode_user.username != h.DEFAULT_USER:
17 17 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_name, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}"><i class="icon-rss-sign"></i></a>
18 18 %else:
19 19 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_name)}" title="${_('RSS Feed')}"><i class="icon-rss-sign"></i></a>
20 20 %endif
21 21 </li>
22 22 </ul>
23 23 </div>
24 24
25 25 <div id="repo-summary" class="summary">
26 26 ${components.summary_detail(breadcrumbs_links=self.breadcrumbs_links(), show_downloads=True)}
27 27 ${components.summary_stats(gravatar_function=self.gravatar_with_user)}
28 28 </div><!--end repo-summary-->
29 29
30 30
31 31 <div class="box" >
32 32 %if not c.repo_commits:
33 33 <div class="title">
34 34 <h3>${_('Quick start')}</h3>
35 35 </div>
36 36 %endif
37 37 <div class="table">
38 38 <div id="shortlog_data">
39 39 <%include file='summary_commits.mako'/>
40 40 </div>
41 41 </div>
42 42 </div>
43 43
44 44 %if c.readme_data:
45 45 <div id="readme" class="anchor">
46 46 <div class="box" >
47 47 <div class="title" title="${h.tooltip(_('Readme file from commit %s:%s') % (c.rhodecode_db_repo.landing_rev[0], c.rhodecode_db_repo.landing_rev[1]))}">
48 48 <h3 class="breadcrumbs">
49 49 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.rhodecode_db_repo.landing_rev[1],f_path=c.readme_file)}">${c.readme_file}</a>
50 50 </h3>
51 51 </div>
52 52 <div class="readme codeblock">
53 53 <div class="readme_box">
54 54 ${c.readme_data|n}
55 55 </div>
56 56 </div>
57 57 </div>
58 58 </div>
59 59 %endif
60 60
61 61 <script type="text/javascript">
62 62 $(document).ready(function(){
63 $('#clone_by_name').on('click',function(e){
64 // show url by name and hide name button
65 $('#clone_url').show();
66 $('#clone_by_name').hide();
67
68 // hide url by id and show name button
69 $('#clone_by_id').show();
70 $('#clone_url_id').hide();
71
72 // hide copy by id
73 $('#clone_by_name_copy').show();
74 $('#clone_by_id_copy').hide();
75
63 $('#clone_option').on('change', function(e) {
64 var selected = $(this).val();
65 $.each(['http', 'http_id', 'ssh'], function (idx, val) {
66 if(val === selected){
67 $('#clone_option_' + val).show();
68 } else {
69 $('#clone_option_' + val).hide();
70 }
76 71 });
77 $('#clone_by_id').on('click',function(e){
78
79 // show url by id and hide id button
80 $('#clone_by_id').hide();
81 $('#clone_url_id').show();
82
83 // hide url by name and show id button
84 $('#clone_by_name').show();
85 $('#clone_url').hide();
86
87 // hide copy by id
88 $('#clone_by_id_copy').show();
89 $('#clone_by_name_copy').hide();
90 72 });
91 73
92 74 var initialCommitData = {
93 75 id: null,
94 76 text: 'tip',
95 77 type: 'tag',
96 78 raw_id: null,
97 79 files_url: null
98 80 };
99 81
100 82 select2RefSwitcher('#download_options', initialCommitData);
101 83
102 84 // on change of download options
103 85 $('#download_options').on('change', function(e) {
104 86 // format of Object {text: "v0.0.3", type: "tag", id: "rev"}
105 87 var ext = '.zip';
106 88 var selected_cs = e.added;
107 89 var fname = e.added.raw_id + ext;
108 90 var href = pyroutes.url('repo_archivefile', {'repo_name': templateContext.repo_name, 'fname':fname});
109 91 // set new label
110 92 $('#archive_link').html('<i class="icon-archive"></i> {0}{1}'.format(escapeHtml(e.added.text), ext));
111 93
112 94 // set new url to button,
113 95 $('#archive_link').attr('href', href)
114 96 });
115 97
116 98
117 99 // load details on summary page expand
118 100 $('#summary_details_expand').on('click', function() {
119 101
120 102 var callback = function (data) {
121 103 % if c.show_stats:
122 104 showRepoStats('lang_stats', data);
123 105 % endif
124 106 };
125 107
126 108 showRepoSize(
127 109 'repo_size_container',
128 110 templateContext.repo_name,
129 111 templateContext.repo_landing_commit,
130 112 callback);
131 113
132 114 })
133 115
134 116 })
135 117 </script>
136 118
137 119 </%def>
General Comments 0
You need to be logged in to leave comments. Login now