##// END OF EJS Templates
users: enable full edit mode for super admins....
super-admin -
r4740:9d84ba0c default
parent child Browse files
Show More
@@ -1,1318 +1,1322 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode import events
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
32 32 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
33 33 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
34 34 from rhodecode.authentication.plugins import auth_rhodecode
35 35 from rhodecode.events import trigger
36 36 from rhodecode.model.db import true, UserNotice
37 37
38 38 from rhodecode.lib import audit_logger, rc_cache, auth
39 39 from rhodecode.lib.exceptions import (
40 40 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
41 41 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
42 42 UserOwnsArtifactsException, DefaultUserException)
43 43 from rhodecode.lib.ext_json import json
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
46 46 from rhodecode.lib import helpers as h
47 47 from rhodecode.lib.helpers import SqlPage
48 48 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
49 49 from rhodecode.model.auth_token import AuthTokenModel
50 50 from rhodecode.model.forms import (
51 51 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
52 52 UserExtraEmailForm, UserExtraIpForm)
53 53 from rhodecode.model.permission import PermissionModel
54 54 from rhodecode.model.repo_group import RepoGroupModel
55 55 from rhodecode.model.ssh_key import SshKeyModel
56 56 from rhodecode.model.user import UserModel
57 57 from rhodecode.model.user_group import UserGroupModel
58 58 from rhodecode.model.db import (
59 59 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
60 60 UserApiKeys, UserSshKeys, RepoGroup)
61 61 from rhodecode.model.meta import Session
62 62
63 63 log = logging.getLogger(__name__)
64 64
65 65
66 66 class AdminUsersView(BaseAppView, DataGridAppView):
67 67
68 68 def load_default_context(self):
69 69 c = self._get_local_tmpl_context()
70 70 return c
71 71
72 72 @LoginRequired()
73 73 @HasPermissionAllDecorator('hg.admin')
74 74 def users_list(self):
75 75 c = self.load_default_context()
76 76 return self._get_template_context(c)
77 77
78 78 @LoginRequired()
79 79 @HasPermissionAllDecorator('hg.admin')
80 80 def users_list_data(self):
81 81 self.load_default_context()
82 82 column_map = {
83 83 'first_name': 'name',
84 84 'last_name': 'lastname',
85 85 }
86 86 draw, start, limit = self._extract_chunk(self.request)
87 87 search_q, order_by, order_dir = self._extract_ordering(
88 88 self.request, column_map=column_map)
89 89 _render = self.request.get_partial_renderer(
90 90 'rhodecode:templates/data_table/_dt_elements.mako')
91 91
92 92 def user_actions(user_id, username):
93 93 return _render("user_actions", user_id, username)
94 94
95 95 users_data_total_count = User.query()\
96 96 .filter(User.username != User.DEFAULT_USER) \
97 97 .count()
98 98
99 99 users_data_total_inactive_count = User.query()\
100 100 .filter(User.username != User.DEFAULT_USER) \
101 101 .filter(User.active != true())\
102 102 .count()
103 103
104 104 # json generate
105 105 base_q = User.query().filter(User.username != User.DEFAULT_USER)
106 106 base_inactive_q = base_q.filter(User.active != true())
107 107
108 108 if search_q:
109 109 like_expression = u'%{}%'.format(safe_unicode(search_q))
110 110 base_q = base_q.filter(or_(
111 111 User.username.ilike(like_expression),
112 112 User._email.ilike(like_expression),
113 113 User.name.ilike(like_expression),
114 114 User.lastname.ilike(like_expression),
115 115 ))
116 116 base_inactive_q = base_q.filter(User.active != true())
117 117
118 118 users_data_total_filtered_count = base_q.count()
119 119 users_data_total_filtered_inactive_count = base_inactive_q.count()
120 120
121 121 sort_col = getattr(User, order_by, None)
122 122 if sort_col:
123 123 if order_dir == 'asc':
124 124 # handle null values properly to order by NULL last
125 125 if order_by in ['last_activity']:
126 126 sort_col = coalesce(sort_col, datetime.date.max)
127 127 sort_col = sort_col.asc()
128 128 else:
129 129 # handle null values properly to order by NULL last
130 130 if order_by in ['last_activity']:
131 131 sort_col = coalesce(sort_col, datetime.date.min)
132 132 sort_col = sort_col.desc()
133 133
134 134 base_q = base_q.order_by(sort_col)
135 135 base_q = base_q.offset(start).limit(limit)
136 136
137 137 users_list = base_q.all()
138 138
139 139 users_data = []
140 140 for user in users_list:
141 141 users_data.append({
142 142 "username": h.gravatar_with_user(self.request, user.username),
143 143 "email": user.email,
144 144 "first_name": user.first_name,
145 145 "last_name": user.last_name,
146 146 "last_login": h.format_date(user.last_login),
147 147 "last_activity": h.format_date(user.last_activity),
148 148 "active": h.bool2icon(user.active),
149 149 "active_raw": user.active,
150 150 "admin": h.bool2icon(user.admin),
151 151 "extern_type": user.extern_type,
152 152 "extern_name": user.extern_name,
153 153 "action": user_actions(user.user_id, user.username),
154 154 })
155 155 data = ({
156 156 'draw': draw,
157 157 'data': users_data,
158 158 'recordsTotal': users_data_total_count,
159 159 'recordsFiltered': users_data_total_filtered_count,
160 160 'recordsTotalInactive': users_data_total_inactive_count,
161 161 'recordsFilteredInactive': users_data_total_filtered_inactive_count
162 162 })
163 163
164 164 return data
165 165
166 166 def _set_personal_repo_group_template_vars(self, c_obj):
167 167 DummyUser = AttributeDict({
168 168 'username': '${username}',
169 169 'user_id': '${user_id}',
170 170 })
171 171 c_obj.default_create_repo_group = RepoGroupModel() \
172 172 .get_default_create_personal_repo_group()
173 173 c_obj.personal_repo_group_name = RepoGroupModel() \
174 174 .get_personal_group_name(DummyUser)
175 175
176 176 @LoginRequired()
177 177 @HasPermissionAllDecorator('hg.admin')
178 178 def users_new(self):
179 179 _ = self.request.translate
180 180 c = self.load_default_context()
181 181 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
182 182 self._set_personal_repo_group_template_vars(c)
183 183 return self._get_template_context(c)
184 184
185 185 @LoginRequired()
186 186 @HasPermissionAllDecorator('hg.admin')
187 187 @CSRFRequired()
188 188 def users_create(self):
189 189 _ = self.request.translate
190 190 c = self.load_default_context()
191 191 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
192 192 user_model = UserModel()
193 193 user_form = UserForm(self.request.translate)()
194 194 try:
195 195 form_result = user_form.to_python(dict(self.request.POST))
196 196 user = user_model.create(form_result)
197 197 Session().flush()
198 198 creation_data = user.get_api_data()
199 199 username = form_result['username']
200 200
201 201 audit_logger.store_web(
202 202 'user.create', action_data={'data': creation_data},
203 203 user=c.rhodecode_user)
204 204
205 205 user_link = h.link_to(
206 206 h.escape(username),
207 207 h.route_path('user_edit', user_id=user.user_id))
208 208 h.flash(h.literal(_('Created user %(user_link)s')
209 209 % {'user_link': user_link}), category='success')
210 210 Session().commit()
211 211 except formencode.Invalid as errors:
212 212 self._set_personal_repo_group_template_vars(c)
213 213 data = render(
214 214 'rhodecode:templates/admin/users/user_add.mako',
215 215 self._get_template_context(c), self.request)
216 216 html = formencode.htmlfill.render(
217 217 data,
218 218 defaults=errors.value,
219 219 errors=errors.error_dict or {},
220 220 prefix_error=False,
221 221 encoding="UTF-8",
222 222 force_defaults=False
223 223 )
224 224 return Response(html)
225 225 except UserCreationError as e:
226 226 h.flash(e, 'error')
227 227 except Exception:
228 228 log.exception("Exception creation of user")
229 229 h.flash(_('Error occurred during creation of user %s')
230 230 % self.request.POST.get('username'), category='error')
231 231 raise HTTPFound(h.route_path('users'))
232 232
233 233
234 234 class UsersView(UserAppView):
235 235 ALLOW_SCOPED_TOKENS = False
236 236 """
237 237 This view has alternative version inside EE, if modified please take a look
238 238 in there as well.
239 239 """
240 240
241 241 def get_auth_plugins(self):
242 242 valid_plugins = []
243 243 authn_registry = get_authn_registry(self.request.registry)
244 244 for plugin in authn_registry.get_plugins_for_authentication():
245 245 if isinstance(plugin, RhodeCodeExternalAuthPlugin):
246 246 valid_plugins.append(plugin)
247 247 elif plugin.name == 'rhodecode':
248 248 valid_plugins.append(plugin)
249 249
250 250 # extend our choices if user has set a bound plugin which isn't enabled at the
251 251 # moment
252 252 extern_type = self.db_user.extern_type
253 253 if extern_type not in [x.uid for x in valid_plugins]:
254 254 try:
255 255 plugin = authn_registry.get_plugin_by_uid(extern_type)
256 256 if plugin:
257 257 valid_plugins.append(plugin)
258 258
259 259 except Exception:
260 260 log.exception(
261 261 'Could not extend user plugins with `{}`'.format(extern_type))
262 262 return valid_plugins
263 263
264 264 def load_default_context(self):
265 265 req = self.request
266 266
267 267 c = self._get_local_tmpl_context()
268 268 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
269 269 c.allowed_languages = [
270 270 ('en', 'English (en)'),
271 271 ('de', 'German (de)'),
272 272 ('fr', 'French (fr)'),
273 273 ('it', 'Italian (it)'),
274 274 ('ja', 'Japanese (ja)'),
275 275 ('pl', 'Polish (pl)'),
276 276 ('pt', 'Portuguese (pt)'),
277 277 ('ru', 'Russian (ru)'),
278 278 ('zh', 'Chinese (zh)'),
279 279 ]
280 280
281 281 c.allowed_extern_types = [
282 282 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
283 283 ]
284 284 perms = req.registry.settings.get('available_permissions')
285 285 if not perms:
286 286 # inject info about available permissions
287 287 auth.set_available_permissions(req.registry.settings)
288 288
289 289 c.available_permissions = req.registry.settings['available_permissions']
290 290 PermissionModel().set_global_permission_choices(
291 291 c, gettext_translator=req.translate)
292 292
293 293 return c
294 294
295 295 @LoginRequired()
296 296 @HasPermissionAllDecorator('hg.admin')
297 297 @CSRFRequired()
298 298 def user_update(self):
299 299 _ = self.request.translate
300 300 c = self.load_default_context()
301 301
302 302 user_id = self.db_user_id
303 303 c.user = self.db_user
304 304
305 305 c.active = 'profile'
306 306 c.extern_type = c.user.extern_type
307 307 c.extern_name = c.user.extern_name
308 308 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
309 309 available_languages = [x[0] for x in c.allowed_languages]
310 310 _form = UserForm(self.request.translate, edit=True,
311 311 available_languages=available_languages,
312 312 old_data={'user_id': user_id,
313 313 'email': c.user.email})()
314
315 c.edit_mode = self.request.POST.get('edit') == '1'
314 316 form_result = {}
315 317 old_values = c.user.get_api_data()
316 318 try:
317 319 form_result = _form.to_python(dict(self.request.POST))
318 320 skip_attrs = ['extern_name']
319 321 # TODO: plugin should define if username can be updated
320 if c.extern_type != "rhodecode":
322
323 if c.extern_type != "rhodecode" and not c.edit_mode:
321 324 # forbid updating username for external accounts
322 325 skip_attrs.append('username')
323 326
324 327 UserModel().update_user(
325 328 user_id, skip_attrs=skip_attrs, **form_result)
326 329
327 330 audit_logger.store_web(
328 331 'user.edit', action_data={'old_data': old_values},
329 332 user=c.rhodecode_user)
330 333
331 334 Session().commit()
332 335 h.flash(_('User updated successfully'), category='success')
333 336 except formencode.Invalid as errors:
334 337 data = render(
335 338 'rhodecode:templates/admin/users/user_edit.mako',
336 339 self._get_template_context(c), self.request)
337 340 html = formencode.htmlfill.render(
338 341 data,
339 342 defaults=errors.value,
340 343 errors=errors.error_dict or {},
341 344 prefix_error=False,
342 345 encoding="UTF-8",
343 346 force_defaults=False
344 347 )
345 348 return Response(html)
346 349 except UserCreationError as e:
347 350 h.flash(e, 'error')
348 351 except Exception:
349 352 log.exception("Exception updating user")
350 353 h.flash(_('Error occurred during update of user %s')
351 354 % form_result.get('username'), category='error')
352 355 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
353 356
354 357 @LoginRequired()
355 358 @HasPermissionAllDecorator('hg.admin')
356 359 @CSRFRequired()
357 360 def user_delete(self):
358 361 _ = self.request.translate
359 362 c = self.load_default_context()
360 363 c.user = self.db_user
361 364
362 365 _repos = c.user.repositories
363 366 _repo_groups = c.user.repository_groups
364 367 _user_groups = c.user.user_groups
365 368 _pull_requests = c.user.user_pull_requests
366 369 _artifacts = c.user.artifacts
367 370
368 371 handle_repos = None
369 372 handle_repo_groups = None
370 373 handle_user_groups = None
371 374 handle_pull_requests = None
372 375 handle_artifacts = None
373 376
374 377 # calls for flash of handle based on handle case detach or delete
375 378 def set_handle_flash_repos():
376 379 handle = handle_repos
377 380 if handle == 'detach':
378 381 h.flash(_('Detached %s repositories') % len(_repos),
379 382 category='success')
380 383 elif handle == 'delete':
381 384 h.flash(_('Deleted %s repositories') % len(_repos),
382 385 category='success')
383 386
384 387 def set_handle_flash_repo_groups():
385 388 handle = handle_repo_groups
386 389 if handle == 'detach':
387 390 h.flash(_('Detached %s repository groups') % len(_repo_groups),
388 391 category='success')
389 392 elif handle == 'delete':
390 393 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
391 394 category='success')
392 395
393 396 def set_handle_flash_user_groups():
394 397 handle = handle_user_groups
395 398 if handle == 'detach':
396 399 h.flash(_('Detached %s user groups') % len(_user_groups),
397 400 category='success')
398 401 elif handle == 'delete':
399 402 h.flash(_('Deleted %s user groups') % len(_user_groups),
400 403 category='success')
401 404
402 405 def set_handle_flash_pull_requests():
403 406 handle = handle_pull_requests
404 407 if handle == 'detach':
405 408 h.flash(_('Detached %s pull requests') % len(_pull_requests),
406 409 category='success')
407 410 elif handle == 'delete':
408 411 h.flash(_('Deleted %s pull requests') % len(_pull_requests),
409 412 category='success')
410 413
411 414 def set_handle_flash_artifacts():
412 415 handle = handle_artifacts
413 416 if handle == 'detach':
414 417 h.flash(_('Detached %s artifacts') % len(_artifacts),
415 418 category='success')
416 419 elif handle == 'delete':
417 420 h.flash(_('Deleted %s artifacts') % len(_artifacts),
418 421 category='success')
419 422
420 423 handle_user = User.get_first_super_admin()
421 424 handle_user_id = safe_int(self.request.POST.get('detach_user_id'))
422 425 if handle_user_id:
423 426 # NOTE(marcink): we get new owner for objects...
424 427 handle_user = User.get_or_404(handle_user_id)
425 428
426 429 if _repos and self.request.POST.get('user_repos'):
427 430 handle_repos = self.request.POST['user_repos']
428 431
429 432 if _repo_groups and self.request.POST.get('user_repo_groups'):
430 433 handle_repo_groups = self.request.POST['user_repo_groups']
431 434
432 435 if _user_groups and self.request.POST.get('user_user_groups'):
433 436 handle_user_groups = self.request.POST['user_user_groups']
434 437
435 438 if _pull_requests and self.request.POST.get('user_pull_requests'):
436 439 handle_pull_requests = self.request.POST['user_pull_requests']
437 440
438 441 if _artifacts and self.request.POST.get('user_artifacts'):
439 442 handle_artifacts = self.request.POST['user_artifacts']
440 443
441 444 old_values = c.user.get_api_data()
442 445
443 446 try:
444 447
445 448 UserModel().delete(
446 449 c.user,
447 450 handle_repos=handle_repos,
448 451 handle_repo_groups=handle_repo_groups,
449 452 handle_user_groups=handle_user_groups,
450 453 handle_pull_requests=handle_pull_requests,
451 454 handle_artifacts=handle_artifacts,
452 455 handle_new_owner=handle_user
453 456 )
454 457
455 458 audit_logger.store_web(
456 459 'user.delete', action_data={'old_data': old_values},
457 460 user=c.rhodecode_user)
458 461
459 462 Session().commit()
460 463 set_handle_flash_repos()
461 464 set_handle_flash_repo_groups()
462 465 set_handle_flash_user_groups()
463 466 set_handle_flash_pull_requests()
464 467 set_handle_flash_artifacts()
465 468 username = h.escape(old_values['username'])
466 469 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
467 470 except (UserOwnsReposException, UserOwnsRepoGroupsException,
468 471 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
469 472 UserOwnsArtifactsException, DefaultUserException) as e:
470 473 h.flash(e, category='warning')
471 474 except Exception:
472 475 log.exception("Exception during deletion of user")
473 476 h.flash(_('An error occurred during deletion of user'),
474 477 category='error')
475 478 raise HTTPFound(h.route_path('users'))
476 479
477 480 @LoginRequired()
478 481 @HasPermissionAllDecorator('hg.admin')
479 482 def user_edit(self):
480 483 _ = self.request.translate
481 484 c = self.load_default_context()
482 485 c.user = self.db_user
483 486
484 487 c.active = 'profile'
485 488 c.extern_type = c.user.extern_type
486 489 c.extern_name = c.user.extern_name
487 490 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
491 c.edit_mode = self.request.GET.get('edit') == '1'
488 492
489 493 defaults = c.user.get_dict()
490 494 defaults.update({'language': c.user.user_data.get('language')})
491 495
492 496 data = render(
493 497 'rhodecode:templates/admin/users/user_edit.mako',
494 498 self._get_template_context(c), self.request)
495 499 html = formencode.htmlfill.render(
496 500 data,
497 501 defaults=defaults,
498 502 encoding="UTF-8",
499 503 force_defaults=False
500 504 )
501 505 return Response(html)
502 506
503 507 @LoginRequired()
504 508 @HasPermissionAllDecorator('hg.admin')
505 509 def user_edit_advanced(self):
506 510 _ = self.request.translate
507 511 c = self.load_default_context()
508 512
509 513 user_id = self.db_user_id
510 514 c.user = self.db_user
511 515
512 516 c.detach_user = User.get_first_super_admin()
513 517 detach_user_id = safe_int(self.request.GET.get('detach_user_id'))
514 518 if detach_user_id:
515 519 c.detach_user = User.get_or_404(detach_user_id)
516 520
517 521 c.active = 'advanced'
518 522 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
519 523 c.personal_repo_group_name = RepoGroupModel()\
520 524 .get_personal_group_name(c.user)
521 525
522 526 c.user_to_review_rules = sorted(
523 527 (x.user for x in c.user.user_review_rules),
524 528 key=lambda u: u.username.lower())
525 529
526 530 defaults = c.user.get_dict()
527 531
528 532 # Interim workaround if the user participated on any pull requests as a
529 533 # reviewer.
530 534 has_review = len(c.user.reviewer_pull_requests)
531 535 c.can_delete_user = not has_review
532 536 c.can_delete_user_message = ''
533 537 inactive_link = h.link_to(
534 538 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
535 539 if has_review == 1:
536 540 c.can_delete_user_message = h.literal(_(
537 541 'The user participates as reviewer in {} pull request and '
538 542 'cannot be deleted. \nYou can set the user to '
539 543 '"{}" instead of deleting it.').format(
540 544 has_review, inactive_link))
541 545 elif has_review:
542 546 c.can_delete_user_message = h.literal(_(
543 547 'The user participates as reviewer in {} pull requests and '
544 548 'cannot be deleted. \nYou can set the user to '
545 549 '"{}" instead of deleting it.').format(
546 550 has_review, inactive_link))
547 551
548 552 data = render(
549 553 'rhodecode:templates/admin/users/user_edit.mako',
550 554 self._get_template_context(c), self.request)
551 555 html = formencode.htmlfill.render(
552 556 data,
553 557 defaults=defaults,
554 558 encoding="UTF-8",
555 559 force_defaults=False
556 560 )
557 561 return Response(html)
558 562
559 563 @LoginRequired()
560 564 @HasPermissionAllDecorator('hg.admin')
561 565 def user_edit_global_perms(self):
562 566 _ = self.request.translate
563 567 c = self.load_default_context()
564 568 c.user = self.db_user
565 569
566 570 c.active = 'global_perms'
567 571
568 572 c.default_user = User.get_default_user()
569 573 defaults = c.user.get_dict()
570 574 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
571 575 defaults.update(c.default_user.get_default_perms())
572 576 defaults.update(c.user.get_default_perms())
573 577
574 578 data = render(
575 579 'rhodecode:templates/admin/users/user_edit.mako',
576 580 self._get_template_context(c), self.request)
577 581 html = formencode.htmlfill.render(
578 582 data,
579 583 defaults=defaults,
580 584 encoding="UTF-8",
581 585 force_defaults=False
582 586 )
583 587 return Response(html)
584 588
585 589 @LoginRequired()
586 590 @HasPermissionAllDecorator('hg.admin')
587 591 @CSRFRequired()
588 592 def user_edit_global_perms_update(self):
589 593 _ = self.request.translate
590 594 c = self.load_default_context()
591 595
592 596 user_id = self.db_user_id
593 597 c.user = self.db_user
594 598
595 599 c.active = 'global_perms'
596 600 try:
597 601 # first stage that verifies the checkbox
598 602 _form = UserIndividualPermissionsForm(self.request.translate)
599 603 form_result = _form.to_python(dict(self.request.POST))
600 604 inherit_perms = form_result['inherit_default_permissions']
601 605 c.user.inherit_default_permissions = inherit_perms
602 606 Session().add(c.user)
603 607
604 608 if not inherit_perms:
605 609 # only update the individual ones if we un check the flag
606 610 _form = UserPermissionsForm(
607 611 self.request.translate,
608 612 [x[0] for x in c.repo_create_choices],
609 613 [x[0] for x in c.repo_create_on_write_choices],
610 614 [x[0] for x in c.repo_group_create_choices],
611 615 [x[0] for x in c.user_group_create_choices],
612 616 [x[0] for x in c.fork_choices],
613 617 [x[0] for x in c.inherit_default_permission_choices])()
614 618
615 619 form_result = _form.to_python(dict(self.request.POST))
616 620 form_result.update({'perm_user_id': c.user.user_id})
617 621
618 622 PermissionModel().update_user_permissions(form_result)
619 623
620 624 # TODO(marcink): implement global permissions
621 625 # audit_log.store_web('user.edit.permissions')
622 626
623 627 Session().commit()
624 628
625 629 h.flash(_('User global permissions updated successfully'),
626 630 category='success')
627 631
628 632 except formencode.Invalid as errors:
629 633 data = render(
630 634 'rhodecode:templates/admin/users/user_edit.mako',
631 635 self._get_template_context(c), self.request)
632 636 html = formencode.htmlfill.render(
633 637 data,
634 638 defaults=errors.value,
635 639 errors=errors.error_dict or {},
636 640 prefix_error=False,
637 641 encoding="UTF-8",
638 642 force_defaults=False
639 643 )
640 644 return Response(html)
641 645 except Exception:
642 646 log.exception("Exception during permissions saving")
643 647 h.flash(_('An error occurred during permissions saving'),
644 648 category='error')
645 649
646 650 affected_user_ids = [user_id]
647 651 PermissionModel().trigger_permission_flush(affected_user_ids)
648 652 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
649 653
650 654 @LoginRequired()
651 655 @HasPermissionAllDecorator('hg.admin')
652 656 @CSRFRequired()
653 657 def user_enable_force_password_reset(self):
654 658 _ = self.request.translate
655 659 c = self.load_default_context()
656 660
657 661 user_id = self.db_user_id
658 662 c.user = self.db_user
659 663
660 664 try:
661 665 c.user.update_userdata(force_password_change=True)
662 666
663 667 msg = _('Force password change enabled for user')
664 668 audit_logger.store_web('user.edit.password_reset.enabled',
665 669 user=c.rhodecode_user)
666 670
667 671 Session().commit()
668 672 h.flash(msg, category='success')
669 673 except Exception:
670 674 log.exception("Exception during password reset for user")
671 675 h.flash(_('An error occurred during password reset for user'),
672 676 category='error')
673 677
674 678 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
675 679
676 680 @LoginRequired()
677 681 @HasPermissionAllDecorator('hg.admin')
678 682 @CSRFRequired()
679 683 def user_disable_force_password_reset(self):
680 684 _ = self.request.translate
681 685 c = self.load_default_context()
682 686
683 687 user_id = self.db_user_id
684 688 c.user = self.db_user
685 689
686 690 try:
687 691 c.user.update_userdata(force_password_change=False)
688 692
689 693 msg = _('Force password change disabled for user')
690 694 audit_logger.store_web(
691 695 'user.edit.password_reset.disabled',
692 696 user=c.rhodecode_user)
693 697
694 698 Session().commit()
695 699 h.flash(msg, category='success')
696 700 except Exception:
697 701 log.exception("Exception during password reset for user")
698 702 h.flash(_('An error occurred during password reset for user'),
699 703 category='error')
700 704
701 705 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
702 706
703 707 @LoginRequired()
704 708 @HasPermissionAllDecorator('hg.admin')
705 709 @CSRFRequired()
706 710 def user_notice_dismiss(self):
707 711 _ = self.request.translate
708 712 c = self.load_default_context()
709 713
710 714 user_id = self.db_user_id
711 715 c.user = self.db_user
712 716 user_notice_id = safe_int(self.request.POST.get('notice_id'))
713 717 notice = UserNotice().query()\
714 718 .filter(UserNotice.user_id == user_id)\
715 719 .filter(UserNotice.user_notice_id == user_notice_id)\
716 720 .scalar()
717 721 read = False
718 722 if notice:
719 723 notice.notice_read = True
720 724 Session().add(notice)
721 725 Session().commit()
722 726 read = True
723 727
724 728 return {'notice': user_notice_id, 'read': read}
725 729
726 730 @LoginRequired()
727 731 @HasPermissionAllDecorator('hg.admin')
728 732 @CSRFRequired()
729 733 def user_create_personal_repo_group(self):
730 734 """
731 735 Create personal repository group for this user
732 736 """
733 737 from rhodecode.model.repo_group import RepoGroupModel
734 738
735 739 _ = self.request.translate
736 740 c = self.load_default_context()
737 741
738 742 user_id = self.db_user_id
739 743 c.user = self.db_user
740 744
741 745 personal_repo_group = RepoGroup.get_user_personal_repo_group(
742 746 c.user.user_id)
743 747 if personal_repo_group:
744 748 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
745 749
746 750 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
747 751 named_personal_group = RepoGroup.get_by_group_name(
748 752 personal_repo_group_name)
749 753 try:
750 754
751 755 if named_personal_group and named_personal_group.user_id == c.user.user_id:
752 756 # migrate the same named group, and mark it as personal
753 757 named_personal_group.personal = True
754 758 Session().add(named_personal_group)
755 759 Session().commit()
756 760 msg = _('Linked repository group `%s` as personal' % (
757 761 personal_repo_group_name,))
758 762 h.flash(msg, category='success')
759 763 elif not named_personal_group:
760 764 RepoGroupModel().create_personal_repo_group(c.user)
761 765
762 766 msg = _('Created repository group `%s`' % (
763 767 personal_repo_group_name,))
764 768 h.flash(msg, category='success')
765 769 else:
766 770 msg = _('Repository group `%s` is already taken' % (
767 771 personal_repo_group_name,))
768 772 h.flash(msg, category='warning')
769 773 except Exception:
770 774 log.exception("Exception during repository group creation")
771 775 msg = _(
772 776 'An error occurred during repository group creation for user')
773 777 h.flash(msg, category='error')
774 778 Session().rollback()
775 779
776 780 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
777 781
778 782 @LoginRequired()
779 783 @HasPermissionAllDecorator('hg.admin')
780 784 def auth_tokens(self):
781 785 _ = self.request.translate
782 786 c = self.load_default_context()
783 787 c.user = self.db_user
784 788
785 789 c.active = 'auth_tokens'
786 790
787 791 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
788 792 c.role_values = [
789 793 (x, AuthTokenModel.cls._get_role_name(x))
790 794 for x in AuthTokenModel.cls.ROLES]
791 795 c.role_options = [(c.role_values, _("Role"))]
792 796 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
793 797 c.user.user_id, show_expired=True)
794 798 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
795 799 return self._get_template_context(c)
796 800
797 801 @LoginRequired()
798 802 @HasPermissionAllDecorator('hg.admin')
799 803 def auth_tokens_view(self):
800 804 _ = self.request.translate
801 805 c = self.load_default_context()
802 806 c.user = self.db_user
803 807
804 808 auth_token_id = self.request.POST.get('auth_token_id')
805 809
806 810 if auth_token_id:
807 811 token = UserApiKeys.get_or_404(auth_token_id)
808 812
809 813 return {
810 814 'auth_token': token.api_key
811 815 }
812 816
813 817 def maybe_attach_token_scope(self, token):
814 818 # implemented in EE edition
815 819 pass
816 820
817 821 @LoginRequired()
818 822 @HasPermissionAllDecorator('hg.admin')
819 823 @CSRFRequired()
820 824 def auth_tokens_add(self):
821 825 _ = self.request.translate
822 826 c = self.load_default_context()
823 827
824 828 user_id = self.db_user_id
825 829 c.user = self.db_user
826 830
827 831 user_data = c.user.get_api_data()
828 832 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
829 833 description = self.request.POST.get('description')
830 834 role = self.request.POST.get('role')
831 835
832 836 token = UserModel().add_auth_token(
833 837 user=c.user.user_id,
834 838 lifetime_minutes=lifetime, role=role, description=description,
835 839 scope_callback=self.maybe_attach_token_scope)
836 840 token_data = token.get_api_data()
837 841
838 842 audit_logger.store_web(
839 843 'user.edit.token.add', action_data={
840 844 'data': {'token': token_data, 'user': user_data}},
841 845 user=self._rhodecode_user, )
842 846 Session().commit()
843 847
844 848 h.flash(_("Auth token successfully created"), category='success')
845 849 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
846 850
847 851 @LoginRequired()
848 852 @HasPermissionAllDecorator('hg.admin')
849 853 @CSRFRequired()
850 854 def auth_tokens_delete(self):
851 855 _ = self.request.translate
852 856 c = self.load_default_context()
853 857
854 858 user_id = self.db_user_id
855 859 c.user = self.db_user
856 860
857 861 user_data = c.user.get_api_data()
858 862
859 863 del_auth_token = self.request.POST.get('del_auth_token')
860 864
861 865 if del_auth_token:
862 866 token = UserApiKeys.get_or_404(del_auth_token)
863 867 token_data = token.get_api_data()
864 868
865 869 AuthTokenModel().delete(del_auth_token, c.user.user_id)
866 870 audit_logger.store_web(
867 871 'user.edit.token.delete', action_data={
868 872 'data': {'token': token_data, 'user': user_data}},
869 873 user=self._rhodecode_user,)
870 874 Session().commit()
871 875 h.flash(_("Auth token successfully deleted"), category='success')
872 876
873 877 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
874 878
875 879 @LoginRequired()
876 880 @HasPermissionAllDecorator('hg.admin')
877 881 def ssh_keys(self):
878 882 _ = self.request.translate
879 883 c = self.load_default_context()
880 884 c.user = self.db_user
881 885
882 886 c.active = 'ssh_keys'
883 887 c.default_key = self.request.GET.get('default_key')
884 888 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
885 889 return self._get_template_context(c)
886 890
887 891 @LoginRequired()
888 892 @HasPermissionAllDecorator('hg.admin')
889 893 def ssh_keys_generate_keypair(self):
890 894 _ = self.request.translate
891 895 c = self.load_default_context()
892 896
893 897 c.user = self.db_user
894 898
895 899 c.active = 'ssh_keys_generate'
896 900 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
897 901 private_format = self.request.GET.get('private_format') \
898 902 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
899 903 c.private, c.public = SshKeyModel().generate_keypair(
900 904 comment=comment, private_format=private_format)
901 905
902 906 return self._get_template_context(c)
903 907
904 908 @LoginRequired()
905 909 @HasPermissionAllDecorator('hg.admin')
906 910 @CSRFRequired()
907 911 def ssh_keys_add(self):
908 912 _ = self.request.translate
909 913 c = self.load_default_context()
910 914
911 915 user_id = self.db_user_id
912 916 c.user = self.db_user
913 917
914 918 user_data = c.user.get_api_data()
915 919 key_data = self.request.POST.get('key_data')
916 920 description = self.request.POST.get('description')
917 921
918 922 fingerprint = 'unknown'
919 923 try:
920 924 if not key_data:
921 925 raise ValueError('Please add a valid public key')
922 926
923 927 key = SshKeyModel().parse_key(key_data.strip())
924 928 fingerprint = key.hash_md5()
925 929
926 930 ssh_key = SshKeyModel().create(
927 931 c.user.user_id, fingerprint, key.keydata, description)
928 932 ssh_key_data = ssh_key.get_api_data()
929 933
930 934 audit_logger.store_web(
931 935 'user.edit.ssh_key.add', action_data={
932 936 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
933 937 user=self._rhodecode_user, )
934 938 Session().commit()
935 939
936 940 # Trigger an event on change of keys.
937 941 trigger(SshKeyFileChangeEvent(), self.request.registry)
938 942
939 943 h.flash(_("Ssh Key successfully created"), category='success')
940 944
941 945 except IntegrityError:
942 946 log.exception("Exception during ssh key saving")
943 947 err = 'Such key with fingerprint `{}` already exists, ' \
944 948 'please use a different one'.format(fingerprint)
945 949 h.flash(_('An error occurred during ssh key saving: {}').format(err),
946 950 category='error')
947 951 except Exception as e:
948 952 log.exception("Exception during ssh key saving")
949 953 h.flash(_('An error occurred during ssh key saving: {}').format(e),
950 954 category='error')
951 955
952 956 return HTTPFound(
953 957 h.route_path('edit_user_ssh_keys', user_id=user_id))
954 958
955 959 @LoginRequired()
956 960 @HasPermissionAllDecorator('hg.admin')
957 961 @CSRFRequired()
958 962 def ssh_keys_delete(self):
959 963 _ = self.request.translate
960 964 c = self.load_default_context()
961 965
962 966 user_id = self.db_user_id
963 967 c.user = self.db_user
964 968
965 969 user_data = c.user.get_api_data()
966 970
967 971 del_ssh_key = self.request.POST.get('del_ssh_key')
968 972
969 973 if del_ssh_key:
970 974 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
971 975 ssh_key_data = ssh_key.get_api_data()
972 976
973 977 SshKeyModel().delete(del_ssh_key, c.user.user_id)
974 978 audit_logger.store_web(
975 979 'user.edit.ssh_key.delete', action_data={
976 980 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
977 981 user=self._rhodecode_user,)
978 982 Session().commit()
979 983 # Trigger an event on change of keys.
980 984 trigger(SshKeyFileChangeEvent(), self.request.registry)
981 985 h.flash(_("Ssh key successfully deleted"), category='success')
982 986
983 987 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
984 988
985 989 @LoginRequired()
986 990 @HasPermissionAllDecorator('hg.admin')
987 991 def emails(self):
988 992 _ = self.request.translate
989 993 c = self.load_default_context()
990 994 c.user = self.db_user
991 995
992 996 c.active = 'emails'
993 997 c.user_email_map = UserEmailMap.query() \
994 998 .filter(UserEmailMap.user == c.user).all()
995 999
996 1000 return self._get_template_context(c)
997 1001
998 1002 @LoginRequired()
999 1003 @HasPermissionAllDecorator('hg.admin')
1000 1004 @CSRFRequired()
1001 1005 def emails_add(self):
1002 1006 _ = self.request.translate
1003 1007 c = self.load_default_context()
1004 1008
1005 1009 user_id = self.db_user_id
1006 1010 c.user = self.db_user
1007 1011
1008 1012 email = self.request.POST.get('new_email')
1009 1013 user_data = c.user.get_api_data()
1010 1014 try:
1011 1015
1012 1016 form = UserExtraEmailForm(self.request.translate)()
1013 1017 data = form.to_python({'email': email})
1014 1018 email = data['email']
1015 1019
1016 1020 UserModel().add_extra_email(c.user.user_id, email)
1017 1021 audit_logger.store_web(
1018 1022 'user.edit.email.add',
1019 1023 action_data={'email': email, 'user': user_data},
1020 1024 user=self._rhodecode_user)
1021 1025 Session().commit()
1022 1026 h.flash(_("Added new email address `%s` for user account") % email,
1023 1027 category='success')
1024 1028 except formencode.Invalid as error:
1025 1029 h.flash(h.escape(error.error_dict['email']), category='error')
1026 1030 except IntegrityError:
1027 1031 log.warning("Email %s already exists", email)
1028 1032 h.flash(_('Email `{}` is already registered for another user.').format(email),
1029 1033 category='error')
1030 1034 except Exception:
1031 1035 log.exception("Exception during email saving")
1032 1036 h.flash(_('An error occurred during email saving'),
1033 1037 category='error')
1034 1038 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1035 1039
1036 1040 @LoginRequired()
1037 1041 @HasPermissionAllDecorator('hg.admin')
1038 1042 @CSRFRequired()
1039 1043 def emails_delete(self):
1040 1044 _ = self.request.translate
1041 1045 c = self.load_default_context()
1042 1046
1043 1047 user_id = self.db_user_id
1044 1048 c.user = self.db_user
1045 1049
1046 1050 email_id = self.request.POST.get('del_email_id')
1047 1051 user_model = UserModel()
1048 1052
1049 1053 email = UserEmailMap.query().get(email_id).email
1050 1054 user_data = c.user.get_api_data()
1051 1055 user_model.delete_extra_email(c.user.user_id, email_id)
1052 1056 audit_logger.store_web(
1053 1057 'user.edit.email.delete',
1054 1058 action_data={'email': email, 'user': user_data},
1055 1059 user=self._rhodecode_user)
1056 1060 Session().commit()
1057 1061 h.flash(_("Removed email address from user account"),
1058 1062 category='success')
1059 1063 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1060 1064
1061 1065 @LoginRequired()
1062 1066 @HasPermissionAllDecorator('hg.admin')
1063 1067 def ips(self):
1064 1068 _ = self.request.translate
1065 1069 c = self.load_default_context()
1066 1070 c.user = self.db_user
1067 1071
1068 1072 c.active = 'ips'
1069 1073 c.user_ip_map = UserIpMap.query() \
1070 1074 .filter(UserIpMap.user == c.user).all()
1071 1075
1072 1076 c.inherit_default_ips = c.user.inherit_default_permissions
1073 1077 c.default_user_ip_map = UserIpMap.query() \
1074 1078 .filter(UserIpMap.user == User.get_default_user()).all()
1075 1079
1076 1080 return self._get_template_context(c)
1077 1081
1078 1082 @LoginRequired()
1079 1083 @HasPermissionAllDecorator('hg.admin')
1080 1084 @CSRFRequired()
1081 1085 # NOTE(marcink): this view is allowed for default users, as we can
1082 1086 # edit their IP white list
1083 1087 def ips_add(self):
1084 1088 _ = self.request.translate
1085 1089 c = self.load_default_context()
1086 1090
1087 1091 user_id = self.db_user_id
1088 1092 c.user = self.db_user
1089 1093
1090 1094 user_model = UserModel()
1091 1095 desc = self.request.POST.get('description')
1092 1096 try:
1093 1097 ip_list = user_model.parse_ip_range(
1094 1098 self.request.POST.get('new_ip'))
1095 1099 except Exception as e:
1096 1100 ip_list = []
1097 1101 log.exception("Exception during ip saving")
1098 1102 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1099 1103 category='error')
1100 1104 added = []
1101 1105 user_data = c.user.get_api_data()
1102 1106 for ip in ip_list:
1103 1107 try:
1104 1108 form = UserExtraIpForm(self.request.translate)()
1105 1109 data = form.to_python({'ip': ip})
1106 1110 ip = data['ip']
1107 1111
1108 1112 user_model.add_extra_ip(c.user.user_id, ip, desc)
1109 1113 audit_logger.store_web(
1110 1114 'user.edit.ip.add',
1111 1115 action_data={'ip': ip, 'user': user_data},
1112 1116 user=self._rhodecode_user)
1113 1117 Session().commit()
1114 1118 added.append(ip)
1115 1119 except formencode.Invalid as error:
1116 1120 msg = error.error_dict['ip']
1117 1121 h.flash(msg, category='error')
1118 1122 except Exception:
1119 1123 log.exception("Exception during ip saving")
1120 1124 h.flash(_('An error occurred during ip saving'),
1121 1125 category='error')
1122 1126 if added:
1123 1127 h.flash(
1124 1128 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1125 1129 category='success')
1126 1130 if 'default_user' in self.request.POST:
1127 1131 # case for editing global IP list we do it for 'DEFAULT' user
1128 1132 raise HTTPFound(h.route_path('admin_permissions_ips'))
1129 1133 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1130 1134
1131 1135 @LoginRequired()
1132 1136 @HasPermissionAllDecorator('hg.admin')
1133 1137 @CSRFRequired()
1134 1138 # NOTE(marcink): this view is allowed for default users, as we can
1135 1139 # edit their IP white list
1136 1140 def ips_delete(self):
1137 1141 _ = self.request.translate
1138 1142 c = self.load_default_context()
1139 1143
1140 1144 user_id = self.db_user_id
1141 1145 c.user = self.db_user
1142 1146
1143 1147 ip_id = self.request.POST.get('del_ip_id')
1144 1148 user_model = UserModel()
1145 1149 user_data = c.user.get_api_data()
1146 1150 ip = UserIpMap.query().get(ip_id).ip_addr
1147 1151 user_model.delete_extra_ip(c.user.user_id, ip_id)
1148 1152 audit_logger.store_web(
1149 1153 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1150 1154 user=self._rhodecode_user)
1151 1155 Session().commit()
1152 1156 h.flash(_("Removed ip address from user whitelist"), category='success')
1153 1157
1154 1158 if 'default_user' in self.request.POST:
1155 1159 # case for editing global IP list we do it for 'DEFAULT' user
1156 1160 raise HTTPFound(h.route_path('admin_permissions_ips'))
1157 1161 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1158 1162
1159 1163 @LoginRequired()
1160 1164 @HasPermissionAllDecorator('hg.admin')
1161 1165 def groups_management(self):
1162 1166 c = self.load_default_context()
1163 1167 c.user = self.db_user
1164 1168 c.data = c.user.group_member
1165 1169
1166 1170 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1167 1171 for group in c.user.group_member]
1168 1172 c.groups = json.dumps(groups)
1169 1173 c.active = 'groups'
1170 1174
1171 1175 return self._get_template_context(c)
1172 1176
1173 1177 @LoginRequired()
1174 1178 @HasPermissionAllDecorator('hg.admin')
1175 1179 @CSRFRequired()
1176 1180 def groups_management_updates(self):
1177 1181 _ = self.request.translate
1178 1182 c = self.load_default_context()
1179 1183
1180 1184 user_id = self.db_user_id
1181 1185 c.user = self.db_user
1182 1186
1183 1187 user_groups = set(self.request.POST.getall('users_group_id'))
1184 1188 user_groups_objects = []
1185 1189
1186 1190 for ugid in user_groups:
1187 1191 user_groups_objects.append(
1188 1192 UserGroupModel().get_group(safe_int(ugid)))
1189 1193 user_group_model = UserGroupModel()
1190 1194 added_to_groups, removed_from_groups = \
1191 1195 user_group_model.change_groups(c.user, user_groups_objects)
1192 1196
1193 1197 user_data = c.user.get_api_data()
1194 1198 for user_group_id in added_to_groups:
1195 1199 user_group = UserGroup.get(user_group_id)
1196 1200 old_values = user_group.get_api_data()
1197 1201 audit_logger.store_web(
1198 1202 'user_group.edit.member.add',
1199 1203 action_data={'user': user_data, 'old_data': old_values},
1200 1204 user=self._rhodecode_user)
1201 1205
1202 1206 for user_group_id in removed_from_groups:
1203 1207 user_group = UserGroup.get(user_group_id)
1204 1208 old_values = user_group.get_api_data()
1205 1209 audit_logger.store_web(
1206 1210 'user_group.edit.member.delete',
1207 1211 action_data={'user': user_data, 'old_data': old_values},
1208 1212 user=self._rhodecode_user)
1209 1213
1210 1214 Session().commit()
1211 1215 c.active = 'user_groups_management'
1212 1216 h.flash(_("Groups successfully changed"), category='success')
1213 1217
1214 1218 return HTTPFound(h.route_path(
1215 1219 'edit_user_groups_management', user_id=user_id))
1216 1220
1217 1221 @LoginRequired()
1218 1222 @HasPermissionAllDecorator('hg.admin')
1219 1223 def user_audit_logs(self):
1220 1224 _ = self.request.translate
1221 1225 c = self.load_default_context()
1222 1226 c.user = self.db_user
1223 1227
1224 1228 c.active = 'audit'
1225 1229
1226 1230 p = safe_int(self.request.GET.get('page', 1), 1)
1227 1231
1228 1232 filter_term = self.request.GET.get('filter')
1229 1233 user_log = UserModel().get_user_log(c.user, filter_term)
1230 1234
1231 1235 def url_generator(page_num):
1232 1236 query_params = {
1233 1237 'page': page_num
1234 1238 }
1235 1239 if filter_term:
1236 1240 query_params['filter'] = filter_term
1237 1241 return self.request.current_route_path(_query=query_params)
1238 1242
1239 1243 c.audit_logs = SqlPage(
1240 1244 user_log, page=p, items_per_page=10, url_maker=url_generator)
1241 1245 c.filter_term = filter_term
1242 1246 return self._get_template_context(c)
1243 1247
1244 1248 @LoginRequired()
1245 1249 @HasPermissionAllDecorator('hg.admin')
1246 1250 def user_audit_logs_download(self):
1247 1251 _ = self.request.translate
1248 1252 c = self.load_default_context()
1249 1253 c.user = self.db_user
1250 1254
1251 1255 user_log = UserModel().get_user_log(c.user, filter_term=None)
1252 1256
1253 1257 audit_log_data = {}
1254 1258 for entry in user_log:
1255 1259 audit_log_data[entry.user_log_id] = entry.get_dict()
1256 1260
1257 1261 response = Response(json.dumps(audit_log_data, indent=4))
1258 1262 response.content_disposition = str(
1259 1263 'attachment; filename=%s' % 'user_{}_audit_logs.json'.format(c.user.user_id))
1260 1264 response.content_type = 'application/json'
1261 1265
1262 1266 return response
1263 1267
1264 1268 @LoginRequired()
1265 1269 @HasPermissionAllDecorator('hg.admin')
1266 1270 def user_perms_summary(self):
1267 1271 _ = self.request.translate
1268 1272 c = self.load_default_context()
1269 1273 c.user = self.db_user
1270 1274
1271 1275 c.active = 'perms_summary'
1272 1276 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1273 1277
1274 1278 return self._get_template_context(c)
1275 1279
1276 1280 @LoginRequired()
1277 1281 @HasPermissionAllDecorator('hg.admin')
1278 1282 def user_perms_summary_json(self):
1279 1283 self.load_default_context()
1280 1284 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1281 1285
1282 1286 return perm_user.permissions
1283 1287
1284 1288 @LoginRequired()
1285 1289 @HasPermissionAllDecorator('hg.admin')
1286 1290 def user_caches(self):
1287 1291 _ = self.request.translate
1288 1292 c = self.load_default_context()
1289 1293 c.user = self.db_user
1290 1294
1291 1295 c.active = 'caches'
1292 1296 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1293 1297
1294 1298 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1295 1299 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1296 1300 c.backend = c.region.backend
1297 1301 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1298 1302
1299 1303 return self._get_template_context(c)
1300 1304
1301 1305 @LoginRequired()
1302 1306 @HasPermissionAllDecorator('hg.admin')
1303 1307 @CSRFRequired()
1304 1308 def user_caches_update(self):
1305 1309 _ = self.request.translate
1306 1310 c = self.load_default_context()
1307 1311 c.user = self.db_user
1308 1312
1309 1313 c.active = 'caches'
1310 1314 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1311 1315
1312 1316 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1313 1317 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1314 1318
1315 1319 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1316 1320
1317 1321 return HTTPFound(h.route_path(
1318 1322 'edit_user_caches', user_id=c.user.user_id))
@@ -1,390 +1,390 b''
1 1 import sys
2 2 import threading
3 3 import weakref
4 4 from base64 import b64encode
5 5 from logging import getLogger
6 6 from os import urandom
7 7
8 8 from redis import StrictRedis
9 9
10 10 __version__ = '3.7.0'
11 11
12 12 loggers = {
13 13 k: getLogger("rhodecode." + ".".join((__name__, k)))
14 14 for k in [
15 15 "acquire",
16 16 "refresh.thread.start",
17 17 "refresh.thread.stop",
18 18 "refresh.thread.exit",
19 19 "refresh.start",
20 20 "refresh.shutdown",
21 21 "refresh.exit",
22 22 "release",
23 23 ]
24 24 }
25 25
26 26 PY3 = sys.version_info[0] == 3
27 27
28 28 if PY3:
29 29 text_type = str
30 30 binary_type = bytes
31 31 else:
32 32 text_type = unicode # noqa
33 33 binary_type = str
34 34
35 35
36 36 # Check if the id match. If not, return an error code.
37 37 UNLOCK_SCRIPT = b"""
38 38 if redis.call("get", KEYS[1]) ~= ARGV[1] then
39 39 return 1
40 40 else
41 41 redis.call("del", KEYS[2])
42 42 redis.call("lpush", KEYS[2], 1)
43 43 redis.call("pexpire", KEYS[2], ARGV[2])
44 44 redis.call("del", KEYS[1])
45 45 return 0
46 46 end
47 47 """
48 48
49 49 # Covers both cases when key doesn't exist and doesn't equal to lock's id
50 50 EXTEND_SCRIPT = b"""
51 51 if redis.call("get", KEYS[1]) ~= ARGV[1] then
52 52 return 1
53 53 elseif redis.call("ttl", KEYS[1]) < 0 then
54 54 return 2
55 55 else
56 56 redis.call("expire", KEYS[1], ARGV[2])
57 57 return 0
58 58 end
59 59 """
60 60
61 61 RESET_SCRIPT = b"""
62 62 redis.call('del', KEYS[2])
63 63 redis.call('lpush', KEYS[2], 1)
64 64 redis.call('pexpire', KEYS[2], ARGV[2])
65 65 return redis.call('del', KEYS[1])
66 66 """
67 67
68 68 RESET_ALL_SCRIPT = b"""
69 69 local locks = redis.call('keys', 'lock:*')
70 70 local signal
71 71 for _, lock in pairs(locks) do
72 72 signal = 'lock-signal:' .. string.sub(lock, 6)
73 73 redis.call('del', signal)
74 74 redis.call('lpush', signal, 1)
75 75 redis.call('expire', signal, 1)
76 76 redis.call('del', lock)
77 77 end
78 78 return #locks
79 79 """
80 80
81 81
82 82 class AlreadyAcquired(RuntimeError):
83 83 pass
84 84
85 85
86 86 class NotAcquired(RuntimeError):
87 87 pass
88 88
89 89
90 90 class AlreadyStarted(RuntimeError):
91 91 pass
92 92
93 93
94 94 class TimeoutNotUsable(RuntimeError):
95 95 pass
96 96
97 97
98 98 class InvalidTimeout(RuntimeError):
99 99 pass
100 100
101 101
102 102 class TimeoutTooLarge(RuntimeError):
103 103 pass
104 104
105 105
106 106 class NotExpirable(RuntimeError):
107 107 pass
108 108
109 109
110 110 class Lock(object):
111 111 """
112 112 A Lock context manager implemented via redis SETNX/BLPOP.
113 113 """
114 114 unlock_script = None
115 115 extend_script = None
116 116 reset_script = None
117 117 reset_all_script = None
118 118
119 119 def __init__(self, redis_client, name, expire=None, id=None, auto_renewal=False, strict=True, signal_expire=1000):
120 120 """
121 121 :param redis_client:
122 122 An instance of :class:`~StrictRedis`.
123 123 :param name:
124 124 The name (redis key) the lock should have.
125 125 :param expire:
126 126 The lock expiry time in seconds. If left at the default (None)
127 127 the lock will not expire.
128 128 :param id:
129 129 The ID (redis value) the lock should have. A random value is
130 130 generated when left at the default.
131 131
132 132 Note that if you specify this then the lock is marked as "held". Acquires
133 133 won't be possible.
134 134 :param auto_renewal:
135 135 If set to ``True``, Lock will automatically renew the lock so that it
136 136 doesn't expire for as long as the lock is held (acquire() called
137 137 or running in a context manager).
138 138
139 139 Implementation note: Renewal will happen using a daemon thread with
140 140 an interval of ``expire*2/3``. If wishing to use a different renewal
141 141 time, subclass Lock, call ``super().__init__()`` then set
142 142 ``self._lock_renewal_interval`` to your desired interval.
143 143 :param strict:
144 144 If set ``True`` then the ``redis_client`` needs to be an instance of ``redis.StrictRedis``.
145 145 :param signal_expire:
146 146 Advanced option to override signal list expiration in milliseconds. Increase it for very slow clients. Default: ``1000``.
147 147 """
148 148 if strict and not isinstance(redis_client, StrictRedis):
149 149 raise ValueError("redis_client must be instance of StrictRedis. "
150 150 "Use strict=False if you know what you're doing.")
151 151 if auto_renewal and expire is None:
152 152 raise ValueError("Expire may not be None when auto_renewal is set")
153 153
154 154 self._client = redis_client
155 155
156 156 if expire:
157 157 expire = int(expire)
158 158 if expire < 0:
159 159 raise ValueError("A negative expire is not acceptable.")
160 160 else:
161 161 expire = None
162 162 self._expire = expire
163 163
164 164 self._signal_expire = signal_expire
165 165 if id is None:
166 166 self._id = b64encode(urandom(18)).decode('ascii')
167 167 elif isinstance(id, binary_type):
168 168 try:
169 169 self._id = id.decode('ascii')
170 170 except UnicodeDecodeError:
171 171 self._id = b64encode(id).decode('ascii')
172 172 elif isinstance(id, text_type):
173 173 self._id = id
174 174 else:
175 175 raise TypeError("Incorrect type for `id`. Must be bytes/str not %s." % type(id))
176 176 self._name = 'lock:' + name
177 177 self._signal = 'lock-signal:' + name
178 178 self._lock_renewal_interval = (float(expire) * 2 / 3
179 179 if auto_renewal
180 180 else None)
181 181 self._lock_renewal_thread = None
182 182
183 183 self.register_scripts(redis_client)
184 184
185 185 @classmethod
186 186 def register_scripts(cls, redis_client):
187 187 global reset_all_script
188 188 if reset_all_script is None:
189 189 reset_all_script = redis_client.register_script(RESET_ALL_SCRIPT)
190 190 cls.unlock_script = redis_client.register_script(UNLOCK_SCRIPT)
191 191 cls.extend_script = redis_client.register_script(EXTEND_SCRIPT)
192 192 cls.reset_script = redis_client.register_script(RESET_SCRIPT)
193 193 cls.reset_all_script = redis_client.register_script(RESET_ALL_SCRIPT)
194 194
195 195 @property
196 196 def _held(self):
197 197 return self.id == self.get_owner_id()
198 198
199 199 def reset(self):
200 200 """
201 201 Forcibly deletes the lock. Use this with care.
202 202 """
203 203 self.reset_script(client=self._client, keys=(self._name, self._signal), args=(self.id, self._signal_expire))
204 204
205 205 @property
206 206 def id(self):
207 207 return self._id
208 208
209 209 def get_owner_id(self):
210 210 owner_id = self._client.get(self._name)
211 211 if isinstance(owner_id, binary_type):
212 212 owner_id = owner_id.decode('ascii', 'replace')
213 213 return owner_id
214 214
215 215 def acquire(self, blocking=True, timeout=None):
216 216 """
217 217 :param blocking:
218 218 Boolean value specifying whether lock should be blocking or not.
219 219 :param timeout:
220 220 An integer value specifying the maximum number of seconds to block.
221 221 """
222 222 logger = loggers["acquire"]
223 223
224 logger.debug("Getting acquire on %r ...", self._name)
224 logger.debug("Getting blocking: %s acquire on %r ...", blocking, self._name)
225 225
226 226 if self._held:
227 227 owner_id = self.get_owner_id()
228 228 raise AlreadyAcquired("Already acquired from this Lock instance. Lock id: {}".format(owner_id))
229 229
230 230 if not blocking and timeout is not None:
231 231 raise TimeoutNotUsable("Timeout cannot be used if blocking=False")
232 232
233 233 if timeout:
234 234 timeout = int(timeout)
235 235 if timeout < 0:
236 236 raise InvalidTimeout("Timeout (%d) cannot be less than or equal to 0" % timeout)
237 237
238 238 if self._expire and not self._lock_renewal_interval and timeout > self._expire:
239 239 raise TimeoutTooLarge("Timeout (%d) cannot be greater than expire (%d)" % (timeout, self._expire))
240 240
241 241 busy = True
242 242 blpop_timeout = timeout or self._expire or 0
243 243 timed_out = False
244 244 while busy:
245 245 busy = not self._client.set(self._name, self._id, nx=True, ex=self._expire)
246 246 if busy:
247 247 if timed_out:
248 248 return False
249 249 elif blocking:
250 250 timed_out = not self._client.blpop(self._signal, blpop_timeout) and timeout
251 251 else:
252 252 logger.warning("Failed to get %r.", self._name)
253 253 return False
254 254
255 255 logger.info("Got lock for %r.", self._name)
256 256 if self._lock_renewal_interval is not None:
257 257 self._start_lock_renewer()
258 258 return True
259 259
260 260 def extend(self, expire=None):
261 261 """Extends expiration time of the lock.
262 262
263 263 :param expire:
264 264 New expiration time. If ``None`` - `expire` provided during
265 265 lock initialization will be taken.
266 266 """
267 267 if expire:
268 268 expire = int(expire)
269 269 if expire < 0:
270 270 raise ValueError("A negative expire is not acceptable.")
271 271 elif self._expire is not None:
272 272 expire = self._expire
273 273 else:
274 274 raise TypeError(
275 275 "To extend a lock 'expire' must be provided as an "
276 276 "argument to extend() method or at initialization time."
277 277 )
278 278
279 279 error = self.extend_script(client=self._client, keys=(self._name, self._signal), args=(self._id, expire))
280 280 if error == 1:
281 281 raise NotAcquired("Lock %s is not acquired or it already expired." % self._name)
282 282 elif error == 2:
283 283 raise NotExpirable("Lock %s has no assigned expiration time" % self._name)
284 284 elif error:
285 285 raise RuntimeError("Unsupported error code %s from EXTEND script" % error)
286 286
287 287 @staticmethod
288 288 def _lock_renewer(lockref, interval, stop):
289 289 """
290 290 Renew the lock key in redis every `interval` seconds for as long
291 291 as `self._lock_renewal_thread.should_exit` is False.
292 292 """
293 293 while not stop.wait(timeout=interval):
294 294 loggers["refresh.thread.start"].debug("Refreshing lock")
295 295 lock = lockref()
296 296 if lock is None:
297 297 loggers["refresh.thread.stop"].debug(
298 298 "The lock no longer exists, stopping lock refreshing"
299 299 )
300 300 break
301 301 lock.extend(expire=lock._expire)
302 302 del lock
303 303 loggers["refresh.thread.exit"].debug("Exit requested, stopping lock refreshing")
304 304
305 305 def _start_lock_renewer(self):
306 306 """
307 307 Starts the lock refresher thread.
308 308 """
309 309 if self._lock_renewal_thread is not None:
310 310 raise AlreadyStarted("Lock refresh thread already started")
311 311
312 312 loggers["refresh.start"].debug(
313 313 "Starting thread to refresh lock every %s seconds",
314 314 self._lock_renewal_interval
315 315 )
316 316 self._lock_renewal_stop = threading.Event()
317 317 self._lock_renewal_thread = threading.Thread(
318 318 group=None,
319 319 target=self._lock_renewer,
320 320 kwargs={'lockref': weakref.ref(self),
321 321 'interval': self._lock_renewal_interval,
322 322 'stop': self._lock_renewal_stop}
323 323 )
324 324 self._lock_renewal_thread.setDaemon(True)
325 325 self._lock_renewal_thread.start()
326 326
327 327 def _stop_lock_renewer(self):
328 328 """
329 329 Stop the lock renewer.
330 330
331 331 This signals the renewal thread and waits for its exit.
332 332 """
333 333 if self._lock_renewal_thread is None or not self._lock_renewal_thread.is_alive():
334 334 return
335 335 loggers["refresh.shutdown"].debug("Signalling the lock refresher to stop")
336 336 self._lock_renewal_stop.set()
337 337 self._lock_renewal_thread.join()
338 338 self._lock_renewal_thread = None
339 339 loggers["refresh.exit"].debug("Lock refresher has stopped")
340 340
341 341 def __enter__(self):
342 342 acquired = self.acquire(blocking=True)
343 343 assert acquired, "Lock wasn't acquired, but blocking=True"
344 344 return self
345 345
346 346 def __exit__(self, exc_type=None, exc_value=None, traceback=None):
347 347 self.release()
348 348
349 349 def release(self):
350 350 """Releases the lock, that was acquired with the same object.
351 351
352 352 .. note::
353 353
354 354 If you want to release a lock that you acquired in a different place you have two choices:
355 355
356 356 * Use ``Lock("name", id=id_from_other_place).release()``
357 357 * Use ``Lock("name").reset()``
358 358 """
359 359 if self._lock_renewal_thread is not None:
360 360 self._stop_lock_renewer()
361 361 loggers["release"].debug("Releasing %r.", self._name)
362 362 error = self.unlock_script(client=self._client, keys=(self._name, self._signal), args=(self._id, self._signal_expire))
363 363 if error == 1:
364 364 raise NotAcquired("Lock %s is not acquired or it already expired." % self._name)
365 365 elif error:
366 366 raise RuntimeError("Unsupported error code %s from EXTEND script." % error)
367 367
368 368 def locked(self):
369 369 """
370 370 Return true if the lock is acquired.
371 371
372 372 Checks that lock with same name already exists. This method returns true, even if
373 373 lock have another id.
374 374 """
375 375 return self._client.exists(self._name) == 1
376 376
377 377
378 378 reset_all_script = None
379 379
380 380
381 381 def reset_all(redis_client):
382 382 """
383 383 Forcibly deletes all locks if its remains (like a crash reason). Use this with care.
384 384
385 385 :param redis_client:
386 386 An instance of :class:`~StrictRedis`.
387 387 """
388 388 Lock.register_scripts(redis_client)
389 389
390 390 reset_all_script(client=redis_client) # noqa
@@ -1,2148 +1,2147 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Helper functions
23 23
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 27 import base64
28 28 import collections
29 29
30 30 import os
31 31 import random
32 32 import hashlib
33 33 import StringIO
34 34 import textwrap
35 35 import urllib
36 36 import math
37 37 import logging
38 38 import re
39 39 import time
40 40 import string
41 41 import hashlib
42 42 import regex
43 43 from collections import OrderedDict
44 44
45 45 import pygments
46 46 import itertools
47 47 import fnmatch
48 48 import bleach
49 49
50 50 from pyramid import compat
51 51 from datetime import datetime
52 52 from functools import partial
53 53 from pygments.formatters.html import HtmlFormatter
54 54 from pygments.lexers import (
55 55 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
56 56
57 57 from pyramid.threadlocal import get_current_request
58 58 from tempita import looper
59 59 from webhelpers2.html import literal, HTML, escape
60 60 from webhelpers2.html._autolink import _auto_link_urls
61 61 from webhelpers2.html.tools import (
62 62 button_to, highlight, js_obfuscate, strip_links, strip_tags)
63 63
64 64 from webhelpers2.text import (
65 65 chop_at, collapse, convert_accented_entities,
66 66 convert_misc_entities, lchop, plural, rchop, remove_formatting,
67 67 replace_whitespace, urlify, truncate, wrap_paragraphs)
68 68 from webhelpers2.date import time_ago_in_words
69 69
70 70 from webhelpers2.html.tags import (
71 71 _input, NotGiven, _make_safe_id_component as safeid,
72 72 form as insecure_form,
73 73 auto_discovery_link, checkbox, end_form, file,
74 74 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
75 75 select as raw_select, stylesheet_link, submit, text, password, textarea,
76 76 ul, radio, Options)
77 77
78 78 from webhelpers2.number import format_byte_size
79 79
80 80 from rhodecode.lib.action_parser import action_parser
81 81 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
82 82 from rhodecode.lib.ext_json import json
83 83 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
84 84 from rhodecode.lib.utils2 import (
85 85 str2bool, safe_unicode, safe_str,
86 86 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
87 87 AttributeDict, safe_int, md5, md5_safe, get_host_info)
88 88 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
89 89 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
90 90 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
91 91 from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
92 92 from rhodecode.lib.index.search_utils import get_matching_line_offsets
93 93 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
94 94 from rhodecode.model.changeset_status import ChangesetStatusModel
95 95 from rhodecode.model.db import Permission, User, Repository, UserApiKeys, FileStore
96 96 from rhodecode.model.repo_group import RepoGroupModel
97 97 from rhodecode.model.settings import IssueTrackerSettingsModel
98 98
99 99
100 100 log = logging.getLogger(__name__)
101 101
102 102
103 103 DEFAULT_USER = User.DEFAULT_USER
104 104 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
105 105
106 106
107 107 def asset(path, ver=None, **kwargs):
108 108 """
109 109 Helper to generate a static asset file path for rhodecode assets
110 110
111 111 eg. h.asset('images/image.png', ver='3923')
112 112
113 113 :param path: path of asset
114 114 :param ver: optional version query param to append as ?ver=
115 115 """
116 116 request = get_current_request()
117 117 query = {}
118 118 query.update(kwargs)
119 119 if ver:
120 120 query = {'ver': ver}
121 121 return request.static_path(
122 122 'rhodecode:public/{}'.format(path), _query=query)
123 123
124 124
125 125 default_html_escape_table = {
126 126 ord('&'): u'&amp;',
127 127 ord('<'): u'&lt;',
128 128 ord('>'): u'&gt;',
129 129 ord('"'): u'&quot;',
130 130 ord("'"): u'&#39;',
131 131 }
132 132
133 133
134 134 def html_escape(text, html_escape_table=default_html_escape_table):
135 135 """Produce entities within text."""
136 136 return text.translate(html_escape_table)
137 137
138 138
139 139 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
140 140 """
141 141 Truncate string ``s`` at the first occurrence of ``sub``.
142 142
143 143 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
144 144 """
145 145 suffix_if_chopped = suffix_if_chopped or ''
146 146 pos = s.find(sub)
147 147 if pos == -1:
148 148 return s
149 149
150 150 if inclusive:
151 151 pos += len(sub)
152 152
153 153 chopped = s[:pos]
154 154 left = s[pos:].strip()
155 155
156 156 if left and suffix_if_chopped:
157 157 chopped += suffix_if_chopped
158 158
159 159 return chopped
160 160
161 161
162 162 def shorter(text, size=20, prefix=False):
163 163 postfix = '...'
164 164 if len(text) > size:
165 165 if prefix:
166 166 # shorten in front
167 167 return postfix + text[-(size - len(postfix)):]
168 168 else:
169 169 return text[:size - len(postfix)] + postfix
170 170 return text
171 171
172 172
173 173 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
174 174 """
175 175 Reset button
176 176 """
177 177 return _input(type, name, value, id, attrs)
178 178
179 179
180 180 def select(name, selected_values, options, id=NotGiven, **attrs):
181 181
182 182 if isinstance(options, (list, tuple)):
183 183 options_iter = options
184 184 # Handle old value,label lists ... where value also can be value,label lists
185 185 options = Options()
186 186 for opt in options_iter:
187 187 if isinstance(opt, tuple) and len(opt) == 2:
188 188 value, label = opt
189 189 elif isinstance(opt, basestring):
190 190 value = label = opt
191 191 else:
192 192 raise ValueError('invalid select option type %r' % type(opt))
193 193
194 194 if isinstance(value, (list, tuple)):
195 195 option_group = options.add_optgroup(label)
196 196 for opt2 in value:
197 197 if isinstance(opt2, tuple) and len(opt2) == 2:
198 198 group_value, group_label = opt2
199 199 elif isinstance(opt2, basestring):
200 200 group_value = group_label = opt2
201 201 else:
202 202 raise ValueError('invalid select option type %r' % type(opt2))
203 203
204 204 option_group.add_option(group_label, group_value)
205 205 else:
206 206 options.add_option(label, value)
207 207
208 208 return raw_select(name, selected_values, options, id=id, **attrs)
209 209
210 210
211 211 def branding(name, length=40):
212 212 return truncate(name, length, indicator="")
213 213
214 214
215 215 def FID(raw_id, path):
216 216 """
217 217 Creates a unique ID for filenode based on it's hash of path and commit
218 218 it's safe to use in urls
219 219
220 220 :param raw_id:
221 221 :param path:
222 222 """
223 223
224 224 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
225 225
226 226
227 227 class _GetError(object):
228 228 """Get error from form_errors, and represent it as span wrapped error
229 229 message
230 230
231 231 :param field_name: field to fetch errors for
232 232 :param form_errors: form errors dict
233 233 """
234 234
235 235 def __call__(self, field_name, form_errors):
236 236 tmpl = """<span class="error_msg">%s</span>"""
237 237 if form_errors and field_name in form_errors:
238 238 return literal(tmpl % form_errors.get(field_name))
239 239
240 240
241 241 get_error = _GetError()
242 242
243 243
244 244 class _ToolTip(object):
245 245
246 246 def __call__(self, tooltip_title, trim_at=50):
247 247 """
248 248 Special function just to wrap our text into nice formatted
249 249 autowrapped text
250 250
251 251 :param tooltip_title:
252 252 """
253 253 tooltip_title = escape(tooltip_title)
254 254 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
255 255 return tooltip_title
256 256
257 257
258 258 tooltip = _ToolTip()
259 259
260 260 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
261 261
262 262
263 263 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
264 264 limit_items=False, linkify_last_item=False, hide_last_item=False,
265 265 copy_path_icon=True):
266 266 if isinstance(file_path, str):
267 267 file_path = safe_unicode(file_path)
268 268
269 269 if at_ref:
270 270 route_qry = {'at': at_ref}
271 271 default_landing_ref = at_ref or landing_ref_name or commit_id
272 272 else:
273 273 route_qry = None
274 274 default_landing_ref = commit_id
275 275
276 276 # first segment is a `HOME` link to repo files root location
277 277 root_name = literal(u'<i class="icon-home"></i>')
278 278
279 279 url_segments = [
280 280 link_to(
281 281 root_name,
282 282 repo_files_by_ref_url(
283 283 repo_name,
284 284 repo_type,
285 285 f_path=None, # None here is a special case for SVN repos,
286 286 # that won't prefix with a ref
287 287 ref_name=default_landing_ref,
288 288 commit_id=commit_id,
289 289 query=route_qry
290 290 )
291 291 )]
292 292
293 293 path_segments = file_path.split('/')
294 294 last_cnt = len(path_segments) - 1
295 295 for cnt, segment in enumerate(path_segments):
296 296 if not segment:
297 297 continue
298 298 segment_html = escape(segment)
299 299
300 300 last_item = cnt == last_cnt
301 301
302 302 if last_item and hide_last_item:
303 303 # iterate over and hide last element
304 304 continue
305 305
306 306 if last_item and linkify_last_item is False:
307 307 # plain version
308 308 url_segments.append(segment_html)
309 309 else:
310 310 url_segments.append(
311 311 link_to(
312 312 segment_html,
313 313 repo_files_by_ref_url(
314 314 repo_name,
315 315 repo_type,
316 316 f_path='/'.join(path_segments[:cnt + 1]),
317 317 ref_name=default_landing_ref,
318 318 commit_id=commit_id,
319 319 query=route_qry
320 320 ),
321 321 ))
322 322
323 323 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
324 324 if limit_items and len(limited_url_segments) < len(url_segments):
325 325 url_segments = limited_url_segments
326 326
327 327 full_path = file_path
328 328 if copy_path_icon:
329 329 icon = files_icon.format(escape(full_path))
330 330 else:
331 331 icon = ''
332 332
333 333 if file_path == '':
334 334 return root_name
335 335 else:
336 336 return literal(' / '.join(url_segments) + icon)
337 337
338 338
339 339 def files_url_data(request):
340 340 matchdict = request.matchdict
341 341
342 342 if 'f_path' not in matchdict:
343 343 matchdict['f_path'] = ''
344 344
345 345 if 'commit_id' not in matchdict:
346 346 matchdict['commit_id'] = 'tip'
347 347
348 348 return json.dumps(matchdict)
349 349
350 350
351 351 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
352 352 _is_svn = is_svn(db_repo_type)
353 353 final_f_path = f_path
354 354
355 355 if _is_svn:
356 356 """
357 357 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
358 358 actually commit_id followed by the ref_name. This should be done only in case
359 359 This is a initial landing url, without additional paths.
360 360
361 361 like: /1000/tags/1.0.0/?at=tags/1.0.0
362 362 """
363 363
364 364 if ref_name and ref_name != 'tip':
365 365 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
366 366 # for SVN we only do this magic prefix if it's root, .eg landing revision
367 367 # of files link. If we are in the tree we don't need this since we traverse the url
368 368 # that has everything stored
369 369 if f_path in ['', '/']:
370 370 final_f_path = '/'.join([ref_name, f_path])
371 371
372 372 # SVN always needs a commit_id explicitly, without a named REF
373 373 default_commit_id = commit_id
374 374 else:
375 375 """
376 376 For git and mercurial we construct a new URL using the names instead of commit_id
377 377 like: /master/some_path?at=master
378 378 """
379 379 # We currently do not support branches with slashes
380 380 if '/' in ref_name:
381 381 default_commit_id = commit_id
382 382 else:
383 383 default_commit_id = ref_name
384 384
385 385 # sometimes we pass f_path as None, to indicate explicit no prefix,
386 386 # we translate it to string to not have None
387 387 final_f_path = final_f_path or ''
388 388
389 389 files_url = route_path(
390 390 'repo_files',
391 391 repo_name=db_repo_name,
392 392 commit_id=default_commit_id,
393 393 f_path=final_f_path,
394 394 _query=query
395 395 )
396 396 return files_url
397 397
398 398
399 399 def code_highlight(code, lexer, formatter, use_hl_filter=False):
400 400 """
401 401 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
402 402
403 403 If ``outfile`` is given and a valid file object (an object
404 404 with a ``write`` method), the result will be written to it, otherwise
405 405 it is returned as a string.
406 406 """
407 407 if use_hl_filter:
408 408 # add HL filter
409 409 from rhodecode.lib.index import search_utils
410 410 lexer.add_filter(search_utils.ElasticSearchHLFilter())
411 411 return pygments.format(pygments.lex(code, lexer), formatter)
412 412
413 413
414 414 class CodeHtmlFormatter(HtmlFormatter):
415 415 """
416 416 My code Html Formatter for source codes
417 417 """
418 418
419 419 def wrap(self, source, outfile):
420 420 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
421 421
422 422 def _wrap_code(self, source):
423 423 for cnt, it in enumerate(source):
424 424 i, t = it
425 425 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
426 426 yield i, t
427 427
428 428 def _wrap_tablelinenos(self, inner):
429 429 dummyoutfile = StringIO.StringIO()
430 430 lncount = 0
431 431 for t, line in inner:
432 432 if t:
433 433 lncount += 1
434 434 dummyoutfile.write(line)
435 435
436 436 fl = self.linenostart
437 437 mw = len(str(lncount + fl - 1))
438 438 sp = self.linenospecial
439 439 st = self.linenostep
440 440 la = self.lineanchors
441 441 aln = self.anchorlinenos
442 442 nocls = self.noclasses
443 443 if sp:
444 444 lines = []
445 445
446 446 for i in range(fl, fl + lncount):
447 447 if i % st == 0:
448 448 if i % sp == 0:
449 449 if aln:
450 450 lines.append('<a href="#%s%d" class="special">%*d</a>' %
451 451 (la, i, mw, i))
452 452 else:
453 453 lines.append('<span class="special">%*d</span>' % (mw, i))
454 454 else:
455 455 if aln:
456 456 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
457 457 else:
458 458 lines.append('%*d' % (mw, i))
459 459 else:
460 460 lines.append('')
461 461 ls = '\n'.join(lines)
462 462 else:
463 463 lines = []
464 464 for i in range(fl, fl + lncount):
465 465 if i % st == 0:
466 466 if aln:
467 467 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
468 468 else:
469 469 lines.append('%*d' % (mw, i))
470 470 else:
471 471 lines.append('')
472 472 ls = '\n'.join(lines)
473 473
474 474 # in case you wonder about the seemingly redundant <div> here: since the
475 475 # content in the other cell also is wrapped in a div, some browsers in
476 476 # some configurations seem to mess up the formatting...
477 477 if nocls:
478 478 yield 0, ('<table class="%stable">' % self.cssclass +
479 479 '<tr><td><div class="linenodiv" '
480 480 'style="background-color: #f0f0f0; padding-right: 10px">'
481 481 '<pre style="line-height: 125%">' +
482 482 ls + '</pre></div></td><td id="hlcode" class="code">')
483 483 else:
484 484 yield 0, ('<table class="%stable">' % self.cssclass +
485 485 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
486 486 ls + '</pre></div></td><td id="hlcode" class="code">')
487 487 yield 0, dummyoutfile.getvalue()
488 488 yield 0, '</td></tr></table>'
489 489
490 490
491 491 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
492 492 def __init__(self, **kw):
493 493 # only show these line numbers if set
494 494 self.only_lines = kw.pop('only_line_numbers', [])
495 495 self.query_terms = kw.pop('query_terms', [])
496 496 self.max_lines = kw.pop('max_lines', 5)
497 497 self.line_context = kw.pop('line_context', 3)
498 498 self.url = kw.pop('url', None)
499 499
500 500 super(CodeHtmlFormatter, self).__init__(**kw)
501 501
502 502 def _wrap_code(self, source):
503 503 for cnt, it in enumerate(source):
504 504 i, t = it
505 505 t = '<pre>%s</pre>' % t
506 506 yield i, t
507 507
508 508 def _wrap_tablelinenos(self, inner):
509 509 yield 0, '<table class="code-highlight %stable">' % self.cssclass
510 510
511 511 last_shown_line_number = 0
512 512 current_line_number = 1
513 513
514 514 for t, line in inner:
515 515 if not t:
516 516 yield t, line
517 517 continue
518 518
519 519 if current_line_number in self.only_lines:
520 520 if last_shown_line_number + 1 != current_line_number:
521 521 yield 0, '<tr>'
522 522 yield 0, '<td class="line">...</td>'
523 523 yield 0, '<td id="hlcode" class="code"></td>'
524 524 yield 0, '</tr>'
525 525
526 526 yield 0, '<tr>'
527 527 if self.url:
528 528 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
529 529 self.url, current_line_number, current_line_number)
530 530 else:
531 531 yield 0, '<td class="line"><a href="">%i</a></td>' % (
532 532 current_line_number)
533 533 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
534 534 yield 0, '</tr>'
535 535
536 536 last_shown_line_number = current_line_number
537 537
538 538 current_line_number += 1
539 539
540 540 yield 0, '</table>'
541 541
542 542
543 543 def hsv_to_rgb(h, s, v):
544 544 """ Convert hsv color values to rgb """
545 545
546 546 if s == 0.0:
547 547 return v, v, v
548 548 i = int(h * 6.0) # XXX assume int() truncates!
549 549 f = (h * 6.0) - i
550 550 p = v * (1.0 - s)
551 551 q = v * (1.0 - s * f)
552 552 t = v * (1.0 - s * (1.0 - f))
553 553 i = i % 6
554 554 if i == 0:
555 555 return v, t, p
556 556 if i == 1:
557 557 return q, v, p
558 558 if i == 2:
559 559 return p, v, t
560 560 if i == 3:
561 561 return p, q, v
562 562 if i == 4:
563 563 return t, p, v
564 564 if i == 5:
565 565 return v, p, q
566 566
567 567
568 568 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
569 569 """
570 570 Generator for getting n of evenly distributed colors using
571 571 hsv color and golden ratio. It always return same order of colors
572 572
573 573 :param n: number of colors to generate
574 574 :param saturation: saturation of returned colors
575 575 :param lightness: lightness of returned colors
576 576 :returns: RGB tuple
577 577 """
578 578
579 579 golden_ratio = 0.618033988749895
580 580 h = 0.22717784590367374
581 581
582 582 for _ in xrange(n):
583 583 h += golden_ratio
584 584 h %= 1
585 585 HSV_tuple = [h, saturation, lightness]
586 586 RGB_tuple = hsv_to_rgb(*HSV_tuple)
587 587 yield map(lambda x: str(int(x * 256)), RGB_tuple)
588 588
589 589
590 590 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
591 591 """
592 592 Returns a function which when called with an argument returns a unique
593 593 color for that argument, eg.
594 594
595 595 :param n: number of colors to generate
596 596 :param saturation: saturation of returned colors
597 597 :param lightness: lightness of returned colors
598 598 :returns: css RGB string
599 599
600 600 >>> color_hash = color_hasher()
601 601 >>> color_hash('hello')
602 602 'rgb(34, 12, 59)'
603 603 >>> color_hash('hello')
604 604 'rgb(34, 12, 59)'
605 605 >>> color_hash('other')
606 606 'rgb(90, 224, 159)'
607 607 """
608 608
609 609 color_dict = {}
610 610 cgenerator = unique_color_generator(
611 611 saturation=saturation, lightness=lightness)
612 612
613 613 def get_color_string(thing):
614 614 if thing in color_dict:
615 615 col = color_dict[thing]
616 616 else:
617 617 col = color_dict[thing] = cgenerator.next()
618 618 return "rgb(%s)" % (', '.join(col))
619 619
620 620 return get_color_string
621 621
622 622
623 623 def get_lexer_safe(mimetype=None, filepath=None):
624 624 """
625 625 Tries to return a relevant pygments lexer using mimetype/filepath name,
626 626 defaulting to plain text if none could be found
627 627 """
628 628 lexer = None
629 629 try:
630 630 if mimetype:
631 631 lexer = get_lexer_for_mimetype(mimetype)
632 632 if not lexer:
633 633 lexer = get_lexer_for_filename(filepath)
634 634 except pygments.util.ClassNotFound:
635 635 pass
636 636
637 637 if not lexer:
638 638 lexer = get_lexer_by_name('text')
639 639
640 640 return lexer
641 641
642 642
643 643 def get_lexer_for_filenode(filenode):
644 644 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
645 645 return lexer
646 646
647 647
648 648 def pygmentize(filenode, **kwargs):
649 649 """
650 650 pygmentize function using pygments
651 651
652 652 :param filenode:
653 653 """
654 654 lexer = get_lexer_for_filenode(filenode)
655 655 return literal(code_highlight(filenode.content, lexer,
656 656 CodeHtmlFormatter(**kwargs)))
657 657
658 658
659 659 def is_following_repo(repo_name, user_id):
660 660 from rhodecode.model.scm import ScmModel
661 661 return ScmModel().is_following_repo(repo_name, user_id)
662 662
663 663
664 664 class _Message(object):
665 665 """A message returned by ``Flash.pop_messages()``.
666 666
667 667 Converting the message to a string returns the message text. Instances
668 668 also have the following attributes:
669 669
670 670 * ``message``: the message text.
671 671 * ``category``: the category specified when the message was created.
672 672 """
673 673
674 674 def __init__(self, category, message, sub_data=None):
675 675 self.category = category
676 676 self.message = message
677 677 self.sub_data = sub_data or {}
678 678
679 679 def __str__(self):
680 680 return self.message
681 681
682 682 __unicode__ = __str__
683 683
684 684 def __html__(self):
685 685 return escape(safe_unicode(self.message))
686 686
687 687
688 688 class Flash(object):
689 689 # List of allowed categories. If None, allow any category.
690 690 categories = ["warning", "notice", "error", "success"]
691 691
692 692 # Default category if none is specified.
693 693 default_category = "notice"
694 694
695 695 def __init__(self, session_key="flash", categories=None,
696 696 default_category=None):
697 697 """
698 698 Instantiate a ``Flash`` object.
699 699
700 700 ``session_key`` is the key to save the messages under in the user's
701 701 session.
702 702
703 703 ``categories`` is an optional list which overrides the default list
704 704 of categories.
705 705
706 706 ``default_category`` overrides the default category used for messages
707 707 when none is specified.
708 708 """
709 709 self.session_key = session_key
710 710 if categories is not None:
711 711 self.categories = categories
712 712 if default_category is not None:
713 713 self.default_category = default_category
714 714 if self.categories and self.default_category not in self.categories:
715 715 raise ValueError(
716 716 "unrecognized default category %r" % (self.default_category,))
717 717
718 718 def pop_messages(self, session=None, request=None):
719 719 """
720 720 Return all accumulated messages and delete them from the session.
721 721
722 722 The return value is a list of ``Message`` objects.
723 723 """
724 724 messages = []
725 725
726 726 if not session:
727 727 if not request:
728 728 request = get_current_request()
729 729 session = request.session
730 730
731 731 # Pop the 'old' pylons flash messages. They are tuples of the form
732 732 # (category, message)
733 733 for cat, msg in session.pop(self.session_key, []):
734 734 messages.append(_Message(cat, msg))
735 735
736 736 # Pop the 'new' pyramid flash messages for each category as list
737 737 # of strings.
738 738 for cat in self.categories:
739 739 for msg in session.pop_flash(queue=cat):
740 740 sub_data = {}
741 741 if hasattr(msg, 'rsplit'):
742 742 flash_data = msg.rsplit('|DELIM|', 1)
743 743 org_message = flash_data[0]
744 744 if len(flash_data) > 1:
745 745 sub_data = json.loads(flash_data[1])
746 746 else:
747 747 org_message = msg
748 748
749 749 messages.append(_Message(cat, org_message, sub_data=sub_data))
750 750
751 751 # Map messages from the default queue to the 'notice' category.
752 752 for msg in session.pop_flash():
753 753 messages.append(_Message('notice', msg))
754 754
755 755 session.save()
756 756 return messages
757 757
758 758 def json_alerts(self, session=None, request=None):
759 759 payloads = []
760 760 messages = flash.pop_messages(session=session, request=request) or []
761 761 for message in messages:
762 762 payloads.append({
763 763 'message': {
764 764 'message': u'{}'.format(message.message),
765 765 'level': message.category,
766 766 'force': True,
767 767 'subdata': message.sub_data
768 768 }
769 769 })
770 770 return json.dumps(payloads)
771 771
772 772 def __call__(self, message, category=None, ignore_duplicate=True,
773 773 session=None, request=None):
774 774
775 775 if not session:
776 776 if not request:
777 777 request = get_current_request()
778 778 session = request.session
779 779
780 780 session.flash(
781 781 message, queue=category, allow_duplicate=not ignore_duplicate)
782 782
783 783
784 784 flash = Flash()
785 785
786 786 #==============================================================================
787 787 # SCM FILTERS available via h.
788 788 #==============================================================================
789 789 from rhodecode.lib.vcs.utils import author_name, author_email
790 790 from rhodecode.lib.utils2 import age, age_from_seconds
791 791 from rhodecode.model.db import User, ChangesetStatus
792 792
793 793
794 794 email = author_email
795 795
796 796
797 797 def capitalize(raw_text):
798 798 return raw_text.capitalize()
799 799
800 800
801 801 def short_id(long_id):
802 802 return long_id[:12]
803 803
804 804
805 805 def hide_credentials(url):
806 806 from rhodecode.lib.utils2 import credentials_filter
807 807 return credentials_filter(url)
808 808
809 809
810 810 import pytz
811 811 import tzlocal
812 812 local_timezone = tzlocal.get_localzone()
813 813
814 814
815 815 def get_timezone(datetime_iso, time_is_local=False):
816 816 tzinfo = '+00:00'
817 817
818 818 # detect if we have a timezone info, otherwise, add it
819 819 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
820 820 force_timezone = os.environ.get('RC_TIMEZONE', '')
821 821 if force_timezone:
822 822 force_timezone = pytz.timezone(force_timezone)
823 823 timezone = force_timezone or local_timezone
824 824 offset = timezone.localize(datetime_iso).strftime('%z')
825 825 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
826 826 return tzinfo
827 827
828 828
829 829 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
830 830 title = value or format_date(datetime_iso)
831 831 tzinfo = get_timezone(datetime_iso, time_is_local=time_is_local)
832 832
833 833 return literal(
834 834 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
835 835 cls='tooltip' if tooltip else '',
836 836 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
837 837 title=title, dt=datetime_iso, tzinfo=tzinfo
838 838 ))
839 839
840 840
841 841 def _shorten_commit_id(commit_id, commit_len=None):
842 842 if commit_len is None:
843 843 request = get_current_request()
844 844 commit_len = request.call_context.visual.show_sha_length
845 845 return commit_id[:commit_len]
846 846
847 847
848 848 def show_id(commit, show_idx=None, commit_len=None):
849 849 """
850 850 Configurable function that shows ID
851 851 by default it's r123:fffeeefffeee
852 852
853 853 :param commit: commit instance
854 854 """
855 855 if show_idx is None:
856 856 request = get_current_request()
857 857 show_idx = request.call_context.visual.show_revision_number
858 858
859 859 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
860 860 if show_idx:
861 861 return 'r%s:%s' % (commit.idx, raw_id)
862 862 else:
863 863 return '%s' % (raw_id, )
864 864
865 865
866 866 def format_date(date):
867 867 """
868 868 use a standardized formatting for dates used in RhodeCode
869 869
870 870 :param date: date/datetime object
871 871 :return: formatted date
872 872 """
873 873
874 874 if date:
875 875 _fmt = "%a, %d %b %Y %H:%M:%S"
876 876 return safe_unicode(date.strftime(_fmt))
877 877
878 878 return u""
879 879
880 880
881 881 class _RepoChecker(object):
882 882
883 883 def __init__(self, backend_alias):
884 884 self._backend_alias = backend_alias
885 885
886 886 def __call__(self, repository):
887 887 if hasattr(repository, 'alias'):
888 888 _type = repository.alias
889 889 elif hasattr(repository, 'repo_type'):
890 890 _type = repository.repo_type
891 891 else:
892 892 _type = repository
893 893 return _type == self._backend_alias
894 894
895 895
896 896 is_git = _RepoChecker('git')
897 897 is_hg = _RepoChecker('hg')
898 898 is_svn = _RepoChecker('svn')
899 899
900 900
901 901 def get_repo_type_by_name(repo_name):
902 902 repo = Repository.get_by_repo_name(repo_name)
903 903 if repo:
904 904 return repo.repo_type
905 905
906 906
907 907 def is_svn_without_proxy(repository):
908 908 if is_svn(repository):
909 909 from rhodecode.model.settings import VcsSettingsModel
910 910 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
911 911 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
912 912 return False
913 913
914 914
915 915 def discover_user(author):
916 916 """
917 917 Tries to discover RhodeCode User based on the author string. Author string
918 918 is typically `FirstName LastName <email@address.com>`
919 919 """
920 920
921 921 # if author is already an instance use it for extraction
922 922 if isinstance(author, User):
923 923 return author
924 924
925 925 # Valid email in the attribute passed, see if they're in the system
926 926 _email = author_email(author)
927 927 if _email != '':
928 928 user = User.get_by_email(_email, case_insensitive=True, cache=True)
929 929 if user is not None:
930 930 return user
931 931
932 932 # Maybe it's a username, we try to extract it and fetch by username ?
933 933 _author = author_name(author)
934 934 user = User.get_by_username(_author, case_insensitive=True, cache=True)
935 935 if user is not None:
936 936 return user
937 937
938 938 return None
939 939
940 940
941 941 def email_or_none(author):
942 942 # extract email from the commit string
943 943 _email = author_email(author)
944 944
945 945 # If we have an email, use it, otherwise
946 946 # see if it contains a username we can get an email from
947 947 if _email != '':
948 948 return _email
949 949 else:
950 950 user = User.get_by_username(
951 951 author_name(author), case_insensitive=True, cache=True)
952 952
953 953 if user is not None:
954 954 return user.email
955 955
956 956 # No valid email, not a valid user in the system, none!
957 957 return None
958 958
959 959
960 960 def link_to_user(author, length=0, **kwargs):
961 961 user = discover_user(author)
962 962 # user can be None, but if we have it already it means we can re-use it
963 963 # in the person() function, so we save 1 intensive-query
964 964 if user:
965 965 author = user
966 966
967 967 display_person = person(author, 'username_or_name_or_email')
968 968 if length:
969 969 display_person = shorter(display_person, length)
970 970
971 971 if user and user.username != user.DEFAULT_USER:
972 972 return link_to(
973 973 escape(display_person),
974 974 route_path('user_profile', username=user.username),
975 975 **kwargs)
976 976 else:
977 977 return escape(display_person)
978 978
979 979
980 980 def link_to_group(users_group_name, **kwargs):
981 981 return link_to(
982 982 escape(users_group_name),
983 983 route_path('user_group_profile', user_group_name=users_group_name),
984 984 **kwargs)
985 985
986 986
987 987 def person(author, show_attr="username_and_name"):
988 988 user = discover_user(author)
989 989 if user:
990 990 return getattr(user, show_attr)
991 991 else:
992 992 _author = author_name(author)
993 993 _email = email(author)
994 994 return _author or _email
995 995
996 996
997 997 def author_string(email):
998 998 if email:
999 999 user = User.get_by_email(email, case_insensitive=True, cache=True)
1000 1000 if user:
1001 1001 if user.first_name or user.last_name:
1002 1002 return '%s %s &lt;%s&gt;' % (
1003 1003 user.first_name, user.last_name, email)
1004 1004 else:
1005 1005 return email
1006 1006 else:
1007 1007 return email
1008 1008 else:
1009 1009 return None
1010 1010
1011 1011
1012 1012 def person_by_id(id_, show_attr="username_and_name"):
1013 1013 # attr to return from fetched user
1014 1014 person_getter = lambda usr: getattr(usr, show_attr)
1015 1015
1016 1016 #maybe it's an ID ?
1017 1017 if str(id_).isdigit() or isinstance(id_, int):
1018 1018 id_ = int(id_)
1019 1019 user = User.get(id_)
1020 1020 if user is not None:
1021 1021 return person_getter(user)
1022 1022 return id_
1023 1023
1024 1024
1025 1025 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1026 1026 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1027 1027 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1028 1028
1029 1029
1030 1030 tags_paterns = OrderedDict((
1031 1031 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
1032 1032 '<div class="metatag" tag="lang">\\2</div>')),
1033 1033
1034 1034 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1035 1035 '<div class="metatag" tag="see">see: \\1 </div>')),
1036 1036
1037 1037 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
1038 1038 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
1039 1039
1040 1040 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1041 1041 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
1042 1042
1043 1043 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
1044 1044 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
1045 1045
1046 1046 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
1047 1047 '<div class="metatag" tag="state \\1">\\1</div>')),
1048 1048
1049 1049 # label in grey
1050 1050 ('label', (re.compile(r'\[([a-z]+)\]'),
1051 1051 '<div class="metatag" tag="label">\\1</div>')),
1052 1052
1053 1053 # generic catch all in grey
1054 1054 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
1055 1055 '<div class="metatag" tag="generic">\\1</div>')),
1056 1056 ))
1057 1057
1058 1058
1059 1059 def extract_metatags(value):
1060 1060 """
1061 1061 Extract supported meta-tags from given text value
1062 1062 """
1063 1063 tags = []
1064 1064 if not value:
1065 1065 return tags, ''
1066 1066
1067 1067 for key, val in tags_paterns.items():
1068 1068 pat, replace_html = val
1069 1069 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1070 1070 value = pat.sub('', value)
1071 1071
1072 1072 return tags, value
1073 1073
1074 1074
1075 1075 def style_metatag(tag_type, value):
1076 1076 """
1077 1077 converts tags from value into html equivalent
1078 1078 """
1079 1079 if not value:
1080 1080 return ''
1081 1081
1082 1082 html_value = value
1083 1083 tag_data = tags_paterns.get(tag_type)
1084 1084 if tag_data:
1085 1085 pat, replace_html = tag_data
1086 1086 # convert to plain `unicode` instead of a markup tag to be used in
1087 1087 # regex expressions. safe_unicode doesn't work here
1088 1088 html_value = pat.sub(replace_html, unicode(value))
1089 1089
1090 1090 return html_value
1091 1091
1092 1092
1093 1093 def bool2icon(value, show_at_false=True):
1094 1094 """
1095 1095 Returns boolean value of a given value, represented as html element with
1096 1096 classes that will represent icons
1097 1097
1098 1098 :param value: given value to convert to html node
1099 1099 """
1100 1100
1101 1101 if value: # does bool conversion
1102 1102 return HTML.tag('i', class_="icon-true", title='True')
1103 1103 else: # not true as bool
1104 1104 if show_at_false:
1105 1105 return HTML.tag('i', class_="icon-false", title='False')
1106 1106 return HTML.tag('i')
1107 1107
1108 1108
1109 1109 def b64(inp):
1110 1110 return base64.b64encode(inp)
1111 1111
1112 1112 #==============================================================================
1113 1113 # PERMS
1114 1114 #==============================================================================
1115 1115 from rhodecode.lib.auth import (
1116 1116 HasPermissionAny, HasPermissionAll,
1117 1117 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1118 1118 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1119 1119 csrf_token_key, AuthUser)
1120 1120
1121 1121
1122 1122 #==============================================================================
1123 1123 # GRAVATAR URL
1124 1124 #==============================================================================
1125 1125 class InitialsGravatar(object):
1126 1126 def __init__(self, email_address, first_name, last_name, size=30,
1127 1127 background=None, text_color='#fff'):
1128 1128 self.size = size
1129 1129 self.first_name = first_name
1130 1130 self.last_name = last_name
1131 1131 self.email_address = email_address
1132 1132 self.background = background or self.str2color(email_address)
1133 1133 self.text_color = text_color
1134 1134
1135 1135 def get_color_bank(self):
1136 1136 """
1137 1137 returns a predefined list of colors that gravatars can use.
1138 1138 Those are randomized distinct colors that guarantee readability and
1139 1139 uniqueness.
1140 1140
1141 1141 generated with: http://phrogz.net/css/distinct-colors.html
1142 1142 """
1143 1143 return [
1144 1144 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1145 1145 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1146 1146 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1147 1147 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1148 1148 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1149 1149 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1150 1150 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1151 1151 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1152 1152 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1153 1153 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1154 1154 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1155 1155 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1156 1156 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1157 1157 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1158 1158 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1159 1159 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1160 1160 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1161 1161 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1162 1162 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1163 1163 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1164 1164 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1165 1165 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1166 1166 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1167 1167 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1168 1168 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1169 1169 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1170 1170 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1171 1171 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1172 1172 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1173 1173 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1174 1174 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1175 1175 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1176 1176 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1177 1177 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1178 1178 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1179 1179 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1180 1180 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1181 1181 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1182 1182 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1183 1183 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1184 1184 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1185 1185 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1186 1186 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1187 1187 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1188 1188 '#4f8c46', '#368dd9', '#5c0073'
1189 1189 ]
1190 1190
1191 1191 def rgb_to_hex_color(self, rgb_tuple):
1192 1192 """
1193 1193 Converts an rgb_tuple passed to an hex color.
1194 1194
1195 1195 :param rgb_tuple: tuple with 3 ints represents rgb color space
1196 1196 """
1197 1197 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1198 1198
1199 1199 def email_to_int_list(self, email_str):
1200 1200 """
1201 1201 Get every byte of the hex digest value of email and turn it to integer.
1202 1202 It's going to be always between 0-255
1203 1203 """
1204 1204 digest = md5_safe(email_str.lower())
1205 1205 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1206 1206
1207 1207 def pick_color_bank_index(self, email_str, color_bank):
1208 1208 return self.email_to_int_list(email_str)[0] % len(color_bank)
1209 1209
1210 1210 def str2color(self, email_str):
1211 1211 """
1212 1212 Tries to map in a stable algorithm an email to color
1213 1213
1214 1214 :param email_str:
1215 1215 """
1216 1216 color_bank = self.get_color_bank()
1217 1217 # pick position (module it's length so we always find it in the
1218 1218 # bank even if it's smaller than 256 values
1219 1219 pos = self.pick_color_bank_index(email_str, color_bank)
1220 1220 return color_bank[pos]
1221 1221
1222 1222 def normalize_email(self, email_address):
1223 1223 import unicodedata
1224 1224 # default host used to fill in the fake/missing email
1225 1225 default_host = u'localhost'
1226 1226
1227 1227 if not email_address:
1228 1228 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1229 1229
1230 1230 email_address = safe_unicode(email_address)
1231 1231
1232 1232 if u'@' not in email_address:
1233 1233 email_address = u'%s@%s' % (email_address, default_host)
1234 1234
1235 1235 if email_address.endswith(u'@'):
1236 1236 email_address = u'%s%s' % (email_address, default_host)
1237 1237
1238 1238 email_address = unicodedata.normalize('NFKD', email_address)\
1239 1239 .encode('ascii', 'ignore')
1240 1240 return email_address
1241 1241
1242 1242 def get_initials(self):
1243 1243 """
1244 1244 Returns 2 letter initials calculated based on the input.
1245 1245 The algorithm picks first given email address, and takes first letter
1246 1246 of part before @, and then the first letter of server name. In case
1247 1247 the part before @ is in a format of `somestring.somestring2` it replaces
1248 1248 the server letter with first letter of somestring2
1249 1249
1250 1250 In case function was initialized with both first and lastname, this
1251 1251 overrides the extraction from email by first letter of the first and
1252 1252 last name. We add special logic to that functionality, In case Full name
1253 1253 is compound, like Guido Von Rossum, we use last part of the last name
1254 1254 (Von Rossum) picking `R`.
1255 1255
1256 1256 Function also normalizes the non-ascii characters to they ascii
1257 1257 representation, eg Δ„ => A
1258 1258 """
1259 1259 import unicodedata
1260 1260 # replace non-ascii to ascii
1261 1261 first_name = unicodedata.normalize(
1262 1262 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1263 1263 last_name = unicodedata.normalize(
1264 1264 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1265 1265
1266 1266 # do NFKD encoding, and also make sure email has proper format
1267 1267 email_address = self.normalize_email(self.email_address)
1268 1268
1269 1269 # first push the email initials
1270 1270 prefix, server = email_address.split('@', 1)
1271 1271
1272 1272 # check if prefix is maybe a 'first_name.last_name' syntax
1273 1273 _dot_split = prefix.rsplit('.', 1)
1274 1274 if len(_dot_split) == 2 and _dot_split[1]:
1275 1275 initials = [_dot_split[0][0], _dot_split[1][0]]
1276 1276 else:
1277 1277 initials = [prefix[0], server[0]]
1278 1278
1279 1279 # then try to replace either first_name or last_name
1280 1280 fn_letter = (first_name or " ")[0].strip()
1281 1281 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1282 1282
1283 1283 if fn_letter:
1284 1284 initials[0] = fn_letter
1285 1285
1286 1286 if ln_letter:
1287 1287 initials[1] = ln_letter
1288 1288
1289 1289 return ''.join(initials).upper()
1290 1290
1291 1291 def get_img_data_by_type(self, font_family, img_type):
1292 1292 default_user = """
1293 1293 <svg xmlns="http://www.w3.org/2000/svg"
1294 1294 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1295 1295 viewBox="-15 -10 439.165 429.164"
1296 1296
1297 1297 xml:space="preserve"
1298 1298 style="background:{background};" >
1299 1299
1300 1300 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1301 1301 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1302 1302 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1303 1303 168.596,153.916,216.671,
1304 1304 204.583,216.671z" fill="{text_color}"/>
1305 1305 <path d="M407.164,374.717L360.88,
1306 1306 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1307 1307 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1308 1308 15.366-44.203,23.488-69.076,23.488c-24.877,
1309 1309 0-48.762-8.122-69.078-23.488
1310 1310 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1311 1311 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1312 1312 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1313 1313 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1314 1314 19.402-10.527 C409.699,390.129,
1315 1315 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1316 1316 </svg>""".format(
1317 1317 size=self.size,
1318 1318 background='#979797', # @grey4
1319 1319 text_color=self.text_color,
1320 1320 font_family=font_family)
1321 1321
1322 1322 return {
1323 1323 "default_user": default_user
1324 1324 }[img_type]
1325 1325
1326 1326 def get_img_data(self, svg_type=None):
1327 1327 """
1328 1328 generates the svg metadata for image
1329 1329 """
1330 1330 fonts = [
1331 1331 '-apple-system',
1332 1332 'BlinkMacSystemFont',
1333 1333 'Segoe UI',
1334 1334 'Roboto',
1335 1335 'Oxygen-Sans',
1336 1336 'Ubuntu',
1337 1337 'Cantarell',
1338 1338 'Helvetica Neue',
1339 1339 'sans-serif'
1340 1340 ]
1341 1341 font_family = ','.join(fonts)
1342 1342 if svg_type:
1343 1343 return self.get_img_data_by_type(font_family, svg_type)
1344 1344
1345 1345 initials = self.get_initials()
1346 1346 img_data = """
1347 1347 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1348 1348 width="{size}" height="{size}"
1349 1349 style="width: 100%; height: 100%; background-color: {background}"
1350 1350 viewBox="0 0 {size} {size}">
1351 1351 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1352 1352 pointer-events="auto" fill="{text_color}"
1353 1353 font-family="{font_family}"
1354 1354 style="font-weight: 400; font-size: {f_size}px;">{text}
1355 1355 </text>
1356 1356 </svg>""".format(
1357 1357 size=self.size,
1358 1358 f_size=self.size/2.05, # scale the text inside the box nicely
1359 1359 background=self.background,
1360 1360 text_color=self.text_color,
1361 1361 text=initials.upper(),
1362 1362 font_family=font_family)
1363 1363
1364 1364 return img_data
1365 1365
1366 1366 def generate_svg(self, svg_type=None):
1367 1367 img_data = self.get_img_data(svg_type)
1368 1368 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1369 1369
1370 1370
1371 1371 def initials_gravatar(request, email_address, first_name, last_name, size=30, store_on_disk=False):
1372 1372
1373 1373 svg_type = None
1374 1374 if email_address == User.DEFAULT_USER_EMAIL:
1375 1375 svg_type = 'default_user'
1376 1376
1377 1377 klass = InitialsGravatar(email_address, first_name, last_name, size)
1378 1378
1379 1379 if store_on_disk:
1380 1380 from rhodecode.apps.file_store import utils as store_utils
1381 1381 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
1382 1382 FileOverSizeException
1383 1383 from rhodecode.model.db import Session
1384 1384
1385 1385 image_key = md5_safe(email_address.lower()
1386 1386 + first_name.lower() + last_name.lower())
1387 1387
1388 1388 storage = store_utils.get_file_storage(request.registry.settings)
1389 1389 filename = '{}.svg'.format(image_key)
1390 1390 subdir = 'gravatars'
1391 1391 # since final name has a counter, we apply the 0
1392 1392 uid = storage.apply_counter(0, store_utils.uid_filename(filename, randomized=False))
1393 1393 store_uid = os.path.join(subdir, uid)
1394 1394
1395 1395 db_entry = FileStore.get_by_store_uid(store_uid)
1396 1396 if db_entry:
1397 1397 return request.route_path('download_file', fid=store_uid)
1398 1398
1399 1399 img_data = klass.get_img_data(svg_type=svg_type)
1400 1400 img_file = store_utils.bytes_to_file_obj(img_data)
1401 1401
1402 1402 try:
1403 1403 store_uid, metadata = storage.save_file(
1404 1404 img_file, filename, directory=subdir,
1405 1405 extensions=['.svg'], randomized_name=False)
1406 1406 except (FileNotAllowedException, FileOverSizeException):
1407 1407 raise
1408 1408
1409 1409 try:
1410 1410 entry = FileStore.create(
1411 1411 file_uid=store_uid, filename=metadata["filename"],
1412 1412 file_hash=metadata["sha256"], file_size=metadata["size"],
1413 1413 file_display_name=filename,
1414 1414 file_description=u'user gravatar `{}`'.format(safe_unicode(filename)),
1415 1415 hidden=True, check_acl=False, user_id=1
1416 1416 )
1417 1417 Session().add(entry)
1418 1418 Session().commit()
1419 1419 log.debug('Stored upload in DB as %s', entry)
1420 1420 except Exception:
1421 1421 raise
1422 1422
1423 1423 return request.route_path('download_file', fid=store_uid)
1424 1424
1425 1425 else:
1426 1426 return klass.generate_svg(svg_type=svg_type)
1427 1427
1428 1428
1429 1429 def gravatar_external(request, gravatar_url_tmpl, email_address, size=30):
1430 1430 return safe_str(gravatar_url_tmpl)\
1431 1431 .replace('{email}', email_address) \
1432 1432 .replace('{md5email}', md5_safe(email_address.lower())) \
1433 1433 .replace('{netloc}', request.host) \
1434 1434 .replace('{scheme}', request.scheme) \
1435 1435 .replace('{size}', safe_str(size))
1436 1436
1437 1437
1438 1438 def gravatar_url(email_address, size=30, request=None):
1439 1439 request = request or get_current_request()
1440 1440 _use_gravatar = request.call_context.visual.use_gravatar
1441 1441
1442 1442 email_address = email_address or User.DEFAULT_USER_EMAIL
1443 1443 if isinstance(email_address, unicode):
1444 1444 # hashlib crashes on unicode items
1445 1445 email_address = safe_str(email_address)
1446 1446
1447 1447 # empty email or default user
1448 1448 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1449 1449 return initials_gravatar(request, User.DEFAULT_USER_EMAIL, '', '', size=size)
1450 1450
1451 1451 if _use_gravatar:
1452 1452 gravatar_url_tmpl = request.call_context.visual.gravatar_url \
1453 1453 or User.DEFAULT_GRAVATAR_URL
1454 1454 return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
1455 1455
1456 1456 else:
1457 1457 return initials_gravatar(request, email_address, '', '', size=size)
1458 1458
1459 1459
1460 1460 def breadcrumb_repo_link(repo):
1461 1461 """
1462 1462 Makes a breadcrumbs path link to repo
1463 1463
1464 1464 ex::
1465 1465 group >> subgroup >> repo
1466 1466
1467 1467 :param repo: a Repository instance
1468 1468 """
1469 1469
1470 1470 path = [
1471 1471 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1472 1472 title='last change:{}'.format(format_date(group.last_commit_change)))
1473 1473 for group in repo.groups_with_parents
1474 1474 ] + [
1475 1475 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1476 1476 title='last change:{}'.format(format_date(repo.last_commit_change)))
1477 1477 ]
1478 1478
1479 1479 return literal(' &raquo; '.join(path))
1480 1480
1481 1481
1482 1482 def breadcrumb_repo_group_link(repo_group):
1483 1483 """
1484 1484 Makes a breadcrumbs path link to repo
1485 1485
1486 1486 ex::
1487 1487 group >> subgroup
1488 1488
1489 1489 :param repo_group: a Repository Group instance
1490 1490 """
1491 1491
1492 1492 path = [
1493 1493 link_to(group.name,
1494 1494 route_path('repo_group_home', repo_group_name=group.group_name),
1495 1495 title='last change:{}'.format(format_date(group.last_commit_change)))
1496 1496 for group in repo_group.parents
1497 1497 ] + [
1498 1498 link_to(repo_group.name,
1499 1499 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1500 1500 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1501 1501 ]
1502 1502
1503 1503 return literal(' &raquo; '.join(path))
1504 1504
1505 1505
1506 1506 def format_byte_size_binary(file_size):
1507 1507 """
1508 1508 Formats file/folder sizes to standard.
1509 1509 """
1510 1510 if file_size is None:
1511 1511 file_size = 0
1512 1512
1513 1513 formatted_size = format_byte_size(file_size, binary=True)
1514 1514 return formatted_size
1515 1515
1516 1516
1517 1517 def urlify_text(text_, safe=True, **href_attrs):
1518 1518 """
1519 1519 Extract urls from text and make html links out of them
1520 1520 """
1521 1521
1522 1522 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1523 1523 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1524 1524
1525 1525 def url_func(match_obj):
1526 1526 url_full = match_obj.groups()[0]
1527 1527 a_options = dict(href_attrs)
1528 1528 a_options['href'] = url_full
1529 1529 a_text = url_full
1530 1530 return HTML.tag("a", a_text, **a_options)
1531 1531
1532 1532 _new_text = url_pat.sub(url_func, text_)
1533 1533
1534 1534 if safe:
1535 1535 return literal(_new_text)
1536 1536 return _new_text
1537 1537
1538 1538
1539 1539 def urlify_commits(text_, repo_name):
1540 1540 """
1541 1541 Extract commit ids from text and make link from them
1542 1542
1543 1543 :param text_:
1544 1544 :param repo_name: repo name to build the URL with
1545 1545 """
1546 1546
1547 1547 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1548 1548
1549 1549 def url_func(match_obj):
1550 1550 commit_id = match_obj.groups()[1]
1551 1551 pref = match_obj.groups()[0]
1552 1552 suf = match_obj.groups()[2]
1553 1553
1554 1554 tmpl = (
1555 1555 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1556 1556 '%(commit_id)s</a>%(suf)s'
1557 1557 )
1558 1558 return tmpl % {
1559 1559 'pref': pref,
1560 1560 'cls': 'revision-link',
1561 1561 'url': route_url(
1562 1562 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1563 1563 'commit_id': commit_id,
1564 1564 'suf': suf,
1565 1565 'hovercard_alt': 'Commit: {}'.format(commit_id),
1566 1566 'hovercard_url': route_url(
1567 1567 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1568 1568 }
1569 1569
1570 1570 new_text = url_pat.sub(url_func, text_)
1571 1571
1572 1572 return new_text
1573 1573
1574 1574
1575 1575 def _process_url_func(match_obj, repo_name, uid, entry,
1576 1576 return_raw_data=False, link_format='html'):
1577 1577 pref = ''
1578 1578 if match_obj.group().startswith(' '):
1579 1579 pref = ' '
1580 1580
1581 1581 issue_id = ''.join(match_obj.groups())
1582 1582
1583 1583 if link_format == 'html':
1584 1584 tmpl = (
1585 1585 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1586 1586 '%(issue-prefix)s%(id-repr)s'
1587 1587 '</a>')
1588 1588 elif link_format == 'html+hovercard':
1589 1589 tmpl = (
1590 1590 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1591 1591 '%(issue-prefix)s%(id-repr)s'
1592 1592 '</a>')
1593 1593 elif link_format in ['rst', 'rst+hovercard']:
1594 1594 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1595 1595 elif link_format in ['markdown', 'markdown+hovercard']:
1596 1596 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1597 1597 else:
1598 1598 raise ValueError('Bad link_format:{}'.format(link_format))
1599 1599
1600 1600 (repo_name_cleaned,
1601 1601 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1602 1602
1603 1603 # variables replacement
1604 1604 named_vars = {
1605 1605 'id': issue_id,
1606 1606 'repo': repo_name,
1607 1607 'repo_name': repo_name_cleaned,
1608 1608 'group_name': parent_group_name,
1609 1609 # set dummy keys so we always have them
1610 1610 'hostname': '',
1611 1611 'netloc': '',
1612 1612 'scheme': ''
1613 1613 }
1614 1614
1615 1615 request = get_current_request()
1616 1616 if request:
1617 1617 # exposes, hostname, netloc, scheme
1618 1618 host_data = get_host_info(request)
1619 1619 named_vars.update(host_data)
1620 1620
1621 1621 # named regex variables
1622 1622 named_vars.update(match_obj.groupdict())
1623 1623 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1624 1624 desc = string.Template(escape(entry['desc'])).safe_substitute(**named_vars)
1625 1625 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1626 1626
1627 1627 def quote_cleaner(input_str):
1628 1628 """Remove quotes as it's HTML"""
1629 1629 return input_str.replace('"', '')
1630 1630
1631 1631 data = {
1632 1632 'pref': pref,
1633 1633 'cls': quote_cleaner('issue-tracker-link'),
1634 1634 'url': quote_cleaner(_url),
1635 1635 'id-repr': issue_id,
1636 1636 'issue-prefix': entry['pref'],
1637 1637 'serv': entry['url'],
1638 1638 'title': bleach.clean(desc, strip=True),
1639 1639 'hovercard_url': hovercard_url
1640 1640 }
1641 1641
1642 1642 if return_raw_data:
1643 1643 return {
1644 1644 'id': issue_id,
1645 1645 'url': _url
1646 1646 }
1647 1647 return tmpl % data
1648 1648
1649 1649
1650 1650 def get_active_pattern_entries(repo_name):
1651 1651 repo = None
1652 1652 if repo_name:
1653 1653 # Retrieving repo_name to avoid invalid repo_name to explode on
1654 1654 # IssueTrackerSettingsModel but still passing invalid name further down
1655 1655 repo = Repository.get_by_repo_name(repo_name, cache=True)
1656 1656
1657 1657 settings_model = IssueTrackerSettingsModel(repo=repo)
1658 1658 active_entries = settings_model.get_settings(cache=True)
1659 1659 return active_entries
1660 1660
1661 1661
1662 1662 pr_pattern_re = regex.compile(r'(?:(?:^!)|(?: !))(\d+)')
1663 1663
1664 1664 allowed_link_formats = [
1665 1665 'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1666 1666
1667 1667
1668 1668 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1669 1669
1670 1670 if link_format not in allowed_link_formats:
1671 1671 raise ValueError('Link format can be only one of:{} got {}'.format(
1672 1672 allowed_link_formats, link_format))
1673 1673
1674 1674 if active_entries is None:
1675 1675 log.debug('Fetch active issue tracker patterns for repo: %s', repo_name)
1676 1676 active_entries = get_active_pattern_entries(repo_name)
1677 1677
1678 1678 issues_data = []
1679 1679 errors = []
1680 1680 new_text = text_string
1681 1681
1682 log.debug('Got %s entries to process', len(active_entries))
1682 log.debug('Got %s pattern entries to process', len(active_entries))
1683 1683 for uid, entry in active_entries.items():
1684 log.debug('found issue tracker entry with uid %s', uid)
1685 1684
1686 1685 if not (entry['pat'] and entry['url']):
1687 1686 log.debug('skipping due to missing data')
1688 1687 continue
1689 1688
1690 1689 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1691 1690 uid, entry['pat'], entry['url'], entry['pref'])
1692 1691
1693 1692 if entry.get('pat_compiled'):
1694 1693 pattern = entry['pat_compiled']
1695 1694 else:
1696 1695 try:
1697 1696 pattern = regex.compile(r'%s' % entry['pat'])
1698 1697 except regex.error as e:
1699 1698 regex_err = ValueError('{}:{}'.format(entry['pat'], e))
1700 1699 log.exception('issue tracker pattern: `%s` failed to compile', regex_err)
1701 1700 errors.append(regex_err)
1702 1701 continue
1703 1702
1704 1703 data_func = partial(
1705 1704 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1706 1705 return_raw_data=True)
1707 1706
1708 1707 for match_obj in pattern.finditer(text_string):
1709 1708 issues_data.append(data_func(match_obj))
1710 1709
1711 1710 url_func = partial(
1712 1711 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1713 1712 link_format=link_format)
1714 1713
1715 1714 new_text = pattern.sub(url_func, new_text)
1716 1715 log.debug('processed prefix:uid `%s`', uid)
1717 1716
1718 1717 # finally use global replace, eg !123 -> pr-link, those will not catch
1719 1718 # if already similar pattern exists
1720 1719 server_url = '${scheme}://${netloc}'
1721 1720 pr_entry = {
1722 1721 'pref': '!',
1723 1722 'url': server_url + '/_admin/pull-requests/${id}',
1724 1723 'desc': 'Pull Request !${id}',
1725 1724 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1726 1725 }
1727 1726 pr_url_func = partial(
1728 1727 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1729 1728 link_format=link_format+'+hovercard')
1730 1729 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1731 1730 log.debug('processed !pr pattern')
1732 1731
1733 1732 return new_text, issues_data, errors
1734 1733
1735 1734
1736 1735 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
1737 1736 issues_container_callback=None, error_container=None):
1738 1737 """
1739 1738 Parses given text message and makes proper links.
1740 1739 issues are linked to given issue-server, and rest is a commit link
1741 1740 """
1742 1741
1743 1742 def escaper(_text):
1744 1743 return _text.replace('<', '&lt;').replace('>', '&gt;')
1745 1744
1746 1745 new_text = escaper(commit_text)
1747 1746
1748 1747 # extract http/https links and make them real urls
1749 1748 new_text = urlify_text(new_text, safe=False)
1750 1749
1751 1750 # urlify commits - extract commit ids and make link out of them, if we have
1752 1751 # the scope of repository present.
1753 1752 if repository:
1754 1753 new_text = urlify_commits(new_text, repository)
1755 1754
1756 1755 # process issue tracker patterns
1757 1756 new_text, issues, errors = process_patterns(
1758 1757 new_text, repository or '', active_entries=active_pattern_entries)
1759 1758
1760 1759 if issues_container_callback is not None:
1761 1760 for issue in issues:
1762 1761 issues_container_callback(issue)
1763 1762
1764 1763 if error_container is not None:
1765 1764 error_container.extend(errors)
1766 1765
1767 1766 return literal(new_text)
1768 1767
1769 1768
1770 1769 def render_binary(repo_name, file_obj):
1771 1770 """
1772 1771 Choose how to render a binary file
1773 1772 """
1774 1773
1775 1774 # unicode
1776 1775 filename = file_obj.name
1777 1776
1778 1777 # images
1779 1778 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1780 1779 if fnmatch.fnmatch(filename, pat=ext):
1781 1780 src = route_path(
1782 1781 'repo_file_raw', repo_name=repo_name,
1783 1782 commit_id=file_obj.commit.raw_id,
1784 1783 f_path=file_obj.path)
1785 1784
1786 1785 return literal(
1787 1786 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1788 1787
1789 1788
1790 1789 def renderer_from_filename(filename, exclude=None):
1791 1790 """
1792 1791 choose a renderer based on filename, this works only for text based files
1793 1792 """
1794 1793
1795 1794 # ipython
1796 1795 for ext in ['*.ipynb']:
1797 1796 if fnmatch.fnmatch(filename, pat=ext):
1798 1797 return 'jupyter'
1799 1798
1800 1799 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1801 1800 if is_markup:
1802 1801 return is_markup
1803 1802 return None
1804 1803
1805 1804
1806 1805 def render(source, renderer='rst', mentions=False, relative_urls=None,
1807 1806 repo_name=None, active_pattern_entries=None, issues_container_callback=None):
1808 1807
1809 1808 def maybe_convert_relative_links(html_source):
1810 1809 if relative_urls:
1811 1810 return relative_links(html_source, relative_urls)
1812 1811 return html_source
1813 1812
1814 1813 if renderer == 'plain':
1815 1814 return literal(
1816 1815 MarkupRenderer.plain(source, leading_newline=False))
1817 1816
1818 1817 elif renderer == 'rst':
1819 1818 if repo_name:
1820 1819 # process patterns on comments if we pass in repo name
1821 1820 source, issues, errors = process_patterns(
1822 1821 source, repo_name, link_format='rst',
1823 1822 active_entries=active_pattern_entries)
1824 1823 if issues_container_callback is not None:
1825 1824 for issue in issues:
1826 1825 issues_container_callback(issue)
1827 1826
1828 1827 return literal(
1829 1828 '<div class="rst-block">%s</div>' %
1830 1829 maybe_convert_relative_links(
1831 1830 MarkupRenderer.rst(source, mentions=mentions)))
1832 1831
1833 1832 elif renderer == 'markdown':
1834 1833 if repo_name:
1835 1834 # process patterns on comments if we pass in repo name
1836 1835 source, issues, errors = process_patterns(
1837 1836 source, repo_name, link_format='markdown',
1838 1837 active_entries=active_pattern_entries)
1839 1838 if issues_container_callback is not None:
1840 1839 for issue in issues:
1841 1840 issues_container_callback(issue)
1842 1841
1843 1842
1844 1843 return literal(
1845 1844 '<div class="markdown-block">%s</div>' %
1846 1845 maybe_convert_relative_links(
1847 1846 MarkupRenderer.markdown(source, flavored=True,
1848 1847 mentions=mentions)))
1849 1848
1850 1849 elif renderer == 'jupyter':
1851 1850 return literal(
1852 1851 '<div class="ipynb">%s</div>' %
1853 1852 maybe_convert_relative_links(
1854 1853 MarkupRenderer.jupyter(source)))
1855 1854
1856 1855 # None means just show the file-source
1857 1856 return None
1858 1857
1859 1858
1860 1859 def commit_status(repo, commit_id):
1861 1860 return ChangesetStatusModel().get_status(repo, commit_id)
1862 1861
1863 1862
1864 1863 def commit_status_lbl(commit_status):
1865 1864 return dict(ChangesetStatus.STATUSES).get(commit_status)
1866 1865
1867 1866
1868 1867 def commit_time(repo_name, commit_id):
1869 1868 repo = Repository.get_by_repo_name(repo_name)
1870 1869 commit = repo.get_commit(commit_id=commit_id)
1871 1870 return commit.date
1872 1871
1873 1872
1874 1873 def get_permission_name(key):
1875 1874 return dict(Permission.PERMS).get(key)
1876 1875
1877 1876
1878 1877 def journal_filter_help(request):
1879 1878 _ = request.translate
1880 1879 from rhodecode.lib.audit_logger import ACTIONS
1881 1880 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1882 1881
1883 1882 return _(
1884 1883 'Example filter terms:\n' +
1885 1884 ' repository:vcs\n' +
1886 1885 ' username:marcin\n' +
1887 1886 ' username:(NOT marcin)\n' +
1888 1887 ' action:*push*\n' +
1889 1888 ' ip:127.0.0.1\n' +
1890 1889 ' date:20120101\n' +
1891 1890 ' date:[20120101100000 TO 20120102]\n' +
1892 1891 '\n' +
1893 1892 'Actions: {actions}\n' +
1894 1893 '\n' +
1895 1894 'Generate wildcards using \'*\' character:\n' +
1896 1895 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1897 1896 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1898 1897 '\n' +
1899 1898 'Optional AND / OR operators in queries\n' +
1900 1899 ' "repository:vcs OR repository:test"\n' +
1901 1900 ' "username:test AND repository:test*"\n'
1902 1901 ).format(actions=actions)
1903 1902
1904 1903
1905 1904 def not_mapped_error(repo_name):
1906 1905 from rhodecode.translation import _
1907 1906 flash(_('%s repository is not mapped to db perhaps'
1908 1907 ' it was created or renamed from the filesystem'
1909 1908 ' please run the application again'
1910 1909 ' in order to rescan repositories') % repo_name, category='error')
1911 1910
1912 1911
1913 1912 def ip_range(ip_addr):
1914 1913 from rhodecode.model.db import UserIpMap
1915 1914 s, e = UserIpMap._get_ip_range(ip_addr)
1916 1915 return '%s - %s' % (s, e)
1917 1916
1918 1917
1919 1918 def form(url, method='post', needs_csrf_token=True, **attrs):
1920 1919 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1921 1920 if method.lower() != 'get' and needs_csrf_token:
1922 1921 raise Exception(
1923 1922 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1924 1923 'CSRF token. If the endpoint does not require such token you can ' +
1925 1924 'explicitly set the parameter needs_csrf_token to false.')
1926 1925
1927 1926 return insecure_form(url, method=method, **attrs)
1928 1927
1929 1928
1930 1929 def secure_form(form_url, method="POST", multipart=False, **attrs):
1931 1930 """Start a form tag that points the action to an url. This
1932 1931 form tag will also include the hidden field containing
1933 1932 the auth token.
1934 1933
1935 1934 The url options should be given either as a string, or as a
1936 1935 ``url()`` function. The method for the form defaults to POST.
1937 1936
1938 1937 Options:
1939 1938
1940 1939 ``multipart``
1941 1940 If set to True, the enctype is set to "multipart/form-data".
1942 1941 ``method``
1943 1942 The method to use when submitting the form, usually either
1944 1943 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1945 1944 hidden input with name _method is added to simulate the verb
1946 1945 over POST.
1947 1946
1948 1947 """
1949 1948
1950 1949 if 'request' in attrs:
1951 1950 session = attrs['request'].session
1952 1951 del attrs['request']
1953 1952 else:
1954 1953 raise ValueError(
1955 1954 'Calling this form requires request= to be passed as argument')
1956 1955
1957 1956 _form = insecure_form(form_url, method, multipart, **attrs)
1958 1957 token = literal(
1959 1958 '<input type="hidden" name="{}" value="{}">'.format(
1960 1959 csrf_token_key, get_csrf_token(session)))
1961 1960
1962 1961 return literal("%s\n%s" % (_form, token))
1963 1962
1964 1963
1965 1964 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1966 1965 select_html = select(name, selected, options, **attrs)
1967 1966
1968 1967 select2 = """
1969 1968 <script>
1970 1969 $(document).ready(function() {
1971 1970 $('#%s').select2({
1972 1971 containerCssClass: 'drop-menu %s',
1973 1972 dropdownCssClass: 'drop-menu-dropdown',
1974 1973 dropdownAutoWidth: true%s
1975 1974 });
1976 1975 });
1977 1976 </script>
1978 1977 """
1979 1978
1980 1979 filter_option = """,
1981 1980 minimumResultsForSearch: -1
1982 1981 """
1983 1982 input_id = attrs.get('id') or name
1984 1983 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1985 1984 filter_enabled = "" if enable_filter else filter_option
1986 1985 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1987 1986
1988 1987 return literal(select_html+select_script)
1989 1988
1990 1989
1991 1990 def get_visual_attr(tmpl_context_var, attr_name):
1992 1991 """
1993 1992 A safe way to get a variable from visual variable of template context
1994 1993
1995 1994 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1996 1995 :param attr_name: name of the attribute we fetch from the c.visual
1997 1996 """
1998 1997 visual = getattr(tmpl_context_var, 'visual', None)
1999 1998 if not visual:
2000 1999 return
2001 2000 else:
2002 2001 return getattr(visual, attr_name, None)
2003 2002
2004 2003
2005 2004 def get_last_path_part(file_node):
2006 2005 if not file_node.path:
2007 2006 return u'/'
2008 2007
2009 2008 path = safe_unicode(file_node.path.split('/')[-1])
2010 2009 return u'../' + path
2011 2010
2012 2011
2013 2012 def route_url(*args, **kwargs):
2014 2013 """
2015 2014 Wrapper around pyramids `route_url` (fully qualified url) function.
2016 2015 """
2017 2016 req = get_current_request()
2018 2017 return req.route_url(*args, **kwargs)
2019 2018
2020 2019
2021 2020 def route_path(*args, **kwargs):
2022 2021 """
2023 2022 Wrapper around pyramids `route_path` function.
2024 2023 """
2025 2024 req = get_current_request()
2026 2025 return req.route_path(*args, **kwargs)
2027 2026
2028 2027
2029 2028 def route_path_or_none(*args, **kwargs):
2030 2029 try:
2031 2030 return route_path(*args, **kwargs)
2032 2031 except KeyError:
2033 2032 return None
2034 2033
2035 2034
2036 2035 def current_route_path(request, **kw):
2037 2036 new_args = request.GET.mixed()
2038 2037 new_args.update(kw)
2039 2038 return request.current_route_path(_query=new_args)
2040 2039
2041 2040
2042 2041 def curl_api_example(method, args):
2043 2042 args_json = json.dumps(OrderedDict([
2044 2043 ('id', 1),
2045 2044 ('auth_token', 'SECRET'),
2046 2045 ('method', method),
2047 2046 ('args', args)
2048 2047 ]))
2049 2048
2050 2049 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2051 2050 api_url=route_url('apiv2'),
2052 2051 args_json=args_json
2053 2052 )
2054 2053
2055 2054
2056 2055 def api_call_example(method, args):
2057 2056 """
2058 2057 Generates an API call example via CURL
2059 2058 """
2060 2059 curl_call = curl_api_example(method, args)
2061 2060
2062 2061 return literal(
2063 2062 curl_call +
2064 2063 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2065 2064 "and needs to be of `api calls` role."
2066 2065 .format(token_url=route_url('my_account_auth_tokens')))
2067 2066
2068 2067
2069 2068 def notification_description(notification, request):
2070 2069 """
2071 2070 Generate notification human readable description based on notification type
2072 2071 """
2073 2072 from rhodecode.model.notification import NotificationModel
2074 2073 return NotificationModel().make_description(
2075 2074 notification, translate=request.translate)
2076 2075
2077 2076
2078 2077 def go_import_header(request, db_repo=None):
2079 2078 """
2080 2079 Creates a header for go-import functionality in Go Lang
2081 2080 """
2082 2081
2083 2082 if not db_repo:
2084 2083 return
2085 2084 if 'go-get' not in request.GET:
2086 2085 return
2087 2086
2088 2087 clone_url = db_repo.clone_url()
2089 2088 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2090 2089 # we have a repo and go-get flag,
2091 2090 return literal('<meta name="go-import" content="{} {} {}">'.format(
2092 2091 prefix, db_repo.repo_type, clone_url))
2093 2092
2094 2093
2095 2094 def reviewer_as_json(*args, **kwargs):
2096 2095 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2097 2096 return _reviewer_as_json(*args, **kwargs)
2098 2097
2099 2098
2100 2099 def get_repo_view_type(request):
2101 2100 route_name = request.matched_route.name
2102 2101 route_to_view_type = {
2103 2102 'repo_changelog': 'commits',
2104 2103 'repo_commits': 'commits',
2105 2104 'repo_files': 'files',
2106 2105 'repo_summary': 'summary',
2107 2106 'repo_commit': 'commit'
2108 2107 }
2109 2108
2110 2109 return route_to_view_type.get(route_name)
2111 2110
2112 2111
2113 2112 def is_active(menu_entry, selected):
2114 2113 """
2115 2114 Returns active class for selecting menus in templates
2116 2115 <li class=${h.is_active('settings', current_active)}></li>
2117 2116 """
2118 2117 if not isinstance(menu_entry, list):
2119 2118 menu_entry = [menu_entry]
2120 2119
2121 2120 if selected in menu_entry:
2122 2121 return "active"
2123 2122
2124 2123
2125 2124 class IssuesRegistry(object):
2126 2125 """
2127 2126 issue_registry = IssuesRegistry()
2128 2127 some_func(issues_callback=issues_registry(...))
2129 2128 """
2130 2129
2131 2130 def __init__(self):
2132 2131 self.issues = []
2133 2132 self.unique_issues = collections.defaultdict(lambda: [])
2134 2133
2135 2134 def __call__(self, commit_dict=None):
2136 2135 def callback(issue):
2137 2136 if commit_dict and issue:
2138 2137 issue['commit'] = commit_dict
2139 2138 self.issues.append(issue)
2140 2139 self.unique_issues[issue['id']].append(issue)
2141 2140 return callback
2142 2141
2143 2142 def get_issues(self):
2144 2143 return self.issues
2145 2144
2146 2145 @property
2147 2146 def issues_unique_count(self):
2148 2147 return len(set(i['id'] for i in self.issues))
@@ -1,170 +1,179 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <div class="panel panel-default user-profile">
4 4 <div class="panel-heading">
5 5 <h3 class="panel-title">
6 6 ${base.gravatar_with_user(c.user.username, 16, tooltip=False, _class='pull-left')}
7 7 &nbsp;- ${_('User Profile')}
8 8 </h3>
9 9 </div>
10 10 <div class="panel-body">
11 11 <div class="user-profile-content">
12 12 ${h.secure_form(h.route_path('user_update', user_id=c.user.user_id), class_='form', request=request)}
13 13 <% readonly = None %>
14 14 <% disabled = "" %>
15 %if c.extern_type != 'rhodecode':
15 % if c.edit_mode:
16 ${h.hidden('edit', '1')}
17 % endif
18 %if c.extern_type != 'rhodecode' and not c.edit_mode:
16 19 <% readonly = "readonly" %>
17 20 <% disabled = " disabled" %>
18 21 <div class="alert-warning" style="margin:0px 0px 20px 0px; padding: 10px">
19 22 <strong>${_('This user was created from external source (%s). Editing some of the settings is limited.' % c.extern_type)}</strong>
20 23 </div>
21 24 %endif
22 25 <div class="form">
23 26 <div class="fields">
24 27 <div class="field">
25 28 <div class="label photo">
26 29 ${_('Photo')}:
27 30 </div>
28 31 <div class="input profile">
29 32 %if c.visual.use_gravatar:
30 33 ${base.gravatar(c.user.email, 100)}
31 34 <p class="help-block">${_('Change the avatar at')} <a href="http://gravatar.com">gravatar.com</a>.</p>
32 35 %else:
33 36 ${base.gravatar(c.user.email, 100)}
34 37 %endif
35 38 </div>
36 39 </div>
37 40 <div class="field">
38 41 <div class="label">
39 42 ${_('Username')}:
40 43 </div>
41 44 <div class="input">
42 45 ${h.text('username', class_='%s medium' % disabled, readonly=readonly)}
46 <br/>
47 % if c.extern_type != 'rhodecode' and c.is_super_admin:
48 <p class="help-block">
49 ${_('Super-admin can edit this field by entering ')} <a href="${h.current_route_path(request, edit=1)}">edit mode</a>
50 </p>
51 % endif
43 52 </div>
44 53 </div>
45 54 <div class="field">
46 55 <div class="label">
47 56 <label for="name">${_('First Name')}:</label>
48 57 </div>
49 58 <div class="input">
50 59 ${h.text('firstname', class_="medium")}
51 60 </div>
52 61 </div>
53 62
54 63 <div class="field">
55 64 <div class="label">
56 65 <label for="lastname">${_('Last Name')}:</label>
57 66 </div>
58 67 <div class="input">
59 68 ${h.text('lastname', class_="medium")}
60 69 </div>
61 70 </div>
62 71
63 72 <div class="field">
64 73 <div class="label">
65 74 <label for="email">${_('Email')}:</label>
66 75 </div>
67 76 <div class="input">
68 77 ## we should be able to edit email !
69 78 ${h.text('email', class_="medium")}
70 79 </div>
71 80 </div>
72 81 <div class="field">
73 82 <div class="label">
74 83 <label for="description">${_('Description')}:</label>
75 84 </div>
76 85 <div class="input textarea editor">
77 86 ${h.textarea('description', rows=10, class_="medium")}
78 87 <% metatags_url = h.literal('''<a href="#metatagsShow" onclick="$('#meta-tags-desc').toggle();return false">meta-tags</a>''') %>
79 88 <span class="help-block">
80 89 % if c.visual.stylify_metatags:
81 90 ${_('Plain text format with {metatags} support.').format(metatags=metatags_url)|n}
82 91 % else:
83 92 ${_('Plain text format.')}
84 93 % endif
85 94 </span>
86 95 <span id="meta-tags-desc" style="display: none">
87 96 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
88 97 ${dt.metatags_help()}
89 98 </span>
90 99 </div>
91 100 </div>
92 101 <div class="field">
93 102 <div class="label">
94 103 ${_('New Password')}:
95 104 </div>
96 105 <div class="input">
97 106 ${h.password('new_password',class_='%s medium' % disabled,autocomplete="off",readonly=readonly)}
98 107 </div>
99 108 </div>
100 109 <div class="field">
101 110 <div class="label">
102 111 ${_('New Password Confirmation')}:
103 112 </div>
104 113 <div class="input">
105 114 ${h.password('password_confirmation',class_="%s medium" % disabled,autocomplete="off",readonly=readonly)}
106 115 </div>
107 116 </div>
108 117 <div class="field">
109 118 <div class="label-text">
110 119 ${_('Active')}:
111 120 </div>
112 121 <div class="input user-checkbox">
113 122 ${h.checkbox('active',value=True)}
114 123 </div>
115 124 </div>
116 125 <div class="field">
117 126 <div class="label-text">
118 127 ${_('Super-admin')}:
119 128 </div>
120 129 <div class="input user-checkbox">
121 130 ${h.checkbox('admin',value=True)}
122 131 </div>
123 132 </div>
124 133 <div class="field">
125 134 <div class="label-text">
126 135 ${_('Authentication type')}:
127 136 </div>
128 137 <div class="input">
129 138 ${h.select('extern_type', c.extern_type, c.allowed_extern_types)}
130 139 <p class="help-block">${_('When user was created using an external source. He is bound to authentication using this method.')}</p>
131 140 </div>
132 141 </div>
133 142 <div class="field">
134 143 <div class="label-text">
135 144 ${_('Name in Source of Record')}:
136 145 </div>
137 146 <div class="input">
138 147 <p>${c.extern_name}</p>
139 148 ${h.hidden('extern_name', readonly="readonly")}
140 149 </div>
141 150 </div>
142 151 <div class="field">
143 152 <div class="label">
144 153 ${_('Language')}:
145 154 </div>
146 155 <div class="input">
147 156 ## allowed_languages is defined in the users.py
148 157 ## c.language comes from base.py as a default language
149 158 ${h.select('language', c.language, c.allowed_languages)}
150 159 <p class="help-block">${h.literal(_('User interface language. Help translate %(rc_link)s into your language.') % {'rc_link': h.link_to('RhodeCode Enterprise', h.route_url('rhodecode_translations'))})}</p>
151 160 </div>
152 161 </div>
153 162 <div class="buttons">
154 163 ${h.submit('save', _('Save'), class_="btn")}
155 164 ${h.reset('reset', _('Reset'), class_="btn")}
156 165 </div>
157 166 </div>
158 167 </div>
159 168 ${h.end_form()}
160 169 </div>
161 170 </div>
162 171 </div>
163 172
164 173 <script>
165 174 $('#language').select2({
166 175 'containerCssClass': "drop-menu",
167 176 'dropdownCssClass': "drop-menu-dropdown",
168 177 'dropdownAutoWidth': true
169 178 });
170 179 </script>
General Comments 0
You need to be logged in to leave comments. Login now