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