##// END OF EJS Templates
feat(2fa): improve flash messages on 2fa settings page
super-admin -
r5372:2a8458db default
parent child Browse files
Show More
@@ -1,858 +1,861 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 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 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 h.flash(_("Successfully saved 2FA settings"), category='success')
233 if state:
234 h.flash(_("2FA has been successfully enabled"), category='success')
235 else:
236 h.flash(_("2FA has been successfully disabled"), category='success')
234 237 raise HTTPFound(self.request.route_path('my_account_configure_2fa'))
235 238
236 239 @LoginRequired()
237 240 @NotAnonymous()
238 241 @CSRFRequired()
239 242 def my_account_2fa_show_recovery_codes(self):
240 243 c = self.load_default_context()
241 244 user_instance = c.auth_user.get_instance()
242 245 user_instance.has_seen_2fa_codes = True
243 246 Session().commit()
244 247 return {'recovery_codes': user_instance.get_2fa_recovery_codes()}
245 248
246 249 @LoginRequired()
247 250 @NotAnonymous()
248 251 @CSRFRequired()
249 252 def my_account_2fa_regenerate_recovery_codes(self):
250 253 _ = self.request.translate
251 254 c = self.load_default_context()
252 255 user_instance = c.auth_user.get_instance()
253 256
254 257 totp_form = TOTPForm(_, user_instance, allow_recovery_code_use=True)()
255 258
256 259 post_items = dict(self.request.POST)
257 260 # NOTE: inject secret, as it's a post configured saved item.
258 261 post_items['secret_totp'] = user_instance.get_secret_2fa()
259 262 try:
260 263 totp_form.to_python(post_items)
261 264 user_instance.regenerate_2fa_recovery_codes()
262 265 Session().commit()
263 266 except formencode.Invalid as errors:
264 267 h.flash(_("Failed to generate new recovery codes: {}").format(errors), category='error')
265 268 raise HTTPFound(self.request.route_path('my_account_configure_2fa'))
266 269 except Exception as e:
267 270 h.flash(_("Failed to generate new recovery codes: {}").format(e), category='error')
268 271 raise HTTPFound(self.request.route_path('my_account_configure_2fa'))
269 272
270 273 raise HTTPFound(self.request.route_path('my_account_configure_2fa', _query={'show-recovery-codes': 1}))
271 274
272 275 @LoginRequired()
273 276 @NotAnonymous()
274 277 def my_account_auth_tokens(self):
275 278 _ = self.request.translate
276 279
277 280 c = self.load_default_context()
278 281 c.active = 'auth_tokens'
279 282 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
280 283 c.role_values = [
281 284 (x, AuthTokenModel.cls._get_role_name(x))
282 285 for x in AuthTokenModel.cls.ROLES]
283 286 c.role_options = [(c.role_values, _("Role"))]
284 287 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
285 288 c.user.user_id, show_expired=True)
286 289 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
287 290 return self._get_template_context(c)
288 291
289 292 @LoginRequired()
290 293 @NotAnonymous()
291 294 @CSRFRequired()
292 295 def my_account_auth_tokens_view(self):
293 296 _ = self.request.translate
294 297 c = self.load_default_context()
295 298
296 299 auth_token_id = self.request.POST.get('auth_token_id')
297 300
298 301 if auth_token_id:
299 302 token = UserApiKeys.get_or_404(auth_token_id)
300 303 if token.user.user_id != c.user.user_id:
301 304 raise HTTPNotFound()
302 305
303 306 return {
304 307 'auth_token': token.api_key
305 308 }
306 309
307 310 def maybe_attach_token_scope(self, token):
308 311 # implemented in EE edition
309 312 pass
310 313
311 314 @LoginRequired()
312 315 @NotAnonymous()
313 316 @CSRFRequired()
314 317 def my_account_auth_tokens_add(self):
315 318 _ = self.request.translate
316 319 c = self.load_default_context()
317 320
318 321 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
319 322 description = self.request.POST.get('description')
320 323 role = self.request.POST.get('role')
321 324
322 325 token = UserModel().add_auth_token(
323 326 user=c.user.user_id,
324 327 lifetime_minutes=lifetime, role=role, description=description,
325 328 scope_callback=self.maybe_attach_token_scope)
326 329 token_data = token.get_api_data()
327 330
328 331 audit_logger.store_web(
329 332 'user.edit.token.add', action_data={
330 333 'data': {'token': token_data, 'user': 'self'}},
331 334 user=self._rhodecode_user, )
332 335 Session().commit()
333 336
334 337 h.flash(_("Auth token successfully created"), category='success')
335 338 return HTTPFound(h.route_path('my_account_auth_tokens'))
336 339
337 340 @LoginRequired()
338 341 @NotAnonymous()
339 342 @CSRFRequired()
340 343 def my_account_auth_tokens_delete(self):
341 344 _ = self.request.translate
342 345 c = self.load_default_context()
343 346
344 347 del_auth_token = self.request.POST.get('del_auth_token')
345 348
346 349 if del_auth_token:
347 350 token = UserApiKeys.get_or_404(del_auth_token)
348 351 token_data = token.get_api_data()
349 352
350 353 AuthTokenModel().delete(del_auth_token, c.user.user_id)
351 354 audit_logger.store_web(
352 355 'user.edit.token.delete', action_data={
353 356 'data': {'token': token_data, 'user': 'self'}},
354 357 user=self._rhodecode_user,)
355 358 Session().commit()
356 359 h.flash(_("Auth token successfully deleted"), category='success')
357 360
358 361 return HTTPFound(h.route_path('my_account_auth_tokens'))
359 362
360 363 @LoginRequired()
361 364 @NotAnonymous()
362 365 def my_account_emails(self):
363 366 _ = self.request.translate
364 367
365 368 c = self.load_default_context()
366 369 c.active = 'emails'
367 370
368 371 c.user_email_map = UserEmailMap.query()\
369 372 .filter(UserEmailMap.user == c.user).all()
370 373
371 374 schema = user_schema.AddEmailSchema().bind(
372 375 username=c.user.username, user_emails=c.user.emails)
373 376
374 377 form = forms.RcForm(schema,
375 378 action=h.route_path('my_account_emails_add'),
376 379 buttons=(forms.buttons.save, forms.buttons.reset))
377 380
378 381 c.form = form
379 382 return self._get_template_context(c)
380 383
381 384 @LoginRequired()
382 385 @NotAnonymous()
383 386 @CSRFRequired()
384 387 def my_account_emails_add(self):
385 388 _ = self.request.translate
386 389 c = self.load_default_context()
387 390 c.active = 'emails'
388 391
389 392 schema = user_schema.AddEmailSchema().bind(
390 393 username=c.user.username, user_emails=c.user.emails)
391 394
392 395 form = forms.RcForm(
393 396 schema, action=h.route_path('my_account_emails_add'),
394 397 buttons=(forms.buttons.save, forms.buttons.reset))
395 398
396 399 controls = list(self.request.POST.items())
397 400 try:
398 401 valid_data = form.validate(controls)
399 402 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
400 403 audit_logger.store_web(
401 404 'user.edit.email.add', action_data={
402 405 'data': {'email': valid_data['email'], 'user': 'self'}},
403 406 user=self._rhodecode_user,)
404 407 Session().commit()
405 408 except formencode.Invalid as error:
406 409 h.flash(h.escape(error.error_dict['email']), category='error')
407 410 except forms.ValidationFailure as e:
408 411 c.user_email_map = UserEmailMap.query() \
409 412 .filter(UserEmailMap.user == c.user).all()
410 413 c.form = e
411 414 return self._get_template_context(c)
412 415 except Exception:
413 416 log.exception("Exception adding email")
414 417 h.flash(_('Error occurred during adding email'),
415 418 category='error')
416 419 else:
417 420 h.flash(_("Successfully added email"), category='success')
418 421
419 422 raise HTTPFound(self.request.route_path('my_account_emails'))
420 423
421 424 @LoginRequired()
422 425 @NotAnonymous()
423 426 @CSRFRequired()
424 427 def my_account_emails_delete(self):
425 428 _ = self.request.translate
426 429 c = self.load_default_context()
427 430
428 431 del_email_id = self.request.POST.get('del_email_id')
429 432 if del_email_id:
430 433 email = UserEmailMap.get_or_404(del_email_id).email
431 434 UserModel().delete_extra_email(c.user.user_id, del_email_id)
432 435 audit_logger.store_web(
433 436 'user.edit.email.delete', action_data={
434 437 'data': {'email': email, 'user': 'self'}},
435 438 user=self._rhodecode_user,)
436 439 Session().commit()
437 440 h.flash(_("Email successfully deleted"),
438 441 category='success')
439 442 return HTTPFound(h.route_path('my_account_emails'))
440 443
441 444 @LoginRequired()
442 445 @NotAnonymous()
443 446 @CSRFRequired()
444 447 def my_account_notifications_test_channelstream(self):
445 448 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
446 449 self._rhodecode_user.username, datetime.datetime.now())
447 450 payload = {
448 451 # 'channel': 'broadcast',
449 452 'type': 'message',
450 453 'timestamp': datetime.datetime.utcnow(),
451 454 'user': 'system',
452 455 'pm_users': [self._rhodecode_user.username],
453 456 'message': {
454 457 'message': message,
455 458 'level': 'info',
456 459 'topic': '/notifications'
457 460 }
458 461 }
459 462
460 463 registry = self.request.registry
461 464 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
462 465 channelstream_config = rhodecode_plugins.get('channelstream', {})
463 466
464 467 try:
465 468 channelstream_request(channelstream_config, [payload], '/message')
466 469 except ChannelstreamException as e:
467 470 log.exception('Failed to send channelstream data')
468 471 return {"response": f'ERROR: {e.__class__.__name__}'}
469 472 return {"response": 'Channelstream data sent. '
470 473 'You should see a new live message now.'}
471 474
472 475 def _load_my_repos_data(self, watched=False):
473 476
474 477 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
475 478
476 479 if watched:
477 480 # repos user watch
478 481 repo_list = Session().query(
479 482 Repository
480 483 ) \
481 484 .join(
482 485 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
483 486 ) \
484 487 .filter(
485 488 UserFollowing.user_id == self._rhodecode_user.user_id
486 489 ) \
487 490 .filter(or_(
488 491 # generate multiple IN to fix limitation problems
489 492 *in_filter_generator(Repository.repo_id, allowed_ids))
490 493 ) \
491 494 .order_by(Repository.repo_name) \
492 495 .all()
493 496
494 497 else:
495 498 # repos user is owner of
496 499 repo_list = Session().query(
497 500 Repository
498 501 ) \
499 502 .filter(
500 503 Repository.user_id == self._rhodecode_user.user_id
501 504 ) \
502 505 .filter(or_(
503 506 # generate multiple IN to fix limitation problems
504 507 *in_filter_generator(Repository.repo_id, allowed_ids))
505 508 ) \
506 509 .order_by(Repository.repo_name) \
507 510 .all()
508 511
509 512 _render = self.request.get_partial_renderer(
510 513 'rhodecode:templates/data_table/_dt_elements.mako')
511 514
512 515 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
513 516 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
514 517 short_name=False, admin=False)
515 518
516 519 repos_data = []
517 520 for repo in repo_list:
518 521 row = {
519 522 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
520 523 repo.private, repo.archived, repo.fork),
521 524 "name_raw": repo.repo_name.lower(),
522 525 }
523 526
524 527 repos_data.append(row)
525 528
526 529 # json used to render the grid
527 530 return ext_json.str_json(repos_data)
528 531
529 532 @LoginRequired()
530 533 @NotAnonymous()
531 534 def my_account_repos(self):
532 535 c = self.load_default_context()
533 536 c.active = 'repos'
534 537
535 538 # json used to render the grid
536 539 c.data = self._load_my_repos_data()
537 540 return self._get_template_context(c)
538 541
539 542 @LoginRequired()
540 543 @NotAnonymous()
541 544 def my_account_watched(self):
542 545 c = self.load_default_context()
543 546 c.active = 'watched'
544 547
545 548 # json used to render the grid
546 549 c.data = self._load_my_repos_data(watched=True)
547 550 return self._get_template_context(c)
548 551
549 552 @LoginRequired()
550 553 @NotAnonymous()
551 554 def my_account_bookmarks(self):
552 555 c = self.load_default_context()
553 556 c.active = 'bookmarks'
554 557
555 558 user_bookmarks = \
556 559 select(UserBookmark, Repository, RepoGroup) \
557 560 .where(UserBookmark.user_id == self._rhodecode_user.user_id) \
558 561 .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \
559 562 .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \
560 563 .order_by(UserBookmark.position.asc())
561 564
562 565 c.user_bookmark_items = Session().execute(user_bookmarks).all()
563 566 return self._get_template_context(c)
564 567
565 568 def _process_bookmark_entry(self, entry, user_id):
566 569 position = safe_int(entry.get('position'))
567 570 cur_position = safe_int(entry.get('cur_position'))
568 571 if position is None:
569 572 return
570 573
571 574 # check if this is an existing entry
572 575 is_new = False
573 576 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
574 577
575 578 if db_entry and str2bool(entry.get('remove')):
576 579 log.debug('Marked bookmark %s for deletion', db_entry)
577 580 Session().delete(db_entry)
578 581 return
579 582
580 583 if not db_entry:
581 584 # new
582 585 db_entry = UserBookmark()
583 586 is_new = True
584 587
585 588 should_save = False
586 589 default_redirect_url = ''
587 590
588 591 # save repo
589 592 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
590 593 repo = Repository.get(entry['bookmark_repo'])
591 594 perm_check = HasRepoPermissionAny(
592 595 'repository.read', 'repository.write', 'repository.admin')
593 596 if repo and perm_check(repo_name=repo.repo_name):
594 597 db_entry.repository = repo
595 598 should_save = True
596 599 default_redirect_url = '${repo_url}'
597 600 # save repo group
598 601 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
599 602 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
600 603 perm_check = HasRepoGroupPermissionAny(
601 604 'group.read', 'group.write', 'group.admin')
602 605
603 606 if repo_group and perm_check(group_name=repo_group.group_name):
604 607 db_entry.repository_group = repo_group
605 608 should_save = True
606 609 default_redirect_url = '${repo_group_url}'
607 610 # save generic info
608 611 elif entry.get('title') and entry.get('redirect_url'):
609 612 should_save = True
610 613
611 614 if should_save:
612 615 # mark user and position
613 616 db_entry.user_id = user_id
614 617 db_entry.position = position
615 618 db_entry.title = entry.get('title')
616 619 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
617 620 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
618 621
619 622 Session().add(db_entry)
620 623
621 624 @LoginRequired()
622 625 @NotAnonymous()
623 626 @CSRFRequired()
624 627 def my_account_bookmarks_update(self):
625 628 _ = self.request.translate
626 629 c = self.load_default_context()
627 630 c.active = 'bookmarks'
628 631
629 632 controls = peppercorn.parse(self.request.POST.items())
630 633 user_id = c.user.user_id
631 634
632 635 # validate positions
633 636 positions = {}
634 637 for entry in controls.get('bookmarks', []):
635 638 position = safe_int(entry['position'])
636 639 if position is None:
637 640 continue
638 641
639 642 if position in positions:
640 643 h.flash(_("Position {} is defined twice. "
641 644 "Please correct this error.").format(position), category='error')
642 645 return HTTPFound(h.route_path('my_account_bookmarks'))
643 646
644 647 entry['position'] = position
645 648 entry['cur_position'] = safe_int(entry.get('cur_position'))
646 649 positions[position] = entry
647 650
648 651 try:
649 652 for entry in positions.values():
650 653 self._process_bookmark_entry(entry, user_id)
651 654
652 655 Session().commit()
653 656 h.flash(_("Update Bookmarks"), category='success')
654 657 except IntegrityError:
655 658 h.flash(_("Failed to update bookmarks. "
656 659 "Make sure an unique position is used."), category='error')
657 660
658 661 return HTTPFound(h.route_path('my_account_bookmarks'))
659 662
660 663 @LoginRequired()
661 664 @NotAnonymous()
662 665 def my_account_goto_bookmark(self):
663 666
664 667 bookmark_id = self.request.matchdict['bookmark_id']
665 668 user_bookmark = UserBookmark().query()\
666 669 .filter(UserBookmark.user_id == self.request.user.user_id) \
667 670 .filter(UserBookmark.position == bookmark_id).scalar()
668 671
669 672 redirect_url = h.route_path('my_account_bookmarks')
670 673 if not user_bookmark:
671 674 raise HTTPFound(redirect_url)
672 675
673 676 # repository set
674 677 if user_bookmark.repository:
675 678 repo_name = user_bookmark.repository.repo_name
676 679 base_redirect_url = h.route_path(
677 680 'repo_summary', repo_name=repo_name)
678 681 if user_bookmark.redirect_url and \
679 682 '${repo_url}' in user_bookmark.redirect_url:
680 683 redirect_url = string.Template(user_bookmark.redirect_url)\
681 684 .safe_substitute({'repo_url': base_redirect_url})
682 685 else:
683 686 redirect_url = base_redirect_url
684 687 # repository group set
685 688 elif user_bookmark.repository_group:
686 689 repo_group_name = user_bookmark.repository_group.group_name
687 690 base_redirect_url = h.route_path(
688 691 'repo_group_home', repo_group_name=repo_group_name)
689 692 if user_bookmark.redirect_url and \
690 693 '${repo_group_url}' in user_bookmark.redirect_url:
691 694 redirect_url = string.Template(user_bookmark.redirect_url)\
692 695 .safe_substitute({'repo_group_url': base_redirect_url})
693 696 else:
694 697 redirect_url = base_redirect_url
695 698 # custom URL set
696 699 elif user_bookmark.redirect_url:
697 700 server_url = h.route_url('home').rstrip('/')
698 701 redirect_url = string.Template(user_bookmark.redirect_url) \
699 702 .safe_substitute({'server_url': server_url})
700 703
701 704 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
702 705 raise HTTPFound(redirect_url)
703 706
704 707 @LoginRequired()
705 708 @NotAnonymous()
706 709 def my_account_perms(self):
707 710 c = self.load_default_context()
708 711 c.active = 'perms'
709 712
710 713 c.perm_user = c.auth_user
711 714 return self._get_template_context(c)
712 715
713 716 @LoginRequired()
714 717 @NotAnonymous()
715 718 def my_notifications(self):
716 719 c = self.load_default_context()
717 720 c.active = 'notifications'
718 721
719 722 return self._get_template_context(c)
720 723
721 724 @LoginRequired()
722 725 @NotAnonymous()
723 726 @CSRFRequired()
724 727 def my_notifications_toggle_visibility(self):
725 728 user = self._rhodecode_db_user
726 729 new_status = not user.user_data.get('notification_status', True)
727 730 user.update_userdata(notification_status=new_status)
728 731 Session().commit()
729 732 return user.user_data['notification_status']
730 733
731 734 def _get_pull_requests_list(self, statuses, filter_type=None):
732 735 draw, start, limit = self._extract_chunk(self.request)
733 736 search_q, order_by, order_dir = self._extract_ordering(self.request)
734 737
735 738 _render = self.request.get_partial_renderer(
736 739 'rhodecode:templates/data_table/_dt_elements.mako')
737 740
738 741 if filter_type == 'awaiting_my_review':
739 742 pull_requests = PullRequestModel().get_im_participating_in_for_review(
740 743 user_id=self._rhodecode_user.user_id,
741 744 statuses=statuses, query=search_q,
742 745 offset=start, length=limit, order_by=order_by,
743 746 order_dir=order_dir)
744 747
745 748 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
746 749 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
747 750 else:
748 751 pull_requests = PullRequestModel().get_im_participating_in(
749 752 user_id=self._rhodecode_user.user_id,
750 753 statuses=statuses, query=search_q,
751 754 offset=start, length=limit, order_by=order_by,
752 755 order_dir=order_dir)
753 756
754 757 pull_requests_total_count = PullRequestModel().count_im_participating_in(
755 758 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
756 759
757 760 data = []
758 761 comments_model = CommentsModel()
759 762 for pr in pull_requests:
760 763 repo_id = pr.target_repo_id
761 764 comments_count = comments_model.get_all_comments(
762 765 repo_id, pull_request=pr, include_drafts=False, count_only=True)
763 766 owned = pr.user_id == self._rhodecode_user.user_id
764 767
765 768 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
766 769 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
767 770 if review_statuses and review_statuses[4]:
768 771 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
769 772 my_review_status = statuses[0][1].status
770 773
771 774 data.append({
772 775 'target_repo': _render('pullrequest_target_repo',
773 776 pr.target_repo.repo_name),
774 777 'name': _render('pullrequest_name',
775 778 pr.pull_request_id, pr.pull_request_state,
776 779 pr.work_in_progress, pr.target_repo.repo_name,
777 780 short=True),
778 781 'name_raw': pr.pull_request_id,
779 782 'status': _render('pullrequest_status',
780 783 pr.calculated_review_status()),
781 784 'my_status': _render('pullrequest_status',
782 785 my_review_status),
783 786 'title': _render('pullrequest_title', pr.title, pr.description),
784 787 'pr_flow': _render('pullrequest_commit_flow', pr),
785 788 'description': h.escape(pr.description),
786 789 'updated_on': _render('pullrequest_updated_on',
787 790 h.datetime_to_time(pr.updated_on),
788 791 pr.versions_count),
789 792 'updated_on_raw': h.datetime_to_time(pr.updated_on),
790 793 'created_on': _render('pullrequest_updated_on',
791 794 h.datetime_to_time(pr.created_on)),
792 795 'created_on_raw': h.datetime_to_time(pr.created_on),
793 796 'state': pr.pull_request_state,
794 797 'author': _render('pullrequest_author',
795 798 pr.author.full_contact, ),
796 799 'author_raw': pr.author.full_name,
797 800 'comments': _render('pullrequest_comments', comments_count),
798 801 'comments_raw': comments_count,
799 802 'closed': pr.is_closed(),
800 803 'owned': owned
801 804 })
802 805
803 806 # json used to render the grid
804 807 data = ({
805 808 'draw': draw,
806 809 'data': data,
807 810 'recordsTotal': pull_requests_total_count,
808 811 'recordsFiltered': pull_requests_total_count,
809 812 })
810 813 return data
811 814
812 815 @LoginRequired()
813 816 @NotAnonymous()
814 817 def my_account_pullrequests(self):
815 818 c = self.load_default_context()
816 819 c.active = 'pullrequests'
817 820 req_get = self.request.GET
818 821
819 822 c.closed = str2bool(req_get.get('closed'))
820 823 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
821 824
822 825 c.selected_filter = 'all'
823 826 if c.closed:
824 827 c.selected_filter = 'all_closed'
825 828 if c.awaiting_my_review:
826 829 c.selected_filter = 'awaiting_my_review'
827 830
828 831 return self._get_template_context(c)
829 832
830 833 @LoginRequired()
831 834 @NotAnonymous()
832 835 def my_account_pullrequests_data(self):
833 836 self.load_default_context()
834 837 req_get = self.request.GET
835 838
836 839 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
837 840 closed = str2bool(req_get.get('closed'))
838 841
839 842 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
840 843 if closed:
841 844 statuses += [PullRequest.STATUS_CLOSED]
842 845
843 846 filter_type = \
844 847 'awaiting_my_review' if awaiting_my_review \
845 848 else None
846 849
847 850 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
848 851 return data
849 852
850 853 @LoginRequired()
851 854 @NotAnonymous()
852 855 def my_account_user_group_membership(self):
853 856 c = self.load_default_context()
854 857 c.active = 'user_group_membership'
855 858 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
856 859 for group in self._rhodecode_db_user.group_member]
857 860 c.user_groups = ext_json.str_json(groups)
858 861 return self._get_template_context(c)
General Comments 0
You need to be logged in to leave comments. Login now