##// END OF EJS Templates
repositories: bring back watch action in summary view
marcink -
r3670:4c16050d new-ui
parent child Browse files
Show More
@@ -1,723 +1,725 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 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 import compat
26 26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
27 27
28 28 from rhodecode.lib import helpers as h, diffs
29 29 from rhodecode.lib.utils2 import (
30 30 StrictAttributeDict, str2bool, safe_int, datetime_to_time, safe_unicode)
31 31 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
32 32 from rhodecode.model import repo
33 33 from rhodecode.model import repo_group
34 34 from rhodecode.model import user_group
35 35 from rhodecode.model import user
36 36 from rhodecode.model.db import User
37 37 from rhodecode.model.scm import ScmModel
38 38 from rhodecode.model.settings import VcsSettingsModel
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 ADMIN_PREFIX = '/_admin'
44 44 STATIC_FILE_PREFIX = '/_static'
45 45
46 46 URL_NAME_REQUIREMENTS = {
47 47 # group name can have a slash in them, but they must not end with a slash
48 48 'group_name': r'.*?[^/]',
49 49 'repo_group_name': r'.*?[^/]',
50 50 # repo names can have a slash in them, but they must not end with a slash
51 51 'repo_name': r'.*?[^/]',
52 52 # file path eats up everything at the end
53 53 'f_path': r'.*',
54 54 # reference types
55 55 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
56 56 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
57 57 }
58 58
59 59
60 60 def add_route_with_slash(config,name, pattern, **kw):
61 61 config.add_route(name, pattern, **kw)
62 62 if not pattern.endswith('/'):
63 63 config.add_route(name + '_slash', pattern + '/', **kw)
64 64
65 65
66 66 def add_route_requirements(route_path, requirements=None):
67 67 """
68 68 Adds regex requirements to pyramid routes using a mapping dict
69 69 e.g::
70 70 add_route_requirements('{repo_name}/settings')
71 71 """
72 72 requirements = requirements or URL_NAME_REQUIREMENTS
73 73 for key, regex in requirements.items():
74 74 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
75 75 return route_path
76 76
77 77
78 78 def get_format_ref_id(repo):
79 79 """Returns a `repo` specific reference formatter function"""
80 80 if h.is_svn(repo):
81 81 return _format_ref_id_svn
82 82 else:
83 83 return _format_ref_id
84 84
85 85
86 86 def _format_ref_id(name, raw_id):
87 87 """Default formatting of a given reference `name`"""
88 88 return name
89 89
90 90
91 91 def _format_ref_id_svn(name, raw_id):
92 92 """Special way of formatting a reference for Subversion including path"""
93 93 return '%s@%s' % (name, raw_id)
94 94
95 95
96 96 class TemplateArgs(StrictAttributeDict):
97 97 pass
98 98
99 99
100 100 class BaseAppView(object):
101 101
102 102 def __init__(self, context, request):
103 103 self.request = request
104 104 self.context = context
105 105 self.session = request.session
106 106 if not hasattr(request, 'user'):
107 107 # NOTE(marcink): edge case, we ended up in matched route
108 108 # but probably of web-app context, e.g API CALL/VCS CALL
109 109 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
110 110 log.warning('Unable to process request `%s` in this scope', request)
111 111 raise HTTPBadRequest()
112 112
113 113 self._rhodecode_user = request.user # auth user
114 114 self._rhodecode_db_user = self._rhodecode_user.get_instance()
115 115 self._maybe_needs_password_change(
116 116 request.matched_route.name, self._rhodecode_db_user)
117 117
118 118 def _maybe_needs_password_change(self, view_name, user_obj):
119 119 log.debug('Checking if user %s needs password change on view %s',
120 120 user_obj, view_name)
121 121 skip_user_views = [
122 122 'logout', 'login',
123 123 'my_account_password', 'my_account_password_update'
124 124 ]
125 125
126 126 if not user_obj:
127 127 return
128 128
129 129 if user_obj.username == User.DEFAULT_USER:
130 130 return
131 131
132 132 now = time.time()
133 133 should_change = user_obj.user_data.get('force_password_change')
134 134 change_after = safe_int(should_change) or 0
135 135 if should_change and now > change_after:
136 136 log.debug('User %s requires password change', user_obj)
137 137 h.flash('You are required to change your password', 'warning',
138 138 ignore_duplicate=True)
139 139
140 140 if view_name not in skip_user_views:
141 141 raise HTTPFound(
142 142 self.request.route_path('my_account_password'))
143 143
144 144 def _log_creation_exception(self, e, repo_name):
145 145 _ = self.request.translate
146 146 reason = None
147 147 if len(e.args) == 2:
148 148 reason = e.args[1]
149 149
150 150 if reason == 'INVALID_CERTIFICATE':
151 151 log.exception(
152 152 'Exception creating a repository: invalid certificate')
153 153 msg = (_('Error creating repository %s: invalid certificate')
154 154 % repo_name)
155 155 else:
156 156 log.exception("Exception creating a repository")
157 157 msg = (_('Error creating repository %s')
158 158 % repo_name)
159 159 return msg
160 160
161 161 def _get_local_tmpl_context(self, include_app_defaults=True):
162 162 c = TemplateArgs()
163 163 c.auth_user = self.request.user
164 164 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
165 165 c.rhodecode_user = self.request.user
166 166
167 167 if include_app_defaults:
168 168 from rhodecode.lib.base import attach_context_attributes
169 169 attach_context_attributes(c, self.request, self.request.user.user_id)
170 170
171 171 c.is_super_admin = c.auth_user.is_admin
172 172
173 173 c.can_create_repo = c.is_super_admin
174 174 c.can_create_repo_group = c.is_super_admin
175 175 c.can_create_user_group = c.is_super_admin
176 176
177 177 c.is_delegated_admin = False
178 178
179 179 if not c.auth_user.is_default and not c.is_super_admin:
180 180 c.can_create_repo = h.HasPermissionAny('hg.create.repository')(
181 181 user=self.request.user)
182 182 repositories = c.auth_user.repositories_admin or c.can_create_repo
183 183
184 184 c.can_create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')(
185 185 user=self.request.user)
186 186 repository_groups = c.auth_user.repository_groups_admin or c.can_create_repo_group
187 187
188 188 c.can_create_user_group = h.HasPermissionAny('hg.usergroup.create.true')(
189 189 user=self.request.user)
190 190 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
191 191 # delegated admin can create, or manage some objects
192 192 c.is_delegated_admin = repositories or repository_groups or user_groups
193 193 return c
194 194
195 195 def _get_template_context(self, tmpl_args, **kwargs):
196 196
197 197 local_tmpl_args = {
198 198 'defaults': {},
199 199 'errors': {},
200 200 'c': tmpl_args
201 201 }
202 202 local_tmpl_args.update(kwargs)
203 203 return local_tmpl_args
204 204
205 205 def load_default_context(self):
206 206 """
207 207 example:
208 208
209 209 def load_default_context(self):
210 210 c = self._get_local_tmpl_context()
211 211 c.custom_var = 'foobar'
212 212
213 213 return c
214 214 """
215 215 raise NotImplementedError('Needs implementation in view class')
216 216
217 217
218 218 class RepoAppView(BaseAppView):
219 219
220 220 def __init__(self, context, request):
221 221 super(RepoAppView, self).__init__(context, request)
222 222 self.db_repo = request.db_repo
223 223 self.db_repo_name = self.db_repo.repo_name
224 224 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
225 225
226 226 def _handle_missing_requirements(self, error):
227 227 log.error(
228 228 'Requirements are missing for repository %s: %s',
229 229 self.db_repo_name, safe_unicode(error))
230 230
231 231 def _get_local_tmpl_context(self, include_app_defaults=True):
232 232 _ = self.request.translate
233 233 c = super(RepoAppView, self)._get_local_tmpl_context(
234 234 include_app_defaults=include_app_defaults)
235 235
236 236 # register common vars for this type of view
237 237 c.rhodecode_db_repo = self.db_repo
238 238 c.repo_name = self.db_repo_name
239 239 c.repository_pull_requests = self.db_repo_pull_requests
240 c.repository_is_user_following = ScmModel().is_following_repo(
241 self.db_repo_name, self._rhodecode_user.user_id)
240 242 self.path_filter = PathFilter(None)
241 243
242 244 c.repository_requirements_missing = {}
243 245 try:
244 246 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
245 247 if self.rhodecode_vcs_repo:
246 248 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
247 249 c.auth_user.username)
248 250 self.path_filter = PathFilter(path_perms)
249 251 except RepositoryRequirementError as e:
250 252 c.repository_requirements_missing = {'error': str(e)}
251 253 self._handle_missing_requirements(e)
252 254 self.rhodecode_vcs_repo = None
253 255
254 256 c.path_filter = self.path_filter # used by atom_feed_entry.mako
255 257
256 258 if self.rhodecode_vcs_repo is None:
257 259 # unable to fetch this repo as vcs instance, report back to user
258 260 h.flash(_(
259 261 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
260 262 "Please check if it exist, or is not damaged.") %
261 263 {'repo_name': c.repo_name},
262 264 category='error', ignore_duplicate=True)
263 265 if c.repository_requirements_missing:
264 266 route = self.request.matched_route.name
265 267 if route.startswith(('edit_repo', 'repo_summary')):
266 268 # allow summary and edit repo on missing requirements
267 269 return c
268 270
269 271 raise HTTPFound(
270 272 h.route_path('repo_summary', repo_name=self.db_repo_name))
271 273
272 274 else: # redirect if we don't show missing requirements
273 275 raise HTTPFound(h.route_path('home'))
274 276
275 277 c.has_origin_repo_read_perm = False
276 278 if self.db_repo.fork:
277 279 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
278 280 'repository.write', 'repository.read', 'repository.admin')(
279 281 self.db_repo.fork.repo_name, 'summary fork link')
280 282
281 283 return c
282 284
283 285 def _get_f_path_unchecked(self, matchdict, default=None):
284 286 """
285 287 Should only be used by redirects, everything else should call _get_f_path
286 288 """
287 289 f_path = matchdict.get('f_path')
288 290 if f_path:
289 291 # fix for multiple initial slashes that causes errors for GIT
290 292 return f_path.lstrip('/')
291 293
292 294 return default
293 295
294 296 def _get_f_path(self, matchdict, default=None):
295 297 f_path_match = self._get_f_path_unchecked(matchdict, default)
296 298 return self.path_filter.assert_path_permissions(f_path_match)
297 299
298 300 def _get_general_setting(self, target_repo, settings_key, default=False):
299 301 settings_model = VcsSettingsModel(repo=target_repo)
300 302 settings = settings_model.get_general_settings()
301 303 return settings.get(settings_key, default)
302 304
303 305 def get_recache_flag(self):
304 306 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
305 307 flag_val = self.request.GET.get(flag_name)
306 308 if str2bool(flag_val):
307 309 return True
308 310 return False
309 311
310 312
311 313 class PathFilter(object):
312 314
313 315 # Expects and instance of BasePathPermissionChecker or None
314 316 def __init__(self, permission_checker):
315 317 self.permission_checker = permission_checker
316 318
317 319 def assert_path_permissions(self, path):
318 320 if path and self.permission_checker and not self.permission_checker.has_access(path):
319 321 raise HTTPForbidden()
320 322 return path
321 323
322 324 def filter_patchset(self, patchset):
323 325 if not self.permission_checker or not patchset:
324 326 return patchset, False
325 327 had_filtered = False
326 328 filtered_patchset = []
327 329 for patch in patchset:
328 330 filename = patch.get('filename', None)
329 331 if not filename or self.permission_checker.has_access(filename):
330 332 filtered_patchset.append(patch)
331 333 else:
332 334 had_filtered = True
333 335 if had_filtered:
334 336 if isinstance(patchset, diffs.LimitedDiffContainer):
335 337 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
336 338 return filtered_patchset, True
337 339 else:
338 340 return patchset, False
339 341
340 342 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
341 343 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
342 344 result = diffset.render_patchset(
343 345 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
344 346 result.has_hidden_changes = has_hidden_changes
345 347 return result
346 348
347 349 def get_raw_patch(self, diff_processor):
348 350 if self.permission_checker is None:
349 351 return diff_processor.as_raw()
350 352 elif self.permission_checker.has_full_access:
351 353 return diff_processor.as_raw()
352 354 else:
353 355 return '# Repository has user-specific filters, raw patch generation is disabled.'
354 356
355 357 @property
356 358 def is_enabled(self):
357 359 return self.permission_checker is not None
358 360
359 361
360 362 class RepoGroupAppView(BaseAppView):
361 363 def __init__(self, context, request):
362 364 super(RepoGroupAppView, self).__init__(context, request)
363 365 self.db_repo_group = request.db_repo_group
364 366 self.db_repo_group_name = self.db_repo_group.group_name
365 367
366 368 def _get_local_tmpl_context(self, include_app_defaults=True):
367 369 _ = self.request.translate
368 370 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
369 371 include_app_defaults=include_app_defaults)
370 372 c.repo_group = self.db_repo_group
371 373 return c
372 374
373 375 def _revoke_perms_on_yourself(self, form_result):
374 376 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
375 377 form_result['perm_updates'])
376 378 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
377 379 form_result['perm_additions'])
378 380 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
379 381 form_result['perm_deletions'])
380 382 admin_perm = 'group.admin'
381 383 if _updates and _updates[0][1] != admin_perm or \
382 384 _additions and _additions[0][1] != admin_perm or \
383 385 _deletions and _deletions[0][1] != admin_perm:
384 386 return True
385 387 return False
386 388
387 389
388 390 class UserGroupAppView(BaseAppView):
389 391 def __init__(self, context, request):
390 392 super(UserGroupAppView, self).__init__(context, request)
391 393 self.db_user_group = request.db_user_group
392 394 self.db_user_group_name = self.db_user_group.users_group_name
393 395
394 396
395 397 class UserAppView(BaseAppView):
396 398 def __init__(self, context, request):
397 399 super(UserAppView, self).__init__(context, request)
398 400 self.db_user = request.db_user
399 401 self.db_user_id = self.db_user.user_id
400 402
401 403 _ = self.request.translate
402 404 if not request.db_user_supports_default:
403 405 if self.db_user.username == User.DEFAULT_USER:
404 406 h.flash(_("Editing user `{}` is disabled.".format(
405 407 User.DEFAULT_USER)), category='warning')
406 408 raise HTTPFound(h.route_path('users'))
407 409
408 410
409 411 class DataGridAppView(object):
410 412 """
411 413 Common class to have re-usable grid rendering components
412 414 """
413 415
414 416 def _extract_ordering(self, request, column_map=None):
415 417 column_map = column_map or {}
416 418 column_index = safe_int(request.GET.get('order[0][column]'))
417 419 order_dir = request.GET.get(
418 420 'order[0][dir]', 'desc')
419 421 order_by = request.GET.get(
420 422 'columns[%s][data][sort]' % column_index, 'name_raw')
421 423
422 424 # translate datatable to DB columns
423 425 order_by = column_map.get(order_by) or order_by
424 426
425 427 search_q = request.GET.get('search[value]')
426 428 return search_q, order_by, order_dir
427 429
428 430 def _extract_chunk(self, request):
429 431 start = safe_int(request.GET.get('start'), 0)
430 432 length = safe_int(request.GET.get('length'), 25)
431 433 draw = safe_int(request.GET.get('draw'))
432 434 return draw, start, length
433 435
434 436 def _get_order_col(self, order_by, model):
435 437 if isinstance(order_by, compat.string_types):
436 438 try:
437 439 return operator.attrgetter(order_by)(model)
438 440 except AttributeError:
439 441 return None
440 442 else:
441 443 return order_by
442 444
443 445
444 446 class BaseReferencesView(RepoAppView):
445 447 """
446 448 Base for reference view for branches, tags and bookmarks.
447 449 """
448 450 def load_default_context(self):
449 451 c = self._get_local_tmpl_context()
450 452
451 453
452 454 return c
453 455
454 456 def load_refs_context(self, ref_items, partials_template):
455 457 _render = self.request.get_partial_renderer(partials_template)
456 458 pre_load = ["author", "date", "message"]
457 459
458 460 is_svn = h.is_svn(self.rhodecode_vcs_repo)
459 461 is_hg = h.is_hg(self.rhodecode_vcs_repo)
460 462
461 463 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
462 464
463 465 closed_refs = {}
464 466 if is_hg:
465 467 closed_refs = self.rhodecode_vcs_repo.branches_closed
466 468
467 469 data = []
468 470 for ref_name, commit_id in ref_items:
469 471 commit = self.rhodecode_vcs_repo.get_commit(
470 472 commit_id=commit_id, pre_load=pre_load)
471 473 closed = ref_name in closed_refs
472 474
473 475 # TODO: johbo: Unify generation of reference links
474 476 use_commit_id = '/' in ref_name or is_svn
475 477
476 478 if use_commit_id:
477 479 files_url = h.route_path(
478 480 'repo_files',
479 481 repo_name=self.db_repo_name,
480 482 f_path=ref_name if is_svn else '',
481 483 commit_id=commit_id)
482 484
483 485 else:
484 486 files_url = h.route_path(
485 487 'repo_files',
486 488 repo_name=self.db_repo_name,
487 489 f_path=ref_name if is_svn else '',
488 490 commit_id=ref_name,
489 491 _query=dict(at=ref_name))
490 492
491 493 data.append({
492 494 "name": _render('name', ref_name, files_url, closed),
493 495 "name_raw": ref_name,
494 496 "date": _render('date', commit.date),
495 497 "date_raw": datetime_to_time(commit.date),
496 498 "author": _render('author', commit.author),
497 499 "commit": _render(
498 500 'commit', commit.message, commit.raw_id, commit.idx),
499 501 "commit_raw": commit.idx,
500 502 "compare": _render(
501 503 'compare', format_ref_id(ref_name, commit.raw_id)),
502 504 })
503 505
504 506 return data
505 507
506 508
507 509 class RepoRoutePredicate(object):
508 510 def __init__(self, val, config):
509 511 self.val = val
510 512
511 513 def text(self):
512 514 return 'repo_route = %s' % self.val
513 515
514 516 phash = text
515 517
516 518 def __call__(self, info, request):
517 519 if hasattr(request, 'vcs_call'):
518 520 # skip vcs calls
519 521 return
520 522
521 523 repo_name = info['match']['repo_name']
522 524 repo_model = repo.RepoModel()
523 525
524 526 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
525 527
526 528 def redirect_if_creating(route_info, db_repo):
527 529 skip_views = ['edit_repo_advanced_delete']
528 530 route = route_info['route']
529 531 # we should skip delete view so we can actually "remove" repositories
530 532 # if they get stuck in creating state.
531 533 if route.name in skip_views:
532 534 return
533 535
534 536 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
535 537 repo_creating_url = request.route_path(
536 538 'repo_creating', repo_name=db_repo.repo_name)
537 539 raise HTTPFound(repo_creating_url)
538 540
539 541 if by_name_match:
540 542 # register this as request object we can re-use later
541 543 request.db_repo = by_name_match
542 544 redirect_if_creating(info, by_name_match)
543 545 return True
544 546
545 547 by_id_match = repo_model.get_repo_by_id(repo_name)
546 548 if by_id_match:
547 549 request.db_repo = by_id_match
548 550 redirect_if_creating(info, by_id_match)
549 551 return True
550 552
551 553 return False
552 554
553 555
554 556 class RepoForbidArchivedRoutePredicate(object):
555 557 def __init__(self, val, config):
556 558 self.val = val
557 559
558 560 def text(self):
559 561 return 'repo_forbid_archived = %s' % self.val
560 562
561 563 phash = text
562 564
563 565 def __call__(self, info, request):
564 566 _ = request.translate
565 567 rhodecode_db_repo = request.db_repo
566 568
567 569 log.debug(
568 570 '%s checking if archived flag for repo for %s',
569 571 self.__class__.__name__, rhodecode_db_repo.repo_name)
570 572
571 573 if rhodecode_db_repo.archived:
572 574 log.warning('Current view is not supported for archived repo:%s',
573 575 rhodecode_db_repo.repo_name)
574 576
575 577 h.flash(
576 578 h.literal(_('Action not supported for archived repository.')),
577 579 category='warning')
578 580 summary_url = request.route_path(
579 581 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
580 582 raise HTTPFound(summary_url)
581 583 return True
582 584
583 585
584 586 class RepoTypeRoutePredicate(object):
585 587 def __init__(self, val, config):
586 588 self.val = val or ['hg', 'git', 'svn']
587 589
588 590 def text(self):
589 591 return 'repo_accepted_type = %s' % self.val
590 592
591 593 phash = text
592 594
593 595 def __call__(self, info, request):
594 596 if hasattr(request, 'vcs_call'):
595 597 # skip vcs calls
596 598 return
597 599
598 600 rhodecode_db_repo = request.db_repo
599 601
600 602 log.debug(
601 603 '%s checking repo type for %s in %s',
602 604 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
603 605
604 606 if rhodecode_db_repo.repo_type in self.val:
605 607 return True
606 608 else:
607 609 log.warning('Current view is not supported for repo type:%s',
608 610 rhodecode_db_repo.repo_type)
609 611 return False
610 612
611 613
612 614 class RepoGroupRoutePredicate(object):
613 615 def __init__(self, val, config):
614 616 self.val = val
615 617
616 618 def text(self):
617 619 return 'repo_group_route = %s' % self.val
618 620
619 621 phash = text
620 622
621 623 def __call__(self, info, request):
622 624 if hasattr(request, 'vcs_call'):
623 625 # skip vcs calls
624 626 return
625 627
626 628 repo_group_name = info['match']['repo_group_name']
627 629 repo_group_model = repo_group.RepoGroupModel()
628 630 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
629 631
630 632 if by_name_match:
631 633 # register this as request object we can re-use later
632 634 request.db_repo_group = by_name_match
633 635 return True
634 636
635 637 return False
636 638
637 639
638 640 class UserGroupRoutePredicate(object):
639 641 def __init__(self, val, config):
640 642 self.val = val
641 643
642 644 def text(self):
643 645 return 'user_group_route = %s' % self.val
644 646
645 647 phash = text
646 648
647 649 def __call__(self, info, request):
648 650 if hasattr(request, 'vcs_call'):
649 651 # skip vcs calls
650 652 return
651 653
652 654 user_group_id = info['match']['user_group_id']
653 655 user_group_model = user_group.UserGroup()
654 656 by_id_match = user_group_model.get(user_group_id, cache=False)
655 657
656 658 if by_id_match:
657 659 # register this as request object we can re-use later
658 660 request.db_user_group = by_id_match
659 661 return True
660 662
661 663 return False
662 664
663 665
664 666 class UserRoutePredicateBase(object):
665 667 supports_default = None
666 668
667 669 def __init__(self, val, config):
668 670 self.val = val
669 671
670 672 def text(self):
671 673 raise NotImplementedError()
672 674
673 675 def __call__(self, info, request):
674 676 if hasattr(request, 'vcs_call'):
675 677 # skip vcs calls
676 678 return
677 679
678 680 user_id = info['match']['user_id']
679 681 user_model = user.User()
680 682 by_id_match = user_model.get(user_id, cache=False)
681 683
682 684 if by_id_match:
683 685 # register this as request object we can re-use later
684 686 request.db_user = by_id_match
685 687 request.db_user_supports_default = self.supports_default
686 688 return True
687 689
688 690 return False
689 691
690 692
691 693 class UserRoutePredicate(UserRoutePredicateBase):
692 694 supports_default = False
693 695
694 696 def text(self):
695 697 return 'user_route = %s' % self.val
696 698
697 699 phash = text
698 700
699 701
700 702 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
701 703 supports_default = True
702 704
703 705 def text(self):
704 706 return 'user_with_default_route = %s' % self.val
705 707
706 708 phash = text
707 709
708 710
709 711 def includeme(config):
710 712 config.add_route_predicate(
711 713 'repo_route', RepoRoutePredicate)
712 714 config.add_route_predicate(
713 715 'repo_accepted_types', RepoTypeRoutePredicate)
714 716 config.add_route_predicate(
715 717 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
716 718 config.add_route_predicate(
717 719 'repo_group_route', RepoGroupRoutePredicate)
718 720 config.add_route_predicate(
719 721 'user_group_route', UserGroupRoutePredicate)
720 722 config.add_route_predicate(
721 723 'user_route_with_default', UserRouteWithDefaultPredicate)
722 724 config.add_route_predicate(
723 725 'user_route', UserRoutePredicate)
@@ -1,386 +1,384 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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
22 22 import logging
23 23 import itertools
24 24
25 25 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
26 26
27 27 from pyramid.view import view_config
28 28 from pyramid.httpexceptions import HTTPBadRequest
29 29 from pyramid.response import Response
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode.apps._base import BaseAppView
33 33 from rhodecode.model.db import (
34 34 or_, joinedload, UserLog, UserFollowing, User, UserApiKeys)
35 35 from rhodecode.model.meta import Session
36 36 import rhodecode.lib.helpers as h
37 37 from rhodecode.lib.helpers import Page
38 38 from rhodecode.lib.user_log_filter import user_log_filter
39 39 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
40 40 from rhodecode.lib.utils2 import safe_int, AttributeDict, md5_safe
41 41 from rhodecode.model.scm import ScmModel
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 class JournalView(BaseAppView):
47 47
48 48 def load_default_context(self):
49 49 c = self._get_local_tmpl_context(include_app_defaults=True)
50 50
51 51 self._load_defaults(c.rhodecode_name)
52 52
53 53 # TODO(marcink): what is this, why we need a global register ?
54 54 c.search_term = self.request.GET.get('filter') or ''
55 55 return c
56 56
57 57 def _get_config(self, rhodecode_name):
58 58 import rhodecode
59 59 config = rhodecode.CONFIG
60 60
61 61 return {
62 62 'language': 'en-us',
63 63 'feed_ttl': '5', # TTL of feed,
64 64 'feed_items_per_page':
65 65 safe_int(config.get('rss_items_per_page', 20)),
66 66 'rhodecode_name': rhodecode_name
67 67 }
68 68
69 69 def _load_defaults(self, rhodecode_name):
70 70 config = self._get_config(rhodecode_name)
71 71 # common values for feeds
72 72 self.language = config["language"]
73 73 self.ttl = config["feed_ttl"]
74 74 self.feed_items_per_page = config['feed_items_per_page']
75 75 self.rhodecode_name = config['rhodecode_name']
76 76
77 77 def _get_daily_aggregate(self, journal):
78 78 groups = []
79 79 for k, g in itertools.groupby(journal, lambda x: x.action_as_day):
80 80 user_group = []
81 81 # groupby username if it's a present value, else
82 82 # fallback to journal username
83 83 for _, g2 in itertools.groupby(
84 84 list(g), lambda x: x.user.username if x.user else x.username):
85 85 l = list(g2)
86 86 user_group.append((l[0].user, l))
87 87
88 88 groups.append((k, user_group,))
89 89
90 90 return groups
91 91
92 92 def _get_journal_data(self, following_repos, search_term):
93 93 repo_ids = [x.follows_repository.repo_id for x in following_repos
94 94 if x.follows_repository is not None]
95 95 user_ids = [x.follows_user.user_id for x in following_repos
96 96 if x.follows_user is not None]
97 97
98 98 filtering_criterion = None
99 99
100 100 if repo_ids and user_ids:
101 101 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
102 102 UserLog.user_id.in_(user_ids))
103 103 if repo_ids and not user_ids:
104 104 filtering_criterion = UserLog.repository_id.in_(repo_ids)
105 105 if not repo_ids and user_ids:
106 106 filtering_criterion = UserLog.user_id.in_(user_ids)
107 107 if filtering_criterion is not None:
108 108 journal = Session().query(UserLog)\
109 109 .options(joinedload(UserLog.user))\
110 110 .options(joinedload(UserLog.repository))
111 111 # filter
112 112 try:
113 113 journal = user_log_filter(journal, search_term)
114 114 except Exception:
115 115 # we want this to crash for now
116 116 raise
117 117 journal = journal.filter(filtering_criterion)\
118 118 .order_by(UserLog.action_date.desc())
119 119 else:
120 120 journal = []
121 121
122 122 return journal
123 123
124 124 def feed_uid(self, entry_id):
125 125 return '{}:{}'.format('journal', md5_safe(entry_id))
126 126
127 127 def _atom_feed(self, repos, search_term, public=True):
128 128 _ = self.request.translate
129 129 journal = self._get_journal_data(repos, search_term)
130 130 if public:
131 131 _link = h.route_url('journal_public_atom')
132 132 _desc = '%s %s %s' % (self.rhodecode_name, _('public journal'),
133 133 'atom feed')
134 134 else:
135 135 _link = h.route_url('journal_atom')
136 136 _desc = '%s %s %s' % (self.rhodecode_name, _('journal'), 'atom feed')
137 137
138 138 feed = Atom1Feed(
139 139 title=_desc, link=_link, description=_desc,
140 140 language=self.language, ttl=self.ttl)
141 141
142 142 for entry in journal[:self.feed_items_per_page]:
143 143 user = entry.user
144 144 if user is None:
145 145 # fix deleted users
146 146 user = AttributeDict({'short_contact': entry.username,
147 147 'email': '',
148 148 'full_contact': ''})
149 149 action, action_extra, ico = h.action_parser(
150 150 self.request, entry, feed=True)
151 151 title = "%s - %s %s" % (user.short_contact, action(),
152 152 entry.repository.repo_name)
153 153 desc = action_extra()
154 154 _url = h.route_url('home')
155 155 if entry.repository is not None:
156 156 _url = h.route_url('repo_changelog',
157 157 repo_name=entry.repository.repo_name)
158 158
159 159 feed.add_item(
160 160 unique_id=self.feed_uid(entry.user_log_id),
161 161 title=title,
162 162 pubdate=entry.action_date,
163 163 link=_url,
164 164 author_email=user.email,
165 165 author_name=user.full_contact,
166 166 description=desc)
167 167
168 168 response = Response(feed.writeString('utf-8'))
169 169 response.content_type = feed.mime_type
170 170 return response
171 171
172 172 def _rss_feed(self, repos, search_term, public=True):
173 173 _ = self.request.translate
174 174 journal = self._get_journal_data(repos, search_term)
175 175 if public:
176 176 _link = h.route_url('journal_public_atom')
177 177 _desc = '%s %s %s' % (
178 178 self.rhodecode_name, _('public journal'), 'rss feed')
179 179 else:
180 180 _link = h.route_url('journal_atom')
181 181 _desc = '%s %s %s' % (
182 182 self.rhodecode_name, _('journal'), 'rss feed')
183 183
184 184 feed = Rss201rev2Feed(
185 185 title=_desc, link=_link, description=_desc,
186 186 language=self.language, ttl=self.ttl)
187 187
188 188 for entry in journal[:self.feed_items_per_page]:
189 189 user = entry.user
190 190 if user is None:
191 191 # fix deleted users
192 192 user = AttributeDict({'short_contact': entry.username,
193 193 'email': '',
194 194 'full_contact': ''})
195 195 action, action_extra, ico = h.action_parser(
196 196 self.request, entry, feed=True)
197 197 title = "%s - %s %s" % (user.short_contact, action(),
198 198 entry.repository.repo_name)
199 199 desc = action_extra()
200 200 _url = h.route_url('home')
201 201 if entry.repository is not None:
202 202 _url = h.route_url('repo_changelog',
203 203 repo_name=entry.repository.repo_name)
204 204
205 205 feed.add_item(
206 206 unique_id=self.feed_uid(entry.user_log_id),
207 207 title=title,
208 208 pubdate=entry.action_date,
209 209 link=_url,
210 210 author_email=user.email,
211 211 author_name=user.full_contact,
212 212 description=desc)
213 213
214 214 response = Response(feed.writeString('utf-8'))
215 215 response.content_type = feed.mime_type
216 216 return response
217 217
218 218 @LoginRequired()
219 219 @NotAnonymous()
220 220 @view_config(
221 221 route_name='journal', request_method='GET',
222 222 renderer=None)
223 223 def journal(self):
224 224 c = self.load_default_context()
225 225
226 226 p = safe_int(self.request.GET.get('page', 1), 1)
227 227 c.user = User.get(self._rhodecode_user.user_id)
228 228 following = Session().query(UserFollowing)\
229 229 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
230 230 .options(joinedload(UserFollowing.follows_repository))\
231 231 .all()
232 232
233 233 journal = self._get_journal_data(following, c.search_term)
234 234
235 235 def url_generator(**kw):
236 236 query_params = {
237 237 'filter': c.search_term
238 238 }
239 239 query_params.update(kw)
240 240 return self.request.current_route_path(_query=query_params)
241 241
242 242 c.journal_pager = Page(
243 243 journal, page=p, items_per_page=20, url=url_generator)
244 244 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
245 245
246 246 c.journal_data = render(
247 247 'rhodecode:templates/journal/journal_data.mako',
248 248 self._get_template_context(c), self.request)
249 249
250 250 if self.request.is_xhr:
251 251 return Response(c.journal_data)
252 252
253 253 html = render(
254 254 'rhodecode:templates/journal/journal.mako',
255 255 self._get_template_context(c), self.request)
256 256 return Response(html)
257 257
258 258 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
259 259 @NotAnonymous()
260 260 @view_config(
261 261 route_name='journal_atom', request_method='GET',
262 262 renderer=None)
263 263 def journal_atom(self):
264 264 """
265 265 Produce an atom-1.0 feed via feedgenerator module
266 266 """
267 267 c = self.load_default_context()
268 268 following_repos = Session().query(UserFollowing)\
269 269 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
270 270 .options(joinedload(UserFollowing.follows_repository))\
271 271 .all()
272 272 return self._atom_feed(following_repos, c.search_term, public=False)
273 273
274 274 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
275 275 @NotAnonymous()
276 276 @view_config(
277 277 route_name='journal_rss', request_method='GET',
278 278 renderer=None)
279 279 def journal_rss(self):
280 280 """
281 281 Produce an rss feed via feedgenerator module
282 282 """
283 283 c = self.load_default_context()
284 284 following_repos = Session().query(UserFollowing)\
285 285 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
286 286 .options(joinedload(UserFollowing.follows_repository))\
287 287 .all()
288 288 return self._rss_feed(following_repos, c.search_term, public=False)
289 289
290 290 @LoginRequired()
291 291 @NotAnonymous()
292 292 @CSRFRequired()
293 293 @view_config(
294 294 route_name='toggle_following', request_method='POST',
295 295 renderer='json_ext')
296 296 def toggle_following(self):
297 297 user_id = self.request.POST.get('follows_user_id')
298 298 if user_id:
299 299 try:
300 ScmModel().toggle_following_user(
301 user_id, self._rhodecode_user.user_id)
300 ScmModel().toggle_following_user(user_id, self._rhodecode_user.user_id)
302 301 Session().commit()
303 302 return 'ok'
304 303 except Exception:
305 304 raise HTTPBadRequest()
306 305
307 306 repo_id = self.request.POST.get('follows_repo_id')
308 307 if repo_id:
309 308 try:
310 ScmModel().toggle_following_repo(
311 repo_id, self._rhodecode_user.user_id)
309 ScmModel().toggle_following_repo(repo_id, self._rhodecode_user.user_id)
312 310 Session().commit()
313 311 return 'ok'
314 312 except Exception:
315 313 raise HTTPBadRequest()
316 314
317 315 raise HTTPBadRequest()
318 316
319 317 @LoginRequired()
320 318 @view_config(
321 319 route_name='journal_public', request_method='GET',
322 320 renderer=None)
323 321 def journal_public(self):
324 322 c = self.load_default_context()
325 323 # Return a rendered template
326 324 p = safe_int(self.request.GET.get('page', 1), 1)
327 325
328 326 c.following = Session().query(UserFollowing)\
329 327 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
330 328 .options(joinedload(UserFollowing.follows_repository))\
331 329 .all()
332 330
333 331 journal = self._get_journal_data(c.following, c.search_term)
334 332
335 333 def url_generator(**kw):
336 334 query_params = {}
337 335 query_params.update(kw)
338 336 return self.request.current_route_path(_query=query_params)
339 337
340 338 c.journal_pager = Page(
341 339 journal, page=p, items_per_page=20, url=url_generator)
342 340 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
343 341
344 342 c.journal_data = render(
345 343 'rhodecode:templates/journal/journal_data.mako',
346 344 self._get_template_context(c), self.request)
347 345
348 346 if self.request.is_xhr:
349 347 return Response(c.journal_data)
350 348
351 349 html = render(
352 350 'rhodecode:templates/journal/public_journal.mako',
353 351 self._get_template_context(c), self.request)
354 352 return Response(html)
355 353
356 354 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
357 355 @view_config(
358 356 route_name='journal_public_atom', request_method='GET',
359 357 renderer=None)
360 358 def journal_public_atom(self):
361 359 """
362 360 Produce an atom-1.0 feed via feedgenerator module
363 361 """
364 362 c = self.load_default_context()
365 363 following_repos = Session().query(UserFollowing)\
366 364 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
367 365 .options(joinedload(UserFollowing.follows_repository))\
368 366 .all()
369 367
370 368 return self._atom_feed(following_repos, c.search_term)
371 369
372 370 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
373 371 @view_config(
374 372 route_name='journal_public_rss', request_method='GET',
375 373 renderer=None)
376 374 def journal_public_rss(self):
377 375 """
378 376 Produce an rss2 feed via feedgenerator module
379 377 """
380 378 c = self.load_default_context()
381 379 following_repos = Session().query(UserFollowing)\
382 380 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
383 381 .options(joinedload(UserFollowing.follows_repository))\
384 382 .all()
385 383
386 384 return self._rss_feed(following_repos, c.search_term)
@@ -1,743 +1,743 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 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 import string
24 24
25 25 import formencode
26 26 import formencode.htmlfill
27 27 import peppercorn
28 28 from pyramid.httpexceptions import HTTPFound
29 29 from pyramid.view import view_config
30 30
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32 from rhodecode import forms
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib import audit_logger
35 35 from rhodecode.lib.ext_json import json
36 36 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired, \
37 37 HasRepoPermissionAny, HasRepoGroupPermissionAny
38 38 from rhodecode.lib.channelstream import (
39 39 channelstream_request, ChannelstreamException)
40 40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
41 41 from rhodecode.model.auth_token import AuthTokenModel
42 42 from rhodecode.model.comment import CommentsModel
43 43 from rhodecode.model.db import (
44 44 IntegrityError, joinedload,
45 45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 46 PullRequest, UserBookmark, RepoGroup)
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.pull_request import PullRequestModel
49 49 from rhodecode.model.scm import RepoList
50 50 from rhodecode.model.user import UserModel
51 51 from rhodecode.model.repo import RepoModel
52 52 from rhodecode.model.user_group import UserGroupModel
53 53 from rhodecode.model.validation_schema.schemas import user_schema
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class MyAccountView(BaseAppView, DataGridAppView):
59 59 ALLOW_SCOPED_TOKENS = False
60 60 """
61 61 This view has alternative version inside EE, if modified please take a look
62 62 in there as well.
63 63 """
64 64
65 65 def load_default_context(self):
66 66 c = self._get_local_tmpl_context()
67 67 c.user = c.auth_user.get_instance()
68 68 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
69 69
70 70 return c
71 71
72 72 @LoginRequired()
73 73 @NotAnonymous()
74 74 @view_config(
75 75 route_name='my_account_profile', request_method='GET',
76 76 renderer='rhodecode:templates/admin/my_account/my_account.mako')
77 77 def my_account_profile(self):
78 78 c = self.load_default_context()
79 79 c.active = 'profile'
80 80 return self._get_template_context(c)
81 81
82 82 @LoginRequired()
83 83 @NotAnonymous()
84 84 @view_config(
85 85 route_name='my_account_password', request_method='GET',
86 86 renderer='rhodecode:templates/admin/my_account/my_account.mako')
87 87 def my_account_password(self):
88 88 c = self.load_default_context()
89 89 c.active = 'password'
90 90 c.extern_type = c.user.extern_type
91 91
92 92 schema = user_schema.ChangePasswordSchema().bind(
93 93 username=c.user.username)
94 94
95 95 form = forms.Form(
96 96 schema,
97 97 action=h.route_path('my_account_password_update'),
98 98 buttons=(forms.buttons.save, forms.buttons.reset))
99 99
100 100 c.form = form
101 101 return self._get_template_context(c)
102 102
103 103 @LoginRequired()
104 104 @NotAnonymous()
105 105 @CSRFRequired()
106 106 @view_config(
107 107 route_name='my_account_password_update', request_method='POST',
108 108 renderer='rhodecode:templates/admin/my_account/my_account.mako')
109 109 def my_account_password_update(self):
110 110 _ = self.request.translate
111 111 c = self.load_default_context()
112 112 c.active = 'password'
113 113 c.extern_type = c.user.extern_type
114 114
115 115 schema = user_schema.ChangePasswordSchema().bind(
116 116 username=c.user.username)
117 117
118 118 form = forms.Form(
119 119 schema, buttons=(forms.buttons.save, forms.buttons.reset))
120 120
121 121 if c.extern_type != 'rhodecode':
122 122 raise HTTPFound(self.request.route_path('my_account_password'))
123 123
124 124 controls = self.request.POST.items()
125 125 try:
126 126 valid_data = form.validate(controls)
127 127 UserModel().update_user(c.user.user_id, **valid_data)
128 128 c.user.update_userdata(force_password_change=False)
129 129 Session().commit()
130 130 except forms.ValidationFailure as e:
131 131 c.form = e
132 132 return self._get_template_context(c)
133 133
134 134 except Exception:
135 135 log.exception("Exception updating password")
136 136 h.flash(_('Error occurred during update of user password'),
137 137 category='error')
138 138 else:
139 139 instance = c.auth_user.get_instance()
140 140 self.session.setdefault('rhodecode_user', {}).update(
141 141 {'password': md5(instance.password)})
142 142 self.session.save()
143 143 h.flash(_("Successfully updated password"), category='success')
144 144
145 145 raise HTTPFound(self.request.route_path('my_account_password'))
146 146
147 147 @LoginRequired()
148 148 @NotAnonymous()
149 149 @view_config(
150 150 route_name='my_account_auth_tokens', request_method='GET',
151 151 renderer='rhodecode:templates/admin/my_account/my_account.mako')
152 152 def my_account_auth_tokens(self):
153 153 _ = self.request.translate
154 154
155 155 c = self.load_default_context()
156 156 c.active = 'auth_tokens'
157 157 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
158 158 c.role_values = [
159 159 (x, AuthTokenModel.cls._get_role_name(x))
160 160 for x in AuthTokenModel.cls.ROLES]
161 161 c.role_options = [(c.role_values, _("Role"))]
162 162 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
163 163 c.user.user_id, show_expired=True)
164 164 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
165 165 return self._get_template_context(c)
166 166
167 167 def maybe_attach_token_scope(self, token):
168 168 # implemented in EE edition
169 169 pass
170 170
171 171 @LoginRequired()
172 172 @NotAnonymous()
173 173 @CSRFRequired()
174 174 @view_config(
175 175 route_name='my_account_auth_tokens_add', request_method='POST',)
176 176 def my_account_auth_tokens_add(self):
177 177 _ = self.request.translate
178 178 c = self.load_default_context()
179 179
180 180 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
181 181 description = self.request.POST.get('description')
182 182 role = self.request.POST.get('role')
183 183
184 184 token = UserModel().add_auth_token(
185 185 user=c.user.user_id,
186 186 lifetime_minutes=lifetime, role=role, description=description,
187 187 scope_callback=self.maybe_attach_token_scope)
188 188 token_data = token.get_api_data()
189 189
190 190 audit_logger.store_web(
191 191 'user.edit.token.add', action_data={
192 192 'data': {'token': token_data, 'user': 'self'}},
193 193 user=self._rhodecode_user, )
194 194 Session().commit()
195 195
196 196 h.flash(_("Auth token successfully created"), category='success')
197 197 return HTTPFound(h.route_path('my_account_auth_tokens'))
198 198
199 199 @LoginRequired()
200 200 @NotAnonymous()
201 201 @CSRFRequired()
202 202 @view_config(
203 203 route_name='my_account_auth_tokens_delete', request_method='POST')
204 204 def my_account_auth_tokens_delete(self):
205 205 _ = self.request.translate
206 206 c = self.load_default_context()
207 207
208 208 del_auth_token = self.request.POST.get('del_auth_token')
209 209
210 210 if del_auth_token:
211 211 token = UserApiKeys.get_or_404(del_auth_token)
212 212 token_data = token.get_api_data()
213 213
214 214 AuthTokenModel().delete(del_auth_token, c.user.user_id)
215 215 audit_logger.store_web(
216 216 'user.edit.token.delete', action_data={
217 217 'data': {'token': token_data, 'user': 'self'}},
218 218 user=self._rhodecode_user,)
219 219 Session().commit()
220 220 h.flash(_("Auth token successfully deleted"), category='success')
221 221
222 222 return HTTPFound(h.route_path('my_account_auth_tokens'))
223 223
224 224 @LoginRequired()
225 225 @NotAnonymous()
226 226 @view_config(
227 227 route_name='my_account_emails', request_method='GET',
228 228 renderer='rhodecode:templates/admin/my_account/my_account.mako')
229 229 def my_account_emails(self):
230 230 _ = self.request.translate
231 231
232 232 c = self.load_default_context()
233 233 c.active = 'emails'
234 234
235 235 c.user_email_map = UserEmailMap.query()\
236 236 .filter(UserEmailMap.user == c.user).all()
237 237
238 238 schema = user_schema.AddEmailSchema().bind(
239 239 username=c.user.username, user_emails=c.user.emails)
240 240
241 241 form = forms.RcForm(schema,
242 242 action=h.route_path('my_account_emails_add'),
243 243 buttons=(forms.buttons.save, forms.buttons.reset))
244 244
245 245 c.form = form
246 246 return self._get_template_context(c)
247 247
248 248 @LoginRequired()
249 249 @NotAnonymous()
250 250 @CSRFRequired()
251 251 @view_config(
252 252 route_name='my_account_emails_add', request_method='POST',
253 253 renderer='rhodecode:templates/admin/my_account/my_account.mako')
254 254 def my_account_emails_add(self):
255 255 _ = self.request.translate
256 256 c = self.load_default_context()
257 257 c.active = 'emails'
258 258
259 259 schema = user_schema.AddEmailSchema().bind(
260 260 username=c.user.username, user_emails=c.user.emails)
261 261
262 262 form = forms.RcForm(
263 263 schema, action=h.route_path('my_account_emails_add'),
264 264 buttons=(forms.buttons.save, forms.buttons.reset))
265 265
266 266 controls = self.request.POST.items()
267 267 try:
268 268 valid_data = form.validate(controls)
269 269 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
270 270 audit_logger.store_web(
271 271 'user.edit.email.add', action_data={
272 272 'data': {'email': valid_data['email'], 'user': 'self'}},
273 273 user=self._rhodecode_user,)
274 274 Session().commit()
275 275 except formencode.Invalid as error:
276 276 h.flash(h.escape(error.error_dict['email']), category='error')
277 277 except forms.ValidationFailure as e:
278 278 c.user_email_map = UserEmailMap.query() \
279 279 .filter(UserEmailMap.user == c.user).all()
280 280 c.form = e
281 281 return self._get_template_context(c)
282 282 except Exception:
283 283 log.exception("Exception adding email")
284 284 h.flash(_('Error occurred during adding email'),
285 285 category='error')
286 286 else:
287 287 h.flash(_("Successfully added email"), category='success')
288 288
289 289 raise HTTPFound(self.request.route_path('my_account_emails'))
290 290
291 291 @LoginRequired()
292 292 @NotAnonymous()
293 293 @CSRFRequired()
294 294 @view_config(
295 295 route_name='my_account_emails_delete', request_method='POST')
296 296 def my_account_emails_delete(self):
297 297 _ = self.request.translate
298 298 c = self.load_default_context()
299 299
300 300 del_email_id = self.request.POST.get('del_email_id')
301 301 if del_email_id:
302 302 email = UserEmailMap.get_or_404(del_email_id).email
303 303 UserModel().delete_extra_email(c.user.user_id, del_email_id)
304 304 audit_logger.store_web(
305 305 'user.edit.email.delete', action_data={
306 306 'data': {'email': email, 'user': 'self'}},
307 307 user=self._rhodecode_user,)
308 308 Session().commit()
309 309 h.flash(_("Email successfully deleted"),
310 310 category='success')
311 311 return HTTPFound(h.route_path('my_account_emails'))
312 312
313 313 @LoginRequired()
314 314 @NotAnonymous()
315 315 @CSRFRequired()
316 316 @view_config(
317 317 route_name='my_account_notifications_test_channelstream',
318 318 request_method='POST', renderer='json_ext')
319 319 def my_account_notifications_test_channelstream(self):
320 320 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
321 321 self._rhodecode_user.username, datetime.datetime.now())
322 322 payload = {
323 323 # 'channel': 'broadcast',
324 324 'type': 'message',
325 325 'timestamp': datetime.datetime.utcnow(),
326 326 'user': 'system',
327 327 'pm_users': [self._rhodecode_user.username],
328 328 'message': {
329 329 'message': message,
330 330 'level': 'info',
331 331 'topic': '/notifications'
332 332 }
333 333 }
334 334
335 335 registry = self.request.registry
336 336 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
337 337 channelstream_config = rhodecode_plugins.get('channelstream', {})
338 338
339 339 try:
340 340 channelstream_request(channelstream_config, [payload], '/message')
341 341 except ChannelstreamException as e:
342 342 log.exception('Failed to send channelstream data')
343 343 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
344 344 return {"response": 'Channelstream data sent. '
345 345 'You should see a new live message now.'}
346 346
347 347 def _load_my_repos_data(self, watched=False):
348 348 if watched:
349 349 admin = False
350 350 follows_repos = Session().query(UserFollowing)\
351 351 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
352 352 .options(joinedload(UserFollowing.follows_repository))\
353 353 .all()
354 354 repo_list = [x.follows_repository for x in follows_repos]
355 355 else:
356 356 admin = True
357 357 repo_list = Repository.get_all_repos(
358 358 user_id=self._rhodecode_user.user_id)
359 359 repo_list = RepoList(repo_list, perm_set=[
360 360 'repository.read', 'repository.write', 'repository.admin'])
361 361
362 362 repos_data = RepoModel().get_repos_as_dict(
363 repo_list=repo_list, admin=admin)
363 repo_list=repo_list, admin=admin, short_name=False)
364 364 # json used to render the grid
365 365 return json.dumps(repos_data)
366 366
367 367 @LoginRequired()
368 368 @NotAnonymous()
369 369 @view_config(
370 370 route_name='my_account_repos', request_method='GET',
371 371 renderer='rhodecode:templates/admin/my_account/my_account.mako')
372 372 def my_account_repos(self):
373 373 c = self.load_default_context()
374 374 c.active = 'repos'
375 375
376 376 # json used to render the grid
377 377 c.data = self._load_my_repos_data()
378 378 return self._get_template_context(c)
379 379
380 380 @LoginRequired()
381 381 @NotAnonymous()
382 382 @view_config(
383 383 route_name='my_account_watched', request_method='GET',
384 384 renderer='rhodecode:templates/admin/my_account/my_account.mako')
385 385 def my_account_watched(self):
386 386 c = self.load_default_context()
387 387 c.active = 'watched'
388 388
389 389 # json used to render the grid
390 390 c.data = self._load_my_repos_data(watched=True)
391 391 return self._get_template_context(c)
392 392
393 393 @LoginRequired()
394 394 @NotAnonymous()
395 395 @view_config(
396 396 route_name='my_account_bookmarks', request_method='GET',
397 397 renderer='rhodecode:templates/admin/my_account/my_account.mako')
398 398 def my_account_bookmarks(self):
399 399 c = self.load_default_context()
400 400 c.active = 'bookmarks'
401 401 return self._get_template_context(c)
402 402
403 403 def _process_entry(self, entry, user_id):
404 404 position = safe_int(entry.get('position'))
405 405 if position is None:
406 406 return
407 407
408 408 # check if this is an existing entry
409 409 is_new = False
410 410 db_entry = UserBookmark().get_by_position_for_user(position, user_id)
411 411
412 412 if db_entry and str2bool(entry.get('remove')):
413 413 log.debug('Marked bookmark %s for deletion', db_entry)
414 414 Session().delete(db_entry)
415 415 return
416 416
417 417 if not db_entry:
418 418 # new
419 419 db_entry = UserBookmark()
420 420 is_new = True
421 421
422 422 should_save = False
423 423 default_redirect_url = ''
424 424
425 425 # save repo
426 426 if entry.get('bookmark_repo'):
427 427 repo = Repository.get(entry['bookmark_repo'])
428 428 perm_check = HasRepoPermissionAny(
429 429 'repository.read', 'repository.write', 'repository.admin')
430 430 if repo and perm_check(repo_name=repo.repo_name):
431 431 db_entry.repository = repo
432 432 should_save = True
433 433 default_redirect_url = '${repo_url}'
434 434 # save repo group
435 435 elif entry.get('bookmark_repo_group'):
436 436 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
437 437 perm_check = HasRepoGroupPermissionAny(
438 438 'group.read', 'group.write', 'group.admin')
439 439
440 440 if repo_group and perm_check(group_name=repo_group.group_name):
441 441 db_entry.repository_group = repo_group
442 442 should_save = True
443 443 default_redirect_url = '${repo_group_url}'
444 444 # save generic info
445 445 elif entry.get('title') and entry.get('redirect_url'):
446 446 should_save = True
447 447
448 448 if should_save:
449 449 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
450 450 # mark user and position
451 451 db_entry.user_id = user_id
452 452 db_entry.position = position
453 453 db_entry.title = entry.get('title')
454 454 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
455 455
456 456 Session().add(db_entry)
457 457
458 458 @LoginRequired()
459 459 @NotAnonymous()
460 460 @CSRFRequired()
461 461 @view_config(
462 462 route_name='my_account_bookmarks_update', request_method='POST')
463 463 def my_account_bookmarks_update(self):
464 464 _ = self.request.translate
465 465 c = self.load_default_context()
466 466 c.active = 'bookmarks'
467 467
468 468 controls = peppercorn.parse(self.request.POST.items())
469 469 user_id = c.user.user_id
470 470
471 471 try:
472 472 for entry in controls.get('bookmarks', []):
473 473 self._process_entry(entry, user_id)
474 474
475 475 Session().commit()
476 476 h.flash(_("Update Bookmarks"), category='success')
477 477 except IntegrityError:
478 478 h.flash(_("Failed to update bookmarks. "
479 479 "Make sure an unique position is used"), category='error')
480 480
481 481 return HTTPFound(h.route_path('my_account_bookmarks'))
482 482
483 483 @LoginRequired()
484 484 @NotAnonymous()
485 485 @view_config(
486 486 route_name='my_account_goto_bookmark', request_method='GET',
487 487 renderer='rhodecode:templates/admin/my_account/my_account.mako')
488 488 def my_account_goto_bookmark(self):
489 489
490 490 bookmark_id = self.request.matchdict['bookmark_id']
491 491 user_bookmark = UserBookmark().query()\
492 492 .filter(UserBookmark.user_id == self.request.user.user_id) \
493 493 .filter(UserBookmark.position == bookmark_id).scalar()
494 494
495 495 redirect_url = h.route_path('my_account_bookmarks')
496 496 if not user_bookmark:
497 497 raise HTTPFound(redirect_url)
498 498
499 499 # repository set
500 500 if user_bookmark.repository:
501 501 repo_name = user_bookmark.repository.repo_name
502 502 base_redirect_url = h.route_path(
503 503 'repo_summary', repo_name=repo_name)
504 504 if user_bookmark.redirect_url and \
505 505 '${repo_url}' in user_bookmark.redirect_url:
506 506 redirect_url = string.Template(user_bookmark.redirect_url)\
507 507 .safe_substitute({'repo_url': base_redirect_url})
508 508 else:
509 509 redirect_url = base_redirect_url
510 510 # repository group set
511 511 elif user_bookmark.repository_group:
512 512 repo_group_name = user_bookmark.repository_group.group_name
513 513 base_redirect_url = h.route_path(
514 514 'repo_group_home', repo_group_name=repo_group_name)
515 515 if user_bookmark.redirect_url and \
516 516 '${repo_group_url}' in user_bookmark.redirect_url:
517 517 redirect_url = string.Template(user_bookmark.redirect_url)\
518 518 .safe_substitute({'repo_group_url': base_redirect_url})
519 519 else:
520 520 redirect_url = base_redirect_url
521 521 # custom URL set
522 522 elif user_bookmark.redirect_url:
523 523 server_url = h.route_url('home').rstrip('/')
524 524 redirect_url = string.Template(user_bookmark.redirect_url) \
525 525 .safe_substitute({'server_url': server_url})
526 526
527 527 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
528 528 raise HTTPFound(redirect_url)
529 529
530 530 @LoginRequired()
531 531 @NotAnonymous()
532 532 @view_config(
533 533 route_name='my_account_perms', request_method='GET',
534 534 renderer='rhodecode:templates/admin/my_account/my_account.mako')
535 535 def my_account_perms(self):
536 536 c = self.load_default_context()
537 537 c.active = 'perms'
538 538
539 539 c.perm_user = c.auth_user
540 540 return self._get_template_context(c)
541 541
542 542 @LoginRequired()
543 543 @NotAnonymous()
544 544 @view_config(
545 545 route_name='my_account_notifications', request_method='GET',
546 546 renderer='rhodecode:templates/admin/my_account/my_account.mako')
547 547 def my_notifications(self):
548 548 c = self.load_default_context()
549 549 c.active = 'notifications'
550 550
551 551 return self._get_template_context(c)
552 552
553 553 @LoginRequired()
554 554 @NotAnonymous()
555 555 @CSRFRequired()
556 556 @view_config(
557 557 route_name='my_account_notifications_toggle_visibility',
558 558 request_method='POST', renderer='json_ext')
559 559 def my_notifications_toggle_visibility(self):
560 560 user = self._rhodecode_db_user
561 561 new_status = not user.user_data.get('notification_status', True)
562 562 user.update_userdata(notification_status=new_status)
563 563 Session().commit()
564 564 return user.user_data['notification_status']
565 565
566 566 @LoginRequired()
567 567 @NotAnonymous()
568 568 @view_config(
569 569 route_name='my_account_edit',
570 570 request_method='GET',
571 571 renderer='rhodecode:templates/admin/my_account/my_account.mako')
572 572 def my_account_edit(self):
573 573 c = self.load_default_context()
574 574 c.active = 'profile_edit'
575 575 c.extern_type = c.user.extern_type
576 576 c.extern_name = c.user.extern_name
577 577
578 578 schema = user_schema.UserProfileSchema().bind(
579 579 username=c.user.username, user_emails=c.user.emails)
580 580 appstruct = {
581 581 'username': c.user.username,
582 582 'email': c.user.email,
583 583 'firstname': c.user.firstname,
584 584 'lastname': c.user.lastname,
585 585 }
586 586 c.form = forms.RcForm(
587 587 schema, appstruct=appstruct,
588 588 action=h.route_path('my_account_update'),
589 589 buttons=(forms.buttons.save, forms.buttons.reset))
590 590
591 591 return self._get_template_context(c)
592 592
593 593 @LoginRequired()
594 594 @NotAnonymous()
595 595 @CSRFRequired()
596 596 @view_config(
597 597 route_name='my_account_update',
598 598 request_method='POST',
599 599 renderer='rhodecode:templates/admin/my_account/my_account.mako')
600 600 def my_account_update(self):
601 601 _ = self.request.translate
602 602 c = self.load_default_context()
603 603 c.active = 'profile_edit'
604 604 c.perm_user = c.auth_user
605 605 c.extern_type = c.user.extern_type
606 606 c.extern_name = c.user.extern_name
607 607
608 608 schema = user_schema.UserProfileSchema().bind(
609 609 username=c.user.username, user_emails=c.user.emails)
610 610 form = forms.RcForm(
611 611 schema, buttons=(forms.buttons.save, forms.buttons.reset))
612 612
613 613 controls = self.request.POST.items()
614 614 try:
615 615 valid_data = form.validate(controls)
616 616 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
617 617 'new_password', 'password_confirmation']
618 618 if c.extern_type != "rhodecode":
619 619 # forbid updating username for external accounts
620 620 skip_attrs.append('username')
621 621 old_email = c.user.email
622 622 UserModel().update_user(
623 623 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
624 624 **valid_data)
625 625 if old_email != valid_data['email']:
626 626 old = UserEmailMap.query() \
627 627 .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first()
628 628 old.email = old_email
629 629 h.flash(_('Your account was updated successfully'), category='success')
630 630 Session().commit()
631 631 except forms.ValidationFailure as e:
632 632 c.form = e
633 633 return self._get_template_context(c)
634 634 except Exception:
635 635 log.exception("Exception updating user")
636 636 h.flash(_('Error occurred during update of user'),
637 637 category='error')
638 638 raise HTTPFound(h.route_path('my_account_profile'))
639 639
640 640 def _get_pull_requests_list(self, statuses):
641 641 draw, start, limit = self._extract_chunk(self.request)
642 642 search_q, order_by, order_dir = self._extract_ordering(self.request)
643 643 _render = self.request.get_partial_renderer(
644 644 'rhodecode:templates/data_table/_dt_elements.mako')
645 645
646 646 pull_requests = PullRequestModel().get_im_participating_in(
647 647 user_id=self._rhodecode_user.user_id,
648 648 statuses=statuses,
649 649 offset=start, length=limit, order_by=order_by,
650 650 order_dir=order_dir)
651 651
652 652 pull_requests_total_count = PullRequestModel().count_im_participating_in(
653 653 user_id=self._rhodecode_user.user_id, statuses=statuses)
654 654
655 655 data = []
656 656 comments_model = CommentsModel()
657 657 for pr in pull_requests:
658 658 repo_id = pr.target_repo_id
659 659 comments = comments_model.get_all_comments(
660 660 repo_id, pull_request=pr)
661 661 owned = pr.user_id == self._rhodecode_user.user_id
662 662
663 663 data.append({
664 664 'target_repo': _render('pullrequest_target_repo',
665 665 pr.target_repo.repo_name),
666 666 'name': _render('pullrequest_name',
667 667 pr.pull_request_id, pr.target_repo.repo_name,
668 668 short=True),
669 669 'name_raw': pr.pull_request_id,
670 670 'status': _render('pullrequest_status',
671 671 pr.calculated_review_status()),
672 672 'title': _render(
673 673 'pullrequest_title', pr.title, pr.description),
674 674 'description': h.escape(pr.description),
675 675 'updated_on': _render('pullrequest_updated_on',
676 676 h.datetime_to_time(pr.updated_on)),
677 677 'updated_on_raw': h.datetime_to_time(pr.updated_on),
678 678 'created_on': _render('pullrequest_updated_on',
679 679 h.datetime_to_time(pr.created_on)),
680 680 'created_on_raw': h.datetime_to_time(pr.created_on),
681 681 'author': _render('pullrequest_author',
682 682 pr.author.full_contact, ),
683 683 'author_raw': pr.author.full_name,
684 684 'comments': _render('pullrequest_comments', len(comments)),
685 685 'comments_raw': len(comments),
686 686 'closed': pr.is_closed(),
687 687 'owned': owned
688 688 })
689 689
690 690 # json used to render the grid
691 691 data = ({
692 692 'draw': draw,
693 693 'data': data,
694 694 'recordsTotal': pull_requests_total_count,
695 695 'recordsFiltered': pull_requests_total_count,
696 696 })
697 697 return data
698 698
699 699 @LoginRequired()
700 700 @NotAnonymous()
701 701 @view_config(
702 702 route_name='my_account_pullrequests',
703 703 request_method='GET',
704 704 renderer='rhodecode:templates/admin/my_account/my_account.mako')
705 705 def my_account_pullrequests(self):
706 706 c = self.load_default_context()
707 707 c.active = 'pullrequests'
708 708 req_get = self.request.GET
709 709
710 710 c.closed = str2bool(req_get.get('pr_show_closed'))
711 711
712 712 return self._get_template_context(c)
713 713
714 714 @LoginRequired()
715 715 @NotAnonymous()
716 716 @view_config(
717 717 route_name='my_account_pullrequests_data',
718 718 request_method='GET', renderer='json_ext')
719 719 def my_account_pullrequests_data(self):
720 720 self.load_default_context()
721 721 req_get = self.request.GET
722 722 closed = str2bool(req_get.get('closed'))
723 723
724 724 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
725 725 if closed:
726 726 statuses += [PullRequest.STATUS_CLOSED]
727 727
728 728 data = self._get_pull_requests_list(statuses=statuses)
729 729 return data
730 730
731 731 @LoginRequired()
732 732 @NotAnonymous()
733 733 @view_config(
734 734 route_name='my_account_user_group_membership',
735 735 request_method='GET',
736 736 renderer='rhodecode:templates/admin/my_account/my_account.mako')
737 737 def my_account_user_group_membership(self):
738 738 c = self.load_default_context()
739 739 c.active = 'user_group_membership'
740 740 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
741 741 for group in self._rhodecode_db_user.group_member]
742 742 c.user_groups = json.dumps(groups)
743 743 return self._get_template_context(c)
@@ -1,392 +1,390 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 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 string
23 23 import rhodecode
24 24
25 25 from pyramid.view import view_config
26 26
27 27 from rhodecode.lib.view_utils import get_format_ref_id
28 28 from rhodecode.apps._base import RepoAppView
29 29 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
30 30 from rhodecode.lib import helpers as h, rc_cache
31 31 from rhodecode.lib.utils2 import safe_str, safe_int
32 32 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
33 33 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
34 34 from rhodecode.lib.ext_json import json
35 35 from rhodecode.lib.vcs.backends.base import EmptyCommit
36 36 from rhodecode.lib.vcs.exceptions import (
37 37 CommitError, EmptyRepositoryError, CommitDoesNotExistError)
38 38 from rhodecode.model.db import Statistics, CacheKey, User
39 39 from rhodecode.model.meta import Session
40 40 from rhodecode.model.repo import ReadmeFinder
41 41 from rhodecode.model.scm import ScmModel
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 class RepoSummaryView(RepoAppView):
47 47
48 48 def load_default_context(self):
49 49 c = self._get_local_tmpl_context(include_app_defaults=True)
50 50 c.rhodecode_repo = None
51 51 if not c.repository_requirements_missing:
52 52 c.rhodecode_repo = self.rhodecode_vcs_repo
53 53 return c
54 54
55 55 def _get_readme_data(self, db_repo, renderer_type):
56 56
57 57 log.debug('Looking for README file')
58 58
59 59 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
60 60 db_repo.repo_id, CacheKey.CACHE_TYPE_README)
61 61 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
62 62 repo_id=self.db_repo.repo_id)
63 63 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
64 64
65 65 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
66 66 def generate_repo_readme(repo_id, _repo_name, _renderer_type):
67 67 readme_data = None
68 68 readme_node = None
69 69 readme_filename = None
70 70 commit = self._get_landing_commit_or_none(db_repo)
71 71 if commit:
72 72 log.debug("Searching for a README file.")
73 73 readme_node = ReadmeFinder(_renderer_type).search(commit)
74 74 if readme_node:
75 75 relative_urls = {
76 76 'raw': h.route_path(
77 77 'repo_file_raw', repo_name=_repo_name,
78 78 commit_id=commit.raw_id, f_path=readme_node.path),
79 79 'standard': h.route_path(
80 80 'repo_files', repo_name=_repo_name,
81 81 commit_id=commit.raw_id, f_path=readme_node.path),
82 82 }
83 83 readme_data = self._render_readme_or_none(
84 84 commit, readme_node, relative_urls)
85 85 readme_filename = readme_node.path
86 86 return readme_data, readme_filename
87 87
88 88 inv_context_manager = rc_cache.InvalidationContext(
89 89 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
90 90 with inv_context_manager as invalidation_context:
91 91 args = (db_repo.repo_id, db_repo.repo_name, renderer_type,)
92 92 # re-compute and store cache if we get invalidate signal
93 93 if invalidation_context.should_invalidate():
94 94 instance = generate_repo_readme.refresh(*args)
95 95 else:
96 96 instance = generate_repo_readme(*args)
97 97
98 98 log.debug(
99 99 'Repo readme generated and computed in %.3fs',
100 100 inv_context_manager.compute_time)
101 101 return instance
102 102
103 103 def _get_landing_commit_or_none(self, db_repo):
104 104 log.debug("Getting the landing commit.")
105 105 try:
106 106 commit = db_repo.get_landing_commit()
107 107 if not isinstance(commit, EmptyCommit):
108 108 return commit
109 109 else:
110 110 log.debug("Repository is empty, no README to render.")
111 111 except CommitError:
112 112 log.exception(
113 113 "Problem getting commit when trying to render the README.")
114 114
115 115 def _render_readme_or_none(self, commit, readme_node, relative_urls):
116 116 log.debug(
117 117 'Found README file `%s` rendering...', readme_node.path)
118 118 renderer = MarkupRenderer()
119 119 try:
120 120 html_source = renderer.render(
121 121 readme_node.content, filename=readme_node.path)
122 122 if relative_urls:
123 123 return relative_links(html_source, relative_urls)
124 124 return html_source
125 125 except Exception:
126 126 log.exception(
127 127 "Exception while trying to render the README")
128 128
129 129 def _load_commits_context(self, c):
130 130 p = safe_int(self.request.GET.get('page'), 1)
131 131 size = safe_int(self.request.GET.get('size'), 10)
132 132
133 133 def url_generator(**kw):
134 134 query_params = {
135 135 'size': size
136 136 }
137 137 query_params.update(kw)
138 138 return h.route_path(
139 139 'repo_summary_commits',
140 140 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
141 141
142 142 pre_load = ['author', 'branch', 'date', 'message']
143 143 try:
144 144 collection = self.rhodecode_vcs_repo.get_commits(
145 145 pre_load=pre_load, translate_tags=False)
146 146 except EmptyRepositoryError:
147 147 collection = self.rhodecode_vcs_repo
148 148
149 149 c.repo_commits = h.RepoPage(
150 150 collection, page=p, items_per_page=size, url=url_generator)
151 151 page_ids = [x.raw_id for x in c.repo_commits]
152 152 c.comments = self.db_repo.get_comments(page_ids)
153 153 c.statuses = self.db_repo.statuses(page_ids)
154 154
155 155 def _prepare_and_set_clone_url(self, c):
156 156 username = ''
157 157 if self._rhodecode_user.username != User.DEFAULT_USER:
158 158 username = safe_str(self._rhodecode_user.username)
159 159
160 160 _def_clone_uri = _def_clone_uri_id = c.clone_uri_tmpl
161 161 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
162 162
163 163 if '{repo}' in _def_clone_uri:
164 164 _def_clone_uri_id = _def_clone_uri.replace('{repo}', '_{repoid}')
165 165 elif '{repoid}' in _def_clone_uri:
166 166 _def_clone_uri_id = _def_clone_uri.replace('_{repoid}', '{repo}')
167 167
168 168 c.clone_repo_url = self.db_repo.clone_url(
169 169 user=username, uri_tmpl=_def_clone_uri)
170 170 c.clone_repo_url_id = self.db_repo.clone_url(
171 171 user=username, uri_tmpl=_def_clone_uri_id)
172 172 c.clone_repo_url_ssh = self.db_repo.clone_url(
173 173 uri_tmpl=_def_clone_uri_ssh, ssh=True)
174 174
175 175 @LoginRequired()
176 176 @HasRepoPermissionAnyDecorator(
177 177 'repository.read', 'repository.write', 'repository.admin')
178 178 @view_config(
179 179 route_name='repo_summary_commits', request_method='GET',
180 180 renderer='rhodecode:templates/summary/summary_commits.mako')
181 181 def summary_commits(self):
182 182 c = self.load_default_context()
183 183 self._prepare_and_set_clone_url(c)
184 184 self._load_commits_context(c)
185 185 return self._get_template_context(c)
186 186
187 187 @LoginRequired()
188 188 @HasRepoPermissionAnyDecorator(
189 189 'repository.read', 'repository.write', 'repository.admin')
190 190 @view_config(
191 191 route_name='repo_summary', request_method='GET',
192 192 renderer='rhodecode:templates/summary/summary.mako')
193 193 @view_config(
194 194 route_name='repo_summary_slash', request_method='GET',
195 195 renderer='rhodecode:templates/summary/summary.mako')
196 196 @view_config(
197 197 route_name='repo_summary_explicit', request_method='GET',
198 198 renderer='rhodecode:templates/summary/summary.mako')
199 199 def summary(self):
200 200 c = self.load_default_context()
201 201
202 202 # Prepare the clone URL
203 203 self._prepare_and_set_clone_url(c)
204 204
205 205 # If enabled, get statistics data
206 206
207 207 c.show_stats = bool(self.db_repo.enable_statistics)
208 208
209 209 stats = Session().query(Statistics) \
210 210 .filter(Statistics.repository == self.db_repo) \
211 211 .scalar()
212 212
213 213 c.stats_percentage = 0
214 214
215 215 if stats and stats.languages:
216 216 c.no_data = False is self.db_repo.enable_statistics
217 217 lang_stats_d = json.loads(stats.languages)
218 218
219 219 # Sort first by decreasing count and second by the file extension,
220 220 # so we have a consistent output.
221 221 lang_stats_items = sorted(lang_stats_d.iteritems(),
222 222 key=lambda k: (-k[1], k[0]))[:10]
223 223 lang_stats = [(x, {"count": y,
224 224 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
225 225 for x, y in lang_stats_items]
226 226
227 227 c.trending_languages = json.dumps(lang_stats)
228 228 else:
229 229 c.no_data = True
230 230 c.trending_languages = json.dumps({})
231 231
232 232 scm_model = ScmModel()
233 233 c.enable_downloads = self.db_repo.enable_downloads
234 234 c.repository_followers = scm_model.get_followers(self.db_repo)
235 235 c.repository_forks = scm_model.get_forks(self.db_repo)
236 c.repository_is_user_following = scm_model.is_following_repo(
237 self.db_repo_name, self._rhodecode_user.user_id)
238 236
239 237 # first interaction with the VCS instance after here...
240 238 if c.repository_requirements_missing:
241 239 self.request.override_renderer = \
242 240 'rhodecode:templates/summary/missing_requirements.mako'
243 241 return self._get_template_context(c)
244 242
245 243 c.readme_data, c.readme_file = \
246 244 self._get_readme_data(self.db_repo, c.visual.default_renderer)
247 245
248 246 # loads the summary commits template context
249 247 self._load_commits_context(c)
250 248
251 249 return self._get_template_context(c)
252 250
253 251 def get_request_commit_id(self):
254 252 return self.request.matchdict['commit_id']
255 253
256 254 @LoginRequired()
257 255 @HasRepoPermissionAnyDecorator(
258 256 'repository.read', 'repository.write', 'repository.admin')
259 257 @view_config(
260 258 route_name='repo_stats', request_method='GET',
261 259 renderer='json_ext')
262 260 def repo_stats(self):
263 261 commit_id = self.get_request_commit_id()
264 262 show_stats = bool(self.db_repo.enable_statistics)
265 263 repo_id = self.db_repo.repo_id
266 264
267 265 cache_seconds = safe_int(
268 266 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
269 267 cache_on = cache_seconds > 0
270 268 log.debug(
271 269 'Computing REPO TREE for repo_id %s commit_id `%s` '
272 270 'with caching: %s[TTL: %ss]' % (
273 271 repo_id, commit_id, cache_on, cache_seconds or 0))
274 272
275 273 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
276 274 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
277 275
278 276 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
279 277 condition=cache_on)
280 278 def compute_stats(repo_id, commit_id, show_stats):
281 279 code_stats = {}
282 280 size = 0
283 281 try:
284 282 scm_instance = self.db_repo.scm_instance()
285 283 commit = scm_instance.get_commit(commit_id)
286 284
287 285 for node in commit.get_filenodes_generator():
288 286 size += node.size
289 287 if not show_stats:
290 288 continue
291 289 ext = string.lower(node.extension)
292 290 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
293 291 if ext_info:
294 292 if ext in code_stats:
295 293 code_stats[ext]['count'] += 1
296 294 else:
297 295 code_stats[ext] = {"count": 1, "desc": ext_info}
298 296 except (EmptyRepositoryError, CommitDoesNotExistError):
299 297 pass
300 298 return {'size': h.format_byte_size_binary(size),
301 299 'code_stats': code_stats}
302 300
303 301 stats = compute_stats(self.db_repo.repo_id, commit_id, show_stats)
304 302 return stats
305 303
306 304 @LoginRequired()
307 305 @HasRepoPermissionAnyDecorator(
308 306 'repository.read', 'repository.write', 'repository.admin')
309 307 @view_config(
310 308 route_name='repo_refs_data', request_method='GET',
311 309 renderer='json_ext')
312 310 def repo_refs_data(self):
313 311 _ = self.request.translate
314 312 self.load_default_context()
315 313
316 314 repo = self.rhodecode_vcs_repo
317 315 refs_to_create = [
318 316 (_("Branch"), repo.branches, 'branch'),
319 317 (_("Tag"), repo.tags, 'tag'),
320 318 (_("Bookmark"), repo.bookmarks, 'book'),
321 319 ]
322 320 res = self._create_reference_data(repo, self.db_repo_name, refs_to_create)
323 321 data = {
324 322 'more': False,
325 323 'results': res
326 324 }
327 325 return data
328 326
329 327 @LoginRequired()
330 328 @HasRepoPermissionAnyDecorator(
331 329 'repository.read', 'repository.write', 'repository.admin')
332 330 @view_config(
333 331 route_name='repo_refs_changelog_data', request_method='GET',
334 332 renderer='json_ext')
335 333 def repo_refs_changelog_data(self):
336 334 _ = self.request.translate
337 335 self.load_default_context()
338 336
339 337 repo = self.rhodecode_vcs_repo
340 338
341 339 refs_to_create = [
342 340 (_("Branches"), repo.branches, 'branch'),
343 341 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
344 342 # TODO: enable when vcs can handle bookmarks filters
345 343 # (_("Bookmarks"), repo.bookmarks, "book"),
346 344 ]
347 345 res = self._create_reference_data(
348 346 repo, self.db_repo_name, refs_to_create)
349 347 data = {
350 348 'more': False,
351 349 'results': res
352 350 }
353 351 return data
354 352
355 353 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
356 354 format_ref_id = get_format_ref_id(repo)
357 355
358 356 result = []
359 357 for title, refs, ref_type in refs_to_create:
360 358 if refs:
361 359 result.append({
362 360 'text': title,
363 361 'children': self._create_reference_items(
364 362 repo, full_repo_name, refs, ref_type,
365 363 format_ref_id),
366 364 })
367 365 return result
368 366
369 367 def _create_reference_items(self, repo, full_repo_name, refs, ref_type, format_ref_id):
370 368 result = []
371 369 is_svn = h.is_svn(repo)
372 370 for ref_name, raw_id in refs.iteritems():
373 371 files_url = self._create_files_url(
374 372 repo, full_repo_name, ref_name, raw_id, is_svn)
375 373 result.append({
376 374 'text': ref_name,
377 375 'id': format_ref_id(ref_name, raw_id),
378 376 'raw_id': raw_id,
379 377 'type': ref_type,
380 378 'files_url': files_url,
381 379 'idx': 0,
382 380 })
383 381 return result
384 382
385 383 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
386 384 use_commit_id = '/' in ref_name or is_svn
387 385 return h.route_path(
388 386 'repo_files',
389 387 repo_name=full_repo_name,
390 388 f_path=ref_name if is_svn else '',
391 389 commit_id=raw_id if use_commit_id else ref_name,
392 390 _query=dict(at=ref_name))
@@ -1,1069 +1,1073 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 os
22 22 import re
23 23 import shutil
24 24 import time
25 25 import logging
26 26 import traceback
27 27 import datetime
28 28
29 29 from pyramid.threadlocal import get_current_request
30 30 from zope.cachedescriptors.property import Lazy as LazyProperty
31 31
32 32 from rhodecode import events
33 33 from rhodecode.lib.auth import HasUserGroupPermissionAny
34 34 from rhodecode.lib.caching_query import FromCache
35 35 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError
36 36 from rhodecode.lib.hooks_base import log_delete_repository
37 37 from rhodecode.lib.user_log_filter import user_log_filter
38 38 from rhodecode.lib.utils import make_db_config
39 39 from rhodecode.lib.utils2 import (
40 40 safe_str, safe_unicode, remove_prefix, obfuscate_url_pw,
41 41 get_current_rhodecode_user, safe_int, datetime_to_time,
42 42 action_logger_generic)
43 43 from rhodecode.lib.vcs.backends import get_backend
44 44 from rhodecode.model import BaseModel
45 45 from rhodecode.model.db import (
46 46 _hash_key, joinedload, or_, Repository, UserRepoToPerm, UserGroupRepoToPerm,
47 47 UserRepoGroupToPerm, UserGroupRepoGroupToPerm, User, Permission,
48 48 Statistics, UserGroup, RepoGroup, RepositoryField, UserLog)
49 49
50 50 from rhodecode.model.settings import VcsSettingsModel
51 51
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 class RepoModel(BaseModel):
57 57
58 58 cls = Repository
59 59
60 60 def _get_user_group(self, users_group):
61 61 return self._get_instance(UserGroup, users_group,
62 62 callback=UserGroup.get_by_group_name)
63 63
64 64 def _get_repo_group(self, repo_group):
65 65 return self._get_instance(RepoGroup, repo_group,
66 66 callback=RepoGroup.get_by_group_name)
67 67
68 68 def _create_default_perms(self, repository, private):
69 69 # create default permission
70 70 default = 'repository.read'
71 71 def_user = User.get_default_user()
72 72 for p in def_user.user_perms:
73 73 if p.permission.permission_name.startswith('repository.'):
74 74 default = p.permission.permission_name
75 75 break
76 76
77 77 default_perm = 'repository.none' if private else default
78 78
79 79 repo_to_perm = UserRepoToPerm()
80 80 repo_to_perm.permission = Permission.get_by_key(default_perm)
81 81
82 82 repo_to_perm.repository = repository
83 83 repo_to_perm.user_id = def_user.user_id
84 84
85 85 return repo_to_perm
86 86
87 87 @LazyProperty
88 88 def repos_path(self):
89 89 """
90 90 Gets the repositories root path from database
91 91 """
92 92 settings_model = VcsSettingsModel(sa=self.sa)
93 93 return settings_model.get_repos_location()
94 94
95 95 def get(self, repo_id):
96 96 repo = self.sa.query(Repository) \
97 97 .filter(Repository.repo_id == repo_id)
98 98
99 99 return repo.scalar()
100 100
101 101 def get_repo(self, repository):
102 102 return self._get_repo(repository)
103 103
104 104 def get_by_repo_name(self, repo_name, cache=False):
105 105 repo = self.sa.query(Repository) \
106 106 .filter(Repository.repo_name == repo_name)
107 107
108 108 if cache:
109 109 name_key = _hash_key(repo_name)
110 110 repo = repo.options(
111 111 FromCache("sql_cache_short", "get_repo_%s" % name_key))
112 112 return repo.scalar()
113 113
114 114 def _extract_id_from_repo_name(self, repo_name):
115 115 if repo_name.startswith('/'):
116 116 repo_name = repo_name.lstrip('/')
117 117 by_id_match = re.match(r'^_(\d{1,})', repo_name)
118 118 if by_id_match:
119 119 return by_id_match.groups()[0]
120 120
121 121 def get_repo_by_id(self, repo_name):
122 122 """
123 123 Extracts repo_name by id from special urls.
124 124 Example url is _11/repo_name
125 125
126 126 :param repo_name:
127 127 :return: repo object if matched else None
128 128 """
129 129
130 130 try:
131 131 _repo_id = self._extract_id_from_repo_name(repo_name)
132 132 if _repo_id:
133 133 return self.get(_repo_id)
134 134 except Exception:
135 135 log.exception('Failed to extract repo_name from URL')
136 136
137 137 return None
138 138
139 139 def get_repos_for_root(self, root, traverse=False):
140 140 if traverse:
141 141 like_expression = u'{}%'.format(safe_unicode(root))
142 142 repos = Repository.query().filter(
143 143 Repository.repo_name.like(like_expression)).all()
144 144 else:
145 145 if root and not isinstance(root, RepoGroup):
146 146 raise ValueError(
147 147 'Root must be an instance '
148 148 'of RepoGroup, got:{} instead'.format(type(root)))
149 149 repos = Repository.query().filter(Repository.group == root).all()
150 150 return repos
151 151
152 152 def get_url(self, repo, request=None, permalink=False):
153 153 if not request:
154 154 request = get_current_request()
155 155
156 156 if not request:
157 157 return
158 158
159 159 if permalink:
160 160 return request.route_url(
161 161 'repo_summary', repo_name='_{}'.format(safe_str(repo.repo_id)))
162 162 else:
163 163 return request.route_url(
164 164 'repo_summary', repo_name=safe_str(repo.repo_name))
165 165
166 166 def get_commit_url(self, repo, commit_id, request=None, permalink=False):
167 167 if not request:
168 168 request = get_current_request()
169 169
170 170 if not request:
171 171 return
172 172
173 173 if permalink:
174 174 return request.route_url(
175 175 'repo_commit', repo_name=safe_str(repo.repo_id),
176 176 commit_id=commit_id)
177 177
178 178 else:
179 179 return request.route_url(
180 180 'repo_commit', repo_name=safe_str(repo.repo_name),
181 181 commit_id=commit_id)
182 182
183 183 def get_repo_log(self, repo, filter_term):
184 184 repo_log = UserLog.query()\
185 185 .filter(or_(UserLog.repository_id == repo.repo_id,
186 186 UserLog.repository_name == repo.repo_name))\
187 187 .options(joinedload(UserLog.user))\
188 188 .options(joinedload(UserLog.repository))\
189 189 .order_by(UserLog.action_date.desc())
190 190
191 191 repo_log = user_log_filter(repo_log, filter_term)
192 192 return repo_log
193 193
194 194 @classmethod
195 195 def update_repoinfo(cls, repositories=None):
196 196 if not repositories:
197 197 repositories = Repository.getAll()
198 198 for repo in repositories:
199 199 repo.update_commit_cache()
200 200
201 201 def get_repos_as_dict(self, repo_list=None, admin=False,
202 super_user_actions=False):
202 super_user_actions=False, short_name=None):
203 203 _render = get_current_request().get_partial_renderer(
204 204 'rhodecode:templates/data_table/_dt_elements.mako')
205 205 c = _render.get_call_context()
206 206
207 207 def quick_menu(repo_name):
208 208 return _render('quick_menu', repo_name)
209 209
210 210 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
211 if short_name is not None:
212 short_name_var = short_name
213 else:
214 short_name_var = not admin
211 215 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
212 short_name=not admin, admin=False)
216 short_name=short_name_var, admin=False)
213 217
214 218 def last_change(last_change):
215 219 if admin and isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
216 220 last_change = last_change + datetime.timedelta(seconds=
217 221 (datetime.datetime.now() - datetime.datetime.utcnow()).seconds)
218 222 return _render("last_change", last_change)
219 223
220 224 def rss_lnk(repo_name):
221 225 return _render("rss", repo_name)
222 226
223 227 def atom_lnk(repo_name):
224 228 return _render("atom", repo_name)
225 229
226 230 def last_rev(repo_name, cs_cache):
227 231 return _render('revision', repo_name, cs_cache.get('revision'),
228 232 cs_cache.get('raw_id'), cs_cache.get('author'),
229 233 cs_cache.get('message'), cs_cache.get('date'))
230 234
231 235 def desc(desc):
232 236 return _render('repo_desc', desc, c.visual.stylify_metatags)
233 237
234 238 def state(repo_state):
235 239 return _render("repo_state", repo_state)
236 240
237 241 def repo_actions(repo_name):
238 242 return _render('repo_actions', repo_name, super_user_actions)
239 243
240 244 def user_profile(username):
241 245 return _render('user_profile', username)
242 246
243 247 repos_data = []
244 248 for repo in repo_list:
245 249 cs_cache = repo.changeset_cache
246 250 row = {
247 251 "menu": quick_menu(repo.repo_name),
248 252
249 253 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
250 254 repo.private, repo.archived, repo.fork),
251 255 "name_raw": repo.repo_name.lower(),
252 256
253 257 "last_change": last_change(repo.last_db_change),
254 258 "last_change_raw": datetime_to_time(repo.last_db_change),
255 259
256 260 "last_changeset": last_rev(repo.repo_name, cs_cache),
257 261 "last_changeset_raw": cs_cache.get('revision'),
258 262
259 263 "desc": desc(repo.description_safe),
260 264 "owner": user_profile(repo.user.username),
261 265
262 266 "state": state(repo.repo_state),
263 267 "rss": rss_lnk(repo.repo_name),
264 268
265 269 "atom": atom_lnk(repo.repo_name),
266 270 }
267 271 if admin:
268 272 row.update({
269 273 "action": repo_actions(repo.repo_name),
270 274 })
271 275 repos_data.append(row)
272 276
273 277 return repos_data
274 278
275 279 def _get_defaults(self, repo_name):
276 280 """
277 281 Gets information about repository, and returns a dict for
278 282 usage in forms
279 283
280 284 :param repo_name:
281 285 """
282 286
283 287 repo_info = Repository.get_by_repo_name(repo_name)
284 288
285 289 if repo_info is None:
286 290 return None
287 291
288 292 defaults = repo_info.get_dict()
289 293 defaults['repo_name'] = repo_info.just_name
290 294
291 295 groups = repo_info.groups_with_parents
292 296 parent_group = groups[-1] if groups else None
293 297
294 298 # we use -1 as this is how in HTML, we mark an empty group
295 299 defaults['repo_group'] = getattr(parent_group, 'group_id', -1)
296 300
297 301 keys_to_process = (
298 302 {'k': 'repo_type', 'strip': False},
299 303 {'k': 'repo_enable_downloads', 'strip': True},
300 304 {'k': 'repo_description', 'strip': True},
301 305 {'k': 'repo_enable_locking', 'strip': True},
302 306 {'k': 'repo_landing_rev', 'strip': True},
303 307 {'k': 'clone_uri', 'strip': False},
304 308 {'k': 'push_uri', 'strip': False},
305 309 {'k': 'repo_private', 'strip': True},
306 310 {'k': 'repo_enable_statistics', 'strip': True}
307 311 )
308 312
309 313 for item in keys_to_process:
310 314 attr = item['k']
311 315 if item['strip']:
312 316 attr = remove_prefix(item['k'], 'repo_')
313 317
314 318 val = defaults[attr]
315 319 if item['k'] == 'repo_landing_rev':
316 320 val = ':'.join(defaults[attr])
317 321 defaults[item['k']] = val
318 322 if item['k'] == 'clone_uri':
319 323 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
320 324 if item['k'] == 'push_uri':
321 325 defaults['push_uri_hidden'] = repo_info.push_uri_hidden
322 326
323 327 # fill owner
324 328 if repo_info.user:
325 329 defaults.update({'user': repo_info.user.username})
326 330 else:
327 331 replacement_user = User.get_first_super_admin().username
328 332 defaults.update({'user': replacement_user})
329 333
330 334 return defaults
331 335
332 336 def update(self, repo, **kwargs):
333 337 try:
334 338 cur_repo = self._get_repo(repo)
335 339 source_repo_name = cur_repo.repo_name
336 340 if 'user' in kwargs:
337 341 cur_repo.user = User.get_by_username(kwargs['user'])
338 342
339 343 if 'repo_group' in kwargs:
340 344 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
341 345 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
342 346
343 347 update_keys = [
344 348 (1, 'repo_description'),
345 349 (1, 'repo_landing_rev'),
346 350 (1, 'repo_private'),
347 351 (1, 'repo_enable_downloads'),
348 352 (1, 'repo_enable_locking'),
349 353 (1, 'repo_enable_statistics'),
350 354 (0, 'clone_uri'),
351 355 (0, 'push_uri'),
352 356 (0, 'fork_id')
353 357 ]
354 358 for strip, k in update_keys:
355 359 if k in kwargs:
356 360 val = kwargs[k]
357 361 if strip:
358 362 k = remove_prefix(k, 'repo_')
359 363
360 364 setattr(cur_repo, k, val)
361 365
362 366 new_name = cur_repo.get_new_name(kwargs['repo_name'])
363 367 cur_repo.repo_name = new_name
364 368
365 369 # if private flag is set, reset default permission to NONE
366 370 if kwargs.get('repo_private'):
367 371 EMPTY_PERM = 'repository.none'
368 372 RepoModel().grant_user_permission(
369 373 repo=cur_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM
370 374 )
371 375
372 376 # handle extra fields
373 377 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX),
374 378 kwargs):
375 379 k = RepositoryField.un_prefix_key(field)
376 380 ex_field = RepositoryField.get_by_key_name(
377 381 key=k, repo=cur_repo)
378 382 if ex_field:
379 383 ex_field.field_value = kwargs[field]
380 384 self.sa.add(ex_field)
381 385 cur_repo.updated_on = datetime.datetime.now()
382 386 self.sa.add(cur_repo)
383 387
384 388 if source_repo_name != new_name:
385 389 # rename repository
386 390 self._rename_filesystem_repo(
387 391 old=source_repo_name, new=new_name)
388 392
389 393 return cur_repo
390 394 except Exception:
391 395 log.error(traceback.format_exc())
392 396 raise
393 397
394 398 def _create_repo(self, repo_name, repo_type, description, owner,
395 399 private=False, clone_uri=None, repo_group=None,
396 400 landing_rev='rev:tip', fork_of=None,
397 401 copy_fork_permissions=False, enable_statistics=False,
398 402 enable_locking=False, enable_downloads=False,
399 403 copy_group_permissions=False,
400 404 state=Repository.STATE_PENDING):
401 405 """
402 406 Create repository inside database with PENDING state, this should be
403 407 only executed by create() repo. With exception of importing existing
404 408 repos
405 409 """
406 410 from rhodecode.model.scm import ScmModel
407 411
408 412 owner = self._get_user(owner)
409 413 fork_of = self._get_repo(fork_of)
410 414 repo_group = self._get_repo_group(safe_int(repo_group))
411 415
412 416 try:
413 417 repo_name = safe_unicode(repo_name)
414 418 description = safe_unicode(description)
415 419 # repo name is just a name of repository
416 420 # while repo_name_full is a full qualified name that is combined
417 421 # with name and path of group
418 422 repo_name_full = repo_name
419 423 repo_name = repo_name.split(Repository.NAME_SEP)[-1]
420 424
421 425 new_repo = Repository()
422 426 new_repo.repo_state = state
423 427 new_repo.enable_statistics = False
424 428 new_repo.repo_name = repo_name_full
425 429 new_repo.repo_type = repo_type
426 430 new_repo.user = owner
427 431 new_repo.group = repo_group
428 432 new_repo.description = description or repo_name
429 433 new_repo.private = private
430 434 new_repo.archived = False
431 435 new_repo.clone_uri = clone_uri
432 436 new_repo.landing_rev = landing_rev
433 437
434 438 new_repo.enable_statistics = enable_statistics
435 439 new_repo.enable_locking = enable_locking
436 440 new_repo.enable_downloads = enable_downloads
437 441
438 442 if repo_group:
439 443 new_repo.enable_locking = repo_group.enable_locking
440 444
441 445 if fork_of:
442 446 parent_repo = fork_of
443 447 new_repo.fork = parent_repo
444 448
445 449 events.trigger(events.RepoPreCreateEvent(new_repo))
446 450
447 451 self.sa.add(new_repo)
448 452
449 453 EMPTY_PERM = 'repository.none'
450 454 if fork_of and copy_fork_permissions:
451 455 repo = fork_of
452 456 user_perms = UserRepoToPerm.query() \
453 457 .filter(UserRepoToPerm.repository == repo).all()
454 458 group_perms = UserGroupRepoToPerm.query() \
455 459 .filter(UserGroupRepoToPerm.repository == repo).all()
456 460
457 461 for perm in user_perms:
458 462 UserRepoToPerm.create(
459 463 perm.user, new_repo, perm.permission)
460 464
461 465 for perm in group_perms:
462 466 UserGroupRepoToPerm.create(
463 467 perm.users_group, new_repo, perm.permission)
464 468 # in case we copy permissions and also set this repo to private
465 469 # override the default user permission to make it a private repo
466 470 if private:
467 471 RepoModel(self.sa).grant_user_permission(
468 472 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
469 473
470 474 elif repo_group and copy_group_permissions:
471 475 user_perms = UserRepoGroupToPerm.query() \
472 476 .filter(UserRepoGroupToPerm.group == repo_group).all()
473 477
474 478 group_perms = UserGroupRepoGroupToPerm.query() \
475 479 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
476 480
477 481 for perm in user_perms:
478 482 perm_name = perm.permission.permission_name.replace(
479 483 'group.', 'repository.')
480 484 perm_obj = Permission.get_by_key(perm_name)
481 485 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
482 486
483 487 for perm in group_perms:
484 488 perm_name = perm.permission.permission_name.replace(
485 489 'group.', 'repository.')
486 490 perm_obj = Permission.get_by_key(perm_name)
487 491 UserGroupRepoToPerm.create(perm.users_group, new_repo, perm_obj)
488 492
489 493 if private:
490 494 RepoModel(self.sa).grant_user_permission(
491 495 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
492 496
493 497 else:
494 498 perm_obj = self._create_default_perms(new_repo, private)
495 499 self.sa.add(perm_obj)
496 500
497 501 # now automatically start following this repository as owner
498 502 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id, owner.user_id)
499 503
500 504 # we need to flush here, in order to check if database won't
501 505 # throw any exceptions, create filesystem dirs at the very end
502 506 self.sa.flush()
503 507 events.trigger(events.RepoCreateEvent(new_repo))
504 508 return new_repo
505 509
506 510 except Exception:
507 511 log.error(traceback.format_exc())
508 512 raise
509 513
510 514 def create(self, form_data, cur_user):
511 515 """
512 516 Create repository using celery tasks
513 517
514 518 :param form_data:
515 519 :param cur_user:
516 520 """
517 521 from rhodecode.lib.celerylib import tasks, run_task
518 522 return run_task(tasks.create_repo, form_data, cur_user)
519 523
520 524 def update_permissions(self, repo, perm_additions=None, perm_updates=None,
521 525 perm_deletions=None, check_perms=True,
522 526 cur_user=None):
523 527 if not perm_additions:
524 528 perm_additions = []
525 529 if not perm_updates:
526 530 perm_updates = []
527 531 if not perm_deletions:
528 532 perm_deletions = []
529 533
530 534 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
531 535
532 536 changes = {
533 537 'added': [],
534 538 'updated': [],
535 539 'deleted': []
536 540 }
537 541 # update permissions
538 542 for member_id, perm, member_type in perm_updates:
539 543 member_id = int(member_id)
540 544 if member_type == 'user':
541 545 member_name = User.get(member_id).username
542 546 # this updates also current one if found
543 547 self.grant_user_permission(
544 548 repo=repo, user=member_id, perm=perm)
545 549 elif member_type == 'user_group':
546 550 # check if we have permissions to alter this usergroup
547 551 member_name = UserGroup.get(member_id).users_group_name
548 552 if not check_perms or HasUserGroupPermissionAny(
549 553 *req_perms)(member_name, user=cur_user):
550 554 self.grant_user_group_permission(
551 555 repo=repo, group_name=member_id, perm=perm)
552 556 else:
553 557 raise ValueError("member_type must be 'user' or 'user_group' "
554 558 "got {} instead".format(member_type))
555 559 changes['updated'].append({'type': member_type, 'id': member_id,
556 560 'name': member_name, 'new_perm': perm})
557 561
558 562 # set new permissions
559 563 for member_id, perm, member_type in perm_additions:
560 564 member_id = int(member_id)
561 565 if member_type == 'user':
562 566 member_name = User.get(member_id).username
563 567 self.grant_user_permission(
564 568 repo=repo, user=member_id, perm=perm)
565 569 elif member_type == 'user_group':
566 570 # check if we have permissions to alter this usergroup
567 571 member_name = UserGroup.get(member_id).users_group_name
568 572 if not check_perms or HasUserGroupPermissionAny(
569 573 *req_perms)(member_name, user=cur_user):
570 574 self.grant_user_group_permission(
571 575 repo=repo, group_name=member_id, perm=perm)
572 576 else:
573 577 raise ValueError("member_type must be 'user' or 'user_group' "
574 578 "got {} instead".format(member_type))
575 579
576 580 changes['added'].append({'type': member_type, 'id': member_id,
577 581 'name': member_name, 'new_perm': perm})
578 582 # delete permissions
579 583 for member_id, perm, member_type in perm_deletions:
580 584 member_id = int(member_id)
581 585 if member_type == 'user':
582 586 member_name = User.get(member_id).username
583 587 self.revoke_user_permission(repo=repo, user=member_id)
584 588 elif member_type == 'user_group':
585 589 # check if we have permissions to alter this usergroup
586 590 member_name = UserGroup.get(member_id).users_group_name
587 591 if not check_perms or HasUserGroupPermissionAny(
588 592 *req_perms)(member_name, user=cur_user):
589 593 self.revoke_user_group_permission(
590 594 repo=repo, group_name=member_id)
591 595 else:
592 596 raise ValueError("member_type must be 'user' or 'user_group' "
593 597 "got {} instead".format(member_type))
594 598
595 599 changes['deleted'].append({'type': member_type, 'id': member_id,
596 600 'name': member_name, 'new_perm': perm})
597 601 return changes
598 602
599 603 def create_fork(self, form_data, cur_user):
600 604 """
601 605 Simple wrapper into executing celery task for fork creation
602 606
603 607 :param form_data:
604 608 :param cur_user:
605 609 """
606 610 from rhodecode.lib.celerylib import tasks, run_task
607 611 return run_task(tasks.create_repo_fork, form_data, cur_user)
608 612
609 613 def archive(self, repo):
610 614 """
611 615 Archive given repository. Set archive flag.
612 616
613 617 :param repo:
614 618 """
615 619 repo = self._get_repo(repo)
616 620 if repo:
617 621
618 622 try:
619 623 repo.archived = True
620 624 self.sa.add(repo)
621 625 self.sa.commit()
622 626 except Exception:
623 627 log.error(traceback.format_exc())
624 628 raise
625 629
626 630 def delete(self, repo, forks=None, pull_requests=None, fs_remove=True, cur_user=None):
627 631 """
628 632 Delete given repository, forks parameter defines what do do with
629 633 attached forks. Throws AttachedForksError if deleted repo has attached
630 634 forks
631 635
632 636 :param repo:
633 637 :param forks: str 'delete' or 'detach'
634 638 :param pull_requests: str 'delete' or None
635 639 :param fs_remove: remove(archive) repo from filesystem
636 640 """
637 641 if not cur_user:
638 642 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
639 643 repo = self._get_repo(repo)
640 644 if repo:
641 645 if forks == 'detach':
642 646 for r in repo.forks:
643 647 r.fork = None
644 648 self.sa.add(r)
645 649 elif forks == 'delete':
646 650 for r in repo.forks:
647 651 self.delete(r, forks='delete')
648 652 elif [f for f in repo.forks]:
649 653 raise AttachedForksError()
650 654
651 655 # check for pull requests
652 656 pr_sources = repo.pull_requests_source
653 657 pr_targets = repo.pull_requests_target
654 658 if pull_requests != 'delete' and (pr_sources or pr_targets):
655 659 raise AttachedPullRequestsError()
656 660
657 661 old_repo_dict = repo.get_dict()
658 662 events.trigger(events.RepoPreDeleteEvent(repo))
659 663 try:
660 664 self.sa.delete(repo)
661 665 if fs_remove:
662 666 self._delete_filesystem_repo(repo)
663 667 else:
664 668 log.debug('skipping removal from filesystem')
665 669 old_repo_dict.update({
666 670 'deleted_by': cur_user,
667 671 'deleted_on': time.time(),
668 672 })
669 673 log_delete_repository(**old_repo_dict)
670 674 events.trigger(events.RepoDeleteEvent(repo))
671 675 except Exception:
672 676 log.error(traceback.format_exc())
673 677 raise
674 678
675 679 def grant_user_permission(self, repo, user, perm):
676 680 """
677 681 Grant permission for user on given repository, or update existing one
678 682 if found
679 683
680 684 :param repo: Instance of Repository, repository_id, or repository name
681 685 :param user: Instance of User, user_id or username
682 686 :param perm: Instance of Permission, or permission_name
683 687 """
684 688 user = self._get_user(user)
685 689 repo = self._get_repo(repo)
686 690 permission = self._get_perm(perm)
687 691
688 692 # check if we have that permission already
689 693 obj = self.sa.query(UserRepoToPerm) \
690 694 .filter(UserRepoToPerm.user == user) \
691 695 .filter(UserRepoToPerm.repository == repo) \
692 696 .scalar()
693 697 if obj is None:
694 698 # create new !
695 699 obj = UserRepoToPerm()
696 700 obj.repository = repo
697 701 obj.user = user
698 702 obj.permission = permission
699 703 self.sa.add(obj)
700 704 log.debug('Granted perm %s to %s on %s', perm, user, repo)
701 705 action_logger_generic(
702 706 'granted permission: {} to user: {} on repo: {}'.format(
703 707 perm, user, repo), namespace='security.repo')
704 708 return obj
705 709
706 710 def revoke_user_permission(self, repo, user):
707 711 """
708 712 Revoke permission for user on given repository
709 713
710 714 :param repo: Instance of Repository, repository_id, or repository name
711 715 :param user: Instance of User, user_id or username
712 716 """
713 717
714 718 user = self._get_user(user)
715 719 repo = self._get_repo(repo)
716 720
717 721 obj = self.sa.query(UserRepoToPerm) \
718 722 .filter(UserRepoToPerm.repository == repo) \
719 723 .filter(UserRepoToPerm.user == user) \
720 724 .scalar()
721 725 if obj:
722 726 self.sa.delete(obj)
723 727 log.debug('Revoked perm on %s on %s', repo, user)
724 728 action_logger_generic(
725 729 'revoked permission from user: {} on repo: {}'.format(
726 730 user, repo), namespace='security.repo')
727 731
728 732 def grant_user_group_permission(self, repo, group_name, perm):
729 733 """
730 734 Grant permission for user group on given repository, or update
731 735 existing one if found
732 736
733 737 :param repo: Instance of Repository, repository_id, or repository name
734 738 :param group_name: Instance of UserGroup, users_group_id,
735 739 or user group name
736 740 :param perm: Instance of Permission, or permission_name
737 741 """
738 742 repo = self._get_repo(repo)
739 743 group_name = self._get_user_group(group_name)
740 744 permission = self._get_perm(perm)
741 745
742 746 # check if we have that permission already
743 747 obj = self.sa.query(UserGroupRepoToPerm) \
744 748 .filter(UserGroupRepoToPerm.users_group == group_name) \
745 749 .filter(UserGroupRepoToPerm.repository == repo) \
746 750 .scalar()
747 751
748 752 if obj is None:
749 753 # create new
750 754 obj = UserGroupRepoToPerm()
751 755
752 756 obj.repository = repo
753 757 obj.users_group = group_name
754 758 obj.permission = permission
755 759 self.sa.add(obj)
756 760 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
757 761 action_logger_generic(
758 762 'granted permission: {} to usergroup: {} on repo: {}'.format(
759 763 perm, group_name, repo), namespace='security.repo')
760 764
761 765 return obj
762 766
763 767 def revoke_user_group_permission(self, repo, group_name):
764 768 """
765 769 Revoke permission for user group on given repository
766 770
767 771 :param repo: Instance of Repository, repository_id, or repository name
768 772 :param group_name: Instance of UserGroup, users_group_id,
769 773 or user group name
770 774 """
771 775 repo = self._get_repo(repo)
772 776 group_name = self._get_user_group(group_name)
773 777
774 778 obj = self.sa.query(UserGroupRepoToPerm) \
775 779 .filter(UserGroupRepoToPerm.repository == repo) \
776 780 .filter(UserGroupRepoToPerm.users_group == group_name) \
777 781 .scalar()
778 782 if obj:
779 783 self.sa.delete(obj)
780 784 log.debug('Revoked perm to %s on %s', repo, group_name)
781 785 action_logger_generic(
782 786 'revoked permission from usergroup: {} on repo: {}'.format(
783 787 group_name, repo), namespace='security.repo')
784 788
785 789 def delete_stats(self, repo_name):
786 790 """
787 791 removes stats for given repo
788 792
789 793 :param repo_name:
790 794 """
791 795 repo = self._get_repo(repo_name)
792 796 try:
793 797 obj = self.sa.query(Statistics) \
794 798 .filter(Statistics.repository == repo).scalar()
795 799 if obj:
796 800 self.sa.delete(obj)
797 801 except Exception:
798 802 log.error(traceback.format_exc())
799 803 raise
800 804
801 805 def add_repo_field(self, repo_name, field_key, field_label, field_value='',
802 806 field_type='str', field_desc=''):
803 807
804 808 repo = self._get_repo(repo_name)
805 809
806 810 new_field = RepositoryField()
807 811 new_field.repository = repo
808 812 new_field.field_key = field_key
809 813 new_field.field_type = field_type # python type
810 814 new_field.field_value = field_value
811 815 new_field.field_desc = field_desc
812 816 new_field.field_label = field_label
813 817 self.sa.add(new_field)
814 818 return new_field
815 819
816 820 def delete_repo_field(self, repo_name, field_key):
817 821 repo = self._get_repo(repo_name)
818 822 field = RepositoryField.get_by_key_name(field_key, repo)
819 823 if field:
820 824 self.sa.delete(field)
821 825
822 826 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
823 827 clone_uri=None, repo_store_location=None,
824 828 use_global_config=False):
825 829 """
826 830 makes repository on filesystem. It's group aware means it'll create
827 831 a repository within a group, and alter the paths accordingly of
828 832 group location
829 833
830 834 :param repo_name:
831 835 :param alias:
832 836 :param parent:
833 837 :param clone_uri:
834 838 :param repo_store_location:
835 839 """
836 840 from rhodecode.lib.utils import is_valid_repo, is_valid_repo_group
837 841 from rhodecode.model.scm import ScmModel
838 842
839 843 if Repository.NAME_SEP in repo_name:
840 844 raise ValueError(
841 845 'repo_name must not contain groups got `%s`' % repo_name)
842 846
843 847 if isinstance(repo_group, RepoGroup):
844 848 new_parent_path = os.sep.join(repo_group.full_path_splitted)
845 849 else:
846 850 new_parent_path = repo_group or ''
847 851
848 852 if repo_store_location:
849 853 _paths = [repo_store_location]
850 854 else:
851 855 _paths = [self.repos_path, new_parent_path, repo_name]
852 856 # we need to make it str for mercurial
853 857 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
854 858
855 859 # check if this path is not a repository
856 860 if is_valid_repo(repo_path, self.repos_path):
857 861 raise Exception('This path %s is a valid repository' % repo_path)
858 862
859 863 # check if this path is a group
860 864 if is_valid_repo_group(repo_path, self.repos_path):
861 865 raise Exception('This path %s is a valid group' % repo_path)
862 866
863 867 log.info('creating repo %s in %s from url: `%s`',
864 868 repo_name, safe_unicode(repo_path),
865 869 obfuscate_url_pw(clone_uri))
866 870
867 871 backend = get_backend(repo_type)
868 872
869 873 config_repo = None if use_global_config else repo_name
870 874 if config_repo and new_parent_path:
871 875 config_repo = Repository.NAME_SEP.join(
872 876 (new_parent_path, config_repo))
873 877 config = make_db_config(clear_session=False, repo=config_repo)
874 878 config.set('extensions', 'largefiles', '')
875 879
876 880 # patch and reset hooks section of UI config to not run any
877 881 # hooks on creating remote repo
878 882 config.clear_section('hooks')
879 883
880 884 # TODO: johbo: Unify this, hardcoded "bare=True" does not look nice
881 885 if repo_type == 'git':
882 886 repo = backend(
883 887 repo_path, config=config, create=True, src_url=clone_uri,
884 888 bare=True)
885 889 else:
886 890 repo = backend(
887 891 repo_path, config=config, create=True, src_url=clone_uri)
888 892
889 893 repo.install_hooks()
890 894
891 895 log.debug('Created repo %s with %s backend',
892 896 safe_unicode(repo_name), safe_unicode(repo_type))
893 897 return repo
894 898
895 899 def _rename_filesystem_repo(self, old, new):
896 900 """
897 901 renames repository on filesystem
898 902
899 903 :param old: old name
900 904 :param new: new name
901 905 """
902 906 log.info('renaming repo from %s to %s', old, new)
903 907
904 908 old_path = os.path.join(self.repos_path, old)
905 909 new_path = os.path.join(self.repos_path, new)
906 910 if os.path.isdir(new_path):
907 911 raise Exception(
908 912 'Was trying to rename to already existing dir %s' % new_path
909 913 )
910 914 shutil.move(old_path, new_path)
911 915
912 916 def _delete_filesystem_repo(self, repo):
913 917 """
914 918 removes repo from filesystem, the removal is acctually made by
915 919 added rm__ prefix into dir, and rename internat .hg/.git dirs so this
916 920 repository is no longer valid for rhodecode, can be undeleted later on
917 921 by reverting the renames on this repository
918 922
919 923 :param repo: repo object
920 924 """
921 925 rm_path = os.path.join(self.repos_path, repo.repo_name)
922 926 repo_group = repo.group
923 927 log.info("Removing repository %s", rm_path)
924 928 # disable hg/git internal that it doesn't get detected as repo
925 929 alias = repo.repo_type
926 930
927 931 config = make_db_config(clear_session=False)
928 932 config.set('extensions', 'largefiles', '')
929 933 bare = getattr(repo.scm_instance(config=config), 'bare', False)
930 934
931 935 # skip this for bare git repos
932 936 if not bare:
933 937 # disable VCS repo
934 938 vcs_path = os.path.join(rm_path, '.%s' % alias)
935 939 if os.path.exists(vcs_path):
936 940 shutil.move(vcs_path, os.path.join(rm_path, 'rm__.%s' % alias))
937 941
938 942 _now = datetime.datetime.now()
939 943 _ms = str(_now.microsecond).rjust(6, '0')
940 944 _d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
941 945 repo.just_name)
942 946 if repo_group:
943 947 # if repository is in group, prefix the removal path with the group
944 948 args = repo_group.full_path_splitted + [_d]
945 949 _d = os.path.join(*args)
946 950
947 951 if os.path.isdir(rm_path):
948 952 shutil.move(rm_path, os.path.join(self.repos_path, _d))
949 953
950 954 # finally cleanup diff-cache if it exists
951 955 cached_diffs_dir = repo.cached_diffs_dir
952 956 if os.path.isdir(cached_diffs_dir):
953 957 shutil.rmtree(cached_diffs_dir)
954 958
955 959
956 960 class ReadmeFinder:
957 961 """
958 962 Utility which knows how to find a readme for a specific commit.
959 963
960 964 The main idea is that this is a configurable algorithm. When creating an
961 965 instance you can define parameters, currently only the `default_renderer`.
962 966 Based on this configuration the method :meth:`search` behaves slightly
963 967 different.
964 968 """
965 969
966 970 readme_re = re.compile(r'^readme(\.[^\.]+)?$', re.IGNORECASE)
967 971 path_re = re.compile(r'^docs?', re.IGNORECASE)
968 972
969 973 default_priorities = {
970 974 None: 0,
971 975 '.text': 2,
972 976 '.txt': 3,
973 977 '.rst': 1,
974 978 '.rest': 2,
975 979 '.md': 1,
976 980 '.mkdn': 2,
977 981 '.mdown': 3,
978 982 '.markdown': 4,
979 983 }
980 984
981 985 path_priority = {
982 986 'doc': 0,
983 987 'docs': 1,
984 988 }
985 989
986 990 FALLBACK_PRIORITY = 99
987 991
988 992 RENDERER_TO_EXTENSION = {
989 993 'rst': ['.rst', '.rest'],
990 994 'markdown': ['.md', 'mkdn', '.mdown', '.markdown'],
991 995 }
992 996
993 997 def __init__(self, default_renderer=None):
994 998 self._default_renderer = default_renderer
995 999 self._renderer_extensions = self.RENDERER_TO_EXTENSION.get(
996 1000 default_renderer, [])
997 1001
998 1002 def search(self, commit, path='/'):
999 1003 """
1000 1004 Find a readme in the given `commit`.
1001 1005 """
1002 1006 nodes = commit.get_nodes(path)
1003 1007 matches = self._match_readmes(nodes)
1004 1008 matches = self._sort_according_to_priority(matches)
1005 1009 if matches:
1006 1010 return matches[0].node
1007 1011
1008 1012 paths = self._match_paths(nodes)
1009 1013 paths = self._sort_paths_according_to_priority(paths)
1010 1014 for path in paths:
1011 1015 match = self.search(commit, path=path)
1012 1016 if match:
1013 1017 return match
1014 1018
1015 1019 return None
1016 1020
1017 1021 def _match_readmes(self, nodes):
1018 1022 for node in nodes:
1019 1023 if not node.is_file():
1020 1024 continue
1021 1025 path = node.path.rsplit('/', 1)[-1]
1022 1026 match = self.readme_re.match(path)
1023 1027 if match:
1024 1028 extension = match.group(1)
1025 1029 yield ReadmeMatch(node, match, self._priority(extension))
1026 1030
1027 1031 def _match_paths(self, nodes):
1028 1032 for node in nodes:
1029 1033 if not node.is_dir():
1030 1034 continue
1031 1035 match = self.path_re.match(node.path)
1032 1036 if match:
1033 1037 yield node.path
1034 1038
1035 1039 def _priority(self, extension):
1036 1040 renderer_priority = (
1037 1041 0 if extension in self._renderer_extensions else 1)
1038 1042 extension_priority = self.default_priorities.get(
1039 1043 extension, self.FALLBACK_PRIORITY)
1040 1044 return (renderer_priority, extension_priority)
1041 1045
1042 1046 def _sort_according_to_priority(self, matches):
1043 1047
1044 1048 def priority_and_path(match):
1045 1049 return (match.priority, match.path)
1046 1050
1047 1051 return sorted(matches, key=priority_and_path)
1048 1052
1049 1053 def _sort_paths_according_to_priority(self, paths):
1050 1054
1051 1055 def priority_and_path(path):
1052 1056 return (self.path_priority.get(path, self.FALLBACK_PRIORITY), path)
1053 1057
1054 1058 return sorted(paths, key=priority_and_path)
1055 1059
1056 1060
1057 1061 class ReadmeMatch:
1058 1062
1059 1063 def __init__(self, node, match, priority):
1060 1064 self.node = node
1061 1065 self._match = match
1062 1066 self.priority = priority
1063 1067
1064 1068 @property
1065 1069 def path(self):
1066 1070 return self.node.path
1067 1071
1068 1072 def __repr__(self):
1069 1073 return '<ReadmeMatch {} priority={}'.format(self.path, self.priority)
@@ -1,74 +1,60 b''
1 1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 var onSuccessFollow = function(target){
20 var f = $(target);
21 var f_cnt = $('#current_followers_count');
19 var onSuccessFollow = function (target) {
20 var targetEl = $(target);
22 21
23 if(f.hasClass('follow')){
24 f.removeClass('follow');
25 f.addClass('following');
26 f.attr('title', _gettext('Stop following this repository'));
27 $(f).html(_gettext('Unfollow'));
28 if(f_cnt.length){
29 var cnt = Number(f_cnt.html())+1;
30 f_cnt.html(cnt);
22 var callback = function () {
23 targetEl.animate({'opacity': 1.00}, 200);
24 if (targetEl.hasClass('watching')) {
25 targetEl.removeClass('watching');
26 targetEl.attr('title', _gettext('Stopped watching this repository'));
27 $(targetEl).html(_gettext('Watch'));
28 } else {
29 targetEl.addClass('watching');
30 targetEl.attr('title', _gettext('Started watching this repository'));
31 $(targetEl).html(_gettext('Unwatch'));
31 32 }
32 }
33 else{
34 f.removeClass('following');
35 f.addClass('follow');
36 f.attr('title', _gettext('Start following this repository'));
37 $(f).html(_gettext('Follow'));
38 if(f_cnt.length){
39 var cnt = Number(f_cnt.html())-1;
40 f_cnt.html(cnt);
41 }
42 }
33 };
34 targetEl.animate({'opacity': 0.15}, 200, callback);
43 35 };
44 36
45 // TODO:: check if the function is needed. 0 usage found
46 var toggleFollowingUser = function(target,follows_user_id,token,user_id){
37
38 var toggleFollowingUser = function (target, follows_user_id) {
47 39 var args = {
48 40 'follows_user_id': follows_user_id,
49 'auth_token': token,
50 41 'csrf_token': CSRF_TOKEN
51 42 };
52 if(user_id != undefined){
53 args.user_id = user_id
54 }
55 ajaxPOST(pyroutes.url('toggle_following'), args, function(){
43
44 ajaxPOST(pyroutes.url('toggle_following'), args, function () {
56 45 onSuccessFollow(target);
57 46 });
58 47 return false;
59 48 };
60 49
61 var toggleFollowingRepo = function(target,follows_repo_id,token,user_id){
50 var toggleFollowingRepo = function (target, follows_repo_id) {
62 51 var args = {
63 52 'follows_repo_id': follows_repo_id,
64 'auth_token': token,
65 53 'csrf_token': CSRF_TOKEN
66 54 };
67 if(user_id != undefined){
68 args.user_id = user_id
69 }
70 ajaxPOST(pyroutes.url('toggle_following'), args, function(){
55
56 ajaxPOST(pyroutes.url('toggle_following'), args, function () {
71 57 onSuccessFollow(target);
72 58 });
73 59 return false;
74 60 };
@@ -1,951 +1,960 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.mako"/>
3 3
4 4 <%include file="/ejs_templates/templates.html"/>
5 5
6 6 <div class="outerwrapper">
7 7 <!-- HEADER -->
8 8 <div class="header">
9 9 <div id="header-inner" class="wrapper">
10 10 <div id="logo">
11 11 <div class="logo-wrapper">
12 12 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
13 13 </div>
14 14 % if c.rhodecode_name:
15 15 <div class="branding">
16 16 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
17 17 </div>
18 18 % endif
19 19 </div>
20 20 <!-- MENU BAR NAV -->
21 21 ${self.menu_bar_nav()}
22 22 <!-- END MENU BAR NAV -->
23 23 </div>
24 24 </div>
25 25 ${self.menu_bar_subnav()}
26 26 <!-- END HEADER -->
27 27
28 28 <!-- CONTENT -->
29 29 <div id="content" class="wrapper">
30 30
31 31 <rhodecode-toast id="notifications"></rhodecode-toast>
32 32
33 33 <div class="main">
34 34 ${next.main()}
35 35 </div>
36 36 </div>
37 37 <!-- END CONTENT -->
38 38
39 39 </div>
40 40 <!-- FOOTER -->
41 41 <div id="footer">
42 42 <div id="footer-inner" class="title wrapper">
43 43 <div>
44 44 <p class="footer-link-right">
45 45 % if c.visual.show_version:
46 46 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
47 47 % endif
48 48 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
49 49 % if c.visual.rhodecode_support_url:
50 50 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
51 51 % endif
52 52 </p>
53 53 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
54 54 <p class="server-instance" style="display:${sid}">
55 55 ## display hidden instance ID if specially defined
56 56 % if c.rhodecode_instanceid:
57 57 ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
58 58 % endif
59 59 </p>
60 60 </div>
61 61 </div>
62 62 </div>
63 63
64 64 <!-- END FOOTER -->
65 65
66 66 ### MAKO DEFS ###
67 67
68 68 <%def name="menu_bar_subnav()">
69 69 </%def>
70 70
71 71 <%def name="breadcrumbs(class_='breadcrumbs')">
72 72 <div class="${class_}">
73 73 ${self.breadcrumbs_links()}
74 74 </div>
75 75 </%def>
76 76
77 77 <%def name="admin_menu(active=None)">
78 78 <%
79 79 def is_active(selected):
80 80 if selected == active:
81 81 return "active"
82 82 %>
83 83
84 84 <div id="context-bar">
85 85 <div class="wrapper">
86 86 <div class="title">
87 87 <div class="title-content">
88 88 <div class="title-main">
89 89 % if c.is_super_admin:
90 90 ${_('Super Admin Panel')}
91 91 % else:
92 92 ${_('Delegated Admin Panel')}
93 93 % endif
94 94 </div>
95 95 </div>
96 96 </div>
97 97
98 98 <ul id="context-pages" class="navigation horizontal-list">
99 99
100 100 ## super admin case
101 101 % if c.is_super_admin:
102 102 <li class="${is_active('audit_logs')}"><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
103 103 <li class="${is_active('repositories')}"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
104 104 <li class="${is_active('repository_groups')}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
105 105 <li class="${is_active('users')}"><a href="${h.route_path('users')}">${_('Users')}</a></li>
106 106 <li class="${is_active('user_groups')}"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
107 107 <li class="${is_active('permissions')}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
108 108 <li class="${is_active('authentication')}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
109 109 <li class="${is_active('integrations')}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
110 110 <li class="${is_active('defaults')}"><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
111 111 <li class="${is_active('settings')}"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
112 112
113 113 ## delegated admin
114 114 % elif c.is_delegated_admin:
115 115 <%
116 116 repositories=c.auth_user.repositories_admin or c.can_create_repo
117 117 repository_groups=c.auth_user.repository_groups_admin or c.can_create_repo_group
118 118 user_groups=c.auth_user.user_groups_admin or c.can_create_user_group
119 119 %>
120 120
121 121 %if repositories:
122 122 <li class="${is_active('repositories')} local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
123 123 %endif
124 124 %if repository_groups:
125 125 <li class="${is_active('repository_groups')} local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
126 126 %endif
127 127 %if user_groups:
128 128 <li class="${is_active('user_groups')} local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
129 129 %endif
130 130 % endif
131 131 </ul>
132 132
133 133 </div>
134 134 <div class="clear"></div>
135 135 </div>
136 136 </%def>
137 137
138 138 <%def name="dt_info_panel(elements)">
139 139 <dl class="dl-horizontal">
140 140 %for dt, dd, title, show_items in elements:
141 141 <dt>${dt}:</dt>
142 142 <dd title="${h.tooltip(title)}">
143 143 %if callable(dd):
144 144 ## allow lazy evaluation of elements
145 145 ${dd()}
146 146 %else:
147 147 ${dd}
148 148 %endif
149 149 %if show_items:
150 150 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
151 151 %endif
152 152 </dd>
153 153
154 154 %if show_items:
155 155 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
156 156 %for item in show_items:
157 157 <dt></dt>
158 158 <dd>${item}</dd>
159 159 %endfor
160 160 </div>
161 161 %endif
162 162
163 163 %endfor
164 164 </dl>
165 165 </%def>
166 166
167 167 <%def name="gravatar(email, size=16)">
168 168 <%
169 169 if (size > 16):
170 170 gravatar_class = 'gravatar gravatar-large'
171 171 else:
172 172 gravatar_class = 'gravatar'
173 173 %>
174 174 <%doc>
175 175 TODO: johbo: For now we serve double size images to make it smooth
176 176 for retina. This is how it worked until now. Should be replaced
177 177 with a better solution at some point.
178 178 </%doc>
179 179 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
180 180 </%def>
181 181
182 182
183 183 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
184 184 <% email = h.email_or_none(contact) %>
185 185 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
186 186 ${self.gravatar(email, size)}
187 187 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
188 188 </div>
189 189 </%def>
190 190
191 191
192 192 <%def name="repo_page_title(repo_instance)">
193 193 <div class="title-content repo-title">
194 194 <div class="title-main">
195 195 ## SVN/HG/GIT icons
196 196 %if h.is_hg(repo_instance):
197 197 <i class="icon-hg"></i>
198 198 %endif
199 199 %if h.is_git(repo_instance):
200 200 <i class="icon-git"></i>
201 201 %endif
202 202 %if h.is_svn(repo_instance):
203 203 <i class="icon-svn"></i>
204 204 %endif
205 205
206 206 ## public/private
207 207 %if repo_instance.private:
208 208 <i class="icon-repo-private"></i>
209 209 %else:
210 210 <i class="icon-repo-public"></i>
211 211 %endif
212 212
213 213 ## repo name with group name
214 214 ${h.breadcrumb_repo_link(repo_instance)}
215 215
216 216 ## Context Actions
217 217 <div class="pull-right">
218 218 %if c.rhodecode_user.username != h.DEFAULT_USER:
219 219 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_name, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
220
221 <a href="#WatchRepo" onclick="toggleFollowingRepo(this, templateContext.repo_id); return false" title="${_('Watch this Repository and actions on it in your personalized journal')}" class="btn btn-sm ${('watching' if c.repository_is_user_following else '')}">
222 % if c.repository_is_user_following:
223 Unwatch
224 % else:
225 Watch
226 % endif
227
228 </a>
220 229 %else:
221 230 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_name)}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
222 231 %endif
223 232 </div>
224 233
225 234 </div>
226 235
227 236 ## FORKED
228 237 %if repo_instance.fork:
229 238 <p class="discreet">
230 239 <i class="icon-code-fork"></i> ${_('Fork of')}
231 240 ${h.link_to_if(c.has_origin_repo_read_perm,repo_instance.fork.repo_name, h.route_path('repo_summary', repo_name=repo_instance.fork.repo_name))}
232 241 </p>
233 242 %endif
234 243
235 244 ## IMPORTED FROM REMOTE
236 245 %if repo_instance.clone_uri:
237 246 <p class="discreet">
238 247 <i class="icon-code-fork"></i> ${_('Clone from')}
239 248 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
240 249 </p>
241 250 %endif
242 251
243 252 ## LOCKING STATUS
244 253 %if repo_instance.locked[0]:
245 254 <p class="locking_locked discreet">
246 255 <i class="icon-repo-lock"></i>
247 256 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
248 257 </p>
249 258 %elif repo_instance.enable_locking:
250 259 <p class="locking_unlocked discreet">
251 260 <i class="icon-repo-unlock"></i>
252 261 ${_('Repository not locked. Pull repository to lock it.')}
253 262 </p>
254 263 %endif
255 264
256 265 </div>
257 266 </%def>
258 267
259 268 <%def name="repo_menu(active=None)">
260 269 <%
261 270 def is_active(selected):
262 271 if selected == active:
263 272 return "active"
264 273 %>
265 274
266 275 <!--- REPO CONTEXT BAR -->
267 276 <div id="context-bar">
268 277 <div class="wrapper">
269 278
270 279 <div class="title">
271 280 ${self.repo_page_title(c.rhodecode_db_repo)}
272 281 </div>
273 282
274 283 <ul id="context-pages" class="navigation horizontal-list">
275 284 <li class="${is_active('summary')}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
276 285 <li class="${is_active('changelog')}"><a class="menulink" href="${h.route_path('repo_changelog', repo_name=c.repo_name)}"><div class="menulabel">${_('Changelog')}</div></a></li>
277 286 <li class="${is_active('files')}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_rev[1], f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
278 287 <li class="${is_active('compare')}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
279 288
280 289 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
281 290 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
282 291 <li class="${is_active('showpullrequest')}">
283 292 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
284 293 <div class="menulabel">
285 294 %if c.repository_pull_requests == 1:
286 295 ${c.repository_pull_requests} ${_('Pull Request')}
287 296 %else:
288 297 ${c.repository_pull_requests} ${_('Pull Requests')}
289 298 %endif
290 299 </div>
291 300 </a>
292 301 </li>
293 302 %endif
294 303
295 304 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
296 305 <li class="${is_active('settings')}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li>
297 306 %endif
298 307
299 308 <li class="${is_active('options')}">
300 309 <a class="menulink dropdown">
301 310 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
302 311 </a>
303 312 <ul class="submenu">
304 313
305 314 %if c.rhodecode_db_repo.fork:
306 315 <li>
307 316 <a title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
308 317 href="${h.route_path('repo_compare',
309 318 repo_name=c.rhodecode_db_repo.fork.repo_name,
310 319 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
311 320 source_ref=c.rhodecode_db_repo.landing_rev[1],
312 321 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
313 322 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
314 323 _query=dict(merge=1))}"
315 324 >
316 325 ${_('Compare fork')}
317 326 </a>
318 327 </li>
319 328 %endif
320 329
321 330 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
322 331 %if c.rhodecode_db_repo.locked[0]:
323 332 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
324 333 %else:
325 334 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock')}</a></li>
326 335 %endif
327 336 %endif
328 337 %if c.rhodecode_user.username != h.DEFAULT_USER:
329 338 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
330 339 <li><a href="${h.route_path('repo_fork_new',repo_name=c.repo_name)}">${_('Fork')}</a></li>
331 340 <li><a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
332 341 %endif
333 342 %endif
334 343 </ul>
335 344 </li>
336 345 </ul>
337 346 </div>
338 347 <div class="clear"></div>
339 348 </div>
340 349 % if c.rhodecode_db_repo.archived:
341 350 <div class="alert alert-warning text-center">
342 351 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
343 352 </div>
344 353 % endif
345 354 <!--- REPO END CONTEXT BAR -->
346 355
347 356 </%def>
348 357
349 358 <%def name="repo_group_page_title(repo_group_instance)">
350 359 <div class="title-content">
351 360 <div class="title-main">
352 361 ## Repository Group icon
353 362 <i class="icon-folder-close"></i>
354 363
355 364 ## repo name with group name
356 365 ${h.breadcrumb_repo_group_link(repo_group_instance)}
357 366 </div>
358 367
359 368 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
360 369 <div class="repo-group-desc discreet">
361 370 ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)}
362 371 </div>
363 372
364 373 </div>
365 374 </%def>
366 375
367 376 <%def name="repo_group_menu(active=None)">
368 377 <%
369 378 def is_active(selected):
370 379 if selected == active:
371 380 return "active"
372 381
373 382 gr_name = c.repo_group.group_name if c.repo_group else None
374 383 # create repositories with write permission on group is set to true
375 384 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
376 385 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
377 386 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
378 387
379 388 %>
380 389
381 390 <!--- REPO GROUP CONTEXT BAR -->
382 391 <div id="context-bar">
383 392 <div class="wrapper">
384 393 <div class="title">
385 394 ${self.repo_group_page_title(c.repo_group)}
386 395 </div>
387 396
388 397 <ul id="context-pages" class="navigation horizontal-list">
389 398 <li class="${is_active('home')}"><a class="menulink" href="${h.route_path('repo_group_home', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Group Home')}</div></a></li>
390 399 % if c.is_super_admin or group_admin:
391 400 <li class="${is_active('settings')}"><a class="menulink" href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}"><div class="menulabel">${_('Group Settings')}</div></a></li>
392 401 % endif
393 402
394 403 <li class="${is_active('options')}">
395 404 <a class="menulink dropdown">
396 405 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
397 406 </a>
398 407 <ul class="submenu">
399 408 %if c.is_super_admin or group_admin or (group_write and create_on_write):
400 409 <li><a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('Add Repository')}</a></li>
401 410 %endif
402 411 %if c.is_super_admin or group_admin:
403 412 <li><a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}">${_(u'Add Parent Group')}</a></li>
404 413 %endif
405 414 </ul>
406 415 </li>
407 416 </ul>
408 417 </div>
409 418 <div class="clear"></div>
410 419 </div>
411 420
412 421 <!--- REPO GROUP CONTEXT BAR -->
413 422
414 423 </%def>
415 424
416 425
417 426 <%def name="usermenu(active=False)">
418 427 ## USER MENU
419 428 <li id="quick_login_li" class="${'active' if active else ''}">
420 429 % if c.rhodecode_user.username == h.DEFAULT_USER:
421 430 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
422 431 ${gravatar(c.rhodecode_user.email, 20)}
423 432 <span class="user">
424 433 <span>${_('Sign in')}</span>
425 434 </span>
426 435 </a>
427 436 % else:
428 437 ## logged in user
429 438 <a id="quick_login_link" class="menulink childs">
430 439 ${gravatar(c.rhodecode_user.email, 20)}
431 440 <span class="user">
432 441 <span class="menu_link_user">${c.rhodecode_user.username}</span>
433 442 <div class="show_more"></div>
434 443 </span>
435 444 </a>
436 445 ## subnav with menu for logged in user
437 446 <div class="user-menu submenu">
438 447 <div id="quick_login">
439 448 %if c.rhodecode_user.username != h.DEFAULT_USER:
440 449 <div class="">
441 450 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
442 451 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
443 452 <div class="email">${c.rhodecode_user.email}</div>
444 453 </div>
445 454 <div class="">
446 455 <ol class="links">
447 456 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
448 457 % if c.rhodecode_user.personal_repo_group:
449 458 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
450 459 % endif
451 460 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
452 461 ## bookmark-items
453 462 <li class="bookmark-items">
454 463 ${_('Bookmarks')}
455 464 <div class="pull-right">
456 465 <a href="${h.route_path('my_account_bookmarks')}">${_('Manage')}</a>
457 466 </div>
458 467 </li>
459 468 % if not c.bookmark_items:
460 469 <li>
461 470 <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
462 471 </li>
463 472 % endif
464 473 % for item in c.bookmark_items:
465 474 <li>
466 475 % if item.repository:
467 476 <div>
468 477 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
469 478 <code>${item.position}</code>
470 479 % if item.repository.repo_type == 'hg':
471 480 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
472 481 % elif item.repository.repo_type == 'git':
473 482 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
474 483 % elif item.repository.repo_type == 'svn':
475 484 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
476 485 % endif
477 486 ${(item.title or h.shorter(item.repository.repo_name, 30))}
478 487 </a>
479 488 </div>
480 489 % elif item.repository_group:
481 490 <div>
482 491 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
483 492 <code>${item.position}</code>
484 493 <i class="icon-folder-close" title="${_('Repository group')}" style="font-size: 16px"></i>
485 494 ${(item.title or h.shorter(item.repository_group.group_name, 30))}
486 495 </a>
487 496 </div>
488 497 % else:
489 498 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
490 499 <code>${item.position}</code>
491 500 ${item.title}
492 501 </a>
493 502 % endif
494 503 </li>
495 504 % endfor
496 505
497 506 <li class="logout">
498 507 ${h.secure_form(h.route_path('logout'), request=request)}
499 508 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
500 509 ${h.end_form()}
501 510 </li>
502 511 </ol>
503 512 </div>
504 513 %endif
505 514 </div>
506 515 </div>
507 516 ## unread counter
508 517 <div class="pill_container">
509 518 <a class="menu_link_notifications ${'empty' if c.unread_notifications == 0 else ''}" href="${h.route_path('notifications_show_all')}">${c.unread_notifications}</a>
510 519 </div>
511 520 % endif
512 521 </li>
513 522 </%def>
514 523
515 524 <%def name="menu_items(active=None)">
516 525 <%
517 526 def is_active(selected):
518 527 if selected == active:
519 528 return "active"
520 529 return ""
521 530 %>
522 531
523 532 <ul id="quick" class="main_nav navigation horizontal-list">
524 533 ## notice box for important system messages
525 534 <li style="display: none">
526 535 <a class="notice-box" href="#openNotice" onclick="showNoticeBox(); return false">
527 536 <div class="menulabel-notice" >
528 537 0
529 538 </div>
530 539 </a>
531 540 </li>
532 541
533 542 ## Main filter
534 543 <li>
535 544 <div class="menulabel main_filter_box">
536 545 <div class="main_filter_input_box">
537 546 <ul class="searchItems">
538 547
539 548 % if c.template_context['search_context']['repo_id']:
540 549 <li class="searchTag searchTagFilter searchTagHidable" >
541 550 ##<a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">
542 551 <span class="tag">
543 552 This repo
544 553 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-delete"></i></a>
545 554 </span>
546 555 ##</a>
547 556 </li>
548 557 % elif c.template_context['search_context']['repo_group_id']:
549 558 <li class="searchTag searchTagFilter searchTagHidable">
550 559 ##<a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">
551 560 <span class="tag">
552 561 This group
553 562 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-delete"></i></a>
554 563 </span>
555 564 ##</a>
556 565 </li>
557 566 % endif
558 567
559 568 <li class="searchTagInput">
560 569 <input class="main_filter_input" id="main_filter" size="15" type="text" name="main_filter" placeholder="${_('search / go to...')}" value="" />
561 570 </li>
562 571 <li class="searchTag searchTagHelp">
563 572 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
564 573 </li>
565 574 </ul>
566 575 </div>
567 576 </div>
568 577
569 578 <div id="main_filter_help" style="display: none">
570 579 - Use '/' key to quickly access this field.
571 580
572 581 - Enter a name of repository, or repository group for quick search.
573 582
574 583 - Prefix query to allow special search:
575 584
576 585 user:admin, to search for usernames, always global
577 586
578 587 user_group:devops, to search for user groups, always global
579 588
580 589 commit:efced4, to search for commits, scoped to repositories or groups
581 590
582 591 file:models.py, to search for file paths, scoped to repositories or groups
583 592
584 593 % if c.template_context['search_context']['repo_id']:
585 594 For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a>
586 595 % elif c.template_context['search_context']['repo_group_id']:
587 596 For advanced full text search visit: <a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">repository group search</a>
588 597 % else:
589 598 For advanced full text search visit: <a href="${h.route_path('search')}">global search</a>
590 599 % endif
591 600 </div>
592 601 </li>
593 602
594 603 ## ROOT MENU
595 604 <li class="${is_active('home')}">
596 605 <a class="menulink" title="${_('Home')}" href="${h.route_path('home')}">
597 606 <div class="menulabel">${_('Home')}</div>
598 607 </a>
599 608 </li>
600 609
601 610 %if c.rhodecode_user.username != h.DEFAULT_USER:
602 611 <li class="${is_active('journal')}">
603 612 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
604 613 <div class="menulabel">${_('Journal')}</div>
605 614 </a>
606 615 </li>
607 616 %else:
608 617 <li class="${is_active('journal')}">
609 618 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
610 619 <div class="menulabel">${_('Public journal')}</div>
611 620 </a>
612 621 </li>
613 622 %endif
614 623
615 624 <li class="${is_active('gists')}">
616 625 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
617 626 <div class="menulabel">${_('Gists')}</div>
618 627 </a>
619 628 </li>
620 629
621 630 % if c.is_super_admin or c.is_delegated_admin:
622 631 <li class="${is_active('admin')}">
623 632 <a class="menulink childs" title="${_('Admin settings')}" href="${h.route_path('admin_home')}">
624 633 <div class="menulabel">${_('Admin')} </div>
625 634 </a>
626 635 </li>
627 636 % endif
628 637
629 638 ## render extra user menu
630 639 ${usermenu(active=(active=='my_account'))}
631 640
632 641 % if c.debug_style:
633 642 <li>
634 643 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
635 644 <div class="menulabel">${_('[Style]')}</div>
636 645 </a>
637 646 </li>
638 647 % endif
639 648 </ul>
640 649
641 650 <script type="text/javascript">
642 651 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
643 652
644 653 var formatRepoResult = function(result, container, query, escapeMarkup) {
645 654 return function(data, escapeMarkup) {
646 655 if (!data.repo_id){
647 656 return data.text; // optgroup text Repositories
648 657 }
649 658
650 659 var tmpl = '';
651 660 var repoType = data['repo_type'];
652 661 var repoName = data['text'];
653 662
654 663 if(data && data.type == 'repo'){
655 664 if(repoType === 'hg'){
656 665 tmpl += '<i class="icon-hg"></i> ';
657 666 }
658 667 else if(repoType === 'git'){
659 668 tmpl += '<i class="icon-git"></i> ';
660 669 }
661 670 else if(repoType === 'svn'){
662 671 tmpl += '<i class="icon-svn"></i> ';
663 672 }
664 673 if(data['private']){
665 674 tmpl += '<i class="icon-lock" ></i> ';
666 675 }
667 676 else if(visualShowPublicIcon){
668 677 tmpl += '<i class="icon-unlock-alt"></i> ';
669 678 }
670 679 }
671 680 tmpl += escapeMarkup(repoName);
672 681 return tmpl;
673 682
674 683 }(result, escapeMarkup);
675 684 };
676 685
677 686 var formatRepoGroupResult = function(result, container, query, escapeMarkup) {
678 687 return function(data, escapeMarkup) {
679 688 if (!data.repo_group_id){
680 689 return data.text; // optgroup text Repositories
681 690 }
682 691
683 692 var tmpl = '';
684 693 var repoGroupName = data['text'];
685 694
686 695 if(data){
687 696
688 697 tmpl += '<i class="icon-folder-close"></i> ';
689 698
690 699 }
691 700 tmpl += escapeMarkup(repoGroupName);
692 701 return tmpl;
693 702
694 703 }(result, escapeMarkup);
695 704 };
696 705
697 706 var escapeRegExChars = function (value) {
698 707 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
699 708 };
700 709
701 710 var getRepoIcon = function(repo_type) {
702 711 if (repo_type === 'hg') {
703 712 return '<i class="icon-hg"></i> ';
704 713 }
705 714 else if (repo_type === 'git') {
706 715 return '<i class="icon-git"></i> ';
707 716 }
708 717 else if (repo_type === 'svn') {
709 718 return '<i class="icon-svn"></i> ';
710 719 }
711 720 return ''
712 721 };
713 722
714 723 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
715 724
716 725 if (value.split(':').length === 2) {
717 726 value = value.split(':')[1]
718 727 }
719 728
720 729 var searchType = data['type'];
721 730 var valueDisplay = data['value_display'];
722 731
723 732 var pattern = '(' + escapeRegExChars(value) + ')';
724 733
725 734 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
726 735
727 736 // highlight match
728 737 if (searchType != 'text') {
729 738 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
730 739 }
731 740
732 741 var icon = '';
733 742
734 743 if (searchType === 'hint') {
735 744 icon += '<i class="icon-folder-close"></i> ';
736 745 }
737 746 // full text search
738 747 else if (searchType === 'search') {
739 748 icon += '<i class="icon-more"></i> ';
740 749 }
741 750 // repository
742 751 else if (searchType === 'repo') {
743 752
744 753 var repoIcon = getRepoIcon(data['repo_type']);
745 754 icon += repoIcon;
746 755
747 756 if (data['private']) {
748 757 icon += '<i class="icon-lock" ></i> ';
749 758 }
750 759 else if (visualShowPublicIcon) {
751 760 icon += '<i class="icon-unlock-alt"></i> ';
752 761 }
753 762 }
754 763 // repository groups
755 764 else if (searchType === 'repo_group') {
756 765 icon += '<i class="icon-folder-close"></i> ';
757 766 }
758 767 // user group
759 768 else if (searchType === 'user_group') {
760 769 icon += '<i class="icon-group"></i> ';
761 770 }
762 771 // user
763 772 else if (searchType === 'user') {
764 773 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
765 774 }
766 775 // commit
767 776 else if (searchType === 'commit') {
768 777 var repo_data = data['repo_data'];
769 778 var repoIcon = getRepoIcon(repo_data['repository_type']);
770 779 if (repoIcon) {
771 780 icon += repoIcon;
772 781 } else {
773 782 icon += '<i class="icon-tag"></i>';
774 783 }
775 784 }
776 785 // file
777 786 else if (searchType === 'file') {
778 787 var repo_data = data['repo_data'];
779 788 var repoIcon = getRepoIcon(repo_data['repository_type']);
780 789 if (repoIcon) {
781 790 icon += repoIcon;
782 791 } else {
783 792 icon += '<i class="icon-tag"></i>';
784 793 }
785 794 }
786 795 // generic text
787 796 else if (searchType === 'text') {
788 797 icon = '';
789 798 }
790 799
791 800 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
792 801 return tmpl.format(icon, valueDisplay);
793 802 };
794 803
795 804 var handleSelect = function(element, suggestion) {
796 805 if (suggestion.type === "hint") {
797 806 // we skip action
798 807 $('#main_filter').focus();
799 808 }
800 809 else if (suggestion.type === "text") {
801 810 // we skip action
802 811 $('#main_filter').focus();
803 812
804 813 } else {
805 814 window.location = suggestion['url'];
806 815 }
807 816 };
808 817
809 818 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
810 819 if (queryLowerCase.split(':').length === 2) {
811 820 queryLowerCase = queryLowerCase.split(':')[1]
812 821 }
813 822 if (suggestion.type === "text") {
814 823 // special case we don't want to "skip" display for
815 824 return true
816 825 }
817 826 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
818 827 };
819 828
820 829 var cleanContext = {
821 830 repo_view_type: null,
822 831
823 832 repo_id: null,
824 833 repo_name: "",
825 834
826 835 repo_group_id: null,
827 836 repo_group_name: null
828 837 };
829 838 var removeGoToFilter = function () {
830 839 $('.searchTagHidable').hide();
831 840 $('#main_filter').autocomplete(
832 841 'setOptions', {params:{search_context: cleanContext}});
833 842 };
834 843
835 844 $('#main_filter').autocomplete({
836 845 serviceUrl: pyroutes.url('goto_switcher_data'),
837 846 params: {
838 847 "search_context": templateContext.search_context
839 848 },
840 849 minChars:2,
841 850 maxHeight:400,
842 851 deferRequestBy: 300, //miliseconds
843 852 tabDisabled: true,
844 853 autoSelectFirst: false,
845 854 formatResult: autocompleteMainFilterFormatResult,
846 855 lookupFilter: autocompleteMainFilterResult,
847 856 onSelect: function (element, suggestion) {
848 857 handleSelect(element, suggestion);
849 858 return false;
850 859 },
851 860 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
852 861 if (jqXHR !== 'abort') {
853 862 alert("Error during search.\nError code: {0}".format(textStatus));
854 863 window.location = '';
855 864 }
856 865 }
857 866 });
858 867
859 868 showMainFilterBox = function () {
860 869 $('#main_filter_help').toggle();
861 870 };
862 871
863 872 $('#main_filter').on('keydown.autocomplete', function (e) {
864 873
865 874 var BACKSPACE = 8;
866 875 var el = $(e.currentTarget);
867 876 if(e.which === BACKSPACE){
868 877 var inputVal = el.val();
869 878 if (inputVal === ""){
870 879 removeGoToFilter()
871 880 }
872 881 }
873 882 });
874 883
875 884 </script>
876 885 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
877 886 </%def>
878 887
879 888 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
880 889 <div class="modal-dialog">
881 890 <div class="modal-content">
882 891 <div class="modal-header">
883 892 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
884 893 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
885 894 </div>
886 895 <div class="modal-body">
887 896 <div class="block-left">
888 897 <table class="keyboard-mappings">
889 898 <tbody>
890 899 <tr>
891 900 <th></th>
892 901 <th>${_('Site-wide shortcuts')}</th>
893 902 </tr>
894 903 <%
895 904 elems = [
896 905 ('/', 'Use quick search box'),
897 906 ('g h', 'Goto home page'),
898 907 ('g g', 'Goto my private gists page'),
899 908 ('g G', 'Goto my public gists page'),
900 909 ('g 0-9', 'Goto bookmarked items from 0-9'),
901 910 ('n r', 'New repository page'),
902 911 ('n g', 'New gist page'),
903 912 ]
904 913 %>
905 914 %for key, desc in elems:
906 915 <tr>
907 916 <td class="keys">
908 917 <span class="key tag">${key}</span>
909 918 </td>
910 919 <td>${desc}</td>
911 920 </tr>
912 921 %endfor
913 922 </tbody>
914 923 </table>
915 924 </div>
916 925 <div class="block-left">
917 926 <table class="keyboard-mappings">
918 927 <tbody>
919 928 <tr>
920 929 <th></th>
921 930 <th>${_('Repositories')}</th>
922 931 </tr>
923 932 <%
924 933 elems = [
925 934 ('g s', 'Goto summary page'),
926 935 ('g c', 'Goto changelog page'),
927 936 ('g f', 'Goto files page'),
928 937 ('g F', 'Goto files page with file search activated'),
929 938 ('g p', 'Goto pull requests page'),
930 939 ('g o', 'Goto repository settings'),
931 940 ('g O', 'Goto repository permissions settings'),
932 941 ]
933 942 %>
934 943 %for key, desc in elems:
935 944 <tr>
936 945 <td class="keys">
937 946 <span class="key tag">${key}</span>
938 947 </td>
939 948 <td>${desc}</td>
940 949 </tr>
941 950 %endfor
942 951 </tbody>
943 952 </table>
944 953 </div>
945 954 </div>
946 955 <div class="modal-footer">
947 956 </div>
948 957 </div><!-- /.modal-content -->
949 958 </div><!-- /.modal-dialog -->
950 959 </div><!-- /.modal -->
951 960
General Comments 0
You need to be logged in to leave comments. Login now