##// END OF EJS Templates
missing requirements: better handling of missing requirements for repositories....
marcink -
r2625:aff7e7b8 default
parent child Browse files
Show More
@@ -1,626 +1,635 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2018 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 import operator
24 24
25 25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
26 26
27 27 from rhodecode.lib import helpers as h, diffs
28 28 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
29 29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
30 30 from rhodecode.model import repo
31 31 from rhodecode.model import repo_group
32 32 from rhodecode.model import user_group
33 33 from rhodecode.model import user
34 34 from rhodecode.model.db import User
35 35 from rhodecode.model.scm import ScmModel
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 ADMIN_PREFIX = '/_admin'
41 41 STATIC_FILE_PREFIX = '/_static'
42 42
43 43 URL_NAME_REQUIREMENTS = {
44 44 # group name can have a slash in them, but they must not end with a slash
45 45 'group_name': r'.*?[^/]',
46 46 'repo_group_name': r'.*?[^/]',
47 47 # repo names can have a slash in them, but they must not end with a slash
48 48 'repo_name': r'.*?[^/]',
49 49 # file path eats up everything at the end
50 50 'f_path': r'.*',
51 51 # reference types
52 52 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
53 53 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
54 54 }
55 55
56 56
57 57 def add_route_with_slash(config,name, pattern, **kw):
58 58 config.add_route(name, pattern, **kw)
59 59 if not pattern.endswith('/'):
60 60 config.add_route(name + '_slash', pattern + '/', **kw)
61 61
62 62
63 63 def add_route_requirements(route_path, requirements=URL_NAME_REQUIREMENTS):
64 64 """
65 65 Adds regex requirements to pyramid routes using a mapping dict
66 66 e.g::
67 67 add_route_requirements('{repo_name}/settings')
68 68 """
69 69 for key, regex in requirements.items():
70 70 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
71 71 return route_path
72 72
73 73
74 74 def get_format_ref_id(repo):
75 75 """Returns a `repo` specific reference formatter function"""
76 76 if h.is_svn(repo):
77 77 return _format_ref_id_svn
78 78 else:
79 79 return _format_ref_id
80 80
81 81
82 82 def _format_ref_id(name, raw_id):
83 83 """Default formatting of a given reference `name`"""
84 84 return name
85 85
86 86
87 87 def _format_ref_id_svn(name, raw_id):
88 88 """Special way of formatting a reference for Subversion including path"""
89 89 return '%s@%s' % (name, raw_id)
90 90
91 91
92 92 class TemplateArgs(StrictAttributeDict):
93 93 pass
94 94
95 95
96 96 class BaseAppView(object):
97 97
98 98 def __init__(self, context, request):
99 99 self.request = request
100 100 self.context = context
101 101 self.session = request.session
102 102 self._rhodecode_user = request.user # auth user
103 103 self._rhodecode_db_user = self._rhodecode_user.get_instance()
104 104 self._maybe_needs_password_change(
105 105 request.matched_route.name, self._rhodecode_db_user)
106 106
107 107 def _maybe_needs_password_change(self, view_name, user_obj):
108 108 log.debug('Checking if user %s needs password change on view %s',
109 109 user_obj, view_name)
110 110 skip_user_views = [
111 111 'logout', 'login',
112 112 'my_account_password', 'my_account_password_update'
113 113 ]
114 114
115 115 if not user_obj:
116 116 return
117 117
118 118 if user_obj.username == User.DEFAULT_USER:
119 119 return
120 120
121 121 now = time.time()
122 122 should_change = user_obj.user_data.get('force_password_change')
123 123 change_after = safe_int(should_change) or 0
124 124 if should_change and now > change_after:
125 125 log.debug('User %s requires password change', user_obj)
126 126 h.flash('You are required to change your password', 'warning',
127 127 ignore_duplicate=True)
128 128
129 129 if view_name not in skip_user_views:
130 130 raise HTTPFound(
131 131 self.request.route_path('my_account_password'))
132 132
133 133 def _log_creation_exception(self, e, repo_name):
134 134 _ = self.request.translate
135 135 reason = None
136 136 if len(e.args) == 2:
137 137 reason = e.args[1]
138 138
139 139 if reason == 'INVALID_CERTIFICATE':
140 140 log.exception(
141 141 'Exception creating a repository: invalid certificate')
142 142 msg = (_('Error creating repository %s: invalid certificate')
143 143 % repo_name)
144 144 else:
145 145 log.exception("Exception creating a repository")
146 146 msg = (_('Error creating repository %s')
147 147 % repo_name)
148 148 return msg
149 149
150 150 def _get_local_tmpl_context(self, include_app_defaults=True):
151 151 c = TemplateArgs()
152 152 c.auth_user = self.request.user
153 153 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
154 154 c.rhodecode_user = self.request.user
155 155
156 156 if include_app_defaults:
157 157 from rhodecode.lib.base import attach_context_attributes
158 158 attach_context_attributes(c, self.request, self.request.user.user_id)
159 159
160 160 return c
161 161
162 162 def _get_template_context(self, tmpl_args, **kwargs):
163 163
164 164 local_tmpl_args = {
165 165 'defaults': {},
166 166 'errors': {},
167 167 'c': tmpl_args
168 168 }
169 169 local_tmpl_args.update(kwargs)
170 170 return local_tmpl_args
171 171
172 172 def load_default_context(self):
173 173 """
174 174 example:
175 175
176 176 def load_default_context(self):
177 177 c = self._get_local_tmpl_context()
178 178 c.custom_var = 'foobar'
179 179
180 180 return c
181 181 """
182 182 raise NotImplementedError('Needs implementation in view class')
183 183
184 184
185 185 class RepoAppView(BaseAppView):
186 186
187 187 def __init__(self, context, request):
188 188 super(RepoAppView, self).__init__(context, request)
189 189 self.db_repo = request.db_repo
190 190 self.db_repo_name = self.db_repo.repo_name
191 191 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
192 192
193 193 def _handle_missing_requirements(self, error):
194 194 log.error(
195 195 'Requirements are missing for repository %s: %s',
196 196 self.db_repo_name, error.message)
197 197
198 198 def _get_local_tmpl_context(self, include_app_defaults=True):
199 199 _ = self.request.translate
200 200 c = super(RepoAppView, self)._get_local_tmpl_context(
201 201 include_app_defaults=include_app_defaults)
202 202
203 203 # register common vars for this type of view
204 204 c.rhodecode_db_repo = self.db_repo
205 205 c.repo_name = self.db_repo_name
206 206 c.repository_pull_requests = self.db_repo_pull_requests
207 207 self.path_filter = PathFilter(None)
208 208
209 c.repository_requirements_missing = False
209 c.repository_requirements_missing = {}
210 210 try:
211 211 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
212 212 if self.rhodecode_vcs_repo:
213 213 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
214 214 c.auth_user.username)
215 215 self.path_filter = PathFilter(path_perms)
216 216 except RepositoryRequirementError as e:
217 c.repository_requirements_missing = True
217 c.repository_requirements_missing = {'error': str(e)}
218 218 self._handle_missing_requirements(e)
219 219 self.rhodecode_vcs_repo = None
220 220
221 221 c.path_filter = self.path_filter # used by atom_feed_entry.mako
222 222
223 if (not c.repository_requirements_missing
224 and self.rhodecode_vcs_repo is None):
223 if self.rhodecode_vcs_repo is None:
225 224 # unable to fetch this repo as vcs instance, report back to user
226 225 h.flash(_(
227 226 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
228 227 "Please check if it exist, or is not damaged.") %
229 228 {'repo_name': c.repo_name},
230 229 category='error', ignore_duplicate=True)
230 if c.repository_requirements_missing:
231 route = self.request.matched_route.name
232 if route.startswith(('edit_repo', 'repo_summary')):
233 # allow summary and edit repo on missing requirements
234 return c
235
236 raise HTTPFound(
237 h.route_path('repo_summary', repo_name=self.db_repo_name))
238
239 else: # redirect if we don't show missing requirements
231 240 raise HTTPFound(h.route_path('home'))
232 241
233 242 return c
234 243
235 244 def _get_f_path_unchecked(self, matchdict, default=None):
236 245 """
237 246 Should only be used by redirects, everything else should call _get_f_path
238 247 """
239 248 f_path = matchdict.get('f_path')
240 249 if f_path:
241 250 # fix for multiple initial slashes that causes errors for GIT
242 251 return f_path.lstrip('/')
243 252
244 253 return default
245 254
246 255 def _get_f_path(self, matchdict, default=None):
247 256 f_path_match = self._get_f_path_unchecked(matchdict, default)
248 257 return self.path_filter.assert_path_permissions(f_path_match)
249 258
250 259
251 260 class PathFilter(object):
252 261
253 262 # Expects and instance of BasePathPermissionChecker or None
254 263 def __init__(self, permission_checker):
255 264 self.permission_checker = permission_checker
256 265
257 266 def assert_path_permissions(self, path):
258 267 if path and self.permission_checker and not self.permission_checker.has_access(path):
259 268 raise HTTPForbidden()
260 269 return path
261 270
262 271 def filter_patchset(self, patchset):
263 272 if not self.permission_checker or not patchset:
264 273 return patchset, False
265 274 had_filtered = False
266 275 filtered_patchset = []
267 276 for patch in patchset:
268 277 filename = patch.get('filename', None)
269 278 if not filename or self.permission_checker.has_access(filename):
270 279 filtered_patchset.append(patch)
271 280 else:
272 281 had_filtered = True
273 282 if had_filtered:
274 283 if isinstance(patchset, diffs.LimitedDiffContainer):
275 284 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
276 285 return filtered_patchset, True
277 286 else:
278 287 return patchset, False
279 288
280 289 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
281 290 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
282 291 result = diffset.render_patchset(filtered_patchset, source_ref=source_ref, target_ref=target_ref)
283 292 result.has_hidden_changes = has_hidden_changes
284 293 return result
285 294
286 295 def get_raw_patch(self, diff_processor):
287 296 if self.permission_checker is None:
288 297 return diff_processor.as_raw()
289 298 elif self.permission_checker.has_full_access:
290 299 return diff_processor.as_raw()
291 300 else:
292 301 return '# Repository has user-specific filters, raw patch generation is disabled.'
293 302
294 303 @property
295 304 def is_enabled(self):
296 305 return self.permission_checker is not None
297 306
298 307
299 308 class RepoGroupAppView(BaseAppView):
300 309 def __init__(self, context, request):
301 310 super(RepoGroupAppView, self).__init__(context, request)
302 311 self.db_repo_group = request.db_repo_group
303 312 self.db_repo_group_name = self.db_repo_group.group_name
304 313
305 314 def _revoke_perms_on_yourself(self, form_result):
306 315 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
307 316 form_result['perm_updates'])
308 317 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
309 318 form_result['perm_additions'])
310 319 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
311 320 form_result['perm_deletions'])
312 321 admin_perm = 'group.admin'
313 322 if _updates and _updates[0][1] != admin_perm or \
314 323 _additions and _additions[0][1] != admin_perm or \
315 324 _deletions and _deletions[0][1] != admin_perm:
316 325 return True
317 326 return False
318 327
319 328
320 329 class UserGroupAppView(BaseAppView):
321 330 def __init__(self, context, request):
322 331 super(UserGroupAppView, self).__init__(context, request)
323 332 self.db_user_group = request.db_user_group
324 333 self.db_user_group_name = self.db_user_group.users_group_name
325 334
326 335
327 336 class UserAppView(BaseAppView):
328 337 def __init__(self, context, request):
329 338 super(UserAppView, self).__init__(context, request)
330 339 self.db_user = request.db_user
331 340 self.db_user_id = self.db_user.user_id
332 341
333 342 _ = self.request.translate
334 343 if not request.db_user_supports_default:
335 344 if self.db_user.username == User.DEFAULT_USER:
336 345 h.flash(_("Editing user `{}` is disabled.".format(
337 346 User.DEFAULT_USER)), category='warning')
338 347 raise HTTPFound(h.route_path('users'))
339 348
340 349
341 350 class DataGridAppView(object):
342 351 """
343 352 Common class to have re-usable grid rendering components
344 353 """
345 354
346 355 def _extract_ordering(self, request, column_map=None):
347 356 column_map = column_map or {}
348 357 column_index = safe_int(request.GET.get('order[0][column]'))
349 358 order_dir = request.GET.get(
350 359 'order[0][dir]', 'desc')
351 360 order_by = request.GET.get(
352 361 'columns[%s][data][sort]' % column_index, 'name_raw')
353 362
354 363 # translate datatable to DB columns
355 364 order_by = column_map.get(order_by) or order_by
356 365
357 366 search_q = request.GET.get('search[value]')
358 367 return search_q, order_by, order_dir
359 368
360 369 def _extract_chunk(self, request):
361 370 start = safe_int(request.GET.get('start'), 0)
362 371 length = safe_int(request.GET.get('length'), 25)
363 372 draw = safe_int(request.GET.get('draw'))
364 373 return draw, start, length
365 374
366 375 def _get_order_col(self, order_by, model):
367 376 if isinstance(order_by, basestring):
368 377 try:
369 378 return operator.attrgetter(order_by)(model)
370 379 except AttributeError:
371 380 return None
372 381 else:
373 382 return order_by
374 383
375 384
376 385 class BaseReferencesView(RepoAppView):
377 386 """
378 387 Base for reference view for branches, tags and bookmarks.
379 388 """
380 389 def load_default_context(self):
381 390 c = self._get_local_tmpl_context()
382 391
383 392
384 393 return c
385 394
386 395 def load_refs_context(self, ref_items, partials_template):
387 396 _render = self.request.get_partial_renderer(partials_template)
388 397 pre_load = ["author", "date", "message"]
389 398
390 399 is_svn = h.is_svn(self.rhodecode_vcs_repo)
391 400 is_hg = h.is_hg(self.rhodecode_vcs_repo)
392 401
393 402 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
394 403
395 404 closed_refs = {}
396 405 if is_hg:
397 406 closed_refs = self.rhodecode_vcs_repo.branches_closed
398 407
399 408 data = []
400 409 for ref_name, commit_id in ref_items:
401 410 commit = self.rhodecode_vcs_repo.get_commit(
402 411 commit_id=commit_id, pre_load=pre_load)
403 412 closed = ref_name in closed_refs
404 413
405 414 # TODO: johbo: Unify generation of reference links
406 415 use_commit_id = '/' in ref_name or is_svn
407 416
408 417 if use_commit_id:
409 418 files_url = h.route_path(
410 419 'repo_files',
411 420 repo_name=self.db_repo_name,
412 421 f_path=ref_name if is_svn else '',
413 422 commit_id=commit_id)
414 423
415 424 else:
416 425 files_url = h.route_path(
417 426 'repo_files',
418 427 repo_name=self.db_repo_name,
419 428 f_path=ref_name if is_svn else '',
420 429 commit_id=ref_name,
421 430 _query=dict(at=ref_name))
422 431
423 432 data.append({
424 433 "name": _render('name', ref_name, files_url, closed),
425 434 "name_raw": ref_name,
426 435 "date": _render('date', commit.date),
427 436 "date_raw": datetime_to_time(commit.date),
428 437 "author": _render('author', commit.author),
429 438 "commit": _render(
430 439 'commit', commit.message, commit.raw_id, commit.idx),
431 440 "commit_raw": commit.idx,
432 441 "compare": _render(
433 442 'compare', format_ref_id(ref_name, commit.raw_id)),
434 443 })
435 444
436 445 return data
437 446
438 447
439 448 class RepoRoutePredicate(object):
440 449 def __init__(self, val, config):
441 450 self.val = val
442 451
443 452 def text(self):
444 453 return 'repo_route = %s' % self.val
445 454
446 455 phash = text
447 456
448 457 def __call__(self, info, request):
449 458
450 459 if hasattr(request, 'vcs_call'):
451 460 # skip vcs calls
452 461 return
453 462
454 463 repo_name = info['match']['repo_name']
455 464 repo_model = repo.RepoModel()
456 465 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
457 466
458 467 def redirect_if_creating(db_repo):
459 468 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
460 469 raise HTTPFound(
461 470 request.route_path('repo_creating',
462 471 repo_name=db_repo.repo_name))
463 472
464 473 if by_name_match:
465 474 # register this as request object we can re-use later
466 475 request.db_repo = by_name_match
467 476 redirect_if_creating(by_name_match)
468 477 return True
469 478
470 479 by_id_match = repo_model.get_repo_by_id(repo_name)
471 480 if by_id_match:
472 481 request.db_repo = by_id_match
473 482 redirect_if_creating(by_id_match)
474 483 return True
475 484
476 485 return False
477 486
478 487
479 488 class RepoTypeRoutePredicate(object):
480 489 def __init__(self, val, config):
481 490 self.val = val or ['hg', 'git', 'svn']
482 491
483 492 def text(self):
484 493 return 'repo_accepted_type = %s' % self.val
485 494
486 495 phash = text
487 496
488 497 def __call__(self, info, request):
489 498 if hasattr(request, 'vcs_call'):
490 499 # skip vcs calls
491 500 return
492 501
493 502 rhodecode_db_repo = request.db_repo
494 503
495 504 log.debug(
496 505 '%s checking repo type for %s in %s',
497 506 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
498 507
499 508 if rhodecode_db_repo.repo_type in self.val:
500 509 return True
501 510 else:
502 511 log.warning('Current view is not supported for repo type:%s',
503 512 rhodecode_db_repo.repo_type)
504 513 #
505 514 # h.flash(h.literal(
506 515 # _('Action not supported for %s.' % rhodecode_repo.alias)),
507 516 # category='warning')
508 517 # return redirect(
509 518 # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name))
510 519
511 520 return False
512 521
513 522
514 523 class RepoGroupRoutePredicate(object):
515 524 def __init__(self, val, config):
516 525 self.val = val
517 526
518 527 def text(self):
519 528 return 'repo_group_route = %s' % self.val
520 529
521 530 phash = text
522 531
523 532 def __call__(self, info, request):
524 533 if hasattr(request, 'vcs_call'):
525 534 # skip vcs calls
526 535 return
527 536
528 537 repo_group_name = info['match']['repo_group_name']
529 538 repo_group_model = repo_group.RepoGroupModel()
530 539 by_name_match = repo_group_model.get_by_group_name(
531 540 repo_group_name, cache=True)
532 541
533 542 if by_name_match:
534 543 # register this as request object we can re-use later
535 544 request.db_repo_group = by_name_match
536 545 return True
537 546
538 547 return False
539 548
540 549
541 550 class UserGroupRoutePredicate(object):
542 551 def __init__(self, val, config):
543 552 self.val = val
544 553
545 554 def text(self):
546 555 return 'user_group_route = %s' % self.val
547 556
548 557 phash = text
549 558
550 559 def __call__(self, info, request):
551 560 if hasattr(request, 'vcs_call'):
552 561 # skip vcs calls
553 562 return
554 563
555 564 user_group_id = info['match']['user_group_id']
556 565 user_group_model = user_group.UserGroup()
557 566 by_id_match = user_group_model.get(
558 567 user_group_id, cache=True)
559 568
560 569 if by_id_match:
561 570 # register this as request object we can re-use later
562 571 request.db_user_group = by_id_match
563 572 return True
564 573
565 574 return False
566 575
567 576
568 577 class UserRoutePredicateBase(object):
569 578 supports_default = None
570 579
571 580 def __init__(self, val, config):
572 581 self.val = val
573 582
574 583 def text(self):
575 584 raise NotImplementedError()
576 585
577 586 def __call__(self, info, request):
578 587 if hasattr(request, 'vcs_call'):
579 588 # skip vcs calls
580 589 return
581 590
582 591 user_id = info['match']['user_id']
583 592 user_model = user.User()
584 593 by_id_match = user_model.get(
585 594 user_id, cache=True)
586 595
587 596 if by_id_match:
588 597 # register this as request object we can re-use later
589 598 request.db_user = by_id_match
590 599 request.db_user_supports_default = self.supports_default
591 600 return True
592 601
593 602 return False
594 603
595 604
596 605 class UserRoutePredicate(UserRoutePredicateBase):
597 606 supports_default = False
598 607
599 608 def text(self):
600 609 return 'user_route = %s' % self.val
601 610
602 611 phash = text
603 612
604 613
605 614 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
606 615 supports_default = True
607 616
608 617 def text(self):
609 618 return 'user_with_default_route = %s' % self.val
610 619
611 620 phash = text
612 621
613 622
614 623 def includeme(config):
615 624 config.add_route_predicate(
616 625 'repo_route', RepoRoutePredicate)
617 626 config.add_route_predicate(
618 627 'repo_accepted_types', RepoTypeRoutePredicate)
619 628 config.add_route_predicate(
620 629 'repo_group_route', RepoGroupRoutePredicate)
621 630 config.add_route_predicate(
622 631 'user_group_route', UserGroupRoutePredicate)
623 632 config.add_route_predicate(
624 633 'user_route_with_default', UserRouteWithDefaultPredicate)
625 634 config.add_route_predicate(
626 635 'user_route', UserRoutePredicate) No newline at end of file
@@ -1,522 +1,523 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 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 re
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.apps.repository.views.repo_summary import RepoSummaryView
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib.compat import OrderedDict
29 29 from rhodecode.lib.utils2 import AttributeDict, safe_str
30 30 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
31 31 from rhodecode.model.db import Repository
32 32 from rhodecode.model.meta import Session
33 33 from rhodecode.model.repo import RepoModel
34 34 from rhodecode.model.scm import ScmModel
35 35 from rhodecode.tests import assert_session_flash
36 36 from rhodecode.tests.fixture import Fixture
37 37 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
38 38
39 39
40 40 fixture = Fixture()
41 41
42 42
43 43 def route_path(name, params=None, **kwargs):
44 44 import urllib
45 45
46 46 base_url = {
47 47 'repo_summary': '/{repo_name}',
48 48 'repo_stats': '/{repo_name}/repo_stats/{commit_id}',
49 49 'repo_refs_data': '/{repo_name}/refs-data',
50 50 'repo_refs_changelog_data': '/{repo_name}/refs-data-changelog',
51 51 'repo_creating_check': '/{repo_name}/repo_creating_check',
52 52 }[name].format(**kwargs)
53 53
54 54 if params:
55 55 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
56 56 return base_url
57 57
58 58
59 59 def assert_clone_url(response, server, repo, disabled=False):
60 60
61 61 response.mustcontain(
62 62 '<input type="text" class="input-monospace clone_url_input" '
63 63 '{disabled}readonly="readonly" '
64 64 'value="http://test_admin@{server}/{repo}"/>'.format(
65 65 server=server, repo=repo, disabled='disabled ' if disabled else ' ')
66 66 )
67 67
68 68
69 69 @pytest.mark.usefixtures('app')
70 70 class TestSummaryView(object):
71 71 def test_index(self, autologin_user, backend, http_host_only_stub):
72 72 repo_id = backend.repo.repo_id
73 73 repo_name = backend.repo_name
74 74 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
75 75 return_value=False):
76 76 response = self.app.get(
77 77 route_path('repo_summary', repo_name=repo_name))
78 78
79 79 # repo type
80 80 response.mustcontain(
81 81 '<i class="icon-%s">' % (backend.alias, )
82 82 )
83 83 # public/private
84 84 response.mustcontain(
85 85 """<i class="icon-unlock-alt">"""
86 86 )
87 87
88 88 # clone url...
89 89 assert_clone_url(response, http_host_only_stub, repo_name)
90 90 assert_clone_url(response, http_host_only_stub, '_{}'.format(repo_id))
91 91
92 92 def test_index_svn_without_proxy(
93 93 self, autologin_user, backend_svn, http_host_only_stub):
94 94 repo_id = backend_svn.repo.repo_id
95 95 repo_name = backend_svn.repo_name
96 96 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
97 97 # clone url...
98 98
99 99 assert_clone_url(response, http_host_only_stub, repo_name, disabled=True)
100 100 assert_clone_url(response, http_host_only_stub, '_{}'.format(repo_id), disabled=True)
101 101
102 102 def test_index_with_trailing_slash(
103 103 self, autologin_user, backend, http_host_only_stub):
104 104
105 105 repo_id = backend.repo.repo_id
106 106 repo_name = backend.repo_name
107 107 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
108 108 return_value=False):
109 109 response = self.app.get(
110 110 route_path('repo_summary', repo_name=repo_name) + '/',
111 111 status=200)
112 112
113 113 # clone url...
114 114 assert_clone_url(response, http_host_only_stub, repo_name)
115 115 assert_clone_url(response, http_host_only_stub, '_{}'.format(repo_id))
116 116
117 117 def test_index_by_id(self, autologin_user, backend):
118 118 repo_id = backend.repo.repo_id
119 119 response = self.app.get(
120 120 route_path('repo_summary', repo_name='_%s' % (repo_id,)))
121 121
122 122 # repo type
123 123 response.mustcontain(
124 124 '<i class="icon-%s">' % (backend.alias, )
125 125 )
126 126 # public/private
127 127 response.mustcontain(
128 128 """<i class="icon-unlock-alt">"""
129 129 )
130 130
131 131 def test_index_by_repo_having_id_path_in_name_hg(self, autologin_user):
132 132 fixture.create_repo(name='repo_1')
133 133 response = self.app.get(route_path('repo_summary', repo_name='repo_1'))
134 134
135 135 try:
136 136 response.mustcontain("repo_1")
137 137 finally:
138 138 RepoModel().delete(Repository.get_by_repo_name('repo_1'))
139 139 Session().commit()
140 140
141 141 def test_index_with_anonymous_access_disabled(
142 142 self, backend, disable_anonymous_user):
143 143 response = self.app.get(
144 144 route_path('repo_summary', repo_name=backend.repo_name), status=302)
145 145 assert 'login' in response.location
146 146
147 147 def _enable_stats(self, repo):
148 148 r = Repository.get_by_repo_name(repo)
149 149 r.enable_statistics = True
150 150 Session().add(r)
151 151 Session().commit()
152 152
153 153 expected_trending = {
154 154 'hg': {
155 155 "py": {"count": 68, "desc": ["Python"]},
156 156 "rst": {"count": 16, "desc": ["Rst"]},
157 157 "css": {"count": 2, "desc": ["Css"]},
158 158 "sh": {"count": 2, "desc": ["Bash"]},
159 159 "bat": {"count": 1, "desc": ["Batch"]},
160 160 "cfg": {"count": 1, "desc": ["Ini"]},
161 161 "html": {"count": 1, "desc": ["EvoqueHtml", "Html"]},
162 162 "ini": {"count": 1, "desc": ["Ini"]},
163 163 "js": {"count": 1, "desc": ["Javascript"]},
164 164 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]}
165 165 },
166 166 'git': {
167 167 "py": {"count": 68, "desc": ["Python"]},
168 168 "rst": {"count": 16, "desc": ["Rst"]},
169 169 "css": {"count": 2, "desc": ["Css"]},
170 170 "sh": {"count": 2, "desc": ["Bash"]},
171 171 "bat": {"count": 1, "desc": ["Batch"]},
172 172 "cfg": {"count": 1, "desc": ["Ini"]},
173 173 "html": {"count": 1, "desc": ["EvoqueHtml", "Html"]},
174 174 "ini": {"count": 1, "desc": ["Ini"]},
175 175 "js": {"count": 1, "desc": ["Javascript"]},
176 176 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]}
177 177 },
178 178 'svn': {
179 179 "py": {"count": 75, "desc": ["Python"]},
180 180 "rst": {"count": 16, "desc": ["Rst"]},
181 181 "html": {"count": 11, "desc": ["EvoqueHtml", "Html"]},
182 182 "css": {"count": 2, "desc": ["Css"]},
183 183 "bat": {"count": 1, "desc": ["Batch"]},
184 184 "cfg": {"count": 1, "desc": ["Ini"]},
185 185 "ini": {"count": 1, "desc": ["Ini"]},
186 186 "js": {"count": 1, "desc": ["Javascript"]},
187 187 "makefile": {"count": 1, "desc": ["Makefile", "Makefile"]},
188 188 "sh": {"count": 1, "desc": ["Bash"]}
189 189 },
190 190 }
191 191
192 192 def test_repo_stats(self, autologin_user, backend, xhr_header):
193 193 response = self.app.get(
194 194 route_path(
195 195 'repo_stats', repo_name=backend.repo_name, commit_id='tip'),
196 196 extra_environ=xhr_header,
197 197 status=200)
198 198 assert re.match(r'6[\d\.]+ KiB', response.json['size'])
199 199
200 200 def test_repo_stats_code_stats_enabled(self, autologin_user, backend, xhr_header):
201 201 repo_name = backend.repo_name
202 202
203 203 # codes stats
204 204 self._enable_stats(repo_name)
205 205 ScmModel().mark_for_invalidation(repo_name)
206 206
207 207 response = self.app.get(
208 208 route_path(
209 209 'repo_stats', repo_name=backend.repo_name, commit_id='tip'),
210 210 extra_environ=xhr_header,
211 211 status=200)
212 212
213 213 expected_data = self.expected_trending[backend.alias]
214 214 returned_stats = response.json['code_stats']
215 215 for k, v in expected_data.items():
216 216 assert v == returned_stats[k]
217 217
218 218 def test_repo_refs_data(self, backend):
219 219 response = self.app.get(
220 220 route_path('repo_refs_data', repo_name=backend.repo_name),
221 221 status=200)
222 222
223 223 # Ensure that there is the correct amount of items in the result
224 224 repo = backend.repo.scm_instance()
225 225 data = response.json['results']
226 226 items = sum(len(section['children']) for section in data)
227 227 repo_refs = len(repo.branches) + len(repo.tags) + len(repo.bookmarks)
228 228 assert items == repo_refs
229 229
230 230 def test_index_shows_missing_requirements_message(
231 231 self, backend, autologin_user):
232 232 repo_name = backend.repo_name
233 233 scm_patcher = mock.patch.object(
234 234 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
235 235
236 236 with scm_patcher:
237 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
237 response = self.app.get(
238 route_path('repo_summary', repo_name=repo_name))
238 239 assert_response = AssertResponse(response)
239 240 assert_response.element_contains(
240 241 '.main .alert-warning strong', 'Missing requirements')
241 242 assert_response.element_contains(
242 243 '.main .alert-warning',
243 244 'Commits cannot be displayed, because this repository '
244 245 'uses one or more extensions, which was not enabled.')
245 246
246 247 def test_missing_requirements_page_does_not_contains_switch_to(
247 248 self, autologin_user, backend):
248 249 repo_name = backend.repo_name
249 250 scm_patcher = mock.patch.object(
250 251 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
251 252
252 253 with scm_patcher:
253 254 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
254 255 response.mustcontain(no='Switch To')
255 256
256 257
257 258 @pytest.mark.usefixtures('app')
258 259 class TestRepoLocation(object):
259 260
260 261 @pytest.mark.parametrize("suffix", [u'', u'ąęł'], ids=['', 'non-ascii'])
261 262 def test_missing_filesystem_repo(
262 263 self, autologin_user, backend, suffix, csrf_token):
263 264 repo = backend.create_repo(name_suffix=suffix)
264 265 repo_name = repo.repo_name
265 266
266 267 # delete from file system
267 268 RepoModel()._delete_filesystem_repo(repo)
268 269
269 270 # test if the repo is still in the database
270 271 new_repo = RepoModel().get_by_repo_name(repo_name)
271 272 assert new_repo.repo_name == repo_name
272 273
273 274 # check if repo is not in the filesystem
274 275 assert not repo_on_filesystem(repo_name)
275 276
276 277 response = self.app.get(
277 278 route_path('repo_summary', repo_name=safe_str(repo_name)), status=302)
278 279
279 280 msg = 'The repository `%s` cannot be loaded in filesystem. ' \
280 281 'Please check if it exist, or is not damaged.' % repo_name
281 282 assert_session_flash(response, msg)
282 283
283 284 @pytest.mark.parametrize("suffix", [u'', u'ąęł'], ids=['', 'non-ascii'])
284 285 def test_missing_filesystem_repo_on_repo_check(
285 286 self, autologin_user, backend, suffix, csrf_token):
286 287 repo = backend.create_repo(name_suffix=suffix)
287 288 repo_name = repo.repo_name
288 289
289 290 # delete from file system
290 291 RepoModel()._delete_filesystem_repo(repo)
291 292
292 293 # test if the repo is still in the database
293 294 new_repo = RepoModel().get_by_repo_name(repo_name)
294 295 assert new_repo.repo_name == repo_name
295 296
296 297 # check if repo is not in the filesystem
297 298 assert not repo_on_filesystem(repo_name)
298 299
299 300 # flush the session
300 301 self.app.get(
301 302 route_path('repo_summary', repo_name=safe_str(repo_name)),
302 303 status=302)
303 304
304 305 response = self.app.get(
305 306 route_path('repo_creating_check', repo_name=safe_str(repo_name)),
306 307 status=200)
307 308 msg = 'The repository `%s` cannot be loaded in filesystem. ' \
308 309 'Please check if it exist, or is not damaged.' % repo_name
309 310 assert_session_flash(response, msg )
310 311
311 312
312 313 @pytest.fixture()
313 314 def summary_view(context_stub, request_stub, user_util):
314 315 """
315 316 Bootstrap view to test the view functions
316 317 """
317 318 request_stub.matched_route = AttributeDict(name='test_view')
318 319
319 320 request_stub.user = user_util.create_user().AuthUser()
320 321 request_stub.db_repo = user_util.create_repo()
321 322
322 323 view = RepoSummaryView(context=context_stub, request=request_stub)
323 324 return view
324 325
325 326
326 327 @pytest.mark.usefixtures('app')
327 328 class TestCreateReferenceData(object):
328 329
329 330 @pytest.fixture
330 331 def example_refs(self):
331 332 section_1_refs = OrderedDict((('a', 'a_id'), ('b', 'b_id')))
332 333 example_refs = [
333 334 ('section_1', section_1_refs, 't1'),
334 335 ('section_2', {'c': 'c_id'}, 't2'),
335 336 ]
336 337 return example_refs
337 338
338 339 def test_generates_refs_based_on_commit_ids(self, example_refs, summary_view):
339 340 repo = mock.Mock()
340 341 repo.name = 'test-repo'
341 342 repo.alias = 'git'
342 343 full_repo_name = 'pytest-repo-group/' + repo.name
343 344
344 345 result = summary_view._create_reference_data(
345 346 repo, full_repo_name, example_refs)
346 347
347 348 expected_files_url = '/{}/files/'.format(full_repo_name)
348 349 expected_result = [
349 350 {
350 351 'children': [
351 352 {
352 353 'id': 'a', 'raw_id': 'a_id', 'text': 'a', 'type': 't1',
353 354 'files_url': expected_files_url + 'a/?at=a',
354 355 },
355 356 {
356 357 'id': 'b', 'raw_id': 'b_id', 'text': 'b', 'type': 't1',
357 358 'files_url': expected_files_url + 'b/?at=b',
358 359 }
359 360 ],
360 361 'text': 'section_1'
361 362 },
362 363 {
363 364 'children': [
364 365 {
365 366 'id': 'c', 'raw_id': 'c_id', 'text': 'c', 'type': 't2',
366 367 'files_url': expected_files_url + 'c/?at=c',
367 368 }
368 369 ],
369 370 'text': 'section_2'
370 371 }]
371 372 assert result == expected_result
372 373
373 374 def test_generates_refs_with_path_for_svn(self, example_refs, summary_view):
374 375 repo = mock.Mock()
375 376 repo.name = 'test-repo'
376 377 repo.alias = 'svn'
377 378 full_repo_name = 'pytest-repo-group/' + repo.name
378 379
379 380 result = summary_view._create_reference_data(
380 381 repo, full_repo_name, example_refs)
381 382
382 383 expected_files_url = '/{}/files/'.format(full_repo_name)
383 384 expected_result = [
384 385 {
385 386 'children': [
386 387 {
387 388 'id': 'a@a_id', 'raw_id': 'a_id',
388 389 'text': 'a', 'type': 't1',
389 390 'files_url': expected_files_url + 'a_id/a?at=a',
390 391 },
391 392 {
392 393 'id': 'b@b_id', 'raw_id': 'b_id',
393 394 'text': 'b', 'type': 't1',
394 395 'files_url': expected_files_url + 'b_id/b?at=b',
395 396 }
396 397 ],
397 398 'text': 'section_1'
398 399 },
399 400 {
400 401 'children': [
401 402 {
402 403 'id': 'c@c_id', 'raw_id': 'c_id',
403 404 'text': 'c', 'type': 't2',
404 405 'files_url': expected_files_url + 'c_id/c?at=c',
405 406 }
406 407 ],
407 408 'text': 'section_2'
408 409 }
409 410 ]
410 411 assert result == expected_result
411 412
412 413
413 414 class TestCreateFilesUrl(object):
414 415
415 416 def test_creates_non_svn_url(self, app, summary_view):
416 417 repo = mock.Mock()
417 418 repo.name = 'abcde'
418 419 full_repo_name = 'test-repo-group/' + repo.name
419 420 ref_name = 'branch1'
420 421 raw_id = 'deadbeef0123456789'
421 422 is_svn = False
422 423
423 424 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
424 425 result = summary_view._create_files_url(
425 426 repo, full_repo_name, ref_name, raw_id, is_svn)
426 427 url_mock.assert_called_once_with(
427 428 'repo_files', repo_name=full_repo_name, commit_id=ref_name,
428 429 f_path='', _query=dict(at=ref_name))
429 430 assert result == url_mock.return_value
430 431
431 432 def test_creates_svn_url(self, app, summary_view):
432 433 repo = mock.Mock()
433 434 repo.name = 'abcde'
434 435 full_repo_name = 'test-repo-group/' + repo.name
435 436 ref_name = 'branch1'
436 437 raw_id = 'deadbeef0123456789'
437 438 is_svn = True
438 439
439 440 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
440 441 result = summary_view._create_files_url(
441 442 repo, full_repo_name, ref_name, raw_id, is_svn)
442 443 url_mock.assert_called_once_with(
443 444 'repo_files', repo_name=full_repo_name, f_path=ref_name,
444 445 commit_id=raw_id, _query=dict(at=ref_name))
445 446 assert result == url_mock.return_value
446 447
447 448 def test_name_has_slashes(self, app, summary_view):
448 449 repo = mock.Mock()
449 450 repo.name = 'abcde'
450 451 full_repo_name = 'test-repo-group/' + repo.name
451 452 ref_name = 'branch1/branch2'
452 453 raw_id = 'deadbeef0123456789'
453 454 is_svn = False
454 455
455 456 with mock.patch('rhodecode.lib.helpers.route_path') as url_mock:
456 457 result = summary_view._create_files_url(
457 458 repo, full_repo_name, ref_name, raw_id, is_svn)
458 459 url_mock.assert_called_once_with(
459 460 'repo_files', repo_name=full_repo_name, commit_id=raw_id,
460 461 f_path='', _query=dict(at=ref_name))
461 462 assert result == url_mock.return_value
462 463
463 464
464 465 class TestReferenceItems(object):
465 466 repo = mock.Mock()
466 467 repo.name = 'pytest-repo'
467 468 repo_full_name = 'pytest-repo-group/' + repo.name
468 469 ref_type = 'branch'
469 470 fake_url = '/abcde/'
470 471
471 472 @staticmethod
472 473 def _format_function(name, id_):
473 474 return 'format_function_{}_{}'.format(name, id_)
474 475
475 476 def test_creates_required_amount_of_items(self, summary_view):
476 477 amount = 100
477 478 refs = {
478 479 'ref{}'.format(i): '{0:040d}'.format(i)
479 480 for i in range(amount)
480 481 }
481 482
482 483 url_patcher = mock.patch.object(summary_view, '_create_files_url')
483 484 svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn',
484 485 return_value=False)
485 486
486 487 with url_patcher as url_mock, svn_patcher:
487 488 result = summary_view._create_reference_items(
488 489 self.repo, self.repo_full_name, refs, self.ref_type,
489 490 self._format_function)
490 491 assert len(result) == amount
491 492 assert url_mock.call_count == amount
492 493
493 494 def test_single_item_details(self, summary_view):
494 495 ref_name = 'ref1'
495 496 ref_id = 'deadbeef'
496 497 refs = {
497 498 ref_name: ref_id
498 499 }
499 500
500 501 svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn',
501 502 return_value=False)
502 503
503 504 url_patcher = mock.patch.object(
504 505 summary_view, '_create_files_url', return_value=self.fake_url)
505 506
506 507 with url_patcher as url_mock, svn_patcher:
507 508 result = summary_view._create_reference_items(
508 509 self.repo, self.repo_full_name, refs, self.ref_type,
509 510 self._format_function)
510 511
511 512 url_mock.assert_called_once_with(
512 513 self.repo, self.repo_full_name, ref_name, ref_id, False)
513 514 expected_result = [
514 515 {
515 516 'text': ref_name,
516 517 'id': self._format_function(ref_name, ref_id),
517 518 'raw_id': ref_id,
518 519 'type': self.ref_type,
519 520 'files_url': self.fake_url
520 521 }
521 522 ]
522 523 assert result == expected_result
@@ -1,32 +1,34 b''
1 1 <%inherit file="/summary/summary_base.mako"/>
2 2
3 3 <%namespace name="components" file="/summary/components.mako"/>
4 4
5 5 <%def name="main()">
6 6 <div class="title">
7 7 ${self.repo_page_title(c.rhodecode_db_repo)}
8 8 </div>
9 9
10 10 <div id="repo-summary" class="summary">
11 11 ${components.summary_detail(breadcrumbs_links=self.breadcrumbs_links(), show_downloads=False)}
12 12 ${components.summary_stats(gravatar_function=self.gravatar_with_user)}
13 13 </div><!--end repo-summary-->
14 14
15 15 <div class="alert alert-dismissable alert-warning">
16 16 <strong>Missing requirements</strong>
17 17 Commits cannot be displayed, because this repository uses one or more extensions, which was not enabled. <br/>
18 18 Please <a href="${h.route_path('edit_repo_vcs', repo_name=c.repo_name)}">enable extension in settings</a>, or contact the repository owner for help.
19 19 Missing extensions could be:
20 20 <pre>
21 21
22 22 - Mercurial largefiles
23 23 - Git LFS
24 24 </pre>
25 <br/>
26 Requirement error: ${c.repository_requirements_missing.get('error')}
25 27 </div>
26 28
27 29 </%def>
28 30
29 31
30 32 <%def name="menu_bar_subnav()">
31 33 ${self.repo_menu(active='summary')}
32 34 </%def>
General Comments 0
You need to be logged in to leave comments. Login now