From 47f72d2b191dc52d0def94b6481a1bb6ba16876e Mon Sep 17 00:00:00 2001 From: Marcin Kuzminski Date: Fri, 21 Apr 2017 17:10:26 +0200 Subject: [PATCH] search: moved search into pyramid views. --- rhodecode/apps/search/__init__.py | 44 ++++++ rhodecode/apps/search/tests/__init__.py | 0 .../search/tests}/test_search.py | 50 +++---- rhodecode/apps/search/views.py | 133 ++++++++++++++++++ rhodecode/config/middleware.py | 1 + rhodecode/config/routing.py | 9 -- rhodecode/controllers/search.py | 112 --------------- rhodecode/templates/base/base.mako | 4 +- rhodecode/templates/search/search.mako | 4 +- .../templates/search/search_content.mako | 2 +- .../tests/{controllers => lib}/test_search.py | 0 11 files changed, 208 insertions(+), 151 deletions(-) create mode 100644 rhodecode/apps/search/__init__.py create mode 100644 rhodecode/apps/search/tests/__init__.py rename rhodecode/{tests/functional => apps/search/tests}/test_search.py (83%) create mode 100644 rhodecode/apps/search/views.py delete mode 100644 rhodecode/controllers/search.py rename rhodecode/tests/{controllers => lib}/test_search.py (100%) diff --git a/rhodecode/apps/search/__init__.py b/rhodecode/apps/search/__init__.py new file mode 100644 index 00000000..3b67d94c --- /dev/null +++ b/rhodecode/apps/search/__init__.py @@ -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 . +# +# 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) \ No newline at end of file diff --git a/rhodecode/apps/search/tests/__init__.py b/rhodecode/apps/search/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rhodecode/tests/functional/test_search.py b/rhodecode/apps/search/tests/test_search.py similarity index 83% rename from rhodecode/tests/functional/test_search.py rename to rhodecode/apps/search/tests/test_search.py index b3748e1c..6c97a335 100644 --- a/rhodecode/tests/functional/test_search.py +++ b/rhodecode/apps/search/tests/test_search.py @@ -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 diff --git a/rhodecode/apps/search/views.py b/rhodecode/apps/search/views.py new file mode 100644 index 00000000..1230bf46 --- /dev/null +++ b/rhodecode/apps/search/views.py @@ -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 . +# +# 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) diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py index b7ec6a3f..0aa57c43 100644 --- a/rhodecode/config/middleware.py +++ b/rhodecode/config/middleware.py @@ -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') diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py index a88054a9..0d698fce 100644 --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -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', diff --git a/rhodecode/controllers/search.py b/rhodecode/controllers/search.py deleted file mode 100644 index 00750942..00000000 --- a/rhodecode/controllers/search.py +++ /dev/null @@ -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 . -# -# 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') diff --git a/rhodecode/templates/base/base.mako b/rhodecode/templates/base/base.mako index 128322d8..45de2b6b 100644 --- a/rhodecode/templates/base/base.mako +++ b/rhodecode/templates/base/base.mako @@ -253,7 +253,7 @@ ${_('Compare fork')} %endif -
  • ${_('Search')}
  • +
  • ${_('Search')}
  • %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 @@
  • - +
  • diff --git a/rhodecode/templates/search/search.mako b/rhodecode/templates/search/search.mako index 76a3bd9d..15d43d76 100644 --- a/rhodecode/templates/search/search.mako +++ b/rhodecode/templates/search/search.mako @@ -45,7 +45,7 @@
    ${self.repo_page_title(c.rhodecode_db_repo)}
    - ${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:
    @@ -53,7 +53,7 @@
    - ${h.form(h.url('search'),method='get')} + ${h.form(h.route_path('search'), method='get')} %endif
    diff --git a/rhodecode/templates/search/search_content.mako b/rhodecode/templates/search/search_content.mako index 1a25a39b..aebe039d 100644 --- a/rhodecode/templates/search/search_content.mako +++ b/rhodecode/templates/search/search_content.mako @@ -54,7 +54,7 @@ ${h.code_highlight(
    ${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'])} diff --git a/rhodecode/tests/controllers/test_search.py b/rhodecode/tests/lib/test_search.py similarity index 100% rename from rhodecode/tests/controllers/test_search.py rename to rhodecode/tests/lib/test_search.py