##// END OF EJS Templates
Support for accent insensitive search with PostgreSQL (#18801)....
Jean-Philippe Lang -
r13607:af5872b7d432
parent child
Show More
@@ -0,0 +1,53
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 module Redmine
19 # Helper module to get information about the Redmine database
20 module Database
21 class << self
22 # Returns true if the database is PostgreSQL
23 def postgresql?
24 (ActiveRecord::Base.connection.adapter_name =~ /postgresql/i).present?
25 end
26
27 # Returns the PostgreSQL version or nil if another DBMS is used
28 def postgresql_version
29 postgresql? ? ActiveRecord::Base.connection.send(:postgresql_version) : nil
30 end
31
32 # Returns true if the database is a PostgreSQL >=9.0 database with the unaccent extension installed
33 def postgresql_unaccent?
34 if postgresql?
35 return @postgresql_unaccent unless @postgresql_unaccent.nil?
36 begin
37 sql = "SELECT name FROM pg_available_extensions WHERE installed_version IS NOT NULL and name = 'unaccent'"
38 @postgresql_unaccent = postgresql_version >= 90000 && ActiveRecord::Base.connection.select_value(sql).present?
39 rescue
40 false
41 end
42 else
43 false
44 end
45 end
46
47 # Resets database information
48 def reset
49 @postgresql_unaccent = nil
50 end
51 end
52 end
53 end
@@ -1,222 +1,225
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 additional associations on this model ?
52 52 searchable_options[:search_custom_fields] = reflect_on_association(:custom_values).present?
53 53 searchable_options[:search_attachments] = reflect_on_association(:attachments).present?
54 54 searchable_options[:search_journals] = reflect_on_association(:journals).present?
55 55
56 56 send :include, Redmine::Acts::Searchable::InstanceMethods
57 57 end
58 58 end
59 59
60 60 module InstanceMethods
61 61 def self.included(base)
62 62 base.extend ClassMethods
63 63 end
64 64
65 65 module ClassMethods
66 66 # Searches the model for the given tokens and user visibility.
67 67 # The projects argument can be either nil (will search all projects), a project or an array of projects.
68 68 # Returns an array that contains the rank and id of all results.
69 69 # In current implementation, the rank is the record timestamp converted as an integer.
70 70 #
71 71 # Valid options:
72 72 # * :titles_only - searches tokens in the first searchable column only
73 73 # * :all_words - searches results that match all token
74 74 # * :
75 75 # * :limit - maximum number of results to return
76 76 #
77 77 # Example:
78 78 # Issue.search_result_ranks_and_ids("foo")
79 79 # # => [[1419595329, 69], [1419595622, 123]]
80 80 def search_result_ranks_and_ids(tokens, user=User.current, projects=nil, options={})
81 81 tokens = [] << tokens unless tokens.is_a?(Array)
82 82 projects = [] << projects if projects.is_a?(Project)
83 83
84 84 columns = searchable_options[:columns]
85 85 columns = columns[0..0] if options[:titles_only]
86 86
87 87 r = []
88 88 queries = 0
89 89
90 90 unless options[:attachments] == 'only'
91 91 r = fetch_ranks_and_ids(
92 92 search_scope(user, projects, options).
93 93 where(search_tokens_condition(columns, tokens, options[:all_words])),
94 94 options[:limit]
95 95 )
96 96 queries += 1
97 97
98 98 if !options[:titles_only] && searchable_options[:search_custom_fields]
99 99 searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true).to_a
100 100
101 101 if searchable_custom_fields.any?
102 102 fields_by_visibility = searchable_custom_fields.group_by {|field|
103 103 field.visibility_by_project_condition(searchable_options[:project_key], user, "#{CustomValue.table_name}.custom_field_id")
104 104 }
105 105 clauses = []
106 106 fields_by_visibility.each do |visibility, fields|
107 107 clauses << "(#{CustomValue.table_name}.custom_field_id IN (#{fields.map(&:id).join(',')}) AND (#{visibility}))"
108 108 end
109 109 visibility = clauses.join(' OR ')
110 110
111 111 r |= fetch_ranks_and_ids(
112 112 search_scope(user, projects, options).
113 113 joins(:custom_values).
114 114 where(visibility).
115 115 where(search_tokens_condition(["#{CustomValue.table_name}.value"], tokens, options[:all_words])),
116 116 options[:limit]
117 117 )
118 118 queries += 1
119 119 end
120 120 end
121 121
122 122 if !options[:titles_only] && searchable_options[:search_journals]
123 123 r |= fetch_ranks_and_ids(
124 124 search_scope(user, projects, options).
125 125 joins(:journals).
126 126 where("#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(user, :view_private_notes)})", false).
127 127 where(search_tokens_condition(["#{Journal.table_name}.notes"], tokens, options[:all_words])),
128 128 options[:limit]
129 129 )
130 130 queries += 1
131 131 end
132 132 end
133 133
134 134 if searchable_options[:search_attachments] && (options[:titles_only] ? options[:attachments] == 'only' : options[:attachments] != '0')
135 135 r |= fetch_ranks_and_ids(
136 136 search_scope(user, projects, options).
137 137 joins(:attachments).
138 138 where(search_tokens_condition(["#{Attachment.table_name}.filename", "#{Attachment.table_name}.description"], tokens, options[:all_words])),
139 139 options[:limit]
140 140 )
141 141 queries += 1
142 142 end
143 143
144 144 if queries > 1
145 145 r = r.sort.reverse
146 146 if options[:limit] && r.size > options[:limit]
147 147 r = r[0, options[:limit]]
148 148 end
149 149 end
150 150
151 151 r
152 152 end
153 153
154 154 def search_tokens_condition(columns, tokens, all_words)
155 155 token_clauses = columns.map {|column| "(#{search_token_match_statement(column)})"}
156 156 sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(all_words ? ' AND ' : ' OR ')
157 157 [sql, * (tokens.collect {|w| "%#{w}%"} * token_clauses.size).sort]
158 158 end
159 159 private :search_tokens_condition
160 160
161 161 def search_token_match_statement(column, value='?')
162 case connection.adapter_name
163 when /postgresql/i
162 if Redmine::Database.postgresql?
163 if Redmine::Database.postgresql_unaccent?
164 "unaccent(#{column}) ILIKE unaccent(#{value})"
165 else
164 166 "#{column} ILIKE #{value}"
167 end
165 168 else
166 169 "#{column} LIKE #{value}"
167 170 end
168 171 end
169 172 private :search_token_match_statement
170 173
171 174 def fetch_ranks_and_ids(scope, limit)
172 175 scope.
173 176 reorder(searchable_options[:date_column] => :desc, :id => :desc).
174 177 limit(limit).
175 178 uniq.
176 179 pluck(searchable_options[:date_column], :id).
177 180 # converts timestamps to integers for faster sort
178 181 map {|timestamp, id| [timestamp.to_i, id]}
179 182 end
180 183 private :fetch_ranks_and_ids
181 184
182 185 # Returns the search scope for user and projects
183 186 def search_scope(user, projects, options={})
184 187 if projects.is_a?(Array) && projects.empty?
185 188 # no results
186 189 return none
187 190 end
188 191
189 192 scope = (searchable_options[:scope] || self)
190 193 if scope.is_a? Proc
191 194 scope = scope.call(options)
192 195 end
193 196
194 197 if respond_to?(:visible) && !searchable_options.has_key?(:permission)
195 198 scope = scope.visible(user)
196 199 else
197 200 permission = searchable_options[:permission] || :view_project
198 201 scope = scope.where(Project.allowed_to_condition(user, permission))
199 202 end
200 203
201 204 if projects
202 205 scope = scope.where("#{searchable_options[:project_key]} IN (?)", projects.map(&:id))
203 206 end
204 207 scope
205 208 end
206 209 private :search_scope
207 210
208 211 # Returns search results of given ids
209 212 def search_results_from_ids(ids)
210 213 where(:id => ids).preload(searchable_options[:preload]).to_a
211 214 end
212 215
213 216 # Returns search results with same arguments as search_result_ranks_and_ids
214 217 def search_results(*args)
215 218 ranks_and_ids = search_result_ranks_and_ids(*args)
216 219 search_results_from_ids(ranks_and_ids.map(&:last))
217 220 end
218 221 end
219 222 end
220 223 end
221 224 end
222 225 end
@@ -1,335 +1,339
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 'net/ldap'
34 34 require 'mocha/setup'
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 def uploaded_test_file(name, mime)
48 48 fixture_file_upload("files/#{name}", mime, true)
49 49 end
50 50
51 51 # Mock out a file
52 52 def self.mock_file
53 53 file = 'a_file.png'
54 54 file.stubs(:size).returns(32)
55 55 file.stubs(:original_filename).returns('a_file.png')
56 56 file.stubs(:content_type).returns('image/png')
57 57 file.stubs(:read).returns(false)
58 58 file
59 59 end
60 60
61 61 def mock_file
62 62 self.class.mock_file
63 63 end
64 64
65 65 def mock_file_with_options(options={})
66 66 file = ''
67 67 file.stubs(:size).returns(32)
68 68 original_filename = options[:original_filename] || nil
69 69 file.stubs(:original_filename).returns(original_filename)
70 70 content_type = options[:content_type] || nil
71 71 file.stubs(:content_type).returns(content_type)
72 72 file.stubs(:read).returns(false)
73 73 file
74 74 end
75 75
76 76 # Use a temporary directory for attachment related tests
77 77 def set_tmp_attachments_directory
78 78 Dir.mkdir "#{Rails.root}/tmp/test" unless File.directory?("#{Rails.root}/tmp/test")
79 79 unless File.directory?("#{Rails.root}/tmp/test/attachments")
80 80 Dir.mkdir "#{Rails.root}/tmp/test/attachments"
81 81 end
82 82 Attachment.storage_path = "#{Rails.root}/tmp/test/attachments"
83 83 end
84 84
85 85 def set_fixtures_attachments_directory
86 86 Attachment.storage_path = "#{Rails.root}/test/fixtures/files"
87 87 end
88 88
89 89 def with_settings(options, &block)
90 90 saved_settings = options.keys.inject({}) do |h, k|
91 91 h[k] = case Setting[k]
92 92 when Symbol, false, true, nil
93 93 Setting[k]
94 94 else
95 95 Setting[k].dup
96 96 end
97 97 h
98 98 end
99 99 options.each {|k, v| Setting[k] = v}
100 100 yield
101 101 ensure
102 102 saved_settings.each {|k, v| Setting[k] = v} if saved_settings
103 103 end
104 104
105 105 # Yields the block with user as the current user
106 106 def with_current_user(user, &block)
107 107 saved_user = User.current
108 108 User.current = user
109 109 yield
110 110 ensure
111 111 User.current = saved_user
112 112 end
113 113
114 114 def with_locale(locale, &block)
115 115 saved_localed = ::I18n.locale
116 116 ::I18n.locale = locale
117 117 yield
118 118 ensure
119 119 ::I18n.locale = saved_localed
120 120 end
121 121
122 122 def self.ldap_configured?
123 123 @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389)
124 124 return @test_ldap.bind
125 125 rescue Exception => e
126 126 # LDAP is not listening
127 127 return nil
128 128 end
129 129
130 130 def self.convert_installed?
131 131 Redmine::Thumbnail.convert_available?
132 132 end
133 133
134 134 def convert_installed?
135 135 self.class.convert_installed?
136 136 end
137 137
138 138 # Returns the path to the test +vendor+ repository
139 139 def self.repository_path(vendor)
140 140 path = Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s
141 141 # Unlike ruby, JRuby returns Rails.root with backslashes under Windows
142 142 path.tr("\\", "/")
143 143 end
144 144
145 145 # Returns the url of the subversion test repository
146 146 def self.subversion_repository_url
147 147 path = repository_path('subversion')
148 148 path = '/' + path unless path.starts_with?('/')
149 149 "file://#{path}"
150 150 end
151 151
152 152 # Returns true if the +vendor+ test repository is configured
153 153 def self.repository_configured?(vendor)
154 154 File.directory?(repository_path(vendor))
155 155 end
156 156
157 157 def repository_path_hash(arr)
158 158 hs = {}
159 159 hs[:path] = arr.join("/")
160 160 hs[:param] = arr.join("/")
161 161 hs
162 162 end
163 163
164 164 def sqlite?
165 165 ActiveRecord::Base.connection.adapter_name =~ /sqlite/i
166 166 end
167 167
168 168 def mysql?
169 169 ActiveRecord::Base.connection.adapter_name =~ /mysql/i
170 170 end
171 171
172 def postgresql?
173 ActiveRecord::Base.connection.adapter_name =~ /postgresql/i
174 end
175
172 176 def assert_save(object)
173 177 saved = object.save
174 178 message = "#{object.class} could not be saved"
175 179 errors = object.errors.full_messages.map {|m| "- #{m}"}
176 180 message << ":\n#{errors.join("\n")}" if errors.any?
177 181 assert_equal true, saved, message
178 182 end
179 183
180 184 def assert_select_error(arg)
181 185 assert_select '#errorExplanation', :text => arg
182 186 end
183 187
184 188 def assert_include(expected, s, message=nil)
185 189 assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"")
186 190 end
187 191
188 192 def assert_not_include(expected, s, message=nil)
189 193 assert !s.include?(expected), (message || "\"#{expected}\" found in \"#{s}\"")
190 194 end
191 195
192 196 def assert_select_in(text, *args, &block)
193 197 d = Nokogiri::HTML(CGI::unescapeHTML(String.new(text))).root
194 198 assert_select(d, *args, &block)
195 199 end
196 200
197 201 def assert_select_email(*args, &block)
198 202 email = ActionMailer::Base.deliveries.last
199 203 assert_not_nil email
200 204 html_body = email.parts.detect {|part| part.content_type.include?('text/html')}.try(&:body)
201 205 assert_not_nil html_body
202 206 assert_select_in html_body.encoded, *args, &block
203 207 end
204 208
205 209 def assert_mail_body_match(expected, mail, message=nil)
206 210 if expected.is_a?(String)
207 211 assert_include expected, mail_body(mail), message
208 212 else
209 213 assert_match expected, mail_body(mail), message
210 214 end
211 215 end
212 216
213 217 def assert_mail_body_no_match(expected, mail, message=nil)
214 218 if expected.is_a?(String)
215 219 assert_not_include expected, mail_body(mail), message
216 220 else
217 221 assert_no_match expected, mail_body(mail), message
218 222 end
219 223 end
220 224
221 225 def mail_body(mail)
222 226 mail.parts.first.body.encoded
223 227 end
224 228
225 229 # Returns the lft value for a new root issue
226 230 def new_issue_lft
227 231 1
228 232 end
229 233 end
230 234
231 235 module Redmine
232 236 class RoutingTest < ActionDispatch::IntegrationTest
233 237 def should_route(arg)
234 238 arg = arg.dup
235 239 request = arg.keys.detect {|key| key.is_a?(String)}
236 240 raise ArgumentError unless request
237 241 options = arg.slice!(request)
238 242
239 243 raise ArgumentError unless request =~ /\A(GET|POST|PUT|PATCH|DELETE)\s+(.+)\z/
240 244 method, path = $1.downcase.to_sym, $2
241 245
242 246 raise ArgumentError unless arg.values.first =~ /\A(.+)#(.+)\z/
243 247 controller, action = $1, $2
244 248
245 249 assert_routing(
246 250 {:method => method, :path => path},
247 251 options.merge(:controller => controller, :action => action)
248 252 )
249 253 end
250 254 end
251 255
252 256 class IntegrationTest < ActionDispatch::IntegrationTest
253 257 def log_user(login, password)
254 258 User.anonymous
255 259 get "/login"
256 260 assert_equal nil, session[:user_id]
257 261 assert_response :success
258 262 assert_template "account/login"
259 263 post "/login", :username => login, :password => password
260 264 assert_equal login, User.find(session[:user_id]).login
261 265 end
262 266
263 267 def credentials(user, password=nil)
264 268 {'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)}
265 269 end
266 270 end
267 271
268 272 module ApiTest
269 273 API_FORMATS = %w(json xml).freeze
270 274
271 275 # Base class for API tests
272 276 class Base < Redmine::IntegrationTest
273 277 def setup
274 278 Setting.rest_api_enabled = '1'
275 279 end
276 280
277 281 def teardown
278 282 Setting.rest_api_enabled = '0'
279 283 end
280 284
281 285 # Uploads content using the XML API and returns the attachment token
282 286 def xml_upload(content, credentials)
283 287 upload('xml', content, credentials)
284 288 end
285 289
286 290 # Uploads content using the JSON API and returns the attachment token
287 291 def json_upload(content, credentials)
288 292 upload('json', content, credentials)
289 293 end
290 294
291 295 def upload(format, content, credentials)
292 296 set_tmp_attachments_directory
293 297 assert_difference 'Attachment.count' do
294 298 post "/uploads.#{format}", content, {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials)
295 299 assert_response :created
296 300 end
297 301 data = response_data
298 302 assert_kind_of Hash, data['upload']
299 303 token = data['upload']['token']
300 304 assert_not_nil token
301 305 token
302 306 end
303 307
304 308 # Parses the response body based on its content type
305 309 def response_data
306 310 unless response.content_type.to_s =~ /^application\/(.+)/
307 311 raise "Unexpected response type: #{response.content_type}"
308 312 end
309 313 format = $1
310 314 case format
311 315 when 'xml'
312 316 Hash.from_xml(response.body)
313 317 when 'json'
314 318 ActiveSupport::JSON.decode(response.body)
315 319 else
316 320 raise "Unknown response format: #{format}"
317 321 end
318 322 end
319 323 end
320 324
321 325 class Routing < Redmine::RoutingTest
322 326 def should_route(arg)
323 327 arg = arg.dup
324 328 request = arg.keys.detect {|key| key.is_a?(String)}
325 329 raise ArgumentError unless request
326 330 options = arg.slice!(request)
327 331
328 332 API_FORMATS.each do |format|
329 333 format_request = request.sub /$/, ".#{format}"
330 334 super options.merge(format_request => arg[request], :format => format)
331 335 end
332 336 end
333 337 end
334 338 end
335 339 end
@@ -1,180 +1,199
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2015 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 152 def test_search_should_be_case_insensitive_with_accented_characters
153 153 unless sqlite?
154 154 issue1 = Issue.generate!(:subject => "Special chars: ÖÖ")
155 155 issue2 = Issue.generate!(:subject => "Special chars: Öö")
156 156
157 157 r = Issue.search_results('ÖÖ')
158 158 assert_include issue1, r
159 159 assert_include issue2, r
160 160 end
161 161 end
162 162
163 163 def test_search_should_be_case_and_accent_insensitive_with_mysql
164 164 if mysql?
165 165 issue1 = Issue.generate!(:subject => "OO")
166 166 issue2 = Issue.generate!(:subject => "oo")
167 167
168 168 r = Issue.search_results('ÖÖ')
169 169 assert_include issue1, r
170 170 assert_include issue2, r
171 171 end
172 172 end
173 173
174 def test_search_should_be_case_and_accent_insensitive_with_postgresql_and_noaccent_extension
175 if postgresql?
176 skip unless Redmine::Database.postgresql_version >= 90000
177 # Extension will be rollbacked with the test transaction
178 ActiveRecord::Base.connection.execute("CREATE EXTENSION IF NOT EXISTS unaccent")
179 Redmine::Database.reset
180 assert Redmine::Database.postgresql_unaccent?
181
182 issue1 = Issue.generate!(:subject => "OO")
183 issue2 = Issue.generate!(:subject => "oo")
184
185 r = Issue.search_results('ÖÖ')
186 assert_include issue1, r
187 assert_include issue2, r
188 end
189 ensure
190 Redmine::Database.reset
191 end
192
174 193 private
175 194
176 195 def remove_permission(role, permission)
177 196 role.permissions = role.permissions - [ permission ]
178 197 role.save
179 198 end
180 199 end
General Comments 0
You need to be logged in to leave comments. Login now