##// END OF EJS Templates
search: moved search into pyramid views.
marcink -
r1685:0f027159 default
parent child Browse files
Show More
@@ -0,0 +1,44 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
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
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 from rhodecode.apps._base import ADMIN_PREFIX
21
22
23 def includeme(config):
24
25 config.add_route(
26 name='search',
27 pattern=ADMIN_PREFIX + '/search')
28
29 config.add_route(
30 name='search_repo',
31 pattern='/{repo_name:.*?[^/]}/search', repo_route=True)
32
33 # Scan module for configuration decorators.
34 config.scan()
35
36
37 # # FULL TEXT SEARCH
38 # rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
39 # controller='search')
40 # rmap.connect('search_repo_home', '/{repo_name}/search',
41 # controller='search',
42 # action='index',
43 # conditions={'function': check_repo},
44 # requirements=URL_NAME_REQUIREMENTS) No newline at end of file
1 NO CONTENT: new file 100644
@@ -0,0 +1,202 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
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
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import os
22
23 import mock
24 import pytest
25 from whoosh import query
26
27 from rhodecode.tests import (
28 TestController, SkipTest, HG_REPO,
29 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
30 from rhodecode.tests.utils import AssertResponse
31
32
33 def route_path(name, **kwargs):
34 from rhodecode.apps._base import ADMIN_PREFIX
35 return {
36 'search':
37 ADMIN_PREFIX + '/search',
38 'search_repo':
39 '/{repo_name}/search',
40
41 }[name].format(**kwargs)
42
43
44 class TestSearchController(TestController):
45
46 def test_index(self):
47 self.log_user()
48 response = self.app.get(route_path('search'))
49 assert_response = AssertResponse(response)
50 assert_response.one_element_exists('input#q')
51
52 def test_search_files_empty_search(self):
53 if os.path.isdir(self.index_location):
54 raise SkipTest('skipped due to existing index')
55 else:
56 self.log_user()
57 response = self.app.get(route_path('search'),
58 {'q': HG_REPO})
59 response.mustcontain('There is no index to search in. '
60 'Please run whoosh indexer')
61
62 def test_search_validation(self):
63 self.log_user()
64 response = self.app.get(route_path('search'),
65 {'q': query, 'type': 'content', 'page_limit': 1000})
66
67 response.mustcontain(
68 'page_limit - 1000 is greater than maximum value 500')
69
70 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
71 ('todo', 23, [
72 'vcs/backends/hg/inmemory.py',
73 'vcs/tests/test_git.py']),
74 ('extension:rst installation', 6, [
75 'docs/index.rst',
76 'docs/installation.rst']),
77 ('def repo', 87, [
78 'vcs/tests/test_git.py',
79 'vcs/tests/test_changesets.py']),
80 ('repository:%s def test' % HG_REPO, 18, [
81 'vcs/tests/test_git.py',
82 'vcs/tests/test_changesets.py']),
83 ('"def main"', 9, [
84 'vcs/__init__.py',
85 'vcs/tests/__init__.py',
86 'vcs/utils/progressbar.py']),
87 ('owner:test_admin', 358, [
88 'vcs/tests/base.py',
89 'MANIFEST.in',
90 'vcs/utils/termcolors.py',
91 'docs/theme/ADC/static/documentation.png']),
92 ('owner:test_admin def main', 72, [
93 'vcs/__init__.py',
94 'vcs/tests/test_utils_filesize.py',
95 'vcs/tests/test_cli.py']),
96 ('owner:michał test', 0, []),
97 ])
98 def test_search_files(self, query, expected_hits, expected_paths):
99 self.log_user()
100 response = self.app.get(route_path('search'),
101 {'q': query, 'type': 'content', 'page_limit': 500})
102
103 response.mustcontain('%s results' % expected_hits)
104 for path in expected_paths:
105 response.mustcontain(path)
106
107 @pytest.mark.parametrize("query, expected_hits, expected_commits", [
108 ('bother to ask where to fetch repo during tests', 3, [
109 ('hg', 'a00c1b6f5d7a6ae678fd553a8b81d92367f7ecf1'),
110 ('git', 'c6eb379775c578a95dad8ddab53f963b80894850'),
111 ('svn', '98')]),
112 ('michał', 0, []),
113 ('changed:tests/utils.py', 36, [
114 ('hg', 'a00c1b6f5d7a6ae678fd553a8b81d92367f7ecf1')]),
115 ('changed:vcs/utils/archivers.py', 11, [
116 ('hg', '25213a5fbb048dff8ba65d21e466a835536e5b70'),
117 ('hg', '47aedd538bf616eedcb0e7d630ea476df0e159c7'),
118 ('hg', 'f5d23247fad4856a1dabd5838afade1e0eed24fb'),
119 ('hg', '04ad456aefd6461aea24f90b63954b6b1ce07b3e'),
120 ('git', 'c994f0de03b2a0aa848a04fc2c0d7e737dba31fc'),
121 ('git', 'd1f898326327e20524fe22417c22d71064fe54a1'),
122 ('git', 'fe568b4081755c12abf6ba673ba777fc02a415f3'),
123 ('git', 'bafe786f0d8c2ff7da5c1dcfcfa577de0b5e92f1')]),
124 ('added:README.rst', 3, [
125 ('hg', '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb'),
126 ('git', 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'),
127 ('svn', '8')]),
128 ('changed:lazy.py', 15, [
129 ('hg', 'eaa291c5e6ae6126a203059de9854ccf7b5baa12'),
130 ('git', '17438a11f72b93f56d0e08e7d1fa79a378578a82'),
131 ('svn', '82'),
132 ('svn', '262'),
133 ('hg', 'f5d23247fad4856a1dabd5838afade1e0eed24fb'),
134 ('git', '33fa3223355104431402a888fa77a4e9956feb3e')
135 ]),
136 ('author:marcin@python-blog.com '
137 'commit_id:b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [
138 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
139 ('b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [
140 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
141 ('b986218b', 1, [
142 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
143 ])
144 def test_search_commit_messages(
145 self, query, expected_hits, expected_commits, enabled_backends):
146 self.log_user()
147 response = self.app.get(route_path('search'),
148 {'q': query, 'type': 'commit', 'page_limit': 500})
149
150 response.mustcontain('%s results' % expected_hits)
151 for backend, commit_id in expected_commits:
152 if backend in enabled_backends:
153 response.mustcontain(commit_id)
154
155 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
156 ('readme.rst', 3, []),
157 ('test*', 75, []),
158 ('*model*', 1, []),
159 ('extension:rst', 48, []),
160 ('extension:rst api', 24, []),
161 ])
162 def test_search_file_paths(self, query, expected_hits, expected_paths):
163 self.log_user()
164 response = self.app.get(route_path('search'),
165 {'q': query, 'type': 'path', 'page_limit': 500})
166
167 response.mustcontain('%s results' % expected_hits)
168 for path in expected_paths:
169 response.mustcontain(path)
170
171 def test_search_commit_message_specific_repo(self, backend):
172 self.log_user()
173 response = self.app.get(
174 route_path('search_repo',repo_name=backend.repo_name),
175 {'q': 'bother to ask where to fetch repo during tests',
176 'type': 'commit'})
177
178 response.mustcontain('1 results')
179
180 def test_filters_are_not_applied_for_admin_user(self):
181 self.log_user()
182 with mock.patch('whoosh.searching.Searcher.search') as search_mock:
183 self.app.get(route_path('search'),
184 {'q': 'test query', 'type': 'commit'})
185 assert search_mock.call_count == 1
186 _, kwargs = search_mock.call_args
187 assert kwargs['filter'] is None
188
189 def test_filters_are_applied_for_normal_user(self, enabled_backends):
190 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
191 with mock.patch('whoosh.searching.Searcher.search') as search_mock:
192 self.app.get(route_path('search'),
193 {'q': 'test query', 'type': 'commit'})
194 assert search_mock.call_count == 1
195 _, kwargs = search_mock.call_args
196 assert isinstance(kwargs['filter'], query.Or)
197 expected_repositories = [
198 'vcs_test_{}'.format(b) for b in enabled_backends]
199 queried_repositories = [
200 name for type_, name in kwargs['filter'].all_terms()]
201 for repository in expected_repositories:
202 assert repository in queried_repositories
@@ -0,0 +1,133 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
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
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22 import urllib
23 from pyramid.view import view_config
24 from webhelpers.util import update_params
25
26 from rhodecode.apps._base import BaseAppView, RepoAppView
27 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
28 from rhodecode.lib.helpers import Page
29 from rhodecode.lib.utils2 import safe_str, safe_int
30 from rhodecode.lib.index import searcher_from_config
31 from rhodecode.model import validation_schema
32 from rhodecode.model.validation_schema.schemas import search_schema
33
34 log = logging.getLogger(__name__)
35
36
37 def search(request, tmpl_context, repo_name):
38 searcher = searcher_from_config(request.registry.settings)
39 formatted_results = []
40 execution_time = ''
41
42 schema = search_schema.SearchParamsSchema()
43
44 search_params = {}
45 errors = []
46 try:
47 search_params = schema.deserialize(
48 dict(search_query=request.GET.get('q'),
49 search_type=request.GET.get('type'),
50 search_sort=request.GET.get('sort'),
51 page_limit=request.GET.get('page_limit'),
52 requested_page=request.GET.get('page'))
53 )
54 except validation_schema.Invalid as e:
55 errors = e.children
56
57 def url_generator(**kw):
58 q = urllib.quote(safe_str(search_query))
59 return update_params(
60 "?q=%s&type=%s" % (q, safe_str(search_type)), **kw)
61
62 c = tmpl_context
63 search_query = search_params.get('search_query')
64 search_type = search_params.get('search_type')
65 search_sort = search_params.get('search_sort')
66 if search_params.get('search_query'):
67 page_limit = search_params['page_limit']
68 requested_page = search_params['requested_page']
69
70 try:
71 search_result = searcher.search(
72 search_query, search_type, c.auth_user, repo_name,
73 requested_page, page_limit, search_sort)
74
75 formatted_results = Page(
76 search_result['results'], page=requested_page,
77 item_count=search_result['count'],
78 items_per_page=page_limit, url=url_generator)
79 finally:
80 searcher.cleanup()
81
82 if not search_result['error']:
83 execution_time = '%s results (%.3f seconds)' % (
84 search_result['count'],
85 search_result['runtime'])
86 elif not errors:
87 node = schema['search_query']
88 errors = [
89 validation_schema.Invalid(node, search_result['error'])]
90
91 c.perm_user = c.auth_user
92 c.repo_name = repo_name
93 c.sort = search_sort
94 c.url_generator = url_generator
95 c.errors = errors
96 c.formatted_results = formatted_results
97 c.runtime = execution_time
98 c.cur_query = search_query
99 c.search_type = search_type
100 c.searcher = searcher
101
102
103 class SearchView(BaseAppView):
104 def load_default_context(self):
105 c = self._get_local_tmpl_context()
106 self._register_global_c(c)
107 return c
108
109 @LoginRequired()
110 @view_config(
111 route_name='search', request_method='GET',
112 renderer='rhodecode:templates/search/search.mako')
113 def search(self):
114 c = self.load_default_context()
115 search(self.request, c, repo_name=None)
116 return self._get_template_context(c)
117
118
119 class SearchRepoView(RepoAppView):
120 def load_default_context(self):
121 c = self._get_local_tmpl_context()
122 self._register_global_c(c)
123 return c
124
125 @LoginRequired()
126 @HasRepoPermissionAnyDecorator('repository.admin')
127 @view_config(
128 route_name='search_repo', request_method='GET',
129 renderer='rhodecode:templates/search/search.mako')
130 def search_repo(self):
131 c = self.load_default_context()
132 search(self.request, c, repo_name=self.db_repo_name)
133 return self._get_template_context(c)
@@ -291,6 +291,7 b' def includeme(config):'
291 291 config.include('rhodecode.apps.login')
292 292 config.include('rhodecode.apps.home')
293 293 config.include('rhodecode.apps.repository')
294 config.include('rhodecode.apps.search')
294 295 config.include('rhodecode.apps.user_profile')
295 296 config.include('rhodecode.apps.my_account')
296 297 config.include('rhodecode.apps.svn_support')
@@ -614,15 +614,6 b' def make_map(config):'
614 614 controller='journal', action='toggle_following', jsroute=True,
615 615 conditions={'method': ['POST']})
616 616
617 # FULL TEXT SEARCH
618 rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
619 controller='search')
620 rmap.connect('search_repo_home', '/{repo_name}/search',
621 controller='search',
622 action='index',
623 conditions={'function': check_repo},
624 requirements=URL_NAME_REQUIREMENTS)
625
626 617 # FEEDS
627 618 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
628 619 controller='feed', action='rss',
@@ -253,7 +253,7 b''
253 253 ${_('Compare fork')}</a></li>
254 254 %endif
255 255
256 <li><a href="${h.url('search_repo_home',repo_name=c.repo_name)}">${_('Search')}</a></li>
256 <li><a href="${h.route_path('search_repo',repo_name=c.repo_name)}">${_('Search')}</a></li>
257 257
258 258 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
259 259 %if c.rhodecode_db_repo.locked[0]:
@@ -399,7 +399,7 b''
399 399 </a>
400 400 </li>
401 401 <li class="${is_active('search')}">
402 <a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.url('search')}">
402 <a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.route_path('search')}">
403 403 <div class="menulabel">${_('Search')}</div>
404 404 </a>
405 405 </li>
@@ -45,7 +45,7 b''
45 45 <div class="title">
46 46 ${self.repo_page_title(c.rhodecode_db_repo)}
47 47 </div>
48 ${h.form(h.url('search_repo_home',repo_name=c.repo_name),method='get')}
48 ${h.form(h.route_path('search_repo',repo_name=c.repo_name),method='get')}
49 49 %else:
50 50 <!-- box / title -->
51 51 <div class="title">
@@ -53,7 +53,7 b''
53 53 <ul class="links">&nbsp;</ul>
54 54 </div>
55 55 <!-- end box / title -->
56 ${h.form(h.url('search'),method='get')}
56 ${h.form(h.route_path('search'), method='get')}
57 57 %endif
58 58 <div class="form search-form">
59 59 <div class="fields">
@@ -54,7 +54,7 b' for line_number in matching_lines:'
54 54 <div class="stats">
55 55 ${h.link_to(h.literal(entry['f_path']), h.url('files_home',repo_name=entry['repository'],revision=entry.get('commit_id', 'tip'),f_path=entry['f_path']))}
56 56 %if entry.get('lines'):
57 | ${entry.get('lines', 0.)} ${ungettext('line', 'lines', entry.get('lines', 0.))}
57 | ${entry.get('lines', 0.)} ${_ungettext('line', 'lines', entry.get('lines', 0.))}
58 58 %endif
59 59 %if entry.get('size'):
60 60 | ${h.format_byte_size_binary(entry['size'])}
1 NO CONTENT: file renamed from rhodecode/tests/controllers/test_search.py to rhodecode/tests/lib/test_search.py
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now