##// END OF EJS Templates
notifications: replace toggle button with actual toggle element - fixes #4171
ergo -
r692:cea437a2 default
parent child Browse files
Show More
@@ -1,371 +1,371 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 my account controller for RhodeCode admin
24 24 """
25 25
26 26 import logging
27 27
28 28 import formencode
29 29 from formencode import htmlfill
30 30 from pylons import request, tmpl_context as c, url, session
31 31 from pylons.controllers.util import redirect
32 32 from pylons.i18n.translation import _
33 33 from sqlalchemy.orm import joinedload
34 34
35 35 from rhodecode import forms
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.lib import auth
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
40 40 from rhodecode.lib.base import BaseController, render
41 41 from rhodecode.lib.utils2 import safe_int, md5
42 42 from rhodecode.lib.ext_json import json
43 43
44 44 from rhodecode.model.validation_schema.schemas import user_schema
45 45 from rhodecode.model.db import (
46 46 Repository, PullRequest, PullRequestReviewers, UserEmailMap, User,
47 47 UserFollowing)
48 48 from rhodecode.model.forms import UserForm
49 49 from rhodecode.model.scm import RepoList
50 50 from rhodecode.model.user import UserModel
51 51 from rhodecode.model.repo import RepoModel
52 52 from rhodecode.model.auth_token import AuthTokenModel
53 53 from rhodecode.model.meta import Session
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class MyAccountController(BaseController):
59 59 """REST Controller styled on the Atom Publishing Protocol"""
60 60 # To properly map this controller, ensure your config/routing.py
61 61 # file has a resource setup:
62 62 # map.resource('setting', 'settings', controller='admin/settings',
63 63 # path_prefix='/admin', name_prefix='admin_')
64 64
65 65 @LoginRequired()
66 66 @NotAnonymous()
67 67 def __before__(self):
68 68 super(MyAccountController, self).__before__()
69 69
70 70 def __load_data(self):
71 71 c.user = User.get(c.rhodecode_user.user_id)
72 72 if c.user.username == User.DEFAULT_USER:
73 73 h.flash(_("You can't edit this user since it's"
74 74 " crucial for entire application"), category='warning')
75 75 return redirect(url('users'))
76 76
77 77 def _load_my_repos_data(self, watched=False):
78 78 if watched:
79 79 admin = False
80 80 follows_repos = Session().query(UserFollowing)\
81 81 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
82 82 .options(joinedload(UserFollowing.follows_repository))\
83 83 .all()
84 84 repo_list = [x.follows_repository for x in follows_repos]
85 85 else:
86 86 admin = True
87 87 repo_list = Repository.get_all_repos(
88 88 user_id=c.rhodecode_user.user_id)
89 89 repo_list = RepoList(repo_list, perm_set=[
90 90 'repository.read', 'repository.write', 'repository.admin'])
91 91
92 92 repos_data = RepoModel().get_repos_as_dict(
93 93 repo_list=repo_list, admin=admin)
94 94 # json used to render the grid
95 95 return json.dumps(repos_data)
96 96
97 97 @auth.CSRFRequired()
98 98 def my_account_update(self):
99 99 """
100 100 POST /_admin/my_account Updates info of my account
101 101 """
102 102 # url('my_account')
103 103 c.active = 'profile_edit'
104 104 self.__load_data()
105 105 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
106 106 ip_addr=self.ip_addr)
107 107 c.extern_type = c.user.extern_type
108 108 c.extern_name = c.user.extern_name
109 109
110 110 defaults = c.user.get_dict()
111 111 update = False
112 112 _form = UserForm(edit=True,
113 113 old_data={'user_id': c.rhodecode_user.user_id,
114 114 'email': c.rhodecode_user.email})()
115 115 form_result = {}
116 116 try:
117 117 post_data = dict(request.POST)
118 118 post_data['new_password'] = ''
119 119 post_data['password_confirmation'] = ''
120 120 form_result = _form.to_python(post_data)
121 121 # skip updating those attrs for my account
122 122 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
123 123 'new_password', 'password_confirmation']
124 124 # TODO: plugin should define if username can be updated
125 125 if c.extern_type != "rhodecode":
126 126 # forbid updating username for external accounts
127 127 skip_attrs.append('username')
128 128
129 129 UserModel().update_user(
130 130 c.rhodecode_user.user_id, skip_attrs=skip_attrs, **form_result)
131 131 h.flash(_('Your account was updated successfully'),
132 132 category='success')
133 133 Session().commit()
134 134 update = True
135 135
136 136 except formencode.Invalid as errors:
137 137 return htmlfill.render(
138 138 render('admin/my_account/my_account.html'),
139 139 defaults=errors.value,
140 140 errors=errors.error_dict or {},
141 141 prefix_error=False,
142 142 encoding="UTF-8",
143 143 force_defaults=False)
144 144 except Exception:
145 145 log.exception("Exception updating user")
146 146 h.flash(_('Error occurred during update of user %s')
147 147 % form_result.get('username'), category='error')
148 148
149 149 if update:
150 150 return redirect('my_account')
151 151
152 152 return htmlfill.render(
153 153 render('admin/my_account/my_account.html'),
154 154 defaults=defaults,
155 155 encoding="UTF-8",
156 156 force_defaults=False
157 157 )
158 158
159 159 def my_account(self):
160 160 """
161 161 GET /_admin/my_account Displays info about my account
162 162 """
163 163 # url('my_account')
164 164 c.active = 'profile'
165 165 self.__load_data()
166 166
167 167 defaults = c.user.get_dict()
168 168 return htmlfill.render(
169 169 render('admin/my_account/my_account.html'),
170 170 defaults=defaults, encoding="UTF-8", force_defaults=False)
171 171
172 172 def my_account_edit(self):
173 173 """
174 174 GET /_admin/my_account/edit Displays edit form of my account
175 175 """
176 176 c.active = 'profile_edit'
177 177 self.__load_data()
178 178 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
179 179 ip_addr=self.ip_addr)
180 180 c.extern_type = c.user.extern_type
181 181 c.extern_name = c.user.extern_name
182 182
183 183 defaults = c.user.get_dict()
184 184 return htmlfill.render(
185 185 render('admin/my_account/my_account.html'),
186 186 defaults=defaults,
187 187 encoding="UTF-8",
188 188 force_defaults=False
189 189 )
190 190
191 191 @auth.CSRFRequired(except_methods=['GET'])
192 192 def my_account_password(self):
193 193 c.active = 'password'
194 194 self.__load_data()
195 195
196 196 schema = user_schema.ChangePasswordSchema().bind(
197 197 username=c.rhodecode_user.username)
198 198
199 199 form = forms.Form(schema,
200 200 buttons=(forms.buttons.save, forms.buttons.reset))
201 201
202 202 if request.method == 'POST':
203 203 controls = request.POST.items()
204 204 try:
205 205 valid_data = form.validate(controls)
206 206 UserModel().update_user(c.rhodecode_user.user_id, **valid_data)
207 207 instance = c.rhodecode_user.get_instance()
208 208 instance.update_userdata(force_password_change=False)
209 209 Session().commit()
210 210 except forms.ValidationFailure as e:
211 211 request.session.flash(
212 212 _('Error occurred during update of user password'),
213 213 queue='error')
214 214 form = e
215 215 except Exception:
216 216 log.exception("Exception updating password")
217 217 request.session.flash(
218 218 _('Error occurred during update of user password'),
219 219 queue='error')
220 220 else:
221 221 session.setdefault('rhodecode_user', {}).update(
222 222 {'password': md5(instance.password)})
223 223 session.save()
224 224 request.session.flash(
225 225 _("Successfully updated password"), queue='success')
226 226 return redirect(url('my_account_password'))
227 227
228 228 c.form = form
229 229 return render('admin/my_account/my_account.html')
230 230
231 231 def my_account_repos(self):
232 232 c.active = 'repos'
233 233 self.__load_data()
234 234
235 235 # json used to render the grid
236 236 c.data = self._load_my_repos_data()
237 237 return render('admin/my_account/my_account.html')
238 238
239 239 def my_account_watched(self):
240 240 c.active = 'watched'
241 241 self.__load_data()
242 242
243 243 # json used to render the grid
244 244 c.data = self._load_my_repos_data(watched=True)
245 245 return render('admin/my_account/my_account.html')
246 246
247 247 def my_account_perms(self):
248 248 c.active = 'perms'
249 249 self.__load_data()
250 250 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
251 251 ip_addr=self.ip_addr)
252 252
253 253 return render('admin/my_account/my_account.html')
254 254
255 255 def my_account_emails(self):
256 256 c.active = 'emails'
257 257 self.__load_data()
258 258
259 259 c.user_email_map = UserEmailMap.query()\
260 260 .filter(UserEmailMap.user == c.user).all()
261 261 return render('admin/my_account/my_account.html')
262 262
263 263 @auth.CSRFRequired()
264 264 def my_account_emails_add(self):
265 265 email = request.POST.get('new_email')
266 266
267 267 try:
268 268 UserModel().add_extra_email(c.rhodecode_user.user_id, email)
269 269 Session().commit()
270 270 h.flash(_("Added new email address `%s` for user account") % email,
271 271 category='success')
272 272 except formencode.Invalid as error:
273 273 msg = error.error_dict['email']
274 274 h.flash(msg, category='error')
275 275 except Exception:
276 276 log.exception("Exception in my_account_emails")
277 277 h.flash(_('An error occurred during email saving'),
278 278 category='error')
279 279 return redirect(url('my_account_emails'))
280 280
281 281 @auth.CSRFRequired()
282 282 def my_account_emails_delete(self):
283 283 email_id = request.POST.get('del_email_id')
284 284 user_model = UserModel()
285 285 user_model.delete_extra_email(c.rhodecode_user.user_id, email_id)
286 286 Session().commit()
287 287 h.flash(_("Removed email address from user account"),
288 288 category='success')
289 289 return redirect(url('my_account_emails'))
290 290
291 291 def my_account_pullrequests(self):
292 292 c.active = 'pullrequests'
293 293 self.__load_data()
294 294 c.show_closed = request.GET.get('pr_show_closed')
295 295
296 296 def _filter(pr):
297 297 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
298 298 if not c.show_closed:
299 299 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
300 300 return s
301 301
302 302 c.my_pull_requests = _filter(
303 303 PullRequest.query().filter(
304 304 PullRequest.user_id == c.rhodecode_user.user_id).all())
305 305 my_prs = [
306 306 x.pull_request for x in PullRequestReviewers.query().filter(
307 307 PullRequestReviewers.user_id == c.rhodecode_user.user_id).all()]
308 308 c.participate_in_pull_requests = _filter(my_prs)
309 309 return render('admin/my_account/my_account.html')
310 310
311 311 def my_account_auth_tokens(self):
312 312 c.active = 'auth_tokens'
313 313 self.__load_data()
314 314 show_expired = True
315 315 c.lifetime_values = [
316 316 (str(-1), _('forever')),
317 317 (str(5), _('5 minutes')),
318 318 (str(60), _('1 hour')),
319 319 (str(60 * 24), _('1 day')),
320 320 (str(60 * 24 * 30), _('1 month')),
321 321 ]
322 322 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
323 323 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
324 324 for x in AuthTokenModel.cls.ROLES]
325 325 c.role_options = [(c.role_values, _("Role"))]
326 326 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
327 327 c.rhodecode_user.user_id, show_expired=show_expired)
328 328 return render('admin/my_account/my_account.html')
329 329
330 330 @auth.CSRFRequired()
331 331 def my_account_auth_tokens_add(self):
332 332 lifetime = safe_int(request.POST.get('lifetime'), -1)
333 333 description = request.POST.get('description')
334 334 role = request.POST.get('role')
335 335 AuthTokenModel().create(c.rhodecode_user.user_id, description, lifetime,
336 336 role)
337 337 Session().commit()
338 338 h.flash(_("Auth token successfully created"), category='success')
339 339 return redirect(url('my_account_auth_tokens'))
340 340
341 341 @auth.CSRFRequired()
342 342 def my_account_auth_tokens_delete(self):
343 343 auth_token = request.POST.get('del_auth_token')
344 344 user_id = c.rhodecode_user.user_id
345 345 if request.POST.get('del_auth_token_builtin'):
346 346 user = User.get(user_id)
347 347 if user:
348 348 user.api_key = generate_auth_token(user.username)
349 349 Session().add(user)
350 350 Session().commit()
351 351 h.flash(_("Auth token successfully reset"), category='success')
352 352 elif auth_token:
353 353 AuthTokenModel().delete(auth_token, c.rhodecode_user.user_id)
354 354 Session().commit()
355 355 h.flash(_("Auth token successfully deleted"), category='success')
356 356
357 357 return redirect(url('my_account_auth_tokens'))
358 358
359 359 def my_notifications(self):
360 360 c.active = 'notifications'
361 361 return render('admin/my_account/my_account.html')
362 362
363 363 @auth.CSRFRequired()
364 364 def my_notifications_toggle_visibility(self):
365 365 user = c.rhodecode_user.get_instance()
366 366 user_data = user.user_data
367 367 status = user_data.get('notification_status', False)
368 368 user_data['notification_status'] = not status
369 369 user.user_data = user_data
370 370 Session().commit()
371 return redirect(url('my_account_notifications'))
371 return json.dumps(user_data['notification_status'])
@@ -1,4 +1,6 b''
1 1 <link rel="import" href="../../../../../bower_components/paper-toggle-button/paper-toggle-button.html">
2 2 <link rel="import" href="../../../../../bower_components/paper-toast/paper-toast.html">
3 3 <link rel="import" href="../../../../../bower_components/paper-tooltip/paper-tooltip.html">
4 4 <link rel="import" href="../../../../../bower_components/paper-button/paper-button.html">
5 <link rel="import" href="../../../../../bower_components/paper-spinner/paper-spinner.html">
6 <link rel="import" href="../../../../../bower_components/iron-ajax/iron-ajax.html">
@@ -1,56 +1,96 b''
1 <template is="dom-bind" id="notificationsPage">
2
3 <iron-ajax id="toggleNotifications"
4 method="post"
5 url="${url('my_account_notifications_toggle_visibility')}"
6 content-type="application/json"
7 loading="{{changeNotificationsLoading}}"
8 on-response="handleNotifications"
9 handle-as="json"></iron-ajax>
10
1 11 <div class="panel panel-default">
2 12 <div class="panel-heading">
3 13 <h3 class="panel-title">${_('Your live notification settings')}</h3>
4 14 </div>
5
6 15 <div class="panel-body">
7 16
8 17 <p><strong>IMPORTANT:</strong> This feature requires enabled channelstream websocket server to function correctly.</p>
9 18
10 19 <p class="hidden">Status of browser notifications permission: <strong id="browser-notification-status"></strong></p>
11 20
12 ${h.secure_form(url('my_account_notifications_toggle_visibility'), method='post', id='notification-status')}
13 <button class="btn btn-default" type="submit">
14 ${_('Notifications')} <strong>${_('Enabled') if c.rhodecode_user.get_instance().user_data.get('notification_status') else _('Disabled')}</strong>
15 </button>
16 ${h.end_form()}
17
18 <a class="btn btn-info" id="test-notification">Test notification</a>
21 <div class="form">
22 <!-- fields -->
23 <div class="fields">
24 <div class="field">
25 <div class="label">
26 <label for="new_email">${_('Notifications status')}:</label>
27 </div>
28 <div class="checkboxes">
29 <div style="display: inline-block">
30 <paper-toggle-button on-change="toggleNotifications" ${'checked' if c.rhodecode_user.get_instance().user_data.get('notification_status') else ''}></paper-toggle-button>
31 <paper-tooltip>Toggle your notifications on/off globally.</paper-tooltip>
32 </div>
33 <template is="dom-if" if="{{changeNotificationsLoading}}">
34 <paper-spinner active style="height: 22px; width: 22px"></paper-spinner>
35 </template>
36 </div>
37 </div>
38 <div class="buttons">
39 <a class="btn btn-info" id="test-notification">Test notification</a>
40 </div>
41 </div>
42 </div>
19 43
20 44 </div>
21 45 </div>
22 46
23 47 <script type="application/javascript">
48 /** because im not creating a custom element for this page
49 * we need to push the function onto the dom-template
50 * ideally we turn this into notification-settings elements
51 * then it will be cleaner
52 */
53 var ctrlr = $('#notificationsPage')[0];
54 ctrlr.toggleNotifications = function(event){
55 var ajax = $('#toggleNotifications')[0];
56 ajax.headers = {"X-CSRF-Token": CSRF_TOKEN}
57 ajax.body = {notification_status:event.target.active};
58 ajax.generateRequest();
59 };
60 ctrlr.handleNotifications = function(event){
61 $('paper-toggle-button')[0].active = event.detail.response;
62 };
24 63
25 64 function checkBrowserStatus(){
26 65 var browserStatus = 'Unknown';
27 66
28 67 if (!("Notification" in window)) {
29 68 browserStatus = 'Not supported'
30 69 }
31 70 else if(Notification.permission === 'denied'){
32 71 browserStatus = 'Denied';
33 72 $('.flash_msg').append('<div class="alert alert-error">Notifications are blocked on browser level - you need to enable them in your browser settings.</div>')
34 73 }
35 74 else if(Notification.permission === 'granted'){
36 75 browserStatus = 'Allowed';
37 76 }
38 77
39 78 $('#browser-notification-status').text(browserStatus);
40 79 };
41 80
42 81 checkBrowserStatus();
43 82
44 83 $('#test-notification').on('click', function(e){
45 84 var levels = ['info', 'error', 'warning', 'success'];
46 85 var level = levels[Math.floor(Math.random()*levels.length)];
47 86 var payload = {
48 87 message: {
49 88 message: 'This is a test notification.',
50 89 level: level,
51 90 testMessage: true
52 91 }
53 92 };
54 93 $.Topic('/notifications').publish(payload);
55 94 })
56 95 </script>
96 </template>
General Comments 0
You need to be logged in to leave comments. Login now