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