##// END OF EJS Templates
Added a quick search form in page header. Search functionality moved to a dedicated controller....
Jean-Philippe Lang -
r486:ebe10fa6452d
parent child
Show More
@@ -0,0 +1,75
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
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 General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class SearchController < ApplicationController
19 layout 'base'
20
21 def index
22 @question = params[:q] || ""
23 @question.strip!
24 @all_words = params[:all_words] || (params[:submit] ? false : true)
25 @scope = params[:scope] || (params[:submit] ? [] : %w(projects issues changesets news documents wiki) )
26
27 # quick jump to an issue
28 if @scope.include?('issues') && @question.match(/^#?(\d+)$/) && Issue.find_by_id($1, :include => :project, :conditions => Project.visible_by(logged_in_user))
29 redirect_to :controller => "issues", :action => "show", :id => $1
30 return
31 end
32
33 if params[:id]
34 find_project
35 return unless check_project_privacy
36 end
37
38 # tokens must be at least 3 character long
39 @tokens = @question.split.uniq.select {|w| w.length > 2 }
40
41 if !@tokens.empty?
42 # no more than 5 tokens to search for
43 @tokens.slice! 5..-1 if @tokens.size > 5
44 # strings used in sql like statement
45 like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}
46 operator = @all_words ? " AND " : " OR "
47 limit = 10
48 @results = []
49 if @project
50 @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'
51 @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'
52 @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'
53 @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')
54 @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')
55 else
56 Project.with_scope(:find => {:conditions => Project.visible_by(logged_in_user)}) do
57 @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'
58 end
59 # if only one project is found, user is redirected to its overview
60 redirect_to :controller => 'projects', :action => 'show', :id => @results.first and return if @results.size == 1
61 end
62 @question = @tokens.join(" ")
63 else
64 @question = ""
65 end
66 end
67
68 private
69 def find_project
70 @project = Project.find(params[:id])
71 @html_title = @project.name
72 rescue ActiveRecord::RecordNotFound
73 render_404
74 end
75 end
@@ -0,0 +1,28
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
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 General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 module SearchHelper
19 def highlight_tokens(text, tokens)
20 return text unless tokens && !tokens.empty?
21 regexp = Regexp.new "(#{tokens.join('|')})", Regexp::IGNORECASE
22 result = ''
23 text.split(regexp).each_with_index do |words, i|
24 result << (i.even? ? (words.length > 100 ? "#{words[0..44]} ... #{words[-45..-1]}" : words) : content_tag('span', words, :class => 'highlight'))
25 end
26 result
27 end
28 end
@@ -0,0 +1,48
1 require File.dirname(__FILE__) + '/../test_helper'
2 require 'search_controller'
3
4 # Re-raise errors caught by the controller.
5 class SearchController; def rescue_action(e) raise e end; end
6
7 class SearchControllerTest < Test::Unit::TestCase
8 fixtures :projects, :issues
9
10 def setup
11 @controller = SearchController.new
12 @request = ActionController::TestRequest.new
13 @response = ActionController::TestResponse.new
14 end
15
16 def test_search_for_projects
17 get :index
18 assert_response :success
19 assert_template 'index'
20
21 get :index, :q => "cook"
22 assert_response :success
23 assert_template 'index'
24 assert assigns(:results).include?(Project.find(1))
25 end
26
27 def test_search_in_project
28 get :index, :id => 1
29 assert_response :success
30 assert_template 'index'
31 assert_not_nil assigns(:project)
32
33 get :index, :id => 1, :q => "can", :scope => ["issues", "news", "documents"]
34 assert_response :success
35 assert_template 'index'
36 end
37
38 def test_quick_jump_to_issue
39 # issue of a public project
40 get :index, :q => "3"
41 assert_redirected_to 'issues/show/3'
42
43 # issue of a private project
44 get :index, :q => "4"
45 assert_response :success
46 assert_template 'index'
47 end
48 end
@@ -612,33 +612,7 class ProjectsController < ApplicationController
612 render :template => "projects/gantt.rhtml"
612 render :template => "projects/gantt.rhtml"
613 end
613 end
614 end
614 end
615
615
616 def search
617 @question = params[:q] || ""
618 @question.strip!
619 @all_words = params[:all_words] || (params[:submit] ? false : true)
620 @scope = params[:scope] || (params[:submit] ? [] : %w(issues changesets news documents wiki) )
621 # tokens must be at least 3 character long
622 @tokens = @question.split.uniq.select {|w| w.length > 2 }
623 if !@tokens.empty?
624 # no more than 5 tokens to search for
625 @tokens.slice! 5..-1 if @tokens.size > 5
626 # strings used in sql like statement
627 like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}
628 operator = @all_words ? " AND " : " OR "
629 limit = 10
630 @results = []
631 @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'
632 @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'
633 @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'
634 @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')
635 @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')
636 @question = @tokens.join(" ")
637 else
638 @question = ""
639 end
640 end
641
642 def feeds
616 def feeds
643 @queries = @project.queries.find :all, :conditions => ["is_public=? or user_id=?", true, (logged_in_user ? logged_in_user.id : 0)]
617 @queries = @project.queries.find :all, :conditions => ["is_public=? or user_id=?", true, (logged_in_user ? logged_in_user.id : 0)]
644 @key = logged_in_user.get_or_create_rss_key.value if logged_in_user
618 @key = logged_in_user.get_or_create_rss_key.value if logged_in_user
@@ -16,14 +16,4
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module ProjectsHelper
18 module ProjectsHelper
19
20 def highlight_tokens(text, tokens)
21 return text unless tokens && !tokens.empty?
22 regexp = Regexp.new "(#{tokens.join('|')})", Regexp::IGNORECASE
23 result = ''
24 text.split(regexp).each_with_index do |words, i|
25 result << (i.even? ? (words.length > 100 ? "#{words[0..44]} ... #{words[-45..-1]}" : words) : content_tag('span', words, :class => 'highlight'))
26 end
27 result
28 end
29 end
19 end
@@ -27,9 +27,13
27 <h2><%= Setting.app_subtitle %></h2>
27 <h2><%= Setting.app_subtitle %></h2>
28 </div>
28 </div>
29 <div style="float: right; padding-right: 1em; padding-top: 0.2em;">
29 <div style="float: right; padding-right: 1em; padding-top: 0.2em;">
30 <% if loggedin? %><small><%=l(:label_logged_as)%> <b><%= @logged_in_user.login %></b></small><% end %>
30 <% if loggedin? %><small><%=l(:label_logged_as)%> <strong><%= @logged_in_user.login %></strong> -</small><% end %>
31 <small><%= toggle_link 'Search', 'quick-search-form', :focus => 'quick-search-input' %></small>
32 <% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get, :id => 'quick-search-form', :style => "display:none;" ) do %>
33 <%= text_field_tag 'q', @question, :size => 15, :class => 'small', :id => 'quick-search-input' %>
34 <% end %>
35 </div>
31 </div>
36 </div>
32 </div>
33
37
34 <div id="navigation">
38 <div id="navigation">
35 <ul>
39 <ul>
@@ -56,6 +60,12
56 <% else %>
60 <% else %>
57 <li class="right"><%= link_to l(:label_login), { :controller => 'account', :action => 'login' }, :class => "icon icon-user" %></li>
61 <li class="right"><%= link_to l(:label_login), { :controller => 'account', :action => 'login' }, :class => "icon icon-user" %></li>
58 <% end %>
62 <% end %>
63
64 <% unless @project.nil? || @project.id.nil? %>
65 <li class="right" style="padding-right:0.8em;">
66 </li>
67 <% end %>
68
59 </ul>
69 </ul>
60 </div>
70 </div>
61
71
@@ -76,7 +86,7
76 <%= link_to l(:label_document_plural), {:controller => 'projects', :action => 'list_documents', :id => @project }, :class => "menuItem" %>
86 <%= link_to l(:label_document_plural), {:controller => 'projects', :action => 'list_documents', :id => @project }, :class => "menuItem" %>
77 <%= link_to l(:label_wiki), {:controller => 'wiki', :id => @project, :page => nil }, :class => "menuItem" if @project.wiki and !@project.wiki.new_record? %>
87 <%= link_to l(:label_wiki), {:controller => 'wiki', :id => @project, :page => nil }, :class => "menuItem" if @project.wiki and !@project.wiki.new_record? %>
78 <%= link_to l(:label_attachment_plural), {:controller => 'projects', :action => 'list_files', :id => @project }, :class => "menuItem" %>
88 <%= link_to l(:label_attachment_plural), {:controller => 'projects', :action => 'list_files', :id => @project }, :class => "menuItem" %>
79 <%= link_to l(:label_search), {:controller => 'projects', :action => 'search', :id => @project }, :class => "menuItem" %>
89 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project }, :class => "menuItem" %>
80 <%= link_to l(:label_repository), {:controller => 'repositories', :action => 'show', :id => @project}, :class => "menuItem" if @project.repository and !@project.repository.new_record? %>
90 <%= link_to l(:label_repository), {:controller => 'repositories', :action => 'show', :id => @project}, :class => "menuItem" if @project.repository and !@project.repository.new_record? %>
81 <%= link_to_if_authorized l(:label_settings), {:controller => 'projects', :action => 'settings', :id => @project }, :class => "menuItem" %>
91 <%= link_to_if_authorized l(:label_settings), {:controller => 'projects', :action => 'settings', :id => @project }, :class => "menuItem" %>
82 </div>
92 </div>
@@ -100,7 +110,7
100 <li><%= link_to l(:label_document_plural), :controller => 'projects', :action => 'list_documents', :id => @project %></li>
110 <li><%= link_to l(:label_document_plural), :controller => 'projects', :action => 'list_documents', :id => @project %></li>
101 <%= content_tag("li", link_to(l(:label_wiki), :controller => 'wiki', :id => @project, :page => nil)) if @project.wiki and !@project.wiki.new_record? %>
111 <%= content_tag("li", link_to(l(:label_wiki), :controller => 'wiki', :id => @project, :page => nil)) if @project.wiki and !@project.wiki.new_record? %>
102 <li><%= link_to l(:label_attachment_plural), :controller => 'projects', :action => 'list_files', :id => @project %></li>
112 <li><%= link_to l(:label_attachment_plural), :controller => 'projects', :action => 'list_files', :id => @project %></li>
103 <li><%= link_to l(:label_search), :controller => 'projects', :action => 'search', :id => @project %></li>
113 <li><%= link_to l(:label_search), :controller => 'search', :action => 'index', :id => @project %></li>
104 <%= content_tag("li", link_to(l(:label_repository), :controller => 'repositories', :action => 'show', :id => @project)) if @project.repository and !@project.repository.new_record? %>
114 <%= content_tag("li", link_to(l(:label_repository), :controller => 'repositories', :action => 'show', :id => @project)) if @project.repository and !@project.repository.new_record? %>
105 <li><%= link_to_if_authorized l(:label_settings), :controller => 'projects', :action => 'settings', :id => @project %></li>
115 <li><%= link_to_if_authorized l(:label_settings), :controller => 'projects', :action => 'settings', :id => @project %></li>
106 </ul>
116 </ul>
@@ -1,50 +1,58
1 <h2><%= l(:label_search) %></h2>
1 <h2><%= l(:label_search) %></h2>
2
2
3 <div class="box">
3 <div class="box">
4 <% form_tag({:action => 'search', :id => @project}, :method => :get) do %>
4 <% form_tag({}, :method => :get) do %>
5 <p><%= text_field_tag 'q', @question, :size => 30 %>
5 <p><%= text_field_tag 'q', @question, :size => 30 %>
6 <%= check_box_tag 'scope[]', 'issues', (@scope.include? 'issues') %> <label><%= l(:label_issue_plural) %></label>
6
7 <% if @project.repository %>
7 <% if @project %>
8 <%= check_box_tag 'scope[]', 'changesets', (@scope.include? 'changesets') %> <label><%= l(:label_revision_plural) %></label>
8 <%= check_box_tag 'scope[]', 'issues', (@scope.include? 'issues') %> <label><%= l(:label_issue_plural) %></label>
9 <% end %>
9 <% if @project.repository %>
10 <%= check_box_tag 'scope[]', 'news', (@scope.include? 'news') %> <label><%= l(:label_news_plural) %></label>
10 <%= check_box_tag 'scope[]', 'changesets', (@scope.include? 'changesets') %> <label><%= l(:label_revision_plural) %></label>
11 <%= check_box_tag 'scope[]', 'documents', (@scope.include? 'documents') %> <label><%= l(:label_document_plural) %></label>
11 <% end %>
12 <% if @project.wiki %>
12 <%= check_box_tag 'scope[]', 'news', (@scope.include? 'news') %> <label><%= l(:label_news_plural) %></label>
13 <%= check_box_tag 'scope[]', 'wiki', (@scope.include? 'wiki') %> <label><%= l(:label_wiki) %></label>
13 <%= check_box_tag 'scope[]', 'documents', (@scope.include? 'documents') %> <label><%= l(:label_document_plural) %></label>
14 <% end %>
14 <% if @project.wiki %>
15 <br />
15 <%= check_box_tag 'scope[]', 'wiki', (@scope.include? 'wiki') %> <label><%= l(:label_wiki) %></label>
16 <%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></p>
16 <% end %>
17 <%= submit_tag l(:button_submit), :name => 'submit' %>
17 <% else %>
18 <% end %>
18 <%= check_box_tag 'scope[]', 'projects', (@scope.include? 'projects') %> <label><%= l(:label_project_plural) %></label>
19 </div>
19 <% end %>
20
20 <br />
21 <% if @results %>
21 <%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></p>
22 <h3><%= lwr(:label_result, @results.length) %></h3>
22 <%= submit_tag l(:button_submit), :name => 'submit' %>
23 <ul>
23 <% end %>
24 <% @results.each do |e| %>
24 </div>
25 <li><p>
25
26 <% if e.is_a? Issue %>
26 <% if @results %>
27 <%= link_to_issue e %>: <%= highlight_tokens(h(e.subject), @tokens) %><br />
27 <h3><%= lwr(:label_result, @results.length) %></h3>
28 <%= highlight_tokens(e.description, @tokens) %><br />
28 <ul>
29 <i><%= e.author.name %>, <%= format_time(e.created_on) %></i>
29 <% @results.each do |e| %>
30 <% elsif e.is_a? News %>
30 <li><p>
31 <%=l(:label_news)%>: <%= link_to highlight_tokens(h(e.title), @tokens), :controller => 'news', :action => 'show', :id => e %><br />
31 <% if e.is_a? Project %>
32 <%= highlight_tokens(e.description, @tokens) %><br />
32 <%= link_to highlight_tokens(h(e.name), @tokens), :controller => 'projects', :action => 'show', :id => e %><br />
33 <i><%= e.author.name %>, <%= format_time(e.created_on) %></i>
33 <%= highlight_tokens(e.description, @tokens) %>
34 <% elsif e.is_a? Document %>
34 <% elsif e.is_a? Issue %>
35 <%=l(:label_document)%>: <%= link_to highlight_tokens(h(e.title), @tokens), :controller => 'documents', :action => 'show', :id => e %><br />
35 <%= link_to_issue e %>: <%= highlight_tokens(h(e.subject), @tokens) %><br />
36 <%= highlight_tokens(e.description, @tokens) %><br />
36 <%= highlight_tokens(e.description, @tokens) %><br />
37 <i><%= format_time(e.created_on) %></i>
37 <i><%= e.author.name %>, <%= format_time(e.created_on) %></i>
38 <% elsif e.is_a? WikiPage %>
38 <% elsif e.is_a? News %>
39 <%=l(:label_wiki)%>: <%= link_to highlight_tokens(h(e.pretty_title), @tokens), :controller => 'wiki', :action => 'index', :id => @project, :page => e.title %><br />
39 <%=l(:label_news)%>: <%= link_to highlight_tokens(h(e.title), @tokens), :controller => 'news', :action => 'show', :id => e %><br />
40 <%= highlight_tokens(e.content.text, @tokens) %><br />
40 <%= highlight_tokens(e.description, @tokens) %><br />
41 <i><%= e.content.author ? e.content.author.name : "Anonymous" %>, <%= format_time(e.content.updated_on) %></i>
41 <i><%= e.author.name %>, <%= format_time(e.created_on) %></i>
42 <% elsif e.is_a? Changeset %>
42 <% elsif e.is_a? Document %>
43 <%=l(:label_revision)%> <%= link_to h(e.revision), :controller => 'repositories', :action => 'revision', :id => @project, :rev => e.revision %><br />
43 <%=l(:label_document)%>: <%= link_to highlight_tokens(h(e.title), @tokens), :controller => 'documents', :action => 'show', :id => e %><br />
44 <%= highlight_tokens(e.comments, @tokens) %><br />
44 <%= highlight_tokens(e.description, @tokens) %><br />
45 <em><%= e.committer.blank? ? e.committer : "Anonymous" %>, <%= format_time(e.committed_on) %></em>
45 <i><%= format_time(e.created_on) %></i>
46 <% end %>
46 <% elsif e.is_a? WikiPage %>
47 </p></li>
47 <%=l(:label_wiki)%>: <%= link_to highlight_tokens(h(e.pretty_title), @tokens), :controller => 'wiki', :action => 'index', :id => @project, :page => e.title %><br />
48 <% end %>
48 <%= highlight_tokens(e.content.text, @tokens) %><br />
49 </ul>
49 <i><%= e.content.author ? e.content.author.name : "Anonymous" %>, <%= format_time(e.content.updated_on) %></i>
50 <% elsif e.is_a? Changeset %>
51 <%=l(:label_revision)%> <%= link_to h(e.revision), :controller => 'repositories', :action => 'revision', :id => @project, :rev => e.revision %><br />
52 <%= highlight_tokens(e.comments, @tokens) %><br />
53 <em><%= e.committer.blank? ? e.committer : "Anonymous" %>, <%= format_time(e.committed_on) %></em>
54 <% end %>
55 </p></li>
56 <% end %>
57 </ul>
50 <% end %> No newline at end of file
58 <% end %>
@@ -66,6 +66,8 font-weight:normal;
66 font-family: Trebuchet MS,Georgia,"Times New Roman",serif;
66 font-family: Trebuchet MS,Georgia,"Times New Roman",serif;
67 }
67 }
68
68
69 #header a {color:#fff;}
70
69 #navigation{
71 #navigation{
70 height:2.2em;
72 height:2.2em;
71 line-height:2.2em;
73 line-height:2.2em;
@@ -125,14 +125,4 class ProjectsControllerTest < Test::Unit::TestCase
125 assert_template 'activity'
125 assert_template 'activity'
126 assert_not_nil assigns(:events_by_day)
126 assert_not_nil assigns(:events_by_day)
127 end
127 end
128
129 def test_search
130 get :search, :id => 1
131 assert_response :success
132 assert_template 'search'
133
134 get :search, :id => 1, :token => "can", :scope => ["issues", "news", "documents"]
135 assert_response :success
136 assert_template 'search'
137 end
138 end
128 end
General Comments 0
You need to be logged in to leave comments. Login now