##// END OF EJS Templates
search: allow result sorting for elasticsearch6
marcink -
r3963:107ed32f default
parent child Browse files
Show More
@@ -1,164 +1,169 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import urllib
22 import urllib
23 from pyramid.view import view_config
23 from pyramid.view import view_config
24 from webhelpers.util import update_params
24 from webhelpers.util import update_params
25
25
26 from rhodecode.apps._base import BaseAppView, RepoAppView, RepoGroupAppView
26 from rhodecode.apps._base import BaseAppView, RepoAppView, RepoGroupAppView
27 from rhodecode.lib.auth import (
27 from rhodecode.lib.auth import (
28 LoginRequired, HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator)
28 LoginRequired, HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator)
29 from rhodecode.lib.helpers import Page
29 from rhodecode.lib.helpers import Page
30 from rhodecode.lib.utils2 import safe_str
30 from rhodecode.lib.utils2 import safe_str
31 from rhodecode.lib.index import searcher_from_config
31 from rhodecode.lib.index import searcher_from_config
32 from rhodecode.model import validation_schema
32 from rhodecode.model import validation_schema
33 from rhodecode.model.validation_schema.schemas import search_schema
33 from rhodecode.model.validation_schema.schemas import search_schema
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 def perform_search(request, tmpl_context, repo_name=None, repo_group_name=None):
38 def perform_search(request, tmpl_context, repo_name=None, repo_group_name=None):
39 searcher = searcher_from_config(request.registry.settings)
39 searcher = searcher_from_config(request.registry.settings)
40 formatted_results = []
40 formatted_results = []
41 execution_time = ''
41 execution_time = ''
42
42
43 schema = search_schema.SearchParamsSchema()
43 schema = search_schema.SearchParamsSchema()
44 search_tags = []
44 search_tags = []
45 search_params = {}
45 search_params = {}
46 errors = []
46 errors = []
47
47 try:
48 try:
48 search_params = schema.deserialize(
49 search_params = schema.deserialize(
49 dict(
50 dict(
50 search_query=request.GET.get('q'),
51 search_query=request.GET.get('q'),
51 search_type=request.GET.get('type'),
52 search_type=request.GET.get('type'),
52 search_sort=request.GET.get('sort'),
53 search_sort=request.GET.get('sort'),
53 search_max_lines=request.GET.get('max_lines'),
54 search_max_lines=request.GET.get('max_lines'),
54 page_limit=request.GET.get('page_limit'),
55 page_limit=request.GET.get('page_limit'),
55 requested_page=request.GET.get('page'),
56 requested_page=request.GET.get('page'),
56 )
57 )
57 )
58 )
58 except validation_schema.Invalid as e:
59 except validation_schema.Invalid as e:
59 errors = e.children
60 errors = e.children
60
61
61 def url_generator(**kw):
62 def url_generator(**kw):
62 q = urllib.quote(safe_str(search_query))
63 q = urllib.quote(safe_str(search_query))
63 return update_params(
64 return update_params(
64 "?q=%s&type=%s&max_lines=%s" % (
65 "?q=%s&type=%s&max_lines=%s&sort=%s" % (
65 q, safe_str(search_type), search_max_lines), **kw)
66 q, safe_str(search_type), search_max_lines, search_sort), **kw)
66
67
67 c = tmpl_context
68 c = tmpl_context
68 search_query = search_params.get('search_query')
69 search_query = search_params.get('search_query')
69 search_type = search_params.get('search_type')
70 search_type = search_params.get('search_type')
70 search_sort = search_params.get('search_sort')
71 search_sort = search_params.get('search_sort')
71 search_max_lines = search_params.get('search_max_lines')
72 search_max_lines = search_params.get('search_max_lines')
72 if search_params.get('search_query'):
73 if search_params.get('search_query'):
73 page_limit = search_params['page_limit']
74 page_limit = search_params['page_limit']
74 requested_page = search_params['requested_page']
75 requested_page = search_params['requested_page']
75
76
76 try:
77 try:
77 search_result = searcher.search(
78 search_result = searcher.search(
78 search_query, search_type, c.auth_user, repo_name, repo_group_name,
79 search_query, search_type, c.auth_user, repo_name, repo_group_name,
79 requested_page=requested_page, page_limit=page_limit, sort=search_sort)
80 requested_page=requested_page, page_limit=page_limit, sort=search_sort)
80
81
81 formatted_results = Page(
82 formatted_results = Page(
82 search_result['results'], page=requested_page,
83 search_result['results'], page=requested_page,
83 item_count=search_result['count'],
84 item_count=search_result['count'],
84 items_per_page=page_limit, url=url_generator)
85 items_per_page=page_limit, url=url_generator)
85 finally:
86 finally:
86 searcher.cleanup()
87 searcher.cleanup()
87
88
88 search_tags = searcher.extract_search_tags(search_query)
89 search_tags = searcher.extract_search_tags(search_query)
89
90
90 if not search_result['error']:
91 if not search_result['error']:
91 execution_time = '%s results (%.4f seconds)' % (
92 execution_time = '%s results (%.4f seconds)' % (
92 search_result['count'],
93 search_result['count'],
93 search_result['runtime'])
94 search_result['runtime'])
94 elif not errors:
95 elif not errors:
95 node = schema['search_query']
96 node = schema['search_query']
96 errors = [
97 errors = [
97 validation_schema.Invalid(node, search_result['error'])]
98 validation_schema.Invalid(node, search_result['error'])]
98
99
99 c.perm_user = c.auth_user
100 c.perm_user = c.auth_user
100 c.repo_name = repo_name
101 c.repo_name = repo_name
101 c.repo_group_name = repo_group_name
102 c.repo_group_name = repo_group_name
102 c.sort = search_sort
103 c.url_generator = url_generator
103 c.url_generator = url_generator
104 c.errors = errors
104 c.errors = errors
105 c.formatted_results = formatted_results
105 c.formatted_results = formatted_results
106 c.runtime = execution_time
106 c.runtime = execution_time
107 c.cur_query = search_query
107 c.cur_query = search_query
108 c.search_type = search_type
108 c.search_type = search_type
109 c.searcher = searcher
109 c.searcher = searcher
110 c.search_tags = search_tags
110 c.search_tags = search_tags
111
111
112 direction, sort_field = searcher.get_sort(search_type, search_sort)
113 c.sort = '{}:{}'.format(direction, sort_field)
114 c.sort_tag = sort_field
115 c.sort_tag_dir = direction
116
112
117
113 class SearchView(BaseAppView):
118 class SearchView(BaseAppView):
114 def load_default_context(self):
119 def load_default_context(self):
115 c = self._get_local_tmpl_context()
120 c = self._get_local_tmpl_context()
116 return c
121 return c
117
122
118 @LoginRequired()
123 @LoginRequired()
119 @view_config(
124 @view_config(
120 route_name='search', request_method='GET',
125 route_name='search', request_method='GET',
121 renderer='rhodecode:templates/search/search.mako')
126 renderer='rhodecode:templates/search/search.mako')
122 def search(self):
127 def search(self):
123 c = self.load_default_context()
128 c = self.load_default_context()
124 perform_search(self.request, c)
129 perform_search(self.request, c)
125 return self._get_template_context(c)
130 return self._get_template_context(c)
126
131
127
132
128 class SearchRepoView(RepoAppView):
133 class SearchRepoView(RepoAppView):
129 def load_default_context(self):
134 def load_default_context(self):
130 c = self._get_local_tmpl_context()
135 c = self._get_local_tmpl_context()
131 c.active = 'search'
136 c.active = 'search'
132 return c
137 return c
133
138
134 @LoginRequired()
139 @LoginRequired()
135 @HasRepoPermissionAnyDecorator(
140 @HasRepoPermissionAnyDecorator(
136 'repository.read', 'repository.write', 'repository.admin')
141 'repository.read', 'repository.write', 'repository.admin')
137 @view_config(
142 @view_config(
138 route_name='search_repo', request_method='GET',
143 route_name='search_repo', request_method='GET',
139 renderer='rhodecode:templates/search/search.mako')
144 renderer='rhodecode:templates/search/search.mako')
140 @view_config(
145 @view_config(
141 route_name='search_repo_alt', request_method='GET',
146 route_name='search_repo_alt', request_method='GET',
142 renderer='rhodecode:templates/search/search.mako')
147 renderer='rhodecode:templates/search/search.mako')
143 def search_repo(self):
148 def search_repo(self):
144 c = self.load_default_context()
149 c = self.load_default_context()
145 perform_search(self.request, c, repo_name=self.db_repo_name)
150 perform_search(self.request, c, repo_name=self.db_repo_name)
146 return self._get_template_context(c)
151 return self._get_template_context(c)
147
152
148
153
149 class SearchRepoGroupView(RepoGroupAppView):
154 class SearchRepoGroupView(RepoGroupAppView):
150 def load_default_context(self):
155 def load_default_context(self):
151 c = self._get_local_tmpl_context()
156 c = self._get_local_tmpl_context()
152 c.active = 'search'
157 c.active = 'search'
153 return c
158 return c
154
159
155 @LoginRequired()
160 @LoginRequired()
156 @HasRepoGroupPermissionAnyDecorator(
161 @HasRepoGroupPermissionAnyDecorator(
157 'group.read', 'group.write', 'group.admin')
162 'group.read', 'group.write', 'group.admin')
158 @view_config(
163 @view_config(
159 route_name='search_repo_group', request_method='GET',
164 route_name='search_repo_group', request_method='GET',
160 renderer='rhodecode:templates/search/search.mako')
165 renderer='rhodecode:templates/search/search.mako')
161 def search_repo_group(self):
166 def search_repo_group(self):
162 c = self.load_default_context()
167 c = self.load_default_context()
163 perform_search(self.request, c, repo_group_name=self.db_repo_group_name)
168 perform_search(self.request, c, repo_group_name=self.db_repo_group_name)
164 return self._get_template_context(c)
169 return self._get_template_context(c)
@@ -1,110 +1,143 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2019 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Index schema for RhodeCode
22 Index schema for RhodeCode
23 """
23 """
24
24
25 import importlib
25 import importlib
26 import logging
26 import logging
27
27
28 from rhodecode.lib.index.search_utils import normalize_text_for_matching
28 from rhodecode.lib.index.search_utils import normalize_text_for_matching
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32 # leave defaults for backward compat
32 # leave defaults for backward compat
33 default_searcher = 'rhodecode.lib.index.whoosh'
33 default_searcher = 'rhodecode.lib.index.whoosh'
34 default_location = '%(here)s/data/index'
34 default_location = '%(here)s/data/index'
35
35
36 ES_VERSION_2 = '2'
36 ES_VERSION_2 = '2'
37 ES_VERSION_6 = '6'
37 ES_VERSION_6 = '6'
38 # for legacy reasons we keep 2 compat as default
38 # for legacy reasons we keep 2 compat as default
39 DEFAULT_ES_VERSION = ES_VERSION_2
39 DEFAULT_ES_VERSION = ES_VERSION_2
40
40
41 from rhodecode_tools.lib.fts_index.elasticsearch_engine_6 import \
41 from rhodecode_tools.lib.fts_index.elasticsearch_engine_6 import \
42 ES_CONFIG # pragma: no cover
42 ES_CONFIG # pragma: no cover
43
43
44
44
45 class BaseSearcher(object):
45 class BaseSearcher(object):
46 query_lang_doc = ''
46 query_lang_doc = ''
47 es_version = None
47 es_version = None
48 name = None
48 name = None
49 DIRECTION_ASC = 'asc'
50 DIRECTION_DESC = 'desc'
49
51
50 def __init__(self):
52 def __init__(self):
51 pass
53 pass
52
54
53 def cleanup(self):
55 def cleanup(self):
54 pass
56 pass
55
57
56 def search(self, query, document_type, search_user,
58 def search(self, query, document_type, search_user,
57 repo_name=None, repo_group_name=None,
59 repo_name=None, repo_group_name=None,
58 raise_on_exc=True):
60 raise_on_exc=True):
59 raise Exception('NotImplemented')
61 raise Exception('NotImplemented')
60
62
61 @staticmethod
63 @staticmethod
62 def query_to_mark(query, default_field=None):
64 def query_to_mark(query, default_field=None):
63 """
65 """
64 Formats the query to mark token for jquery.mark.js highlighting. ES could
66 Formats the query to mark token for jquery.mark.js highlighting. ES could
65 have a different format optionally.
67 have a different format optionally.
66
68
67 :param default_field:
69 :param default_field:
68 :param query:
70 :param query:
69 """
71 """
70 return ' '.join(normalize_text_for_matching(query).split())
72 return ' '.join(normalize_text_for_matching(query).split())
71
73
72 @property
74 @property
73 def is_es_6(self):
75 def is_es_6(self):
74 return self.es_version == ES_VERSION_6
76 return self.es_version == ES_VERSION_6
75
77
76 def get_handlers(self):
78 def get_handlers(self):
77 return {}
79 return {}
78
80
79 @staticmethod
81 @staticmethod
80 def extract_search_tags(query):
82 def extract_search_tags(query):
81 return []
83 return []
82
84
83 @staticmethod
85 @staticmethod
84 def escape_specials(val):
86 def escape_specials(val):
85 """
87 """
86 Handle and escape reserved chars for search
88 Handle and escape reserved chars for search
87 """
89 """
88 return val
90 return val
89
91
92 @staticmethod
93 def get_sort(search_type, search_val):
94 """
95 Method used to parse the GET search sort value to a field and direction.
96 e.g asc:lines == asc, lines
97
98 There's also a legacy support for newfirst/oldfirst which defines commit
99 sorting only
100 """
101
102 direction = BaseSearcher.DIRECTION_ASC
103 sort_field = None
104
105 if not search_val:
106 return direction, sort_field
107
108 if search_val.startswith('asc:'):
109 sort_field = search_val[4:]
110 direction = BaseSearcher.DIRECTION_ASC
111 elif search_val.startswith('desc:'):
112 sort_field = search_val[5:]
113 direction = BaseSearcher.DIRECTION_DESC
114 elif search_val == 'newfirst' and search_type == 'commit':
115 sort_field = 'date'
116 direction = BaseSearcher.DIRECTION_DESC
117 elif search_val == 'oldfirst' and search_type == 'commit':
118 sort_field = 'date'
119 direction = BaseSearcher.DIRECTION_ASC
120
121 return direction, sort_field
122
90
123
91 def search_config(config, prefix='search.'):
124 def search_config(config, prefix='search.'):
92 _config = {}
125 _config = {}
93 for key in config.keys():
126 for key in config.keys():
94 if key.startswith(prefix):
127 if key.startswith(prefix):
95 _config[key[len(prefix):]] = config[key]
128 _config[key[len(prefix):]] = config[key]
96 return _config
129 return _config
97
130
98
131
99 def searcher_from_config(config, prefix='search.'):
132 def searcher_from_config(config, prefix='search.'):
100 _config = search_config(config, prefix)
133 _config = search_config(config, prefix)
101
134
102 if 'location' not in _config:
135 if 'location' not in _config:
103 _config['location'] = default_location
136 _config['location'] = default_location
104 if 'es_version' not in _config:
137 if 'es_version' not in _config:
105 # use old legacy ES version set to 2
138 # use old legacy ES version set to 2
106 _config['es_version'] = '2'
139 _config['es_version'] = '2'
107
140
108 imported = importlib.import_module(_config.get('module', default_searcher))
141 imported = importlib.import_module(_config.get('module', default_searcher))
109 searcher = imported.Searcher(config=_config)
142 searcher = imported.Searcher(config=_config)
110 return searcher
143 return searcher
@@ -1,46 +1,58 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import colander
22 import colander
23
23
24
24
25 def sort_validator(node, value):
26 if value in ['oldfirst', 'newfirst']:
27 return value
28 if value.startswith('asc:'):
29 return value
30 if value.startswith('desc:'):
31 return value
32
33 msg = u'Invalid search sort, must be `oldfirst`, `newfirst`, or start with asc: or desc:'
34 raise colander.Invalid(node, msg)
35
36
25 class SearchParamsSchema(colander.MappingSchema):
37 class SearchParamsSchema(colander.MappingSchema):
26 search_query = colander.SchemaNode(
38 search_query = colander.SchemaNode(
27 colander.String(),
39 colander.String(),
28 missing='')
40 missing='')
29 search_type = colander.SchemaNode(
41 search_type = colander.SchemaNode(
30 colander.String(),
42 colander.String(),
31 missing='content',
43 missing='content',
32 validator=colander.OneOf(['content', 'path', 'commit', 'repository']))
44 validator=colander.OneOf(['content', 'path', 'commit', 'repository']))
33 search_sort = colander.SchemaNode(
45 search_sort = colander.SchemaNode(
34 colander.String(),
46 colander.String(),
35 missing='newfirst',
47 missing='newfirst',
36 validator=colander.OneOf(['oldfirst', 'newfirst']))
48 validator=sort_validator)
37 search_max_lines = colander.SchemaNode(
49 search_max_lines = colander.SchemaNode(
38 colander.Integer(),
50 colander.Integer(),
39 missing=10)
51 missing=10)
40 page_limit = colander.SchemaNode(
52 page_limit = colander.SchemaNode(
41 colander.Integer(),
53 colander.Integer(),
42 missing=10,
54 missing=10,
43 validator=colander.Range(1, 500))
55 validator=colander.Range(1, 500))
44 requested_page = colander.SchemaNode(
56 requested_page = colander.SchemaNode(
45 colander.Integer(),
57 colander.Integer(),
46 missing=1)
58 missing=1)
@@ -1,217 +1,241 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.mako"/>
2 <%inherit file="/base/base.mako"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 %if c.repo_name:
5 %if c.repo_name:
6 ${_('Search inside repository {repo_name}').format(repo_name=c.repo_name)}
6 ${_('Search inside repository {repo_name}').format(repo_name=c.repo_name)}
7 %elif c.repo_group_name:
7 %elif c.repo_group_name:
8 ${_('Search inside repository group {repo_group_name}').format(repo_group_name=c.repo_group_name)}
8 ${_('Search inside repository group {repo_group_name}').format(repo_group_name=c.repo_group_name)}
9 %else:
9 %else:
10 ${_('Search inside all accessible repositories')}
10 ${_('Search inside all accessible repositories')}
11 %endif
11 %endif
12 %if c.rhodecode_name:
12 %if c.rhodecode_name:
13 &middot; ${h.branding(c.rhodecode_name)}
13 &middot; ${h.branding(c.rhodecode_name)}
14 %endif
14 %endif
15 </%def>
15 </%def>
16
16
17 <%def name="breadcrumbs_links()">
17 <%def name="breadcrumbs_links()">
18 %if c.repo_name:
18 %if c.repo_name:
19 ${_('Search inside repository {repo_name}').format(repo_name=c.repo_name)}
19 ${_('Search inside repository {repo_name}').format(repo_name=c.repo_name)}
20 %elif c.repo_group_name:
20 %elif c.repo_group_name:
21 ${_('Search inside repository group {repo_group_name}').format(repo_group_name=c.repo_group_name)}
21 ${_('Search inside repository group {repo_group_name}').format(repo_group_name=c.repo_group_name)}
22 %else:
22 %else:
23 ${_('Search inside all accessible repositories')}
23 ${_('Search inside all accessible repositories')}
24 %endif
24 %endif
25
25
26 </%def>
26 </%def>
27
27
28 <%def name="menu_bar_nav()">
28 <%def name="menu_bar_nav()">
29 %if c.repo_name:
29 %if c.repo_name:
30 ${self.menu_items(active='search')}
30 ${self.menu_items(active='search')}
31 %elif c.repo_group_name:
31 %elif c.repo_group_name:
32 ${self.menu_items(active='search')}
32 ${self.menu_items(active='search')}
33 %else:
33 %else:
34 ${self.menu_items(active='search')}
34 ${self.menu_items(active='search')}
35 %endif
35 %endif
36 </%def>
36 </%def>
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 <% active_entry = {'content':'files', 'path':'files', 'commit':'commits'}.get(c.search_type, 'summary')%>
40 <% active_entry = {'content':'files', 'path':'files', 'commit':'commits'}.get(c.search_type, 'summary')%>
41 ${self.repo_menu(active=active_entry)}
41 ${self.repo_menu(active=active_entry)}
42 %elif c.repo_group_name:
42 %elif c.repo_group_name:
43 ${self.repo_group_menu(active='home')}
43 ${self.repo_group_menu(active='home')}
44 %endif
44 %endif
45 </%def>
45 </%def>
46
46
47 <%def name="repo_icon(db_repo)">
47 <%def name="repo_icon(db_repo)">
48 %if h.is_hg(db_repo):
48 %if h.is_hg(db_repo):
49 <i class="icon-hg"></i>
49 <i class="icon-hg"></i>
50 %endif
50 %endif
51 %if h.is_git(db_repo):
51 %if h.is_git(db_repo):
52 <i class="icon-git"></i>
52 <i class="icon-git"></i>
53 %endif
53 %endif
54 %if h.is_svn(db_repo):
54 %if h.is_svn(db_repo):
55 <i class="icon-svn"></i>
55 <i class="icon-svn"></i>
56 %endif
56 %endif
57 </%def>
57 </%def>
58
58
59 <%def name="repo_group_icon()">
59 <%def name="repo_group_icon()">
60 <i class="icon-repo-group"></i>
60 <i class="icon-repo-group"></i>
61 </%def>
61 </%def>
62
62
63
64 <%def name="field_sort(field_name)">
65
66 <%
67 if c.sort.startswith('asc:'):
68 return c.url_generator(sort='desc:{}'.format(field_name))
69 elif c.sort.startswith('desc:'):
70 return c.url_generator(sort='asc:{}'.format(field_name))
71
72 return 'asc:{}'.format(field_name)
73 %>
74 </%def>
75
76
63 <%def name="main()">
77 <%def name="main()">
64 <div class="box">
78 <div class="box">
65 %if c.repo_name:
79 %if c.repo_name:
66 <!-- box / title -->
80 <!-- box / title -->
67 ${h.form(h.route_path('search_repo',repo_name=c.repo_name),method='get')}
81 ${h.form(h.route_path('search_repo',repo_name=c.repo_name),method='get')}
68 %elif c.repo_group_name:
82 %elif c.repo_group_name:
69 <!-- box / title -->
83 <!-- box / title -->
70 ${h.form(h.route_path('search_repo_group',repo_group_name=c.repo_group_name),method='get')}
84 ${h.form(h.route_path('search_repo_group',repo_group_name=c.repo_group_name),method='get')}
71 %else:
85 %else:
72 <!-- box / title -->
86 <!-- box / title -->
73 <div class="title">
87 <div class="title">
74 ${self.breadcrumbs()}
88 ${self.breadcrumbs()}
75 <ul class="links">&nbsp;</ul>
89 <ul class="links">&nbsp;</ul>
76 </div>
90 </div>
77 <!-- end box / title -->
91 <!-- end box / title -->
78 ${h.form(h.route_path('search'), method='get')}
92 ${h.form(h.route_path('search'), method='get')}
79 %endif
93 %endif
80 <div class="form search-form">
94 <div class="form search-form">
81 <div class="fields">
95 <div class="fields">
82
96
83 ${h.text('q', c.cur_query, placeholder="Enter query...")}
97 ${h.text('q', c.cur_query, placeholder="Enter query...")}
84
98
85 ${h.select('type',c.search_type,[('content',_('Files')), ('path',_('File path')),('commit',_('Commits'))],id='id_search_type')}
99 ${h.select('type',c.search_type,[('content',_('Files')), ('path',_('File path')),('commit',_('Commits'))],id='id_search_type')}
86 ${h.hidden('max_lines', '10')}
100 ${h.hidden('max_lines', '10')}
87
101
88 <input type="submit" value="${_('Search')}" class="btn"/>
102 <input type="submit" value="${_('Search')}" class="btn"/>
89 <br/>
103 <br/>
90
104
91 <div class="search-tags">
105 <div class="search-tags">
92 <span class="tag tag8">
106 <span class="tag tag8">
93 %if c.repo_name:
107 %if c.repo_name:
94 <a href="${h.route_path('search', _query={'q': c.cur_query, 'type': request.GET.get('type', 'content')})}">${_('Global Search')}</a>
108 <a href="${h.route_path('search', _query={'q': c.cur_query, 'type': request.GET.get('type', 'content')})}">${_('Global Search')}</a>
95 %elif c.repo_group_name:
109 %elif c.repo_group_name:
96 <a href="${h.route_path('search', _query={'q': c.cur_query, 'type': request.GET.get('type', 'content')})}">${_('Global Search')}</a>
110 <a href="${h.route_path('search', _query={'q': c.cur_query, 'type': request.GET.get('type', 'content')})}">${_('Global Search')}</a>
97 % else:
111 % else:
98 ${_('Global Search')}
112 ${_('Global Search')}
99 %endif
113 %endif
100 </span>
114 </span>
101
115
102 %if c.repo_name:
116 %if c.repo_name:
103 Β»
117 Β»
104 <span class="tag tag8">
118 <span class="tag tag8">
105 ${repo_icon(c.rhodecode_db_repo)}
119 ${repo_icon(c.rhodecode_db_repo)}
106 ${c.repo_name}
120 ${c.repo_name}
107 </span>
121 </span>
108
122
109 %elif c.repo_group_name:
123 %elif c.repo_group_name:
110 Β»
124 Β»
111 <span class="tag tag8">
125 <span class="tag tag8">
112 ${repo_group_icon()}
126 ${repo_group_icon()}
113 ${c.repo_group_name}
127 ${c.repo_group_name}
114 </span>
128 </span>
115 %endif
129 %endif
116
130
131 % if c.sort_tag:
132 <span class="tag tag8">
133 % if c.sort_tag_dir == 'asc':
134 <i class="icon-angle-down"></i>
135 % elif c.sort_tag_dir == 'desc':
136 <i class="icon-angle-up"></i>
137 % endif
138 ${_('sort')}:${c.sort_tag}
139 </span>
140 % endif
117
141
118 % for search_tag in c.search_tags:
142 % for search_tag in c.search_tags:
119 <br/><span class="tag disabled" style="margin-top: 3px">${search_tag}</span>
143 <br/><span class="tag disabled" style="margin-top: 3px">${search_tag}</span>
120 % endfor
144 % endfor
121
145
122 </div>
146 </div>
123
147
124 <div class="search-feedback-items">
148 <div class="search-feedback-items">
125 % for error in c.errors:
149 % for error in c.errors:
126 <span class="error-message">
150 <span class="error-message">
127 % for k,v in error.asdict().items():
151 % for k,v in error.asdict().items():
128 ${k} - ${v}
152 ${k} - ${v}
129 % endfor
153 % endfor
130 </span>
154 </span>
131 % endfor
155 % endfor
132 <div class="field">
156 <div class="field">
133 <p class="filterexample" style="position: inherit" onclick="$('#search-help').toggle()">${_('Query Langague examples')}</p>
157 <p class="filterexample" style="position: inherit" onclick="$('#search-help').toggle()">${_('Query Language examples')}</p>
134 <pre id="search-help" style="display: none">\
158 <pre id="search-help" style="display: none">\
135
159
136 % if c.searcher.name == 'whoosh':
160 % if c.searcher.name == 'whoosh':
137 Example filter terms for `Whoosh` search:
161 Example filter terms for `Whoosh` search:
138 query lang: <a href="${c.searcher.query_lang_doc}">Whoosh Query Language</a>
162 query lang: <a href="${c.searcher.query_lang_doc}">Whoosh Query Language</a>
139 Whoosh has limited query capabilities. For advanced search use ElasticSearch 6 from RhodeCode EE edition.
163 Whoosh has limited query capabilities. For advanced search use ElasticSearch 6 from RhodeCode EE edition.
140
164
141 Generate wildcards using '*' character:
165 Generate wildcards using '*' character:
142 "repo_name:vcs*" - search everything starting with 'vcs'
166 "repo_name:vcs*" - search everything starting with 'vcs'
143 "repo_name:*vcs*" - search for repository containing 'vcs'
167 "repo_name:*vcs*" - search for repository containing 'vcs'
144
168
145 Optional AND / OR operators in queries
169 Optional AND / OR operators in queries
146 "repo_name:vcs OR repo_name:test"
170 "repo_name:vcs OR repo_name:test"
147 "owner:test AND repo_name:test*" AND extension:py
171 "owner:test AND repo_name:test*" AND extension:py
148
172
149 Move advanced search is available via ElasticSearch6 backend in EE edition.
173 Move advanced search is available via ElasticSearch6 backend in EE edition.
150 % elif c.searcher.name == 'elasticsearch' and c.searcher.es_version == '2':
174 % elif c.searcher.name == 'elasticsearch' and c.searcher.es_version == '2':
151 Example filter terms for `ElasticSearch-${c.searcher.es_version}`search:
175 Example filter terms for `ElasticSearch-${c.searcher.es_version}`search:
152 ElasticSearch-2 has limited query capabilities. For advanced search use ElasticSearch 6 from RhodeCode EE edition.
176 ElasticSearch-2 has limited query capabilities. For advanced search use ElasticSearch 6 from RhodeCode EE edition.
153
177
154 search type: content (File Content)
178 search type: content (File Content)
155 indexed fields: content
179 indexed fields: content
156
180
157 # search for `fix` string in all files
181 # search for `fix` string in all files
158 fix
182 fix
159
183
160 search type: commit (Commit message)
184 search type: commit (Commit message)
161 indexed fields: message
185 indexed fields: message
162
186
163 search type: path (File name)
187 search type: path (File name)
164 indexed fields: path
188 indexed fields: path
165
189
166 % else:
190 % else:
167 Example filter terms for `ElasticSearch-${c.searcher.es_version}`search:
191 Example filter terms for `ElasticSearch-${c.searcher.es_version}`search:
168 query lang: <a href="${c.searcher.query_lang_doc}">ES 6 Query Language</a>
192 query lang: <a href="${c.searcher.query_lang_doc}">ES 6 Query Language</a>
169 The reserved characters needed espace by `\`: + - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /
193 The reserved characters needed espace by `\`: + - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /
170 % for handler in c.searcher.get_handlers().values():
194 % for handler in c.searcher.get_handlers().values():
171
195
172 search type: ${handler.search_type_label}
196 search type: ${handler.search_type_label}
173 *indexed fields*: ${', '.join( [('\n ' if x[0]%4==0 else '')+x[1] for x in enumerate(handler.es_6_field_names)])}
197 *indexed fields*: ${', '.join( [('\n ' if x[0]%4==0 else '')+x[1] for x in enumerate(handler.es_6_field_names)])}
174 % for entry in handler.es_6_example_queries:
198 % for entry in handler.es_6_example_queries:
175 ${entry.rstrip()}
199 ${entry.rstrip()}
176 % endfor
200 % endfor
177 % endfor
201 % endfor
178
202
179 % endif
203 % endif
180 </pre>
204 </pre>
181 </div>
205 </div>
182
206
183 <div class="field">${c.runtime}</div>
207 <div class="field">${c.runtime}</div>
184 </div>
208 </div>
185 </div>
209 </div>
186 </div>
210 </div>
187
211
188 ${h.end_form()}
212 ${h.end_form()}
189 <div class="search">
213 <div class="search">
190 % if c.search_type == 'content':
214 % if c.search_type == 'content':
191 <%include file='search_content.mako'/>
215 <%include file='search_content.mako'/>
192 % elif c.search_type == 'path':
216 % elif c.search_type == 'path':
193 <%include file='search_path.mako'/>
217 <%include file='search_path.mako'/>
194 % elif c.search_type == 'commit':
218 % elif c.search_type == 'commit':
195 <%include file='search_commit.mako'/>
219 <%include file='search_commit.mako'/>
196 % elif c.search_type == 'repository':
220 % elif c.search_type == 'repository':
197 <%include file='search_repository.mako'/>
221 <%include file='search_repository.mako'/>
198 % endif
222 % endif
199 </div>
223 </div>
200 </div>
224 </div>
201 <script>
225 <script>
202 $(document).ready(function(){
226 $(document).ready(function(){
203 $("#id_search_type").select2({
227 $("#id_search_type").select2({
204 'containerCssClass': "drop-menu",
228 'containerCssClass': "drop-menu",
205 'dropdownCssClass': "drop-menu-dropdown",
229 'dropdownCssClass': "drop-menu-dropdown",
206 'dropdownAutoWidth': true,
230 'dropdownAutoWidth': true,
207 'minimumResultsForSearch': -1
231 'minimumResultsForSearch': -1
208 });
232 });
209
233
210 $('#q').autoGrowInput({maxWidth: 920});
234 $('#q').autoGrowInput({maxWidth: 920});
211
235
212 setTimeout(function() {
236 setTimeout(function() {
213 $('#q').keyup()
237 $('#q').keyup()
214 }, 1);
238 }, 1);
215 })
239 })
216 </script>
240 </script>
217 </%def>
241 </%def>
@@ -1,98 +1,98 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="search" file="/search/search.mako"/>
2 <%namespace name="search" file="/search/search.mako"/>
3
3
4 % if c.formatted_results:
4 % if c.formatted_results:
5
5
6 <table class="rctable search-results">
6 <table class="rctable search-results">
7 <tr>
7 <tr>
8 <th>${_('Repository')}</th>
8 <th>${_('Repository')}</th>
9 <th>${_('Commit')}</th>
9 <th>${_('Commit')}</th>
10 <th></th>
10 <th></th>
11 <th>${_('Commit message')}</th>
11 <th>
12 <a href="${search.field_sort('message.raw')}">${_('Commit message')}</a>
13 </th>
12 <th>
14 <th>
13 %if c.sort == 'newfirst':
15 <a href="${search.field_sort('date')}">${_('Age')}</a>
14 <a href="${c.url_generator(sort='oldfirst')}">${_('Age (new first)')}</a>
15 %else:
16 <a href="${c.url_generator(sort='newfirst')}">${_('Age (old first)')}</a>
17 %endif
18 </th>
16 </th>
19 <th>${_('Author')}</th>
17 <th>
18 <a href="${search.field_sort('author.email.raw')}">${_('Author')}</a>
19 </th>
20 </tr>
20 </tr>
21 %for entry in c.formatted_results:
21 %for entry in c.formatted_results:
22 ## search results are additionally filtered, and this check is just a safe gate
22 ## search results are additionally filtered, and this check is just a safe gate
23 % if c.rhodecode_user.is_admin or h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results commit check'):
23 % if c.rhodecode_user.is_admin or h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results commit check'):
24 <tr class="body">
24 <tr class="body">
25 <td class="td-componentname">
25 <td class="td-componentname">
26 <% repo_type = entry.get('repo_type') or h.get_repo_type_by_name(entry.get('repository')) %>
26 <% repo_type = entry.get('repo_type') or h.get_repo_type_by_name(entry.get('repository')) %>
27 ${search.repo_icon(repo_type)}
27 ${search.repo_icon(repo_type)}
28 ${h.link_to(entry['repository'], h.route_path('repo_summary',repo_name=entry['repository']))}
28 ${h.link_to(entry['repository'], h.route_path('repo_summary',repo_name=entry['repository']))}
29 </td>
29 </td>
30 <td class="td-hash">
30 <td class="td-hash">
31 ${h.link_to(h._shorten_commit_id(entry['commit_id']),
31 ${h.link_to(h._shorten_commit_id(entry['commit_id']),
32 h.route_path('repo_commit',repo_name=entry['repository'],commit_id=entry['commit_id']))}
32 h.route_path('repo_commit',repo_name=entry['repository'],commit_id=entry['commit_id']))}
33 </td>
33 </td>
34 <td class="td-message expand_commit search open" data-commit-id="${h.md5_safe(entry['repository'])+entry['commit_id']}" id="t-${h.md5_safe(entry['repository'])+entry['commit_id']}" title="${_('Expand commit message')}">
34 <td class="td-message expand_commit search open" data-commit-id="${h.md5_safe(entry['repository'])+entry['commit_id']}" id="t-${h.md5_safe(entry['repository'])+entry['commit_id']}" title="${_('Expand commit message')}">
35 <div>
35 <div>
36 <i class="icon-expand-linked"></i>&nbsp;
36 <i class="icon-expand-linked"></i>&nbsp;
37 </div>
37 </div>
38 </td>
38 </td>
39 <td data-commit-id="${h.md5_safe(entry['repository'])+entry['commit_id']}" id="c-${h.md5_safe(entry['repository'])+entry['commit_id']}" class="message td-description open">
39 <td data-commit-id="${h.md5_safe(entry['repository'])+entry['commit_id']}" id="c-${h.md5_safe(entry['repository'])+entry['commit_id']}" class="message td-description open">
40 %if entry.get('message_hl'):
40 %if entry.get('message_hl'):
41 ${h.literal(entry['message_hl'])}
41 ${h.literal(entry['message_hl'])}
42 %else:
42 %else:
43 ${h.urlify_commit_message(entry['message'], entry['repository'])}
43 ${h.urlify_commit_message(entry['message'], entry['repository'])}
44 %endif
44 %endif
45 </td>
45 </td>
46 <td class="td-time">
46 <td class="td-time">
47 ${h.age_component(h.time_to_utcdatetime(entry['date']))}
47 ${h.age_component(h.time_to_utcdatetime(entry['date']))}
48 </td>
48 </td>
49
49
50 <td class="td-user author">
50 <td class="td-user author">
51 <%
51 <%
52 ## es6 stores this as object
52 ## es6 stores this as object
53 author = entry['author']
53 author = entry['author']
54 if isinstance(author, dict):
54 if isinstance(author, dict):
55 author = author['email']
55 author = author['email']
56 %>
56 %>
57 ${base.gravatar_with_user(author)}
57 ${base.gravatar_with_user(author)}
58 </td>
58 </td>
59 </tr>
59 </tr>
60 % endif
60 % endif
61 %endfor
61 %endfor
62 </table>
62 </table>
63
63
64 %if c.cur_query:
64 %if c.cur_query:
65 <div class="pagination-wh pagination-left">
65 <div class="pagination-wh pagination-left">
66 ${c.formatted_results.pager('$link_previous ~2~ $link_next')}
66 ${c.formatted_results.pager('$link_previous ~2~ $link_next')}
67 </div>
67 </div>
68 %endif
68 %endif
69
69
70 <script>
70 <script>
71 $('.expand_commit').on('click',function(e){
71 $('.expand_commit').on('click',function(e){
72 var target_expand = $(this);
72 var target_expand = $(this);
73 var cid = target_expand.data('commit-id');
73 var cid = target_expand.data('commit-id');
74
74
75 if (target_expand.hasClass('open')){
75 if (target_expand.hasClass('open')){
76 $('#c-'+cid).css({'height': '1.5em', 'white-space': 'nowrap', 'text-overflow': 'ellipsis', 'overflow':'hidden'});
76 $('#c-'+cid).css({'height': '1.5em', 'white-space': 'nowrap', 'text-overflow': 'ellipsis', 'overflow':'hidden'});
77 $('#t-'+cid).css({'height': 'auto', 'line-height': '.9em', 'text-overflow': 'ellipsis', 'overflow':'hidden'});
77 $('#t-'+cid).css({'height': 'auto', 'line-height': '.9em', 'text-overflow': 'ellipsis', 'overflow':'hidden'});
78 target_expand.removeClass('open');
78 target_expand.removeClass('open');
79 }
79 }
80 else {
80 else {
81 $('#c-'+cid).css({'height': 'auto', 'white-space': 'normal', 'text-overflow': 'initial', 'overflow':'visible'});
81 $('#c-'+cid).css({'height': 'auto', 'white-space': 'normal', 'text-overflow': 'initial', 'overflow':'visible'});
82 $('#t-'+cid).css({'height': 'auto', 'max-height': 'none', 'text-overflow': 'initial', 'overflow':'visible'});
82 $('#t-'+cid).css({'height': 'auto', 'max-height': 'none', 'text-overflow': 'initial', 'overflow':'visible'});
83 target_expand.addClass('open');
83 target_expand.addClass('open');
84 }
84 }
85 });
85 });
86
86
87 $(".message.td-description").mark(
87 $(".message.td-description").mark(
88 "${c.searcher.query_to_mark(c.cur_query, 'message')}",
88 "${c.searcher.query_to_mark(c.cur_query, 'message')}",
89 {
89 {
90 "className": 'match',
90 "className": 'match',
91 "accuracy": "complementary",
91 "accuracy": "complementary",
92 "ignorePunctuation": ":._(){}[]!'+=".split("")
92 "ignorePunctuation": ":._(){}[]!'+=".split("")
93 }
93 }
94 );
94 );
95
95
96 </script>
96 </script>
97
97
98 % endif
98 % endif
@@ -1,46 +1,53 b''
1 <%namespace name="search" file="/search/search.mako"/>
1 <%namespace name="search" file="/search/search.mako"/>
2
2
3 % if c.formatted_results:
3 % if c.formatted_results:
4
4
5 <table class="rctable search-results">
5 <table class="rctable search-results">
6 <tr>
6 <tr>
7 <th>${_('Repository')}</th>
7 <th>${_('Repository')}</th>
8 <th>${_('File')}</th>
8 <th>
9 <th>${_('Size')}</th>
9 <a href="${search.field_sort('file.raw')}">${_('File')}</a>
10 <th>${_('Lines')}</th>
10 </th>
11 <th>
12 <a href="${search.field_sort('size')}">${_('Size')}</a>
13 </th>
14 <th>
15 <a href="${search.field_sort('lines')}">${_('Lines')}</a>
16 </th>
11 </tr>
17 </tr>
12 %for entry in c.formatted_results:
18 %for entry in c.formatted_results:
13 ## search results are additionally filtered, and this check is just a safe gate
19 ## search results are additionally filtered, and this check is just a safe gate
14 % if c.rhodecode_user.is_admin or h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results path check'):
20 % if c.rhodecode_user.is_admin or h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(entry['repository'], 'search results path check'):
15 <tr class="body">
21 <tr class="body">
16 <td class="td-componentname">
22 <td class="td-componentname">
17 <% repo_type = entry.get('repo_type') or h.get_repo_type_by_name(entry.get('repository')) %>
23 <% repo_type = entry.get('repo_type') or h.get_repo_type_by_name(entry.get('repository')) %>
18 ${search.repo_icon(repo_type)}
24 ${search.repo_icon(repo_type)}
19 ${h.link_to(entry['repository'], h.route_path('repo_summary',repo_name=entry['repository']))}
25 ${h.link_to(entry['repository'], h.route_path('repo_summary',repo_name=entry['repository']))}
20 </td>
26 </td>
21 <td class="td-componentname">
27 <td class="td-componentname">
28 <i class="icon-file"></i>
22 ${h.link_to(h.literal(entry['f_path']),
29 ${h.link_to(h.literal(entry['f_path']),
23 h.route_path('repo_files',repo_name=entry['repository'],commit_id='tip',f_path=entry['f_path']))}
30 h.route_path('repo_files',repo_name=entry['repository'],commit_id='tip',f_path=entry['f_path']))}
24 </td>
31 </td>
25 <td>
32 <td>
26 %if entry.get('size'):
33 %if entry.get('size'):
27 ${h.format_byte_size_binary(entry['size'])}
34 ${h.format_byte_size_binary(entry['size'])}
28 %endif
35 %endif
29 </td>
36 </td>
30 <td>
37 <td>
31 %if entry.get('lines'):
38 %if entry.get('lines'):
32 ${entry.get('lines', 0.)} ${_ungettext('line', 'lines', entry.get('lines', 0.))}
39 ${entry.get('lines', 0.)}
33 %endif
40 %endif
34 </td>
41 </td>
35 </tr>
42 </tr>
36 % endif
43 % endif
37 %endfor
44 %endfor
38 </table>
45 </table>
39
46
40 %if c.cur_query:
47 %if c.cur_query:
41 <div class="pagination-wh pagination-left">
48 <div class="pagination-wh pagination-left">
42 ${c.formatted_results.pager('$link_previous ~2~ $link_next')}
49 ${c.formatted_results.pager('$link_previous ~2~ $link_next')}
43 </div>
50 </div>
44 %endif
51 %endif
45
52
46 % endif
53 % endif
General Comments 0
You need to be logged in to leave comments. Login now