Show More
@@ -33,7 +33,7 b' from rhodecode.lib.index import searcher' | |||||
33 | from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int |
|
33 | from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int | |
34 | from rhodecode.lib.ext_json import json |
|
34 | from rhodecode.lib.ext_json import json | |
35 | from rhodecode.model.db import ( |
|
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 | from rhodecode.model.repo import RepoModel |
|
37 | from rhodecode.model.repo import RepoModel | |
38 | from rhodecode.model.repo_group import RepoGroupModel |
|
38 | from rhodecode.model.repo_group import RepoGroupModel | |
39 | from rhodecode.model.scm import RepoGroupList, RepoList |
|
39 | from rhodecode.model.scm import RepoGroupList, RepoList | |
@@ -105,21 +105,27 b' class HomeView(BaseAppView):' | |||||
105 |
|
105 | |||
106 | return {'suggestions': _user_groups} |
|
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 | org_query = name_contains |
|
109 | org_query = name_contains | |
110 | allowed_ids = self._rhodecode_user.repo_acl_ids( |
|
110 | allowed_ids = self._rhodecode_user.repo_acl_ids( | |
111 | ['repository.read', 'repository.write', 'repository.admin'], |
|
111 | ['repository.read', 'repository.write', 'repository.admin'], | |
112 | cache=False, name_filter=name_contains) or [-1] |
|
112 | cache=False, name_filter=name_contains) or [-1] | |
113 |
|
113 | |||
114 | query = Repository.query()\ |
|
114 | query = Repository.query()\ | |
115 | .order_by(func.length(Repository.repo_name))\ |
|
|||
116 | .order_by(Repository.repo_name)\ |
|
|||
117 | .filter(Repository.archived.isnot(true()))\ |
|
115 | .filter(Repository.archived.isnot(true()))\ | |
118 | .filter(or_( |
|
116 | .filter(or_( | |
119 | # generate multiple IN to fix limitation problems |
|
117 | # generate multiple IN to fix limitation problems | |
120 | *in_filter_generator(Repository.repo_id, allowed_ids) |
|
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 | if repo_type: |
|
129 | if repo_type: | |
124 | query = query.filter(Repository.repo_type == repo_type) |
|
130 | query = query.filter(Repository.repo_type == repo_type) | |
125 |
|
131 | |||
@@ -145,20 +151,26 b' class HomeView(BaseAppView):' | |||||
145 | } |
|
151 | } | |
146 | for obj in acl_iter] |
|
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 | org_query = name_contains |
|
155 | org_query = name_contains | |
150 | allowed_ids = self._rhodecode_user.repo_group_acl_ids( |
|
156 | allowed_ids = self._rhodecode_user.repo_group_acl_ids( | |
151 | ['group.read', 'group.write', 'group.admin'], |
|
157 | ['group.read', 'group.write', 'group.admin'], | |
152 | cache=False, name_filter=name_contains) or [-1] |
|
158 | cache=False, name_filter=name_contains) or [-1] | |
153 |
|
159 | |||
154 | query = RepoGroup.query()\ |
|
160 | query = RepoGroup.query()\ | |
155 | .order_by(func.length(RepoGroup.group_name))\ |
|
|||
156 | .order_by(RepoGroup.group_name) \ |
|
|||
157 | .filter(or_( |
|
161 | .filter(or_( | |
158 | # generate multiple IN to fix limitation problems |
|
162 | # generate multiple IN to fix limitation problems | |
159 | *in_filter_generator(RepoGroup.group_id, allowed_ids) |
|
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 | if name_contains: |
|
174 | if name_contains: | |
163 | ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) |
|
175 | ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) | |
164 | query = query.filter( |
|
176 | query = query.filter( | |
@@ -183,11 +195,17 b' class HomeView(BaseAppView):' | |||||
183 | def _get_user_list(self, name_contains=None, limit=20): |
|
195 | def _get_user_list(self, name_contains=None, limit=20): | |
184 | org_query = name_contains |
|
196 | org_query = name_contains | |
185 | if not name_contains: |
|
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 | if len(name_contains) != 1: |
|
206 | if len(name_contains) != 1: | |
190 | return [] |
|
207 | return [], False | |
|
208 | ||||
191 | name_contains = name_contains[0] |
|
209 | name_contains = name_contains[0] | |
192 |
|
210 | |||
193 | query = User.query()\ |
|
211 | query = User.query()\ | |
@@ -207,22 +225,28 b' class HomeView(BaseAppView):' | |||||
207 | { |
|
225 | { | |
208 | 'id': obj.user_id, |
|
226 | 'id': obj.user_id, | |
209 | 'value': org_query, |
|
227 | 'value': org_query, | |
210 | 'value_display': obj.username, |
|
228 | 'value_display': 'user: `{}`'.format(obj.username), | |
211 | 'type': 'user', |
|
229 | 'type': 'user', | |
212 | 'icon_link': h.gravatar_url(obj.email, 30), |
|
230 | 'icon_link': h.gravatar_url(obj.email, 30), | |
213 | 'url': h.route_path( |
|
231 | 'url': h.route_path( | |
214 | 'user_profile', username=obj.username) |
|
232 | 'user_profile', username=obj.username) | |
215 | } |
|
233 | } | |
216 | for obj in acl_iter] |
|
234 | for obj in acl_iter], True | |
217 |
|
235 | |||
218 | def _get_user_groups_list(self, name_contains=None, limit=20): |
|
236 | def _get_user_groups_list(self, name_contains=None, limit=20): | |
219 | org_query = name_contains |
|
237 | org_query = name_contains | |
220 | if not name_contains: |
|
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 | if len(name_contains) != 1: |
|
247 | if len(name_contains) != 1: | |
225 | return [] |
|
248 | return [], False | |
|
249 | ||||
226 | name_contains = name_contains[0] |
|
250 | name_contains = name_contains[0] | |
227 |
|
251 | |||
228 | query = UserGroup.query()\ |
|
252 | query = UserGroup.query()\ | |
@@ -241,27 +265,34 b' class HomeView(BaseAppView):' | |||||
241 | { |
|
265 | { | |
242 | 'id': obj.users_group_id, |
|
266 | 'id': obj.users_group_id, | |
243 | 'value': org_query, |
|
267 | 'value': org_query, | |
244 | 'value_display': obj.users_group_name, |
|
268 | 'value_display': 'user_group: `{}`'.format(obj.users_group_name), | |
245 | 'type': 'user_group', |
|
269 | 'type': 'user_group', | |
246 | 'url': h.route_path( |
|
270 | 'url': h.route_path( | |
247 | 'user_group_profile', user_group_name=obj.users_group_name) |
|
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 | org_query = query |
|
282 | org_query = query | |
253 | if not query or len(query) < 3 or not searcher: |
|
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 | if len(commit_hashes) != 1: |
|
288 | if len(commit_hashes) != 1: | |
259 | return [] |
|
289 | return [], False | |
|
290 | ||||
260 | commit_hash = commit_hashes[0] |
|
291 | commit_hash = commit_hashes[0] | |
261 |
|
292 | |||
262 | result = searcher.search( |
|
293 | result = searcher.search( | |
263 | 'commit_id:{}*'.format(commit_hash), 'commit', auth_user, |
|
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 | commits = [] |
|
297 | commits = [] | |
267 | for entry in result['results']: |
|
298 | for entry in result['results']: | |
@@ -286,22 +317,29 b' class HomeView(BaseAppView):' | |||||
286 | } |
|
317 | } | |
287 |
|
318 | |||
288 | commits.append(commit_entry) |
|
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 | org_query = query |
|
329 | org_query = query | |
293 | if not query or len(query) < 3 or not searcher: |
|
330 | if not query or len(query) < 3 or not searcher: | |
294 | return [] |
|
331 | return [], False | |
295 |
|
332 | |||
296 |
paths_re = re.compile('(?:file: |
|
333 | paths_re = re.compile('(?:file:[ ]?)(.+)').findall(query) | |
297 | if len(paths_re) != 1: |
|
334 | if len(paths_re) != 1: | |
298 | return [] |
|
335 | return [], False | |
|
336 | ||||
299 | file_path = paths_re[0] |
|
337 | file_path = paths_re[0] | |
300 |
|
338 | |||
301 | search_path = searcher.escape_specials(file_path) |
|
339 | search_path = searcher.escape_specials(file_path) | |
302 | result = searcher.search( |
|
340 | result = searcher.search( | |
303 | 'file.raw:*{}*'.format(search_path), 'path', auth_user, |
|
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 | files = [] |
|
344 | files = [] | |
307 | for entry in result['results']: |
|
345 | for entry in result['results']: | |
@@ -327,7 +365,7 b' class HomeView(BaseAppView):' | |||||
327 | } |
|
365 | } | |
328 |
|
366 | |||
329 | files.append(file_entry) |
|
367 | files.append(file_entry) | |
330 | return files |
|
368 | return files, True | |
331 |
|
369 | |||
332 | @LoginRequired() |
|
370 | @LoginRequired() | |
333 | @view_config( |
|
371 | @view_config( | |
@@ -405,8 +443,7 b' class HomeView(BaseAppView):' | |||||
405 | qry = query |
|
443 | qry = query | |
406 | return {'q': qry, 'type': 'content'} |
|
444 | return {'q': qry, 'type': 'content'} | |
407 | label = u'File search for `{}` in this repository.'.format(query) |
|
445 | label = u'File search for `{}` in this repository.'.format(query) | |
408 | queries.append( |
|
446 | file_qry = { | |
409 | { |
|
|||
410 |
|
|
447 | 'id': -10, | |
411 |
|
|
448 | 'value': query, | |
412 |
|
|
449 | 'value_display': label, | |
@@ -415,7 +452,6 b' class HomeView(BaseAppView):' | |||||
415 |
|
|
452 | repo_name=repo_name, | |
416 |
|
|
453 | _query=query_modifier()) | |
417 | } |
|
454 | } | |
418 | ) |
|
|||
419 |
|
455 | |||
420 | # commits |
|
456 | # commits | |
421 | def query_modifier(): |
|
457 | def query_modifier(): | |
@@ -423,8 +459,7 b' class HomeView(BaseAppView):' | |||||
423 | return {'q': qry, 'type': 'commit'} |
|
459 | return {'q': qry, 'type': 'commit'} | |
424 |
|
460 | |||
425 | label = u'Commit search for `{}` in this repository.'.format(query) |
|
461 | label = u'Commit search for `{}` in this repository.'.format(query) | |
426 | queries.append( |
|
462 | commit_qry = { | |
427 | { |
|
|||
428 |
|
|
463 | 'id': -20, | |
429 |
|
|
464 | 'value': query, | |
430 |
|
|
465 | 'value_display': label, | |
@@ -433,7 +468,13 b' class HomeView(BaseAppView):' | |||||
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 | elif is_es_6 and repo_group_name: |
|
479 | elif is_es_6 and repo_group_name: | |
439 | # files |
|
480 | # files | |
@@ -442,8 +483,7 b' class HomeView(BaseAppView):' | |||||
442 | return {'q': qry, 'type': 'content'} |
|
483 | return {'q': qry, 'type': 'content'} | |
443 |
|
484 | |||
444 | label = u'File search for `{}` in this repository group'.format(query) |
|
485 | label = u'File search for `{}` in this repository group'.format(query) | |
445 | queries.append( |
|
486 | file_qry = { | |
446 | { |
|
|||
447 |
|
|
487 | 'id': -30, | |
448 |
|
|
488 | 'value': query, | |
449 |
|
|
489 | 'value_display': label, | |
@@ -452,7 +492,6 b' class HomeView(BaseAppView):' | |||||
452 |
|
|
492 | repo_group_name=repo_group_name, | |
453 |
|
|
493 | _query=query_modifier()) | |
454 | } |
|
494 | } | |
455 | ) |
|
|||
456 |
|
495 | |||
457 | # commits |
|
496 | # commits | |
458 | def query_modifier(): |
|
497 | def query_modifier(): | |
@@ -460,8 +499,7 b' class HomeView(BaseAppView):' | |||||
460 | return {'q': qry, 'type': 'commit'} |
|
499 | return {'q': qry, 'type': 'commit'} | |
461 |
|
500 | |||
462 | label = u'Commit search for `{}` in this repository group'.format(query) |
|
501 | label = u'Commit search for `{}` in this repository group'.format(query) | |
463 | queries.append( |
|
502 | commit_qry = { | |
464 | { |
|
|||
465 |
|
|
503 | 'id': -40, | |
466 |
|
|
504 | 'value': query, | |
467 |
|
|
505 | 'value_display': label, | |
@@ -470,8 +508,15 b' class HomeView(BaseAppView):' | |||||
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 | if not queries: |
|
520 | if not queries: | |
476 | queries.append( |
|
521 | queries.append( | |
477 | { |
|
522 | { | |
@@ -510,57 +555,76 b' class HomeView(BaseAppView):' | |||||
510 | if not query: |
|
555 | if not query: | |
511 | return {'suggestions': res} |
|
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 | searcher = searcher_from_config(self.request.registry.settings) |
|
566 | searcher = searcher_from_config(self.request.registry.settings) | |
514 | for _q in self._get_default_search_queries(self.request.GET, searcher, query): |
|
567 | has_specialized_search = False | |
515 | res.append(_q) |
|
|||
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 | repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]')) |
|
577 | repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]')) | |
518 | if repo_group_id: |
|
578 | if repo_group_id: | |
519 | repo_group = RepoGroup.get(repo_group_id) |
|
579 | repo_group = RepoGroup.get(repo_group_id) | |
520 | composed_hint = '{}/{}'.format(repo_group.group_name, query) |
|
580 | prefix_match = False | |
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 | }) |
|
|||
531 |
|
581 | |||
532 | repo_groups = self._get_repo_group_list(query) |
|
582 | # user: type search | |
533 | for serialized_repo_group in repo_groups: |
|
583 | if not prefix_match: | |
534 | res.append(serialized_repo_group) |
|
584 | users, prefix_match = self._get_user_list(query) | |
535 |
|
585 | if users: | ||
536 | repos = self._get_repo_list(query) |
|
586 | has_specialized_search = True | |
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) |
|
|||
544 | for serialized_user in users: |
|
587 | for serialized_user in users: | |
545 | res.append(serialized_user) |
|
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 | for serialized_user_group in user_groups: |
|
598 | for serialized_user_group in user_groups: | |
549 | res.append(serialized_user_group) |
|
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 | if commits: |
|
608 | if commits: | |
|
609 | has_specialized_search = True | |||
553 | unique_repos = collections.OrderedDict() |
|
610 | unique_repos = collections.OrderedDict() | |
554 | for commit in commits: |
|
611 | for commit in commits: | |
555 | repo_name = commit['repo'] |
|
612 | repo_name = commit['repo'] | |
556 | unique_repos.setdefault(repo_name, []).append(commit) |
|
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 | for commit in commits: |
|
616 | for commit in commits: | |
560 | res.append(commit) |
|
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 | if paths: |
|
626 | if paths: | |
|
627 | has_specialized_search = True | |||
564 | unique_repos = collections.OrderedDict() |
|
628 | unique_repos = collections.OrderedDict() | |
565 | for path in paths: |
|
629 | for path in paths: | |
566 | repo_name = path['repo'] |
|
630 | repo_name = path['repo'] | |
@@ -569,6 +633,29 b' class HomeView(BaseAppView):' | |||||
569 | for repo, paths in unique_repos.items(): |
|
633 | for repo, paths in unique_repos.items(): | |
570 | for path in paths: |
|
634 | for path in paths: | |
571 | res.append(path) |
|
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 | return {'suggestions': res} |
|
660 | return {'suggestions': res} | |
574 |
|
661 |
@@ -2037,6 +2037,6 b' def get_repo_view_type(request):' | |||||
2037 | 'repo_files': 'files', |
|
2037 | 'repo_files': 'files', | |
2038 | 'repo_summary': 'summary', |
|
2038 | 'repo_summary': 'summary', | |
2039 | 'repo_commit': 'commit' |
|
2039 | 'repo_commit': 'commit' | |
|
2040 | } | |||
2040 |
|
2041 | |||
2041 | } |
|
|||
2042 | return route_to_view_type.get(route_name) |
|
2042 | return route_to_view_type.get(route_name) |
@@ -39,7 +39,7 b' from sqlalchemy import (' | |||||
39 | Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column, |
|
39 | Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column, | |
40 | Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary, |
|
40 | Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary, | |
41 | Text, Float, PickleType) |
|
41 | Text, Float, PickleType) | |
42 | from sqlalchemy.sql.expression import true, false |
|
42 | from sqlalchemy.sql.expression import true, false, case | |
43 | from sqlalchemy.sql.functions import coalesce, count # pragma: no cover |
|
43 | from sqlalchemy.sql.functions import coalesce, count # pragma: no cover | |
44 | from sqlalchemy.orm import ( |
|
44 | from sqlalchemy.orm import ( | |
45 | relationship, joinedload, class_mapper, validates, aliased) |
|
45 | relationship, joinedload, class_mapper, validates, aliased) |
@@ -644,6 +644,40 b' ul#context-pages {' | |||||
644 |
|
644 | |||
645 | .main_filter_input_box { |
|
645 | .main_filter_input_box { | |
646 | display: inline-block; |
|
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 | .main_filter_box { |
|
683 | .main_filter_box { | |
@@ -667,7 +701,8 b' ul#context-pages {' | |||||
667 | color: @nav-grey; |
|
701 | color: @nav-grey; | |
668 | background: @grey3; |
|
702 | background: @grey3; | |
669 | min-height: 18px; |
|
703 | min-height: 18px; | |
670 |
|
704 | border:none; | ||
|
705 | border-radius: 0; | |||
671 |
|
706 | |||
672 | &:active { |
|
707 | &:active { | |
673 | color: @grey2 !important; |
|
708 | color: @grey2 !important; |
@@ -528,7 +528,19 b'' | |||||
528 | if ($.isFunction(serviceUrl)) { |
|
528 | if ($.isFunction(serviceUrl)) { | |
529 | serviceUrl = serviceUrl.call(that.element, query); |
|
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 | response = that.cachedResponse[cacheKey]; |
|
544 | response = that.cachedResponse[cacheKey]; | |
533 | } |
|
545 | } | |
534 |
|
546 | |||
@@ -536,7 +548,7 b'' | |||||
536 | that.suggestions = response.suggestions; |
|
548 | that.suggestions = response.suggestions; | |
537 | that.suggest(); |
|
549 | that.suggest(); | |
538 | } else if (!that.isBadQuery(query)) { |
|
550 | } else if (!that.isBadQuery(query)) { | |
539 |
if (options.onSearchStart.call(that.element, |
|
551 | if (options.onSearchStart.call(that.element, params) === false) { | |
540 | return; |
|
552 | return; | |
541 | } |
|
553 | } | |
542 | if (that.currentRequest) { |
|
554 | if (that.currentRequest) { |
@@ -233,7 +233,6 b'' | |||||
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> |
|
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 | <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> |
|
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 | <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> |
|
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 | ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()" |
|
237 | ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()" | |
239 | %if c.rhodecode_db_repo.repo_type in ['git','hg']: |
|
238 | %if c.rhodecode_db_repo.repo_type in ['git','hg']: | |
@@ -338,7 +337,6 b'' | |||||
338 | <div class="wrapper"> |
|
337 | <div class="wrapper"> | |
339 | <ul id="context-pages" class="navigation horizontal-list"> |
|
338 | <ul id="context-pages" class="navigation horizontal-list"> | |
340 | <li class="${is_active('home')}"><a class="menulink" href="${h.route_path('repo_group_home', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Group Home')}</div></a></li> |
|
339 | <li class="${is_active('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 | <li class="${is_active('options')}"> |
|
341 | <li class="${is_active('options')}"> | |
344 | <a class="menulink dropdown"> |
|
342 | <a class="menulink dropdown"> | |
@@ -486,10 +484,35 b'' | |||||
486 | <li> |
|
484 | <li> | |
487 | <div class="menulabel main_filter_box"> |
|
485 | <div class="menulabel main_filter_box"> | |
488 | <div class="main_filter_input_box"> |
|
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 | <input class="main_filter_input" id="main_filter" size="15" type="text" name="main_filter" placeholder="${_('search / go to...')}" value=""/> |
|
510 | <input class="main_filter_input" id="main_filter" size="15" type="text" name="main_filter" placeholder="${_('search / go to...')}" value="" /> | |
490 |
</ |
|
511 | </li> | |
491 |
< |
|
512 | <li class="searchTag searchTagHelp"> | |
492 | <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a> |
|
513 | <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a> | |
|
514 | </li> | |||
|
515 | </ul> | |||
493 | </div> |
|
516 | </div> | |
494 | </div> |
|
517 | </div> | |
495 |
|
518 | |||
@@ -500,13 +523,21 b'' | |||||
500 |
|
523 | |||
501 | - Prefix query to allow special search: |
|
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 |
|
534 | % if c.template_context['search_context']['repo_id']: | |
508 |
|
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> | ||
509 | file:models.py, to search for file paths |
|
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 | </div> |
|
541 | </div> | |
511 | </li> |
|
542 | </li> | |
512 |
|
543 | |||
@@ -622,20 +653,9 b'' | |||||
622 | }(result, escapeMarkup); |
|
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') { | |
@@ -650,9 +670,23 b'' | |||||
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 | valueDisplay = Select2.util.escapeMarkup(valueDisplay); |
|
684 | valueDisplay = Select2.util.escapeMarkup(valueDisplay); | |
|
685 | ||||
|
686 | // highlight match | |||
|
687 | if (searchType != 'text') { | |||
655 | valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>'); |
|
688 | valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>'); | |
|
689 | } | |||
656 |
|
690 | |||
657 | var icon = ''; |
|
691 | var icon = ''; | |
658 |
|
692 | |||
@@ -684,6 +718,7 b'' | |||||
684 | else if (searchType === 'user_group') { |
|
718 | else if (searchType === 'user_group') { | |
685 | icon += '<i class="icon-group"></i> '; |
|
719 | icon += '<i class="icon-group"></i> '; | |
686 | } |
|
720 | } | |
|
721 | // user | |||
687 | else if (searchType === 'user') { |
|
722 | else if (searchType === 'user') { | |
688 | icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']); |
|
723 | icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']); | |
689 | } |
|
724 | } | |
@@ -707,6 +742,10 b'' | |||||
707 | icon += '<i class="icon-tag"></i>'; |
|
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 | var tmpl = '<div class="ac-container-wrap">{0}{1}</div>'; |
|
750 | var tmpl = '<div class="ac-container-wrap">{0}{1}</div>'; | |
712 | return tmpl.format(icon, valueDisplay); |
|
751 | return tmpl.format(icon, valueDisplay); | |
@@ -716,25 +755,52 b'' | |||||
716 | if (suggestion.type === "hint") { |
|
755 | if (suggestion.type === "hint") { | |
717 | // we skip action |
|
756 | // we skip action | |
718 | $('#main_filter').focus(); |
|
757 | $('#main_filter').focus(); | |
|
758 | } | |||
|
759 | else if (suggestion.type === "text") { | |||
|
760 | // we skip action | |||
|
761 | $('#main_filter').focus(); | |||
|
762 | ||||
719 | } else { |
|
763 | } else { | |
720 | window.location = suggestion['url']; |
|
764 | window.location = suggestion['url']; | |
721 | } |
|
765 | } | |
722 | }; |
|
766 | }; | |
|
767 | ||||
723 | var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) { |
|
768 | var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) { | |
724 | if (queryLowerCase.split(':').length === 2) { |
|
769 | if (queryLowerCase.split(':').length === 2) { | |
725 | queryLowerCase = queryLowerCase.split(':')[1] |
|
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 | return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1; |
|
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 | $('#main_filter').autocomplete({ |
|
794 | $('#main_filter').autocomplete({ | |
731 | serviceUrl: pyroutes.url('goto_switcher_data'), |
|
795 | serviceUrl: pyroutes.url('goto_switcher_data'), | |
732 | params: {"search_context": templateContext.search_context}, |
|
796 | params: { | |
|
797 | "search_context": templateContext.search_context | |||
|
798 | }, | |||
733 | minChars:2, |
|
799 | minChars:2, | |
734 | maxHeight:400, |
|
800 | maxHeight:400, | |
735 | deferRequestBy: 300, //miliseconds |
|
801 | deferRequestBy: 300, //miliseconds | |
736 | tabDisabled: true, |
|
802 | tabDisabled: true, | |
737 |
autoSelectFirst: |
|
803 | autoSelectFirst: false, | |
738 | formatResult: autocompleteMainFilterFormatResult, |
|
804 | formatResult: autocompleteMainFilterFormatResult, | |
739 | lookupFilter: autocompleteMainFilterResult, |
|
805 | lookupFilter: autocompleteMainFilterResult, | |
740 | onSelect: function (element, suggestion) { |
|
806 | onSelect: function (element, suggestion) { | |
@@ -753,6 +819,18 b'' | |||||
753 | $('#main_filter_help').toggle(); |
|
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 | </script> |
|
834 | </script> | |
757 | <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script> |
|
835 | <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script> | |
758 | </%def> |
|
836 | </%def> |
@@ -7,7 +7,7 b" go_import_header = ''" | |||||
7 | if hasattr(c, 'rhodecode_db_repo'): |
|
7 | if hasattr(c, 'rhodecode_db_repo'): | |
8 | c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type |
|
8 | c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type | |
9 | c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_rev[1] |
|
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 | c.template_context['repo_view_type'] = h.get_repo_view_type(request) |
|
11 | c.template_context['repo_view_type'] = h.get_repo_view_type(request) | |
12 |
|
12 | |||
13 | if getattr(c, 'repo_group', None): |
|
13 | if getattr(c, 'repo_group', None): | |
@@ -29,6 +29,7 b" c.template_context['default_user'] = {" | |||||
29 | c.template_context['search_context'] = { |
|
29 | c.template_context['search_context'] = { | |
30 | 'repo_group_id': c.template_context.get('repo_group_id'), |
|
30 | 'repo_group_id': c.template_context.get('repo_group_id'), | |
31 | 'repo_group_name': c.template_context.get('repo_group_name'), |
|
31 | 'repo_group_name': c.template_context.get('repo_group_name'), | |
|
32 | 'repo_id': c.template_context.get('repo_id'), | |||
32 | 'repo_name': c.template_context.get('repo_name'), |
|
33 | 'repo_name': c.template_context.get('repo_name'), | |
33 | 'repo_view_type': c.template_context.get('repo_view_type'), |
|
34 | 'repo_view_type': c.template_context.get('repo_view_type'), | |
34 | } |
|
35 | } |
@@ -37,9 +37,9 b'' | |||||
37 |
|
37 | |||
38 | <%def name="menu_bar_subnav()"> |
|
38 | <%def name="menu_bar_subnav()"> | |
39 | %if c.repo_name: |
|
39 | %if c.repo_name: | |
40 |
${self.repo_menu(active='s |
|
40 | ${self.repo_menu(active='summary')} | |
41 | %elif c.repo_group_name: |
|
41 | %elif c.repo_group_name: | |
42 |
${self.repo_group_menu(active=' |
|
42 | ${self.repo_group_menu(active='home')} | |
43 | %endif |
|
43 | %endif | |
44 | </%def> |
|
44 | </%def> | |
45 |
|
45 |
General Comments 0
You need to be logged in to leave comments.
Login now