##// END OF EJS Templates
Makes user autocompleters work with firstname and lastname....
Jean-Philippe Lang -
r8755:008557581d8a
parent child
Show More
@@ -1,74 +1,84
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Principal < ActiveRecord::Base
19 19 set_table_name "#{table_name_prefix}users#{table_name_suffix}"
20 20
21 21 has_many :members, :foreign_key => 'user_id', :dependent => :destroy
22 22 has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
23 23 has_many :projects, :through => :memberships
24 24 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
25 25
26 26 # Groups and active users
27 27 named_scope :active, :conditions => "#{Principal.table_name}.status = 1"
28 28
29 29 named_scope :like, lambda {|q|
30 s = "%#{q.to_s.strip.downcase}%"
31 {:conditions => ["LOWER(login) LIKE :s OR LOWER(firstname) LIKE :s OR LOWER(lastname) LIKE :s OR LOWER(mail) LIKE :s", {:s => s}],
32 :order => 'type, login, lastname, firstname, mail'
33 }
30 if q.blank?
31 {}
32 else
33 q = q.to_s.downcase
34 pattern = "%#{q}%"
35 sql = "LOWER(login) LIKE :p OR LOWER(firstname) LIKE :p OR LOWER(lastname) LIKE :p OR LOWER(mail) LIKE :p"
36 params = {:p => pattern}
37 if q =~ /^(.+)\s+(.+)$/
38 a, b = "#{$1}%", "#{$2}%"
39 sql << " OR (LOWER(firstname) LIKE :a AND LOWER(lastname) LIKE :b) OR (LOWER(firstname) LIKE :b AND LOWER(lastname) LIKE :a)"
40 params.merge!(:a => a, :b => b)
41 end
42 {:conditions => [sql, params]}
43 end
34 44 }
35 45
36 46 # Principals that are members of a collection of projects
37 47 named_scope :member_of, lambda {|projects|
38 48 if projects.empty?
39 49 {:conditions => "1=0"}
40 50 else
41 51 ids = projects.map(&:id)
42 52 {:conditions => ["#{Principal.table_name}.status = 1 AND #{Principal.table_name}.id IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids]}
43 53 end
44 54 }
45 55
46 56 before_create :set_default_empty_values
47 57
48 58 def name(formatter = nil)
49 59 to_s
50 60 end
51 61
52 62 def <=>(principal)
53 63 if principal.nil?
54 64 -1
55 65 elsif self.class.name == principal.class.name
56 66 self.to_s.downcase <=> principal.to_s.downcase
57 67 else
58 68 # groups after users
59 69 principal.class.name <=> self.class.name
60 70 end
61 71 end
62 72
63 73 protected
64 74
65 75 # Make sure we don't try to insert NULL values (see #4632)
66 76 def set_default_empty_values
67 77 self.login ||= ''
68 78 self.hashed_password ||= ''
69 79 self.firstname ||= ''
70 80 self.lastname ||= ''
71 81 self.mail ||= ''
72 82 true
73 83 end
74 84 end
@@ -1,650 +1,642
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "digest/sha1"
19 19
20 20 class User < Principal
21 21 include Redmine::SafeAttributes
22 22
23 23 # Account statuses
24 24 STATUS_ANONYMOUS = 0
25 25 STATUS_ACTIVE = 1
26 26 STATUS_REGISTERED = 2
27 27 STATUS_LOCKED = 3
28 28
29 29 # Different ways of displaying/sorting users
30 30 USER_FORMATS = {
31 31 :firstname_lastname => {:string => '#{firstname} #{lastname}', :order => %w(firstname lastname id)},
32 32 :firstname => {:string => '#{firstname}', :order => %w(firstname id)},
33 33 :lastname_firstname => {:string => '#{lastname} #{firstname}', :order => %w(lastname firstname id)},
34 34 :lastname_coma_firstname => {:string => '#{lastname}, #{firstname}', :order => %w(lastname firstname id)},
35 35 :username => {:string => '#{login}', :order => %w(login id)},
36 36 }
37 37
38 38 MAIL_NOTIFICATION_OPTIONS = [
39 39 ['all', :label_user_mail_option_all],
40 40 ['selected', :label_user_mail_option_selected],
41 41 ['only_my_events', :label_user_mail_option_only_my_events],
42 42 ['only_assigned', :label_user_mail_option_only_assigned],
43 43 ['only_owner', :label_user_mail_option_only_owner],
44 44 ['none', :label_user_mail_option_none]
45 45 ]
46 46
47 47 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
48 48 :after_remove => Proc.new {|user, group| group.user_removed(user)}
49 49 has_many :changesets, :dependent => :nullify
50 50 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
51 51 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
52 52 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
53 53 belongs_to :auth_source
54 54
55 55 # Active non-anonymous users scope
56 56 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
57 57 named_scope :logged, :conditions => "#{User.table_name}.status <> #{STATUS_ANONYMOUS}"
58 58 named_scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
59 named_scope :like, lambda {|arg|
60 if arg.blank?
61 {}
62 else
63 pattern = "%#{arg.to_s.strip.downcase}%"
64 {:conditions => ["LOWER(login) LIKE :p OR LOWER(firstname) LIKE :p OR LOWER(lastname) LIKE :p OR LOWER(mail) LIKE :p", {:p => pattern}]}
65 end
66 }
67 59
68 60 acts_as_customizable
69 61
70 62 attr_accessor :password, :password_confirmation
71 63 attr_accessor :last_before_login_on
72 64 # Prevents unauthorized assignments
73 65 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
74 66
75 67 LOGIN_LENGTH_LIMIT = 60
76 68 MAIL_LENGTH_LIMIT = 60
77 69
78 70 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
79 71 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
80 72 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
81 73 # Login must contain lettres, numbers, underscores only
82 74 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
83 75 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
84 76 validates_length_of :firstname, :lastname, :maximum => 30
85 77 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_blank => true
86 78 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
87 79 validates_confirmation_of :password, :allow_nil => true
88 80 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
89 81 validate :validate_password_length
90 82
91 83 before_create :set_mail_notification
92 84 before_save :update_hashed_password
93 85 before_destroy :remove_references_before_destroy
94 86
95 87 named_scope :in_group, lambda {|group|
96 88 group_id = group.is_a?(Group) ? group.id : group.to_i
97 89 { :conditions => ["#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
98 90 }
99 91 named_scope :not_in_group, lambda {|group|
100 92 group_id = group.is_a?(Group) ? group.id : group.to_i
101 93 { :conditions => ["#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
102 94 }
103 95
104 96 def set_mail_notification
105 97 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
106 98 true
107 99 end
108 100
109 101 def update_hashed_password
110 102 # update hashed_password if password was set
111 103 if self.password && self.auth_source_id.blank?
112 104 salt_password(password)
113 105 end
114 106 end
115 107
116 108 def reload(*args)
117 109 @name = nil
118 110 @projects_by_role = nil
119 111 super
120 112 end
121 113
122 114 def mail=(arg)
123 115 write_attribute(:mail, arg.to_s.strip)
124 116 end
125 117
126 118 def identity_url=(url)
127 119 if url.blank?
128 120 write_attribute(:identity_url, '')
129 121 else
130 122 begin
131 123 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
132 124 rescue OpenIdAuthentication::InvalidOpenId
133 125 # Invlaid url, don't save
134 126 end
135 127 end
136 128 self.read_attribute(:identity_url)
137 129 end
138 130
139 131 # Returns the user that matches provided login and password, or nil
140 132 def self.try_to_login(login, password)
141 133 # Make sure no one can sign in with an empty password
142 134 return nil if password.to_s.empty?
143 135 user = find_by_login(login)
144 136 if user
145 137 # user is already in local database
146 138 return nil if !user.active?
147 139 if user.auth_source
148 140 # user has an external authentication method
149 141 return nil unless user.auth_source.authenticate(login, password)
150 142 else
151 143 # authentication with local password
152 144 return nil unless user.check_password?(password)
153 145 end
154 146 else
155 147 # user is not yet registered, try to authenticate with available sources
156 148 attrs = AuthSource.authenticate(login, password)
157 149 if attrs
158 150 user = new(attrs)
159 151 user.login = login
160 152 user.language = Setting.default_language
161 153 if user.save
162 154 user.reload
163 155 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
164 156 end
165 157 end
166 158 end
167 159 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
168 160 user
169 161 rescue => text
170 162 raise text
171 163 end
172 164
173 165 # Returns the user who matches the given autologin +key+ or nil
174 166 def self.try_to_autologin(key)
175 167 tokens = Token.find_all_by_action_and_value('autologin', key)
176 168 # Make sure there's only 1 token that matches the key
177 169 if tokens.size == 1
178 170 token = tokens.first
179 171 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
180 172 token.user.update_attribute(:last_login_on, Time.now)
181 173 token.user
182 174 end
183 175 end
184 176 end
185 177
186 178 def self.name_formatter(formatter = nil)
187 179 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
188 180 end
189 181
190 182 # Returns an array of fields names than can be used to make an order statement for users
191 183 # according to how user names are displayed
192 184 # Examples:
193 185 #
194 186 # User.fields_for_order_statement => ['users.login', 'users.id']
195 187 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
196 188 def self.fields_for_order_statement(table=nil)
197 189 table ||= table_name
198 190 name_formatter[:order].map {|field| "#{table}.#{field}"}
199 191 end
200 192
201 193 # Return user's full name for display
202 194 def name(formatter = nil)
203 195 f = self.class.name_formatter(formatter)
204 196 if formatter
205 197 eval('"' + f[:string] + '"')
206 198 else
207 199 @name ||= eval('"' + f[:string] + '"')
208 200 end
209 201 end
210 202
211 203 def active?
212 204 self.status == STATUS_ACTIVE
213 205 end
214 206
215 207 def registered?
216 208 self.status == STATUS_REGISTERED
217 209 end
218 210
219 211 def locked?
220 212 self.status == STATUS_LOCKED
221 213 end
222 214
223 215 def activate
224 216 self.status = STATUS_ACTIVE
225 217 end
226 218
227 219 def register
228 220 self.status = STATUS_REGISTERED
229 221 end
230 222
231 223 def lock
232 224 self.status = STATUS_LOCKED
233 225 end
234 226
235 227 def activate!
236 228 update_attribute(:status, STATUS_ACTIVE)
237 229 end
238 230
239 231 def register!
240 232 update_attribute(:status, STATUS_REGISTERED)
241 233 end
242 234
243 235 def lock!
244 236 update_attribute(:status, STATUS_LOCKED)
245 237 end
246 238
247 239 # Returns true if +clear_password+ is the correct user's password, otherwise false
248 240 def check_password?(clear_password)
249 241 if auth_source_id.present?
250 242 auth_source.authenticate(self.login, clear_password)
251 243 else
252 244 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
253 245 end
254 246 end
255 247
256 248 # Generates a random salt and computes hashed_password for +clear_password+
257 249 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
258 250 def salt_password(clear_password)
259 251 self.salt = User.generate_salt
260 252 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
261 253 end
262 254
263 255 # Does the backend storage allow this user to change their password?
264 256 def change_password_allowed?
265 257 return true if auth_source.nil?
266 258 return auth_source.allow_password_changes?
267 259 end
268 260
269 261 # Generate and set a random password. Useful for automated user creation
270 262 # Based on Token#generate_token_value
271 263 #
272 264 def random_password
273 265 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
274 266 password = ''
275 267 40.times { |i| password << chars[rand(chars.size-1)] }
276 268 self.password = password
277 269 self.password_confirmation = password
278 270 self
279 271 end
280 272
281 273 def pref
282 274 self.preference ||= UserPreference.new(:user => self)
283 275 end
284 276
285 277 def time_zone
286 278 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
287 279 end
288 280
289 281 def wants_comments_in_reverse_order?
290 282 self.pref[:comments_sorting] == 'desc'
291 283 end
292 284
293 285 # Return user's RSS key (a 40 chars long string), used to access feeds
294 286 def rss_key
295 287 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
296 288 token.value
297 289 end
298 290
299 291 # Return user's API key (a 40 chars long string), used to access the API
300 292 def api_key
301 293 token = self.api_token || self.create_api_token(:action => 'api')
302 294 token.value
303 295 end
304 296
305 297 # Return an array of project ids for which the user has explicitly turned mail notifications on
306 298 def notified_projects_ids
307 299 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
308 300 end
309 301
310 302 def notified_project_ids=(ids)
311 303 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
312 304 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
313 305 @notified_projects_ids = nil
314 306 notified_projects_ids
315 307 end
316 308
317 309 def valid_notification_options
318 310 self.class.valid_notification_options(self)
319 311 end
320 312
321 313 # Only users that belong to more than 1 project can select projects for which they are notified
322 314 def self.valid_notification_options(user=nil)
323 315 # Note that @user.membership.size would fail since AR ignores
324 316 # :include association option when doing a count
325 317 if user.nil? || user.memberships.length < 1
326 318 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
327 319 else
328 320 MAIL_NOTIFICATION_OPTIONS
329 321 end
330 322 end
331 323
332 324 # Find a user account by matching the exact login and then a case-insensitive
333 325 # version. Exact matches will be given priority.
334 326 def self.find_by_login(login)
335 327 # force string comparison to be case sensitive on MySQL
336 328 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
337 329
338 330 # First look for an exact match
339 331 user = first(:conditions => ["#{type_cast} login = ?", login])
340 332 # Fail over to case-insensitive if none was found
341 333 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
342 334 end
343 335
344 336 def self.find_by_rss_key(key)
345 337 token = Token.find_by_value(key)
346 338 token && token.user.active? ? token.user : nil
347 339 end
348 340
349 341 def self.find_by_api_key(key)
350 342 token = Token.find_by_action_and_value('api', key)
351 343 token && token.user.active? ? token.user : nil
352 344 end
353 345
354 346 # Makes find_by_mail case-insensitive
355 347 def self.find_by_mail(mail)
356 348 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
357 349 end
358 350
359 351 def to_s
360 352 name
361 353 end
362 354
363 355 # Returns the current day according to user's time zone
364 356 def today
365 357 if time_zone.nil?
366 358 Date.today
367 359 else
368 360 Time.now.in_time_zone(time_zone).to_date
369 361 end
370 362 end
371 363
372 364 def logged?
373 365 true
374 366 end
375 367
376 368 def anonymous?
377 369 !logged?
378 370 end
379 371
380 372 # Return user's roles for project
381 373 def roles_for_project(project)
382 374 roles = []
383 375 # No role on archived projects
384 376 return roles unless project && project.active?
385 377 if logged?
386 378 # Find project membership
387 379 membership = memberships.detect {|m| m.project_id == project.id}
388 380 if membership
389 381 roles = membership.roles
390 382 else
391 383 @role_non_member ||= Role.non_member
392 384 roles << @role_non_member
393 385 end
394 386 else
395 387 @role_anonymous ||= Role.anonymous
396 388 roles << @role_anonymous
397 389 end
398 390 roles
399 391 end
400 392
401 393 # Return true if the user is a member of project
402 394 def member_of?(project)
403 395 !roles_for_project(project).detect {|role| role.member?}.nil?
404 396 end
405 397
406 398 # Returns a hash of user's projects grouped by roles
407 399 def projects_by_role
408 400 return @projects_by_role if @projects_by_role
409 401
410 402 @projects_by_role = Hash.new {|h,k| h[k]=[]}
411 403 memberships.each do |membership|
412 404 membership.roles.each do |role|
413 405 @projects_by_role[role] << membership.project if membership.project
414 406 end
415 407 end
416 408 @projects_by_role.each do |role, projects|
417 409 projects.uniq!
418 410 end
419 411
420 412 @projects_by_role
421 413 end
422 414
423 415 # Returns true if user is arg or belongs to arg
424 416 def is_or_belongs_to?(arg)
425 417 if arg.is_a?(User)
426 418 self == arg
427 419 elsif arg.is_a?(Group)
428 420 arg.users.include?(self)
429 421 else
430 422 false
431 423 end
432 424 end
433 425
434 426 # Return true if the user is allowed to do the specified action on a specific context
435 427 # Action can be:
436 428 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
437 429 # * a permission Symbol (eg. :edit_project)
438 430 # Context can be:
439 431 # * a project : returns true if user is allowed to do the specified action on this project
440 432 # * an array of projects : returns true if user is allowed on every project
441 433 # * nil with options[:global] set : check if user has at least one role allowed for this action,
442 434 # or falls back to Non Member / Anonymous permissions depending if the user is logged
443 435 def allowed_to?(action, context, options={}, &block)
444 436 if context && context.is_a?(Project)
445 437 # No action allowed on archived projects
446 438 return false unless context.active?
447 439 # No action allowed on disabled modules
448 440 return false unless context.allows_to?(action)
449 441 # Admin users are authorized for anything else
450 442 return true if admin?
451 443
452 444 roles = roles_for_project(context)
453 445 return false unless roles
454 446 roles.detect {|role|
455 447 (context.is_public? || role.member?) &&
456 448 role.allowed_to?(action) &&
457 449 (block_given? ? yield(role, self) : true)
458 450 }
459 451 elsif context && context.is_a?(Array)
460 452 # Authorize if user is authorized on every element of the array
461 453 context.map do |project|
462 454 allowed_to?(action, project, options, &block)
463 455 end.inject do |memo,allowed|
464 456 memo && allowed
465 457 end
466 458 elsif options[:global]
467 459 # Admin users are always authorized
468 460 return true if admin?
469 461
470 462 # authorize if user has at least one role that has this permission
471 463 roles = memberships.collect {|m| m.roles}.flatten.uniq
472 464 roles << (self.logged? ? Role.non_member : Role.anonymous)
473 465 roles.detect {|role|
474 466 role.allowed_to?(action) &&
475 467 (block_given? ? yield(role, self) : true)
476 468 }
477 469 else
478 470 false
479 471 end
480 472 end
481 473
482 474 # Is the user allowed to do the specified action on any project?
483 475 # See allowed_to? for the actions and valid options.
484 476 def allowed_to_globally?(action, options, &block)
485 477 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
486 478 end
487 479
488 480 safe_attributes 'login',
489 481 'firstname',
490 482 'lastname',
491 483 'mail',
492 484 'mail_notification',
493 485 'language',
494 486 'custom_field_values',
495 487 'custom_fields',
496 488 'identity_url'
497 489
498 490 safe_attributes 'status',
499 491 'auth_source_id',
500 492 :if => lambda {|user, current_user| current_user.admin?}
501 493
502 494 safe_attributes 'group_ids',
503 495 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
504 496
505 497 # Utility method to help check if a user should be notified about an
506 498 # event.
507 499 #
508 500 # TODO: only supports Issue events currently
509 501 def notify_about?(object)
510 502 case mail_notification
511 503 when 'all'
512 504 true
513 505 when 'selected'
514 506 # user receives notifications for created/assigned issues on unselected projects
515 507 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
516 508 true
517 509 else
518 510 false
519 511 end
520 512 when 'none'
521 513 false
522 514 when 'only_my_events'
523 515 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
524 516 true
525 517 else
526 518 false
527 519 end
528 520 when 'only_assigned'
529 521 if object.is_a?(Issue) && (is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
530 522 true
531 523 else
532 524 false
533 525 end
534 526 when 'only_owner'
535 527 if object.is_a?(Issue) && object.author == self
536 528 true
537 529 else
538 530 false
539 531 end
540 532 else
541 533 false
542 534 end
543 535 end
544 536
545 537 def self.current=(user)
546 538 @current_user = user
547 539 end
548 540
549 541 def self.current
550 542 @current_user ||= User.anonymous
551 543 end
552 544
553 545 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
554 546 # one anonymous user per database.
555 547 def self.anonymous
556 548 anonymous_user = AnonymousUser.find(:first)
557 549 if anonymous_user.nil?
558 550 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
559 551 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
560 552 end
561 553 anonymous_user
562 554 end
563 555
564 556 # Salts all existing unsalted passwords
565 557 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
566 558 # This method is used in the SaltPasswords migration and is to be kept as is
567 559 def self.salt_unsalted_passwords!
568 560 transaction do
569 561 User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
570 562 next if user.hashed_password.blank?
571 563 salt = User.generate_salt
572 564 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
573 565 User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
574 566 end
575 567 end
576 568 end
577 569
578 570 protected
579 571
580 572 def validate_password_length
581 573 # Password length validation based on setting
582 574 if !password.nil? && password.size < Setting.password_min_length.to_i
583 575 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
584 576 end
585 577 end
586 578
587 579 private
588 580
589 581 # Removes references that are not handled by associations
590 582 # Things that are not deleted are reassociated with the anonymous user
591 583 def remove_references_before_destroy
592 584 return if self.id.nil?
593 585
594 586 substitute = User.anonymous
595 587 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
596 588 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
597 589 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
598 590 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
599 591 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
600 592 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
601 593 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
602 594 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
603 595 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
604 596 # Remove private queries and keep public ones
605 597 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
606 598 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
607 599 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
608 600 Token.delete_all ['user_id = ?', id]
609 601 Watcher.delete_all ['user_id = ?', id]
610 602 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
611 603 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
612 604 end
613 605
614 606 # Return password digest
615 607 def self.hash_password(clear_password)
616 608 Digest::SHA1.hexdigest(clear_password || "")
617 609 end
618 610
619 611 # Returns a 128bits random salt as a hex string (32 chars long)
620 612 def self.generate_salt
621 613 ActiveSupport::SecureRandom.hex(16)
622 614 end
623 615
624 616 end
625 617
626 618 class AnonymousUser < User
627 619 validate :validate_anonymous_uniqueness, :on => :create
628 620
629 621 def validate_anonymous_uniqueness
630 622 # There should be only one AnonymousUser in the database
631 623 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.find(:first)
632 624 end
633 625
634 626 def available_custom_fields
635 627 []
636 628 end
637 629
638 630 # Overrides a few properties
639 631 def logged?; false end
640 632 def admin; false end
641 633 def name(*args); I18n.t(:label_user_anonymous) end
642 634 def mail; nil end
643 635 def time_zone; nil end
644 636 def rss_key; nil end
645 637
646 638 # Anonymous user can not be destroyed
647 639 def destroy
648 640 false
649 641 end
650 642 end
@@ -1,79 +1,94
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class PrincipalTest < ActiveSupport::TestCase
21 21
22 22 def test_active_scope_should_return_groups_and_active_users
23 23 result = Principal.active.all
24 24 assert_include Group.first, result
25 25 assert_not_nil result.detect {|p| p.is_a?(User)}
26 26 assert_nil result.detect {|p| p.is_a?(User) && !p.active?}
27 27 assert_nil result.detect {|p| p.is_a?(AnonymousUser)}
28 28 end
29 29
30 30 def test_member_of_scope_should_return_the_union_of_all_members
31 31 projects = Project.find_all_by_id(1, 2)
32 32 assert_equal projects.map(&:principals).flatten.sort, Principal.member_of(projects).sort
33 33 end
34 34
35 35 context "#like" do
36 36 setup do
37 37 Principal.generate!(:login => 'login')
38 38 Principal.generate!(:login => 'login2')
39 39
40 40 Principal.generate!(:firstname => 'firstname')
41 41 Principal.generate!(:firstname => 'firstname2')
42 42
43 43 Principal.generate!(:lastname => 'lastname')
44 44 Principal.generate!(:lastname => 'lastname2')
45 45
46 46 Principal.generate!(:mail => 'mail@example.com')
47 47 Principal.generate!(:mail => 'mail2@example.com')
48
49 @palmer = Principal.generate!(:firstname => 'David', :lastname => 'Palmer')
48 50 end
49 51
50 52 should "search login" do
51 53 results = Principal.like('login')
52 54
53 55 assert_equal 2, results.count
54 56 assert results.all? {|u| u.login.match(/login/) }
55 57 end
56 58
57 59 should "search firstname" do
58 60 results = Principal.like('firstname')
59 61
60 62 assert_equal 2, results.count
61 63 assert results.all? {|u| u.firstname.match(/firstname/) }
62 64 end
63 65
64 66 should "search lastname" do
65 67 results = Principal.like('lastname')
66 68
67 69 assert_equal 2, results.count
68 70 assert results.all? {|u| u.lastname.match(/lastname/) }
69 71 end
70 72
71 73 should "search mail" do
72 74 results = Principal.like('mail')
73 75
74 76 assert_equal 2, results.count
75 77 assert results.all? {|u| u.mail.match(/mail/) }
76 78 end
77 end
78 79
80 should "search firstname and lastname" do
81 results = Principal.like('david palm')
82
83 assert_equal 1, results.count
84 assert_equal @palmer, results.first
85 end
86
87 should "search lastname and firstname" do
88 results = Principal.like('palmer davi')
89
90 assert_equal 1, results.count
91 assert_equal @palmer, results.first
92 end
93 end
79 94 end
General Comments 0
You need to be logged in to leave comments. Login now