##// END OF EJS Templates
go-to search: updated logic of goto switcher...
marcink -
r3556:a75b51f8 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,659 +1,746 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 func, true, or_, in_filter_generator, Repository, RepoGroup, User, UserGroup)
36 func, true, or_, case, 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 def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
108 def _get_repo_list(self, name_contains=None, repo_type=None, repo_group_name='', 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 .order_by(func.length(Repository.repo_name))\
116 .order_by(Repository.repo_name)\
117 115 .filter(Repository.archived.isnot(true()))\
118 116 .filter(or_(
119 117 # generate multiple IN to fix limitation problems
120 118 *in_filter_generator(Repository.repo_id, allowed_ids)
121 119 ))
122 120
121 query = query.order_by(case(
122 [
123 (Repository.repo_name.startswith(repo_group_name), repo_group_name+'/'),
124 ],
125 ))
126 query = query.order_by(func.length(Repository.repo_name))
127 query = query.order_by(Repository.repo_name)
128
123 129 if repo_type:
124 130 query = query.filter(Repository.repo_type == repo_type)
125 131
126 132 if name_contains:
127 133 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
128 134 query = query.filter(
129 135 Repository.repo_name.ilike(ilike_expression))
130 136 query = query.limit(limit)
131 137
132 138 acl_iter = query
133 139
134 140 return [
135 141 {
136 142 'id': obj.repo_name,
137 143 'value': org_query,
138 144 'value_display': obj.repo_name,
139 145 'text': obj.repo_name,
140 146 'type': 'repo',
141 147 'repo_id': obj.repo_id,
142 148 'repo_type': obj.repo_type,
143 149 'private': obj.private,
144 150 'url': h.route_path('repo_summary', repo_name=obj.repo_name)
145 151 }
146 152 for obj in acl_iter]
147 153
148 def _get_repo_group_list(self, name_contains=None, limit=20):
154 def _get_repo_group_list(self, name_contains=None, repo_group_name='', limit=20):
149 155 org_query = name_contains
150 156 allowed_ids = self._rhodecode_user.repo_group_acl_ids(
151 157 ['group.read', 'group.write', 'group.admin'],
152 158 cache=False, name_filter=name_contains) or [-1]
153 159
154 160 query = RepoGroup.query()\
155 .order_by(func.length(RepoGroup.group_name))\
156 .order_by(RepoGroup.group_name) \
157 161 .filter(or_(
158 162 # generate multiple IN to fix limitation problems
159 163 *in_filter_generator(RepoGroup.group_id, allowed_ids)
160 164 ))
161 165
166 query = query.order_by(case(
167 [
168 (RepoGroup.group_name.startswith(repo_group_name), repo_group_name+'/'),
169 ],
170 ))
171 query = query.order_by(func.length(RepoGroup.group_name))
172 query = query.order_by(RepoGroup.group_name)
173
162 174 if name_contains:
163 175 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
164 176 query = query.filter(
165 177 RepoGroup.group_name.ilike(ilike_expression))
166 178 query = query.limit(limit)
167 179
168 180 acl_iter = query
169 181
170 182 return [
171 183 {
172 184 'id': obj.group_name,
173 185 'value': org_query,
174 186 'value_display': obj.group_name,
175 187 'text': obj.group_name,
176 188 'type': 'repo_group',
177 189 'repo_group_id': obj.group_id,
178 190 'url': h.route_path(
179 191 'repo_group_home', repo_group_name=obj.group_name)
180 192 }
181 193 for obj in acl_iter]
182 194
183 195 def _get_user_list(self, name_contains=None, limit=20):
184 196 org_query = name_contains
185 197 if not name_contains:
186 return []
198 return [], False
187 199
188 name_contains = re.compile('(?:user:)(.+)').findall(name_contains)
200 # TODO(marcink): should all logged in users be allowed to search others?
201 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
202 if not allowed_user_search:
203 return [], False
204
205 name_contains = re.compile('(?:user:[ ]?)(.+)').findall(name_contains)
189 206 if len(name_contains) != 1:
190 return []
207 return [], False
208
191 209 name_contains = name_contains[0]
192 210
193 211 query = User.query()\
194 212 .order_by(func.length(User.username))\
195 213 .order_by(User.username) \
196 214 .filter(User.username != User.DEFAULT_USER)
197 215
198 216 if name_contains:
199 217 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
200 218 query = query.filter(
201 219 User.username.ilike(ilike_expression))
202 220 query = query.limit(limit)
203 221
204 222 acl_iter = query
205 223
206 224 return [
207 225 {
208 226 'id': obj.user_id,
209 227 'value': org_query,
210 'value_display': obj.username,
228 'value_display': 'user: `{}`'.format(obj.username),
211 229 'type': 'user',
212 230 'icon_link': h.gravatar_url(obj.email, 30),
213 231 'url': h.route_path(
214 232 'user_profile', username=obj.username)
215 233 }
216 for obj in acl_iter]
234 for obj in acl_iter], True
217 235
218 236 def _get_user_groups_list(self, name_contains=None, limit=20):
219 237 org_query = name_contains
220 238 if not name_contains:
221 return []
239 return [], False
222 240
223 name_contains = re.compile('(?:user_group:)(.+)').findall(name_contains)
241 # TODO(marcink): should all logged in users be allowed to search others?
242 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
243 if not allowed_user_search:
244 return [], False
245
246 name_contains = re.compile('(?:user_group:[ ]?)(.+)').findall(name_contains)
224 247 if len(name_contains) != 1:
225 return []
248 return [], False
249
226 250 name_contains = name_contains[0]
227 251
228 252 query = UserGroup.query()\
229 253 .order_by(func.length(UserGroup.users_group_name))\
230 254 .order_by(UserGroup.users_group_name)
231 255
232 256 if name_contains:
233 257 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
234 258 query = query.filter(
235 259 UserGroup.users_group_name.ilike(ilike_expression))
236 260 query = query.limit(limit)
237 261
238 262 acl_iter = query
239 263
240 264 return [
241 265 {
242 266 'id': obj.users_group_id,
243 267 'value': org_query,
244 'value_display': obj.users_group_name,
268 'value_display': 'user_group: `{}`'.format(obj.users_group_name),
245 269 'type': 'user_group',
246 270 'url': h.route_path(
247 271 'user_group_profile', user_group_name=obj.users_group_name)
248 272 }
249 for obj in acl_iter]
273 for obj in acl_iter], True
250 274
251 def _get_hash_commit_list(self, auth_user, searcher, query):
275 def _get_hash_commit_list(self, auth_user, searcher, query, repo=None, repo_group=None):
276 repo_name = repo_group_name = None
277 if repo:
278 repo_name = repo.repo_name
279 if repo_group:
280 repo_group_name = repo_group.group_name
281
252 282 org_query = query
253 283 if not query or len(query) < 3 or not searcher:
254 return []
284 return [], False
255 285
256 commit_hashes = re.compile('(?:commit:)([0-9a-f]{2,40})').findall(query)
286 commit_hashes = re.compile('(?:commit:[ ]?)([0-9a-f]{2,40})').findall(query)
257 287
258 288 if len(commit_hashes) != 1:
259 return []
289 return [], False
290
260 291 commit_hash = commit_hashes[0]
261 292
262 293 result = searcher.search(
263 294 'commit_id:{}*'.format(commit_hash), 'commit', auth_user,
264 raise_on_exc=False)
295 repo_name, repo_group_name, raise_on_exc=False)
265 296
266 297 commits = []
267 298 for entry in result['results']:
268 299 repo_data = {
269 300 'repository_id': entry.get('repository_id'),
270 301 'repository_type': entry.get('repo_type'),
271 302 'repository_name': entry.get('repository'),
272 303 }
273 304
274 305 commit_entry = {
275 306 'id': entry['commit_id'],
276 307 'value': org_query,
277 308 'value_display': '`{}` commit: {}'.format(
278 309 entry['repository'], entry['commit_id']),
279 310 'type': 'commit',
280 311 'repo': entry['repository'],
281 312 'repo_data': repo_data,
282 313
283 314 'url': h.route_path(
284 315 'repo_commit',
285 316 repo_name=entry['repository'], commit_id=entry['commit_id'])
286 317 }
287 318
288 319 commits.append(commit_entry)
289 return commits
320 return commits, True
290 321
291 def _get_path_list(self, auth_user, searcher, query):
322 def _get_path_list(self, auth_user, searcher, query, repo=None, repo_group=None):
323 repo_name = repo_group_name = None
324 if repo:
325 repo_name = repo.repo_name
326 if repo_group:
327 repo_group_name = repo_group.group_name
328
292 329 org_query = query
293 330 if not query or len(query) < 3 or not searcher:
294 return []
331 return [], False
295 332
296 paths_re = re.compile('(?:file:)(.{1,})').findall(query)
333 paths_re = re.compile('(?:file:[ ]?)(.+)').findall(query)
297 334 if len(paths_re) != 1:
298 return []
335 return [], False
336
299 337 file_path = paths_re[0]
300 338
301 339 search_path = searcher.escape_specials(file_path)
302 340 result = searcher.search(
303 341 'file.raw:*{}*'.format(search_path), 'path', auth_user,
304 raise_on_exc=False)
342 repo_name, repo_group_name, raise_on_exc=False)
305 343
306 344 files = []
307 345 for entry in result['results']:
308 346 repo_data = {
309 347 'repository_id': entry.get('repository_id'),
310 348 'repository_type': entry.get('repo_type'),
311 349 'repository_name': entry.get('repository'),
312 350 }
313 351
314 352 file_entry = {
315 353 'id': entry['commit_id'],
316 354 'value': org_query,
317 355 'value_display': '`{}` file: {}'.format(
318 356 entry['repository'], entry['file']),
319 357 'type': 'file',
320 358 'repo': entry['repository'],
321 359 'repo_data': repo_data,
322 360
323 361 'url': h.route_path(
324 362 'repo_files',
325 363 repo_name=entry['repository'], commit_id=entry['commit_id'],
326 364 f_path=entry['file'])
327 365 }
328 366
329 367 files.append(file_entry)
330 return files
368 return files, True
331 369
332 370 @LoginRequired()
333 371 @view_config(
334 372 route_name='repo_list_data', request_method='GET',
335 373 renderer='json_ext', xhr=True)
336 374 def repo_list_data(self):
337 375 _ = self.request.translate
338 376 self.load_default_context()
339 377
340 378 query = self.request.GET.get('query')
341 379 repo_type = self.request.GET.get('repo_type')
342 380 log.debug('generating repo list, query:%s, repo_type:%s',
343 381 query, repo_type)
344 382
345 383 res = []
346 384 repos = self._get_repo_list(query, repo_type=repo_type)
347 385 if repos:
348 386 res.append({
349 387 'text': _('Repositories'),
350 388 'children': repos
351 389 })
352 390
353 391 data = {
354 392 'more': False,
355 393 'results': res
356 394 }
357 395 return data
358 396
359 397 @LoginRequired()
360 398 @view_config(
361 399 route_name='repo_group_list_data', request_method='GET',
362 400 renderer='json_ext', xhr=True)
363 401 def repo_group_list_data(self):
364 402 _ = self.request.translate
365 403 self.load_default_context()
366 404
367 405 query = self.request.GET.get('query')
368 406
369 407 log.debug('generating repo group list, query:%s',
370 408 query)
371 409
372 410 res = []
373 411 repo_groups = self._get_repo_group_list(query)
374 412 if repo_groups:
375 413 res.append({
376 414 'text': _('Repository Groups'),
377 415 'children': repo_groups
378 416 })
379 417
380 418 data = {
381 419 'more': False,
382 420 'results': res
383 421 }
384 422 return data
385 423
386 424 def _get_default_search_queries(self, search_context, searcher, query):
387 425 if not searcher:
388 426 return []
389 427
390 428 is_es_6 = searcher.is_es_6
391 429
392 430 queries = []
393 431 repo_group_name, repo_name, repo_context = None, None, None
394 432
395 433 # repo group context
396 434 if search_context.get('search_context[repo_group_name]'):
397 435 repo_group_name = search_context.get('search_context[repo_group_name]')
398 436 if search_context.get('search_context[repo_name]'):
399 437 repo_name = search_context.get('search_context[repo_name]')
400 438 repo_context = search_context.get('search_context[repo_view_type]')
401 439
402 440 if is_es_6 and repo_name:
403 441 # files
404 442 def query_modifier():
405 443 qry = query
406 444 return {'q': qry, 'type': 'content'}
407 445 label = u'File search for `{}` in this repository.'.format(query)
408 queries.append(
409 {
446 file_qry = {
410 447 'id': -10,
411 448 'value': query,
412 449 'value_display': label,
413 450 'type': 'search',
414 451 'url': h.route_path('search_repo',
415 452 repo_name=repo_name,
416 453 _query=query_modifier())
417 454 }
418 )
419 455
420 456 # commits
421 457 def query_modifier():
422 458 qry = query
423 459 return {'q': qry, 'type': 'commit'}
424 460
425 461 label = u'Commit search for `{}` in this repository.'.format(query)
426 queries.append(
427 {
462 commit_qry = {
428 463 'id': -20,
429 464 'value': query,
430 465 'value_display': label,
431 466 'type': 'search',
432 467 'url': h.route_path('search_repo',
433 468 repo_name=repo_name,
434 469 _query=query_modifier())
435 470 }
436 )
471
472 if repo_context in ['commit', 'changelog']:
473 queries.extend([commit_qry, file_qry])
474 elif repo_context in ['files', 'summary']:
475 queries.extend([file_qry, commit_qry])
476 else:
477 queries.extend([commit_qry, file_qry])
437 478
438 479 elif is_es_6 and repo_group_name:
439 480 # files
440 481 def query_modifier():
441 482 qry = query
442 483 return {'q': qry, 'type': 'content'}
443 484
444 485 label = u'File search for `{}` in this repository group'.format(query)
445 queries.append(
446 {
486 file_qry = {
447 487 'id': -30,
448 488 'value': query,
449 489 'value_display': label,
450 490 'type': 'search',
451 491 'url': h.route_path('search_repo_group',
452 492 repo_group_name=repo_group_name,
453 493 _query=query_modifier())
454 494 }
455 )
456 495
457 496 # commits
458 497 def query_modifier():
459 498 qry = query
460 499 return {'q': qry, 'type': 'commit'}
461 500
462 501 label = u'Commit search for `{}` in this repository group'.format(query)
463 queries.append(
464 {
502 commit_qry = {
465 503 'id': -40,
466 504 'value': query,
467 505 'value_display': label,
468 506 'type': 'search',
469 507 'url': h.route_path('search_repo_group',
470 508 repo_group_name=repo_group_name,
471 509 _query=query_modifier())
472 510 }
473 )
474 511
512 if repo_context in ['commit', 'changelog']:
513 queries.extend([commit_qry, file_qry])
514 elif repo_context in ['files', 'summary']:
515 queries.extend([file_qry, commit_qry])
516 else:
517 queries.extend([commit_qry, file_qry])
518
519 # Global, not scoped
475 520 if not queries:
476 521 queries.append(
477 522 {
478 523 'id': -1,
479 524 'value': query,
480 525 'value_display': u'File search for: `{}`'.format(query),
481 526 'type': 'search',
482 527 'url': h.route_path('search',
483 528 _query={'q': query, 'type': 'content'})
484 529 })
485 530 queries.append(
486 531 {
487 532 'id': -2,
488 533 'value': query,
489 534 'value_display': u'Commit search for: `{}`'.format(query),
490 535 'type': 'search',
491 536 'url': h.route_path('search',
492 537 _query={'q': query, 'type': 'commit'})
493 538 })
494 539
495 540 return queries
496 541
497 542 @LoginRequired()
498 543 @view_config(
499 544 route_name='goto_switcher_data', request_method='GET',
500 545 renderer='json_ext', xhr=True)
501 546 def goto_switcher_data(self):
502 547 c = self.load_default_context()
503 548
504 549 _ = self.request.translate
505 550
506 551 query = self.request.GET.get('query')
507 552 log.debug('generating main filter data, query %s', query)
508 553
509 554 res = []
510 555 if not query:
511 556 return {'suggestions': res}
512 557
558 def no_match(name):
559 return {
560 'id': -1,
561 'value': "",
562 'value_display': name,
563 'type': 'text',
564 'url': ""
565 }
513 566 searcher = searcher_from_config(self.request.registry.settings)
514 for _q in self._get_default_search_queries(self.request.GET, searcher, query):
515 res.append(_q)
567 has_specialized_search = False
516 568
569 # set repo context
570 repo = None
571 repo_id = safe_int(self.request.GET.get('search_context[repo_id]'))
572 if repo_id:
573 repo = Repository.get(repo_id)
574
575 # set group context
576 repo_group = None
517 577 repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]'))
518 578 if repo_group_id:
519 579 repo_group = RepoGroup.get(repo_group_id)
520 composed_hint = '{}/{}'.format(repo_group.group_name, query)
521 show_hint = not query.startswith(repo_group.group_name)
522 if repo_group and show_hint:
523 hint = u'Repository search inside: `{}`'.format(composed_hint)
524 res.append({
525 'id': -1,
526 'value': composed_hint,
527 'value_display': hint,
528 'type': 'hint',
529 'url': ""
530 })
580 prefix_match = False
531 581
532 repo_groups = self._get_repo_group_list(query)
533 for serialized_repo_group in repo_groups:
534 res.append(serialized_repo_group)
535
536 repos = self._get_repo_list(query)
537 for serialized_repo in repos:
538 res.append(serialized_repo)
539
540 # TODO(marcink): should all logged in users be allowed to search others?
541 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
542 if allowed_user_search:
543 users = self._get_user_list(query)
582 # user: type search
583 if not prefix_match:
584 users, prefix_match = self._get_user_list(query)
585 if users:
586 has_specialized_search = True
544 587 for serialized_user in users:
545 588 res.append(serialized_user)
589 elif prefix_match:
590 has_specialized_search = True
591 res.append(no_match('No matching users found'))
546 592
547 user_groups = self._get_user_groups_list(query)
593 # user_group: type search
594 if not prefix_match:
595 user_groups, prefix_match = self._get_user_groups_list(query)
596 if user_groups:
597 has_specialized_search = True
548 598 for serialized_user_group in user_groups:
549 599 res.append(serialized_user_group)
600 elif prefix_match:
601 has_specialized_search = True
602 res.append(no_match('No matching user groups found'))
550 603
551 commits = self._get_hash_commit_list(c.auth_user, searcher, query)
604 # FTS commit: type search
605 if not prefix_match:
606 commits, prefix_match = self._get_hash_commit_list(
607 c.auth_user, searcher, query, repo, repo_group)
552 608 if commits:
609 has_specialized_search = True
553 610 unique_repos = collections.OrderedDict()
554 611 for commit in commits:
555 612 repo_name = commit['repo']
556 613 unique_repos.setdefault(repo_name, []).append(commit)
557 614
558 for repo, commits in unique_repos.items():
615 for _repo, commits in unique_repos.items():
559 616 for commit in commits:
560 617 res.append(commit)
618 elif prefix_match:
619 has_specialized_search = True
620 res.append(no_match('No matching commits found'))
561 621
562 paths = self._get_path_list(c.auth_user, searcher, query)
622 # FTS file: type search
623 if not prefix_match:
624 paths, prefix_match = self._get_path_list(
625 c.auth_user, searcher, query, repo, repo_group)
563 626 if paths:
627 has_specialized_search = True
564 628 unique_repos = collections.OrderedDict()
565 629 for path in paths:
566 630 repo_name = path['repo']
567 631 unique_repos.setdefault(repo_name, []).append(path)
568 632
569 633 for repo, paths in unique_repos.items():
570 634 for path in paths:
571 635 res.append(path)
636 elif prefix_match:
637 has_specialized_search = True
638 res.append(no_match('No matching files found'))
639
640 # main suggestions
641 if not has_specialized_search:
642 repo_group_name = ''
643 if repo_group:
644 repo_group_name = repo_group.group_name
645
646 for _q in self._get_default_search_queries(self.request.GET, searcher, query):
647 res.append(_q)
648
649 repo_groups = self._get_repo_group_list(query, repo_group_name=repo_group_name)
650 for serialized_repo_group in repo_groups:
651 res.append(serialized_repo_group)
652
653 repos = self._get_repo_list(query, repo_group_name=repo_group_name)
654 for serialized_repo in repos:
655 res.append(serialized_repo)
656
657 if not repos and not repo_groups:
658 res.append(no_match('No matches found'))
572 659
573 660 return {'suggestions': res}
574 661
575 662 def _get_groups_and_repos(self, repo_group_id=None):
576 663 # repo groups groups
577 664 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
578 665 _perms = ['group.read', 'group.write', 'group.admin']
579 666 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
580 667 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
581 668 repo_group_list=repo_group_list_acl, admin=False)
582 669
583 670 # repositories
584 671 repo_list = Repository.get_all_repos(group_id=repo_group_id)
585 672 _perms = ['repository.read', 'repository.write', 'repository.admin']
586 673 repo_list_acl = RepoList(repo_list, perm_set=_perms)
587 674 repo_data = RepoModel().get_repos_as_dict(
588 675 repo_list=repo_list_acl, admin=False)
589 676
590 677 return repo_data, repo_group_data
591 678
592 679 @LoginRequired()
593 680 @view_config(
594 681 route_name='home', request_method='GET',
595 682 renderer='rhodecode:templates/index.mako')
596 683 def main_page(self):
597 684 c = self.load_default_context()
598 685 c.repo_group = None
599 686
600 687 repo_data, repo_group_data = self._get_groups_and_repos()
601 688 # json used to render the grids
602 689 c.repos_data = json.dumps(repo_data)
603 690 c.repo_groups_data = json.dumps(repo_group_data)
604 691
605 692 return self._get_template_context(c)
606 693
607 694 @LoginRequired()
608 695 @HasRepoGroupPermissionAnyDecorator(
609 696 'group.read', 'group.write', 'group.admin')
610 697 @view_config(
611 698 route_name='repo_group_home', request_method='GET',
612 699 renderer='rhodecode:templates/index_repo_group.mako')
613 700 @view_config(
614 701 route_name='repo_group_home_slash', request_method='GET',
615 702 renderer='rhodecode:templates/index_repo_group.mako')
616 703 def repo_group_main_page(self):
617 704 c = self.load_default_context()
618 705 c.repo_group = self.request.db_repo_group
619 706 repo_data, repo_group_data = self._get_groups_and_repos(
620 707 c.repo_group.group_id)
621 708
622 709 # json used to render the grids
623 710 c.repos_data = json.dumps(repo_data)
624 711 c.repo_groups_data = json.dumps(repo_group_data)
625 712
626 713 return self._get_template_context(c)
627 714
628 715 @LoginRequired()
629 716 @CSRFRequired()
630 717 @view_config(
631 718 route_name='markup_preview', request_method='POST',
632 719 renderer='string', xhr=True)
633 720 def markup_preview(self):
634 721 # Technically a CSRF token is not needed as no state changes with this
635 722 # call. However, as this is a POST is better to have it, so automated
636 723 # tools don't flag it as potential CSRF.
637 724 # Post is required because the payload could be bigger than the maximum
638 725 # allowed by GET.
639 726
640 727 text = self.request.POST.get('text')
641 728 renderer = self.request.POST.get('renderer') or 'rst'
642 729 if text:
643 730 return h.render(text, renderer=renderer, mentions=True)
644 731 return ''
645 732
646 733 @LoginRequired()
647 734 @CSRFRequired()
648 735 @view_config(
649 736 route_name='store_user_session_value', request_method='POST',
650 737 renderer='string', xhr=True)
651 738 def store_user_session_attr(self):
652 739 key = self.request.POST.get('key')
653 740 val = self.request.POST.get('val')
654 741
655 742 existing_value = self.request.session.get(key)
656 743 if existing_value != val:
657 744 self.request.session[key] = val
658 745
659 746 return 'stored:{}'.format(key)
@@ -1,2042 +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 1498 def breadcrumb_repo_group_link(repo_group):
1499 1499 """
1500 1500 Makes a breadcrumbs path link to repo
1501 1501
1502 1502 ex::
1503 1503 group >> subgroup
1504 1504
1505 1505 :param repo_group: a Repository Group instance
1506 1506 """
1507 1507
1508 1508 path = [
1509 1509 link_to(group.name,
1510 1510 route_path('repo_group_home', repo_group_name=group.group_name))
1511 1511 for group in repo_group.parents
1512 1512 ] + [
1513 1513 link_to(repo_group.name,
1514 1514 route_path('repo_group_home', repo_group_name=repo_group.group_name))
1515 1515 ]
1516 1516
1517 1517 return literal(' &raquo; '.join(path))
1518 1518
1519 1519
1520 1520 def format_byte_size_binary(file_size):
1521 1521 """
1522 1522 Formats file/folder sizes to standard.
1523 1523 """
1524 1524 if file_size is None:
1525 1525 file_size = 0
1526 1526
1527 1527 formatted_size = format_byte_size(file_size, binary=True)
1528 1528 return formatted_size
1529 1529
1530 1530
1531 1531 def urlify_text(text_, safe=True):
1532 1532 """
1533 1533 Extrac urls from text and make html links out of them
1534 1534
1535 1535 :param text_:
1536 1536 """
1537 1537
1538 1538 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1539 1539 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1540 1540
1541 1541 def url_func(match_obj):
1542 1542 url_full = match_obj.groups()[0]
1543 1543 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1544 1544 _newtext = url_pat.sub(url_func, text_)
1545 1545 if safe:
1546 1546 return literal(_newtext)
1547 1547 return _newtext
1548 1548
1549 1549
1550 1550 def urlify_commits(text_, repository):
1551 1551 """
1552 1552 Extract commit ids from text and make link from them
1553 1553
1554 1554 :param text_:
1555 1555 :param repository: repo name to build the URL with
1556 1556 """
1557 1557
1558 1558 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1559 1559
1560 1560 def url_func(match_obj):
1561 1561 commit_id = match_obj.groups()[1]
1562 1562 pref = match_obj.groups()[0]
1563 1563 suf = match_obj.groups()[2]
1564 1564
1565 1565 tmpl = (
1566 1566 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1567 1567 '%(commit_id)s</a>%(suf)s'
1568 1568 )
1569 1569 return tmpl % {
1570 1570 'pref': pref,
1571 1571 'cls': 'revision-link',
1572 1572 'url': route_url('repo_commit', repo_name=repository, commit_id=commit_id),
1573 1573 'commit_id': commit_id,
1574 1574 'suf': suf
1575 1575 }
1576 1576
1577 1577 newtext = URL_PAT.sub(url_func, text_)
1578 1578
1579 1579 return newtext
1580 1580
1581 1581
1582 1582 def _process_url_func(match_obj, repo_name, uid, entry,
1583 1583 return_raw_data=False, link_format='html'):
1584 1584 pref = ''
1585 1585 if match_obj.group().startswith(' '):
1586 1586 pref = ' '
1587 1587
1588 1588 issue_id = ''.join(match_obj.groups())
1589 1589
1590 1590 if link_format == 'html':
1591 1591 tmpl = (
1592 1592 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1593 1593 '%(issue-prefix)s%(id-repr)s'
1594 1594 '</a>')
1595 1595 elif link_format == 'rst':
1596 1596 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1597 1597 elif link_format == 'markdown':
1598 1598 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1599 1599 else:
1600 1600 raise ValueError('Bad link_format:{}'.format(link_format))
1601 1601
1602 1602 (repo_name_cleaned,
1603 1603 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1604 1604
1605 1605 # variables replacement
1606 1606 named_vars = {
1607 1607 'id': issue_id,
1608 1608 'repo': repo_name,
1609 1609 'repo_name': repo_name_cleaned,
1610 1610 'group_name': parent_group_name
1611 1611 }
1612 1612 # named regex variables
1613 1613 named_vars.update(match_obj.groupdict())
1614 1614 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1615 1615
1616 1616 def quote_cleaner(input_str):
1617 1617 """Remove quotes as it's HTML"""
1618 1618 return input_str.replace('"', '')
1619 1619
1620 1620 data = {
1621 1621 'pref': pref,
1622 1622 'cls': quote_cleaner('issue-tracker-link'),
1623 1623 'url': quote_cleaner(_url),
1624 1624 'id-repr': issue_id,
1625 1625 'issue-prefix': entry['pref'],
1626 1626 'serv': entry['url'],
1627 1627 }
1628 1628 if return_raw_data:
1629 1629 return {
1630 1630 'id': issue_id,
1631 1631 'url': _url
1632 1632 }
1633 1633 return tmpl % data
1634 1634
1635 1635
1636 1636 def get_active_pattern_entries(repo_name):
1637 1637 repo = None
1638 1638 if repo_name:
1639 1639 # Retrieving repo_name to avoid invalid repo_name to explode on
1640 1640 # IssueTrackerSettingsModel but still passing invalid name further down
1641 1641 repo = Repository.get_by_repo_name(repo_name, cache=True)
1642 1642
1643 1643 settings_model = IssueTrackerSettingsModel(repo=repo)
1644 1644 active_entries = settings_model.get_settings(cache=True)
1645 1645 return active_entries
1646 1646
1647 1647
1648 1648 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1649 1649
1650 1650 allowed_formats = ['html', 'rst', 'markdown']
1651 1651 if link_format not in allowed_formats:
1652 1652 raise ValueError('Link format can be only one of:{} got {}'.format(
1653 1653 allowed_formats, link_format))
1654 1654
1655 1655 active_entries = active_entries or get_active_pattern_entries(repo_name)
1656 1656 issues_data = []
1657 1657 newtext = text_string
1658 1658
1659 1659 for uid, entry in active_entries.items():
1660 1660 log.debug('found issue tracker entry with uid %s', uid)
1661 1661
1662 1662 if not (entry['pat'] and entry['url']):
1663 1663 log.debug('skipping due to missing data')
1664 1664 continue
1665 1665
1666 1666 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1667 1667 uid, entry['pat'], entry['url'], entry['pref'])
1668 1668
1669 1669 try:
1670 1670 pattern = re.compile(r'%s' % entry['pat'])
1671 1671 except re.error:
1672 1672 log.exception(
1673 1673 'issue tracker pattern: `%s` failed to compile',
1674 1674 entry['pat'])
1675 1675 continue
1676 1676
1677 1677 data_func = partial(
1678 1678 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1679 1679 return_raw_data=True)
1680 1680
1681 1681 for match_obj in pattern.finditer(text_string):
1682 1682 issues_data.append(data_func(match_obj))
1683 1683
1684 1684 url_func = partial(
1685 1685 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1686 1686 link_format=link_format)
1687 1687
1688 1688 newtext = pattern.sub(url_func, newtext)
1689 1689 log.debug('processed prefix:uid `%s`', uid)
1690 1690
1691 1691 return newtext, issues_data
1692 1692
1693 1693
1694 1694 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1695 1695 """
1696 1696 Parses given text message and makes proper links.
1697 1697 issues are linked to given issue-server, and rest is a commit link
1698 1698
1699 1699 :param commit_text:
1700 1700 :param repository:
1701 1701 """
1702 1702 def escaper(string):
1703 1703 return string.replace('<', '&lt;').replace('>', '&gt;')
1704 1704
1705 1705 newtext = escaper(commit_text)
1706 1706
1707 1707 # extract http/https links and make them real urls
1708 1708 newtext = urlify_text(newtext, safe=False)
1709 1709
1710 1710 # urlify commits - extract commit ids and make link out of them, if we have
1711 1711 # the scope of repository present.
1712 1712 if repository:
1713 1713 newtext = urlify_commits(newtext, repository)
1714 1714
1715 1715 # process issue tracker patterns
1716 1716 newtext, issues = process_patterns(newtext, repository or '',
1717 1717 active_entries=active_pattern_entries)
1718 1718
1719 1719 return literal(newtext)
1720 1720
1721 1721
1722 1722 def render_binary(repo_name, file_obj):
1723 1723 """
1724 1724 Choose how to render a binary file
1725 1725 """
1726 1726
1727 1727 filename = file_obj.name
1728 1728
1729 1729 # images
1730 1730 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1731 1731 if fnmatch.fnmatch(filename, pat=ext):
1732 1732 alt = escape(filename)
1733 1733 src = route_path(
1734 1734 'repo_file_raw', repo_name=repo_name,
1735 1735 commit_id=file_obj.commit.raw_id,
1736 1736 f_path=file_obj.path)
1737 1737 return literal(
1738 1738 '<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1739 1739
1740 1740
1741 1741 def renderer_from_filename(filename, exclude=None):
1742 1742 """
1743 1743 choose a renderer based on filename, this works only for text based files
1744 1744 """
1745 1745
1746 1746 # ipython
1747 1747 for ext in ['*.ipynb']:
1748 1748 if fnmatch.fnmatch(filename, pat=ext):
1749 1749 return 'jupyter'
1750 1750
1751 1751 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1752 1752 if is_markup:
1753 1753 return is_markup
1754 1754 return None
1755 1755
1756 1756
1757 1757 def render(source, renderer='rst', mentions=False, relative_urls=None,
1758 1758 repo_name=None):
1759 1759
1760 1760 def maybe_convert_relative_links(html_source):
1761 1761 if relative_urls:
1762 1762 return relative_links(html_source, relative_urls)
1763 1763 return html_source
1764 1764
1765 1765 if renderer == 'plain':
1766 1766 return literal(
1767 1767 MarkupRenderer.plain(source, leading_newline=False))
1768 1768
1769 1769 elif renderer == 'rst':
1770 1770 if repo_name:
1771 1771 # process patterns on comments if we pass in repo name
1772 1772 source, issues = process_patterns(
1773 1773 source, repo_name, link_format='rst')
1774 1774
1775 1775 return literal(
1776 1776 '<div class="rst-block">%s</div>' %
1777 1777 maybe_convert_relative_links(
1778 1778 MarkupRenderer.rst(source, mentions=mentions)))
1779 1779
1780 1780 elif renderer == 'markdown':
1781 1781 if repo_name:
1782 1782 # process patterns on comments if we pass in repo name
1783 1783 source, issues = process_patterns(
1784 1784 source, repo_name, link_format='markdown')
1785 1785
1786 1786 return literal(
1787 1787 '<div class="markdown-block">%s</div>' %
1788 1788 maybe_convert_relative_links(
1789 1789 MarkupRenderer.markdown(source, flavored=True,
1790 1790 mentions=mentions)))
1791 1791
1792 1792 elif renderer == 'jupyter':
1793 1793 return literal(
1794 1794 '<div class="ipynb">%s</div>' %
1795 1795 maybe_convert_relative_links(
1796 1796 MarkupRenderer.jupyter(source)))
1797 1797
1798 1798 # None means just show the file-source
1799 1799 return None
1800 1800
1801 1801
1802 1802 def commit_status(repo, commit_id):
1803 1803 return ChangesetStatusModel().get_status(repo, commit_id)
1804 1804
1805 1805
1806 1806 def commit_status_lbl(commit_status):
1807 1807 return dict(ChangesetStatus.STATUSES).get(commit_status)
1808 1808
1809 1809
1810 1810 def commit_time(repo_name, commit_id):
1811 1811 repo = Repository.get_by_repo_name(repo_name)
1812 1812 commit = repo.get_commit(commit_id=commit_id)
1813 1813 return commit.date
1814 1814
1815 1815
1816 1816 def get_permission_name(key):
1817 1817 return dict(Permission.PERMS).get(key)
1818 1818
1819 1819
1820 1820 def journal_filter_help(request):
1821 1821 _ = request.translate
1822 1822 from rhodecode.lib.audit_logger import ACTIONS
1823 1823 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1824 1824
1825 1825 return _(
1826 1826 'Example filter terms:\n' +
1827 1827 ' repository:vcs\n' +
1828 1828 ' username:marcin\n' +
1829 1829 ' username:(NOT marcin)\n' +
1830 1830 ' action:*push*\n' +
1831 1831 ' ip:127.0.0.1\n' +
1832 1832 ' date:20120101\n' +
1833 1833 ' date:[20120101100000 TO 20120102]\n' +
1834 1834 '\n' +
1835 1835 'Actions: {actions}\n' +
1836 1836 '\n' +
1837 1837 'Generate wildcards using \'*\' character:\n' +
1838 1838 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1839 1839 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1840 1840 '\n' +
1841 1841 'Optional AND / OR operators in queries\n' +
1842 1842 ' "repository:vcs OR repository:test"\n' +
1843 1843 ' "username:test AND repository:test*"\n'
1844 1844 ).format(actions=actions)
1845 1845
1846 1846
1847 1847 def not_mapped_error(repo_name):
1848 1848 from rhodecode.translation import _
1849 1849 flash(_('%s repository is not mapped to db perhaps'
1850 1850 ' it was created or renamed from the filesystem'
1851 1851 ' please run the application again'
1852 1852 ' in order to rescan repositories') % repo_name, category='error')
1853 1853
1854 1854
1855 1855 def ip_range(ip_addr):
1856 1856 from rhodecode.model.db import UserIpMap
1857 1857 s, e = UserIpMap._get_ip_range(ip_addr)
1858 1858 return '%s - %s' % (s, e)
1859 1859
1860 1860
1861 1861 def form(url, method='post', needs_csrf_token=True, **attrs):
1862 1862 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1863 1863 if method.lower() != 'get' and needs_csrf_token:
1864 1864 raise Exception(
1865 1865 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1866 1866 'CSRF token. If the endpoint does not require such token you can ' +
1867 1867 'explicitly set the parameter needs_csrf_token to false.')
1868 1868
1869 1869 return wh_form(url, method=method, **attrs)
1870 1870
1871 1871
1872 1872 def secure_form(form_url, method="POST", multipart=False, **attrs):
1873 1873 """Start a form tag that points the action to an url. This
1874 1874 form tag will also include the hidden field containing
1875 1875 the auth token.
1876 1876
1877 1877 The url options should be given either as a string, or as a
1878 1878 ``url()`` function. The method for the form defaults to POST.
1879 1879
1880 1880 Options:
1881 1881
1882 1882 ``multipart``
1883 1883 If set to True, the enctype is set to "multipart/form-data".
1884 1884 ``method``
1885 1885 The method to use when submitting the form, usually either
1886 1886 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1887 1887 hidden input with name _method is added to simulate the verb
1888 1888 over POST.
1889 1889
1890 1890 """
1891 1891 from webhelpers.pylonslib.secure_form import insecure_form
1892 1892
1893 1893 if 'request' in attrs:
1894 1894 session = attrs['request'].session
1895 1895 del attrs['request']
1896 1896 else:
1897 1897 raise ValueError(
1898 1898 'Calling this form requires request= to be passed as argument')
1899 1899
1900 1900 form = insecure_form(form_url, method, multipart, **attrs)
1901 1901 token = literal(
1902 1902 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1903 1903 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1904 1904
1905 1905 return literal("%s\n%s" % (form, token))
1906 1906
1907 1907
1908 1908 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1909 1909 select_html = select(name, selected, options, **attrs)
1910 1910 select2 = """
1911 1911 <script>
1912 1912 $(document).ready(function() {
1913 1913 $('#%s').select2({
1914 1914 containerCssClass: 'drop-menu',
1915 1915 dropdownCssClass: 'drop-menu-dropdown',
1916 1916 dropdownAutoWidth: true%s
1917 1917 });
1918 1918 });
1919 1919 </script>
1920 1920 """
1921 1921 filter_option = """,
1922 1922 minimumResultsForSearch: -1
1923 1923 """
1924 1924 input_id = attrs.get('id') or name
1925 1925 filter_enabled = "" if enable_filter else filter_option
1926 1926 select_script = literal(select2 % (input_id, filter_enabled))
1927 1927
1928 1928 return literal(select_html+select_script)
1929 1929
1930 1930
1931 1931 def get_visual_attr(tmpl_context_var, attr_name):
1932 1932 """
1933 1933 A safe way to get a variable from visual variable of template context
1934 1934
1935 1935 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1936 1936 :param attr_name: name of the attribute we fetch from the c.visual
1937 1937 """
1938 1938 visual = getattr(tmpl_context_var, 'visual', None)
1939 1939 if not visual:
1940 1940 return
1941 1941 else:
1942 1942 return getattr(visual, attr_name, None)
1943 1943
1944 1944
1945 1945 def get_last_path_part(file_node):
1946 1946 if not file_node.path:
1947 1947 return u''
1948 1948
1949 1949 path = safe_unicode(file_node.path.split('/')[-1])
1950 1950 return u'../' + path
1951 1951
1952 1952
1953 1953 def route_url(*args, **kwargs):
1954 1954 """
1955 1955 Wrapper around pyramids `route_url` (fully qualified url) function.
1956 1956 """
1957 1957 req = get_current_request()
1958 1958 return req.route_url(*args, **kwargs)
1959 1959
1960 1960
1961 1961 def route_path(*args, **kwargs):
1962 1962 """
1963 1963 Wrapper around pyramids `route_path` function.
1964 1964 """
1965 1965 req = get_current_request()
1966 1966 return req.route_path(*args, **kwargs)
1967 1967
1968 1968
1969 1969 def route_path_or_none(*args, **kwargs):
1970 1970 try:
1971 1971 return route_path(*args, **kwargs)
1972 1972 except KeyError:
1973 1973 return None
1974 1974
1975 1975
1976 1976 def current_route_path(request, **kw):
1977 1977 new_args = request.GET.mixed()
1978 1978 new_args.update(kw)
1979 1979 return request.current_route_path(_query=new_args)
1980 1980
1981 1981
1982 1982 def api_call_example(method, args):
1983 1983 """
1984 1984 Generates an API call example via CURL
1985 1985 """
1986 1986 args_json = json.dumps(OrderedDict([
1987 1987 ('id', 1),
1988 1988 ('auth_token', 'SECRET'),
1989 1989 ('method', method),
1990 1990 ('args', args)
1991 1991 ]))
1992 1992 return literal(
1993 1993 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
1994 1994 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
1995 1995 "and needs to be of `api calls` role."
1996 1996 .format(
1997 1997 api_url=route_url('apiv2'),
1998 1998 token_url=route_url('my_account_auth_tokens'),
1999 1999 data=args_json))
2000 2000
2001 2001
2002 2002 def notification_description(notification, request):
2003 2003 """
2004 2004 Generate notification human readable description based on notification type
2005 2005 """
2006 2006 from rhodecode.model.notification import NotificationModel
2007 2007 return NotificationModel().make_description(
2008 2008 notification, translate=request.translate)
2009 2009
2010 2010
2011 2011 def go_import_header(request, db_repo=None):
2012 2012 """
2013 2013 Creates a header for go-import functionality in Go Lang
2014 2014 """
2015 2015
2016 2016 if not db_repo:
2017 2017 return
2018 2018 if 'go-get' not in request.GET:
2019 2019 return
2020 2020
2021 2021 clone_url = db_repo.clone_url()
2022 2022 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2023 2023 # we have a repo and go-get flag,
2024 2024 return literal('<meta name="go-import" content="{} {} {}">'.format(
2025 2025 prefix, db_repo.repo_type, clone_url))
2026 2026
2027 2027
2028 2028 def reviewer_as_json(*args, **kwargs):
2029 2029 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2030 2030 return _reviewer_as_json(*args, **kwargs)
2031 2031
2032 2032
2033 2033 def get_repo_view_type(request):
2034 2034 route_name = request.matched_route.name
2035 2035 route_to_view_type = {
2036 2036 'repo_changelog': 'changelog',
2037 2037 'repo_files': 'files',
2038 2038 'repo_summary': 'summary',
2039 2039 'repo_commit': 'commit'
2040 }
2040 2041
2041 }
2042 2042 return route_to_view_type.get(route_name)
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,698 +1,733 b''
1 1 // navigation.less
2 2 // For use in RhodeCode applications;
3 3 // see style guide documentation for guidelines.
4 4
5 5 // HEADER NAVIGATION
6 6
7 7 .horizontal-list {
8 8 float: right;
9 9 display: block;
10 10 margin: 0;
11 11 padding: 0;
12 12 -webkit-padding-start: 0;
13 13 text-align: left;
14 14 font-size: @navigation-fontsize;
15 15 color: @grey6;
16 16 z-index:10;
17 17
18 18 li {
19 19 line-height: 1em;
20 20 list-style-type: none;
21 21
22 22 a {
23 23 padding: 0 .5em;
24 24
25 25 &.menu_link_notifications {
26 26 .pill(7px,@rcblue);
27 27 display: inline;
28 28 margin: 0 7px 0 .7em;
29 29 font-size: @basefontsize;
30 30 color: white;
31 31
32 32 &.empty {
33 33 background-color: @grey4;
34 34 }
35 35
36 36 &:hover {
37 37 background-color: @rcdarkblue;
38 38 }
39 39 }
40 40 }
41 41 .pill_container {
42 42 margin: 1.25em 0px 0px 0px;
43 43 float: right;
44 44 }
45 45
46 46 &#quick_login_li {
47 47 &:hover {
48 48 color: @grey5;
49 49 }
50 50
51 51 a.menu_link_notifications {
52 52 color: white;
53 53 }
54 54
55 55 .user {
56 56 padding-bottom: 10px;
57 57 }
58 58
59 59 &.open {
60 60 .user {
61 61 border-bottom: 5px solid @rcblue;
62 62 }
63 63 }
64 64 }
65 65
66 66 &:before { content: none; }
67 67
68 68 &:last-child {
69 69 .menulabel {
70 70 padding-right: 0;
71 71 border-right: none;
72 72
73 73 .show_more {
74 74 padding-right: 0;
75 75 }
76 76 }
77 77
78 78 &> a {
79 79 border-bottom: none;
80 80 }
81 81 }
82 82
83 83 &.active {
84 84 border-bottom: 5px solid @rcblue;
85 85 }
86 86
87 87 &.open {
88 88
89 89 a {
90 90 color: white;
91 91 }
92 92 }
93 93
94 94 &:focus {
95 95 outline: none;
96 96 }
97 97
98 98 ul li {
99 99 display: block;
100 100
101 101 &:last-child> a {
102 102 border-bottom: none;
103 103 }
104 104
105 105 ul li:last-child a {
106 106 /* we don't expect more then 3 levels of submenu and the third
107 107 level can have different html structure */
108 108 border-bottom: none;
109 109 }
110 110 }
111 111 }
112 112
113 113 > li {
114 114 float: left;
115 115 display: block;
116 116 padding: 0;
117 117
118 118 > a,
119 119 &.has_select2 a {
120 120 display: block;
121 121 padding: 10px 0 2px;
122 122 }
123 123
124 124 .menulabel {
125 125 padding: 0 .5em;
126 126 line-height: 1em;
127 127 // for this specifically we do not use a variable
128 128 border-right: 1px solid @grey4;
129 129 }
130 130
131 131 .pr_notifications {
132 132 padding-left: .5em;
133 133 }
134 134
135 135 .pr_notifications + .menulabel {
136 136 display:inline;
137 137 padding-left: 0;
138 138 }
139 139
140 140 &:hover,
141 141 &.open,
142 142 &.active {
143 143 a {
144 144 color: @grey1;
145 145 }
146 146 }
147 147 }
148 148
149 149 pre {
150 150 margin: 0;
151 151 padding: 0;
152 152 }
153 153
154 154 .select2-container,
155 155 .menulink.childs {
156 156 position: relative;
157 157 }
158 158
159 159 #quick_login {
160 160
161 161 li a {
162 162 padding: .5em 0;
163 163 border-bottom: none;
164 164 color: @grey2;
165 165
166 166 &:hover { color: @grey1; }
167 167 }
168 168 }
169 169
170 170 #quick_login_link {
171 171 display: inline-block;
172 172
173 173 .gravatar {
174 174 border: 1px solid @grey5;
175 175 }
176 176
177 177 .gravatar-login {
178 178 height: 20px;
179 179 width: 20px;
180 180 margin: -8px 0;
181 181 padding: 0;
182 182 }
183 183
184 184 &:hover .user {
185 185 color: @grey6;
186 186 }
187 187 }
188 188 }
189 189 .header .horizontal-list {
190 190
191 191 li {
192 192
193 193 &#quick_login_li {
194 194 padding-left: .5em;
195 195
196 196 &:hover #quick_login_link {
197 197 color: inherit;
198 198 }
199 199
200 200 .menu_link_user {
201 201 padding: 0 2px;
202 202 }
203 203 }
204 204 list-style-type: none;
205 205 }
206 206
207 207 > li {
208 208
209 209 a {
210 210 padding: 18px 0 12px 0;
211 211 color: @nav-grey;
212 212
213 213 &.menu_link_notifications {
214 214 padding: 1px 8px;
215 215 }
216 216 }
217 217
218 218 &:hover,
219 219 &.open,
220 220 &.active {
221 221 .pill_container a {
222 222 // don't select text for the pill container, it has it' own
223 223 // hover behaviour
224 224 color: @nav-grey;
225 225 }
226 226 }
227 227
228 228 &:hover,
229 229 &.open,
230 230 &.active {
231 231 a {
232 232 color: @grey6;
233 233 }
234 234 }
235 235
236 236 .select2-dropdown-open a {
237 237 color: @grey6;
238 238 }
239 239
240 240 .repo-switcher {
241 241 padding-left: 0;
242 242
243 243 .menulabel {
244 244 padding-left: 0;
245 245 }
246 246 }
247 247 }
248 248
249 249 li ul li {
250 250 background-color:@grey2;
251 251
252 252 a {
253 253 padding: .5em 0;
254 254 border-bottom: @border-thickness solid @border-default-color;
255 255 color: @grey6;
256 256 }
257 257
258 258 &:last-child a, &.last a{
259 259 border-bottom: none;
260 260 }
261 261
262 262 &:hover {
263 263 background-color: @grey3;
264 264 }
265 265 }
266 266
267 267 .submenu {
268 268 margin-top: 5px;
269 269 }
270 270 }
271 271
272 272 // SUBMENUS
273 273 .navigation .submenu {
274 274 display: none;
275 275 }
276 276
277 277 .navigation li.open {
278 278 .submenu {
279 279 display: block;
280 280 }
281 281 }
282 282
283 283 .navigation li:last-child .submenu {
284 284 right: -20px;
285 285 left: auto;
286 286 }
287 287
288 288 .submenu {
289 289 position: absolute;
290 290 top: 100%;
291 291 left: 0;
292 292 min-width: 150px;
293 293 margin: 6px 0 0;
294 294 padding: 0;
295 295 text-align: left;
296 296 font-family: @text-light;
297 297 border-radius: @border-radius;
298 298 z-index: 20;
299 299
300 300 li {
301 301 display: block;
302 302 margin: 0;
303 303 padding: 0 .5em;
304 304 line-height: 1em;
305 305 color: @grey3;
306 306 background-color: @grey6;
307 307 list-style-type: none;
308 308
309 309 a {
310 310 display: block;
311 311 width: 100%;
312 312 padding: .5em 0;
313 313 border-right: none;
314 314 border-bottom: @border-thickness solid white;
315 315 color: @grey3;
316 316 }
317 317
318 318 ul {
319 319 display: none;
320 320 position: absolute;
321 321 top: 0;
322 322 right: 100%;
323 323 padding: 0;
324 324 z-index: 30;
325 325 }
326 326 &:hover {
327 327 background-color: @grey5;
328 328 -webkit-transition: background .3s;
329 329 -moz-transition: background .3s;
330 330 -o-transition: background .3s;
331 331 transition: background .3s;
332 332
333 333 ul {
334 334 display: block;
335 335 }
336 336 }
337 337 }
338 338
339 339 }
340 340
341 341
342 342
343 343
344 344 // repo dropdown
345 345 .quick_repo_menu {
346 346 width: 15px;
347 347 text-align: center;
348 348 position: relative;
349 349 cursor: pointer;
350 350
351 351 div {
352 352 overflow: visible !important;
353 353 }
354 354
355 355 &.sorting {
356 356 cursor: auto;
357 357 }
358 358
359 359 &:hover {
360 360 .menu_items_container {
361 361 position: absolute;
362 362 display: block;
363 363 }
364 364 .menu_items {
365 365 display: block;
366 366 }
367 367 }
368 368
369 369 i {
370 370 margin: 0;
371 371 color: @grey4;
372 372 }
373 373
374 374 .menu_items_container {
375 375 position: absolute;
376 376 top: 0;
377 377 left: 100%;
378 378 margin: 0;
379 379 padding: 0;
380 380 list-style: none;
381 381 background-color: @grey6;
382 382 z-index: 999;
383 383 text-align: left;
384 384
385 385 a {
386 386 color: @grey2;
387 387 }
388 388
389 389 ul.menu_items {
390 390 margin: 0;
391 391 padding: 0;
392 392 }
393 393
394 394 li {
395 395 margin: 0;
396 396 padding: 0;
397 397 line-height: 1em;
398 398 list-style-type: none;
399 399
400 400 a {
401 401 display: block;
402 402 height: 16px;
403 403 padding: 8px; //must add up to td height (28px)
404 404 width: 120px; // set width
405 405
406 406 &:hover {
407 407 background-color: @grey5;
408 408 -webkit-transition: background .3s;
409 409 -moz-transition: background .3s;
410 410 -o-transition: background .3s;
411 411 transition: background .3s;
412 412 }
413 413 }
414 414 }
415 415 }
416 416 }
417 417
418 418 // Header Repository Switcher
419 419 // Select2 Dropdown
420 420 #select2-drop.select2-drop.repo-switcher-dropdown {
421 421 width: auto !important;
422 422 margin-top: 5px;
423 423 padding: 1em 0;
424 424 text-align: left;
425 425 .border-radius-bottom(@border-radius);
426 426 border-color: transparent;
427 427 color: @grey6;
428 428 background-color: @grey2;
429 429
430 430 input {
431 431 min-width: 90%;
432 432 }
433 433
434 434 ul.select2-result-sub {
435 435
436 436 li {
437 437 line-height: 1em;
438 438
439 439 &:hover,
440 440 &.select2-highlighted {
441 441 background-color: @grey3;
442 442 }
443 443 }
444 444
445 445 &:before { content: none; }
446 446 }
447 447
448 448 ul.select2-results {
449 449 min-width: 200px;
450 450 margin: 0;
451 451 padding: 0;
452 452 list-style-type: none;
453 453 overflow-x: visible;
454 454 overflow-y: scroll;
455 455
456 456 li {
457 457 padding: 0 8px;
458 458 line-height: 1em;
459 459 color: @grey6;
460 460
461 461 &>.select2-result-label {
462 462 padding: 8px 0;
463 463 border-bottom: @border-thickness solid @grey3;
464 464 white-space: nowrap;
465 465 color: @grey5;
466 466 cursor: pointer;
467 467 }
468 468
469 469 &.select2-result-with-children {
470 470 margin: 0;
471 471 padding: 0;
472 472 }
473 473
474 474 &.select2-result-unselectable > .select2-result-label {
475 475 margin: 0 8px;
476 476 }
477 477
478 478 }
479 479 }
480 480
481 481 ul.select2-result-sub {
482 482 margin: 0;
483 483 padding: 0;
484 484
485 485 li {
486 486 display: block;
487 487 margin: 0;
488 488 border-right: none;
489 489 line-height: 1em;
490 490 font-family: @text-light;
491 491 color: @grey2;
492 492 list-style-type: none;
493 493
494 494 &:hover {
495 495 background-color: @grey3;
496 496 }
497 497 }
498 498 }
499 499 }
500 500
501 501
502 502 #context-bar {
503 503 display: block;
504 504 margin: 0 auto;
505 505 padding: 0 @header-padding;
506 506 background-color: @grey6;
507 507 border-bottom: @border-thickness solid @grey5;
508 508
509 509 .clear {
510 510 clear: both;
511 511 }
512 512 }
513 513
514 514 ul#context-pages {
515 515 li {
516 516 line-height: 1em;
517 517 list-style-type: none;
518 518
519 519 a {
520 520 color: @grey3;
521 521 }
522 522
523 523 &.active {
524 524 // special case, non-variable color
525 525 border-bottom: 4px solid @nav-grey;
526 526
527 527 a {
528 528 color: @grey1;
529 529 }
530 530 }
531 531 }
532 532 }
533 533
534 534 // PAGINATION
535 535
536 536 .pagination {
537 537 border: @border-thickness solid @rcblue;
538 538 color: @rcblue;
539 539
540 540 .current {
541 541 color: @grey4;
542 542 }
543 543 }
544 544
545 545 .dataTables_processing {
546 546 text-align: center;
547 547 font-size: 1.1em;
548 548 position: relative;
549 549 top: 95px;
550 550 }
551 551
552 552 .dataTables_paginate, .pagination-wh {
553 553 text-align: left;
554 554 display: inline-block;
555 555 border-left: 1px solid @rcblue;
556 556 float: none;
557 557 overflow: hidden;
558 558
559 559 .paginate_button, .pager_curpage,
560 560 .pager_link, .pg-previous, .pg-next, .pager_dotdot {
561 561 display: inline-block;
562 562 padding: @menupadding/4 @menupadding;
563 563 border: 1px solid @rcblue;
564 564 border-left: 0;
565 565 color: @rcblue;
566 566 cursor: pointer;
567 567 float: left;
568 568 }
569 569
570 570 .pager_curpage, .pager_dotdot,
571 571 .paginate_button.current, .paginate_button.disabled,
572 572 .disabled {
573 573 color: @grey3;
574 574 cursor: default;
575 575 }
576 576
577 577 .ellipsis {
578 578 display: inline-block;
579 579 text-align: left;
580 580 padding: @menupadding/4 @menupadding;
581 581 border: 1px solid @rcblue;
582 582 border-left: 0;
583 583 float: left;
584 584 }
585 585 }
586 586
587 587 // SIDEBAR
588 588
589 589 .sidebar {
590 590 .block-left;
591 591 clear: left;
592 592 max-width: @sidebar-width;
593 593 margin-right: @sidebarpadding;
594 594 padding-right: @sidebarpadding;
595 595 font-family: @text-regular;
596 596 color: @grey1;
597 597
598 598 &#graph_nodes {
599 599 clear:both;
600 600 width: auto;
601 601 margin-left: -100px;
602 602 padding: 0;
603 603 border: none;
604 604 }
605 605
606 606 .nav-pills {
607 607 margin: 0;
608 608 }
609 609
610 610 .nav {
611 611 list-style: none;
612 612 padding: 0;
613 613
614 614 li {
615 615 padding-bottom: @menupadding;
616 616 line-height: 1em;
617 617 color: @grey4;
618 618 list-style-type: none;
619 619
620 620 &.active a {
621 621 color: @grey2;
622 622 }
623 623
624 624 a {
625 625 color: @grey4;
626 626 }
627 627 }
628 628
629 629 }
630 630 }
631 631
632 632 .main_filter_help_box {
633 633 padding: 7px 7px;
634 634 border-top: 1px solid @grey4;
635 635 border-right: 1px solid @grey4;
636 636 border-bottom: 1px solid @grey4;
637 637 display: inline-block;
638 638 vertical-align: top;
639 639 background: inherit;
640 640 position: absolute;
641 641 right: 8px;
642 642 top: 9px;
643 643 }
644 644
645 645 .main_filter_input_box {
646 646 display: inline-block;
647
648 .searchItems {
649 display:flex;
650 background: #666666;
651 padding: 0px;
652
653 a {
654 border: none !important;
655 }
656 }
657
658 .searchTag {
659 line-height: 28px;
660 padding: 0px 4px;
661
662 .tag {
663 color: @nav-grey;
664 border-color: @nav-grey;
665 }
666 }
667
668 .searchTagFilter {
669 background-color: @grey3 !important;
670 }
671
672 .searchTagHelp {
673 background-color: @grey2 !important;
674 }
675 .searchTagHelp:hover {
676 background-color: @grey2 !important;
677 }
678 .searchTagInput {
679 background-color: @grey3 !important;
680 }
647 681 }
648 682
649 683 .main_filter_box {
650 684 margin: 9px 0 0 0;
651 685 }
652 686
653 687 #main_filter_help {
654 688 background: @grey3;
655 689 border: 1px solid black;
656 690 position: absolute;
657 691 white-space: pre;
658 692 z-index: 9999;
659 693 color: @nav-grey;
660 694 margin: 1px 7px;
661 695 padding: 0 10px;
662 696 }
663 697
664 698 .main_filter_input {
665 699 padding: 5px;
666 700 min-width: 260px;
667 701 color: @nav-grey;
668 702 background: @grey3;
669 703 min-height: 18px;
670
704 border:none;
705 border-radius: 0;
671 706
672 707 &:active {
673 708 color: @grey2 !important;
674 709 background: white !important;
675 710 }
676 711 &:focus {
677 712 color: @grey2 !important;
678 713 background: white !important;
679 714 }
680 715 }
681 716
682 717
683 718
684 719 .main_filter_input::placeholder {
685 720 color: @nav-grey;
686 721 opacity: 1;
687 722 }
688 723
689 724 .notice-box {
690 725 display:block !important;
691 726 padding: 9px 0 !important;
692 727 }
693 728
694 729 .menulabel-notice {
695 730 border: 1px solid @color5;
696 731 padding:7px 10px;
697 732 color: @color5;
698 733 }
@@ -1,936 +1,948 b''
1 1 /**
2 2 * Ajax Autocomplete for jQuery, version dev
3 3 * RhodeCode additions
4 4 * (c) 2014 Tomas Kirda
5 5 * (c) 2014 Marcin Kuzminski
6 6 *
7 7 * Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license.
8 8 * For details, see the web site: https://github.com/devbridge/jQuery-Autocomplete
9 9 */
10 10 // Expose plugin as an AMD module if AMD loader is present:
11 11 (function (factory) {
12 12 'use strict';
13 13 if (typeof define === 'function' && define.amd) {
14 14 // AMD. Register as an anonymous module.
15 15 define(['jquery'], factory);
16 16 } else if (typeof exports === 'object' && typeof require === 'function') {
17 17 // Browserify
18 18 factory(require('jquery'));
19 19 } else {
20 20 // Browser globals
21 21 factory(jQuery);
22 22 }
23 23 }(function ($) {
24 24 'use strict';
25 25
26 26 var
27 27 utils = (function () {
28 28 return {
29 29 escapeRegExChars: function (value) {
30 30 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
31 31 },
32 32 createNode: function (containerClass) {
33 33 var div = document.createElement('div');
34 34 div.className = containerClass;
35 35 div.style.position = 'absolute';
36 36 div.style.display = 'none';
37 37 return div;
38 38 }
39 39 };
40 40 }()),
41 41
42 42 keys = {
43 43 ESC: 27,
44 44 TAB: 9,
45 45 RETURN: 13,
46 46 LEFT: 37,
47 47 UP: 38,
48 48 RIGHT: 39,
49 49 DOWN: 40
50 50 };
51 51
52 52 function Autocomplete(el, options) {
53 53 var noop = function () { },
54 54 that = this,
55 55 defaults = {
56 56 ajaxSettings: {},
57 57 autoSelectFirst: false,
58 58 appendTo: document.body,
59 59 serviceUrl: null,
60 60 lookup: null,
61 61 width: 'auto',
62 62 minChars: 1,
63 63 maxHeight: 300,
64 64 deferRequestBy: 0,
65 65 params: {},
66 66 formatResult: Autocomplete.formatResult,
67 67 lookupFilter: Autocomplete.lookupFilter,
68 68 delimiter: null,
69 69 zIndex: 9999,
70 70 type: 'GET',
71 71 noCache: false,
72 72 onSelect: noop,
73 73 onSearchStart: noop,
74 74 onSearchComplete: noop,
75 75 onSearchError: noop,
76 76 containerClass: 'autocomplete-suggestions',
77 77 tabDisabled: false,
78 78 dataType: 'text',
79 79 currentRequest: null,
80 80 triggerSelectOnValidInput: false,
81 81 preventBadQueries: true,
82 82 paramName: 'query',
83 83 transformResult: function (response) {
84 84 return typeof response === 'string' ? $.parseJSON(response) : response;
85 85 },
86 86 showNoSuggestionNotice: false,
87 87 noSuggestionNotice: _gettext('No results'),
88 88 orientation: 'bottom',
89 89 forceFixPosition: false,
90 90 replaceOnArrowKey: true
91 91 };
92 92
93 93 // Shared variables:
94 94 that.element = el;
95 95 that.el = $(el);
96 96 that.suggestions = [];
97 97 that.badQueries = [];
98 98 that.selectedIndex = -1;
99 99 that.currentValue = that.element.value;
100 100 that.intervalId = 0;
101 101 that.cachedResponse = {};
102 102 that.onChangeInterval = null;
103 103 that.onChange = null;
104 104 that.isLocal = false;
105 105 that.suggestionsContainer = null;
106 106 that.noSuggestionsContainer = null;
107 107 that.options = $.extend({}, defaults, options);
108 108 that.classes = {
109 109 selected: 'autocomplete-selected',
110 110 suggestion: 'autocomplete-suggestion'
111 111 };
112 112 that.hint = null;
113 113 that.hintValue = '';
114 114 that.selection = null;
115 115
116 116 // Initialize and set options:
117 117 that.initialize();
118 118 that.setOptions(options);
119 119 }
120 120
121 121 Autocomplete.utils = utils;
122 122
123 123 $.Autocomplete = Autocomplete;
124 124
125 125 Autocomplete.formatResult = function (suggestion, currentValue) {
126 126 var pattern = '(' + utils.escapeRegExChars(currentValue) + ')';
127 127 return suggestion.value.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
128 128 };
129 129 Autocomplete.lookupFilter = function (suggestion, originalQuery, queryLowerCase) {
130 130 return suggestion.value.toLowerCase().indexOf(queryLowerCase) !== -1;
131 131 };
132 132
133 133 Autocomplete.prototype = {
134 134
135 135 killerFn: null,
136 136
137 137 initialize: function () {
138 138 var that = this,
139 139 suggestionSelector = '.' + that.classes.suggestion,
140 140 selected = that.classes.selected,
141 141 options = that.options,
142 142 container;
143 143
144 144 // Remove autocomplete attribute to prevent native suggestions:
145 145 that.element.setAttribute('autocomplete', 'off');
146 146
147 147 that.killerFn = function (e) {
148 148 if ($(e.target).closest('.' + that.options.containerClass).length === 0) {
149 149 that.killSuggestions();
150 150 that.disableKillerFn();
151 151 }
152 152 };
153 153
154 154 // html() deals with many types: htmlString or Element or Array or jQuery
155 155 that.noSuggestionsContainer = $('<div class="autocomplete-no-suggestion"></div>')
156 156 .html(this.options.noSuggestionNotice).get(0);
157 157
158 158 that.suggestionsContainer = Autocomplete.utils.createNode(options.containerClass);
159 159
160 160 container = $(that.suggestionsContainer);
161 161
162 162 container.appendTo(options.appendTo);
163 163
164 164 // Only set width if it was provided:
165 165 if (options.width !== 'auto') {
166 166 container.width(options.width);
167 167 }
168 168
169 169 // Listen for mouse over event on suggestions list:
170 170 container.on('mouseover.autocomplete', suggestionSelector, function () {
171 171 that.activate($(this).data('index'));
172 172 });
173 173
174 174 // Deselect active element when mouse leaves suggestions container:
175 175 container.on('mouseout.autocomplete', function () {
176 176 that.selectedIndex = -1;
177 177 container.children('.' + selected).removeClass(selected);
178 178 });
179 179
180 180 // Listen for click event on suggestions list:
181 181 container.on('click.autocomplete', suggestionSelector, function () {
182 182 that.select($(this).data('index'));
183 183 });
184 184
185 185 that.fixPositionCapture = function () {
186 186 if (that.visible) {
187 187 that.fixPosition();
188 188 }
189 189 };
190 190
191 191 $(window).on('resize.autocomplete', that.fixPositionCapture);
192 192
193 193 that.el.on('keydown.autocomplete', function (e) { that.onKeyPress(e); });
194 194 that.el.on('keyup.autocomplete', function (e) { that.onKeyUp(e); });
195 195 that.el.on('blur.autocomplete', function () { that.onBlur(); });
196 196 that.el.on('focus.autocomplete', function () { that.onFocus(); });
197 197 that.el.on('change.autocomplete', function (e) { that.onKeyUp(e); });
198 198 },
199 199
200 200 onFocus: function () {
201 201 var that = this;
202 202 that.fixPosition();
203 203 if (that.options.minChars <= that.el.val().length) {
204 204 that.onValueChange();
205 205 }
206 206 },
207 207
208 208 onBlur: function () {
209 209 this.enableKillerFn();
210 210 },
211 211
212 212 setOptions: function (suppliedOptions) {
213 213 var that = this,
214 214 options = that.options;
215 215
216 216 $.extend(options, suppliedOptions);
217 217
218 218 that.isLocal = $.isArray(options.lookup);
219 219
220 220 if (that.isLocal) {
221 221 options.lookup = that.verifySuggestionsFormat(options.lookup);
222 222 }
223 223
224 224 options.orientation = that.validateOrientation(options.orientation, 'bottom');
225 225
226 226 // Adjust height, width and z-index:
227 227 $(that.suggestionsContainer).css({
228 228 'max-height': options.maxHeight + 'px',
229 229 'width': options.width + 'px',
230 230 'z-index': options.zIndex
231 231 });
232 232 },
233 233
234 234 clearCache: function () {
235 235 this.cachedResponse = {};
236 236 this.badQueries = [];
237 237 },
238 238
239 239 clear: function () {
240 240 this.clearCache();
241 241 this.currentValue = '';
242 242 this.suggestions = [];
243 243 },
244 244
245 245 disable: function () {
246 246 var that = this;
247 247 that.disabled = true;
248 248 if (that.currentRequest) {
249 249 that.currentRequest.abort();
250 250 }
251 251 },
252 252
253 253 enable: function () {
254 254 this.disabled = false;
255 255 },
256 256
257 257 fixPosition: function () {
258 258 // Use only when container has already its content
259 259
260 260 var that = this,
261 261 $container = $(that.suggestionsContainer),
262 262 containerParent = $container.parent().get(0);
263 263 // Fix position automatically when appended to body.
264 264 // In other cases force parameter must be given.
265 265 if (containerParent !== document.body && !that.options.forceFixPosition)
266 266 return;
267 267
268 268 // Choose orientation
269 269 var orientation = that.options.orientation,
270 270 containerHeight = $container.outerHeight(),
271 271 height = that.el.outerHeight(),
272 272 offset = that.el.offset(),
273 273 styles = { 'top': offset.top, 'left': offset.left };
274 274
275 275 if (orientation == 'auto') {
276 276 var viewPortHeight = $(window).height(),
277 277 scrollTop = $(window).scrollTop(),
278 278 topOverflow = -scrollTop + offset.top - containerHeight,
279 279 bottomOverflow = scrollTop + viewPortHeight - (offset.top + height + containerHeight);
280 280
281 281 orientation = (Math.max(topOverflow, bottomOverflow) === topOverflow)
282 282 ? 'top'
283 283 : 'bottom';
284 284 }
285 285
286 286 if (orientation === 'top') {
287 287 styles.top += -containerHeight;
288 288 } else {
289 289 styles.top += height;
290 290 }
291 291
292 292 // If container is not positioned to body,
293 293 // correct its position using offset parent offset
294 294 if(containerParent !== document.body) {
295 295 var opacity = $container.css('opacity'),
296 296 parentOffsetDiff;
297 297
298 298 if (!that.visible){
299 299 $container.css('opacity', 0).show();
300 300 }
301 301
302 302 parentOffsetDiff = $container.offsetParent().offset();
303 303 styles.top -= parentOffsetDiff.top;
304 304 styles.left -= parentOffsetDiff.left;
305 305
306 306 if (!that.visible){
307 307 $container.css('opacity', opacity).hide();
308 308 }
309 309 }
310 310
311 311 // -2px to account for suggestions border.
312 312 if (that.options.width === 'auto') {
313 313 styles.width = (that.el.outerWidth() - 2) + 'px';
314 314 }
315 315
316 316 $container.css(styles);
317 317 },
318 318
319 319 enableKillerFn: function () {
320 320 var that = this;
321 321 $(document).on('click.autocomplete', that.killerFn);
322 322 },
323 323
324 324 disableKillerFn: function () {
325 325 var that = this;
326 326 $(document).off('click.autocomplete', that.killerFn);
327 327 },
328 328
329 329 killSuggestions: function () {
330 330 var that = this;
331 331 that.stopKillSuggestions();
332 332 that.intervalId = window.setInterval(function () {
333 333 that.hide();
334 334 that.stopKillSuggestions();
335 335 }, 50);
336 336 },
337 337
338 338 stopKillSuggestions: function () {
339 339 window.clearInterval(this.intervalId);
340 340 },
341 341
342 342 isCursorAtEnd: function () {
343 343 var that = this,
344 344 valLength = that.el.val().length,
345 345 selectionStart = that.element.selectionStart,
346 346 range;
347 347
348 348 if (typeof selectionStart === 'number') {
349 349 return selectionStart === valLength;
350 350 }
351 351 if (document.selection) {
352 352 range = document.selection.createRange();
353 353 range.moveStart('character', -valLength);
354 354 return valLength === range.text.length;
355 355 }
356 356 return true;
357 357 },
358 358
359 359 onKeyPress: function (e) {
360 360 var that = this;
361 361
362 362 // If suggestions are hidden and user presses arrow down, display suggestions:
363 363 if (!that.disabled && !that.visible && e.which === keys.DOWN && that.currentValue) {
364 364 that.suggest();
365 365 return;
366 366 }
367 367
368 368 if (that.disabled || !that.visible) {
369 369 return;
370 370 }
371 371
372 372 switch (e.which) {
373 373 case keys.ESC:
374 374 that.el.val(that.currentValue);
375 375 that.hide();
376 376 break;
377 377 case keys.RIGHT:
378 378 if (that.hint && that.options.onHint && that.isCursorAtEnd()) {
379 379 that.selectHint();
380 380 break;
381 381 }
382 382 return;
383 383 case keys.TAB:
384 384 if (that.hint && that.options.onHint) {
385 385 that.selectHint();
386 386 return;
387 387 }
388 388 // Fall through to RETURN
389 389 case keys.RETURN:
390 390 if (that.selectedIndex === -1) {
391 391 that.hide();
392 392 return;
393 393 }
394 394 that.select(that.selectedIndex);
395 395 if (e.which === keys.TAB && that.options.tabDisabled === false) {
396 396 return;
397 397 }
398 398 break;
399 399 case keys.UP:
400 400 that.moveUp();
401 401 break;
402 402 case keys.DOWN:
403 403 that.moveDown();
404 404 break;
405 405 default:
406 406 return;
407 407 }
408 408
409 409 // Cancel event if function did not return:
410 410 e.stopImmediatePropagation();
411 411 e.preventDefault();
412 412 },
413 413
414 414 onKeyUp: function (e) {
415 415 var that = this;
416 416
417 417 if (that.disabled) {
418 418 return;
419 419 }
420 420
421 421 switch (e.which) {
422 422 case keys.UP:
423 423 case keys.DOWN:
424 424 return;
425 425 }
426 426
427 427 clearInterval(that.onChangeInterval);
428 428
429 429 if (that.currentValue !== that.el.val()) {
430 430 that.findBestHint();
431 431 if (that.options.deferRequestBy > 0) {
432 432 // Defer lookup in case when value changes very quickly:
433 433 that.onChangeInterval = setInterval(function () {
434 434 that.onValueChange();
435 435 }, that.options.deferRequestBy);
436 436 } else {
437 437 that.onValueChange();
438 438 }
439 439 }
440 440 },
441 441
442 442 onValueChange: function () {
443 443 var that = this,
444 444 options = that.options,
445 445 value = that.el.val(),
446 446 query = that.getQuery(value),
447 447 index;
448 448
449 449 if (that.selection && that.currentValue !== query) {
450 450 that.selection = null;
451 451 (options.onInvalidateSelection || $.noop).call(that.element);
452 452 }
453 453
454 454 clearInterval(that.onChangeInterval);
455 455 that.currentValue = value;
456 456 that.selectedIndex = -1;
457 457
458 458 // Check existing suggestion for the match before proceeding:
459 459 if (options.triggerSelectOnValidInput) {
460 460 index = that.findSuggestionIndex(query);
461 461 if (index !== -1) {
462 462 that.select(index);
463 463 return;
464 464 }
465 465 }
466 466
467 467 if (query.length < options.minChars) {
468 468 that.hide();
469 469 } else {
470 470 that.getSuggestions(query);
471 471 }
472 472 },
473 473
474 474 findSuggestionIndex: function (query) {
475 475 var that = this,
476 476 index = -1,
477 477 queryLowerCase = query.toLowerCase();
478 478
479 479 $.each(that.suggestions, function (i, suggestion) {
480 480 if (suggestion.value.toLowerCase() === queryLowerCase) {
481 481 index = i;
482 482 return false;
483 483 }
484 484 });
485 485
486 486 return index;
487 487 },
488 488
489 489 getQuery: function (value) {
490 490 var delimiter = this.options.delimiter,
491 491 parts;
492 492
493 493 if (!delimiter) {
494 494 return value;
495 495 }
496 496 parts = value.split(delimiter);
497 497 return $.trim(parts[parts.length - 1]);
498 498 },
499 499
500 500 getSuggestionsLocal: function (query) {
501 501 var that = this,
502 502 options = that.options,
503 503 queryLowerCase = query.toLowerCase(),
504 504 data;
505 505
506 506 // re-pack the data as it was comming from AJAX
507 507 data = {
508 508 suggestions: data
509 509 };
510 510 return data;
511 511 },
512 512
513 513 getSuggestions: function (query) {
514 514 var response,
515 515 that = this,
516 516 options = that.options,
517 517 serviceUrl = options.serviceUrl,
518 518 params,
519 519 cacheKey,
520 520 ajaxSettings;
521 521
522 522 options.params[options.paramName] = query;
523 523 params = options.ignoreParams ? null : options.params;
524 524
525 525 if (that.isLocal) {
526 526 response = that.getSuggestionsLocal(query);
527 527 } else {
528 528 if ($.isFunction(serviceUrl)) {
529 529 serviceUrl = serviceUrl.call(that.element, query);
530 530 }
531 cacheKey = serviceUrl + '?' + $.param(params || {});
531
532 var callParams = {};
533 //make an evaluated copy of params
534 $.each(params, function(index, value) {
535 if($.isFunction(value)){
536 callParams[index] = value();
537 }
538 else {
539 callParams[index] = value;
540 }
541 });
542
543 cacheKey = serviceUrl + '?' + $.param(callParams);
532 544 response = that.cachedResponse[cacheKey];
533 545 }
534 546
535 547 if (response && $.isArray(response.suggestions)) {
536 548 that.suggestions = response.suggestions;
537 549 that.suggest();
538 550 } else if (!that.isBadQuery(query)) {
539 if (options.onSearchStart.call(that.element, options.params) === false) {
551 if (options.onSearchStart.call(that.element, params) === false) {
540 552 return;
541 553 }
542 554 if (that.currentRequest) {
543 555 that.currentRequest.abort();
544 556 }
545 557
546 558 ajaxSettings = {
547 559 url: serviceUrl,
548 560 data: params,
549 561 type: options.type,
550 562 dataType: options.dataType
551 563 };
552 564
553 565 $.extend(ajaxSettings, options.ajaxSettings);
554 566
555 567 that.currentRequest = $.ajax(ajaxSettings).done(function (data) {
556 568 var result;
557 569 that.currentRequest = null;
558 570 result = options.transformResult(data);
559 571 that.processResponse(result, query, cacheKey);
560 572 options.onSearchComplete.call(that.element, query, result.suggestions);
561 573 }).fail(function (jqXHR, textStatus, errorThrown) {
562 574 options.onSearchError.call(that.element, query, jqXHR, textStatus, errorThrown);
563 575 });
564 576 }
565 577 },
566 578
567 579 isBadQuery: function (q) {
568 580 if (!this.options.preventBadQueries){
569 581 return false;
570 582 }
571 583
572 584 var badQueries = this.badQueries,
573 585 i = badQueries.length;
574 586
575 587 while (i--) {
576 588 if (q.indexOf(badQueries[i]) === 0) {
577 589 return true;
578 590 }
579 591 }
580 592
581 593 return false;
582 594 },
583 595
584 596 hide: function () {
585 597 var that = this;
586 598 that.visible = false;
587 599 that.selectedIndex = -1;
588 600 $(that.suggestionsContainer).hide();
589 601 that.signalHint(null);
590 602 },
591 603
592 604 suggest: function () {
593 605
594 606 var that = this,
595 607 options = that.options,
596 608 formatResult = options.formatResult,
597 609 filterResult = options.lookupFilter,
598 610 value = that.getQuery(that.currentValue),
599 611 className = that.classes.suggestion,
600 612 classSelected = that.classes.selected,
601 613 container = $(that.suggestionsContainer),
602 614 noSuggestionsContainer = $(that.noSuggestionsContainer),
603 615 beforeRender = options.beforeRender,
604 616 limit = parseInt(that.options.lookupLimit, 10),
605 617 html = '',
606 618 index;
607 619
608 620 // filter and limit given results
609 621 var filtered_suggestions = $.grep(that.suggestions, function (suggestion) {
610 622 return filterResult(suggestion, value, value.toLowerCase(), that.element);
611 623 });
612 624
613 625 if (limit && filtered_suggestions.length > limit) {
614 626 filtered_suggestions = filtered_suggestions.slice(0, limit);
615 627 }
616 628
617 629 if (filtered_suggestions.length === 0) {
618 630 this.options.showNoSuggestionNotice ? this.noSuggestions() : this.hide();
619 631 return;
620 632 }
621 633
622 634 if (options.triggerSelectOnValidInput) {
623 635 index = that.findSuggestionIndex(value);
624 636 if (index !== -1) {
625 637 that.select(index);
626 638 return;
627 639 }
628 640 }
629 641
630 642 // Build suggestions inner HTML:
631 643 $.each(filtered_suggestions, function (i, suggestion) {
632 644 html += '<div class="' + className + '" data-index="' + i + '">' + formatResult(suggestion, value, Autocomplete.formatResult, that.element) + '</div>';
633 645 });
634 646 // set internal suggestion for INDEX pick to work correctly
635 647 that.suggestions = filtered_suggestions;
636 648 this.adjustContainerWidth();
637 649
638 650 noSuggestionsContainer.detach();
639 651 container.html(html);
640 652
641 653 // Select first value by default:
642 654 if (options.autoSelectFirst) {
643 655 that.selectedIndex = 0;
644 656 container.children().first().addClass(classSelected);
645 657 }
646 658
647 659 if ($.isFunction(beforeRender)) {
648 660 beforeRender.call(that.element, container);
649 661 }
650 662
651 663 that.fixPosition();
652 664
653 665 container.show();
654 666 that.visible = true;
655 667
656 668 that.findBestHint();
657 669 },
658 670
659 671 noSuggestions: function() {
660 672 var that = this,
661 673 container = $(that.suggestionsContainer),
662 674 noSuggestionsContainer = $(that.noSuggestionsContainer);
663 675
664 676 this.adjustContainerWidth();
665 677
666 678 // Some explicit steps. Be careful here as it easy to get
667 679 // noSuggestionsContainer removed from DOM if not detached properly.
668 680 noSuggestionsContainer.detach();
669 681 container.empty(); // clean suggestions if any
670 682 container.append(noSuggestionsContainer);
671 683
672 684 that.fixPosition();
673 685
674 686 container.show();
675 687 that.visible = true;
676 688 },
677 689
678 690 adjustContainerWidth: function() {
679 691 var that = this,
680 692 options = that.options,
681 693 width,
682 694 container = $(that.suggestionsContainer);
683 695
684 696 // If width is auto, adjust width before displaying suggestions,
685 697 // because if instance was created before input had width, it will be zero.
686 698 // Also it adjusts if input width has changed.
687 699 // -2px to account for suggestions border.
688 700 if (options.width === 'auto') {
689 701 width = that.el.outerWidth() - 2;
690 702 container.width(width > 0 ? width : 300);
691 703 }
692 704 },
693 705
694 706 findBestHint: function () {
695 707 var that = this,
696 708 value = that.el.val().toLowerCase(),
697 709 bestMatch = null;
698 710
699 711 if (!value) {
700 712 return;
701 713 }
702 714
703 715 $.each(that.suggestions, function (i, suggestion) {
704 716 var foundMatch = suggestion.value.toLowerCase().indexOf(value) === 0;
705 717 if (foundMatch) {
706 718 bestMatch = suggestion;
707 719 }
708 720 return !foundMatch;
709 721 });
710 722 that.signalHint(bestMatch);
711 723 },
712 724
713 725 signalHint: function (suggestion) {
714 726 var hintValue = '',
715 727 that = this;
716 728 if (suggestion) {
717 729 hintValue = that.currentValue + suggestion.value.substr(that.currentValue.length);
718 730 }
719 731 if (that.hintValue !== hintValue) {
720 732 that.hintValue = hintValue;
721 733 that.hint = suggestion;
722 734 (this.options.onHint || $.noop)(hintValue);
723 735 }
724 736 },
725 737
726 738 verifySuggestionsFormat: function (suggestions) {
727 739 // If suggestions is string array, convert them to supported format:
728 740 if (suggestions.length && typeof suggestions[0] === 'string') {
729 741 return $.map(suggestions, function (value) {
730 742 return { value: value, data: null };
731 743 });
732 744 }
733 745
734 746 return suggestions;
735 747 },
736 748
737 749 validateOrientation: function(orientation, fallback) {
738 750 orientation = $.trim(orientation || '').toLowerCase();
739 751
740 752 if($.inArray(orientation, ['auto', 'bottom', 'top']) === -1){
741 753 orientation = fallback;
742 754 }
743 755
744 756 return orientation;
745 757 },
746 758
747 759 processResponse: function (result, originalQuery, cacheKey) {
748 760 var that = this,
749 761 options = that.options;
750 762
751 763 result.suggestions = that.verifySuggestionsFormat(result.suggestions);
752 764
753 765 // Cache results if cache is not disabled:
754 766 if (!options.noCache) {
755 767 that.cachedResponse[cacheKey] = result;
756 768 if (options.preventBadQueries && result.suggestions.length === 0) {
757 769 that.badQueries.push(originalQuery);
758 770 }
759 771 }
760 772
761 773 // Return if originalQuery is not matching current query:
762 774 if (originalQuery !== that.getQuery(that.currentValue)) {
763 775 return;
764 776 }
765 777
766 778 that.suggestions = result.suggestions;
767 779 that.suggest();
768 780 },
769 781
770 782 activate: function (index) {
771 783 var that = this,
772 784 activeItem,
773 785 selected = that.classes.selected,
774 786 container = $(that.suggestionsContainer),
775 787 children = container.find('.' + that.classes.suggestion);
776 788
777 789 container.find('.' + selected).removeClass(selected);
778 790
779 791 that.selectedIndex = index;
780 792
781 793 if (that.selectedIndex !== -1 && children.length > that.selectedIndex) {
782 794 activeItem = children.get(that.selectedIndex);
783 795 $(activeItem).addClass(selected);
784 796 return activeItem;
785 797 }
786 798
787 799 return null;
788 800 },
789 801
790 802 selectHint: function () {
791 803 var that = this,
792 804 i = $.inArray(that.hint, that.suggestions);
793 805 that.select(i);
794 806 },
795 807
796 808 select: function (index) {
797 809 var that = this;
798 810 that.hide();
799 811 that.onSelect(index);
800 812 },
801 813
802 814 moveUp: function () {
803 815 var that = this;
804 816
805 817 if (that.selectedIndex === -1) {
806 818 return;
807 819 }
808 820
809 821 if (that.selectedIndex === 0) {
810 822 $(that.suggestionsContainer).children().first().removeClass(that.classes.selected);
811 823 that.selectedIndex = -1;
812 824 that.el.val(that.currentValue);
813 825 that.findBestHint();
814 826 return;
815 827 }
816 828
817 829 that.adjustScroll(that.selectedIndex - 1);
818 830 },
819 831
820 832 moveDown: function () {
821 833 var that = this;
822 834
823 835 if (that.selectedIndex === (that.suggestions.length - 1)) {
824 836 return;
825 837 }
826 838
827 839 that.adjustScroll(that.selectedIndex + 1);
828 840 },
829 841
830 842 adjustScroll: function (index) {
831 843 var that = this,
832 844 activeItem = that.activate(index),
833 845 offsetTop,
834 846 upperBound,
835 847 lowerBound,
836 848 heightDelta = 25;
837 849
838 850 if (!activeItem) {
839 851 return;
840 852 }
841 853
842 854 offsetTop = activeItem.offsetTop;
843 855 upperBound = $(that.suggestionsContainer).scrollTop();
844 856 lowerBound = upperBound + that.options.maxHeight - heightDelta;
845 857
846 858 if (offsetTop < upperBound) {
847 859 $(that.suggestionsContainer).scrollTop(offsetTop);
848 860 } else if (offsetTop > lowerBound) {
849 861 $(that.suggestionsContainer).scrollTop(offsetTop - that.options.maxHeight + heightDelta);
850 862 }
851 863
852 864 if (that.options.replaceOnArrowKey) {
853 865 that.el.val(that.getValue(that.suggestions[index].value));
854 866 }
855 867 that.signalHint(null);
856 868 },
857 869
858 870 onSelect: function (index) {
859 871 var that = this,
860 872 onSelectCallback = that.options.onSelect,
861 873 suggestion = that.suggestions[index];
862 874
863 875 that.currentValue = that.getValue(suggestion.value);
864 876 var prevElem = {'value': that.el.val(),
865 877 'caret': that.element.selectionStart}
866 878
867 879 if (that.currentValue !== that.el.val()) {
868 880 that.el.val(that.currentValue);
869 881 }
870 882
871 883 that.signalHint(null);
872 884 that.suggestions = [];
873 885 that.selection = suggestion;
874 886
875 887 if ($.isFunction(onSelectCallback)) {
876 888 onSelectCallback.call(this, that.element, suggestion, prevElem);
877 889 }
878 890 },
879 891
880 892 getValue: function (value) {
881 893 var that = this,
882 894 delimiter = that.options.delimiter,
883 895 currentValue,
884 896 parts;
885 897
886 898 if (!delimiter) {
887 899 return value;
888 900 }
889 901
890 902 currentValue = that.currentValue;
891 903 parts = currentValue.split(delimiter);
892 904
893 905 if (parts.length === 1) {
894 906 return value;
895 907 }
896 908
897 909 return currentValue.substr(0, currentValue.length - parts[parts.length - 1].length) + value;
898 910 },
899 911
900 912 dispose: function () {
901 913 var that = this;
902 914 that.el.off('.autocomplete').removeData('autocomplete');
903 915 that.disableKillerFn();
904 916 $(window).off('resize.autocomplete', that.fixPositionCapture);
905 917 $(that.suggestionsContainer).remove();
906 918 }
907 919 };
908 920
909 921 // Create chainable jQuery plugin:
910 922 $.fn.autocomplete = $.fn.devbridgeAutocomplete = function (options, args) {
911 923 var dataKey = 'autocomplete';
912 924 // If function invoked without argument return
913 925 // instance of the first matched element:
914 926 if (arguments.length === 0) {
915 927 return this.first().data(dataKey);
916 928 }
917 929
918 930 return this.each(function () {
919 931 var inputElement = $(this),
920 932 instance = inputElement.data(dataKey);
921 933
922 934 if (typeof options === 'string') {
923 935 if (instance && typeof instance[options] === 'function') {
924 936 instance[options](args);
925 937 }
926 938 } else {
927 939 // If instance already exists, destroy it:
928 940 if (instance && instance.dispose) {
929 941 instance.dispose();
930 942 }
931 943 instance = new Autocomplete(this, options);
932 944 inputElement.data(dataKey, instance);
933 945 }
934 946 });
935 947 };
936 948 }));
@@ -1,832 +1,910 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.mako"/>
3 3
4 4 <%include file="/ejs_templates/templates.html"/>
5 5
6 6 <div class="outerwrapper">
7 7 <!-- HEADER -->
8 8 <div class="header">
9 9 <div id="header-inner" class="wrapper">
10 10 <div id="logo">
11 11 <div class="logo-wrapper">
12 12 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
13 13 </div>
14 14 % if c.rhodecode_name:
15 15 <div class="branding">
16 16 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
17 17 </div>
18 18 % endif
19 19 </div>
20 20 <!-- MENU BAR NAV -->
21 21 ${self.menu_bar_nav()}
22 22 <!-- END MENU BAR NAV -->
23 23 </div>
24 24 </div>
25 25 ${self.menu_bar_subnav()}
26 26 <!-- END HEADER -->
27 27
28 28 <!-- CONTENT -->
29 29 <div id="content" class="wrapper">
30 30
31 31 <rhodecode-toast id="notifications"></rhodecode-toast>
32 32
33 33 <div class="main">
34 34 ${next.main()}
35 35 </div>
36 36 </div>
37 37 <!-- END CONTENT -->
38 38
39 39 </div>
40 40 <!-- FOOTER -->
41 41 <div id="footer">
42 42 <div id="footer-inner" class="title wrapper">
43 43 <div>
44 44 <p class="footer-link-right">
45 45 % if c.visual.show_version:
46 46 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
47 47 % endif
48 48 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
49 49 % if c.visual.rhodecode_support_url:
50 50 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
51 51 % endif
52 52 </p>
53 53 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
54 54 <p class="server-instance" style="display:${sid}">
55 55 ## display hidden instance ID if specially defined
56 56 % if c.rhodecode_instanceid:
57 57 ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
58 58 % endif
59 59 </p>
60 60 </div>
61 61 </div>
62 62 </div>
63 63
64 64 <!-- END FOOTER -->
65 65
66 66 ### MAKO DEFS ###
67 67
68 68 <%def name="menu_bar_subnav()">
69 69 </%def>
70 70
71 71 <%def name="breadcrumbs(class_='breadcrumbs')">
72 72 <div class="${class_}">
73 73 ${self.breadcrumbs_links()}
74 74 </div>
75 75 </%def>
76 76
77 77 <%def name="admin_menu()">
78 78 <ul class="admin_menu submenu">
79 79 <li><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
80 80 <li><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
81 81 <li><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
82 82 <li><a href="${h.route_path('users')}">${_('Users')}</a></li>
83 83 <li><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
84 84 <li><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
85 85 <li><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
86 86 <li><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
87 87 <li><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
88 88 <li class="last"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
89 89 </ul>
90 90 </%def>
91 91
92 92
93 93 <%def name="dt_info_panel(elements)">
94 94 <dl class="dl-horizontal">
95 95 %for dt, dd, title, show_items in elements:
96 96 <dt>${dt}:</dt>
97 97 <dd title="${h.tooltip(title)}">
98 98 %if callable(dd):
99 99 ## allow lazy evaluation of elements
100 100 ${dd()}
101 101 %else:
102 102 ${dd}
103 103 %endif
104 104 %if show_items:
105 105 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
106 106 %endif
107 107 </dd>
108 108
109 109 %if show_items:
110 110 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
111 111 %for item in show_items:
112 112 <dt></dt>
113 113 <dd>${item}</dd>
114 114 %endfor
115 115 </div>
116 116 %endif
117 117
118 118 %endfor
119 119 </dl>
120 120 </%def>
121 121
122 122
123 123 <%def name="gravatar(email, size=16)">
124 124 <%
125 125 if (size > 16):
126 126 gravatar_class = 'gravatar gravatar-large'
127 127 else:
128 128 gravatar_class = 'gravatar'
129 129 %>
130 130 <%doc>
131 131 TODO: johbo: For now we serve double size images to make it smooth
132 132 for retina. This is how it worked until now. Should be replaced
133 133 with a better solution at some point.
134 134 </%doc>
135 135 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
136 136 </%def>
137 137
138 138
139 139 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
140 140 <% email = h.email_or_none(contact) %>
141 141 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
142 142 ${self.gravatar(email, size)}
143 143 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
144 144 </div>
145 145 </%def>
146 146
147 147
148 148 ## admin menu used for people that have some admin resources
149 149 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
150 150 <ul class="submenu">
151 151 %if repositories:
152 152 <li class="local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
153 153 %endif
154 154 %if repository_groups:
155 155 <li class="local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
156 156 %endif
157 157 %if user_groups:
158 158 <li class="local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
159 159 %endif
160 160 </ul>
161 161 </%def>
162 162
163 163 <%def name="repo_page_title(repo_instance)">
164 164 <div class="title-content">
165 165 <div class="title-main">
166 166 ## SVN/HG/GIT icons
167 167 %if h.is_hg(repo_instance):
168 168 <i class="icon-hg"></i>
169 169 %endif
170 170 %if h.is_git(repo_instance):
171 171 <i class="icon-git"></i>
172 172 %endif
173 173 %if h.is_svn(repo_instance):
174 174 <i class="icon-svn"></i>
175 175 %endif
176 176
177 177 ## public/private
178 178 %if repo_instance.private:
179 179 <i class="icon-repo-private"></i>
180 180 %else:
181 181 <i class="icon-repo-public"></i>
182 182 %endif
183 183
184 184 ## repo name with group name
185 185 ${h.breadcrumb_repo_link(repo_instance)}
186 186
187 187 </div>
188 188
189 189 ## FORKED
190 190 %if repo_instance.fork:
191 191 <p>
192 192 <i class="icon-code-fork"></i> ${_('Fork of')}
193 193 ${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))}
194 194 </p>
195 195 %endif
196 196
197 197 ## IMPORTED FROM REMOTE
198 198 %if repo_instance.clone_uri:
199 199 <p>
200 200 <i class="icon-code-fork"></i> ${_('Clone from')}
201 201 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
202 202 </p>
203 203 %endif
204 204
205 205 ## LOCKING STATUS
206 206 %if repo_instance.locked[0]:
207 207 <p class="locking_locked">
208 208 <i class="icon-repo-lock"></i>
209 209 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
210 210 </p>
211 211 %elif repo_instance.enable_locking:
212 212 <p class="locking_unlocked">
213 213 <i class="icon-repo-unlock"></i>
214 214 ${_('Repository not locked. Pull repository to lock it.')}
215 215 </p>
216 216 %endif
217 217
218 218 </div>
219 219 </%def>
220 220
221 221 <%def name="repo_menu(active=None)">
222 222 <%
223 223 def is_active(selected):
224 224 if selected == active:
225 225 return "active"
226 226 %>
227 227
228 228 <!--- CONTEXT BAR -->
229 229 <div id="context-bar">
230 230 <div class="wrapper">
231 231 <ul id="context-pages" class="navigation horizontal-list">
232 232 <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>
233 233 <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>
234 234 <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>
235 235 <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>
236 <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>
237 236
238 237 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
239 238 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
240 239 <li class="${is_active('showpullrequest')}">
241 240 <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)}">
242 241 %if c.repository_pull_requests:
243 242 <span class="pr_notifications">${c.repository_pull_requests}</span>
244 243 %endif
245 244 <div class="menulabel">${_('Pull Requests')}</div>
246 245 </a>
247 246 </li>
248 247 %endif
249 248
250 249 <li class="${is_active('options')}">
251 250 <a class="menulink dropdown">
252 251 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
253 252 </a>
254 253 <ul class="submenu">
255 254 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
256 255 <li><a href="${h.route_path('edit_repo',repo_name=c.repo_name)}">${_('Repository Settings')}</a></li>
257 256 %endif
258 257 %if c.rhodecode_db_repo.fork:
259 258 <li>
260 259 <a title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
261 260 href="${h.route_path('repo_compare',
262 261 repo_name=c.rhodecode_db_repo.fork.repo_name,
263 262 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
264 263 source_ref=c.rhodecode_db_repo.landing_rev[1],
265 264 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
266 265 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
267 266 _query=dict(merge=1))}"
268 267 >
269 268 ${_('Compare fork')}
270 269 </a>
271 270 </li>
272 271 %endif
273 272
274 273 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
275 274 %if c.rhodecode_db_repo.locked[0]:
276 275 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
277 276 %else:
278 277 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock')}</a></li>
279 278 %endif
280 279 %endif
281 280 %if c.rhodecode_user.username != h.DEFAULT_USER:
282 281 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
283 282 <li><a href="${h.route_path('repo_fork_new',repo_name=c.repo_name)}">${_('Fork')}</a></li>
284 283 <li><a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
285 284 %endif
286 285 %endif
287 286 </ul>
288 287 </li>
289 288 </ul>
290 289 </div>
291 290 <div class="clear"></div>
292 291 </div>
293 292 % if c.rhodecode_db_repo.archived:
294 293 <div class="alert alert-warning text-center">
295 294 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
296 295 </div>
297 296 % endif
298 297 <!--- END CONTEXT BAR -->
299 298
300 299 </%def>
301 300
302 301 <%def name="repo_group_page_title(repo_group_instance)">
303 302 <div class="title-content">
304 303 <div class="title-main">
305 304 ## Repository Group icon
306 305 <i class="icon-folder-close"></i>
307 306
308 307 ## repo name with group name
309 308 ${h.breadcrumb_repo_group_link(repo_group_instance)}
310 309 </div>
311 310
312 311 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
313 312 <div class="repo-group-desc">
314 313 ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)}
315 314 </div>
316 315
317 316 </div>
318 317 </%def>
319 318
320 319 <%def name="repo_group_menu(active=None)">
321 320 <%
322 321 def is_active(selected):
323 322 if selected == active:
324 323 return "active"
325 324
326 325 is_admin = h.HasPermissionAny('hg.admin')('can create repos index page')
327 326
328 327 gr_name = c.repo_group.group_name if c.repo_group else None
329 328 # create repositories with write permission on group is set to true
330 329 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
331 330 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
332 331 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
333 332
334 333 %>
335 334
336 335 <!--- CONTEXT BAR -->
337 336 <div id="context-bar">
338 337 <div class="wrapper">
339 338 <ul id="context-pages" class="navigation horizontal-list">
340 339 <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>
341 <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>
342 340
343 341 <li class="${is_active('options')}">
344 342 <a class="menulink dropdown">
345 343 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
346 344 </a>
347 345 <ul class="submenu">
348 346 %if is_admin or group_admin:
349 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>
350 348 %endif
351 349 %if is_admin or group_admin or (group_write and create_on_write):
352 350 <li><a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('Add Repository')}</a></li>
353 351 %endif
354 352 %if is_admin or group_admin:
355 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>
356 354 %endif
357 355 </ul>
358 356 </li>
359 357 </ul>
360 358 </div>
361 359 <div class="clear"></div>
362 360 </div>
363 361
364 362 <!--- END CONTEXT BAR -->
365 363
366 364 </%def>
367 365
368 366
369 367 <%def name="usermenu(active=False)">
370 368 ## USER MENU
371 369 <li id="quick_login_li" class="${'active' if active else ''}">
372 370 % if c.rhodecode_user.username == h.DEFAULT_USER:
373 371 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
374 372 ${gravatar(c.rhodecode_user.email, 20)}
375 373 <span class="user">
376 374 <span>${_('Sign in')}</span>
377 375 </span>
378 376 </a>
379 377 % else:
380 378 ## logged in user
381 379 <a id="quick_login_link" class="menulink childs">
382 380 ${gravatar(c.rhodecode_user.email, 20)}
383 381 <span class="user">
384 382 <span class="menu_link_user">${c.rhodecode_user.username}</span>
385 383 <div class="show_more"></div>
386 384 </span>
387 385 </a>
388 386 ## subnav with menu for logged in user
389 387 <div class="user-menu submenu">
390 388 <div id="quick_login">
391 389 %if c.rhodecode_user.username != h.DEFAULT_USER:
392 390 <div class="">
393 391 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
394 392 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
395 393 <div class="email">${c.rhodecode_user.email}</div>
396 394 </div>
397 395 <div class="">
398 396 <ol class="links">
399 397 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
400 398 % if c.rhodecode_user.personal_repo_group:
401 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>
402 400 % endif
403 401 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
404 402 ## bookmark-items
405 403 <li class="bookmark-items">
406 404 ${_('Bookmarks')}
407 405 <div class="pull-right">
408 406 <a href="${h.route_path('my_account_bookmarks')}">${_('Manage')}</a>
409 407 </div>
410 408 </li>
411 409 % if not c.bookmark_items:
412 410 <li>
413 411 <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
414 412 </li>
415 413 % endif
416 414 % for item in c.bookmark_items:
417 415 <li>
418 416 % if item.repository:
419 417 <div>
420 418 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
421 419 <code>${item.position}</code>
422 420 % if item.repository.repo_type == 'hg':
423 421 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
424 422 % elif item.repository.repo_type == 'git':
425 423 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
426 424 % elif item.repository.repo_type == 'svn':
427 425 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
428 426 % endif
429 427 ${(item.title or h.shorter(item.repository.repo_name, 30))}
430 428 </a>
431 429 </div>
432 430 % elif item.repository_group:
433 431 <div>
434 432 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
435 433 <code>${item.position}</code>
436 434 <i class="icon-folder-close" title="${_('Repository group')}" style="font-size: 16px"></i>
437 435 ${(item.title or h.shorter(item.repository_group.group_name, 30))}
438 436 </a>
439 437 </div>
440 438 % else:
441 439 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
442 440 <code>${item.position}</code>
443 441 ${item.title}
444 442 </a>
445 443 % endif
446 444 </li>
447 445 % endfor
448 446
449 447 <li class="logout">
450 448 ${h.secure_form(h.route_path('logout'), request=request)}
451 449 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
452 450 ${h.end_form()}
453 451 </li>
454 452 </ol>
455 453 </div>
456 454 %endif
457 455 </div>
458 456 </div>
459 457 ## unread counter
460 458 <div class="pill_container">
461 459 <a class="menu_link_notifications ${'empty' if c.unread_notifications == 0 else ''}" href="${h.route_path('notifications_show_all')}">${c.unread_notifications}</a>
462 460 </div>
463 461 % endif
464 462 </li>
465 463 </%def>
466 464
467 465 <%def name="menu_items(active=None)">
468 466 <%
469 467 def is_active(selected):
470 468 if selected == active:
471 469 return "active"
472 470 return ""
473 471 %>
474 472
475 473 <ul id="quick" class="main_nav navigation horizontal-list">
476 474 ## notice box for important system messages
477 475 <li style="display: none">
478 476 <a class="notice-box" href="#openNotice" onclick="showNoticeBox(); return false">
479 477 <div class="menulabel-notice" >
480 478 0
481 479 </div>
482 480 </a>
483 481 </li>
484 482
485 483 ## Main filter
486 484 <li>
487 485 <div class="menulabel main_filter_box">
488 486 <div class="main_filter_input_box">
487 <ul class="searchItems">
488
489 % if c.template_context['search_context']['repo_id']:
490 <li class="searchTag searchTagFilter searchTagHidable" >
491 ##<a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">
492 <span class="tag">
493 This repo
494 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-delete"></i></a>
495 </span>
496 ##</a>
497 </li>
498 % elif c.template_context['search_context']['repo_group_id']:
499 <li class="searchTag searchTagFilter searchTagHidable">
500 ##<a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">
501 <span class="tag">
502 This group
503 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-delete"></i></a>
504 </span>
505 ##</a>
506 </li>
507 % endif
508
509 <li class="searchTagInput">
489 510 <input class="main_filter_input" id="main_filter" size="15" type="text" name="main_filter" placeholder="${_('search / go to...')}" value=""/>
490 </div>
491 <div class="main_filter_help_box">
511 </li>
512 <li class="searchTag searchTagHelp">
492 513 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
514 </li>
515 </ul>
493 516 </div>
494 517 </div>
495 518
496 519 <div id="main_filter_help" style="display: none">
497 520 - Use '/' key to quickly access this field.
498 521
499 522 - Enter a name of repository, or repository group for quick search.
500 523
501 524 - Prefix query to allow special search:
502 525
503 user:admin, to search for usernames
526 user:admin, to search for usernames, always global
527
528 user_group:devops, to search for user groups, always global
504 529
505 user_group:devops, to search for user groups
530 commit:efced4, to search for commits, scoped to repositories or groups
531
532 file:models.py, to search for file paths, scoped to repositories or groups
506 533
507 commit:efced4, to search for commits
508
509 file:models.py, to search for file paths
534 % if c.template_context['search_context']['repo_id']:
535 For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a>
536 % elif c.template_context['search_context']['repo_group_id']:
537 For advanced full text search visit: <a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">repository group search</a>
538 % else:
539 For advanced full text search visit: <a href="${h.route_path('search')}">global search</a>
540 % endif
510 541 </div>
511 542 </li>
512 543
513 544 ## ROOT MENU
514 545 <li class="${is_active('home')}">
515 546 <a class="menulink" title="${_('Home')}" href="${h.route_path('home')}">
516 547 <div class="menulabel">${_('Home')}</div>
517 548 </a>
518 549 </li>
519 550
520 551 %if c.rhodecode_user.username != h.DEFAULT_USER:
521 552 <li class="${is_active('journal')}">
522 553 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
523 554 <div class="menulabel">${_('Journal')}</div>
524 555 </a>
525 556 </li>
526 557 %else:
527 558 <li class="${is_active('journal')}">
528 559 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
529 560 <div class="menulabel">${_('Public journal')}</div>
530 561 </a>
531 562 </li>
532 563 %endif
533 564
534 565 <li class="${is_active('gists')}">
535 566 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
536 567 <div class="menulabel">${_('Gists')}</div>
537 568 </a>
538 569 </li>
539 570
540 571 % if h.HasPermissionAll('hg.admin')('access admin main page'):
541 572 <li class="${is_active('admin')}">
542 573 <a class="menulink childs" title="${_('Admin settings')}" href="#" onclick="return false;">
543 574 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
544 575 </a>
545 576 ${admin_menu()}
546 577 </li>
547 578 % elif c.rhodecode_user.repositories_admin or c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
548 579 <li class="${is_active('admin')}">
549 580 <a class="menulink childs" title="${_('Delegated Admin settings')}">
550 581 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
551 582 </a>
552 583 ${admin_menu_simple(c.rhodecode_user.repositories_admin,
553 584 c.rhodecode_user.repository_groups_admin,
554 585 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
555 586 </li>
556 587 % endif
557 588 ## render extra user menu
558 589 ${usermenu(active=(active=='my_account'))}
559 590
560 591 % if c.debug_style:
561 592 <li>
562 593 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
563 594 <div class="menulabel">${_('[Style]')}</div>
564 595 </a>
565 596 </li>
566 597 % endif
567 598 </ul>
568 599
569 600 <script type="text/javascript">
570 601 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
571 602
572 603 var formatRepoResult = function(result, container, query, escapeMarkup) {
573 604 return function(data, escapeMarkup) {
574 605 if (!data.repo_id){
575 606 return data.text; // optgroup text Repositories
576 607 }
577 608
578 609 var tmpl = '';
579 610 var repoType = data['repo_type'];
580 611 var repoName = data['text'];
581 612
582 613 if(data && data.type == 'repo'){
583 614 if(repoType === 'hg'){
584 615 tmpl += '<i class="icon-hg"></i> ';
585 616 }
586 617 else if(repoType === 'git'){
587 618 tmpl += '<i class="icon-git"></i> ';
588 619 }
589 620 else if(repoType === 'svn'){
590 621 tmpl += '<i class="icon-svn"></i> ';
591 622 }
592 623 if(data['private']){
593 624 tmpl += '<i class="icon-lock" ></i> ';
594 625 }
595 626 else if(visualShowPublicIcon){
596 627 tmpl += '<i class="icon-unlock-alt"></i> ';
597 628 }
598 629 }
599 630 tmpl += escapeMarkup(repoName);
600 631 return tmpl;
601 632
602 633 }(result, escapeMarkup);
603 634 };
604 635
605 636 var formatRepoGroupResult = function(result, container, query, escapeMarkup) {
606 637 return function(data, escapeMarkup) {
607 638 if (!data.repo_group_id){
608 639 return data.text; // optgroup text Repositories
609 640 }
610 641
611 642 var tmpl = '';
612 643 var repoGroupName = data['text'];
613 644
614 645 if(data){
615 646
616 647 tmpl += '<i class="icon-folder-close"></i> ';
617 648
618 649 }
619 650 tmpl += escapeMarkup(repoGroupName);
620 651 return tmpl;
621 652
622 653 }(result, escapeMarkup);
623 654 };
624 655
625
626 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
627
628 if (value.split(':').length === 2) {
629 value = value.split(':')[1]
630 }
631
632 var searchType = data['type'];
633 var valueDisplay = data['value_display'];
634
635 656 var escapeRegExChars = function (value) {
636 657 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
637 658 };
638 var pattern = '(' + escapeRegExChars(value) + ')';
639 659
640 660 var getRepoIcon = function(repo_type) {
641 661 if (repo_type === 'hg') {
642 662 return '<i class="icon-hg"></i> ';
643 663 }
644 664 else if (repo_type === 'git') {
645 665 return '<i class="icon-git"></i> ';
646 666 }
647 667 else if (repo_type === 'svn') {
648 668 return '<i class="icon-svn"></i> ';
649 669 }
650 670 return ''
651 671 };
652 672
653 // highlight match
673 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
674
675 if (value.split(':').length === 2) {
676 value = value.split(':')[1]
677 }
678
679 var searchType = data['type'];
680 var valueDisplay = data['value_display'];
681
682 var pattern = '(' + escapeRegExChars(value) + ')';
683
654 684 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
685
686 // highlight match
687 if (searchType != 'text') {
655 688 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
689 }
656 690
657 691 var icon = '';
658 692
659 693 if (searchType === 'hint') {
660 694 icon += '<i class="icon-folder-close"></i> ';
661 695 }
662 696 // full text search
663 697 else if (searchType === 'search') {
664 698 icon += '<i class="icon-more"></i> ';
665 699 }
666 700 // repository
667 701 else if (searchType === 'repo') {
668 702
669 703 var repoIcon = getRepoIcon(data['repo_type']);
670 704 icon += repoIcon;
671 705
672 706 if (data['private']) {
673 707 icon += '<i class="icon-lock" ></i> ';
674 708 }
675 709 else if (visualShowPublicIcon) {
676 710 icon += '<i class="icon-unlock-alt"></i> ';
677 711 }
678 712 }
679 713 // repository groups
680 714 else if (searchType === 'repo_group') {
681 715 icon += '<i class="icon-folder-close"></i> ';
682 716 }
683 717 // user group
684 718 else if (searchType === 'user_group') {
685 719 icon += '<i class="icon-group"></i> ';
686 720 }
721 // user
687 722 else if (searchType === 'user') {
688 723 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
689 724 }
690 725 // commit
691 726 else if (searchType === 'commit') {
692 727 var repo_data = data['repo_data'];
693 728 var repoIcon = getRepoIcon(repo_data['repository_type']);
694 729 if (repoIcon) {
695 730 icon += repoIcon;
696 731 } else {
697 732 icon += '<i class="icon-tag"></i>';
698 733 }
699 734 }
700 735 // file
701 736 else if (searchType === 'file') {
702 737 var repo_data = data['repo_data'];
703 738 var repoIcon = getRepoIcon(repo_data['repository_type']);
704 739 if (repoIcon) {
705 740 icon += repoIcon;
706 741 } else {
707 742 icon += '<i class="icon-tag"></i>';
708 743 }
709 744 }
745 // generic text
746 else if (searchType === 'text') {
747 icon = '';
748 }
710 749
711 750 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
712 751 return tmpl.format(icon, valueDisplay);
713 752 };
714 753
715 754 var handleSelect = function(element, suggestion) {
716 755 if (suggestion.type === "hint") {
717 756 // we skip action
718 757 $('#main_filter').focus();
758 }
759 else if (suggestion.type === "text") {
760 // we skip action
761 $('#main_filter').focus();
762
719 763 } else {
720 764 window.location = suggestion['url'];
721 765 }
722 766 };
767
723 768 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
724 769 if (queryLowerCase.split(':').length === 2) {
725 770 queryLowerCase = queryLowerCase.split(':')[1]
726 771 }
772 if (suggestion.type === "text") {
773 // special case we don't want to "skip" display for
774 return true
775 }
727 776 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
728 777 };
729 778
779 var cleanContext = {
780 repo_view_type: null,
781
782 repo_id: null,
783 repo_name: "",
784
785 repo_group_id: null,
786 repo_group_name: null
787 };
788 var removeGoToFilter = function () {
789 $('.searchTagHidable').hide();
790 $('#main_filter').autocomplete(
791 'setOptions', {params:{search_context: cleanContext}});
792 };
793
730 794 $('#main_filter').autocomplete({
731 795 serviceUrl: pyroutes.url('goto_switcher_data'),
732 params: {"search_context": templateContext.search_context},
796 params: {
797 "search_context": templateContext.search_context
798 },
733 799 minChars:2,
734 800 maxHeight:400,
735 801 deferRequestBy: 300, //miliseconds
736 802 tabDisabled: true,
737 autoSelectFirst: true,
803 autoSelectFirst: false,
738 804 formatResult: autocompleteMainFilterFormatResult,
739 805 lookupFilter: autocompleteMainFilterResult,
740 806 onSelect: function (element, suggestion) {
741 807 handleSelect(element, suggestion);
742 808 return false;
743 809 },
744 810 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
745 811 if (jqXHR !== 'abort') {
746 812 alert("Error during search.\nError code: {0}".format(textStatus));
747 813 window.location = '';
748 814 }
749 815 }
750 816 });
751 817
752 818 showMainFilterBox = function () {
753 819 $('#main_filter_help').toggle();
754 820 };
755 821
822 $('#main_filter').on('keydown.autocomplete', function (e) {
823
824 var BACKSPACE = 8;
825 var el = $(e.currentTarget);
826 if(e.which === BACKSPACE){
827 var inputVal = el.val();
828 if (inputVal === ""){
829 removeGoToFilter()
830 }
831 }
832 });
833
756 834 </script>
757 835 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
758 836 </%def>
759 837
760 838 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
761 839 <div class="modal-dialog">
762 840 <div class="modal-content">
763 841 <div class="modal-header">
764 842 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
765 843 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
766 844 </div>
767 845 <div class="modal-body">
768 846 <div class="block-left">
769 847 <table class="keyboard-mappings">
770 848 <tbody>
771 849 <tr>
772 850 <th></th>
773 851 <th>${_('Site-wide shortcuts')}</th>
774 852 </tr>
775 853 <%
776 854 elems = [
777 855 ('/', 'Use quick search box'),
778 856 ('g h', 'Goto home page'),
779 857 ('g g', 'Goto my private gists page'),
780 858 ('g G', 'Goto my public gists page'),
781 859 ('g 0-9', 'Goto bookmarked items from 0-9'),
782 860 ('n r', 'New repository page'),
783 861 ('n g', 'New gist page'),
784 862 ]
785 863 %>
786 864 %for key, desc in elems:
787 865 <tr>
788 866 <td class="keys">
789 867 <span class="key tag">${key}</span>
790 868 </td>
791 869 <td>${desc}</td>
792 870 </tr>
793 871 %endfor
794 872 </tbody>
795 873 </table>
796 874 </div>
797 875 <div class="block-left">
798 876 <table class="keyboard-mappings">
799 877 <tbody>
800 878 <tr>
801 879 <th></th>
802 880 <th>${_('Repositories')}</th>
803 881 </tr>
804 882 <%
805 883 elems = [
806 884 ('g s', 'Goto summary page'),
807 885 ('g c', 'Goto changelog page'),
808 886 ('g f', 'Goto files page'),
809 887 ('g F', 'Goto files page with file search activated'),
810 888 ('g p', 'Goto pull requests page'),
811 889 ('g o', 'Goto repository settings'),
812 890 ('g O', 'Goto repository permissions settings'),
813 891 ]
814 892 %>
815 893 %for key, desc in elems:
816 894 <tr>
817 895 <td class="keys">
818 896 <span class="key tag">${key}</span>
819 897 </td>
820 898 <td>${desc}</td>
821 899 </tr>
822 900 %endfor
823 901 </tbody>
824 902 </table>
825 903 </div>
826 904 </div>
827 905 <div class="modal-footer">
828 906 </div>
829 907 </div><!-- /.modal-content -->
830 908 </div><!-- /.modal-dialog -->
831 909 </div><!-- /.modal -->
832 910
@@ -1,158 +1,159 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <!DOCTYPE html>
3 3
4 4 <%
5 5 c.template_context['repo_name'] = getattr(c, 'repo_name', '')
6 6 go_import_header = ''
7 7 if hasattr(c, 'rhodecode_db_repo'):
8 8 c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type
9 9 c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_rev[1]
10 ## check repo context
10 c.template_context['repo_id'] = c.rhodecode_db_repo.repo_id
11 11 c.template_context['repo_view_type'] = h.get_repo_view_type(request)
12 12
13 13 if getattr(c, 'repo_group', None):
14 14 c.template_context['repo_group_id'] = c.repo_group.group_id
15 15 c.template_context['repo_group_name'] = c.repo_group.group_name
16 16
17 17 if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id:
18 18 c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username
19 19 c.template_context['rhodecode_user']['email'] = c.rhodecode_user.email
20 20 c.template_context['rhodecode_user']['notification_status'] = c.rhodecode_user.get_instance().user_data.get('notification_status', True)
21 21 c.template_context['rhodecode_user']['first_name'] = c.rhodecode_user.first_name
22 22 c.template_context['rhodecode_user']['last_name'] = c.rhodecode_user.last_name
23 23
24 24 c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default_renderer')
25 25 c.template_context['default_user'] = {
26 26 'username': h.DEFAULT_USER,
27 27 'user_id': 1
28 28 }
29 29 c.template_context['search_context'] = {
30 30 'repo_group_id': c.template_context.get('repo_group_id'),
31 31 'repo_group_name': c.template_context.get('repo_group_name'),
32 'repo_id': c.template_context.get('repo_id'),
32 33 'repo_name': c.template_context.get('repo_name'),
33 34 'repo_view_type': c.template_context.get('repo_view_type'),
34 35 }
35 36
36 37 %>
37 38 <html xmlns="http://www.w3.org/1999/xhtml">
38 39 <head>
39 40 <title>${self.title()}</title>
40 41 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
41 42
42 43 ${h.go_import_header(request, getattr(c, 'rhodecode_db_repo', None))}
43 44
44 45 % if 'safari' in (request.user_agent or '').lower():
45 46 <meta name="referrer" content="origin">
46 47 % else:
47 48 <meta name="referrer" content="origin-when-cross-origin">
48 49 % endif
49 50
50 51 <%def name="robots()">
51 52 <meta name="robots" content="index, nofollow"/>
52 53 </%def>
53 54 ${self.robots()}
54 55 <link rel="icon" href="${h.asset('images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" />
55 56 <script src="${h.asset('js/vendors/webcomponentsjs/custom-elements-es5-adapter.js', ver=c.rhodecode_version_hash)}"></script>
56 57 <script src="${h.asset('js/vendors/webcomponentsjs/webcomponents-bundle.js', ver=c.rhodecode_version_hash)}"></script>
57 58
58 59 ## CSS definitions
59 60 <%def name="css()">
60 61 <link rel="stylesheet" type="text/css" href="${h.asset('css/style.css', ver=c.rhodecode_version_hash)}" media="screen"/>
61 62 ## EXTRA FOR CSS
62 63 ${self.css_extra()}
63 64 </%def>
64 65 ## CSS EXTRA - optionally inject some extra CSS stuff needed for specific websites
65 66 <%def name="css_extra()">
66 67 </%def>
67 68
68 69 ${self.css()}
69 70
70 71 ## JAVASCRIPT
71 72 <%def name="js()">
72 73
73 74 <script src="${h.asset('js/rhodecode/i18n/%s.js' % c.language, ver=c.rhodecode_version_hash)}"></script>
74 75 <script type="text/javascript">
75 76 // register templateContext to pass template variables to JS
76 77 var templateContext = ${h.json.dumps(c.template_context)|n};
77 78
78 79 var APPLICATION_URL = "${h.route_path('home').rstrip('/')}";
79 80 var APPLICATION_PLUGINS = [];
80 81 var ASSET_URL = "${h.asset('')}";
81 82 var DEFAULT_RENDERER = "${h.get_visual_attr(c, 'default_renderer')}";
82 83 var CSRF_TOKEN = "${getattr(c, 'csrf_token', '')}";
83 84
84 85 var APPENLIGHT = {
85 86 enabled: ${'true' if getattr(c, 'appenlight_enabled', False) else 'false'},
86 87 key: '${getattr(c, "appenlight_api_public_key", "")}',
87 88 % if getattr(c, 'appenlight_server_url', None):
88 89 serverUrl: '${getattr(c, "appenlight_server_url", "")}',
89 90 % endif
90 91 requestInfo: {
91 92 % if getattr(c, 'rhodecode_user', None):
92 93 ip: '${c.rhodecode_user.ip_addr}',
93 94 username: '${c.rhodecode_user.username}'
94 95 % endif
95 96 },
96 97 tags: {
97 98 rhodecode_version: '${c.rhodecode_version}',
98 99 rhodecode_edition: '${c.rhodecode_edition}'
99 100 }
100 101 };
101 102
102 103 </script>
103 104 <%include file="/base/plugins_base.mako"/>
104 105 <!--[if lt IE 9]>
105 106 <script language="javascript" type="text/javascript" src="${h.asset('js/src/excanvas.min.js')}"></script>
106 107 <![endif]-->
107 108 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
108 109 <script> var alertMessagePayloads = ${h.flash.json_alerts(request=request)|n}; </script>
109 110 ## avoide escaping the %N
110 111 <script language="javascript" type="text/javascript" src="${h.asset('js/scripts.js', ver=c.rhodecode_version_hash)}"></script>
111 112 <script>CodeMirror.modeURL = "${h.asset('') + 'js/mode/%N/%N.js?ver='+c.rhodecode_version_hash}";</script>
112 113
113 114
114 115 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
115 116 ${self.js_extra()}
116 117
117 118 <script type="text/javascript">
118 119 Rhodecode = (function() {
119 120 function _Rhodecode() {
120 121 this.comments = new CommentsController();
121 122 }
122 123 return new _Rhodecode();
123 124 })();
124 125
125 126 $(document).ready(function(){
126 127 show_more_event();
127 128 timeagoActivate();
128 129 clipboardActivate();
129 130 })
130 131 </script>
131 132
132 133 </%def>
133 134
134 135 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
135 136 <%def name="js_extra()"></%def>
136 137 ${self.js()}
137 138
138 139 <%def name="head_extra()"></%def>
139 140 ${self.head_extra()}
140 141 ## extra stuff
141 142 %if c.pre_code:
142 143 ${c.pre_code|n}
143 144 %endif
144 145 </head>
145 146 <body id="body">
146 147 <noscript>
147 148 <div class="noscript-error">
148 149 ${_('Please enable JavaScript to use RhodeCode Enterprise')}
149 150 </div>
150 151 </noscript>
151 152
152 153 ${next.body()}
153 154 %if c.post_code:
154 155 ${c.post_code|n}
155 156 %endif
156 157 <rhodecode-app></rhodecode-app>
157 158 </body>
158 159 </html>
@@ -1,222 +1,222 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 6 ${_('Search inside repository {repo_name}').format(repo_name=c.repo_name)}
7 7 %elif c.repo_group_name:
8 8 ${_('Search inside repository group {repo_group_name}').format(repo_group_name=c.repo_group_name)}
9 9 %else:
10 10 ${_('Search inside all accessible repositories')}
11 11 %endif
12 12 %if c.rhodecode_name:
13 13 &middot; ${h.branding(c.rhodecode_name)}
14 14 %endif
15 15 </%def>
16 16
17 17 <%def name="breadcrumbs_links()">
18 18 %if c.repo_name:
19 19 ${_('Search inside repository {repo_name}').format(repo_name=c.repo_name)}
20 20 %elif c.repo_group_name:
21 21 ${_('Search inside repository group {repo_group_name}').format(repo_group_name=c.repo_group_name)}
22 22 %else:
23 23 ${_('Search inside all accessible repositories')}
24 24 %endif
25 25
26 26 </%def>
27 27
28 28 <%def name="menu_bar_nav()">
29 29 %if c.repo_name:
30 30 ${self.menu_items(active='search')}
31 31 %elif c.repo_group_name:
32 32 ${self.menu_items(active='search')}
33 33 %else:
34 34 ${self.menu_items(active='search')}
35 35 %endif
36 36 </%def>
37 37
38 38 <%def name="menu_bar_subnav()">
39 39 %if c.repo_name:
40 ${self.repo_menu(active='search')}
40 ${self.repo_menu(active='summary')}
41 41 %elif c.repo_group_name:
42 ${self.repo_group_menu(active='search')}
42 ${self.repo_group_menu(active='home')}
43 43 %endif
44 44 </%def>
45 45
46 46 <%def name="repo_icon(db_repo)">
47 47 %if h.is_hg(db_repo):
48 48 <i class="icon-hg"></i>
49 49 %endif
50 50 %if h.is_git(db_repo):
51 51 <i class="icon-git"></i>
52 52 %endif
53 53 %if h.is_svn(db_repo):
54 54 <i class="icon-svn"></i>
55 55 %endif
56 56 </%def>
57 57
58 58 <%def name="repo_group_icon()">
59 59 <i class="icon-folder-close"></i>
60 60 </%def>
61 61
62 62 <%def name="main()">
63 63 <div class="box">
64 64 %if c.repo_name:
65 65 <!-- box / title -->
66 66 <div class="title">
67 67 ${self.repo_page_title(c.rhodecode_db_repo)}
68 68 </div>
69 69 ${h.form(h.route_path('search_repo',repo_name=c.repo_name),method='get')}
70 70 %elif c.repo_group_name:
71 71 <!-- box / title -->
72 72 <div class="title">
73 73 ${self.repo_group_page_title(c.repo_group)}
74 74 </div>
75 75 ${h.form(h.route_path('search_repo_group',repo_group_name=c.repo_group_name),method='get')}
76 76 %else:
77 77 <!-- box / title -->
78 78 <div class="title">
79 79 ${self.breadcrumbs()}
80 80 <ul class="links">&nbsp;</ul>
81 81 </div>
82 82 <!-- end box / title -->
83 83 ${h.form(h.route_path('search'), method='get')}
84 84 %endif
85 85 <div class="form search-form">
86 86 <div class="fields">
87 87
88 88 ${h.text('q', c.cur_query, placeholder="Enter query...")}
89 89
90 90 ${h.select('type',c.search_type,[('content',_('Files')), ('path',_('File path')),('commit',_('Commits'))],id='id_search_type')}
91 91 ${h.hidden('max_lines', '10')}
92 92
93 93 <input type="submit" value="${_('Search')}" class="btn"/>
94 94 <br/>
95 95
96 96 <div class="search-tags">
97 97 <span class="tag tag8">
98 98 %if c.repo_name:
99 99 <a href="${h.route_path('search', _query={'q': c.cur_query, 'type': request.GET.get('type', 'content')})}">${_('Global Search')}</a>
100 100 %elif c.repo_group_name:
101 101 <a href="${h.route_path('search', _query={'q': c.cur_query, 'type': request.GET.get('type', 'content')})}">${_('Global Search')}</a>
102 102 % else:
103 103 ${_('Global Search')}
104 104 %endif
105 105 </span>
106 106
107 107 %if c.repo_name:
108 108 Β»
109 109 <span class="tag tag8">
110 110 ${repo_icon(c.rhodecode_db_repo)}
111 111 ${c.repo_name}
112 112 </span>
113 113
114 114 %elif c.repo_group_name:
115 115 Β»
116 116 <span class="tag tag8">
117 117 ${repo_group_icon()}
118 118 ${c.repo_group_name}
119 119 </span>
120 120 %endif
121 121
122 122
123 123 % for search_tag in c.search_tags:
124 124 <br/><span class="tag disabled" style="margin-top: 3px">${search_tag}</span>
125 125 % endfor
126 126
127 127 </div>
128 128
129 129 <div class="search-feedback-items">
130 130 % for error in c.errors:
131 131 <span class="error-message">
132 132 % for k,v in error.asdict().items():
133 133 ${k} - ${v}
134 134 % endfor
135 135 </span>
136 136 % endfor
137 137 <div class="field">
138 138 <p class="filterexample" style="position: inherit" onclick="$('#search-help').toggle()">${_('Query Langague examples')}</p>
139 139 <pre id="search-help" style="display: none">\
140 140
141 141 % if c.searcher.name == 'whoosh':
142 142 Example filter terms for `Whoosh` search:
143 143 query lang: <a href="${c.searcher.query_lang_doc}">Whoosh Query Language</a>
144 144 Whoosh has limited query capabilities. For advanced search use ElasticSearch 6 from RhodeCode EE edition.
145 145
146 146 Generate wildcards using '*' character:
147 147 "repo_name:vcs*" - search everything starting with 'vcs'
148 148 "repo_name:*vcs*" - search for repository containing 'vcs'
149 149
150 150 Optional AND / OR operators in queries
151 151 "repo_name:vcs OR repo_name:test"
152 152 "owner:test AND repo_name:test*" AND extension:py
153 153
154 154 Move advanced search is available via ElasticSearch6 backend in EE edition.
155 155 % elif c.searcher.name == 'elasticsearch' and c.searcher.es_version == '2':
156 156 Example filter terms for `ElasticSearch-${c.searcher.es_version}`search:
157 157 ElasticSearch-2 has limited query capabilities. For advanced search use ElasticSearch 6 from RhodeCode EE edition.
158 158
159 159 search type: content (File Content)
160 160 indexed fields: content
161 161
162 162 # search for `fix` string in all files
163 163 fix
164 164
165 165 search type: commit (Commit message)
166 166 indexed fields: message
167 167
168 168 search type: path (File name)
169 169 indexed fields: path
170 170
171 171 % else:
172 172 Example filter terms for `ElasticSearch-${c.searcher.es_version}`search:
173 173 query lang: <a href="${c.searcher.query_lang_doc}">ES 6 Query Language</a>
174 174 The reserved characters needed espace by `\`: + - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /
175 175 % for handler in c.searcher.get_handlers().values():
176 176
177 177 search type: ${handler.search_type_label}
178 178 *indexed fields*: ${', '.join( [('\n ' if x[0]%4==0 else '')+x[1] for x in enumerate(handler.es_6_field_names)])}
179 179 % for entry in handler.es_6_example_queries:
180 180 ${entry.rstrip()}
181 181 % endfor
182 182 % endfor
183 183
184 184 % endif
185 185 </pre>
186 186 </div>
187 187
188 188 <div class="field">${c.runtime}</div>
189 189 </div>
190 190 </div>
191 191 </div>
192 192
193 193 ${h.end_form()}
194 194 <div class="search">
195 195 % if c.search_type == 'content':
196 196 <%include file='search_content.mako'/>
197 197 % elif c.search_type == 'path':
198 198 <%include file='search_path.mako'/>
199 199 % elif c.search_type == 'commit':
200 200 <%include file='search_commit.mako'/>
201 201 % elif c.search_type == 'repository':
202 202 <%include file='search_repository.mako'/>
203 203 % endif
204 204 </div>
205 205 </div>
206 206 <script>
207 207 $(document).ready(function(){
208 208 $("#id_search_type").select2({
209 209 'containerCssClass': "drop-menu",
210 210 'dropdownCssClass': "drop-menu-dropdown",
211 211 'dropdownAutoWidth': true,
212 212 'minimumResultsForSearch': -1
213 213 });
214 214
215 215 $('#q').autoGrowInput({maxWidth: 920});
216 216
217 217 setTimeout(function() {
218 218 $('#q').keyup()
219 219 }, 1);
220 220 })
221 221 </script>
222 222 </%def>
General Comments 0
You need to be logged in to leave comments. Login now