search: moved search into pyramid views.

This commit is contained in:
Marcin Kuzminski 2017-04-21 17:10:26 +02:00
parent 8f5b7122aa
commit 47f72d2b19
11 changed files with 208 additions and 151 deletions

View file

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
from rhodecode.apps._base import ADMIN_PREFIX
def includeme(config):
config.add_route(
name='search',
pattern=ADMIN_PREFIX + '/search')
config.add_route(
name='search_repo',
pattern='/{repo_name:.*?[^/]}/search', repo_route=True)
# Scan module for configuration decorators.
config.scan()
# # FULL TEXT SEARCH
# rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
# controller='search')
# rmap.connect('search_repo_home', '/{repo_name}/search',
# controller='search',
# action='index',
# conditions={'function': check_repo},
# requirements=URL_NAME_REQUIREMENTS)

View file

View file

@ -25,16 +25,27 @@ import pytest
from whoosh import query
from rhodecode.tests import (
TestController, url, SkipTest, HG_REPO,
TestController, SkipTest, HG_REPO,
TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
from rhodecode.tests.utils import AssertResponse
def route_path(name, **kwargs):
from rhodecode.apps._base import ADMIN_PREFIX
return {
'search':
ADMIN_PREFIX + '/search',
'search_repo':
'/{repo_name}/search',
}[name].format(**kwargs)
class TestSearchController(TestController):
def test_index(self):
self.log_user()
response = self.app.get(url(controller='search', action='index'))
response = self.app.get(route_path('search'))
assert_response = AssertResponse(response)
assert_response.one_element_exists('input#q')
@ -43,17 +54,15 @@ class TestSearchController(TestController):
raise SkipTest('skipped due to existing index')
else:
self.log_user()
response = self.app.get(url(controller='search', action='index'),
response = self.app.get(route_path('search'),
{'q': HG_REPO})
response.mustcontain('There is no index to search in. '
'Please run whoosh indexer')
def test_search_validation(self):
self.log_user()
response = self.app.get(
url(controller='search', action='index'), {'q': query,
'type': 'content',
'page_limit': 1000})
response = self.app.get(route_path('search'),
{'q': query, 'type': 'content', 'page_limit': 1000})
response.mustcontain(
'page_limit - 1000 is greater than maximum value 500')
@ -88,10 +97,8 @@ class TestSearchController(TestController):
])
def test_search_files(self, query, expected_hits, expected_paths):
self.log_user()
response = self.app.get(
url(controller='search', action='index'), {'q': query,
'type': 'content',
'page_limit': 500})
response = self.app.get(route_path('search'),
{'q': query, 'type': 'content', 'page_limit': 500})
response.mustcontain('%s results' % expected_hits)
for path in expected_paths:
@ -137,10 +144,8 @@ class TestSearchController(TestController):
def test_search_commit_messages(
self, query, expected_hits, expected_commits, enabled_backends):
self.log_user()
response = self.app.get(
url(controller='search', action='index'), {'q': query,
'type': 'commit',
'page_limit': 500})
response = self.app.get(route_path('search'),
{'q': query, 'type': 'commit', 'page_limit': 500})
response.mustcontain('%s results' % expected_hits)
for backend, commit_id in expected_commits:
@ -156,10 +161,8 @@ class TestSearchController(TestController):
])
def test_search_file_paths(self, query, expected_hits, expected_paths):
self.log_user()
response = self.app.get(
url(controller='search', action='index'), {'q': query,
'type': 'path',
'page_limit': 500})
response = self.app.get(route_path('search'),
{'q': query, 'type': 'path', 'page_limit': 500})
response.mustcontain('%s results' % expected_hits)
for path in expected_paths:
@ -168,8 +171,7 @@ class TestSearchController(TestController):
def test_search_commit_message_specific_repo(self, backend):
self.log_user()
response = self.app.get(
url(controller='search', action='index',
repo_name=backend.repo_name),
route_path('search_repo',repo_name=backend.repo_name),
{'q': 'bother to ask where to fetch repo during tests',
'type': 'commit'})
@ -178,8 +180,7 @@ class TestSearchController(TestController):
def test_filters_are_not_applied_for_admin_user(self):
self.log_user()
with mock.patch('whoosh.searching.Searcher.search') as search_mock:
self.app.get(
url(controller='search', action='index'),
self.app.get(route_path('search'),
{'q': 'test query', 'type': 'commit'})
assert search_mock.call_count == 1
_, kwargs = search_mock.call_args
@ -188,8 +189,7 @@ class TestSearchController(TestController):
def test_filters_are_applied_for_normal_user(self, enabled_backends):
self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
with mock.patch('whoosh.searching.Searcher.search') as search_mock:
self.app.get(
url(controller='search', action='index'),
self.app.get(route_path('search'),
{'q': 'test query', 'type': 'commit'})
assert search_mock.call_count == 1
_, kwargs = search_mock.call_args

View file

@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2011-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import logging
import urllib
from pyramid.view import view_config
from webhelpers.util import update_params
from rhodecode.apps._base import BaseAppView, RepoAppView
from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
from rhodecode.lib.helpers import Page
from rhodecode.lib.utils2 import safe_str, safe_int
from rhodecode.lib.index import searcher_from_config
from rhodecode.model import validation_schema
from rhodecode.model.validation_schema.schemas import search_schema
log = logging.getLogger(__name__)
def search(request, tmpl_context, repo_name):
searcher = searcher_from_config(request.registry.settings)
formatted_results = []
execution_time = ''
schema = search_schema.SearchParamsSchema()
search_params = {}
errors = []
try:
search_params = schema.deserialize(
dict(search_query=request.GET.get('q'),
search_type=request.GET.get('type'),
search_sort=request.GET.get('sort'),
page_limit=request.GET.get('page_limit'),
requested_page=request.GET.get('page'))
)
except validation_schema.Invalid as e:
errors = e.children
def url_generator(**kw):
q = urllib.quote(safe_str(search_query))
return update_params(
"?q=%s&type=%s" % (q, safe_str(search_type)), **kw)
c = tmpl_context
search_query = search_params.get('search_query')
search_type = search_params.get('search_type')
search_sort = search_params.get('search_sort')
if search_params.get('search_query'):
page_limit = search_params['page_limit']
requested_page = search_params['requested_page']
try:
search_result = searcher.search(
search_query, search_type, c.auth_user, repo_name,
requested_page, page_limit, search_sort)
formatted_results = Page(
search_result['results'], page=requested_page,
item_count=search_result['count'],
items_per_page=page_limit, url=url_generator)
finally:
searcher.cleanup()
if not search_result['error']:
execution_time = '%s results (%.3f seconds)' % (
search_result['count'],
search_result['runtime'])
elif not errors:
node = schema['search_query']
errors = [
validation_schema.Invalid(node, search_result['error'])]
c.perm_user = c.auth_user
c.repo_name = repo_name
c.sort = search_sort
c.url_generator = url_generator
c.errors = errors
c.formatted_results = formatted_results
c.runtime = execution_time
c.cur_query = search_query
c.search_type = search_type
c.searcher = searcher
class SearchView(BaseAppView):
def load_default_context(self):
c = self._get_local_tmpl_context()
self._register_global_c(c)
return c
@LoginRequired()
@view_config(
route_name='search', request_method='GET',
renderer='rhodecode:templates/search/search.mako')
def search(self):
c = self.load_default_context()
search(self.request, c, repo_name=None)
return self._get_template_context(c)
class SearchRepoView(RepoAppView):
def load_default_context(self):
c = self._get_local_tmpl_context()
self._register_global_c(c)
return c
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.admin')
@view_config(
route_name='search_repo', request_method='GET',
renderer='rhodecode:templates/search/search.mako')
def search_repo(self):
c = self.load_default_context()
search(self.request, c, repo_name=self.db_repo_name)
return self._get_template_context(c)

View file

@ -291,6 +291,7 @@ def includeme(config):
config.include('rhodecode.apps.login')
config.include('rhodecode.apps.home')
config.include('rhodecode.apps.repository')
config.include('rhodecode.apps.search')
config.include('rhodecode.apps.user_profile')
config.include('rhodecode.apps.my_account')
config.include('rhodecode.apps.svn_support')

View file

@ -614,15 +614,6 @@ def make_map(config):
controller='journal', action='toggle_following', jsroute=True,
conditions={'method': ['POST']})
# FULL TEXT SEARCH
rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
controller='search')
rmap.connect('search_repo_home', '/{repo_name}/search',
controller='search',
action='index',
conditions={'function': check_repo},
requirements=URL_NAME_REQUIREMENTS)
# FEEDS
rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
controller='feed', action='rss',

View file

@ -1,112 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2010-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
"""
Search controller for RhodeCode
"""
import logging
import urllib
from pylons import request, config, tmpl_context as c
from webhelpers.util import update_params
from rhodecode.lib.auth import LoginRequired, AuthUser
from rhodecode.lib.base import BaseRepoController, render
from rhodecode.lib.helpers import Page
from rhodecode.lib.utils2 import safe_str, safe_int
from rhodecode.lib.index import searcher_from_config
from rhodecode.model import validation_schema
from rhodecode.model.validation_schema.schemas import search_schema
log = logging.getLogger(__name__)
class SearchController(BaseRepoController):
@LoginRequired()
def index(self, repo_name=None):
searcher = searcher_from_config(config)
formatted_results = []
execution_time = ''
schema = search_schema.SearchParamsSchema()
search_params = {}
errors = []
try:
search_params = schema.deserialize(
dict(search_query=request.GET.get('q'),
search_type=request.GET.get('type'),
search_sort=request.GET.get('sort'),
page_limit=request.GET.get('page_limit'),
requested_page=request.GET.get('page'))
)
except validation_schema.Invalid as e:
errors = e.children
def url_generator(**kw):
q = urllib.quote(safe_str(search_query))
return update_params(
"?q=%s&type=%s" % (q, safe_str(search_type)), **kw)
search_query = search_params.get('search_query')
search_type = search_params.get('search_type')
search_sort = search_params.get('search_sort')
if search_params.get('search_query'):
page_limit = search_params['page_limit']
requested_page = search_params['requested_page']
c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
ip_addr=self.ip_addr)
try:
search_result = searcher.search(
search_query, search_type, c.perm_user, repo_name,
requested_page, page_limit, search_sort)
formatted_results = Page(
search_result['results'], page=requested_page,
item_count=search_result['count'],
items_per_page=page_limit, url=url_generator)
finally:
searcher.cleanup()
if not search_result['error']:
execution_time = '%s results (%.3f seconds)' % (
search_result['count'],
search_result['runtime'])
elif not errors:
node = schema['search_query']
errors = [
validation_schema.Invalid(node, search_result['error'])]
c.sort = search_sort
c.url_generator = url_generator
c.errors = errors
c.formatted_results = formatted_results
c.runtime = execution_time
c.cur_query = search_query
c.search_type = search_type
c.searcher = searcher
# Return a rendered template
return render('/search/search.mako')

View file

@ -253,7 +253,7 @@
${_('Compare fork')}</a></li>
%endif
<li><a href="${h.url('search_repo_home',repo_name=c.repo_name)}">${_('Search')}</a></li>
<li><a href="${h.route_path('search_repo',repo_name=c.repo_name)}">${_('Search')}</a></li>
%if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
%if c.rhodecode_db_repo.locked[0]:
@ -399,7 +399,7 @@
</a>
</li>
<li class="${is_active('search')}">
<a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.url('search')}">
<a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.route_path('search')}">
<div class="menulabel">${_('Search')}</div>
</a>
</li>

View file

@ -45,7 +45,7 @@
<div class="title">
${self.repo_page_title(c.rhodecode_db_repo)}
</div>
${h.form(h.url('search_repo_home',repo_name=c.repo_name),method='get')}
${h.form(h.route_path('search_repo',repo_name=c.repo_name),method='get')}
%else:
<!-- box / title -->
<div class="title">
@ -53,7 +53,7 @@
<ul class="links">&nbsp;</ul>
</div>
<!-- end box / title -->
${h.form(h.url('search'),method='get')}
${h.form(h.route_path('search'), method='get')}
%endif
<div class="form search-form">
<div class="fields">

View file

@ -54,7 +54,7 @@ ${h.code_highlight(
<div class="stats">
${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']))}
%if entry.get('lines'):
| ${entry.get('lines', 0.)} ${ungettext('line', 'lines', entry.get('lines', 0.))}
| ${entry.get('lines', 0.)} ${_ungettext('line', 'lines', entry.get('lines', 0.))}
%endif
%if entry.get('size'):
| ${h.format_byte_size_binary(entry['size'])}