##// END OF EJS Templates
auth-tokens: allow specifing custom expiration date manually....
marcink -
r2083:37b1bdd7 default
parent child Browse files
Show More
@@ -1,674 +1,668 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 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 import time
21 22 import logging
22 23 import datetime
23 24 import formencode
24 25 import formencode.htmlfill
25 26
26 27 from pyramid.httpexceptions import HTTPFound
27 28 from pyramid.view import view_config
28 29 from sqlalchemy.sql.functions import coalesce
29 30 from sqlalchemy.exc import IntegrityError
30 31
31 32 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 33 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
33 34 from rhodecode.events import trigger
34 35
35 36 from rhodecode.lib import audit_logger
36 37 from rhodecode.lib.ext_json import json
37 38 from rhodecode.lib.auth import (
38 39 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
39 40 from rhodecode.lib import helpers as h
40 41 from rhodecode.lib.utils2 import safe_int, safe_unicode
41 42 from rhodecode.model.auth_token import AuthTokenModel
42 43 from rhodecode.model.ssh_key import SshKeyModel
43 44 from rhodecode.model.user import UserModel
44 45 from rhodecode.model.user_group import UserGroupModel
45 46 from rhodecode.model.db import (
46 47 or_, User, UserIpMap, UserEmailMap, UserApiKeys, UserSshKeys)
47 48 from rhodecode.model.meta import Session
48 49
49 50 log = logging.getLogger(__name__)
50 51
51 52
52 53 class AdminUsersView(BaseAppView, DataGridAppView):
53 54 ALLOW_SCOPED_TOKENS = False
54 55 """
55 56 This view has alternative version inside EE, if modified please take a look
56 57 in there as well.
57 58 """
58 59
59 60 def load_default_context(self):
60 61 c = self._get_local_tmpl_context()
61 62 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
62 63 self._register_global_c(c)
63 64 return c
64 65
65 66 def _redirect_for_default_user(self, username):
66 67 _ = self.request.translate
67 68 if username == User.DEFAULT_USER:
68 69 h.flash(_("You can't edit this user"), category='warning')
69 70 # TODO(marcink): redirect to 'users' admin panel once this
70 71 # is a pyramid view
71 72 raise HTTPFound('/')
72 73
73 74 @LoginRequired()
74 75 @HasPermissionAllDecorator('hg.admin')
75 76 @view_config(
76 77 route_name='users', request_method='GET',
77 78 renderer='rhodecode:templates/admin/users/users.mako')
78 79 def users_list(self):
79 80 c = self.load_default_context()
80 81 return self._get_template_context(c)
81 82
82 83 @LoginRequired()
83 84 @HasPermissionAllDecorator('hg.admin')
84 85 @view_config(
85 86 # renderer defined below
86 87 route_name='users_data', request_method='GET',
87 88 renderer='json_ext', xhr=True)
88 89 def users_list_data(self):
89 90 column_map = {
90 91 'first_name': 'name',
91 92 'last_name': 'lastname',
92 93 }
93 94 draw, start, limit = self._extract_chunk(self.request)
94 95 search_q, order_by, order_dir = self._extract_ordering(
95 96 self.request, column_map=column_map)
96 97
97 98 _render = self.request.get_partial_renderer(
98 99 'data_table/_dt_elements.mako')
99 100
100 101 def user_actions(user_id, username):
101 102 return _render("user_actions", user_id, username)
102 103
103 104 users_data_total_count = User.query()\
104 105 .filter(User.username != User.DEFAULT_USER) \
105 106 .count()
106 107
107 108 # json generate
108 109 base_q = User.query().filter(User.username != User.DEFAULT_USER)
109 110
110 111 if search_q:
111 112 like_expression = u'%{}%'.format(safe_unicode(search_q))
112 113 base_q = base_q.filter(or_(
113 114 User.username.ilike(like_expression),
114 115 User._email.ilike(like_expression),
115 116 User.name.ilike(like_expression),
116 117 User.lastname.ilike(like_expression),
117 118 ))
118 119
119 120 users_data_total_filtered_count = base_q.count()
120 121
121 122 sort_col = getattr(User, order_by, None)
122 123 if sort_col:
123 124 if order_dir == 'asc':
124 125 # handle null values properly to order by NULL last
125 126 if order_by in ['last_activity']:
126 127 sort_col = coalesce(sort_col, datetime.date.max)
127 128 sort_col = sort_col.asc()
128 129 else:
129 130 # handle null values properly to order by NULL last
130 131 if order_by in ['last_activity']:
131 132 sort_col = coalesce(sort_col, datetime.date.min)
132 133 sort_col = sort_col.desc()
133 134
134 135 base_q = base_q.order_by(sort_col)
135 136 base_q = base_q.offset(start).limit(limit)
136 137
137 138 users_list = base_q.all()
138 139
139 140 users_data = []
140 141 for user in users_list:
141 142 users_data.append({
142 143 "username": h.gravatar_with_user(self.request, user.username),
143 144 "email": user.email,
144 145 "first_name": user.first_name,
145 146 "last_name": user.last_name,
146 147 "last_login": h.format_date(user.last_login),
147 148 "last_activity": h.format_date(user.last_activity),
148 149 "active": h.bool2icon(user.active),
149 150 "active_raw": user.active,
150 151 "admin": h.bool2icon(user.admin),
151 152 "extern_type": user.extern_type,
152 153 "extern_name": user.extern_name,
153 154 "action": user_actions(user.user_id, user.username),
154 155 })
155 156
156 157 data = ({
157 158 'draw': draw,
158 159 'data': users_data,
159 160 'recordsTotal': users_data_total_count,
160 161 'recordsFiltered': users_data_total_filtered_count,
161 162 })
162 163
163 164 return data
164 165
165 166 @LoginRequired()
166 167 @HasPermissionAllDecorator('hg.admin')
167 168 @view_config(
168 169 route_name='edit_user_auth_tokens', request_method='GET',
169 170 renderer='rhodecode:templates/admin/users/user_edit.mako')
170 171 def auth_tokens(self):
171 172 _ = self.request.translate
172 173 c = self.load_default_context()
173 174
174 175 user_id = self.request.matchdict.get('user_id')
175 176 c.user = User.get_or_404(user_id)
176 177 self._redirect_for_default_user(c.user.username)
177 178
178 179 c.active = 'auth_tokens'
179 180
180 c.lifetime_values = [
181 (str(-1), _('forever')),
182 (str(5), _('5 minutes')),
183 (str(60), _('1 hour')),
184 (str(60 * 24), _('1 day')),
185 (str(60 * 24 * 30), _('1 month')),
186 ]
187 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
181 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
188 182 c.role_values = [
189 183 (x, AuthTokenModel.cls._get_role_name(x))
190 184 for x in AuthTokenModel.cls.ROLES]
191 185 c.role_options = [(c.role_values, _("Role"))]
192 186 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
193 187 c.user.user_id, show_expired=True)
194 188 return self._get_template_context(c)
195 189
196 190 def maybe_attach_token_scope(self, token):
197 191 # implemented in EE edition
198 192 pass
199 193
200 194 @LoginRequired()
201 195 @HasPermissionAllDecorator('hg.admin')
202 196 @CSRFRequired()
203 197 @view_config(
204 198 route_name='edit_user_auth_tokens_add', request_method='POST')
205 199 def auth_tokens_add(self):
206 200 _ = self.request.translate
207 201 c = self.load_default_context()
208 202
209 203 user_id = self.request.matchdict.get('user_id')
210 204 c.user = User.get_or_404(user_id)
211 205
212 206 self._redirect_for_default_user(c.user.username)
213 207
214 208 user_data = c.user.get_api_data()
215 209 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
216 210 description = self.request.POST.get('description')
217 211 role = self.request.POST.get('role')
218 212
219 213 token = AuthTokenModel().create(
220 214 c.user.user_id, description, lifetime, role)
221 215 token_data = token.get_api_data()
222 216
223 217 self.maybe_attach_token_scope(token)
224 218 audit_logger.store_web(
225 219 'user.edit.token.add', action_data={
226 220 'data': {'token': token_data, 'user': user_data}},
227 221 user=self._rhodecode_user, )
228 222 Session().commit()
229 223
230 224 h.flash(_("Auth token successfully created"), category='success')
231 225 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
232 226
233 227 @LoginRequired()
234 228 @HasPermissionAllDecorator('hg.admin')
235 229 @CSRFRequired()
236 230 @view_config(
237 231 route_name='edit_user_auth_tokens_delete', request_method='POST')
238 232 def auth_tokens_delete(self):
239 233 _ = self.request.translate
240 234 c = self.load_default_context()
241 235
242 236 user_id = self.request.matchdict.get('user_id')
243 237 c.user = User.get_or_404(user_id)
244 238 self._redirect_for_default_user(c.user.username)
245 239 user_data = c.user.get_api_data()
246 240
247 241 del_auth_token = self.request.POST.get('del_auth_token')
248 242
249 243 if del_auth_token:
250 244 token = UserApiKeys.get_or_404(del_auth_token)
251 245 token_data = token.get_api_data()
252 246
253 247 AuthTokenModel().delete(del_auth_token, c.user.user_id)
254 248 audit_logger.store_web(
255 249 'user.edit.token.delete', action_data={
256 250 'data': {'token': token_data, 'user': user_data}},
257 251 user=self._rhodecode_user,)
258 252 Session().commit()
259 253 h.flash(_("Auth token successfully deleted"), category='success')
260 254
261 255 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
262 256
263 257 @LoginRequired()
264 258 @HasPermissionAllDecorator('hg.admin')
265 259 @view_config(
266 260 route_name='edit_user_ssh_keys', request_method='GET',
267 261 renderer='rhodecode:templates/admin/users/user_edit.mako')
268 262 def ssh_keys(self):
269 263 _ = self.request.translate
270 264 c = self.load_default_context()
271 265
272 266 user_id = self.request.matchdict.get('user_id')
273 267 c.user = User.get_or_404(user_id)
274 268 self._redirect_for_default_user(c.user.username)
275 269
276 270 c.active = 'ssh_keys'
277 271 c.default_key = self.request.GET.get('default_key')
278 272 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
279 273 return self._get_template_context(c)
280 274
281 275 @LoginRequired()
282 276 @HasPermissionAllDecorator('hg.admin')
283 277 @view_config(
284 278 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
285 279 renderer='rhodecode:templates/admin/users/user_edit.mako')
286 280 def ssh_keys_generate_keypair(self):
287 281 _ = self.request.translate
288 282 c = self.load_default_context()
289 283
290 284 user_id = self.request.matchdict.get('user_id')
291 285 c.user = User.get_or_404(user_id)
292 286 self._redirect_for_default_user(c.user.username)
293 287
294 288 c.active = 'ssh_keys_generate'
295 289 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
296 290 c.private, c.public = SshKeyModel().generate_keypair(comment=comment)
297 291
298 292 return self._get_template_context(c)
299 293
300 294 @LoginRequired()
301 295 @HasPermissionAllDecorator('hg.admin')
302 296 @CSRFRequired()
303 297 @view_config(
304 298 route_name='edit_user_ssh_keys_add', request_method='POST')
305 299 def ssh_keys_add(self):
306 300 _ = self.request.translate
307 301 c = self.load_default_context()
308 302
309 303 user_id = self.request.matchdict.get('user_id')
310 304 c.user = User.get_or_404(user_id)
311 305
312 306 self._redirect_for_default_user(c.user.username)
313 307
314 308 user_data = c.user.get_api_data()
315 309 key_data = self.request.POST.get('key_data')
316 310 description = self.request.POST.get('description')
317 311
318 312 try:
319 313 if not key_data:
320 314 raise ValueError('Please add a valid public key')
321 315
322 316 key = SshKeyModel().parse_key(key_data.strip())
323 317 fingerprint = key.hash_md5()
324 318
325 319 ssh_key = SshKeyModel().create(
326 320 c.user.user_id, fingerprint, key_data, description)
327 321 ssh_key_data = ssh_key.get_api_data()
328 322
329 323 audit_logger.store_web(
330 324 'user.edit.ssh_key.add', action_data={
331 325 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
332 326 user=self._rhodecode_user, )
333 327 Session().commit()
334 328
335 329 # Trigger an event on change of keys.
336 330 trigger(SshKeyFileChangeEvent(), self.request.registry)
337 331
338 332 h.flash(_("Ssh Key successfully created"), category='success')
339 333
340 334 except IntegrityError:
341 335 log.exception("Exception during ssh key saving")
342 336 h.flash(_('An error occurred during ssh key saving: {}').format(
343 337 'Such key already exists, please use a different one'),
344 338 category='error')
345 339 except Exception as e:
346 340 log.exception("Exception during ssh key saving")
347 341 h.flash(_('An error occurred during ssh key saving: {}').format(e),
348 342 category='error')
349 343
350 344 return HTTPFound(
351 345 h.route_path('edit_user_ssh_keys', user_id=user_id))
352 346
353 347 @LoginRequired()
354 348 @HasPermissionAllDecorator('hg.admin')
355 349 @CSRFRequired()
356 350 @view_config(
357 351 route_name='edit_user_ssh_keys_delete', request_method='POST')
358 352 def ssh_keys_delete(self):
359 353 _ = self.request.translate
360 354 c = self.load_default_context()
361 355
362 356 user_id = self.request.matchdict.get('user_id')
363 357 c.user = User.get_or_404(user_id)
364 358 self._redirect_for_default_user(c.user.username)
365 359 user_data = c.user.get_api_data()
366 360
367 361 del_ssh_key = self.request.POST.get('del_ssh_key')
368 362
369 363 if del_ssh_key:
370 364 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
371 365 ssh_key_data = ssh_key.get_api_data()
372 366
373 367 SshKeyModel().delete(del_ssh_key, c.user.user_id)
374 368 audit_logger.store_web(
375 369 'user.edit.ssh_key.delete', action_data={
376 370 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
377 371 user=self._rhodecode_user,)
378 372 Session().commit()
379 373 # Trigger an event on change of keys.
380 374 trigger(SshKeyFileChangeEvent(), self.request.registry)
381 375 h.flash(_("Ssh key successfully deleted"), category='success')
382 376
383 377 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
384 378
385 379 @LoginRequired()
386 380 @HasPermissionAllDecorator('hg.admin')
387 381 @view_config(
388 382 route_name='edit_user_emails', request_method='GET',
389 383 renderer='rhodecode:templates/admin/users/user_edit.mako')
390 384 def emails(self):
391 385 _ = self.request.translate
392 386 c = self.load_default_context()
393 387
394 388 user_id = self.request.matchdict.get('user_id')
395 389 c.user = User.get_or_404(user_id)
396 390 self._redirect_for_default_user(c.user.username)
397 391
398 392 c.active = 'emails'
399 393 c.user_email_map = UserEmailMap.query() \
400 394 .filter(UserEmailMap.user == c.user).all()
401 395
402 396 return self._get_template_context(c)
403 397
404 398 @LoginRequired()
405 399 @HasPermissionAllDecorator('hg.admin')
406 400 @CSRFRequired()
407 401 @view_config(
408 402 route_name='edit_user_emails_add', request_method='POST')
409 403 def emails_add(self):
410 404 _ = self.request.translate
411 405 c = self.load_default_context()
412 406
413 407 user_id = self.request.matchdict.get('user_id')
414 408 c.user = User.get_or_404(user_id)
415 409 self._redirect_for_default_user(c.user.username)
416 410
417 411 email = self.request.POST.get('new_email')
418 412 user_data = c.user.get_api_data()
419 413 try:
420 414 UserModel().add_extra_email(c.user.user_id, email)
421 415 audit_logger.store_web(
422 416 'user.edit.email.add', action_data={'email': email, 'user': user_data},
423 417 user=self._rhodecode_user)
424 418 Session().commit()
425 419 h.flash(_("Added new email address `%s` for user account") % email,
426 420 category='success')
427 421 except formencode.Invalid as error:
428 422 h.flash(h.escape(error.error_dict['email']), category='error')
429 423 except Exception:
430 424 log.exception("Exception during email saving")
431 425 h.flash(_('An error occurred during email saving'),
432 426 category='error')
433 427 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
434 428
435 429 @LoginRequired()
436 430 @HasPermissionAllDecorator('hg.admin')
437 431 @CSRFRequired()
438 432 @view_config(
439 433 route_name='edit_user_emails_delete', request_method='POST')
440 434 def emails_delete(self):
441 435 _ = self.request.translate
442 436 c = self.load_default_context()
443 437
444 438 user_id = self.request.matchdict.get('user_id')
445 439 c.user = User.get_or_404(user_id)
446 440 self._redirect_for_default_user(c.user.username)
447 441
448 442 email_id = self.request.POST.get('del_email_id')
449 443 user_model = UserModel()
450 444
451 445 email = UserEmailMap.query().get(email_id).email
452 446 user_data = c.user.get_api_data()
453 447 user_model.delete_extra_email(c.user.user_id, email_id)
454 448 audit_logger.store_web(
455 449 'user.edit.email.delete', action_data={'email': email, 'user': user_data},
456 450 user=self._rhodecode_user)
457 451 Session().commit()
458 452 h.flash(_("Removed email address from user account"),
459 453 category='success')
460 454 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
461 455
462 456 @LoginRequired()
463 457 @HasPermissionAllDecorator('hg.admin')
464 458 @view_config(
465 459 route_name='edit_user_ips', request_method='GET',
466 460 renderer='rhodecode:templates/admin/users/user_edit.mako')
467 461 def ips(self):
468 462 _ = self.request.translate
469 463 c = self.load_default_context()
470 464
471 465 user_id = self.request.matchdict.get('user_id')
472 466 c.user = User.get_or_404(user_id)
473 467 self._redirect_for_default_user(c.user.username)
474 468
475 469 c.active = 'ips'
476 470 c.user_ip_map = UserIpMap.query() \
477 471 .filter(UserIpMap.user == c.user).all()
478 472
479 473 c.inherit_default_ips = c.user.inherit_default_permissions
480 474 c.default_user_ip_map = UserIpMap.query() \
481 475 .filter(UserIpMap.user == User.get_default_user()).all()
482 476
483 477 return self._get_template_context(c)
484 478
485 479 @LoginRequired()
486 480 @HasPermissionAllDecorator('hg.admin')
487 481 @CSRFRequired()
488 482 @view_config(
489 483 route_name='edit_user_ips_add', request_method='POST')
490 484 def ips_add(self):
491 485 _ = self.request.translate
492 486 c = self.load_default_context()
493 487
494 488 user_id = self.request.matchdict.get('user_id')
495 489 c.user = User.get_or_404(user_id)
496 490 # NOTE(marcink): this view is allowed for default users, as we can
497 491 # edit their IP white list
498 492
499 493 user_model = UserModel()
500 494 desc = self.request.POST.get('description')
501 495 try:
502 496 ip_list = user_model.parse_ip_range(
503 497 self.request.POST.get('new_ip'))
504 498 except Exception as e:
505 499 ip_list = []
506 500 log.exception("Exception during ip saving")
507 501 h.flash(_('An error occurred during ip saving:%s' % (e,)),
508 502 category='error')
509 503 added = []
510 504 user_data = c.user.get_api_data()
511 505 for ip in ip_list:
512 506 try:
513 507 user_model.add_extra_ip(c.user.user_id, ip, desc)
514 508 audit_logger.store_web(
515 509 'user.edit.ip.add', action_data={'ip': ip, 'user': user_data},
516 510 user=self._rhodecode_user)
517 511 Session().commit()
518 512 added.append(ip)
519 513 except formencode.Invalid as error:
520 514 msg = error.error_dict['ip']
521 515 h.flash(msg, category='error')
522 516 except Exception:
523 517 log.exception("Exception during ip saving")
524 518 h.flash(_('An error occurred during ip saving'),
525 519 category='error')
526 520 if added:
527 521 h.flash(
528 522 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
529 523 category='success')
530 524 if 'default_user' in self.request.POST:
531 525 # case for editing global IP list we do it for 'DEFAULT' user
532 526 raise HTTPFound(h.route_path('admin_permissions_ips'))
533 527 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
534 528
535 529 @LoginRequired()
536 530 @HasPermissionAllDecorator('hg.admin')
537 531 @CSRFRequired()
538 532 @view_config(
539 533 route_name='edit_user_ips_delete', request_method='POST')
540 534 def ips_delete(self):
541 535 _ = self.request.translate
542 536 c = self.load_default_context()
543 537
544 538 user_id = self.request.matchdict.get('user_id')
545 539 c.user = User.get_or_404(user_id)
546 540 # NOTE(marcink): this view is allowed for default users, as we can
547 541 # edit their IP white list
548 542
549 543 ip_id = self.request.POST.get('del_ip_id')
550 544 user_model = UserModel()
551 545 user_data = c.user.get_api_data()
552 546 ip = UserIpMap.query().get(ip_id).ip_addr
553 547 user_model.delete_extra_ip(c.user.user_id, ip_id)
554 548 audit_logger.store_web(
555 549 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
556 550 user=self._rhodecode_user)
557 551 Session().commit()
558 552 h.flash(_("Removed ip address from user whitelist"), category='success')
559 553
560 554 if 'default_user' in self.request.POST:
561 555 # case for editing global IP list we do it for 'DEFAULT' user
562 556 raise HTTPFound(h.route_path('admin_permissions_ips'))
563 557 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
564 558
565 559 @LoginRequired()
566 560 @HasPermissionAllDecorator('hg.admin')
567 561 @view_config(
568 562 route_name='edit_user_groups_management', request_method='GET',
569 563 renderer='rhodecode:templates/admin/users/user_edit.mako')
570 564 def groups_management(self):
571 565 c = self.load_default_context()
572 566
573 567 user_id = self.request.matchdict.get('user_id')
574 568 c.user = User.get_or_404(user_id)
575 569 c.data = c.user.group_member
576 570 self._redirect_for_default_user(c.user.username)
577 571 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
578 572 for group in c.user.group_member]
579 573 c.groups = json.dumps(groups)
580 574 c.active = 'groups'
581 575
582 576 return self._get_template_context(c)
583 577
584 578 @LoginRequired()
585 579 @HasPermissionAllDecorator('hg.admin')
586 580 @CSRFRequired()
587 581 @view_config(
588 582 route_name='edit_user_groups_management_updates', request_method='POST')
589 583 def groups_management_updates(self):
590 584 _ = self.request.translate
591 585 c = self.load_default_context()
592 586
593 587 user_id = self.request.matchdict.get('user_id')
594 588 c.user = User.get_or_404(user_id)
595 589 self._redirect_for_default_user(c.user.username)
596 590
597 591 user_groups = set(self.request.POST.getall('users_group_id'))
598 592 user_groups_objects = []
599 593
600 594 for ugid in user_groups:
601 595 user_groups_objects.append(
602 596 UserGroupModel().get_group(safe_int(ugid)))
603 597 user_group_model = UserGroupModel()
604 598 user_group_model.change_groups(c.user, user_groups_objects)
605 599
606 600 Session().commit()
607 601 c.active = 'user_groups_management'
608 602 h.flash(_("Groups successfully changed"), category='success')
609 603
610 604 return HTTPFound(h.route_path(
611 605 'edit_user_groups_management', user_id=user_id))
612 606
613 607 @LoginRequired()
614 608 @HasPermissionAllDecorator('hg.admin')
615 609 @view_config(
616 610 route_name='edit_user_audit_logs', request_method='GET',
617 611 renderer='rhodecode:templates/admin/users/user_edit.mako')
618 612 def user_audit_logs(self):
619 613 _ = self.request.translate
620 614 c = self.load_default_context()
621 615
622 616 user_id = self.request.matchdict.get('user_id')
623 617 c.user = User.get_or_404(user_id)
624 618 self._redirect_for_default_user(c.user.username)
625 619 c.active = 'audit'
626 620
627 621 p = safe_int(self.request.GET.get('page', 1), 1)
628 622
629 623 filter_term = self.request.GET.get('filter')
630 624 user_log = UserModel().get_user_log(c.user, filter_term)
631 625
632 626 def url_generator(**kw):
633 627 if filter_term:
634 628 kw['filter'] = filter_term
635 629 return self.request.current_route_path(_query=kw)
636 630
637 631 c.audit_logs = h.Page(
638 632 user_log, page=p, items_per_page=10, url=url_generator)
639 633 c.filter_term = filter_term
640 634 return self._get_template_context(c)
641 635
642 636 @LoginRequired()
643 637 @HasPermissionAllDecorator('hg.admin')
644 638 @view_config(
645 639 route_name='edit_user_perms_summary', request_method='GET',
646 640 renderer='rhodecode:templates/admin/users/user_edit.mako')
647 641 def user_perms_summary(self):
648 642 _ = self.request.translate
649 643 c = self.load_default_context()
650 644
651 645 user_id = self.request.matchdict.get('user_id')
652 646 c.user = User.get_or_404(user_id)
653 647 self._redirect_for_default_user(c.user.username)
654 648
655 649 c.active = 'perms_summary'
656 650 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
657 651
658 652 return self._get_template_context(c)
659 653
660 654 @LoginRequired()
661 655 @HasPermissionAllDecorator('hg.admin')
662 656 @view_config(
663 657 route_name='edit_user_perms_summary_json', request_method='GET',
664 658 renderer='json_ext')
665 659 def user_perms_summary_json(self):
666 660 self.load_default_context()
667 661
668 662 user_id = self.request.matchdict.get('user_id')
669 663 user = User.get_or_404(user_id)
670 664 self._redirect_for_default_user(user.username)
671 665
672 666 perm_user = user.AuthUser(ip_addr=self.request.remote_addr)
673 667
674 668 return perm_user.permissions
@@ -1,587 +1,579 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 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
24 24 import formencode
25 25 import formencode.htmlfill
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.view import view_config
28 28 from pyramid.renderers import render
29 29 from pyramid.response import Response
30 30
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32 from rhodecode import forms
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib import audit_logger
35 35 from rhodecode.lib.ext_json import json
36 36 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
37 37 from rhodecode.lib.channelstream import (
38 38 channelstream_request, ChannelstreamException)
39 39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
40 40 from rhodecode.model.auth_token import AuthTokenModel
41 41 from rhodecode.model.comment import CommentsModel
42 42 from rhodecode.model.db import (
43 43 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload,
44 44 PullRequest)
45 45 from rhodecode.model.forms import UserForm
46 46 from rhodecode.model.meta import Session
47 47 from rhodecode.model.pull_request import PullRequestModel
48 48 from rhodecode.model.scm import RepoList
49 49 from rhodecode.model.user import UserModel
50 50 from rhodecode.model.repo import RepoModel
51 51 from rhodecode.model.validation_schema.schemas import user_schema
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 class MyAccountView(BaseAppView, DataGridAppView):
57 57 ALLOW_SCOPED_TOKENS = False
58 58 """
59 59 This view has alternative version inside EE, if modified please take a look
60 60 in there as well.
61 61 """
62 62
63 63 def load_default_context(self):
64 64 c = self._get_local_tmpl_context()
65 65 c.user = c.auth_user.get_instance()
66 66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
67 67 self._register_global_c(c)
68 68 return c
69 69
70 70 @LoginRequired()
71 71 @NotAnonymous()
72 72 @view_config(
73 73 route_name='my_account_profile', request_method='GET',
74 74 renderer='rhodecode:templates/admin/my_account/my_account.mako')
75 75 def my_account_profile(self):
76 76 c = self.load_default_context()
77 77 c.active = 'profile'
78 78 return self._get_template_context(c)
79 79
80 80 @LoginRequired()
81 81 @NotAnonymous()
82 82 @view_config(
83 83 route_name='my_account_password', request_method='GET',
84 84 renderer='rhodecode:templates/admin/my_account/my_account.mako')
85 85 def my_account_password(self):
86 86 c = self.load_default_context()
87 87 c.active = 'password'
88 88 c.extern_type = c.user.extern_type
89 89
90 90 schema = user_schema.ChangePasswordSchema().bind(
91 91 username=c.user.username)
92 92
93 93 form = forms.Form(
94 94 schema,
95 95 action=h.route_path('my_account_password_update'),
96 96 buttons=(forms.buttons.save, forms.buttons.reset))
97 97
98 98 c.form = form
99 99 return self._get_template_context(c)
100 100
101 101 @LoginRequired()
102 102 @NotAnonymous()
103 103 @CSRFRequired()
104 104 @view_config(
105 105 route_name='my_account_password_update', request_method='POST',
106 106 renderer='rhodecode:templates/admin/my_account/my_account.mako')
107 107 def my_account_password_update(self):
108 108 _ = self.request.translate
109 109 c = self.load_default_context()
110 110 c.active = 'password'
111 111 c.extern_type = c.user.extern_type
112 112
113 113 schema = user_schema.ChangePasswordSchema().bind(
114 114 username=c.user.username)
115 115
116 116 form = forms.Form(
117 117 schema, buttons=(forms.buttons.save, forms.buttons.reset))
118 118
119 119 if c.extern_type != 'rhodecode':
120 120 raise HTTPFound(self.request.route_path('my_account_password'))
121 121
122 122 controls = self.request.POST.items()
123 123 try:
124 124 valid_data = form.validate(controls)
125 125 UserModel().update_user(c.user.user_id, **valid_data)
126 126 c.user.update_userdata(force_password_change=False)
127 127 Session().commit()
128 128 except forms.ValidationFailure as e:
129 129 c.form = e
130 130 return self._get_template_context(c)
131 131
132 132 except Exception:
133 133 log.exception("Exception updating password")
134 134 h.flash(_('Error occurred during update of user password'),
135 135 category='error')
136 136 else:
137 137 instance = c.auth_user.get_instance()
138 138 self.session.setdefault('rhodecode_user', {}).update(
139 139 {'password': md5(instance.password)})
140 140 self.session.save()
141 141 h.flash(_("Successfully updated password"), category='success')
142 142
143 143 raise HTTPFound(self.request.route_path('my_account_password'))
144 144
145 145 @LoginRequired()
146 146 @NotAnonymous()
147 147 @view_config(
148 148 route_name='my_account_auth_tokens', request_method='GET',
149 149 renderer='rhodecode:templates/admin/my_account/my_account.mako')
150 150 def my_account_auth_tokens(self):
151 151 _ = self.request.translate
152 152
153 153 c = self.load_default_context()
154 154 c.active = 'auth_tokens'
155
156 c.lifetime_values = [
157 (str(-1), _('forever')),
158 (str(5), _('5 minutes')),
159 (str(60), _('1 hour')),
160 (str(60 * 24), _('1 day')),
161 (str(60 * 24 * 30), _('1 month')),
162 ]
163 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
155 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
164 156 c.role_values = [
165 157 (x, AuthTokenModel.cls._get_role_name(x))
166 158 for x in AuthTokenModel.cls.ROLES]
167 159 c.role_options = [(c.role_values, _("Role"))]
168 160 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
169 161 c.user.user_id, show_expired=True)
170 162 return self._get_template_context(c)
171 163
172 164 def maybe_attach_token_scope(self, token):
173 165 # implemented in EE edition
174 166 pass
175 167
176 168 @LoginRequired()
177 169 @NotAnonymous()
178 170 @CSRFRequired()
179 171 @view_config(
180 172 route_name='my_account_auth_tokens_add', request_method='POST',)
181 173 def my_account_auth_tokens_add(self):
182 174 _ = self.request.translate
183 175 c = self.load_default_context()
184 176
185 177 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
186 178 description = self.request.POST.get('description')
187 179 role = self.request.POST.get('role')
188 180
189 181 token = AuthTokenModel().create(
190 182 c.user.user_id, description, lifetime, role)
191 183 token_data = token.get_api_data()
192 184
193 185 self.maybe_attach_token_scope(token)
194 186 audit_logger.store_web(
195 187 'user.edit.token.add', action_data={
196 188 'data': {'token': token_data, 'user': 'self'}},
197 189 user=self._rhodecode_user, )
198 190 Session().commit()
199 191
200 192 h.flash(_("Auth token successfully created"), category='success')
201 193 return HTTPFound(h.route_path('my_account_auth_tokens'))
202 194
203 195 @LoginRequired()
204 196 @NotAnonymous()
205 197 @CSRFRequired()
206 198 @view_config(
207 199 route_name='my_account_auth_tokens_delete', request_method='POST')
208 200 def my_account_auth_tokens_delete(self):
209 201 _ = self.request.translate
210 202 c = self.load_default_context()
211 203
212 204 del_auth_token = self.request.POST.get('del_auth_token')
213 205
214 206 if del_auth_token:
215 207 token = UserApiKeys.get_or_404(del_auth_token)
216 208 token_data = token.get_api_data()
217 209
218 210 AuthTokenModel().delete(del_auth_token, c.user.user_id)
219 211 audit_logger.store_web(
220 212 'user.edit.token.delete', action_data={
221 213 'data': {'token': token_data, 'user': 'self'}},
222 214 user=self._rhodecode_user,)
223 215 Session().commit()
224 216 h.flash(_("Auth token successfully deleted"), category='success')
225 217
226 218 return HTTPFound(h.route_path('my_account_auth_tokens'))
227 219
228 220 @LoginRequired()
229 221 @NotAnonymous()
230 222 @view_config(
231 223 route_name='my_account_emails', request_method='GET',
232 224 renderer='rhodecode:templates/admin/my_account/my_account.mako')
233 225 def my_account_emails(self):
234 226 _ = self.request.translate
235 227
236 228 c = self.load_default_context()
237 229 c.active = 'emails'
238 230
239 231 c.user_email_map = UserEmailMap.query()\
240 232 .filter(UserEmailMap.user == c.user).all()
241 233 return self._get_template_context(c)
242 234
243 235 @LoginRequired()
244 236 @NotAnonymous()
245 237 @CSRFRequired()
246 238 @view_config(
247 239 route_name='my_account_emails_add', request_method='POST')
248 240 def my_account_emails_add(self):
249 241 _ = self.request.translate
250 242 c = self.load_default_context()
251 243
252 244 email = self.request.POST.get('new_email')
253 245
254 246 try:
255 247 UserModel().add_extra_email(c.user.user_id, email)
256 248 audit_logger.store_web(
257 249 'user.edit.email.add', action_data={
258 250 'data': {'email': email, 'user': 'self'}},
259 251 user=self._rhodecode_user,)
260 252
261 253 Session().commit()
262 254 h.flash(_("Added new email address `%s` for user account") % email,
263 255 category='success')
264 256 except formencode.Invalid as error:
265 257 h.flash(h.escape(error.error_dict['email']), category='error')
266 258 except Exception:
267 259 log.exception("Exception in my_account_emails")
268 260 h.flash(_('An error occurred during email saving'),
269 261 category='error')
270 262 return HTTPFound(h.route_path('my_account_emails'))
271 263
272 264 @LoginRequired()
273 265 @NotAnonymous()
274 266 @CSRFRequired()
275 267 @view_config(
276 268 route_name='my_account_emails_delete', request_method='POST')
277 269 def my_account_emails_delete(self):
278 270 _ = self.request.translate
279 271 c = self.load_default_context()
280 272
281 273 del_email_id = self.request.POST.get('del_email_id')
282 274 if del_email_id:
283 275 email = UserEmailMap.get_or_404(del_email_id).email
284 276 UserModel().delete_extra_email(c.user.user_id, del_email_id)
285 277 audit_logger.store_web(
286 278 'user.edit.email.delete', action_data={
287 279 'data': {'email': email, 'user': 'self'}},
288 280 user=self._rhodecode_user,)
289 281 Session().commit()
290 282 h.flash(_("Email successfully deleted"),
291 283 category='success')
292 284 return HTTPFound(h.route_path('my_account_emails'))
293 285
294 286 @LoginRequired()
295 287 @NotAnonymous()
296 288 @CSRFRequired()
297 289 @view_config(
298 290 route_name='my_account_notifications_test_channelstream',
299 291 request_method='POST', renderer='json_ext')
300 292 def my_account_notifications_test_channelstream(self):
301 293 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
302 294 self._rhodecode_user.username, datetime.datetime.now())
303 295 payload = {
304 296 # 'channel': 'broadcast',
305 297 'type': 'message',
306 298 'timestamp': datetime.datetime.utcnow(),
307 299 'user': 'system',
308 300 'pm_users': [self._rhodecode_user.username],
309 301 'message': {
310 302 'message': message,
311 303 'level': 'info',
312 304 'topic': '/notifications'
313 305 }
314 306 }
315 307
316 308 registry = self.request.registry
317 309 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
318 310 channelstream_config = rhodecode_plugins.get('channelstream', {})
319 311
320 312 try:
321 313 channelstream_request(channelstream_config, [payload], '/message')
322 314 except ChannelstreamException as e:
323 315 log.exception('Failed to send channelstream data')
324 316 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
325 317 return {"response": 'Channelstream data sent. '
326 318 'You should see a new live message now.'}
327 319
328 320 def _load_my_repos_data(self, watched=False):
329 321 if watched:
330 322 admin = False
331 323 follows_repos = Session().query(UserFollowing)\
332 324 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
333 325 .options(joinedload(UserFollowing.follows_repository))\
334 326 .all()
335 327 repo_list = [x.follows_repository for x in follows_repos]
336 328 else:
337 329 admin = True
338 330 repo_list = Repository.get_all_repos(
339 331 user_id=self._rhodecode_user.user_id)
340 332 repo_list = RepoList(repo_list, perm_set=[
341 333 'repository.read', 'repository.write', 'repository.admin'])
342 334
343 335 repos_data = RepoModel().get_repos_as_dict(
344 336 repo_list=repo_list, admin=admin)
345 337 # json used to render the grid
346 338 return json.dumps(repos_data)
347 339
348 340 @LoginRequired()
349 341 @NotAnonymous()
350 342 @view_config(
351 343 route_name='my_account_repos', request_method='GET',
352 344 renderer='rhodecode:templates/admin/my_account/my_account.mako')
353 345 def my_account_repos(self):
354 346 c = self.load_default_context()
355 347 c.active = 'repos'
356 348
357 349 # json used to render the grid
358 350 c.data = self._load_my_repos_data()
359 351 return self._get_template_context(c)
360 352
361 353 @LoginRequired()
362 354 @NotAnonymous()
363 355 @view_config(
364 356 route_name='my_account_watched', request_method='GET',
365 357 renderer='rhodecode:templates/admin/my_account/my_account.mako')
366 358 def my_account_watched(self):
367 359 c = self.load_default_context()
368 360 c.active = 'watched'
369 361
370 362 # json used to render the grid
371 363 c.data = self._load_my_repos_data(watched=True)
372 364 return self._get_template_context(c)
373 365
374 366 @LoginRequired()
375 367 @NotAnonymous()
376 368 @view_config(
377 369 route_name='my_account_perms', request_method='GET',
378 370 renderer='rhodecode:templates/admin/my_account/my_account.mako')
379 371 def my_account_perms(self):
380 372 c = self.load_default_context()
381 373 c.active = 'perms'
382 374
383 375 c.perm_user = c.auth_user
384 376 return self._get_template_context(c)
385 377
386 378 @LoginRequired()
387 379 @NotAnonymous()
388 380 @view_config(
389 381 route_name='my_account_notifications', request_method='GET',
390 382 renderer='rhodecode:templates/admin/my_account/my_account.mako')
391 383 def my_notifications(self):
392 384 c = self.load_default_context()
393 385 c.active = 'notifications'
394 386
395 387 return self._get_template_context(c)
396 388
397 389 @LoginRequired()
398 390 @NotAnonymous()
399 391 @CSRFRequired()
400 392 @view_config(
401 393 route_name='my_account_notifications_toggle_visibility',
402 394 request_method='POST', renderer='json_ext')
403 395 def my_notifications_toggle_visibility(self):
404 396 user = self._rhodecode_db_user
405 397 new_status = not user.user_data.get('notification_status', True)
406 398 user.update_userdata(notification_status=new_status)
407 399 Session().commit()
408 400 return user.user_data['notification_status']
409 401
410 402 @LoginRequired()
411 403 @NotAnonymous()
412 404 @view_config(
413 405 route_name='my_account_edit',
414 406 request_method='GET',
415 407 renderer='rhodecode:templates/admin/my_account/my_account.mako')
416 408 def my_account_edit(self):
417 409 c = self.load_default_context()
418 410 c.active = 'profile_edit'
419 411
420 412 c.perm_user = c.auth_user
421 413 c.extern_type = c.user.extern_type
422 414 c.extern_name = c.user.extern_name
423 415
424 416 defaults = c.user.get_dict()
425 417
426 418 data = render('rhodecode:templates/admin/my_account/my_account.mako',
427 419 self._get_template_context(c), self.request)
428 420 html = formencode.htmlfill.render(
429 421 data,
430 422 defaults=defaults,
431 423 encoding="UTF-8",
432 424 force_defaults=False
433 425 )
434 426 return Response(html)
435 427
436 428 @LoginRequired()
437 429 @NotAnonymous()
438 430 @CSRFRequired()
439 431 @view_config(
440 432 route_name='my_account_update',
441 433 request_method='POST',
442 434 renderer='rhodecode:templates/admin/my_account/my_account.mako')
443 435 def my_account_update(self):
444 436 _ = self.request.translate
445 437 c = self.load_default_context()
446 438 c.active = 'profile_edit'
447 439
448 440 c.perm_user = c.auth_user
449 441 c.extern_type = c.user.extern_type
450 442 c.extern_name = c.user.extern_name
451 443
452 444 _form = UserForm(edit=True,
453 445 old_data={'user_id': self._rhodecode_user.user_id,
454 446 'email': self._rhodecode_user.email})()
455 447 form_result = {}
456 448 try:
457 449 post_data = dict(self.request.POST)
458 450 post_data['new_password'] = ''
459 451 post_data['password_confirmation'] = ''
460 452 form_result = _form.to_python(post_data)
461 453 # skip updating those attrs for my account
462 454 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
463 455 'new_password', 'password_confirmation']
464 456 # TODO: plugin should define if username can be updated
465 457 if c.extern_type != "rhodecode":
466 458 # forbid updating username for external accounts
467 459 skip_attrs.append('username')
468 460
469 461 UserModel().update_user(
470 462 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
471 463 **form_result)
472 464 h.flash(_('Your account was updated successfully'),
473 465 category='success')
474 466 Session().commit()
475 467
476 468 except formencode.Invalid as errors:
477 469 data = render(
478 470 'rhodecode:templates/admin/my_account/my_account.mako',
479 471 self._get_template_context(c), self.request)
480 472
481 473 html = formencode.htmlfill.render(
482 474 data,
483 475 defaults=errors.value,
484 476 errors=errors.error_dict or {},
485 477 prefix_error=False,
486 478 encoding="UTF-8",
487 479 force_defaults=False)
488 480 return Response(html)
489 481
490 482 except Exception:
491 483 log.exception("Exception updating user")
492 484 h.flash(_('Error occurred during update of user %s')
493 485 % form_result.get('username'), category='error')
494 486 raise HTTPFound(h.route_path('my_account_profile'))
495 487
496 488 raise HTTPFound(h.route_path('my_account_profile'))
497 489
498 490 def _get_pull_requests_list(self, statuses):
499 491 draw, start, limit = self._extract_chunk(self.request)
500 492 search_q, order_by, order_dir = self._extract_ordering(self.request)
501 493 _render = self.request.get_partial_renderer(
502 494 'data_table/_dt_elements.mako')
503 495
504 496 pull_requests = PullRequestModel().get_im_participating_in(
505 497 user_id=self._rhodecode_user.user_id,
506 498 statuses=statuses,
507 499 offset=start, length=limit, order_by=order_by,
508 500 order_dir=order_dir)
509 501
510 502 pull_requests_total_count = PullRequestModel().count_im_participating_in(
511 503 user_id=self._rhodecode_user.user_id, statuses=statuses)
512 504
513 505 data = []
514 506 comments_model = CommentsModel()
515 507 for pr in pull_requests:
516 508 repo_id = pr.target_repo_id
517 509 comments = comments_model.get_all_comments(
518 510 repo_id, pull_request=pr)
519 511 owned = pr.user_id == self._rhodecode_user.user_id
520 512
521 513 data.append({
522 514 'target_repo': _render('pullrequest_target_repo',
523 515 pr.target_repo.repo_name),
524 516 'name': _render('pullrequest_name',
525 517 pr.pull_request_id, pr.target_repo.repo_name,
526 518 short=True),
527 519 'name_raw': pr.pull_request_id,
528 520 'status': _render('pullrequest_status',
529 521 pr.calculated_review_status()),
530 522 'title': _render(
531 523 'pullrequest_title', pr.title, pr.description),
532 524 'description': h.escape(pr.description),
533 525 'updated_on': _render('pullrequest_updated_on',
534 526 h.datetime_to_time(pr.updated_on)),
535 527 'updated_on_raw': h.datetime_to_time(pr.updated_on),
536 528 'created_on': _render('pullrequest_updated_on',
537 529 h.datetime_to_time(pr.created_on)),
538 530 'created_on_raw': h.datetime_to_time(pr.created_on),
539 531 'author': _render('pullrequest_author',
540 532 pr.author.full_contact, ),
541 533 'author_raw': pr.author.full_name,
542 534 'comments': _render('pullrequest_comments', len(comments)),
543 535 'comments_raw': len(comments),
544 536 'closed': pr.is_closed(),
545 537 'owned': owned
546 538 })
547 539
548 540 # json used to render the grid
549 541 data = ({
550 542 'draw': draw,
551 543 'data': data,
552 544 'recordsTotal': pull_requests_total_count,
553 545 'recordsFiltered': pull_requests_total_count,
554 546 })
555 547 return data
556 548
557 549 @LoginRequired()
558 550 @NotAnonymous()
559 551 @view_config(
560 552 route_name='my_account_pullrequests',
561 553 request_method='GET',
562 554 renderer='rhodecode:templates/admin/my_account/my_account.mako')
563 555 def my_account_pullrequests(self):
564 556 c = self.load_default_context()
565 557 c.active = 'pullrequests'
566 558 req_get = self.request.GET
567 559
568 560 c.closed = str2bool(req_get.get('pr_show_closed'))
569 561
570 562 return self._get_template_context(c)
571 563
572 564 @LoginRequired()
573 565 @NotAnonymous()
574 566 @view_config(
575 567 route_name='my_account_pullrequests_data',
576 568 request_method='GET', renderer='json_ext')
577 569 def my_account_pullrequests_data(self):
578 570 req_get = self.request.GET
579 571 closed = str2bool(req_get.get('closed'))
580 572
581 573 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
582 574 if closed:
583 575 statuses += [PullRequest.STATUS_CLOSED]
584 576
585 577 data = self._get_pull_requests_list(statuses=statuses)
586 578 return data
587 579
@@ -1,102 +1,124 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2017 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 authentication tokens model for RhodeCode
23 23 """
24 24
25 25 import time
26 26 import logging
27 27 import traceback
28 28 from sqlalchemy import or_
29 29
30 30 from rhodecode.model import BaseModel
31 31 from rhodecode.model.db import UserApiKeys
32 32 from rhodecode.model.meta import Session
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 class AuthTokenModel(BaseModel):
38 38 cls = UserApiKeys
39 39
40 @classmethod
41 def get_lifetime_values(cls, translator):
42 from rhodecode.lib import helpers as h
43 _ = translator
44
45 def date_after_min(mins):
46 after = time.time() + (60 * mins)
47 return h.format_date(h.time_to_datetime(after))
48
49 return [
50 (str(-1),
51 _('forever')),
52 (str(5),
53 _('5 minutes {end_date}').format(end_date=date_after_min(5))),
54 (str(60),
55 _('1 hour {end_date}').format(end_date=date_after_min(60))),
56 (str(60 * 24),
57 _('1 day {end_date}').format(end_date=date_after_min(60 * 24))),
58 (str(60 * 24 * 30),
59 _('1 month {end_date}').format(end_date=date_after_min(60 * 24 * 30))),
60 ]
61
40 62 def create(self, user, description, lifetime=-1, role=UserApiKeys.ROLE_ALL):
41 63 """
42 64 :param user: user or user_id
43 65 :param description: description of ApiKey
44 66 :param lifetime: expiration time in minutes
45 67 :param role: role for the apikey
46 68 """
47 69 from rhodecode.lib.auth import generate_auth_token
48 70
49 71 user = self._get_user(user)
50 72
51 73 new_auth_token = UserApiKeys()
52 74 new_auth_token.api_key = generate_auth_token(user.username)
53 75 new_auth_token.user_id = user.user_id
54 76 new_auth_token.description = description
55 77 new_auth_token.role = role
56 78 new_auth_token.expires = time.time() + (lifetime * 60) \
57 79 if lifetime != -1 else -1
58 80 Session().add(new_auth_token)
59 81
60 82 return new_auth_token
61 83
62 84 def delete(self, auth_token_id, user=None):
63 85 """
64 86 Deletes given api_key, if user is set it also filters the object for
65 87 deletion by given user.
66 88 """
67 89 auth_token = UserApiKeys.query().filter(
68 90 UserApiKeys.user_api_key_id == auth_token_id)
69 91
70 92 if user:
71 93 user = self._get_user(user)
72 94 auth_token = auth_token.filter(UserApiKeys.user_id == user.user_id)
73 95 auth_token = auth_token.scalar()
74 96
75 97 if auth_token:
76 98 try:
77 99 Session().delete(auth_token)
78 100 except Exception:
79 101 log.error(traceback.format_exc())
80 102 raise
81 103
82 104 def get_auth_tokens(self, user, show_expired=True):
83 105 user = self._get_user(user)
84 106 user_auth_tokens = UserApiKeys.query()\
85 107 .filter(UserApiKeys.user_id == user.user_id)
86 108 if not show_expired:
87 109 user_auth_tokens = user_auth_tokens\
88 110 .filter(or_(UserApiKeys.expires == -1,
89 111 UserApiKeys.expires >= time.time()))
90 112 user_auth_tokens = user_auth_tokens.order_by(
91 113 UserApiKeys.user_api_key_id)
92 114 return user_auth_tokens
93 115
94 116 def get_auth_token(self, auth_token):
95 117 auth_token = UserApiKeys.query().filter(
96 118 UserApiKeys.api_key == auth_token)
97 119 auth_token = auth_token \
98 120 .filter(or_(UserApiKeys.expires == -1,
99 121 UserApiKeys.expires >= time.time()))\
100 122 .first()
101 123
102 124 return auth_token
@@ -1,501 +1,560 b''
1 1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 RhodeCode JS Files
21 21 **/
22 22
23 23 if (typeof console == "undefined" || typeof console.log == "undefined"){
24 24 console = { log: function() {} }
25 25 }
26 26
27 27 // TODO: move the following function to submodules
28 28
29 29 /**
30 30 * show more
31 31 */
32 32 var show_more_event = function(){
33 33 $('table .show_more').click(function(e) {
34 34 var cid = e.target.id.substring(1);
35 35 var button = $(this);
36 36 if (button.hasClass('open')) {
37 37 $('#'+cid).hide();
38 38 button.removeClass('open');
39 39 } else {
40 40 $('#'+cid).show();
41 41 button.addClass('open one');
42 42 }
43 43 });
44 44 };
45 45
46 46 var compare_radio_buttons = function(repo_name, compare_ref_type){
47 47 $('#compare_action').on('click', function(e){
48 48 e.preventDefault();
49 49
50 50 var source = $('input[name=compare_source]:checked').val();
51 51 var target = $('input[name=compare_target]:checked').val();
52 52 if(source && target){
53 53 var url_data = {
54 54 repo_name: repo_name,
55 55 source_ref: source,
56 56 source_ref_type: compare_ref_type,
57 57 target_ref: target,
58 58 target_ref_type: compare_ref_type,
59 59 merge: 1
60 60 };
61 61 window.location = pyroutes.url('repo_compare', url_data);
62 62 }
63 63 });
64 64 $('.compare-radio-button').on('click', function(e){
65 65 var source = $('input[name=compare_source]:checked').val();
66 66 var target = $('input[name=compare_target]:checked').val();
67 67 if(source && target){
68 68 $('#compare_action').removeAttr("disabled");
69 69 $('#compare_action').removeClass("disabled");
70 70 }
71 71 })
72 72 };
73 73
74 74 var showRepoSize = function(target, repo_name, commit_id, callback) {
75 75 var container = $('#' + target);
76 76 var url = pyroutes.url('repo_stats',
77 77 {"repo_name": repo_name, "commit_id": commit_id});
78 78
79 79 if (!container.hasClass('loaded')) {
80 80 $.ajax({url: url})
81 81 .complete(function (data) {
82 82 var responseJSON = data.responseJSON;
83 83 container.addClass('loaded');
84 84 container.html(responseJSON.size);
85 85 callback(responseJSON.code_stats)
86 86 })
87 87 .fail(function (data) {
88 88 console.log('failed to load repo stats');
89 89 });
90 90 }
91 91
92 92 };
93 93
94 94 var showRepoStats = function(target, data){
95 95 var container = $('#' + target);
96 96
97 97 if (container.hasClass('loaded')) {
98 98 return
99 99 }
100 100
101 101 var total = 0;
102 102 var no_data = true;
103 103 var tbl = document.createElement('table');
104 104 tbl.setAttribute('class', 'trending_language_tbl');
105 105
106 106 $.each(data, function(key, val){
107 107 total += val.count;
108 108 });
109 109
110 110 var sortedStats = [];
111 111 for (var obj in data){
112 112 sortedStats.push([obj, data[obj]])
113 113 }
114 114 var sortedData = sortedStats.sort(function (a, b) {
115 115 return b[1].count - a[1].count
116 116 });
117 117 var cnt = 0;
118 118 $.each(sortedData, function(idx, val){
119 119 cnt += 1;
120 120 no_data = false;
121 121
122 122 var hide = cnt > 2;
123 123 var tr = document.createElement('tr');
124 124 if (hide) {
125 125 tr.setAttribute('style', 'display:none');
126 126 tr.setAttribute('class', 'stats_hidden');
127 127 }
128 128
129 129 var key = val[0];
130 130 var obj = {"desc": val[1].desc, "count": val[1].count};
131 131
132 132 var percentage = Math.round((obj.count / total * 100), 2);
133 133
134 134 var td1 = document.createElement('td');
135 135 td1.width = 300;
136 136 var trending_language_label = document.createElement('div');
137 137 trending_language_label.innerHTML = obj.desc + " (.{0})".format(key);
138 138 td1.appendChild(trending_language_label);
139 139
140 140 var td2 = document.createElement('td');
141 141 var trending_language = document.createElement('div');
142 142 var nr_files = obj.count +" "+ _ngettext('file', 'files', obj.count);
143 143
144 144 trending_language.title = key + " " + nr_files;
145 145
146 146 trending_language.innerHTML = "<span>" + percentage + "% " + nr_files
147 147 + "</span><b>" + percentage + "% " + nr_files + "</b>";
148 148
149 149 trending_language.setAttribute("class", 'trending_language');
150 150 $('b', trending_language)[0].style.width = percentage + "%";
151 151 td2.appendChild(trending_language);
152 152
153 153 tr.appendChild(td1);
154 154 tr.appendChild(td2);
155 155 tbl.appendChild(tr);
156 156 if (cnt == 3) {
157 157 var show_more = document.createElement('tr');
158 158 var td = document.createElement('td');
159 159 lnk = document.createElement('a');
160 160
161 161 lnk.href = '#';
162 162 lnk.innerHTML = _gettext('Show more');
163 163 lnk.id = 'code_stats_show_more';
164 164 td.appendChild(lnk);
165 165
166 166 show_more.appendChild(td);
167 167 show_more.appendChild(document.createElement('td'));
168 168 tbl.appendChild(show_more);
169 169 }
170 170 });
171 171
172 172 $(container).html(tbl);
173 173 $(container).addClass('loaded');
174 174
175 175 $('#code_stats_show_more').on('click', function (e) {
176 176 e.preventDefault();
177 177 $('.stats_hidden').each(function (idx) {
178 178 $(this).css("display", "");
179 179 });
180 180 $('#code_stats_show_more').hide();
181 181 });
182 182
183 183 };
184 184
185 185 // returns a node from given html;
186 186 var fromHTML = function(html){
187 187 var _html = document.createElement('element');
188 188 _html.innerHTML = html;
189 189 return _html;
190 190 };
191 191
192 192 // Toggle Collapsable Content
193 193 function collapsableContent() {
194 194
195 195 $('.collapsable-content').not('.no-hide').hide();
196 196
197 197 $('.btn-collapse').unbind(); //in case we've been here before
198 198 $('.btn-collapse').click(function() {
199 199 var button = $(this);
200 200 var togglename = $(this).data("toggle");
201 201 $('.collapsable-content[data-toggle='+togglename+']').toggle();
202 202 if ($(this).html()=="Show Less")
203 203 $(this).html("Show More");
204 204 else
205 205 $(this).html("Show Less");
206 206 });
207 207 };
208 208
209 209 var timeagoActivate = function() {
210 210 $("time.timeago").timeago();
211 211 };
212 212
213 213
214 214 var clipboardActivate = function() {
215 215 /*
216 216 *
217 217 * <i class="tooltip icon-plus clipboard-action" data-clipboard-text="${commit.raw_id}" title="${_('Copy the full commit id')}"></i>
218 218 * */
219 219 var clipboard = new Clipboard('.clipboard-action');
220 220
221 221 clipboard.on('success', function(e) {
222 222 var callback = function () {
223 223 $(e.trigger).animate({'opacity': 1.00}, 200)
224 224 };
225 225 $(e.trigger).animate({'opacity': 0.15}, 200, callback);
226 226 e.clearSelection();
227 227 });
228 228 };
229 229
230 230
231 231 // Formatting values in a Select2 dropdown of commit references
232 232 var formatSelect2SelectionRefs = function(commit_ref){
233 233 var tmpl = '';
234 234 if (!commit_ref.text || commit_ref.type === 'sha'){
235 235 return commit_ref.text;
236 236 }
237 237 if (commit_ref.type === 'branch'){
238 238 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
239 239 } else if (commit_ref.type === 'tag'){
240 240 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
241 241 } else if (commit_ref.type === 'book'){
242 242 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
243 243 }
244 244 return tmpl.concat(commit_ref.text);
245 245 };
246 246
247 247 // takes a given html element and scrolls it down offset pixels
248 248 function offsetScroll(element, offset) {
249 249 setTimeout(function() {
250 250 var location = element.offset().top;
251 251 // some browsers use body, some use html
252 252 $('html, body').animate({ scrollTop: (location - offset) });
253 253 }, 100);
254 254 }
255 255
256 256 // scroll an element `percent`% from the top of page in `time` ms
257 257 function scrollToElement(element, percent, time) {
258 258 percent = (percent === undefined ? 25 : percent);
259 259 time = (time === undefined ? 100 : time);
260 260
261 261 var $element = $(element);
262 262 if ($element.length == 0) {
263 263 throw('Cannot scroll to {0}'.format(element))
264 264 }
265 265 var elOffset = $element.offset().top;
266 266 var elHeight = $element.height();
267 267 var windowHeight = $(window).height();
268 268 var offset = elOffset;
269 269 if (elHeight < windowHeight) {
270 270 offset = elOffset - ((windowHeight / (100 / percent)) - (elHeight / 2));
271 271 }
272 272 setTimeout(function() {
273 273 $('html, body').animate({ scrollTop: offset});
274 274 }, time);
275 275 }
276 276
277 277 /**
278 278 * global hooks after DOM is loaded
279 279 */
280 280 $(document).ready(function() {
281 281 firefoxAnchorFix();
282 282
283 283 $('.navigation a.menulink').on('click', function(e){
284 284 var menuitem = $(this).parent('li');
285 285 if (menuitem.hasClass('open')) {
286 286 menuitem.removeClass('open');
287 287 } else {
288 288 menuitem.addClass('open');
289 289 $(document).on('click', function(event) {
290 290 if (!$(event.target).closest(menuitem).length) {
291 291 menuitem.removeClass('open');
292 292 }
293 293 });
294 294 }
295 295 });
296 296 $('.compare_view_files').on(
297 297 'mouseenter mouseleave', 'tr.line .lineno a',function(event) {
298 298 if (event.type === "mouseenter") {
299 299 $(this).parents('tr.line').addClass('hover');
300 300 } else {
301 301 $(this).parents('tr.line').removeClass('hover');
302 302 }
303 303 });
304 304
305 305 $('.compare_view_files').on(
306 306 'mouseenter mouseleave', 'tr.line .add-comment-line a',function(event){
307 307 if (event.type === "mouseenter") {
308 308 $(this).parents('tr.line').addClass('commenting');
309 309 } else {
310 310 $(this).parents('tr.line').removeClass('commenting');
311 311 }
312 312 });
313 313
314 314 $('body').on( /* TODO: replace the $('.compare_view_files').on('click') below
315 315 when new diffs are integrated */
316 316 'click', '.cb-lineno a', function(event) {
317 317
318 318 if ($(this).attr('data-line-no') !== ""){
319 319 $('.cb-line-selected').removeClass('cb-line-selected');
320 320 var td = $(this).parent();
321 321 td.addClass('cb-line-selected'); // line number td
322 322 td.prev().addClass('cb-line-selected'); // line data td
323 323 td.next().addClass('cb-line-selected'); // line content td
324 324
325 325 // Replace URL without jumping to it if browser supports.
326 326 // Default otherwise
327 327 if (history.pushState) {
328 328 var new_location = location.href.rstrip('#');
329 329 if (location.hash) {
330 330 new_location = new_location.replace(location.hash, "");
331 331 }
332 332
333 333 // Make new anchor url
334 334 new_location = new_location + $(this).attr('href');
335 335 history.pushState(true, document.title, new_location);
336 336
337 337 return false;
338 338 }
339 339 }
340 340 });
341 341
342 342 $('.compare_view_files').on( /* TODO: replace this with .cb function above
343 343 when new diffs are integrated */
344 344 'click', 'tr.line .lineno a',function(event) {
345 345 if ($(this).text() != ""){
346 346 $('tr.line').removeClass('selected');
347 347 $(this).parents("tr.line").addClass('selected');
348 348
349 349 // Replace URL without jumping to it if browser supports.
350 350 // Default otherwise
351 351 if (history.pushState) {
352 352 var new_location = location.href;
353 353 if (location.hash){
354 354 new_location = new_location.replace(location.hash, "");
355 355 }
356 356
357 357 // Make new anchor url
358 358 var new_location = new_location+$(this).attr('href');
359 359 history.pushState(true, document.title, new_location);
360 360
361 361 return false;
362 362 }
363 363 }
364 364 });
365 365
366 366 $('.compare_view_files').on(
367 367 'click', 'tr.line .add-comment-line a',function(event) {
368 368 var tr = $(event.currentTarget).parents('tr.line')[0];
369 369 injectInlineForm(tr);
370 370 return false;
371 371 });
372 372
373 373 $('.collapse_file').on('click', function(e) {
374 374 e.stopPropagation();
375 375 if ($(e.target).is('a')) { return; }
376 376 var node = $(e.delegateTarget).first();
377 377 var icon = $($(node.children().first()).children().first());
378 378 var id = node.attr('fid');
379 379 var target = $('#'+id);
380 380 var tr = $('#tr_'+id);
381 381 var diff = $('#diff_'+id);
382 382 if(node.hasClass('expand_file')){
383 383 node.removeClass('expand_file');
384 384 icon.removeClass('expand_file_icon');
385 385 node.addClass('collapse_file');
386 386 icon.addClass('collapse_file_icon');
387 387 diff.show();
388 388 tr.show();
389 389 target.show();
390 390 } else {
391 391 node.removeClass('collapse_file');
392 392 icon.removeClass('collapse_file_icon');
393 393 node.addClass('expand_file');
394 394 icon.addClass('expand_file_icon');
395 395 diff.hide();
396 396 tr.hide();
397 397 target.hide();
398 398 }
399 399 });
400 400
401 401 $('#expand_all_files').click(function() {
402 402 $('.expand_file').each(function() {
403 403 var node = $(this);
404 404 var icon = $($(node.children().first()).children().first());
405 405 var id = $(this).attr('fid');
406 406 var target = $('#'+id);
407 407 var tr = $('#tr_'+id);
408 408 var diff = $('#diff_'+id);
409 409 node.removeClass('expand_file');
410 410 icon.removeClass('expand_file_icon');
411 411 node.addClass('collapse_file');
412 412 icon.addClass('collapse_file_icon');
413 413 diff.show();
414 414 tr.show();
415 415 target.show();
416 416 });
417 417 });
418 418
419 419 $('#collapse_all_files').click(function() {
420 420 $('.collapse_file').each(function() {
421 421 var node = $(this);
422 422 var icon = $($(node.children().first()).children().first());
423 423 var id = $(this).attr('fid');
424 424 var target = $('#'+id);
425 425 var tr = $('#tr_'+id);
426 426 var diff = $('#diff_'+id);
427 427 node.removeClass('collapse_file');
428 428 icon.removeClass('collapse_file_icon');
429 429 node.addClass('expand_file');
430 430 icon.addClass('expand_file_icon');
431 431 diff.hide();
432 432 tr.hide();
433 433 target.hide();
434 434 });
435 435 });
436 436
437 437 // Mouse over behavior for comments and line selection
438 438
439 439 // Select the line that comes from the url anchor
440 440 // At the time of development, Chrome didn't seem to support jquery's :target
441 441 // element, so I had to scroll manually
442 442
443 443 if (location.hash) {
444 444 var result = splitDelimitedHash(location.hash);
445 445 var loc = result.loc;
446 446 if (loc.length > 1) {
447 447
448 448 var highlightable_line_tds = [];
449 449
450 450 // source code line format
451 451 var page_highlights = loc.substring(
452 452 loc.indexOf('#') + 1).split('L');
453 453
454 454 if (page_highlights.length > 1) {
455 455 var highlight_ranges = page_highlights[1].split(",");
456 456 var h_lines = [];
457 457 for (var pos in highlight_ranges) {
458 458 var _range = highlight_ranges[pos].split('-');
459 459 if (_range.length === 2) {
460 460 var start = parseInt(_range[0]);
461 461 var end = parseInt(_range[1]);
462 462 if (start < end) {
463 463 for (var i = start; i <= end; i++) {
464 464 h_lines.push(i);
465 465 }
466 466 }
467 467 }
468 468 else {
469 469 h_lines.push(parseInt(highlight_ranges[pos]));
470 470 }
471 471 }
472 472 for (pos in h_lines) {
473 473 var line_td = $('td.cb-lineno#L' + h_lines[pos]);
474 474 if (line_td.length) {
475 475 highlightable_line_tds.push(line_td);
476 476 }
477 477 }
478 478 }
479 479
480 480 // now check a direct id reference (diff page)
481 481 if ($(loc).length && $(loc).hasClass('cb-lineno')) {
482 482 highlightable_line_tds.push($(loc));
483 483 }
484 484 $.each(highlightable_line_tds, function (i, $td) {
485 485 $td.addClass('cb-line-selected'); // line number td
486 486 $td.prev().addClass('cb-line-selected'); // line data
487 487 $td.next().addClass('cb-line-selected'); // line content
488 488 });
489 489
490 490 if (highlightable_line_tds.length) {
491 491 var $first_line_td = highlightable_line_tds[0];
492 492 scrollToElement($first_line_td);
493 493 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
494 494 td: $first_line_td,
495 495 remainder: result.remainder
496 496 });
497 497 }
498 498 }
499 499 }
500 500 collapsableContent();
501 501 });
502
503 var feedLifetimeOptions = function(query, initialData){
504 var data = {results: []};
505 var isQuery = typeof query.term !== 'undefined';
506
507 var section = _gettext('Lifetime');
508 var children = [];
509
510 //filter results
511 $.each(initialData.results, function(idx, value) {
512
513 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
514 children.push({
515 'id': this.id,
516 'text': this.text
517 })
518 }
519
520 });
521 data.results.push({
522 'text': section,
523 'children': children
524 });
525
526 if (isQuery) {
527
528 var now = moment.utc();
529
530 var parseQuery = function(entry, now){
531 var fmt = 'DD/MM/YYYY H:mm';
532 var parsed = moment.utc(entry, fmt);
533 var diffInMin = parsed.diff(now, 'minutes');
534
535 if (diffInMin > 0){
536 return {
537 id: diffInMin,
538 text: parsed.format(fmt)
539 }
540 } else {
541 return {
542 id: undefined,
543 text: parsed.format('DD/MM/YYYY') + ' ' + _gettext('date not in future')
544 }
545 }
546
547
548 };
549
550 data.results.push({
551 'text': _gettext('Specified expiration date'),
552 'children': [{
553 'id': parseQuery(query.term, now).id,
554 'text': parseQuery(query.term, now).text
555 }]
556 });
557 }
558
559 query.callback(data);
560 };
@@ -1,160 +1,180 b''
1 1 <div class="panel panel-default">
2 2 <div class="panel-heading">
3 3 <h3 class="panel-title">${_('Authentication Tokens')}</h3>
4 4 </div>
5 5 <div class="panel-body">
6 6 <div class="apikeys_wrap">
7 7 <p>
8 8 ${_('Each token can have a role. Token with a role can be used only in given context, '
9 9 'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
10 10 </p>
11 11 <table class="rctable auth_tokens">
12 12 <tr>
13 13 <th>${_('Token')}</th>
14 14 <th>${_('Scope')}</th>
15 15 <th>${_('Description')}</th>
16 16 <th>${_('Role')}</th>
17 17 <th>${_('Expiration')}</th>
18 18 <th>${_('Action')}</th>
19 19 </tr>
20 20 %if c.user_auth_tokens:
21 21 %for auth_token in c.user_auth_tokens:
22 22 <tr class="${'expired' if auth_token.expired else ''}">
23 23 <td class="truncate-wrap td-authtoken">
24 24 <div class="user_auth_tokens truncate autoexpand">
25 25 <code>${auth_token.api_key}</code>
26 26 </div>
27 27 </td>
28 28 <td class="td">${auth_token.scope_humanized}</td>
29 29 <td class="td-wrap">${auth_token.description}</td>
30 30 <td class="td-tags">
31 31 <span class="tag disabled">${auth_token.role_humanized}</span>
32 32 </td>
33 33 <td class="td-exp">
34 34 %if auth_token.expires == -1:
35 35 ${_('never')}
36 36 %else:
37 37 %if auth_token.expired:
38 38 <span style="text-decoration: line-through">${h.age_component(h.time_to_utcdatetime(auth_token.expires))}</span>
39 39 %else:
40 40 ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
41 41 %endif
42 42 %endif
43 43 </td>
44 44 <td class="td-action">
45 45 ${h.secure_form(h.route_path('my_account_auth_tokens_delete'), method='POST', request=request)}
46 46 ${h.hidden('del_auth_token', auth_token.user_api_key_id)}
47 47 <button class="btn btn-link btn-danger" type="submit"
48 48 onclick="return confirm('${_('Confirm to remove this auth token: %s') % auth_token.token_obfuscated}');">
49 49 ${_('Delete')}
50 50 </button>
51 51 ${h.end_form()}
52 52 </td>
53 53 </tr>
54 54 %endfor
55 55 %else:
56 56 <tr><td><div class="ip">${_('No additional auth tokens specified')}</div></td></tr>
57 57 %endif
58 58 </table>
59 59 </div>
60 60
61 61 <div class="user_auth_tokens">
62 62 ${h.secure_form(h.route_path('my_account_auth_tokens_add'), method='POST', request=request)}
63 63 <div class="form form-vertical">
64 64 <!-- fields -->
65 65 <div class="fields">
66 66 <div class="field">
67 67 <div class="label">
68 68 <label for="new_email">${_('New authentication token')}:</label>
69 69 </div>
70 70 <div class="input">
71 71 ${h.text('description', class_='medium', placeholder=_('Description'))}
72 ${h.select('lifetime', '', c.lifetime_options)}
72 ${h.hidden('lifetime')}
73 73 ${h.select('role', '', c.role_options)}
74 74
75 75 % if c.allow_scoped_tokens:
76 76 ${h.hidden('scope_repo_id')}
77 77 % else:
78 78 ${h.select('scope_repo_id_disabled', '', ['Scopes available in EE edition'], disabled='disabled')}
79 79 % endif
80 80 </div>
81 81 <p class="help-block">
82 82 ${_('Repository scope works only with tokens with VCS type.')}
83 83 </p>
84 84 </div>
85 85 <div class="buttons">
86 86 ${h.submit('save',_('Add'),class_="btn")}
87 87 ${h.reset('reset',_('Reset'),class_="btn")}
88 88 </div>
89 89 </div>
90 90 </div>
91 91 ${h.end_form()}
92 92 </div>
93 93 </div>
94 94 </div>
95 95 <script>
96 96 $(document).ready(function(){
97 97
98 98 var select2Options = {
99 99 'containerCssClass': "drop-menu",
100 100 'dropdownCssClass': "drop-menu-dropdown",
101 101 'dropdownAutoWidth': true
102 102 };
103 $("#lifetime").select2(select2Options);
104 103 $("#role").select2(select2Options);
105 104
105
106 var preloadData = {
107 results: [
108 % for entry in c.lifetime_values:
109 {id:${entry[0]}, text:"${entry[1]}"}${'' if loop.last else ','}
110 % endfor
111 ]
112 };
113
114 $("#lifetime").select2({
115 containerCssClass: "drop-menu",
116 dropdownCssClass: "drop-menu-dropdown",
117 dropdownAutoWidth: true,
118 data: preloadData,
119 placeholder: ${_('Select or enter expiration date')},
120 query: function(query) {
121 feedLifetimeOptions(query, preloadData);
122 }
123 });
124
125
106 126 var repoFilter = function(data) {
107 127 var results = [];
108 128
109 129 if (!data.results[0]) {
110 130 return data
111 131 }
112 132
113 133 $.each(data.results[0].children, function() {
114 134 // replace name to ID for submision
115 135 this.id = this.obj.repo_id;
116 136 results.push(this);
117 137 });
118 138
119 139 data.results[0].children = results;
120 140 return data;
121 141 };
122 142
123 143 $("#scope_repo_id_disabled").select2(select2Options);
124 144
125 145 $("#scope_repo_id").select2({
126 146 cachedDataSource: {},
127 147 minimumInputLength: 2,
128 148 placeholder: "${_('repository scope')}",
129 149 dropdownAutoWidth: true,
130 150 containerCssClass: "drop-menu",
131 151 dropdownCssClass: "drop-menu-dropdown",
132 152 formatResult: formatResult,
133 153 query: $.debounce(250, function(query){
134 154 self = this;
135 155 var cacheKey = query.term;
136 156 var cachedData = self.cachedDataSource[cacheKey];
137 157
138 158 if (cachedData) {
139 159 query.callback({results: cachedData.results});
140 160 } else {
141 161 $.ajax({
142 162 url: pyroutes.url('repo_list_data'),
143 163 data: {'query': query.term},
144 164 dataType: 'json',
145 165 type: 'GET',
146 166 success: function(data) {
147 167 data = repoFilter(data);
148 168 self.cachedDataSource[cacheKey] = data;
149 169 query.callback({results: data.results});
150 170 },
151 171 error: function(data, textStatus, errorThrown) {
152 172 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
153 173 }
154 174 })
155 175 }
156 176 })
157 177 });
158 178
159 179 });
160 180 </script>
@@ -1,157 +1,176 b''
1 1 <div class="panel panel-default">
2 2 <div class="panel-heading">
3 3 <h3 class="panel-title">${_('Authentication Tokens')}</h3>
4 4 </div>
5 5 <div class="panel-body">
6 6 <div class="apikeys_wrap">
7 7 <p>
8 8 ${_('Each token can have a role. Token with a role can be used only in given context, '
9 9 'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
10 10 </p>
11 11 <table class="rctable auth_tokens">
12 12 <tr>
13 13 <th>${_('Token')}</th>
14 14 <th>${_('Scope')}</th>
15 15 <th>${_('Description')}</th>
16 16 <th>${_('Role')}</th>
17 17 <th>${_('Expiration')}</th>
18 18 <th>${_('Action')}</th>
19 19 </tr>
20 20 %if c.user_auth_tokens:
21 21 %for auth_token in c.user_auth_tokens:
22 22 <tr class="${'expired' if auth_token.expired else ''}">
23 23 <td class="truncate-wrap td-authtoken"><div class="user_auth_tokens truncate autoexpand"><code>${auth_token.api_key}</code></div></td>
24 24 <td class="td">${auth_token.scope_humanized}</td>
25 25 <td class="td-wrap">${auth_token.description}</td>
26 26 <td class="td-tags">
27 27 <span class="tag disabled">${auth_token.role_humanized}</span>
28 28 </td>
29 29 <td class="td-exp">
30 30 %if auth_token.expires == -1:
31 31 ${_('never')}
32 32 %else:
33 33 %if auth_token.expired:
34 34 <span style="text-decoration: line-through">${h.age_component(h.time_to_utcdatetime(auth_token.expires))}</span>
35 35 %else:
36 36 ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
37 37 %endif
38 38 %endif
39 39 </td>
40 40 <td class="td-action">
41 41 ${h.secure_form(h.route_path('edit_user_auth_tokens_delete', user_id=c.user.user_id), method='POST', request=request)}
42 42 ${h.hidden('del_auth_token', auth_token.user_api_key_id)}
43 43 <button class="btn btn-link btn-danger" type="submit"
44 44 onclick="return confirm('${_('Confirm to remove this auth token: %s') % auth_token.token_obfuscated}');">
45 45 ${_('Delete')}
46 46 </button>
47 47 ${h.end_form()}
48 48 </td>
49 49 </tr>
50 50 %endfor
51 51 %else:
52 52 <tr><td><div class="ip">${_('No additional auth tokens specified')}</div></td></tr>
53 53 %endif
54 54 </table>
55 55 </div>
56 56
57 57 <div class="user_auth_tokens">
58 58 ${h.secure_form(h.route_path('edit_user_auth_tokens_add', user_id=c.user.user_id), method='POST', request=request)}
59 59 <div class="form form-vertical">
60 60 <!-- fields -->
61 61 <div class="fields">
62 62 <div class="field">
63 63 <div class="label">
64 64 <label for="new_email">${_('New authentication token')}:</label>
65 65 </div>
66 66 <div class="input">
67 67 ${h.text('description', class_='medium', placeholder=_('Description'))}
68 ${h.select('lifetime', '', c.lifetime_options)}
68 ${h.hidden('lifetime')}
69 69 ${h.select('role', '', c.role_options)}
70 70
71 71 % if c.allow_scoped_tokens:
72 72 ${h.hidden('scope_repo_id')}
73 73 % else:
74 74 ${h.select('scope_repo_id_disabled', '', ['Scopes available in EE edition'], disabled='disabled')}
75 75 % endif
76 76 </div>
77 77 <p class="help-block">
78 78 ${_('Repository scope works only with tokens with VCS type.')}
79 79 </p>
80 80 </div>
81 81 <div class="buttons">
82 82 ${h.submit('save',_('Add'),class_="btn")}
83 83 ${h.reset('reset',_('Reset'),class_="btn")}
84 84 </div>
85 85 </div>
86 86 </div>
87 87 ${h.end_form()}
88 88 </div>
89 89 </div>
90 90 </div>
91 91
92 92 <script>
93 93
94 94 $(document).ready(function(){
95 95 var select2Options = {
96 96 'containerCssClass': "drop-menu",
97 97 'dropdownCssClass': "drop-menu-dropdown",
98 98 'dropdownAutoWidth': true
99 99 };
100 $("#lifetime").select2(select2Options);
101 100 $("#role").select2(select2Options);
102 101
102 var preloadData = {
103 results: [
104 % for entry in c.lifetime_values:
105 {id:${entry[0]}, text:"${entry[1]}"}${'' if loop.last else ','}
106 % endfor
107 ]
108 };
109
110 $("#lifetime").select2({
111 containerCssClass: "drop-menu",
112 dropdownCssClass: "drop-menu-dropdown",
113 dropdownAutoWidth: true,
114 data: preloadData,
115 placeholder: ${_('Select or enter expiration date')},
116 query: function(query) {
117 feedLifetimeOptions(query, preloadData);
118 }
119 });
120
121
103 122 var repoFilter = function(data) {
104 123 var results = [];
105 124
106 125 if (!data.results[0]) {
107 126 return data
108 127 }
109 128
110 129 $.each(data.results[0].children, function() {
111 130 // replace name to ID for submision
112 131 this.id = this.obj.repo_id;
113 132 results.push(this);
114 133 });
115 134
116 135 data.results[0].children = results;
117 136 return data;
118 137 };
119 138
120 139 $("#scope_repo_id_disabled").select2(select2Options);
121 140
122 141 $("#scope_repo_id").select2({
123 142 cachedDataSource: {},
124 143 minimumInputLength: 2,
125 144 placeholder: "${_('repository scope')}",
126 145 dropdownAutoWidth: true,
127 146 containerCssClass: "drop-menu",
128 147 dropdownCssClass: "drop-menu-dropdown",
129 148 formatResult: formatResult,
130 149 query: $.debounce(250, function(query){
131 150 self = this;
132 151 var cacheKey = query.term;
133 152 var cachedData = self.cachedDataSource[cacheKey];
134 153
135 154 if (cachedData) {
136 155 query.callback({results: cachedData.results});
137 156 } else {
138 157 $.ajax({
139 158 url: pyroutes.url('repo_list_data'),
140 159 data: {'query': query.term},
141 160 dataType: 'json',
142 161 type: 'GET',
143 162 success: function(data) {
144 163 data = repoFilter(data);
145 164 self.cachedDataSource[cacheKey] = data;
146 165 query.callback({results: data.results});
147 166 },
148 167 error: function(data, textStatus, errorThrown) {
149 168 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
150 169 }
151 170 })
152 171 }
153 172 })
154 173 });
155 174
156 175 });
157 176 </script>
General Comments 0
You need to be logged in to leave comments. Login now