##// END OF EJS Templates
data-grid: create a common re-usable app with datagrid rendering helpers
marcink -
r1646:4bd0052b default
parent child Browse files
Show More
@@ -1,163 +1,191 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import logging
23 23 from pylons import tmpl_context as c
24 24 from pyramid.httpexceptions import HTTPFound
25 25
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int
28 28 from rhodecode.model import repo
29 29 from rhodecode.model.db import User
30 30 from rhodecode.model.scm import ScmModel
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 ADMIN_PREFIX = '/_admin'
36 36 STATIC_FILE_PREFIX = '/_static'
37 37
38 38
39 39 class TemplateArgs(StrictAttributeDict):
40 40 pass
41 41
42 42
43 43 class BaseAppView(object):
44 44
45 45 def __init__(self, context, request):
46 46 self.request = request
47 47 self.context = context
48 48 self.session = request.session
49 49 self._rhodecode_user = request.user # auth user
50 50 self._rhodecode_db_user = self._rhodecode_user.get_instance()
51 51 self._maybe_needs_password_change(
52 52 request.matched_route.name, self._rhodecode_db_user)
53 53
54 54 def _maybe_needs_password_change(self, view_name, user_obj):
55 55 log.debug('Checking if user %s needs password change on view %s',
56 56 user_obj, view_name)
57 57 skip_user_views = [
58 58 'logout', 'login',
59 59 'my_account_password', 'my_account_password_update'
60 60 ]
61 61
62 62 if not user_obj:
63 63 return
64 64
65 65 if user_obj.username == User.DEFAULT_USER:
66 66 return
67 67
68 68 now = time.time()
69 69 should_change = user_obj.user_data.get('force_password_change')
70 70 change_after = safe_int(should_change) or 0
71 71 if should_change and now > change_after:
72 72 log.debug('User %s requires password change', user_obj)
73 73 h.flash('You are required to change your password', 'warning',
74 74 ignore_duplicate=True)
75 75
76 76 if view_name not in skip_user_views:
77 77 raise HTTPFound(
78 78 self.request.route_path('my_account_password'))
79 79
80 80 def _get_local_tmpl_context(self):
81 81 c = TemplateArgs()
82 82 c.auth_user = self.request.user
83 83 return c
84 84
85 85 def _register_global_c(self, tmpl_args):
86 86 """
87 87 Registers attributes to pylons global `c`
88 88 """
89 89 # TODO(marcink): remove once pyramid migration is finished
90 90 for k, v in tmpl_args.items():
91 91 setattr(c, k, v)
92 92
93 93 def _get_template_context(self, tmpl_args):
94 94 self._register_global_c(tmpl_args)
95 95
96 96 local_tmpl_args = {
97 97 'defaults': {},
98 98 'errors': {},
99 99 }
100 100 local_tmpl_args.update(tmpl_args)
101 101 return local_tmpl_args
102 102
103 103 def load_default_context(self):
104 104 """
105 105 example:
106 106
107 107 def load_default_context(self):
108 108 c = self._get_local_tmpl_context()
109 109 c.custom_var = 'foobar'
110 110 self._register_global_c(c)
111 111 return c
112 112 """
113 113 raise NotImplementedError('Needs implementation in view class')
114 114
115 115
116 116 class RepoAppView(BaseAppView):
117 117
118 118 def __init__(self, context, request):
119 119 super(RepoAppView, self).__init__(context, request)
120 120 self.db_repo = request.db_repo
121 121 self.db_repo_name = self.db_repo.repo_name
122 122 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
123 123
124 124 def _get_local_tmpl_context(self):
125 125 c = super(RepoAppView, self)._get_local_tmpl_context()
126 126 # register common vars for this type of view
127 127 c.rhodecode_db_repo = self.db_repo
128 128 c.repo_name = self.db_repo_name
129 129 c.repository_pull_requests = self.db_repo_pull_requests
130 130 return c
131 131
132 132
133 class DataGridAppView(object):
134 """
135 Common class to have re-usable grid rendering components
136 """
137
138 def _extract_ordering(self, request):
139 column_index = safe_int(request.GET.get('order[0][column]'))
140 order_dir = request.GET.get(
141 'order[0][dir]', 'desc')
142 order_by = request.GET.get(
143 'columns[%s][data][sort]' % column_index, 'name_raw')
144
145 # translate datatable to DB columns
146 order_by = {
147 'first_name': 'name',
148 'last_name': 'lastname',
149 }.get(order_by) or order_by
150
151 search_q = request.GET.get('search[value]')
152 return search_q, order_by, order_dir
153
154 def _extract_chunk(self, request):
155 start = safe_int(request.GET.get('start'), 0)
156 length = safe_int(request.GET.get('length'), 25)
157 draw = safe_int(request.GET.get('draw'))
158 return draw, start, length
159
160
133 161 class RepoRoutePredicate(object):
134 162 def __init__(self, val, config):
135 163 self.val = val
136 164
137 165 def text(self):
138 166 return 'repo_route = %s' % self.val
139 167
140 168 phash = text
141 169
142 170 def __call__(self, info, request):
143 171 repo_name = info['match']['repo_name']
144 172 repo_model = repo.RepoModel()
145 173 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
146 174 # if we match quickly from database, short circuit the operation,
147 175 # and validate repo based on the type.
148 176 if by_name_match:
149 177 # register this as request object we can re-use later
150 178 request.db_repo = by_name_match
151 179 return True
152 180
153 181 by_id_match = repo_model.get_repo_by_id(repo_name)
154 182 if by_id_match:
155 183 request.db_repo = by_id_match
156 184 return True
157 185
158 186 return False
159 187
160 188
161 189 def includeme(config):
162 190 config.add_route_predicate(
163 191 'repo_route', RepoRoutePredicate)
@@ -1,328 +1,306 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23
24 24 from pyramid.httpexceptions import HTTPFound
25 25 from pyramid.view import view_config
26 26 from sqlalchemy.sql.functions import coalesce
27 27
28 28 from rhodecode.lib.helpers import Page
29 29 from rhodecode_tools.lib.ext_json import json
30 30
31 from rhodecode.apps._base import BaseAppView
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32 from rhodecode.lib.auth import (
33 33 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
34 34 from rhodecode.lib import helpers as h
35 35 from rhodecode.lib.utils import PartialRenderer
36 36 from rhodecode.lib.utils2 import safe_int, safe_unicode
37 37 from rhodecode.model.auth_token import AuthTokenModel
38 38 from rhodecode.model.user import UserModel
39 39 from rhodecode.model.user_group import UserGroupModel
40 40 from rhodecode.model.db import User, or_
41 41 from rhodecode.model.meta import Session
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 class AdminUsersView(BaseAppView):
46 class AdminUsersViewData(BaseAppView, DataGridAppView):
47 47 ALLOW_SCOPED_TOKENS = False
48 48 """
49 49 This view has alternative version inside EE, if modified please take a look
50 50 in there as well.
51 51 """
52 52
53 53 def load_default_context(self):
54 54 c = self._get_local_tmpl_context()
55 55 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
56 56 self._register_global_c(c)
57 57 return c
58 58
59 59 def _redirect_for_default_user(self, username):
60 60 _ = self.request.translate
61 61 if username == User.DEFAULT_USER:
62 62 h.flash(_("You can't edit this user"), category='warning')
63 63 # TODO(marcink): redirect to 'users' admin panel once this
64 64 # is a pyramid view
65 65 raise HTTPFound('/')
66 66
67 def _extract_ordering(self, request):
68 column_index = safe_int(request.GET.get('order[0][column]'))
69 order_dir = request.GET.get(
70 'order[0][dir]', 'desc')
71 order_by = request.GET.get(
72 'columns[%s][data][sort]' % column_index, 'name_raw')
73
74 # translate datatable to DB columns
75 order_by = {
76 'first_name': 'name',
77 'last_name': 'lastname',
78 }.get(order_by) or order_by
79
80 search_q = request.GET.get('search[value]')
81 return search_q, order_by, order_dir
82
83 def _extract_chunk(self, request):
84 start = safe_int(request.GET.get('start'), 0)
85 length = safe_int(request.GET.get('length'), 25)
86 draw = safe_int(request.GET.get('draw'))
87 return draw, start, length
88
89 67 @HasPermissionAllDecorator('hg.admin')
90 68 @view_config(
91 69 route_name='users', request_method='GET',
92 70 renderer='rhodecode:templates/admin/users/users.mako')
93 71 def users_list(self):
94 72 c = self.load_default_context()
95 73 return self._get_template_context(c)
96 74
97 75 @HasPermissionAllDecorator('hg.admin')
98 76 @view_config(
99 77 # renderer defined below
100 78 route_name='users_data', request_method='GET', renderer='json',
101 79 xhr=True)
102 80 def users_list_data(self):
103 81 draw, start, limit = self._extract_chunk(self.request)
104 82 search_q, order_by, order_dir = self._extract_ordering(self.request)
105 83
106 84 _render = PartialRenderer('data_table/_dt_elements.mako')
107 85
108 86 def user_actions(user_id, username):
109 87 return _render("user_actions", user_id, username)
110 88
111 89 users_data_total_count = User.query()\
112 90 .filter(User.username != User.DEFAULT_USER) \
113 91 .count()
114 92
115 93 # json generate
116 94 base_q = User.query().filter(User.username != User.DEFAULT_USER)
117 95
118 96 if search_q:
119 97 like_expression = u'%{}%'.format(safe_unicode(search_q))
120 98 base_q = base_q.filter(or_(
121 99 User.username.ilike(like_expression),
122 100 User._email.ilike(like_expression),
123 101 User.name.ilike(like_expression),
124 102 User.lastname.ilike(like_expression),
125 103 ))
126 104
127 105 users_data_total_filtered_count = base_q.count()
128 106
129 107 sort_col = getattr(User, order_by, None)
130 108 if sort_col:
131 109 if order_dir == 'asc':
132 110 # handle null values properly to order by NULL last
133 111 if order_by in ['last_activity']:
134 112 sort_col = coalesce(sort_col, datetime.date.max)
135 113 sort_col = sort_col.asc()
136 114 else:
137 115 # handle null values properly to order by NULL last
138 116 if order_by in ['last_activity']:
139 117 sort_col = coalesce(sort_col, datetime.date.min)
140 118 sort_col = sort_col.desc()
141 119
142 120 base_q = base_q.order_by(sort_col)
143 121 base_q = base_q.offset(start).limit(limit)
144 122
145 123 users_list = base_q.all()
146 124
147 125 users_data = []
148 126 for user in users_list:
149 127 users_data.append({
150 128 "username": h.gravatar_with_user(user.username),
151 129 "email": user.email,
152 130 "first_name": h.escape(user.name),
153 131 "last_name": h.escape(user.lastname),
154 132 "last_login": h.format_date(user.last_login),
155 133 "last_activity": h.format_date(user.last_activity),
156 134 "active": h.bool2icon(user.active),
157 135 "active_raw": user.active,
158 136 "admin": h.bool2icon(user.admin),
159 137 "extern_type": user.extern_type,
160 138 "extern_name": user.extern_name,
161 139 "action": user_actions(user.user_id, user.username),
162 140 })
163 141
164 142 data = ({
165 143 'draw': draw,
166 144 'data': users_data,
167 145 'recordsTotal': users_data_total_count,
168 146 'recordsFiltered': users_data_total_filtered_count,
169 147 })
170 148
171 149 return data
172 150
173 151 @LoginRequired()
174 152 @HasPermissionAllDecorator('hg.admin')
175 153 @view_config(
176 154 route_name='edit_user_auth_tokens', request_method='GET',
177 155 renderer='rhodecode:templates/admin/users/user_edit.mako')
178 156 def auth_tokens(self):
179 157 _ = self.request.translate
180 158 c = self.load_default_context()
181 159
182 160 user_id = self.request.matchdict.get('user_id')
183 161 c.user = User.get_or_404(user_id, pyramid_exc=True)
184 162 self._redirect_for_default_user(c.user.username)
185 163
186 164 c.active = 'auth_tokens'
187 165
188 166 c.lifetime_values = [
189 167 (str(-1), _('forever')),
190 168 (str(5), _('5 minutes')),
191 169 (str(60), _('1 hour')),
192 170 (str(60 * 24), _('1 day')),
193 171 (str(60 * 24 * 30), _('1 month')),
194 172 ]
195 173 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
196 174 c.role_values = [
197 175 (x, AuthTokenModel.cls._get_role_name(x))
198 176 for x in AuthTokenModel.cls.ROLES]
199 177 c.role_options = [(c.role_values, _("Role"))]
200 178 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
201 179 c.user.user_id, show_expired=True)
202 180 return self._get_template_context(c)
203 181
204 182 def maybe_attach_token_scope(self, token):
205 183 # implemented in EE edition
206 184 pass
207 185
208 186 @LoginRequired()
209 187 @HasPermissionAllDecorator('hg.admin')
210 188 @CSRFRequired()
211 189 @view_config(
212 190 route_name='edit_user_auth_tokens_add', request_method='POST')
213 191 def auth_tokens_add(self):
214 192 _ = self.request.translate
215 193 c = self.load_default_context()
216 194
217 195 user_id = self.request.matchdict.get('user_id')
218 196 c.user = User.get_or_404(user_id, pyramid_exc=True)
219 197 self._redirect_for_default_user(c.user.username)
220 198
221 199 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
222 200 description = self.request.POST.get('description')
223 201 role = self.request.POST.get('role')
224 202
225 203 token = AuthTokenModel().create(
226 204 c.user.user_id, description, lifetime, role)
227 205 self.maybe_attach_token_scope(token)
228 206 Session().commit()
229 207
230 208 h.flash(_("Auth token successfully created"), category='success')
231 209 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
232 210
233 211 @LoginRequired()
234 212 @HasPermissionAllDecorator('hg.admin')
235 213 @CSRFRequired()
236 214 @view_config(
237 215 route_name='edit_user_auth_tokens_delete', request_method='POST')
238 216 def auth_tokens_delete(self):
239 217 _ = self.request.translate
240 218 c = self.load_default_context()
241 219
242 220 user_id = self.request.matchdict.get('user_id')
243 221 c.user = User.get_or_404(user_id, pyramid_exc=True)
244 222 self._redirect_for_default_user(c.user.username)
245 223
246 224 del_auth_token = self.request.POST.get('del_auth_token')
247 225
248 226 if del_auth_token:
249 227 AuthTokenModel().delete(del_auth_token, c.user.user_id)
250 228 Session().commit()
251 229 h.flash(_("Auth token successfully deleted"), category='success')
252 230
253 231 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
254 232
255 233 @LoginRequired()
256 234 @HasPermissionAllDecorator('hg.admin')
257 235 @view_config(
258 236 route_name='edit_user_groups_management', request_method='GET',
259 237 renderer='rhodecode:templates/admin/users/user_edit.mako')
260 238 def groups_management(self):
261 239 c = self.load_default_context()
262 240
263 241 user_id = self.request.matchdict.get('user_id')
264 242 c.user = User.get_or_404(user_id, pyramid_exc=True)
265 243 c.data = c.user.group_member
266 244 self._redirect_for_default_user(c.user.username)
267 245 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group) for group in c.user.group_member]
268 246 c.groups = json.dumps(groups)
269 247 c.active = 'groups'
270 248
271 249 return self._get_template_context(c)
272 250
273 251 @LoginRequired()
274 252 @HasPermissionAllDecorator('hg.admin')
275 253 @view_config(
276 254 route_name='edit_user_groups_management_updates', request_method='POST')
277 255 def groups_management_updates(self):
278 256 _ = self.request.translate
279 257 c = self.load_default_context()
280 258
281 259 user_id = self.request.matchdict.get('user_id')
282 260 c.user = User.get_or_404(user_id, pyramid_exc=True)
283 261 self._redirect_for_default_user(c.user.username)
284 262
285 263 users_groups = set(self.request.POST.getall('users_group_id'))
286 264 users_groups_model = []
287 265
288 266 for ugid in users_groups:
289 267 users_groups_model.append(UserGroupModel().get_group(safe_int(ugid)))
290 268 user_group_model = UserGroupModel()
291 269 user_group_model.change_groups(c.user, users_groups_model)
292 270
293 271 Session().commit()
294 272 c.active = 'user_groups_management'
295 273 h.flash(_("Groups successfully changed"), category='success')
296 274
297 275 return HTTPFound(h.route_path(
298 276 'edit_user_groups_management', user_id=user_id))
299 277
300 278 @LoginRequired()
301 279 @HasPermissionAllDecorator('hg.admin')
302 280 @view_config(
303 281 route_name='edit_user_audit_logs', request_method='GET',
304 282 renderer='rhodecode:templates/admin/users/user_edit.mako')
305 283 def user_audit_logs(self):
306 284 _ = self.request.translate
307 285 c = self.load_default_context()
308 286
309 287 user_id = self.request.matchdict.get('user_id')
310 288 c.user = User.get_or_404(user_id, pyramid_exc=True)
311 289 self._redirect_for_default_user(c.user.username)
312 290 c.active = 'audit'
313 291
314 292 p = safe_int(self.request.GET.get('page', 1), 1)
315 293
316 294 filter_term = self.request.GET.get('filter')
317 295 c.user_log = UserModel().get_user_log(c.user, filter_term)
318 296
319 297 def url_generator(**kw):
320 298 if filter_term:
321 299 kw['filter'] = filter_term
322 300 return self.request.current_route_path(_query=kw)
323 301
324 302 c.user_log = Page(c.user_log, page=p, items_per_page=10,
325 303 url=url_generator)
326 304 c.filter_term = filter_term
327 305 return self._get_template_context(c)
328 306
General Comments 0
You need to be logged in to leave comments. Login now