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