##// END OF EJS Templates
search: added per-backend sorting fields....
dan -
r3968:2ec277c2 default
parent child Browse files
Show More
@@ -1,93 +1,115 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-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 pytest
21 import pytest
22 from rhodecode.tests import HG_REPO
22 from rhodecode.tests import HG_REPO
23 from rhodecode.api.tests.utils import (
23 from rhodecode.api.tests.utils import (
24 build_data, api_call, assert_error, assert_ok)
24 build_data, api_call, assert_error, assert_ok)
25
25
26
26
27 @pytest.mark.usefixtures("testuser_api", "app")
27 @pytest.mark.usefixtures("testuser_api", "app")
28 class TestApiSearch(object):
28 class TestApiSearch(object):
29
29
30 @pytest.mark.parametrize("sort_dir", [
31 "asc",
32 "desc",
33 ])
34 @pytest.mark.parametrize("sort", [
35 "xxx",
36 "author_email",
37 "date",
38 "message",
39 ])
30 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
40 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
31 ('todo', 23, [
41 ('todo', 23, [
32 'vcs/backends/hg/inmemory.py',
42 'vcs/backends/hg/inmemory.py',
33 'vcs/tests/test_git.py']),
43 'vcs/tests/test_git.py']),
34 ('extension:rst installation', 6, [
44 ('extension:rst installation', 6, [
35 'docs/index.rst',
45 'docs/index.rst',
36 'docs/installation.rst']),
46 'docs/installation.rst']),
37 ('def repo', 87, [
47 ('def repo', 87, [
38 'vcs/tests/test_git.py',
48 'vcs/tests/test_git.py',
39 'vcs/tests/test_changesets.py']),
49 'vcs/tests/test_changesets.py']),
40 ('repository:%s def test' % HG_REPO, 18, [
50 ('repository:%s def test' % HG_REPO, 18, [
41 'vcs/tests/test_git.py',
51 'vcs/tests/test_git.py',
42 'vcs/tests/test_changesets.py']),
52 'vcs/tests/test_changesets.py']),
43 ('"def main"', 9, [
53 ('"def main"', 9, [
44 'vcs/__init__.py',
54 'vcs/__init__.py',
45 'vcs/tests/__init__.py',
55 'vcs/tests/__init__.py',
46 'vcs/utils/progressbar.py']),
56 'vcs/utils/progressbar.py']),
47 ('owner:test_admin', 358, [
57 ('owner:test_admin', 358, [
48 'vcs/tests/base.py',
58 'vcs/tests/base.py',
49 'MANIFEST.in',
59 'MANIFEST.in',
50 'vcs/utils/termcolors.py',
60 'vcs/utils/termcolors.py',
51 'docs/theme/ADC/static/documentation.png']),
61 'docs/theme/ADC/static/documentation.png']),
52 ('owner:test_admin def main', 72, [
62 ('owner:test_admin def main', 72, [
53 'vcs/__init__.py',
63 'vcs/__init__.py',
54 'vcs/tests/test_utils_filesize.py',
64 'vcs/tests/test_utils_filesize.py',
55 'vcs/tests/test_cli.py']),
65 'vcs/tests/test_cli.py']),
56 ('owner:michaΕ‚ test', 0, []),
66 ('owner:michaΕ‚ test', 0, []),
57 ])
67 ])
58 def test_search_content_results(self, query, expected_hits, expected_paths):
68 def test_search_content_results(self, sort_dir, sort, query, expected_hits, expected_paths):
59 id_, params = build_data(
69 id_, params = build_data(
60 self.apikey_regular, 'search',
70 self.apikey_regular, 'search',
61 search_query=query,
71 search_query=query,
72 search_sort='{}:{}'.format(sort_dir, sort),
62 search_type='content')
73 search_type='content')
63
74
64 response = api_call(self.app, params)
75 response = api_call(self.app, params)
65 json_response = response.json
76 json_response = response.json
66
77
67 assert json_response['result']['item_count'] == expected_hits
78 assert json_response['result']['item_count'] == expected_hits
68 paths = [x['f_path'] for x in json_response['result']['results']]
79 paths = [x['f_path'] for x in json_response['result']['results']]
69
80
70 for expected_path in expected_paths:
81 for expected_path in expected_paths:
71 assert expected_path in paths
82 assert expected_path in paths
72
83
84 @pytest.mark.parametrize("sort_dir", [
85 "asc",
86 "desc",
87 ])
88 @pytest.mark.parametrize("sort", [
89 "xxx",
90 "date",
91 "file",
92 "size",
93 ])
73 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
94 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
74 ('readme.rst', 3, []),
95 ('readme.rst', 3, []),
75 ('test*', 75, []),
96 ('test*', 75, []),
76 ('*model*', 1, []),
97 ('*model*', 1, []),
77 ('extension:rst', 48, []),
98 ('extension:rst', 48, []),
78 ('extension:rst api', 24, []),
99 ('extension:rst api', 24, []),
79 ])
100 ])
80 def test_search_file_paths(self, query, expected_hits, expected_paths):
101 def test_search_file_paths(self, sort_dir, sort, query, expected_hits, expected_paths):
81 id_, params = build_data(
102 id_, params = build_data(
82 self.apikey_regular, 'search',
103 self.apikey_regular, 'search',
83 search_query=query,
104 search_query=query,
105 search_sort='{}:{}'.format(sort_dir, sort),
84 search_type='path')
106 search_type='path')
85
107
86 response = api_call(self.app, params)
108 response = api_call(self.app, params)
87 json_response = response.json
109 json_response = response.json
88
110
89 assert json_response['result']['item_count'] == expected_hits
111 assert json_response['result']['item_count'] == expected_hits
90 paths = [x['f_path'] for x in json_response['result']['results']]
112 paths = [x['f_path'] for x in json_response['result']['results']]
91
113
92 for expected_path in expected_paths:
114 for expected_path in expected_paths:
93 assert expected_path in paths
115 assert expected_path in paths
@@ -1,169 +1,173 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
48 try:
48 try:
49 search_params = schema.deserialize(
49 search_params = schema.deserialize(
50 dict(
50 dict(
51 search_query=request.GET.get('q'),
51 search_query=request.GET.get('q'),
52 search_type=request.GET.get('type'),
52 search_type=request.GET.get('type'),
53 search_sort=request.GET.get('sort'),
53 search_sort=request.GET.get('sort'),
54 search_max_lines=request.GET.get('max_lines'),
54 search_max_lines=request.GET.get('max_lines'),
55 page_limit=request.GET.get('page_limit'),
55 page_limit=request.GET.get('page_limit'),
56 requested_page=request.GET.get('page'),
56 requested_page=request.GET.get('page'),
57 )
57 )
58 )
58 )
59 except validation_schema.Invalid as e:
59 except validation_schema.Invalid as e:
60 errors = e.children
60 errors = e.children
61
61
62 def url_generator(**kw):
62 def url_generator(**kw):
63 q = urllib.quote(safe_str(search_query))
63 q = urllib.quote(safe_str(search_query))
64 return update_params(
64 return update_params(
65 "?q=%s&type=%s&max_lines=%s&sort=%s" % (
65 "?q=%s&type=%s&max_lines=%s&sort=%s" % (
66 q, safe_str(search_type), search_max_lines, search_sort), **kw)
66 q, safe_str(search_type), search_max_lines, search_sort), **kw)
67
67
68 c = tmpl_context
68 c = tmpl_context
69 search_query = search_params.get('search_query')
69 search_query = search_params.get('search_query')
70 search_type = search_params.get('search_type')
70 search_type = search_params.get('search_type')
71 search_sort = search_params.get('search_sort')
71 search_sort = search_params.get('search_sort')
72 search_max_lines = search_params.get('search_max_lines')
72 search_max_lines = search_params.get('search_max_lines')
73 if search_params.get('search_query'):
73 if search_params.get('search_query'):
74 page_limit = search_params['page_limit']
74 page_limit = search_params['page_limit']
75 requested_page = search_params['requested_page']
75 requested_page = search_params['requested_page']
76
76
77 try:
77 try:
78 search_result = searcher.search(
78 search_result = searcher.search(
79 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,
80 requested_page=requested_page, page_limit=page_limit, sort=search_sort)
80 requested_page=requested_page, page_limit=page_limit, sort=search_sort)
81
81
82 formatted_results = Page(
82 formatted_results = Page(
83 search_result['results'], page=requested_page,
83 search_result['results'], page=requested_page,
84 item_count=search_result['count'],
84 item_count=search_result['count'],
85 items_per_page=page_limit, url=url_generator)
85 items_per_page=page_limit, url=url_generator)
86 finally:
86 finally:
87 searcher.cleanup()
87 searcher.cleanup()
88
88
89 search_tags = searcher.extract_search_tags(search_query)
89 search_tags = searcher.extract_search_tags(search_query)
90
90
91 if not search_result['error']:
91 if not search_result['error']:
92 execution_time = '%s results (%.4f seconds)' % (
92 execution_time = '%s results (%.4f seconds)' % (
93 search_result['count'],
93 search_result['count'],
94 search_result['runtime'])
94 search_result['runtime'])
95 elif not errors:
95 elif not errors:
96 node = schema['search_query']
96 node = schema['search_query']
97 errors = [
97 errors = [
98 validation_schema.Invalid(node, search_result['error'])]
98 validation_schema.Invalid(node, search_result['error'])]
99
99
100 c.perm_user = c.auth_user
100 c.perm_user = c.auth_user
101 c.repo_name = repo_name
101 c.repo_name = repo_name
102 c.repo_group_name = repo_group_name
102 c.repo_group_name = repo_group_name
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)
112 direction, sort_field = searcher.get_sort(search_type, search_sort)
113 c.sort = '{}:{}'.format(direction, sort_field)
113 sort_definition = searcher.sort_def(search_type, direction, sort_field)
114 c.sort_tag = sort_field
114 c.sort = ''
115 c.sort_tag = None
115 c.sort_tag_dir = direction
116 c.sort_tag_dir = direction
117 if sort_definition:
118 c.sort = '{}:{}'.format(direction, sort_field)
119 c.sort_tag = sort_field
116
120
117
121
118 class SearchView(BaseAppView):
122 class SearchView(BaseAppView):
119 def load_default_context(self):
123 def load_default_context(self):
120 c = self._get_local_tmpl_context()
124 c = self._get_local_tmpl_context()
121 return c
125 return c
122
126
123 @LoginRequired()
127 @LoginRequired()
124 @view_config(
128 @view_config(
125 route_name='search', request_method='GET',
129 route_name='search', request_method='GET',
126 renderer='rhodecode:templates/search/search.mako')
130 renderer='rhodecode:templates/search/search.mako')
127 def search(self):
131 def search(self):
128 c = self.load_default_context()
132 c = self.load_default_context()
129 perform_search(self.request, c)
133 perform_search(self.request, c)
130 return self._get_template_context(c)
134 return self._get_template_context(c)
131
135
132
136
133 class SearchRepoView(RepoAppView):
137 class SearchRepoView(RepoAppView):
134 def load_default_context(self):
138 def load_default_context(self):
135 c = self._get_local_tmpl_context()
139 c = self._get_local_tmpl_context()
136 c.active = 'search'
140 c.active = 'search'
137 return c
141 return c
138
142
139 @LoginRequired()
143 @LoginRequired()
140 @HasRepoPermissionAnyDecorator(
144 @HasRepoPermissionAnyDecorator(
141 'repository.read', 'repository.write', 'repository.admin')
145 'repository.read', 'repository.write', 'repository.admin')
142 @view_config(
146 @view_config(
143 route_name='search_repo', request_method='GET',
147 route_name='search_repo', request_method='GET',
144 renderer='rhodecode:templates/search/search.mako')
148 renderer='rhodecode:templates/search/search.mako')
145 @view_config(
149 @view_config(
146 route_name='search_repo_alt', request_method='GET',
150 route_name='search_repo_alt', request_method='GET',
147 renderer='rhodecode:templates/search/search.mako')
151 renderer='rhodecode:templates/search/search.mako')
148 def search_repo(self):
152 def search_repo(self):
149 c = self.load_default_context()
153 c = self.load_default_context()
150 perform_search(self.request, c, repo_name=self.db_repo_name)
154 perform_search(self.request, c, repo_name=self.db_repo_name)
151 return self._get_template_context(c)
155 return self._get_template_context(c)
152
156
153
157
154 class SearchRepoGroupView(RepoGroupAppView):
158 class SearchRepoGroupView(RepoGroupAppView):
155 def load_default_context(self):
159 def load_default_context(self):
156 c = self._get_local_tmpl_context()
160 c = self._get_local_tmpl_context()
157 c.active = 'search'
161 c.active = 'search'
158 return c
162 return c
159
163
160 @LoginRequired()
164 @LoginRequired()
161 @HasRepoGroupPermissionAnyDecorator(
165 @HasRepoGroupPermissionAnyDecorator(
162 'group.read', 'group.write', 'group.admin')
166 'group.read', 'group.write', 'group.admin')
163 @view_config(
167 @view_config(
164 route_name='search_repo_group', request_method='GET',
168 route_name='search_repo_group', request_method='GET',
165 renderer='rhodecode:templates/search/search.mako')
169 renderer='rhodecode:templates/search/search.mako')
166 def search_repo_group(self):
170 def search_repo_group(self):
167 c = self.load_default_context()
171 c = self.load_default_context()
168 perform_search(self.request, c, repo_group_name=self.db_repo_group_name)
172 perform_search(self.request, c, repo_group_name=self.db_repo_group_name)
169 return self._get_template_context(c)
173 return self._get_template_context(c)
@@ -1,143 +1,152 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'
49 DIRECTION_ASC = 'asc'
50 DIRECTION_DESC = 'desc'
50 DIRECTION_DESC = 'desc'
51
51
52 def __init__(self):
52 def __init__(self):
53 pass
53 pass
54
54
55 def cleanup(self):
55 def cleanup(self):
56 pass
56 pass
57
57
58 def search(self, query, document_type, search_user,
58 def search(self, query, document_type, search_user,
59 repo_name=None, repo_group_name=None,
59 repo_name=None, repo_group_name=None,
60 raise_on_exc=True):
60 raise_on_exc=True):
61 raise Exception('NotImplemented')
61 raise Exception('NotImplemented')
62
62
63 @staticmethod
63 @staticmethod
64 def query_to_mark(query, default_field=None):
64 def query_to_mark(query, default_field=None):
65 """
65 """
66 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
67 have a different format optionally.
67 have a different format optionally.
68
68
69 :param default_field:
69 :param default_field:
70 :param query:
70 :param query:
71 """
71 """
72 return ' '.join(normalize_text_for_matching(query).split())
72 return ' '.join(normalize_text_for_matching(query).split())
73
73
74 @property
74 @property
75 def is_es_6(self):
75 def is_es_6(self):
76 return self.es_version == ES_VERSION_6
76 return self.es_version == ES_VERSION_6
77
77
78 def get_handlers(self):
78 def get_handlers(self):
79 return {}
79 return {}
80
80
81 @staticmethod
81 @staticmethod
82 def extract_search_tags(query):
82 def extract_search_tags(query):
83 return []
83 return []
84
84
85 @staticmethod
85 @staticmethod
86 def escape_specials(val):
86 def escape_specials(val):
87 """
87 """
88 Handle and escape reserved chars for search
88 Handle and escape reserved chars for search
89 """
89 """
90 return val
90 return val
91
91
92 def sort_def(self, search_type, direction, sort_field):
93 """
94 Defines sorting for search. This function should decide if for given
95 search_type, sorting can be done with sort_field.
96
97 It also should translate common sort fields into backend specific. e.g elasticsearch
98 """
99 raise NotImplementedError()
100
92 @staticmethod
101 @staticmethod
93 def get_sort(search_type, search_val):
102 def get_sort(search_type, search_val):
94 """
103 """
95 Method used to parse the GET search sort value to a field and direction.
104 Method used to parse the GET search sort value to a field and direction.
96 e.g asc:lines == asc, lines
105 e.g asc:lines == asc, lines
97
106
98 There's also a legacy support for newfirst/oldfirst which defines commit
107 There's also a legacy support for newfirst/oldfirst which defines commit
99 sorting only
108 sorting only
100 """
109 """
101
110
102 direction = BaseSearcher.DIRECTION_ASC
111 direction = BaseSearcher.DIRECTION_ASC
103 sort_field = None
112 sort_field = None
104
113
105 if not search_val:
114 if not search_val:
106 return direction, sort_field
115 return direction, sort_field
107
116
108 if search_val.startswith('asc:'):
117 if search_val.startswith('asc:'):
109 sort_field = search_val[4:]
118 sort_field = search_val[4:]
110 direction = BaseSearcher.DIRECTION_ASC
119 direction = BaseSearcher.DIRECTION_ASC
111 elif search_val.startswith('desc:'):
120 elif search_val.startswith('desc:'):
112 sort_field = search_val[5:]
121 sort_field = search_val[5:]
113 direction = BaseSearcher.DIRECTION_DESC
122 direction = BaseSearcher.DIRECTION_DESC
114 elif search_val == 'newfirst' and search_type == 'commit':
123 elif search_val == 'newfirst' and search_type == 'commit':
115 sort_field = 'date'
124 sort_field = 'date'
116 direction = BaseSearcher.DIRECTION_DESC
125 direction = BaseSearcher.DIRECTION_DESC
117 elif search_val == 'oldfirst' and search_type == 'commit':
126 elif search_val == 'oldfirst' and search_type == 'commit':
118 sort_field = 'date'
127 sort_field = 'date'
119 direction = BaseSearcher.DIRECTION_ASC
128 direction = BaseSearcher.DIRECTION_ASC
120
129
121 return direction, sort_field
130 return direction, sort_field
122
131
123
132
124 def search_config(config, prefix='search.'):
133 def search_config(config, prefix='search.'):
125 _config = {}
134 _config = {}
126 for key in config.keys():
135 for key in config.keys():
127 if key.startswith(prefix):
136 if key.startswith(prefix):
128 _config[key[len(prefix):]] = config[key]
137 _config[key[len(prefix):]] = config[key]
129 return _config
138 return _config
130
139
131
140
132 def searcher_from_config(config, prefix='search.'):
141 def searcher_from_config(config, prefix='search.'):
133 _config = search_config(config, prefix)
142 _config = search_config(config, prefix)
134
143
135 if 'location' not in _config:
144 if 'location' not in _config:
136 _config['location'] = default_location
145 _config['location'] = default_location
137 if 'es_version' not in _config:
146 if 'es_version' not in _config:
138 # use old legacy ES version set to 2
147 # use old legacy ES version set to 2
139 _config['es_version'] = '2'
148 _config['es_version'] = '2'
140
149
141 imported = importlib.import_module(_config.get('module', default_searcher))
150 imported = importlib.import_module(_config.get('module', default_searcher))
142 searcher = imported.Searcher(config=_config)
151 searcher = imported.Searcher(config=_config)
143 return searcher
152 return searcher
@@ -1,293 +1,311 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 from __future__ import absolute_import
25 from __future__ import absolute_import
26 import os
26 import os
27 import re
27 import re
28 import logging
28 import logging
29
29
30 from whoosh import query as query_lib
30 from whoosh import query as query_lib
31 from whoosh.highlight import HtmlFormatter, ContextFragmenter
31 from whoosh.highlight import HtmlFormatter, ContextFragmenter
32 from whoosh.index import create_in, open_dir, exists_in, EmptyIndexError
32 from whoosh.index import create_in, open_dir, exists_in, EmptyIndexError
33 from whoosh.qparser import QueryParser, QueryParserError
33 from whoosh.qparser import QueryParser, QueryParserError
34
34
35 import rhodecode.lib.helpers as h
35 import rhodecode.lib.helpers as h
36 from rhodecode.lib.index import BaseSearcher
36 from rhodecode.lib.index import BaseSearcher
37 from rhodecode.lib.utils2 import safe_unicode
37 from rhodecode.lib.utils2 import safe_unicode
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41
41
42 try:
42 try:
43 # we first try to import from rhodecode tools, fallback to copies if
43 # we first try to import from rhodecode tools, fallback to copies if
44 # we're unable to
44 # we're unable to
45 from rhodecode_tools.lib.fts_index.whoosh_schema import (
45 from rhodecode_tools.lib.fts_index.whoosh_schema import (
46 ANALYZER, FILE_INDEX_NAME, FILE_SCHEMA, COMMIT_INDEX_NAME,
46 ANALYZER, FILE_INDEX_NAME, FILE_SCHEMA, COMMIT_INDEX_NAME,
47 COMMIT_SCHEMA)
47 COMMIT_SCHEMA)
48 except ImportError:
48 except ImportError:
49 log.warning('rhodecode_tools schema not available, doing a fallback '
49 log.warning('rhodecode_tools schema not available, doing a fallback '
50 'import from `rhodecode.lib.index.whoosh_fallback_schema`')
50 'import from `rhodecode.lib.index.whoosh_fallback_schema`')
51 from rhodecode.lib.index.whoosh_fallback_schema import (
51 from rhodecode.lib.index.whoosh_fallback_schema import (
52 ANALYZER, FILE_INDEX_NAME, FILE_SCHEMA, COMMIT_INDEX_NAME,
52 ANALYZER, FILE_INDEX_NAME, FILE_SCHEMA, COMMIT_INDEX_NAME,
53 COMMIT_SCHEMA)
53 COMMIT_SCHEMA)
54
54
55
55
56 FORMATTER = HtmlFormatter('span', between='\n<span class="break">...</span>\n')
56 FORMATTER = HtmlFormatter('span', between='\n<span class="break">...</span>\n')
57 FRAGMENTER = ContextFragmenter(200)
57 FRAGMENTER = ContextFragmenter(200)
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61
61
62 class WhooshSearcher(BaseSearcher):
62 class WhooshSearcher(BaseSearcher):
63 # this also shows in UI
63 # this also shows in UI
64 query_lang_doc = 'http://whoosh.readthedocs.io/en/latest/querylang.html'
64 query_lang_doc = 'http://whoosh.readthedocs.io/en/latest/querylang.html'
65 name = 'whoosh'
65 name = 'whoosh'
66
66
67 def __init__(self, config):
67 def __init__(self, config):
68 super(Searcher, self).__init__()
68 super(Searcher, self).__init__()
69 self.config = config
69 self.config = config
70 if not os.path.isdir(self.config['location']):
70 if not os.path.isdir(self.config['location']):
71 os.makedirs(self.config['location'])
71 os.makedirs(self.config['location'])
72
72
73 opener = create_in
73 opener = create_in
74 if exists_in(self.config['location'], indexname=FILE_INDEX_NAME):
74 if exists_in(self.config['location'], indexname=FILE_INDEX_NAME):
75 opener = open_dir
75 opener = open_dir
76 file_index = opener(self.config['location'], schema=FILE_SCHEMA,
76 file_index = opener(self.config['location'], schema=FILE_SCHEMA,
77 indexname=FILE_INDEX_NAME)
77 indexname=FILE_INDEX_NAME)
78
78
79 opener = create_in
79 opener = create_in
80 if exists_in(self.config['location'], indexname=COMMIT_INDEX_NAME):
80 if exists_in(self.config['location'], indexname=COMMIT_INDEX_NAME):
81 opener = open_dir
81 opener = open_dir
82 changeset_index = opener(self.config['location'], schema=COMMIT_SCHEMA,
82 changeset_index = opener(self.config['location'], schema=COMMIT_SCHEMA,
83 indexname=COMMIT_INDEX_NAME)
83 indexname=COMMIT_INDEX_NAME)
84
84
85 self.commit_schema = COMMIT_SCHEMA
85 self.commit_schema = COMMIT_SCHEMA
86 self.commit_index = changeset_index
86 self.commit_index = changeset_index
87 self.file_schema = FILE_SCHEMA
87 self.file_schema = FILE_SCHEMA
88 self.file_index = file_index
88 self.file_index = file_index
89 self.searcher = None
89 self.searcher = None
90
90
91 def cleanup(self):
91 def cleanup(self):
92 if self.searcher:
92 if self.searcher:
93 self.searcher.close()
93 self.searcher.close()
94
94
95 def _extend_query(self, query):
95 def _extend_query(self, query):
96 hashes = re.compile('([0-9a-f]{5,40})').findall(query)
96 hashes = re.compile('([0-9a-f]{5,40})').findall(query)
97 if hashes:
97 if hashes:
98 hashes_or_query = ' OR '.join('commit_id:%s*' % h for h in hashes)
98 hashes_or_query = ' OR '.join('commit_id:%s*' % h for h in hashes)
99 query = u'(%s) OR %s' % (query, hashes_or_query)
99 query = u'(%s) OR %s' % (query, hashes_or_query)
100 return query
100 return query
101
101
102 def sort_def(self, search_type, direction, sort_field):
103
104 if search_type == 'commit':
105 field_defs = {
106 'message': 'message',
107 'date': 'date',
108 'author_email': 'author',
109 }
110 elif search_type == 'path':
111 field_defs = {
112 'file': 'path',
113 'size': 'size',
114 'lines': 'lines',
115 }
116 elif search_type == 'content':
117 # NOTE(dan): content doesn't support any sorting
118 field_defs = {}
119 else:
120 return ''
121
122 if sort_field in field_defs:
123 return field_defs[sort_field]
124
102 def search(self, query, document_type, search_user,
125 def search(self, query, document_type, search_user,
103 repo_name=None, repo_group_name=None,
126 repo_name=None, repo_group_name=None,
104 requested_page=1, page_limit=10, sort=None, raise_on_exc=True):
127 requested_page=1, page_limit=10, sort=None, raise_on_exc=True):
105
128
106 original_query = query
129 original_query = query
107 query = self._extend_query(query)
130 query = self._extend_query(query)
108
131
109 log.debug(u'QUERY: %s on %s', query, document_type)
132 log.debug(u'QUERY: %s on %s', query, document_type)
110 result = {
133 result = {
111 'results': [],
134 'results': [],
112 'count': 0,
135 'count': 0,
113 'error': None,
136 'error': None,
114 'runtime': 0
137 'runtime': 0
115 }
138 }
116 search_type, index_name, schema_defn = self._prepare_for_search(
139 search_type, index_name, schema_defn = self._prepare_for_search(
117 document_type)
140 document_type)
118 self._init_searcher(index_name)
141 self._init_searcher(index_name)
119 try:
142 try:
120 qp = QueryParser(search_type, schema=schema_defn)
143 qp = QueryParser(search_type, schema=schema_defn)
121 allowed_repos_filter = self._get_repo_filter(
144 allowed_repos_filter = self._get_repo_filter(
122 search_user, repo_name)
145 search_user, repo_name)
123 try:
146 try:
124 query = qp.parse(safe_unicode(query))
147 query = qp.parse(safe_unicode(query))
125 log.debug('query: %s (%s)', query, repr(query))
148 log.debug('query: %s (%s)', query, repr(query))
126
149
127 def sort_def(_direction, _sort_field):
128 field2whoosh = {
129 'message.raw': 'message',
130 'author.email.raw': 'author',
131 }
132 return field2whoosh.get(_sort_field) or _sort_field
133
134 reverse, sorted_by = False, None
150 reverse, sorted_by = False, None
135 direction, sort_field = self.get_sort(search_type, sort)
151 direction, sort_field = self.get_sort(search_type, sort)
136 if sort_field:
152 if sort_field:
137 if direction == Searcher.DIRECTION_DESC:
153 sort_definition = self.sort_def(search_type, direction, sort_field)
138 reverse = True
154 if sort_definition:
139 if direction == Searcher.DIRECTION_ASC:
155 sorted_by = sort_definition
140 reverse = False
156 if direction == Searcher.DIRECTION_DESC:
141 sorted_by = sort_def(direction, sort_field)
157 reverse = True
158 if direction == Searcher.DIRECTION_ASC:
159 reverse = False
142
160
143 whoosh_results = self.searcher.search(
161 whoosh_results = self.searcher.search(
144 query, filter=allowed_repos_filter, limit=None,
162 query, filter=allowed_repos_filter, limit=None,
145 sortedby=sorted_by, reverse=reverse)
163 sortedby=sorted_by, reverse=reverse)
146
164
147 # fixes for 32k limit that whoosh uses for highlight
165 # fixes for 32k limit that whoosh uses for highlight
148 whoosh_results.fragmenter.charlimit = None
166 whoosh_results.fragmenter.charlimit = None
149 res_ln = whoosh_results.scored_length()
167 res_ln = whoosh_results.scored_length()
150 result['runtime'] = whoosh_results.runtime
168 result['runtime'] = whoosh_results.runtime
151 result['count'] = res_ln
169 result['count'] = res_ln
152 result['results'] = WhooshResultWrapper(
170 result['results'] = WhooshResultWrapper(
153 search_type, res_ln, whoosh_results)
171 search_type, res_ln, whoosh_results)
154
172
155 except QueryParserError:
173 except QueryParserError:
156 result['error'] = 'Invalid search query. Try quoting it.'
174 result['error'] = 'Invalid search query. Try quoting it.'
157 except (EmptyIndexError, IOError, OSError):
175 except (EmptyIndexError, IOError, OSError):
158 msg = 'There is no index to search in. Please run whoosh indexer'
176 msg = 'There is no index to search in. Please run whoosh indexer'
159 log.exception(msg)
177 log.exception(msg)
160 result['error'] = msg
178 result['error'] = msg
161 except Exception:
179 except Exception:
162 msg = 'An error occurred during this search operation'
180 msg = 'An error occurred during this search operation'
163 log.exception(msg)
181 log.exception(msg)
164 result['error'] = msg
182 result['error'] = msg
165
183
166 return result
184 return result
167
185
168 def statistics(self, translator):
186 def statistics(self, translator):
169 _ = translator
187 _ = translator
170 stats = [
188 stats = [
171 {'key': _('Index Type'), 'value': 'Whoosh'},
189 {'key': _('Index Type'), 'value': 'Whoosh'},
172 {'sep': True},
190 {'sep': True},
173
191
174 {'key': _('File Index'), 'value': str(self.file_index)},
192 {'key': _('File Index'), 'value': str(self.file_index)},
175 {'key': _('Indexed documents'), 'value': self.file_index.doc_count()},
193 {'key': _('Indexed documents'), 'value': self.file_index.doc_count()},
176 {'key': _('Last update'), 'value': h.time_to_datetime(self.file_index.last_modified())},
194 {'key': _('Last update'), 'value': h.time_to_datetime(self.file_index.last_modified())},
177
195
178 {'sep': True},
196 {'sep': True},
179
197
180 {'key': _('Commit index'), 'value': str(self.commit_index)},
198 {'key': _('Commit index'), 'value': str(self.commit_index)},
181 {'key': _('Indexed documents'), 'value': str(self.commit_index.doc_count())},
199 {'key': _('Indexed documents'), 'value': str(self.commit_index.doc_count())},
182 {'key': _('Last update'), 'value': h.time_to_datetime(self.commit_index.last_modified())}
200 {'key': _('Last update'), 'value': h.time_to_datetime(self.commit_index.last_modified())}
183 ]
201 ]
184 return stats
202 return stats
185
203
186 def _get_repo_filter(self, auth_user, repo_name):
204 def _get_repo_filter(self, auth_user, repo_name):
187
205
188 allowed_to_search = [
206 allowed_to_search = [
189 repo for repo, perm in
207 repo for repo, perm in
190 auth_user.permissions['repositories'].items()
208 auth_user.permissions['repositories'].items()
191 if perm != 'repository.none']
209 if perm != 'repository.none']
192
210
193 if repo_name:
211 if repo_name:
194 repo_filter = [query_lib.Term('repository', repo_name)]
212 repo_filter = [query_lib.Term('repository', repo_name)]
195
213
196 elif 'hg.admin' in auth_user.permissions.get('global', []):
214 elif 'hg.admin' in auth_user.permissions.get('global', []):
197 return None
215 return None
198
216
199 else:
217 else:
200 repo_filter = [query_lib.Term('repository', _rn)
218 repo_filter = [query_lib.Term('repository', _rn)
201 for _rn in allowed_to_search]
219 for _rn in allowed_to_search]
202 # in case we're not allowed to search anywhere, it's a trick
220 # in case we're not allowed to search anywhere, it's a trick
203 # to tell whoosh we're filtering, on ALL results
221 # to tell whoosh we're filtering, on ALL results
204 repo_filter = repo_filter or [query_lib.Term('repository', '')]
222 repo_filter = repo_filter or [query_lib.Term('repository', '')]
205
223
206 return query_lib.Or(repo_filter)
224 return query_lib.Or(repo_filter)
207
225
208 def _prepare_for_search(self, cur_type):
226 def _prepare_for_search(self, cur_type):
209 search_type = {
227 search_type = {
210 'content': 'content',
228 'content': 'content',
211 'commit': 'message',
229 'commit': 'message',
212 'path': 'path',
230 'path': 'path',
213 'repository': 'repository'
231 'repository': 'repository'
214 }.get(cur_type, 'content')
232 }.get(cur_type, 'content')
215
233
216 index_name = {
234 index_name = {
217 'content': FILE_INDEX_NAME,
235 'content': FILE_INDEX_NAME,
218 'commit': COMMIT_INDEX_NAME,
236 'commit': COMMIT_INDEX_NAME,
219 'path': FILE_INDEX_NAME
237 'path': FILE_INDEX_NAME
220 }.get(cur_type, FILE_INDEX_NAME)
238 }.get(cur_type, FILE_INDEX_NAME)
221
239
222 schema_defn = {
240 schema_defn = {
223 'content': self.file_schema,
241 'content': self.file_schema,
224 'commit': self.commit_schema,
242 'commit': self.commit_schema,
225 'path': self.file_schema
243 'path': self.file_schema
226 }.get(cur_type, self.file_schema)
244 }.get(cur_type, self.file_schema)
227
245
228 log.debug('IDX: %s', index_name)
246 log.debug('IDX: %s', index_name)
229 log.debug('SCHEMA: %s', schema_defn)
247 log.debug('SCHEMA: %s', schema_defn)
230 return search_type, index_name, schema_defn
248 return search_type, index_name, schema_defn
231
249
232 def _init_searcher(self, index_name):
250 def _init_searcher(self, index_name):
233 idx = open_dir(self.config['location'], indexname=index_name)
251 idx = open_dir(self.config['location'], indexname=index_name)
234 self.searcher = idx.searcher()
252 self.searcher = idx.searcher()
235 return self.searcher
253 return self.searcher
236
254
237
255
238 Searcher = WhooshSearcher
256 Searcher = WhooshSearcher
239
257
240
258
241 class WhooshResultWrapper(object):
259 class WhooshResultWrapper(object):
242 def __init__(self, search_type, total_hits, results):
260 def __init__(self, search_type, total_hits, results):
243 self.search_type = search_type
261 self.search_type = search_type
244 self.results = results
262 self.results = results
245 self.total_hits = total_hits
263 self.total_hits = total_hits
246
264
247 def __str__(self):
265 def __str__(self):
248 return '<%s at %s>' % (self.__class__.__name__, len(self))
266 return '<%s at %s>' % (self.__class__.__name__, len(self))
249
267
250 def __repr__(self):
268 def __repr__(self):
251 return self.__str__()
269 return self.__str__()
252
270
253 def __len__(self):
271 def __len__(self):
254 return self.total_hits
272 return self.total_hits
255
273
256 def __iter__(self):
274 def __iter__(self):
257 """
275 """
258 Allows Iteration over results,and lazy generate content
276 Allows Iteration over results,and lazy generate content
259
277
260 *Requires* implementation of ``__getitem__`` method.
278 *Requires* implementation of ``__getitem__`` method.
261 """
279 """
262 for hit in self.results:
280 for hit in self.results:
263 yield self.get_full_content(hit)
281 yield self.get_full_content(hit)
264
282
265 def __getitem__(self, key):
283 def __getitem__(self, key):
266 """
284 """
267 Slicing of resultWrapper
285 Slicing of resultWrapper
268 """
286 """
269 i, j = key.start, key.stop
287 i, j = key.start, key.stop
270 for hit in self.results[i:j]:
288 for hit in self.results[i:j]:
271 yield self.get_full_content(hit)
289 yield self.get_full_content(hit)
272
290
273 def get_full_content(self, hit):
291 def get_full_content(self, hit):
274 # TODO: marcink: this feels like an overkill, there's a lot of data
292 # TODO: marcink: this feels like an overkill, there's a lot of data
275 # inside hit object, and we don't need all
293 # inside hit object, and we don't need all
276 res = dict(hit)
294 res = dict(hit)
277 # elastic search uses that, we set it empty so it fallbacks to regular HL logic
295 # elastic search uses that, we set it empty so it fallbacks to regular HL logic
278 res['content_highlight'] = ''
296 res['content_highlight'] = ''
279
297
280 f_path = '' # pragma: no cover
298 f_path = '' # pragma: no cover
281 if self.search_type in ['content', 'path']:
299 if self.search_type in ['content', 'path']:
282 f_path = res['path'][len(res['repository']):]
300 f_path = res['path'][len(res['repository']):]
283 f_path = f_path.lstrip(os.sep)
301 f_path = f_path.lstrip(os.sep)
284
302
285 if self.search_type == 'content':
303 if self.search_type == 'content':
286 res.update({'content_short_hl': hit.highlights('content'),
304 res.update({'content_short_hl': hit.highlights('content'),
287 'f_path': f_path})
305 'f_path': f_path})
288 elif self.search_type == 'path':
306 elif self.search_type == 'path':
289 res.update({'f_path': f_path})
307 res.update({'f_path': f_path})
290 elif self.search_type == 'message':
308 elif self.search_type == 'message':
291 res.update({'message_hl': hit.highlights('message')})
309 res.update({'message_hl': hit.highlights('message')})
292
310
293 return res
311 return res
@@ -1,241 +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
63
64 <%def name="field_sort(field_name)">
64 <%def name="field_sort(field_name)">
65
65
66 <%
66 <%
67 if c.sort.startswith('asc:'):
67 if c.sort.startswith('asc:'):
68 return c.url_generator(sort='desc:{}'.format(field_name))
68 return c.url_generator(sort='desc:{}'.format(field_name))
69 elif c.sort.startswith('desc:'):
69 elif c.sort.startswith('desc:'):
70 return c.url_generator(sort='asc:{}'.format(field_name))
70 return c.url_generator(sort='asc:{}'.format(field_name))
71
71
72 return 'asc:{}'.format(field_name)
72 return c.url_generator(sort='asc:{}'.format(field_name))
73 %>
73 %>
74 </%def>
74 </%def>
75
75
76
76
77 <%def name="main()">
77 <%def name="main()">
78 <div class="box">
78 <div class="box">
79 %if c.repo_name:
79 %if c.repo_name:
80 <!-- box / title -->
80 <!-- box / title -->
81 ${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')}
82 %elif c.repo_group_name:
82 %elif c.repo_group_name:
83 <!-- box / title -->
83 <!-- box / title -->
84 ${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')}
85 %else:
85 %else:
86 <!-- box / title -->
86 <!-- box / title -->
87 <div class="title">
87 <div class="title">
88 ${self.breadcrumbs()}
88 ${self.breadcrumbs()}
89 <ul class="links">&nbsp;</ul>
89 <ul class="links">&nbsp;</ul>
90 </div>
90 </div>
91 <!-- end box / title -->
91 <!-- end box / title -->
92 ${h.form(h.route_path('search'), method='get')}
92 ${h.form(h.route_path('search'), method='get')}
93 %endif
93 %endif
94 <div class="form search-form">
94 <div class="form search-form">
95 <div class="fields">
95 <div class="fields">
96
96
97 ${h.text('q', c.cur_query, placeholder="Enter query...")}
97 ${h.text('q', c.cur_query, placeholder="Enter query...")}
98
98
99 ${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')}
100 ${h.hidden('max_lines', '10')}
100 ${h.hidden('max_lines', '10')}
101
101
102 <input type="submit" value="${_('Search')}" class="btn"/>
102 <input type="submit" value="${_('Search')}" class="btn"/>
103 <br/>
103 <br/>
104
104
105 <div class="search-tags">
105 <div class="search-tags">
106 <span class="tag tag8">
106 <span class="tag tag8">
107 %if c.repo_name:
107 %if c.repo_name:
108 <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>
109 %elif c.repo_group_name:
109 %elif c.repo_group_name:
110 <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>
111 % else:
111 % else:
112 ${_('Global Search')}
112 ${_('Global Search')}
113 %endif
113 %endif
114 </span>
114 </span>
115
115
116 %if c.repo_name:
116 %if c.repo_name:
117 Β»
117 Β»
118 <span class="tag tag8">
118 <span class="tag tag8">
119 ${repo_icon(c.rhodecode_db_repo)}
119 ${repo_icon(c.rhodecode_db_repo)}
120 ${c.repo_name}
120 ${c.repo_name}
121 </span>
121 </span>
122
122
123 %elif c.repo_group_name:
123 %elif c.repo_group_name:
124 Β»
124 Β»
125 <span class="tag tag8">
125 <span class="tag tag8">
126 ${repo_group_icon()}
126 ${repo_group_icon()}
127 ${c.repo_group_name}
127 ${c.repo_group_name}
128 </span>
128 </span>
129 %endif
129 %endif
130
130
131 % if c.sort_tag:
131 % if c.sort_tag:
132 <span class="tag tag8">
132 <span class="tag tag8">
133 % if c.sort_tag_dir == 'asc':
133 % if c.sort_tag_dir == 'asc':
134 <i class="icon-angle-down"></i>
134 <i class="icon-angle-down"></i>
135 % elif c.sort_tag_dir == 'desc':
135 % elif c.sort_tag_dir == 'desc':
136 <i class="icon-angle-up"></i>
136 <i class="icon-angle-up"></i>
137 % endif
137 % endif
138 ${_('sort')}:${c.sort_tag}
138 ${_('sort')}:${c.sort_tag}
139 </span>
139 </span>
140 % endif
140 % endif
141
141
142 % for search_tag in c.search_tags:
142 % for search_tag in c.search_tags:
143 <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>
144 % endfor
144 % endfor
145
145
146 </div>
146 </div>
147
147
148 <div class="search-feedback-items">
148 <div class="search-feedback-items">
149 % for error in c.errors:
149 % for error in c.errors:
150 <span class="error-message">
150 <span class="error-message">
151 % for k,v in error.asdict().items():
151 % for k,v in error.asdict().items():
152 ${k} - ${v}
152 ${k} - ${v}
153 % endfor
153 % endfor
154 </span>
154 </span>
155 % endfor
155 % endfor
156 <div class="field">
156 <div class="field">
157 <p class="filterexample" style="position: inherit" onclick="$('#search-help').toggle()">${_('Query Language examples')}</p>
157 <p class="filterexample" style="position: inherit" onclick="$('#search-help').toggle()">${_('Query Language examples')}</p>
158 <pre id="search-help" style="display: none">\
158 <pre id="search-help" style="display: none">\
159
159
160 % if c.searcher.name == 'whoosh':
160 % if c.searcher.name == 'whoosh':
161 Example filter terms for `Whoosh` search:
161 Example filter terms for `Whoosh` search:
162 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>
163 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.
164
164
165 Generate wildcards using '*' character:
165 Generate wildcards using '*' character:
166 "repo_name:vcs*" - search everything starting with 'vcs'
166 "repo_name:vcs*" - search everything starting with 'vcs'
167 "repo_name:*vcs*" - search for repository containing 'vcs'
167 "repo_name:*vcs*" - search for repository containing 'vcs'
168
168
169 Optional AND / OR operators in queries
169 Optional AND / OR operators in queries
170 "repo_name:vcs OR repo_name:test"
170 "repo_name:vcs OR repo_name:test"
171 "owner:test AND repo_name:test*" AND extension:py
171 "owner:test AND repo_name:test*" AND extension:py
172
172
173 Move advanced search is available via ElasticSearch6 backend in EE edition.
173 Move advanced search is available via ElasticSearch6 backend in EE edition.
174 % elif c.searcher.name == 'elasticsearch' and c.searcher.es_version == '2':
174 % elif c.searcher.name == 'elasticsearch' and c.searcher.es_version == '2':
175 Example filter terms for `ElasticSearch-${c.searcher.es_version}`search:
175 Example filter terms for `ElasticSearch-${c.searcher.es_version}`search:
176 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.
177
177
178 search type: content (File Content)
178 search type: content (File Content)
179 indexed fields: content
179 indexed fields: content
180
180
181 # search for `fix` string in all files
181 # search for `fix` string in all files
182 fix
182 fix
183
183
184 search type: commit (Commit message)
184 search type: commit (Commit message)
185 indexed fields: message
185 indexed fields: message
186
186
187 search type: path (File name)
187 search type: path (File name)
188 indexed fields: path
188 indexed fields: path
189
189
190 % else:
190 % else:
191 Example filter terms for `ElasticSearch-${c.searcher.es_version}`search:
191 Example filter terms for `ElasticSearch-${c.searcher.es_version}`search:
192 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>
193 The reserved characters needed espace by `\`: + - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /
193 The reserved characters needed espace by `\`: + - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /
194 % for handler in c.searcher.get_handlers().values():
194 % for handler in c.searcher.get_handlers().values():
195
195
196 search type: ${handler.search_type_label}
196 search type: ${handler.search_type_label}
197 *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)])}
198 % for entry in handler.es_6_example_queries:
198 % for entry in handler.es_6_example_queries:
199 ${entry.rstrip()}
199 ${entry.rstrip()}
200 % endfor
200 % endfor
201 % endfor
201 % endfor
202
202
203 % endif
203 % endif
204 </pre>
204 </pre>
205 </div>
205 </div>
206
206
207 <div class="field">${c.runtime}</div>
207 <div class="field">${c.runtime}</div>
208 </div>
208 </div>
209 </div>
209 </div>
210 </div>
210 </div>
211
211
212 ${h.end_form()}
212 ${h.end_form()}
213 <div class="search">
213 <div class="search">
214 % if c.search_type == 'content':
214 % if c.search_type == 'content':
215 <%include file='search_content.mako'/>
215 <%include file='search_content.mako'/>
216 % elif c.search_type == 'path':
216 % elif c.search_type == 'path':
217 <%include file='search_path.mako'/>
217 <%include file='search_path.mako'/>
218 % elif c.search_type == 'commit':
218 % elif c.search_type == 'commit':
219 <%include file='search_commit.mako'/>
219 <%include file='search_commit.mako'/>
220 % elif c.search_type == 'repository':
220 % elif c.search_type == 'repository':
221 <%include file='search_repository.mako'/>
221 <%include file='search_repository.mako'/>
222 % endif
222 % endif
223 </div>
223 </div>
224 </div>
224 </div>
225 <script>
225 <script>
226 $(document).ready(function(){
226 $(document).ready(function(){
227 $("#id_search_type").select2({
227 $("#id_search_type").select2({
228 'containerCssClass': "drop-menu",
228 'containerCssClass': "drop-menu",
229 'dropdownCssClass': "drop-menu-dropdown",
229 'dropdownCssClass': "drop-menu-dropdown",
230 'dropdownAutoWidth': true,
230 'dropdownAutoWidth': true,
231 'minimumResultsForSearch': -1
231 'minimumResultsForSearch': -1
232 });
232 });
233
233
234 $('#q').autoGrowInput({maxWidth: 920});
234 $('#q').autoGrowInput({maxWidth: 920});
235
235
236 setTimeout(function() {
236 setTimeout(function() {
237 $('#q').keyup()
237 $('#q').keyup()
238 }, 1);
238 }, 1);
239 })
239 })
240 </script>
240 </script>
241 </%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>
11 <th>
12 <a href="${search.field_sort('message.raw')}">${_('Commit message')}</a>
12 <a href="${search.field_sort('message')}">${_('Commit message')}</a>
13 </th>
13 </th>
14 <th>
14 <th>
15 <a href="${search.field_sort('date')}">${_('Age')}</a>
15 <a href="${search.field_sort('date')}">${_('Commit date')}</a>
16 </th>
16 </th>
17 <th>
17 <th>
18 <a href="${search.field_sort('author.email.raw')}">${_('Author')}</a>
18 <a href="${search.field_sort('author_email')}">${_('Author')}</a>
19 </th>
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,53 +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>
8 <th>
9 <a href="${search.field_sort('file.raw')}">${_('File')}</a>
9 <a href="${search.field_sort('file')}">${_('File')}</a>
10 </th>
10 </th>
11 <th>
11 <th>
12 <a href="${search.field_sort('size')}">${_('Size')}</a>
12 <a href="${search.field_sort('size')}">${_('Size')}</a>
13 </th>
13 </th>
14 <th>
14 <th>
15 <a href="${search.field_sort('lines')}">${_('Lines')}</a>
15 <a href="${search.field_sort('lines')}">${_('Lines')}</a>
16 </th>
16 </th>
17 </tr>
17 </tr>
18 %for entry in c.formatted_results:
18 %for entry in c.formatted_results:
19 ## 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
20 % 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'):
21 <tr class="body">
21 <tr class="body">
22 <td class="td-componentname">
22 <td class="td-componentname">
23 <% 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')) %>
24 ${search.repo_icon(repo_type)}
24 ${search.repo_icon(repo_type)}
25 ${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']))}
26 </td>
26 </td>
27 <td class="td-componentname">
27 <td class="td-componentname">
28 <i class="icon-file"></i>
28 <i class="icon-file"></i>
29 ${h.link_to(h.literal(entry['f_path']),
29 ${h.link_to(h.literal(entry['f_path']),
30 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']))}
31 </td>
31 </td>
32 <td>
32 <td>
33 %if entry.get('size'):
33 %if entry.get('size'):
34 ${h.format_byte_size_binary(entry['size'])}
34 ${h.format_byte_size_binary(entry['size'])}
35 %endif
35 %endif
36 </td>
36 </td>
37 <td>
37 <td>
38 %if entry.get('lines'):
38 %if entry.get('lines'):
39 ${entry.get('lines', 0.)}
39 ${entry.get('lines', 0.)}
40 %endif
40 %endif
41 </td>
41 </td>
42 </tr>
42 </tr>
43 % endif
43 % endif
44 %endfor
44 %endfor
45 </table>
45 </table>
46
46
47 %if c.cur_query:
47 %if c.cur_query:
48 <div class="pagination-wh pagination-left">
48 <div class="pagination-wh pagination-left">
49 ${c.formatted_results.pager('$link_previous ~2~ $link_next')}
49 ${c.formatted_results.pager('$link_previous ~2~ $link_next')}
50 </div>
50 </div>
51 %endif
51 %endif
52
52
53 % endif
53 % endif
General Comments 0
You need to be logged in to leave comments. Login now