From ebe10fa6452de7ea6c5759bfd9c7b439091b54cd Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 30 Apr 2007 08:52:39 +0000 Subject: [PATCH] Added a quick search form in page header. Search functionality moved to a dedicated controller. When used: * outside of a project: searches projects * inside a project: searches issues, changesets, news, documents and wiki pages of the current project If an issue number is given, user is redirected to the corresponding issue. git-svn-id: http://redmine.rubyforge.org/svn/trunk@489 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/projects_controller.rb | 28 +----- app/controllers/search_controller.rb | 75 +++++++++++++++ app/helpers/projects_helper.rb | 10 -- app/helpers/search_helper.rb | 28 ++++++ app/views/layouts/base.rhtml | 18 +++- .../{projects/search.rhtml => search/index.rhtml} | 106 +++++++++++---------- public/stylesheets/application.css | 2 + test/functional/projects_controller_test.rb | 10 -- test/functional/search_controller_test.rb | 48 ++++++++++ 9 files changed, 225 insertions(+), 100 deletions(-) create mode 100644 app/controllers/search_controller.rb create mode 100644 app/helpers/search_helper.rb rename app/views/{projects/search.rhtml => search/index.rhtml} (64%) create mode 100644 test/functional/search_controller_test.rb diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index a4712605..ffc71ce1 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -612,33 +612,7 @@ class ProjectsController < ApplicationController render :template => "projects/gantt.rhtml" end end - - def search - @question = params[:q] || "" - @question.strip! - @all_words = params[:all_words] || (params[:submit] ? false : true) - @scope = params[:scope] || (params[:submit] ? [] : %w(issues changesets news documents wiki) ) - # tokens must be at least 3 character long - @tokens = @question.split.uniq.select {|w| w.length > 2 } - if !@tokens.empty? - # no more than 5 tokens to search for - @tokens.slice! 5..-1 if @tokens.size > 5 - # strings used in sql like statement - like_tokens = @tokens.collect {|w| "%#{w.downcase}%"} - operator = @all_words ? " AND " : " OR " - limit = 10 - @results = [] - @results += @project.issues.find(:all, :limit => limit, :include => :author, :conditions => [ (["(LOWER(subject) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] ) if @scope.include? 'issues' - @results += @project.news.find(:all, :limit => limit, :conditions => [ (["(LOWER(title) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort], :include => :author ) if @scope.include? 'news' - @results += @project.documents.find(:all, :limit => limit, :conditions => [ (["(LOWER(title) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] ) if @scope.include? 'documents' - @results += @project.wiki.pages.find(:all, :limit => limit, :include => :content, :conditions => [ (["(LOWER(title) like ? OR LOWER(text) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] ) if @project.wiki && @scope.include?('wiki') - @results += @project.repository.changesets.find(:all, :limit => limit, :conditions => [ (["(LOWER(comments) like ?)"] * like_tokens.size).join(operator), * (like_tokens).sort] ) if @project.repository && @scope.include?('changesets') - @question = @tokens.join(" ") - else - @question = "" - end - end - + def feeds @queries = @project.queries.find :all, :conditions => ["is_public=? or user_id=?", true, (logged_in_user ? logged_in_user.id : 0)] @key = logged_in_user.get_or_create_rss_key.value if logged_in_user diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb new file mode 100644 index 00000000..c463e4a3 --- /dev/null +++ b/app/controllers/search_controller.rb @@ -0,0 +1,75 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# 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 General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class SearchController < ApplicationController + layout 'base' + + def index + @question = params[:q] || "" + @question.strip! + @all_words = params[:all_words] || (params[:submit] ? false : true) + @scope = params[:scope] || (params[:submit] ? [] : %w(projects issues changesets news documents wiki) ) + + # quick jump to an issue + if @scope.include?('issues') && @question.match(/^#?(\d+)$/) && Issue.find_by_id($1, :include => :project, :conditions => Project.visible_by(logged_in_user)) + redirect_to :controller => "issues", :action => "show", :id => $1 + return + end + + if params[:id] + find_project + return unless check_project_privacy + end + + # tokens must be at least 3 character long + @tokens = @question.split.uniq.select {|w| w.length > 2 } + + if !@tokens.empty? + # no more than 5 tokens to search for + @tokens.slice! 5..-1 if @tokens.size > 5 + # strings used in sql like statement + like_tokens = @tokens.collect {|w| "%#{w.downcase}%"} + operator = @all_words ? " AND " : " OR " + limit = 10 + @results = [] + if @project + @results += @project.issues.find(:all, :limit => limit, :include => :author, :conditions => [ (["(LOWER(subject) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] ) if @scope.include? 'issues' + @results += @project.news.find(:all, :limit => limit, :conditions => [ (["(LOWER(title) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort], :include => :author ) if @scope.include? 'news' + @results += @project.documents.find(:all, :limit => limit, :conditions => [ (["(LOWER(title) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] ) if @scope.include? 'documents' + @results += @project.wiki.pages.find(:all, :limit => limit, :include => :content, :conditions => [ (["(LOWER(title) like ? OR LOWER(text) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] ) if @project.wiki && @scope.include?('wiki') + @results += @project.repository.changesets.find(:all, :limit => limit, :conditions => [ (["(LOWER(comments) like ?)"] * like_tokens.size).join(operator), * (like_tokens).sort] ) if @project.repository && @scope.include?('changesets') + else + Project.with_scope(:find => {:conditions => Project.visible_by(logged_in_user)}) do + @results += Project.find(:all, :limit => limit, :conditions => [ (["(LOWER(name) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] ) if @scope.include? 'projects' + end + # if only one project is found, user is redirected to its overview + redirect_to :controller => 'projects', :action => 'show', :id => @results.first and return if @results.size == 1 + end + @question = @tokens.join(" ") + else + @question = "" + end + end + +private + def find_project + @project = Project.find(params[:id]) + @html_title = @project.name + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 43febd41..c7e80b0a 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -16,14 +16,4 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. module ProjectsHelper - - def highlight_tokens(text, tokens) - return text unless tokens && !tokens.empty? - regexp = Regexp.new "(#{tokens.join('|')})", Regexp::IGNORECASE - result = '' - text.split(regexp).each_with_index do |words, i| - result << (i.even? ? (words.length > 100 ? "#{words[0..44]} ... #{words[-45..-1]}" : words) : content_tag('span', words, :class => 'highlight')) - end - result - end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb new file mode 100644 index 00000000..bc408a30 --- /dev/null +++ b/app/helpers/search_helper.rb @@ -0,0 +1,28 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# 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 General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module SearchHelper + def highlight_tokens(text, tokens) + return text unless tokens && !tokens.empty? + regexp = Regexp.new "(#{tokens.join('|')})", Regexp::IGNORECASE + result = '' + text.split(regexp).each_with_index do |words, i| + result << (i.even? ? (words.length > 100 ? "#{words[0..44]} ... #{words[-45..-1]}" : words) : content_tag('span', words, :class => 'highlight')) + end + result + end +end diff --git a/app/views/layouts/base.rhtml b/app/views/layouts/base.rhtml index 3d6390b9..945803b0 100644 --- a/app/views/layouts/base.rhtml +++ b/app/views/layouts/base.rhtml @@ -27,9 +27,13 @@

<%= Setting.app_subtitle %>

- <% if loggedin? %><%=l(:label_logged_as)%> <%= @logged_in_user.login %><% end %> + <% if loggedin? %><%=l(:label_logged_as)%> <%= @logged_in_user.login %> -<% end %> + <%= toggle_link 'Search', 'quick-search-form', :focus => 'quick-search-input' %> + <% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get, :id => 'quick-search-form', :style => "display:none;" ) do %> + <%= text_field_tag 'q', @question, :size => 15, :class => 'small', :id => 'quick-search-input' %> + <% end %> +
- @@ -76,7 +86,7 @@ <%= link_to l(:label_document_plural), {:controller => 'projects', :action => 'list_documents', :id => @project }, :class => "menuItem" %> <%= link_to l(:label_wiki), {:controller => 'wiki', :id => @project, :page => nil }, :class => "menuItem" if @project.wiki and !@project.wiki.new_record? %> <%= link_to l(:label_attachment_plural), {:controller => 'projects', :action => 'list_files', :id => @project }, :class => "menuItem" %> - <%= link_to l(:label_search), {:controller => 'projects', :action => 'search', :id => @project }, :class => "menuItem" %> + <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project }, :class => "menuItem" %> <%= link_to l(:label_repository), {:controller => 'repositories', :action => 'show', :id => @project}, :class => "menuItem" if @project.repository and !@project.repository.new_record? %> <%= link_to_if_authorized l(:label_settings), {:controller => 'projects', :action => 'settings', :id => @project }, :class => "menuItem" %> @@ -100,7 +110,7 @@
  • <%= link_to l(:label_document_plural), :controller => 'projects', :action => 'list_documents', :id => @project %>
  • <%= content_tag("li", link_to(l(:label_wiki), :controller => 'wiki', :id => @project, :page => nil)) if @project.wiki and !@project.wiki.new_record? %>
  • <%= link_to l(:label_attachment_plural), :controller => 'projects', :action => 'list_files', :id => @project %>
  • -
  • <%= link_to l(:label_search), :controller => 'projects', :action => 'search', :id => @project %>
  • +
  • <%= link_to l(:label_search), :controller => 'search', :action => 'index', :id => @project %>
  • <%= content_tag("li", link_to(l(:label_repository), :controller => 'repositories', :action => 'show', :id => @project)) if @project.repository and !@project.repository.new_record? %>
  • <%= link_to_if_authorized l(:label_settings), :controller => 'projects', :action => 'settings', :id => @project %>
  • diff --git a/app/views/projects/search.rhtml b/app/views/search/index.rhtml similarity index 64% rename from app/views/projects/search.rhtml rename to app/views/search/index.rhtml index d13302b6..110f8141 100644 --- a/app/views/projects/search.rhtml +++ b/app/views/search/index.rhtml @@ -1,50 +1,58 @@ -

    <%= l(:label_search) %>

    - -
    -<% form_tag({:action => 'search', :id => @project}, :method => :get) do %> -

    <%= text_field_tag 'q', @question, :size => 30 %> -<%= check_box_tag 'scope[]', 'issues', (@scope.include? 'issues') %> -<% if @project.repository %> -<%= check_box_tag 'scope[]', 'changesets', (@scope.include? 'changesets') %> -<% end %> -<%= check_box_tag 'scope[]', 'news', (@scope.include? 'news') %> -<%= check_box_tag 'scope[]', 'documents', (@scope.include? 'documents') %> -<% if @project.wiki %> -<%= check_box_tag 'scope[]', 'wiki', (@scope.include? 'wiki') %> -<% end %> -
    -<%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %>

    -<%= submit_tag l(:button_submit), :name => 'submit' %> -<% end %> -
    - -<% if @results %> -

    <%= lwr(:label_result, @results.length) %>

    - +

    <%= l(:label_search) %>

    + +
    +<% form_tag({}, :method => :get) do %> +

    <%= text_field_tag 'q', @question, :size => 30 %> + +<% if @project %> + <%= check_box_tag 'scope[]', 'issues', (@scope.include? 'issues') %> + <% if @project.repository %> + <%= check_box_tag 'scope[]', 'changesets', (@scope.include? 'changesets') %> + <% end %> + <%= check_box_tag 'scope[]', 'news', (@scope.include? 'news') %> + <%= check_box_tag 'scope[]', 'documents', (@scope.include? 'documents') %> + <% if @project.wiki %> + <%= check_box_tag 'scope[]', 'wiki', (@scope.include? 'wiki') %> + <% end %> +<% else %> + <%= check_box_tag 'scope[]', 'projects', (@scope.include? 'projects') %> +<% end %> +
    +<%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %>

    +<%= submit_tag l(:button_submit), :name => 'submit' %> +<% end %> +
    + +<% if @results %> +

    <%= lwr(:label_result, @results.length) %>

    + <% end %> \ No newline at end of file diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index b5c631e4..2555b61f 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -66,6 +66,8 @@ font-weight:normal; font-family: Trebuchet MS,Georgia,"Times New Roman",serif; } +#header a {color:#fff;} + #navigation{ height:2.2em; line-height:2.2em; diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index 644da830..6e76be2d 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -125,14 +125,4 @@ class ProjectsControllerTest < Test::Unit::TestCase assert_template 'activity' assert_not_nil assigns(:events_by_day) end - - def test_search - get :search, :id => 1 - assert_response :success - assert_template 'search' - - get :search, :id => 1, :token => "can", :scope => ["issues", "news", "documents"] - assert_response :success - assert_template 'search' - end end diff --git a/test/functional/search_controller_test.rb b/test/functional/search_controller_test.rb new file mode 100644 index 00000000..8fa2e089 --- /dev/null +++ b/test/functional/search_controller_test.rb @@ -0,0 +1,48 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'search_controller' + +# Re-raise errors caught by the controller. +class SearchController; def rescue_action(e) raise e end; end + +class SearchControllerTest < Test::Unit::TestCase + fixtures :projects, :issues + + def setup + @controller = SearchController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_search_for_projects + get :index + assert_response :success + assert_template 'index' + + get :index, :q => "cook" + assert_response :success + assert_template 'index' + assert assigns(:results).include?(Project.find(1)) + end + + def test_search_in_project + get :index, :id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:project) + + get :index, :id => 1, :q => "can", :scope => ["issues", "news", "documents"] + assert_response :success + assert_template 'index' + end + + def test_quick_jump_to_issue + # issue of a public project + get :index, :q => "3" + assert_redirected_to 'issues/show/3' + + # issue of a private project + get :index, :q => "4" + assert_response :success + assert_template 'index' + end +end -- 2.11.0