##// END OF EJS Templates
Restore accent insensitive search with mysql (#18537)....
Jean-Philippe Lang -
r13385:30175bf85e4d
parent child
Show More
@@ -1,167 +1,165
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 # Adds the search methods to the class.
26 # Adds the search methods to the class.
27 #
27 #
28 # Options:
28 # Options:
29 # * :columns - a column or an array of columns to search
29 # * :columns - a column or an array of columns to search
30 # * :project_key - project foreign key (default to project_id)
30 # * :project_key - project foreign key (default to project_id)
31 # * :date_column - name of the datetime column used to sort results (default to :created_on)
31 # * :date_column - name of the datetime column used to sort results (default to :created_on)
32 # * :permission - permission required to search the model
32 # * :permission - permission required to search the model
33 # * :scope - scope used to search results
33 # * :scope - scope used to search results
34 # * :preload - associations to preload when loading results for display
34 # * :preload - associations to preload when loading results for display
35 def acts_as_searchable(options = {})
35 def acts_as_searchable(options = {})
36 return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods)
36 return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods)
37 options.assert_valid_keys(:columns, :project_key, :date_column, :permission, :scope, :preload)
37 options.assert_valid_keys(:columns, :project_key, :date_column, :permission, :scope, :preload)
38
38
39 cattr_accessor :searchable_options
39 cattr_accessor :searchable_options
40 self.searchable_options = options
40 self.searchable_options = options
41
41
42 if searchable_options[:columns].nil?
42 if searchable_options[:columns].nil?
43 raise 'No searchable column defined.'
43 raise 'No searchable column defined.'
44 elsif !searchable_options[:columns].is_a?(Array)
44 elsif !searchable_options[:columns].is_a?(Array)
45 searchable_options[:columns] = [] << searchable_options[:columns]
45 searchable_options[:columns] = [] << searchable_options[:columns]
46 end
46 end
47
47
48 searchable_options[:project_key] ||= "#{table_name}.project_id"
48 searchable_options[:project_key] ||= "#{table_name}.project_id"
49 searchable_options[:date_column] ||= :created_on
49 searchable_options[:date_column] ||= :created_on
50
50
51 # Should we search custom fields on this model ?
51 # Should we search custom fields on this model ?
52 searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil?
52 searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil?
53
53
54 send :include, Redmine::Acts::Searchable::InstanceMethods
54 send :include, Redmine::Acts::Searchable::InstanceMethods
55 end
55 end
56 end
56 end
57
57
58 module InstanceMethods
58 module InstanceMethods
59 def self.included(base)
59 def self.included(base)
60 base.extend ClassMethods
60 base.extend ClassMethods
61 end
61 end
62
62
63 module ClassMethods
63 module ClassMethods
64 # Searches the model for the given tokens and user visibility.
64 # Searches the model for the given tokens and user visibility.
65 # The projects argument can be either nil (will search all projects), a project or an array of projects.
65 # The projects argument can be either nil (will search all projects), a project or an array of projects.
66 # Returns an array that contains the rank and id of all results.
66 # Returns an array that contains the rank and id of all results.
67 # In current implementation, the rank is the record timestamp.
67 # In current implementation, the rank is the record timestamp.
68 #
68 #
69 # Valid options:
69 # Valid options:
70 # * :titles_only - searches tokens in the first searchable column only
70 # * :titles_only - searches tokens in the first searchable column only
71 # * :all_words - searches results that match all token
71 # * :all_words - searches results that match all token
72 # * :limit - maximum number of results to return
72 # * :limit - maximum number of results to return
73 #
73 #
74 # Example:
74 # Example:
75 # Issue.search_result_ranks_and_ids("foo")
75 # Issue.search_result_ranks_and_ids("foo")
76 # # => [[Tue, 26 Jun 2007 22:16:00 UTC +00:00, 69], [Mon, 08 Oct 2007 14:31:00 UTC +00:00, 123]]
76 # # => [[Tue, 26 Jun 2007 22:16:00 UTC +00:00, 69], [Mon, 08 Oct 2007 14:31:00 UTC +00:00, 123]]
77 def search_result_ranks_and_ids(tokens, user=User.current, projects=nil, options={})
77 def search_result_ranks_and_ids(tokens, user=User.current, projects=nil, options={})
78 if projects.is_a?(Array) && projects.empty?
78 if projects.is_a?(Array) && projects.empty?
79 # no results
79 # no results
80 return []
80 return []
81 end
81 end
82
82
83 tokens = [] << tokens unless tokens.is_a?(Array)
83 tokens = [] << tokens unless tokens.is_a?(Array)
84 projects = [] << projects if projects.is_a?(Project)
84 projects = [] << projects if projects.is_a?(Project)
85
85
86 columns = searchable_options[:columns]
86 columns = searchable_options[:columns]
87 columns = columns[0..0] if options[:titles_only]
87 columns = columns[0..0] if options[:titles_only]
88
88
89 token_clauses = columns.collect {|column| "(#{search_token_match_statement(column)})"}
89 token_clauses = columns.collect {|column| "(#{search_token_match_statement(column)})"}
90
90
91 if !options[:titles_only] && searchable_options[:search_custom_fields]
91 if !options[:titles_only] && searchable_options[:search_custom_fields]
92 searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true)
92 searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true)
93 fields_by_visibility = searchable_custom_fields.group_by {|field|
93 fields_by_visibility = searchable_custom_fields.group_by {|field|
94 field.visibility_by_project_condition(searchable_options[:project_key], user, "cfs.custom_field_id")
94 field.visibility_by_project_condition(searchable_options[:project_key], user, "cfs.custom_field_id")
95 }
95 }
96 # only 1 subquery for all custom fields with the same visibility statement
96 # only 1 subquery for all custom fields with the same visibility statement
97 fields_by_visibility.each do |visibility, fields|
97 fields_by_visibility.each do |visibility, fields|
98 ids = fields.map(&:id).join(',')
98 ids = fields.map(&:id).join(',')
99 sql = "#{table_name}.id IN (SELECT cfs.customized_id FROM #{CustomValue.table_name} cfs" +
99 sql = "#{table_name}.id IN (SELECT cfs.customized_id FROM #{CustomValue.table_name} cfs" +
100 " WHERE cfs.customized_type='#{self.name}' AND cfs.customized_id=#{table_name}.id" +
100 " WHERE cfs.customized_type='#{self.name}' AND cfs.customized_id=#{table_name}.id" +
101 " AND cfs.custom_field_id IN (#{ids})" +
101 " AND cfs.custom_field_id IN (#{ids})" +
102 " AND #{search_token_match_statement('cfs.value')}" +
102 " AND #{search_token_match_statement('cfs.value')}" +
103 " AND #{visibility})"
103 " AND #{visibility})"
104 token_clauses << sql
104 token_clauses << sql
105 end
105 end
106 end
106 end
107
107
108 sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
108 sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
109
109
110 tokens_conditions = [sql, * (tokens.collect {|w| "%#{w}%"} * token_clauses.size).sort]
110 tokens_conditions = [sql, * (tokens.collect {|w| "%#{w}%"} * token_clauses.size).sort]
111
111
112 search_scope(user, projects).
112 search_scope(user, projects).
113 reorder(searchable_options[:date_column] => :desc, :id => :desc).
113 reorder(searchable_options[:date_column] => :desc, :id => :desc).
114 where(tokens_conditions).
114 where(tokens_conditions).
115 limit(options[:limit]).
115 limit(options[:limit]).
116 uniq.
116 uniq.
117 pluck(searchable_options[:date_column], :id)
117 pluck(searchable_options[:date_column], :id)
118 end
118 end
119
119
120 def search_token_match_statement(column, value='?')
120 def search_token_match_statement(column, value='?')
121 case connection.adapter_name
121 case connection.adapter_name
122 when /postgresql/i
122 when /postgresql/i
123 "#{column} ILIKE #{value}"
123 "#{column} ILIKE #{value}"
124 when /mysql/i
125 "LOWER(#{column}) COLLATE utf8_bin LIKE LOWER(#{value})"
126 else
124 else
127 "#{column} LIKE #{value}"
125 "#{column} LIKE #{value}"
128 end
126 end
129 end
127 end
130 private :search_token_match_statement
128 private :search_token_match_statement
131
129
132 # Returns the search scope for user and projects
130 # Returns the search scope for user and projects
133 def search_scope(user, projects)
131 def search_scope(user, projects)
134 scope = (searchable_options[:scope] || self)
132 scope = (searchable_options[:scope] || self)
135 if scope.is_a? Proc
133 if scope.is_a? Proc
136 scope = scope.call
134 scope = scope.call
137 end
135 end
138
136
139 if respond_to?(:visible) && !searchable_options.has_key?(:permission)
137 if respond_to?(:visible) && !searchable_options.has_key?(:permission)
140 scope = scope.visible(user)
138 scope = scope.visible(user)
141 else
139 else
142 permission = searchable_options[:permission] || :view_project
140 permission = searchable_options[:permission] || :view_project
143 scope = scope.where(Project.allowed_to_condition(user, permission))
141 scope = scope.where(Project.allowed_to_condition(user, permission))
144 end
142 end
145
143
146 if projects
144 if projects
147 scope = scope.where("#{searchable_options[:project_key]} IN (?)", projects.map(&:id))
145 scope = scope.where("#{searchable_options[:project_key]} IN (?)", projects.map(&:id))
148 end
146 end
149 scope
147 scope
150 end
148 end
151 private :search_scope
149 private :search_scope
152
150
153 # Returns search results of given ids
151 # Returns search results of given ids
154 def search_results_from_ids(ids)
152 def search_results_from_ids(ids)
155 where(:id => ids).preload(searchable_options[:preload]).to_a
153 where(:id => ids).preload(searchable_options[:preload]).to_a
156 end
154 end
157
155
158 # Returns search results with same arguments as search_result_ranks_and_ids
156 # Returns search results with same arguments as search_result_ranks_and_ids
159 def search_results(*args)
157 def search_results(*args)
160 ranks_and_ids = search_result_ranks_and_ids(*args)
158 ranks_and_ids = search_result_ranks_and_ids(*args)
161 search_results_from_ids(ranks_and_ids.map(&:last))
159 search_results_from_ids(ranks_and_ids.map(&:last))
162 end
160 end
163 end
161 end
164 end
162 end
165 end
163 end
166 end
164 end
167 end
165 end
@@ -1,292 +1,296
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 if ENV["COVERAGE"]
18 if ENV["COVERAGE"]
19 require 'simplecov'
19 require 'simplecov'
20 require File.expand_path(File.dirname(__FILE__) + "/coverage/html_formatter")
20 require File.expand_path(File.dirname(__FILE__) + "/coverage/html_formatter")
21 SimpleCov.formatter = Redmine::Coverage::HtmlFormatter
21 SimpleCov.formatter = Redmine::Coverage::HtmlFormatter
22 SimpleCov.start 'rails'
22 SimpleCov.start 'rails'
23 end
23 end
24
24
25 ENV["RAILS_ENV"] = "test"
25 ENV["RAILS_ENV"] = "test"
26 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
26 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
27 require 'rails/test_help'
27 require 'rails/test_help'
28 require Rails.root.join('test', 'mocks', 'open_id_authentication_mock.rb').to_s
28 require Rails.root.join('test', 'mocks', 'open_id_authentication_mock.rb').to_s
29
29
30 require File.expand_path(File.dirname(__FILE__) + '/object_helpers')
30 require File.expand_path(File.dirname(__FILE__) + '/object_helpers')
31 include ObjectHelpers
31 include ObjectHelpers
32
32
33 require 'awesome_nested_set/version'
33 require 'awesome_nested_set/version'
34 require 'net/ldap'
34 require 'net/ldap'
35
35
36 class ActionView::TestCase
36 class ActionView::TestCase
37 helper :application
37 helper :application
38 include ApplicationHelper
38 include ApplicationHelper
39 end
39 end
40
40
41 class ActiveSupport::TestCase
41 class ActiveSupport::TestCase
42 include ActionDispatch::TestProcess
42 include ActionDispatch::TestProcess
43
43
44 self.use_transactional_fixtures = true
44 self.use_transactional_fixtures = true
45 self.use_instantiated_fixtures = false
45 self.use_instantiated_fixtures = false
46
46
47 #ESCAPED_CANT = 'can&#x27;t'
47 #ESCAPED_CANT = 'can&#x27;t'
48 #ESCAPED_UCANT = 'Can&#x27;t'
48 #ESCAPED_UCANT = 'Can&#x27;t'
49 # Rails 4.0.2
49 # Rails 4.0.2
50 ESCAPED_CANT = 'can&#39;t'
50 ESCAPED_CANT = 'can&#39;t'
51 ESCAPED_UCANT = 'Can&#39;t'
51 ESCAPED_UCANT = 'Can&#39;t'
52
52
53 def uploaded_test_file(name, mime)
53 def uploaded_test_file(name, mime)
54 fixture_file_upload("files/#{name}", mime, true)
54 fixture_file_upload("files/#{name}", mime, true)
55 end
55 end
56
56
57 # Mock out a file
57 # Mock out a file
58 def self.mock_file
58 def self.mock_file
59 file = 'a_file.png'
59 file = 'a_file.png'
60 file.stubs(:size).returns(32)
60 file.stubs(:size).returns(32)
61 file.stubs(:original_filename).returns('a_file.png')
61 file.stubs(:original_filename).returns('a_file.png')
62 file.stubs(:content_type).returns('image/png')
62 file.stubs(:content_type).returns('image/png')
63 file.stubs(:read).returns(false)
63 file.stubs(:read).returns(false)
64 file
64 file
65 end
65 end
66
66
67 def mock_file
67 def mock_file
68 self.class.mock_file
68 self.class.mock_file
69 end
69 end
70
70
71 def mock_file_with_options(options={})
71 def mock_file_with_options(options={})
72 file = ''
72 file = ''
73 file.stubs(:size).returns(32)
73 file.stubs(:size).returns(32)
74 original_filename = options[:original_filename] || nil
74 original_filename = options[:original_filename] || nil
75 file.stubs(:original_filename).returns(original_filename)
75 file.stubs(:original_filename).returns(original_filename)
76 content_type = options[:content_type] || nil
76 content_type = options[:content_type] || nil
77 file.stubs(:content_type).returns(content_type)
77 file.stubs(:content_type).returns(content_type)
78 file.stubs(:read).returns(false)
78 file.stubs(:read).returns(false)
79 file
79 file
80 end
80 end
81
81
82 # Use a temporary directory for attachment related tests
82 # Use a temporary directory for attachment related tests
83 def set_tmp_attachments_directory
83 def set_tmp_attachments_directory
84 Dir.mkdir "#{Rails.root}/tmp/test" unless File.directory?("#{Rails.root}/tmp/test")
84 Dir.mkdir "#{Rails.root}/tmp/test" unless File.directory?("#{Rails.root}/tmp/test")
85 unless File.directory?("#{Rails.root}/tmp/test/attachments")
85 unless File.directory?("#{Rails.root}/tmp/test/attachments")
86 Dir.mkdir "#{Rails.root}/tmp/test/attachments"
86 Dir.mkdir "#{Rails.root}/tmp/test/attachments"
87 end
87 end
88 Attachment.storage_path = "#{Rails.root}/tmp/test/attachments"
88 Attachment.storage_path = "#{Rails.root}/tmp/test/attachments"
89 end
89 end
90
90
91 def set_fixtures_attachments_directory
91 def set_fixtures_attachments_directory
92 Attachment.storage_path = "#{Rails.root}/test/fixtures/files"
92 Attachment.storage_path = "#{Rails.root}/test/fixtures/files"
93 end
93 end
94
94
95 def with_settings(options, &block)
95 def with_settings(options, &block)
96 saved_settings = options.keys.inject({}) do |h, k|
96 saved_settings = options.keys.inject({}) do |h, k|
97 h[k] = case Setting[k]
97 h[k] = case Setting[k]
98 when Symbol, false, true, nil
98 when Symbol, false, true, nil
99 Setting[k]
99 Setting[k]
100 else
100 else
101 Setting[k].dup
101 Setting[k].dup
102 end
102 end
103 h
103 h
104 end
104 end
105 options.each {|k, v| Setting[k] = v}
105 options.each {|k, v| Setting[k] = v}
106 yield
106 yield
107 ensure
107 ensure
108 saved_settings.each {|k, v| Setting[k] = v} if saved_settings
108 saved_settings.each {|k, v| Setting[k] = v} if saved_settings
109 end
109 end
110
110
111 # Yields the block with user as the current user
111 # Yields the block with user as the current user
112 def with_current_user(user, &block)
112 def with_current_user(user, &block)
113 saved_user = User.current
113 saved_user = User.current
114 User.current = user
114 User.current = user
115 yield
115 yield
116 ensure
116 ensure
117 User.current = saved_user
117 User.current = saved_user
118 end
118 end
119
119
120 def with_locale(locale, &block)
120 def with_locale(locale, &block)
121 saved_localed = ::I18n.locale
121 saved_localed = ::I18n.locale
122 ::I18n.locale = locale
122 ::I18n.locale = locale
123 yield
123 yield
124 ensure
124 ensure
125 ::I18n.locale = saved_localed
125 ::I18n.locale = saved_localed
126 end
126 end
127
127
128 def self.ldap_configured?
128 def self.ldap_configured?
129 @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389)
129 @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389)
130 return @test_ldap.bind
130 return @test_ldap.bind
131 rescue Exception => e
131 rescue Exception => e
132 # LDAP is not listening
132 # LDAP is not listening
133 return nil
133 return nil
134 end
134 end
135
135
136 def self.convert_installed?
136 def self.convert_installed?
137 Redmine::Thumbnail.convert_available?
137 Redmine::Thumbnail.convert_available?
138 end
138 end
139
139
140 # Returns the path to the test +vendor+ repository
140 # Returns the path to the test +vendor+ repository
141 def self.repository_path(vendor)
141 def self.repository_path(vendor)
142 path = Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s
142 path = Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s
143 # Unlike ruby, JRuby returns Rails.root with backslashes under Windows
143 # Unlike ruby, JRuby returns Rails.root with backslashes under Windows
144 path.tr("\\", "/")
144 path.tr("\\", "/")
145 end
145 end
146
146
147 # Returns the url of the subversion test repository
147 # Returns the url of the subversion test repository
148 def self.subversion_repository_url
148 def self.subversion_repository_url
149 path = repository_path('subversion')
149 path = repository_path('subversion')
150 path = '/' + path unless path.starts_with?('/')
150 path = '/' + path unless path.starts_with?('/')
151 "file://#{path}"
151 "file://#{path}"
152 end
152 end
153
153
154 # Returns true if the +vendor+ test repository is configured
154 # Returns true if the +vendor+ test repository is configured
155 def self.repository_configured?(vendor)
155 def self.repository_configured?(vendor)
156 File.directory?(repository_path(vendor))
156 File.directory?(repository_path(vendor))
157 end
157 end
158
158
159 def repository_path_hash(arr)
159 def repository_path_hash(arr)
160 hs = {}
160 hs = {}
161 hs[:path] = arr.join("/")
161 hs[:path] = arr.join("/")
162 hs[:param] = arr.join("/")
162 hs[:param] = arr.join("/")
163 hs
163 hs
164 end
164 end
165
165
166 def sqlite?
166 def sqlite?
167 ActiveRecord::Base.connection.adapter_name =~ /sqlite/i
167 ActiveRecord::Base.connection.adapter_name =~ /sqlite/i
168 end
168 end
169
169
170 def mysql?
171 ActiveRecord::Base.connection.adapter_name =~ /mysql/i
172 end
173
170 def assert_save(object)
174 def assert_save(object)
171 saved = object.save
175 saved = object.save
172 message = "#{object.class} could not be saved"
176 message = "#{object.class} could not be saved"
173 errors = object.errors.full_messages.map {|m| "- #{m}"}
177 errors = object.errors.full_messages.map {|m| "- #{m}"}
174 message << ":\n#{errors.join("\n")}" if errors.any?
178 message << ":\n#{errors.join("\n")}" if errors.any?
175 assert_equal true, saved, message
179 assert_equal true, saved, message
176 end
180 end
177
181
178 def assert_select_error(arg)
182 def assert_select_error(arg)
179 assert_select '#errorExplanation', :text => arg
183 assert_select '#errorExplanation', :text => arg
180 end
184 end
181
185
182 def assert_include(expected, s, message=nil)
186 def assert_include(expected, s, message=nil)
183 assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"")
187 assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"")
184 end
188 end
185
189
186 def assert_not_include(expected, s, message=nil)
190 def assert_not_include(expected, s, message=nil)
187 assert !s.include?(expected), (message || "\"#{expected}\" found in \"#{s}\"")
191 assert !s.include?(expected), (message || "\"#{expected}\" found in \"#{s}\"")
188 end
192 end
189
193
190 def assert_select_in(text, *args, &block)
194 def assert_select_in(text, *args, &block)
191 d = HTML::Document.new(CGI::unescapeHTML(String.new(text))).root
195 d = HTML::Document.new(CGI::unescapeHTML(String.new(text))).root
192 assert_select(d, *args, &block)
196 assert_select(d, *args, &block)
193 end
197 end
194
198
195 def assert_mail_body_match(expected, mail, message=nil)
199 def assert_mail_body_match(expected, mail, message=nil)
196 if expected.is_a?(String)
200 if expected.is_a?(String)
197 assert_include expected, mail_body(mail), message
201 assert_include expected, mail_body(mail), message
198 else
202 else
199 assert_match expected, mail_body(mail), message
203 assert_match expected, mail_body(mail), message
200 end
204 end
201 end
205 end
202
206
203 def assert_mail_body_no_match(expected, mail, message=nil)
207 def assert_mail_body_no_match(expected, mail, message=nil)
204 if expected.is_a?(String)
208 if expected.is_a?(String)
205 assert_not_include expected, mail_body(mail), message
209 assert_not_include expected, mail_body(mail), message
206 else
210 else
207 assert_no_match expected, mail_body(mail), message
211 assert_no_match expected, mail_body(mail), message
208 end
212 end
209 end
213 end
210
214
211 def mail_body(mail)
215 def mail_body(mail)
212 mail.parts.first.body.encoded
216 mail.parts.first.body.encoded
213 end
217 end
214
218
215 # awesome_nested_set new node lft and rgt value changed this refactor revision.
219 # awesome_nested_set new node lft and rgt value changed this refactor revision.
216 # https://github.com/collectiveidea/awesome_nested_set/commit/199fca9bb938e40200cd90714dc69247ef017c61
220 # https://github.com/collectiveidea/awesome_nested_set/commit/199fca9bb938e40200cd90714dc69247ef017c61
217 # The reason of behavior change is that "self.class.base_class.unscoped" was added to this line.
221 # The reason of behavior change is that "self.class.base_class.unscoped" was added to this line.
218 # https://github.com/collectiveidea/awesome_nested_set/commit/199fca9bb9#diff-f61b59a5e6319024e211b0ffdd0e4ef1R273
222 # https://github.com/collectiveidea/awesome_nested_set/commit/199fca9bb9#diff-f61b59a5e6319024e211b0ffdd0e4ef1R273
219 # It seems correct behavior because of this line comment.
223 # It seems correct behavior because of this line comment.
220 # https://github.com/collectiveidea/awesome_nested_set/blame/199fca9bb9/lib/awesome_nested_set/model.rb#L278
224 # https://github.com/collectiveidea/awesome_nested_set/blame/199fca9bb9/lib/awesome_nested_set/model.rb#L278
221 def new_issue_lft
225 def new_issue_lft
222 # ::AwesomeNestedSet::VERSION > "2.1.6" ? Issue.maximum(:rgt) + 1 : 1
226 # ::AwesomeNestedSet::VERSION > "2.1.6" ? Issue.maximum(:rgt) + 1 : 1
223 Issue.maximum(:rgt) + 1
227 Issue.maximum(:rgt) + 1
224 end
228 end
225 end
229 end
226
230
227 module Redmine
231 module Redmine
228 class RoutingTest < ActionDispatch::IntegrationTest
232 class RoutingTest < ActionDispatch::IntegrationTest
229 def should_route(arg)
233 def should_route(arg)
230 arg = arg.dup
234 arg = arg.dup
231 request = arg.keys.detect {|key| key.is_a?(String)}
235 request = arg.keys.detect {|key| key.is_a?(String)}
232 raise ArgumentError unless request
236 raise ArgumentError unless request
233 options = arg.slice!(request)
237 options = arg.slice!(request)
234
238
235 raise ArgumentError unless request =~ /\A(GET|POST|PUT|PATCH|DELETE)\s+(.+)\z/
239 raise ArgumentError unless request =~ /\A(GET|POST|PUT|PATCH|DELETE)\s+(.+)\z/
236 method, path = $1.downcase.to_sym, $2
240 method, path = $1.downcase.to_sym, $2
237
241
238 raise ArgumentError unless arg.values.first =~ /\A(.+)#(.+)\z/
242 raise ArgumentError unless arg.values.first =~ /\A(.+)#(.+)\z/
239 controller, action = $1, $2
243 controller, action = $1, $2
240
244
241 assert_routing(
245 assert_routing(
242 {:method => method, :path => path},
246 {:method => method, :path => path},
243 options.merge(:controller => controller, :action => action)
247 options.merge(:controller => controller, :action => action)
244 )
248 )
245 end
249 end
246 end
250 end
247
251
248 class IntegrationTest < ActionDispatch::IntegrationTest
252 class IntegrationTest < ActionDispatch::IntegrationTest
249 def log_user(login, password)
253 def log_user(login, password)
250 User.anonymous
254 User.anonymous
251 get "/login"
255 get "/login"
252 assert_equal nil, session[:user_id]
256 assert_equal nil, session[:user_id]
253 assert_response :success
257 assert_response :success
254 assert_template "account/login"
258 assert_template "account/login"
255 post "/login", :username => login, :password => password
259 post "/login", :username => login, :password => password
256 assert_equal login, User.find(session[:user_id]).login
260 assert_equal login, User.find(session[:user_id]).login
257 end
261 end
258
262
259 def credentials(user, password=nil)
263 def credentials(user, password=nil)
260 {'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)}
264 {'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)}
261 end
265 end
262 end
266 end
263
267
264 module ApiTest
268 module ApiTest
265 API_FORMATS = %w(json xml).freeze
269 API_FORMATS = %w(json xml).freeze
266
270
267 # Base class for API tests
271 # Base class for API tests
268 class Base < Redmine::IntegrationTest
272 class Base < Redmine::IntegrationTest
269 def setup
273 def setup
270 Setting.rest_api_enabled = '1'
274 Setting.rest_api_enabled = '1'
271 end
275 end
272
276
273 def teardown
277 def teardown
274 Setting.rest_api_enabled = '0'
278 Setting.rest_api_enabled = '0'
275 end
279 end
276 end
280 end
277
281
278 class Routing < Redmine::RoutingTest
282 class Routing < Redmine::RoutingTest
279 def should_route(arg)
283 def should_route(arg)
280 arg = arg.dup
284 arg = arg.dup
281 request = arg.keys.detect {|key| key.is_a?(String)}
285 request = arg.keys.detect {|key| key.is_a?(String)}
282 raise ArgumentError unless request
286 raise ArgumentError unless request
283 options = arg.slice!(request)
287 options = arg.slice!(request)
284
288
285 API_FORMATS.each do |format|
289 API_FORMATS.each do |format|
286 format_request = request.sub /$/, ".#{format}"
290 format_request = request.sub /$/, ".#{format}"
287 super options.merge(format_request => arg[request], :format => format)
291 super options.merge(format_request => arg[request], :format => format)
288 end
292 end
289 end
293 end
290 end
294 end
291 end
295 end
292 end
296 end
@@ -1,171 +1,180
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.expand_path('../../test_helper', __FILE__)
20 require File.expand_path('../../test_helper', __FILE__)
21
21
22 class SearchTest < ActiveSupport::TestCase
22 class SearchTest < ActiveSupport::TestCase
23 fixtures :users,
23 fixtures :users,
24 :members,
24 :members,
25 :member_roles,
25 :member_roles,
26 :projects,
26 :projects,
27 :projects_trackers,
27 :projects_trackers,
28 :roles,
28 :roles,
29 :enabled_modules,
29 :enabled_modules,
30 :issues,
30 :issues,
31 :trackers,
31 :trackers,
32 :issue_statuses,
32 :issue_statuses,
33 :enumerations,
33 :enumerations,
34 :journals,
34 :journals,
35 :journal_details,
35 :journal_details,
36 :repositories,
36 :repositories,
37 :changesets
37 :changesets
38
38
39 def setup
39 def setup
40 @project = Project.find(1)
40 @project = Project.find(1)
41 @issue_keyword = '%unable to print recipes%'
41 @issue_keyword = '%unable to print recipes%'
42 @issue = Issue.find(1)
42 @issue = Issue.find(1)
43 @changeset_keyword = '%very first commit%'
43 @changeset_keyword = '%very first commit%'
44 @changeset = Changeset.find(100)
44 @changeset = Changeset.find(100)
45 end
45 end
46
46
47 def test_search_by_anonymous
47 def test_search_by_anonymous
48 User.current = nil
48 User.current = nil
49
49
50 r = Issue.search_results(@issue_keyword)
50 r = Issue.search_results(@issue_keyword)
51 assert r.include?(@issue)
51 assert r.include?(@issue)
52 r = Changeset.search_results(@changeset_keyword)
52 r = Changeset.search_results(@changeset_keyword)
53 assert r.include?(@changeset)
53 assert r.include?(@changeset)
54
54
55 # Removes the :view_changesets permission from Anonymous role
55 # Removes the :view_changesets permission from Anonymous role
56 remove_permission Role.anonymous, :view_changesets
56 remove_permission Role.anonymous, :view_changesets
57 User.current = nil
57 User.current = nil
58
58
59 r = Issue.search_results(@issue_keyword)
59 r = Issue.search_results(@issue_keyword)
60 assert r.include?(@issue)
60 assert r.include?(@issue)
61 r = Changeset.search_results(@changeset_keyword)
61 r = Changeset.search_results(@changeset_keyword)
62 assert !r.include?(@changeset)
62 assert !r.include?(@changeset)
63
63
64 # Make the project private
64 # Make the project private
65 @project.update_attribute :is_public, false
65 @project.update_attribute :is_public, false
66 r = Issue.search_results(@issue_keyword)
66 r = Issue.search_results(@issue_keyword)
67 assert !r.include?(@issue)
67 assert !r.include?(@issue)
68 r = Changeset.search_results(@changeset_keyword)
68 r = Changeset.search_results(@changeset_keyword)
69 assert !r.include?(@changeset)
69 assert !r.include?(@changeset)
70 end
70 end
71
71
72 def test_search_by_user
72 def test_search_by_user
73 User.current = User.find_by_login('rhill')
73 User.current = User.find_by_login('rhill')
74 assert User.current.memberships.empty?
74 assert User.current.memberships.empty?
75
75
76 r = Issue.search_results(@issue_keyword)
76 r = Issue.search_results(@issue_keyword)
77 assert r.include?(@issue)
77 assert r.include?(@issue)
78 r = Changeset.search_results(@changeset_keyword)
78 r = Changeset.search_results(@changeset_keyword)
79 assert r.include?(@changeset)
79 assert r.include?(@changeset)
80
80
81 # Removes the :view_changesets permission from Non member role
81 # Removes the :view_changesets permission from Non member role
82 remove_permission Role.non_member, :view_changesets
82 remove_permission Role.non_member, :view_changesets
83 User.current = User.find_by_login('rhill')
83 User.current = User.find_by_login('rhill')
84
84
85 r = Issue.search_results(@issue_keyword)
85 r = Issue.search_results(@issue_keyword)
86 assert r.include?(@issue)
86 assert r.include?(@issue)
87 r = Changeset.search_results(@changeset_keyword)
87 r = Changeset.search_results(@changeset_keyword)
88 assert !r.include?(@changeset)
88 assert !r.include?(@changeset)
89
89
90 # Make the project private
90 # Make the project private
91 @project.update_attribute :is_public, false
91 @project.update_attribute :is_public, false
92 r = Issue.search_results(@issue_keyword)
92 r = Issue.search_results(@issue_keyword)
93 assert !r.include?(@issue)
93 assert !r.include?(@issue)
94 r = Changeset.search_results(@changeset_keyword)
94 r = Changeset.search_results(@changeset_keyword)
95 assert !r.include?(@changeset)
95 assert !r.include?(@changeset)
96 end
96 end
97
97
98 def test_search_by_allowed_member
98 def test_search_by_allowed_member
99 User.current = User.find_by_login('jsmith')
99 User.current = User.find_by_login('jsmith')
100 assert User.current.projects.include?(@project)
100 assert User.current.projects.include?(@project)
101
101
102 r = Issue.search_results(@issue_keyword)
102 r = Issue.search_results(@issue_keyword)
103 assert r.include?(@issue)
103 assert r.include?(@issue)
104 r = Changeset.search_results(@changeset_keyword)
104 r = Changeset.search_results(@changeset_keyword)
105 assert r.include?(@changeset)
105 assert r.include?(@changeset)
106
106
107 # Make the project private
107 # Make the project private
108 @project.update_attribute :is_public, false
108 @project.update_attribute :is_public, false
109 r = Issue.search_results(@issue_keyword)
109 r = Issue.search_results(@issue_keyword)
110 assert r.include?(@issue)
110 assert r.include?(@issue)
111 r = Changeset.search_results(@changeset_keyword)
111 r = Changeset.search_results(@changeset_keyword)
112 assert r.include?(@changeset)
112 assert r.include?(@changeset)
113 end
113 end
114
114
115 def test_search_by_unallowed_member
115 def test_search_by_unallowed_member
116 # Removes the :view_changesets permission from user's and non member role
116 # Removes the :view_changesets permission from user's and non member role
117 remove_permission Role.find(1), :view_changesets
117 remove_permission Role.find(1), :view_changesets
118 remove_permission Role.non_member, :view_changesets
118 remove_permission Role.non_member, :view_changesets
119
119
120 User.current = User.find_by_login('jsmith')
120 User.current = User.find_by_login('jsmith')
121 assert User.current.projects.include?(@project)
121 assert User.current.projects.include?(@project)
122
122
123 r = Issue.search_results(@issue_keyword)
123 r = Issue.search_results(@issue_keyword)
124 assert r.include?(@issue)
124 assert r.include?(@issue)
125 r = Changeset.search_results(@changeset_keyword)
125 r = Changeset.search_results(@changeset_keyword)
126 assert !r.include?(@changeset)
126 assert !r.include?(@changeset)
127
127
128 # Make the project private
128 # Make the project private
129 @project.update_attribute :is_public, false
129 @project.update_attribute :is_public, false
130 r = Issue.search_results(@issue_keyword)
130 r = Issue.search_results(@issue_keyword)
131 assert r.include?(@issue)
131 assert r.include?(@issue)
132 r = Changeset.search_results(@changeset_keyword)
132 r = Changeset.search_results(@changeset_keyword)
133 assert !r.include?(@changeset)
133 assert !r.include?(@changeset)
134 end
134 end
135
135
136 def test_search_issue_with_multiple_hits_in_journals
136 def test_search_issue_with_multiple_hits_in_journals
137 issue = Issue.find(1)
137 issue = Issue.find(1)
138 assert_equal 2, issue.journals.where("notes LIKE '%notes%'").count
138 assert_equal 2, issue.journals.where("notes LIKE '%notes%'").count
139
139
140 r = Issue.search_results('%notes%')
140 r = Issue.search_results('%notes%')
141 assert_equal 1, r.size
141 assert_equal 1, r.size
142 assert_equal issue, r.first
142 assert_equal issue, r.first
143 end
143 end
144
144
145 def test_search_should_be_case_insensitive
145 def test_search_should_be_case_insensitive
146 issue = Issue.generate!(:subject => "AzerTY")
146 issue = Issue.generate!(:subject => "AzerTY")
147
147
148 r = Issue.search_results('AZERty')
148 r = Issue.search_results('AZERty')
149 assert_include issue, r
149 assert_include issue, r
150 end
150 end
151
151
152 def test_search_should_not_use_ruby_downcase
152 def test_search_should_be_case_insensitive_with_accented_characters
153 skip "SQLite does not support case insensitive match for non-ASCII characters" if sqlite?
153 unless sqlite?
154 issue1 = Issue.generate!(:subject => "Special chars: ÖÖ")
154 issue1 = Issue.generate!(:subject => "Special chars: ÖÖ")
155 issue2 = Issue.generate!(:subject => "Special chars: Öö")
155 issue2 = Issue.generate!(:subject => "Special chars: Öö")
156 Issue.generate!(:subject => "Special chars: oo")
156
157 Issue.generate!(:subject => "Special chars: OO")
157 r = Issue.search_results('ÖÖ')
158
158 assert_include issue1, r
159 r = Issue.search_results('ÖÖ')
159 assert_include issue2, r
160 assert_include issue1, r
160 end
161 assert_include issue2, r
161 end
162 assert_equal 2, r.size
162
163 def test_search_should_be_case_and_accent_insensitive_with_mysql
164 if mysql?
165 issue1 = Issue.generate!(:subject => "OO")
166 issue2 = Issue.generate!(:subject => "oo")
167
168 r = Issue.search_results('ÖÖ')
169 assert_include issue1, r
170 assert_include issue2, r
171 end
163 end
172 end
164
173
165 private
174 private
166
175
167 def remove_permission(role, permission)
176 def remove_permission(role, permission)
168 role.permissions = role.permissions - [ permission ]
177 role.permissions = role.permissions - [ permission ]
169 role.save
178 role.save
170 end
179 end
171 end
180 end
General Comments 0
You need to be logged in to leave comments. Login now