##// END OF EJS Templates
fix(2fa): fixed case of imports for templates.
super-admin -
r5371:2e9ebb75 default
parent child Browse files
Show More
@@ -1,858 +1,858 b''
1 1 # Copyright (C) 2016-2023 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 import time
20 20 import logging
21 21 import datetime
22 22 import string
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
28 28
29 29 from rhodecode.apps._base import BaseAppView, DataGridAppView
30 30 from rhodecode import forms
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib import audit_logger
33 33 from rhodecode.lib import ext_json
34 34 from rhodecode.lib.auth import (
35 35 LoginRequired, NotAnonymous, CSRFRequired,
36 36 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
37 37 from rhodecode.lib.channelstream import (
38 38 channelstream_request, ChannelstreamException)
39 39 from rhodecode.lib.hash_utils import md5_safe
40 40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
41 41 from rhodecode.model.auth_token import AuthTokenModel
42 42 from rhodecode.model.comment import CommentsModel
43 43 from rhodecode.model.db import (
44 44 IntegrityError, or_, in_filter_generator, select,
45 45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 46 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
47 47 from rhodecode.model.forms import TOTPForm
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 return c
69 69
70 70 @LoginRequired()
71 71 @NotAnonymous()
72 72 def my_account_profile(self):
73 73 c = self.load_default_context()
74 74 c.active = 'profile'
75 75 c.extern_type = c.user.extern_type
76 76 return self._get_template_context(c)
77 77
78 78 @LoginRequired()
79 79 @NotAnonymous()
80 80 def my_account_edit(self):
81 81 c = self.load_default_context()
82 82 c.active = 'profile_edit'
83 83 c.extern_type = c.user.extern_type
84 84 c.extern_name = c.user.extern_name
85 85
86 86 schema = user_schema.UserProfileSchema().bind(
87 87 username=c.user.username, user_emails=c.user.emails)
88 88 appstruct = {
89 89 'username': c.user.username,
90 90 'email': c.user.email,
91 91 'firstname': c.user.firstname,
92 92 'lastname': c.user.lastname,
93 93 'description': c.user.description,
94 94 }
95 95 c.form = forms.RcForm(
96 96 schema, appstruct=appstruct,
97 97 action=h.route_path('my_account_update'),
98 98 buttons=(forms.buttons.save, forms.buttons.reset))
99 99
100 100 return self._get_template_context(c)
101 101
102 102 @LoginRequired()
103 103 @NotAnonymous()
104 104 @CSRFRequired()
105 105 def my_account_update(self):
106 106 _ = self.request.translate
107 107 c = self.load_default_context()
108 108 c.active = 'profile_edit'
109 109 c.perm_user = c.auth_user
110 110 c.extern_type = c.user.extern_type
111 111 c.extern_name = c.user.extern_name
112 112
113 113 schema = user_schema.UserProfileSchema().bind(
114 114 username=c.user.username, user_emails=c.user.emails)
115 115 form = forms.RcForm(
116 116 schema, buttons=(forms.buttons.save, forms.buttons.reset))
117 117
118 118 controls = list(self.request.POST.items())
119 119 try:
120 120 valid_data = form.validate(controls)
121 121 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
122 122 'new_password', 'password_confirmation']
123 123 if c.extern_type != "rhodecode":
124 124 # forbid updating username for external accounts
125 125 skip_attrs.append('username')
126 126 old_email = c.user.email
127 127 UserModel().update_user(
128 128 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
129 129 **valid_data)
130 130 if old_email != valid_data['email']:
131 131 old = UserEmailMap.query() \
132 132 .filter(UserEmailMap.user == c.user)\
133 133 .filter(UserEmailMap.email == valid_data['email'])\
134 134 .first()
135 135 old.email = old_email
136 136 h.flash(_('Your account was updated successfully'), category='success')
137 137 Session().commit()
138 138 except forms.ValidationFailure as e:
139 139 c.form = e
140 140 return self._get_template_context(c)
141 141
142 142 except Exception:
143 143 log.exception("Exception updating user")
144 144 h.flash(_('Error occurred during update of user'),
145 145 category='error')
146 146 raise HTTPFound(h.route_path('my_account_profile'))
147 147
148 148 @LoginRequired()
149 149 @NotAnonymous()
150 150 def my_account_password(self):
151 151 c = self.load_default_context()
152 152 c.active = 'password'
153 153 c.extern_type = c.user.extern_type
154 154
155 155 schema = user_schema.ChangePasswordSchema().bind(
156 156 username=c.user.username)
157 157
158 158 form = forms.Form(
159 159 schema,
160 160 action=h.route_path('my_account_password_update'),
161 161 buttons=(forms.buttons.save, forms.buttons.reset))
162 162
163 163 c.form = form
164 164 return self._get_template_context(c)
165 165
166 166 @LoginRequired()
167 167 @NotAnonymous()
168 168 @CSRFRequired()
169 169 def my_account_password_update(self):
170 170 _ = self.request.translate
171 171 c = self.load_default_context()
172 172 c.active = 'password'
173 173 c.extern_type = c.user.extern_type
174 174
175 175 schema = user_schema.ChangePasswordSchema().bind(
176 176 username=c.user.username)
177 177
178 178 form = forms.Form(
179 179 schema, buttons=(forms.buttons.save, forms.buttons.reset))
180 180
181 181 if c.extern_type != 'rhodecode':
182 182 raise HTTPFound(self.request.route_path('my_account_password'))
183 183
184 184 controls = list(self.request.POST.items())
185 185 try:
186 186 valid_data = form.validate(controls)
187 187 UserModel().update_user(c.user.user_id, **valid_data)
188 188 c.user.update_userdata(force_password_change=False)
189 189 Session().commit()
190 190 except forms.ValidationFailure as e:
191 191 c.form = e
192 192 return self._get_template_context(c)
193 193
194 194 except Exception:
195 195 log.exception("Exception updating password")
196 196 h.flash(_('Error occurred during update of user password'),
197 197 category='error')
198 198 else:
199 199 instance = c.auth_user.get_instance()
200 200 self.session.setdefault('rhodecode_user', {}).update(
201 201 {'password': md5_safe(instance.password)})
202 202 self.session.save()
203 203 h.flash(_("Successfully updated password"), category='success')
204 204
205 205 raise HTTPFound(self.request.route_path('my_account_password'))
206 206
207 207 @LoginRequired()
208 208 @NotAnonymous()
209 209 def my_account_2fa(self):
210 210 _ = self.request.translate
211 211 c = self.load_default_context()
212 c.active = '2FA'
212 c.active = '2fa'
213 213 user_instance = c.auth_user.get_instance()
214 214 locked_by_admin = user_instance.has_forced_2fa
215 215 c.state_of_2fa = user_instance.has_enabled_2fa
216 216 c.user_seen_2fa_recovery_codes = user_instance.has_seen_2fa_codes
217 217 c.locked_2fa = str2bool(locked_by_admin)
218 218 return self._get_template_context(c)
219 219
220 220 @LoginRequired()
221 221 @NotAnonymous()
222 222 @CSRFRequired()
223 223 def my_account_2fa_update(self):
224 224 _ = self.request.translate
225 225 c = self.load_default_context()
226 c.active = '2FA'
226 c.active = '2fa'
227 227 user_instance = c.auth_user.get_instance()
228 228
229 229 state = self.request.POST.get('2fa_status') == '1'
230 230 user_instance.has_enabled_2fa = state
231 231 user_instance.update_userdata(update_2fa=time.time())
232 232 Session().commit()
233 233 h.flash(_("Successfully saved 2FA settings"), category='success')
234 234 raise HTTPFound(self.request.route_path('my_account_configure_2fa'))
235 235
236 236 @LoginRequired()
237 237 @NotAnonymous()
238 238 @CSRFRequired()
239 239 def my_account_2fa_show_recovery_codes(self):
240 240 c = self.load_default_context()
241 241 user_instance = c.auth_user.get_instance()
242 242 user_instance.has_seen_2fa_codes = True
243 243 Session().commit()
244 244 return {'recovery_codes': user_instance.get_2fa_recovery_codes()}
245 245
246 246 @LoginRequired()
247 247 @NotAnonymous()
248 248 @CSRFRequired()
249 249 def my_account_2fa_regenerate_recovery_codes(self):
250 250 _ = self.request.translate
251 251 c = self.load_default_context()
252 252 user_instance = c.auth_user.get_instance()
253 253
254 254 totp_form = TOTPForm(_, user_instance, allow_recovery_code_use=True)()
255 255
256 256 post_items = dict(self.request.POST)
257 257 # NOTE: inject secret, as it's a post configured saved item.
258 258 post_items['secret_totp'] = user_instance.get_secret_2fa()
259 259 try:
260 260 totp_form.to_python(post_items)
261 261 user_instance.regenerate_2fa_recovery_codes()
262 262 Session().commit()
263 263 except formencode.Invalid as errors:
264 264 h.flash(_("Failed to generate new recovery codes: {}").format(errors), category='error')
265 265 raise HTTPFound(self.request.route_path('my_account_configure_2fa'))
266 266 except Exception as e:
267 267 h.flash(_("Failed to generate new recovery codes: {}").format(e), category='error')
268 268 raise HTTPFound(self.request.route_path('my_account_configure_2fa'))
269 269
270 270 raise HTTPFound(self.request.route_path('my_account_configure_2fa', _query={'show-recovery-codes': 1}))
271 271
272 272 @LoginRequired()
273 273 @NotAnonymous()
274 274 def my_account_auth_tokens(self):
275 275 _ = self.request.translate
276 276
277 277 c = self.load_default_context()
278 278 c.active = 'auth_tokens'
279 279 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
280 280 c.role_values = [
281 281 (x, AuthTokenModel.cls._get_role_name(x))
282 282 for x in AuthTokenModel.cls.ROLES]
283 283 c.role_options = [(c.role_values, _("Role"))]
284 284 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
285 285 c.user.user_id, show_expired=True)
286 286 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
287 287 return self._get_template_context(c)
288 288
289 289 @LoginRequired()
290 290 @NotAnonymous()
291 291 @CSRFRequired()
292 292 def my_account_auth_tokens_view(self):
293 293 _ = self.request.translate
294 294 c = self.load_default_context()
295 295
296 296 auth_token_id = self.request.POST.get('auth_token_id')
297 297
298 298 if auth_token_id:
299 299 token = UserApiKeys.get_or_404(auth_token_id)
300 300 if token.user.user_id != c.user.user_id:
301 301 raise HTTPNotFound()
302 302
303 303 return {
304 304 'auth_token': token.api_key
305 305 }
306 306
307 307 def maybe_attach_token_scope(self, token):
308 308 # implemented in EE edition
309 309 pass
310 310
311 311 @LoginRequired()
312 312 @NotAnonymous()
313 313 @CSRFRequired()
314 314 def my_account_auth_tokens_add(self):
315 315 _ = self.request.translate
316 316 c = self.load_default_context()
317 317
318 318 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
319 319 description = self.request.POST.get('description')
320 320 role = self.request.POST.get('role')
321 321
322 322 token = UserModel().add_auth_token(
323 323 user=c.user.user_id,
324 324 lifetime_minutes=lifetime, role=role, description=description,
325 325 scope_callback=self.maybe_attach_token_scope)
326 326 token_data = token.get_api_data()
327 327
328 328 audit_logger.store_web(
329 329 'user.edit.token.add', action_data={
330 330 'data': {'token': token_data, 'user': 'self'}},
331 331 user=self._rhodecode_user, )
332 332 Session().commit()
333 333
334 334 h.flash(_("Auth token successfully created"), category='success')
335 335 return HTTPFound(h.route_path('my_account_auth_tokens'))
336 336
337 337 @LoginRequired()
338 338 @NotAnonymous()
339 339 @CSRFRequired()
340 340 def my_account_auth_tokens_delete(self):
341 341 _ = self.request.translate
342 342 c = self.load_default_context()
343 343
344 344 del_auth_token = self.request.POST.get('del_auth_token')
345 345
346 346 if del_auth_token:
347 347 token = UserApiKeys.get_or_404(del_auth_token)
348 348 token_data = token.get_api_data()
349 349
350 350 AuthTokenModel().delete(del_auth_token, c.user.user_id)
351 351 audit_logger.store_web(
352 352 'user.edit.token.delete', action_data={
353 353 'data': {'token': token_data, 'user': 'self'}},
354 354 user=self._rhodecode_user,)
355 355 Session().commit()
356 356 h.flash(_("Auth token successfully deleted"), category='success')
357 357
358 358 return HTTPFound(h.route_path('my_account_auth_tokens'))
359 359
360 360 @LoginRequired()
361 361 @NotAnonymous()
362 362 def my_account_emails(self):
363 363 _ = self.request.translate
364 364
365 365 c = self.load_default_context()
366 366 c.active = 'emails'
367 367
368 368 c.user_email_map = UserEmailMap.query()\
369 369 .filter(UserEmailMap.user == c.user).all()
370 370
371 371 schema = user_schema.AddEmailSchema().bind(
372 372 username=c.user.username, user_emails=c.user.emails)
373 373
374 374 form = forms.RcForm(schema,
375 375 action=h.route_path('my_account_emails_add'),
376 376 buttons=(forms.buttons.save, forms.buttons.reset))
377 377
378 378 c.form = form
379 379 return self._get_template_context(c)
380 380
381 381 @LoginRequired()
382 382 @NotAnonymous()
383 383 @CSRFRequired()
384 384 def my_account_emails_add(self):
385 385 _ = self.request.translate
386 386 c = self.load_default_context()
387 387 c.active = 'emails'
388 388
389 389 schema = user_schema.AddEmailSchema().bind(
390 390 username=c.user.username, user_emails=c.user.emails)
391 391
392 392 form = forms.RcForm(
393 393 schema, action=h.route_path('my_account_emails_add'),
394 394 buttons=(forms.buttons.save, forms.buttons.reset))
395 395
396 396 controls = list(self.request.POST.items())
397 397 try:
398 398 valid_data = form.validate(controls)
399 399 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
400 400 audit_logger.store_web(
401 401 'user.edit.email.add', action_data={
402 402 'data': {'email': valid_data['email'], 'user': 'self'}},
403 403 user=self._rhodecode_user,)
404 404 Session().commit()
405 405 except formencode.Invalid as error:
406 406 h.flash(h.escape(error.error_dict['email']), category='error')
407 407 except forms.ValidationFailure as e:
408 408 c.user_email_map = UserEmailMap.query() \
409 409 .filter(UserEmailMap.user == c.user).all()
410 410 c.form = e
411 411 return self._get_template_context(c)
412 412 except Exception:
413 413 log.exception("Exception adding email")
414 414 h.flash(_('Error occurred during adding email'),
415 415 category='error')
416 416 else:
417 417 h.flash(_("Successfully added email"), category='success')
418 418
419 419 raise HTTPFound(self.request.route_path('my_account_emails'))
420 420
421 421 @LoginRequired()
422 422 @NotAnonymous()
423 423 @CSRFRequired()
424 424 def my_account_emails_delete(self):
425 425 _ = self.request.translate
426 426 c = self.load_default_context()
427 427
428 428 del_email_id = self.request.POST.get('del_email_id')
429 429 if del_email_id:
430 430 email = UserEmailMap.get_or_404(del_email_id).email
431 431 UserModel().delete_extra_email(c.user.user_id, del_email_id)
432 432 audit_logger.store_web(
433 433 'user.edit.email.delete', action_data={
434 434 'data': {'email': email, 'user': 'self'}},
435 435 user=self._rhodecode_user,)
436 436 Session().commit()
437 437 h.flash(_("Email successfully deleted"),
438 438 category='success')
439 439 return HTTPFound(h.route_path('my_account_emails'))
440 440
441 441 @LoginRequired()
442 442 @NotAnonymous()
443 443 @CSRFRequired()
444 444 def my_account_notifications_test_channelstream(self):
445 445 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
446 446 self._rhodecode_user.username, datetime.datetime.now())
447 447 payload = {
448 448 # 'channel': 'broadcast',
449 449 'type': 'message',
450 450 'timestamp': datetime.datetime.utcnow(),
451 451 'user': 'system',
452 452 'pm_users': [self._rhodecode_user.username],
453 453 'message': {
454 454 'message': message,
455 455 'level': 'info',
456 456 'topic': '/notifications'
457 457 }
458 458 }
459 459
460 460 registry = self.request.registry
461 461 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
462 462 channelstream_config = rhodecode_plugins.get('channelstream', {})
463 463
464 464 try:
465 465 channelstream_request(channelstream_config, [payload], '/message')
466 466 except ChannelstreamException as e:
467 467 log.exception('Failed to send channelstream data')
468 468 return {"response": f'ERROR: {e.__class__.__name__}'}
469 469 return {"response": 'Channelstream data sent. '
470 470 'You should see a new live message now.'}
471 471
472 472 def _load_my_repos_data(self, watched=False):
473 473
474 474 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
475 475
476 476 if watched:
477 477 # repos user watch
478 478 repo_list = Session().query(
479 479 Repository
480 480 ) \
481 481 .join(
482 482 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
483 483 ) \
484 484 .filter(
485 485 UserFollowing.user_id == self._rhodecode_user.user_id
486 486 ) \
487 487 .filter(or_(
488 488 # generate multiple IN to fix limitation problems
489 489 *in_filter_generator(Repository.repo_id, allowed_ids))
490 490 ) \
491 491 .order_by(Repository.repo_name) \
492 492 .all()
493 493
494 494 else:
495 495 # repos user is owner of
496 496 repo_list = Session().query(
497 497 Repository
498 498 ) \
499 499 .filter(
500 500 Repository.user_id == self._rhodecode_user.user_id
501 501 ) \
502 502 .filter(or_(
503 503 # generate multiple IN to fix limitation problems
504 504 *in_filter_generator(Repository.repo_id, allowed_ids))
505 505 ) \
506 506 .order_by(Repository.repo_name) \
507 507 .all()
508 508
509 509 _render = self.request.get_partial_renderer(
510 510 'rhodecode:templates/data_table/_dt_elements.mako')
511 511
512 512 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
513 513 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
514 514 short_name=False, admin=False)
515 515
516 516 repos_data = []
517 517 for repo in repo_list:
518 518 row = {
519 519 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
520 520 repo.private, repo.archived, repo.fork),
521 521 "name_raw": repo.repo_name.lower(),
522 522 }
523 523
524 524 repos_data.append(row)
525 525
526 526 # json used to render the grid
527 527 return ext_json.str_json(repos_data)
528 528
529 529 @LoginRequired()
530 530 @NotAnonymous()
531 531 def my_account_repos(self):
532 532 c = self.load_default_context()
533 533 c.active = 'repos'
534 534
535 535 # json used to render the grid
536 536 c.data = self._load_my_repos_data()
537 537 return self._get_template_context(c)
538 538
539 539 @LoginRequired()
540 540 @NotAnonymous()
541 541 def my_account_watched(self):
542 542 c = self.load_default_context()
543 543 c.active = 'watched'
544 544
545 545 # json used to render the grid
546 546 c.data = self._load_my_repos_data(watched=True)
547 547 return self._get_template_context(c)
548 548
549 549 @LoginRequired()
550 550 @NotAnonymous()
551 551 def my_account_bookmarks(self):
552 552 c = self.load_default_context()
553 553 c.active = 'bookmarks'
554 554
555 555 user_bookmarks = \
556 556 select(UserBookmark, Repository, RepoGroup) \
557 557 .where(UserBookmark.user_id == self._rhodecode_user.user_id) \
558 558 .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \
559 559 .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \
560 560 .order_by(UserBookmark.position.asc())
561 561
562 562 c.user_bookmark_items = Session().execute(user_bookmarks).all()
563 563 return self._get_template_context(c)
564 564
565 565 def _process_bookmark_entry(self, entry, user_id):
566 566 position = safe_int(entry.get('position'))
567 567 cur_position = safe_int(entry.get('cur_position'))
568 568 if position is None:
569 569 return
570 570
571 571 # check if this is an existing entry
572 572 is_new = False
573 573 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
574 574
575 575 if db_entry and str2bool(entry.get('remove')):
576 576 log.debug('Marked bookmark %s for deletion', db_entry)
577 577 Session().delete(db_entry)
578 578 return
579 579
580 580 if not db_entry:
581 581 # new
582 582 db_entry = UserBookmark()
583 583 is_new = True
584 584
585 585 should_save = False
586 586 default_redirect_url = ''
587 587
588 588 # save repo
589 589 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
590 590 repo = Repository.get(entry['bookmark_repo'])
591 591 perm_check = HasRepoPermissionAny(
592 592 'repository.read', 'repository.write', 'repository.admin')
593 593 if repo and perm_check(repo_name=repo.repo_name):
594 594 db_entry.repository = repo
595 595 should_save = True
596 596 default_redirect_url = '${repo_url}'
597 597 # save repo group
598 598 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
599 599 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
600 600 perm_check = HasRepoGroupPermissionAny(
601 601 'group.read', 'group.write', 'group.admin')
602 602
603 603 if repo_group and perm_check(group_name=repo_group.group_name):
604 604 db_entry.repository_group = repo_group
605 605 should_save = True
606 606 default_redirect_url = '${repo_group_url}'
607 607 # save generic info
608 608 elif entry.get('title') and entry.get('redirect_url'):
609 609 should_save = True
610 610
611 611 if should_save:
612 612 # mark user and position
613 613 db_entry.user_id = user_id
614 614 db_entry.position = position
615 615 db_entry.title = entry.get('title')
616 616 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
617 617 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
618 618
619 619 Session().add(db_entry)
620 620
621 621 @LoginRequired()
622 622 @NotAnonymous()
623 623 @CSRFRequired()
624 624 def my_account_bookmarks_update(self):
625 625 _ = self.request.translate
626 626 c = self.load_default_context()
627 627 c.active = 'bookmarks'
628 628
629 629 controls = peppercorn.parse(self.request.POST.items())
630 630 user_id = c.user.user_id
631 631
632 632 # validate positions
633 633 positions = {}
634 634 for entry in controls.get('bookmarks', []):
635 635 position = safe_int(entry['position'])
636 636 if position is None:
637 637 continue
638 638
639 639 if position in positions:
640 640 h.flash(_("Position {} is defined twice. "
641 641 "Please correct this error.").format(position), category='error')
642 642 return HTTPFound(h.route_path('my_account_bookmarks'))
643 643
644 644 entry['position'] = position
645 645 entry['cur_position'] = safe_int(entry.get('cur_position'))
646 646 positions[position] = entry
647 647
648 648 try:
649 649 for entry in positions.values():
650 650 self._process_bookmark_entry(entry, user_id)
651 651
652 652 Session().commit()
653 653 h.flash(_("Update Bookmarks"), category='success')
654 654 except IntegrityError:
655 655 h.flash(_("Failed to update bookmarks. "
656 656 "Make sure an unique position is used."), category='error')
657 657
658 658 return HTTPFound(h.route_path('my_account_bookmarks'))
659 659
660 660 @LoginRequired()
661 661 @NotAnonymous()
662 662 def my_account_goto_bookmark(self):
663 663
664 664 bookmark_id = self.request.matchdict['bookmark_id']
665 665 user_bookmark = UserBookmark().query()\
666 666 .filter(UserBookmark.user_id == self.request.user.user_id) \
667 667 .filter(UserBookmark.position == bookmark_id).scalar()
668 668
669 669 redirect_url = h.route_path('my_account_bookmarks')
670 670 if not user_bookmark:
671 671 raise HTTPFound(redirect_url)
672 672
673 673 # repository set
674 674 if user_bookmark.repository:
675 675 repo_name = user_bookmark.repository.repo_name
676 676 base_redirect_url = h.route_path(
677 677 'repo_summary', repo_name=repo_name)
678 678 if user_bookmark.redirect_url and \
679 679 '${repo_url}' in user_bookmark.redirect_url:
680 680 redirect_url = string.Template(user_bookmark.redirect_url)\
681 681 .safe_substitute({'repo_url': base_redirect_url})
682 682 else:
683 683 redirect_url = base_redirect_url
684 684 # repository group set
685 685 elif user_bookmark.repository_group:
686 686 repo_group_name = user_bookmark.repository_group.group_name
687 687 base_redirect_url = h.route_path(
688 688 'repo_group_home', repo_group_name=repo_group_name)
689 689 if user_bookmark.redirect_url and \
690 690 '${repo_group_url}' in user_bookmark.redirect_url:
691 691 redirect_url = string.Template(user_bookmark.redirect_url)\
692 692 .safe_substitute({'repo_group_url': base_redirect_url})
693 693 else:
694 694 redirect_url = base_redirect_url
695 695 # custom URL set
696 696 elif user_bookmark.redirect_url:
697 697 server_url = h.route_url('home').rstrip('/')
698 698 redirect_url = string.Template(user_bookmark.redirect_url) \
699 699 .safe_substitute({'server_url': server_url})
700 700
701 701 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
702 702 raise HTTPFound(redirect_url)
703 703
704 704 @LoginRequired()
705 705 @NotAnonymous()
706 706 def my_account_perms(self):
707 707 c = self.load_default_context()
708 708 c.active = 'perms'
709 709
710 710 c.perm_user = c.auth_user
711 711 return self._get_template_context(c)
712 712
713 713 @LoginRequired()
714 714 @NotAnonymous()
715 715 def my_notifications(self):
716 716 c = self.load_default_context()
717 717 c.active = 'notifications'
718 718
719 719 return self._get_template_context(c)
720 720
721 721 @LoginRequired()
722 722 @NotAnonymous()
723 723 @CSRFRequired()
724 724 def my_notifications_toggle_visibility(self):
725 725 user = self._rhodecode_db_user
726 726 new_status = not user.user_data.get('notification_status', True)
727 727 user.update_userdata(notification_status=new_status)
728 728 Session().commit()
729 729 return user.user_data['notification_status']
730 730
731 731 def _get_pull_requests_list(self, statuses, filter_type=None):
732 732 draw, start, limit = self._extract_chunk(self.request)
733 733 search_q, order_by, order_dir = self._extract_ordering(self.request)
734 734
735 735 _render = self.request.get_partial_renderer(
736 736 'rhodecode:templates/data_table/_dt_elements.mako')
737 737
738 738 if filter_type == 'awaiting_my_review':
739 739 pull_requests = PullRequestModel().get_im_participating_in_for_review(
740 740 user_id=self._rhodecode_user.user_id,
741 741 statuses=statuses, query=search_q,
742 742 offset=start, length=limit, order_by=order_by,
743 743 order_dir=order_dir)
744 744
745 745 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
746 746 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
747 747 else:
748 748 pull_requests = PullRequestModel().get_im_participating_in(
749 749 user_id=self._rhodecode_user.user_id,
750 750 statuses=statuses, query=search_q,
751 751 offset=start, length=limit, order_by=order_by,
752 752 order_dir=order_dir)
753 753
754 754 pull_requests_total_count = PullRequestModel().count_im_participating_in(
755 755 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
756 756
757 757 data = []
758 758 comments_model = CommentsModel()
759 759 for pr in pull_requests:
760 760 repo_id = pr.target_repo_id
761 761 comments_count = comments_model.get_all_comments(
762 762 repo_id, pull_request=pr, include_drafts=False, count_only=True)
763 763 owned = pr.user_id == self._rhodecode_user.user_id
764 764
765 765 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
766 766 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
767 767 if review_statuses and review_statuses[4]:
768 768 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
769 769 my_review_status = statuses[0][1].status
770 770
771 771 data.append({
772 772 'target_repo': _render('pullrequest_target_repo',
773 773 pr.target_repo.repo_name),
774 774 'name': _render('pullrequest_name',
775 775 pr.pull_request_id, pr.pull_request_state,
776 776 pr.work_in_progress, pr.target_repo.repo_name,
777 777 short=True),
778 778 'name_raw': pr.pull_request_id,
779 779 'status': _render('pullrequest_status',
780 780 pr.calculated_review_status()),
781 781 'my_status': _render('pullrequest_status',
782 782 my_review_status),
783 783 'title': _render('pullrequest_title', pr.title, pr.description),
784 784 'pr_flow': _render('pullrequest_commit_flow', pr),
785 785 'description': h.escape(pr.description),
786 786 'updated_on': _render('pullrequest_updated_on',
787 787 h.datetime_to_time(pr.updated_on),
788 788 pr.versions_count),
789 789 'updated_on_raw': h.datetime_to_time(pr.updated_on),
790 790 'created_on': _render('pullrequest_updated_on',
791 791 h.datetime_to_time(pr.created_on)),
792 792 'created_on_raw': h.datetime_to_time(pr.created_on),
793 793 'state': pr.pull_request_state,
794 794 'author': _render('pullrequest_author',
795 795 pr.author.full_contact, ),
796 796 'author_raw': pr.author.full_name,
797 797 'comments': _render('pullrequest_comments', comments_count),
798 798 'comments_raw': comments_count,
799 799 'closed': pr.is_closed(),
800 800 'owned': owned
801 801 })
802 802
803 803 # json used to render the grid
804 804 data = ({
805 805 'draw': draw,
806 806 'data': data,
807 807 'recordsTotal': pull_requests_total_count,
808 808 'recordsFiltered': pull_requests_total_count,
809 809 })
810 810 return data
811 811
812 812 @LoginRequired()
813 813 @NotAnonymous()
814 814 def my_account_pullrequests(self):
815 815 c = self.load_default_context()
816 816 c.active = 'pullrequests'
817 817 req_get = self.request.GET
818 818
819 819 c.closed = str2bool(req_get.get('closed'))
820 820 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
821 821
822 822 c.selected_filter = 'all'
823 823 if c.closed:
824 824 c.selected_filter = 'all_closed'
825 825 if c.awaiting_my_review:
826 826 c.selected_filter = 'awaiting_my_review'
827 827
828 828 return self._get_template_context(c)
829 829
830 830 @LoginRequired()
831 831 @NotAnonymous()
832 832 def my_account_pullrequests_data(self):
833 833 self.load_default_context()
834 834 req_get = self.request.GET
835 835
836 836 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
837 837 closed = str2bool(req_get.get('closed'))
838 838
839 839 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
840 840 if closed:
841 841 statuses += [PullRequest.STATUS_CLOSED]
842 842
843 843 filter_type = \
844 844 'awaiting_my_review' if awaiting_my_review \
845 845 else None
846 846
847 847 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
848 848 return data
849 849
850 850 @LoginRequired()
851 851 @NotAnonymous()
852 852 def my_account_user_group_membership(self):
853 853 c = self.load_default_context()
854 854 c.active = 'user_group_membership'
855 855 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
856 856 for group in self._rhodecode_db_user.group_member]
857 857 c.user_groups = ext_json.str_json(groups)
858 858 return self._get_template_context(c)
@@ -1,57 +1,57 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('My account')} ${c.rhodecode_user.username}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="breadcrumbs_links()">
11 11 ${_('My Account')}
12 12 </%def>
13 13
14 14 <%def name="menu_bar_nav()">
15 15 ${self.menu_items(active='my_account')}
16 16 </%def>
17 17
18 18 <%def name="main()">
19 19 <div class="box">
20 20 <div class="title">
21 21 ${self.breadcrumbs()}
22 22 </div>
23 23
24 24 <div class="sidebar-col-wrapper scw-small">
25 25 ##main
26 26 <div class="sidebar">
27 27 <ul class="nav nav-pills nav-stacked">
28 28 <li class="${h.is_active(['profile', 'profile_edit'], c.active)}"><a href="${h.route_path('my_account_profile')}">${_('Profile')}</a></li>
29 29 <li class="${h.is_active('emails', c.active)}"><a href="${h.route_path('my_account_emails')}">${_('Emails')}</a></li>
30 30 <li class="${h.is_active('password', c.active)}"><a href="${h.route_path('my_account_password')}">${_('Password')}</a></li>
31 <li class="${h.is_active('2FA', c.active)}"><a href="${h.route_path('my_account_configure_2fa')}">${_('2FA')}</a></li>
31 <li class="${h.is_active('2fa', c.active)}"><a href="${h.route_path('my_account_configure_2fa')}">${_('2FA')}</a></li>
32 32 <li class="${h.is_active('bookmarks', c.active)}"><a href="${h.route_path('my_account_bookmarks')}">${_('Bookmarks')}</a></li>
33 33 <li class="${h.is_active('auth_tokens', c.active)}"><a href="${h.route_path('my_account_auth_tokens')}">${_('Auth Tokens')}</a></li>
34 34 <li class="${h.is_active(['ssh_keys', 'ssh_keys_generate'], c.active)}"><a href="${h.route_path('my_account_ssh_keys')}">${_('SSH Keys')}</a></li>
35 35 <li class="${h.is_active('user_group_membership', c.active)}"><a href="${h.route_path('my_account_user_group_membership')}">${_('User Group Membership')}</a></li>
36 36
37 37 ## TODO: Find a better integration of oauth/saml views into navigation.
38 38 <% my_account_external_url = h.route_path_or_none('my_account_external_identity') %>
39 39 % if my_account_external_url:
40 40 <li class="${h.is_active('external_identity', c.active)}"><a href="${my_account_external_url}">${_('External Identities')}</a></li>
41 41 % endif
42 42
43 43 <li class="${h.is_active('repos', c.active)}"><a href="${h.route_path('my_account_repos')}">${_('Owned Repositories')}</a></li>
44 44 <li class="${h.is_active('watched', c.active)}"><a href="${h.route_path('my_account_watched')}">${_('Watched Repositories')}</a></li>
45 45 <li class="${h.is_active('pullrequests', c.active)}"><a href="${h.route_path('my_account_pullrequests')}">${_('Pull Requests')}</a></li>
46 46 <li class="${h.is_active('perms', c.active)}"><a href="${h.route_path('my_account_perms')}">${_('Permissions')}</a></li>
47 47 <li class="${h.is_active('my_notifications', c.active)}"><a href="${h.route_path('my_account_notifications')}">${_('Live Notifications')}</a></li>
48 48 </ul>
49 49 </div>
50 50
51 51 <div class="main-content-full-width">
52 52 <%include file="/admin/my_account/my_account_${c.active}.mako"/>
53 53 </div>
54 54 </div>
55 55 </div>
56 56
57 57 </%def>
General Comments 0
You need to be logged in to leave comments. Login now