##// END OF EJS Templates
auth-tokens: allow specifing custom expiration date manually....
marcink -
r2083:37b1bdd7 default
parent child Browse files
Show More
@@ -1,674 +1,668 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import time
21 import logging
22 import logging
22 import datetime
23 import datetime
23 import formencode
24 import formencode
24 import formencode.htmlfill
25 import formencode.htmlfill
25
26
26 from pyramid.httpexceptions import HTTPFound
27 from pyramid.httpexceptions import HTTPFound
27 from pyramid.view import view_config
28 from pyramid.view import view_config
28 from sqlalchemy.sql.functions import coalesce
29 from sqlalchemy.sql.functions import coalesce
29 from sqlalchemy.exc import IntegrityError
30 from sqlalchemy.exc import IntegrityError
30
31
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
33 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
33 from rhodecode.events import trigger
34 from rhodecode.events import trigger
34
35
35 from rhodecode.lib import audit_logger
36 from rhodecode.lib import audit_logger
36 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
38 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
39 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
39 from rhodecode.lib import helpers as h
40 from rhodecode.lib import helpers as h
40 from rhodecode.lib.utils2 import safe_int, safe_unicode
41 from rhodecode.lib.utils2 import safe_int, safe_unicode
41 from rhodecode.model.auth_token import AuthTokenModel
42 from rhodecode.model.auth_token import AuthTokenModel
42 from rhodecode.model.ssh_key import SshKeyModel
43 from rhodecode.model.ssh_key import SshKeyModel
43 from rhodecode.model.user import UserModel
44 from rhodecode.model.user import UserModel
44 from rhodecode.model.user_group import UserGroupModel
45 from rhodecode.model.user_group import UserGroupModel
45 from rhodecode.model.db import (
46 from rhodecode.model.db import (
46 or_, User, UserIpMap, UserEmailMap, UserApiKeys, UserSshKeys)
47 or_, User, UserIpMap, UserEmailMap, UserApiKeys, UserSshKeys)
47 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
48
49
49 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
50
51
51
52
52 class AdminUsersView(BaseAppView, DataGridAppView):
53 class AdminUsersView(BaseAppView, DataGridAppView):
53 ALLOW_SCOPED_TOKENS = False
54 ALLOW_SCOPED_TOKENS = False
54 """
55 """
55 This view has alternative version inside EE, if modified please take a look
56 This view has alternative version inside EE, if modified please take a look
56 in there as well.
57 in there as well.
57 """
58 """
58
59
59 def load_default_context(self):
60 def load_default_context(self):
60 c = self._get_local_tmpl_context()
61 c = self._get_local_tmpl_context()
61 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
62 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
62 self._register_global_c(c)
63 self._register_global_c(c)
63 return c
64 return c
64
65
65 def _redirect_for_default_user(self, username):
66 def _redirect_for_default_user(self, username):
66 _ = self.request.translate
67 _ = self.request.translate
67 if username == User.DEFAULT_USER:
68 if username == User.DEFAULT_USER:
68 h.flash(_("You can't edit this user"), category='warning')
69 h.flash(_("You can't edit this user"), category='warning')
69 # TODO(marcink): redirect to 'users' admin panel once this
70 # TODO(marcink): redirect to 'users' admin panel once this
70 # is a pyramid view
71 # is a pyramid view
71 raise HTTPFound('/')
72 raise HTTPFound('/')
72
73
73 @LoginRequired()
74 @LoginRequired()
74 @HasPermissionAllDecorator('hg.admin')
75 @HasPermissionAllDecorator('hg.admin')
75 @view_config(
76 @view_config(
76 route_name='users', request_method='GET',
77 route_name='users', request_method='GET',
77 renderer='rhodecode:templates/admin/users/users.mako')
78 renderer='rhodecode:templates/admin/users/users.mako')
78 def users_list(self):
79 def users_list(self):
79 c = self.load_default_context()
80 c = self.load_default_context()
80 return self._get_template_context(c)
81 return self._get_template_context(c)
81
82
82 @LoginRequired()
83 @LoginRequired()
83 @HasPermissionAllDecorator('hg.admin')
84 @HasPermissionAllDecorator('hg.admin')
84 @view_config(
85 @view_config(
85 # renderer defined below
86 # renderer defined below
86 route_name='users_data', request_method='GET',
87 route_name='users_data', request_method='GET',
87 renderer='json_ext', xhr=True)
88 renderer='json_ext', xhr=True)
88 def users_list_data(self):
89 def users_list_data(self):
89 column_map = {
90 column_map = {
90 'first_name': 'name',
91 'first_name': 'name',
91 'last_name': 'lastname',
92 'last_name': 'lastname',
92 }
93 }
93 draw, start, limit = self._extract_chunk(self.request)
94 draw, start, limit = self._extract_chunk(self.request)
94 search_q, order_by, order_dir = self._extract_ordering(
95 search_q, order_by, order_dir = self._extract_ordering(
95 self.request, column_map=column_map)
96 self.request, column_map=column_map)
96
97
97 _render = self.request.get_partial_renderer(
98 _render = self.request.get_partial_renderer(
98 'data_table/_dt_elements.mako')
99 'data_table/_dt_elements.mako')
99
100
100 def user_actions(user_id, username):
101 def user_actions(user_id, username):
101 return _render("user_actions", user_id, username)
102 return _render("user_actions", user_id, username)
102
103
103 users_data_total_count = User.query()\
104 users_data_total_count = User.query()\
104 .filter(User.username != User.DEFAULT_USER) \
105 .filter(User.username != User.DEFAULT_USER) \
105 .count()
106 .count()
106
107
107 # json generate
108 # json generate
108 base_q = User.query().filter(User.username != User.DEFAULT_USER)
109 base_q = User.query().filter(User.username != User.DEFAULT_USER)
109
110
110 if search_q:
111 if search_q:
111 like_expression = u'%{}%'.format(safe_unicode(search_q))
112 like_expression = u'%{}%'.format(safe_unicode(search_q))
112 base_q = base_q.filter(or_(
113 base_q = base_q.filter(or_(
113 User.username.ilike(like_expression),
114 User.username.ilike(like_expression),
114 User._email.ilike(like_expression),
115 User._email.ilike(like_expression),
115 User.name.ilike(like_expression),
116 User.name.ilike(like_expression),
116 User.lastname.ilike(like_expression),
117 User.lastname.ilike(like_expression),
117 ))
118 ))
118
119
119 users_data_total_filtered_count = base_q.count()
120 users_data_total_filtered_count = base_q.count()
120
121
121 sort_col = getattr(User, order_by, None)
122 sort_col = getattr(User, order_by, None)
122 if sort_col:
123 if sort_col:
123 if order_dir == 'asc':
124 if order_dir == 'asc':
124 # handle null values properly to order by NULL last
125 # handle null values properly to order by NULL last
125 if order_by in ['last_activity']:
126 if order_by in ['last_activity']:
126 sort_col = coalesce(sort_col, datetime.date.max)
127 sort_col = coalesce(sort_col, datetime.date.max)
127 sort_col = sort_col.asc()
128 sort_col = sort_col.asc()
128 else:
129 else:
129 # handle null values properly to order by NULL last
130 # handle null values properly to order by NULL last
130 if order_by in ['last_activity']:
131 if order_by in ['last_activity']:
131 sort_col = coalesce(sort_col, datetime.date.min)
132 sort_col = coalesce(sort_col, datetime.date.min)
132 sort_col = sort_col.desc()
133 sort_col = sort_col.desc()
133
134
134 base_q = base_q.order_by(sort_col)
135 base_q = base_q.order_by(sort_col)
135 base_q = base_q.offset(start).limit(limit)
136 base_q = base_q.offset(start).limit(limit)
136
137
137 users_list = base_q.all()
138 users_list = base_q.all()
138
139
139 users_data = []
140 users_data = []
140 for user in users_list:
141 for user in users_list:
141 users_data.append({
142 users_data.append({
142 "username": h.gravatar_with_user(self.request, user.username),
143 "username": h.gravatar_with_user(self.request, user.username),
143 "email": user.email,
144 "email": user.email,
144 "first_name": user.first_name,
145 "first_name": user.first_name,
145 "last_name": user.last_name,
146 "last_name": user.last_name,
146 "last_login": h.format_date(user.last_login),
147 "last_login": h.format_date(user.last_login),
147 "last_activity": h.format_date(user.last_activity),
148 "last_activity": h.format_date(user.last_activity),
148 "active": h.bool2icon(user.active),
149 "active": h.bool2icon(user.active),
149 "active_raw": user.active,
150 "active_raw": user.active,
150 "admin": h.bool2icon(user.admin),
151 "admin": h.bool2icon(user.admin),
151 "extern_type": user.extern_type,
152 "extern_type": user.extern_type,
152 "extern_name": user.extern_name,
153 "extern_name": user.extern_name,
153 "action": user_actions(user.user_id, user.username),
154 "action": user_actions(user.user_id, user.username),
154 })
155 })
155
156
156 data = ({
157 data = ({
157 'draw': draw,
158 'draw': draw,
158 'data': users_data,
159 'data': users_data,
159 'recordsTotal': users_data_total_count,
160 'recordsTotal': users_data_total_count,
160 'recordsFiltered': users_data_total_filtered_count,
161 'recordsFiltered': users_data_total_filtered_count,
161 })
162 })
162
163
163 return data
164 return data
164
165
165 @LoginRequired()
166 @LoginRequired()
166 @HasPermissionAllDecorator('hg.admin')
167 @HasPermissionAllDecorator('hg.admin')
167 @view_config(
168 @view_config(
168 route_name='edit_user_auth_tokens', request_method='GET',
169 route_name='edit_user_auth_tokens', request_method='GET',
169 renderer='rhodecode:templates/admin/users/user_edit.mako')
170 renderer='rhodecode:templates/admin/users/user_edit.mako')
170 def auth_tokens(self):
171 def auth_tokens(self):
171 _ = self.request.translate
172 _ = self.request.translate
172 c = self.load_default_context()
173 c = self.load_default_context()
173
174
174 user_id = self.request.matchdict.get('user_id')
175 user_id = self.request.matchdict.get('user_id')
175 c.user = User.get_or_404(user_id)
176 c.user = User.get_or_404(user_id)
176 self._redirect_for_default_user(c.user.username)
177 self._redirect_for_default_user(c.user.username)
177
178
178 c.active = 'auth_tokens'
179 c.active = 'auth_tokens'
179
180
180 c.lifetime_values = [
181 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
181 (str(-1), _('forever')),
182 (str(5), _('5 minutes')),
183 (str(60), _('1 hour')),
184 (str(60 * 24), _('1 day')),
185 (str(60 * 24 * 30), _('1 month')),
186 ]
187 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
188 c.role_values = [
182 c.role_values = [
189 (x, AuthTokenModel.cls._get_role_name(x))
183 (x, AuthTokenModel.cls._get_role_name(x))
190 for x in AuthTokenModel.cls.ROLES]
184 for x in AuthTokenModel.cls.ROLES]
191 c.role_options = [(c.role_values, _("Role"))]
185 c.role_options = [(c.role_values, _("Role"))]
192 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
186 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
193 c.user.user_id, show_expired=True)
187 c.user.user_id, show_expired=True)
194 return self._get_template_context(c)
188 return self._get_template_context(c)
195
189
196 def maybe_attach_token_scope(self, token):
190 def maybe_attach_token_scope(self, token):
197 # implemented in EE edition
191 # implemented in EE edition
198 pass
192 pass
199
193
200 @LoginRequired()
194 @LoginRequired()
201 @HasPermissionAllDecorator('hg.admin')
195 @HasPermissionAllDecorator('hg.admin')
202 @CSRFRequired()
196 @CSRFRequired()
203 @view_config(
197 @view_config(
204 route_name='edit_user_auth_tokens_add', request_method='POST')
198 route_name='edit_user_auth_tokens_add', request_method='POST')
205 def auth_tokens_add(self):
199 def auth_tokens_add(self):
206 _ = self.request.translate
200 _ = self.request.translate
207 c = self.load_default_context()
201 c = self.load_default_context()
208
202
209 user_id = self.request.matchdict.get('user_id')
203 user_id = self.request.matchdict.get('user_id')
210 c.user = User.get_or_404(user_id)
204 c.user = User.get_or_404(user_id)
211
205
212 self._redirect_for_default_user(c.user.username)
206 self._redirect_for_default_user(c.user.username)
213
207
214 user_data = c.user.get_api_data()
208 user_data = c.user.get_api_data()
215 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
209 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
216 description = self.request.POST.get('description')
210 description = self.request.POST.get('description')
217 role = self.request.POST.get('role')
211 role = self.request.POST.get('role')
218
212
219 token = AuthTokenModel().create(
213 token = AuthTokenModel().create(
220 c.user.user_id, description, lifetime, role)
214 c.user.user_id, description, lifetime, role)
221 token_data = token.get_api_data()
215 token_data = token.get_api_data()
222
216
223 self.maybe_attach_token_scope(token)
217 self.maybe_attach_token_scope(token)
224 audit_logger.store_web(
218 audit_logger.store_web(
225 'user.edit.token.add', action_data={
219 'user.edit.token.add', action_data={
226 'data': {'token': token_data, 'user': user_data}},
220 'data': {'token': token_data, 'user': user_data}},
227 user=self._rhodecode_user, )
221 user=self._rhodecode_user, )
228 Session().commit()
222 Session().commit()
229
223
230 h.flash(_("Auth token successfully created"), category='success')
224 h.flash(_("Auth token successfully created"), category='success')
231 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
225 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
232
226
233 @LoginRequired()
227 @LoginRequired()
234 @HasPermissionAllDecorator('hg.admin')
228 @HasPermissionAllDecorator('hg.admin')
235 @CSRFRequired()
229 @CSRFRequired()
236 @view_config(
230 @view_config(
237 route_name='edit_user_auth_tokens_delete', request_method='POST')
231 route_name='edit_user_auth_tokens_delete', request_method='POST')
238 def auth_tokens_delete(self):
232 def auth_tokens_delete(self):
239 _ = self.request.translate
233 _ = self.request.translate
240 c = self.load_default_context()
234 c = self.load_default_context()
241
235
242 user_id = self.request.matchdict.get('user_id')
236 user_id = self.request.matchdict.get('user_id')
243 c.user = User.get_or_404(user_id)
237 c.user = User.get_or_404(user_id)
244 self._redirect_for_default_user(c.user.username)
238 self._redirect_for_default_user(c.user.username)
245 user_data = c.user.get_api_data()
239 user_data = c.user.get_api_data()
246
240
247 del_auth_token = self.request.POST.get('del_auth_token')
241 del_auth_token = self.request.POST.get('del_auth_token')
248
242
249 if del_auth_token:
243 if del_auth_token:
250 token = UserApiKeys.get_or_404(del_auth_token)
244 token = UserApiKeys.get_or_404(del_auth_token)
251 token_data = token.get_api_data()
245 token_data = token.get_api_data()
252
246
253 AuthTokenModel().delete(del_auth_token, c.user.user_id)
247 AuthTokenModel().delete(del_auth_token, c.user.user_id)
254 audit_logger.store_web(
248 audit_logger.store_web(
255 'user.edit.token.delete', action_data={
249 'user.edit.token.delete', action_data={
256 'data': {'token': token_data, 'user': user_data}},
250 'data': {'token': token_data, 'user': user_data}},
257 user=self._rhodecode_user,)
251 user=self._rhodecode_user,)
258 Session().commit()
252 Session().commit()
259 h.flash(_("Auth token successfully deleted"), category='success')
253 h.flash(_("Auth token successfully deleted"), category='success')
260
254
261 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
255 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
262
256
263 @LoginRequired()
257 @LoginRequired()
264 @HasPermissionAllDecorator('hg.admin')
258 @HasPermissionAllDecorator('hg.admin')
265 @view_config(
259 @view_config(
266 route_name='edit_user_ssh_keys', request_method='GET',
260 route_name='edit_user_ssh_keys', request_method='GET',
267 renderer='rhodecode:templates/admin/users/user_edit.mako')
261 renderer='rhodecode:templates/admin/users/user_edit.mako')
268 def ssh_keys(self):
262 def ssh_keys(self):
269 _ = self.request.translate
263 _ = self.request.translate
270 c = self.load_default_context()
264 c = self.load_default_context()
271
265
272 user_id = self.request.matchdict.get('user_id')
266 user_id = self.request.matchdict.get('user_id')
273 c.user = User.get_or_404(user_id)
267 c.user = User.get_or_404(user_id)
274 self._redirect_for_default_user(c.user.username)
268 self._redirect_for_default_user(c.user.username)
275
269
276 c.active = 'ssh_keys'
270 c.active = 'ssh_keys'
277 c.default_key = self.request.GET.get('default_key')
271 c.default_key = self.request.GET.get('default_key')
278 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
272 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
279 return self._get_template_context(c)
273 return self._get_template_context(c)
280
274
281 @LoginRequired()
275 @LoginRequired()
282 @HasPermissionAllDecorator('hg.admin')
276 @HasPermissionAllDecorator('hg.admin')
283 @view_config(
277 @view_config(
284 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
278 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
285 renderer='rhodecode:templates/admin/users/user_edit.mako')
279 renderer='rhodecode:templates/admin/users/user_edit.mako')
286 def ssh_keys_generate_keypair(self):
280 def ssh_keys_generate_keypair(self):
287 _ = self.request.translate
281 _ = self.request.translate
288 c = self.load_default_context()
282 c = self.load_default_context()
289
283
290 user_id = self.request.matchdict.get('user_id')
284 user_id = self.request.matchdict.get('user_id')
291 c.user = User.get_or_404(user_id)
285 c.user = User.get_or_404(user_id)
292 self._redirect_for_default_user(c.user.username)
286 self._redirect_for_default_user(c.user.username)
293
287
294 c.active = 'ssh_keys_generate'
288 c.active = 'ssh_keys_generate'
295 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
289 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
296 c.private, c.public = SshKeyModel().generate_keypair(comment=comment)
290 c.private, c.public = SshKeyModel().generate_keypair(comment=comment)
297
291
298 return self._get_template_context(c)
292 return self._get_template_context(c)
299
293
300 @LoginRequired()
294 @LoginRequired()
301 @HasPermissionAllDecorator('hg.admin')
295 @HasPermissionAllDecorator('hg.admin')
302 @CSRFRequired()
296 @CSRFRequired()
303 @view_config(
297 @view_config(
304 route_name='edit_user_ssh_keys_add', request_method='POST')
298 route_name='edit_user_ssh_keys_add', request_method='POST')
305 def ssh_keys_add(self):
299 def ssh_keys_add(self):
306 _ = self.request.translate
300 _ = self.request.translate
307 c = self.load_default_context()
301 c = self.load_default_context()
308
302
309 user_id = self.request.matchdict.get('user_id')
303 user_id = self.request.matchdict.get('user_id')
310 c.user = User.get_or_404(user_id)
304 c.user = User.get_or_404(user_id)
311
305
312 self._redirect_for_default_user(c.user.username)
306 self._redirect_for_default_user(c.user.username)
313
307
314 user_data = c.user.get_api_data()
308 user_data = c.user.get_api_data()
315 key_data = self.request.POST.get('key_data')
309 key_data = self.request.POST.get('key_data')
316 description = self.request.POST.get('description')
310 description = self.request.POST.get('description')
317
311
318 try:
312 try:
319 if not key_data:
313 if not key_data:
320 raise ValueError('Please add a valid public key')
314 raise ValueError('Please add a valid public key')
321
315
322 key = SshKeyModel().parse_key(key_data.strip())
316 key = SshKeyModel().parse_key(key_data.strip())
323 fingerprint = key.hash_md5()
317 fingerprint = key.hash_md5()
324
318
325 ssh_key = SshKeyModel().create(
319 ssh_key = SshKeyModel().create(
326 c.user.user_id, fingerprint, key_data, description)
320 c.user.user_id, fingerprint, key_data, description)
327 ssh_key_data = ssh_key.get_api_data()
321 ssh_key_data = ssh_key.get_api_data()
328
322
329 audit_logger.store_web(
323 audit_logger.store_web(
330 'user.edit.ssh_key.add', action_data={
324 'user.edit.ssh_key.add', action_data={
331 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
325 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
332 user=self._rhodecode_user, )
326 user=self._rhodecode_user, )
333 Session().commit()
327 Session().commit()
334
328
335 # Trigger an event on change of keys.
329 # Trigger an event on change of keys.
336 trigger(SshKeyFileChangeEvent(), self.request.registry)
330 trigger(SshKeyFileChangeEvent(), self.request.registry)
337
331
338 h.flash(_("Ssh Key successfully created"), category='success')
332 h.flash(_("Ssh Key successfully created"), category='success')
339
333
340 except IntegrityError:
334 except IntegrityError:
341 log.exception("Exception during ssh key saving")
335 log.exception("Exception during ssh key saving")
342 h.flash(_('An error occurred during ssh key saving: {}').format(
336 h.flash(_('An error occurred during ssh key saving: {}').format(
343 'Such key already exists, please use a different one'),
337 'Such key already exists, please use a different one'),
344 category='error')
338 category='error')
345 except Exception as e:
339 except Exception as e:
346 log.exception("Exception during ssh key saving")
340 log.exception("Exception during ssh key saving")
347 h.flash(_('An error occurred during ssh key saving: {}').format(e),
341 h.flash(_('An error occurred during ssh key saving: {}').format(e),
348 category='error')
342 category='error')
349
343
350 return HTTPFound(
344 return HTTPFound(
351 h.route_path('edit_user_ssh_keys', user_id=user_id))
345 h.route_path('edit_user_ssh_keys', user_id=user_id))
352
346
353 @LoginRequired()
347 @LoginRequired()
354 @HasPermissionAllDecorator('hg.admin')
348 @HasPermissionAllDecorator('hg.admin')
355 @CSRFRequired()
349 @CSRFRequired()
356 @view_config(
350 @view_config(
357 route_name='edit_user_ssh_keys_delete', request_method='POST')
351 route_name='edit_user_ssh_keys_delete', request_method='POST')
358 def ssh_keys_delete(self):
352 def ssh_keys_delete(self):
359 _ = self.request.translate
353 _ = self.request.translate
360 c = self.load_default_context()
354 c = self.load_default_context()
361
355
362 user_id = self.request.matchdict.get('user_id')
356 user_id = self.request.matchdict.get('user_id')
363 c.user = User.get_or_404(user_id)
357 c.user = User.get_or_404(user_id)
364 self._redirect_for_default_user(c.user.username)
358 self._redirect_for_default_user(c.user.username)
365 user_data = c.user.get_api_data()
359 user_data = c.user.get_api_data()
366
360
367 del_ssh_key = self.request.POST.get('del_ssh_key')
361 del_ssh_key = self.request.POST.get('del_ssh_key')
368
362
369 if del_ssh_key:
363 if del_ssh_key:
370 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
364 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
371 ssh_key_data = ssh_key.get_api_data()
365 ssh_key_data = ssh_key.get_api_data()
372
366
373 SshKeyModel().delete(del_ssh_key, c.user.user_id)
367 SshKeyModel().delete(del_ssh_key, c.user.user_id)
374 audit_logger.store_web(
368 audit_logger.store_web(
375 'user.edit.ssh_key.delete', action_data={
369 'user.edit.ssh_key.delete', action_data={
376 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
370 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
377 user=self._rhodecode_user,)
371 user=self._rhodecode_user,)
378 Session().commit()
372 Session().commit()
379 # Trigger an event on change of keys.
373 # Trigger an event on change of keys.
380 trigger(SshKeyFileChangeEvent(), self.request.registry)
374 trigger(SshKeyFileChangeEvent(), self.request.registry)
381 h.flash(_("Ssh key successfully deleted"), category='success')
375 h.flash(_("Ssh key successfully deleted"), category='success')
382
376
383 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
377 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
384
378
385 @LoginRequired()
379 @LoginRequired()
386 @HasPermissionAllDecorator('hg.admin')
380 @HasPermissionAllDecorator('hg.admin')
387 @view_config(
381 @view_config(
388 route_name='edit_user_emails', request_method='GET',
382 route_name='edit_user_emails', request_method='GET',
389 renderer='rhodecode:templates/admin/users/user_edit.mako')
383 renderer='rhodecode:templates/admin/users/user_edit.mako')
390 def emails(self):
384 def emails(self):
391 _ = self.request.translate
385 _ = self.request.translate
392 c = self.load_default_context()
386 c = self.load_default_context()
393
387
394 user_id = self.request.matchdict.get('user_id')
388 user_id = self.request.matchdict.get('user_id')
395 c.user = User.get_or_404(user_id)
389 c.user = User.get_or_404(user_id)
396 self._redirect_for_default_user(c.user.username)
390 self._redirect_for_default_user(c.user.username)
397
391
398 c.active = 'emails'
392 c.active = 'emails'
399 c.user_email_map = UserEmailMap.query() \
393 c.user_email_map = UserEmailMap.query() \
400 .filter(UserEmailMap.user == c.user).all()
394 .filter(UserEmailMap.user == c.user).all()
401
395
402 return self._get_template_context(c)
396 return self._get_template_context(c)
403
397
404 @LoginRequired()
398 @LoginRequired()
405 @HasPermissionAllDecorator('hg.admin')
399 @HasPermissionAllDecorator('hg.admin')
406 @CSRFRequired()
400 @CSRFRequired()
407 @view_config(
401 @view_config(
408 route_name='edit_user_emails_add', request_method='POST')
402 route_name='edit_user_emails_add', request_method='POST')
409 def emails_add(self):
403 def emails_add(self):
410 _ = self.request.translate
404 _ = self.request.translate
411 c = self.load_default_context()
405 c = self.load_default_context()
412
406
413 user_id = self.request.matchdict.get('user_id')
407 user_id = self.request.matchdict.get('user_id')
414 c.user = User.get_or_404(user_id)
408 c.user = User.get_or_404(user_id)
415 self._redirect_for_default_user(c.user.username)
409 self._redirect_for_default_user(c.user.username)
416
410
417 email = self.request.POST.get('new_email')
411 email = self.request.POST.get('new_email')
418 user_data = c.user.get_api_data()
412 user_data = c.user.get_api_data()
419 try:
413 try:
420 UserModel().add_extra_email(c.user.user_id, email)
414 UserModel().add_extra_email(c.user.user_id, email)
421 audit_logger.store_web(
415 audit_logger.store_web(
422 'user.edit.email.add', action_data={'email': email, 'user': user_data},
416 'user.edit.email.add', action_data={'email': email, 'user': user_data},
423 user=self._rhodecode_user)
417 user=self._rhodecode_user)
424 Session().commit()
418 Session().commit()
425 h.flash(_("Added new email address `%s` for user account") % email,
419 h.flash(_("Added new email address `%s` for user account") % email,
426 category='success')
420 category='success')
427 except formencode.Invalid as error:
421 except formencode.Invalid as error:
428 h.flash(h.escape(error.error_dict['email']), category='error')
422 h.flash(h.escape(error.error_dict['email']), category='error')
429 except Exception:
423 except Exception:
430 log.exception("Exception during email saving")
424 log.exception("Exception during email saving")
431 h.flash(_('An error occurred during email saving'),
425 h.flash(_('An error occurred during email saving'),
432 category='error')
426 category='error')
433 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
427 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
434
428
435 @LoginRequired()
429 @LoginRequired()
436 @HasPermissionAllDecorator('hg.admin')
430 @HasPermissionAllDecorator('hg.admin')
437 @CSRFRequired()
431 @CSRFRequired()
438 @view_config(
432 @view_config(
439 route_name='edit_user_emails_delete', request_method='POST')
433 route_name='edit_user_emails_delete', request_method='POST')
440 def emails_delete(self):
434 def emails_delete(self):
441 _ = self.request.translate
435 _ = self.request.translate
442 c = self.load_default_context()
436 c = self.load_default_context()
443
437
444 user_id = self.request.matchdict.get('user_id')
438 user_id = self.request.matchdict.get('user_id')
445 c.user = User.get_or_404(user_id)
439 c.user = User.get_or_404(user_id)
446 self._redirect_for_default_user(c.user.username)
440 self._redirect_for_default_user(c.user.username)
447
441
448 email_id = self.request.POST.get('del_email_id')
442 email_id = self.request.POST.get('del_email_id')
449 user_model = UserModel()
443 user_model = UserModel()
450
444
451 email = UserEmailMap.query().get(email_id).email
445 email = UserEmailMap.query().get(email_id).email
452 user_data = c.user.get_api_data()
446 user_data = c.user.get_api_data()
453 user_model.delete_extra_email(c.user.user_id, email_id)
447 user_model.delete_extra_email(c.user.user_id, email_id)
454 audit_logger.store_web(
448 audit_logger.store_web(
455 'user.edit.email.delete', action_data={'email': email, 'user': user_data},
449 'user.edit.email.delete', action_data={'email': email, 'user': user_data},
456 user=self._rhodecode_user)
450 user=self._rhodecode_user)
457 Session().commit()
451 Session().commit()
458 h.flash(_("Removed email address from user account"),
452 h.flash(_("Removed email address from user account"),
459 category='success')
453 category='success')
460 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
454 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
461
455
462 @LoginRequired()
456 @LoginRequired()
463 @HasPermissionAllDecorator('hg.admin')
457 @HasPermissionAllDecorator('hg.admin')
464 @view_config(
458 @view_config(
465 route_name='edit_user_ips', request_method='GET',
459 route_name='edit_user_ips', request_method='GET',
466 renderer='rhodecode:templates/admin/users/user_edit.mako')
460 renderer='rhodecode:templates/admin/users/user_edit.mako')
467 def ips(self):
461 def ips(self):
468 _ = self.request.translate
462 _ = self.request.translate
469 c = self.load_default_context()
463 c = self.load_default_context()
470
464
471 user_id = self.request.matchdict.get('user_id')
465 user_id = self.request.matchdict.get('user_id')
472 c.user = User.get_or_404(user_id)
466 c.user = User.get_or_404(user_id)
473 self._redirect_for_default_user(c.user.username)
467 self._redirect_for_default_user(c.user.username)
474
468
475 c.active = 'ips'
469 c.active = 'ips'
476 c.user_ip_map = UserIpMap.query() \
470 c.user_ip_map = UserIpMap.query() \
477 .filter(UserIpMap.user == c.user).all()
471 .filter(UserIpMap.user == c.user).all()
478
472
479 c.inherit_default_ips = c.user.inherit_default_permissions
473 c.inherit_default_ips = c.user.inherit_default_permissions
480 c.default_user_ip_map = UserIpMap.query() \
474 c.default_user_ip_map = UserIpMap.query() \
481 .filter(UserIpMap.user == User.get_default_user()).all()
475 .filter(UserIpMap.user == User.get_default_user()).all()
482
476
483 return self._get_template_context(c)
477 return self._get_template_context(c)
484
478
485 @LoginRequired()
479 @LoginRequired()
486 @HasPermissionAllDecorator('hg.admin')
480 @HasPermissionAllDecorator('hg.admin')
487 @CSRFRequired()
481 @CSRFRequired()
488 @view_config(
482 @view_config(
489 route_name='edit_user_ips_add', request_method='POST')
483 route_name='edit_user_ips_add', request_method='POST')
490 def ips_add(self):
484 def ips_add(self):
491 _ = self.request.translate
485 _ = self.request.translate
492 c = self.load_default_context()
486 c = self.load_default_context()
493
487
494 user_id = self.request.matchdict.get('user_id')
488 user_id = self.request.matchdict.get('user_id')
495 c.user = User.get_or_404(user_id)
489 c.user = User.get_or_404(user_id)
496 # NOTE(marcink): this view is allowed for default users, as we can
490 # NOTE(marcink): this view is allowed for default users, as we can
497 # edit their IP white list
491 # edit their IP white list
498
492
499 user_model = UserModel()
493 user_model = UserModel()
500 desc = self.request.POST.get('description')
494 desc = self.request.POST.get('description')
501 try:
495 try:
502 ip_list = user_model.parse_ip_range(
496 ip_list = user_model.parse_ip_range(
503 self.request.POST.get('new_ip'))
497 self.request.POST.get('new_ip'))
504 except Exception as e:
498 except Exception as e:
505 ip_list = []
499 ip_list = []
506 log.exception("Exception during ip saving")
500 log.exception("Exception during ip saving")
507 h.flash(_('An error occurred during ip saving:%s' % (e,)),
501 h.flash(_('An error occurred during ip saving:%s' % (e,)),
508 category='error')
502 category='error')
509 added = []
503 added = []
510 user_data = c.user.get_api_data()
504 user_data = c.user.get_api_data()
511 for ip in ip_list:
505 for ip in ip_list:
512 try:
506 try:
513 user_model.add_extra_ip(c.user.user_id, ip, desc)
507 user_model.add_extra_ip(c.user.user_id, ip, desc)
514 audit_logger.store_web(
508 audit_logger.store_web(
515 'user.edit.ip.add', action_data={'ip': ip, 'user': user_data},
509 'user.edit.ip.add', action_data={'ip': ip, 'user': user_data},
516 user=self._rhodecode_user)
510 user=self._rhodecode_user)
517 Session().commit()
511 Session().commit()
518 added.append(ip)
512 added.append(ip)
519 except formencode.Invalid as error:
513 except formencode.Invalid as error:
520 msg = error.error_dict['ip']
514 msg = error.error_dict['ip']
521 h.flash(msg, category='error')
515 h.flash(msg, category='error')
522 except Exception:
516 except Exception:
523 log.exception("Exception during ip saving")
517 log.exception("Exception during ip saving")
524 h.flash(_('An error occurred during ip saving'),
518 h.flash(_('An error occurred during ip saving'),
525 category='error')
519 category='error')
526 if added:
520 if added:
527 h.flash(
521 h.flash(
528 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
522 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
529 category='success')
523 category='success')
530 if 'default_user' in self.request.POST:
524 if 'default_user' in self.request.POST:
531 # case for editing global IP list we do it for 'DEFAULT' user
525 # case for editing global IP list we do it for 'DEFAULT' user
532 raise HTTPFound(h.route_path('admin_permissions_ips'))
526 raise HTTPFound(h.route_path('admin_permissions_ips'))
533 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
527 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
534
528
535 @LoginRequired()
529 @LoginRequired()
536 @HasPermissionAllDecorator('hg.admin')
530 @HasPermissionAllDecorator('hg.admin')
537 @CSRFRequired()
531 @CSRFRequired()
538 @view_config(
532 @view_config(
539 route_name='edit_user_ips_delete', request_method='POST')
533 route_name='edit_user_ips_delete', request_method='POST')
540 def ips_delete(self):
534 def ips_delete(self):
541 _ = self.request.translate
535 _ = self.request.translate
542 c = self.load_default_context()
536 c = self.load_default_context()
543
537
544 user_id = self.request.matchdict.get('user_id')
538 user_id = self.request.matchdict.get('user_id')
545 c.user = User.get_or_404(user_id)
539 c.user = User.get_or_404(user_id)
546 # NOTE(marcink): this view is allowed for default users, as we can
540 # NOTE(marcink): this view is allowed for default users, as we can
547 # edit their IP white list
541 # edit their IP white list
548
542
549 ip_id = self.request.POST.get('del_ip_id')
543 ip_id = self.request.POST.get('del_ip_id')
550 user_model = UserModel()
544 user_model = UserModel()
551 user_data = c.user.get_api_data()
545 user_data = c.user.get_api_data()
552 ip = UserIpMap.query().get(ip_id).ip_addr
546 ip = UserIpMap.query().get(ip_id).ip_addr
553 user_model.delete_extra_ip(c.user.user_id, ip_id)
547 user_model.delete_extra_ip(c.user.user_id, ip_id)
554 audit_logger.store_web(
548 audit_logger.store_web(
555 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
549 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
556 user=self._rhodecode_user)
550 user=self._rhodecode_user)
557 Session().commit()
551 Session().commit()
558 h.flash(_("Removed ip address from user whitelist"), category='success')
552 h.flash(_("Removed ip address from user whitelist"), category='success')
559
553
560 if 'default_user' in self.request.POST:
554 if 'default_user' in self.request.POST:
561 # case for editing global IP list we do it for 'DEFAULT' user
555 # case for editing global IP list we do it for 'DEFAULT' user
562 raise HTTPFound(h.route_path('admin_permissions_ips'))
556 raise HTTPFound(h.route_path('admin_permissions_ips'))
563 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
557 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
564
558
565 @LoginRequired()
559 @LoginRequired()
566 @HasPermissionAllDecorator('hg.admin')
560 @HasPermissionAllDecorator('hg.admin')
567 @view_config(
561 @view_config(
568 route_name='edit_user_groups_management', request_method='GET',
562 route_name='edit_user_groups_management', request_method='GET',
569 renderer='rhodecode:templates/admin/users/user_edit.mako')
563 renderer='rhodecode:templates/admin/users/user_edit.mako')
570 def groups_management(self):
564 def groups_management(self):
571 c = self.load_default_context()
565 c = self.load_default_context()
572
566
573 user_id = self.request.matchdict.get('user_id')
567 user_id = self.request.matchdict.get('user_id')
574 c.user = User.get_or_404(user_id)
568 c.user = User.get_or_404(user_id)
575 c.data = c.user.group_member
569 c.data = c.user.group_member
576 self._redirect_for_default_user(c.user.username)
570 self._redirect_for_default_user(c.user.username)
577 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
571 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
578 for group in c.user.group_member]
572 for group in c.user.group_member]
579 c.groups = json.dumps(groups)
573 c.groups = json.dumps(groups)
580 c.active = 'groups'
574 c.active = 'groups'
581
575
582 return self._get_template_context(c)
576 return self._get_template_context(c)
583
577
584 @LoginRequired()
578 @LoginRequired()
585 @HasPermissionAllDecorator('hg.admin')
579 @HasPermissionAllDecorator('hg.admin')
586 @CSRFRequired()
580 @CSRFRequired()
587 @view_config(
581 @view_config(
588 route_name='edit_user_groups_management_updates', request_method='POST')
582 route_name='edit_user_groups_management_updates', request_method='POST')
589 def groups_management_updates(self):
583 def groups_management_updates(self):
590 _ = self.request.translate
584 _ = self.request.translate
591 c = self.load_default_context()
585 c = self.load_default_context()
592
586
593 user_id = self.request.matchdict.get('user_id')
587 user_id = self.request.matchdict.get('user_id')
594 c.user = User.get_or_404(user_id)
588 c.user = User.get_or_404(user_id)
595 self._redirect_for_default_user(c.user.username)
589 self._redirect_for_default_user(c.user.username)
596
590
597 user_groups = set(self.request.POST.getall('users_group_id'))
591 user_groups = set(self.request.POST.getall('users_group_id'))
598 user_groups_objects = []
592 user_groups_objects = []
599
593
600 for ugid in user_groups:
594 for ugid in user_groups:
601 user_groups_objects.append(
595 user_groups_objects.append(
602 UserGroupModel().get_group(safe_int(ugid)))
596 UserGroupModel().get_group(safe_int(ugid)))
603 user_group_model = UserGroupModel()
597 user_group_model = UserGroupModel()
604 user_group_model.change_groups(c.user, user_groups_objects)
598 user_group_model.change_groups(c.user, user_groups_objects)
605
599
606 Session().commit()
600 Session().commit()
607 c.active = 'user_groups_management'
601 c.active = 'user_groups_management'
608 h.flash(_("Groups successfully changed"), category='success')
602 h.flash(_("Groups successfully changed"), category='success')
609
603
610 return HTTPFound(h.route_path(
604 return HTTPFound(h.route_path(
611 'edit_user_groups_management', user_id=user_id))
605 'edit_user_groups_management', user_id=user_id))
612
606
613 @LoginRequired()
607 @LoginRequired()
614 @HasPermissionAllDecorator('hg.admin')
608 @HasPermissionAllDecorator('hg.admin')
615 @view_config(
609 @view_config(
616 route_name='edit_user_audit_logs', request_method='GET',
610 route_name='edit_user_audit_logs', request_method='GET',
617 renderer='rhodecode:templates/admin/users/user_edit.mako')
611 renderer='rhodecode:templates/admin/users/user_edit.mako')
618 def user_audit_logs(self):
612 def user_audit_logs(self):
619 _ = self.request.translate
613 _ = self.request.translate
620 c = self.load_default_context()
614 c = self.load_default_context()
621
615
622 user_id = self.request.matchdict.get('user_id')
616 user_id = self.request.matchdict.get('user_id')
623 c.user = User.get_or_404(user_id)
617 c.user = User.get_or_404(user_id)
624 self._redirect_for_default_user(c.user.username)
618 self._redirect_for_default_user(c.user.username)
625 c.active = 'audit'
619 c.active = 'audit'
626
620
627 p = safe_int(self.request.GET.get('page', 1), 1)
621 p = safe_int(self.request.GET.get('page', 1), 1)
628
622
629 filter_term = self.request.GET.get('filter')
623 filter_term = self.request.GET.get('filter')
630 user_log = UserModel().get_user_log(c.user, filter_term)
624 user_log = UserModel().get_user_log(c.user, filter_term)
631
625
632 def url_generator(**kw):
626 def url_generator(**kw):
633 if filter_term:
627 if filter_term:
634 kw['filter'] = filter_term
628 kw['filter'] = filter_term
635 return self.request.current_route_path(_query=kw)
629 return self.request.current_route_path(_query=kw)
636
630
637 c.audit_logs = h.Page(
631 c.audit_logs = h.Page(
638 user_log, page=p, items_per_page=10, url=url_generator)
632 user_log, page=p, items_per_page=10, url=url_generator)
639 c.filter_term = filter_term
633 c.filter_term = filter_term
640 return self._get_template_context(c)
634 return self._get_template_context(c)
641
635
642 @LoginRequired()
636 @LoginRequired()
643 @HasPermissionAllDecorator('hg.admin')
637 @HasPermissionAllDecorator('hg.admin')
644 @view_config(
638 @view_config(
645 route_name='edit_user_perms_summary', request_method='GET',
639 route_name='edit_user_perms_summary', request_method='GET',
646 renderer='rhodecode:templates/admin/users/user_edit.mako')
640 renderer='rhodecode:templates/admin/users/user_edit.mako')
647 def user_perms_summary(self):
641 def user_perms_summary(self):
648 _ = self.request.translate
642 _ = self.request.translate
649 c = self.load_default_context()
643 c = self.load_default_context()
650
644
651 user_id = self.request.matchdict.get('user_id')
645 user_id = self.request.matchdict.get('user_id')
652 c.user = User.get_or_404(user_id)
646 c.user = User.get_or_404(user_id)
653 self._redirect_for_default_user(c.user.username)
647 self._redirect_for_default_user(c.user.username)
654
648
655 c.active = 'perms_summary'
649 c.active = 'perms_summary'
656 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
650 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
657
651
658 return self._get_template_context(c)
652 return self._get_template_context(c)
659
653
660 @LoginRequired()
654 @LoginRequired()
661 @HasPermissionAllDecorator('hg.admin')
655 @HasPermissionAllDecorator('hg.admin')
662 @view_config(
656 @view_config(
663 route_name='edit_user_perms_summary_json', request_method='GET',
657 route_name='edit_user_perms_summary_json', request_method='GET',
664 renderer='json_ext')
658 renderer='json_ext')
665 def user_perms_summary_json(self):
659 def user_perms_summary_json(self):
666 self.load_default_context()
660 self.load_default_context()
667
661
668 user_id = self.request.matchdict.get('user_id')
662 user_id = self.request.matchdict.get('user_id')
669 user = User.get_or_404(user_id)
663 user = User.get_or_404(user_id)
670 self._redirect_for_default_user(user.username)
664 self._redirect_for_default_user(user.username)
671
665
672 perm_user = user.AuthUser(ip_addr=self.request.remote_addr)
666 perm_user = user.AuthUser(ip_addr=self.request.remote_addr)
673
667
674 return perm_user.permissions
668 return perm_user.permissions
@@ -1,587 +1,579 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import datetime
22 import datetime
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
27 from pyramid.view import view_config
27 from pyramid.view import view_config
28 from pyramid.renderers import render
28 from pyramid.renderers import render
29 from pyramid.response import Response
29 from pyramid.response import Response
30
30
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 from rhodecode import forms
32 from rhodecode import forms
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
34 from rhodecode.lib import audit_logger
34 from rhodecode.lib import audit_logger
35 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
36 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
37 from rhodecode.lib.channelstream import (
37 from rhodecode.lib.channelstream import (
38 channelstream_request, ChannelstreamException)
38 channelstream_request, ChannelstreamException)
39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
40 from rhodecode.model.auth_token import AuthTokenModel
40 from rhodecode.model.auth_token import AuthTokenModel
41 from rhodecode.model.comment import CommentsModel
41 from rhodecode.model.comment import CommentsModel
42 from rhodecode.model.db import (
42 from rhodecode.model.db import (
43 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload,
43 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload,
44 PullRequest)
44 PullRequest)
45 from rhodecode.model.forms import UserForm
45 from rhodecode.model.forms import UserForm
46 from rhodecode.model.meta import Session
46 from rhodecode.model.meta import Session
47 from rhodecode.model.pull_request import PullRequestModel
47 from rhodecode.model.pull_request import PullRequestModel
48 from rhodecode.model.scm import RepoList
48 from rhodecode.model.scm import RepoList
49 from rhodecode.model.user import UserModel
49 from rhodecode.model.user import UserModel
50 from rhodecode.model.repo import RepoModel
50 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.validation_schema.schemas import user_schema
51 from rhodecode.model.validation_schema.schemas import user_schema
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 class MyAccountView(BaseAppView, DataGridAppView):
56 class MyAccountView(BaseAppView, DataGridAppView):
57 ALLOW_SCOPED_TOKENS = False
57 ALLOW_SCOPED_TOKENS = False
58 """
58 """
59 This view has alternative version inside EE, if modified please take a look
59 This view has alternative version inside EE, if modified please take a look
60 in there as well.
60 in there as well.
61 """
61 """
62
62
63 def load_default_context(self):
63 def load_default_context(self):
64 c = self._get_local_tmpl_context()
64 c = self._get_local_tmpl_context()
65 c.user = c.auth_user.get_instance()
65 c.user = c.auth_user.get_instance()
66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
67 self._register_global_c(c)
67 self._register_global_c(c)
68 return c
68 return c
69
69
70 @LoginRequired()
70 @LoginRequired()
71 @NotAnonymous()
71 @NotAnonymous()
72 @view_config(
72 @view_config(
73 route_name='my_account_profile', request_method='GET',
73 route_name='my_account_profile', request_method='GET',
74 renderer='rhodecode:templates/admin/my_account/my_account.mako')
74 renderer='rhodecode:templates/admin/my_account/my_account.mako')
75 def my_account_profile(self):
75 def my_account_profile(self):
76 c = self.load_default_context()
76 c = self.load_default_context()
77 c.active = 'profile'
77 c.active = 'profile'
78 return self._get_template_context(c)
78 return self._get_template_context(c)
79
79
80 @LoginRequired()
80 @LoginRequired()
81 @NotAnonymous()
81 @NotAnonymous()
82 @view_config(
82 @view_config(
83 route_name='my_account_password', request_method='GET',
83 route_name='my_account_password', request_method='GET',
84 renderer='rhodecode:templates/admin/my_account/my_account.mako')
84 renderer='rhodecode:templates/admin/my_account/my_account.mako')
85 def my_account_password(self):
85 def my_account_password(self):
86 c = self.load_default_context()
86 c = self.load_default_context()
87 c.active = 'password'
87 c.active = 'password'
88 c.extern_type = c.user.extern_type
88 c.extern_type = c.user.extern_type
89
89
90 schema = user_schema.ChangePasswordSchema().bind(
90 schema = user_schema.ChangePasswordSchema().bind(
91 username=c.user.username)
91 username=c.user.username)
92
92
93 form = forms.Form(
93 form = forms.Form(
94 schema,
94 schema,
95 action=h.route_path('my_account_password_update'),
95 action=h.route_path('my_account_password_update'),
96 buttons=(forms.buttons.save, forms.buttons.reset))
96 buttons=(forms.buttons.save, forms.buttons.reset))
97
97
98 c.form = form
98 c.form = form
99 return self._get_template_context(c)
99 return self._get_template_context(c)
100
100
101 @LoginRequired()
101 @LoginRequired()
102 @NotAnonymous()
102 @NotAnonymous()
103 @CSRFRequired()
103 @CSRFRequired()
104 @view_config(
104 @view_config(
105 route_name='my_account_password_update', request_method='POST',
105 route_name='my_account_password_update', request_method='POST',
106 renderer='rhodecode:templates/admin/my_account/my_account.mako')
106 renderer='rhodecode:templates/admin/my_account/my_account.mako')
107 def my_account_password_update(self):
107 def my_account_password_update(self):
108 _ = self.request.translate
108 _ = self.request.translate
109 c = self.load_default_context()
109 c = self.load_default_context()
110 c.active = 'password'
110 c.active = 'password'
111 c.extern_type = c.user.extern_type
111 c.extern_type = c.user.extern_type
112
112
113 schema = user_schema.ChangePasswordSchema().bind(
113 schema = user_schema.ChangePasswordSchema().bind(
114 username=c.user.username)
114 username=c.user.username)
115
115
116 form = forms.Form(
116 form = forms.Form(
117 schema, buttons=(forms.buttons.save, forms.buttons.reset))
117 schema, buttons=(forms.buttons.save, forms.buttons.reset))
118
118
119 if c.extern_type != 'rhodecode':
119 if c.extern_type != 'rhodecode':
120 raise HTTPFound(self.request.route_path('my_account_password'))
120 raise HTTPFound(self.request.route_path('my_account_password'))
121
121
122 controls = self.request.POST.items()
122 controls = self.request.POST.items()
123 try:
123 try:
124 valid_data = form.validate(controls)
124 valid_data = form.validate(controls)
125 UserModel().update_user(c.user.user_id, **valid_data)
125 UserModel().update_user(c.user.user_id, **valid_data)
126 c.user.update_userdata(force_password_change=False)
126 c.user.update_userdata(force_password_change=False)
127 Session().commit()
127 Session().commit()
128 except forms.ValidationFailure as e:
128 except forms.ValidationFailure as e:
129 c.form = e
129 c.form = e
130 return self._get_template_context(c)
130 return self._get_template_context(c)
131
131
132 except Exception:
132 except Exception:
133 log.exception("Exception updating password")
133 log.exception("Exception updating password")
134 h.flash(_('Error occurred during update of user password'),
134 h.flash(_('Error occurred during update of user password'),
135 category='error')
135 category='error')
136 else:
136 else:
137 instance = c.auth_user.get_instance()
137 instance = c.auth_user.get_instance()
138 self.session.setdefault('rhodecode_user', {}).update(
138 self.session.setdefault('rhodecode_user', {}).update(
139 {'password': md5(instance.password)})
139 {'password': md5(instance.password)})
140 self.session.save()
140 self.session.save()
141 h.flash(_("Successfully updated password"), category='success')
141 h.flash(_("Successfully updated password"), category='success')
142
142
143 raise HTTPFound(self.request.route_path('my_account_password'))
143 raise HTTPFound(self.request.route_path('my_account_password'))
144
144
145 @LoginRequired()
145 @LoginRequired()
146 @NotAnonymous()
146 @NotAnonymous()
147 @view_config(
147 @view_config(
148 route_name='my_account_auth_tokens', request_method='GET',
148 route_name='my_account_auth_tokens', request_method='GET',
149 renderer='rhodecode:templates/admin/my_account/my_account.mako')
149 renderer='rhodecode:templates/admin/my_account/my_account.mako')
150 def my_account_auth_tokens(self):
150 def my_account_auth_tokens(self):
151 _ = self.request.translate
151 _ = self.request.translate
152
152
153 c = self.load_default_context()
153 c = self.load_default_context()
154 c.active = 'auth_tokens'
154 c.active = 'auth_tokens'
155
155 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
156 c.lifetime_values = [
157 (str(-1), _('forever')),
158 (str(5), _('5 minutes')),
159 (str(60), _('1 hour')),
160 (str(60 * 24), _('1 day')),
161 (str(60 * 24 * 30), _('1 month')),
162 ]
163 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
164 c.role_values = [
156 c.role_values = [
165 (x, AuthTokenModel.cls._get_role_name(x))
157 (x, AuthTokenModel.cls._get_role_name(x))
166 for x in AuthTokenModel.cls.ROLES]
158 for x in AuthTokenModel.cls.ROLES]
167 c.role_options = [(c.role_values, _("Role"))]
159 c.role_options = [(c.role_values, _("Role"))]
168 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
160 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
169 c.user.user_id, show_expired=True)
161 c.user.user_id, show_expired=True)
170 return self._get_template_context(c)
162 return self._get_template_context(c)
171
163
172 def maybe_attach_token_scope(self, token):
164 def maybe_attach_token_scope(self, token):
173 # implemented in EE edition
165 # implemented in EE edition
174 pass
166 pass
175
167
176 @LoginRequired()
168 @LoginRequired()
177 @NotAnonymous()
169 @NotAnonymous()
178 @CSRFRequired()
170 @CSRFRequired()
179 @view_config(
171 @view_config(
180 route_name='my_account_auth_tokens_add', request_method='POST',)
172 route_name='my_account_auth_tokens_add', request_method='POST',)
181 def my_account_auth_tokens_add(self):
173 def my_account_auth_tokens_add(self):
182 _ = self.request.translate
174 _ = self.request.translate
183 c = self.load_default_context()
175 c = self.load_default_context()
184
176
185 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
177 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
186 description = self.request.POST.get('description')
178 description = self.request.POST.get('description')
187 role = self.request.POST.get('role')
179 role = self.request.POST.get('role')
188
180
189 token = AuthTokenModel().create(
181 token = AuthTokenModel().create(
190 c.user.user_id, description, lifetime, role)
182 c.user.user_id, description, lifetime, role)
191 token_data = token.get_api_data()
183 token_data = token.get_api_data()
192
184
193 self.maybe_attach_token_scope(token)
185 self.maybe_attach_token_scope(token)
194 audit_logger.store_web(
186 audit_logger.store_web(
195 'user.edit.token.add', action_data={
187 'user.edit.token.add', action_data={
196 'data': {'token': token_data, 'user': 'self'}},
188 'data': {'token': token_data, 'user': 'self'}},
197 user=self._rhodecode_user, )
189 user=self._rhodecode_user, )
198 Session().commit()
190 Session().commit()
199
191
200 h.flash(_("Auth token successfully created"), category='success')
192 h.flash(_("Auth token successfully created"), category='success')
201 return HTTPFound(h.route_path('my_account_auth_tokens'))
193 return HTTPFound(h.route_path('my_account_auth_tokens'))
202
194
203 @LoginRequired()
195 @LoginRequired()
204 @NotAnonymous()
196 @NotAnonymous()
205 @CSRFRequired()
197 @CSRFRequired()
206 @view_config(
198 @view_config(
207 route_name='my_account_auth_tokens_delete', request_method='POST')
199 route_name='my_account_auth_tokens_delete', request_method='POST')
208 def my_account_auth_tokens_delete(self):
200 def my_account_auth_tokens_delete(self):
209 _ = self.request.translate
201 _ = self.request.translate
210 c = self.load_default_context()
202 c = self.load_default_context()
211
203
212 del_auth_token = self.request.POST.get('del_auth_token')
204 del_auth_token = self.request.POST.get('del_auth_token')
213
205
214 if del_auth_token:
206 if del_auth_token:
215 token = UserApiKeys.get_or_404(del_auth_token)
207 token = UserApiKeys.get_or_404(del_auth_token)
216 token_data = token.get_api_data()
208 token_data = token.get_api_data()
217
209
218 AuthTokenModel().delete(del_auth_token, c.user.user_id)
210 AuthTokenModel().delete(del_auth_token, c.user.user_id)
219 audit_logger.store_web(
211 audit_logger.store_web(
220 'user.edit.token.delete', action_data={
212 'user.edit.token.delete', action_data={
221 'data': {'token': token_data, 'user': 'self'}},
213 'data': {'token': token_data, 'user': 'self'}},
222 user=self._rhodecode_user,)
214 user=self._rhodecode_user,)
223 Session().commit()
215 Session().commit()
224 h.flash(_("Auth token successfully deleted"), category='success')
216 h.flash(_("Auth token successfully deleted"), category='success')
225
217
226 return HTTPFound(h.route_path('my_account_auth_tokens'))
218 return HTTPFound(h.route_path('my_account_auth_tokens'))
227
219
228 @LoginRequired()
220 @LoginRequired()
229 @NotAnonymous()
221 @NotAnonymous()
230 @view_config(
222 @view_config(
231 route_name='my_account_emails', request_method='GET',
223 route_name='my_account_emails', request_method='GET',
232 renderer='rhodecode:templates/admin/my_account/my_account.mako')
224 renderer='rhodecode:templates/admin/my_account/my_account.mako')
233 def my_account_emails(self):
225 def my_account_emails(self):
234 _ = self.request.translate
226 _ = self.request.translate
235
227
236 c = self.load_default_context()
228 c = self.load_default_context()
237 c.active = 'emails'
229 c.active = 'emails'
238
230
239 c.user_email_map = UserEmailMap.query()\
231 c.user_email_map = UserEmailMap.query()\
240 .filter(UserEmailMap.user == c.user).all()
232 .filter(UserEmailMap.user == c.user).all()
241 return self._get_template_context(c)
233 return self._get_template_context(c)
242
234
243 @LoginRequired()
235 @LoginRequired()
244 @NotAnonymous()
236 @NotAnonymous()
245 @CSRFRequired()
237 @CSRFRequired()
246 @view_config(
238 @view_config(
247 route_name='my_account_emails_add', request_method='POST')
239 route_name='my_account_emails_add', request_method='POST')
248 def my_account_emails_add(self):
240 def my_account_emails_add(self):
249 _ = self.request.translate
241 _ = self.request.translate
250 c = self.load_default_context()
242 c = self.load_default_context()
251
243
252 email = self.request.POST.get('new_email')
244 email = self.request.POST.get('new_email')
253
245
254 try:
246 try:
255 UserModel().add_extra_email(c.user.user_id, email)
247 UserModel().add_extra_email(c.user.user_id, email)
256 audit_logger.store_web(
248 audit_logger.store_web(
257 'user.edit.email.add', action_data={
249 'user.edit.email.add', action_data={
258 'data': {'email': email, 'user': 'self'}},
250 'data': {'email': email, 'user': 'self'}},
259 user=self._rhodecode_user,)
251 user=self._rhodecode_user,)
260
252
261 Session().commit()
253 Session().commit()
262 h.flash(_("Added new email address `%s` for user account") % email,
254 h.flash(_("Added new email address `%s` for user account") % email,
263 category='success')
255 category='success')
264 except formencode.Invalid as error:
256 except formencode.Invalid as error:
265 h.flash(h.escape(error.error_dict['email']), category='error')
257 h.flash(h.escape(error.error_dict['email']), category='error')
266 except Exception:
258 except Exception:
267 log.exception("Exception in my_account_emails")
259 log.exception("Exception in my_account_emails")
268 h.flash(_('An error occurred during email saving'),
260 h.flash(_('An error occurred during email saving'),
269 category='error')
261 category='error')
270 return HTTPFound(h.route_path('my_account_emails'))
262 return HTTPFound(h.route_path('my_account_emails'))
271
263
272 @LoginRequired()
264 @LoginRequired()
273 @NotAnonymous()
265 @NotAnonymous()
274 @CSRFRequired()
266 @CSRFRequired()
275 @view_config(
267 @view_config(
276 route_name='my_account_emails_delete', request_method='POST')
268 route_name='my_account_emails_delete', request_method='POST')
277 def my_account_emails_delete(self):
269 def my_account_emails_delete(self):
278 _ = self.request.translate
270 _ = self.request.translate
279 c = self.load_default_context()
271 c = self.load_default_context()
280
272
281 del_email_id = self.request.POST.get('del_email_id')
273 del_email_id = self.request.POST.get('del_email_id')
282 if del_email_id:
274 if del_email_id:
283 email = UserEmailMap.get_or_404(del_email_id).email
275 email = UserEmailMap.get_or_404(del_email_id).email
284 UserModel().delete_extra_email(c.user.user_id, del_email_id)
276 UserModel().delete_extra_email(c.user.user_id, del_email_id)
285 audit_logger.store_web(
277 audit_logger.store_web(
286 'user.edit.email.delete', action_data={
278 'user.edit.email.delete', action_data={
287 'data': {'email': email, 'user': 'self'}},
279 'data': {'email': email, 'user': 'self'}},
288 user=self._rhodecode_user,)
280 user=self._rhodecode_user,)
289 Session().commit()
281 Session().commit()
290 h.flash(_("Email successfully deleted"),
282 h.flash(_("Email successfully deleted"),
291 category='success')
283 category='success')
292 return HTTPFound(h.route_path('my_account_emails'))
284 return HTTPFound(h.route_path('my_account_emails'))
293
285
294 @LoginRequired()
286 @LoginRequired()
295 @NotAnonymous()
287 @NotAnonymous()
296 @CSRFRequired()
288 @CSRFRequired()
297 @view_config(
289 @view_config(
298 route_name='my_account_notifications_test_channelstream',
290 route_name='my_account_notifications_test_channelstream',
299 request_method='POST', renderer='json_ext')
291 request_method='POST', renderer='json_ext')
300 def my_account_notifications_test_channelstream(self):
292 def my_account_notifications_test_channelstream(self):
301 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
293 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
302 self._rhodecode_user.username, datetime.datetime.now())
294 self._rhodecode_user.username, datetime.datetime.now())
303 payload = {
295 payload = {
304 # 'channel': 'broadcast',
296 # 'channel': 'broadcast',
305 'type': 'message',
297 'type': 'message',
306 'timestamp': datetime.datetime.utcnow(),
298 'timestamp': datetime.datetime.utcnow(),
307 'user': 'system',
299 'user': 'system',
308 'pm_users': [self._rhodecode_user.username],
300 'pm_users': [self._rhodecode_user.username],
309 'message': {
301 'message': {
310 'message': message,
302 'message': message,
311 'level': 'info',
303 'level': 'info',
312 'topic': '/notifications'
304 'topic': '/notifications'
313 }
305 }
314 }
306 }
315
307
316 registry = self.request.registry
308 registry = self.request.registry
317 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
309 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
318 channelstream_config = rhodecode_plugins.get('channelstream', {})
310 channelstream_config = rhodecode_plugins.get('channelstream', {})
319
311
320 try:
312 try:
321 channelstream_request(channelstream_config, [payload], '/message')
313 channelstream_request(channelstream_config, [payload], '/message')
322 except ChannelstreamException as e:
314 except ChannelstreamException as e:
323 log.exception('Failed to send channelstream data')
315 log.exception('Failed to send channelstream data')
324 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
316 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
325 return {"response": 'Channelstream data sent. '
317 return {"response": 'Channelstream data sent. '
326 'You should see a new live message now.'}
318 'You should see a new live message now.'}
327
319
328 def _load_my_repos_data(self, watched=False):
320 def _load_my_repos_data(self, watched=False):
329 if watched:
321 if watched:
330 admin = False
322 admin = False
331 follows_repos = Session().query(UserFollowing)\
323 follows_repos = Session().query(UserFollowing)\
332 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
324 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
333 .options(joinedload(UserFollowing.follows_repository))\
325 .options(joinedload(UserFollowing.follows_repository))\
334 .all()
326 .all()
335 repo_list = [x.follows_repository for x in follows_repos]
327 repo_list = [x.follows_repository for x in follows_repos]
336 else:
328 else:
337 admin = True
329 admin = True
338 repo_list = Repository.get_all_repos(
330 repo_list = Repository.get_all_repos(
339 user_id=self._rhodecode_user.user_id)
331 user_id=self._rhodecode_user.user_id)
340 repo_list = RepoList(repo_list, perm_set=[
332 repo_list = RepoList(repo_list, perm_set=[
341 'repository.read', 'repository.write', 'repository.admin'])
333 'repository.read', 'repository.write', 'repository.admin'])
342
334
343 repos_data = RepoModel().get_repos_as_dict(
335 repos_data = RepoModel().get_repos_as_dict(
344 repo_list=repo_list, admin=admin)
336 repo_list=repo_list, admin=admin)
345 # json used to render the grid
337 # json used to render the grid
346 return json.dumps(repos_data)
338 return json.dumps(repos_data)
347
339
348 @LoginRequired()
340 @LoginRequired()
349 @NotAnonymous()
341 @NotAnonymous()
350 @view_config(
342 @view_config(
351 route_name='my_account_repos', request_method='GET',
343 route_name='my_account_repos', request_method='GET',
352 renderer='rhodecode:templates/admin/my_account/my_account.mako')
344 renderer='rhodecode:templates/admin/my_account/my_account.mako')
353 def my_account_repos(self):
345 def my_account_repos(self):
354 c = self.load_default_context()
346 c = self.load_default_context()
355 c.active = 'repos'
347 c.active = 'repos'
356
348
357 # json used to render the grid
349 # json used to render the grid
358 c.data = self._load_my_repos_data()
350 c.data = self._load_my_repos_data()
359 return self._get_template_context(c)
351 return self._get_template_context(c)
360
352
361 @LoginRequired()
353 @LoginRequired()
362 @NotAnonymous()
354 @NotAnonymous()
363 @view_config(
355 @view_config(
364 route_name='my_account_watched', request_method='GET',
356 route_name='my_account_watched', request_method='GET',
365 renderer='rhodecode:templates/admin/my_account/my_account.mako')
357 renderer='rhodecode:templates/admin/my_account/my_account.mako')
366 def my_account_watched(self):
358 def my_account_watched(self):
367 c = self.load_default_context()
359 c = self.load_default_context()
368 c.active = 'watched'
360 c.active = 'watched'
369
361
370 # json used to render the grid
362 # json used to render the grid
371 c.data = self._load_my_repos_data(watched=True)
363 c.data = self._load_my_repos_data(watched=True)
372 return self._get_template_context(c)
364 return self._get_template_context(c)
373
365
374 @LoginRequired()
366 @LoginRequired()
375 @NotAnonymous()
367 @NotAnonymous()
376 @view_config(
368 @view_config(
377 route_name='my_account_perms', request_method='GET',
369 route_name='my_account_perms', request_method='GET',
378 renderer='rhodecode:templates/admin/my_account/my_account.mako')
370 renderer='rhodecode:templates/admin/my_account/my_account.mako')
379 def my_account_perms(self):
371 def my_account_perms(self):
380 c = self.load_default_context()
372 c = self.load_default_context()
381 c.active = 'perms'
373 c.active = 'perms'
382
374
383 c.perm_user = c.auth_user
375 c.perm_user = c.auth_user
384 return self._get_template_context(c)
376 return self._get_template_context(c)
385
377
386 @LoginRequired()
378 @LoginRequired()
387 @NotAnonymous()
379 @NotAnonymous()
388 @view_config(
380 @view_config(
389 route_name='my_account_notifications', request_method='GET',
381 route_name='my_account_notifications', request_method='GET',
390 renderer='rhodecode:templates/admin/my_account/my_account.mako')
382 renderer='rhodecode:templates/admin/my_account/my_account.mako')
391 def my_notifications(self):
383 def my_notifications(self):
392 c = self.load_default_context()
384 c = self.load_default_context()
393 c.active = 'notifications'
385 c.active = 'notifications'
394
386
395 return self._get_template_context(c)
387 return self._get_template_context(c)
396
388
397 @LoginRequired()
389 @LoginRequired()
398 @NotAnonymous()
390 @NotAnonymous()
399 @CSRFRequired()
391 @CSRFRequired()
400 @view_config(
392 @view_config(
401 route_name='my_account_notifications_toggle_visibility',
393 route_name='my_account_notifications_toggle_visibility',
402 request_method='POST', renderer='json_ext')
394 request_method='POST', renderer='json_ext')
403 def my_notifications_toggle_visibility(self):
395 def my_notifications_toggle_visibility(self):
404 user = self._rhodecode_db_user
396 user = self._rhodecode_db_user
405 new_status = not user.user_data.get('notification_status', True)
397 new_status = not user.user_data.get('notification_status', True)
406 user.update_userdata(notification_status=new_status)
398 user.update_userdata(notification_status=new_status)
407 Session().commit()
399 Session().commit()
408 return user.user_data['notification_status']
400 return user.user_data['notification_status']
409
401
410 @LoginRequired()
402 @LoginRequired()
411 @NotAnonymous()
403 @NotAnonymous()
412 @view_config(
404 @view_config(
413 route_name='my_account_edit',
405 route_name='my_account_edit',
414 request_method='GET',
406 request_method='GET',
415 renderer='rhodecode:templates/admin/my_account/my_account.mako')
407 renderer='rhodecode:templates/admin/my_account/my_account.mako')
416 def my_account_edit(self):
408 def my_account_edit(self):
417 c = self.load_default_context()
409 c = self.load_default_context()
418 c.active = 'profile_edit'
410 c.active = 'profile_edit'
419
411
420 c.perm_user = c.auth_user
412 c.perm_user = c.auth_user
421 c.extern_type = c.user.extern_type
413 c.extern_type = c.user.extern_type
422 c.extern_name = c.user.extern_name
414 c.extern_name = c.user.extern_name
423
415
424 defaults = c.user.get_dict()
416 defaults = c.user.get_dict()
425
417
426 data = render('rhodecode:templates/admin/my_account/my_account.mako',
418 data = render('rhodecode:templates/admin/my_account/my_account.mako',
427 self._get_template_context(c), self.request)
419 self._get_template_context(c), self.request)
428 html = formencode.htmlfill.render(
420 html = formencode.htmlfill.render(
429 data,
421 data,
430 defaults=defaults,
422 defaults=defaults,
431 encoding="UTF-8",
423 encoding="UTF-8",
432 force_defaults=False
424 force_defaults=False
433 )
425 )
434 return Response(html)
426 return Response(html)
435
427
436 @LoginRequired()
428 @LoginRequired()
437 @NotAnonymous()
429 @NotAnonymous()
438 @CSRFRequired()
430 @CSRFRequired()
439 @view_config(
431 @view_config(
440 route_name='my_account_update',
432 route_name='my_account_update',
441 request_method='POST',
433 request_method='POST',
442 renderer='rhodecode:templates/admin/my_account/my_account.mako')
434 renderer='rhodecode:templates/admin/my_account/my_account.mako')
443 def my_account_update(self):
435 def my_account_update(self):
444 _ = self.request.translate
436 _ = self.request.translate
445 c = self.load_default_context()
437 c = self.load_default_context()
446 c.active = 'profile_edit'
438 c.active = 'profile_edit'
447
439
448 c.perm_user = c.auth_user
440 c.perm_user = c.auth_user
449 c.extern_type = c.user.extern_type
441 c.extern_type = c.user.extern_type
450 c.extern_name = c.user.extern_name
442 c.extern_name = c.user.extern_name
451
443
452 _form = UserForm(edit=True,
444 _form = UserForm(edit=True,
453 old_data={'user_id': self._rhodecode_user.user_id,
445 old_data={'user_id': self._rhodecode_user.user_id,
454 'email': self._rhodecode_user.email})()
446 'email': self._rhodecode_user.email})()
455 form_result = {}
447 form_result = {}
456 try:
448 try:
457 post_data = dict(self.request.POST)
449 post_data = dict(self.request.POST)
458 post_data['new_password'] = ''
450 post_data['new_password'] = ''
459 post_data['password_confirmation'] = ''
451 post_data['password_confirmation'] = ''
460 form_result = _form.to_python(post_data)
452 form_result = _form.to_python(post_data)
461 # skip updating those attrs for my account
453 # skip updating those attrs for my account
462 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
454 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
463 'new_password', 'password_confirmation']
455 'new_password', 'password_confirmation']
464 # TODO: plugin should define if username can be updated
456 # TODO: plugin should define if username can be updated
465 if c.extern_type != "rhodecode":
457 if c.extern_type != "rhodecode":
466 # forbid updating username for external accounts
458 # forbid updating username for external accounts
467 skip_attrs.append('username')
459 skip_attrs.append('username')
468
460
469 UserModel().update_user(
461 UserModel().update_user(
470 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
462 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
471 **form_result)
463 **form_result)
472 h.flash(_('Your account was updated successfully'),
464 h.flash(_('Your account was updated successfully'),
473 category='success')
465 category='success')
474 Session().commit()
466 Session().commit()
475
467
476 except formencode.Invalid as errors:
468 except formencode.Invalid as errors:
477 data = render(
469 data = render(
478 'rhodecode:templates/admin/my_account/my_account.mako',
470 'rhodecode:templates/admin/my_account/my_account.mako',
479 self._get_template_context(c), self.request)
471 self._get_template_context(c), self.request)
480
472
481 html = formencode.htmlfill.render(
473 html = formencode.htmlfill.render(
482 data,
474 data,
483 defaults=errors.value,
475 defaults=errors.value,
484 errors=errors.error_dict or {},
476 errors=errors.error_dict or {},
485 prefix_error=False,
477 prefix_error=False,
486 encoding="UTF-8",
478 encoding="UTF-8",
487 force_defaults=False)
479 force_defaults=False)
488 return Response(html)
480 return Response(html)
489
481
490 except Exception:
482 except Exception:
491 log.exception("Exception updating user")
483 log.exception("Exception updating user")
492 h.flash(_('Error occurred during update of user %s')
484 h.flash(_('Error occurred during update of user %s')
493 % form_result.get('username'), category='error')
485 % form_result.get('username'), category='error')
494 raise HTTPFound(h.route_path('my_account_profile'))
486 raise HTTPFound(h.route_path('my_account_profile'))
495
487
496 raise HTTPFound(h.route_path('my_account_profile'))
488 raise HTTPFound(h.route_path('my_account_profile'))
497
489
498 def _get_pull_requests_list(self, statuses):
490 def _get_pull_requests_list(self, statuses):
499 draw, start, limit = self._extract_chunk(self.request)
491 draw, start, limit = self._extract_chunk(self.request)
500 search_q, order_by, order_dir = self._extract_ordering(self.request)
492 search_q, order_by, order_dir = self._extract_ordering(self.request)
501 _render = self.request.get_partial_renderer(
493 _render = self.request.get_partial_renderer(
502 'data_table/_dt_elements.mako')
494 'data_table/_dt_elements.mako')
503
495
504 pull_requests = PullRequestModel().get_im_participating_in(
496 pull_requests = PullRequestModel().get_im_participating_in(
505 user_id=self._rhodecode_user.user_id,
497 user_id=self._rhodecode_user.user_id,
506 statuses=statuses,
498 statuses=statuses,
507 offset=start, length=limit, order_by=order_by,
499 offset=start, length=limit, order_by=order_by,
508 order_dir=order_dir)
500 order_dir=order_dir)
509
501
510 pull_requests_total_count = PullRequestModel().count_im_participating_in(
502 pull_requests_total_count = PullRequestModel().count_im_participating_in(
511 user_id=self._rhodecode_user.user_id, statuses=statuses)
503 user_id=self._rhodecode_user.user_id, statuses=statuses)
512
504
513 data = []
505 data = []
514 comments_model = CommentsModel()
506 comments_model = CommentsModel()
515 for pr in pull_requests:
507 for pr in pull_requests:
516 repo_id = pr.target_repo_id
508 repo_id = pr.target_repo_id
517 comments = comments_model.get_all_comments(
509 comments = comments_model.get_all_comments(
518 repo_id, pull_request=pr)
510 repo_id, pull_request=pr)
519 owned = pr.user_id == self._rhodecode_user.user_id
511 owned = pr.user_id == self._rhodecode_user.user_id
520
512
521 data.append({
513 data.append({
522 'target_repo': _render('pullrequest_target_repo',
514 'target_repo': _render('pullrequest_target_repo',
523 pr.target_repo.repo_name),
515 pr.target_repo.repo_name),
524 'name': _render('pullrequest_name',
516 'name': _render('pullrequest_name',
525 pr.pull_request_id, pr.target_repo.repo_name,
517 pr.pull_request_id, pr.target_repo.repo_name,
526 short=True),
518 short=True),
527 'name_raw': pr.pull_request_id,
519 'name_raw': pr.pull_request_id,
528 'status': _render('pullrequest_status',
520 'status': _render('pullrequest_status',
529 pr.calculated_review_status()),
521 pr.calculated_review_status()),
530 'title': _render(
522 'title': _render(
531 'pullrequest_title', pr.title, pr.description),
523 'pullrequest_title', pr.title, pr.description),
532 'description': h.escape(pr.description),
524 'description': h.escape(pr.description),
533 'updated_on': _render('pullrequest_updated_on',
525 'updated_on': _render('pullrequest_updated_on',
534 h.datetime_to_time(pr.updated_on)),
526 h.datetime_to_time(pr.updated_on)),
535 'updated_on_raw': h.datetime_to_time(pr.updated_on),
527 'updated_on_raw': h.datetime_to_time(pr.updated_on),
536 'created_on': _render('pullrequest_updated_on',
528 'created_on': _render('pullrequest_updated_on',
537 h.datetime_to_time(pr.created_on)),
529 h.datetime_to_time(pr.created_on)),
538 'created_on_raw': h.datetime_to_time(pr.created_on),
530 'created_on_raw': h.datetime_to_time(pr.created_on),
539 'author': _render('pullrequest_author',
531 'author': _render('pullrequest_author',
540 pr.author.full_contact, ),
532 pr.author.full_contact, ),
541 'author_raw': pr.author.full_name,
533 'author_raw': pr.author.full_name,
542 'comments': _render('pullrequest_comments', len(comments)),
534 'comments': _render('pullrequest_comments', len(comments)),
543 'comments_raw': len(comments),
535 'comments_raw': len(comments),
544 'closed': pr.is_closed(),
536 'closed': pr.is_closed(),
545 'owned': owned
537 'owned': owned
546 })
538 })
547
539
548 # json used to render the grid
540 # json used to render the grid
549 data = ({
541 data = ({
550 'draw': draw,
542 'draw': draw,
551 'data': data,
543 'data': data,
552 'recordsTotal': pull_requests_total_count,
544 'recordsTotal': pull_requests_total_count,
553 'recordsFiltered': pull_requests_total_count,
545 'recordsFiltered': pull_requests_total_count,
554 })
546 })
555 return data
547 return data
556
548
557 @LoginRequired()
549 @LoginRequired()
558 @NotAnonymous()
550 @NotAnonymous()
559 @view_config(
551 @view_config(
560 route_name='my_account_pullrequests',
552 route_name='my_account_pullrequests',
561 request_method='GET',
553 request_method='GET',
562 renderer='rhodecode:templates/admin/my_account/my_account.mako')
554 renderer='rhodecode:templates/admin/my_account/my_account.mako')
563 def my_account_pullrequests(self):
555 def my_account_pullrequests(self):
564 c = self.load_default_context()
556 c = self.load_default_context()
565 c.active = 'pullrequests'
557 c.active = 'pullrequests'
566 req_get = self.request.GET
558 req_get = self.request.GET
567
559
568 c.closed = str2bool(req_get.get('pr_show_closed'))
560 c.closed = str2bool(req_get.get('pr_show_closed'))
569
561
570 return self._get_template_context(c)
562 return self._get_template_context(c)
571
563
572 @LoginRequired()
564 @LoginRequired()
573 @NotAnonymous()
565 @NotAnonymous()
574 @view_config(
566 @view_config(
575 route_name='my_account_pullrequests_data',
567 route_name='my_account_pullrequests_data',
576 request_method='GET', renderer='json_ext')
568 request_method='GET', renderer='json_ext')
577 def my_account_pullrequests_data(self):
569 def my_account_pullrequests_data(self):
578 req_get = self.request.GET
570 req_get = self.request.GET
579 closed = str2bool(req_get.get('closed'))
571 closed = str2bool(req_get.get('closed'))
580
572
581 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
573 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
582 if closed:
574 if closed:
583 statuses += [PullRequest.STATUS_CLOSED]
575 statuses += [PullRequest.STATUS_CLOSED]
584
576
585 data = self._get_pull_requests_list(statuses=statuses)
577 data = self._get_pull_requests_list(statuses=statuses)
586 return data
578 return data
587
579
@@ -1,102 +1,124 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2013-2017 RhodeCode GmbH
3 # Copyright (C) 2013-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 authentication tokens model for RhodeCode
22 authentication tokens model for RhodeCode
23 """
23 """
24
24
25 import time
25 import time
26 import logging
26 import logging
27 import traceback
27 import traceback
28 from sqlalchemy import or_
28 from sqlalchemy import or_
29
29
30 from rhodecode.model import BaseModel
30 from rhodecode.model import BaseModel
31 from rhodecode.model.db import UserApiKeys
31 from rhodecode.model.db import UserApiKeys
32 from rhodecode.model.meta import Session
32 from rhodecode.model.meta import Session
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 class AuthTokenModel(BaseModel):
37 class AuthTokenModel(BaseModel):
38 cls = UserApiKeys
38 cls = UserApiKeys
39
39
40 @classmethod
41 def get_lifetime_values(cls, translator):
42 from rhodecode.lib import helpers as h
43 _ = translator
44
45 def date_after_min(mins):
46 after = time.time() + (60 * mins)
47 return h.format_date(h.time_to_datetime(after))
48
49 return [
50 (str(-1),
51 _('forever')),
52 (str(5),
53 _('5 minutes {end_date}').format(end_date=date_after_min(5))),
54 (str(60),
55 _('1 hour {end_date}').format(end_date=date_after_min(60))),
56 (str(60 * 24),
57 _('1 day {end_date}').format(end_date=date_after_min(60 * 24))),
58 (str(60 * 24 * 30),
59 _('1 month {end_date}').format(end_date=date_after_min(60 * 24 * 30))),
60 ]
61
40 def create(self, user, description, lifetime=-1, role=UserApiKeys.ROLE_ALL):
62 def create(self, user, description, lifetime=-1, role=UserApiKeys.ROLE_ALL):
41 """
63 """
42 :param user: user or user_id
64 :param user: user or user_id
43 :param description: description of ApiKey
65 :param description: description of ApiKey
44 :param lifetime: expiration time in minutes
66 :param lifetime: expiration time in minutes
45 :param role: role for the apikey
67 :param role: role for the apikey
46 """
68 """
47 from rhodecode.lib.auth import generate_auth_token
69 from rhodecode.lib.auth import generate_auth_token
48
70
49 user = self._get_user(user)
71 user = self._get_user(user)
50
72
51 new_auth_token = UserApiKeys()
73 new_auth_token = UserApiKeys()
52 new_auth_token.api_key = generate_auth_token(user.username)
74 new_auth_token.api_key = generate_auth_token(user.username)
53 new_auth_token.user_id = user.user_id
75 new_auth_token.user_id = user.user_id
54 new_auth_token.description = description
76 new_auth_token.description = description
55 new_auth_token.role = role
77 new_auth_token.role = role
56 new_auth_token.expires = time.time() + (lifetime * 60) \
78 new_auth_token.expires = time.time() + (lifetime * 60) \
57 if lifetime != -1 else -1
79 if lifetime != -1 else -1
58 Session().add(new_auth_token)
80 Session().add(new_auth_token)
59
81
60 return new_auth_token
82 return new_auth_token
61
83
62 def delete(self, auth_token_id, user=None):
84 def delete(self, auth_token_id, user=None):
63 """
85 """
64 Deletes given api_key, if user is set it also filters the object for
86 Deletes given api_key, if user is set it also filters the object for
65 deletion by given user.
87 deletion by given user.
66 """
88 """
67 auth_token = UserApiKeys.query().filter(
89 auth_token = UserApiKeys.query().filter(
68 UserApiKeys.user_api_key_id == auth_token_id)
90 UserApiKeys.user_api_key_id == auth_token_id)
69
91
70 if user:
92 if user:
71 user = self._get_user(user)
93 user = self._get_user(user)
72 auth_token = auth_token.filter(UserApiKeys.user_id == user.user_id)
94 auth_token = auth_token.filter(UserApiKeys.user_id == user.user_id)
73 auth_token = auth_token.scalar()
95 auth_token = auth_token.scalar()
74
96
75 if auth_token:
97 if auth_token:
76 try:
98 try:
77 Session().delete(auth_token)
99 Session().delete(auth_token)
78 except Exception:
100 except Exception:
79 log.error(traceback.format_exc())
101 log.error(traceback.format_exc())
80 raise
102 raise
81
103
82 def get_auth_tokens(self, user, show_expired=True):
104 def get_auth_tokens(self, user, show_expired=True):
83 user = self._get_user(user)
105 user = self._get_user(user)
84 user_auth_tokens = UserApiKeys.query()\
106 user_auth_tokens = UserApiKeys.query()\
85 .filter(UserApiKeys.user_id == user.user_id)
107 .filter(UserApiKeys.user_id == user.user_id)
86 if not show_expired:
108 if not show_expired:
87 user_auth_tokens = user_auth_tokens\
109 user_auth_tokens = user_auth_tokens\
88 .filter(or_(UserApiKeys.expires == -1,
110 .filter(or_(UserApiKeys.expires == -1,
89 UserApiKeys.expires >= time.time()))
111 UserApiKeys.expires >= time.time()))
90 user_auth_tokens = user_auth_tokens.order_by(
112 user_auth_tokens = user_auth_tokens.order_by(
91 UserApiKeys.user_api_key_id)
113 UserApiKeys.user_api_key_id)
92 return user_auth_tokens
114 return user_auth_tokens
93
115
94 def get_auth_token(self, auth_token):
116 def get_auth_token(self, auth_token):
95 auth_token = UserApiKeys.query().filter(
117 auth_token = UserApiKeys.query().filter(
96 UserApiKeys.api_key == auth_token)
118 UserApiKeys.api_key == auth_token)
97 auth_token = auth_token \
119 auth_token = auth_token \
98 .filter(or_(UserApiKeys.expires == -1,
120 .filter(or_(UserApiKeys.expires == -1,
99 UserApiKeys.expires >= time.time()))\
121 UserApiKeys.expires >= time.time()))\
100 .first()
122 .first()
101
123
102 return auth_token
124 return auth_token
@@ -1,501 +1,560 b''
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
1 // # Copyright (C) 2010-2017 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 /**
19 /**
20 RhodeCode JS Files
20 RhodeCode JS Files
21 **/
21 **/
22
22
23 if (typeof console == "undefined" || typeof console.log == "undefined"){
23 if (typeof console == "undefined" || typeof console.log == "undefined"){
24 console = { log: function() {} }
24 console = { log: function() {} }
25 }
25 }
26
26
27 // TODO: move the following function to submodules
27 // TODO: move the following function to submodules
28
28
29 /**
29 /**
30 * show more
30 * show more
31 */
31 */
32 var show_more_event = function(){
32 var show_more_event = function(){
33 $('table .show_more').click(function(e) {
33 $('table .show_more').click(function(e) {
34 var cid = e.target.id.substring(1);
34 var cid = e.target.id.substring(1);
35 var button = $(this);
35 var button = $(this);
36 if (button.hasClass('open')) {
36 if (button.hasClass('open')) {
37 $('#'+cid).hide();
37 $('#'+cid).hide();
38 button.removeClass('open');
38 button.removeClass('open');
39 } else {
39 } else {
40 $('#'+cid).show();
40 $('#'+cid).show();
41 button.addClass('open one');
41 button.addClass('open one');
42 }
42 }
43 });
43 });
44 };
44 };
45
45
46 var compare_radio_buttons = function(repo_name, compare_ref_type){
46 var compare_radio_buttons = function(repo_name, compare_ref_type){
47 $('#compare_action').on('click', function(e){
47 $('#compare_action').on('click', function(e){
48 e.preventDefault();
48 e.preventDefault();
49
49
50 var source = $('input[name=compare_source]:checked').val();
50 var source = $('input[name=compare_source]:checked').val();
51 var target = $('input[name=compare_target]:checked').val();
51 var target = $('input[name=compare_target]:checked').val();
52 if(source && target){
52 if(source && target){
53 var url_data = {
53 var url_data = {
54 repo_name: repo_name,
54 repo_name: repo_name,
55 source_ref: source,
55 source_ref: source,
56 source_ref_type: compare_ref_type,
56 source_ref_type: compare_ref_type,
57 target_ref: target,
57 target_ref: target,
58 target_ref_type: compare_ref_type,
58 target_ref_type: compare_ref_type,
59 merge: 1
59 merge: 1
60 };
60 };
61 window.location = pyroutes.url('repo_compare', url_data);
61 window.location = pyroutes.url('repo_compare', url_data);
62 }
62 }
63 });
63 });
64 $('.compare-radio-button').on('click', function(e){
64 $('.compare-radio-button').on('click', function(e){
65 var source = $('input[name=compare_source]:checked').val();
65 var source = $('input[name=compare_source]:checked').val();
66 var target = $('input[name=compare_target]:checked').val();
66 var target = $('input[name=compare_target]:checked').val();
67 if(source && target){
67 if(source && target){
68 $('#compare_action').removeAttr("disabled");
68 $('#compare_action').removeAttr("disabled");
69 $('#compare_action').removeClass("disabled");
69 $('#compare_action').removeClass("disabled");
70 }
70 }
71 })
71 })
72 };
72 };
73
73
74 var showRepoSize = function(target, repo_name, commit_id, callback) {
74 var showRepoSize = function(target, repo_name, commit_id, callback) {
75 var container = $('#' + target);
75 var container = $('#' + target);
76 var url = pyroutes.url('repo_stats',
76 var url = pyroutes.url('repo_stats',
77 {"repo_name": repo_name, "commit_id": commit_id});
77 {"repo_name": repo_name, "commit_id": commit_id});
78
78
79 if (!container.hasClass('loaded')) {
79 if (!container.hasClass('loaded')) {
80 $.ajax({url: url})
80 $.ajax({url: url})
81 .complete(function (data) {
81 .complete(function (data) {
82 var responseJSON = data.responseJSON;
82 var responseJSON = data.responseJSON;
83 container.addClass('loaded');
83 container.addClass('loaded');
84 container.html(responseJSON.size);
84 container.html(responseJSON.size);
85 callback(responseJSON.code_stats)
85 callback(responseJSON.code_stats)
86 })
86 })
87 .fail(function (data) {
87 .fail(function (data) {
88 console.log('failed to load repo stats');
88 console.log('failed to load repo stats');
89 });
89 });
90 }
90 }
91
91
92 };
92 };
93
93
94 var showRepoStats = function(target, data){
94 var showRepoStats = function(target, data){
95 var container = $('#' + target);
95 var container = $('#' + target);
96
96
97 if (container.hasClass('loaded')) {
97 if (container.hasClass('loaded')) {
98 return
98 return
99 }
99 }
100
100
101 var total = 0;
101 var total = 0;
102 var no_data = true;
102 var no_data = true;
103 var tbl = document.createElement('table');
103 var tbl = document.createElement('table');
104 tbl.setAttribute('class', 'trending_language_tbl');
104 tbl.setAttribute('class', 'trending_language_tbl');
105
105
106 $.each(data, function(key, val){
106 $.each(data, function(key, val){
107 total += val.count;
107 total += val.count;
108 });
108 });
109
109
110 var sortedStats = [];
110 var sortedStats = [];
111 for (var obj in data){
111 for (var obj in data){
112 sortedStats.push([obj, data[obj]])
112 sortedStats.push([obj, data[obj]])
113 }
113 }
114 var sortedData = sortedStats.sort(function (a, b) {
114 var sortedData = sortedStats.sort(function (a, b) {
115 return b[1].count - a[1].count
115 return b[1].count - a[1].count
116 });
116 });
117 var cnt = 0;
117 var cnt = 0;
118 $.each(sortedData, function(idx, val){
118 $.each(sortedData, function(idx, val){
119 cnt += 1;
119 cnt += 1;
120 no_data = false;
120 no_data = false;
121
121
122 var hide = cnt > 2;
122 var hide = cnt > 2;
123 var tr = document.createElement('tr');
123 var tr = document.createElement('tr');
124 if (hide) {
124 if (hide) {
125 tr.setAttribute('style', 'display:none');
125 tr.setAttribute('style', 'display:none');
126 tr.setAttribute('class', 'stats_hidden');
126 tr.setAttribute('class', 'stats_hidden');
127 }
127 }
128
128
129 var key = val[0];
129 var key = val[0];
130 var obj = {"desc": val[1].desc, "count": val[1].count};
130 var obj = {"desc": val[1].desc, "count": val[1].count};
131
131
132 var percentage = Math.round((obj.count / total * 100), 2);
132 var percentage = Math.round((obj.count / total * 100), 2);
133
133
134 var td1 = document.createElement('td');
134 var td1 = document.createElement('td');
135 td1.width = 300;
135 td1.width = 300;
136 var trending_language_label = document.createElement('div');
136 var trending_language_label = document.createElement('div');
137 trending_language_label.innerHTML = obj.desc + " (.{0})".format(key);
137 trending_language_label.innerHTML = obj.desc + " (.{0})".format(key);
138 td1.appendChild(trending_language_label);
138 td1.appendChild(trending_language_label);
139
139
140 var td2 = document.createElement('td');
140 var td2 = document.createElement('td');
141 var trending_language = document.createElement('div');
141 var trending_language = document.createElement('div');
142 var nr_files = obj.count +" "+ _ngettext('file', 'files', obj.count);
142 var nr_files = obj.count +" "+ _ngettext('file', 'files', obj.count);
143
143
144 trending_language.title = key + " " + nr_files;
144 trending_language.title = key + " " + nr_files;
145
145
146 trending_language.innerHTML = "<span>" + percentage + "% " + nr_files
146 trending_language.innerHTML = "<span>" + percentage + "% " + nr_files
147 + "</span><b>" + percentage + "% " + nr_files + "</b>";
147 + "</span><b>" + percentage + "% " + nr_files + "</b>";
148
148
149 trending_language.setAttribute("class", 'trending_language');
149 trending_language.setAttribute("class", 'trending_language');
150 $('b', trending_language)[0].style.width = percentage + "%";
150 $('b', trending_language)[0].style.width = percentage + "%";
151 td2.appendChild(trending_language);
151 td2.appendChild(trending_language);
152
152
153 tr.appendChild(td1);
153 tr.appendChild(td1);
154 tr.appendChild(td2);
154 tr.appendChild(td2);
155 tbl.appendChild(tr);
155 tbl.appendChild(tr);
156 if (cnt == 3) {
156 if (cnt == 3) {
157 var show_more = document.createElement('tr');
157 var show_more = document.createElement('tr');
158 var td = document.createElement('td');
158 var td = document.createElement('td');
159 lnk = document.createElement('a');
159 lnk = document.createElement('a');
160
160
161 lnk.href = '#';
161 lnk.href = '#';
162 lnk.innerHTML = _gettext('Show more');
162 lnk.innerHTML = _gettext('Show more');
163 lnk.id = 'code_stats_show_more';
163 lnk.id = 'code_stats_show_more';
164 td.appendChild(lnk);
164 td.appendChild(lnk);
165
165
166 show_more.appendChild(td);
166 show_more.appendChild(td);
167 show_more.appendChild(document.createElement('td'));
167 show_more.appendChild(document.createElement('td'));
168 tbl.appendChild(show_more);
168 tbl.appendChild(show_more);
169 }
169 }
170 });
170 });
171
171
172 $(container).html(tbl);
172 $(container).html(tbl);
173 $(container).addClass('loaded');
173 $(container).addClass('loaded');
174
174
175 $('#code_stats_show_more').on('click', function (e) {
175 $('#code_stats_show_more').on('click', function (e) {
176 e.preventDefault();
176 e.preventDefault();
177 $('.stats_hidden').each(function (idx) {
177 $('.stats_hidden').each(function (idx) {
178 $(this).css("display", "");
178 $(this).css("display", "");
179 });
179 });
180 $('#code_stats_show_more').hide();
180 $('#code_stats_show_more').hide();
181 });
181 });
182
182
183 };
183 };
184
184
185 // returns a node from given html;
185 // returns a node from given html;
186 var fromHTML = function(html){
186 var fromHTML = function(html){
187 var _html = document.createElement('element');
187 var _html = document.createElement('element');
188 _html.innerHTML = html;
188 _html.innerHTML = html;
189 return _html;
189 return _html;
190 };
190 };
191
191
192 // Toggle Collapsable Content
192 // Toggle Collapsable Content
193 function collapsableContent() {
193 function collapsableContent() {
194
194
195 $('.collapsable-content').not('.no-hide').hide();
195 $('.collapsable-content').not('.no-hide').hide();
196
196
197 $('.btn-collapse').unbind(); //in case we've been here before
197 $('.btn-collapse').unbind(); //in case we've been here before
198 $('.btn-collapse').click(function() {
198 $('.btn-collapse').click(function() {
199 var button = $(this);
199 var button = $(this);
200 var togglename = $(this).data("toggle");
200 var togglename = $(this).data("toggle");
201 $('.collapsable-content[data-toggle='+togglename+']').toggle();
201 $('.collapsable-content[data-toggle='+togglename+']').toggle();
202 if ($(this).html()=="Show Less")
202 if ($(this).html()=="Show Less")
203 $(this).html("Show More");
203 $(this).html("Show More");
204 else
204 else
205 $(this).html("Show Less");
205 $(this).html("Show Less");
206 });
206 });
207 };
207 };
208
208
209 var timeagoActivate = function() {
209 var timeagoActivate = function() {
210 $("time.timeago").timeago();
210 $("time.timeago").timeago();
211 };
211 };
212
212
213
213
214 var clipboardActivate = function() {
214 var clipboardActivate = function() {
215 /*
215 /*
216 *
216 *
217 * <i class="tooltip icon-plus clipboard-action" data-clipboard-text="${commit.raw_id}" title="${_('Copy the full commit id')}"></i>
217 * <i class="tooltip icon-plus clipboard-action" data-clipboard-text="${commit.raw_id}" title="${_('Copy the full commit id')}"></i>
218 * */
218 * */
219 var clipboard = new Clipboard('.clipboard-action');
219 var clipboard = new Clipboard('.clipboard-action');
220
220
221 clipboard.on('success', function(e) {
221 clipboard.on('success', function(e) {
222 var callback = function () {
222 var callback = function () {
223 $(e.trigger).animate({'opacity': 1.00}, 200)
223 $(e.trigger).animate({'opacity': 1.00}, 200)
224 };
224 };
225 $(e.trigger).animate({'opacity': 0.15}, 200, callback);
225 $(e.trigger).animate({'opacity': 0.15}, 200, callback);
226 e.clearSelection();
226 e.clearSelection();
227 });
227 });
228 };
228 };
229
229
230
230
231 // Formatting values in a Select2 dropdown of commit references
231 // Formatting values in a Select2 dropdown of commit references
232 var formatSelect2SelectionRefs = function(commit_ref){
232 var formatSelect2SelectionRefs = function(commit_ref){
233 var tmpl = '';
233 var tmpl = '';
234 if (!commit_ref.text || commit_ref.type === 'sha'){
234 if (!commit_ref.text || commit_ref.type === 'sha'){
235 return commit_ref.text;
235 return commit_ref.text;
236 }
236 }
237 if (commit_ref.type === 'branch'){
237 if (commit_ref.type === 'branch'){
238 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
238 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
239 } else if (commit_ref.type === 'tag'){
239 } else if (commit_ref.type === 'tag'){
240 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
240 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
241 } else if (commit_ref.type === 'book'){
241 } else if (commit_ref.type === 'book'){
242 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
242 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
243 }
243 }
244 return tmpl.concat(commit_ref.text);
244 return tmpl.concat(commit_ref.text);
245 };
245 };
246
246
247 // takes a given html element and scrolls it down offset pixels
247 // takes a given html element and scrolls it down offset pixels
248 function offsetScroll(element, offset) {
248 function offsetScroll(element, offset) {
249 setTimeout(function() {
249 setTimeout(function() {
250 var location = element.offset().top;
250 var location = element.offset().top;
251 // some browsers use body, some use html
251 // some browsers use body, some use html
252 $('html, body').animate({ scrollTop: (location - offset) });
252 $('html, body').animate({ scrollTop: (location - offset) });
253 }, 100);
253 }, 100);
254 }
254 }
255
255
256 // scroll an element `percent`% from the top of page in `time` ms
256 // scroll an element `percent`% from the top of page in `time` ms
257 function scrollToElement(element, percent, time) {
257 function scrollToElement(element, percent, time) {
258 percent = (percent === undefined ? 25 : percent);
258 percent = (percent === undefined ? 25 : percent);
259 time = (time === undefined ? 100 : time);
259 time = (time === undefined ? 100 : time);
260
260
261 var $element = $(element);
261 var $element = $(element);
262 if ($element.length == 0) {
262 if ($element.length == 0) {
263 throw('Cannot scroll to {0}'.format(element))
263 throw('Cannot scroll to {0}'.format(element))
264 }
264 }
265 var elOffset = $element.offset().top;
265 var elOffset = $element.offset().top;
266 var elHeight = $element.height();
266 var elHeight = $element.height();
267 var windowHeight = $(window).height();
267 var windowHeight = $(window).height();
268 var offset = elOffset;
268 var offset = elOffset;
269 if (elHeight < windowHeight) {
269 if (elHeight < windowHeight) {
270 offset = elOffset - ((windowHeight / (100 / percent)) - (elHeight / 2));
270 offset = elOffset - ((windowHeight / (100 / percent)) - (elHeight / 2));
271 }
271 }
272 setTimeout(function() {
272 setTimeout(function() {
273 $('html, body').animate({ scrollTop: offset});
273 $('html, body').animate({ scrollTop: offset});
274 }, time);
274 }, time);
275 }
275 }
276
276
277 /**
277 /**
278 * global hooks after DOM is loaded
278 * global hooks after DOM is loaded
279 */
279 */
280 $(document).ready(function() {
280 $(document).ready(function() {
281 firefoxAnchorFix();
281 firefoxAnchorFix();
282
282
283 $('.navigation a.menulink').on('click', function(e){
283 $('.navigation a.menulink').on('click', function(e){
284 var menuitem = $(this).parent('li');
284 var menuitem = $(this).parent('li');
285 if (menuitem.hasClass('open')) {
285 if (menuitem.hasClass('open')) {
286 menuitem.removeClass('open');
286 menuitem.removeClass('open');
287 } else {
287 } else {
288 menuitem.addClass('open');
288 menuitem.addClass('open');
289 $(document).on('click', function(event) {
289 $(document).on('click', function(event) {
290 if (!$(event.target).closest(menuitem).length) {
290 if (!$(event.target).closest(menuitem).length) {
291 menuitem.removeClass('open');
291 menuitem.removeClass('open');
292 }
292 }
293 });
293 });
294 }
294 }
295 });
295 });
296 $('.compare_view_files').on(
296 $('.compare_view_files').on(
297 'mouseenter mouseleave', 'tr.line .lineno a',function(event) {
297 'mouseenter mouseleave', 'tr.line .lineno a',function(event) {
298 if (event.type === "mouseenter") {
298 if (event.type === "mouseenter") {
299 $(this).parents('tr.line').addClass('hover');
299 $(this).parents('tr.line').addClass('hover');
300 } else {
300 } else {
301 $(this).parents('tr.line').removeClass('hover');
301 $(this).parents('tr.line').removeClass('hover');
302 }
302 }
303 });
303 });
304
304
305 $('.compare_view_files').on(
305 $('.compare_view_files').on(
306 'mouseenter mouseleave', 'tr.line .add-comment-line a',function(event){
306 'mouseenter mouseleave', 'tr.line .add-comment-line a',function(event){
307 if (event.type === "mouseenter") {
307 if (event.type === "mouseenter") {
308 $(this).parents('tr.line').addClass('commenting');
308 $(this).parents('tr.line').addClass('commenting');
309 } else {
309 } else {
310 $(this).parents('tr.line').removeClass('commenting');
310 $(this).parents('tr.line').removeClass('commenting');
311 }
311 }
312 });
312 });
313
313
314 $('body').on( /* TODO: replace the $('.compare_view_files').on('click') below
314 $('body').on( /* TODO: replace the $('.compare_view_files').on('click') below
315 when new diffs are integrated */
315 when new diffs are integrated */
316 'click', '.cb-lineno a', function(event) {
316 'click', '.cb-lineno a', function(event) {
317
317
318 if ($(this).attr('data-line-no') !== ""){
318 if ($(this).attr('data-line-no') !== ""){
319 $('.cb-line-selected').removeClass('cb-line-selected');
319 $('.cb-line-selected').removeClass('cb-line-selected');
320 var td = $(this).parent();
320 var td = $(this).parent();
321 td.addClass('cb-line-selected'); // line number td
321 td.addClass('cb-line-selected'); // line number td
322 td.prev().addClass('cb-line-selected'); // line data td
322 td.prev().addClass('cb-line-selected'); // line data td
323 td.next().addClass('cb-line-selected'); // line content td
323 td.next().addClass('cb-line-selected'); // line content td
324
324
325 // Replace URL without jumping to it if browser supports.
325 // Replace URL without jumping to it if browser supports.
326 // Default otherwise
326 // Default otherwise
327 if (history.pushState) {
327 if (history.pushState) {
328 var new_location = location.href.rstrip('#');
328 var new_location = location.href.rstrip('#');
329 if (location.hash) {
329 if (location.hash) {
330 new_location = new_location.replace(location.hash, "");
330 new_location = new_location.replace(location.hash, "");
331 }
331 }
332
332
333 // Make new anchor url
333 // Make new anchor url
334 new_location = new_location + $(this).attr('href');
334 new_location = new_location + $(this).attr('href');
335 history.pushState(true, document.title, new_location);
335 history.pushState(true, document.title, new_location);
336
336
337 return false;
337 return false;
338 }
338 }
339 }
339 }
340 });
340 });
341
341
342 $('.compare_view_files').on( /* TODO: replace this with .cb function above
342 $('.compare_view_files').on( /* TODO: replace this with .cb function above
343 when new diffs are integrated */
343 when new diffs are integrated */
344 'click', 'tr.line .lineno a',function(event) {
344 'click', 'tr.line .lineno a',function(event) {
345 if ($(this).text() != ""){
345 if ($(this).text() != ""){
346 $('tr.line').removeClass('selected');
346 $('tr.line').removeClass('selected');
347 $(this).parents("tr.line").addClass('selected');
347 $(this).parents("tr.line").addClass('selected');
348
348
349 // Replace URL without jumping to it if browser supports.
349 // Replace URL without jumping to it if browser supports.
350 // Default otherwise
350 // Default otherwise
351 if (history.pushState) {
351 if (history.pushState) {
352 var new_location = location.href;
352 var new_location = location.href;
353 if (location.hash){
353 if (location.hash){
354 new_location = new_location.replace(location.hash, "");
354 new_location = new_location.replace(location.hash, "");
355 }
355 }
356
356
357 // Make new anchor url
357 // Make new anchor url
358 var new_location = new_location+$(this).attr('href');
358 var new_location = new_location+$(this).attr('href');
359 history.pushState(true, document.title, new_location);
359 history.pushState(true, document.title, new_location);
360
360
361 return false;
361 return false;
362 }
362 }
363 }
363 }
364 });
364 });
365
365
366 $('.compare_view_files').on(
366 $('.compare_view_files').on(
367 'click', 'tr.line .add-comment-line a',function(event) {
367 'click', 'tr.line .add-comment-line a',function(event) {
368 var tr = $(event.currentTarget).parents('tr.line')[0];
368 var tr = $(event.currentTarget).parents('tr.line')[0];
369 injectInlineForm(tr);
369 injectInlineForm(tr);
370 return false;
370 return false;
371 });
371 });
372
372
373 $('.collapse_file').on('click', function(e) {
373 $('.collapse_file').on('click', function(e) {
374 e.stopPropagation();
374 e.stopPropagation();
375 if ($(e.target).is('a')) { return; }
375 if ($(e.target).is('a')) { return; }
376 var node = $(e.delegateTarget).first();
376 var node = $(e.delegateTarget).first();
377 var icon = $($(node.children().first()).children().first());
377 var icon = $($(node.children().first()).children().first());
378 var id = node.attr('fid');
378 var id = node.attr('fid');
379 var target = $('#'+id);
379 var target = $('#'+id);
380 var tr = $('#tr_'+id);
380 var tr = $('#tr_'+id);
381 var diff = $('#diff_'+id);
381 var diff = $('#diff_'+id);
382 if(node.hasClass('expand_file')){
382 if(node.hasClass('expand_file')){
383 node.removeClass('expand_file');
383 node.removeClass('expand_file');
384 icon.removeClass('expand_file_icon');
384 icon.removeClass('expand_file_icon');
385 node.addClass('collapse_file');
385 node.addClass('collapse_file');
386 icon.addClass('collapse_file_icon');
386 icon.addClass('collapse_file_icon');
387 diff.show();
387 diff.show();
388 tr.show();
388 tr.show();
389 target.show();
389 target.show();
390 } else {
390 } else {
391 node.removeClass('collapse_file');
391 node.removeClass('collapse_file');
392 icon.removeClass('collapse_file_icon');
392 icon.removeClass('collapse_file_icon');
393 node.addClass('expand_file');
393 node.addClass('expand_file');
394 icon.addClass('expand_file_icon');
394 icon.addClass('expand_file_icon');
395 diff.hide();
395 diff.hide();
396 tr.hide();
396 tr.hide();
397 target.hide();
397 target.hide();
398 }
398 }
399 });
399 });
400
400
401 $('#expand_all_files').click(function() {
401 $('#expand_all_files').click(function() {
402 $('.expand_file').each(function() {
402 $('.expand_file').each(function() {
403 var node = $(this);
403 var node = $(this);
404 var icon = $($(node.children().first()).children().first());
404 var icon = $($(node.children().first()).children().first());
405 var id = $(this).attr('fid');
405 var id = $(this).attr('fid');
406 var target = $('#'+id);
406 var target = $('#'+id);
407 var tr = $('#tr_'+id);
407 var tr = $('#tr_'+id);
408 var diff = $('#diff_'+id);
408 var diff = $('#diff_'+id);
409 node.removeClass('expand_file');
409 node.removeClass('expand_file');
410 icon.removeClass('expand_file_icon');
410 icon.removeClass('expand_file_icon');
411 node.addClass('collapse_file');
411 node.addClass('collapse_file');
412 icon.addClass('collapse_file_icon');
412 icon.addClass('collapse_file_icon');
413 diff.show();
413 diff.show();
414 tr.show();
414 tr.show();
415 target.show();
415 target.show();
416 });
416 });
417 });
417 });
418
418
419 $('#collapse_all_files').click(function() {
419 $('#collapse_all_files').click(function() {
420 $('.collapse_file').each(function() {
420 $('.collapse_file').each(function() {
421 var node = $(this);
421 var node = $(this);
422 var icon = $($(node.children().first()).children().first());
422 var icon = $($(node.children().first()).children().first());
423 var id = $(this).attr('fid');
423 var id = $(this).attr('fid');
424 var target = $('#'+id);
424 var target = $('#'+id);
425 var tr = $('#tr_'+id);
425 var tr = $('#tr_'+id);
426 var diff = $('#diff_'+id);
426 var diff = $('#diff_'+id);
427 node.removeClass('collapse_file');
427 node.removeClass('collapse_file');
428 icon.removeClass('collapse_file_icon');
428 icon.removeClass('collapse_file_icon');
429 node.addClass('expand_file');
429 node.addClass('expand_file');
430 icon.addClass('expand_file_icon');
430 icon.addClass('expand_file_icon');
431 diff.hide();
431 diff.hide();
432 tr.hide();
432 tr.hide();
433 target.hide();
433 target.hide();
434 });
434 });
435 });
435 });
436
436
437 // Mouse over behavior for comments and line selection
437 // Mouse over behavior for comments and line selection
438
438
439 // Select the line that comes from the url anchor
439 // Select the line that comes from the url anchor
440 // At the time of development, Chrome didn't seem to support jquery's :target
440 // At the time of development, Chrome didn't seem to support jquery's :target
441 // element, so I had to scroll manually
441 // element, so I had to scroll manually
442
442
443 if (location.hash) {
443 if (location.hash) {
444 var result = splitDelimitedHash(location.hash);
444 var result = splitDelimitedHash(location.hash);
445 var loc = result.loc;
445 var loc = result.loc;
446 if (loc.length > 1) {
446 if (loc.length > 1) {
447
447
448 var highlightable_line_tds = [];
448 var highlightable_line_tds = [];
449
449
450 // source code line format
450 // source code line format
451 var page_highlights = loc.substring(
451 var page_highlights = loc.substring(
452 loc.indexOf('#') + 1).split('L');
452 loc.indexOf('#') + 1).split('L');
453
453
454 if (page_highlights.length > 1) {
454 if (page_highlights.length > 1) {
455 var highlight_ranges = page_highlights[1].split(",");
455 var highlight_ranges = page_highlights[1].split(",");
456 var h_lines = [];
456 var h_lines = [];
457 for (var pos in highlight_ranges) {
457 for (var pos in highlight_ranges) {
458 var _range = highlight_ranges[pos].split('-');
458 var _range = highlight_ranges[pos].split('-');
459 if (_range.length === 2) {
459 if (_range.length === 2) {
460 var start = parseInt(_range[0]);
460 var start = parseInt(_range[0]);
461 var end = parseInt(_range[1]);
461 var end = parseInt(_range[1]);
462 if (start < end) {
462 if (start < end) {
463 for (var i = start; i <= end; i++) {
463 for (var i = start; i <= end; i++) {
464 h_lines.push(i);
464 h_lines.push(i);
465 }
465 }
466 }
466 }
467 }
467 }
468 else {
468 else {
469 h_lines.push(parseInt(highlight_ranges[pos]));
469 h_lines.push(parseInt(highlight_ranges[pos]));
470 }
470 }
471 }
471 }
472 for (pos in h_lines) {
472 for (pos in h_lines) {
473 var line_td = $('td.cb-lineno#L' + h_lines[pos]);
473 var line_td = $('td.cb-lineno#L' + h_lines[pos]);
474 if (line_td.length) {
474 if (line_td.length) {
475 highlightable_line_tds.push(line_td);
475 highlightable_line_tds.push(line_td);
476 }
476 }
477 }
477 }
478 }
478 }
479
479
480 // now check a direct id reference (diff page)
480 // now check a direct id reference (diff page)
481 if ($(loc).length && $(loc).hasClass('cb-lineno')) {
481 if ($(loc).length && $(loc).hasClass('cb-lineno')) {
482 highlightable_line_tds.push($(loc));
482 highlightable_line_tds.push($(loc));
483 }
483 }
484 $.each(highlightable_line_tds, function (i, $td) {
484 $.each(highlightable_line_tds, function (i, $td) {
485 $td.addClass('cb-line-selected'); // line number td
485 $td.addClass('cb-line-selected'); // line number td
486 $td.prev().addClass('cb-line-selected'); // line data
486 $td.prev().addClass('cb-line-selected'); // line data
487 $td.next().addClass('cb-line-selected'); // line content
487 $td.next().addClass('cb-line-selected'); // line content
488 });
488 });
489
489
490 if (highlightable_line_tds.length) {
490 if (highlightable_line_tds.length) {
491 var $first_line_td = highlightable_line_tds[0];
491 var $first_line_td = highlightable_line_tds[0];
492 scrollToElement($first_line_td);
492 scrollToElement($first_line_td);
493 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
493 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
494 td: $first_line_td,
494 td: $first_line_td,
495 remainder: result.remainder
495 remainder: result.remainder
496 });
496 });
497 }
497 }
498 }
498 }
499 }
499 }
500 collapsableContent();
500 collapsableContent();
501 });
501 });
502
503 var feedLifetimeOptions = function(query, initialData){
504 var data = {results: []};
505 var isQuery = typeof query.term !== 'undefined';
506
507 var section = _gettext('Lifetime');
508 var children = [];
509
510 //filter results
511 $.each(initialData.results, function(idx, value) {
512
513 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
514 children.push({
515 'id': this.id,
516 'text': this.text
517 })
518 }
519
520 });
521 data.results.push({
522 'text': section,
523 'children': children
524 });
525
526 if (isQuery) {
527
528 var now = moment.utc();
529
530 var parseQuery = function(entry, now){
531 var fmt = 'DD/MM/YYYY H:mm';
532 var parsed = moment.utc(entry, fmt);
533 var diffInMin = parsed.diff(now, 'minutes');
534
535 if (diffInMin > 0){
536 return {
537 id: diffInMin,
538 text: parsed.format(fmt)
539 }
540 } else {
541 return {
542 id: undefined,
543 text: parsed.format('DD/MM/YYYY') + ' ' + _gettext('date not in future')
544 }
545 }
546
547
548 };
549
550 data.results.push({
551 'text': _gettext('Specified expiration date'),
552 'children': [{
553 'id': parseQuery(query.term, now).id,
554 'text': parseQuery(query.term, now).text
555 }]
556 });
557 }
558
559 query.callback(data);
560 };
@@ -1,160 +1,180 b''
1 <div class="panel panel-default">
1 <div class="panel panel-default">
2 <div class="panel-heading">
2 <div class="panel-heading">
3 <h3 class="panel-title">${_('Authentication Tokens')}</h3>
3 <h3 class="panel-title">${_('Authentication Tokens')}</h3>
4 </div>
4 </div>
5 <div class="panel-body">
5 <div class="panel-body">
6 <div class="apikeys_wrap">
6 <div class="apikeys_wrap">
7 <p>
7 <p>
8 ${_('Each token can have a role. Token with a role can be used only in given context, '
8 ${_('Each token can have a role. Token with a role can be used only in given context, '
9 'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
9 'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
10 </p>
10 </p>
11 <table class="rctable auth_tokens">
11 <table class="rctable auth_tokens">
12 <tr>
12 <tr>
13 <th>${_('Token')}</th>
13 <th>${_('Token')}</th>
14 <th>${_('Scope')}</th>
14 <th>${_('Scope')}</th>
15 <th>${_('Description')}</th>
15 <th>${_('Description')}</th>
16 <th>${_('Role')}</th>
16 <th>${_('Role')}</th>
17 <th>${_('Expiration')}</th>
17 <th>${_('Expiration')}</th>
18 <th>${_('Action')}</th>
18 <th>${_('Action')}</th>
19 </tr>
19 </tr>
20 %if c.user_auth_tokens:
20 %if c.user_auth_tokens:
21 %for auth_token in c.user_auth_tokens:
21 %for auth_token in c.user_auth_tokens:
22 <tr class="${'expired' if auth_token.expired else ''}">
22 <tr class="${'expired' if auth_token.expired else ''}">
23 <td class="truncate-wrap td-authtoken">
23 <td class="truncate-wrap td-authtoken">
24 <div class="user_auth_tokens truncate autoexpand">
24 <div class="user_auth_tokens truncate autoexpand">
25 <code>${auth_token.api_key}</code>
25 <code>${auth_token.api_key}</code>
26 </div>
26 </div>
27 </td>
27 </td>
28 <td class="td">${auth_token.scope_humanized}</td>
28 <td class="td">${auth_token.scope_humanized}</td>
29 <td class="td-wrap">${auth_token.description}</td>
29 <td class="td-wrap">${auth_token.description}</td>
30 <td class="td-tags">
30 <td class="td-tags">
31 <span class="tag disabled">${auth_token.role_humanized}</span>
31 <span class="tag disabled">${auth_token.role_humanized}</span>
32 </td>
32 </td>
33 <td class="td-exp">
33 <td class="td-exp">
34 %if auth_token.expires == -1:
34 %if auth_token.expires == -1:
35 ${_('never')}
35 ${_('never')}
36 %else:
36 %else:
37 %if auth_token.expired:
37 %if auth_token.expired:
38 <span style="text-decoration: line-through">${h.age_component(h.time_to_utcdatetime(auth_token.expires))}</span>
38 <span style="text-decoration: line-through">${h.age_component(h.time_to_utcdatetime(auth_token.expires))}</span>
39 %else:
39 %else:
40 ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
40 ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
41 %endif
41 %endif
42 %endif
42 %endif
43 </td>
43 </td>
44 <td class="td-action">
44 <td class="td-action">
45 ${h.secure_form(h.route_path('my_account_auth_tokens_delete'), method='POST', request=request)}
45 ${h.secure_form(h.route_path('my_account_auth_tokens_delete'), method='POST', request=request)}
46 ${h.hidden('del_auth_token', auth_token.user_api_key_id)}
46 ${h.hidden('del_auth_token', auth_token.user_api_key_id)}
47 <button class="btn btn-link btn-danger" type="submit"
47 <button class="btn btn-link btn-danger" type="submit"
48 onclick="return confirm('${_('Confirm to remove this auth token: %s') % auth_token.token_obfuscated}');">
48 onclick="return confirm('${_('Confirm to remove this auth token: %s') % auth_token.token_obfuscated}');">
49 ${_('Delete')}
49 ${_('Delete')}
50 </button>
50 </button>
51 ${h.end_form()}
51 ${h.end_form()}
52 </td>
52 </td>
53 </tr>
53 </tr>
54 %endfor
54 %endfor
55 %else:
55 %else:
56 <tr><td><div class="ip">${_('No additional auth tokens specified')}</div></td></tr>
56 <tr><td><div class="ip">${_('No additional auth tokens specified')}</div></td></tr>
57 %endif
57 %endif
58 </table>
58 </table>
59 </div>
59 </div>
60
60
61 <div class="user_auth_tokens">
61 <div class="user_auth_tokens">
62 ${h.secure_form(h.route_path('my_account_auth_tokens_add'), method='POST', request=request)}
62 ${h.secure_form(h.route_path('my_account_auth_tokens_add'), method='POST', request=request)}
63 <div class="form form-vertical">
63 <div class="form form-vertical">
64 <!-- fields -->
64 <!-- fields -->
65 <div class="fields">
65 <div class="fields">
66 <div class="field">
66 <div class="field">
67 <div class="label">
67 <div class="label">
68 <label for="new_email">${_('New authentication token')}:</label>
68 <label for="new_email">${_('New authentication token')}:</label>
69 </div>
69 </div>
70 <div class="input">
70 <div class="input">
71 ${h.text('description', class_='medium', placeholder=_('Description'))}
71 ${h.text('description', class_='medium', placeholder=_('Description'))}
72 ${h.select('lifetime', '', c.lifetime_options)}
72 ${h.hidden('lifetime')}
73 ${h.select('role', '', c.role_options)}
73 ${h.select('role', '', c.role_options)}
74
74
75 % if c.allow_scoped_tokens:
75 % if c.allow_scoped_tokens:
76 ${h.hidden('scope_repo_id')}
76 ${h.hidden('scope_repo_id')}
77 % else:
77 % else:
78 ${h.select('scope_repo_id_disabled', '', ['Scopes available in EE edition'], disabled='disabled')}
78 ${h.select('scope_repo_id_disabled', '', ['Scopes available in EE edition'], disabled='disabled')}
79 % endif
79 % endif
80 </div>
80 </div>
81 <p class="help-block">
81 <p class="help-block">
82 ${_('Repository scope works only with tokens with VCS type.')}
82 ${_('Repository scope works only with tokens with VCS type.')}
83 </p>
83 </p>
84 </div>
84 </div>
85 <div class="buttons">
85 <div class="buttons">
86 ${h.submit('save',_('Add'),class_="btn")}
86 ${h.submit('save',_('Add'),class_="btn")}
87 ${h.reset('reset',_('Reset'),class_="btn")}
87 ${h.reset('reset',_('Reset'),class_="btn")}
88 </div>
88 </div>
89 </div>
89 </div>
90 </div>
90 </div>
91 ${h.end_form()}
91 ${h.end_form()}
92 </div>
92 </div>
93 </div>
93 </div>
94 </div>
94 </div>
95 <script>
95 <script>
96 $(document).ready(function(){
96 $(document).ready(function(){
97
97
98 var select2Options = {
98 var select2Options = {
99 'containerCssClass': "drop-menu",
99 'containerCssClass': "drop-menu",
100 'dropdownCssClass': "drop-menu-dropdown",
100 'dropdownCssClass': "drop-menu-dropdown",
101 'dropdownAutoWidth': true
101 'dropdownAutoWidth': true
102 };
102 };
103 $("#lifetime").select2(select2Options);
104 $("#role").select2(select2Options);
103 $("#role").select2(select2Options);
105
104
105
106 var preloadData = {
107 results: [
108 % for entry in c.lifetime_values:
109 {id:${entry[0]}, text:"${entry[1]}"}${'' if loop.last else ','}
110 % endfor
111 ]
112 };
113
114 $("#lifetime").select2({
115 containerCssClass: "drop-menu",
116 dropdownCssClass: "drop-menu-dropdown",
117 dropdownAutoWidth: true,
118 data: preloadData,
119 placeholder: ${_('Select or enter expiration date')},
120 query: function(query) {
121 feedLifetimeOptions(query, preloadData);
122 }
123 });
124
125
106 var repoFilter = function(data) {
126 var repoFilter = function(data) {
107 var results = [];
127 var results = [];
108
128
109 if (!data.results[0]) {
129 if (!data.results[0]) {
110 return data
130 return data
111 }
131 }
112
132
113 $.each(data.results[0].children, function() {
133 $.each(data.results[0].children, function() {
114 // replace name to ID for submision
134 // replace name to ID for submision
115 this.id = this.obj.repo_id;
135 this.id = this.obj.repo_id;
116 results.push(this);
136 results.push(this);
117 });
137 });
118
138
119 data.results[0].children = results;
139 data.results[0].children = results;
120 return data;
140 return data;
121 };
141 };
122
142
123 $("#scope_repo_id_disabled").select2(select2Options);
143 $("#scope_repo_id_disabled").select2(select2Options);
124
144
125 $("#scope_repo_id").select2({
145 $("#scope_repo_id").select2({
126 cachedDataSource: {},
146 cachedDataSource: {},
127 minimumInputLength: 2,
147 minimumInputLength: 2,
128 placeholder: "${_('repository scope')}",
148 placeholder: "${_('repository scope')}",
129 dropdownAutoWidth: true,
149 dropdownAutoWidth: true,
130 containerCssClass: "drop-menu",
150 containerCssClass: "drop-menu",
131 dropdownCssClass: "drop-menu-dropdown",
151 dropdownCssClass: "drop-menu-dropdown",
132 formatResult: formatResult,
152 formatResult: formatResult,
133 query: $.debounce(250, function(query){
153 query: $.debounce(250, function(query){
134 self = this;
154 self = this;
135 var cacheKey = query.term;
155 var cacheKey = query.term;
136 var cachedData = self.cachedDataSource[cacheKey];
156 var cachedData = self.cachedDataSource[cacheKey];
137
157
138 if (cachedData) {
158 if (cachedData) {
139 query.callback({results: cachedData.results});
159 query.callback({results: cachedData.results});
140 } else {
160 } else {
141 $.ajax({
161 $.ajax({
142 url: pyroutes.url('repo_list_data'),
162 url: pyroutes.url('repo_list_data'),
143 data: {'query': query.term},
163 data: {'query': query.term},
144 dataType: 'json',
164 dataType: 'json',
145 type: 'GET',
165 type: 'GET',
146 success: function(data) {
166 success: function(data) {
147 data = repoFilter(data);
167 data = repoFilter(data);
148 self.cachedDataSource[cacheKey] = data;
168 self.cachedDataSource[cacheKey] = data;
149 query.callback({results: data.results});
169 query.callback({results: data.results});
150 },
170 },
151 error: function(data, textStatus, errorThrown) {
171 error: function(data, textStatus, errorThrown) {
152 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
172 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
153 }
173 }
154 })
174 })
155 }
175 }
156 })
176 })
157 });
177 });
158
178
159 });
179 });
160 </script>
180 </script>
@@ -1,157 +1,176 b''
1 <div class="panel panel-default">
1 <div class="panel panel-default">
2 <div class="panel-heading">
2 <div class="panel-heading">
3 <h3 class="panel-title">${_('Authentication Tokens')}</h3>
3 <h3 class="panel-title">${_('Authentication Tokens')}</h3>
4 </div>
4 </div>
5 <div class="panel-body">
5 <div class="panel-body">
6 <div class="apikeys_wrap">
6 <div class="apikeys_wrap">
7 <p>
7 <p>
8 ${_('Each token can have a role. Token with a role can be used only in given context, '
8 ${_('Each token can have a role. Token with a role can be used only in given context, '
9 'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
9 'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
10 </p>
10 </p>
11 <table class="rctable auth_tokens">
11 <table class="rctable auth_tokens">
12 <tr>
12 <tr>
13 <th>${_('Token')}</th>
13 <th>${_('Token')}</th>
14 <th>${_('Scope')}</th>
14 <th>${_('Scope')}</th>
15 <th>${_('Description')}</th>
15 <th>${_('Description')}</th>
16 <th>${_('Role')}</th>
16 <th>${_('Role')}</th>
17 <th>${_('Expiration')}</th>
17 <th>${_('Expiration')}</th>
18 <th>${_('Action')}</th>
18 <th>${_('Action')}</th>
19 </tr>
19 </tr>
20 %if c.user_auth_tokens:
20 %if c.user_auth_tokens:
21 %for auth_token in c.user_auth_tokens:
21 %for auth_token in c.user_auth_tokens:
22 <tr class="${'expired' if auth_token.expired else ''}">
22 <tr class="${'expired' if auth_token.expired else ''}">
23 <td class="truncate-wrap td-authtoken"><div class="user_auth_tokens truncate autoexpand"><code>${auth_token.api_key}</code></div></td>
23 <td class="truncate-wrap td-authtoken"><div class="user_auth_tokens truncate autoexpand"><code>${auth_token.api_key}</code></div></td>
24 <td class="td">${auth_token.scope_humanized}</td>
24 <td class="td">${auth_token.scope_humanized}</td>
25 <td class="td-wrap">${auth_token.description}</td>
25 <td class="td-wrap">${auth_token.description}</td>
26 <td class="td-tags">
26 <td class="td-tags">
27 <span class="tag disabled">${auth_token.role_humanized}</span>
27 <span class="tag disabled">${auth_token.role_humanized}</span>
28 </td>
28 </td>
29 <td class="td-exp">
29 <td class="td-exp">
30 %if auth_token.expires == -1:
30 %if auth_token.expires == -1:
31 ${_('never')}
31 ${_('never')}
32 %else:
32 %else:
33 %if auth_token.expired:
33 %if auth_token.expired:
34 <span style="text-decoration: line-through">${h.age_component(h.time_to_utcdatetime(auth_token.expires))}</span>
34 <span style="text-decoration: line-through">${h.age_component(h.time_to_utcdatetime(auth_token.expires))}</span>
35 %else:
35 %else:
36 ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
36 ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
37 %endif
37 %endif
38 %endif
38 %endif
39 </td>
39 </td>
40 <td class="td-action">
40 <td class="td-action">
41 ${h.secure_form(h.route_path('edit_user_auth_tokens_delete', user_id=c.user.user_id), method='POST', request=request)}
41 ${h.secure_form(h.route_path('edit_user_auth_tokens_delete', user_id=c.user.user_id), method='POST', request=request)}
42 ${h.hidden('del_auth_token', auth_token.user_api_key_id)}
42 ${h.hidden('del_auth_token', auth_token.user_api_key_id)}
43 <button class="btn btn-link btn-danger" type="submit"
43 <button class="btn btn-link btn-danger" type="submit"
44 onclick="return confirm('${_('Confirm to remove this auth token: %s') % auth_token.token_obfuscated}');">
44 onclick="return confirm('${_('Confirm to remove this auth token: %s') % auth_token.token_obfuscated}');">
45 ${_('Delete')}
45 ${_('Delete')}
46 </button>
46 </button>
47 ${h.end_form()}
47 ${h.end_form()}
48 </td>
48 </td>
49 </tr>
49 </tr>
50 %endfor
50 %endfor
51 %else:
51 %else:
52 <tr><td><div class="ip">${_('No additional auth tokens specified')}</div></td></tr>
52 <tr><td><div class="ip">${_('No additional auth tokens specified')}</div></td></tr>
53 %endif
53 %endif
54 </table>
54 </table>
55 </div>
55 </div>
56
56
57 <div class="user_auth_tokens">
57 <div class="user_auth_tokens">
58 ${h.secure_form(h.route_path('edit_user_auth_tokens_add', user_id=c.user.user_id), method='POST', request=request)}
58 ${h.secure_form(h.route_path('edit_user_auth_tokens_add', user_id=c.user.user_id), method='POST', request=request)}
59 <div class="form form-vertical">
59 <div class="form form-vertical">
60 <!-- fields -->
60 <!-- fields -->
61 <div class="fields">
61 <div class="fields">
62 <div class="field">
62 <div class="field">
63 <div class="label">
63 <div class="label">
64 <label for="new_email">${_('New authentication token')}:</label>
64 <label for="new_email">${_('New authentication token')}:</label>
65 </div>
65 </div>
66 <div class="input">
66 <div class="input">
67 ${h.text('description', class_='medium', placeholder=_('Description'))}
67 ${h.text('description', class_='medium', placeholder=_('Description'))}
68 ${h.select('lifetime', '', c.lifetime_options)}
68 ${h.hidden('lifetime')}
69 ${h.select('role', '', c.role_options)}
69 ${h.select('role', '', c.role_options)}
70
70
71 % if c.allow_scoped_tokens:
71 % if c.allow_scoped_tokens:
72 ${h.hidden('scope_repo_id')}
72 ${h.hidden('scope_repo_id')}
73 % else:
73 % else:
74 ${h.select('scope_repo_id_disabled', '', ['Scopes available in EE edition'], disabled='disabled')}
74 ${h.select('scope_repo_id_disabled', '', ['Scopes available in EE edition'], disabled='disabled')}
75 % endif
75 % endif
76 </div>
76 </div>
77 <p class="help-block">
77 <p class="help-block">
78 ${_('Repository scope works only with tokens with VCS type.')}
78 ${_('Repository scope works only with tokens with VCS type.')}
79 </p>
79 </p>
80 </div>
80 </div>
81 <div class="buttons">
81 <div class="buttons">
82 ${h.submit('save',_('Add'),class_="btn")}
82 ${h.submit('save',_('Add'),class_="btn")}
83 ${h.reset('reset',_('Reset'),class_="btn")}
83 ${h.reset('reset',_('Reset'),class_="btn")}
84 </div>
84 </div>
85 </div>
85 </div>
86 </div>
86 </div>
87 ${h.end_form()}
87 ${h.end_form()}
88 </div>
88 </div>
89 </div>
89 </div>
90 </div>
90 </div>
91
91
92 <script>
92 <script>
93
93
94 $(document).ready(function(){
94 $(document).ready(function(){
95 var select2Options = {
95 var select2Options = {
96 'containerCssClass': "drop-menu",
96 'containerCssClass': "drop-menu",
97 'dropdownCssClass': "drop-menu-dropdown",
97 'dropdownCssClass': "drop-menu-dropdown",
98 'dropdownAutoWidth': true
98 'dropdownAutoWidth': true
99 };
99 };
100 $("#lifetime").select2(select2Options);
101 $("#role").select2(select2Options);
100 $("#role").select2(select2Options);
102
101
102 var preloadData = {
103 results: [
104 % for entry in c.lifetime_values:
105 {id:${entry[0]}, text:"${entry[1]}"}${'' if loop.last else ','}
106 % endfor
107 ]
108 };
109
110 $("#lifetime").select2({
111 containerCssClass: "drop-menu",
112 dropdownCssClass: "drop-menu-dropdown",
113 dropdownAutoWidth: true,
114 data: preloadData,
115 placeholder: ${_('Select or enter expiration date')},
116 query: function(query) {
117 feedLifetimeOptions(query, preloadData);
118 }
119 });
120
121
103 var repoFilter = function(data) {
122 var repoFilter = function(data) {
104 var results = [];
123 var results = [];
105
124
106 if (!data.results[0]) {
125 if (!data.results[0]) {
107 return data
126 return data
108 }
127 }
109
128
110 $.each(data.results[0].children, function() {
129 $.each(data.results[0].children, function() {
111 // replace name to ID for submision
130 // replace name to ID for submision
112 this.id = this.obj.repo_id;
131 this.id = this.obj.repo_id;
113 results.push(this);
132 results.push(this);
114 });
133 });
115
134
116 data.results[0].children = results;
135 data.results[0].children = results;
117 return data;
136 return data;
118 };
137 };
119
138
120 $("#scope_repo_id_disabled").select2(select2Options);
139 $("#scope_repo_id_disabled").select2(select2Options);
121
140
122 $("#scope_repo_id").select2({
141 $("#scope_repo_id").select2({
123 cachedDataSource: {},
142 cachedDataSource: {},
124 minimumInputLength: 2,
143 minimumInputLength: 2,
125 placeholder: "${_('repository scope')}",
144 placeholder: "${_('repository scope')}",
126 dropdownAutoWidth: true,
145 dropdownAutoWidth: true,
127 containerCssClass: "drop-menu",
146 containerCssClass: "drop-menu",
128 dropdownCssClass: "drop-menu-dropdown",
147 dropdownCssClass: "drop-menu-dropdown",
129 formatResult: formatResult,
148 formatResult: formatResult,
130 query: $.debounce(250, function(query){
149 query: $.debounce(250, function(query){
131 self = this;
150 self = this;
132 var cacheKey = query.term;
151 var cacheKey = query.term;
133 var cachedData = self.cachedDataSource[cacheKey];
152 var cachedData = self.cachedDataSource[cacheKey];
134
153
135 if (cachedData) {
154 if (cachedData) {
136 query.callback({results: cachedData.results});
155 query.callback({results: cachedData.results});
137 } else {
156 } else {
138 $.ajax({
157 $.ajax({
139 url: pyroutes.url('repo_list_data'),
158 url: pyroutes.url('repo_list_data'),
140 data: {'query': query.term},
159 data: {'query': query.term},
141 dataType: 'json',
160 dataType: 'json',
142 type: 'GET',
161 type: 'GET',
143 success: function(data) {
162 success: function(data) {
144 data = repoFilter(data);
163 data = repoFilter(data);
145 self.cachedDataSource[cacheKey] = data;
164 self.cachedDataSource[cacheKey] = data;
146 query.callback({results: data.results});
165 query.callback({results: data.results});
147 },
166 },
148 error: function(data, textStatus, errorThrown) {
167 error: function(data, textStatus, errorThrown) {
149 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
168 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
150 }
169 }
151 })
170 })
152 }
171 }
153 })
172 })
154 });
173 });
155
174
156 });
175 });
157 </script>
176 </script>
General Comments 0
You need to be logged in to leave comments. Login now