##// END OF EJS Templates
Additional tests for SearchController and handle my_projects scope without memberships....
Jean-Philippe Lang -
r8146:bb0cd34f4e2e
parent child
Show More
@@ -1,162 +1,202
1 require File.expand_path('../../test_helper', __FILE__)
1 require File.expand_path('../../test_helper', __FILE__)
2 require 'search_controller'
2 require 'search_controller'
3
3
4 # Re-raise errors caught by the controller.
4 # Re-raise errors caught by the controller.
5 class SearchController; def rescue_action(e) raise e end; end
5 class SearchController; def rescue_action(e) raise e end; end
6
6
7 class SearchControllerTest < ActionController::TestCase
7 class SearchControllerTest < ActionController::TestCase
8 fixtures :projects, :enabled_modules, :roles, :users, :members, :member_roles,
8 fixtures :projects, :enabled_modules, :roles, :users, :members, :member_roles,
9 :issues, :trackers, :issue_statuses,
9 :issues, :trackers, :issue_statuses,
10 :custom_fields, :custom_values,
10 :custom_fields, :custom_values,
11 :repositories, :changesets
11 :repositories, :changesets
12
12
13 def setup
13 def setup
14 @controller = SearchController.new
14 @controller = SearchController.new
15 @request = ActionController::TestRequest.new
15 @request = ActionController::TestRequest.new
16 @response = ActionController::TestResponse.new
16 @response = ActionController::TestResponse.new
17 User.current = nil
17 User.current = nil
18 end
18 end
19
19
20 def test_search_for_projects
20 def test_search_for_projects
21 get :index
21 get :index
22 assert_response :success
22 assert_response :success
23 assert_template 'index'
23 assert_template 'index'
24
24
25 get :index, :q => "cook"
25 get :index, :q => "cook"
26 assert_response :success
26 assert_response :success
27 assert_template 'index'
27 assert_template 'index'
28 assert assigns(:results).include?(Project.find(1))
28 assert assigns(:results).include?(Project.find(1))
29 end
29 end
30
30
31 def test_search_all_projects
31 def test_search_all_projects
32 get :index, :q => 'recipe subproject commit', :all_words => ''
32 get :index, :q => 'recipe subproject commit', :all_words => ''
33 assert_response :success
33 assert_response :success
34 assert_template 'index'
34 assert_template 'index'
35
35
36 assert assigns(:results).include?(Issue.find(2))
36 assert assigns(:results).include?(Issue.find(2))
37 assert assigns(:results).include?(Issue.find(5))
37 assert assigns(:results).include?(Issue.find(5))
38 assert assigns(:results).include?(Changeset.find(101))
38 assert assigns(:results).include?(Changeset.find(101))
39 assert_tag :dt, :attributes => { :class => /issue/ },
39 assert_tag :dt, :attributes => { :class => /issue/ },
40 :child => { :tag => 'a', :content => /Add ingredients categories/ },
40 :child => { :tag => 'a', :content => /Add ingredients categories/ },
41 :sibling => { :tag => 'dd', :content => /should be classified by categories/ }
41 :sibling => { :tag => 'dd', :content => /should be classified by categories/ }
42
42
43 assert assigns(:results_by_type).is_a?(Hash)
43 assert assigns(:results_by_type).is_a?(Hash)
44 assert_equal 5, assigns(:results_by_type)['changesets']
44 assert_equal 5, assigns(:results_by_type)['changesets']
45 assert_tag :a, :content => 'Changesets (5)'
45 assert_tag :a, :content => 'Changesets (5)'
46 end
46 end
47
47
48 def test_search_issues
48 def test_search_issues
49 get :index, :q => 'issue', :issues => 1
49 get :index, :q => 'issue', :issues => 1
50 assert_response :success
50 assert_response :success
51 assert_template 'index'
51 assert_template 'index'
52
52
53 assert_equal true, assigns(:all_words)
53 assert_equal true, assigns(:all_words)
54 assert_equal false, assigns(:titles_only)
54 assert_equal false, assigns(:titles_only)
55 assert assigns(:results).include?(Issue.find(8))
55 assert assigns(:results).include?(Issue.find(8))
56 assert assigns(:results).include?(Issue.find(5))
56 assert assigns(:results).include?(Issue.find(5))
57 assert_tag :dt, :attributes => { :class => /issue closed/ },
57 assert_tag :dt, :attributes => { :class => /issue closed/ },
58 :child => { :tag => 'a', :content => /Closed/ }
58 :child => { :tag => 'a', :content => /Closed/ }
59 end
59 end
60
60
61 def test_search_all_projects_with_scope_param
62 get :index, :q => 'issue', :scope => 'all'
63 assert_response :success
64 assert_template 'index'
65 assert assigns(:results).present?
66 end
67
68 def test_search_my_projects
69 @request.session[:user_id] = 2
70 get :index, :id => 1, :q => 'recipe subproject', :scope => 'my_projects', :all_words => ''
71 assert_response :success
72 assert_template 'index'
73 assert assigns(:results).include?(Issue.find(1))
74 assert !assigns(:results).include?(Issue.find(5))
75 end
76
77 def test_search_my_projects_without_memberships
78 # anonymous user has no memberships
79 get :index, :id => 1, :q => 'recipe subproject', :scope => 'my_projects', :all_words => ''
80 assert_response :success
81 assert_template 'index'
82 assert assigns(:results).empty?
83 end
84
61 def test_search_project_and_subprojects
85 def test_search_project_and_subprojects
62 get :index, :id => 1, :q => 'recipe subproject', :scope => 'subprojects', :all_words => ''
86 get :index, :id => 1, :q => 'recipe subproject', :scope => 'subprojects', :all_words => ''
63 assert_response :success
87 assert_response :success
64 assert_template 'index'
88 assert_template 'index'
65 assert assigns(:results).include?(Issue.find(1))
89 assert assigns(:results).include?(Issue.find(1))
66 assert assigns(:results).include?(Issue.find(5))
90 assert assigns(:results).include?(Issue.find(5))
67 end
91 end
68
92
69 def test_search_without_searchable_custom_fields
93 def test_search_without_searchable_custom_fields
70 CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}"
94 CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}"
71
95
72 get :index, :id => 1
96 get :index, :id => 1
73 assert_response :success
97 assert_response :success
74 assert_template 'index'
98 assert_template 'index'
75 assert_not_nil assigns(:project)
99 assert_not_nil assigns(:project)
76
100
77 get :index, :id => 1, :q => "can"
101 get :index, :id => 1, :q => "can"
78 assert_response :success
102 assert_response :success
79 assert_template 'index'
103 assert_template 'index'
80 end
104 end
81
105
82 def test_search_with_searchable_custom_fields
106 def test_search_with_searchable_custom_fields
83 get :index, :id => 1, :q => "stringforcustomfield"
107 get :index, :id => 1, :q => "stringforcustomfield"
84 assert_response :success
108 assert_response :success
85 results = assigns(:results)
109 results = assigns(:results)
86 assert_not_nil results
110 assert_not_nil results
87 assert_equal 1, results.size
111 assert_equal 1, results.size
88 assert results.include?(Issue.find(7))
112 assert results.include?(Issue.find(7))
89 end
113 end
90
114
91 def test_search_all_words
115 def test_search_all_words
92 # 'all words' is on by default
116 # 'all words' is on by default
93 get :index, :id => 1, :q => 'recipe updating saving', :all_words => '1'
117 get :index, :id => 1, :q => 'recipe updating saving', :all_words => '1'
94 assert_equal true, assigns(:all_words)
118 assert_equal true, assigns(:all_words)
95 results = assigns(:results)
119 results = assigns(:results)
96 assert_not_nil results
120 assert_not_nil results
97 assert_equal 1, results.size
121 assert_equal 1, results.size
98 assert results.include?(Issue.find(3))
122 assert results.include?(Issue.find(3))
99 end
123 end
100
124
101 def test_search_one_of_the_words
125 def test_search_one_of_the_words
102 get :index, :id => 1, :q => 'recipe updating saving', :all_words => ''
126 get :index, :id => 1, :q => 'recipe updating saving', :all_words => ''
103 assert_equal false, assigns(:all_words)
127 assert_equal false, assigns(:all_words)
104 results = assigns(:results)
128 results = assigns(:results)
105 assert_not_nil results
129 assert_not_nil results
106 assert_equal 3, results.size
130 assert_equal 3, results.size
107 assert results.include?(Issue.find(3))
131 assert results.include?(Issue.find(3))
108 end
132 end
109
133
110 def test_search_titles_only_without_result
134 def test_search_titles_only_without_result
111 get :index, :id => 1, :q => 'recipe updating saving', :titles_only => '1'
135 get :index, :id => 1, :q => 'recipe updating saving', :titles_only => '1'
112 results = assigns(:results)
136 results = assigns(:results)
113 assert_not_nil results
137 assert_not_nil results
114 assert_equal 0, results.size
138 assert_equal 0, results.size
115 end
139 end
116
140
117 def test_search_titles_only
141 def test_search_titles_only
118 get :index, :id => 1, :q => 'recipe', :titles_only => '1'
142 get :index, :id => 1, :q => 'recipe', :titles_only => '1'
119 assert_equal true, assigns(:titles_only)
143 assert_equal true, assigns(:titles_only)
120 results = assigns(:results)
144 results = assigns(:results)
121 assert_not_nil results
145 assert_not_nil results
122 assert_equal 2, results.size
146 assert_equal 2, results.size
123 end
147 end
124
148
125 def test_search_content
149 def test_search_content
126 Issue.update_all("description = 'This is a searchkeywordinthecontent'", "id=1")
150 Issue.update_all("description = 'This is a searchkeywordinthecontent'", "id=1")
127
151
128 get :index, :id => 1, :q => 'searchkeywordinthecontent', :titles_only => ''
152 get :index, :id => 1, :q => 'searchkeywordinthecontent', :titles_only => ''
129 assert_equal false, assigns(:titles_only)
153 assert_equal false, assigns(:titles_only)
130 results = assigns(:results)
154 results = assigns(:results)
131 assert_not_nil results
155 assert_not_nil results
132 assert_equal 1, results.size
156 assert_equal 1, results.size
133 end
157 end
134
158
159 def test_search_with_offset
160 get :index, :q => 'coo', :offset => '20080806073000'
161 assert_response :success
162 results = assigns(:results)
163 assert results.any?
164 assert results.map(&:event_datetime).max < '20080806T073000'.to_time
165 end
166
167 def test_search_previous_with_offset
168 get :index, :q => 'coo', :offset => '20080806073000', :previous => '1'
169 assert_response :success
170 results = assigns(:results)
171 assert results.any?
172 assert results.map(&:event_datetime).min >= '20080806T073000'.to_time
173 end
174
135 def test_search_with_invalid_project_id
175 def test_search_with_invalid_project_id
136 get :index, :id => 195, :q => 'recipe'
176 get :index, :id => 195, :q => 'recipe'
137 assert_response 404
177 assert_response 404
138 assert_nil assigns(:results)
178 assert_nil assigns(:results)
139 end
179 end
140
180
141 def test_quick_jump_to_issue
181 def test_quick_jump_to_issue
142 # issue of a public project
182 # issue of a public project
143 get :index, :q => "3"
183 get :index, :q => "3"
144 assert_redirected_to '/issues/3'
184 assert_redirected_to '/issues/3'
145
185
146 # issue of a private project
186 # issue of a private project
147 get :index, :q => "4"
187 get :index, :q => "4"
148 assert_response :success
188 assert_response :success
149 assert_template 'index'
189 assert_template 'index'
150 end
190 end
151
191
152 def test_large_integer
192 def test_large_integer
153 get :index, :q => '4615713488'
193 get :index, :q => '4615713488'
154 assert_response :success
194 assert_response :success
155 assert_template 'index'
195 assert_template 'index'
156 end
196 end
157
197
158 def test_tokens_with_quotes
198 def test_tokens_with_quotes
159 get :index, :id => 1, :q => '"good bye" hello "bye bye"'
199 get :index, :id => 1, :q => '"good bye" hello "bye bye"'
160 assert_equal ["good bye", "hello", "bye bye"], assigns(:tokens)
200 assert_equal ["good bye", "hello", "bye bye"], assigns(:tokens)
161 end
201 end
162 end
202 end
@@ -1,130 +1,135
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module Redmine
18 module Redmine
19 module Acts
19 module Acts
20 module Searchable
20 module Searchable
21 def self.included(base)
21 def self.included(base)
22 base.extend ClassMethods
22 base.extend ClassMethods
23 end
23 end
24
24
25 module ClassMethods
25 module ClassMethods
26 # Options:
26 # Options:
27 # * :columns - a column or an array of columns to search
27 # * :columns - a column or an array of columns to search
28 # * :project_key - project foreign key (default to project_id)
28 # * :project_key - project foreign key (default to project_id)
29 # * :date_column - name of the datetime column (default to created_on)
29 # * :date_column - name of the datetime column (default to created_on)
30 # * :sort_order - name of the column used to sort results (default to :date_column or created_on)
30 # * :sort_order - name of the column used to sort results (default to :date_column or created_on)
31 # * :permission - permission required to search the model (default to :view_"objects")
31 # * :permission - permission required to search the model (default to :view_"objects")
32 def acts_as_searchable(options = {})
32 def acts_as_searchable(options = {})
33 return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods)
33 return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods)
34
34
35 cattr_accessor :searchable_options
35 cattr_accessor :searchable_options
36 self.searchable_options = options
36 self.searchable_options = options
37
37
38 if searchable_options[:columns].nil?
38 if searchable_options[:columns].nil?
39 raise 'No searchable column defined.'
39 raise 'No searchable column defined.'
40 elsif !searchable_options[:columns].is_a?(Array)
40 elsif !searchable_options[:columns].is_a?(Array)
41 searchable_options[:columns] = [] << searchable_options[:columns]
41 searchable_options[:columns] = [] << searchable_options[:columns]
42 end
42 end
43
43
44 searchable_options[:project_key] ||= "#{table_name}.project_id"
44 searchable_options[:project_key] ||= "#{table_name}.project_id"
45 searchable_options[:date_column] ||= "#{table_name}.created_on"
45 searchable_options[:date_column] ||= "#{table_name}.created_on"
46 searchable_options[:order_column] ||= searchable_options[:date_column]
46 searchable_options[:order_column] ||= searchable_options[:date_column]
47
47
48 # Should we search custom fields on this model ?
48 # Should we search custom fields on this model ?
49 searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil?
49 searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil?
50
50
51 send :include, Redmine::Acts::Searchable::InstanceMethods
51 send :include, Redmine::Acts::Searchable::InstanceMethods
52 end
52 end
53 end
53 end
54
54
55 module InstanceMethods
55 module InstanceMethods
56 def self.included(base)
56 def self.included(base)
57 base.extend ClassMethods
57 base.extend ClassMethods
58 end
58 end
59
59
60 module ClassMethods
60 module ClassMethods
61 # Searches the model for the given tokens
61 # Searches the model for the given tokens
62 # projects argument can be either nil (will search all projects), a project or an array of projects
62 # projects argument can be either nil (will search all projects), a project or an array of projects
63 # Returns the results and the results count
63 # Returns the results and the results count
64 def search(tokens, projects=nil, options={})
64 def search(tokens, projects=nil, options={})
65 if projects.is_a?(Array) && projects.empty?
66 # no results
67 return [[], 0]
68 end
69
65 # TODO: make user an argument
70 # TODO: make user an argument
66 user = User.current
71 user = User.current
67 tokens = [] << tokens unless tokens.is_a?(Array)
72 tokens = [] << tokens unless tokens.is_a?(Array)
68 projects = [] << projects unless projects.nil? || projects.is_a?(Array)
73 projects = [] << projects unless projects.nil? || projects.is_a?(Array)
69
74
70 find_options = {:include => searchable_options[:include]}
75 find_options = {:include => searchable_options[:include]}
71 find_options[:order] = "#{searchable_options[:order_column]} " + (options[:before] ? 'DESC' : 'ASC')
76 find_options[:order] = "#{searchable_options[:order_column]} " + (options[:before] ? 'DESC' : 'ASC')
72
77
73 limit_options = {}
78 limit_options = {}
74 limit_options[:limit] = options[:limit] if options[:limit]
79 limit_options[:limit] = options[:limit] if options[:limit]
75 if options[:offset]
80 if options[:offset]
76 limit_options[:conditions] = "(#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')"
81 limit_options[:conditions] = "(#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')"
77 end
82 end
78
83
79 columns = searchable_options[:columns]
84 columns = searchable_options[:columns]
80 columns = columns[0..0] if options[:titles_only]
85 columns = columns[0..0] if options[:titles_only]
81
86
82 token_clauses = columns.collect {|column| "(LOWER(#{column}) LIKE ?)"}
87 token_clauses = columns.collect {|column| "(LOWER(#{column}) LIKE ?)"}
83
88
84 if !options[:titles_only] && searchable_options[:search_custom_fields]
89 if !options[:titles_only] && searchable_options[:search_custom_fields]
85 searchable_custom_field_ids = CustomField.find(:all,
90 searchable_custom_field_ids = CustomField.find(:all,
86 :select => 'id',
91 :select => 'id',
87 :conditions => { :type => "#{self.name}CustomField",
92 :conditions => { :type => "#{self.name}CustomField",
88 :searchable => true }).collect(&:id)
93 :searchable => true }).collect(&:id)
89 if searchable_custom_field_ids.any?
94 if searchable_custom_field_ids.any?
90 custom_field_sql = "#{table_name}.id IN (SELECT customized_id FROM #{CustomValue.table_name}" +
95 custom_field_sql = "#{table_name}.id IN (SELECT customized_id FROM #{CustomValue.table_name}" +
91 " WHERE customized_type='#{self.name}' AND customized_id=#{table_name}.id AND LOWER(value) LIKE ?" +
96 " WHERE customized_type='#{self.name}' AND customized_id=#{table_name}.id AND LOWER(value) LIKE ?" +
92 " AND #{CustomValue.table_name}.custom_field_id IN (#{searchable_custom_field_ids.join(',')}))"
97 " AND #{CustomValue.table_name}.custom_field_id IN (#{searchable_custom_field_ids.join(',')}))"
93 token_clauses << custom_field_sql
98 token_clauses << custom_field_sql
94 end
99 end
95 end
100 end
96
101
97 sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
102 sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
98
103
99 find_options[:conditions] = [sql, * (tokens.collect {|w| "%#{w.downcase}%"} * token_clauses.size).sort]
104 find_options[:conditions] = [sql, * (tokens.collect {|w| "%#{w.downcase}%"} * token_clauses.size).sort]
100
105
101 scope = self
106 scope = self
102 project_conditions = []
107 project_conditions = []
103 if searchable_options.has_key?(:permission)
108 if searchable_options.has_key?(:permission)
104 project_conditions << Project.allowed_to_condition(user, searchable_options[:permission] || :view_project)
109 project_conditions << Project.allowed_to_condition(user, searchable_options[:permission] || :view_project)
105 elsif respond_to?(:visible)
110 elsif respond_to?(:visible)
106 scope = scope.visible(user)
111 scope = scope.visible(user)
107 else
112 else
108 ActiveSupport::Deprecation.warn "acts_as_searchable with implicit :permission option is deprecated. Add a visible scope to the #{self.name} model or use explicit :permission option."
113 ActiveSupport::Deprecation.warn "acts_as_searchable with implicit :permission option is deprecated. Add a visible scope to the #{self.name} model or use explicit :permission option."
109 project_conditions << Project.allowed_to_condition(user, "view_#{self.name.underscore.pluralize}".to_sym)
114 project_conditions << Project.allowed_to_condition(user, "view_#{self.name.underscore.pluralize}".to_sym)
110 end
115 end
111 # TODO: use visible scope options instead
116 # TODO: use visible scope options instead
112 project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
117 project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
113 project_conditions = project_conditions.empty? ? nil : project_conditions.join(' AND ')
118 project_conditions = project_conditions.empty? ? nil : project_conditions.join(' AND ')
114
119
115 results = []
120 results = []
116 results_count = 0
121 results_count = 0
117
122
118 with_scope(:find => {:conditions => project_conditions}) do
123 with_scope(:find => {:conditions => project_conditions}) do
119 with_scope(:find => find_options) do
124 with_scope(:find => find_options) do
120 results_count = scope.count(:all)
125 results_count = scope.count(:all)
121 results = scope.find(:all, limit_options)
126 results = scope.find(:all, limit_options)
122 end
127 end
123 end
128 end
124 [results, results_count]
129 [results, results_count]
125 end
130 end
126 end
131 end
127 end
132 end
128 end
133 end
129 end
134 end
130 end
135 end
General Comments 0
You need to be logged in to leave comments. Login now