##// END OF EJS Templates
user-accounts: show info about password for external accounts.
marcink -
r4240:86414242 stable
parent child Browse files
Show More
@@ -1,800 +1,801 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 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 string
24 24
25 25 import formencode
26 26 import formencode.htmlfill
27 27 import peppercorn
28 28 from pyramid.httpexceptions import HTTPFound
29 29 from pyramid.view import view_config
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 (
37 37 LoginRequired, NotAnonymous, CSRFRequired,
38 38 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
39 39 from rhodecode.lib.channelstream import (
40 40 channelstream_request, ChannelstreamException)
41 41 from rhodecode.lib.utils2 import safe_int, md5, str2bool
42 42 from rhodecode.model.auth_token import AuthTokenModel
43 43 from rhodecode.model.comment import CommentsModel
44 44 from rhodecode.model.db import (
45 45 IntegrityError, or_, in_filter_generator,
46 46 Repository, UserEmailMap, UserApiKeys, UserFollowing,
47 47 PullRequest, UserBookmark, RepoGroup)
48 48 from rhodecode.model.meta import Session
49 49 from rhodecode.model.pull_request import PullRequestModel
50 50 from rhodecode.model.user import UserModel
51 51 from rhodecode.model.user_group import UserGroupModel
52 52 from rhodecode.model.validation_schema.schemas import user_schema
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 class MyAccountView(BaseAppView, DataGridAppView):
58 58 ALLOW_SCOPED_TOKENS = False
59 59 """
60 60 This view has alternative version inside EE, if modified please take a look
61 61 in there as well.
62 62 """
63 63
64 64 def load_default_context(self):
65 65 c = self._get_local_tmpl_context()
66 66 c.user = c.auth_user.get_instance()
67 67 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
68 68
69 69 return c
70 70
71 71 @LoginRequired()
72 72 @NotAnonymous()
73 73 @view_config(
74 74 route_name='my_account_profile', request_method='GET',
75 75 renderer='rhodecode:templates/admin/my_account/my_account.mako')
76 76 def my_account_profile(self):
77 77 c = self.load_default_context()
78 78 c.active = 'profile'
79 c.extern_type = c.user.extern_type
79 80 return self._get_template_context(c)
80 81
81 82 @LoginRequired()
82 83 @NotAnonymous()
83 84 @view_config(
84 85 route_name='my_account_password', request_method='GET',
85 86 renderer='rhodecode:templates/admin/my_account/my_account.mako')
86 87 def my_account_password(self):
87 88 c = self.load_default_context()
88 89 c.active = 'password'
89 90 c.extern_type = c.user.extern_type
90 91
91 92 schema = user_schema.ChangePasswordSchema().bind(
92 93 username=c.user.username)
93 94
94 95 form = forms.Form(
95 96 schema,
96 97 action=h.route_path('my_account_password_update'),
97 98 buttons=(forms.buttons.save, forms.buttons.reset))
98 99
99 100 c.form = form
100 101 return self._get_template_context(c)
101 102
102 103 @LoginRequired()
103 104 @NotAnonymous()
104 105 @CSRFRequired()
105 106 @view_config(
106 107 route_name='my_account_password_update', request_method='POST',
107 108 renderer='rhodecode:templates/admin/my_account/my_account.mako')
108 109 def my_account_password_update(self):
109 110 _ = self.request.translate
110 111 c = self.load_default_context()
111 112 c.active = 'password'
112 113 c.extern_type = c.user.extern_type
113 114
114 115 schema = user_schema.ChangePasswordSchema().bind(
115 116 username=c.user.username)
116 117
117 118 form = forms.Form(
118 119 schema, buttons=(forms.buttons.save, forms.buttons.reset))
119 120
120 121 if c.extern_type != 'rhodecode':
121 122 raise HTTPFound(self.request.route_path('my_account_password'))
122 123
123 124 controls = self.request.POST.items()
124 125 try:
125 126 valid_data = form.validate(controls)
126 127 UserModel().update_user(c.user.user_id, **valid_data)
127 128 c.user.update_userdata(force_password_change=False)
128 129 Session().commit()
129 130 except forms.ValidationFailure as e:
130 131 c.form = e
131 132 return self._get_template_context(c)
132 133
133 134 except Exception:
134 135 log.exception("Exception updating password")
135 136 h.flash(_('Error occurred during update of user password'),
136 137 category='error')
137 138 else:
138 139 instance = c.auth_user.get_instance()
139 140 self.session.setdefault('rhodecode_user', {}).update(
140 141 {'password': md5(instance.password)})
141 142 self.session.save()
142 143 h.flash(_("Successfully updated password"), category='success')
143 144
144 145 raise HTTPFound(self.request.route_path('my_account_password'))
145 146
146 147 @LoginRequired()
147 148 @NotAnonymous()
148 149 @view_config(
149 150 route_name='my_account_auth_tokens', request_method='GET',
150 151 renderer='rhodecode:templates/admin/my_account/my_account.mako')
151 152 def my_account_auth_tokens(self):
152 153 _ = self.request.translate
153 154
154 155 c = self.load_default_context()
155 156 c.active = 'auth_tokens'
156 157 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
157 158 c.role_values = [
158 159 (x, AuthTokenModel.cls._get_role_name(x))
159 160 for x in AuthTokenModel.cls.ROLES]
160 161 c.role_options = [(c.role_values, _("Role"))]
161 162 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
162 163 c.user.user_id, show_expired=True)
163 164 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
164 165 return self._get_template_context(c)
165 166
166 167 def maybe_attach_token_scope(self, token):
167 168 # implemented in EE edition
168 169 pass
169 170
170 171 @LoginRequired()
171 172 @NotAnonymous()
172 173 @CSRFRequired()
173 174 @view_config(
174 175 route_name='my_account_auth_tokens_add', request_method='POST',)
175 176 def my_account_auth_tokens_add(self):
176 177 _ = self.request.translate
177 178 c = self.load_default_context()
178 179
179 180 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
180 181 description = self.request.POST.get('description')
181 182 role = self.request.POST.get('role')
182 183
183 184 token = UserModel().add_auth_token(
184 185 user=c.user.user_id,
185 186 lifetime_minutes=lifetime, role=role, description=description,
186 187 scope_callback=self.maybe_attach_token_scope)
187 188 token_data = token.get_api_data()
188 189
189 190 audit_logger.store_web(
190 191 'user.edit.token.add', action_data={
191 192 'data': {'token': token_data, 'user': 'self'}},
192 193 user=self._rhodecode_user, )
193 194 Session().commit()
194 195
195 196 h.flash(_("Auth token successfully created"), category='success')
196 197 return HTTPFound(h.route_path('my_account_auth_tokens'))
197 198
198 199 @LoginRequired()
199 200 @NotAnonymous()
200 201 @CSRFRequired()
201 202 @view_config(
202 203 route_name='my_account_auth_tokens_delete', request_method='POST')
203 204 def my_account_auth_tokens_delete(self):
204 205 _ = self.request.translate
205 206 c = self.load_default_context()
206 207
207 208 del_auth_token = self.request.POST.get('del_auth_token')
208 209
209 210 if del_auth_token:
210 211 token = UserApiKeys.get_or_404(del_auth_token)
211 212 token_data = token.get_api_data()
212 213
213 214 AuthTokenModel().delete(del_auth_token, c.user.user_id)
214 215 audit_logger.store_web(
215 216 'user.edit.token.delete', action_data={
216 217 'data': {'token': token_data, 'user': 'self'}},
217 218 user=self._rhodecode_user,)
218 219 Session().commit()
219 220 h.flash(_("Auth token successfully deleted"), category='success')
220 221
221 222 return HTTPFound(h.route_path('my_account_auth_tokens'))
222 223
223 224 @LoginRequired()
224 225 @NotAnonymous()
225 226 @view_config(
226 227 route_name='my_account_emails', request_method='GET',
227 228 renderer='rhodecode:templates/admin/my_account/my_account.mako')
228 229 def my_account_emails(self):
229 230 _ = self.request.translate
230 231
231 232 c = self.load_default_context()
232 233 c.active = 'emails'
233 234
234 235 c.user_email_map = UserEmailMap.query()\
235 236 .filter(UserEmailMap.user == c.user).all()
236 237
237 238 schema = user_schema.AddEmailSchema().bind(
238 239 username=c.user.username, user_emails=c.user.emails)
239 240
240 241 form = forms.RcForm(schema,
241 242 action=h.route_path('my_account_emails_add'),
242 243 buttons=(forms.buttons.save, forms.buttons.reset))
243 244
244 245 c.form = form
245 246 return self._get_template_context(c)
246 247
247 248 @LoginRequired()
248 249 @NotAnonymous()
249 250 @CSRFRequired()
250 251 @view_config(
251 252 route_name='my_account_emails_add', request_method='POST',
252 253 renderer='rhodecode:templates/admin/my_account/my_account.mako')
253 254 def my_account_emails_add(self):
254 255 _ = self.request.translate
255 256 c = self.load_default_context()
256 257 c.active = 'emails'
257 258
258 259 schema = user_schema.AddEmailSchema().bind(
259 260 username=c.user.username, user_emails=c.user.emails)
260 261
261 262 form = forms.RcForm(
262 263 schema, action=h.route_path('my_account_emails_add'),
263 264 buttons=(forms.buttons.save, forms.buttons.reset))
264 265
265 266 controls = self.request.POST.items()
266 267 try:
267 268 valid_data = form.validate(controls)
268 269 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
269 270 audit_logger.store_web(
270 271 'user.edit.email.add', action_data={
271 272 'data': {'email': valid_data['email'], 'user': 'self'}},
272 273 user=self._rhodecode_user,)
273 274 Session().commit()
274 275 except formencode.Invalid as error:
275 276 h.flash(h.escape(error.error_dict['email']), category='error')
276 277 except forms.ValidationFailure as e:
277 278 c.user_email_map = UserEmailMap.query() \
278 279 .filter(UserEmailMap.user == c.user).all()
279 280 c.form = e
280 281 return self._get_template_context(c)
281 282 except Exception:
282 283 log.exception("Exception adding email")
283 284 h.flash(_('Error occurred during adding email'),
284 285 category='error')
285 286 else:
286 287 h.flash(_("Successfully added email"), category='success')
287 288
288 289 raise HTTPFound(self.request.route_path('my_account_emails'))
289 290
290 291 @LoginRequired()
291 292 @NotAnonymous()
292 293 @CSRFRequired()
293 294 @view_config(
294 295 route_name='my_account_emails_delete', request_method='POST')
295 296 def my_account_emails_delete(self):
296 297 _ = self.request.translate
297 298 c = self.load_default_context()
298 299
299 300 del_email_id = self.request.POST.get('del_email_id')
300 301 if del_email_id:
301 302 email = UserEmailMap.get_or_404(del_email_id).email
302 303 UserModel().delete_extra_email(c.user.user_id, del_email_id)
303 304 audit_logger.store_web(
304 305 'user.edit.email.delete', action_data={
305 306 'data': {'email': email, 'user': 'self'}},
306 307 user=self._rhodecode_user,)
307 308 Session().commit()
308 309 h.flash(_("Email successfully deleted"),
309 310 category='success')
310 311 return HTTPFound(h.route_path('my_account_emails'))
311 312
312 313 @LoginRequired()
313 314 @NotAnonymous()
314 315 @CSRFRequired()
315 316 @view_config(
316 317 route_name='my_account_notifications_test_channelstream',
317 318 request_method='POST', renderer='json_ext')
318 319 def my_account_notifications_test_channelstream(self):
319 320 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
320 321 self._rhodecode_user.username, datetime.datetime.now())
321 322 payload = {
322 323 # 'channel': 'broadcast',
323 324 'type': 'message',
324 325 'timestamp': datetime.datetime.utcnow(),
325 326 'user': 'system',
326 327 'pm_users': [self._rhodecode_user.username],
327 328 'message': {
328 329 'message': message,
329 330 'level': 'info',
330 331 'topic': '/notifications'
331 332 }
332 333 }
333 334
334 335 registry = self.request.registry
335 336 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
336 337 channelstream_config = rhodecode_plugins.get('channelstream', {})
337 338
338 339 try:
339 340 channelstream_request(channelstream_config, [payload], '/message')
340 341 except ChannelstreamException as e:
341 342 log.exception('Failed to send channelstream data')
342 343 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
343 344 return {"response": 'Channelstream data sent. '
344 345 'You should see a new live message now.'}
345 346
346 347 def _load_my_repos_data(self, watched=False):
347 348
348 349 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
349 350
350 351 if watched:
351 352 # repos user watch
352 353 repo_list = Session().query(
353 354 Repository
354 355 ) \
355 356 .join(
356 357 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
357 358 ) \
358 359 .filter(
359 360 UserFollowing.user_id == self._rhodecode_user.user_id
360 361 ) \
361 362 .filter(or_(
362 363 # generate multiple IN to fix limitation problems
363 364 *in_filter_generator(Repository.repo_id, allowed_ids))
364 365 ) \
365 366 .order_by(Repository.repo_name) \
366 367 .all()
367 368
368 369 else:
369 370 # repos user is owner of
370 371 repo_list = Session().query(
371 372 Repository
372 373 ) \
373 374 .filter(
374 375 Repository.user_id == self._rhodecode_user.user_id
375 376 ) \
376 377 .filter(or_(
377 378 # generate multiple IN to fix limitation problems
378 379 *in_filter_generator(Repository.repo_id, allowed_ids))
379 380 ) \
380 381 .order_by(Repository.repo_name) \
381 382 .all()
382 383
383 384 _render = self.request.get_partial_renderer(
384 385 'rhodecode:templates/data_table/_dt_elements.mako')
385 386
386 387 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
387 388 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
388 389 short_name=False, admin=False)
389 390
390 391 repos_data = []
391 392 for repo in repo_list:
392 393 row = {
393 394 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
394 395 repo.private, repo.archived, repo.fork),
395 396 "name_raw": repo.repo_name.lower(),
396 397 }
397 398
398 399 repos_data.append(row)
399 400
400 401 # json used to render the grid
401 402 return json.dumps(repos_data)
402 403
403 404 @LoginRequired()
404 405 @NotAnonymous()
405 406 @view_config(
406 407 route_name='my_account_repos', request_method='GET',
407 408 renderer='rhodecode:templates/admin/my_account/my_account.mako')
408 409 def my_account_repos(self):
409 410 c = self.load_default_context()
410 411 c.active = 'repos'
411 412
412 413 # json used to render the grid
413 414 c.data = self._load_my_repos_data()
414 415 return self._get_template_context(c)
415 416
416 417 @LoginRequired()
417 418 @NotAnonymous()
418 419 @view_config(
419 420 route_name='my_account_watched', request_method='GET',
420 421 renderer='rhodecode:templates/admin/my_account/my_account.mako')
421 422 def my_account_watched(self):
422 423 c = self.load_default_context()
423 424 c.active = 'watched'
424 425
425 426 # json used to render the grid
426 427 c.data = self._load_my_repos_data(watched=True)
427 428 return self._get_template_context(c)
428 429
429 430 @LoginRequired()
430 431 @NotAnonymous()
431 432 @view_config(
432 433 route_name='my_account_bookmarks', request_method='GET',
433 434 renderer='rhodecode:templates/admin/my_account/my_account.mako')
434 435 def my_account_bookmarks(self):
435 436 c = self.load_default_context()
436 437 c.active = 'bookmarks'
437 438 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
438 439 self._rhodecode_db_user.user_id, cache=False)
439 440 return self._get_template_context(c)
440 441
441 442 def _process_bookmark_entry(self, entry, user_id):
442 443 position = safe_int(entry.get('position'))
443 444 cur_position = safe_int(entry.get('cur_position'))
444 445 if position is None:
445 446 return
446 447
447 448 # check if this is an existing entry
448 449 is_new = False
449 450 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
450 451
451 452 if db_entry and str2bool(entry.get('remove')):
452 453 log.debug('Marked bookmark %s for deletion', db_entry)
453 454 Session().delete(db_entry)
454 455 return
455 456
456 457 if not db_entry:
457 458 # new
458 459 db_entry = UserBookmark()
459 460 is_new = True
460 461
461 462 should_save = False
462 463 default_redirect_url = ''
463 464
464 465 # save repo
465 466 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
466 467 repo = Repository.get(entry['bookmark_repo'])
467 468 perm_check = HasRepoPermissionAny(
468 469 'repository.read', 'repository.write', 'repository.admin')
469 470 if repo and perm_check(repo_name=repo.repo_name):
470 471 db_entry.repository = repo
471 472 should_save = True
472 473 default_redirect_url = '${repo_url}'
473 474 # save repo group
474 475 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
475 476 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
476 477 perm_check = HasRepoGroupPermissionAny(
477 478 'group.read', 'group.write', 'group.admin')
478 479
479 480 if repo_group and perm_check(group_name=repo_group.group_name):
480 481 db_entry.repository_group = repo_group
481 482 should_save = True
482 483 default_redirect_url = '${repo_group_url}'
483 484 # save generic info
484 485 elif entry.get('title') and entry.get('redirect_url'):
485 486 should_save = True
486 487
487 488 if should_save:
488 489 # mark user and position
489 490 db_entry.user_id = user_id
490 491 db_entry.position = position
491 492 db_entry.title = entry.get('title')
492 493 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
493 494 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
494 495
495 496 Session().add(db_entry)
496 497
497 498 @LoginRequired()
498 499 @NotAnonymous()
499 500 @CSRFRequired()
500 501 @view_config(
501 502 route_name='my_account_bookmarks_update', request_method='POST')
502 503 def my_account_bookmarks_update(self):
503 504 _ = self.request.translate
504 505 c = self.load_default_context()
505 506 c.active = 'bookmarks'
506 507
507 508 controls = peppercorn.parse(self.request.POST.items())
508 509 user_id = c.user.user_id
509 510
510 511 # validate positions
511 512 positions = {}
512 513 for entry in controls.get('bookmarks', []):
513 514 position = safe_int(entry['position'])
514 515 if position is None:
515 516 continue
516 517
517 518 if position in positions:
518 519 h.flash(_("Position {} is defined twice. "
519 520 "Please correct this error.").format(position), category='error')
520 521 return HTTPFound(h.route_path('my_account_bookmarks'))
521 522
522 523 entry['position'] = position
523 524 entry['cur_position'] = safe_int(entry.get('cur_position'))
524 525 positions[position] = entry
525 526
526 527 try:
527 528 for entry in positions.values():
528 529 self._process_bookmark_entry(entry, user_id)
529 530
530 531 Session().commit()
531 532 h.flash(_("Update Bookmarks"), category='success')
532 533 except IntegrityError:
533 534 h.flash(_("Failed to update bookmarks. "
534 535 "Make sure an unique position is used."), category='error')
535 536
536 537 return HTTPFound(h.route_path('my_account_bookmarks'))
537 538
538 539 @LoginRequired()
539 540 @NotAnonymous()
540 541 @view_config(
541 542 route_name='my_account_goto_bookmark', request_method='GET',
542 543 renderer='rhodecode:templates/admin/my_account/my_account.mako')
543 544 def my_account_goto_bookmark(self):
544 545
545 546 bookmark_id = self.request.matchdict['bookmark_id']
546 547 user_bookmark = UserBookmark().query()\
547 548 .filter(UserBookmark.user_id == self.request.user.user_id) \
548 549 .filter(UserBookmark.position == bookmark_id).scalar()
549 550
550 551 redirect_url = h.route_path('my_account_bookmarks')
551 552 if not user_bookmark:
552 553 raise HTTPFound(redirect_url)
553 554
554 555 # repository set
555 556 if user_bookmark.repository:
556 557 repo_name = user_bookmark.repository.repo_name
557 558 base_redirect_url = h.route_path(
558 559 'repo_summary', repo_name=repo_name)
559 560 if user_bookmark.redirect_url and \
560 561 '${repo_url}' in user_bookmark.redirect_url:
561 562 redirect_url = string.Template(user_bookmark.redirect_url)\
562 563 .safe_substitute({'repo_url': base_redirect_url})
563 564 else:
564 565 redirect_url = base_redirect_url
565 566 # repository group set
566 567 elif user_bookmark.repository_group:
567 568 repo_group_name = user_bookmark.repository_group.group_name
568 569 base_redirect_url = h.route_path(
569 570 'repo_group_home', repo_group_name=repo_group_name)
570 571 if user_bookmark.redirect_url and \
571 572 '${repo_group_url}' in user_bookmark.redirect_url:
572 573 redirect_url = string.Template(user_bookmark.redirect_url)\
573 574 .safe_substitute({'repo_group_url': base_redirect_url})
574 575 else:
575 576 redirect_url = base_redirect_url
576 577 # custom URL set
577 578 elif user_bookmark.redirect_url:
578 579 server_url = h.route_url('home').rstrip('/')
579 580 redirect_url = string.Template(user_bookmark.redirect_url) \
580 581 .safe_substitute({'server_url': server_url})
581 582
582 583 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
583 584 raise HTTPFound(redirect_url)
584 585
585 586 @LoginRequired()
586 587 @NotAnonymous()
587 588 @view_config(
588 589 route_name='my_account_perms', request_method='GET',
589 590 renderer='rhodecode:templates/admin/my_account/my_account.mako')
590 591 def my_account_perms(self):
591 592 c = self.load_default_context()
592 593 c.active = 'perms'
593 594
594 595 c.perm_user = c.auth_user
595 596 return self._get_template_context(c)
596 597
597 598 @LoginRequired()
598 599 @NotAnonymous()
599 600 @view_config(
600 601 route_name='my_account_notifications', request_method='GET',
601 602 renderer='rhodecode:templates/admin/my_account/my_account.mako')
602 603 def my_notifications(self):
603 604 c = self.load_default_context()
604 605 c.active = 'notifications'
605 606
606 607 return self._get_template_context(c)
607 608
608 609 @LoginRequired()
609 610 @NotAnonymous()
610 611 @CSRFRequired()
611 612 @view_config(
612 613 route_name='my_account_notifications_toggle_visibility',
613 614 request_method='POST', renderer='json_ext')
614 615 def my_notifications_toggle_visibility(self):
615 616 user = self._rhodecode_db_user
616 617 new_status = not user.user_data.get('notification_status', True)
617 618 user.update_userdata(notification_status=new_status)
618 619 Session().commit()
619 620 return user.user_data['notification_status']
620 621
621 622 @LoginRequired()
622 623 @NotAnonymous()
623 624 @view_config(
624 625 route_name='my_account_edit',
625 626 request_method='GET',
626 627 renderer='rhodecode:templates/admin/my_account/my_account.mako')
627 628 def my_account_edit(self):
628 629 c = self.load_default_context()
629 630 c.active = 'profile_edit'
630 631 c.extern_type = c.user.extern_type
631 632 c.extern_name = c.user.extern_name
632 633
633 634 schema = user_schema.UserProfileSchema().bind(
634 635 username=c.user.username, user_emails=c.user.emails)
635 636 appstruct = {
636 637 'username': c.user.username,
637 638 'email': c.user.email,
638 639 'firstname': c.user.firstname,
639 640 'lastname': c.user.lastname,
640 641 'description': c.user.description,
641 642 }
642 643 c.form = forms.RcForm(
643 644 schema, appstruct=appstruct,
644 645 action=h.route_path('my_account_update'),
645 646 buttons=(forms.buttons.save, forms.buttons.reset))
646 647
647 648 return self._get_template_context(c)
648 649
649 650 @LoginRequired()
650 651 @NotAnonymous()
651 652 @CSRFRequired()
652 653 @view_config(
653 654 route_name='my_account_update',
654 655 request_method='POST',
655 656 renderer='rhodecode:templates/admin/my_account/my_account.mako')
656 657 def my_account_update(self):
657 658 _ = self.request.translate
658 659 c = self.load_default_context()
659 660 c.active = 'profile_edit'
660 661 c.perm_user = c.auth_user
661 662 c.extern_type = c.user.extern_type
662 663 c.extern_name = c.user.extern_name
663 664
664 665 schema = user_schema.UserProfileSchema().bind(
665 666 username=c.user.username, user_emails=c.user.emails)
666 667 form = forms.RcForm(
667 668 schema, buttons=(forms.buttons.save, forms.buttons.reset))
668 669
669 670 controls = self.request.POST.items()
670 671 try:
671 672 valid_data = form.validate(controls)
672 673 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
673 674 'new_password', 'password_confirmation']
674 675 if c.extern_type != "rhodecode":
675 676 # forbid updating username for external accounts
676 677 skip_attrs.append('username')
677 678 old_email = c.user.email
678 679 UserModel().update_user(
679 680 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
680 681 **valid_data)
681 682 if old_email != valid_data['email']:
682 683 old = UserEmailMap.query() \
683 684 .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first()
684 685 old.email = old_email
685 686 h.flash(_('Your account was updated successfully'), category='success')
686 687 Session().commit()
687 688 except forms.ValidationFailure as e:
688 689 c.form = e
689 690 return self._get_template_context(c)
690 691 except Exception:
691 692 log.exception("Exception updating user")
692 693 h.flash(_('Error occurred during update of user'),
693 694 category='error')
694 695 raise HTTPFound(h.route_path('my_account_profile'))
695 696
696 697 def _get_pull_requests_list(self, statuses):
697 698 draw, start, limit = self._extract_chunk(self.request)
698 699 search_q, order_by, order_dir = self._extract_ordering(self.request)
699 700 _render = self.request.get_partial_renderer(
700 701 'rhodecode:templates/data_table/_dt_elements.mako')
701 702
702 703 pull_requests = PullRequestModel().get_im_participating_in(
703 704 user_id=self._rhodecode_user.user_id,
704 705 statuses=statuses,
705 706 offset=start, length=limit, order_by=order_by,
706 707 order_dir=order_dir)
707 708
708 709 pull_requests_total_count = PullRequestModel().count_im_participating_in(
709 710 user_id=self._rhodecode_user.user_id, statuses=statuses)
710 711
711 712 data = []
712 713 comments_model = CommentsModel()
713 714 for pr in pull_requests:
714 715 repo_id = pr.target_repo_id
715 716 comments = comments_model.get_all_comments(
716 717 repo_id, pull_request=pr)
717 718 owned = pr.user_id == self._rhodecode_user.user_id
718 719
719 720 data.append({
720 721 'target_repo': _render('pullrequest_target_repo',
721 722 pr.target_repo.repo_name),
722 723 'name': _render('pullrequest_name',
723 724 pr.pull_request_id, pr.pull_request_state,
724 725 pr.work_in_progress, pr.target_repo.repo_name,
725 726 short=True),
726 727 'name_raw': pr.pull_request_id,
727 728 'status': _render('pullrequest_status',
728 729 pr.calculated_review_status()),
729 730 'title': _render('pullrequest_title', pr.title, pr.description),
730 731 'description': h.escape(pr.description),
731 732 'updated_on': _render('pullrequest_updated_on',
732 733 h.datetime_to_time(pr.updated_on)),
733 734 'updated_on_raw': h.datetime_to_time(pr.updated_on),
734 735 'created_on': _render('pullrequest_updated_on',
735 736 h.datetime_to_time(pr.created_on)),
736 737 'created_on_raw': h.datetime_to_time(pr.created_on),
737 738 'state': pr.pull_request_state,
738 739 'author': _render('pullrequest_author',
739 740 pr.author.full_contact, ),
740 741 'author_raw': pr.author.full_name,
741 742 'comments': _render('pullrequest_comments', len(comments)),
742 743 'comments_raw': len(comments),
743 744 'closed': pr.is_closed(),
744 745 'owned': owned
745 746 })
746 747
747 748 # json used to render the grid
748 749 data = ({
749 750 'draw': draw,
750 751 'data': data,
751 752 'recordsTotal': pull_requests_total_count,
752 753 'recordsFiltered': pull_requests_total_count,
753 754 })
754 755 return data
755 756
756 757 @LoginRequired()
757 758 @NotAnonymous()
758 759 @view_config(
759 760 route_name='my_account_pullrequests',
760 761 request_method='GET',
761 762 renderer='rhodecode:templates/admin/my_account/my_account.mako')
762 763 def my_account_pullrequests(self):
763 764 c = self.load_default_context()
764 765 c.active = 'pullrequests'
765 766 req_get = self.request.GET
766 767
767 768 c.closed = str2bool(req_get.get('pr_show_closed'))
768 769
769 770 return self._get_template_context(c)
770 771
771 772 @LoginRequired()
772 773 @NotAnonymous()
773 774 @view_config(
774 775 route_name='my_account_pullrequests_data',
775 776 request_method='GET', renderer='json_ext')
776 777 def my_account_pullrequests_data(self):
777 778 self.load_default_context()
778 779 req_get = self.request.GET
779 780 closed = str2bool(req_get.get('closed'))
780 781
781 782 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
782 783 if closed:
783 784 statuses += [PullRequest.STATUS_CLOSED]
784 785
785 786 data = self._get_pull_requests_list(statuses=statuses)
786 787 return data
787 788
788 789 @LoginRequired()
789 790 @NotAnonymous()
790 791 @view_config(
791 792 route_name='my_account_user_group_membership',
792 793 request_method='GET',
793 794 renderer='rhodecode:templates/admin/my_account/my_account.mako')
794 795 def my_account_user_group_membership(self):
795 796 c = self.load_default_context()
796 797 c.active = 'user_group_membership'
797 798 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
798 799 for group in self._rhodecode_db_user.group_member]
799 800 c.user_groups = json.dumps(groups)
800 801 return self._get_template_context(c)
@@ -1,190 +1,190 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 ${_('Authentication tokens can be used to interact with the API, or VCS-over-http. '
9 9 'Each token can have a role. Token with a role can be used only in given context, '
10 10 'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
11 11 </p>
12 12 <table class="rctable auth_tokens">
13 13 <tr>
14 14 <th>${_('Token')}</th>
15 15 <th>${_('Description')}</th>
16 16 <th>${_('Role')}</th>
17 17 <th>${_('Repository Scope')}</th>
18 18 <th>${_('Expiration')}</th>
19 19 <th>${_('Action')}</th>
20 20 </tr>
21 21 %if c.user_auth_tokens:
22 22 %for auth_token in c.user_auth_tokens:
23 23 <tr class="${('expired' if auth_token.expired else '')}">
24 24 <td class="truncate-wrap td-authtoken">
25 25 <div class="user_auth_tokens truncate autoexpand">
26 26 <code>${auth_token.api_key}</code>
27 27 </div>
28 28 </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">${auth_token.scope_humanized}</td>
34 34 <td class="td-exp">
35 35 %if auth_token.expires == -1:
36 36 ${_('never')}
37 37 %else:
38 38 %if auth_token.expired:
39 39 <span style="text-decoration: line-through">${h.age_component(h.time_to_utcdatetime(auth_token.expires))}</span>
40 40 %else:
41 41 ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
42 42 %endif
43 43 %endif
44 44 </td>
45 45 <td class="td-action">
46 46 ${h.secure_form(h.route_path('my_account_auth_tokens_delete'), request=request)}
47 47 ${h.hidden('del_auth_token', auth_token.user_api_key_id)}
48 48 <button class="btn btn-link btn-danger" type="submit"
49 49 onclick="return confirm('${_('Confirm to remove this auth token: %s') % auth_token.token_obfuscated}');">
50 50 ${_('Delete')}
51 51 </button>
52 52 ${h.end_form()}
53 53 </td>
54 54 </tr>
55 55 %endfor
56 56 %else:
57 57 <tr><td><div class="ip">${_('No additional auth tokens specified')}</div></td></tr>
58 58 %endif
59 59 </table>
60 60 </div>
61 61
62 62 <div class="user_auth_tokens">
63 63 ${h.secure_form(h.route_path('my_account_auth_tokens_add'), request=request)}
64 64 <div class="form form-vertical">
65 65 <!-- fields -->
66 66 <div class="fields">
67 67 <div class="field">
68 68 <div class="label">
69 69 <label for="new_email">${_('New authentication token')}:</label>
70 70 </div>
71 71 <div class="input">
72 72 ${h.text('description', class_='medium', placeholder=_('Description'))}
73 73 ${h.hidden('lifetime')}
74 ${h.select('role', '', c.role_options)}
74 ${h.select('role', request.GET.get('token_role', ''), c.role_options)}
75 75
76 76 % if c.allow_scoped_tokens:
77 77 ${h.hidden('scope_repo_id')}
78 78 % else:
79 79 ${h.select('scope_repo_id_disabled', '', ['Scopes available in EE edition'], disabled='disabled')}
80 80 % endif
81 81 </div>
82 82 <p class="help-block">
83 83 ${_('Repository scope works only with tokens with VCS type.')}
84 84 </p>
85 85 </div>
86 86 <div class="buttons">
87 87 ${h.submit('save',_('Add'),class_="btn")}
88 88 ${h.reset('reset',_('Reset'),class_="btn")}
89 89 </div>
90 90 </div>
91 91 </div>
92 92 ${h.end_form()}
93 93 </div>
94 94 </div>
95 95 </div>
96 96
97 97 <script>
98 98 $(document).ready(function(){
99 99
100 100 var select2Options = {
101 101 'containerCssClass': "drop-menu",
102 102 'dropdownCssClass': "drop-menu-dropdown",
103 103 'dropdownAutoWidth': true
104 104 };
105 105 $("#role").select2(select2Options);
106 106
107 107 var preloadData = {
108 108 results: [
109 109 % for entry in c.lifetime_values:
110 110 {id:${entry[0]}, text:"${entry[1]}"}${'' if loop.last else ','}
111 111 % endfor
112 112 ]
113 113 };
114 114
115 115 $("#lifetime").select2({
116 116 containerCssClass: "drop-menu",
117 117 dropdownCssClass: "drop-menu-dropdown",
118 118 dropdownAutoWidth: true,
119 119 data: preloadData,
120 120 placeholder: "${_('Select or enter expiration date')}",
121 121 query: function(query) {
122 122 feedLifetimeOptions(query, preloadData);
123 123 }
124 124 });
125 125
126 126
127 127 var repoFilter = function(data) {
128 128 var results = [];
129 129
130 130 if (!data.results[0]) {
131 131 return data
132 132 }
133 133
134 134 $.each(data.results[0].children, function() {
135 135 // replace name to ID for submision
136 136 this.id = this.repo_id;
137 137 results.push(this);
138 138 });
139 139
140 140 data.results[0].children = results;
141 141 return data;
142 142 };
143 143
144 144 $("#scope_repo_id_disabled").select2(select2Options);
145 145
146 146 var selectVcsScope = function() {
147 147 // select vcs scope and disable input
148 148 $("#role").select2("val", "${c.role_vcs}").trigger('change');
149 149 $("#role").select2("readonly", true)
150 150 };
151 151
152 152 $("#scope_repo_id").select2({
153 153 cachedDataSource: {},
154 154 minimumInputLength: 2,
155 155 placeholder: "${_('repository scope')}",
156 156 dropdownAutoWidth: true,
157 157 containerCssClass: "drop-menu",
158 158 dropdownCssClass: "drop-menu-dropdown",
159 159 formatResult: formatRepoResult,
160 160 query: $.debounce(250, function(query){
161 161 self = this;
162 162 var cacheKey = query.term;
163 163 var cachedData = self.cachedDataSource[cacheKey];
164 164
165 165 if (cachedData) {
166 166 query.callback({results: cachedData.results});
167 167 } else {
168 168 $.ajax({
169 169 url: pyroutes.url('repo_list_data'),
170 170 data: {'query': query.term},
171 171 dataType: 'json',
172 172 type: 'GET',
173 173 success: function(data) {
174 174 data = repoFilter(data);
175 175 self.cachedDataSource[cacheKey] = data;
176 176 query.callback({results: data.results});
177 177 },
178 178 error: function(data, textStatus, errorThrown) {
179 179 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
180 180 }
181 181 })
182 182 }
183 183 })
184 184 });
185 185 $("#scope_repo_id").on('select2-selecting', function(e){
186 186 selectVcsScope()
187 187 });
188 188
189 189 });
190 190 </script>
@@ -1,13 +1,15 b''
1 1 <%namespace name="widgets" file="/widgets.mako"/>
2 2
3 3 <%widgets:panel title="${_('Change Your Account Password')}">
4 4
5 5 % if c.extern_type != 'rhodecode':
6 6 <p>${_('Your user account details are managed by an external source. Details cannot be managed here.')}
7 <br/>${_('For VCS access please generate')}
8 <a href="${h.route_path('my_account_auth_tokens', _query={'token_role':'token_role_vcs'})}">Authentication Token</a> or <a href="${h.route_path('my_account_ssh_keys_generate')}">SSH Key</a>.
7 9 <br/>${_('Source type')}: <strong>${c.extern_type}</strong>
8 10 </p>
9 11 % else:
10 12 ${c.form.render() | n}
11 13 % endif
12 14
13 15 </%widgets:panel>
@@ -1,76 +1,87 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
3 3
4 4 <div class="panel panel-default user-profile">
5 5 <div class="panel-heading">
6 6 <h3 class="panel-title">${_('My Profile')}</h3>
7 7 <a href="${h.route_path('my_account_edit')}" class="panel-edit">${_('Edit')}</a>
8 8 </div>
9 9
10 10 <div class="panel-body fields">
11 %if c.extern_type != 'rhodecode':
12 <% readonly = "readonly" %>
13 <% disabled = " disabled" %>
14 <div class="alert-warning" style="margin:0px 0px 20px 0px; padding: 10px">
15 <strong>${_('This user was created from external source (%s). Editing some of the settings is limited.' % c.extern_type)}</strong>
16 </div>
17 <div style="margin:-10px 0px 20px 0px;">
18 ${_('For VCS access please generate')}
19 <a href="${h.route_path('my_account_auth_tokens', _query={'token_role':'token_role_vcs'})}">Authentication Token</a> or <a href="${h.route_path('my_account_ssh_keys_generate')}">SSH Key</a>.
20 </div>
21 %endif
11 22 <div class="field">
12 23 <div class="label">
13 24 ${_('Photo')}:
14 25 </div>
15 26 <div class="input">
16 27 <div class="text-as-placeholder">
17 28 %if c.visual.use_gravatar:
18 29 ${base.gravatar(c.user.email, 100)}
19 30 %else:
20 31 ${base.gravatar(c.user.email, 100)}
21 32 %endif
22 33 </div>
23 34 </div>
24 35 </div>
25 36 <div class="field">
26 37 <div class="label">
27 38 ${_('Username')}:
28 39 </div>
29 40 <div class="input">
30 41 <div class="text-as-placeholder">
31 42 ${c.user.username}
32 43 </div>
33 44 </div>
34 45 </div>
35 46 <div class="field">
36 47 <div class="label">
37 48 ${_('First Name')}:
38 49 </div>
39 50 <div class="input">
40 51 <div class="text-as-placeholder">
41 52 ${c.user.first_name}
42 53 </div>
43 54 </div>
44 55 </div>
45 56 <div class="field">
46 57 <div class="label">
47 58 ${_('Last Name')}:
48 59 </div>
49 60 <div class="input">
50 61 <div class="text-as-placeholder">
51 62 ${c.user.last_name}
52 63 </div>
53 64 </div>
54 65 </div>
55 66 <div class="field">
56 67 <div class="label">
57 68 ${_('Description')}:
58 69 </div>
59 70 <div class="input">
60 71 <div class="text-as-placeholder">
61 72 ${dt.render_description(c.user.description, c.visual.stylify_metatags)}
62 73 </div>
63 74 </div>
64 75 </div>
65 76 <div class="field">
66 77 <div class="label">
67 78 ${_('Email')}:
68 79 </div>
69 80 <div class="input">
70 81 <div class="text-as-placeholder">
71 82 ${c.user.email or _('Missing email, please update your user email address.')}
72 83 </div>
73 84 </div>
74 85 </div>
75 86 </div>
76 87 </div> No newline at end of file
@@ -1,70 +1,76 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2 <div class="panel panel-default user-profile">
3 3 <div class="panel-heading">
4 4 <h3 class="panel-title">${_('My Profile')}</h3>
5 5 <a href="${h.route_path('my_account_profile')}" class="panel-edit">Close</a>
6 6 </div>
7 7
8 8 <div class="panel-body">
9 9 <% readonly = None %>
10 10 <% disabled = "" %>
11 11
12 12 %if c.extern_type != 'rhodecode':
13 13 <% readonly = "readonly" %>
14 14 <% disabled = "disabled" %>
15 <div class="alert-warning" style="margin:0px 0px 20px 0px; padding: 10px">
16 <strong>${_('This user was created from external source (%s). Editing some of the settings is limited.' % c.extern_type)}</strong>
17 </div>
18 <div style="margin:-10px 0px 20px 0px;">
19 ${_('For VCS access please generate')}
20 <a href="${h.route_path('my_account_auth_tokens', _query={'token_role':'token_role_vcs'})}">Authentication Token</a> or <a href="${h.route_path('my_account_ssh_keys_generate')}">SSH Key</a>.
21 </div>
22 %endif
23
24 %if c.extern_type != 'rhodecode':
15 25 <div class="infoform">
16 26 <div class="fields">
17 <p>${_('Your user account details are managed by an external source. Details cannot be managed here.')}
18 <br/>${_('Source type')}: <strong>${c.extern_type}</strong>
19 </p>
20
21 27 <div class="field">
22 28 <div class="label">
23 29 <label for="username">${_('Username')}:</label>
24 30 </div>
25 31 <div class="input">
26 32 ${c.user.username}
27 33 </div>
28 34 </div>
29 35
30 36 <div class="field">
31 37 <div class="label">
32 38 <label for="name">${_('First Name')}:</label>
33 39 </div>
34 40 <div class="input">
35 41 ${c.user.firstname}
36 42 </div>
37 43 </div>
38 44
39 45 <div class="field">
40 46 <div class="label">
41 47 <label for="lastname">${_('Last Name')}:</label>
42 48 </div>
43 49 <div class="input-valuedisplay">
44 50 ${c.user.lastname}
45 51 </div>
46 52 </div>
47 53 </div>
48 54 </div>
49 55 % else:
50 56 <div class="form">
51 57 <div class="fields">
52 58 <div class="field">
53 59 <div class="label photo">
54 60 ${_('Photo')}:
55 61 </div>
56 62 <div class="input profile">
57 63 %if c.visual.use_gravatar:
58 64 ${base.gravatar(c.user.email, 100)}
59 65 <p class="help-block">${_('Change your avatar at')} <a href="http://gravatar.com">gravatar.com</a>.</p>
60 66 %else:
61 67 ${base.gravatar(c.user.email, 100)}
62 68 %endif
63 69 </div>
64 70 </div>
65 71 ${c.form.render()| n}
66 72 </div>
67 73 </div>
68 74 % endif
69 75 </div>
70 76 </div> No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now