##// END OF EJS Templates
Merged r6201 from trunk....
Jean-Philippe Lang -
r6082:2a799e83675a
parent child
Show More
@@ -1,114 +1,114
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class SearchController < ApplicationController
19 19 before_filter :find_optional_project
20 20
21 21 helper :messages
22 22 include MessagesHelper
23 23
24 24 def index
25 25 @question = params[:q] || ""
26 26 @question.strip!
27 @all_words = params[:all_words] || (params[:submit] ? false : true)
28 @titles_only = !params[:titles_only].nil?
27 @all_words = params[:all_words] ? params[:all_words].present? : true
28 @titles_only = params[:titles_only] ? params[:titles_only].present? : false
29 29
30 30 projects_to_search =
31 31 case params[:scope]
32 32 when 'all'
33 33 nil
34 34 when 'my_projects'
35 35 User.current.memberships.collect(&:project)
36 36 when 'subprojects'
37 37 @project ? (@project.self_and_descendants.active) : nil
38 38 else
39 39 @project
40 40 end
41 41
42 42 offset = nil
43 43 begin; offset = params[:offset].to_time if params[:offset]; rescue; end
44 44
45 45 # quick jump to an issue
46 46 if @question.match(/^#?(\d+)$/) && Issue.visible.find_by_id($1.to_i)
47 47 redirect_to :controller => "issues", :action => "show", :id => $1
48 48 return
49 49 end
50 50
51 51 @object_types = Redmine::Search.available_search_types.dup
52 52 if projects_to_search.is_a? Project
53 53 # don't search projects
54 54 @object_types.delete('projects')
55 55 # only show what the user is allowed to view
56 56 @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
57 57 end
58 58
59 59 @scope = @object_types.select {|t| params[t]}
60 60 @scope = @object_types if @scope.empty?
61 61
62 62 # extract tokens from the question
63 63 # eg. hello "bye bye" => ["hello", "bye bye"]
64 64 @tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
65 65 # tokens must be at least 2 characters long
66 66 @tokens = @tokens.uniq.select {|w| w.length > 1 }
67 67
68 68 if !@tokens.empty?
69 69 # no more than 5 tokens to search for
70 70 @tokens.slice! 5..-1 if @tokens.size > 5
71 71
72 72 @results = []
73 73 @results_by_type = Hash.new {|h,k| h[k] = 0}
74 74
75 75 limit = 10
76 76 @scope.each do |s|
77 77 r, c = s.singularize.camelcase.constantize.search(@tokens, projects_to_search,
78 78 :all_words => @all_words,
79 79 :titles_only => @titles_only,
80 80 :limit => (limit+1),
81 81 :offset => offset,
82 82 :before => params[:previous].nil?)
83 83 @results += r
84 84 @results_by_type[s] += c
85 85 end
86 86 @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
87 87 if params[:previous].nil?
88 88 @pagination_previous_date = @results[0].event_datetime if offset && @results[0]
89 89 if @results.size > limit
90 90 @pagination_next_date = @results[limit-1].event_datetime
91 91 @results = @results[0, limit]
92 92 end
93 93 else
94 94 @pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
95 95 if @results.size > limit
96 96 @pagination_previous_date = @results[-(limit)].event_datetime
97 97 @results = @results[-(limit), limit]
98 98 end
99 99 end
100 100 else
101 101 @question = ""
102 102 end
103 103 render :layout => false if request.xhr?
104 104 end
105 105
106 106 private
107 107 def find_optional_project
108 108 return true unless params[:id]
109 109 @project = Project.find(params[:id])
110 110 check_project_privacy
111 111 rescue ActiveRecord::RecordNotFound
112 112 render_404
113 113 end
114 114 end
@@ -1,64 +1,64
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module SearchHelper
19 19 def highlight_tokens(text, tokens)
20 20 return text unless text && tokens && !tokens.empty?
21 21 re_tokens = tokens.collect {|t| Regexp.escape(t)}
22 22 regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE
23 23 result = ''
24 24 text.split(regexp).each_with_index do |words, i|
25 25 if result.length > 1200
26 26 # maximum length of the preview reached
27 27 result << '...'
28 28 break
29 29 end
30 30 words = words.mb_chars
31 31 if i.even?
32 32 result << h(words.length > 100 ? "#{words.slice(0..44)} ... #{words.slice(-45..-1)}" : words)
33 33 else
34 34 t = (tokens.index(words.downcase) || 0) % 4
35 35 result << content_tag('span', h(words), :class => "highlight token-#{t}")
36 36 end
37 37 end
38 38 result
39 39 end
40 40
41 41 def type_label(t)
42 42 l("label_#{t.singularize}_plural", :default => t.to_s.humanize)
43 43 end
44 44
45 45 def project_select_tag
46 46 options = [[l(:label_project_all), 'all']]
47 47 options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty?
48 48 options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.descendants.active.empty?
49 49 options << [@project.name, ''] unless @project.nil?
50 50 select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1
51 51 end
52 52
53 53 def render_results_by_type(results_by_type)
54 54 links = []
55 55 # Sorts types by results count
56 56 results_by_type.keys.sort {|a, b| results_by_type[b] <=> results_by_type[a]}.each do |t|
57 57 c = results_by_type[t]
58 58 next if c == 0
59 59 text = "#{type_label(t)} (#{c})"
60 links << link_to(text, :q => params[:q], :titles_only => params[:title_only], :all_words => params[:all_words], :scope => params[:scope], t => 1)
60 links << link_to(text, :q => params[:q], :titles_only => params[:titles_only], :all_words => params[:all_words], :scope => params[:scope], t => 1)
61 61 end
62 62 ('<ul>' + links.map {|link| content_tag('li', link)}.join(' ') + '</ul>') unless links.empty?
63 63 end
64 64 end
@@ -1,47 +1,49
1 1 <h2><%= l(:label_search) %></h2>
2 2
3 3 <div class="box">
4 4 <% form_tag({}, :method => :get) do %>
5 5 <p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %>
6 6 <%= javascript_tag "Field.focus('search-input')" %>
7 7 <%= project_select_tag %>
8 <%= hidden_field_tag 'all_words', '', :id => nil %>
8 9 <label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label>
10 <%= hidden_field_tag 'titles_only', '', :id => nil %>
9 11 <label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label>
10 12 </p>
11 13 <p>
12 14 <% @object_types.each do |t| %>
13 15 <label><%= check_box_tag t, 1, @scope.include?(t) %> <%= type_label(t) %></label>
14 16 <% end %>
15 17 </p>
16 18
17 19 <p><%= submit_tag l(:button_submit), :name => 'submit' %></p>
18 20 <% end %>
19 21 </div>
20 22
21 23 <% if @results %>
22 24 <div id="search-results-counts">
23 25 <%= render_results_by_type(@results_by_type) unless @scope.size == 1 %>
24 26 </div>
25 27
26 28 <h3><%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)</h3>
27 29 <dl id="search-results">
28 30 <% @results.each do |e| %>
29 31 <dt class="<%= e.event_type %>"><%= content_tag('span', h(e.project), :class => 'project') unless @project == e.project %> <%= link_to highlight_tokens(truncate(e.event_title, :length => 255), @tokens), e.event_url %></dt>
30 32 <dd><span class="description"><%= highlight_tokens(e.event_description, @tokens) %></span>
31 33 <span class="author"><%= format_time(e.event_datetime) %></span></dd>
32 34 <% end %>
33 35 </dl>
34 36 <% end %>
35 37
36 38 <p><center>
37 39 <% if @pagination_previous_date %>
38 40 <%= link_to_content_update('&#171; ' + l(:label_previous),
39 41 params.merge(:previous => 1, :offset => @pagination_previous_date.strftime("%Y%m%d%H%M%S"))) %>&nbsp;
40 42 <% end %>
41 43 <% if @pagination_next_date %>
42 44 <%= link_to_content_update(l(:label_next) + ' &#187;',
43 45 params.merge(:previous => nil, :offset => @pagination_next_date.strftime("%Y%m%d%H%M%S"))) %>
44 46 <% end %>
45 47 </center></p>
46 48
47 49 <% html_title(l(:label_search)) -%>
@@ -1,147 +1,162
1 1 require File.expand_path('../../test_helper', __FILE__)
2 2 require 'search_controller'
3 3
4 4 # Re-raise errors caught by the controller.
5 5 class SearchController; def rescue_action(e) raise e end; end
6 6
7 7 class SearchControllerTest < ActionController::TestCase
8 8 fixtures :projects, :enabled_modules, :roles, :users, :members, :member_roles,
9 9 :issues, :trackers, :issue_statuses,
10 10 :custom_fields, :custom_values,
11 11 :repositories, :changesets
12 12
13 13 def setup
14 14 @controller = SearchController.new
15 15 @request = ActionController::TestRequest.new
16 16 @response = ActionController::TestResponse.new
17 17 User.current = nil
18 18 end
19 19
20 20 def test_search_for_projects
21 21 get :index
22 22 assert_response :success
23 23 assert_template 'index'
24 24
25 25 get :index, :q => "cook"
26 26 assert_response :success
27 27 assert_template 'index'
28 28 assert assigns(:results).include?(Project.find(1))
29 29 end
30 30
31 31 def test_search_all_projects
32 get :index, :q => 'recipe subproject commit', :submit => 'Search'
32 get :index, :q => 'recipe subproject commit', :all_words => ''
33 33 assert_response :success
34 34 assert_template 'index'
35 35
36 36 assert assigns(:results).include?(Issue.find(2))
37 37 assert assigns(:results).include?(Issue.find(5))
38 38 assert assigns(:results).include?(Changeset.find(101))
39 39 assert_tag :dt, :attributes => { :class => /issue/ },
40 40 :child => { :tag => 'a', :content => /Add ingredients categories/ },
41 41 :sibling => { :tag => 'dd', :content => /should be classified by categories/ }
42 42
43 43 assert assigns(:results_by_type).is_a?(Hash)
44 44 assert_equal 5, assigns(:results_by_type)['changesets']
45 45 assert_tag :a, :content => 'Changesets (5)'
46 46 end
47 47
48 48 def test_search_issues
49 49 get :index, :q => 'issue', :issues => 1
50 50 assert_response :success
51 51 assert_template 'index'
52 52
53 assert_equal true, assigns(:all_words)
54 assert_equal false, assigns(:titles_only)
53 55 assert assigns(:results).include?(Issue.find(8))
54 56 assert assigns(:results).include?(Issue.find(5))
55 57 assert_tag :dt, :attributes => { :class => /issue closed/ },
56 58 :child => { :tag => 'a', :content => /Closed/ }
57 59 end
58 60
59 61 def test_search_project_and_subprojects
60 get :index, :id => 1, :q => 'recipe subproject', :scope => 'subprojects', :submit => 'Search'
62 get :index, :id => 1, :q => 'recipe subproject', :scope => 'subprojects', :all_words => ''
61 63 assert_response :success
62 64 assert_template 'index'
63 65 assert assigns(:results).include?(Issue.find(1))
64 66 assert assigns(:results).include?(Issue.find(5))
65 67 end
66 68
67 69 def test_search_without_searchable_custom_fields
68 70 CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}"
69 71
70 72 get :index, :id => 1
71 73 assert_response :success
72 74 assert_template 'index'
73 75 assert_not_nil assigns(:project)
74 76
75 77 get :index, :id => 1, :q => "can"
76 78 assert_response :success
77 79 assert_template 'index'
78 80 end
79 81
80 82 def test_search_with_searchable_custom_fields
81 83 get :index, :id => 1, :q => "stringforcustomfield"
82 84 assert_response :success
83 85 results = assigns(:results)
84 86 assert_not_nil results
85 87 assert_equal 1, results.size
86 88 assert results.include?(Issue.find(7))
87 89 end
88 90
89 91 def test_search_all_words
90 92 # 'all words' is on by default
91 get :index, :id => 1, :q => 'recipe updating saving'
93 get :index, :id => 1, :q => 'recipe updating saving', :all_words => '1'
94 assert_equal true, assigns(:all_words)
92 95 results = assigns(:results)
93 96 assert_not_nil results
94 97 assert_equal 1, results.size
95 98 assert results.include?(Issue.find(3))
96 99 end
97 100
98 101 def test_search_one_of_the_words
99 get :index, :id => 1, :q => 'recipe updating saving', :submit => 'Search'
102 get :index, :id => 1, :q => 'recipe updating saving', :all_words => ''
103 assert_equal false, assigns(:all_words)
100 104 results = assigns(:results)
101 105 assert_not_nil results
102 106 assert_equal 3, results.size
103 107 assert results.include?(Issue.find(3))
104 108 end
105 109
106 110 def test_search_titles_only_without_result
107 get :index, :id => 1, :q => 'recipe updating saving', :all_words => '1', :titles_only => '1', :submit => 'Search'
111 get :index, :id => 1, :q => 'recipe updating saving', :titles_only => '1'
108 112 results = assigns(:results)
109 113 assert_not_nil results
110 114 assert_equal 0, results.size
111 115 end
112 116
113 117 def test_search_titles_only
114 get :index, :id => 1, :q => 'recipe', :titles_only => '1', :submit => 'Search'
118 get :index, :id => 1, :q => 'recipe', :titles_only => '1'
119 assert_equal true, assigns(:titles_only)
115 120 results = assigns(:results)
116 121 assert_not_nil results
117 122 assert_equal 2, results.size
118 123 end
119 124
125 def test_search_content
126 Issue.update_all("description = 'This is a searchkeywordinthecontent'", "id=1")
127
128 get :index, :id => 1, :q => 'searchkeywordinthecontent', :titles_only => ''
129 assert_equal false, assigns(:titles_only)
130 results = assigns(:results)
131 assert_not_nil results
132 assert_equal 1, results.size
133 end
134
120 135 def test_search_with_invalid_project_id
121 136 get :index, :id => 195, :q => 'recipe'
122 137 assert_response 404
123 138 assert_nil assigns(:results)
124 139 end
125 140
126 141 def test_quick_jump_to_issue
127 142 # issue of a public project
128 143 get :index, :q => "3"
129 144 assert_redirected_to '/issues/3'
130 145
131 146 # issue of a private project
132 147 get :index, :q => "4"
133 148 assert_response :success
134 149 assert_template 'index'
135 150 end
136 151
137 152 def test_large_integer
138 153 get :index, :q => '4615713488'
139 154 assert_response :success
140 155 assert_template 'index'
141 156 end
142 157
143 158 def test_tokens_with_quotes
144 159 get :index, :id => 1, :q => '"good bye" hello "bye bye"'
145 160 assert_equal ["good bye", "hello", "bye bye"], assigns(:tokens)
146 161 end
147 162 end
General Comments 0
You need to be logged in to leave comments. Login now