##// END OF EJS Templates
data-grid-app: added universal function for extracting ordering....
marcink -
r2040:a04ecb8d default
parent child Browse files
Show More
@@ -1,461 +1,471 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 import operator
23 24
24 25 from pyramid.httpexceptions import HTTPFound
25 26
26 27 from rhodecode.lib import helpers as h
27 28 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
28 29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
29 30 from rhodecode.model import repo
30 31 from rhodecode.model import repo_group
31 32 from rhodecode.model.db import User
32 33 from rhodecode.model.scm import ScmModel
33 34
34 35 log = logging.getLogger(__name__)
35 36
36 37
37 38 ADMIN_PREFIX = '/_admin'
38 39 STATIC_FILE_PREFIX = '/_static'
39 40
40 41 URL_NAME_REQUIREMENTS = {
41 42 # group name can have a slash in them, but they must not end with a slash
42 43 'group_name': r'.*?[^/]',
43 44 'repo_group_name': r'.*?[^/]',
44 45 # repo names can have a slash in them, but they must not end with a slash
45 46 'repo_name': r'.*?[^/]',
46 47 # file path eats up everything at the end
47 48 'f_path': r'.*',
48 49 # reference types
49 50 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
50 51 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
51 52 }
52 53
53 54
54 55 def add_route_with_slash(config,name, pattern, **kw):
55 56 config.add_route(name, pattern, **kw)
56 57 if not pattern.endswith('/'):
57 58 config.add_route(name + '_slash', pattern + '/', **kw)
58 59
59 60
60 61 def add_route_requirements(route_path, requirements=URL_NAME_REQUIREMENTS):
61 62 """
62 63 Adds regex requirements to pyramid routes using a mapping dict
63 64 e.g::
64 65 add_route_requirements('{repo_name}/settings')
65 66 """
66 67 for key, regex in requirements.items():
67 68 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
68 69 return route_path
69 70
70 71
71 72 def get_format_ref_id(repo):
72 73 """Returns a `repo` specific reference formatter function"""
73 74 if h.is_svn(repo):
74 75 return _format_ref_id_svn
75 76 else:
76 77 return _format_ref_id
77 78
78 79
79 80 def _format_ref_id(name, raw_id):
80 81 """Default formatting of a given reference `name`"""
81 82 return name
82 83
83 84
84 85 def _format_ref_id_svn(name, raw_id):
85 86 """Special way of formatting a reference for Subversion including path"""
86 87 return '%s@%s' % (name, raw_id)
87 88
88 89
89 90 class TemplateArgs(StrictAttributeDict):
90 91 pass
91 92
92 93
93 94 class BaseAppView(object):
94 95
95 96 def __init__(self, context, request):
96 97 self.request = request
97 98 self.context = context
98 99 self.session = request.session
99 100 self._rhodecode_user = request.user # auth user
100 101 self._rhodecode_db_user = self._rhodecode_user.get_instance()
101 102 self._maybe_needs_password_change(
102 103 request.matched_route.name, self._rhodecode_db_user)
103 104
104 105 def _maybe_needs_password_change(self, view_name, user_obj):
105 106 log.debug('Checking if user %s needs password change on view %s',
106 107 user_obj, view_name)
107 108 skip_user_views = [
108 109 'logout', 'login',
109 110 'my_account_password', 'my_account_password_update'
110 111 ]
111 112
112 113 if not user_obj:
113 114 return
114 115
115 116 if user_obj.username == User.DEFAULT_USER:
116 117 return
117 118
118 119 now = time.time()
119 120 should_change = user_obj.user_data.get('force_password_change')
120 121 change_after = safe_int(should_change) or 0
121 122 if should_change and now > change_after:
122 123 log.debug('User %s requires password change', user_obj)
123 124 h.flash('You are required to change your password', 'warning',
124 125 ignore_duplicate=True)
125 126
126 127 if view_name not in skip_user_views:
127 128 raise HTTPFound(
128 129 self.request.route_path('my_account_password'))
129 130
130 131 def _log_creation_exception(self, e, repo_name):
131 132 _ = self.request.translate
132 133 reason = None
133 134 if len(e.args) == 2:
134 135 reason = e.args[1]
135 136
136 137 if reason == 'INVALID_CERTIFICATE':
137 138 log.exception(
138 139 'Exception creating a repository: invalid certificate')
139 140 msg = (_('Error creating repository %s: invalid certificate')
140 141 % repo_name)
141 142 else:
142 143 log.exception("Exception creating a repository")
143 144 msg = (_('Error creating repository %s')
144 145 % repo_name)
145 146 return msg
146 147
147 148 def _get_local_tmpl_context(self, include_app_defaults=False):
148 149 c = TemplateArgs()
149 150 c.auth_user = self.request.user
150 151 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
151 152 c.rhodecode_user = self.request.user
152 153
153 154 if include_app_defaults:
154 155 # NOTE(marcink): after full pyramid migration include_app_defaults
155 156 # should be turned on by default
156 157 from rhodecode.lib.base import attach_context_attributes
157 158 attach_context_attributes(c, self.request, self.request.user.user_id)
158 159
159 160 return c
160 161
161 162 def _register_global_c(self, tmpl_args):
162 163 """
163 164 Registers attributes to pylons global `c`
164 165 """
165 166
166 167 # TODO(marcink): remove once pyramid migration is finished
167 168 from pylons import tmpl_context as c
168 169 try:
169 170 for k, v in tmpl_args.items():
170 171 setattr(c, k, v)
171 172 except TypeError:
172 173 log.exception('Failed to register pylons C')
173 174 pass
174 175
175 176 def _get_template_context(self, tmpl_args):
176 177 self._register_global_c(tmpl_args)
177 178
178 179 local_tmpl_args = {
179 180 'defaults': {},
180 181 'errors': {},
181 182 # register a fake 'c' to be used in templates instead of global
182 183 # pylons c, after migration to pyramid we should rename it to 'c'
183 184 # make sure we replace usage of _c in templates too
184 185 '_c': tmpl_args
185 186 }
186 187 local_tmpl_args.update(tmpl_args)
187 188 return local_tmpl_args
188 189
189 190 def load_default_context(self):
190 191 """
191 192 example:
192 193
193 194 def load_default_context(self):
194 195 c = self._get_local_tmpl_context()
195 196 c.custom_var = 'foobar'
196 197 self._register_global_c(c)
197 198 return c
198 199 """
199 200 raise NotImplementedError('Needs implementation in view class')
200 201
201 202
202 203 class RepoAppView(BaseAppView):
203 204
204 205 def __init__(self, context, request):
205 206 super(RepoAppView, self).__init__(context, request)
206 207 self.db_repo = request.db_repo
207 208 self.db_repo_name = self.db_repo.repo_name
208 209 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
209 210
210 211 def _handle_missing_requirements(self, error):
211 212 log.error(
212 213 'Requirements are missing for repository %s: %s',
213 214 self.db_repo_name, error.message)
214 215
215 216 def _get_local_tmpl_context(self, include_app_defaults=False):
216 217 _ = self.request.translate
217 218 c = super(RepoAppView, self)._get_local_tmpl_context(
218 219 include_app_defaults=include_app_defaults)
219 220
220 221 # register common vars for this type of view
221 222 c.rhodecode_db_repo = self.db_repo
222 223 c.repo_name = self.db_repo_name
223 224 c.repository_pull_requests = self.db_repo_pull_requests
224 225
225 226 c.repository_requirements_missing = False
226 227 try:
227 228 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
228 229 except RepositoryRequirementError as e:
229 230 c.repository_requirements_missing = True
230 231 self._handle_missing_requirements(e)
231 232 self.rhodecode_vcs_repo = None
232 233
233 234 if (not c.repository_requirements_missing
234 235 and self.rhodecode_vcs_repo is None):
235 236 # unable to fetch this repo as vcs instance, report back to user
236 237 h.flash(_(
237 238 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
238 239 "Please check if it exist, or is not damaged.") %
239 240 {'repo_name': c.repo_name},
240 241 category='error', ignore_duplicate=True)
241 242 raise HTTPFound(h.route_path('home'))
242 243
243 244 return c
244 245
245 246 def _get_f_path(self, matchdict, default=None):
246 247 f_path = matchdict.get('f_path')
247 248 if f_path:
248 249 # fix for multiple initial slashes that causes errors for GIT
249 250 return f_path.lstrip('/')
250 251
251 252 return default
252 253
253 254
254 255 class RepoGroupAppView(BaseAppView):
255 256 def __init__(self, context, request):
256 257 super(RepoGroupAppView, self).__init__(context, request)
257 258 self.db_repo_group = request.db_repo_group
258 259 self.db_repo_group_name = self.db_repo_group.group_name
259 260
260 261
261 262 class DataGridAppView(object):
262 263 """
263 264 Common class to have re-usable grid rendering components
264 265 """
265 266
266 267 def _extract_ordering(self, request, column_map=None):
267 268 column_map = column_map or {}
268 269 column_index = safe_int(request.GET.get('order[0][column]'))
269 270 order_dir = request.GET.get(
270 271 'order[0][dir]', 'desc')
271 272 order_by = request.GET.get(
272 273 'columns[%s][data][sort]' % column_index, 'name_raw')
273 274
274 275 # translate datatable to DB columns
275 276 order_by = column_map.get(order_by) or order_by
276 277
277 278 search_q = request.GET.get('search[value]')
278 279 return search_q, order_by, order_dir
279 280
280 281 def _extract_chunk(self, request):
281 282 start = safe_int(request.GET.get('start'), 0)
282 283 length = safe_int(request.GET.get('length'), 25)
283 284 draw = safe_int(request.GET.get('draw'))
284 285 return draw, start, length
285 286
287 def _get_order_col(self, order_by, model):
288 if isinstance(order_by, basestring):
289 try:
290 return operator.attrgetter(order_by)(model)
291 except AttributeError:
292 return None
293 else:
294 return order_by
295
286 296
287 297 class BaseReferencesView(RepoAppView):
288 298 """
289 299 Base for reference view for branches, tags and bookmarks.
290 300 """
291 301 def load_default_context(self):
292 302 c = self._get_local_tmpl_context()
293 303
294 304 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
295 305 c.repo_info = self.db_repo
296 306
297 307 self._register_global_c(c)
298 308 return c
299 309
300 310 def load_refs_context(self, ref_items, partials_template):
301 311 _render = self.request.get_partial_renderer(partials_template)
302 312 pre_load = ["author", "date", "message"]
303 313
304 314 is_svn = h.is_svn(self.rhodecode_vcs_repo)
305 315 is_hg = h.is_hg(self.rhodecode_vcs_repo)
306 316
307 317 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
308 318
309 319 closed_refs = {}
310 320 if is_hg:
311 321 closed_refs = self.rhodecode_vcs_repo.branches_closed
312 322
313 323 data = []
314 324 for ref_name, commit_id in ref_items:
315 325 commit = self.rhodecode_vcs_repo.get_commit(
316 326 commit_id=commit_id, pre_load=pre_load)
317 327 closed = ref_name in closed_refs
318 328
319 329 # TODO: johbo: Unify generation of reference links
320 330 use_commit_id = '/' in ref_name or is_svn
321 331
322 332 if use_commit_id:
323 333 files_url = h.route_path(
324 334 'repo_files',
325 335 repo_name=self.db_repo_name,
326 336 f_path=ref_name if is_svn else '',
327 337 commit_id=commit_id)
328 338
329 339 else:
330 340 files_url = h.route_path(
331 341 'repo_files',
332 342 repo_name=self.db_repo_name,
333 343 f_path=ref_name if is_svn else '',
334 344 commit_id=ref_name,
335 345 _query=dict(at=ref_name))
336 346
337 347 data.append({
338 348 "name": _render('name', ref_name, files_url, closed),
339 349 "name_raw": ref_name,
340 350 "date": _render('date', commit.date),
341 351 "date_raw": datetime_to_time(commit.date),
342 352 "author": _render('author', commit.author),
343 353 "commit": _render(
344 354 'commit', commit.message, commit.raw_id, commit.idx),
345 355 "commit_raw": commit.idx,
346 356 "compare": _render(
347 357 'compare', format_ref_id(ref_name, commit.raw_id)),
348 358 })
349 359
350 360 return data
351 361
352 362
353 363 class RepoRoutePredicate(object):
354 364 def __init__(self, val, config):
355 365 self.val = val
356 366
357 367 def text(self):
358 368 return 'repo_route = %s' % self.val
359 369
360 370 phash = text
361 371
362 372 def __call__(self, info, request):
363 373
364 374 if hasattr(request, 'vcs_call'):
365 375 # skip vcs calls
366 376 return
367 377
368 378 repo_name = info['match']['repo_name']
369 379 repo_model = repo.RepoModel()
370 380 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
371 381
372 382 def redirect_if_creating(db_repo):
373 383 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
374 384 raise HTTPFound(
375 385 request.route_path('repo_creating',
376 386 repo_name=db_repo.repo_name))
377 387
378 388 if by_name_match:
379 389 # register this as request object we can re-use later
380 390 request.db_repo = by_name_match
381 391 redirect_if_creating(by_name_match)
382 392 return True
383 393
384 394 by_id_match = repo_model.get_repo_by_id(repo_name)
385 395 if by_id_match:
386 396 request.db_repo = by_id_match
387 397 redirect_if_creating(by_id_match)
388 398 return True
389 399
390 400 return False
391 401
392 402
393 403 class RepoTypeRoutePredicate(object):
394 404 def __init__(self, val, config):
395 405 self.val = val or ['hg', 'git', 'svn']
396 406
397 407 def text(self):
398 408 return 'repo_accepted_type = %s' % self.val
399 409
400 410 phash = text
401 411
402 412 def __call__(self, info, request):
403 413 if hasattr(request, 'vcs_call'):
404 414 # skip vcs calls
405 415 return
406 416
407 417 rhodecode_db_repo = request.db_repo
408 418
409 419 log.debug(
410 420 '%s checking repo type for %s in %s',
411 421 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
412 422
413 423 if rhodecode_db_repo.repo_type in self.val:
414 424 return True
415 425 else:
416 426 log.warning('Current view is not supported for repo type:%s',
417 427 rhodecode_db_repo.repo_type)
418 428 #
419 429 # h.flash(h.literal(
420 430 # _('Action not supported for %s.' % rhodecode_repo.alias)),
421 431 # category='warning')
422 432 # return redirect(
423 433 # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name))
424 434
425 435 return False
426 436
427 437
428 438 class RepoGroupRoutePredicate(object):
429 439 def __init__(self, val, config):
430 440 self.val = val
431 441
432 442 def text(self):
433 443 return 'repo_group_route = %s' % self.val
434 444
435 445 phash = text
436 446
437 447 def __call__(self, info, request):
438 448 if hasattr(request, 'vcs_call'):
439 449 # skip vcs calls
440 450 return
441 451
442 452 repo_group_name = info['match']['repo_group_name']
443 453 repo_group_model = repo_group.RepoGroupModel()
444 454 by_name_match = repo_group_model.get_by_group_name(
445 455 repo_group_name, cache=True)
446 456
447 457 if by_name_match:
448 458 # register this as request object we can re-use later
449 459 request.db_repo_group = by_name_match
450 460 return True
451 461
452 462 return False
453 463
454 464
455 465 def includeme(config):
456 466 config.add_route_predicate(
457 467 'repo_route', RepoRoutePredicate)
458 468 config.add_route_predicate(
459 469 'repo_accepted_types', RepoTypeRoutePredicate)
460 470 config.add_route_predicate(
461 471 'repo_group_route', RepoGroupRoutePredicate)
General Comments 0
You need to be logged in to leave comments. Login now