##// END OF EJS Templates
admin-users: replace the previous nullslast() with db independent solution....
marcink -
r1630:ff004025 default
parent child Browse files
Show More
@@ -1,317 +1,328 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
23
23 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
24 from pyramid.view import view_config
25 from pyramid.view import view_config
26 from sqlalchemy.sql.functions import coalesce
25
27
26 from rhodecode.lib.helpers import Page
28 from rhodecode.lib.helpers import Page
27 from rhodecode_tools.lib.ext_json import json
29 from rhodecode_tools.lib.ext_json import json
28
30
29 from rhodecode.apps._base import BaseAppView
31 from rhodecode.apps._base import BaseAppView
30 from rhodecode.lib.auth import (
32 from rhodecode.lib.auth import (
31 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
33 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
32 from rhodecode.lib import helpers as h
34 from rhodecode.lib import helpers as h
33 from rhodecode.lib.utils import PartialRenderer
35 from rhodecode.lib.utils import PartialRenderer
34 from rhodecode.lib.utils2 import safe_int, safe_unicode
36 from rhodecode.lib.utils2 import safe_int, safe_unicode
35 from rhodecode.model.auth_token import AuthTokenModel
37 from rhodecode.model.auth_token import AuthTokenModel
36 from rhodecode.model.user import UserModel
38 from rhodecode.model.user import UserModel
37 from rhodecode.model.user_group import UserGroupModel
39 from rhodecode.model.user_group import UserGroupModel
38 from rhodecode.model.db import User, or_
40 from rhodecode.model.db import User, or_
39 from rhodecode.model.meta import Session
41 from rhodecode.model.meta import Session
40
42
41 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
42
44
43
45
44 class AdminUsersView(BaseAppView):
46 class AdminUsersView(BaseAppView):
45 ALLOW_SCOPED_TOKENS = False
47 ALLOW_SCOPED_TOKENS = False
46 """
48 """
47 This view has alternative version inside EE, if modified please take a look
49 This view has alternative version inside EE, if modified please take a look
48 in there as well.
50 in there as well.
49 """
51 """
50
52
51 def load_default_context(self):
53 def load_default_context(self):
52 c = self._get_local_tmpl_context()
54 c = self._get_local_tmpl_context()
53 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
55 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
54 self._register_global_c(c)
56 self._register_global_c(c)
55 return c
57 return c
56
58
57 def _redirect_for_default_user(self, username):
59 def _redirect_for_default_user(self, username):
58 _ = self.request.translate
60 _ = self.request.translate
59 if username == User.DEFAULT_USER:
61 if username == User.DEFAULT_USER:
60 h.flash(_("You can't edit this user"), category='warning')
62 h.flash(_("You can't edit this user"), category='warning')
61 # TODO(marcink): redirect to 'users' admin panel once this
63 # TODO(marcink): redirect to 'users' admin panel once this
62 # is a pyramid view
64 # is a pyramid view
63 raise HTTPFound('/')
65 raise HTTPFound('/')
64
66
65 def _extract_ordering(self, request):
67 def _extract_ordering(self, request):
66 column_index = safe_int(request.GET.get('order[0][column]'))
68 column_index = safe_int(request.GET.get('order[0][column]'))
67 order_dir = request.GET.get(
69 order_dir = request.GET.get(
68 'order[0][dir]', 'desc')
70 'order[0][dir]', 'desc')
69 order_by = request.GET.get(
71 order_by = request.GET.get(
70 'columns[%s][data][sort]' % column_index, 'name_raw')
72 'columns[%s][data][sort]' % column_index, 'name_raw')
71
73
72 # translate datatable to DB columns
74 # translate datatable to DB columns
73 order_by = {
75 order_by = {
74 'first_name': 'name',
76 'first_name': 'name',
75 'last_name': 'lastname',
77 'last_name': 'lastname',
76 }.get(order_by) or order_by
78 }.get(order_by) or order_by
77
79
78 search_q = request.GET.get('search[value]')
80 search_q = request.GET.get('search[value]')
79 return search_q, order_by, order_dir
81 return search_q, order_by, order_dir
80
82
81 def _extract_chunk(self, request):
83 def _extract_chunk(self, request):
82 start = safe_int(request.GET.get('start'), 0)
84 start = safe_int(request.GET.get('start'), 0)
83 length = safe_int(request.GET.get('length'), 25)
85 length = safe_int(request.GET.get('length'), 25)
84 draw = safe_int(request.GET.get('draw'))
86 draw = safe_int(request.GET.get('draw'))
85 return draw, start, length
87 return draw, start, length
86
88
87 @HasPermissionAllDecorator('hg.admin')
89 @HasPermissionAllDecorator('hg.admin')
88 @view_config(
90 @view_config(
89 route_name='users', request_method='GET',
91 route_name='users', request_method='GET',
90 renderer='rhodecode:templates/admin/users/users.mako')
92 renderer='rhodecode:templates/admin/users/users.mako')
91 def users_list(self):
93 def users_list(self):
92 c = self.load_default_context()
94 c = self.load_default_context()
93 return self._get_template_context(c)
95 return self._get_template_context(c)
94
96
95 @HasPermissionAllDecorator('hg.admin')
97 @HasPermissionAllDecorator('hg.admin')
96 @view_config(
98 @view_config(
97 # renderer defined below
99 # renderer defined below
98 route_name='users_data', request_method='GET', renderer='json',
100 route_name='users_data', request_method='GET', renderer='json',
99 xhr=True)
101 xhr=True)
100 def users_list_data(self):
102 def users_list_data(self):
101 draw, start, limit = self._extract_chunk(self.request)
103 draw, start, limit = self._extract_chunk(self.request)
102 search_q, order_by, order_dir = self._extract_ordering(self.request)
104 search_q, order_by, order_dir = self._extract_ordering(self.request)
103
105
104 _render = PartialRenderer('data_table/_dt_elements.mako')
106 _render = PartialRenderer('data_table/_dt_elements.mako')
105
107
106 def user_actions(user_id, username):
108 def user_actions(user_id, username):
107 return _render("user_actions", user_id, username)
109 return _render("user_actions", user_id, username)
108
110
109 users_data_total_count = User.query()\
111 users_data_total_count = User.query()\
110 .filter(User.username != User.DEFAULT_USER) \
112 .filter(User.username != User.DEFAULT_USER) \
111 .count()
113 .count()
112
114
113 # json generate
115 # json generate
114 base_q = User.query().filter(User.username != User.DEFAULT_USER)
116 base_q = User.query().filter(User.username != User.DEFAULT_USER)
115
117
116 if search_q:
118 if search_q:
117 like_expression = u'%{}%'.format(safe_unicode(search_q))
119 like_expression = u'%{}%'.format(safe_unicode(search_q))
118 base_q = base_q.filter(or_(
120 base_q = base_q.filter(or_(
119 User.username.ilike(like_expression),
121 User.username.ilike(like_expression),
120 User._email.ilike(like_expression),
122 User._email.ilike(like_expression),
121 User.name.ilike(like_expression),
123 User.name.ilike(like_expression),
122 User.lastname.ilike(like_expression),
124 User.lastname.ilike(like_expression),
123 ))
125 ))
124
126
125 users_data_total_filtered_count = base_q.count()
127 users_data_total_filtered_count = base_q.count()
126
128
127 sort_col = getattr(User, order_by, None)
129 sort_col = getattr(User, order_by, None)
128 if sort_col and order_dir == 'asc':
130 if sort_col:
129 base_q = base_q.order_by(sort_col.asc())
131 if order_dir == 'asc':
130 elif sort_col:
132 # handle null values properly to order by NULL last
131 base_q = base_q.order_by(sort_col.desc())
133 if order_by in ['last_activity']:
134 sort_col = coalesce(sort_col, datetime.date.max)
135 sort_col = sort_col.asc()
136 else:
137 # handle null values properly to order by NULL last
138 if order_by in ['last_activity']:
139 sort_col = coalesce(sort_col, datetime.date.min)
140 sort_col = sort_col.desc()
132
141
142 base_q = base_q.order_by(sort_col)
133 base_q = base_q.offset(start).limit(limit)
143 base_q = base_q.offset(start).limit(limit)
144
134 users_list = base_q.all()
145 users_list = base_q.all()
135
146
136 users_data = []
147 users_data = []
137 for user in users_list:
148 for user in users_list:
138 users_data.append({
149 users_data.append({
139 "username": h.gravatar_with_user(user.username),
150 "username": h.gravatar_with_user(user.username),
140 "email": user.email,
151 "email": user.email,
141 "first_name": h.escape(user.name),
152 "first_name": h.escape(user.name),
142 "last_name": h.escape(user.lastname),
153 "last_name": h.escape(user.lastname),
143 "last_login": h.format_date(user.last_login),
154 "last_login": h.format_date(user.last_login),
144 "last_activity": h.format_date(user.last_activity),
155 "last_activity": h.format_date(user.last_activity),
145 "active": h.bool2icon(user.active),
156 "active": h.bool2icon(user.active),
146 "active_raw": user.active,
157 "active_raw": user.active,
147 "admin": h.bool2icon(user.admin),
158 "admin": h.bool2icon(user.admin),
148 "extern_type": user.extern_type,
159 "extern_type": user.extern_type,
149 "extern_name": user.extern_name,
160 "extern_name": user.extern_name,
150 "action": user_actions(user.user_id, user.username),
161 "action": user_actions(user.user_id, user.username),
151 })
162 })
152
163
153 data = ({
164 data = ({
154 'draw': draw,
165 'draw': draw,
155 'data': users_data,
166 'data': users_data,
156 'recordsTotal': users_data_total_count,
167 'recordsTotal': users_data_total_count,
157 'recordsFiltered': users_data_total_filtered_count,
168 'recordsFiltered': users_data_total_filtered_count,
158 })
169 })
159
170
160 return data
171 return data
161
172
162 @LoginRequired()
173 @LoginRequired()
163 @HasPermissionAllDecorator('hg.admin')
174 @HasPermissionAllDecorator('hg.admin')
164 @view_config(
175 @view_config(
165 route_name='edit_user_auth_tokens', request_method='GET',
176 route_name='edit_user_auth_tokens', request_method='GET',
166 renderer='rhodecode:templates/admin/users/user_edit.mako')
177 renderer='rhodecode:templates/admin/users/user_edit.mako')
167 def auth_tokens(self):
178 def auth_tokens(self):
168 _ = self.request.translate
179 _ = self.request.translate
169 c = self.load_default_context()
180 c = self.load_default_context()
170
181
171 user_id = self.request.matchdict.get('user_id')
182 user_id = self.request.matchdict.get('user_id')
172 c.user = User.get_or_404(user_id, pyramid_exc=True)
183 c.user = User.get_or_404(user_id, pyramid_exc=True)
173 self._redirect_for_default_user(c.user.username)
184 self._redirect_for_default_user(c.user.username)
174
185
175 c.active = 'auth_tokens'
186 c.active = 'auth_tokens'
176
187
177 c.lifetime_values = [
188 c.lifetime_values = [
178 (str(-1), _('forever')),
189 (str(-1), _('forever')),
179 (str(5), _('5 minutes')),
190 (str(5), _('5 minutes')),
180 (str(60), _('1 hour')),
191 (str(60), _('1 hour')),
181 (str(60 * 24), _('1 day')),
192 (str(60 * 24), _('1 day')),
182 (str(60 * 24 * 30), _('1 month')),
193 (str(60 * 24 * 30), _('1 month')),
183 ]
194 ]
184 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
195 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
185 c.role_values = [
196 c.role_values = [
186 (x, AuthTokenModel.cls._get_role_name(x))
197 (x, AuthTokenModel.cls._get_role_name(x))
187 for x in AuthTokenModel.cls.ROLES]
198 for x in AuthTokenModel.cls.ROLES]
188 c.role_options = [(c.role_values, _("Role"))]
199 c.role_options = [(c.role_values, _("Role"))]
189 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
200 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
190 c.user.user_id, show_expired=True)
201 c.user.user_id, show_expired=True)
191 return self._get_template_context(c)
202 return self._get_template_context(c)
192
203
193 def maybe_attach_token_scope(self, token):
204 def maybe_attach_token_scope(self, token):
194 # implemented in EE edition
205 # implemented in EE edition
195 pass
206 pass
196
207
197 @LoginRequired()
208 @LoginRequired()
198 @HasPermissionAllDecorator('hg.admin')
209 @HasPermissionAllDecorator('hg.admin')
199 @CSRFRequired()
210 @CSRFRequired()
200 @view_config(
211 @view_config(
201 route_name='edit_user_auth_tokens_add', request_method='POST')
212 route_name='edit_user_auth_tokens_add', request_method='POST')
202 def auth_tokens_add(self):
213 def auth_tokens_add(self):
203 _ = self.request.translate
214 _ = self.request.translate
204 c = self.load_default_context()
215 c = self.load_default_context()
205
216
206 user_id = self.request.matchdict.get('user_id')
217 user_id = self.request.matchdict.get('user_id')
207 c.user = User.get_or_404(user_id, pyramid_exc=True)
218 c.user = User.get_or_404(user_id, pyramid_exc=True)
208 self._redirect_for_default_user(c.user.username)
219 self._redirect_for_default_user(c.user.username)
209
220
210 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
221 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
211 description = self.request.POST.get('description')
222 description = self.request.POST.get('description')
212 role = self.request.POST.get('role')
223 role = self.request.POST.get('role')
213
224
214 token = AuthTokenModel().create(
225 token = AuthTokenModel().create(
215 c.user.user_id, description, lifetime, role)
226 c.user.user_id, description, lifetime, role)
216 self.maybe_attach_token_scope(token)
227 self.maybe_attach_token_scope(token)
217 Session().commit()
228 Session().commit()
218
229
219 h.flash(_("Auth token successfully created"), category='success')
230 h.flash(_("Auth token successfully created"), category='success')
220 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
231 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
221
232
222 @LoginRequired()
233 @LoginRequired()
223 @HasPermissionAllDecorator('hg.admin')
234 @HasPermissionAllDecorator('hg.admin')
224 @CSRFRequired()
235 @CSRFRequired()
225 @view_config(
236 @view_config(
226 route_name='edit_user_auth_tokens_delete', request_method='POST')
237 route_name='edit_user_auth_tokens_delete', request_method='POST')
227 def auth_tokens_delete(self):
238 def auth_tokens_delete(self):
228 _ = self.request.translate
239 _ = self.request.translate
229 c = self.load_default_context()
240 c = self.load_default_context()
230
241
231 user_id = self.request.matchdict.get('user_id')
242 user_id = self.request.matchdict.get('user_id')
232 c.user = User.get_or_404(user_id, pyramid_exc=True)
243 c.user = User.get_or_404(user_id, pyramid_exc=True)
233 self._redirect_for_default_user(c.user.username)
244 self._redirect_for_default_user(c.user.username)
234
245
235 del_auth_token = self.request.POST.get('del_auth_token')
246 del_auth_token = self.request.POST.get('del_auth_token')
236
247
237 if del_auth_token:
248 if del_auth_token:
238 AuthTokenModel().delete(del_auth_token, c.user.user_id)
249 AuthTokenModel().delete(del_auth_token, c.user.user_id)
239 Session().commit()
250 Session().commit()
240 h.flash(_("Auth token successfully deleted"), category='success')
251 h.flash(_("Auth token successfully deleted"), category='success')
241
252
242 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
253 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
243
254
244 @LoginRequired()
255 @LoginRequired()
245 @HasPermissionAllDecorator('hg.admin')
256 @HasPermissionAllDecorator('hg.admin')
246 @view_config(
257 @view_config(
247 route_name='edit_user_groups_management', request_method='GET',
258 route_name='edit_user_groups_management', request_method='GET',
248 renderer='rhodecode:templates/admin/users/user_edit.mako')
259 renderer='rhodecode:templates/admin/users/user_edit.mako')
249 def groups_management(self):
260 def groups_management(self):
250 c = self.load_default_context()
261 c = self.load_default_context()
251
262
252 user_id = self.request.matchdict.get('user_id')
263 user_id = self.request.matchdict.get('user_id')
253 c.user = User.get_or_404(user_id, pyramid_exc=True)
264 c.user = User.get_or_404(user_id, pyramid_exc=True)
254 c.data = c.user.group_member
265 c.data = c.user.group_member
255 self._redirect_for_default_user(c.user.username)
266 self._redirect_for_default_user(c.user.username)
256 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group) for group in c.user.group_member]
267 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group) for group in c.user.group_member]
257 c.groups = json.dumps(groups)
268 c.groups = json.dumps(groups)
258 c.active = 'groups'
269 c.active = 'groups'
259
270
260 return self._get_template_context(c)
271 return self._get_template_context(c)
261
272
262 @LoginRequired()
273 @LoginRequired()
263 @HasPermissionAllDecorator('hg.admin')
274 @HasPermissionAllDecorator('hg.admin')
264 @view_config(
275 @view_config(
265 route_name='edit_user_groups_management_updates', request_method='POST')
276 route_name='edit_user_groups_management_updates', request_method='POST')
266 def groups_management_updates(self):
277 def groups_management_updates(self):
267 _ = self.request.translate
278 _ = self.request.translate
268 c = self.load_default_context()
279 c = self.load_default_context()
269
280
270 user_id = self.request.matchdict.get('user_id')
281 user_id = self.request.matchdict.get('user_id')
271 c.user = User.get_or_404(user_id, pyramid_exc=True)
282 c.user = User.get_or_404(user_id, pyramid_exc=True)
272 self._redirect_for_default_user(c.user.username)
283 self._redirect_for_default_user(c.user.username)
273
284
274 users_groups = set(self.request.POST.getall('users_group_id'))
285 users_groups = set(self.request.POST.getall('users_group_id'))
275 users_groups_model = []
286 users_groups_model = []
276
287
277 for ugid in users_groups:
288 for ugid in users_groups:
278 users_groups_model.append(UserGroupModel().get_group(safe_int(ugid)))
289 users_groups_model.append(UserGroupModel().get_group(safe_int(ugid)))
279 user_group_model = UserGroupModel()
290 user_group_model = UserGroupModel()
280 user_group_model.change_groups(c.user, users_groups_model)
291 user_group_model.change_groups(c.user, users_groups_model)
281
292
282 Session().commit()
293 Session().commit()
283 c.active = 'user_groups_management'
294 c.active = 'user_groups_management'
284 h.flash(_("Groups successfully changed"), category='success')
295 h.flash(_("Groups successfully changed"), category='success')
285
296
286 return HTTPFound(h.route_path(
297 return HTTPFound(h.route_path(
287 'edit_user_groups_management', user_id=user_id))
298 'edit_user_groups_management', user_id=user_id))
288
299
289 @LoginRequired()
300 @LoginRequired()
290 @HasPermissionAllDecorator('hg.admin')
301 @HasPermissionAllDecorator('hg.admin')
291 @view_config(
302 @view_config(
292 route_name='edit_user_audit_logs', request_method='GET',
303 route_name='edit_user_audit_logs', request_method='GET',
293 renderer='rhodecode:templates/admin/users/user_edit.mako')
304 renderer='rhodecode:templates/admin/users/user_edit.mako')
294 def user_audit_logs(self):
305 def user_audit_logs(self):
295 _ = self.request.translate
306 _ = self.request.translate
296 c = self.load_default_context()
307 c = self.load_default_context()
297
308
298 user_id = self.request.matchdict.get('user_id')
309 user_id = self.request.matchdict.get('user_id')
299 c.user = User.get_or_404(user_id, pyramid_exc=True)
310 c.user = User.get_or_404(user_id, pyramid_exc=True)
300 self._redirect_for_default_user(c.user.username)
311 self._redirect_for_default_user(c.user.username)
301 c.active = 'audit'
312 c.active = 'audit'
302
313
303 p = safe_int(self.request.GET.get('page', 1), 1)
314 p = safe_int(self.request.GET.get('page', 1), 1)
304
315
305 filter_term = self.request.GET.get('filter')
316 filter_term = self.request.GET.get('filter')
306 c.user_log = UserModel().get_user_log(c.user, filter_term)
317 c.user_log = UserModel().get_user_log(c.user, filter_term)
307
318
308 def url_generator(**kw):
319 def url_generator(**kw):
309 if filter_term:
320 if filter_term:
310 kw['filter'] = filter_term
321 kw['filter'] = filter_term
311 return self.request.current_route_path(_query=kw)
322 return self.request.current_route_path(_query=kw)
312
323
313 c.user_log = Page(c.user_log, page=p, items_per_page=10,
324 c.user_log = Page(c.user_log, page=p, items_per_page=10,
314 url=url_generator)
325 url=url_generator)
315 c.filter_term = filter_term
326 c.filter_term = filter_term
316 return self._get_template_context(c)
327 return self._get_template_context(c)
317
328
General Comments 0
You need to be logged in to leave comments. Login now