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