##// END OF EJS Templates
search: new UI for search, and repo group context search...
dan -
r3442:3bc8f801 default
parent child Browse files
Show More
@@ -1,687 +1,694 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, 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 return c
172 172
173 173 def _get_template_context(self, tmpl_args, **kwargs):
174 174
175 175 local_tmpl_args = {
176 176 'defaults': {},
177 177 'errors': {},
178 178 'c': tmpl_args
179 179 }
180 180 local_tmpl_args.update(kwargs)
181 181 return local_tmpl_args
182 182
183 183 def load_default_context(self):
184 184 """
185 185 example:
186 186
187 187 def load_default_context(self):
188 188 c = self._get_local_tmpl_context()
189 189 c.custom_var = 'foobar'
190 190
191 191 return c
192 192 """
193 193 raise NotImplementedError('Needs implementation in view class')
194 194
195 195
196 196 class RepoAppView(BaseAppView):
197 197
198 198 def __init__(self, context, request):
199 199 super(RepoAppView, self).__init__(context, request)
200 200 self.db_repo = request.db_repo
201 201 self.db_repo_name = self.db_repo.repo_name
202 202 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
203 203
204 204 def _handle_missing_requirements(self, error):
205 205 log.error(
206 206 'Requirements are missing for repository %s: %s',
207 207 self.db_repo_name, safe_unicode(error))
208 208
209 209 def _get_local_tmpl_context(self, include_app_defaults=True):
210 210 _ = self.request.translate
211 211 c = super(RepoAppView, self)._get_local_tmpl_context(
212 212 include_app_defaults=include_app_defaults)
213 213
214 214 # register common vars for this type of view
215 215 c.rhodecode_db_repo = self.db_repo
216 216 c.repo_name = self.db_repo_name
217 217 c.repository_pull_requests = self.db_repo_pull_requests
218 218 self.path_filter = PathFilter(None)
219 219
220 220 c.repository_requirements_missing = {}
221 221 try:
222 222 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
223 223 if self.rhodecode_vcs_repo:
224 224 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
225 225 c.auth_user.username)
226 226 self.path_filter = PathFilter(path_perms)
227 227 except RepositoryRequirementError as e:
228 228 c.repository_requirements_missing = {'error': str(e)}
229 229 self._handle_missing_requirements(e)
230 230 self.rhodecode_vcs_repo = None
231 231
232 232 c.path_filter = self.path_filter # used by atom_feed_entry.mako
233 233
234 234 if self.rhodecode_vcs_repo is None:
235 235 # unable to fetch this repo as vcs instance, report back to user
236 236 h.flash(_(
237 237 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
238 238 "Please check if it exist, or is not damaged.") %
239 239 {'repo_name': c.repo_name},
240 240 category='error', ignore_duplicate=True)
241 241 if c.repository_requirements_missing:
242 242 route = self.request.matched_route.name
243 243 if route.startswith(('edit_repo', 'repo_summary')):
244 244 # allow summary and edit repo on missing requirements
245 245 return c
246 246
247 247 raise HTTPFound(
248 248 h.route_path('repo_summary', repo_name=self.db_repo_name))
249 249
250 250 else: # redirect if we don't show missing requirements
251 251 raise HTTPFound(h.route_path('home'))
252 252
253 253 c.has_origin_repo_read_perm = False
254 254 if self.db_repo.fork:
255 255 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
256 256 'repository.write', 'repository.read', 'repository.admin')(
257 257 self.db_repo.fork.repo_name, 'summary fork link')
258 258
259 259 return c
260 260
261 261 def _get_f_path_unchecked(self, matchdict, default=None):
262 262 """
263 263 Should only be used by redirects, everything else should call _get_f_path
264 264 """
265 265 f_path = matchdict.get('f_path')
266 266 if f_path:
267 267 # fix for multiple initial slashes that causes errors for GIT
268 268 return f_path.lstrip('/')
269 269
270 270 return default
271 271
272 272 def _get_f_path(self, matchdict, default=None):
273 273 f_path_match = self._get_f_path_unchecked(matchdict, default)
274 274 return self.path_filter.assert_path_permissions(f_path_match)
275 275
276 276 def _get_general_setting(self, target_repo, settings_key, default=False):
277 277 settings_model = VcsSettingsModel(repo=target_repo)
278 278 settings = settings_model.get_general_settings()
279 279 return settings.get(settings_key, default)
280 280
281 281
282 282 class PathFilter(object):
283 283
284 284 # Expects and instance of BasePathPermissionChecker or None
285 285 def __init__(self, permission_checker):
286 286 self.permission_checker = permission_checker
287 287
288 288 def assert_path_permissions(self, path):
289 289 if path and self.permission_checker and not self.permission_checker.has_access(path):
290 290 raise HTTPForbidden()
291 291 return path
292 292
293 293 def filter_patchset(self, patchset):
294 294 if not self.permission_checker or not patchset:
295 295 return patchset, False
296 296 had_filtered = False
297 297 filtered_patchset = []
298 298 for patch in patchset:
299 299 filename = patch.get('filename', None)
300 300 if not filename or self.permission_checker.has_access(filename):
301 301 filtered_patchset.append(patch)
302 302 else:
303 303 had_filtered = True
304 304 if had_filtered:
305 305 if isinstance(patchset, diffs.LimitedDiffContainer):
306 306 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
307 307 return filtered_patchset, True
308 308 else:
309 309 return patchset, False
310 310
311 311 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
312 312 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
313 313 result = diffset.render_patchset(
314 314 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
315 315 result.has_hidden_changes = has_hidden_changes
316 316 return result
317 317
318 318 def get_raw_patch(self, diff_processor):
319 319 if self.permission_checker is None:
320 320 return diff_processor.as_raw()
321 321 elif self.permission_checker.has_full_access:
322 322 return diff_processor.as_raw()
323 323 else:
324 324 return '# Repository has user-specific filters, raw patch generation is disabled.'
325 325
326 326 @property
327 327 def is_enabled(self):
328 328 return self.permission_checker is not None
329 329
330 330
331 331 class RepoGroupAppView(BaseAppView):
332 332 def __init__(self, context, request):
333 333 super(RepoGroupAppView, self).__init__(context, request)
334 334 self.db_repo_group = request.db_repo_group
335 335 self.db_repo_group_name = self.db_repo_group.group_name
336 336
337 def _get_local_tmpl_context(self, include_app_defaults=True):
338 _ = self.request.translate
339 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
340 include_app_defaults=include_app_defaults)
341 c.repo_group = self.db_repo_group
342 return c
343
337 344 def _revoke_perms_on_yourself(self, form_result):
338 345 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
339 346 form_result['perm_updates'])
340 347 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
341 348 form_result['perm_additions'])
342 349 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
343 350 form_result['perm_deletions'])
344 351 admin_perm = 'group.admin'
345 352 if _updates and _updates[0][1] != admin_perm or \
346 353 _additions and _additions[0][1] != admin_perm or \
347 354 _deletions and _deletions[0][1] != admin_perm:
348 355 return True
349 356 return False
350 357
351 358
352 359 class UserGroupAppView(BaseAppView):
353 360 def __init__(self, context, request):
354 361 super(UserGroupAppView, self).__init__(context, request)
355 362 self.db_user_group = request.db_user_group
356 363 self.db_user_group_name = self.db_user_group.users_group_name
357 364
358 365
359 366 class UserAppView(BaseAppView):
360 367 def __init__(self, context, request):
361 368 super(UserAppView, self).__init__(context, request)
362 369 self.db_user = request.db_user
363 370 self.db_user_id = self.db_user.user_id
364 371
365 372 _ = self.request.translate
366 373 if not request.db_user_supports_default:
367 374 if self.db_user.username == User.DEFAULT_USER:
368 375 h.flash(_("Editing user `{}` is disabled.".format(
369 376 User.DEFAULT_USER)), category='warning')
370 377 raise HTTPFound(h.route_path('users'))
371 378
372 379
373 380 class DataGridAppView(object):
374 381 """
375 382 Common class to have re-usable grid rendering components
376 383 """
377 384
378 385 def _extract_ordering(self, request, column_map=None):
379 386 column_map = column_map or {}
380 387 column_index = safe_int(request.GET.get('order[0][column]'))
381 388 order_dir = request.GET.get(
382 389 'order[0][dir]', 'desc')
383 390 order_by = request.GET.get(
384 391 'columns[%s][data][sort]' % column_index, 'name_raw')
385 392
386 393 # translate datatable to DB columns
387 394 order_by = column_map.get(order_by) or order_by
388 395
389 396 search_q = request.GET.get('search[value]')
390 397 return search_q, order_by, order_dir
391 398
392 399 def _extract_chunk(self, request):
393 400 start = safe_int(request.GET.get('start'), 0)
394 401 length = safe_int(request.GET.get('length'), 25)
395 402 draw = safe_int(request.GET.get('draw'))
396 403 return draw, start, length
397 404
398 405 def _get_order_col(self, order_by, model):
399 406 if isinstance(order_by, compat.string_types):
400 407 try:
401 408 return operator.attrgetter(order_by)(model)
402 409 except AttributeError:
403 410 return None
404 411 else:
405 412 return order_by
406 413
407 414
408 415 class BaseReferencesView(RepoAppView):
409 416 """
410 417 Base for reference view for branches, tags and bookmarks.
411 418 """
412 419 def load_default_context(self):
413 420 c = self._get_local_tmpl_context()
414 421
415 422
416 423 return c
417 424
418 425 def load_refs_context(self, ref_items, partials_template):
419 426 _render = self.request.get_partial_renderer(partials_template)
420 427 pre_load = ["author", "date", "message"]
421 428
422 429 is_svn = h.is_svn(self.rhodecode_vcs_repo)
423 430 is_hg = h.is_hg(self.rhodecode_vcs_repo)
424 431
425 432 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
426 433
427 434 closed_refs = {}
428 435 if is_hg:
429 436 closed_refs = self.rhodecode_vcs_repo.branches_closed
430 437
431 438 data = []
432 439 for ref_name, commit_id in ref_items:
433 440 commit = self.rhodecode_vcs_repo.get_commit(
434 441 commit_id=commit_id, pre_load=pre_load)
435 442 closed = ref_name in closed_refs
436 443
437 444 # TODO: johbo: Unify generation of reference links
438 445 use_commit_id = '/' in ref_name or is_svn
439 446
440 447 if use_commit_id:
441 448 files_url = h.route_path(
442 449 'repo_files',
443 450 repo_name=self.db_repo_name,
444 451 f_path=ref_name if is_svn else '',
445 452 commit_id=commit_id)
446 453
447 454 else:
448 455 files_url = h.route_path(
449 456 'repo_files',
450 457 repo_name=self.db_repo_name,
451 458 f_path=ref_name if is_svn else '',
452 459 commit_id=ref_name,
453 460 _query=dict(at=ref_name))
454 461
455 462 data.append({
456 463 "name": _render('name', ref_name, files_url, closed),
457 464 "name_raw": ref_name,
458 465 "date": _render('date', commit.date),
459 466 "date_raw": datetime_to_time(commit.date),
460 467 "author": _render('author', commit.author),
461 468 "commit": _render(
462 469 'commit', commit.message, commit.raw_id, commit.idx),
463 470 "commit_raw": commit.idx,
464 471 "compare": _render(
465 472 'compare', format_ref_id(ref_name, commit.raw_id)),
466 473 })
467 474
468 475 return data
469 476
470 477
471 478 class RepoRoutePredicate(object):
472 479 def __init__(self, val, config):
473 480 self.val = val
474 481
475 482 def text(self):
476 483 return 'repo_route = %s' % self.val
477 484
478 485 phash = text
479 486
480 487 def __call__(self, info, request):
481 488 if hasattr(request, 'vcs_call'):
482 489 # skip vcs calls
483 490 return
484 491
485 492 repo_name = info['match']['repo_name']
486 493 repo_model = repo.RepoModel()
487 494
488 495 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
489 496
490 497 def redirect_if_creating(route_info, db_repo):
491 498 skip_views = ['edit_repo_advanced_delete']
492 499 route = route_info['route']
493 500 # we should skip delete view so we can actually "remove" repositories
494 501 # if they get stuck in creating state.
495 502 if route.name in skip_views:
496 503 return
497 504
498 505 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
499 506 repo_creating_url = request.route_path(
500 507 'repo_creating', repo_name=db_repo.repo_name)
501 508 raise HTTPFound(repo_creating_url)
502 509
503 510 if by_name_match:
504 511 # register this as request object we can re-use later
505 512 request.db_repo = by_name_match
506 513 redirect_if_creating(info, by_name_match)
507 514 return True
508 515
509 516 by_id_match = repo_model.get_repo_by_id(repo_name)
510 517 if by_id_match:
511 518 request.db_repo = by_id_match
512 519 redirect_if_creating(info, by_id_match)
513 520 return True
514 521
515 522 return False
516 523
517 524
518 525 class RepoForbidArchivedRoutePredicate(object):
519 526 def __init__(self, val, config):
520 527 self.val = val
521 528
522 529 def text(self):
523 530 return 'repo_forbid_archived = %s' % self.val
524 531
525 532 phash = text
526 533
527 534 def __call__(self, info, request):
528 535 _ = request.translate
529 536 rhodecode_db_repo = request.db_repo
530 537
531 538 log.debug(
532 539 '%s checking if archived flag for repo for %s',
533 540 self.__class__.__name__, rhodecode_db_repo.repo_name)
534 541
535 542 if rhodecode_db_repo.archived:
536 543 log.warning('Current view is not supported for archived repo:%s',
537 544 rhodecode_db_repo.repo_name)
538 545
539 546 h.flash(
540 547 h.literal(_('Action not supported for archived repository.')),
541 548 category='warning')
542 549 summary_url = request.route_path(
543 550 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
544 551 raise HTTPFound(summary_url)
545 552 return True
546 553
547 554
548 555 class RepoTypeRoutePredicate(object):
549 556 def __init__(self, val, config):
550 557 self.val = val or ['hg', 'git', 'svn']
551 558
552 559 def text(self):
553 560 return 'repo_accepted_type = %s' % self.val
554 561
555 562 phash = text
556 563
557 564 def __call__(self, info, request):
558 565 if hasattr(request, 'vcs_call'):
559 566 # skip vcs calls
560 567 return
561 568
562 569 rhodecode_db_repo = request.db_repo
563 570
564 571 log.debug(
565 572 '%s checking repo type for %s in %s',
566 573 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
567 574
568 575 if rhodecode_db_repo.repo_type in self.val:
569 576 return True
570 577 else:
571 578 log.warning('Current view is not supported for repo type:%s',
572 579 rhodecode_db_repo.repo_type)
573 580 return False
574 581
575 582
576 583 class RepoGroupRoutePredicate(object):
577 584 def __init__(self, val, config):
578 585 self.val = val
579 586
580 587 def text(self):
581 588 return 'repo_group_route = %s' % self.val
582 589
583 590 phash = text
584 591
585 592 def __call__(self, info, request):
586 593 if hasattr(request, 'vcs_call'):
587 594 # skip vcs calls
588 595 return
589 596
590 597 repo_group_name = info['match']['repo_group_name']
591 598 repo_group_model = repo_group.RepoGroupModel()
592 599 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
593 600
594 601 if by_name_match:
595 602 # register this as request object we can re-use later
596 603 request.db_repo_group = by_name_match
597 604 return True
598 605
599 606 return False
600 607
601 608
602 609 class UserGroupRoutePredicate(object):
603 610 def __init__(self, val, config):
604 611 self.val = val
605 612
606 613 def text(self):
607 614 return 'user_group_route = %s' % self.val
608 615
609 616 phash = text
610 617
611 618 def __call__(self, info, request):
612 619 if hasattr(request, 'vcs_call'):
613 620 # skip vcs calls
614 621 return
615 622
616 623 user_group_id = info['match']['user_group_id']
617 624 user_group_model = user_group.UserGroup()
618 625 by_id_match = user_group_model.get(user_group_id, cache=False)
619 626
620 627 if by_id_match:
621 628 # register this as request object we can re-use later
622 629 request.db_user_group = by_id_match
623 630 return True
624 631
625 632 return False
626 633
627 634
628 635 class UserRoutePredicateBase(object):
629 636 supports_default = None
630 637
631 638 def __init__(self, val, config):
632 639 self.val = val
633 640
634 641 def text(self):
635 642 raise NotImplementedError()
636 643
637 644 def __call__(self, info, request):
638 645 if hasattr(request, 'vcs_call'):
639 646 # skip vcs calls
640 647 return
641 648
642 649 user_id = info['match']['user_id']
643 650 user_model = user.User()
644 651 by_id_match = user_model.get(user_id, cache=False)
645 652
646 653 if by_id_match:
647 654 # register this as request object we can re-use later
648 655 request.db_user = by_id_match
649 656 request.db_user_supports_default = self.supports_default
650 657 return True
651 658
652 659 return False
653 660
654 661
655 662 class UserRoutePredicate(UserRoutePredicateBase):
656 663 supports_default = False
657 664
658 665 def text(self):
659 666 return 'user_route = %s' % self.val
660 667
661 668 phash = text
662 669
663 670
664 671 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
665 672 supports_default = True
666 673
667 674 def text(self):
668 675 return 'user_with_default_route = %s' % self.val
669 676
670 677 phash = text
671 678
672 679
673 680 def includeme(config):
674 681 config.add_route_predicate(
675 682 'repo_route', RepoRoutePredicate)
676 683 config.add_route_predicate(
677 684 'repo_accepted_types', RepoTypeRoutePredicate)
678 685 config.add_route_predicate(
679 686 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
680 687 config.add_route_predicate(
681 688 'repo_group_route', RepoGroupRoutePredicate)
682 689 config.add_route_predicate(
683 690 'user_group_route', UserGroupRoutePredicate)
684 691 config.add_route_predicate(
685 692 'user_route_with_default', UserRouteWithDefaultPredicate)
686 693 config.add_route_predicate(
687 694 'user_route', UserRoutePredicate)
@@ -1,587 +1,596 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 re
22 22 import logging
23 23 import collections
24 24
25 25 from pyramid.view import view_config
26 26
27 27 from rhodecode.apps._base import BaseAppView
28 28 from rhodecode.lib import helpers as h
29 29 from rhodecode.lib.auth import (
30 30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator,
31 31 CSRFRequired)
32 32 from rhodecode.lib.index import searcher_from_config
33 33 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
34 34 from rhodecode.lib.ext_json import json
35 35 from rhodecode.model.db import (
36 36 func, true, or_, in_filter_generator, Repository, RepoGroup, User, UserGroup)
37 37 from rhodecode.model.repo import RepoModel
38 38 from rhodecode.model.repo_group import RepoGroupModel
39 39 from rhodecode.model.scm import RepoGroupList, RepoList
40 40 from rhodecode.model.user import UserModel
41 41 from rhodecode.model.user_group import UserGroupModel
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 class HomeView(BaseAppView):
47 47
48 48 def load_default_context(self):
49 49 c = self._get_local_tmpl_context()
50 50 c.user = c.auth_user.get_instance()
51 51
52 52 return c
53 53
54 54 @LoginRequired()
55 55 @view_config(
56 56 route_name='user_autocomplete_data', request_method='GET',
57 57 renderer='json_ext', xhr=True)
58 58 def user_autocomplete_data(self):
59 59 self.load_default_context()
60 60 query = self.request.GET.get('query')
61 61 active = str2bool(self.request.GET.get('active') or True)
62 62 include_groups = str2bool(self.request.GET.get('user_groups'))
63 63 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
64 64 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
65 65
66 66 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
67 67 query, active, include_groups)
68 68
69 69 _users = UserModel().get_users(
70 70 name_contains=query, only_active=active)
71 71
72 72 def maybe_skip_default_user(usr):
73 73 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
74 74 return False
75 75 return True
76 76 _users = filter(maybe_skip_default_user, _users)
77 77
78 78 if include_groups:
79 79 # extend with user groups
80 80 _user_groups = UserGroupModel().get_user_groups(
81 81 name_contains=query, only_active=active,
82 82 expand_groups=expand_groups)
83 83 _users = _users + _user_groups
84 84
85 85 return {'suggestions': _users}
86 86
87 87 @LoginRequired()
88 88 @NotAnonymous()
89 89 @view_config(
90 90 route_name='user_group_autocomplete_data', request_method='GET',
91 91 renderer='json_ext', xhr=True)
92 92 def user_group_autocomplete_data(self):
93 93 self.load_default_context()
94 94 query = self.request.GET.get('query')
95 95 active = str2bool(self.request.GET.get('active') or True)
96 96 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
97 97
98 98 log.debug('generating user group list, query:%s, active:%s',
99 99 query, active)
100 100
101 101 _user_groups = UserGroupModel().get_user_groups(
102 102 name_contains=query, only_active=active,
103 103 expand_groups=expand_groups)
104 104 _user_groups = _user_groups
105 105
106 106 return {'suggestions': _user_groups}
107 107
108 108 def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
109 109 org_query = name_contains
110 110 allowed_ids = self._rhodecode_user.repo_acl_ids(
111 111 ['repository.read', 'repository.write', 'repository.admin'],
112 112 cache=False, name_filter=name_contains) or [-1]
113 113
114 114 query = Repository.query()\
115 115 .order_by(func.length(Repository.repo_name))\
116 116 .order_by(Repository.repo_name)\
117 117 .filter(Repository.archived.isnot(true()))\
118 118 .filter(or_(
119 119 # generate multiple IN to fix limitation problems
120 120 *in_filter_generator(Repository.repo_id, allowed_ids)
121 121 ))
122 122
123 123 if repo_type:
124 124 query = query.filter(Repository.repo_type == repo_type)
125 125
126 126 if name_contains:
127 127 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
128 128 query = query.filter(
129 129 Repository.repo_name.ilike(ilike_expression))
130 130 query = query.limit(limit)
131 131
132 132 acl_iter = query
133 133
134 134 return [
135 135 {
136 136 'id': obj.repo_name,
137 137 'value': org_query,
138 138 'value_display': obj.repo_name,
139 139 'text': obj.repo_name,
140 140 'type': 'repo',
141 141 'repo_id': obj.repo_id,
142 142 'repo_type': obj.repo_type,
143 143 'private': obj.private,
144 144 'url': h.route_path('repo_summary', repo_name=obj.repo_name)
145 145 }
146 146 for obj in acl_iter]
147 147
148 148 def _get_repo_group_list(self, name_contains=None, limit=20):
149 149 org_query = name_contains
150 150 allowed_ids = self._rhodecode_user.repo_group_acl_ids(
151 151 ['group.read', 'group.write', 'group.admin'],
152 152 cache=False, name_filter=name_contains) or [-1]
153 153
154 154 query = RepoGroup.query()\
155 155 .order_by(func.length(RepoGroup.group_name))\
156 156 .order_by(RepoGroup.group_name) \
157 157 .filter(or_(
158 158 # generate multiple IN to fix limitation problems
159 159 *in_filter_generator(RepoGroup.group_id, allowed_ids)
160 160 ))
161 161
162 162 if name_contains:
163 163 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
164 164 query = query.filter(
165 165 RepoGroup.group_name.ilike(ilike_expression))
166 166 query = query.limit(limit)
167 167
168 168 acl_iter = query
169 169
170 170 return [
171 171 {
172 172 'id': obj.group_name,
173 173 'value': org_query,
174 174 'value_display': obj.group_name,
175 175 'text': obj.group_name,
176 176 'type': 'repo_group',
177 177 'repo_group_id': obj.group_id,
178 178 'url': h.route_path(
179 179 'repo_group_home', repo_group_name=obj.group_name)
180 180 }
181 181 for obj in acl_iter]
182 182
183 183 def _get_user_list(self, name_contains=None, limit=20):
184 184 org_query = name_contains
185 185 if not name_contains:
186 186 return []
187 187
188 188 name_contains = re.compile('(?:user:)(.+)').findall(name_contains)
189 189 if len(name_contains) != 1:
190 190 return []
191 191 name_contains = name_contains[0]
192 192
193 193 query = User.query()\
194 194 .order_by(func.length(User.username))\
195 195 .order_by(User.username) \
196 196 .filter(User.username != User.DEFAULT_USER)
197 197
198 198 if name_contains:
199 199 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
200 200 query = query.filter(
201 201 User.username.ilike(ilike_expression))
202 202 query = query.limit(limit)
203 203
204 204 acl_iter = query
205 205
206 206 return [
207 207 {
208 208 'id': obj.user_id,
209 209 'value': org_query,
210 210 'value_display': obj.username,
211 211 'type': 'user',
212 212 'icon_link': h.gravatar_url(obj.email, 30),
213 213 'url': h.route_path(
214 214 'user_profile', username=obj.username)
215 215 }
216 216 for obj in acl_iter]
217 217
218 218 def _get_user_groups_list(self, name_contains=None, limit=20):
219 219 org_query = name_contains
220 220 if not name_contains:
221 221 return []
222 222
223 223 name_contains = re.compile('(?:user_group:)(.+)').findall(name_contains)
224 224 if len(name_contains) != 1:
225 225 return []
226 226 name_contains = name_contains[0]
227 227
228 228 query = UserGroup.query()\
229 229 .order_by(func.length(UserGroup.users_group_name))\
230 230 .order_by(UserGroup.users_group_name)
231 231
232 232 if name_contains:
233 233 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
234 234 query = query.filter(
235 235 UserGroup.users_group_name.ilike(ilike_expression))
236 236 query = query.limit(limit)
237 237
238 238 acl_iter = query
239 239
240 240 return [
241 241 {
242 242 'id': obj.users_group_id,
243 243 'value': org_query,
244 244 'value_display': obj.users_group_name,
245 245 'type': 'user_group',
246 246 'url': h.route_path(
247 247 'user_group_profile', user_group_name=obj.users_group_name)
248 248 }
249 249 for obj in acl_iter]
250 250
251 251 def _get_hash_commit_list(self, auth_user, searcher, query):
252 252 org_query = query
253 253 if not query or len(query) < 3 or not searcher:
254 254 return []
255 255
256 256 commit_hashes = re.compile('(?:commit:)([0-9a-f]{2,40})').findall(query)
257 257
258 258 if len(commit_hashes) != 1:
259 259 return []
260 260 commit_hash = commit_hashes[0]
261 261
262 262 result = searcher.search(
263 263 'commit_id:{}*'.format(commit_hash), 'commit', auth_user,
264 264 raise_on_exc=False)
265 265
266 266 return [
267 267 {
268 268 'id': entry['commit_id'],
269 269 'value': org_query,
270 270 'value_display': 'repo `{}` commit: {}'.format(
271 271 entry['repository'], entry['commit_id']),
272 272 'type': 'commit',
273 273 'repo': entry['repository'],
274 274 'url': h.route_path(
275 275 'repo_commit',
276 276 repo_name=entry['repository'], commit_id=entry['commit_id'])
277 277 }
278 278 for entry in result['results']]
279 279
280 280 @LoginRequired()
281 281 @view_config(
282 282 route_name='repo_list_data', request_method='GET',
283 283 renderer='json_ext', xhr=True)
284 284 def repo_list_data(self):
285 285 _ = self.request.translate
286 286 self.load_default_context()
287 287
288 288 query = self.request.GET.get('query')
289 289 repo_type = self.request.GET.get('repo_type')
290 290 log.debug('generating repo list, query:%s, repo_type:%s',
291 291 query, repo_type)
292 292
293 293 res = []
294 294 repos = self._get_repo_list(query, repo_type=repo_type)
295 295 if repos:
296 296 res.append({
297 297 'text': _('Repositories'),
298 298 'children': repos
299 299 })
300 300
301 301 data = {
302 302 'more': False,
303 303 'results': res
304 304 }
305 305 return data
306 306
307 307 @LoginRequired()
308 308 @view_config(
309 309 route_name='repo_group_list_data', request_method='GET',
310 310 renderer='json_ext', xhr=True)
311 311 def repo_group_list_data(self):
312 312 _ = self.request.translate
313 313 self.load_default_context()
314 314
315 315 query = self.request.GET.get('query')
316 316
317 317 log.debug('generating repo group list, query:%s',
318 318 query)
319 319
320 320 res = []
321 321 repo_groups = self._get_repo_group_list(query)
322 322 if repo_groups:
323 323 res.append({
324 324 'text': _('Repository Groups'),
325 325 'children': repo_groups
326 326 })
327 327
328 328 data = {
329 329 'more': False,
330 330 'results': res
331 331 }
332 332 return data
333 333
334 334 def _get_default_search_queries(self, search_context, searcher, query):
335 335 if not searcher:
336 336 return []
337
337 338 is_es_6 = searcher.is_es_6
338 339
339 340 queries = []
340 341 repo_group_name, repo_name, repo_context = None, None, None
341 342
342 343 # repo group context
343 344 if search_context.get('search_context[repo_group_name]'):
344 345 repo_group_name = search_context.get('search_context[repo_group_name]')
345 346 if search_context.get('search_context[repo_name]'):
346 347 repo_name = search_context.get('search_context[repo_name]')
347 348 repo_context = search_context.get('search_context[repo_view_type]')
348 349
349 350 if is_es_6 and repo_name:
350 351 # files
351 352 def query_modifier():
352 353 qry = query
353 354 return {'q': qry, 'type': 'content'}
354 355 label = u'File search for `{}` in this repository.'.format(query)
355 356 queries.append(
356 357 {
357 358 'id': -10,
358 359 'value': query,
359 360 'value_display': label,
360 361 'type': 'search',
361 362 'url': h.route_path('search_repo',
362 363 repo_name=repo_name,
363 364 _query=query_modifier())
364 365 }
365 366 )
366 367
367 368 # commits
368 369 def query_modifier():
369 370 qry = query
370 371 return {'q': qry, 'type': 'commit'}
371 372
372 373 label = u'Commit search for `{}` in this repository.'.format(query)
373 374 queries.append(
374 375 {
375 376 'id': -10,
376 377 'value': query,
377 378 'value_display': label,
378 379 'type': 'search',
379 380 'url': h.route_path('search_repo',
380 381 repo_name=repo_name,
381 382 _query=query_modifier())
382 383 }
383 384 )
384 385
385 386 elif is_es_6 and repo_group_name:
386 387 # files
387 388 def query_modifier():
388 389 qry = query
389 390 return {'q': qry, 'type': 'content'}
390 391
391 392 label = u'File search for `{}` in this repository group'.format(query)
392 393 queries.append(
393 394 {
394 395 'id': -20,
395 396 'value': query,
396 397 'value_display': label,
397 398 'type': 'search',
398 399 'url': h.route_path('search_repo_group',
399 400 repo_group_name=repo_group_name,
400 401 _query=query_modifier())
401 402 }
402 403 )
403 404
404 405 # commits
405 406 def query_modifier():
406 407 qry = query
407 408 return {'q': qry, 'type': 'commit'}
408 409
409 410 label = u'Commit search for `{}` in this repository group'.format(query)
410 411 queries.append(
411 412 {
412 413 'id': -20,
413 414 'value': query,
414 415 'value_display': label,
415 416 'type': 'search',
416 417 'url': h.route_path('search_repo_group',
417 418 repo_group_name=repo_group_name,
418 419 _query=query_modifier())
419 420 }
420 421 )
421 422
422 423 if not queries:
423 424 queries.append(
424 425 {
425 426 'id': -1,
426 427 'value': query,
427 'value_display': u'Search for: `{}`'.format(query),
428 'value_display': u'Commit search for: `{}`'.format(query),
428 429 'type': 'search',
429 430 'url': h.route_path('search',
430 431 _query={'q': query, 'type': 'content'})
431 }
432 )
432 })
433 queries.append(
434 {
435 'id': -1,
436 'value': query,
437 'value_display': u'File search for: `{}`'.format(query),
438 'type': 'search',
439 'url': h.route_path('search',
440 _query={'q': query, 'type': 'commit'})
441 })
433 442
434 443 return queries
435 444
436 445 @LoginRequired()
437 446 @view_config(
438 447 route_name='goto_switcher_data', request_method='GET',
439 448 renderer='json_ext', xhr=True)
440 449 def goto_switcher_data(self):
441 450 c = self.load_default_context()
442 451
443 452 _ = self.request.translate
444 453
445 454 query = self.request.GET.get('query')
446 455 log.debug('generating main filter data, query %s', query)
447 456
448 457 res = []
449 458 if not query:
450 459 return {'suggestions': res}
451 460
452 461 searcher = searcher_from_config(self.request.registry.settings)
453 462 for _q in self._get_default_search_queries(self.request.GET, searcher, query):
454 463 res.append(_q)
455 464
456 465 repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]'))
457 466 if repo_group_id:
458 467 repo_group = RepoGroup.get(repo_group_id)
459 468 composed_hint = '{}/{}'.format(repo_group.group_name, query)
460 469 show_hint = not query.startswith(repo_group.group_name)
461 470 if repo_group and show_hint:
462 471 hint = u'Repository search inside: `{}`'.format(composed_hint)
463 472 res.append({
464 473 'id': -1,
465 474 'value': composed_hint,
466 475 'value_display': hint,
467 476 'type': 'hint',
468 477 'url': ""
469 478 })
470 479
471 480 repo_groups = self._get_repo_group_list(query)
472 481 for serialized_repo_group in repo_groups:
473 482 res.append(serialized_repo_group)
474 483
475 484 repos = self._get_repo_list(query)
476 485 for serialized_repo in repos:
477 486 res.append(serialized_repo)
478 487
479 488 # TODO(marcink): should all logged in users be allowed to search others?
480 489 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
481 490 if allowed_user_search:
482 491 users = self._get_user_list(query)
483 492 for serialized_user in users:
484 493 res.append(serialized_user)
485 494
486 495 user_groups = self._get_user_groups_list(query)
487 496 for serialized_user_group in user_groups:
488 497 res.append(serialized_user_group)
489 498
490 499 commits = self._get_hash_commit_list(c.auth_user, searcher, query)
491 500 if commits:
492 501 unique_repos = collections.OrderedDict()
493 502 for commit in commits:
494 503 repo_name = commit['repo']
495 504 unique_repos.setdefault(repo_name, []).append(commit)
496 505
497 506 for repo, commits in unique_repos.items():
498 507 for commit in commits:
499 508 res.append(commit)
500 509
501 510 return {'suggestions': res}
502 511
503 512 def _get_groups_and_repos(self, repo_group_id=None):
504 513 # repo groups groups
505 514 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
506 515 _perms = ['group.read', 'group.write', 'group.admin']
507 516 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
508 517 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
509 518 repo_group_list=repo_group_list_acl, admin=False)
510 519
511 520 # repositories
512 521 repo_list = Repository.get_all_repos(group_id=repo_group_id)
513 522 _perms = ['repository.read', 'repository.write', 'repository.admin']
514 523 repo_list_acl = RepoList(repo_list, perm_set=_perms)
515 524 repo_data = RepoModel().get_repos_as_dict(
516 525 repo_list=repo_list_acl, admin=False)
517 526
518 527 return repo_data, repo_group_data
519 528
520 529 @LoginRequired()
521 530 @view_config(
522 531 route_name='home', request_method='GET',
523 532 renderer='rhodecode:templates/index.mako')
524 533 def main_page(self):
525 534 c = self.load_default_context()
526 535 c.repo_group = None
527 536
528 537 repo_data, repo_group_data = self._get_groups_and_repos()
529 538 # json used to render the grids
530 539 c.repos_data = json.dumps(repo_data)
531 540 c.repo_groups_data = json.dumps(repo_group_data)
532 541
533 542 return self._get_template_context(c)
534 543
535 544 @LoginRequired()
536 545 @HasRepoGroupPermissionAnyDecorator(
537 546 'group.read', 'group.write', 'group.admin')
538 547 @view_config(
539 548 route_name='repo_group_home', request_method='GET',
540 549 renderer='rhodecode:templates/index_repo_group.mako')
541 550 @view_config(
542 551 route_name='repo_group_home_slash', request_method='GET',
543 552 renderer='rhodecode:templates/index_repo_group.mako')
544 553 def repo_group_main_page(self):
545 554 c = self.load_default_context()
546 555 c.repo_group = self.request.db_repo_group
547 556 repo_data, repo_group_data = self._get_groups_and_repos(
548 557 c.repo_group.group_id)
549 558
550 559 # json used to render the grids
551 560 c.repos_data = json.dumps(repo_data)
552 561 c.repo_groups_data = json.dumps(repo_group_data)
553 562
554 563 return self._get_template_context(c)
555 564
556 565 @LoginRequired()
557 566 @CSRFRequired()
558 567 @view_config(
559 568 route_name='markup_preview', request_method='POST',
560 569 renderer='string', xhr=True)
561 570 def markup_preview(self):
562 571 # Technically a CSRF token is not needed as no state changes with this
563 572 # call. However, as this is a POST is better to have it, so automated
564 573 # tools don't flag it as potential CSRF.
565 574 # Post is required because the payload could be bigger than the maximum
566 575 # allowed by GET.
567 576
568 577 text = self.request.POST.get('text')
569 578 renderer = self.request.POST.get('renderer') or 'rst'
570 579 if text:
571 580 return h.render(text, renderer=renderer, mentions=True)
572 581 return ''
573 582
574 583 @LoginRequired()
575 584 @CSRFRequired()
576 585 @view_config(
577 586 route_name='store_user_session_value', request_method='POST',
578 587 renderer='string', xhr=True)
579 588 def store_user_session_attr(self):
580 589 key = self.request.POST.get('key')
581 590 val = self.request.POST.get('val')
582 591
583 592 existing_value = self.request.session.get(key)
584 593 if existing_value != val:
585 594 self.request.session[key] = val
586 595
587 596 return 'stored:{}'.format(key)
@@ -1,161 +1,164 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 urllib
23 23 from pyramid.view import view_config
24 24 from webhelpers.util import update_params
25 25
26 26 from rhodecode.apps._base import BaseAppView, RepoAppView, RepoGroupAppView
27 27 from rhodecode.lib.auth import (
28 28 LoginRequired, HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator)
29 29 from rhodecode.lib.helpers import Page
30 30 from rhodecode.lib.utils2 import safe_str
31 31 from rhodecode.lib.index import searcher_from_config
32 32 from rhodecode.model import validation_schema
33 33 from rhodecode.model.validation_schema.schemas import search_schema
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 def perform_search(request, tmpl_context, repo_name=None, repo_group_name=None):
39 39 searcher = searcher_from_config(request.registry.settings)
40 40 formatted_results = []
41 41 execution_time = ''
42 42
43 43 schema = search_schema.SearchParamsSchema()
44
44 search_tags = []
45 45 search_params = {}
46 46 errors = []
47 47 try:
48 48 search_params = schema.deserialize(
49 49 dict(
50 50 search_query=request.GET.get('q'),
51 51 search_type=request.GET.get('type'),
52 52 search_sort=request.GET.get('sort'),
53 53 search_max_lines=request.GET.get('max_lines'),
54 54 page_limit=request.GET.get('page_limit'),
55 55 requested_page=request.GET.get('page'),
56 56 )
57 57 )
58 58 except validation_schema.Invalid as e:
59 59 errors = e.children
60 60
61 61 def url_generator(**kw):
62 62 q = urllib.quote(safe_str(search_query))
63 63 return update_params(
64 64 "?q=%s&type=%s&max_lines=%s" % (
65 65 q, safe_str(search_type), search_max_lines), **kw)
66 66
67 67 c = tmpl_context
68 68 search_query = search_params.get('search_query')
69 69 search_type = search_params.get('search_type')
70 70 search_sort = search_params.get('search_sort')
71 71 search_max_lines = search_params.get('search_max_lines')
72 72 if search_params.get('search_query'):
73 73 page_limit = search_params['page_limit']
74 74 requested_page = search_params['requested_page']
75 75
76 76 try:
77 77 search_result = searcher.search(
78 78 search_query, search_type, c.auth_user, repo_name, repo_group_name,
79 79 requested_page=requested_page, page_limit=page_limit, sort=search_sort)
80 80
81 81 formatted_results = Page(
82 82 search_result['results'], page=requested_page,
83 83 item_count=search_result['count'],
84 84 items_per_page=page_limit, url=url_generator)
85 85 finally:
86 86 searcher.cleanup()
87 87
88 search_tags = searcher.extract_search_tags(search_query)
89
88 90 if not search_result['error']:
89 91 execution_time = '%s results (%.3f seconds)' % (
90 92 search_result['count'],
91 93 search_result['runtime'])
92 94 elif not errors:
93 95 node = schema['search_query']
94 96 errors = [
95 97 validation_schema.Invalid(node, search_result['error'])]
96 98
97 99 c.perm_user = c.auth_user
98 100 c.repo_name = repo_name
99 101 c.repo_group_name = repo_group_name
100 102 c.sort = search_sort
101 103 c.url_generator = url_generator
102 104 c.errors = errors
103 105 c.formatted_results = formatted_results
104 106 c.runtime = execution_time
105 107 c.cur_query = search_query
106 108 c.search_type = search_type
107 109 c.searcher = searcher
110 c.search_tags = search_tags
108 111
109 112
110 113 class SearchView(BaseAppView):
111 114 def load_default_context(self):
112 115 c = self._get_local_tmpl_context()
113 116 return c
114 117
115 118 @LoginRequired()
116 119 @view_config(
117 120 route_name='search', request_method='GET',
118 121 renderer='rhodecode:templates/search/search.mako')
119 122 def search(self):
120 123 c = self.load_default_context()
121 124 perform_search(self.request, c)
122 125 return self._get_template_context(c)
123 126
124 127
125 128 class SearchRepoView(RepoAppView):
126 129 def load_default_context(self):
127 130 c = self._get_local_tmpl_context()
128 131 c.active = 'search'
129 132 return c
130 133
131 134 @LoginRequired()
132 135 @HasRepoPermissionAnyDecorator(
133 136 'repository.read', 'repository.write', 'repository.admin')
134 137 @view_config(
135 138 route_name='search_repo', request_method='GET',
136 139 renderer='rhodecode:templates/search/search.mako')
137 140 @view_config(
138 141 route_name='search_repo_alt', request_method='GET',
139 142 renderer='rhodecode:templates/search/search.mako')
140 143 def search_repo(self):
141 144 c = self.load_default_context()
142 145 perform_search(self.request, c, repo_name=self.db_repo_name)
143 146 return self._get_template_context(c)
144 147
145 148
146 149 class SearchRepoGroupView(RepoGroupAppView):
147 150 def load_default_context(self):
148 151 c = self._get_local_tmpl_context()
149 152 c.active = 'search'
150 153 return c
151 154
152 155 @LoginRequired()
153 156 @HasRepoGroupPermissionAnyDecorator(
154 157 'group.read', 'group.write', 'group.admin')
155 158 @view_config(
156 159 route_name='search_repo_group', request_method='GET',
157 160 renderer='rhodecode:templates/search/search.mako')
158 161 def search_repo_group(self):
159 162 c = self.load_default_context()
160 163 perform_search(self.request, c, repo_group_name=self.db_repo_group_name)
161 164 return self._get_template_context(c)
@@ -1,2020 +1,2042 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 Helper functions
23 23
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 27
28 28 import os
29 29 import random
30 30 import hashlib
31 31 import StringIO
32 32 import textwrap
33 33 import urllib
34 34 import math
35 35 import logging
36 36 import re
37 37 import time
38 38 import string
39 39 import hashlib
40 40 from collections import OrderedDict
41 41
42 42 import pygments
43 43 import itertools
44 44 import fnmatch
45 45 import bleach
46 46
47 47 from pyramid import compat
48 48 from datetime import datetime
49 49 from functools import partial
50 50 from pygments.formatters.html import HtmlFormatter
51 51 from pygments.lexers import (
52 52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53 53
54 54 from pyramid.threadlocal import get_current_request
55 55
56 56 from webhelpers.html import literal, HTML, escape
57 57 from webhelpers.html.tools import *
58 58 from webhelpers.html.builder import make_tag
59 59 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
60 60 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
61 61 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
62 62 submit, text, password, textarea, title, ul, xml_declaration, radio
63 63 from webhelpers.html.tools import auto_link, button_to, highlight, \
64 64 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
65 65 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
66 66 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
67 67 replace_whitespace, urlify, truncate, wrap_paragraphs
68 68 from webhelpers.date import time_ago_in_words
69 69 from webhelpers.paginate import Page as _Page
70 70 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
71 71 convert_boolean_attrs, NotGiven, _make_safe_id_component
72 72 from webhelpers2.number import format_byte_size
73 73
74 74 from rhodecode.lib.action_parser import action_parser
75 75 from rhodecode.lib.ext_json import json
76 76 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
77 77 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
78 78 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
79 79 AttributeDict, safe_int, md5, md5_safe
80 80 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
81 81 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
82 82 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
83 83 from rhodecode.lib.index.search_utils import get_matching_line_offsets
84 84 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
85 85 from rhodecode.model.changeset_status import ChangesetStatusModel
86 86 from rhodecode.model.db import Permission, User, Repository
87 87 from rhodecode.model.repo_group import RepoGroupModel
88 88 from rhodecode.model.settings import IssueTrackerSettingsModel
89 89
90 90
91 91 log = logging.getLogger(__name__)
92 92
93 93
94 94 DEFAULT_USER = User.DEFAULT_USER
95 95 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
96 96
97 97
98 98 def asset(path, ver=None, **kwargs):
99 99 """
100 100 Helper to generate a static asset file path for rhodecode assets
101 101
102 102 eg. h.asset('images/image.png', ver='3923')
103 103
104 104 :param path: path of asset
105 105 :param ver: optional version query param to append as ?ver=
106 106 """
107 107 request = get_current_request()
108 108 query = {}
109 109 query.update(kwargs)
110 110 if ver:
111 111 query = {'ver': ver}
112 112 return request.static_path(
113 113 'rhodecode:public/{}'.format(path), _query=query)
114 114
115 115
116 116 default_html_escape_table = {
117 117 ord('&'): u'&amp;',
118 118 ord('<'): u'&lt;',
119 119 ord('>'): u'&gt;',
120 120 ord('"'): u'&quot;',
121 121 ord("'"): u'&#39;',
122 122 }
123 123
124 124
125 125 def html_escape(text, html_escape_table=default_html_escape_table):
126 126 """Produce entities within text."""
127 127 return text.translate(html_escape_table)
128 128
129 129
130 130 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
131 131 """
132 132 Truncate string ``s`` at the first occurrence of ``sub``.
133 133
134 134 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
135 135 """
136 136 suffix_if_chopped = suffix_if_chopped or ''
137 137 pos = s.find(sub)
138 138 if pos == -1:
139 139 return s
140 140
141 141 if inclusive:
142 142 pos += len(sub)
143 143
144 144 chopped = s[:pos]
145 145 left = s[pos:].strip()
146 146
147 147 if left and suffix_if_chopped:
148 148 chopped += suffix_if_chopped
149 149
150 150 return chopped
151 151
152 152
153 153 def shorter(text, size=20):
154 154 postfix = '...'
155 155 if len(text) > size:
156 156 return text[:size - len(postfix)] + postfix
157 157 return text
158 158
159 159
160 160 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
161 161 """
162 162 Reset button
163 163 """
164 164 _set_input_attrs(attrs, type, name, value)
165 165 _set_id_attr(attrs, id, name)
166 166 convert_boolean_attrs(attrs, ["disabled"])
167 167 return HTML.input(**attrs)
168 168
169 169 reset = _reset
170 170 safeid = _make_safe_id_component
171 171
172 172
173 173 def branding(name, length=40):
174 174 return truncate(name, length, indicator="")
175 175
176 176
177 177 def FID(raw_id, path):
178 178 """
179 179 Creates a unique ID for filenode based on it's hash of path and commit
180 180 it's safe to use in urls
181 181
182 182 :param raw_id:
183 183 :param path:
184 184 """
185 185
186 186 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
187 187
188 188
189 189 class _GetError(object):
190 190 """Get error from form_errors, and represent it as span wrapped error
191 191 message
192 192
193 193 :param field_name: field to fetch errors for
194 194 :param form_errors: form errors dict
195 195 """
196 196
197 197 def __call__(self, field_name, form_errors):
198 198 tmpl = """<span class="error_msg">%s</span>"""
199 199 if form_errors and field_name in form_errors:
200 200 return literal(tmpl % form_errors.get(field_name))
201 201
202 202 get_error = _GetError()
203 203
204 204
205 205 class _ToolTip(object):
206 206
207 207 def __call__(self, tooltip_title, trim_at=50):
208 208 """
209 209 Special function just to wrap our text into nice formatted
210 210 autowrapped text
211 211
212 212 :param tooltip_title:
213 213 """
214 214 tooltip_title = escape(tooltip_title)
215 215 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
216 216 return tooltip_title
217 217 tooltip = _ToolTip()
218 218
219 219
220 220 def files_breadcrumbs(repo_name, commit_id, file_path):
221 221 if isinstance(file_path, str):
222 222 file_path = safe_unicode(file_path)
223 223
224 224 # TODO: johbo: Is this always a url like path, or is this operating
225 225 # system dependent?
226 226 path_segments = file_path.split('/')
227 227
228 228 repo_name_html = escape(repo_name)
229 229 if len(path_segments) == 1 and path_segments[0] == '':
230 230 url_segments = [repo_name_html]
231 231 else:
232 232 url_segments = [
233 233 link_to(
234 234 repo_name_html,
235 235 route_path(
236 236 'repo_files',
237 237 repo_name=repo_name,
238 238 commit_id=commit_id,
239 239 f_path=''),
240 240 class_='pjax-link')]
241 241
242 242 last_cnt = len(path_segments) - 1
243 243 for cnt, segment in enumerate(path_segments):
244 244 if not segment:
245 245 continue
246 246 segment_html = escape(segment)
247 247
248 248 if cnt != last_cnt:
249 249 url_segments.append(
250 250 link_to(
251 251 segment_html,
252 252 route_path(
253 253 'repo_files',
254 254 repo_name=repo_name,
255 255 commit_id=commit_id,
256 256 f_path='/'.join(path_segments[:cnt + 1])),
257 257 class_='pjax-link'))
258 258 else:
259 259 url_segments.append(segment_html)
260 260
261 261 return literal('/'.join(url_segments))
262 262
263 263
264 264 def code_highlight(code, lexer, formatter, use_hl_filter=False):
265 265 """
266 266 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
267 267
268 268 If ``outfile`` is given and a valid file object (an object
269 269 with a ``write`` method), the result will be written to it, otherwise
270 270 it is returned as a string.
271 271 """
272 272 if use_hl_filter:
273 273 # add HL filter
274 274 from rhodecode.lib.index import search_utils
275 275 lexer.add_filter(search_utils.ElasticSearchHLFilter())
276 276 return pygments.format(pygments.lex(code, lexer), formatter)
277 277
278 278
279 279 class CodeHtmlFormatter(HtmlFormatter):
280 280 """
281 281 My code Html Formatter for source codes
282 282 """
283 283
284 284 def wrap(self, source, outfile):
285 285 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
286 286
287 287 def _wrap_code(self, source):
288 288 for cnt, it in enumerate(source):
289 289 i, t = it
290 290 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
291 291 yield i, t
292 292
293 293 def _wrap_tablelinenos(self, inner):
294 294 dummyoutfile = StringIO.StringIO()
295 295 lncount = 0
296 296 for t, line in inner:
297 297 if t:
298 298 lncount += 1
299 299 dummyoutfile.write(line)
300 300
301 301 fl = self.linenostart
302 302 mw = len(str(lncount + fl - 1))
303 303 sp = self.linenospecial
304 304 st = self.linenostep
305 305 la = self.lineanchors
306 306 aln = self.anchorlinenos
307 307 nocls = self.noclasses
308 308 if sp:
309 309 lines = []
310 310
311 311 for i in range(fl, fl + lncount):
312 312 if i % st == 0:
313 313 if i % sp == 0:
314 314 if aln:
315 315 lines.append('<a href="#%s%d" class="special">%*d</a>' %
316 316 (la, i, mw, i))
317 317 else:
318 318 lines.append('<span class="special">%*d</span>' % (mw, i))
319 319 else:
320 320 if aln:
321 321 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
322 322 else:
323 323 lines.append('%*d' % (mw, i))
324 324 else:
325 325 lines.append('')
326 326 ls = '\n'.join(lines)
327 327 else:
328 328 lines = []
329 329 for i in range(fl, fl + lncount):
330 330 if i % st == 0:
331 331 if aln:
332 332 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
333 333 else:
334 334 lines.append('%*d' % (mw, i))
335 335 else:
336 336 lines.append('')
337 337 ls = '\n'.join(lines)
338 338
339 339 # in case you wonder about the seemingly redundant <div> here: since the
340 340 # content in the other cell also is wrapped in a div, some browsers in
341 341 # some configurations seem to mess up the formatting...
342 342 if nocls:
343 343 yield 0, ('<table class="%stable">' % self.cssclass +
344 344 '<tr><td><div class="linenodiv" '
345 345 'style="background-color: #f0f0f0; padding-right: 10px">'
346 346 '<pre style="line-height: 125%">' +
347 347 ls + '</pre></div></td><td id="hlcode" class="code">')
348 348 else:
349 349 yield 0, ('<table class="%stable">' % self.cssclass +
350 350 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
351 351 ls + '</pre></div></td><td id="hlcode" class="code">')
352 352 yield 0, dummyoutfile.getvalue()
353 353 yield 0, '</td></tr></table>'
354 354
355 355
356 356 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
357 357 def __init__(self, **kw):
358 358 # only show these line numbers if set
359 359 self.only_lines = kw.pop('only_line_numbers', [])
360 360 self.query_terms = kw.pop('query_terms', [])
361 361 self.max_lines = kw.pop('max_lines', 5)
362 362 self.line_context = kw.pop('line_context', 3)
363 363 self.url = kw.pop('url', None)
364 364
365 365 super(CodeHtmlFormatter, self).__init__(**kw)
366 366
367 367 def _wrap_code(self, source):
368 368 for cnt, it in enumerate(source):
369 369 i, t = it
370 370 t = '<pre>%s</pre>' % t
371 371 yield i, t
372 372
373 373 def _wrap_tablelinenos(self, inner):
374 374 yield 0, '<table class="code-highlight %stable">' % self.cssclass
375 375
376 376 last_shown_line_number = 0
377 377 current_line_number = 1
378 378
379 379 for t, line in inner:
380 380 if not t:
381 381 yield t, line
382 382 continue
383 383
384 384 if current_line_number in self.only_lines:
385 385 if last_shown_line_number + 1 != current_line_number:
386 386 yield 0, '<tr>'
387 387 yield 0, '<td class="line">...</td>'
388 388 yield 0, '<td id="hlcode" class="code"></td>'
389 389 yield 0, '</tr>'
390 390
391 391 yield 0, '<tr>'
392 392 if self.url:
393 393 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
394 394 self.url, current_line_number, current_line_number)
395 395 else:
396 396 yield 0, '<td class="line"><a href="">%i</a></td>' % (
397 397 current_line_number)
398 398 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
399 399 yield 0, '</tr>'
400 400
401 401 last_shown_line_number = current_line_number
402 402
403 403 current_line_number += 1
404 404
405 405 yield 0, '</table>'
406 406
407 407
408 408 def hsv_to_rgb(h, s, v):
409 409 """ Convert hsv color values to rgb """
410 410
411 411 if s == 0.0:
412 412 return v, v, v
413 413 i = int(h * 6.0) # XXX assume int() truncates!
414 414 f = (h * 6.0) - i
415 415 p = v * (1.0 - s)
416 416 q = v * (1.0 - s * f)
417 417 t = v * (1.0 - s * (1.0 - f))
418 418 i = i % 6
419 419 if i == 0:
420 420 return v, t, p
421 421 if i == 1:
422 422 return q, v, p
423 423 if i == 2:
424 424 return p, v, t
425 425 if i == 3:
426 426 return p, q, v
427 427 if i == 4:
428 428 return t, p, v
429 429 if i == 5:
430 430 return v, p, q
431 431
432 432
433 433 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
434 434 """
435 435 Generator for getting n of evenly distributed colors using
436 436 hsv color and golden ratio. It always return same order of colors
437 437
438 438 :param n: number of colors to generate
439 439 :param saturation: saturation of returned colors
440 440 :param lightness: lightness of returned colors
441 441 :returns: RGB tuple
442 442 """
443 443
444 444 golden_ratio = 0.618033988749895
445 445 h = 0.22717784590367374
446 446
447 447 for _ in xrange(n):
448 448 h += golden_ratio
449 449 h %= 1
450 450 HSV_tuple = [h, saturation, lightness]
451 451 RGB_tuple = hsv_to_rgb(*HSV_tuple)
452 452 yield map(lambda x: str(int(x * 256)), RGB_tuple)
453 453
454 454
455 455 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
456 456 """
457 457 Returns a function which when called with an argument returns a unique
458 458 color for that argument, eg.
459 459
460 460 :param n: number of colors to generate
461 461 :param saturation: saturation of returned colors
462 462 :param lightness: lightness of returned colors
463 463 :returns: css RGB string
464 464
465 465 >>> color_hash = color_hasher()
466 466 >>> color_hash('hello')
467 467 'rgb(34, 12, 59)'
468 468 >>> color_hash('hello')
469 469 'rgb(34, 12, 59)'
470 470 >>> color_hash('other')
471 471 'rgb(90, 224, 159)'
472 472 """
473 473
474 474 color_dict = {}
475 475 cgenerator = unique_color_generator(
476 476 saturation=saturation, lightness=lightness)
477 477
478 478 def get_color_string(thing):
479 479 if thing in color_dict:
480 480 col = color_dict[thing]
481 481 else:
482 482 col = color_dict[thing] = cgenerator.next()
483 483 return "rgb(%s)" % (', '.join(col))
484 484
485 485 return get_color_string
486 486
487 487
488 488 def get_lexer_safe(mimetype=None, filepath=None):
489 489 """
490 490 Tries to return a relevant pygments lexer using mimetype/filepath name,
491 491 defaulting to plain text if none could be found
492 492 """
493 493 lexer = None
494 494 try:
495 495 if mimetype:
496 496 lexer = get_lexer_for_mimetype(mimetype)
497 497 if not lexer:
498 498 lexer = get_lexer_for_filename(filepath)
499 499 except pygments.util.ClassNotFound:
500 500 pass
501 501
502 502 if not lexer:
503 503 lexer = get_lexer_by_name('text')
504 504
505 505 return lexer
506 506
507 507
508 508 def get_lexer_for_filenode(filenode):
509 509 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
510 510 return lexer
511 511
512 512
513 513 def pygmentize(filenode, **kwargs):
514 514 """
515 515 pygmentize function using pygments
516 516
517 517 :param filenode:
518 518 """
519 519 lexer = get_lexer_for_filenode(filenode)
520 520 return literal(code_highlight(filenode.content, lexer,
521 521 CodeHtmlFormatter(**kwargs)))
522 522
523 523
524 524 def is_following_repo(repo_name, user_id):
525 525 from rhodecode.model.scm import ScmModel
526 526 return ScmModel().is_following_repo(repo_name, user_id)
527 527
528 528
529 529 class _Message(object):
530 530 """A message returned by ``Flash.pop_messages()``.
531 531
532 532 Converting the message to a string returns the message text. Instances
533 533 also have the following attributes:
534 534
535 535 * ``message``: the message text.
536 536 * ``category``: the category specified when the message was created.
537 537 """
538 538
539 539 def __init__(self, category, message):
540 540 self.category = category
541 541 self.message = message
542 542
543 543 def __str__(self):
544 544 return self.message
545 545
546 546 __unicode__ = __str__
547 547
548 548 def __html__(self):
549 549 return escape(safe_unicode(self.message))
550 550
551 551
552 552 class Flash(object):
553 553 # List of allowed categories. If None, allow any category.
554 554 categories = ["warning", "notice", "error", "success"]
555 555
556 556 # Default category if none is specified.
557 557 default_category = "notice"
558 558
559 559 def __init__(self, session_key="flash", categories=None,
560 560 default_category=None):
561 561 """
562 562 Instantiate a ``Flash`` object.
563 563
564 564 ``session_key`` is the key to save the messages under in the user's
565 565 session.
566 566
567 567 ``categories`` is an optional list which overrides the default list
568 568 of categories.
569 569
570 570 ``default_category`` overrides the default category used for messages
571 571 when none is specified.
572 572 """
573 573 self.session_key = session_key
574 574 if categories is not None:
575 575 self.categories = categories
576 576 if default_category is not None:
577 577 self.default_category = default_category
578 578 if self.categories and self.default_category not in self.categories:
579 579 raise ValueError(
580 580 "unrecognized default category %r" % (self.default_category,))
581 581
582 582 def pop_messages(self, session=None, request=None):
583 583 """
584 584 Return all accumulated messages and delete them from the session.
585 585
586 586 The return value is a list of ``Message`` objects.
587 587 """
588 588 messages = []
589 589
590 590 if not session:
591 591 if not request:
592 592 request = get_current_request()
593 593 session = request.session
594 594
595 595 # Pop the 'old' pylons flash messages. They are tuples of the form
596 596 # (category, message)
597 597 for cat, msg in session.pop(self.session_key, []):
598 598 messages.append(_Message(cat, msg))
599 599
600 600 # Pop the 'new' pyramid flash messages for each category as list
601 601 # of strings.
602 602 for cat in self.categories:
603 603 for msg in session.pop_flash(queue=cat):
604 604 messages.append(_Message(cat, msg))
605 605 # Map messages from the default queue to the 'notice' category.
606 606 for msg in session.pop_flash():
607 607 messages.append(_Message('notice', msg))
608 608
609 609 session.save()
610 610 return messages
611 611
612 612 def json_alerts(self, session=None, request=None):
613 613 payloads = []
614 614 messages = flash.pop_messages(session=session, request=request)
615 615 if messages:
616 616 for message in messages:
617 617 subdata = {}
618 618 if hasattr(message.message, 'rsplit'):
619 619 flash_data = message.message.rsplit('|DELIM|', 1)
620 620 org_message = flash_data[0]
621 621 if len(flash_data) > 1:
622 622 subdata = json.loads(flash_data[1])
623 623 else:
624 624 org_message = message.message
625 625 payloads.append({
626 626 'message': {
627 627 'message': u'{}'.format(org_message),
628 628 'level': message.category,
629 629 'force': True,
630 630 'subdata': subdata
631 631 }
632 632 })
633 633 return json.dumps(payloads)
634 634
635 635 def __call__(self, message, category=None, ignore_duplicate=False,
636 636 session=None, request=None):
637 637
638 638 if not session:
639 639 if not request:
640 640 request = get_current_request()
641 641 session = request.session
642 642
643 643 session.flash(
644 644 message, queue=category, allow_duplicate=not ignore_duplicate)
645 645
646 646
647 647 flash = Flash()
648 648
649 649 #==============================================================================
650 650 # SCM FILTERS available via h.
651 651 #==============================================================================
652 652 from rhodecode.lib.vcs.utils import author_name, author_email
653 653 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
654 654 from rhodecode.model.db import User, ChangesetStatus
655 655
656 656 capitalize = lambda x: x.capitalize()
657 657 email = author_email
658 658 short_id = lambda x: x[:12]
659 659 hide_credentials = lambda x: ''.join(credentials_filter(x))
660 660
661 661
662 662 import pytz
663 663 import tzlocal
664 664 local_timezone = tzlocal.get_localzone()
665 665
666 666
667 667 def age_component(datetime_iso, value=None, time_is_local=False):
668 668 title = value or format_date(datetime_iso)
669 669 tzinfo = '+00:00'
670 670
671 671 # detect if we have a timezone info, otherwise, add it
672 672 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
673 673 force_timezone = os.environ.get('RC_TIMEZONE', '')
674 674 if force_timezone:
675 675 force_timezone = pytz.timezone(force_timezone)
676 676 timezone = force_timezone or local_timezone
677 677 offset = timezone.localize(datetime_iso).strftime('%z')
678 678 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
679 679
680 680 return literal(
681 681 '<time class="timeago tooltip" '
682 682 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
683 683 datetime_iso, title, tzinfo))
684 684
685 685
686 686 def _shorten_commit_id(commit_id, commit_len=None):
687 687 if commit_len is None:
688 688 request = get_current_request()
689 689 commit_len = request.call_context.visual.show_sha_length
690 690 return commit_id[:commit_len]
691 691
692 692
693 693 def show_id(commit, show_idx=None, commit_len=None):
694 694 """
695 695 Configurable function that shows ID
696 696 by default it's r123:fffeeefffeee
697 697
698 698 :param commit: commit instance
699 699 """
700 700 if show_idx is None:
701 701 request = get_current_request()
702 702 show_idx = request.call_context.visual.show_revision_number
703 703
704 704 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
705 705 if show_idx:
706 706 return 'r%s:%s' % (commit.idx, raw_id)
707 707 else:
708 708 return '%s' % (raw_id, )
709 709
710 710
711 711 def format_date(date):
712 712 """
713 713 use a standardized formatting for dates used in RhodeCode
714 714
715 715 :param date: date/datetime object
716 716 :return: formatted date
717 717 """
718 718
719 719 if date:
720 720 _fmt = "%a, %d %b %Y %H:%M:%S"
721 721 return safe_unicode(date.strftime(_fmt))
722 722
723 723 return u""
724 724
725 725
726 726 class _RepoChecker(object):
727 727
728 728 def __init__(self, backend_alias):
729 729 self._backend_alias = backend_alias
730 730
731 731 def __call__(self, repository):
732 732 if hasattr(repository, 'alias'):
733 733 _type = repository.alias
734 734 elif hasattr(repository, 'repo_type'):
735 735 _type = repository.repo_type
736 736 else:
737 737 _type = repository
738 738 return _type == self._backend_alias
739 739
740 740
741 741 is_git = _RepoChecker('git')
742 742 is_hg = _RepoChecker('hg')
743 743 is_svn = _RepoChecker('svn')
744 744
745 745
746 746 def get_repo_type_by_name(repo_name):
747 747 repo = Repository.get_by_repo_name(repo_name)
748 748 if repo:
749 749 return repo.repo_type
750 750
751 751
752 752 def is_svn_without_proxy(repository):
753 753 if is_svn(repository):
754 754 from rhodecode.model.settings import VcsSettingsModel
755 755 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
756 756 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
757 757 return False
758 758
759 759
760 760 def discover_user(author):
761 761 """
762 762 Tries to discover RhodeCode User based on the autho string. Author string
763 763 is typically `FirstName LastName <email@address.com>`
764 764 """
765 765
766 766 # if author is already an instance use it for extraction
767 767 if isinstance(author, User):
768 768 return author
769 769
770 770 # Valid email in the attribute passed, see if they're in the system
771 771 _email = author_email(author)
772 772 if _email != '':
773 773 user = User.get_by_email(_email, case_insensitive=True, cache=True)
774 774 if user is not None:
775 775 return user
776 776
777 777 # Maybe it's a username, we try to extract it and fetch by username ?
778 778 _author = author_name(author)
779 779 user = User.get_by_username(_author, case_insensitive=True, cache=True)
780 780 if user is not None:
781 781 return user
782 782
783 783 return None
784 784
785 785
786 786 def email_or_none(author):
787 787 # extract email from the commit string
788 788 _email = author_email(author)
789 789
790 790 # If we have an email, use it, otherwise
791 791 # see if it contains a username we can get an email from
792 792 if _email != '':
793 793 return _email
794 794 else:
795 795 user = User.get_by_username(
796 796 author_name(author), case_insensitive=True, cache=True)
797 797
798 798 if user is not None:
799 799 return user.email
800 800
801 801 # No valid email, not a valid user in the system, none!
802 802 return None
803 803
804 804
805 805 def link_to_user(author, length=0, **kwargs):
806 806 user = discover_user(author)
807 807 # user can be None, but if we have it already it means we can re-use it
808 808 # in the person() function, so we save 1 intensive-query
809 809 if user:
810 810 author = user
811 811
812 812 display_person = person(author, 'username_or_name_or_email')
813 813 if length:
814 814 display_person = shorter(display_person, length)
815 815
816 816 if user:
817 817 return link_to(
818 818 escape(display_person),
819 819 route_path('user_profile', username=user.username),
820 820 **kwargs)
821 821 else:
822 822 return escape(display_person)
823 823
824 824
825 825 def link_to_group(users_group_name, **kwargs):
826 826 return link_to(
827 827 escape(users_group_name),
828 828 route_path('user_group_profile', user_group_name=users_group_name),
829 829 **kwargs)
830 830
831 831
832 832 def person(author, show_attr="username_and_name"):
833 833 user = discover_user(author)
834 834 if user:
835 835 return getattr(user, show_attr)
836 836 else:
837 837 _author = author_name(author)
838 838 _email = email(author)
839 839 return _author or _email
840 840
841 841
842 842 def author_string(email):
843 843 if email:
844 844 user = User.get_by_email(email, case_insensitive=True, cache=True)
845 845 if user:
846 846 if user.first_name or user.last_name:
847 847 return '%s %s &lt;%s&gt;' % (
848 848 user.first_name, user.last_name, email)
849 849 else:
850 850 return email
851 851 else:
852 852 return email
853 853 else:
854 854 return None
855 855
856 856
857 857 def person_by_id(id_, show_attr="username_and_name"):
858 858 # attr to return from fetched user
859 859 person_getter = lambda usr: getattr(usr, show_attr)
860 860
861 861 #maybe it's an ID ?
862 862 if str(id_).isdigit() or isinstance(id_, int):
863 863 id_ = int(id_)
864 864 user = User.get(id_)
865 865 if user is not None:
866 866 return person_getter(user)
867 867 return id_
868 868
869 869
870 870 def gravatar_with_user(request, author, show_disabled=False):
871 871 _render = request.get_partial_renderer(
872 872 'rhodecode:templates/base/base.mako')
873 873 return _render('gravatar_with_user', author, show_disabled=show_disabled)
874 874
875 875
876 876 tags_paterns = OrderedDict((
877 877 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
878 878 '<div class="metatag" tag="lang">\\2</div>')),
879 879
880 880 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
881 881 '<div class="metatag" tag="see">see: \\1 </div>')),
882 882
883 883 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
884 884 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
885 885
886 886 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
887 887 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
888 888
889 889 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
890 890 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
891 891
892 892 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
893 893 '<div class="metatag" tag="state \\1">\\1</div>')),
894 894
895 895 # label in grey
896 896 ('label', (re.compile(r'\[([a-z]+)\]'),
897 897 '<div class="metatag" tag="label">\\1</div>')),
898 898
899 899 # generic catch all in grey
900 900 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
901 901 '<div class="metatag" tag="generic">\\1</div>')),
902 902 ))
903 903
904 904
905 905 def extract_metatags(value):
906 906 """
907 907 Extract supported meta-tags from given text value
908 908 """
909 909 tags = []
910 910 if not value:
911 911 return tags, ''
912 912
913 913 for key, val in tags_paterns.items():
914 914 pat, replace_html = val
915 915 tags.extend([(key, x.group()) for x in pat.finditer(value)])
916 916 value = pat.sub('', value)
917 917
918 918 return tags, value
919 919
920 920
921 921 def style_metatag(tag_type, value):
922 922 """
923 923 converts tags from value into html equivalent
924 924 """
925 925 if not value:
926 926 return ''
927 927
928 928 html_value = value
929 929 tag_data = tags_paterns.get(tag_type)
930 930 if tag_data:
931 931 pat, replace_html = tag_data
932 932 # convert to plain `unicode` instead of a markup tag to be used in
933 933 # regex expressions. safe_unicode doesn't work here
934 934 html_value = pat.sub(replace_html, unicode(value))
935 935
936 936 return html_value
937 937
938 938
939 939 def bool2icon(value, show_at_false=True):
940 940 """
941 941 Returns boolean value of a given value, represented as html element with
942 942 classes that will represent icons
943 943
944 944 :param value: given value to convert to html node
945 945 """
946 946
947 947 if value: # does bool conversion
948 948 return HTML.tag('i', class_="icon-true")
949 949 else: # not true as bool
950 950 if show_at_false:
951 951 return HTML.tag('i', class_="icon-false")
952 952 return HTML.tag('i')
953 953
954 954 #==============================================================================
955 955 # PERMS
956 956 #==============================================================================
957 957 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
958 958 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
959 959 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
960 960 csrf_token_key
961 961
962 962
963 963 #==============================================================================
964 964 # GRAVATAR URL
965 965 #==============================================================================
966 966 class InitialsGravatar(object):
967 967 def __init__(self, email_address, first_name, last_name, size=30,
968 968 background=None, text_color='#fff'):
969 969 self.size = size
970 970 self.first_name = first_name
971 971 self.last_name = last_name
972 972 self.email_address = email_address
973 973 self.background = background or self.str2color(email_address)
974 974 self.text_color = text_color
975 975
976 976 def get_color_bank(self):
977 977 """
978 978 returns a predefined list of colors that gravatars can use.
979 979 Those are randomized distinct colors that guarantee readability and
980 980 uniqueness.
981 981
982 982 generated with: http://phrogz.net/css/distinct-colors.html
983 983 """
984 984 return [
985 985 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
986 986 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
987 987 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
988 988 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
989 989 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
990 990 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
991 991 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
992 992 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
993 993 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
994 994 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
995 995 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
996 996 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
997 997 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
998 998 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
999 999 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1000 1000 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1001 1001 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1002 1002 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1003 1003 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1004 1004 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1005 1005 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1006 1006 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1007 1007 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1008 1008 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1009 1009 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1010 1010 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1011 1011 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1012 1012 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1013 1013 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1014 1014 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1015 1015 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1016 1016 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1017 1017 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1018 1018 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1019 1019 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1020 1020 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1021 1021 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1022 1022 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1023 1023 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1024 1024 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1025 1025 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1026 1026 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1027 1027 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1028 1028 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1029 1029 '#4f8c46', '#368dd9', '#5c0073'
1030 1030 ]
1031 1031
1032 1032 def rgb_to_hex_color(self, rgb_tuple):
1033 1033 """
1034 1034 Converts an rgb_tuple passed to an hex color.
1035 1035
1036 1036 :param rgb_tuple: tuple with 3 ints represents rgb color space
1037 1037 """
1038 1038 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1039 1039
1040 1040 def email_to_int_list(self, email_str):
1041 1041 """
1042 1042 Get every byte of the hex digest value of email and turn it to integer.
1043 1043 It's going to be always between 0-255
1044 1044 """
1045 1045 digest = md5_safe(email_str.lower())
1046 1046 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1047 1047
1048 1048 def pick_color_bank_index(self, email_str, color_bank):
1049 1049 return self.email_to_int_list(email_str)[0] % len(color_bank)
1050 1050
1051 1051 def str2color(self, email_str):
1052 1052 """
1053 1053 Tries to map in a stable algorithm an email to color
1054 1054
1055 1055 :param email_str:
1056 1056 """
1057 1057 color_bank = self.get_color_bank()
1058 1058 # pick position (module it's length so we always find it in the
1059 1059 # bank even if it's smaller than 256 values
1060 1060 pos = self.pick_color_bank_index(email_str, color_bank)
1061 1061 return color_bank[pos]
1062 1062
1063 1063 def normalize_email(self, email_address):
1064 1064 import unicodedata
1065 1065 # default host used to fill in the fake/missing email
1066 1066 default_host = u'localhost'
1067 1067
1068 1068 if not email_address:
1069 1069 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1070 1070
1071 1071 email_address = safe_unicode(email_address)
1072 1072
1073 1073 if u'@' not in email_address:
1074 1074 email_address = u'%s@%s' % (email_address, default_host)
1075 1075
1076 1076 if email_address.endswith(u'@'):
1077 1077 email_address = u'%s%s' % (email_address, default_host)
1078 1078
1079 1079 email_address = unicodedata.normalize('NFKD', email_address)\
1080 1080 .encode('ascii', 'ignore')
1081 1081 return email_address
1082 1082
1083 1083 def get_initials(self):
1084 1084 """
1085 1085 Returns 2 letter initials calculated based on the input.
1086 1086 The algorithm picks first given email address, and takes first letter
1087 1087 of part before @, and then the first letter of server name. In case
1088 1088 the part before @ is in a format of `somestring.somestring2` it replaces
1089 1089 the server letter with first letter of somestring2
1090 1090
1091 1091 In case function was initialized with both first and lastname, this
1092 1092 overrides the extraction from email by first letter of the first and
1093 1093 last name. We add special logic to that functionality, In case Full name
1094 1094 is compound, like Guido Von Rossum, we use last part of the last name
1095 1095 (Von Rossum) picking `R`.
1096 1096
1097 1097 Function also normalizes the non-ascii characters to they ascii
1098 1098 representation, eg Δ„ => A
1099 1099 """
1100 1100 import unicodedata
1101 1101 # replace non-ascii to ascii
1102 1102 first_name = unicodedata.normalize(
1103 1103 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1104 1104 last_name = unicodedata.normalize(
1105 1105 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1106 1106
1107 1107 # do NFKD encoding, and also make sure email has proper format
1108 1108 email_address = self.normalize_email(self.email_address)
1109 1109
1110 1110 # first push the email initials
1111 1111 prefix, server = email_address.split('@', 1)
1112 1112
1113 1113 # check if prefix is maybe a 'first_name.last_name' syntax
1114 1114 _dot_split = prefix.rsplit('.', 1)
1115 1115 if len(_dot_split) == 2 and _dot_split[1]:
1116 1116 initials = [_dot_split[0][0], _dot_split[1][0]]
1117 1117 else:
1118 1118 initials = [prefix[0], server[0]]
1119 1119
1120 1120 # then try to replace either first_name or last_name
1121 1121 fn_letter = (first_name or " ")[0].strip()
1122 1122 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1123 1123
1124 1124 if fn_letter:
1125 1125 initials[0] = fn_letter
1126 1126
1127 1127 if ln_letter:
1128 1128 initials[1] = ln_letter
1129 1129
1130 1130 return ''.join(initials).upper()
1131 1131
1132 1132 def get_img_data_by_type(self, font_family, img_type):
1133 1133 default_user = """
1134 1134 <svg xmlns="http://www.w3.org/2000/svg"
1135 1135 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1136 1136 viewBox="-15 -10 439.165 429.164"
1137 1137
1138 1138 xml:space="preserve"
1139 1139 style="background:{background};" >
1140 1140
1141 1141 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1142 1142 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1143 1143 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1144 1144 168.596,153.916,216.671,
1145 1145 204.583,216.671z" fill="{text_color}"/>
1146 1146 <path d="M407.164,374.717L360.88,
1147 1147 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1148 1148 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1149 1149 15.366-44.203,23.488-69.076,23.488c-24.877,
1150 1150 0-48.762-8.122-69.078-23.488
1151 1151 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1152 1152 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1153 1153 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1154 1154 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1155 1155 19.402-10.527 C409.699,390.129,
1156 1156 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1157 1157 </svg>""".format(
1158 1158 size=self.size,
1159 1159 background='#979797', # @grey4
1160 1160 text_color=self.text_color,
1161 1161 font_family=font_family)
1162 1162
1163 1163 return {
1164 1164 "default_user": default_user
1165 1165 }[img_type]
1166 1166
1167 1167 def get_img_data(self, svg_type=None):
1168 1168 """
1169 1169 generates the svg metadata for image
1170 1170 """
1171 1171 fonts = [
1172 1172 '-apple-system',
1173 1173 'BlinkMacSystemFont',
1174 1174 'Segoe UI',
1175 1175 'Roboto',
1176 1176 'Oxygen-Sans',
1177 1177 'Ubuntu',
1178 1178 'Cantarell',
1179 1179 'Helvetica Neue',
1180 1180 'sans-serif'
1181 1181 ]
1182 1182 font_family = ','.join(fonts)
1183 1183 if svg_type:
1184 1184 return self.get_img_data_by_type(font_family, svg_type)
1185 1185
1186 1186 initials = self.get_initials()
1187 1187 img_data = """
1188 1188 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1189 1189 width="{size}" height="{size}"
1190 1190 style="width: 100%; height: 100%; background-color: {background}"
1191 1191 viewBox="0 0 {size} {size}">
1192 1192 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1193 1193 pointer-events="auto" fill="{text_color}"
1194 1194 font-family="{font_family}"
1195 1195 style="font-weight: 400; font-size: {f_size}px;">{text}
1196 1196 </text>
1197 1197 </svg>""".format(
1198 1198 size=self.size,
1199 1199 f_size=self.size/1.85, # scale the text inside the box nicely
1200 1200 background=self.background,
1201 1201 text_color=self.text_color,
1202 1202 text=initials.upper(),
1203 1203 font_family=font_family)
1204 1204
1205 1205 return img_data
1206 1206
1207 1207 def generate_svg(self, svg_type=None):
1208 1208 img_data = self.get_img_data(svg_type)
1209 1209 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1210 1210
1211 1211
1212 1212 def initials_gravatar(email_address, first_name, last_name, size=30):
1213 1213 svg_type = None
1214 1214 if email_address == User.DEFAULT_USER_EMAIL:
1215 1215 svg_type = 'default_user'
1216 1216 klass = InitialsGravatar(email_address, first_name, last_name, size)
1217 1217 return klass.generate_svg(svg_type=svg_type)
1218 1218
1219 1219
1220 1220 def gravatar_url(email_address, size=30, request=None):
1221 1221 request = get_current_request()
1222 1222 _use_gravatar = request.call_context.visual.use_gravatar
1223 1223 _gravatar_url = request.call_context.visual.gravatar_url
1224 1224
1225 1225 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1226 1226
1227 1227 email_address = email_address or User.DEFAULT_USER_EMAIL
1228 1228 if isinstance(email_address, unicode):
1229 1229 # hashlib crashes on unicode items
1230 1230 email_address = safe_str(email_address)
1231 1231
1232 1232 # empty email or default user
1233 1233 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1234 1234 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1235 1235
1236 1236 if _use_gravatar:
1237 1237 # TODO: Disuse pyramid thread locals. Think about another solution to
1238 1238 # get the host and schema here.
1239 1239 request = get_current_request()
1240 1240 tmpl = safe_str(_gravatar_url)
1241 1241 tmpl = tmpl.replace('{email}', email_address)\
1242 1242 .replace('{md5email}', md5_safe(email_address.lower())) \
1243 1243 .replace('{netloc}', request.host)\
1244 1244 .replace('{scheme}', request.scheme)\
1245 1245 .replace('{size}', safe_str(size))
1246 1246 return tmpl
1247 1247 else:
1248 1248 return initials_gravatar(email_address, '', '', size=size)
1249 1249
1250 1250
1251 1251 class Page(_Page):
1252 1252 """
1253 1253 Custom pager to match rendering style with paginator
1254 1254 """
1255 1255
1256 1256 def _get_pos(self, cur_page, max_page, items):
1257 1257 edge = (items / 2) + 1
1258 1258 if (cur_page <= edge):
1259 1259 radius = max(items / 2, items - cur_page)
1260 1260 elif (max_page - cur_page) < edge:
1261 1261 radius = (items - 1) - (max_page - cur_page)
1262 1262 else:
1263 1263 radius = items / 2
1264 1264
1265 1265 left = max(1, (cur_page - (radius)))
1266 1266 right = min(max_page, cur_page + (radius))
1267 1267 return left, cur_page, right
1268 1268
1269 1269 def _range(self, regexp_match):
1270 1270 """
1271 1271 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1272 1272
1273 1273 Arguments:
1274 1274
1275 1275 regexp_match
1276 1276 A "re" (regular expressions) match object containing the
1277 1277 radius of linked pages around the current page in
1278 1278 regexp_match.group(1) as a string
1279 1279
1280 1280 This function is supposed to be called as a callable in
1281 1281 re.sub.
1282 1282
1283 1283 """
1284 1284 radius = int(regexp_match.group(1))
1285 1285
1286 1286 # Compute the first and last page number within the radius
1287 1287 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1288 1288 # -> leftmost_page = 5
1289 1289 # -> rightmost_page = 9
1290 1290 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1291 1291 self.last_page,
1292 1292 (radius * 2) + 1)
1293 1293 nav_items = []
1294 1294
1295 1295 # Create a link to the first page (unless we are on the first page
1296 1296 # or there would be no need to insert '..' spacers)
1297 1297 if self.page != self.first_page and self.first_page < leftmost_page:
1298 1298 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1299 1299
1300 1300 # Insert dots if there are pages between the first page
1301 1301 # and the currently displayed page range
1302 1302 if leftmost_page - self.first_page > 1:
1303 1303 # Wrap in a SPAN tag if nolink_attr is set
1304 1304 text = '..'
1305 1305 if self.dotdot_attr:
1306 1306 text = HTML.span(c=text, **self.dotdot_attr)
1307 1307 nav_items.append(text)
1308 1308
1309 1309 for thispage in xrange(leftmost_page, rightmost_page + 1):
1310 1310 # Hilight the current page number and do not use a link
1311 1311 if thispage == self.page:
1312 1312 text = '%s' % (thispage,)
1313 1313 # Wrap in a SPAN tag if nolink_attr is set
1314 1314 if self.curpage_attr:
1315 1315 text = HTML.span(c=text, **self.curpage_attr)
1316 1316 nav_items.append(text)
1317 1317 # Otherwise create just a link to that page
1318 1318 else:
1319 1319 text = '%s' % (thispage,)
1320 1320 nav_items.append(self._pagerlink(thispage, text))
1321 1321
1322 1322 # Insert dots if there are pages between the displayed
1323 1323 # page numbers and the end of the page range
1324 1324 if self.last_page - rightmost_page > 1:
1325 1325 text = '..'
1326 1326 # Wrap in a SPAN tag if nolink_attr is set
1327 1327 if self.dotdot_attr:
1328 1328 text = HTML.span(c=text, **self.dotdot_attr)
1329 1329 nav_items.append(text)
1330 1330
1331 1331 # Create a link to the very last page (unless we are on the last
1332 1332 # page or there would be no need to insert '..' spacers)
1333 1333 if self.page != self.last_page and rightmost_page < self.last_page:
1334 1334 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1335 1335
1336 1336 ## prerender links
1337 1337 #_page_link = url.current()
1338 1338 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1339 1339 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1340 1340 return self.separator.join(nav_items)
1341 1341
1342 1342 def pager(self, format='~2~', page_param='page', partial_param='partial',
1343 1343 show_if_single_page=False, separator=' ', onclick=None,
1344 1344 symbol_first='<<', symbol_last='>>',
1345 1345 symbol_previous='<', symbol_next='>',
1346 1346 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1347 1347 curpage_attr={'class': 'pager_curpage'},
1348 1348 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1349 1349
1350 1350 self.curpage_attr = curpage_attr
1351 1351 self.separator = separator
1352 1352 self.pager_kwargs = kwargs
1353 1353 self.page_param = page_param
1354 1354 self.partial_param = partial_param
1355 1355 self.onclick = onclick
1356 1356 self.link_attr = link_attr
1357 1357 self.dotdot_attr = dotdot_attr
1358 1358
1359 1359 # Don't show navigator if there is no more than one page
1360 1360 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1361 1361 return ''
1362 1362
1363 1363 from string import Template
1364 1364 # Replace ~...~ in token format by range of pages
1365 1365 result = re.sub(r'~(\d+)~', self._range, format)
1366 1366
1367 1367 # Interpolate '%' variables
1368 1368 result = Template(result).safe_substitute({
1369 1369 'first_page': self.first_page,
1370 1370 'last_page': self.last_page,
1371 1371 'page': self.page,
1372 1372 'page_count': self.page_count,
1373 1373 'items_per_page': self.items_per_page,
1374 1374 'first_item': self.first_item,
1375 1375 'last_item': self.last_item,
1376 1376 'item_count': self.item_count,
1377 1377 'link_first': self.page > self.first_page and \
1378 1378 self._pagerlink(self.first_page, symbol_first) or '',
1379 1379 'link_last': self.page < self.last_page and \
1380 1380 self._pagerlink(self.last_page, symbol_last) or '',
1381 1381 'link_previous': self.previous_page and \
1382 1382 self._pagerlink(self.previous_page, symbol_previous) \
1383 1383 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1384 1384 'link_next': self.next_page and \
1385 1385 self._pagerlink(self.next_page, symbol_next) \
1386 1386 or HTML.span(symbol_next, class_="pg-next disabled")
1387 1387 })
1388 1388
1389 1389 return literal(result)
1390 1390
1391 1391
1392 1392 #==============================================================================
1393 1393 # REPO PAGER, PAGER FOR REPOSITORY
1394 1394 #==============================================================================
1395 1395 class RepoPage(Page):
1396 1396
1397 1397 def __init__(self, collection, page=1, items_per_page=20,
1398 1398 item_count=None, url=None, **kwargs):
1399 1399
1400 1400 """Create a "RepoPage" instance. special pager for paging
1401 1401 repository
1402 1402 """
1403 1403 self._url_generator = url
1404 1404
1405 1405 # Safe the kwargs class-wide so they can be used in the pager() method
1406 1406 self.kwargs = kwargs
1407 1407
1408 1408 # Save a reference to the collection
1409 1409 self.original_collection = collection
1410 1410
1411 1411 self.collection = collection
1412 1412
1413 1413 # The self.page is the number of the current page.
1414 1414 # The first page has the number 1!
1415 1415 try:
1416 1416 self.page = int(page) # make it int() if we get it as a string
1417 1417 except (ValueError, TypeError):
1418 1418 self.page = 1
1419 1419
1420 1420 self.items_per_page = items_per_page
1421 1421
1422 1422 # Unless the user tells us how many items the collections has
1423 1423 # we calculate that ourselves.
1424 1424 if item_count is not None:
1425 1425 self.item_count = item_count
1426 1426 else:
1427 1427 self.item_count = len(self.collection)
1428 1428
1429 1429 # Compute the number of the first and last available page
1430 1430 if self.item_count > 0:
1431 1431 self.first_page = 1
1432 1432 self.page_count = int(math.ceil(float(self.item_count) /
1433 1433 self.items_per_page))
1434 1434 self.last_page = self.first_page + self.page_count - 1
1435 1435
1436 1436 # Make sure that the requested page number is the range of
1437 1437 # valid pages
1438 1438 if self.page > self.last_page:
1439 1439 self.page = self.last_page
1440 1440 elif self.page < self.first_page:
1441 1441 self.page = self.first_page
1442 1442
1443 1443 # Note: the number of items on this page can be less than
1444 1444 # items_per_page if the last page is not full
1445 1445 self.first_item = max(0, (self.item_count) - (self.page *
1446 1446 items_per_page))
1447 1447 self.last_item = ((self.item_count - 1) - items_per_page *
1448 1448 (self.page - 1))
1449 1449
1450 1450 self.items = list(self.collection[self.first_item:self.last_item + 1])
1451 1451
1452 1452 # Links to previous and next page
1453 1453 if self.page > self.first_page:
1454 1454 self.previous_page = self.page - 1
1455 1455 else:
1456 1456 self.previous_page = None
1457 1457
1458 1458 if self.page < self.last_page:
1459 1459 self.next_page = self.page + 1
1460 1460 else:
1461 1461 self.next_page = None
1462 1462
1463 1463 # No items available
1464 1464 else:
1465 1465 self.first_page = None
1466 1466 self.page_count = 0
1467 1467 self.last_page = None
1468 1468 self.first_item = None
1469 1469 self.last_item = None
1470 1470 self.previous_page = None
1471 1471 self.next_page = None
1472 1472 self.items = []
1473 1473
1474 1474 # This is a subclass of the 'list' type. Initialise the list now.
1475 1475 list.__init__(self, reversed(self.items))
1476 1476
1477 1477
1478 1478 def breadcrumb_repo_link(repo):
1479 1479 """
1480 1480 Makes a breadcrumbs path link to repo
1481 1481
1482 1482 ex::
1483 1483 group >> subgroup >> repo
1484 1484
1485 1485 :param repo: a Repository instance
1486 1486 """
1487 1487
1488 1488 path = [
1489 1489 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1490 1490 for group in repo.groups_with_parents
1491 1491 ] + [
1492 1492 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name))
1493 1493 ]
1494 1494
1495 1495 return literal(' &raquo; '.join(path))
1496 1496
1497 1497
1498 def breadcrumb_repo_group_link(repo_group):
1499 """
1500 Makes a breadcrumbs path link to repo
1501
1502 ex::
1503 group >> subgroup
1504
1505 :param repo_group: a Repository Group instance
1506 """
1507
1508 path = [
1509 link_to(group.name,
1510 route_path('repo_group_home', repo_group_name=group.group_name))
1511 for group in repo_group.parents
1512 ] + [
1513 link_to(repo_group.name,
1514 route_path('repo_group_home', repo_group_name=repo_group.group_name))
1515 ]
1516
1517 return literal(' &raquo; '.join(path))
1518
1519
1498 1520 def format_byte_size_binary(file_size):
1499 1521 """
1500 1522 Formats file/folder sizes to standard.
1501 1523 """
1502 1524 if file_size is None:
1503 1525 file_size = 0
1504 1526
1505 1527 formatted_size = format_byte_size(file_size, binary=True)
1506 1528 return formatted_size
1507 1529
1508 1530
1509 1531 def urlify_text(text_, safe=True):
1510 1532 """
1511 1533 Extrac urls from text and make html links out of them
1512 1534
1513 1535 :param text_:
1514 1536 """
1515 1537
1516 1538 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1517 1539 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1518 1540
1519 1541 def url_func(match_obj):
1520 1542 url_full = match_obj.groups()[0]
1521 1543 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1522 1544 _newtext = url_pat.sub(url_func, text_)
1523 1545 if safe:
1524 1546 return literal(_newtext)
1525 1547 return _newtext
1526 1548
1527 1549
1528 1550 def urlify_commits(text_, repository):
1529 1551 """
1530 1552 Extract commit ids from text and make link from them
1531 1553
1532 1554 :param text_:
1533 1555 :param repository: repo name to build the URL with
1534 1556 """
1535 1557
1536 1558 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1537 1559
1538 1560 def url_func(match_obj):
1539 1561 commit_id = match_obj.groups()[1]
1540 1562 pref = match_obj.groups()[0]
1541 1563 suf = match_obj.groups()[2]
1542 1564
1543 1565 tmpl = (
1544 1566 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1545 1567 '%(commit_id)s</a>%(suf)s'
1546 1568 )
1547 1569 return tmpl % {
1548 1570 'pref': pref,
1549 1571 'cls': 'revision-link',
1550 1572 'url': route_url('repo_commit', repo_name=repository, commit_id=commit_id),
1551 1573 'commit_id': commit_id,
1552 1574 'suf': suf
1553 1575 }
1554 1576
1555 1577 newtext = URL_PAT.sub(url_func, text_)
1556 1578
1557 1579 return newtext
1558 1580
1559 1581
1560 1582 def _process_url_func(match_obj, repo_name, uid, entry,
1561 1583 return_raw_data=False, link_format='html'):
1562 1584 pref = ''
1563 1585 if match_obj.group().startswith(' '):
1564 1586 pref = ' '
1565 1587
1566 1588 issue_id = ''.join(match_obj.groups())
1567 1589
1568 1590 if link_format == 'html':
1569 1591 tmpl = (
1570 1592 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1571 1593 '%(issue-prefix)s%(id-repr)s'
1572 1594 '</a>')
1573 1595 elif link_format == 'rst':
1574 1596 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1575 1597 elif link_format == 'markdown':
1576 1598 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1577 1599 else:
1578 1600 raise ValueError('Bad link_format:{}'.format(link_format))
1579 1601
1580 1602 (repo_name_cleaned,
1581 1603 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1582 1604
1583 1605 # variables replacement
1584 1606 named_vars = {
1585 1607 'id': issue_id,
1586 1608 'repo': repo_name,
1587 1609 'repo_name': repo_name_cleaned,
1588 1610 'group_name': parent_group_name
1589 1611 }
1590 1612 # named regex variables
1591 1613 named_vars.update(match_obj.groupdict())
1592 1614 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1593 1615
1594 1616 def quote_cleaner(input_str):
1595 1617 """Remove quotes as it's HTML"""
1596 1618 return input_str.replace('"', '')
1597 1619
1598 1620 data = {
1599 1621 'pref': pref,
1600 1622 'cls': quote_cleaner('issue-tracker-link'),
1601 1623 'url': quote_cleaner(_url),
1602 1624 'id-repr': issue_id,
1603 1625 'issue-prefix': entry['pref'],
1604 1626 'serv': entry['url'],
1605 1627 }
1606 1628 if return_raw_data:
1607 1629 return {
1608 1630 'id': issue_id,
1609 1631 'url': _url
1610 1632 }
1611 1633 return tmpl % data
1612 1634
1613 1635
1614 1636 def get_active_pattern_entries(repo_name):
1615 1637 repo = None
1616 1638 if repo_name:
1617 1639 # Retrieving repo_name to avoid invalid repo_name to explode on
1618 1640 # IssueTrackerSettingsModel but still passing invalid name further down
1619 1641 repo = Repository.get_by_repo_name(repo_name, cache=True)
1620 1642
1621 1643 settings_model = IssueTrackerSettingsModel(repo=repo)
1622 1644 active_entries = settings_model.get_settings(cache=True)
1623 1645 return active_entries
1624 1646
1625 1647
1626 1648 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1627 1649
1628 1650 allowed_formats = ['html', 'rst', 'markdown']
1629 1651 if link_format not in allowed_formats:
1630 1652 raise ValueError('Link format can be only one of:{} got {}'.format(
1631 1653 allowed_formats, link_format))
1632 1654
1633 1655 active_entries = active_entries or get_active_pattern_entries(repo_name)
1634 1656 issues_data = []
1635 1657 newtext = text_string
1636 1658
1637 1659 for uid, entry in active_entries.items():
1638 1660 log.debug('found issue tracker entry with uid %s', uid)
1639 1661
1640 1662 if not (entry['pat'] and entry['url']):
1641 1663 log.debug('skipping due to missing data')
1642 1664 continue
1643 1665
1644 1666 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1645 1667 uid, entry['pat'], entry['url'], entry['pref'])
1646 1668
1647 1669 try:
1648 1670 pattern = re.compile(r'%s' % entry['pat'])
1649 1671 except re.error:
1650 1672 log.exception(
1651 1673 'issue tracker pattern: `%s` failed to compile',
1652 1674 entry['pat'])
1653 1675 continue
1654 1676
1655 1677 data_func = partial(
1656 1678 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1657 1679 return_raw_data=True)
1658 1680
1659 1681 for match_obj in pattern.finditer(text_string):
1660 1682 issues_data.append(data_func(match_obj))
1661 1683
1662 1684 url_func = partial(
1663 1685 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1664 1686 link_format=link_format)
1665 1687
1666 1688 newtext = pattern.sub(url_func, newtext)
1667 1689 log.debug('processed prefix:uid `%s`', uid)
1668 1690
1669 1691 return newtext, issues_data
1670 1692
1671 1693
1672 1694 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1673 1695 """
1674 1696 Parses given text message and makes proper links.
1675 1697 issues are linked to given issue-server, and rest is a commit link
1676 1698
1677 1699 :param commit_text:
1678 1700 :param repository:
1679 1701 """
1680 1702 def escaper(string):
1681 1703 return string.replace('<', '&lt;').replace('>', '&gt;')
1682 1704
1683 1705 newtext = escaper(commit_text)
1684 1706
1685 1707 # extract http/https links and make them real urls
1686 1708 newtext = urlify_text(newtext, safe=False)
1687 1709
1688 1710 # urlify commits - extract commit ids and make link out of them, if we have
1689 1711 # the scope of repository present.
1690 1712 if repository:
1691 1713 newtext = urlify_commits(newtext, repository)
1692 1714
1693 1715 # process issue tracker patterns
1694 1716 newtext, issues = process_patterns(newtext, repository or '',
1695 1717 active_entries=active_pattern_entries)
1696 1718
1697 1719 return literal(newtext)
1698 1720
1699 1721
1700 1722 def render_binary(repo_name, file_obj):
1701 1723 """
1702 1724 Choose how to render a binary file
1703 1725 """
1704 1726
1705 1727 filename = file_obj.name
1706 1728
1707 1729 # images
1708 1730 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1709 1731 if fnmatch.fnmatch(filename, pat=ext):
1710 1732 alt = escape(filename)
1711 1733 src = route_path(
1712 1734 'repo_file_raw', repo_name=repo_name,
1713 1735 commit_id=file_obj.commit.raw_id,
1714 1736 f_path=file_obj.path)
1715 1737 return literal(
1716 1738 '<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1717 1739
1718 1740
1719 1741 def renderer_from_filename(filename, exclude=None):
1720 1742 """
1721 1743 choose a renderer based on filename, this works only for text based files
1722 1744 """
1723 1745
1724 1746 # ipython
1725 1747 for ext in ['*.ipynb']:
1726 1748 if fnmatch.fnmatch(filename, pat=ext):
1727 1749 return 'jupyter'
1728 1750
1729 1751 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1730 1752 if is_markup:
1731 1753 return is_markup
1732 1754 return None
1733 1755
1734 1756
1735 1757 def render(source, renderer='rst', mentions=False, relative_urls=None,
1736 1758 repo_name=None):
1737 1759
1738 1760 def maybe_convert_relative_links(html_source):
1739 1761 if relative_urls:
1740 1762 return relative_links(html_source, relative_urls)
1741 1763 return html_source
1742 1764
1743 1765 if renderer == 'plain':
1744 1766 return literal(
1745 1767 MarkupRenderer.plain(source, leading_newline=False))
1746 1768
1747 1769 elif renderer == 'rst':
1748 1770 if repo_name:
1749 1771 # process patterns on comments if we pass in repo name
1750 1772 source, issues = process_patterns(
1751 1773 source, repo_name, link_format='rst')
1752 1774
1753 1775 return literal(
1754 1776 '<div class="rst-block">%s</div>' %
1755 1777 maybe_convert_relative_links(
1756 1778 MarkupRenderer.rst(source, mentions=mentions)))
1757 1779
1758 1780 elif renderer == 'markdown':
1759 1781 if repo_name:
1760 1782 # process patterns on comments if we pass in repo name
1761 1783 source, issues = process_patterns(
1762 1784 source, repo_name, link_format='markdown')
1763 1785
1764 1786 return literal(
1765 1787 '<div class="markdown-block">%s</div>' %
1766 1788 maybe_convert_relative_links(
1767 1789 MarkupRenderer.markdown(source, flavored=True,
1768 1790 mentions=mentions)))
1769 1791
1770 1792 elif renderer == 'jupyter':
1771 1793 return literal(
1772 1794 '<div class="ipynb">%s</div>' %
1773 1795 maybe_convert_relative_links(
1774 1796 MarkupRenderer.jupyter(source)))
1775 1797
1776 1798 # None means just show the file-source
1777 1799 return None
1778 1800
1779 1801
1780 1802 def commit_status(repo, commit_id):
1781 1803 return ChangesetStatusModel().get_status(repo, commit_id)
1782 1804
1783 1805
1784 1806 def commit_status_lbl(commit_status):
1785 1807 return dict(ChangesetStatus.STATUSES).get(commit_status)
1786 1808
1787 1809
1788 1810 def commit_time(repo_name, commit_id):
1789 1811 repo = Repository.get_by_repo_name(repo_name)
1790 1812 commit = repo.get_commit(commit_id=commit_id)
1791 1813 return commit.date
1792 1814
1793 1815
1794 1816 def get_permission_name(key):
1795 1817 return dict(Permission.PERMS).get(key)
1796 1818
1797 1819
1798 1820 def journal_filter_help(request):
1799 1821 _ = request.translate
1800 1822 from rhodecode.lib.audit_logger import ACTIONS
1801 1823 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1802 1824
1803 1825 return _(
1804 1826 'Example filter terms:\n' +
1805 1827 ' repository:vcs\n' +
1806 1828 ' username:marcin\n' +
1807 1829 ' username:(NOT marcin)\n' +
1808 1830 ' action:*push*\n' +
1809 1831 ' ip:127.0.0.1\n' +
1810 1832 ' date:20120101\n' +
1811 1833 ' date:[20120101100000 TO 20120102]\n' +
1812 1834 '\n' +
1813 1835 'Actions: {actions}\n' +
1814 1836 '\n' +
1815 1837 'Generate wildcards using \'*\' character:\n' +
1816 1838 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1817 1839 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1818 1840 '\n' +
1819 1841 'Optional AND / OR operators in queries\n' +
1820 1842 ' "repository:vcs OR repository:test"\n' +
1821 1843 ' "username:test AND repository:test*"\n'
1822 1844 ).format(actions=actions)
1823 1845
1824 1846
1825 1847 def not_mapped_error(repo_name):
1826 1848 from rhodecode.translation import _
1827 1849 flash(_('%s repository is not mapped to db perhaps'
1828 1850 ' it was created or renamed from the filesystem'
1829 1851 ' please run the application again'
1830 1852 ' in order to rescan repositories') % repo_name, category='error')
1831 1853
1832 1854
1833 1855 def ip_range(ip_addr):
1834 1856 from rhodecode.model.db import UserIpMap
1835 1857 s, e = UserIpMap._get_ip_range(ip_addr)
1836 1858 return '%s - %s' % (s, e)
1837 1859
1838 1860
1839 1861 def form(url, method='post', needs_csrf_token=True, **attrs):
1840 1862 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1841 1863 if method.lower() != 'get' and needs_csrf_token:
1842 1864 raise Exception(
1843 1865 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1844 1866 'CSRF token. If the endpoint does not require such token you can ' +
1845 1867 'explicitly set the parameter needs_csrf_token to false.')
1846 1868
1847 1869 return wh_form(url, method=method, **attrs)
1848 1870
1849 1871
1850 1872 def secure_form(form_url, method="POST", multipart=False, **attrs):
1851 1873 """Start a form tag that points the action to an url. This
1852 1874 form tag will also include the hidden field containing
1853 1875 the auth token.
1854 1876
1855 1877 The url options should be given either as a string, or as a
1856 1878 ``url()`` function. The method for the form defaults to POST.
1857 1879
1858 1880 Options:
1859 1881
1860 1882 ``multipart``
1861 1883 If set to True, the enctype is set to "multipart/form-data".
1862 1884 ``method``
1863 1885 The method to use when submitting the form, usually either
1864 1886 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1865 1887 hidden input with name _method is added to simulate the verb
1866 1888 over POST.
1867 1889
1868 1890 """
1869 1891 from webhelpers.pylonslib.secure_form import insecure_form
1870 1892
1871 1893 if 'request' in attrs:
1872 1894 session = attrs['request'].session
1873 1895 del attrs['request']
1874 1896 else:
1875 1897 raise ValueError(
1876 1898 'Calling this form requires request= to be passed as argument')
1877 1899
1878 1900 form = insecure_form(form_url, method, multipart, **attrs)
1879 1901 token = literal(
1880 1902 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1881 1903 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1882 1904
1883 1905 return literal("%s\n%s" % (form, token))
1884 1906
1885 1907
1886 1908 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1887 1909 select_html = select(name, selected, options, **attrs)
1888 1910 select2 = """
1889 1911 <script>
1890 1912 $(document).ready(function() {
1891 1913 $('#%s').select2({
1892 1914 containerCssClass: 'drop-menu',
1893 1915 dropdownCssClass: 'drop-menu-dropdown',
1894 1916 dropdownAutoWidth: true%s
1895 1917 });
1896 1918 });
1897 1919 </script>
1898 1920 """
1899 1921 filter_option = """,
1900 1922 minimumResultsForSearch: -1
1901 1923 """
1902 1924 input_id = attrs.get('id') or name
1903 1925 filter_enabled = "" if enable_filter else filter_option
1904 1926 select_script = literal(select2 % (input_id, filter_enabled))
1905 1927
1906 1928 return literal(select_html+select_script)
1907 1929
1908 1930
1909 1931 def get_visual_attr(tmpl_context_var, attr_name):
1910 1932 """
1911 1933 A safe way to get a variable from visual variable of template context
1912 1934
1913 1935 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1914 1936 :param attr_name: name of the attribute we fetch from the c.visual
1915 1937 """
1916 1938 visual = getattr(tmpl_context_var, 'visual', None)
1917 1939 if not visual:
1918 1940 return
1919 1941 else:
1920 1942 return getattr(visual, attr_name, None)
1921 1943
1922 1944
1923 1945 def get_last_path_part(file_node):
1924 1946 if not file_node.path:
1925 1947 return u''
1926 1948
1927 1949 path = safe_unicode(file_node.path.split('/')[-1])
1928 1950 return u'../' + path
1929 1951
1930 1952
1931 1953 def route_url(*args, **kwargs):
1932 1954 """
1933 1955 Wrapper around pyramids `route_url` (fully qualified url) function.
1934 1956 """
1935 1957 req = get_current_request()
1936 1958 return req.route_url(*args, **kwargs)
1937 1959
1938 1960
1939 1961 def route_path(*args, **kwargs):
1940 1962 """
1941 1963 Wrapper around pyramids `route_path` function.
1942 1964 """
1943 1965 req = get_current_request()
1944 1966 return req.route_path(*args, **kwargs)
1945 1967
1946 1968
1947 1969 def route_path_or_none(*args, **kwargs):
1948 1970 try:
1949 1971 return route_path(*args, **kwargs)
1950 1972 except KeyError:
1951 1973 return None
1952 1974
1953 1975
1954 1976 def current_route_path(request, **kw):
1955 1977 new_args = request.GET.mixed()
1956 1978 new_args.update(kw)
1957 1979 return request.current_route_path(_query=new_args)
1958 1980
1959 1981
1960 1982 def api_call_example(method, args):
1961 1983 """
1962 1984 Generates an API call example via CURL
1963 1985 """
1964 1986 args_json = json.dumps(OrderedDict([
1965 1987 ('id', 1),
1966 1988 ('auth_token', 'SECRET'),
1967 1989 ('method', method),
1968 1990 ('args', args)
1969 1991 ]))
1970 1992 return literal(
1971 1993 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
1972 1994 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
1973 1995 "and needs to be of `api calls` role."
1974 1996 .format(
1975 1997 api_url=route_url('apiv2'),
1976 1998 token_url=route_url('my_account_auth_tokens'),
1977 1999 data=args_json))
1978 2000
1979 2001
1980 2002 def notification_description(notification, request):
1981 2003 """
1982 2004 Generate notification human readable description based on notification type
1983 2005 """
1984 2006 from rhodecode.model.notification import NotificationModel
1985 2007 return NotificationModel().make_description(
1986 2008 notification, translate=request.translate)
1987 2009
1988 2010
1989 2011 def go_import_header(request, db_repo=None):
1990 2012 """
1991 2013 Creates a header for go-import functionality in Go Lang
1992 2014 """
1993 2015
1994 2016 if not db_repo:
1995 2017 return
1996 2018 if 'go-get' not in request.GET:
1997 2019 return
1998 2020
1999 2021 clone_url = db_repo.clone_url()
2000 2022 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2001 2023 # we have a repo and go-get flag,
2002 2024 return literal('<meta name="go-import" content="{} {} {}">'.format(
2003 2025 prefix, db_repo.repo_type, clone_url))
2004 2026
2005 2027
2006 2028 def reviewer_as_json(*args, **kwargs):
2007 2029 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2008 2030 return _reviewer_as_json(*args, **kwargs)
2009 2031
2010 2032
2011 2033 def get_repo_view_type(request):
2012 2034 route_name = request.matched_route.name
2013 2035 route_to_view_type = {
2014 2036 'repo_changelog': 'changelog',
2015 2037 'repo_files': 'files',
2016 2038 'repo_summary': 'summary',
2017 2039 'repo_commit': 'commit'
2018 2040
2019 2041 }
2020 2042 return route_to_view_type.get(route_name)
@@ -1,99 +1,103 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-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 Index schema for RhodeCode
23 23 """
24 24
25 25 import importlib
26 26 import logging
27 27
28 28 from rhodecode.lib.index.search_utils import normalize_text_for_matching
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32 # leave defaults for backward compat
33 33 default_searcher = 'rhodecode.lib.index.whoosh'
34 34 default_location = '%(here)s/data/index'
35 35
36 36 ES_VERSION_2 = '2'
37 37 ES_VERSION_6 = '6'
38 38 # for legacy reasons we keep 2 compat as default
39 39 DEFAULT_ES_VERSION = ES_VERSION_2
40 40
41 41 from rhodecode_tools.lib.fts_index.elasticsearch_engine_6 import \
42 42 ES_CONFIG # pragma: no cover
43 43
44 44
45 45 class BaseSearcher(object):
46 46 query_lang_doc = ''
47 47 es_version = None
48 48 name = None
49 49
50 50 def __init__(self):
51 51 pass
52 52
53 53 def cleanup(self):
54 54 pass
55 55
56 56 def search(self, query, document_type, search_user,
57 57 repo_name=None, repo_group_name=None,
58 58 raise_on_exc=True):
59 59 raise Exception('NotImplemented')
60 60
61 61 @staticmethod
62 62 def query_to_mark(query, default_field=None):
63 63 """
64 64 Formats the query to mark token for jquery.mark.js highlighting. ES could
65 65 have a different format optionally.
66 66
67 67 :param default_field:
68 68 :param query:
69 69 """
70 70 return ' '.join(normalize_text_for_matching(query).split())
71 71
72 72 @property
73 73 def is_es_6(self):
74 74 return self.es_version == ES_VERSION_6
75 75
76 76 def get_handlers(self):
77 77 return {}
78 78
79 @staticmethod
80 def extract_search_tags(query):
81 return []
82
79 83
80 84 def search_config(config, prefix='search.'):
81 85 _config = {}
82 86 for key in config.keys():
83 87 if key.startswith(prefix):
84 88 _config[key[len(prefix):]] = config[key]
85 89 return _config
86 90
87 91
88 92 def searcher_from_config(config, prefix='search.'):
89 93 _config = search_config(config, prefix)
90 94
91 95 if 'location' not in _config:
92 96 _config['location'] = default_location
93 97 if 'es_version' not in _config:
94 98 # use old legacy ES version set to 2
95 99 _config['es_version'] = '2'
96 100
97 101 imported = importlib.import_module(_config.get('module', default_searcher))
98 102 searcher = imported.Searcher(config=_config)
99 103 return searcher
@@ -1,257 +1,197 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-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 import re
21 21
22 22 import pygments.filter
23 23 import pygments.filters
24 24 from pygments.token import Comment
25 25
26 26 HL_BEG_MARKER = '__RCSearchHLMarkBEG__'
27 27 HL_END_MARKER = '__RCSearchHLMarkEND__'
28 28 HL_MARKER_RE = '{}(.*?){}'.format(HL_BEG_MARKER, HL_END_MARKER)
29 29
30 30
31 31 class ElasticSearchHLFilter(pygments.filters.Filter):
32 32 _names = [HL_BEG_MARKER, HL_END_MARKER]
33 33
34 34 def __init__(self, **options):
35 35 pygments.filters.Filter.__init__(self, **options)
36 36
37 37 def filter(self, lexer, stream):
38 38 def tokenize(_value):
39 39 for token in re.split('({}|{})'.format(
40 40 self._names[0], self._names[1]), _value):
41 41 if token:
42 42 yield token
43 43
44 44 hl = False
45 45 for ttype, value in stream:
46 46
47 47 if self._names[0] in value or self._names[1] in value:
48 48 for item in tokenize(value):
49 49 if item == self._names[0]:
50 50 # skip marker, but start HL
51 51 hl = True
52 52 continue
53 53 elif item == self._names[1]:
54 54 hl = False
55 55 continue
56 56
57 57 if hl:
58 58 yield Comment.ElasticMatch, item
59 59 else:
60 60 yield ttype, item
61 61 else:
62 62 if hl:
63 63 yield Comment.ElasticMatch, value
64 64 else:
65 65 yield ttype, value
66 66
67 67
68 68 def extract_phrases(text_query):
69 69 """
70 70 Extracts phrases from search term string making sure phrases
71 71 contained in double quotes are kept together - and discarding empty values
72 72 or fully whitespace values eg.
73 73
74 74 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
75 75
76 76 """
77 77
78 78 in_phrase = False
79 79 buf = ''
80 80 phrases = []
81 81 for char in text_query:
82 82 if in_phrase:
83 83 if char == '"': # end phrase
84 84 phrases.append(buf)
85 85 buf = ''
86 86 in_phrase = False
87 87 continue
88 88 else:
89 89 buf += char
90 90 continue
91 91 else:
92 92 if char == '"': # start phrase
93 93 in_phrase = True
94 94 phrases.append(buf)
95 95 buf = ''
96 96 continue
97 97 elif char == ' ':
98 98 phrases.append(buf)
99 99 buf = ''
100 100 continue
101 101 else:
102 102 buf += char
103 103
104 104 phrases.append(buf)
105 105 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
106 106 return phrases
107 107
108 108
109 109 def get_matching_phrase_offsets(text, phrases):
110 110 """
111 111 Returns a list of string offsets in `text` that the list of `terms` match
112 112
113 113 >>> get_matching_phrase_offsets('some text here', ['some', 'here'])
114 114 [(0, 4), (10, 14)]
115 115
116 116 """
117 117 phrases = phrases or []
118 118 offsets = []
119 119
120 120 for phrase in phrases:
121 121 for match in re.finditer(phrase, text):
122 122 offsets.append((match.start(), match.end()))
123 123
124 124 return offsets
125 125
126 126
127 127 def get_matching_markers_offsets(text, markers=None):
128 128 """
129 129 Returns a list of string offsets in `text` that the are between matching markers
130 130
131 131 >>> get_matching_markers_offsets('$1some$2 text $1here$2 marked', ['\$1(.*?)\$2'])
132 132 [(0, 5), (16, 22)]
133 133
134 134 """
135 135 markers = markers or [HL_MARKER_RE]
136 136 offsets = []
137 137
138 138 if markers:
139 139 for mark in markers:
140 140 for match in re.finditer(mark, text):
141 141 offsets.append((match.start(), match.end()))
142 142
143 143 return offsets
144 144
145 145
146 146 def normalize_text_for_matching(x):
147 147 """
148 148 Replaces all non alfanum characters to spaces and lower cases the string,
149 149 useful for comparing two text strings without punctuation
150 150 """
151 151 return re.sub(r'[^\w]', ' ', x.lower())
152 152
153 153
154 154 def get_matching_line_offsets(lines, terms=None, markers=None):
155 155 """ Return a set of `lines` indices (starting from 1) matching a
156 156 text search query, along with `context` lines above/below matching lines
157 157
158 158 :param lines: list of strings representing lines
159 159 :param terms: search term string to match in lines eg. 'some text'
160 160 :param markers: instead of terms, use highlight markers instead that
161 161 mark beginning and end for matched item. eg. ['START(.*?)END']
162 162
163 163 eg.
164 164
165 165 text = '''
166 166 words words words
167 167 words words words
168 168 some text some
169 169 words words words
170 170 words words words
171 171 text here what
172 172 '''
173 173 get_matching_line_offsets(text, 'text', context=1)
174 174 6, {3: [(5, 9)], 6: [(0, 4)]]
175 175
176 176 """
177 177 matching_lines = {}
178 178 line_index = 0
179 179
180 180 if terms:
181 181 phrases = [normalize_text_for_matching(phrase)
182 182 for phrase in extract_phrases(terms)]
183 183
184 184 for line_index, line in enumerate(lines.splitlines(), start=1):
185 185 normalized_line = normalize_text_for_matching(line)
186 186 match_offsets = get_matching_phrase_offsets(normalized_line, phrases)
187 187 if match_offsets:
188 188 matching_lines[line_index] = match_offsets
189 189
190 190 else:
191 191 markers = markers or [HL_MARKER_RE]
192 192 for line_index, line in enumerate(lines.splitlines(), start=1):
193 193 match_offsets = get_matching_markers_offsets(line, markers=markers)
194 194 if match_offsets:
195 195 matching_lines[line_index] = match_offsets
196 196
197 197 return line_index, matching_lines
198
199
200 def lucene_query_parser():
201 # from pyparsing lucene_grammar
202 from pyparsing import (
203 Literal, CaselessKeyword, Forward, Regex, QuotedString, Suppress,
204 Optional, Group, infixNotation, opAssoc, ParserElement, pyparsing_common)
205
206 ParserElement.enablePackrat()
207
208 COLON, LBRACK, RBRACK, LBRACE, RBRACE, TILDE, CARAT = map(Literal, ":[]{}~^")
209 LPAR, RPAR = map(Suppress, "()")
210 and_, or_, not_, to_ = map(CaselessKeyword, "AND OR NOT TO".split())
211 keyword = and_ | or_ | not_ | to_
212
213 expression = Forward()
214
215 valid_word = Regex(r'([a-zA-Z0-9*_+.-]|\\[!(){}\[\]^"~*?\\:])+').setName("word")
216 valid_word.setParseAction(
217 lambda t: t[0]
218 .replace('\\\\', chr(127))
219 .replace('\\', '')
220 .replace(chr(127), '\\')
221 )
222
223 string = QuotedString('"')
224
225 required_modifier = Literal("+")("required")
226 prohibit_modifier = Literal("-")("prohibit")
227 integer = Regex(r"\d+").setParseAction(lambda t: int(t[0]))
228 proximity_modifier = Group(TILDE + integer("proximity"))
229 number = pyparsing_common.fnumber()
230 fuzzy_modifier = TILDE + Optional(number, default=0.5)("fuzzy")
231
232 term = Forward()
233 field_name = valid_word().setName("fieldname")
234 incl_range_search = Group(LBRACK + term("lower") + to_ + term("upper") + RBRACK)
235 excl_range_search = Group(LBRACE + term("lower") + to_ + term("upper") + RBRACE)
236 range_search = incl_range_search("incl_range") | excl_range_search("excl_range")
237 boost = (CARAT + number("boost"))
238
239 string_expr = Group(string + proximity_modifier) | string
240 word_expr = Group(valid_word + fuzzy_modifier) | valid_word
241 term << (Optional(field_name("field") + COLON) +
242 (word_expr | string_expr | range_search | Group(
243 LPAR + expression + RPAR)) +
244 Optional(boost))
245 term.setParseAction(lambda t: [t] if 'field' in t or 'boost' in t else None)
246
247 expression << infixNotation(
248 term,
249 [
250 (required_modifier | prohibit_modifier, 1, opAssoc.RIGHT),
251 ((not_ | '!').setParseAction(lambda: "NOT"), 1, opAssoc.RIGHT),
252 ((and_ | '&&').setParseAction(lambda: "AND"), 2, opAssoc.LEFT),
253 (Optional(or_ | '||').setParseAction(lambda: "OR"), 2, opAssoc.LEFT),
254 ]
255 )
256
257 return expression
@@ -1,2470 +1,2474 b''
1 1 //Primary CSS
2 2
3 3 //--- IMPORTS ------------------//
4 4
5 5 @import 'helpers';
6 6 @import 'mixins';
7 7 @import 'rcicons';
8 8 @import 'variables';
9 9 @import 'bootstrap-variables';
10 10 @import 'form-bootstrap';
11 11 @import 'codemirror';
12 12 @import 'legacy_code_styles';
13 13 @import 'readme-box';
14 14 @import 'progress-bar';
15 15
16 16 @import 'type';
17 17 @import 'alerts';
18 18 @import 'buttons';
19 19 @import 'tags';
20 20 @import 'code-block';
21 21 @import 'examples';
22 22 @import 'login';
23 23 @import 'main-content';
24 24 @import 'select2';
25 25 @import 'comments';
26 26 @import 'panels-bootstrap';
27 27 @import 'panels';
28 28 @import 'deform';
29 29
30 30 //--- BASE ------------------//
31 31 .noscript-error {
32 32 top: 0;
33 33 left: 0;
34 34 width: 100%;
35 35 z-index: 101;
36 36 text-align: center;
37 37 font-size: 120%;
38 38 color: white;
39 39 background-color: @alert2;
40 40 padding: 5px 0 5px 0;
41 41 font-weight: @text-semibold-weight;
42 42 font-family: @text-semibold;
43 43 }
44 44
45 45 html {
46 46 display: table;
47 47 height: 100%;
48 48 width: 100%;
49 49 }
50 50
51 51 body {
52 52 display: table-cell;
53 53 width: 100%;
54 54 }
55 55
56 56 //--- LAYOUT ------------------//
57 57
58 58 .hidden{
59 59 display: none !important;
60 60 }
61 61
62 62 .box{
63 63 float: left;
64 64 width: 100%;
65 65 }
66 66
67 67 .browser-header {
68 68 clear: both;
69 69 }
70 70 .main {
71 71 clear: both;
72 72 padding:0 0 @pagepadding;
73 73 height: auto;
74 74
75 75 &:after { //clearfix
76 76 content:"";
77 77 clear:both;
78 78 width:100%;
79 79 display:block;
80 80 }
81 81 }
82 82
83 83 .action-link{
84 84 margin-left: @padding;
85 85 padding-left: @padding;
86 86 border-left: @border-thickness solid @border-default-color;
87 87 }
88 88
89 89 input + .action-link, .action-link.first{
90 90 border-left: none;
91 91 }
92 92
93 93 .action-link.last{
94 94 margin-right: @padding;
95 95 padding-right: @padding;
96 96 }
97 97
98 98 .action-link.active,
99 99 .action-link.active a{
100 100 color: @grey4;
101 101 }
102 102
103 103 .action-link.disabled {
104 104 color: @grey4;
105 105 cursor: inherit;
106 106 }
107 107
108 108 .clipboard-action {
109 109 cursor: pointer;
110 110 }
111 111
112 112 ul.simple-list{
113 113 list-style: none;
114 114 margin: 0;
115 115 padding: 0;
116 116 }
117 117
118 118 .main-content {
119 119 padding-bottom: @pagepadding;
120 120 }
121 121
122 122 .wide-mode-wrapper {
123 123 max-width:4000px !important;
124 124 }
125 125
126 126 .wrapper {
127 127 position: relative;
128 128 max-width: @wrapper-maxwidth;
129 129 margin: 0 auto;
130 130 }
131 131
132 132 #content {
133 133 clear: both;
134 134 padding: 0 @contentpadding;
135 135 }
136 136
137 137 .advanced-settings-fields{
138 138 input{
139 139 margin-left: @textmargin;
140 140 margin-right: @padding/2;
141 141 }
142 142 }
143 143
144 144 .cs_files_title {
145 145 margin: @pagepadding 0 0;
146 146 }
147 147
148 148 input.inline[type="file"] {
149 149 display: inline;
150 150 }
151 151
152 152 .error_page {
153 153 margin: 10% auto;
154 154
155 155 h1 {
156 156 color: @grey2;
157 157 }
158 158
159 159 .alert {
160 160 margin: @padding 0;
161 161 }
162 162
163 163 .error-branding {
164 164 color: @grey4;
165 165 font-weight: @text-semibold-weight;
166 166 font-family: @text-semibold;
167 167 }
168 168
169 169 .error_message {
170 170 font-family: @text-regular;
171 171 }
172 172
173 173 .sidebar {
174 174 min-height: 275px;
175 175 margin: 0;
176 176 padding: 0 0 @sidebarpadding @sidebarpadding;
177 177 border: none;
178 178 }
179 179
180 180 .main-content {
181 181 position: relative;
182 182 margin: 0 @sidebarpadding @sidebarpadding;
183 183 padding: 0 0 0 @sidebarpadding;
184 184 border-left: @border-thickness solid @grey5;
185 185
186 186 @media (max-width:767px) {
187 187 clear: both;
188 188 width: 100%;
189 189 margin: 0;
190 190 border: none;
191 191 }
192 192 }
193 193
194 194 .inner-column {
195 195 float: left;
196 196 width: 29.75%;
197 197 min-height: 150px;
198 198 margin: @sidebarpadding 2% 0 0;
199 199 padding: 0 2% 0 0;
200 200 border-right: @border-thickness solid @grey5;
201 201
202 202 @media (max-width:767px) {
203 203 clear: both;
204 204 width: 100%;
205 205 border: none;
206 206 }
207 207
208 208 ul {
209 209 padding-left: 1.25em;
210 210 }
211 211
212 212 &:last-child {
213 213 margin: @sidebarpadding 0 0;
214 214 border: none;
215 215 }
216 216
217 217 h4 {
218 218 margin: 0 0 @padding;
219 219 font-weight: @text-semibold-weight;
220 220 font-family: @text-semibold;
221 221 }
222 222 }
223 223 }
224 224 .error-page-logo {
225 225 width: 130px;
226 226 height: 160px;
227 227 }
228 228
229 229 // HEADER
230 230 .header {
231 231
232 232 // TODO: johbo: Fix login pages, so that they work without a min-height
233 233 // for the header and then remove the min-height. I chose a smaller value
234 234 // intentionally here to avoid rendering issues in the main navigation.
235 235 min-height: 49px;
236 236
237 237 position: relative;
238 238 vertical-align: bottom;
239 239 padding: 0 @header-padding;
240 240 background-color: @grey2;
241 241 color: @grey5;
242 242
243 243 .title {
244 244 overflow: visible;
245 245 }
246 246
247 247 &:before,
248 248 &:after {
249 249 content: "";
250 250 clear: both;
251 251 width: 100%;
252 252 }
253 253
254 254 // TODO: johbo: Avoids breaking "Repositories" chooser
255 255 .select2-container .select2-choice .select2-arrow {
256 256 display: none;
257 257 }
258 258 }
259 259
260 260 #header-inner {
261 261 &.title {
262 262 margin: 0;
263 263 }
264 264 &:before,
265 265 &:after {
266 266 content: "";
267 267 clear: both;
268 268 }
269 269 }
270 270
271 271 // Gists
272 272 #files_data {
273 273 clear: both; //for firefox
274 274 }
275 275 #gistid {
276 276 margin-right: @padding;
277 277 }
278 278
279 279 // Global Settings Editor
280 280 .textarea.editor {
281 281 float: left;
282 282 position: relative;
283 283 max-width: @texteditor-width;
284 284
285 285 select {
286 286 position: absolute;
287 287 top:10px;
288 288 right:0;
289 289 }
290 290
291 291 .CodeMirror {
292 292 margin: 0;
293 293 }
294 294
295 295 .help-block {
296 296 margin: 0 0 @padding;
297 297 padding:.5em;
298 298 background-color: @grey6;
299 299 &.pre-formatting {
300 300 white-space: pre;
301 301 }
302 302 }
303 303 }
304 304
305 305 ul.auth_plugins {
306 306 margin: @padding 0 @padding @legend-width;
307 307 padding: 0;
308 308
309 309 li {
310 310 margin-bottom: @padding;
311 311 line-height: 1em;
312 312 list-style-type: none;
313 313
314 314 .auth_buttons .btn {
315 315 margin-right: @padding;
316 316 }
317 317
318 318 }
319 319 }
320 320
321 321
322 322 // My Account PR list
323 323
324 324 #show_closed {
325 325 margin: 0 1em 0 0;
326 326 }
327 327
328 328 .pullrequestlist {
329 329 .closed {
330 330 background-color: @grey6;
331 331 }
332 332 .td-status {
333 333 padding-left: .5em;
334 334 }
335 335 .log-container .truncate {
336 336 height: 2.75em;
337 337 white-space: pre-line;
338 338 }
339 339 table.rctable .user {
340 340 padding-left: 0;
341 341 }
342 342 table.rctable {
343 343 td.td-description,
344 344 .rc-user {
345 345 min-width: auto;
346 346 }
347 347 }
348 348 }
349 349
350 350 // Pull Requests
351 351
352 352 .pullrequests_section_head {
353 353 display: block;
354 354 clear: both;
355 355 margin: @padding 0;
356 356 font-weight: @text-bold-weight;
357 357 font-family: @text-bold;
358 358 }
359 359
360 360 .pr-origininfo, .pr-targetinfo {
361 361 position: relative;
362 362
363 363 .tag {
364 364 display: inline-block;
365 365 margin: 0 1em .5em 0;
366 366 }
367 367
368 368 .clone-url {
369 369 display: inline-block;
370 370 margin: 0 0 .5em 0;
371 371 padding: 0;
372 372 line-height: 1.2em;
373 373 }
374 374 }
375 375
376 376 .pr-mergeinfo {
377 377 min-width: 95% !important;
378 378 padding: 0 !important;
379 379 border: 0;
380 380 }
381 381 .pr-mergeinfo-copy {
382 382 padding: 0 0;
383 383 }
384 384
385 385 .pr-pullinfo {
386 386 min-width: 95% !important;
387 387 padding: 0 !important;
388 388 border: 0;
389 389 }
390 390 .pr-pullinfo-copy {
391 391 padding: 0 0;
392 392 }
393 393
394 394
395 395 #pr-title-input {
396 396 width: 72%;
397 397 font-size: 1em;
398 398 margin: 0;
399 399 padding: 0 0 0 @padding/4;
400 400 line-height: 1.7em;
401 401 color: @text-color;
402 402 letter-spacing: .02em;
403 403 font-weight: @text-bold-weight;
404 404 font-family: @text-bold;
405 405 }
406 406
407 407 #pullrequest_title {
408 408 width: 100%;
409 409 box-sizing: border-box;
410 410 }
411 411
412 412 #pr_open_message {
413 413 border: @border-thickness solid #fff;
414 414 border-radius: @border-radius;
415 415 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
416 416 text-align: left;
417 417 overflow: hidden;
418 418 }
419 419
420 420 .pr-submit-button {
421 421 float: right;
422 422 margin: 0 0 0 5px;
423 423 }
424 424
425 425 .pr-spacing-container {
426 426 padding: 20px;
427 427 clear: both
428 428 }
429 429
430 430 #pr-description-input {
431 431 margin-bottom: 0;
432 432 }
433 433
434 434 .pr-description-label {
435 435 vertical-align: top;
436 436 }
437 437
438 438 .perms_section_head {
439 439 min-width: 625px;
440 440
441 441 h2 {
442 442 margin-bottom: 0;
443 443 }
444 444
445 445 .label-checkbox {
446 446 float: left;
447 447 }
448 448
449 449 &.field {
450 450 margin: @space 0 @padding;
451 451 }
452 452
453 453 &:first-child.field {
454 454 margin-top: 0;
455 455
456 456 .label {
457 457 margin-top: 0;
458 458 padding-top: 0;
459 459 }
460 460
461 461 .radios {
462 462 padding-top: 0;
463 463 }
464 464 }
465 465
466 466 .radios {
467 467 position: relative;
468 468 width: 505px;
469 469 }
470 470 }
471 471
472 472 //--- MODULES ------------------//
473 473
474 474
475 475 // Server Announcement
476 476 #server-announcement {
477 477 width: 95%;
478 478 margin: @padding auto;
479 479 padding: @padding;
480 480 border-width: 2px;
481 481 border-style: solid;
482 482 .border-radius(2px);
483 483 font-weight: @text-bold-weight;
484 484 font-family: @text-bold;
485 485
486 486 &.info { border-color: @alert4; background-color: @alert4-inner; }
487 487 &.warning { border-color: @alert3; background-color: @alert3-inner; }
488 488 &.error { border-color: @alert2; background-color: @alert2-inner; }
489 489 &.success { border-color: @alert1; background-color: @alert1-inner; }
490 490 &.neutral { border-color: @grey3; background-color: @grey6; }
491 491 }
492 492
493 493 // Fixed Sidebar Column
494 494 .sidebar-col-wrapper {
495 495 padding-left: @sidebar-all-width;
496 496
497 497 .sidebar {
498 498 width: @sidebar-width;
499 499 margin-left: -@sidebar-all-width;
500 500 }
501 501 }
502 502
503 503 .sidebar-col-wrapper.scw-small {
504 504 padding-left: @sidebar-small-all-width;
505 505
506 506 .sidebar {
507 507 width: @sidebar-small-width;
508 508 margin-left: -@sidebar-small-all-width;
509 509 }
510 510 }
511 511
512 512
513 513 // FOOTER
514 514 #footer {
515 515 padding: 0;
516 516 text-align: center;
517 517 vertical-align: middle;
518 518 color: @grey2;
519 519 background-color: @grey6;
520 520
521 521 p {
522 522 margin: 0;
523 523 padding: 1em;
524 524 line-height: 1em;
525 525 }
526 526
527 527 .server-instance { //server instance
528 528 display: none;
529 529 }
530 530
531 531 .title {
532 532 float: none;
533 533 margin: 0 auto;
534 534 }
535 535 }
536 536
537 537 button.close {
538 538 padding: 0;
539 539 cursor: pointer;
540 540 background: transparent;
541 541 border: 0;
542 542 .box-shadow(none);
543 543 -webkit-appearance: none;
544 544 }
545 545
546 546 .close {
547 547 float: right;
548 548 font-size: 21px;
549 549 font-family: @text-bootstrap;
550 550 line-height: 1em;
551 551 font-weight: bold;
552 552 color: @grey2;
553 553
554 554 &:hover,
555 555 &:focus {
556 556 color: @grey1;
557 557 text-decoration: none;
558 558 cursor: pointer;
559 559 }
560 560 }
561 561
562 562 // GRID
563 563 .sorting,
564 564 .sorting_desc,
565 565 .sorting_asc {
566 566 cursor: pointer;
567 567 }
568 568 .sorting_desc:after {
569 569 content: "\00A0\25B2";
570 570 font-size: .75em;
571 571 }
572 572 .sorting_asc:after {
573 573 content: "\00A0\25BC";
574 574 font-size: .68em;
575 575 }
576 576
577 577
578 578 .user_auth_tokens {
579 579
580 580 &.truncate {
581 581 white-space: nowrap;
582 582 overflow: hidden;
583 583 text-overflow: ellipsis;
584 584 }
585 585
586 586 .fields .field .input {
587 587 margin: 0;
588 588 }
589 589
590 590 input#description {
591 591 width: 100px;
592 592 margin: 0;
593 593 }
594 594
595 595 .drop-menu {
596 596 // TODO: johbo: Remove this, should work out of the box when
597 597 // having multiple inputs inline
598 598 margin: 0 0 0 5px;
599 599 }
600 600 }
601 601 #user_list_table {
602 602 .closed {
603 603 background-color: @grey6;
604 604 }
605 605 }
606 606
607 607
608 608 input {
609 609 &.disabled {
610 610 opacity: .5;
611 611 }
612 612 }
613 613
614 614 // remove extra padding in firefox
615 615 input::-moz-focus-inner { border:0; padding:0 }
616 616
617 617 .adjacent input {
618 618 margin-bottom: @padding;
619 619 }
620 620
621 621 .permissions_boxes {
622 622 display: block;
623 623 }
624 624
625 625 //FORMS
626 626
627 627 .medium-inline,
628 628 input#description.medium-inline {
629 629 display: inline;
630 630 width: @medium-inline-input-width;
631 631 min-width: 100px;
632 632 }
633 633
634 634 select {
635 635 //reset
636 636 -webkit-appearance: none;
637 637 -moz-appearance: none;
638 638
639 639 display: inline-block;
640 640 height: 28px;
641 641 width: auto;
642 642 margin: 0 @padding @padding 0;
643 643 padding: 0 18px 0 8px;
644 644 line-height:1em;
645 645 font-size: @basefontsize;
646 646 border: @border-thickness solid @rcblue;
647 647 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
648 648 color: @rcblue;
649 649
650 650 &:after {
651 651 content: "\00A0\25BE";
652 652 }
653 653
654 654 &:focus {
655 655 outline: none;
656 656 }
657 657 }
658 658
659 659 option {
660 660 &:focus {
661 661 outline: none;
662 662 }
663 663 }
664 664
665 665 input,
666 666 textarea {
667 667 padding: @input-padding;
668 668 border: @input-border-thickness solid @border-highlight-color;
669 669 .border-radius (@border-radius);
670 670 font-family: @text-light;
671 671 font-size: @basefontsize;
672 672
673 673 &.input-sm {
674 674 padding: 5px;
675 675 }
676 676
677 677 &#description {
678 678 min-width: @input-description-minwidth;
679 679 min-height: 1em;
680 680 padding: 10px;
681 681 }
682 682 }
683 683
684 684 .field-sm {
685 685 input,
686 686 textarea {
687 687 padding: 5px;
688 688 }
689 689 }
690 690
691 691 textarea {
692 692 display: block;
693 693 clear: both;
694 694 width: 100%;
695 695 min-height: 100px;
696 696 margin-bottom: @padding;
697 697 .box-sizing(border-box);
698 698 overflow: auto;
699 699 }
700 700
701 701 label {
702 702 font-family: @text-light;
703 703 }
704 704
705 705 // GRAVATARS
706 706 // centers gravatar on username to the right
707 707
708 708 .gravatar {
709 709 display: inline;
710 710 min-width: 16px;
711 711 min-height: 16px;
712 712 margin: -5px 0;
713 713 padding: 0;
714 714 line-height: 1em;
715 715 border: 1px solid @grey4;
716 716 box-sizing: content-box;
717 717
718 718 &.gravatar-large {
719 719 margin: -0.5em .25em -0.5em 0;
720 720 }
721 721
722 722 & + .user {
723 723 display: inline;
724 724 margin: 0;
725 725 padding: 0 0 0 .17em;
726 726 line-height: 1em;
727 727 }
728 728 }
729 729
730 730 .user-inline-data {
731 731 display: inline-block;
732 732 float: left;
733 733 padding-left: .5em;
734 734 line-height: 1.3em;
735 735 }
736 736
737 737 .rc-user { // gravatar + user wrapper
738 738 float: left;
739 739 position: relative;
740 740 min-width: 100px;
741 741 max-width: 200px;
742 742 min-height: (@gravatar-size + @border-thickness * 2); // account for border
743 743 display: block;
744 744 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
745 745
746 746
747 747 .gravatar {
748 748 display: block;
749 749 position: absolute;
750 750 top: 0;
751 751 left: 0;
752 752 min-width: @gravatar-size;
753 753 min-height: @gravatar-size;
754 754 margin: 0;
755 755 }
756 756
757 757 .user {
758 758 display: block;
759 759 max-width: 175px;
760 760 padding-top: 2px;
761 761 overflow: hidden;
762 762 text-overflow: ellipsis;
763 763 }
764 764 }
765 765
766 766 .gist-gravatar,
767 767 .journal_container {
768 768 .gravatar-large {
769 769 margin: 0 .5em -10px 0;
770 770 }
771 771 }
772 772
773 773
774 774 // ADMIN SETTINGS
775 775
776 776 // Tag Patterns
777 777 .tag_patterns {
778 778 .tag_input {
779 779 margin-bottom: @padding;
780 780 }
781 781 }
782 782
783 783 .locked_input {
784 784 position: relative;
785 785
786 786 input {
787 787 display: inline;
788 788 margin: 3px 5px 0px 0px;
789 789 }
790 790
791 791 br {
792 792 display: none;
793 793 }
794 794
795 795 .error-message {
796 796 float: left;
797 797 width: 100%;
798 798 }
799 799
800 800 .lock_input_button {
801 801 display: inline;
802 802 }
803 803
804 804 .help-block {
805 805 clear: both;
806 806 }
807 807 }
808 808
809 809 // Notifications
810 810
811 811 .notifications_buttons {
812 812 margin: 0 0 @space 0;
813 813 padding: 0;
814 814
815 815 .btn {
816 816 display: inline-block;
817 817 }
818 818 }
819 819
820 820 .notification-list {
821 821
822 822 div {
823 823 display: inline-block;
824 824 vertical-align: middle;
825 825 }
826 826
827 827 .container {
828 828 display: block;
829 829 margin: 0 0 @padding 0;
830 830 }
831 831
832 832 .delete-notifications {
833 833 margin-left: @padding;
834 834 text-align: right;
835 835 cursor: pointer;
836 836 }
837 837
838 838 .read-notifications {
839 839 margin-left: @padding/2;
840 840 text-align: right;
841 841 width: 35px;
842 842 cursor: pointer;
843 843 }
844 844
845 845 .icon-minus-sign {
846 846 color: @alert2;
847 847 }
848 848
849 849 .icon-ok-sign {
850 850 color: @alert1;
851 851 }
852 852 }
853 853
854 854 .user_settings {
855 855 float: left;
856 856 clear: both;
857 857 display: block;
858 858 width: 100%;
859 859
860 860 .gravatar_box {
861 861 margin-bottom: @padding;
862 862
863 863 &:after {
864 864 content: " ";
865 865 clear: both;
866 866 width: 100%;
867 867 }
868 868 }
869 869
870 870 .fields .field {
871 871 clear: both;
872 872 }
873 873 }
874 874
875 875 .advanced_settings {
876 876 margin-bottom: @space;
877 877
878 878 .help-block {
879 879 margin-left: 0;
880 880 }
881 881
882 882 button + .help-block {
883 883 margin-top: @padding;
884 884 }
885 885 }
886 886
887 887 // admin settings radio buttons and labels
888 888 .label-2 {
889 889 float: left;
890 890 width: @label2-width;
891 891
892 892 label {
893 893 color: @grey1;
894 894 }
895 895 }
896 896 .checkboxes {
897 897 float: left;
898 898 width: @checkboxes-width;
899 899 margin-bottom: @padding;
900 900
901 901 .checkbox {
902 902 width: 100%;
903 903
904 904 label {
905 905 margin: 0;
906 906 padding: 0;
907 907 }
908 908 }
909 909
910 910 .checkbox + .checkbox {
911 911 display: inline-block;
912 912 }
913 913
914 914 label {
915 915 margin-right: 1em;
916 916 }
917 917 }
918 918
919 919 // CHANGELOG
920 920 .container_header {
921 921 float: left;
922 922 display: block;
923 923 width: 100%;
924 924 margin: @padding 0 @padding;
925 925
926 926 #filter_changelog {
927 927 float: left;
928 928 margin-right: @padding;
929 929 }
930 930
931 931 .breadcrumbs_light {
932 932 display: inline-block;
933 933 }
934 934 }
935 935
936 936 .info_box {
937 937 float: right;
938 938 }
939 939
940 940
941 941 #graph_nodes {
942 942 padding-top: 43px;
943 943 }
944 944
945 945 #graph_content{
946 946
947 947 // adjust for table headers so that graph renders properly
948 948 // #graph_nodes padding - table cell padding
949 949 padding-top: (@space - (@basefontsize * 2.4));
950 950
951 951 &.graph_full_width {
952 952 width: 100%;
953 953 max-width: 100%;
954 954 }
955 955 }
956 956
957 957 #graph {
958 958 .flag_status {
959 959 margin: 0;
960 960 }
961 961
962 962 .pagination-left {
963 963 float: left;
964 964 clear: both;
965 965 }
966 966
967 967 .log-container {
968 968 max-width: 345px;
969 969
970 970 .message{
971 971 max-width: 340px;
972 972 }
973 973 }
974 974
975 975 .graph-col-wrapper {
976 976 padding-left: 110px;
977 977
978 978 #graph_nodes {
979 979 width: 100px;
980 980 margin-left: -110px;
981 981 float: left;
982 982 clear: left;
983 983 }
984 984 }
985 985
986 986 .load-more-commits {
987 987 text-align: center;
988 988 }
989 989 .load-more-commits:hover {
990 990 background-color: @grey7;
991 991 }
992 992 .load-more-commits {
993 993 a {
994 994 display: block;
995 995 }
996 996 }
997 997 }
998 998
999 999 #filter_changelog {
1000 1000 float: left;
1001 1001 }
1002 1002
1003 1003
1004 1004 //--- THEME ------------------//
1005 1005
1006 1006 #logo {
1007 1007 float: left;
1008 1008 margin: 9px 0 0 0;
1009 1009
1010 1010 .header {
1011 1011 background-color: transparent;
1012 1012 }
1013 1013
1014 1014 a {
1015 1015 display: inline-block;
1016 1016 }
1017 1017
1018 1018 img {
1019 1019 height:30px;
1020 1020 }
1021 1021 }
1022 1022
1023 1023 .logo-wrapper {
1024 1024 float:left;
1025 1025 }
1026 1026
1027 1027 .branding{
1028 1028 float: left;
1029 1029 padding: 9px 2px;
1030 1030 line-height: 1em;
1031 1031 font-size: @navigation-fontsize;
1032 1032 }
1033 1033
1034 1034 img {
1035 1035 border: none;
1036 1036 outline: none;
1037 1037 }
1038 1038 user-profile-header
1039 1039 label {
1040 1040
1041 1041 input[type="checkbox"] {
1042 1042 margin-right: 1em;
1043 1043 }
1044 1044 input[type="radio"] {
1045 1045 margin-right: 1em;
1046 1046 }
1047 1047 }
1048 1048
1049 1049 .flag_status {
1050 1050 margin: 2px 8px 6px 2px;
1051 1051 &.under_review {
1052 1052 .circle(5px, @alert3);
1053 1053 }
1054 1054 &.approved {
1055 1055 .circle(5px, @alert1);
1056 1056 }
1057 1057 &.rejected,
1058 1058 &.forced_closed{
1059 1059 .circle(5px, @alert2);
1060 1060 }
1061 1061 &.not_reviewed {
1062 1062 .circle(5px, @grey5);
1063 1063 }
1064 1064 }
1065 1065
1066 1066 .flag_status_comment_box {
1067 1067 margin: 5px 6px 0px 2px;
1068 1068 }
1069 1069 .test_pattern_preview {
1070 1070 margin: @space 0;
1071 1071
1072 1072 p {
1073 1073 margin-bottom: 0;
1074 1074 border-bottom: @border-thickness solid @border-default-color;
1075 1075 color: @grey3;
1076 1076 }
1077 1077
1078 1078 .btn {
1079 1079 margin-bottom: @padding;
1080 1080 }
1081 1081 }
1082 1082 #test_pattern_result {
1083 1083 display: none;
1084 1084 &:extend(pre);
1085 1085 padding: .9em;
1086 1086 color: @grey3;
1087 1087 background-color: @grey7;
1088 1088 border-right: @border-thickness solid @border-default-color;
1089 1089 border-bottom: @border-thickness solid @border-default-color;
1090 1090 border-left: @border-thickness solid @border-default-color;
1091 1091 }
1092 1092
1093 1093 #repo_vcs_settings {
1094 1094 #inherit_overlay_vcs_default {
1095 1095 display: none;
1096 1096 }
1097 1097 #inherit_overlay_vcs_custom {
1098 1098 display: custom;
1099 1099 }
1100 1100 &.inherited {
1101 1101 #inherit_overlay_vcs_default {
1102 1102 display: block;
1103 1103 }
1104 1104 #inherit_overlay_vcs_custom {
1105 1105 display: none;
1106 1106 }
1107 1107 }
1108 1108 }
1109 1109
1110 1110 .issue-tracker-link {
1111 1111 color: @rcblue;
1112 1112 }
1113 1113
1114 1114 // Issue Tracker Table Show/Hide
1115 1115 #repo_issue_tracker {
1116 1116 #inherit_overlay {
1117 1117 display: none;
1118 1118 }
1119 1119 #custom_overlay {
1120 1120 display: custom;
1121 1121 }
1122 1122 &.inherited {
1123 1123 #inherit_overlay {
1124 1124 display: block;
1125 1125 }
1126 1126 #custom_overlay {
1127 1127 display: none;
1128 1128 }
1129 1129 }
1130 1130 }
1131 1131 table.issuetracker {
1132 1132 &.readonly {
1133 1133 tr, td {
1134 1134 color: @grey3;
1135 1135 }
1136 1136 }
1137 1137 .edit {
1138 1138 display: none;
1139 1139 }
1140 1140 .editopen {
1141 1141 .edit {
1142 1142 display: inline;
1143 1143 }
1144 1144 .entry {
1145 1145 display: none;
1146 1146 }
1147 1147 }
1148 1148 tr td.td-action {
1149 1149 min-width: 117px;
1150 1150 }
1151 1151 td input {
1152 1152 max-width: none;
1153 1153 min-width: 30px;
1154 1154 width: 80%;
1155 1155 }
1156 1156 .issuetracker_pref input {
1157 1157 width: 40%;
1158 1158 }
1159 1159 input.edit_issuetracker_update {
1160 1160 margin-right: 0;
1161 1161 width: auto;
1162 1162 }
1163 1163 }
1164 1164
1165 1165 table.integrations {
1166 1166 .td-icon {
1167 1167 width: 20px;
1168 1168 .integration-icon {
1169 1169 height: 20px;
1170 1170 width: 20px;
1171 1171 }
1172 1172 }
1173 1173 }
1174 1174
1175 1175 .integrations {
1176 1176 a.integration-box {
1177 1177 color: @text-color;
1178 1178 &:hover {
1179 1179 .panel {
1180 1180 background: #fbfbfb;
1181 1181 }
1182 1182 }
1183 1183 .integration-icon {
1184 1184 width: 30px;
1185 1185 height: 30px;
1186 1186 margin-right: 20px;
1187 1187 float: left;
1188 1188 }
1189 1189
1190 1190 .panel-body {
1191 1191 padding: 10px;
1192 1192 }
1193 1193 .panel {
1194 1194 margin-bottom: 10px;
1195 1195 }
1196 1196 h2 {
1197 1197 display: inline-block;
1198 1198 margin: 0;
1199 1199 min-width: 140px;
1200 1200 }
1201 1201 }
1202 1202 a.integration-box.dummy-integration {
1203 1203 color: @grey4
1204 1204 }
1205 1205 }
1206 1206
1207 1207 //Permissions Settings
1208 1208 #add_perm {
1209 1209 margin: 0 0 @padding;
1210 1210 cursor: pointer;
1211 1211 }
1212 1212
1213 1213 .perm_ac {
1214 1214 input {
1215 1215 width: 95%;
1216 1216 }
1217 1217 }
1218 1218
1219 1219 .autocomplete-suggestions {
1220 1220 width: auto !important; // overrides autocomplete.js
1221 1221 margin: 0;
1222 1222 border: @border-thickness solid @rcblue;
1223 1223 border-radius: @border-radius;
1224 1224 color: @rcblue;
1225 1225 background-color: white;
1226 1226 }
1227 1227 .autocomplete-selected {
1228 1228 background: #F0F0F0;
1229 1229 }
1230 1230 .ac-container-wrap {
1231 1231 margin: 0;
1232 1232 padding: 8px;
1233 1233 border-bottom: @border-thickness solid @rclightblue;
1234 1234 list-style-type: none;
1235 1235 cursor: pointer;
1236 1236
1237 1237 &:hover {
1238 1238 background-color: @rclightblue;
1239 1239 }
1240 1240
1241 1241 img {
1242 1242 height: @gravatar-size;
1243 1243 width: @gravatar-size;
1244 1244 margin-right: 1em;
1245 1245 }
1246 1246
1247 1247 strong {
1248 1248 font-weight: normal;
1249 1249 }
1250 1250 }
1251 1251
1252 1252 // Settings Dropdown
1253 1253 .user-menu .container {
1254 1254 padding: 0 4px;
1255 1255 margin: 0;
1256 1256 }
1257 1257
1258 1258 .user-menu .gravatar {
1259 1259 cursor: pointer;
1260 1260 }
1261 1261
1262 1262 .codeblock {
1263 1263 margin-bottom: @padding;
1264 1264 clear: both;
1265 1265
1266 1266 .stats {
1267 1267 overflow: hidden;
1268 1268 }
1269 1269
1270 1270 .message{
1271 1271 textarea{
1272 1272 margin: 0;
1273 1273 }
1274 1274 }
1275 1275
1276 1276 .code-header {
1277 1277 .stats {
1278 1278 line-height: 2em;
1279 1279
1280 1280 .revision_id {
1281 1281 margin-left: 0;
1282 1282 }
1283 1283 .buttons {
1284 1284 padding-right: 0;
1285 1285 }
1286 1286 }
1287 1287
1288 1288 .item{
1289 1289 margin-right: 0.5em;
1290 1290 }
1291 1291 }
1292 1292
1293 1293 #editor_container{
1294 1294 position: relative;
1295 1295 margin: @padding;
1296 1296 }
1297 1297 }
1298 1298
1299 1299 #file_history_container {
1300 1300 display: none;
1301 1301 }
1302 1302
1303 1303 .file-history-inner {
1304 1304 margin-bottom: 10px;
1305 1305 }
1306 1306
1307 1307 // Pull Requests
1308 1308 .summary-details {
1309 1309 width: 72%;
1310 1310 }
1311 1311 .pr-summary {
1312 1312 border-bottom: @border-thickness solid @grey5;
1313 1313 margin-bottom: @space;
1314 1314 }
1315 1315 .reviewers-title {
1316 1316 width: 25%;
1317 1317 min-width: 200px;
1318 1318 }
1319 1319 .reviewers {
1320 1320 width: 25%;
1321 1321 min-width: 200px;
1322 1322 }
1323 1323 .reviewers ul li {
1324 1324 position: relative;
1325 1325 width: 100%;
1326 1326 padding-bottom: 8px;
1327 1327 list-style-type: none;
1328 1328 }
1329 1329
1330 1330 .reviewer_entry {
1331 1331 min-height: 55px;
1332 1332 }
1333 1333
1334 1334 .reviewers_member {
1335 1335 width: 100%;
1336 1336 overflow: auto;
1337 1337 }
1338 1338 .reviewer_reason {
1339 1339 padding-left: 20px;
1340 1340 line-height: 1.5em;
1341 1341 }
1342 1342 .reviewer_status {
1343 1343 display: inline-block;
1344 1344 vertical-align: top;
1345 1345 width: 25px;
1346 1346 min-width: 25px;
1347 1347 height: 1.2em;
1348 1348 margin-top: 3px;
1349 1349 line-height: 1em;
1350 1350 }
1351 1351
1352 1352 .reviewer_name {
1353 1353 display: inline-block;
1354 1354 max-width: 83%;
1355 1355 padding-right: 20px;
1356 1356 vertical-align: middle;
1357 1357 line-height: 1;
1358 1358
1359 1359 .rc-user {
1360 1360 min-width: 0;
1361 1361 margin: -2px 1em 0 0;
1362 1362 }
1363 1363
1364 1364 .reviewer {
1365 1365 float: left;
1366 1366 }
1367 1367 }
1368 1368
1369 1369 .reviewer_member_mandatory {
1370 1370 position: absolute;
1371 1371 left: 15px;
1372 1372 top: 8px;
1373 1373 width: 16px;
1374 1374 font-size: 11px;
1375 1375 margin: 0;
1376 1376 padding: 0;
1377 1377 color: black;
1378 1378 }
1379 1379
1380 1380 .reviewer_member_mandatory_remove,
1381 1381 .reviewer_member_remove {
1382 1382 position: absolute;
1383 1383 right: 0;
1384 1384 top: 0;
1385 1385 width: 16px;
1386 1386 margin-bottom: 10px;
1387 1387 padding: 0;
1388 1388 color: black;
1389 1389 }
1390 1390
1391 1391 .reviewer_member_mandatory_remove {
1392 1392 color: @grey4;
1393 1393 }
1394 1394
1395 1395 .reviewer_member_status {
1396 1396 margin-top: 5px;
1397 1397 }
1398 1398 .pr-summary #summary{
1399 1399 width: 100%;
1400 1400 }
1401 1401 .pr-summary .action_button:hover {
1402 1402 border: 0;
1403 1403 cursor: pointer;
1404 1404 }
1405 1405 .pr-details-title {
1406 1406 padding-bottom: 8px;
1407 1407 border-bottom: @border-thickness solid @grey5;
1408 1408
1409 1409 .action_button.disabled {
1410 1410 color: @grey4;
1411 1411 cursor: inherit;
1412 1412 }
1413 1413 .action_button {
1414 1414 color: @rcblue;
1415 1415 }
1416 1416 }
1417 1417 .pr-details-content {
1418 1418 margin-top: @textmargin;
1419 1419 margin-bottom: @textmargin;
1420 1420 }
1421 1421
1422 1422 .pr-reviewer-rules {
1423 1423 padding: 10px 0px 20px 0px;
1424 1424 }
1425 1425
1426 1426 .group_members {
1427 1427 margin-top: 0;
1428 1428 padding: 0;
1429 1429 list-style: outside none none;
1430 1430
1431 1431 img {
1432 1432 height: @gravatar-size;
1433 1433 width: @gravatar-size;
1434 1434 margin-right: .5em;
1435 1435 margin-left: 3px;
1436 1436 }
1437 1437
1438 1438 .to-delete {
1439 1439 .user {
1440 1440 text-decoration: line-through;
1441 1441 }
1442 1442 }
1443 1443 }
1444 1444
1445 1445 .compare_view_commits_title {
1446 1446 .disabled {
1447 1447 cursor: inherit;
1448 1448 &:hover{
1449 1449 background-color: inherit;
1450 1450 color: inherit;
1451 1451 }
1452 1452 }
1453 1453 }
1454 1454
1455 1455 .subtitle-compare {
1456 1456 margin: -15px 0px 0px 0px;
1457 1457 }
1458 1458
1459 1459 .comments-summary-td {
1460 1460 border-top: 1px dashed @grey5;
1461 1461 }
1462 1462
1463 1463 // new entry in group_members
1464 1464 .td-author-new-entry {
1465 1465 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1466 1466 }
1467 1467
1468 1468 .usergroup_member_remove {
1469 1469 width: 16px;
1470 1470 margin-bottom: 10px;
1471 1471 padding: 0;
1472 1472 color: black !important;
1473 1473 cursor: pointer;
1474 1474 }
1475 1475
1476 1476 .reviewer_ac .ac-input {
1477 1477 width: 92%;
1478 1478 margin-bottom: 1em;
1479 1479 }
1480 1480
1481 1481 .compare_view_commits tr{
1482 1482 height: 20px;
1483 1483 }
1484 1484 .compare_view_commits td {
1485 1485 vertical-align: top;
1486 1486 padding-top: 10px;
1487 1487 }
1488 1488 .compare_view_commits .author {
1489 1489 margin-left: 5px;
1490 1490 }
1491 1491
1492 1492 .compare_view_commits {
1493 1493 .color-a {
1494 1494 color: @alert1;
1495 1495 }
1496 1496
1497 1497 .color-c {
1498 1498 color: @color3;
1499 1499 }
1500 1500
1501 1501 .color-r {
1502 1502 color: @color5;
1503 1503 }
1504 1504
1505 1505 .color-a-bg {
1506 1506 background-color: @alert1;
1507 1507 }
1508 1508
1509 1509 .color-c-bg {
1510 1510 background-color: @alert3;
1511 1511 }
1512 1512
1513 1513 .color-r-bg {
1514 1514 background-color: @alert2;
1515 1515 }
1516 1516
1517 1517 .color-a-border {
1518 1518 border: 1px solid @alert1;
1519 1519 }
1520 1520
1521 1521 .color-c-border {
1522 1522 border: 1px solid @alert3;
1523 1523 }
1524 1524
1525 1525 .color-r-border {
1526 1526 border: 1px solid @alert2;
1527 1527 }
1528 1528
1529 1529 .commit-change-indicator {
1530 1530 width: 15px;
1531 1531 height: 15px;
1532 1532 position: relative;
1533 1533 left: 15px;
1534 1534 }
1535 1535
1536 1536 .commit-change-content {
1537 1537 text-align: center;
1538 1538 vertical-align: middle;
1539 1539 line-height: 15px;
1540 1540 }
1541 1541 }
1542 1542
1543 1543 .compare_view_filepath {
1544 1544 color: @grey1;
1545 1545 }
1546 1546
1547 1547 .show_more {
1548 1548 display: inline-block;
1549 1549 width: 0;
1550 1550 height: 0;
1551 1551 vertical-align: middle;
1552 1552 content: "";
1553 1553 border: 4px solid;
1554 1554 border-right-color: transparent;
1555 1555 border-bottom-color: transparent;
1556 1556 border-left-color: transparent;
1557 1557 font-size: 0;
1558 1558 }
1559 1559
1560 1560 .journal_more .show_more {
1561 1561 display: inline;
1562 1562
1563 1563 &:after {
1564 1564 content: none;
1565 1565 }
1566 1566 }
1567 1567
1568 1568 .compare_view_commits .collapse_commit:after {
1569 1569 cursor: pointer;
1570 1570 content: "\00A0\25B4";
1571 1571 margin-left: -3px;
1572 1572 font-size: 17px;
1573 1573 color: @grey4;
1574 1574 }
1575 1575
1576 1576 .diff_links {
1577 1577 margin-left: 8px;
1578 1578 }
1579 1579
1580 1580 div.ancestor {
1581 1581 margin: -30px 0px;
1582 1582 }
1583 1583
1584 1584 .cs_icon_td input[type="checkbox"] {
1585 1585 display: none;
1586 1586 }
1587 1587
1588 1588 .cs_icon_td .expand_file_icon:after {
1589 1589 cursor: pointer;
1590 1590 content: "\00A0\25B6";
1591 1591 font-size: 12px;
1592 1592 color: @grey4;
1593 1593 }
1594 1594
1595 1595 .cs_icon_td .collapse_file_icon:after {
1596 1596 cursor: pointer;
1597 1597 content: "\00A0\25BC";
1598 1598 font-size: 12px;
1599 1599 color: @grey4;
1600 1600 }
1601 1601
1602 1602 /*new binary
1603 1603 NEW_FILENODE = 1
1604 1604 DEL_FILENODE = 2
1605 1605 MOD_FILENODE = 3
1606 1606 RENAMED_FILENODE = 4
1607 1607 COPIED_FILENODE = 5
1608 1608 CHMOD_FILENODE = 6
1609 1609 BIN_FILENODE = 7
1610 1610 */
1611 1611 .cs_files_expand {
1612 1612 font-size: @basefontsize + 5px;
1613 1613 line-height: 1.8em;
1614 1614 float: right;
1615 1615 }
1616 1616
1617 1617 .cs_files_expand span{
1618 1618 color: @rcblue;
1619 1619 cursor: pointer;
1620 1620 }
1621 1621 .cs_files {
1622 1622 clear: both;
1623 1623 padding-bottom: @padding;
1624 1624
1625 1625 .cur_cs {
1626 1626 margin: 10px 2px;
1627 1627 font-weight: bold;
1628 1628 }
1629 1629
1630 1630 .node {
1631 1631 float: left;
1632 1632 }
1633 1633
1634 1634 .changes {
1635 1635 float: right;
1636 1636 color: white;
1637 1637 font-size: @basefontsize - 4px;
1638 1638 margin-top: 4px;
1639 1639 opacity: 0.6;
1640 1640 filter: Alpha(opacity=60); /* IE8 and earlier */
1641 1641
1642 1642 .added {
1643 1643 background-color: @alert1;
1644 1644 float: left;
1645 1645 text-align: center;
1646 1646 }
1647 1647
1648 1648 .deleted {
1649 1649 background-color: @alert2;
1650 1650 float: left;
1651 1651 text-align: center;
1652 1652 }
1653 1653
1654 1654 .bin {
1655 1655 background-color: @alert1;
1656 1656 text-align: center;
1657 1657 }
1658 1658
1659 1659 /*new binary*/
1660 1660 .bin.bin1 {
1661 1661 background-color: @alert1;
1662 1662 text-align: center;
1663 1663 }
1664 1664
1665 1665 /*deleted binary*/
1666 1666 .bin.bin2 {
1667 1667 background-color: @alert2;
1668 1668 text-align: center;
1669 1669 }
1670 1670
1671 1671 /*mod binary*/
1672 1672 .bin.bin3 {
1673 1673 background-color: @grey2;
1674 1674 text-align: center;
1675 1675 }
1676 1676
1677 1677 /*rename file*/
1678 1678 .bin.bin4 {
1679 1679 background-color: @alert4;
1680 1680 text-align: center;
1681 1681 }
1682 1682
1683 1683 /*copied file*/
1684 1684 .bin.bin5 {
1685 1685 background-color: @alert4;
1686 1686 text-align: center;
1687 1687 }
1688 1688
1689 1689 /*chmod file*/
1690 1690 .bin.bin6 {
1691 1691 background-color: @grey2;
1692 1692 text-align: center;
1693 1693 }
1694 1694 }
1695 1695 }
1696 1696
1697 1697 .cs_files .cs_added, .cs_files .cs_A,
1698 1698 .cs_files .cs_added, .cs_files .cs_M,
1699 1699 .cs_files .cs_added, .cs_files .cs_D {
1700 1700 height: 16px;
1701 1701 padding-right: 10px;
1702 1702 margin-top: 7px;
1703 1703 text-align: left;
1704 1704 }
1705 1705
1706 1706 .cs_icon_td {
1707 1707 min-width: 16px;
1708 1708 width: 16px;
1709 1709 }
1710 1710
1711 1711 .pull-request-merge {
1712 1712 border: 1px solid @grey5;
1713 1713 padding: 10px 0px 20px;
1714 1714 margin-top: 10px;
1715 1715 margin-bottom: 20px;
1716 1716 }
1717 1717
1718 1718 .pull-request-merge ul {
1719 1719 padding: 0px 0px;
1720 1720 }
1721 1721
1722 1722 .pull-request-merge li {
1723 1723 list-style-type: none;
1724 1724 }
1725 1725
1726 1726 .pull-request-merge .pull-request-wrap {
1727 1727 height: auto;
1728 1728 padding: 0px 0px;
1729 1729 text-align: right;
1730 1730 }
1731 1731
1732 1732 .pull-request-merge span {
1733 1733 margin-right: 5px;
1734 1734 }
1735 1735
1736 1736 .pull-request-merge-actions {
1737 1737 min-height: 30px;
1738 1738 padding: 0px 0px;
1739 1739 }
1740 1740
1741 1741 .pull-request-merge-info {
1742 1742 padding: 0px 5px 5px 0px;
1743 1743 }
1744 1744
1745 1745 .merge-status {
1746 1746 margin-right: 5px;
1747 1747 }
1748 1748
1749 1749 .merge-message {
1750 1750 font-size: 1.2em
1751 1751 }
1752 1752
1753 1753 .merge-message.success i,
1754 1754 .merge-icon.success i {
1755 1755 color:@alert1;
1756 1756 }
1757 1757
1758 1758 .merge-message.warning i,
1759 1759 .merge-icon.warning i {
1760 1760 color: @alert3;
1761 1761 }
1762 1762
1763 1763 .merge-message.error i,
1764 1764 .merge-icon.error i {
1765 1765 color:@alert2;
1766 1766 }
1767 1767
1768 1768 .pr-versions {
1769 1769 font-size: 1.1em;
1770 1770
1771 1771 table {
1772 1772 padding: 0px 5px;
1773 1773 }
1774 1774
1775 1775 td {
1776 1776 line-height: 15px;
1777 1777 }
1778 1778
1779 1779 .flag_status {
1780 1780 margin: 0;
1781 1781 }
1782 1782
1783 1783 .compare-radio-button {
1784 1784 position: relative;
1785 1785 top: -3px;
1786 1786 }
1787 1787 }
1788 1788
1789 1789
1790 1790 #close_pull_request {
1791 1791 margin-right: 0px;
1792 1792 }
1793 1793
1794 1794 .empty_data {
1795 1795 color: @grey4;
1796 1796 }
1797 1797
1798 1798 #changeset_compare_view_content {
1799 1799 margin-bottom: @space;
1800 1800 clear: both;
1801 1801 width: 100%;
1802 1802 box-sizing: border-box;
1803 1803 .border-radius(@border-radius);
1804 1804
1805 1805 .help-block {
1806 1806 margin: @padding 0;
1807 1807 color: @text-color;
1808 1808 &.pre-formatting {
1809 1809 white-space: pre;
1810 1810 }
1811 1811 }
1812 1812
1813 1813 .empty_data {
1814 1814 margin: @padding 0;
1815 1815 }
1816 1816
1817 1817 .alert {
1818 1818 margin-bottom: @space;
1819 1819 }
1820 1820 }
1821 1821
1822 1822 .table_disp {
1823 1823 .status {
1824 1824 width: auto;
1825 1825
1826 1826 .flag_status {
1827 1827 float: left;
1828 1828 }
1829 1829 }
1830 1830 }
1831 1831
1832 1832
1833 1833 .creation_in_progress {
1834 1834 color: @grey4
1835 1835 }
1836 1836
1837 1837 .status_box_menu {
1838 1838 margin: 0;
1839 1839 }
1840 1840
1841 1841 .notification-table{
1842 1842 margin-bottom: @space;
1843 1843 display: table;
1844 1844 width: 100%;
1845 1845
1846 1846 .container{
1847 1847 display: table-row;
1848 1848
1849 1849 .notification-header{
1850 1850 border-bottom: @border-thickness solid @border-default-color;
1851 1851 }
1852 1852
1853 1853 .notification-subject{
1854 1854 display: table-cell;
1855 1855 }
1856 1856 }
1857 1857 }
1858 1858
1859 1859 // Notifications
1860 1860 .notification-header{
1861 1861 display: table;
1862 1862 width: 100%;
1863 1863 padding: floor(@basefontsize/2) 0;
1864 1864 line-height: 1em;
1865 1865
1866 1866 .desc, .delete-notifications, .read-notifications{
1867 1867 display: table-cell;
1868 1868 text-align: left;
1869 1869 }
1870 1870
1871 1871 .desc{
1872 1872 width: 1163px;
1873 1873 }
1874 1874
1875 1875 .delete-notifications, .read-notifications{
1876 1876 width: 35px;
1877 1877 min-width: 35px; //fixes when only one button is displayed
1878 1878 }
1879 1879 }
1880 1880
1881 1881 .notification-body {
1882 1882 .markdown-block,
1883 1883 .rst-block {
1884 1884 padding: @padding 0;
1885 1885 }
1886 1886
1887 1887 .notification-subject {
1888 1888 padding: @textmargin 0;
1889 1889 border-bottom: @border-thickness solid @border-default-color;
1890 1890 }
1891 1891 }
1892 1892
1893 1893
1894 1894 .notifications_buttons{
1895 1895 float: right;
1896 1896 }
1897 1897
1898 1898 #notification-status{
1899 1899 display: inline;
1900 1900 }
1901 1901
1902 1902 // Repositories
1903 1903
1904 1904 #summary.fields{
1905 1905 display: table;
1906 1906
1907 1907 .field{
1908 1908 display: table-row;
1909 1909
1910 1910 .label-summary{
1911 1911 display: table-cell;
1912 1912 min-width: @label-summary-minwidth;
1913 1913 padding-top: @padding/2;
1914 1914 padding-bottom: @padding/2;
1915 1915 padding-right: @padding/2;
1916 1916 }
1917 1917
1918 1918 .input{
1919 1919 display: table-cell;
1920 1920 padding: @padding/2;
1921 1921
1922 1922 input{
1923 1923 min-width: 29em;
1924 1924 padding: @padding/4;
1925 1925 }
1926 1926 }
1927 1927 .statistics, .downloads{
1928 1928 .disabled{
1929 1929 color: @grey4;
1930 1930 }
1931 1931 }
1932 1932 }
1933 1933 }
1934 1934
1935 1935 #summary{
1936 1936 width: 70%;
1937 1937 }
1938 1938
1939 1939
1940 1940 // Journal
1941 1941 .journal.title {
1942 1942 h5 {
1943 1943 float: left;
1944 1944 margin: 0;
1945 1945 width: 70%;
1946 1946 }
1947 1947
1948 1948 ul {
1949 1949 float: right;
1950 1950 display: inline-block;
1951 1951 margin: 0;
1952 1952 width: 30%;
1953 1953 text-align: right;
1954 1954
1955 1955 li {
1956 1956 display: inline;
1957 1957 font-size: @journal-fontsize;
1958 1958 line-height: 1em;
1959 1959
1960 1960 list-style-type: none;
1961 1961 }
1962 1962 }
1963 1963 }
1964 1964
1965 1965 .filterexample {
1966 1966 position: absolute;
1967 1967 top: 95px;
1968 1968 left: @contentpadding;
1969 1969 color: @rcblue;
1970 1970 font-size: 11px;
1971 1971 font-family: @text-regular;
1972 1972 cursor: help;
1973 1973
1974 1974 &:hover {
1975 1975 color: @rcdarkblue;
1976 1976 }
1977 1977
1978 1978 @media (max-width:768px) {
1979 1979 position: relative;
1980 1980 top: auto;
1981 1981 left: auto;
1982 1982 display: block;
1983 1983 }
1984 1984 }
1985 1985
1986 1986
1987 1987 #journal{
1988 1988 margin-bottom: @space;
1989 1989
1990 1990 .journal_day{
1991 1991 margin-bottom: @textmargin/2;
1992 1992 padding-bottom: @textmargin/2;
1993 1993 font-size: @journal-fontsize;
1994 1994 border-bottom: @border-thickness solid @border-default-color;
1995 1995 }
1996 1996
1997 1997 .journal_container{
1998 1998 margin-bottom: @space;
1999 1999
2000 2000 .journal_user{
2001 2001 display: inline-block;
2002 2002 }
2003 2003 .journal_action_container{
2004 2004 display: block;
2005 2005 margin-top: @textmargin;
2006 2006
2007 2007 div{
2008 2008 display: inline;
2009 2009 }
2010 2010
2011 2011 div.journal_action_params{
2012 2012 display: block;
2013 2013 }
2014 2014
2015 2015 div.journal_repo:after{
2016 2016 content: "\A";
2017 2017 white-space: pre;
2018 2018 }
2019 2019
2020 2020 div.date{
2021 2021 display: block;
2022 2022 margin-bottom: @textmargin;
2023 2023 }
2024 2024 }
2025 2025 }
2026 2026 }
2027 2027
2028 2028 // Files
2029 2029 .edit-file-title {
2030 2030 border-bottom: @border-thickness solid @border-default-color;
2031 2031
2032 2032 .breadcrumbs {
2033 2033 margin-bottom: 0;
2034 2034 }
2035 2035 }
2036 2036
2037 2037 .edit-file-fieldset {
2038 2038 margin-top: @sidebarpadding;
2039 2039
2040 2040 .fieldset {
2041 2041 .left-label {
2042 2042 width: 13%;
2043 2043 }
2044 2044 .right-content {
2045 2045 width: 87%;
2046 2046 max-width: 100%;
2047 2047 }
2048 2048 .filename-label {
2049 2049 margin-top: 13px;
2050 2050 }
2051 2051 .commit-message-label {
2052 2052 margin-top: 4px;
2053 2053 }
2054 2054 .file-upload-input {
2055 2055 input {
2056 2056 display: none;
2057 2057 }
2058 2058 margin-top: 10px;
2059 2059 }
2060 2060 .file-upload-label {
2061 2061 margin-top: 10px;
2062 2062 }
2063 2063 p {
2064 2064 margin-top: 5px;
2065 2065 }
2066 2066
2067 2067 }
2068 2068 .custom-path-link {
2069 2069 margin-left: 5px;
2070 2070 }
2071 2071 #commit {
2072 2072 resize: vertical;
2073 2073 }
2074 2074 }
2075 2075
2076 2076 .delete-file-preview {
2077 2077 max-height: 250px;
2078 2078 }
2079 2079
2080 2080 .new-file,
2081 2081 #filter_activate,
2082 2082 #filter_deactivate {
2083 2083 float: left;
2084 2084 margin: 0 0 0 15px;
2085 2085 }
2086 2086
2087 2087 h3.files_location{
2088 2088 line-height: 2.4em;
2089 2089 }
2090 2090
2091 2091 .browser-nav {
2092 2092 display: table;
2093 2093 margin-bottom: @space;
2094 2094
2095 2095
2096 2096 .info_box {
2097 2097 display: inline-table;
2098 2098 height: 2.5em;
2099 2099
2100 2100 .browser-cur-rev, .info_box_elem {
2101 2101 display: table-cell;
2102 2102 vertical-align: middle;
2103 2103 }
2104 2104
2105 2105 .info_box_elem {
2106 2106 border-top: @border-thickness solid @rcblue;
2107 2107 border-bottom: @border-thickness solid @rcblue;
2108 2108
2109 2109 #at_rev, a {
2110 2110 padding: 0.6em 0.9em;
2111 2111 margin: 0;
2112 2112 .box-shadow(none);
2113 2113 border: 0;
2114 2114 height: 12px;
2115 2115 }
2116 2116
2117 2117 input#at_rev {
2118 2118 max-width: 50px;
2119 2119 text-align: right;
2120 2120 }
2121 2121
2122 2122 &.previous {
2123 2123 border: @border-thickness solid @rcblue;
2124 2124 .disabled {
2125 2125 color: @grey4;
2126 2126 cursor: not-allowed;
2127 2127 }
2128 2128 }
2129 2129
2130 2130 &.next {
2131 2131 border: @border-thickness solid @rcblue;
2132 2132 .disabled {
2133 2133 color: @grey4;
2134 2134 cursor: not-allowed;
2135 2135 }
2136 2136 }
2137 2137 }
2138 2138
2139 2139 .browser-cur-rev {
2140 2140
2141 2141 span{
2142 2142 margin: 0;
2143 2143 color: @rcblue;
2144 2144 height: 12px;
2145 2145 display: inline-block;
2146 2146 padding: 0.7em 1em ;
2147 2147 border: @border-thickness solid @rcblue;
2148 2148 margin-right: @padding;
2149 2149 }
2150 2150 }
2151 2151 }
2152 2152
2153 2153 .search_activate {
2154 2154 display: table-cell;
2155 2155 vertical-align: middle;
2156 2156
2157 2157 input, label{
2158 2158 margin: 0;
2159 2159 padding: 0;
2160 2160 }
2161 2161
2162 2162 input{
2163 2163 margin-left: @textmargin;
2164 2164 }
2165 2165
2166 2166 }
2167 2167 }
2168 2168
2169 2169 .browser-cur-rev{
2170 2170 margin-bottom: @textmargin;
2171 2171 }
2172 2172
2173 2173 #node_filter_box_loading{
2174 2174 .info_text;
2175 2175 }
2176 2176
2177 2177 .browser-search {
2178 2178 margin: -25px 0px 5px 0px;
2179 2179 }
2180 2180
2181 2181 .node-filter {
2182 2182 font-size: @repo-title-fontsize;
2183 2183 padding: 4px 0px 0px 0px;
2184 2184
2185 2185 .node-filter-path {
2186 2186 float: left;
2187 2187 color: @grey4;
2188 2188 }
2189 2189 .node-filter-input {
2190 2190 float: left;
2191 2191 margin: -2px 0px 0px 2px;
2192 2192 input {
2193 2193 padding: 2px;
2194 2194 border: none;
2195 2195 font-size: @repo-title-fontsize;
2196 2196 }
2197 2197 }
2198 2198 }
2199 2199
2200 2200
2201 2201 .browser-result{
2202 2202 td a{
2203 2203 margin-left: 0.5em;
2204 2204 display: inline-block;
2205 2205
2206 2206 em {
2207 2207 font-weight: @text-bold-weight;
2208 2208 font-family: @text-bold;
2209 2209 }
2210 2210 }
2211 2211 }
2212 2212
2213 2213 .browser-highlight{
2214 2214 background-color: @grey5-alpha;
2215 2215 }
2216 2216
2217 2217
2218 2218 // Search
2219 2219
2220 2220 .search-form{
2221 2221 #q {
2222 2222 width: @search-form-width;
2223 2223 }
2224 2224 .fields{
2225 2225 margin: 0 0 @space;
2226 2226 }
2227 2227
2228 2228 label{
2229 2229 display: inline-block;
2230 2230 margin-right: @textmargin;
2231 2231 padding-top: 0.25em;
2232 2232 }
2233 2233
2234 2234
2235 2235 .results{
2236 2236 clear: both;
2237 2237 margin: 0 0 @padding;
2238 2238 }
2239
2240 .search-tags {
2241 padding: 5px 0;
2242 }
2239 2243 }
2240 2244
2241 2245 div.search-feedback-items {
2242 2246 display: inline-block;
2243 2247 }
2244 2248
2245 2249 div.search-code-body {
2246 2250 background-color: #ffffff; padding: 5px 0 5px 10px;
2247 2251 pre {
2248 2252 .match { background-color: #faffa6;}
2249 2253 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2250 2254 }
2251 2255 }
2252 2256
2253 2257 .expand_commit.search {
2254 2258 .show_more.open {
2255 2259 height: auto;
2256 2260 max-height: none;
2257 2261 }
2258 2262 }
2259 2263
2260 2264 .search-results {
2261 2265
2262 2266 h2 {
2263 2267 margin-bottom: 0;
2264 2268 }
2265 2269 .codeblock {
2266 2270 border: none;
2267 2271 background: transparent;
2268 2272 }
2269 2273
2270 2274 .codeblock-header {
2271 2275 border: none;
2272 2276 background: transparent;
2273 2277 }
2274 2278
2275 2279 .code-body {
2276 2280 border: @border-thickness solid @border-default-color;
2277 2281 .border-radius(@border-radius);
2278 2282 }
2279 2283
2280 2284 .td-commit {
2281 2285 &:extend(pre);
2282 2286 border-bottom: @border-thickness solid @border-default-color;
2283 2287 }
2284 2288
2285 2289 .message {
2286 2290 height: auto;
2287 2291 max-width: 350px;
2288 2292 white-space: normal;
2289 2293 text-overflow: initial;
2290 2294 overflow: visible;
2291 2295
2292 2296 .match { background-color: #faffa6;}
2293 2297 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2294 2298 }
2295 2299
2296 2300 }
2297 2301
2298 2302 table.rctable td.td-search-results div {
2299 2303 max-width: 100%;
2300 2304 }
2301 2305
2302 2306 #tip-box, .tip-box{
2303 2307 padding: @menupadding/2;
2304 2308 display: block;
2305 2309 border: @border-thickness solid @border-highlight-color;
2306 2310 .border-radius(@border-radius);
2307 2311 background-color: white;
2308 2312 z-index: 99;
2309 2313 white-space: pre-wrap;
2310 2314 }
2311 2315
2312 2316 #linktt {
2313 2317 width: 79px;
2314 2318 }
2315 2319
2316 2320 #help_kb .modal-content{
2317 2321 max-width: 750px;
2318 2322 margin: 10% auto;
2319 2323
2320 2324 table{
2321 2325 td,th{
2322 2326 border-bottom: none;
2323 2327 line-height: 2.5em;
2324 2328 }
2325 2329 th{
2326 2330 padding-bottom: @textmargin/2;
2327 2331 }
2328 2332 td.keys{
2329 2333 text-align: center;
2330 2334 }
2331 2335 }
2332 2336
2333 2337 .block-left{
2334 2338 width: 45%;
2335 2339 margin-right: 5%;
2336 2340 }
2337 2341 .modal-footer{
2338 2342 clear: both;
2339 2343 }
2340 2344 .key.tag{
2341 2345 padding: 0.5em;
2342 2346 background-color: @rcblue;
2343 2347 color: white;
2344 2348 border-color: @rcblue;
2345 2349 .box-shadow(none);
2346 2350 }
2347 2351 }
2348 2352
2349 2353
2350 2354
2351 2355 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2352 2356
2353 2357 @import 'statistics-graph';
2354 2358 @import 'tables';
2355 2359 @import 'forms';
2356 2360 @import 'diff';
2357 2361 @import 'summary';
2358 2362 @import 'navigation';
2359 2363
2360 2364 //--- SHOW/HIDE SECTIONS --//
2361 2365
2362 2366 .btn-collapse {
2363 2367 float: right;
2364 2368 text-align: right;
2365 2369 font-family: @text-light;
2366 2370 font-size: @basefontsize;
2367 2371 cursor: pointer;
2368 2372 border: none;
2369 2373 color: @rcblue;
2370 2374 }
2371 2375
2372 2376 table.rctable,
2373 2377 table.dataTable {
2374 2378 .btn-collapse {
2375 2379 float: right;
2376 2380 text-align: right;
2377 2381 }
2378 2382 }
2379 2383
2380 2384 table.rctable {
2381 2385 &.permissions {
2382 2386
2383 2387 th.td-owner {
2384 2388 padding: 0;
2385 2389 }
2386 2390
2387 2391 th {
2388 2392 font-weight: normal;
2389 2393 padding: 0 5px;
2390 2394 }
2391 2395
2392 2396 }
2393 2397 }
2394 2398
2395 2399
2396 2400 // TODO: johbo: Fix for IE10, this avoids that we see a border
2397 2401 // and padding around checkboxes and radio boxes. Move to the right place,
2398 2402 // or better: Remove this once we did the form refactoring.
2399 2403 input[type=checkbox],
2400 2404 input[type=radio] {
2401 2405 padding: 0;
2402 2406 border: none;
2403 2407 }
2404 2408
2405 2409 .toggle-ajax-spinner{
2406 2410 height: 16px;
2407 2411 width: 16px;
2408 2412 }
2409 2413
2410 2414
2411 2415 .markup-form .clearfix {
2412 2416 .border-radius(@border-radius);
2413 2417 margin: 0px;
2414 2418 }
2415 2419
2416 2420 .markup-form-area {
2417 2421 padding: 8px 12px;
2418 2422 border: 1px solid @grey4;
2419 2423 .border-radius(@border-radius);
2420 2424 }
2421 2425
2422 2426 .markup-form-area-header .nav-links {
2423 2427 display: flex;
2424 2428 flex-flow: row wrap;
2425 2429 -webkit-flex-flow: row wrap;
2426 2430 width: 100%;
2427 2431 }
2428 2432
2429 2433 .markup-form-area-footer {
2430 2434 display: flex;
2431 2435 }
2432 2436
2433 2437 .markup-form-area-footer .toolbar {
2434 2438
2435 2439 }
2436 2440
2437 2441 // markup Form
2438 2442 div.markup-form {
2439 2443 margin-top: 20px;
2440 2444 }
2441 2445
2442 2446 .markup-form strong {
2443 2447 display: block;
2444 2448 margin-bottom: 15px;
2445 2449 }
2446 2450
2447 2451 .markup-form textarea {
2448 2452 width: 100%;
2449 2453 height: 100px;
2450 2454 font-family: @text-monospace;
2451 2455 }
2452 2456
2453 2457 form.markup-form {
2454 2458 margin-top: 10px;
2455 2459 margin-left: 10px;
2456 2460 }
2457 2461
2458 2462 .markup-form .comment-block-ta,
2459 2463 .markup-form .preview-box {
2460 2464 .border-radius(@border-radius);
2461 2465 .box-sizing(border-box);
2462 2466 background-color: white;
2463 2467 }
2464 2468
2465 2469 .markup-form .preview-box.unloaded {
2466 2470 height: 50px;
2467 2471 text-align: center;
2468 2472 padding: 20px;
2469 2473 background-color: white;
2470 2474 }
@@ -1,548 +1,552 b''
1 1 //
2 2 // Typography
3 3 // modified from Bootstrap
4 4 // --------------------------------------------------
5 5
6 6 // Base
7 7 body {
8 8 font-size: @basefontsize;
9 9 font-family: @text-light;
10 10 letter-spacing: .02em;
11 11 color: @grey2;
12 12 }
13 13
14 14 #content, label{
15 15 font-size: @basefontsize;
16 16 }
17 17
18 18 label {
19 19 color: @grey2;
20 20 }
21 21
22 22 ::selection { background: @rchighlightblue; }
23 23
24 24 // Headings
25 25 // -------------------------
26 26
27 27 h1, h2, h3, h4, h5, h6,
28 28 .h1, .h2, .h3, .h4, .h5, .h6 {
29 29 margin: 0 0 @textmargin 0;
30 30 padding: 0;
31 31 line-height: 1.8em;
32 32 color: @text-color;
33 33 a {
34 34 color: @rcblue;
35 35 }
36 36 }
37 37
38 38 h1, .h1 { font-size: 1.54em; font-weight: @text-bold-weight; font-family: @text-bold; }
39 39 h2, .h2 { font-size: 1.23em; font-weight: @text-semibold-weight; font-family: @text-semibold; }
40 40 h3, .h3 { font-size: 1.23em; font-family: @text-regular; }
41 41 h4, .h4 { font-size: 1em; font-weight: @text-bold-weight; font-family: @text-bold; }
42 42 h5, .h5 { font-size: 1em; font-weight: @text-bold-weight; font-family: @text-bold; }
43 43 h6, .h6 { font-size: 1em; font-weight: @text-bold-weight; font-family: @text-bold; }
44 44
45 45 // Breadcrumbs
46 46 .breadcrumbs {
47 47 font-size: @repo-title-fontsize;
48 48 margin: 0;
49 49 }
50 50
51 51 .breadcrumbs_light {
52 52 float:left;
53 53 font-size: 1.3em;
54 54 line-height: 38px;
55 55 }
56 56
57 57 // Body text
58 58 // -------------------------
59 59
60 60 p {
61 61 margin: 0 0 @textmargin 0;
62 62 padding: 0;
63 63 line-height: 2em;
64 64 }
65 65
66 66 .lead {
67 67 margin-bottom: @textmargin;
68 68 font-weight: 300;
69 69 line-height: 1.4;
70 70
71 71 @media (min-width: @screen-sm-min) {
72 72 font-size: (@basefontsize * 1.5);
73 73 }
74 74 }
75 75
76 76 a,
77 77 .link {
78 78 color: @rcblue;
79 79 text-decoration: none;
80 80 outline: none;
81 81 cursor: pointer;
82 82
83 83 &:focus {
84 84 outline: none;
85 85 }
86 86
87 87 &:hover {
88 88 color: @rcdarkblue;
89 89 }
90 90 }
91 91
92 92 img {
93 93 border: none;
94 94 outline: none;
95 95 }
96 96
97 97 strong {
98 98 font-weight: @text-bold-weight;
99 99 font-family: @text-bold;
100 100 }
101 101
102 102 em {
103 103 font-family: @text-italic;
104 104 font-style: italic;
105 105 }
106 106
107 107 strong em,
108 108 em strong {
109 109 font-style: italic;
110 110 font-weight: @text-bold-italic-weight;
111 111 font-family: @text-bold-italic;
112 112 }
113 113
114 114 //TODO: lisa: b and i are depreciated, but we are still using them in places.
115 115 // Should probably make some decision whether to keep or lose these.
116 116 b {
117 117
118 118 }
119 119
120 120 i {
121 121 font-style: normal;
122 122 }
123 123
124 124 label {
125 125 color: @text-color;
126 126
127 127 input[type="checkbox"] {
128 128 margin-right: 1em;
129 129 }
130 130 input[type="radio"] {
131 131 margin-right: 1em;
132 132 }
133 133 }
134 134
135 135 code,
136 136 .code {
137 137 font-size: .95em;
138 138 font-family: @text-code;
139 139 color: @grey3;
140 140
141 141 a {
142 142 color: lighten(@rcblue,10%)
143 143 }
144 144 }
145 145
146 146 pre {
147 147 margin: 0;
148 148 padding: 0;
149 149 border: 0;
150 150 outline: 0;
151 151 font-size: @basefontsize*.95;
152 152 line-height: 1.4em;
153 153 font-family: @text-code;
154 154 color: @grey3;
155 155 }
156 156
157 157 // Emphasis & misc
158 158 // -------------------------
159 159
160 160 small,
161 161 .small {
162 162 font-size: 75%;
163 163 font-weight: normal;
164 164 line-height: 1em;
165 165 }
166 166
167 167 mark,
168 168 .mark {
169 169 padding: .2em;
170 170 }
171 171
172 172 // Alignment
173 173 .text-left { text-align: left; }
174 174 .text-right { text-align: right; }
175 175 .text-center { text-align: center; }
176 176 .text-justify { text-align: justify; }
177 177 .text-nowrap { white-space: nowrap; }
178 178
179 179 // Transformation
180 180 .text-lowercase { text-transform: lowercase; }
181 181 .text-uppercase { text-transform: uppercase; }
182 182 .text-capitalize { text-transform: capitalize; }
183 183
184 184 // Contextual colors
185 185 .text-muted {
186 186 color: @grey4;
187 187 }
188 188 .text-primary {
189 189 color: @rcblue;
190 190 }
191 191 .text-success {
192 192 color: @alert1;
193 193 }
194 194 .text-info {
195 195 color: @alert4;
196 196 }
197 197 .text-warning {
198 198 color: @alert3;
199 199 }
200 200 .text-danger {
201 201 color: @alert2;
202 202 }
203 203
204 204 // Contextual backgrounds
205 205 .bg-primary {
206 206 background-color: white;
207 207 }
208 208 .bg-success {
209 209 background-color: @alert1;
210 210 }
211 211 .bg-info {
212 212 background-color: @alert4;
213 213 }
214 214 .bg-warning {
215 215 background-color: @alert3;
216 216 }
217 217 .bg-danger {
218 218 background-color: @alert2;
219 219 }
220 220
221 221
222 222 // Page header
223 223 // -------------------------
224 224
225 225 .page-header {
226 226 margin: @pagepadding 0 @textmargin;
227 227 border-bottom: @border-thickness solid @grey5;
228 228 }
229 229
230 230 .title {
231 231 clear: both;
232 232 float: left;
233 233 width: 100%;
234 234 margin: @pagepadding/2 0 @pagepadding;
235 235
236 236 .breadcrumbs {
237 237 float: left;
238 238 clear: both;
239 239 width: 700px;
240 240 margin: 0;
241 241
242 242 .q_filter_box {
243 243 margin-right: @padding;
244 244 }
245 245 }
246 246
247 247 h1 a {
248 248 color: @rcblue;
249 249 }
250 250
251 251 input{
252 252 margin-right: @padding;
253 253 }
254 254
255 255 h5, .h5 {
256 256 color: @grey1;
257 257 margin-bottom: @space;
258 258
259 259 span {
260 260 display: inline-block;
261 261 }
262 262 }
263 263
264 264 p {
265 265 margin-bottom: 0;
266 266 }
267 267
268 268 .links {
269 269 float: right;
270 270 display: inline;
271 271 margin: 0;
272 272 padding-left: 0;
273 273 list-style: none;
274 274 text-align: right;
275 275
276 276 li {
277 277 float: right;
278 278 list-style-type: none;
279 279 }
280 280
281 281 a {
282 282 display: inline-block;
283 283 margin-left: @textmargin/2;
284 284 }
285 285 }
286 286
287 287 .title-content {
288 288 float: left;
289 289 margin: 0;
290 290 padding: 0;
291 291
292 292 & + .breadcrumbs {
293 293 margin-top: @padding;
294 294 }
295 295
296 296 & + .links {
297 297 margin-top: -@button-padding;
298 298
299 299 & + .breadcrumbs {
300 300 margin-top: @padding;
301 301 }
302 302 }
303
304 .repo-group-desc {
305 padding: 8px 0px 0px 0px;
306 }
303 307 }
304 308
305 309 .title-main {
306 310 font-size: @repo-title-fontsize;
307 311 }
308 312
309 313 .title-description {
310 314 margin-top: .5em;
311 315 }
312 316
313 317 .q_filter_box {
314 318 width: 200px;
315 319 }
316 320
317 321 }
318 322
319 323 #readme .title {
320 324 text-transform: none;
321 325 }
322 326
323 327 // Lists
324 328 // -------------------------
325 329
326 330 // Unordered and Ordered lists
327 331 ul,
328 332 ol {
329 333 margin-top: 0;
330 334 margin-bottom: @textmargin;
331 335 ul,
332 336 ol {
333 337 margin-bottom: 0;
334 338 }
335 339 }
336 340
337 341 li {
338 342 line-height: 2em;
339 343 }
340 344
341 345 ul li {
342 346 position: relative;
343 347 list-style-type: disc;
344 348
345 349 p:first-child {
346 350 display:inline;
347 351 }
348 352 }
349 353
350 354 // List options
351 355
352 356 // Unstyled keeps list items block level, just removes default browser padding and list-style
353 357 .list-unstyled {
354 358 padding-left: 0;
355 359 list-style: none;
356 360 li:before { content: none; }
357 361 }
358 362
359 363 // Inline turns list items into inline-block
360 364 .list-inline {
361 365 .list-unstyled();
362 366 margin-left: -5px;
363 367
364 368 > li {
365 369 display: inline-block;
366 370 padding-left: 5px;
367 371 padding-right: 5px;
368 372 }
369 373 }
370 374
371 375 // Description Lists
372 376
373 377 dl {
374 378 margin-top: 0; // Remove browser default
375 379 margin-bottom: @textmargin;
376 380 }
377 381
378 382 dt,
379 383 dd {
380 384 line-height: 1.4em;
381 385 }
382 386
383 387 dt {
384 388 margin: @textmargin 0 0 0;
385 389 font-weight: @text-bold-weight;
386 390 font-family: @text-bold;
387 391 }
388 392
389 393 dd {
390 394 margin-left: 0; // Undo browser default
391 395 }
392 396
393 397 // Horizontal description lists
394 398 // Defaults to being stacked without any of the below styles applied, until the
395 399 // grid breakpoint is reached (default of ~768px).
396 400 // These are used in forms as well; see style guide.
397 401 // TODO: lisa: These should really not be used in forms.
398 402
399 403 .dl-horizontal {
400 404
401 405 overflow: hidden;
402 406 margin-bottom: @space;
403 407
404 408 dt, dd {
405 409 float: left;
406 410 margin: 5px 0 5px 0;
407 411 }
408 412
409 413 dt {
410 414 clear: left;
411 415 width: @label-width - @form-vertical-margin;
412 416 }
413 417
414 418 dd {
415 419 &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present
416 420 margin-left: @form-vertical-margin;
417 421 max-width: @form-max-width - (@label-width - @form-vertical-margin) - @form-vertical-margin;
418 422 }
419 423
420 424 pre {
421 425 margin: 0;
422 426 }
423 427
424 428 &.settings {
425 429 dt {
426 430 text-align: left;
427 431 }
428 432 }
429 433
430 434 @media (min-width: 768px) {
431 435 dt {
432 436 float: left;
433 437 width: 185px;
434 438 clear: left;
435 439 text-align: right;
436 440 }
437 441 dd {
438 442 margin-left: 20px;
439 443 }
440 444 }
441 445 }
442 446
443 447
444 448 // Misc
445 449 // -------------------------
446 450
447 451 // Abbreviations and acronyms
448 452 abbr[title],
449 453 abbr[data-original-title] {
450 454 cursor: help;
451 455 border-bottom: @border-thickness dotted @grey4;
452 456 }
453 457 .initialism {
454 458 font-size: 90%;
455 459 text-transform: uppercase;
456 460 }
457 461
458 462 // Blockquotes
459 463 blockquote {
460 464 padding: 1em 2em;
461 465 margin: 0 0 2em;
462 466 font-size: @basefontsize;
463 467 border-left: 2px solid @grey6;
464 468
465 469 p,
466 470 ul,
467 471 ol {
468 472 &:last-child {
469 473 margin-bottom: 0;
470 474 }
471 475 }
472 476
473 477 footer,
474 478 small,
475 479 .small {
476 480 display: block;
477 481 font-size: 80%;
478 482
479 483 &:before {
480 484 content: '\2014 \00A0'; // em dash, nbsp
481 485 }
482 486 }
483 487 }
484 488
485 489 // Opposite alignment of blockquote
486 490 //
487 491 .blockquote-reverse,
488 492 blockquote.pull-right {
489 493 padding-right: 15px;
490 494 padding-left: 0;
491 495 border-right: 5px solid @grey6;
492 496 border-left: 0;
493 497 text-align: right;
494 498
495 499 // Account for citation
496 500 footer,
497 501 small,
498 502 .small {
499 503 &:before { content: ''; }
500 504 &:after {
501 505 content: '\00A0 \2014'; // nbsp, em dash
502 506 }
503 507 }
504 508 }
505 509
506 510 // Addresses
507 511 address {
508 512 margin-bottom: 2em;
509 513 font-style: normal;
510 514 line-height: 1.8em;
511 515 }
512 516
513 517 .error-message {
514 518 display: block;
515 519 margin: @padding/3 0;
516 520 color: @alert2;
517 521 }
518 522
519 523 .issue-tracker-link {
520 524 color: @rcblue;
521 525 }
522 526
523 527 .info_text{
524 528 font-size: @basefontsize;
525 529 color: @grey4;
526 530 font-family: @text-regular;
527 531 }
528 532
529 533 .help-block-inline {
530 534 margin: 0;
531 535 }
532 536
533 537 // help block text
534 538 .help-block {
535 539 display: block;
536 540 margin: 0 0 @padding;
537 541 color: @grey4;
538 542 font-family: @text-light;
539 543 &.pre-formatting {
540 544 white-space: pre-wrap;
541 545 }
542 546 }
543 547
544 548 .error-message {
545 549 display: block;
546 550 margin: @padding/3 0;
547 551 color: @alert2;
548 552 }
@@ -1,362 +1,364 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('favicon', '/favicon.ico', []);
16 16 pyroutes.register('robots', '/robots.txt', []);
17 17 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
18 18 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
19 19 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
20 20 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
21 21 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
22 22 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/_settings/integrations', ['repo_group_name']);
23 23 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']);
24 24 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/_settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
25 25 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/_settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
26 26 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/_settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
27 27 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
28 28 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
29 29 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
30 30 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
31 31 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
32 32 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
33 33 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
34 34 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
35 35 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
36 36 pyroutes.register('ops_ping_legacy', '/_admin/ping', []);
37 37 pyroutes.register('ops_error_test_legacy', '/_admin/error_test', []);
38 38 pyroutes.register('admin_home', '/_admin', []);
39 39 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
40 40 pyroutes.register('admin_audit_log_entry', '/_admin/audit_logs/%(audit_log_id)s', ['audit_log_id']);
41 41 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
42 42 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
43 43 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
44 44 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
45 45 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
46 46 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
47 47 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
48 48 pyroutes.register('admin_settings_exception_tracker', '/_admin/settings/exceptions', []);
49 49 pyroutes.register('admin_settings_exception_tracker_delete_all', '/_admin/settings/exceptions/delete', []);
50 50 pyroutes.register('admin_settings_exception_tracker_show', '/_admin/settings/exceptions/%(exception_id)s', ['exception_id']);
51 51 pyroutes.register('admin_settings_exception_tracker_delete', '/_admin/settings/exceptions/%(exception_id)s/delete', ['exception_id']);
52 52 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
53 53 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
54 54 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
55 55 pyroutes.register('admin_settings_process_management_data', '/_admin/settings/process_management/data', []);
56 56 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
57 57 pyroutes.register('admin_settings_process_management_master_signal', '/_admin/settings/process_management/master_signal', []);
58 58 pyroutes.register('admin_defaults_repositories', '/_admin/defaults/repositories', []);
59 59 pyroutes.register('admin_defaults_repositories_update', '/_admin/defaults/repositories/update', []);
60 60 pyroutes.register('admin_settings', '/_admin/settings', []);
61 61 pyroutes.register('admin_settings_update', '/_admin/settings/update', []);
62 62 pyroutes.register('admin_settings_global', '/_admin/settings/global', []);
63 63 pyroutes.register('admin_settings_global_update', '/_admin/settings/global/update', []);
64 64 pyroutes.register('admin_settings_vcs', '/_admin/settings/vcs', []);
65 65 pyroutes.register('admin_settings_vcs_update', '/_admin/settings/vcs/update', []);
66 66 pyroutes.register('admin_settings_vcs_svn_pattern_delete', '/_admin/settings/vcs/svn_pattern_delete', []);
67 67 pyroutes.register('admin_settings_mapping', '/_admin/settings/mapping', []);
68 68 pyroutes.register('admin_settings_mapping_update', '/_admin/settings/mapping/update', []);
69 69 pyroutes.register('admin_settings_visual', '/_admin/settings/visual', []);
70 70 pyroutes.register('admin_settings_visual_update', '/_admin/settings/visual/update', []);
71 71 pyroutes.register('admin_settings_issuetracker', '/_admin/settings/issue-tracker', []);
72 72 pyroutes.register('admin_settings_issuetracker_update', '/_admin/settings/issue-tracker/update', []);
73 73 pyroutes.register('admin_settings_issuetracker_test', '/_admin/settings/issue-tracker/test', []);
74 74 pyroutes.register('admin_settings_issuetracker_delete', '/_admin/settings/issue-tracker/delete', []);
75 75 pyroutes.register('admin_settings_email', '/_admin/settings/email', []);
76 76 pyroutes.register('admin_settings_email_update', '/_admin/settings/email/update', []);
77 77 pyroutes.register('admin_settings_hooks', '/_admin/settings/hooks', []);
78 78 pyroutes.register('admin_settings_hooks_update', '/_admin/settings/hooks/update', []);
79 79 pyroutes.register('admin_settings_hooks_delete', '/_admin/settings/hooks/delete', []);
80 80 pyroutes.register('admin_settings_search', '/_admin/settings/search', []);
81 81 pyroutes.register('admin_settings_labs', '/_admin/settings/labs', []);
82 82 pyroutes.register('admin_settings_labs_update', '/_admin/settings/labs/update', []);
83 83 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
84 84 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
85 85 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
86 86 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
87 87 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
88 88 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
89 89 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
90 90 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
91 91 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
92 92 pyroutes.register('admin_permissions_ssh_keys', '/_admin/permissions/ssh_keys', []);
93 93 pyroutes.register('admin_permissions_ssh_keys_data', '/_admin/permissions/ssh_keys/data', []);
94 94 pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []);
95 95 pyroutes.register('users', '/_admin/users', []);
96 96 pyroutes.register('users_data', '/_admin/users_data', []);
97 97 pyroutes.register('users_create', '/_admin/users/create', []);
98 98 pyroutes.register('users_new', '/_admin/users/new', []);
99 99 pyroutes.register('user_edit', '/_admin/users/%(user_id)s/edit', ['user_id']);
100 100 pyroutes.register('user_edit_advanced', '/_admin/users/%(user_id)s/edit/advanced', ['user_id']);
101 101 pyroutes.register('user_edit_global_perms', '/_admin/users/%(user_id)s/edit/global_permissions', ['user_id']);
102 102 pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']);
103 103 pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']);
104 104 pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']);
105 105 pyroutes.register('user_enable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_enable', ['user_id']);
106 106 pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
107 107 pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
108 108 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
109 109 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
110 110 pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
111 111 pyroutes.register('edit_user_ssh_keys_add', '/_admin/users/%(user_id)s/edit/ssh_keys/new', ['user_id']);
112 112 pyroutes.register('edit_user_ssh_keys_delete', '/_admin/users/%(user_id)s/edit/ssh_keys/delete', ['user_id']);
113 113 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
114 114 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
115 115 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
116 116 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
117 117 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
118 118 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
119 119 pyroutes.register('edit_user_perms_summary', '/_admin/users/%(user_id)s/edit/permissions_summary', ['user_id']);
120 120 pyroutes.register('edit_user_perms_summary_json', '/_admin/users/%(user_id)s/edit/permissions_summary/json', ['user_id']);
121 121 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
122 122 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
123 123 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
124 124 pyroutes.register('edit_user_caches', '/_admin/users/%(user_id)s/edit/caches', ['user_id']);
125 125 pyroutes.register('edit_user_caches_update', '/_admin/users/%(user_id)s/edit/caches/update', ['user_id']);
126 126 pyroutes.register('user_groups', '/_admin/user_groups', []);
127 127 pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
128 128 pyroutes.register('user_groups_new', '/_admin/user_groups/new', []);
129 129 pyroutes.register('user_groups_create', '/_admin/user_groups/create', []);
130 130 pyroutes.register('repos', '/_admin/repos', []);
131 131 pyroutes.register('repo_new', '/_admin/repos/new', []);
132 132 pyroutes.register('repo_create', '/_admin/repos/create', []);
133 133 pyroutes.register('repo_groups', '/_admin/repo_groups', []);
134 134 pyroutes.register('repo_group_new', '/_admin/repo_group/new', []);
135 135 pyroutes.register('repo_group_create', '/_admin/repo_group/create', []);
136 136 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
137 137 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
138 138 pyroutes.register('channelstream_proxy', '/_channelstream', []);
139 139 pyroutes.register('upload_file', '/_file_store/upload', []);
140 140 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
141 141 pyroutes.register('logout', '/_admin/logout', []);
142 142 pyroutes.register('reset_password', '/_admin/password_reset', []);
143 143 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
144 144 pyroutes.register('home', '/', []);
145 145 pyroutes.register('user_autocomplete_data', '/_users', []);
146 146 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
147 147 pyroutes.register('repo_list_data', '/_repos', []);
148 148 pyroutes.register('repo_group_list_data', '/_repo_groups', []);
149 149 pyroutes.register('goto_switcher_data', '/_goto_data', []);
150 150 pyroutes.register('markup_preview', '/_markup_preview', []);
151 151 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
152 152 pyroutes.register('journal', '/_admin/journal', []);
153 153 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
154 154 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
155 155 pyroutes.register('journal_public', '/_admin/public_journal', []);
156 156 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
157 157 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
158 158 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
159 159 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
160 160 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
161 161 pyroutes.register('repo_creating', '/%(repo_name)s/repo_creating', ['repo_name']);
162 162 pyroutes.register('repo_creating_check', '/%(repo_name)s/repo_creating_check', ['repo_name']);
163 163 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
164 164 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
165 165 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
166 166 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
167 167 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
168 168 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
169 169 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
170 170 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
171 171 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
172 172 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
173 173 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
174 174 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
175 175 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
176 176 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
177 177 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
178 178 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
179 179 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
180 180 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
181 181 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
182 182 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
183 183 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
184 184 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
185 185 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
186 186 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
187 187 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
188 188 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
189 189 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
190 190 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
191 191 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
192 192 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
193 193 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
194 194 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
195 195 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
196 196 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
197 197 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
198 198 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
199 199 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
200 200 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
201 201 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
202 202 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
203 203 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
204 204 pyroutes.register('repo_changelog_elements', '/%(repo_name)s/changelog_elements', ['repo_name']);
205 205 pyroutes.register('repo_changelog_elements_file', '/%(repo_name)s/changelog_elements/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
206 206 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
207 207 pyroutes.register('repo_compare', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
208 208 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
209 209 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
210 210 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
211 211 pyroutes.register('repo_fork_new', '/%(repo_name)s/fork', ['repo_name']);
212 212 pyroutes.register('repo_fork_create', '/%(repo_name)s/fork/create', ['repo_name']);
213 213 pyroutes.register('repo_forks_show_all', '/%(repo_name)s/forks', ['repo_name']);
214 214 pyroutes.register('repo_forks_data', '/%(repo_name)s/forks/data', ['repo_name']);
215 215 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
216 216 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
217 217 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
218 218 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
219 219 pyroutes.register('pullrequest_repo_targets', '/%(repo_name)s/pull-request/repo-targets', ['repo_name']);
220 220 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
221 221 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
222 222 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
223 223 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
224 224 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
225 225 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
226 226 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
227 227 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
228 228 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
229 229 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
230 230 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
231 231 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
232 232 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
233 233 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
234 234 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
235 235 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
236 236 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
237 237 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
238 238 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
239 239 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
240 240 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
241 241 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
242 242 pyroutes.register('repo_edit_toggle_locking', '/%(repo_name)s/settings/toggle_locking', ['repo_name']);
243 243 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
244 244 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
245 245 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
246 246 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
247 247 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
248 248 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
249 249 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
250 250 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
251 251 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
252 252 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
253 253 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
254 254 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
255 255 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
256 256 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
257 257 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
258 258 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
259 259 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
260 260 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed/rss', ['repo_name']);
261 261 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed/atom', ['repo_name']);
262 262 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
263 263 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
264 264 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
265 265 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
266 266 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
267 267 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
268 268 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
269 269 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
270 270 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
271 271 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
272 272 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
273 273 pyroutes.register('edit_user_group_perms_summary_json', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary/json', ['user_group_id']);
274 274 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
275 275 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
276 276 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
277 277 pyroutes.register('edit_user_group_global_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions/update', ['user_group_id']);
278 278 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
279 279 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
280 280 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
281 281 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
282 282 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
283 283 pyroutes.register('search', '/_admin/search', []);
284 pyroutes.register('search_repo', '/%(repo_name)s/search', ['repo_name']);
284 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
285 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
286 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
285 287 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
286 288 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
287 289 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
288 290 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
289 291 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
290 292 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
291 293 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
292 294 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
293 295 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
294 296 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
295 297 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
296 298 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
297 299 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
298 300 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
299 301 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
300 302 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
301 303 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
302 304 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
303 305 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
304 306 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
305 307 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
306 308 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
307 309 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
308 310 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
309 311 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
310 312 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
311 313 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
312 314 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
313 315 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
314 316 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
315 317 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
316 318 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
317 319 pyroutes.register('gists_show', '/_admin/gists', []);
318 320 pyroutes.register('gists_new', '/_admin/gists/new', []);
319 321 pyroutes.register('gists_create', '/_admin/gists/create', []);
320 322 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
321 323 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
322 324 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
323 325 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
324 326 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
325 327 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
326 328 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
327 329 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
328 330 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
329 331 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
330 332 pyroutes.register('apiv2', '/_admin/api', []);
331 333 pyroutes.register('admin_settings_license', '/_admin/settings/license', []);
332 334 pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []);
333 335 pyroutes.register('login', '/_admin/login', []);
334 336 pyroutes.register('register', '/_admin/register', []);
335 337 pyroutes.register('repo_reviewers_review_rule_new', '/%(repo_name)s/settings/review/rules/new', ['repo_name']);
336 338 pyroutes.register('repo_reviewers_review_rule_edit', '/%(repo_name)s/settings/review/rules/%(rule_id)s', ['repo_name', 'rule_id']);
337 339 pyroutes.register('repo_reviewers_review_rule_delete', '/%(repo_name)s/settings/review/rules/%(rule_id)s/delete', ['repo_name', 'rule_id']);
338 340 pyroutes.register('plugin_admin_chat', '/_admin/plugin_admin_chat/%(action)s', ['action']);
339 341 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
340 342 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
341 343 pyroutes.register('admin_settings_scheduler_show_tasks', '/_admin/settings/scheduler/_tasks', []);
342 344 pyroutes.register('admin_settings_scheduler_show_all', '/_admin/settings/scheduler', []);
343 345 pyroutes.register('admin_settings_scheduler_new', '/_admin/settings/scheduler/new', []);
344 346 pyroutes.register('admin_settings_scheduler_create', '/_admin/settings/scheduler/create', []);
345 347 pyroutes.register('admin_settings_scheduler_edit', '/_admin/settings/scheduler/%(schedule_id)s', ['schedule_id']);
346 348 pyroutes.register('admin_settings_scheduler_update', '/_admin/settings/scheduler/%(schedule_id)s/update', ['schedule_id']);
347 349 pyroutes.register('admin_settings_scheduler_delete', '/_admin/settings/scheduler/%(schedule_id)s/delete', ['schedule_id']);
348 350 pyroutes.register('admin_settings_scheduler_execute', '/_admin/settings/scheduler/%(schedule_id)s/execute', ['schedule_id']);
349 351 pyroutes.register('admin_settings_automation', '/_admin/settings/automation', []);
350 352 pyroutes.register('admin_settings_automation_update', '/_admin/settings/automation/%(entry_id)s/update', ['entry_id']);
351 353 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
352 354 pyroutes.register('admin_permissions_branch_update', '/_admin/permissions/branch/update', []);
353 355 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
354 356 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
355 357 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
356 358 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
357 359 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
358 360 pyroutes.register('repo_automation_update', '/%(repo_name)s/settings/automation/%(entry_id)s/update', ['repo_name', 'entry_id']);
359 361 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
360 362 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
361 363 pyroutes.register('edit_repo_perms_branch_delete', '/%(repo_name)s/settings/branch_permissions/%(rule_id)s/delete', ['repo_name', 'rule_id']);
362 364 }
@@ -1,68 +1,60 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3 <%namespace name="widgets" file="/widgets.mako"/>
4 4
5 5 <%def name="breadcrumbs_links()">
6 6 %if c.repo:
7 ${h.link_to('Settings',h.route_path('edit_repo', repo_name=c.repo.repo_name))}
8 &raquo;
9 ${h.link_to(_('Integrations'),request.route_url(route_name='repo_integrations_home', repo_name=c.repo.repo_name))}
7 ${_('Settings')}
10 8 %elif c.repo_group:
11 ${h.link_to(_('Admin'),h.route_path('admin_home'))}
12 &raquo;
13 ${h.link_to(_('Repository Groups'),h.route_path('repo_groups'))}
14 &raquo;
15 ${h.link_to(c.repo_group.group_name,h.route_path('edit_repo_group', repo_group_name=c.repo_group.group_name))}
16 &raquo;
17 ${h.link_to(_('Integrations'),request.route_url(route_name='repo_group_integrations_home', repo_group_name=c.repo_group.group_name))}
9 ${_('Settings')}
18 10 %else:
19 11 ${h.link_to(_('Admin'),h.route_path('admin_home'))}
20 12 &raquo;
21 13 ${h.link_to(_('Settings'),h.route_path('admin_settings'))}
22 14 &raquo;
23 15 ${h.link_to(_('Integrations'),request.route_url(route_name='global_integrations_home'))}
24 16 %endif
25 17 &raquo;
26 18 ${_('Create new integration')}
27 19 </%def>
28 20 <%widgets:panel class_='integrations'>
29 21 <%def name="title()">
30 22 %if c.repo:
31 23 ${_('Create New Integration for repository: {repo_name}').format(repo_name=c.repo.repo_name)}
32 24 %elif c.repo_group:
33 25 ${_('Create New Integration for repository group: {repo_group_name}').format(repo_group_name=c.repo_group.group_name)}
34 26 %else:
35 27 ${_('Create New Global Integration')}
36 28 %endif
37 29 </%def>
38 30
39 31 %for integration, IntegrationObject in c.available_integrations.items():
40 32 <%
41 33 if c.repo:
42 34 create_url = request.route_path('repo_integrations_create',
43 35 repo_name=c.repo.repo_name,
44 36 integration=integration)
45 37 elif c.repo_group:
46 38 create_url = request.route_path('repo_group_integrations_create',
47 39 repo_group_name=c.repo_group.group_name,
48 40 integration=integration)
49 41 else:
50 42 create_url = request.route_path('global_integrations_create',
51 43 integration=integration)
52 44 if IntegrationObject.is_dummy:
53 45 create_url = request.current_route_path()
54 46 %>
55 47 <a href="${create_url}" class="integration-box ${'dummy-integration' if IntegrationObject.is_dummy else ''}">
56 48 <%widgets:panel>
57 49 <h2>
58 50 <div class="integration-icon">
59 51 ${IntegrationObject.icon()|n}
60 52 </div>
61 53 ${IntegrationObject.display_name}
62 54 </h2>
63 55 ${IntegrationObject.description or _('No description available')}
64 56 </%widgets:panel>
65 57 </a>
66 58 %endfor
67 59 <div style="clear:both"></div>
68 60 </%widgets:panel>
@@ -1,61 +1,48 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s repository group settings') % c.repo_group.name}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 <%def name="breadcrumbs_links()">
12 ${h.link_to(_('Admin'),h.route_path('admin_home'))}
13 &raquo;
14 ${h.link_to(_('Repository Groups'),h.route_path('repo_groups'))}
15 %if c.repo_group.parent_group:
16 &raquo; ${h.link_to(c.repo_group.parent_group.name, h.route_path('repo_group_home', repo_group_name=c.repo_group.parent_group.group_name))}
17 %endif
18 &raquo; ${c.repo_group.name}
11 <%def name="menu_bar_nav()">
12 ${self.menu_items(active='admin')}
19 13 </%def>
20 14
21 <%def name="breadcrumbs_side_links()">
22 <ul class="links">
23 <li>
24 <a href="${h.route_path('repo_group_new', _query=dict(parent_group=c.repo_group.group_id))}" class="btn btn-small btn-success">${_(u'Add Child Group')}</a>
25 </li>
26 </ul>
27 </%def>
28
29 <%def name="menu_bar_nav()">
30 ${self.menu_items(active='admin')}
15 <%def name="menu_bar_subnav()">
16 ${self.repo_group_menu(active='options')}
31 17 </%def>
32 18
33 19 <%def name="main_content()">
34 20 <%include file="/admin/repo_groups/repo_group_edit_${c.active}.mako"/>
35 21 </%def>
36 22
37 23 <%def name="main()">
24
38 25 <div class="box">
39 26 <div class="title">
40 ${self.breadcrumbs()}
41 ${self.breadcrumbs_side_links()}
27 ${self.repo_group_page_title(c.repo_group)}
42 28 </div>
43 29
44 30 <div class="sidebar-col-wrapper">
45 31 ##main
46 32 <div class="sidebar">
47 33 <ul class="nav nav-pills nav-stacked">
48 34 <li class="${'active' if c.active=='settings' else ''}"><a href="${h.route_path('edit_repo_group', repo_group_name=c.repo_group.group_name)}">${_('Settings')}</a></li>
49 35 <li class="${'active' if c.active=='permissions' else ''}"><a href="${h.route_path('edit_repo_group_perms', repo_group_name=c.repo_group.group_name)}">${_('Permissions')}</a></li>
50 36 <li class="${'active' if c.active=='advanced' else ''}"><a href="${h.route_path('edit_repo_group_advanced', repo_group_name=c.repo_group.group_name)}">${_('Advanced')}</a></li>
51 37 <li class="${'active' if c.active=='integrations' else ''}"><a href="${h.route_path('repo_group_integrations_home', repo_group_name=c.repo_group.group_name)}">${_('Integrations')}</a></li>
52 38 </ul>
53 39 </div>
54 40
55 41 <div class="main-content-full-width">
56 42 ${self.main_content()}
57 43 </div>
58 44
59 45 </div>
60 46 </div>
47
61 48 </%def>
@@ -1,108 +1,108 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ##
3 3 ## See also repo_settings.html
4 4 ##
5 5 <%inherit file="/base/base.mako"/>
6 6
7 7 <%def name="title()">
8 8 ${_('%s repository settings') % c.rhodecode_db_repo.repo_name}
9 9 %if c.rhodecode_name:
10 10 &middot; ${h.branding(c.rhodecode_name)}
11 11 %endif
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()">
15 15 ${_('Settings')}
16 16 </%def>
17 17
18 18 <%def name="menu_bar_nav()">
19 19 ${self.menu_items(active='repositories')}
20 20 </%def>
21 21
22 22 <%def name="menu_bar_subnav()">
23 23 ${self.repo_menu(active='options')}
24 24 </%def>
25 25
26 26 <%def name="main_content()">
27 27 % if hasattr(c, 'repo_edit_template'):
28 28 <%include file="${c.repo_edit_template}"/>
29 29 % else:
30 30 <%include file="/admin/repos/repo_edit_${c.active}.mako"/>
31 31 % endif
32 32 </%def>
33 33
34 34
35 35 <%def name="main()">
36 36 <div class="box">
37 37 <div class="title">
38 38 ${self.repo_page_title(c.rhodecode_db_repo)}
39 ${self.breadcrumbs()}
39
40 40 </div>
41 41
42 42 <div class="sidebar-col-wrapper scw-small">
43 43 <div class="sidebar">
44 44 <ul class="nav nav-pills nav-stacked">
45 45 <li class="${'active' if c.active=='settings' else ''}">
46 46 <a href="${h.route_path('edit_repo', repo_name=c.repo_name)}">${_('Settings')}</a>
47 47 </li>
48 48 <li class="${'active' if c.active=='permissions' else ''}">
49 49 <a href="${h.route_path('edit_repo_perms', repo_name=c.repo_name)}">${_('Permissions')}</a>
50 50 </li>
51 51 <li class="${'active' if c.active=='permissions_branch' else ''}">
52 52 <a href="${h.route_path('edit_repo_perms_branch', repo_name=c.repo_name)}">${_('Branch Permissions')}</a>
53 53 </li>
54 54 <li class="${'active' if c.active=='advanced' else ''}">
55 55 <a href="${h.route_path('edit_repo_advanced', repo_name=c.repo_name)}">${_('Advanced')}</a>
56 56 </li>
57 57 <li class="${'active' if c.active=='vcs' else ''}">
58 58 <a href="${h.route_path('edit_repo_vcs', repo_name=c.repo_name)}">${_('VCS')}</a>
59 59 </li>
60 60 <li class="${'active' if c.active=='fields' else ''}">
61 61 <a href="${h.route_path('edit_repo_fields', repo_name=c.repo_name)}">${_('Extra Fields')}</a>
62 62 </li>
63 63 <li class="${'active' if c.active=='issuetracker' else ''}">
64 64 <a href="${h.route_path('edit_repo_issuetracker', repo_name=c.repo_name)}">${_('Issue Tracker')}</a>
65 65 </li>
66 66 <li class="${'active' if c.active=='caches' else ''}">
67 67 <a href="${h.route_path('edit_repo_caches', repo_name=c.repo_name)}">${_('Caches')}</a>
68 68 </li>
69 69 %if c.rhodecode_db_repo.repo_type != 'svn':
70 70 <li class="${'active' if c.active=='remote' else ''}">
71 71 <a href="${h.route_path('edit_repo_remote', repo_name=c.repo_name)}">${_('Remote sync')}</a>
72 72 </li>
73 73 %endif
74 74 <li class="${'active' if c.active=='statistics' else ''}">
75 75 <a href="${h.route_path('edit_repo_statistics', repo_name=c.repo_name)}">${_('Statistics')}</a>
76 76 </li>
77 77 <li class="${'active' if c.active=='integrations' else ''}">
78 78 <a href="${h.route_path('repo_integrations_home', repo_name=c.repo_name)}">${_('Integrations')}</a>
79 79 </li>
80 80 %if c.rhodecode_db_repo.repo_type != 'svn':
81 81 <li class="${'active' if c.active=='reviewers' else ''}">
82 82 <a href="${h.route_path('repo_reviewers', repo_name=c.repo_name)}">${_('Reviewer Rules')}</a>
83 83 </li>
84 84 %endif
85 85 <li class="${'active' if c.active=='automation' else ''}">
86 86 <a href="${h.route_path('repo_automation', repo_name=c.repo_name)}">${_('Automation')}</a>
87 87 </li>
88 88 <li class="${'active' if c.active=='maintenance' else ''}">
89 89 <a href="${h.route_path('edit_repo_maintenance', repo_name=c.repo_name)}">${_('Maintenance')}</a>
90 90 </li>
91 91 <li class="${'active' if c.active=='strip' else ''}">
92 92 <a href="${h.route_path('edit_repo_strip', repo_name=c.repo_name)}">${_('Strip')}</a>
93 93 </li>
94 94 <li class="${'active' if c.active=='audit' else ''}">
95 95 <a href="${h.route_path('edit_repo_audit_logs', repo_name=c.repo_name)}">${_('Audit logs')}</a>
96 96 </li>
97 97
98 98 </ul>
99 99 </div>
100 100
101 101 <div class="main-content-full-width">
102 102 ${self.main_content()}
103 103 </div>
104 104
105 105 </div>
106 106 </div>
107 107
108 108 </%def> No newline at end of file
@@ -1,729 +1,796 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-216x60.png')}" alt="RhodeCode"/></a>
13 13 </div>
14 14 %if c.rhodecode_name:
15 15 <div class="branding">- ${h.branding(c.rhodecode_name)}</div>
16 16 %endif
17 17 </div>
18 18 <!-- MENU BAR NAV -->
19 19 ${self.menu_bar_nav()}
20 20 <!-- END MENU BAR NAV -->
21 21 </div>
22 22 </div>
23 23 ${self.menu_bar_subnav()}
24 24 <!-- END HEADER -->
25 25
26 26 <!-- CONTENT -->
27 27 <div id="content" class="wrapper">
28 28
29 29 <rhodecode-toast id="notifications"></rhodecode-toast>
30 30
31 31 <div class="main">
32 32 ${next.main()}
33 33 </div>
34 34 </div>
35 35 <!-- END CONTENT -->
36 36
37 37 </div>
38 38 <!-- FOOTER -->
39 39 <div id="footer">
40 40 <div id="footer-inner" class="title wrapper">
41 41 <div>
42 42 <p class="footer-link-right">
43 43 % if c.visual.show_version:
44 44 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
45 45 % endif
46 46 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
47 47 % if c.visual.rhodecode_support_url:
48 48 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
49 49 % endif
50 50 </p>
51 51 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
52 52 <p class="server-instance" style="display:${sid}">
53 53 ## display hidden instance ID if specially defined
54 54 % if c.rhodecode_instanceid:
55 55 ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
56 56 % endif
57 57 </p>
58 58 </div>
59 59 </div>
60 60 </div>
61 61
62 62 <!-- END FOOTER -->
63 63
64 64 ### MAKO DEFS ###
65 65
66 66 <%def name="menu_bar_subnav()">
67 67 </%def>
68 68
69 69 <%def name="breadcrumbs(class_='breadcrumbs')">
70 70 <div class="${class_}">
71 71 ${self.breadcrumbs_links()}
72 72 </div>
73 73 </%def>
74 74
75 75 <%def name="admin_menu()">
76 76 <ul class="admin_menu submenu">
77 77 <li><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
78 78 <li><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
79 79 <li><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
80 80 <li><a href="${h.route_path('users')}">${_('Users')}</a></li>
81 81 <li><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
82 82 <li><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
83 83 <li><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
84 84 <li><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
85 85 <li><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
86 86 <li class="last"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
87 87 </ul>
88 88 </%def>
89 89
90 90
91 91 <%def name="dt_info_panel(elements)">
92 92 <dl class="dl-horizontal">
93 93 %for dt, dd, title, show_items in elements:
94 94 <dt>${dt}:</dt>
95 95 <dd title="${h.tooltip(title)}">
96 96 %if callable(dd):
97 97 ## allow lazy evaluation of elements
98 98 ${dd()}
99 99 %else:
100 100 ${dd}
101 101 %endif
102 102 %if show_items:
103 103 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
104 104 %endif
105 105 </dd>
106 106
107 107 %if show_items:
108 108 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
109 109 %for item in show_items:
110 110 <dt></dt>
111 111 <dd>${item}</dd>
112 112 %endfor
113 113 </div>
114 114 %endif
115 115
116 116 %endfor
117 117 </dl>
118 118 </%def>
119 119
120 120
121 121 <%def name="gravatar(email, size=16)">
122 122 <%
123 123 if (size > 16):
124 124 gravatar_class = 'gravatar gravatar-large'
125 125 else:
126 126 gravatar_class = 'gravatar'
127 127 %>
128 128 <%doc>
129 129 TODO: johbo: For now we serve double size images to make it smooth
130 130 for retina. This is how it worked until now. Should be replaced
131 131 with a better solution at some point.
132 132 </%doc>
133 133 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
134 134 </%def>
135 135
136 136
137 137 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
138 138 <% email = h.email_or_none(contact) %>
139 139 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
140 140 ${self.gravatar(email, size)}
141 141 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
142 142 </div>
143 143 </%def>
144 144
145 145
146 146 ## admin menu used for people that have some admin resources
147 147 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
148 148 <ul class="submenu">
149 149 %if repositories:
150 150 <li class="local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
151 151 %endif
152 152 %if repository_groups:
153 153 <li class="local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
154 154 %endif
155 155 %if user_groups:
156 156 <li class="local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
157 157 %endif
158 158 </ul>
159 159 </%def>
160 160
161 161 <%def name="repo_page_title(repo_instance)">
162 162 <div class="title-content">
163 163 <div class="title-main">
164 164 ## SVN/HG/GIT icons
165 165 %if h.is_hg(repo_instance):
166 166 <i class="icon-hg"></i>
167 167 %endif
168 168 %if h.is_git(repo_instance):
169 169 <i class="icon-git"></i>
170 170 %endif
171 171 %if h.is_svn(repo_instance):
172 172 <i class="icon-svn"></i>
173 173 %endif
174 174
175 175 ## public/private
176 176 %if repo_instance.private:
177 177 <i class="icon-repo-private"></i>
178 178 %else:
179 179 <i class="icon-repo-public"></i>
180 180 %endif
181 181
182 182 ## repo name with group name
183 ${h.breadcrumb_repo_link(c.rhodecode_db_repo)}
183 ${h.breadcrumb_repo_link(repo_instance)}
184 184
185 185 </div>
186 186
187 187 ## FORKED
188 188 %if repo_instance.fork:
189 189 <p>
190 190 <i class="icon-code-fork"></i> ${_('Fork of')}
191 191 ${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))}
192 192 </p>
193 193 %endif
194 194
195 195 ## IMPORTED FROM REMOTE
196 196 %if repo_instance.clone_uri:
197 197 <p>
198 198 <i class="icon-code-fork"></i> ${_('Clone from')}
199 199 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
200 200 </p>
201 201 %endif
202 202
203 203 ## LOCKING STATUS
204 204 %if repo_instance.locked[0]:
205 205 <p class="locking_locked">
206 206 <i class="icon-repo-lock"></i>
207 207 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
208 208 </p>
209 209 %elif repo_instance.enable_locking:
210 210 <p class="locking_unlocked">
211 211 <i class="icon-repo-unlock"></i>
212 212 ${_('Repository not locked. Pull repository to lock it.')}
213 213 </p>
214 214 %endif
215 215
216 216 </div>
217 217 </%def>
218 218
219 219 <%def name="repo_menu(active=None)">
220 220 <%
221 221 def is_active(selected):
222 222 if selected == active:
223 223 return "active"
224 224 %>
225 225
226 226 <!--- CONTEXT BAR -->
227 227 <div id="context-bar">
228 228 <div class="wrapper">
229 229 <ul id="context-pages" class="navigation horizontal-list">
230 230 <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>
231 231 <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>
232 232 <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>
233 233 <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>
234 234 <li class="${is_active('search')}"><a class="menulink" href="${h.route_path('search_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Search')}</div></a></li>
235 235
236 236 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
237 237 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
238 238 <li class="${is_active('showpullrequest')}">
239 239 <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)}">
240 240 %if c.repository_pull_requests:
241 241 <span class="pr_notifications">${c.repository_pull_requests}</span>
242 242 %endif
243 243 <div class="menulabel">${_('Pull Requests')}</div>
244 244 </a>
245 245 </li>
246 246 %endif
247 247
248 248 <li class="${is_active('options')}">
249 249 <a class="menulink dropdown">
250 250 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
251 251 </a>
252 252 <ul class="submenu">
253 253 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
254 <li><a href="${h.route_path('edit_repo',repo_name=c.repo_name)}">${_('Settings')}</a></li>
254 <li><a href="${h.route_path('edit_repo',repo_name=c.repo_name)}">${_('Repository Settings')}</a></li>
255 255 %endif
256 256 %if c.rhodecode_db_repo.fork:
257 257 <li>
258 258 <a title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
259 259 href="${h.route_path('repo_compare',
260 260 repo_name=c.rhodecode_db_repo.fork.repo_name,
261 261 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
262 262 source_ref=c.rhodecode_db_repo.landing_rev[1],
263 263 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
264 264 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
265 265 _query=dict(merge=1))}"
266 266 >
267 267 ${_('Compare fork')}
268 268 </a>
269 269 </li>
270 270 %endif
271 271
272 272 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
273 273 %if c.rhodecode_db_repo.locked[0]:
274 274 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
275 275 %else:
276 276 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock')}</a></li>
277 277 %endif
278 278 %endif
279 279 %if c.rhodecode_user.username != h.DEFAULT_USER:
280 280 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
281 281 <li><a href="${h.route_path('repo_fork_new',repo_name=c.repo_name)}">${_('Fork')}</a></li>
282 282 <li><a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
283 283 %endif
284 284 %endif
285 285 </ul>
286 286 </li>
287 287 </ul>
288 288 </div>
289 289 <div class="clear"></div>
290 290 </div>
291 291 % if c.rhodecode_db_repo.archived:
292 292 <div class="alert alert-warning text-center">
293 293 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
294 294 </div>
295 295 % endif
296 296 <!--- END CONTEXT BAR -->
297 297
298 298 </%def>
299 299
300 <%def name="repo_group_page_title(repo_group_instance)">
301 <div class="title-content">
302 <div class="title-main">
303 ## Repository Group icon
304 <i class="icon-folder-close"></i>
305
306 ## repo name with group name
307 ${h.breadcrumb_repo_group_link(repo_group_instance)}
308 </div>
309
310 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
311 <div class="repo-group-desc">
312 ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)}
313 </div>
314
315 </div>
316 </%def>
317
318 <%def name="repo_group_menu(active=None)">
319 <%
320 def is_active(selected):
321 if selected == active:
322 return "active"
323
324 is_admin = h.HasPermissionAny('hg.admin')('can create repos index page')
325
326 gr_name = c.repo_group.group_name if c.repo_group else None
327 # create repositories with write permission on group is set to true
328 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
329 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
330 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
331
332 %>
333
334 <!--- CONTEXT BAR -->
335 <div id="context-bar">
336 <div class="wrapper">
337 <ul id="context-pages" class="navigation horizontal-list">
338 <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>
339 <li class="${is_active('search')}"><a class="menulink" href="${h.route_path('search_repo_group', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Search')}</div></a></li>
340
341 <li class="${is_active('options')}">
342 <a class="menulink dropdown">
343 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
344 </a>
345 <ul class="submenu">
346 %if is_admin or group_admin:
347 <li><a 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')}">${_('Group Settings')}</a></li>
348 %endif
349 %if is_admin or group_admin or (group_write and create_on_write):
350 <li><a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('Add Repository')}</a></li>
351 %endif
352 %if is_admin or group_admin:
353 <li><a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}">${_(u'Add Parent Group')}</a></li>
354 %endif
355 </ul>
356 </li>
357 </ul>
358 </div>
359 <div class="clear"></div>
360 </div>
361
362 <!--- END CONTEXT BAR -->
363
364 </%def>
365
366
300 367 <%def name="usermenu(active=False)">
301 368 ## USER MENU
302 369 <li id="quick_login_li" class="${'active' if active else ''}">
303 370 % if c.rhodecode_user.username == h.DEFAULT_USER:
304 371 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
305 372 ${gravatar(c.rhodecode_user.email, 20)}
306 373 <span class="user">
307 374 <span>${_('Sign in')}</span>
308 375 </span>
309 376 </a>
310 377 % else:
311 378 ## logged in user
312 379 <a id="quick_login_link" class="menulink childs">
313 380 ${gravatar(c.rhodecode_user.email, 20)}
314 381 <span class="user">
315 382 <span class="menu_link_user">${c.rhodecode_user.username}</span>
316 383 <div class="show_more"></div>
317 384 </span>
318 385 </a>
319 386 ## subnav with menu for logged in user
320 387 <div class="user-menu submenu">
321 388 <div id="quick_login">
322 389 %if c.rhodecode_user.username != h.DEFAULT_USER:
323 390 <div class="">
324 391 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
325 392 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
326 393 <div class="email">${c.rhodecode_user.email}</div>
327 394 </div>
328 395 <div class="">
329 396 <ol class="links">
330 397 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
331 398 % if c.rhodecode_user.personal_repo_group:
332 399 <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>
333 400 % endif
334 401 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
335 402 ## bookmark-items
336 403 <li class="bookmark-items">
337 404 ${_('Bookmarks')}
338 405 <div class="pull-right">
339 406 <a href="${h.route_path('my_account_bookmarks')}">${_('Manage')}</a>
340 407 </div>
341 408 </li>
342 409 % if not c.bookmark_items:
343 410 <li>
344 411 <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
345 412 </li>
346 413 % endif
347 414 % for item in c.bookmark_items:
348 415 <li>
349 416 % if item.repository:
350 417 <div>
351 418 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
352 419 <code>${item.position}</code>
353 420 % if item.repository.repo_type == 'hg':
354 421 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
355 422 % elif item.repository.repo_type == 'git':
356 423 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
357 424 % elif item.repository.repo_type == 'svn':
358 425 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
359 426 % endif
360 427 ${(item.title or h.shorter(item.repository.repo_name, 30))}
361 428 </a>
362 429 </div>
363 430 % elif item.repository_group:
364 431 <div>
365 432 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
366 433 <code>${item.position}</code>
367 434 <i class="icon-folder-close" title="${_('Repository group')}" style="font-size: 16px"></i>
368 435 ${(item.title or h.shorter(item.repository_group.group_name, 30))}
369 436 </a>
370 437 </div>
371 438 % else:
372 439 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
373 440 <code>${item.position}</code>
374 441 ${item.title}
375 442 </a>
376 443 % endif
377 444 </li>
378 445 % endfor
379 446
380 447 <li class="logout">
381 448 ${h.secure_form(h.route_path('logout'), request=request)}
382 449 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
383 450 ${h.end_form()}
384 451 </li>
385 452 </ol>
386 453 </div>
387 454 %endif
388 455 </div>
389 456 </div>
390 457 ## unread counter
391 458 <div class="pill_container">
392 459 <a class="menu_link_notifications ${'empty' if c.unread_notifications == 0 else ''}" href="${h.route_path('notifications_show_all')}">${c.unread_notifications}</a>
393 460 </div>
394 461 % endif
395 462 </li>
396 463 </%def>
397 464
398 465 <%def name="menu_items(active=None)">
399 466 <%
400 467 def is_active(selected):
401 468 if selected == active:
402 469 return "active"
403 470 return ""
404 471 %>
405 472
406 473 <ul id="quick" class="main_nav navigation horizontal-list">
407 474 ## notice box for important system messages
408 475 <li style="display: none">
409 476 <a class="notice-box" href="#openNotice" onclick="showNoticeBox(); return false">
410 477 <div class="menulabel-notice" >
411 478 0
412 479 </div>
413 480 </a>
414 481 </li>
415 482
416 483 ## Main filter
417 484 <li>
418 485 <div class="menulabel main_filter_box">
419 486 <div class="main_filter_input_box">
420 487 <input class="main_filter_input" id="main_filter" size="15" type="text" name="main_filter" placeholder="${_('search / go to...')}" value=""/>
421 488 </div>
422 489 <div class="main_filter_help_box">
423 490 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
424 491 </div>
425 492 </div>
426 493
427 494 <div id="main_filter_help" style="display: none">
428 495 Use '/' key to quickly access this field.
429 496 Enter name of repository, or repository group for quick search.
430 497
431 498 Prefix query to allow special search:
432 499
433 500 user:admin, to search for usernames
434 501
435 502 user_group:devops, to search for user groups
436 503
437 504 commit:efced4, to search for commits
438 505
439 506 </div>
440 507 </li>
441 508
442 509 ## ROOT MENU
443 510 %if c.rhodecode_user.username != h.DEFAULT_USER:
444 511 <li class="${is_active('journal')}">
445 512 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
446 513 <div class="menulabel">${_('Journal')}</div>
447 514 </a>
448 515 </li>
449 516 %else:
450 517 <li class="${is_active('journal')}">
451 518 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
452 519 <div class="menulabel">${_('Public journal')}</div>
453 520 </a>
454 521 </li>
455 522 %endif
456 523 <li class="${is_active('gists')}">
457 524 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
458 525 <div class="menulabel">${_('Gists')}</div>
459 526 </a>
460 527 </li>
461 528 <li class="${is_active('search')}">
462 529 <a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.route_path('search')}">
463 530 <div class="menulabel">${_('Search')}</div>
464 531 </a>
465 532 </li>
466 533 % if h.HasPermissionAll('hg.admin')('access admin main page'):
467 534 <li class="${is_active('admin')}">
468 535 <a class="menulink childs" title="${_('Admin settings')}" href="#" onclick="return false;">
469 536 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
470 537 </a>
471 538 ${admin_menu()}
472 539 </li>
473 540 % elif c.rhodecode_user.repositories_admin or c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
474 541 <li class="${is_active('admin')}">
475 542 <a class="menulink childs" title="${_('Delegated Admin settings')}">
476 543 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
477 544 </a>
478 545 ${admin_menu_simple(c.rhodecode_user.repositories_admin,
479 546 c.rhodecode_user.repository_groups_admin,
480 547 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
481 548 </li>
482 549 % endif
483 550 ## render extra user menu
484 551 ${usermenu(active=(active=='my_account'))}
485 552
486 553 % if c.debug_style:
487 554 <li>
488 555 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
489 556 <div class="menulabel">${_('[Style]')}</div>
490 557 </a>
491 558 </li>
492 559 % endif
493 560 </ul>
494 561
495 562 <script type="text/javascript">
496 563 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
497 564
498 565 var formatRepoResult = function(result, container, query, escapeMarkup) {
499 566 return function(data, escapeMarkup) {
500 567 if (!data.repo_id){
501 568 return data.text; // optgroup text Repositories
502 569 }
503 570
504 571 var tmpl = '';
505 572 var repoType = data['repo_type'];
506 573 var repoName = data['text'];
507 574
508 575 if(data && data.type == 'repo'){
509 576 if(repoType === 'hg'){
510 577 tmpl += '<i class="icon-hg"></i> ';
511 578 }
512 579 else if(repoType === 'git'){
513 580 tmpl += '<i class="icon-git"></i> ';
514 581 }
515 582 else if(repoType === 'svn'){
516 583 tmpl += '<i class="icon-svn"></i> ';
517 584 }
518 585 if(data['private']){
519 586 tmpl += '<i class="icon-lock" ></i> ';
520 587 }
521 588 else if(visualShowPublicIcon){
522 589 tmpl += '<i class="icon-unlock-alt"></i> ';
523 590 }
524 591 }
525 592 tmpl += escapeMarkup(repoName);
526 593 return tmpl;
527 594
528 595 }(result, escapeMarkup);
529 596 };
530 597
531 598 var formatRepoGroupResult = function(result, container, query, escapeMarkup) {
532 599 return function(data, escapeMarkup) {
533 600 if (!data.repo_group_id){
534 601 return data.text; // optgroup text Repositories
535 602 }
536 603
537 604 var tmpl = '';
538 605 var repoGroupName = data['text'];
539 606
540 607 if(data){
541 608
542 609 tmpl += '<i class="icon-folder-close"></i> ';
543 610
544 611 }
545 612 tmpl += escapeMarkup(repoGroupName);
546 613 return tmpl;
547 614
548 615 }(result, escapeMarkup);
549 616 };
550 617
551 618
552 619 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
553 620
554 621 if (value.split(':').length === 2) {
555 622 value = value.split(':')[1]
556 623 }
557 624
558 625 var searchType = data['type'];
559 626 var valueDisplay = data['value_display'];
560 627
561 628 var escapeRegExChars = function (value) {
562 629 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
563 630 };
564 631 var pattern = '(' + escapeRegExChars(value) + ')';
565 632
566 633 // highlight match
567 634 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
568 635 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
569 636
570 637 var icon = '';
571 638
572 639 if (searchType === 'hint') {
573 640 icon += '<i class="icon-folder-close"></i> ';
574 641 }
575 642 else if (searchType === 'search') {
576 643 icon += '<i class="icon-more"></i> ';
577 644 }
578 645 else if (searchType === 'repo') {
579 646 if (data['repo_type'] === 'hg') {
580 647 icon += '<i class="icon-hg"></i> ';
581 648 }
582 649 else if (data['repo_type'] === 'git') {
583 650 icon += '<i class="icon-git"></i> ';
584 651 }
585 652 else if (data['repo_type'] === 'svn') {
586 653 icon += '<i class="icon-svn"></i> ';
587 654 }
588 655 if (data['private']) {
589 656 icon += '<i class="icon-lock" ></i> ';
590 657 }
591 658 else if (visualShowPublicIcon) {
592 659 icon += '<i class="icon-unlock-alt"></i> ';
593 660 }
594 661 }
595 662 else if (searchType === 'repo_group') {
596 663 icon += '<i class="icon-folder-close"></i> ';
597 664 }
598 665 else if (searchType === 'user_group') {
599 666 icon += '<i class="icon-group"></i> ';
600 667 }
601 668 else if (searchType === 'user') {
602 669 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
603 670 }
604 671 else if (searchType === 'commit') {
605 672 icon += '<i class="icon-tag"></i>';
606 673 }
607 674
608 675 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
609 676 return tmpl.format(icon, valueDisplay);
610 677 };
611 678
612 679 var handleSelect = function(element, suggestion) {
613 680 if (suggestion.type === "hint") {
614 681 // we skip action
615 682 $('#main_filter').focus();
616 683 } else {
617 684 window.location = suggestion['url'];
618 685 }
619 686 };
620 687 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
621 688 if (queryLowerCase.split(':').length === 2) {
622 689 queryLowerCase = queryLowerCase.split(':')[1]
623 690 }
624 691 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
625 692 };
626 693
627 694 $('#main_filter').autocomplete({
628 695 serviceUrl: pyroutes.url('goto_switcher_data'),
629 696 params: {"search_context": templateContext.search_context},
630 697 minChars:2,
631 698 maxHeight:400,
632 699 deferRequestBy: 300, //miliseconds
633 700 tabDisabled: true,
634 701 autoSelectFirst: true,
635 702 formatResult: autocompleteMainFilterFormatResult,
636 703 lookupFilter: autocompleteMainFilterResult,
637 704 onSelect: function (element, suggestion) {
638 705 handleSelect(element, suggestion);
639 706 return false;
640 707 },
641 708 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
642 709 if (jqXHR !== 'abort') {
643 710 alert("Error during search.\nError code: {0}".format(textStatus));
644 711 window.location = '';
645 712 }
646 713 }
647 714 });
648 715
649 716 showMainFilterBox = function () {
650 717 $('#main_filter_help').toggle();
651 718 }
652 719
653 720 </script>
654 721 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
655 722 </%def>
656 723
657 724 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
658 725 <div class="modal-dialog">
659 726 <div class="modal-content">
660 727 <div class="modal-header">
661 728 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
662 729 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
663 730 </div>
664 731 <div class="modal-body">
665 732 <div class="block-left">
666 733 <table class="keyboard-mappings">
667 734 <tbody>
668 735 <tr>
669 736 <th></th>
670 737 <th>${_('Site-wide shortcuts')}</th>
671 738 </tr>
672 739 <%
673 740 elems = [
674 741 ('/', 'Use quick search box'),
675 742 ('g h', 'Goto home page'),
676 743 ('g g', 'Goto my private gists page'),
677 744 ('g G', 'Goto my public gists page'),
678 745 ('g 0-9', 'Goto bookmarked items from 0-9'),
679 746 ('n r', 'New repository page'),
680 747 ('n g', 'New gist page'),
681 748 ]
682 749 %>
683 750 %for key, desc in elems:
684 751 <tr>
685 752 <td class="keys">
686 753 <span class="key tag">${key}</span>
687 754 </td>
688 755 <td>${desc}</td>
689 756 </tr>
690 757 %endfor
691 758 </tbody>
692 759 </table>
693 760 </div>
694 761 <div class="block-left">
695 762 <table class="keyboard-mappings">
696 763 <tbody>
697 764 <tr>
698 765 <th></th>
699 766 <th>${_('Repositories')}</th>
700 767 </tr>
701 768 <%
702 769 elems = [
703 770 ('g s', 'Goto summary page'),
704 771 ('g c', 'Goto changelog page'),
705 772 ('g f', 'Goto files page'),
706 773 ('g F', 'Goto files page with file search activated'),
707 774 ('g p', 'Goto pull requests page'),
708 775 ('g o', 'Goto repository settings'),
709 776 ('g O', 'Goto repository permissions settings'),
710 777 ]
711 778 %>
712 779 %for key, desc in elems:
713 780 <tr>
714 781 <td class="keys">
715 782 <span class="key tag">${key}</span>
716 783 </td>
717 784 <td>${desc}</td>
718 785 </tr>
719 786 %endfor
720 787 </tbody>
721 788 </table>
722 789 </div>
723 790 </div>
724 791 <div class="modal-footer">
725 792 </div>
726 793 </div><!-- /.modal-content -->
727 794 </div><!-- /.modal-dialog -->
728 795 </div><!-- /.modal -->
729 796
@@ -1,141 +1,138 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3
4 <%def name="menu_bar_subnav()">
5 % if c.repo_group:
6 ${self.repo_group_menu(active='home')}
7 % endif
8 </%def>
9
10
3 11 <%def name="main()">
4 12 <div class="box">
5 13 <!-- box / title -->
6 14 <div class="title">
7 <div class="block-left breadcrumbs">
8 ${self.breadcrumbs()}
9 <span id="match_container" style="display:none"><span id="match_count">0</span> ${_('matches')}</span>
15 % if c.repo_group:
16 ${self.repo_group_page_title(c.repo_group)}
17 ## context actions
18 <div>
19 <ul class="links icon-only-links block-right">
20 <li></li>
21 </ul>
10 22 </div>
23 % endif
24
11 25 %if c.rhodecode_user.username != h.DEFAULT_USER:
12 26 <div class="block-right">
13 27 <%
14 28 is_admin = h.HasPermissionAny('hg.admin')('can create repos index page')
15 29 create_repo = h.HasPermissionAny('hg.create.repository')('can create repository index page')
16 30 create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')('can create repository groups index page')
17 31 create_user_group = h.HasPermissionAny('hg.usergroup.create.true')('can create user groups index page')
18
19 gr_name = c.repo_group.group_name if c.repo_group else None
20 # create repositories with write permission on group is set to true
21 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
22 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
23 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
24 32 %>
25 33
26 34 %if not c.repo_group:
27 35 ## no repository group context here
28 36 %if is_admin or create_repo:
29 37 <a href="${h.route_path('repo_new')}" class="btn btn-small btn-success btn-primary">${_('Add Repository')}</a>
30 38 %endif
31 39
32 40 %if is_admin or create_repo_group:
33 41 <a href="${h.route_path('repo_group_new')}" class="btn btn-small btn-default">${_(u'Add Repository Group')}</a>
34 42 %endif
35 %else:
36 ##we're inside other repository group other terms apply
37 %if is_admin or group_admin or (group_write and create_on_write):
38 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}" class="btn btn-small btn-success btn-primary">${_('Add Repository')}</a>
39 %endif
40 %if is_admin or group_admin:
41 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}" class="btn btn-small btn-default">${_(u'Add Repository Group')}</a>
42 %endif
43 %if is_admin or group_admin:
44 <a 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')}" class="btn btn-small btn-primary">${_('Edit Repository Group')}</a>
45 %endif
46 43 %endif
47 44 </div>
48 45 %endif
49 46 </div>
50 47 <!-- end box / title -->
51 48 <div class="table">
52 49 <div id="groups_list_wrap">
53 50 <table id="group_list_table" class="display" style="width: 100%"></table>
54 51 </div>
55 52 </div>
56 53
57 54 <div class="table">
58 55 <div id="repos_list_wrap">
59 56 <table id="repo_list_table" class="display" style="width: 100%"></table>
60 57 </div>
61 58 </div>
62 59
63 60 ## no repository groups and repos present, show something to the users
64 61 % if c.repo_groups_data == '[]' and c.repos_data == '[]':
65 62 <div class="table">
66 63 <h2 class="no-object-border">
67 64 ${_('No repositories or repositories groups exists here.')}
68 65 </h2>
69 66 </div>
70 67 % endif
71 68
72 69 </div>
73 70 <script>
74 71 $(document).ready(function() {
75 72
76 73 // repo group list
77 74 % if c.repo_groups_data != '[]':
78 75 $('#group_list_table').DataTable({
79 76 data: ${c.repo_groups_data|n},
80 77 dom: 'rtp',
81 78 pageLength: ${c.visual.dashboard_items},
82 79 order: [[ 0, "asc" ]],
83 80 columns: [
84 81 { data: {"_": "name",
85 82 "sort": "name_raw"}, title: "${_('Name')}", className: "td-componentname" },
86 83 { data: 'menu', "bSortable": false, className: "quick_repo_menu" },
87 84 { data: {"_": "desc",
88 85 "sort": "desc"}, title: "${_('Description')}", className: "td-description" },
89 86 { data: {"_": "last_change",
90 87 "sort": "last_change_raw",
91 88 "type": Number}, title: "${_('Last Change')}", className: "td-time" },
92 89 { data: {"_": "owner",
93 90 "sort": "owner"}, title: "${_('Owner')}", className: "td-user" }
94 91 ],
95 92 language: {
96 93 paginate: DEFAULT_GRID_PAGINATION,
97 94 emptyTable: _gettext("No repository groups available yet.")
98 95 },
99 96 "drawCallback": function( settings, json ) {
100 97 timeagoActivate();
101 98 quick_repo_menu();
102 99 }
103 100 });
104 101 % endif
105 102
106 103 // repo list
107 104 % if c.repos_data != '[]':
108 105 $('#repo_list_table').DataTable({
109 106 data: ${c.repos_data|n},
110 107 dom: 'rtp',
111 108 order: [[ 0, "asc" ]],
112 109 pageLength: ${c.visual.dashboard_items},
113 110 columns: [
114 111 { data: {"_": "name",
115 112 "sort": "name_raw"}, title: "${_('Name')}", className: "truncate-wrap td-componentname" },
116 113 { data: 'menu', "bSortable": false, className: "quick_repo_menu" },
117 114 { data: {"_": "desc",
118 115 "sort": "desc"}, title: "${_('Description')}", className: "td-description" },
119 116 { data: {"_": "last_change",
120 117 "sort": "last_change_raw",
121 118 "type": Number}, title: "${_('Last Change')}", className: "td-time" },
122 119 { data: {"_": "last_changeset",
123 120 "sort": "last_changeset_raw",
124 121 "type": Number}, title: "${_('Commit')}", className: "td-hash" },
125 122 { data: {"_": "owner",
126 123 "sort": "owner"}, title: "${_('Owner')}", className: "td-user" }
127 124 ],
128 125 language: {
129 126 paginate: DEFAULT_GRID_PAGINATION,
130 127 emptyTable: _gettext("No repositories available yet.")
131 128 },
132 129 "drawCallback": function( settings, json ) {
133 130 timeagoActivate();
134 131 quick_repo_menu();
135 132 }
136 133 });
137 134 % endif
138 135
139 136 });
140 137 </script>
141 138 </%def>
@@ -1,548 +1,548 b''
1 1 <%inherit file="/base/base.mako"/>
2 2 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${c.repo_name} ${_('New pull request')}
6 6 </%def>
7 7
8 8 <%def name="breadcrumbs_links()">
9 9 ${_('New pull request')}
10 10 </%def>
11 11
12 12 <%def name="menu_bar_nav()">
13 13 ${self.menu_items(active='repositories')}
14 14 </%def>
15 15
16 16 <%def name="menu_bar_subnav()">
17 17 ${self.repo_menu(active='showpullrequest')}
18 18 </%def>
19 19
20 20 <%def name="main()">
21 21 <div class="box">
22 22 <div class="title">
23 23 ${self.repo_page_title(c.rhodecode_db_repo)}
24 24 </div>
25 25
26 26 ${h.secure_form(h.route_path('pullrequest_create', repo_name=c.repo_name, _query=request.GET.mixed()), id='pull_request_form', request=request)}
27 27
28 28 ${self.breadcrumbs()}
29 29
30 30 <div class="box pr-summary">
31 31
32 32 <div class="summary-details block-left">
33 33
34 34
35 35 <div class="pr-details-title">
36 ${_('Pull request summary')}
36 ${_('Summary')}
37 37 </div>
38 38
39 39 <div class="form" style="padding-top: 10px">
40 40 <!-- fields -->
41 41
42 42 <div class="fields" >
43 43
44 44 <div class="field">
45 45 <div class="label">
46 46 <label for="pullrequest_title">${_('Title')}:</label>
47 47 </div>
48 48 <div class="input">
49 49 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
50 50 </div>
51 51 </div>
52 52
53 53 <div class="field">
54 54 <div class="label label-textarea">
55 55 <label for="pullrequest_desc">${_('Description')}:</label>
56 56 </div>
57 57 <div class="textarea text-area editor">
58 58 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
59 59 ${dt.markup_form('pullrequest_desc')}
60 60 </div>
61 61 </div>
62 62
63 63 <div class="field">
64 64 <div class="label label-textarea">
65 65 <label for="commit_flow">${_('Commit flow')}:</label>
66 66 </div>
67 67
68 68 ## TODO: johbo: Abusing the "content" class here to get the
69 69 ## desired effect. Should be replaced by a proper solution.
70 70
71 71 ##ORG
72 72 <div class="content">
73 73 <strong>${_('Source repository')}:</strong>
74 74 ${c.rhodecode_db_repo.description}
75 75 </div>
76 76 <div class="content">
77 77 ${h.hidden('source_repo')}
78 78 ${h.hidden('source_ref')}
79 79 </div>
80 80
81 81 ##OTHER, most Probably the PARENT OF THIS FORK
82 82 <div class="content">
83 83 ## filled with JS
84 84 <div id="target_repo_desc"></div>
85 85 </div>
86 86
87 87 <div class="content">
88 88 ${h.hidden('target_repo')}
89 89 ${h.hidden('target_ref')}
90 90 <span id="target_ref_loading" style="display: none">
91 91 ${_('Loading refs...')}
92 92 </span>
93 93 </div>
94 94 </div>
95 95
96 96 <div class="field">
97 97 <div class="label label-textarea">
98 98 <label for="pullrequest_submit"></label>
99 99 </div>
100 100 <div class="input">
101 101 <div class="pr-submit-button">
102 102 <input id="pr_submit" class="btn" name="save" type="submit" value="${_('Submit Pull Request')}">
103 103 </div>
104 104 <div id="pr_open_message"></div>
105 105 </div>
106 106 </div>
107 107
108 108 <div class="pr-spacing-container"></div>
109 109 </div>
110 110 </div>
111 111 </div>
112 112 <div>
113 113 ## AUTHOR
114 114 <div class="reviewers-title block-right">
115 115 <div class="pr-details-title">
116 116 ${_('Author of this pull request')}
117 117 </div>
118 118 </div>
119 119 <div class="block-right pr-details-content reviewers">
120 120 <ul class="group_members">
121 121 <li>
122 122 ${self.gravatar_with_user(c.rhodecode_user.email, 16)}
123 123 </li>
124 124 </ul>
125 125 </div>
126 126
127 127 ## REVIEW RULES
128 128 <div id="review_rules" style="display: none" class="reviewers-title block-right">
129 129 <div class="pr-details-title">
130 130 ${_('Reviewer rules')}
131 131 </div>
132 132 <div class="pr-reviewer-rules">
133 133 ## review rules will be appended here, by default reviewers logic
134 134 </div>
135 135 </div>
136 136
137 137 ## REVIEWERS
138 138 <div class="reviewers-title block-right">
139 139 <div class="pr-details-title">
140 140 ${_('Pull request reviewers')}
141 141 <span class="calculate-reviewers"> - ${_('loading...')}</span>
142 142 </div>
143 143 </div>
144 144 <div id="reviewers" class="block-right pr-details-content reviewers">
145 145 ## members goes here, filled via JS based on initial selection !
146 146 <input type="hidden" name="__start__" value="review_members:sequence">
147 147 <ul id="review_members" class="group_members"></ul>
148 148 <input type="hidden" name="__end__" value="review_members:sequence">
149 149 <div id="add_reviewer_input" class='ac'>
150 150 <div class="reviewer_ac">
151 151 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
152 152 <div id="reviewers_container"></div>
153 153 </div>
154 154 </div>
155 155 </div>
156 156 </div>
157 157 </div>
158 158 <div class="box">
159 159 <div>
160 160 ## overview pulled by ajax
161 161 <div id="pull_request_overview"></div>
162 162 </div>
163 163 </div>
164 164 ${h.end_form()}
165 165 </div>
166 166
167 167 <script type="text/javascript">
168 168 $(function(){
169 169 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
170 170 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
171 171 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
172 172 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
173 173
174 174 var $pullRequestForm = $('#pull_request_form');
175 175 var $pullRequestSubmit = $('#pr_submit', $pullRequestForm);
176 176 var $sourceRepo = $('#source_repo', $pullRequestForm);
177 177 var $targetRepo = $('#target_repo', $pullRequestForm);
178 178 var $sourceRef = $('#source_ref', $pullRequestForm);
179 179 var $targetRef = $('#target_ref', $pullRequestForm);
180 180
181 181 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
182 182 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
183 183
184 184 var targetRepo = function() { return $targetRepo.eq(0).val() };
185 185 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
186 186
187 187 var calculateContainerWidth = function() {
188 188 var maxWidth = 0;
189 189 var repoSelect2Containers = ['#source_repo', '#target_repo'];
190 190 $.each(repoSelect2Containers, function(idx, value) {
191 191 $(value).select2('container').width('auto');
192 192 var curWidth = $(value).select2('container').width();
193 193 if (maxWidth <= curWidth) {
194 194 maxWidth = curWidth;
195 195 }
196 196 $.each(repoSelect2Containers, function(idx, value) {
197 197 $(value).select2('container').width(maxWidth + 10);
198 198 });
199 199 });
200 200 };
201 201
202 202 var initRefSelection = function(selectedRef) {
203 203 return function(element, callback) {
204 204 // translate our select2 id into a text, it's a mapping to show
205 205 // simple label when selecting by internal ID.
206 206 var id, refData;
207 207 if (selectedRef === undefined || selectedRef === null) {
208 208 id = element.val();
209 209 refData = element.val().split(':');
210 210
211 211 if (refData.length !== 3){
212 212 refData = ["", "", ""]
213 213 }
214 214 } else {
215 215 id = selectedRef;
216 216 refData = selectedRef.split(':');
217 217 }
218 218
219 219 var text = refData[1];
220 220 if (refData[0] === 'rev') {
221 221 text = text.substring(0, 12);
222 222 }
223 223
224 224 var data = {id: id, text: text};
225 225 callback(data);
226 226 };
227 227 };
228 228
229 229 var formatRefSelection = function(data, container, escapeMarkup) {
230 230 var prefix = '';
231 231 var refData = data.id.split(':');
232 232 if (refData[0] === 'branch') {
233 233 prefix = '<i class="icon-branch"></i>';
234 234 }
235 235 else if (refData[0] === 'book') {
236 236 prefix = '<i class="icon-bookmark"></i>';
237 237 }
238 238 else if (refData[0] === 'tag') {
239 239 prefix = '<i class="icon-tag"></i>';
240 240 }
241 241
242 242 var originalOption = data.element;
243 243 return prefix + escapeMarkup(data.text);
244 244 };formatSelection:
245 245
246 246 // custom code mirror
247 247 var codeMirrorInstance = $('#pullrequest_desc').get(0).MarkupForm.cm;
248 248
249 249 reviewersController = new ReviewersController();
250 250
251 251 var queryTargetRepo = function(self, query) {
252 252 // cache ALL results if query is empty
253 253 var cacheKey = query.term || '__';
254 254 var cachedData = self.cachedDataSource[cacheKey];
255 255
256 256 if (cachedData) {
257 257 query.callback({results: cachedData.results});
258 258 } else {
259 259 $.ajax({
260 260 url: pyroutes.url('pullrequest_repo_targets', {'repo_name': templateContext.repo_name}),
261 261 data: {query: query.term},
262 262 dataType: 'json',
263 263 type: 'GET',
264 264 success: function(data) {
265 265 self.cachedDataSource[cacheKey] = data;
266 266 query.callback({results: data.results});
267 267 },
268 268 error: function(data, textStatus, errorThrown) {
269 269 alert(
270 270 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
271 271 }
272 272 });
273 273 }
274 274 };
275 275
276 276 var queryTargetRefs = function(initialData, query) {
277 277 var data = {results: []};
278 278 // filter initialData
279 279 $.each(initialData, function() {
280 280 var section = this.text;
281 281 var children = [];
282 282 $.each(this.children, function() {
283 283 if (query.term.length === 0 ||
284 284 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
285 285 children.push({'id': this.id, 'text': this.text})
286 286 }
287 287 });
288 288 data.results.push({'text': section, 'children': children})
289 289 });
290 290 query.callback({results: data.results});
291 291 };
292 292
293 293 var loadRepoRefDiffPreview = function() {
294 294
295 295 var url_data = {
296 296 'repo_name': targetRepo(),
297 297 'target_repo': sourceRepo(),
298 298 'source_ref': targetRef()[2],
299 299 'source_ref_type': 'rev',
300 300 'target_ref': sourceRef()[2],
301 301 'target_ref_type': 'rev',
302 302 'merge': true,
303 303 '_': Date.now() // bypass browser caching
304 304 }; // gather the source/target ref and repo here
305 305
306 306 if (sourceRef().length !== 3 || targetRef().length !== 3) {
307 307 prButtonLock(true, "${_('Please select source and target')}");
308 308 return;
309 309 }
310 310 var url = pyroutes.url('repo_compare', url_data);
311 311
312 312 // lock PR button, so we cannot send PR before it's calculated
313 313 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
314 314
315 315 if (loadRepoRefDiffPreview._currentRequest) {
316 316 loadRepoRefDiffPreview._currentRequest.abort();
317 317 }
318 318
319 319 loadRepoRefDiffPreview._currentRequest = $.get(url)
320 320 .error(function(data, textStatus, errorThrown) {
321 321 if (textStatus !== 'abort') {
322 322 alert(
323 323 "Error while processing request.\nError code {0} ({1}).".format(
324 324 data.status, data.statusText));
325 325 }
326 326
327 327 })
328 328 .done(function(data) {
329 329 loadRepoRefDiffPreview._currentRequest = null;
330 330 $('#pull_request_overview').html(data);
331 331
332 332 var commitElements = $(data).find('tr[commit_id]');
333 333
334 334 var prTitleAndDesc = getTitleAndDescription(
335 335 sourceRef()[1], commitElements, 5);
336 336
337 337 var title = prTitleAndDesc[0];
338 338 var proposedDescription = prTitleAndDesc[1];
339 339
340 340 var useGeneratedTitle = (
341 341 $('#pullrequest_title').hasClass('autogenerated-title') ||
342 342 $('#pullrequest_title').val() === "");
343 343
344 344 if (title && useGeneratedTitle) {
345 345 // use generated title if we haven't specified our own
346 346 $('#pullrequest_title').val(title);
347 347 $('#pullrequest_title').addClass('autogenerated-title');
348 348
349 349 }
350 350
351 351 var useGeneratedDescription = (
352 352 !codeMirrorInstance._userDefinedValue ||
353 353 codeMirrorInstance.getValue() === "");
354 354
355 355 if (proposedDescription && useGeneratedDescription) {
356 356 // set proposed content, if we haven't defined our own,
357 357 // or we don't have description written
358 358 codeMirrorInstance._userDefinedValue = false; // reset state
359 359 codeMirrorInstance.setValue(proposedDescription);
360 360 }
361 361
362 362 // refresh our codeMirror so events kicks in and it's change aware
363 363 codeMirrorInstance.refresh();
364 364
365 365 var msg = '';
366 366 if (commitElements.length === 1) {
367 367 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
368 368 } else {
369 369 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
370 370 }
371 371
372 372 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
373 373
374 374 if (commitElements.length) {
375 375 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
376 376 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
377 377 }
378 378 else {
379 379 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
380 380 }
381 381
382 382
383 383 });
384 384 };
385 385
386 386 var Select2Box = function(element, overrides) {
387 387 var globalDefaults = {
388 388 dropdownAutoWidth: true,
389 389 containerCssClass: "drop-menu",
390 390 dropdownCssClass: "drop-menu-dropdown"
391 391 };
392 392
393 393 var initSelect2 = function(defaultOptions) {
394 394 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
395 395 element.select2(options);
396 396 };
397 397
398 398 return {
399 399 initRef: function() {
400 400 var defaultOptions = {
401 401 minimumResultsForSearch: 5,
402 402 formatSelection: formatRefSelection
403 403 };
404 404
405 405 initSelect2(defaultOptions);
406 406 },
407 407
408 408 initRepo: function(defaultValue, readOnly) {
409 409 var defaultOptions = {
410 410 initSelection : function (element, callback) {
411 411 var data = {id: defaultValue, text: defaultValue};
412 412 callback(data);
413 413 }
414 414 };
415 415
416 416 initSelect2(defaultOptions);
417 417
418 418 element.select2('val', defaultSourceRepo);
419 419 if (readOnly === true) {
420 420 element.select2('readonly', true);
421 421 }
422 422 }
423 423 };
424 424 };
425 425
426 426 var initTargetRefs = function(refsData, selectedRef) {
427 427
428 428 Select2Box($targetRef, {
429 429 placeholder: "${_('Select commit reference')}",
430 430 query: function(query) {
431 431 queryTargetRefs(refsData, query);
432 432 },
433 433 initSelection : initRefSelection(selectedRef)
434 434 }).initRef();
435 435
436 436 if (!(selectedRef === undefined)) {
437 437 $targetRef.select2('val', selectedRef);
438 438 }
439 439 };
440 440
441 441 var targetRepoChanged = function(repoData) {
442 442 // generate new DESC of target repo displayed next to select
443 443 var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
444 444 $('#target_repo_desc').html(
445 445 "<strong>${_('Target repository')}</strong>: {0}. <a href=\"{1}\">Switch base, and use as source.</a>".format(repoData['description'], prLink)
446 446 );
447 447
448 448 // generate dynamic select2 for refs.
449 449 initTargetRefs(repoData['refs']['select2_refs'],
450 450 repoData['refs']['selected_ref']);
451 451
452 452 };
453 453
454 454 var sourceRefSelect2 = Select2Box($sourceRef, {
455 455 placeholder: "${_('Select commit reference')}",
456 456 query: function(query) {
457 457 var initialData = defaultSourceRepoData['refs']['select2_refs'];
458 458 queryTargetRefs(initialData, query)
459 459 },
460 460 initSelection: initRefSelection()
461 461 }
462 462 );
463 463
464 464 var sourceRepoSelect2 = Select2Box($sourceRepo, {
465 465 query: function(query) {}
466 466 });
467 467
468 468 var targetRepoSelect2 = Select2Box($targetRepo, {
469 469 cachedDataSource: {},
470 470 query: $.debounce(250, function(query) {
471 471 queryTargetRepo(this, query);
472 472 }),
473 473 formatResult: formatRepoResult
474 474 });
475 475
476 476 sourceRefSelect2.initRef();
477 477
478 478 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
479 479
480 480 targetRepoSelect2.initRepo(defaultTargetRepo, false);
481 481
482 482 $sourceRef.on('change', function(e){
483 483 loadRepoRefDiffPreview();
484 484 reviewersController.loadDefaultReviewers(
485 485 sourceRepo(), sourceRef(), targetRepo(), targetRef());
486 486 });
487 487
488 488 $targetRef.on('change', function(e){
489 489 loadRepoRefDiffPreview();
490 490 reviewersController.loadDefaultReviewers(
491 491 sourceRepo(), sourceRef(), targetRepo(), targetRef());
492 492 });
493 493
494 494 $targetRepo.on('change', function(e){
495 495 var repoName = $(this).val();
496 496 calculateContainerWidth();
497 497 $targetRef.select2('destroy');
498 498 $('#target_ref_loading').show();
499 499
500 500 $.ajax({
501 501 url: pyroutes.url('pullrequest_repo_refs',
502 502 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
503 503 data: {},
504 504 dataType: 'json',
505 505 type: 'GET',
506 506 success: function(data) {
507 507 $('#target_ref_loading').hide();
508 508 targetRepoChanged(data);
509 509 loadRepoRefDiffPreview();
510 510 },
511 511 error: function(data, textStatus, errorThrown) {
512 512 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
513 513 }
514 514 })
515 515
516 516 });
517 517
518 518 $pullRequestForm.on('submit', function(e){
519 519 // Flush changes into textarea
520 520 codeMirrorInstance.save();
521 521 prButtonLock(true, null, 'all');
522 522 });
523 523
524 524 prButtonLock(true, "${_('Please select source and target')}", 'all');
525 525
526 526 // auto-load on init, the target refs select2
527 527 calculateContainerWidth();
528 528 targetRepoChanged(defaultTargetRepoData);
529 529
530 530 $('#pullrequest_title').on('keyup', function(e){
531 531 $(this).removeClass('autogenerated-title');
532 532 });
533 533
534 534 % if c.default_source_ref:
535 535 // in case we have a pre-selected value, use it now
536 536 $sourceRef.select2('val', '${c.default_source_ref}');
537 537 // diff preview load
538 538 loadRepoRefDiffPreview();
539 539 // default reviewers
540 540 reviewersController.loadDefaultReviewers(
541 541 sourceRepo(), sourceRef(), targetRepo(), targetRef());
542 542 % endif
543 543
544 544 ReviewerAutoComplete('#user');
545 545 });
546 546 </script>
547 547
548 548 </%def>
@@ -1,152 +1,201 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 %if c.repo_name:
6 ${_('Search inside repository %(repo_name)s') % {'repo_name': c.repo_name}}
6 ${_('Search inside repository {repo_name}').format(repo_name=c.repo_name)}
7 %elif c.repo_group_name:
8 ${_('Search inside repository group {repo_group_name}').format(repo_group_name=c.repo_group_name)}
7 9 %else:
8 10 ${_('Search inside all accessible repositories')}
9 11 %endif
10 12 %if c.rhodecode_name:
11 13 &middot; ${h.branding(c.rhodecode_name)}
12 14 %endif
13 15 </%def>
14 16
15 17 <%def name="breadcrumbs_links()">
16 18 %if c.repo_name:
17 ${_('Search inside repository %(repo_name)s') % {'repo_name': c.repo_name}}
19 ${_('Search inside repository {repo_name}').format(repo_name=c.repo_name)}
20 %elif c.repo_group_name:
21 ${_('Search inside repository group {repo_group_name}').format(repo_group_name=c.repo_group_name)}
18 22 %else:
19 23 ${_('Search inside all accessible repositories')}
20 24 %endif
21 25
22 26 </%def>
23 27
24 28 <%def name="menu_bar_nav()">
25 29 %if c.repo_name:
26 ${self.menu_items(active='repositories')}
30 ${self.menu_items(active='search')}
31 %elif c.repo_group_name:
32 ${self.menu_items(active='search')}
27 33 %else:
28 34 ${self.menu_items(active='search')}
29 35 %endif
30 36 </%def>
31 37
32 38 <%def name="menu_bar_subnav()">
33 39 %if c.repo_name:
34 ${self.repo_menu(active='search')}
40 ${self.repo_menu(active='search')}
41 %elif c.repo_group_name:
42 ${self.repo_group_menu(active='search')}
35 43 %endif
36 44 </%def>
37 45
38 46 <%def name="main()">
39 47 <div class="box">
40 48 %if c.repo_name:
41 49 <!-- box / title -->
42 50 <div class="title">
43 51 ${self.repo_page_title(c.rhodecode_db_repo)}
44 52 </div>
45 53 ${h.form(h.route_path('search_repo',repo_name=c.repo_name),method='get')}
54 %elif c.repo_group_name:
55 <!-- box / title -->
56 <div class="title">
57 ${self.repo_group_page_title(c.repo_group)}
58 </div>
59 ${h.form(h.route_path('search_repo_group',repo_group_name=c.repo_group_name),method='get')}
46 60 %else:
47 61 <!-- box / title -->
48 62 <div class="title">
49 63 ${self.breadcrumbs()}
50 64 <ul class="links">&nbsp;</ul>
51 65 </div>
52 66 <!-- end box / title -->
53 67 ${h.form(h.route_path('search'), method='get')}
54 68 %endif
55 69 <div class="form search-form">
56 70 <div class="fields">
71
57 72 ${h.text('q', c.cur_query, placeholder="Enter query...")}
58 73
59 74 ${h.select('type',c.search_type,[('content',_('Files')), ('path',_('File path')),('commit',_('Commits'))],id='id_search_type')}
60 75 ${h.hidden('max_lines', '10')}
76
61 77 <input type="submit" value="${_('Search')}" class="btn"/>
62 78 <br/>
63 79
80 <div class="search-tags">
81 %if c.repo_name:
82 <span class="tag tag-ok disabled">
83 %if h.is_hg(c.rhodecode_db_repo):
84 <i class="icon-hg"></i>
85 %endif
86 %if h.is_git(c.rhodecode_db_repo):
87 <i class="icon-git"></i>
88 %endif
89 %if h.is_svn(c.rhodecode_db_repo):
90 <i class="icon-svn"></i>
91 %endif
92 ${c.repo_name}
93 </span>
94
95 %elif c.repo_group_name:
96 <span class="tag tag-ok disabled">
97 <i class="icon-folder-close"></i>
98 ${c.repo_group_name}
99 </span>
100 %endif
101
102 % for search_tag in c.search_tags:
103 <span class="tag tag-ok disabled">${search_tag}</span>
104 % endfor
105
106 </div>
107
64 108 <div class="search-feedback-items">
65 109 % for error in c.errors:
66 110 <span class="error-message">
67 111 % for k,v in error.asdict().items():
68 112 ${k} - ${v}
69 113 % endfor
70 114 </span>
71 115 % endfor
72 116 <div class="field">
73 117 <p class="filterexample" style="position: inherit" onclick="$('#search-help').toggle()">${_('Query Langague examples')}</p>
74 118 <pre id="search-help" style="display: none">\
75 119
76 120 % if c.searcher.name == 'whoosh':
77 121 Example filter terms for `Whoosh` search:
78 122 query lang: <a href="${c.searcher.query_lang_doc}">Whoosh Query Language</a>
79 123 Whoosh has limited query capabilities. For advanced search use ElasticSearch 6 from RhodeCode EE edition.
80 124
81 125 Generate wildcards using '*' character:
82 126 "repo_name:vcs*" - search everything starting with 'vcs'
83 127 "repo_name:*vcs*" - search for repository containing 'vcs'
84 128
85 129 Optional AND / OR operators in queries
86 130 "repo_name:vcs OR repo_name:test"
87 131 "owner:test AND repo_name:test*" AND extension:py
88 132
89 133 Move advanced search is available via ElasticSearch6 backend in EE edition.
90 134 % elif c.searcher.name == 'elasticsearch' and c.searcher.es_version == '2':
91 135 Example filter terms for `ElasticSearch-${c.searcher.es_version}`search:
92 136 ElasticSearch-2 has limited query capabilities. For advanced search use ElasticSearch 6 from RhodeCode EE edition.
93 137
94 138 search type: content (File Content)
95 139 indexed fields: content
96 140
97 141 # search for `fix` string in all files
98 142 fix
99 143
100 144 search type: commit (Commit message)
101 145 indexed fields: message
102 146
103 147 search type: path (File name)
104 148 indexed fields: path
105 149
106 150 % else:
107 151 Example filter terms for `ElasticSearch-${c.searcher.es_version}`search:
108 152 query lang: <a href="${c.searcher.query_lang_doc}">ES 6 Query Language</a>
109 153 The reserved characters needed espace by `\`: + - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /
110 154 % for handler in c.searcher.get_handlers().values():
111 155
112 156 search type: ${handler.search_type_label}
113 157 *indexed fields*: ${', '.join( [('\n ' if x[0]%4==0 else '')+x[1] for x in enumerate(handler.es_6_field_names)])}
114 158 % for entry in handler.es_6_example_queries:
115 159 ${entry.rstrip()}
116 160 % endfor
117 161 % endfor
118 162
119 163 % endif
120 164 </pre>
121 165 </div>
122 166
123 167 <div class="field">${c.runtime}</div>
124 168 </div>
125 169 </div>
126 170 </div>
127 171
128 172 ${h.end_form()}
129 173 <div class="search">
130 174 % if c.search_type == 'content':
131 175 <%include file='search_content.mako'/>
132 176 % elif c.search_type == 'path':
133 177 <%include file='search_path.mako'/>
134 178 % elif c.search_type == 'commit':
135 179 <%include file='search_commit.mako'/>
136 180 % elif c.search_type == 'repository':
137 181 <%include file='search_repository.mako'/>
138 182 % endif
139 183 </div>
140 184 </div>
141 185 <script>
142 186 $(document).ready(function(){
143 $('#q').autoGrowInput();
144 187 $("#id_search_type").select2({
145 188 'containerCssClass': "drop-menu",
146 189 'dropdownCssClass': "drop-menu-dropdown",
147 190 'dropdownAutoWidth': true,
148 191 'minimumResultsForSearch': -1
149 192 });
193
194 $('#q').autoGrowInput({maxWidth: 920});
195
196 setTimeout(function() {
197 $('#q').keyup()
198 }, 1);
150 199 })
151 200 </script>
152 201 </%def>
@@ -1,125 +1,128 b''
1 1 <%inherit file="/summary/summary_base.mako"/>
2 2
3 3 <%namespace name="components" file="/summary/components.mako"/>
4 4
5 5
6 6 <%def name="menu_bar_subnav()">
7 7 ${self.repo_menu(active='summary')}
8 8 </%def>
9 9
10 10 <%def name="main()">
11 11
12 12 <div class="title">
13 13 ${self.repo_page_title(c.rhodecode_db_repo)}
14 <ul class="links icon-only-links block-right">
15 <li>
16 %if c.rhodecode_user.username != h.DEFAULT_USER:
17 <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')}"><i class="icon-rss-sign"></i></a>
18 %else:
19 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_name)}" title="${_('RSS Feed')}"><i class="icon-rss-sign"></i></a>
20 %endif
21 </li>
22 </ul>
14 ## Context Action
15 <div>
16 <ul class="links icon-only-links block-right">
17 <li>
18 %if c.rhodecode_user.username != h.DEFAULT_USER:
19 <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>
20 %else:
21 <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>
22 %endif
23 </li>
24 </ul>
25 </div>
23 26 </div>
24 27
25 28 <div id="repo-summary" class="summary">
26 29 ${components.summary_detail(breadcrumbs_links=self.breadcrumbs_links(), show_downloads=True)}
27 30 ${components.summary_stats(gravatar_function=self.gravatar_with_user)}
28 31 </div><!--end repo-summary-->
29 32
30 33
31 34 <div class="box" >
32 35 %if not c.repo_commits:
33 36 <div class="title">
34 37 <h3>${_('Quick start')}</h3>
35 38 </div>
36 39 %endif
37 40 <div class="table">
38 41 <div id="shortlog_data">
39 42 <%include file='summary_commits.mako'/>
40 43 </div>
41 44 </div>
42 45 </div>
43 46
44 47 %if c.readme_data:
45 48 <div id="readme" class="anchor">
46 49 <div class="box" >
47 50 <div class="title" title="${h.tooltip(_('Readme file from commit %s:%s') % (c.rhodecode_db_repo.landing_rev[0], c.rhodecode_db_repo.landing_rev[1]))}">
48 51 <h3 class="breadcrumbs">
49 52 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.rhodecode_db_repo.landing_rev[1],f_path=c.readme_file)}">${c.readme_file}</a>
50 53 </h3>
51 54 </div>
52 55 <div class="readme codeblock">
53 56 <div class="readme_box">
54 57 ${c.readme_data|n}
55 58 </div>
56 59 </div>
57 60 </div>
58 61 </div>
59 62 %endif
60 63
61 64 <script type="text/javascript">
62 65 $(document).ready(function(){
63 66
64 67 var showCloneField = function(clone_url_format){
65 68 $.each(['http', 'http_id', 'ssh'], function (idx, val) {
66 69 if(val === clone_url_format){
67 70 $('#clone_option_' + val).show();
68 71 $('#clone_option').val(val)
69 72 } else {
70 73 $('#clone_option_' + val).hide();
71 74 }
72 75 });
73 76 };
74 77 // default taken from session
75 78 showCloneField(templateContext.session_attrs.clone_url_format);
76 79
77 80 $('#clone_option').on('change', function(e) {
78 81 var selected = $(this).val();
79 82
80 83 storeUserSessionAttr('rc_user_session_attr.clone_url_format', selected);
81 84 showCloneField(selected)
82 85 });
83 86
84 87 var initialCommitData = {
85 88 id: null,
86 89 text: 'tip',
87 90 type: 'tag',
88 91 raw_id: null,
89 92 files_url: null
90 93 };
91 94
92 95 select2RefSwitcher('#download_options', initialCommitData);
93 96
94 97 // on change of download options
95 98 $('#download_options').on('change', function(e) {
96 99 // format of Object {text: "v0.0.3", type: "tag", id: "rev"}
97 100 var ext = '.zip';
98 101 var selected_cs = e.added;
99 102 var fname = e.added.raw_id + ext;
100 103 var href = pyroutes.url('repo_archivefile', {'repo_name': templateContext.repo_name, 'fname':fname});
101 104 // set new label
102 105 $('#archive_link').html('<i class="icon-archive"></i> {0}{1}'.format(escapeHtml(e.added.text), ext));
103 106
104 107 // set new url to button,
105 108 $('#archive_link').attr('href', href)
106 109 });
107 110
108 111
109 112 // calculate size of repository
110 113 calculateSize = function () {
111 114
112 115 var callback = function (data) {
113 116 % if c.show_stats:
114 117 showRepoStats('lang_stats', data);
115 118 % endif
116 119 };
117 120
118 121 showRepoSize('repo_size_container', templateContext.repo_name, templateContext.repo_landing_commit, callback);
119 122
120 123 }
121 124
122 125 })
123 126 </script>
124 127
125 128 </%def>
General Comments 0
You need to be logged in to leave comments. Login now