##// END OF EJS Templates
search: add option to search within a repository group.
dan -
r3441:d273b8e9 default
parent child Browse files
Show More
@@ -1,563 +1,587 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import re
21 import re
22 import logging
22 import logging
23 import collections
23 import collections
24
24
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26
26
27 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
29 from rhodecode.lib.auth import (
29 from rhodecode.lib.auth import (
30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator,
30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator,
31 CSRFRequired)
31 CSRFRequired)
32 from rhodecode.lib.index import searcher_from_config
32 from rhodecode.lib.index import searcher_from_config
33 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
33 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
34 from rhodecode.lib.ext_json import json
34 from rhodecode.lib.ext_json import json
35 from rhodecode.model.db import (
35 from rhodecode.model.db import (
36 func, true, or_, in_filter_generator, Repository, RepoGroup, User, UserGroup)
36 func, true, or_, in_filter_generator, Repository, RepoGroup, User, UserGroup)
37 from rhodecode.model.repo import RepoModel
37 from rhodecode.model.repo import RepoModel
38 from rhodecode.model.repo_group import RepoGroupModel
38 from rhodecode.model.repo_group import RepoGroupModel
39 from rhodecode.model.scm import RepoGroupList, RepoList
39 from rhodecode.model.scm import RepoGroupList, RepoList
40 from rhodecode.model.user import UserModel
40 from rhodecode.model.user import UserModel
41 from rhodecode.model.user_group import UserGroupModel
41 from rhodecode.model.user_group import UserGroupModel
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 class HomeView(BaseAppView):
46 class HomeView(BaseAppView):
47
47
48 def load_default_context(self):
48 def load_default_context(self):
49 c = self._get_local_tmpl_context()
49 c = self._get_local_tmpl_context()
50 c.user = c.auth_user.get_instance()
50 c.user = c.auth_user.get_instance()
51
51
52 return c
52 return c
53
53
54 @LoginRequired()
54 @LoginRequired()
55 @view_config(
55 @view_config(
56 route_name='user_autocomplete_data', request_method='GET',
56 route_name='user_autocomplete_data', request_method='GET',
57 renderer='json_ext', xhr=True)
57 renderer='json_ext', xhr=True)
58 def user_autocomplete_data(self):
58 def user_autocomplete_data(self):
59 self.load_default_context()
59 self.load_default_context()
60 query = self.request.GET.get('query')
60 query = self.request.GET.get('query')
61 active = str2bool(self.request.GET.get('active') or True)
61 active = str2bool(self.request.GET.get('active') or True)
62 include_groups = str2bool(self.request.GET.get('user_groups'))
62 include_groups = str2bool(self.request.GET.get('user_groups'))
63 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
63 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
64 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
64 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
65
65
66 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
66 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
67 query, active, include_groups)
67 query, active, include_groups)
68
68
69 _users = UserModel().get_users(
69 _users = UserModel().get_users(
70 name_contains=query, only_active=active)
70 name_contains=query, only_active=active)
71
71
72 def maybe_skip_default_user(usr):
72 def maybe_skip_default_user(usr):
73 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
73 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
74 return False
74 return False
75 return True
75 return True
76 _users = filter(maybe_skip_default_user, _users)
76 _users = filter(maybe_skip_default_user, _users)
77
77
78 if include_groups:
78 if include_groups:
79 # extend with user groups
79 # extend with user groups
80 _user_groups = UserGroupModel().get_user_groups(
80 _user_groups = UserGroupModel().get_user_groups(
81 name_contains=query, only_active=active,
81 name_contains=query, only_active=active,
82 expand_groups=expand_groups)
82 expand_groups=expand_groups)
83 _users = _users + _user_groups
83 _users = _users + _user_groups
84
84
85 return {'suggestions': _users}
85 return {'suggestions': _users}
86
86
87 @LoginRequired()
87 @LoginRequired()
88 @NotAnonymous()
88 @NotAnonymous()
89 @view_config(
89 @view_config(
90 route_name='user_group_autocomplete_data', request_method='GET',
90 route_name='user_group_autocomplete_data', request_method='GET',
91 renderer='json_ext', xhr=True)
91 renderer='json_ext', xhr=True)
92 def user_group_autocomplete_data(self):
92 def user_group_autocomplete_data(self):
93 self.load_default_context()
93 self.load_default_context()
94 query = self.request.GET.get('query')
94 query = self.request.GET.get('query')
95 active = str2bool(self.request.GET.get('active') or True)
95 active = str2bool(self.request.GET.get('active') or True)
96 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
96 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
97
97
98 log.debug('generating user group list, query:%s, active:%s',
98 log.debug('generating user group list, query:%s, active:%s',
99 query, active)
99 query, active)
100
100
101 _user_groups = UserGroupModel().get_user_groups(
101 _user_groups = UserGroupModel().get_user_groups(
102 name_contains=query, only_active=active,
102 name_contains=query, only_active=active,
103 expand_groups=expand_groups)
103 expand_groups=expand_groups)
104 _user_groups = _user_groups
104 _user_groups = _user_groups
105
105
106 return {'suggestions': _user_groups}
106 return {'suggestions': _user_groups}
107
107
108 def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
108 def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
109 org_query = name_contains
109 org_query = name_contains
110 allowed_ids = self._rhodecode_user.repo_acl_ids(
110 allowed_ids = self._rhodecode_user.repo_acl_ids(
111 ['repository.read', 'repository.write', 'repository.admin'],
111 ['repository.read', 'repository.write', 'repository.admin'],
112 cache=False, name_filter=name_contains) or [-1]
112 cache=False, name_filter=name_contains) or [-1]
113
113
114 query = Repository.query()\
114 query = Repository.query()\
115 .order_by(func.length(Repository.repo_name))\
115 .order_by(func.length(Repository.repo_name))\
116 .order_by(Repository.repo_name)\
116 .order_by(Repository.repo_name)\
117 .filter(Repository.archived.isnot(true()))\
117 .filter(Repository.archived.isnot(true()))\
118 .filter(or_(
118 .filter(or_(
119 # generate multiple IN to fix limitation problems
119 # generate multiple IN to fix limitation problems
120 *in_filter_generator(Repository.repo_id, allowed_ids)
120 *in_filter_generator(Repository.repo_id, allowed_ids)
121 ))
121 ))
122
122
123 if repo_type:
123 if repo_type:
124 query = query.filter(Repository.repo_type == repo_type)
124 query = query.filter(Repository.repo_type == repo_type)
125
125
126 if name_contains:
126 if name_contains:
127 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
127 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
128 query = query.filter(
128 query = query.filter(
129 Repository.repo_name.ilike(ilike_expression))
129 Repository.repo_name.ilike(ilike_expression))
130 query = query.limit(limit)
130 query = query.limit(limit)
131
131
132 acl_iter = query
132 acl_iter = query
133
133
134 return [
134 return [
135 {
135 {
136 'id': obj.repo_name,
136 'id': obj.repo_name,
137 'value': org_query,
137 'value': org_query,
138 'value_display': obj.repo_name,
138 'value_display': obj.repo_name,
139 'text': obj.repo_name,
139 'text': obj.repo_name,
140 'type': 'repo',
140 'type': 'repo',
141 'repo_id': obj.repo_id,
141 'repo_id': obj.repo_id,
142 'repo_type': obj.repo_type,
142 'repo_type': obj.repo_type,
143 'private': obj.private,
143 'private': obj.private,
144 'url': h.route_path('repo_summary', repo_name=obj.repo_name)
144 'url': h.route_path('repo_summary', repo_name=obj.repo_name)
145 }
145 }
146 for obj in acl_iter]
146 for obj in acl_iter]
147
147
148 def _get_repo_group_list(self, name_contains=None, limit=20):
148 def _get_repo_group_list(self, name_contains=None, limit=20):
149 org_query = name_contains
149 org_query = name_contains
150 allowed_ids = self._rhodecode_user.repo_group_acl_ids(
150 allowed_ids = self._rhodecode_user.repo_group_acl_ids(
151 ['group.read', 'group.write', 'group.admin'],
151 ['group.read', 'group.write', 'group.admin'],
152 cache=False, name_filter=name_contains) or [-1]
152 cache=False, name_filter=name_contains) or [-1]
153
153
154 query = RepoGroup.query()\
154 query = RepoGroup.query()\
155 .order_by(func.length(RepoGroup.group_name))\
155 .order_by(func.length(RepoGroup.group_name))\
156 .order_by(RepoGroup.group_name) \
156 .order_by(RepoGroup.group_name) \
157 .filter(or_(
157 .filter(or_(
158 # generate multiple IN to fix limitation problems
158 # generate multiple IN to fix limitation problems
159 *in_filter_generator(RepoGroup.group_id, allowed_ids)
159 *in_filter_generator(RepoGroup.group_id, allowed_ids)
160 ))
160 ))
161
161
162 if name_contains:
162 if name_contains:
163 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
163 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
164 query = query.filter(
164 query = query.filter(
165 RepoGroup.group_name.ilike(ilike_expression))
165 RepoGroup.group_name.ilike(ilike_expression))
166 query = query.limit(limit)
166 query = query.limit(limit)
167
167
168 acl_iter = query
168 acl_iter = query
169
169
170 return [
170 return [
171 {
171 {
172 'id': obj.group_name,
172 'id': obj.group_name,
173 'value': org_query,
173 'value': org_query,
174 'value_display': obj.group_name,
174 'value_display': obj.group_name,
175 'text': obj.group_name,
175 'text': obj.group_name,
176 'type': 'repo_group',
176 'type': 'repo_group',
177 'repo_group_id': obj.group_id,
177 'repo_group_id': obj.group_id,
178 'url': h.route_path(
178 'url': h.route_path(
179 'repo_group_home', repo_group_name=obj.group_name)
179 'repo_group_home', repo_group_name=obj.group_name)
180 }
180 }
181 for obj in acl_iter]
181 for obj in acl_iter]
182
182
183 def _get_user_list(self, name_contains=None, limit=20):
183 def _get_user_list(self, name_contains=None, limit=20):
184 org_query = name_contains
184 org_query = name_contains
185 if not name_contains:
185 if not name_contains:
186 return []
186 return []
187
187
188 name_contains = re.compile('(?:user:)(.+)').findall(name_contains)
188 name_contains = re.compile('(?:user:)(.+)').findall(name_contains)
189 if len(name_contains) != 1:
189 if len(name_contains) != 1:
190 return []
190 return []
191 name_contains = name_contains[0]
191 name_contains = name_contains[0]
192
192
193 query = User.query()\
193 query = User.query()\
194 .order_by(func.length(User.username))\
194 .order_by(func.length(User.username))\
195 .order_by(User.username) \
195 .order_by(User.username) \
196 .filter(User.username != User.DEFAULT_USER)
196 .filter(User.username != User.DEFAULT_USER)
197
197
198 if name_contains:
198 if name_contains:
199 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
199 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
200 query = query.filter(
200 query = query.filter(
201 User.username.ilike(ilike_expression))
201 User.username.ilike(ilike_expression))
202 query = query.limit(limit)
202 query = query.limit(limit)
203
203
204 acl_iter = query
204 acl_iter = query
205
205
206 return [
206 return [
207 {
207 {
208 'id': obj.user_id,
208 'id': obj.user_id,
209 'value': org_query,
209 'value': org_query,
210 'value_display': obj.username,
210 'value_display': obj.username,
211 'type': 'user',
211 'type': 'user',
212 'icon_link': h.gravatar_url(obj.email, 30),
212 'icon_link': h.gravatar_url(obj.email, 30),
213 'url': h.route_path(
213 'url': h.route_path(
214 'user_profile', username=obj.username)
214 'user_profile', username=obj.username)
215 }
215 }
216 for obj in acl_iter]
216 for obj in acl_iter]
217
217
218 def _get_user_groups_list(self, name_contains=None, limit=20):
218 def _get_user_groups_list(self, name_contains=None, limit=20):
219 org_query = name_contains
219 org_query = name_contains
220 if not name_contains:
220 if not name_contains:
221 return []
221 return []
222
222
223 name_contains = re.compile('(?:user_group:)(.+)').findall(name_contains)
223 name_contains = re.compile('(?:user_group:)(.+)').findall(name_contains)
224 if len(name_contains) != 1:
224 if len(name_contains) != 1:
225 return []
225 return []
226 name_contains = name_contains[0]
226 name_contains = name_contains[0]
227
227
228 query = UserGroup.query()\
228 query = UserGroup.query()\
229 .order_by(func.length(UserGroup.users_group_name))\
229 .order_by(func.length(UserGroup.users_group_name))\
230 .order_by(UserGroup.users_group_name)
230 .order_by(UserGroup.users_group_name)
231
231
232 if name_contains:
232 if name_contains:
233 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
233 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
234 query = query.filter(
234 query = query.filter(
235 UserGroup.users_group_name.ilike(ilike_expression))
235 UserGroup.users_group_name.ilike(ilike_expression))
236 query = query.limit(limit)
236 query = query.limit(limit)
237
237
238 acl_iter = query
238 acl_iter = query
239
239
240 return [
240 return [
241 {
241 {
242 'id': obj.users_group_id,
242 'id': obj.users_group_id,
243 'value': org_query,
243 'value': org_query,
244 'value_display': obj.users_group_name,
244 'value_display': obj.users_group_name,
245 'type': 'user_group',
245 'type': 'user_group',
246 'url': h.route_path(
246 'url': h.route_path(
247 'user_group_profile', user_group_name=obj.users_group_name)
247 'user_group_profile', user_group_name=obj.users_group_name)
248 }
248 }
249 for obj in acl_iter]
249 for obj in acl_iter]
250
250
251 def _get_hash_commit_list(self, auth_user, searcher, query):
251 def _get_hash_commit_list(self, auth_user, searcher, query):
252 org_query = query
252 org_query = query
253 if not query or len(query) < 3 or not searcher:
253 if not query or len(query) < 3 or not searcher:
254 return []
254 return []
255
255
256 commit_hashes = re.compile('(?:commit:)([0-9a-f]{2,40})').findall(query)
256 commit_hashes = re.compile('(?:commit:)([0-9a-f]{2,40})').findall(query)
257
257
258 if len(commit_hashes) != 1:
258 if len(commit_hashes) != 1:
259 return []
259 return []
260 commit_hash = commit_hashes[0]
260 commit_hash = commit_hashes[0]
261
261
262 result = searcher.search(
262 result = searcher.search(
263 'commit_id:{}*'.format(commit_hash), 'commit', auth_user,
263 'commit_id:{}*'.format(commit_hash), 'commit', auth_user,
264 raise_on_exc=False)
264 raise_on_exc=False)
265
265
266 return [
266 return [
267 {
267 {
268 'id': entry['commit_id'],
268 'id': entry['commit_id'],
269 'value': org_query,
269 'value': org_query,
270 'value_display': 'repo `{}` commit: {}'.format(
270 'value_display': 'repo `{}` commit: {}'.format(
271 entry['repository'], entry['commit_id']),
271 entry['repository'], entry['commit_id']),
272 'type': 'commit',
272 'type': 'commit',
273 'repo': entry['repository'],
273 'repo': entry['repository'],
274 'url': h.route_path(
274 'url': h.route_path(
275 'repo_commit',
275 'repo_commit',
276 repo_name=entry['repository'], commit_id=entry['commit_id'])
276 repo_name=entry['repository'], commit_id=entry['commit_id'])
277 }
277 }
278 for entry in result['results']]
278 for entry in result['results']]
279
279
280 @LoginRequired()
280 @LoginRequired()
281 @view_config(
281 @view_config(
282 route_name='repo_list_data', request_method='GET',
282 route_name='repo_list_data', request_method='GET',
283 renderer='json_ext', xhr=True)
283 renderer='json_ext', xhr=True)
284 def repo_list_data(self):
284 def repo_list_data(self):
285 _ = self.request.translate
285 _ = self.request.translate
286 self.load_default_context()
286 self.load_default_context()
287
287
288 query = self.request.GET.get('query')
288 query = self.request.GET.get('query')
289 repo_type = self.request.GET.get('repo_type')
289 repo_type = self.request.GET.get('repo_type')
290 log.debug('generating repo list, query:%s, repo_type:%s',
290 log.debug('generating repo list, query:%s, repo_type:%s',
291 query, repo_type)
291 query, repo_type)
292
292
293 res = []
293 res = []
294 repos = self._get_repo_list(query, repo_type=repo_type)
294 repos = self._get_repo_list(query, repo_type=repo_type)
295 if repos:
295 if repos:
296 res.append({
296 res.append({
297 'text': _('Repositories'),
297 'text': _('Repositories'),
298 'children': repos
298 'children': repos
299 })
299 })
300
300
301 data = {
301 data = {
302 'more': False,
302 'more': False,
303 'results': res
303 'results': res
304 }
304 }
305 return data
305 return data
306
306
307 @LoginRequired()
307 @LoginRequired()
308 @view_config(
308 @view_config(
309 route_name='repo_group_list_data', request_method='GET',
309 route_name='repo_group_list_data', request_method='GET',
310 renderer='json_ext', xhr=True)
310 renderer='json_ext', xhr=True)
311 def repo_group_list_data(self):
311 def repo_group_list_data(self):
312 _ = self.request.translate
312 _ = self.request.translate
313 self.load_default_context()
313 self.load_default_context()
314
314
315 query = self.request.GET.get('query')
315 query = self.request.GET.get('query')
316
316
317 log.debug('generating repo group list, query:%s',
317 log.debug('generating repo group list, query:%s',
318 query)
318 query)
319
319
320 res = []
320 res = []
321 repo_groups = self._get_repo_group_list(query)
321 repo_groups = self._get_repo_group_list(query)
322 if repo_groups:
322 if repo_groups:
323 res.append({
323 res.append({
324 'text': _('Repository Groups'),
324 'text': _('Repository Groups'),
325 'children': repo_groups
325 'children': repo_groups
326 })
326 })
327
327
328 data = {
328 data = {
329 'more': False,
329 'more': False,
330 'results': res
330 'results': res
331 }
331 }
332 return data
332 return data
333
333
334 def _get_default_search_queries(self, search_context, searcher, query):
334 def _get_default_search_queries(self, search_context, searcher, query):
335 if not searcher:
335 if not searcher:
336 return []
336 return []
337 is_es_6 = searcher.is_es_6
337 is_es_6 = searcher.is_es_6
338
338
339 queries = []
339 queries = []
340 repo_group_name, repo_name, repo_context = None, None, None
340 repo_group_name, repo_name, repo_context = None, None, None
341
341
342 # repo group context
342 # repo group context
343 if search_context.get('search_context[repo_group_name]'):
343 if search_context.get('search_context[repo_group_name]'):
344 repo_group_name = search_context.get('search_context[repo_group_name]')
344 repo_group_name = search_context.get('search_context[repo_group_name]')
345 if search_context.get('search_context[repo_name]'):
345 if search_context.get('search_context[repo_name]'):
346 repo_name = search_context.get('search_context[repo_name]')
346 repo_name = search_context.get('search_context[repo_name]')
347 repo_context = search_context.get('search_context[repo_view_type]')
347 repo_context = search_context.get('search_context[repo_view_type]')
348
348
349 if is_es_6 and repo_name:
349 if is_es_6 and repo_name:
350 # files
350 def query_modifier():
351 def query_modifier():
351 qry = '{} repo_name.raw:{} '.format(
352 qry = query
352 query, searcher.escape_specials(repo_name))
353 return {'q': qry, 'type': 'content'}
353 return {'q': qry, 'type': 'content'}
354 label = u'Search for `{}` through files in this repository.'.format(query)
354 label = u'File search for `{}` in this repository.'.format(query)
355 queries.append(
356 {
357 'id': -10,
358 'value': query,
359 'value_display': label,
360 'type': 'search',
361 'url': h.route_path('search_repo',
362 repo_name=repo_name,
363 _query=query_modifier())
364 }
365 )
366
367 # commits
368 def query_modifier():
369 qry = query
370 return {'q': qry, 'type': 'commit'}
371
372 label = u'Commit search for `{}` in this repository.'.format(query)
355 queries.append(
373 queries.append(
356 {
374 {
357 'id': -10,
375 'id': -10,
358 'value': query,
376 'value': query,
359 'value_display': label,
377 'value_display': label,
360 'type': 'search',
378 'type': 'search',
361 'url': h.route_path(
379 'url': h.route_path('search_repo',
362 'search_repo', repo_name=repo_name, _query=query_modifier())
380 repo_name=repo_name,
363 }
381 _query=query_modifier())
364 )
365
366 def query_modifier():
367 qry = '{} repo_name.raw:{} '.format(
368 query, searcher.escape_specials(repo_name))
369 return {'q': qry, 'type': 'commit'}
370 label = u'Search for `{}` through commits in this repository.'.format(query)
371 queries.append(
372 {
373 'id': -10,
374 'value': query,
375 'value_display': label,
376 'type': 'search',
377 'url': h.route_path(
378 'search_repo', repo_name=repo_name, _query=query_modifier())
379 }
382 }
380 )
383 )
381
384
382 elif is_es_6 and repo_group_name:
385 elif is_es_6 and repo_group_name:
386 # files
383 def query_modifier():
387 def query_modifier():
384 qry = '{} repo_name.raw:{} '.format(
388 qry = query
385 query, searcher.escape_specials(repo_group_name + '/*'))
386 return {'q': qry, 'type': 'content'}
389 return {'q': qry, 'type': 'content'}
387 label = u'Search for `{}` through files in this repository group'.format(query)
390
391 label = u'File search for `{}` in this repository group'.format(query)
388 queries.append(
392 queries.append(
389 {
393 {
390 'id': -20,
394 'id': -20,
391 'value': query,
395 'value': query,
392 'value_display': label,
396 'value_display': label,
393 'type': 'search',
397 'type': 'search',
394 'url': h.route_path('search', _query=query_modifier())
398 'url': h.route_path('search_repo_group',
399 repo_group_name=repo_group_name,
400 _query=query_modifier())
401 }
402 )
403
404 # commits
405 def query_modifier():
406 qry = query
407 return {'q': qry, 'type': 'commit'}
408
409 label = u'Commit search for `{}` in this repository group'.format(query)
410 queries.append(
411 {
412 'id': -20,
413 'value': query,
414 'value_display': label,
415 'type': 'search',
416 'url': h.route_path('search_repo_group',
417 repo_group_name=repo_group_name,
418 _query=query_modifier())
395 }
419 }
396 )
420 )
397
421
398 if not queries:
422 if not queries:
399 queries.append(
423 queries.append(
400 {
424 {
401 'id': -1,
425 'id': -1,
402 'value': query,
426 'value': query,
403 'value_display': u'Search for: `{}`'.format(query),
427 'value_display': u'Search for: `{}`'.format(query),
404 'type': 'search',
428 'type': 'search',
405 'url': h.route_path('search',
429 'url': h.route_path('search',
406 _query={'q': query, 'type': 'content'})
430 _query={'q': query, 'type': 'content'})
407 }
431 }
408 )
432 )
409
433
410 return queries
434 return queries
411
435
412 @LoginRequired()
436 @LoginRequired()
413 @view_config(
437 @view_config(
414 route_name='goto_switcher_data', request_method='GET',
438 route_name='goto_switcher_data', request_method='GET',
415 renderer='json_ext', xhr=True)
439 renderer='json_ext', xhr=True)
416 def goto_switcher_data(self):
440 def goto_switcher_data(self):
417 c = self.load_default_context()
441 c = self.load_default_context()
418
442
419 _ = self.request.translate
443 _ = self.request.translate
420
444
421 query = self.request.GET.get('query')
445 query = self.request.GET.get('query')
422 log.debug('generating main filter data, query %s', query)
446 log.debug('generating main filter data, query %s', query)
423
447
424 res = []
448 res = []
425 if not query:
449 if not query:
426 return {'suggestions': res}
450 return {'suggestions': res}
427
451
428 searcher = searcher_from_config(self.request.registry.settings)
452 searcher = searcher_from_config(self.request.registry.settings)
429 for _q in self._get_default_search_queries(self.request.GET, searcher, query):
453 for _q in self._get_default_search_queries(self.request.GET, searcher, query):
430 res.append(_q)
454 res.append(_q)
431
455
432 repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]'))
456 repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]'))
433 if repo_group_id:
457 if repo_group_id:
434 repo_group = RepoGroup.get(repo_group_id)
458 repo_group = RepoGroup.get(repo_group_id)
435 composed_hint = '{}/{}'.format(repo_group.group_name, query)
459 composed_hint = '{}/{}'.format(repo_group.group_name, query)
436 show_hint = not query.startswith(repo_group.group_name)
460 show_hint = not query.startswith(repo_group.group_name)
437 if repo_group and show_hint:
461 if repo_group and show_hint:
438 hint = u'Repository search inside: `{}`'.format(composed_hint)
462 hint = u'Repository search inside: `{}`'.format(composed_hint)
439 res.append({
463 res.append({
440 'id': -1,
464 'id': -1,
441 'value': composed_hint,
465 'value': composed_hint,
442 'value_display': hint,
466 'value_display': hint,
443 'type': 'hint',
467 'type': 'hint',
444 'url': ""
468 'url': ""
445 })
469 })
446
470
447 repo_groups = self._get_repo_group_list(query)
471 repo_groups = self._get_repo_group_list(query)
448 for serialized_repo_group in repo_groups:
472 for serialized_repo_group in repo_groups:
449 res.append(serialized_repo_group)
473 res.append(serialized_repo_group)
450
474
451 repos = self._get_repo_list(query)
475 repos = self._get_repo_list(query)
452 for serialized_repo in repos:
476 for serialized_repo in repos:
453 res.append(serialized_repo)
477 res.append(serialized_repo)
454
478
455 # TODO(marcink): should all logged in users be allowed to search others?
479 # TODO(marcink): should all logged in users be allowed to search others?
456 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
480 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
457 if allowed_user_search:
481 if allowed_user_search:
458 users = self._get_user_list(query)
482 users = self._get_user_list(query)
459 for serialized_user in users:
483 for serialized_user in users:
460 res.append(serialized_user)
484 res.append(serialized_user)
461
485
462 user_groups = self._get_user_groups_list(query)
486 user_groups = self._get_user_groups_list(query)
463 for serialized_user_group in user_groups:
487 for serialized_user_group in user_groups:
464 res.append(serialized_user_group)
488 res.append(serialized_user_group)
465
489
466 commits = self._get_hash_commit_list(c.auth_user, searcher, query)
490 commits = self._get_hash_commit_list(c.auth_user, searcher, query)
467 if commits:
491 if commits:
468 unique_repos = collections.OrderedDict()
492 unique_repos = collections.OrderedDict()
469 for commit in commits:
493 for commit in commits:
470 repo_name = commit['repo']
494 repo_name = commit['repo']
471 unique_repos.setdefault(repo_name, []).append(commit)
495 unique_repos.setdefault(repo_name, []).append(commit)
472
496
473 for repo, commits in unique_repos.items():
497 for repo, commits in unique_repos.items():
474 for commit in commits:
498 for commit in commits:
475 res.append(commit)
499 res.append(commit)
476
500
477 return {'suggestions': res}
501 return {'suggestions': res}
478
502
479 def _get_groups_and_repos(self, repo_group_id=None):
503 def _get_groups_and_repos(self, repo_group_id=None):
480 # repo groups groups
504 # repo groups groups
481 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
505 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
482 _perms = ['group.read', 'group.write', 'group.admin']
506 _perms = ['group.read', 'group.write', 'group.admin']
483 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
507 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
484 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
508 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
485 repo_group_list=repo_group_list_acl, admin=False)
509 repo_group_list=repo_group_list_acl, admin=False)
486
510
487 # repositories
511 # repositories
488 repo_list = Repository.get_all_repos(group_id=repo_group_id)
512 repo_list = Repository.get_all_repos(group_id=repo_group_id)
489 _perms = ['repository.read', 'repository.write', 'repository.admin']
513 _perms = ['repository.read', 'repository.write', 'repository.admin']
490 repo_list_acl = RepoList(repo_list, perm_set=_perms)
514 repo_list_acl = RepoList(repo_list, perm_set=_perms)
491 repo_data = RepoModel().get_repos_as_dict(
515 repo_data = RepoModel().get_repos_as_dict(
492 repo_list=repo_list_acl, admin=False)
516 repo_list=repo_list_acl, admin=False)
493
517
494 return repo_data, repo_group_data
518 return repo_data, repo_group_data
495
519
496 @LoginRequired()
520 @LoginRequired()
497 @view_config(
521 @view_config(
498 route_name='home', request_method='GET',
522 route_name='home', request_method='GET',
499 renderer='rhodecode:templates/index.mako')
523 renderer='rhodecode:templates/index.mako')
500 def main_page(self):
524 def main_page(self):
501 c = self.load_default_context()
525 c = self.load_default_context()
502 c.repo_group = None
526 c.repo_group = None
503
527
504 repo_data, repo_group_data = self._get_groups_and_repos()
528 repo_data, repo_group_data = self._get_groups_and_repos()
505 # json used to render the grids
529 # json used to render the grids
506 c.repos_data = json.dumps(repo_data)
530 c.repos_data = json.dumps(repo_data)
507 c.repo_groups_data = json.dumps(repo_group_data)
531 c.repo_groups_data = json.dumps(repo_group_data)
508
532
509 return self._get_template_context(c)
533 return self._get_template_context(c)
510
534
511 @LoginRequired()
535 @LoginRequired()
512 @HasRepoGroupPermissionAnyDecorator(
536 @HasRepoGroupPermissionAnyDecorator(
513 'group.read', 'group.write', 'group.admin')
537 'group.read', 'group.write', 'group.admin')
514 @view_config(
538 @view_config(
515 route_name='repo_group_home', request_method='GET',
539 route_name='repo_group_home', request_method='GET',
516 renderer='rhodecode:templates/index_repo_group.mako')
540 renderer='rhodecode:templates/index_repo_group.mako')
517 @view_config(
541 @view_config(
518 route_name='repo_group_home_slash', request_method='GET',
542 route_name='repo_group_home_slash', request_method='GET',
519 renderer='rhodecode:templates/index_repo_group.mako')
543 renderer='rhodecode:templates/index_repo_group.mako')
520 def repo_group_main_page(self):
544 def repo_group_main_page(self):
521 c = self.load_default_context()
545 c = self.load_default_context()
522 c.repo_group = self.request.db_repo_group
546 c.repo_group = self.request.db_repo_group
523 repo_data, repo_group_data = self._get_groups_and_repos(
547 repo_data, repo_group_data = self._get_groups_and_repos(
524 c.repo_group.group_id)
548 c.repo_group.group_id)
525
549
526 # json used to render the grids
550 # json used to render the grids
527 c.repos_data = json.dumps(repo_data)
551 c.repos_data = json.dumps(repo_data)
528 c.repo_groups_data = json.dumps(repo_group_data)
552 c.repo_groups_data = json.dumps(repo_group_data)
529
553
530 return self._get_template_context(c)
554 return self._get_template_context(c)
531
555
532 @LoginRequired()
556 @LoginRequired()
533 @CSRFRequired()
557 @CSRFRequired()
534 @view_config(
558 @view_config(
535 route_name='markup_preview', request_method='POST',
559 route_name='markup_preview', request_method='POST',
536 renderer='string', xhr=True)
560 renderer='string', xhr=True)
537 def markup_preview(self):
561 def markup_preview(self):
538 # Technically a CSRF token is not needed as no state changes with this
562 # Technically a CSRF token is not needed as no state changes with this
539 # call. However, as this is a POST is better to have it, so automated
563 # call. However, as this is a POST is better to have it, so automated
540 # tools don't flag it as potential CSRF.
564 # tools don't flag it as potential CSRF.
541 # Post is required because the payload could be bigger than the maximum
565 # Post is required because the payload could be bigger than the maximum
542 # allowed by GET.
566 # allowed by GET.
543
567
544 text = self.request.POST.get('text')
568 text = self.request.POST.get('text')
545 renderer = self.request.POST.get('renderer') or 'rst'
569 renderer = self.request.POST.get('renderer') or 'rst'
546 if text:
570 if text:
547 return h.render(text, renderer=renderer, mentions=True)
571 return h.render(text, renderer=renderer, mentions=True)
548 return ''
572 return ''
549
573
550 @LoginRequired()
574 @LoginRequired()
551 @CSRFRequired()
575 @CSRFRequired()
552 @view_config(
576 @view_config(
553 route_name='store_user_session_value', request_method='POST',
577 route_name='store_user_session_value', request_method='POST',
554 renderer='string', xhr=True)
578 renderer='string', xhr=True)
555 def store_user_session_attr(self):
579 def store_user_session_attr(self):
556 key = self.request.POST.get('key')
580 key = self.request.POST.get('key')
557 val = self.request.POST.get('val')
581 val = self.request.POST.get('val')
558
582
559 existing_value = self.request.session.get(key)
583 existing_value = self.request.session.get(key)
560 if existing_value != val:
584 if existing_value != val:
561 self.request.session[key] = val
585 self.request.session[key] = val
562
586
563 return 'stored:{}'.format(key)
587 return 'stored:{}'.format(key)
@@ -1,34 +1,43 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 from rhodecode.apps._base import ADMIN_PREFIX
20 from rhodecode.apps._base import ADMIN_PREFIX
21
21
22
22
23 def includeme(config):
23 def includeme(config):
24
24
25 config.add_route(
25 config.add_route(
26 name='search',
26 name='search',
27 pattern=ADMIN_PREFIX + '/search')
27 pattern=ADMIN_PREFIX + '/search')
28
28
29 config.add_route(
29 config.add_route(
30 name='search_repo',
30 name='search_repo',
31 pattern='/{repo_name:.*?[^/]}/_search', repo_route=True)
32
33 config.add_route(
34 name='search_repo_alt',
31 pattern='/{repo_name:.*?[^/]}/search', repo_route=True)
35 pattern='/{repo_name:.*?[^/]}/search', repo_route=True)
32
36
37 config.add_route(
38 name='search_repo_group',
39 pattern='/{repo_group_name:.*?[^/]}/_search',
40 repo_group_route=True)
41
33 # Scan module for configuration decorators.
42 # Scan module for configuration decorators.
34 config.scan('.views', ignore='.tests')
43 config.scan('.views', ignore='.tests')
@@ -1,138 +1,161 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
26 from rhodecode.apps._base import BaseAppView, RepoAppView, RepoGroupAppView
27 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
27 from rhodecode.lib.auth import (
28 LoginRequired, HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator)
28 from rhodecode.lib.helpers import Page
29 from rhodecode.lib.helpers import Page
29 from rhodecode.lib.utils2 import safe_str
30 from rhodecode.lib.utils2 import safe_str
30 from rhodecode.lib.index import searcher_from_config
31 from rhodecode.lib.index import searcher_from_config
31 from rhodecode.model import validation_schema
32 from rhodecode.model import validation_schema
32 from rhodecode.model.validation_schema.schemas import search_schema
33 from rhodecode.model.validation_schema.schemas import search_schema
33
34
34 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
35
36
36
37
37 def search(request, tmpl_context, repo_name):
38 def perform_search(request, tmpl_context, repo_name=None, repo_group_name=None):
38 searcher = searcher_from_config(request.registry.settings)
39 searcher = searcher_from_config(request.registry.settings)
39 formatted_results = []
40 formatted_results = []
40 execution_time = ''
41 execution_time = ''
41
42
42 schema = search_schema.SearchParamsSchema()
43 schema = search_schema.SearchParamsSchema()
43
44
44 search_params = {}
45 search_params = {}
45 errors = []
46 errors = []
46 try:
47 try:
47 search_params = schema.deserialize(
48 search_params = schema.deserialize(
48 dict(
49 dict(
49 search_query=request.GET.get('q'),
50 search_query=request.GET.get('q'),
50 search_type=request.GET.get('type'),
51 search_type=request.GET.get('type'),
51 search_sort=request.GET.get('sort'),
52 search_sort=request.GET.get('sort'),
52 search_max_lines=request.GET.get('max_lines'),
53 search_max_lines=request.GET.get('max_lines'),
53 page_limit=request.GET.get('page_limit'),
54 page_limit=request.GET.get('page_limit'),
54 requested_page=request.GET.get('page'),
55 requested_page=request.GET.get('page'),
55 )
56 )
56 )
57 )
57 except validation_schema.Invalid as e:
58 except validation_schema.Invalid as e:
58 errors = e.children
59 errors = e.children
59
60
60 def url_generator(**kw):
61 def url_generator(**kw):
61 q = urllib.quote(safe_str(search_query))
62 q = urllib.quote(safe_str(search_query))
62 return update_params(
63 return update_params(
63 "?q=%s&type=%s&max_lines=%s" % (q, safe_str(search_type), search_max_lines), **kw)
64 "?q=%s&type=%s&max_lines=%s" % (
65 q, safe_str(search_type), search_max_lines), **kw)
64
66
65 c = tmpl_context
67 c = tmpl_context
66 search_query = search_params.get('search_query')
68 search_query = search_params.get('search_query')
67 search_type = search_params.get('search_type')
69 search_type = search_params.get('search_type')
68 search_sort = search_params.get('search_sort')
70 search_sort = search_params.get('search_sort')
69 search_max_lines = search_params.get('search_max_lines')
71 search_max_lines = search_params.get('search_max_lines')
70 if search_params.get('search_query'):
72 if search_params.get('search_query'):
71 page_limit = search_params['page_limit']
73 page_limit = search_params['page_limit']
72 requested_page = search_params['requested_page']
74 requested_page = search_params['requested_page']
73
75
74 try:
76 try:
75 search_result = searcher.search(
77 search_result = searcher.search(
76 search_query, search_type, c.auth_user, repo_name,
78 search_query, search_type, c.auth_user, repo_name, repo_group_name,
77 requested_page, page_limit, search_sort)
79 requested_page=requested_page, page_limit=page_limit, sort=search_sort)
78
80
79 formatted_results = Page(
81 formatted_results = Page(
80 search_result['results'], page=requested_page,
82 search_result['results'], page=requested_page,
81 item_count=search_result['count'],
83 item_count=search_result['count'],
82 items_per_page=page_limit, url=url_generator)
84 items_per_page=page_limit, url=url_generator)
83 finally:
85 finally:
84 searcher.cleanup()
86 searcher.cleanup()
85
87
86 if not search_result['error']:
88 if not search_result['error']:
87 execution_time = '%s results (%.3f seconds)' % (
89 execution_time = '%s results (%.3f seconds)' % (
88 search_result['count'],
90 search_result['count'],
89 search_result['runtime'])
91 search_result['runtime'])
90 elif not errors:
92 elif not errors:
91 node = schema['search_query']
93 node = schema['search_query']
92 errors = [
94 errors = [
93 validation_schema.Invalid(node, search_result['error'])]
95 validation_schema.Invalid(node, search_result['error'])]
94
96
95 c.perm_user = c.auth_user
97 c.perm_user = c.auth_user
96 c.repo_name = repo_name
98 c.repo_name = repo_name
99 c.repo_group_name = repo_group_name
97 c.sort = search_sort
100 c.sort = search_sort
98 c.url_generator = url_generator
101 c.url_generator = url_generator
99 c.errors = errors
102 c.errors = errors
100 c.formatted_results = formatted_results
103 c.formatted_results = formatted_results
101 c.runtime = execution_time
104 c.runtime = execution_time
102 c.cur_query = search_query
105 c.cur_query = search_query
103 c.search_type = search_type
106 c.search_type = search_type
104 c.searcher = searcher
107 c.searcher = searcher
105
108
106
109
107 class SearchView(BaseAppView):
110 class SearchView(BaseAppView):
108 def load_default_context(self):
111 def load_default_context(self):
109 c = self._get_local_tmpl_context()
112 c = self._get_local_tmpl_context()
110
111 return c
113 return c
112
114
113 @LoginRequired()
115 @LoginRequired()
114 @view_config(
116 @view_config(
115 route_name='search', request_method='GET',
117 route_name='search', request_method='GET',
116 renderer='rhodecode:templates/search/search.mako')
118 renderer='rhodecode:templates/search/search.mako')
117 def search(self):
119 def search(self):
118 c = self.load_default_context()
120 c = self.load_default_context()
119 search(self.request, c, repo_name=None)
121 perform_search(self.request, c)
120 return self._get_template_context(c)
122 return self._get_template_context(c)
121
123
122
124
123 class SearchRepoView(RepoAppView):
125 class SearchRepoView(RepoAppView):
124 def load_default_context(self):
126 def load_default_context(self):
125 c = self._get_local_tmpl_context()
127 c = self._get_local_tmpl_context()
126 c.active = 'search'
128 c.active = 'search'
127 return c
129 return c
128
130
129 @LoginRequired()
131 @LoginRequired()
130 @HasRepoPermissionAnyDecorator(
132 @HasRepoPermissionAnyDecorator(
131 'repository.read', 'repository.write', 'repository.admin')
133 'repository.read', 'repository.write', 'repository.admin')
132 @view_config(
134 @view_config(
133 route_name='search_repo', request_method='GET',
135 route_name='search_repo', request_method='GET',
134 renderer='rhodecode:templates/search/search.mako')
136 renderer='rhodecode:templates/search/search.mako')
137 @view_config(
138 route_name='search_repo_alt', request_method='GET',
139 renderer='rhodecode:templates/search/search.mako')
135 def search_repo(self):
140 def search_repo(self):
136 c = self.load_default_context()
141 c = self.load_default_context()
137 search(self.request, c, repo_name=self.db_repo_name)
142 perform_search(self.request, c, repo_name=self.db_repo_name)
138 return self._get_template_context(c)
143 return self._get_template_context(c)
144
145
146 class SearchRepoGroupView(RepoGroupAppView):
147 def load_default_context(self):
148 c = self._get_local_tmpl_context()
149 c.active = 'search'
150 return c
151
152 @LoginRequired()
153 @HasRepoGroupPermissionAnyDecorator(
154 'group.read', 'group.write', 'group.admin')
155 @view_config(
156 route_name='search_repo_group', request_method='GET',
157 renderer='rhodecode:templates/search/search.mako')
158 def search_repo_group(self):
159 c = self.load_default_context()
160 perform_search(self.request, c, repo_group_name=self.db_repo_group_name)
161 return self._get_template_context(c)
@@ -1,98 +1,99 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
49
50 def __init__(self):
50 def __init__(self):
51 pass
51 pass
52
52
53 def cleanup(self):
53 def cleanup(self):
54 pass
54 pass
55
55
56 def search(self, query, document_type, search_user, repo_name=None,
56 def search(self, query, document_type, search_user,
57 repo_name=None, repo_group_name=None,
57 raise_on_exc=True):
58 raise_on_exc=True):
58 raise Exception('NotImplemented')
59 raise Exception('NotImplemented')
59
60
60 @staticmethod
61 @staticmethod
61 def query_to_mark(query, default_field=None):
62 def query_to_mark(query, default_field=None):
62 """
63 """
63 Formats the query to mark token for jquery.mark.js highlighting. ES could
64 Formats the query to mark token for jquery.mark.js highlighting. ES could
64 have a different format optionally.
65 have a different format optionally.
65
66
66 :param default_field:
67 :param default_field:
67 :param query:
68 :param query:
68 """
69 """
69 return ' '.join(normalize_text_for_matching(query).split())
70 return ' '.join(normalize_text_for_matching(query).split())
70
71
71 @property
72 @property
72 def is_es_6(self):
73 def is_es_6(self):
73 return self.es_version == ES_VERSION_6
74 return self.es_version == ES_VERSION_6
74
75
75 def get_handlers(self):
76 def get_handlers(self):
76 return {}
77 return {}
77
78
78
79
79 def search_config(config, prefix='search.'):
80 def search_config(config, prefix='search.'):
80 _config = {}
81 _config = {}
81 for key in config.keys():
82 for key in config.keys():
82 if key.startswith(prefix):
83 if key.startswith(prefix):
83 _config[key[len(prefix):]] = config[key]
84 _config[key[len(prefix):]] = config[key]
84 return _config
85 return _config
85
86
86
87
87 def searcher_from_config(config, prefix='search.'):
88 def searcher_from_config(config, prefix='search.'):
88 _config = search_config(config, prefix)
89 _config = search_config(config, prefix)
89
90
90 if 'location' not in _config:
91 if 'location' not in _config:
91 _config['location'] = default_location
92 _config['location'] = default_location
92 if 'es_version' not in _config:
93 if 'es_version' not in _config:
93 # use old legacy ES version set to 2
94 # use old legacy ES version set to 2
94 _config['es_version'] = '2'
95 _config['es_version'] = '2'
95
96
96 imported = importlib.import_module(_config.get('module', default_searcher))
97 imported = importlib.import_module(_config.get('module', default_searcher))
97 searcher = imported.Searcher(config=_config)
98 searcher = imported.Searcher(config=_config)
98 return searcher
99 return searcher
@@ -1,286 +1,286 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 search(self, query, document_type, search_user,
102 def search(self, query, document_type, search_user,
103 repo_name=None, requested_page=1, page_limit=10, sort=None,
103 repo_name=None, repo_group_name=None,
104 raise_on_exc=True):
104 requested_page=1, page_limit=10, sort=None, raise_on_exc=True):
105
105
106 original_query = query
106 original_query = query
107 query = self._extend_query(query)
107 query = self._extend_query(query)
108
108
109 log.debug(u'QUERY: %s on %s', query, document_type)
109 log.debug(u'QUERY: %s on %s', query, document_type)
110 result = {
110 result = {
111 'results': [],
111 'results': [],
112 'count': 0,
112 'count': 0,
113 'error': None,
113 'error': None,
114 'runtime': 0
114 'runtime': 0
115 }
115 }
116 search_type, index_name, schema_defn = self._prepare_for_search(
116 search_type, index_name, schema_defn = self._prepare_for_search(
117 document_type)
117 document_type)
118 self._init_searcher(index_name)
118 self._init_searcher(index_name)
119 try:
119 try:
120 qp = QueryParser(search_type, schema=schema_defn)
120 qp = QueryParser(search_type, schema=schema_defn)
121 allowed_repos_filter = self._get_repo_filter(
121 allowed_repos_filter = self._get_repo_filter(
122 search_user, repo_name)
122 search_user, repo_name)
123 try:
123 try:
124 query = qp.parse(safe_unicode(query))
124 query = qp.parse(safe_unicode(query))
125 log.debug('query: %s (%s)', query, repr(query))
125 log.debug('query: %s (%s)', query, repr(query))
126
126
127 reverse, sortedby = False, None
127 reverse, sortedby = False, None
128 if search_type == 'message':
128 if search_type == 'message':
129 if sort == 'oldfirst':
129 if sort == 'oldfirst':
130 sortedby = 'date'
130 sortedby = 'date'
131 reverse = False
131 reverse = False
132 elif sort == 'newfirst':
132 elif sort == 'newfirst':
133 sortedby = 'date'
133 sortedby = 'date'
134 reverse = True
134 reverse = True
135
135
136 whoosh_results = self.searcher.search(
136 whoosh_results = self.searcher.search(
137 query, filter=allowed_repos_filter, limit=None,
137 query, filter=allowed_repos_filter, limit=None,
138 sortedby=sortedby, reverse=reverse)
138 sortedby=sortedby, reverse=reverse)
139
139
140 # fixes for 32k limit that whoosh uses for highlight
140 # fixes for 32k limit that whoosh uses for highlight
141 whoosh_results.fragmenter.charlimit = None
141 whoosh_results.fragmenter.charlimit = None
142 res_ln = whoosh_results.scored_length()
142 res_ln = whoosh_results.scored_length()
143 result['runtime'] = whoosh_results.runtime
143 result['runtime'] = whoosh_results.runtime
144 result['count'] = res_ln
144 result['count'] = res_ln
145 result['results'] = WhooshResultWrapper(
145 result['results'] = WhooshResultWrapper(
146 search_type, res_ln, whoosh_results)
146 search_type, res_ln, whoosh_results)
147
147
148 except QueryParserError:
148 except QueryParserError:
149 result['error'] = 'Invalid search query. Try quoting it.'
149 result['error'] = 'Invalid search query. Try quoting it.'
150 except (EmptyIndexError, IOError, OSError):
150 except (EmptyIndexError, IOError, OSError):
151 msg = 'There is no index to search in. Please run whoosh indexer'
151 msg = 'There is no index to search in. Please run whoosh indexer'
152 log.exception(msg)
152 log.exception(msg)
153 result['error'] = msg
153 result['error'] = msg
154 except Exception:
154 except Exception:
155 msg = 'An error occurred during this search operation'
155 msg = 'An error occurred during this search operation'
156 log.exception(msg)
156 log.exception(msg)
157 result['error'] = msg
157 result['error'] = msg
158
158
159 return result
159 return result
160
160
161 def statistics(self, translator):
161 def statistics(self, translator):
162 _ = translator
162 _ = translator
163 stats = [
163 stats = [
164 {'key': _('Index Type'), 'value': 'Whoosh'},
164 {'key': _('Index Type'), 'value': 'Whoosh'},
165 {'sep': True},
165 {'sep': True},
166
166
167 {'key': _('File Index'), 'value': str(self.file_index)},
167 {'key': _('File Index'), 'value': str(self.file_index)},
168 {'key': _('Indexed documents'), 'value': self.file_index.doc_count()},
168 {'key': _('Indexed documents'), 'value': self.file_index.doc_count()},
169 {'key': _('Last update'), 'value': h.time_to_datetime(self.file_index.last_modified())},
169 {'key': _('Last update'), 'value': h.time_to_datetime(self.file_index.last_modified())},
170
170
171 {'sep': True},
171 {'sep': True},
172
172
173 {'key': _('Commit index'), 'value': str(self.commit_index)},
173 {'key': _('Commit index'), 'value': str(self.commit_index)},
174 {'key': _('Indexed documents'), 'value': str(self.commit_index.doc_count())},
174 {'key': _('Indexed documents'), 'value': str(self.commit_index.doc_count())},
175 {'key': _('Last update'), 'value': h.time_to_datetime(self.commit_index.last_modified())}
175 {'key': _('Last update'), 'value': h.time_to_datetime(self.commit_index.last_modified())}
176 ]
176 ]
177 return stats
177 return stats
178
178
179 def _get_repo_filter(self, auth_user, repo_name):
179 def _get_repo_filter(self, auth_user, repo_name):
180
180
181 allowed_to_search = [
181 allowed_to_search = [
182 repo for repo, perm in
182 repo for repo, perm in
183 auth_user.permissions['repositories'].items()
183 auth_user.permissions['repositories'].items()
184 if perm != 'repository.none']
184 if perm != 'repository.none']
185
185
186 if repo_name:
186 if repo_name:
187 repo_filter = [query_lib.Term('repository', repo_name)]
187 repo_filter = [query_lib.Term('repository', repo_name)]
188
188
189 elif 'hg.admin' in auth_user.permissions.get('global', []):
189 elif 'hg.admin' in auth_user.permissions.get('global', []):
190 return None
190 return None
191
191
192 else:
192 else:
193 repo_filter = [query_lib.Term('repository', _rn)
193 repo_filter = [query_lib.Term('repository', _rn)
194 for _rn in allowed_to_search]
194 for _rn in allowed_to_search]
195 # in case we're not allowed to search anywhere, it's a trick
195 # in case we're not allowed to search anywhere, it's a trick
196 # to tell whoosh we're filtering, on ALL results
196 # to tell whoosh we're filtering, on ALL results
197 repo_filter = repo_filter or [query_lib.Term('repository', '')]
197 repo_filter = repo_filter or [query_lib.Term('repository', '')]
198
198
199 return query_lib.Or(repo_filter)
199 return query_lib.Or(repo_filter)
200
200
201 def _prepare_for_search(self, cur_type):
201 def _prepare_for_search(self, cur_type):
202 search_type = {
202 search_type = {
203 'content': 'content',
203 'content': 'content',
204 'commit': 'message',
204 'commit': 'message',
205 'path': 'path',
205 'path': 'path',
206 'repository': 'repository'
206 'repository': 'repository'
207 }.get(cur_type, 'content')
207 }.get(cur_type, 'content')
208
208
209 index_name = {
209 index_name = {
210 'content': FILE_INDEX_NAME,
210 'content': FILE_INDEX_NAME,
211 'commit': COMMIT_INDEX_NAME,
211 'commit': COMMIT_INDEX_NAME,
212 'path': FILE_INDEX_NAME
212 'path': FILE_INDEX_NAME
213 }.get(cur_type, FILE_INDEX_NAME)
213 }.get(cur_type, FILE_INDEX_NAME)
214
214
215 schema_defn = {
215 schema_defn = {
216 'content': self.file_schema,
216 'content': self.file_schema,
217 'commit': self.commit_schema,
217 'commit': self.commit_schema,
218 'path': self.file_schema
218 'path': self.file_schema
219 }.get(cur_type, self.file_schema)
219 }.get(cur_type, self.file_schema)
220
220
221 log.debug('IDX: %s', index_name)
221 log.debug('IDX: %s', index_name)
222 log.debug('SCHEMA: %s', schema_defn)
222 log.debug('SCHEMA: %s', schema_defn)
223 return search_type, index_name, schema_defn
223 return search_type, index_name, schema_defn
224
224
225 def _init_searcher(self, index_name):
225 def _init_searcher(self, index_name):
226 idx = open_dir(self.config['location'], indexname=index_name)
226 idx = open_dir(self.config['location'], indexname=index_name)
227 self.searcher = idx.searcher()
227 self.searcher = idx.searcher()
228 return self.searcher
228 return self.searcher
229
229
230
230
231 Searcher = WhooshSearcher
231 Searcher = WhooshSearcher
232
232
233
233
234 class WhooshResultWrapper(object):
234 class WhooshResultWrapper(object):
235 def __init__(self, search_type, total_hits, results):
235 def __init__(self, search_type, total_hits, results):
236 self.search_type = search_type
236 self.search_type = search_type
237 self.results = results
237 self.results = results
238 self.total_hits = total_hits
238 self.total_hits = total_hits
239
239
240 def __str__(self):
240 def __str__(self):
241 return '<%s at %s>' % (self.__class__.__name__, len(self))
241 return '<%s at %s>' % (self.__class__.__name__, len(self))
242
242
243 def __repr__(self):
243 def __repr__(self):
244 return self.__str__()
244 return self.__str__()
245
245
246 def __len__(self):
246 def __len__(self):
247 return self.total_hits
247 return self.total_hits
248
248
249 def __iter__(self):
249 def __iter__(self):
250 """
250 """
251 Allows Iteration over results,and lazy generate content
251 Allows Iteration over results,and lazy generate content
252
252
253 *Requires* implementation of ``__getitem__`` method.
253 *Requires* implementation of ``__getitem__`` method.
254 """
254 """
255 for hit in self.results:
255 for hit in self.results:
256 yield self.get_full_content(hit)
256 yield self.get_full_content(hit)
257
257
258 def __getitem__(self, key):
258 def __getitem__(self, key):
259 """
259 """
260 Slicing of resultWrapper
260 Slicing of resultWrapper
261 """
261 """
262 i, j = key.start, key.stop
262 i, j = key.start, key.stop
263 for hit in self.results[i:j]:
263 for hit in self.results[i:j]:
264 yield self.get_full_content(hit)
264 yield self.get_full_content(hit)
265
265
266 def get_full_content(self, hit):
266 def get_full_content(self, hit):
267 # TODO: marcink: this feels like an overkill, there's a lot of data
267 # TODO: marcink: this feels like an overkill, there's a lot of data
268 # inside hit object, and we don't need all
268 # inside hit object, and we don't need all
269 res = dict(hit)
269 res = dict(hit)
270 # elastic search uses that, we set it empty so it fallbacks to regular HL logic
270 # elastic search uses that, we set it empty so it fallbacks to regular HL logic
271 res['content_highlight'] = ''
271 res['content_highlight'] = ''
272
272
273 f_path = '' # pragma: no cover
273 f_path = '' # pragma: no cover
274 if self.search_type in ['content', 'path']:
274 if self.search_type in ['content', 'path']:
275 f_path = res['path'][len(res['repository']):]
275 f_path = res['path'][len(res['repository']):]
276 f_path = f_path.lstrip(os.sep)
276 f_path = f_path.lstrip(os.sep)
277
277
278 if self.search_type == 'content':
278 if self.search_type == 'content':
279 res.update({'content_short_hl': hit.highlights('content'),
279 res.update({'content_short_hl': hit.highlights('content'),
280 'f_path': f_path})
280 'f_path': f_path})
281 elif self.search_type == 'path':
281 elif self.search_type == 'path':
282 res.update({'f_path': f_path})
282 res.update({'f_path': f_path})
283 elif self.search_type == 'message':
283 elif self.search_type == 'message':
284 res.update({'message_hl': hit.highlights('message')})
284 res.update({'message_hl': hit.highlights('message')})
285
285
286 return res
286 return res
General Comments 0
You need to be logged in to leave comments. Login now