##// END OF EJS Templates
users: add sorting by last activity to the new users grid.
marcink -
r1547:ed2ccabe default
parent child Browse files
Show More
@@ -1,238 +1,237 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
22
23 from pyramid.httpexceptions import HTTPFound
23 from pyramid.httpexceptions import HTTPFound
24 from pyramid.view import view_config
24 from pyramid.view import view_config
25
25
26 from rhodecode.apps._base import BaseAppView
26 from rhodecode.apps._base import BaseAppView
27 from rhodecode.lib.auth import (
27 from rhodecode.lib.auth import (
28 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
28 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
29 from rhodecode.lib import helpers as h
29 from rhodecode.lib import helpers as h
30 from rhodecode.lib.utils import PartialRenderer
30 from rhodecode.lib.utils import PartialRenderer
31 from rhodecode.lib.utils2 import safe_int, safe_unicode
31 from rhodecode.lib.utils2 import safe_int, safe_unicode
32 from rhodecode.model.auth_token import AuthTokenModel
32 from rhodecode.model.auth_token import AuthTokenModel
33 from rhodecode.model.db import User, or_
33 from rhodecode.model.db import User, or_
34 from rhodecode.model.meta import Session
34 from rhodecode.model.meta import Session
35
35
36 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
37
37
38
38
39 class AdminUsersView(BaseAppView):
39 class AdminUsersView(BaseAppView):
40 ALLOW_SCOPED_TOKENS = False
40 ALLOW_SCOPED_TOKENS = False
41 """
41 """
42 This view has alternative version inside EE, if modified please take a look
42 This view has alternative version inside EE, if modified please take a look
43 in there as well.
43 in there as well.
44 """
44 """
45
45
46 def load_default_context(self):
46 def load_default_context(self):
47 c = self._get_local_tmpl_context()
47 c = self._get_local_tmpl_context()
48 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
48 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
49 self._register_global_c(c)
49 self._register_global_c(c)
50 return c
50 return c
51
51
52 def _redirect_for_default_user(self, username):
52 def _redirect_for_default_user(self, username):
53 _ = self.request.translate
53 _ = self.request.translate
54 if username == User.DEFAULT_USER:
54 if username == User.DEFAULT_USER:
55 h.flash(_("You can't edit this user"), category='warning')
55 h.flash(_("You can't edit this user"), category='warning')
56 # TODO(marcink): redirect to 'users' admin panel once this
56 # TODO(marcink): redirect to 'users' admin panel once this
57 # is a pyramid view
57 # is a pyramid view
58 raise HTTPFound('/')
58 raise HTTPFound('/')
59
59
60 def _extract_ordering(self, request):
60 def _extract_ordering(self, request):
61 column_index = safe_int(request.GET.get('order[0][column]'))
61 column_index = safe_int(request.GET.get('order[0][column]'))
62 order_dir = request.GET.get(
62 order_dir = request.GET.get(
63 'order[0][dir]', 'desc')
63 'order[0][dir]', 'desc')
64 order_by = request.GET.get(
64 order_by = request.GET.get(
65 'columns[%s][data][sort]' % column_index, 'name_raw')
65 'columns[%s][data][sort]' % column_index, 'name_raw')
66
66
67 # translate datatable to DB columns
67 # translate datatable to DB columns
68 order_by = {
68 order_by = {
69 'first_name': 'name',
69 'first_name': 'name',
70 'last_name': 'lastname',
70 'last_name': 'lastname',
71 'last_activity': ''
72 }.get(order_by) or order_by
71 }.get(order_by) or order_by
73
72
74 search_q = request.GET.get('search[value]')
73 search_q = request.GET.get('search[value]')
75 return search_q, order_by, order_dir
74 return search_q, order_by, order_dir
76
75
77 def _extract_chunk(self, request):
76 def _extract_chunk(self, request):
78 start = safe_int(request.GET.get('start'), 0)
77 start = safe_int(request.GET.get('start'), 0)
79 length = safe_int(request.GET.get('length'), 25)
78 length = safe_int(request.GET.get('length'), 25)
80 draw = safe_int(request.GET.get('draw'))
79 draw = safe_int(request.GET.get('draw'))
81 return draw, start, length
80 return draw, start, length
82
81
83 @HasPermissionAllDecorator('hg.admin')
82 @HasPermissionAllDecorator('hg.admin')
84 @view_config(
83 @view_config(
85 route_name='users', request_method='GET',
84 route_name='users', request_method='GET',
86 renderer='rhodecode:templates/admin/users/users.mako')
85 renderer='rhodecode:templates/admin/users/users.mako')
87 def users_list(self):
86 def users_list(self):
88 c = self.load_default_context()
87 c = self.load_default_context()
89 return self._get_template_context(c)
88 return self._get_template_context(c)
90
89
91 @HasPermissionAllDecorator('hg.admin')
90 @HasPermissionAllDecorator('hg.admin')
92 @view_config(
91 @view_config(
93 # renderer defined below
92 # renderer defined below
94 route_name='users_data', request_method='GET', renderer='json',
93 route_name='users_data', request_method='GET', renderer='json',
95 xhr=True)
94 xhr=True)
96 def users_list_data(self):
95 def users_list_data(self):
97 draw, start, limit = self._extract_chunk(self.request)
96 draw, start, limit = self._extract_chunk(self.request)
98 search_q, order_by, order_dir = self._extract_ordering(self.request)
97 search_q, order_by, order_dir = self._extract_ordering(self.request)
99
98
100 _render = PartialRenderer('data_table/_dt_elements.mako')
99 _render = PartialRenderer('data_table/_dt_elements.mako')
101
100
102 def user_actions(user_id, username):
101 def user_actions(user_id, username):
103 return _render("user_actions", user_id, username)
102 return _render("user_actions", user_id, username)
104
103
105 users_data_total_count = User.query()\
104 users_data_total_count = User.query()\
106 .filter(User.username != User.DEFAULT_USER) \
105 .filter(User.username != User.DEFAULT_USER) \
107 .count()
106 .count()
108
107
109 # json generate
108 # json generate
110 base_q = User.query().filter(User.username != User.DEFAULT_USER)
109 base_q = User.query().filter(User.username != User.DEFAULT_USER)
111
110
112 if search_q:
111 if search_q:
113 like_expression = u'%{}%'.format(safe_unicode(search_q))
112 like_expression = u'%{}%'.format(safe_unicode(search_q))
114 base_q = base_q.filter(or_(
113 base_q = base_q.filter(or_(
115 User.username.ilike(like_expression),
114 User.username.ilike(like_expression),
116 User._email.ilike(like_expression),
115 User._email.ilike(like_expression),
117 User.name.ilike(like_expression),
116 User.name.ilike(like_expression),
118 User.lastname.ilike(like_expression),
117 User.lastname.ilike(like_expression),
119 ))
118 ))
120
119
121 users_data_total_filtered_count = base_q.count()
120 users_data_total_filtered_count = base_q.count()
122
121
123 sort_col = getattr(User, order_by, None)
122 sort_col = getattr(User, order_by, None)
124 if sort_col and order_dir == 'asc':
123 if sort_col and order_dir == 'asc':
125 base_q = base_q.order_by(sort_col.asc())
124 base_q = base_q.order_by(sort_col.asc())
126 elif sort_col:
125 elif sort_col:
127 base_q = base_q.order_by(sort_col.desc())
126 base_q = base_q.order_by(sort_col.desc())
128
127
129 base_q = base_q.offset(start).limit(limit)
128 base_q = base_q.offset(start).limit(limit)
130 users_list = base_q.all()
129 users_list = base_q.all()
131
130
132 users_data = []
131 users_data = []
133 for user in users_list:
132 for user in users_list:
134 users_data.append({
133 users_data.append({
135 "username": h.gravatar_with_user(user.username),
134 "username": h.gravatar_with_user(user.username),
136 "email": user.email,
135 "email": user.email,
137 "first_name": h.escape(user.name),
136 "first_name": h.escape(user.name),
138 "last_name": h.escape(user.lastname),
137 "last_name": h.escape(user.lastname),
139 "last_login": h.format_date(user.last_login),
138 "last_login": h.format_date(user.last_login),
140 "last_activity": h.format_date(user.last_activity),
139 "last_activity": h.format_date(user.last_activity),
141 "active": h.bool2icon(user.active),
140 "active": h.bool2icon(user.active),
142 "active_raw": user.active,
141 "active_raw": user.active,
143 "admin": h.bool2icon(user.admin),
142 "admin": h.bool2icon(user.admin),
144 "extern_type": user.extern_type,
143 "extern_type": user.extern_type,
145 "extern_name": user.extern_name,
144 "extern_name": user.extern_name,
146 "action": user_actions(user.user_id, user.username),
145 "action": user_actions(user.user_id, user.username),
147 })
146 })
148
147
149 data = ({
148 data = ({
150 'draw': draw,
149 'draw': draw,
151 'data': users_data,
150 'data': users_data,
152 'recordsTotal': users_data_total_count,
151 'recordsTotal': users_data_total_count,
153 'recordsFiltered': users_data_total_filtered_count,
152 'recordsFiltered': users_data_total_filtered_count,
154 })
153 })
155
154
156 return data
155 return data
157
156
158 @LoginRequired()
157 @LoginRequired()
159 @HasPermissionAllDecorator('hg.admin')
158 @HasPermissionAllDecorator('hg.admin')
160 @view_config(
159 @view_config(
161 route_name='edit_user_auth_tokens', request_method='GET',
160 route_name='edit_user_auth_tokens', request_method='GET',
162 renderer='rhodecode:templates/admin/users/user_edit.mako')
161 renderer='rhodecode:templates/admin/users/user_edit.mako')
163 def auth_tokens(self):
162 def auth_tokens(self):
164 _ = self.request.translate
163 _ = self.request.translate
165 c = self.load_default_context()
164 c = self.load_default_context()
166
165
167 user_id = self.request.matchdict.get('user_id')
166 user_id = self.request.matchdict.get('user_id')
168 c.user = User.get_or_404(user_id, pyramid_exc=True)
167 c.user = User.get_or_404(user_id, pyramid_exc=True)
169 self._redirect_for_default_user(c.user.username)
168 self._redirect_for_default_user(c.user.username)
170
169
171 c.active = 'auth_tokens'
170 c.active = 'auth_tokens'
172
171
173 c.lifetime_values = [
172 c.lifetime_values = [
174 (str(-1), _('forever')),
173 (str(-1), _('forever')),
175 (str(5), _('5 minutes')),
174 (str(5), _('5 minutes')),
176 (str(60), _('1 hour')),
175 (str(60), _('1 hour')),
177 (str(60 * 24), _('1 day')),
176 (str(60 * 24), _('1 day')),
178 (str(60 * 24 * 30), _('1 month')),
177 (str(60 * 24 * 30), _('1 month')),
179 ]
178 ]
180 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
179 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
181 c.role_values = [
180 c.role_values = [
182 (x, AuthTokenModel.cls._get_role_name(x))
181 (x, AuthTokenModel.cls._get_role_name(x))
183 for x in AuthTokenModel.cls.ROLES]
182 for x in AuthTokenModel.cls.ROLES]
184 c.role_options = [(c.role_values, _("Role"))]
183 c.role_options = [(c.role_values, _("Role"))]
185 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
184 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
186 c.user.user_id, show_expired=True)
185 c.user.user_id, show_expired=True)
187 return self._get_template_context(c)
186 return self._get_template_context(c)
188
187
189 def maybe_attach_token_scope(self, token):
188 def maybe_attach_token_scope(self, token):
190 # implemented in EE edition
189 # implemented in EE edition
191 pass
190 pass
192
191
193 @LoginRequired()
192 @LoginRequired()
194 @HasPermissionAllDecorator('hg.admin')
193 @HasPermissionAllDecorator('hg.admin')
195 @CSRFRequired()
194 @CSRFRequired()
196 @view_config(
195 @view_config(
197 route_name='edit_user_auth_tokens_add', request_method='POST')
196 route_name='edit_user_auth_tokens_add', request_method='POST')
198 def auth_tokens_add(self):
197 def auth_tokens_add(self):
199 _ = self.request.translate
198 _ = self.request.translate
200 c = self.load_default_context()
199 c = self.load_default_context()
201
200
202 user_id = self.request.matchdict.get('user_id')
201 user_id = self.request.matchdict.get('user_id')
203 c.user = User.get_or_404(user_id, pyramid_exc=True)
202 c.user = User.get_or_404(user_id, pyramid_exc=True)
204 self._redirect_for_default_user(c.user.username)
203 self._redirect_for_default_user(c.user.username)
205
204
206 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
205 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
207 description = self.request.POST.get('description')
206 description = self.request.POST.get('description')
208 role = self.request.POST.get('role')
207 role = self.request.POST.get('role')
209
208
210 token = AuthTokenModel().create(
209 token = AuthTokenModel().create(
211 c.user.user_id, description, lifetime, role)
210 c.user.user_id, description, lifetime, role)
212 self.maybe_attach_token_scope(token)
211 self.maybe_attach_token_scope(token)
213 Session().commit()
212 Session().commit()
214
213
215 h.flash(_("Auth token successfully created"), category='success')
214 h.flash(_("Auth token successfully created"), category='success')
216 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
215 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
217
216
218 @LoginRequired()
217 @LoginRequired()
219 @HasPermissionAllDecorator('hg.admin')
218 @HasPermissionAllDecorator('hg.admin')
220 @CSRFRequired()
219 @CSRFRequired()
221 @view_config(
220 @view_config(
222 route_name='edit_user_auth_tokens_delete', request_method='POST')
221 route_name='edit_user_auth_tokens_delete', request_method='POST')
223 def auth_tokens_delete(self):
222 def auth_tokens_delete(self):
224 _ = self.request.translate
223 _ = self.request.translate
225 c = self.load_default_context()
224 c = self.load_default_context()
226
225
227 user_id = self.request.matchdict.get('user_id')
226 user_id = self.request.matchdict.get('user_id')
228 c.user = User.get_or_404(user_id, pyramid_exc=True)
227 c.user = User.get_or_404(user_id, pyramid_exc=True)
229 self._redirect_for_default_user(c.user.username)
228 self._redirect_for_default_user(c.user.username)
230
229
231 del_auth_token = self.request.POST.get('del_auth_token')
230 del_auth_token = self.request.POST.get('del_auth_token')
232
231
233 if del_auth_token:
232 if del_auth_token:
234 AuthTokenModel().delete(del_auth_token, c.user.user_id)
233 AuthTokenModel().delete(del_auth_token, c.user.user_id)
235 Session().commit()
234 Session().commit()
236 h.flash(_("Auth token successfully deleted"), category='success')
235 h.flash(_("Auth token successfully deleted"), category='success')
237
236
238 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
237 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
@@ -1,119 +1,119 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.mako"/>
2 <%inherit file="/base/base.mako"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${_('Users administration')}
5 ${_('Users administration')}
6 %if c.rhodecode_name:
6 %if c.rhodecode_name:
7 &middot; ${h.branding(c.rhodecode_name)}
7 &middot; ${h.branding(c.rhodecode_name)}
8 %endif
8 %endif
9 </%def>
9 </%def>
10
10
11 <%def name="breadcrumbs_links()">
11 <%def name="breadcrumbs_links()">
12 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
12 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
13 ${h.link_to(_('Admin'),h.url('admin_home'))} &raquo; <span id="user_count">0</span>
13 ${h.link_to(_('Admin'),h.url('admin_home'))} &raquo; <span id="user_count">0</span>
14 </%def>
14 </%def>
15
15
16 <%def name="menu_bar_nav()">
16 <%def name="menu_bar_nav()">
17 ${self.menu_items(active='admin')}
17 ${self.menu_items(active='admin')}
18 </%def>
18 </%def>
19
19
20 <%def name="main()">
20 <%def name="main()">
21
21
22 <div class="box">
22 <div class="box">
23
23
24 <div class="title">
24 <div class="title">
25 ${self.breadcrumbs()}
25 ${self.breadcrumbs()}
26 <ul class="links">
26 <ul class="links">
27 <li>
27 <li>
28 <a href="${h.url('new_user')}" class="btn btn-small btn-success">${_(u'Add User')}</a>
28 <a href="${h.url('new_user')}" class="btn btn-small btn-success">${_(u'Add User')}</a>
29 </li>
29 </li>
30 </ul>
30 </ul>
31 </div>
31 </div>
32
32
33 <div id="repos_list_wrap">
33 <div id="repos_list_wrap">
34 <table id="user_list_table" class="display"></table>
34 <table id="user_list_table" class="display"></table>
35 </div>
35 </div>
36 </div>
36 </div>
37
37
38 <script type="text/javascript">
38 <script type="text/javascript">
39
39
40 $(document).ready(function() {
40 $(document).ready(function() {
41
41
42 var getDatatableCount = function(){
42 var getDatatableCount = function(){
43 var table = $('#user_list_table').dataTable();
43 var table = $('#user_list_table').dataTable();
44 var page = table.api().page.info();
44 var page = table.api().page.info();
45 var active = page.recordsDisplay;
45 var active = page.recordsDisplay;
46 var total = page.recordsTotal;
46 var total = page.recordsTotal;
47
47
48 var _text = _gettext("{0} out of {1} users").format(active, total);
48 var _text = _gettext("{0} out of {1} users").format(active, total);
49 $('#user_count').text(_text);
49 $('#user_count').text(_text);
50 };
50 };
51
51
52 // user list
52 // user list
53 $('#user_list_table').DataTable({
53 $('#user_list_table').DataTable({
54 processing: true,
54 processing: true,
55 serverSide: true,
55 serverSide: true,
56 ajax: "${h.route_path('users_data')}",
56 ajax: "${h.route_path('users_data')}",
57 dom: 'rtp',
57 dom: 'rtp',
58 pageLength: ${c.visual.admin_grid_items},
58 pageLength: ${c.visual.admin_grid_items},
59 order: [[ 0, "asc" ]],
59 order: [[ 0, "asc" ]],
60 columns: [
60 columns: [
61 { data: {"_": "username",
61 { data: {"_": "username",
62 "sort": "username"}, title: "${_('Username')}", className: "td-user" },
62 "sort": "username"}, title: "${_('Username')}", className: "td-user" },
63 { data: {"_": "email",
63 { data: {"_": "email",
64 "sort": "email"}, title: "${_('Email')}", className: "td-email" },
64 "sort": "email"}, title: "${_('Email')}", className: "td-email" },
65 { data: {"_": "first_name",
65 { data: {"_": "first_name",
66 "sort": "first_name"}, title: "${_('First Name')}", className: "td-user" },
66 "sort": "first_name"}, title: "${_('First Name')}", className: "td-user" },
67 { data: {"_": "last_name",
67 { data: {"_": "last_name",
68 "sort": "last_name"}, title: "${_('Last Name')}", className: "td-user" },
68 "sort": "last_name"}, title: "${_('Last Name')}", className: "td-user" },
69 { data: {"_": "last_activity",
69 { data: {"_": "last_activity",
70 "sort": "last_activity",
70 "sort": "last_activity",
71 "type": Number}, title: "${_('Last activity')}", className: "td-time", orderable: false },
71 "type": Number}, title: "${_('Last activity')}", className: "td-time" },
72 { data: {"_": "active",
72 { data: {"_": "active",
73 "sort": "active"}, title: "${_('Active')}", className: "td-active" },
73 "sort": "active"}, title: "${_('Active')}", className: "td-active" },
74 { data: {"_": "admin",
74 { data: {"_": "admin",
75 "sort": "admin"}, title: "${_('Admin')}", className: "td-admin" },
75 "sort": "admin"}, title: "${_('Admin')}", className: "td-admin" },
76 { data: {"_": "extern_type",
76 { data: {"_": "extern_type",
77 "sort": "extern_type"}, title: "${_('Auth type')}", className: "td-type" },
77 "sort": "extern_type"}, title: "${_('Auth type')}", className: "td-type" },
78 { data: {"_": "action",
78 { data: {"_": "action",
79 "sort": "action"}, title: "${_('Action')}", className: "td-action" }
79 "sort": "action"}, title: "${_('Action')}", className: "td-action" }
80 ],
80 ],
81 language: {
81 language: {
82 paginate: DEFAULT_GRID_PAGINATION,
82 paginate: DEFAULT_GRID_PAGINATION,
83 sProcessing: _gettext('loading...'),
83 sProcessing: _gettext('loading...'),
84 emptyTable: _gettext("No users available yet.")
84 emptyTable: _gettext("No users available yet.")
85 },
85 },
86
86
87 "createdRow": function ( row, data, index ) {
87 "createdRow": function ( row, data, index ) {
88 if (!data['active_raw']){
88 if (!data['active_raw']){
89 $(row).addClass('closed')
89 $(row).addClass('closed')
90 }
90 }
91 }
91 }
92 });
92 });
93
93
94 $('#user_list_table').on('xhr.dt', function(e, settings, json, xhr){
94 $('#user_list_table').on('xhr.dt', function(e, settings, json, xhr){
95 $('#user_list_table').css('opacity', 1);
95 $('#user_list_table').css('opacity', 1);
96 });
96 });
97
97
98 $('#user_list_table').on('preXhr.dt', function(e, settings, data){
98 $('#user_list_table').on('preXhr.dt', function(e, settings, data){
99 $('#user_list_table').css('opacity', 0.3);
99 $('#user_list_table').css('opacity', 0.3);
100 });
100 });
101
101
102 // refresh counters on draw
102 // refresh counters on draw
103 $('#user_list_table').on('draw.dt', function(){
103 $('#user_list_table').on('draw.dt', function(){
104 getDatatableCount();
104 getDatatableCount();
105 });
105 });
106
106
107 // filter
107 // filter
108 $('#q_filter').on('keyup',
108 $('#q_filter').on('keyup',
109 $.debounce(250, function() {
109 $.debounce(250, function() {
110 $('#user_list_table').DataTable().search(
110 $('#user_list_table').DataTable().search(
111 $('#q_filter').val()
111 $('#q_filter').val()
112 ).draw();
112 ).draw();
113 })
113 })
114 );
114 );
115
115
116 });
116 });
117 </script>
117 </script>
118
118
119 </%def>
119 </%def>
General Comments 0
You need to be logged in to leave comments. Login now