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