##// END OF EJS Templates
Changed the way the visibility SQL statement is built....
Jean-Philippe Lang -
r5020:5f889932b6ce
parent child
Show More
@@ -1,841 +1,847
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
1 # Redmine - project management software
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 Project < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20
21 21 # Project statuses
22 22 STATUS_ACTIVE = 1
23 23 STATUS_ARCHIVED = 9
24 24
25 25 # Maximum length for project identifiers
26 26 IDENTIFIER_MAX_LENGTH = 100
27 27
28 28 # Specific overidden Activities
29 29 has_many :time_entry_activities
30 30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
31 31 has_many :memberships, :class_name => 'Member'
32 32 has_many :member_principals, :class_name => 'Member',
33 33 :include => :principal,
34 34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
35 35 has_many :users, :through => :members
36 36 has_many :principals, :through => :member_principals, :source => :principal
37 37
38 38 has_many :enabled_modules, :dependent => :delete_all
39 39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
41 41 has_many :issue_changes, :through => :issues, :source => :journals
42 42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 43 has_many :time_entries, :dependent => :delete_all
44 44 has_many :queries, :dependent => :delete_all
45 45 has_many :documents, :dependent => :destroy
46 46 has_many :news, :dependent => :delete_all, :include => :author
47 47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 48 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 49 has_one :repository, :dependent => :destroy
50 50 has_many :changesets, :through => :repository
51 51 has_one :wiki, :dependent => :destroy
52 52 # Custom field for the project issues
53 53 has_and_belongs_to_many :issue_custom_fields,
54 54 :class_name => 'IssueCustomField',
55 55 :order => "#{CustomField.table_name}.position",
56 56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 57 :association_foreign_key => 'custom_field_id'
58 58
59 59 acts_as_nested_set :order => 'name'
60 60 acts_as_attachable :view_permission => :view_files,
61 61 :delete_permission => :manage_files
62 62
63 63 acts_as_customizable
64 64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 67 :author => nil
68 68
69 69 attr_protected :status
70 70
71 71 validates_presence_of :name, :identifier
72 72 validates_uniqueness_of :identifier
73 73 validates_associated :repository, :wiki
74 74 validates_length_of :name, :maximum => 255
75 75 validates_length_of :homepage, :maximum => 255
76 76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 77 # donwcase letters, digits, dashes but not digits only
78 78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
79 79 # reserved words
80 80 validates_exclusion_of :identifier, :in => %w( new )
81 81
82 82 before_destroy :delete_all_members, :destroy_children
83 83
84 84 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
85 85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
86 86 named_scope :all_public, { :conditions => { :is_public => true } }
87 87 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
88 88
89 89 def initialize(attributes = nil)
90 90 super
91 91
92 92 initialized = (attributes || {}).stringify_keys
93 93 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
94 94 self.identifier = Project.next_identifier
95 95 end
96 96 if !initialized.key?('is_public')
97 97 self.is_public = Setting.default_projects_public?
98 98 end
99 99 if !initialized.key?('enabled_module_names')
100 100 self.enabled_module_names = Setting.default_projects_modules
101 101 end
102 102 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
103 103 self.trackers = Tracker.all
104 104 end
105 105 end
106 106
107 107 def identifier=(identifier)
108 108 super unless identifier_frozen?
109 109 end
110 110
111 111 def identifier_frozen?
112 112 errors[:identifier].nil? && !(new_record? || identifier.blank?)
113 113 end
114 114
115 115 # returns latest created projects
116 116 # non public projects will be returned only if user is a member of those
117 117 def self.latest(user=nil, count=5)
118 118 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
119 119 end
120 120
121 121 # Returns a SQL :conditions string used to find all active projects for the specified user.
122 122 #
123 123 # Examples:
124 124 # Projects.visible_by(admin) => "projects.status = 1"
125 125 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
126 126 def self.visible_by(user=nil)
127 127 user ||= User.current
128 128 if user && user.admin?
129 129 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
130 130 elsif user && user.memberships.any?
131 131 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
132 132 else
133 133 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
134 134 end
135 135 end
136 136
137 137 def self.allowed_to_condition(user, permission, options={})
138 statements = []
139 138 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
140 139 if perm = Redmine::AccessControl.permission(permission)
141 140 unless perm.project_module.nil?
142 141 # If the permission belongs to a project module, make sure the module is enabled
143 142 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
144 143 end
145 144 end
146 145 if options[:project]
147 146 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
148 147 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
149 148 base_statement = "(#{project_statement}) AND (#{base_statement})"
150 149 end
150
151 151 if user.admin?
152 # no restriction
152 base_statement
153 153 else
154 statements << "1=0"
154 statement_by_role = {}
155 155 if user.logged?
156 156 if Role.non_member.allowed_to?(permission) && !options[:member]
157 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
157 statement_by_role[Role.non_member] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
158 end
159 user.projects_by_role.each do |role, projects|
160 if role.allowed_to?(permission)
161 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
162 end
158 163 end
159 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
160 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
161 164 else
162 165 if Role.anonymous.allowed_to?(permission) && !options[:member]
163 # anonymous user allowed on public project
164 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
166 statement_by_role[Role.anonymous] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
165 167 end
166 168 end
169 if statement_by_role.empty?
170 "1=0"
171 else
172 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
173 end
167 174 end
168 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
169 175 end
170 176
171 177 # Returns the Systemwide and project specific activities
172 178 def activities(include_inactive=false)
173 179 if include_inactive
174 180 return all_activities
175 181 else
176 182 return active_activities
177 183 end
178 184 end
179 185
180 186 # Will create a new Project specific Activity or update an existing one
181 187 #
182 188 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
183 189 # does not successfully save.
184 190 def update_or_create_time_entry_activity(id, activity_hash)
185 191 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
186 192 self.create_time_entry_activity_if_needed(activity_hash)
187 193 else
188 194 activity = project.time_entry_activities.find_by_id(id.to_i)
189 195 activity.update_attributes(activity_hash) if activity
190 196 end
191 197 end
192 198
193 199 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
194 200 #
195 201 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
196 202 # does not successfully save.
197 203 def create_time_entry_activity_if_needed(activity)
198 204 if activity['parent_id']
199 205
200 206 parent_activity = TimeEntryActivity.find(activity['parent_id'])
201 207 activity['name'] = parent_activity.name
202 208 activity['position'] = parent_activity.position
203 209
204 210 if Enumeration.overridding_change?(activity, parent_activity)
205 211 project_activity = self.time_entry_activities.create(activity)
206 212
207 213 if project_activity.new_record?
208 214 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
209 215 else
210 216 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
211 217 end
212 218 end
213 219 end
214 220 end
215 221
216 222 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
217 223 #
218 224 # Examples:
219 225 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
220 226 # project.project_condition(false) => "projects.id = 1"
221 227 def project_condition(with_subprojects)
222 228 cond = "#{Project.table_name}.id = #{id}"
223 229 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
224 230 cond
225 231 end
226 232
227 233 def self.find(*args)
228 234 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
229 235 project = find_by_identifier(*args)
230 236 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
231 237 project
232 238 else
233 239 super
234 240 end
235 241 end
236 242
237 243 def to_param
238 244 # id is used for projects with a numeric identifier (compatibility)
239 245 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
240 246 end
241 247
242 248 def active?
243 249 self.status == STATUS_ACTIVE
244 250 end
245 251
246 252 def archived?
247 253 self.status == STATUS_ARCHIVED
248 254 end
249 255
250 256 # Archives the project and its descendants
251 257 def archive
252 258 # Check that there is no issue of a non descendant project that is assigned
253 259 # to one of the project or descendant versions
254 260 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
255 261 if v_ids.any? && Issue.find(:first, :include => :project,
256 262 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
257 263 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
258 264 return false
259 265 end
260 266 Project.transaction do
261 267 archive!
262 268 end
263 269 true
264 270 end
265 271
266 272 # Unarchives the project
267 273 # All its ancestors must be active
268 274 def unarchive
269 275 return false if ancestors.detect {|a| !a.active?}
270 276 update_attribute :status, STATUS_ACTIVE
271 277 end
272 278
273 279 # Returns an array of projects the project can be moved to
274 280 # by the current user
275 281 def allowed_parents
276 282 return @allowed_parents if @allowed_parents
277 283 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
278 284 @allowed_parents = @allowed_parents - self_and_descendants
279 285 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
280 286 @allowed_parents << nil
281 287 end
282 288 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
283 289 @allowed_parents << parent
284 290 end
285 291 @allowed_parents
286 292 end
287 293
288 294 # Sets the parent of the project with authorization check
289 295 def set_allowed_parent!(p)
290 296 unless p.nil? || p.is_a?(Project)
291 297 if p.to_s.blank?
292 298 p = nil
293 299 else
294 300 p = Project.find_by_id(p)
295 301 return false unless p
296 302 end
297 303 end
298 304 if p.nil?
299 305 if !new_record? && allowed_parents.empty?
300 306 return false
301 307 end
302 308 elsif !allowed_parents.include?(p)
303 309 return false
304 310 end
305 311 set_parent!(p)
306 312 end
307 313
308 314 # Sets the parent of the project
309 315 # Argument can be either a Project, a String, a Fixnum or nil
310 316 def set_parent!(p)
311 317 unless p.nil? || p.is_a?(Project)
312 318 if p.to_s.blank?
313 319 p = nil
314 320 else
315 321 p = Project.find_by_id(p)
316 322 return false unless p
317 323 end
318 324 end
319 325 if p == parent && !p.nil?
320 326 # Nothing to do
321 327 true
322 328 elsif p.nil? || (p.active? && move_possible?(p))
323 329 # Insert the project so that target's children or root projects stay alphabetically sorted
324 330 sibs = (p.nil? ? self.class.roots : p.children)
325 331 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
326 332 if to_be_inserted_before
327 333 move_to_left_of(to_be_inserted_before)
328 334 elsif p.nil?
329 335 if sibs.empty?
330 336 # move_to_root adds the project in first (ie. left) position
331 337 move_to_root
332 338 else
333 339 move_to_right_of(sibs.last) unless self == sibs.last
334 340 end
335 341 else
336 342 # move_to_child_of adds the project in last (ie.right) position
337 343 move_to_child_of(p)
338 344 end
339 345 Issue.update_versions_from_hierarchy_change(self)
340 346 true
341 347 else
342 348 # Can not move to the given target
343 349 false
344 350 end
345 351 end
346 352
347 353 # Returns an array of the trackers used by the project and its active sub projects
348 354 def rolled_up_trackers
349 355 @rolled_up_trackers ||=
350 356 Tracker.find(:all, :include => :projects,
351 357 :select => "DISTINCT #{Tracker.table_name}.*",
352 358 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
353 359 :order => "#{Tracker.table_name}.position")
354 360 end
355 361
356 362 # Closes open and locked project versions that are completed
357 363 def close_completed_versions
358 364 Version.transaction do
359 365 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
360 366 if version.completed?
361 367 version.update_attribute(:status, 'closed')
362 368 end
363 369 end
364 370 end
365 371 end
366 372
367 373 # Returns a scope of the Versions on subprojects
368 374 def rolled_up_versions
369 375 @rolled_up_versions ||=
370 376 Version.scoped(:include => :project,
371 377 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
372 378 end
373 379
374 380 # Returns a scope of the Versions used by the project
375 381 def shared_versions
376 382 @shared_versions ||=
377 383 Version.scoped(:include => :project,
378 384 :conditions => "#{Project.table_name}.id = #{id}" +
379 385 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
380 386 " #{Version.table_name}.sharing = 'system'" +
381 387 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
382 388 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
383 389 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
384 390 "))")
385 391 end
386 392
387 393 # Returns a hash of project users grouped by role
388 394 def users_by_role
389 395 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
390 396 m.roles.each do |r|
391 397 h[r] ||= []
392 398 h[r] << m.user
393 399 end
394 400 h
395 401 end
396 402 end
397 403
398 404 # Deletes all project's members
399 405 def delete_all_members
400 406 me, mr = Member.table_name, MemberRole.table_name
401 407 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
402 408 Member.delete_all(['project_id = ?', id])
403 409 end
404 410
405 411 # Users issues can be assigned to
406 412 def assignable_users
407 413 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
408 414 end
409 415
410 416 # Returns the mail adresses of users that should be always notified on project events
411 417 def recipients
412 418 notified_users.collect {|user| user.mail}
413 419 end
414 420
415 421 # Returns the users that should be notified on project events
416 422 def notified_users
417 423 # TODO: User part should be extracted to User#notify_about?
418 424 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
419 425 end
420 426
421 427 # Returns an array of all custom fields enabled for project issues
422 428 # (explictly associated custom fields and custom fields enabled for all projects)
423 429 def all_issue_custom_fields
424 430 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
425 431 end
426 432
427 433 def project
428 434 self
429 435 end
430 436
431 437 def <=>(project)
432 438 name.downcase <=> project.name.downcase
433 439 end
434 440
435 441 def to_s
436 442 name
437 443 end
438 444
439 445 # Returns a short description of the projects (first lines)
440 446 def short_description(length = 255)
441 447 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
442 448 end
443 449
444 450 def css_classes
445 451 s = 'project'
446 452 s << ' root' if root?
447 453 s << ' child' if child?
448 454 s << (leaf? ? ' leaf' : ' parent')
449 455 s
450 456 end
451 457
452 458 # The earliest start date of a project, based on it's issues and versions
453 459 def start_date
454 460 [
455 461 issues.minimum('start_date'),
456 462 shared_versions.collect(&:effective_date),
457 463 shared_versions.collect(&:start_date)
458 464 ].flatten.compact.min
459 465 end
460 466
461 467 # The latest due date of an issue or version
462 468 def due_date
463 469 [
464 470 issues.maximum('due_date'),
465 471 shared_versions.collect(&:effective_date),
466 472 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
467 473 ].flatten.compact.max
468 474 end
469 475
470 476 def overdue?
471 477 active? && !due_date.nil? && (due_date < Date.today)
472 478 end
473 479
474 480 # Returns the percent completed for this project, based on the
475 481 # progress on it's versions.
476 482 def completed_percent(options={:include_subprojects => false})
477 483 if options.delete(:include_subprojects)
478 484 total = self_and_descendants.collect(&:completed_percent).sum
479 485
480 486 total / self_and_descendants.count
481 487 else
482 488 if versions.count > 0
483 489 total = versions.collect(&:completed_pourcent).sum
484 490
485 491 total / versions.count
486 492 else
487 493 100
488 494 end
489 495 end
490 496 end
491 497
492 498 # Return true if this project is allowed to do the specified action.
493 499 # action can be:
494 500 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
495 501 # * a permission Symbol (eg. :edit_project)
496 502 def allows_to?(action)
497 503 if action.is_a? Hash
498 504 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
499 505 else
500 506 allowed_permissions.include? action
501 507 end
502 508 end
503 509
504 510 def module_enabled?(module_name)
505 511 module_name = module_name.to_s
506 512 enabled_modules.detect {|m| m.name == module_name}
507 513 end
508 514
509 515 def enabled_module_names=(module_names)
510 516 if module_names && module_names.is_a?(Array)
511 517 module_names = module_names.collect(&:to_s).reject(&:blank?)
512 518 # remove disabled modules
513 519 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
514 520 # add new modules
515 521 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
516 522 else
517 523 enabled_modules.clear
518 524 end
519 525 end
520 526
521 527 # Returns an array of the enabled modules names
522 528 def enabled_module_names
523 529 enabled_modules.collect(&:name)
524 530 end
525 531
526 532 safe_attributes 'name',
527 533 'description',
528 534 'homepage',
529 535 'is_public',
530 536 'identifier',
531 537 'custom_field_values',
532 538 'custom_fields',
533 539 'tracker_ids',
534 540 'issue_custom_field_ids'
535 541
536 542 safe_attributes 'enabled_module_names',
537 543 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
538 544
539 545 # Returns an array of projects that are in this project's hierarchy
540 546 #
541 547 # Example: parents, children, siblings
542 548 def hierarchy
543 549 parents = project.self_and_ancestors || []
544 550 descendants = project.descendants || []
545 551 project_hierarchy = parents | descendants # Set union
546 552 end
547 553
548 554 # Returns an auto-generated project identifier based on the last identifier used
549 555 def self.next_identifier
550 556 p = Project.find(:first, :order => 'created_on DESC')
551 557 p.nil? ? nil : p.identifier.to_s.succ
552 558 end
553 559
554 560 # Copies and saves the Project instance based on the +project+.
555 561 # Duplicates the source project's:
556 562 # * Wiki
557 563 # * Versions
558 564 # * Categories
559 565 # * Issues
560 566 # * Members
561 567 # * Queries
562 568 #
563 569 # Accepts an +options+ argument to specify what to copy
564 570 #
565 571 # Examples:
566 572 # project.copy(1) # => copies everything
567 573 # project.copy(1, :only => 'members') # => copies members only
568 574 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
569 575 def copy(project, options={})
570 576 project = project.is_a?(Project) ? project : Project.find(project)
571 577
572 578 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
573 579 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
574 580
575 581 Project.transaction do
576 582 if save
577 583 reload
578 584 to_be_copied.each do |name|
579 585 send "copy_#{name}", project
580 586 end
581 587 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
582 588 save
583 589 end
584 590 end
585 591 end
586 592
587 593
588 594 # Copies +project+ and returns the new instance. This will not save
589 595 # the copy
590 596 def self.copy_from(project)
591 597 begin
592 598 project = project.is_a?(Project) ? project : Project.find(project)
593 599 if project
594 600 # clear unique attributes
595 601 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
596 602 copy = Project.new(attributes)
597 603 copy.enabled_modules = project.enabled_modules
598 604 copy.trackers = project.trackers
599 605 copy.custom_values = project.custom_values.collect {|v| v.clone}
600 606 copy.issue_custom_fields = project.issue_custom_fields
601 607 return copy
602 608 else
603 609 return nil
604 610 end
605 611 rescue ActiveRecord::RecordNotFound
606 612 return nil
607 613 end
608 614 end
609 615
610 616 # Yields the given block for each project with its level in the tree
611 617 def self.project_tree(projects, &block)
612 618 ancestors = []
613 619 projects.sort_by(&:lft).each do |project|
614 620 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
615 621 ancestors.pop
616 622 end
617 623 yield project, ancestors.size
618 624 ancestors << project
619 625 end
620 626 end
621 627
622 628 private
623 629
624 630 # Destroys children before destroying self
625 631 def destroy_children
626 632 children.each do |child|
627 633 child.destroy
628 634 end
629 635 end
630 636
631 637 # Copies wiki from +project+
632 638 def copy_wiki(project)
633 639 # Check that the source project has a wiki first
634 640 unless project.wiki.nil?
635 641 self.wiki ||= Wiki.new
636 642 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
637 643 wiki_pages_map = {}
638 644 project.wiki.pages.each do |page|
639 645 # Skip pages without content
640 646 next if page.content.nil?
641 647 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
642 648 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
643 649 new_wiki_page.content = new_wiki_content
644 650 wiki.pages << new_wiki_page
645 651 wiki_pages_map[page.id] = new_wiki_page
646 652 end
647 653 wiki.save
648 654 # Reproduce page hierarchy
649 655 project.wiki.pages.each do |page|
650 656 if page.parent_id && wiki_pages_map[page.id]
651 657 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
652 658 wiki_pages_map[page.id].save
653 659 end
654 660 end
655 661 end
656 662 end
657 663
658 664 # Copies versions from +project+
659 665 def copy_versions(project)
660 666 project.versions.each do |version|
661 667 new_version = Version.new
662 668 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
663 669 self.versions << new_version
664 670 end
665 671 end
666 672
667 673 # Copies issue categories from +project+
668 674 def copy_issue_categories(project)
669 675 project.issue_categories.each do |issue_category|
670 676 new_issue_category = IssueCategory.new
671 677 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
672 678 self.issue_categories << new_issue_category
673 679 end
674 680 end
675 681
676 682 # Copies issues from +project+
677 683 def copy_issues(project)
678 684 # Stores the source issue id as a key and the copied issues as the
679 685 # value. Used to map the two togeather for issue relations.
680 686 issues_map = {}
681 687
682 688 # Get issues sorted by root_id, lft so that parent issues
683 689 # get copied before their children
684 690 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
685 691 new_issue = Issue.new
686 692 new_issue.copy_from(issue)
687 693 new_issue.project = self
688 694 # Reassign fixed_versions by name, since names are unique per
689 695 # project and the versions for self are not yet saved
690 696 if issue.fixed_version
691 697 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
692 698 end
693 699 # Reassign the category by name, since names are unique per
694 700 # project and the categories for self are not yet saved
695 701 if issue.category
696 702 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
697 703 end
698 704 # Parent issue
699 705 if issue.parent_id
700 706 if copied_parent = issues_map[issue.parent_id]
701 707 new_issue.parent_issue_id = copied_parent.id
702 708 end
703 709 end
704 710
705 711 self.issues << new_issue
706 712 if new_issue.new_record?
707 713 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
708 714 else
709 715 issues_map[issue.id] = new_issue unless new_issue.new_record?
710 716 end
711 717 end
712 718
713 719 # Relations after in case issues related each other
714 720 project.issues.each do |issue|
715 721 new_issue = issues_map[issue.id]
716 722 unless new_issue
717 723 # Issue was not copied
718 724 next
719 725 end
720 726
721 727 # Relations
722 728 issue.relations_from.each do |source_relation|
723 729 new_issue_relation = IssueRelation.new
724 730 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
725 731 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
726 732 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
727 733 new_issue_relation.issue_to = source_relation.issue_to
728 734 end
729 735 new_issue.relations_from << new_issue_relation
730 736 end
731 737
732 738 issue.relations_to.each do |source_relation|
733 739 new_issue_relation = IssueRelation.new
734 740 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
735 741 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
736 742 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
737 743 new_issue_relation.issue_from = source_relation.issue_from
738 744 end
739 745 new_issue.relations_to << new_issue_relation
740 746 end
741 747 end
742 748 end
743 749
744 750 # Copies members from +project+
745 751 def copy_members(project)
746 752 # Copy users first, then groups to handle members with inherited and given roles
747 753 members_to_copy = []
748 754 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
749 755 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
750 756
751 757 members_to_copy.each do |member|
752 758 new_member = Member.new
753 759 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
754 760 # only copy non inherited roles
755 761 # inherited roles will be added when copying the group membership
756 762 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
757 763 next if role_ids.empty?
758 764 new_member.role_ids = role_ids
759 765 new_member.project = self
760 766 self.members << new_member
761 767 end
762 768 end
763 769
764 770 # Copies queries from +project+
765 771 def copy_queries(project)
766 772 project.queries.each do |query|
767 773 new_query = Query.new
768 774 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
769 775 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
770 776 new_query.project = self
771 777 self.queries << new_query
772 778 end
773 779 end
774 780
775 781 # Copies boards from +project+
776 782 def copy_boards(project)
777 783 project.boards.each do |board|
778 784 new_board = Board.new
779 785 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
780 786 new_board.project = self
781 787 self.boards << new_board
782 788 end
783 789 end
784 790
785 791 def allowed_permissions
786 792 @allowed_permissions ||= begin
787 793 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
788 794 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
789 795 end
790 796 end
791 797
792 798 def allowed_actions
793 799 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
794 800 end
795 801
796 802 # Returns all the active Systemwide and project specific activities
797 803 def active_activities
798 804 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
799 805
800 806 if overridden_activity_ids.empty?
801 807 return TimeEntryActivity.shared.active
802 808 else
803 809 return system_activities_and_project_overrides
804 810 end
805 811 end
806 812
807 813 # Returns all the Systemwide and project specific activities
808 814 # (inactive and active)
809 815 def all_activities
810 816 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
811 817
812 818 if overridden_activity_ids.empty?
813 819 return TimeEntryActivity.shared
814 820 else
815 821 return system_activities_and_project_overrides(true)
816 822 end
817 823 end
818 824
819 825 # Returns the systemwide active activities merged with the project specific overrides
820 826 def system_activities_and_project_overrides(include_inactive=false)
821 827 if include_inactive
822 828 return TimeEntryActivity.shared.
823 829 find(:all,
824 830 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
825 831 self.time_entry_activities
826 832 else
827 833 return TimeEntryActivity.shared.active.
828 834 find(:all,
829 835 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
830 836 self.time_entry_activities.active
831 837 end
832 838 end
833 839
834 840 # Archives subprojects recursively
835 841 def archive!
836 842 children.each do |subproject|
837 843 subproject.send :archive!
838 844 end
839 845 update_attribute :status, STATUS_ARCHIVED
840 846 end
841 847 end
@@ -1,572 +1,590
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
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 USER_FORMATS = {
30 30 :firstname_lastname => '#{firstname} #{lastname}',
31 31 :firstname => '#{firstname}',
32 32 :lastname_firstname => '#{lastname} #{firstname}',
33 33 :lastname_coma_firstname => '#{lastname}, #{firstname}',
34 34 :username => '#{login}'
35 35 }
36 36
37 37 MAIL_NOTIFICATION_OPTIONS = [
38 38 ['all', :label_user_mail_option_all],
39 39 ['selected', :label_user_mail_option_selected],
40 40 ['only_my_events', :label_user_mail_option_only_my_events],
41 41 ['only_assigned', :label_user_mail_option_only_assigned],
42 42 ['only_owner', :label_user_mail_option_only_owner],
43 43 ['none', :label_user_mail_option_none]
44 44 ]
45 45
46 46 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
47 47 :after_remove => Proc.new {|user, group| group.user_removed(user)}
48 48 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
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
58 58 acts_as_customizable
59 59
60 60 attr_accessor :password, :password_confirmation
61 61 attr_accessor :last_before_login_on
62 62 # Prevents unauthorized assignments
63 63 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
64 64
65 65 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
66 66 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
67 67 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
68 68 # Login must contain lettres, numbers, underscores only
69 69 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
70 70 validates_length_of :login, :maximum => 30
71 71 validates_length_of :firstname, :lastname, :maximum => 30
72 72 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
73 73 validates_length_of :mail, :maximum => 60, :allow_nil => true
74 74 validates_confirmation_of :password, :allow_nil => true
75 75 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
76 76
77 77 before_destroy :remove_references_before_destroy
78 78
79 79 def before_create
80 80 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
81 81 true
82 82 end
83 83
84 84 def before_save
85 85 # update hashed_password if password was set
86 86 if self.password && self.auth_source_id.blank?
87 87 salt_password(password)
88 88 end
89 89 end
90 90
91 91 def reload(*args)
92 92 @name = nil
93 @projects_by_role = nil
93 94 super
94 95 end
95 96
96 97 def mail=(arg)
97 98 write_attribute(:mail, arg.to_s.strip)
98 99 end
99 100
100 101 def identity_url=(url)
101 102 if url.blank?
102 103 write_attribute(:identity_url, '')
103 104 else
104 105 begin
105 106 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
106 107 rescue OpenIdAuthentication::InvalidOpenId
107 108 # Invlaid url, don't save
108 109 end
109 110 end
110 111 self.read_attribute(:identity_url)
111 112 end
112 113
113 114 # Returns the user that matches provided login and password, or nil
114 115 def self.try_to_login(login, password)
115 116 # Make sure no one can sign in with an empty password
116 117 return nil if password.to_s.empty?
117 118 user = find_by_login(login)
118 119 if user
119 120 # user is already in local database
120 121 return nil if !user.active?
121 122 if user.auth_source
122 123 # user has an external authentication method
123 124 return nil unless user.auth_source.authenticate(login, password)
124 125 else
125 126 # authentication with local password
126 127 return nil unless user.check_password?(password)
127 128 end
128 129 else
129 130 # user is not yet registered, try to authenticate with available sources
130 131 attrs = AuthSource.authenticate(login, password)
131 132 if attrs
132 133 user = new(attrs)
133 134 user.login = login
134 135 user.language = Setting.default_language
135 136 if user.save
136 137 user.reload
137 138 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
138 139 end
139 140 end
140 141 end
141 142 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
142 143 user
143 144 rescue => text
144 145 raise text
145 146 end
146 147
147 148 # Returns the user who matches the given autologin +key+ or nil
148 149 def self.try_to_autologin(key)
149 150 tokens = Token.find_all_by_action_and_value('autologin', key)
150 151 # Make sure there's only 1 token that matches the key
151 152 if tokens.size == 1
152 153 token = tokens.first
153 154 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
154 155 token.user.update_attribute(:last_login_on, Time.now)
155 156 token.user
156 157 end
157 158 end
158 159 end
159 160
160 161 # Return user's full name for display
161 162 def name(formatter = nil)
162 163 if formatter
163 164 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
164 165 else
165 166 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
166 167 end
167 168 end
168 169
169 170 def active?
170 171 self.status == STATUS_ACTIVE
171 172 end
172 173
173 174 def registered?
174 175 self.status == STATUS_REGISTERED
175 176 end
176 177
177 178 def locked?
178 179 self.status == STATUS_LOCKED
179 180 end
180 181
181 182 def activate
182 183 self.status = STATUS_ACTIVE
183 184 end
184 185
185 186 def register
186 187 self.status = STATUS_REGISTERED
187 188 end
188 189
189 190 def lock
190 191 self.status = STATUS_LOCKED
191 192 end
192 193
193 194 def activate!
194 195 update_attribute(:status, STATUS_ACTIVE)
195 196 end
196 197
197 198 def register!
198 199 update_attribute(:status, STATUS_REGISTERED)
199 200 end
200 201
201 202 def lock!
202 203 update_attribute(:status, STATUS_LOCKED)
203 204 end
204 205
205 206 # Returns true if +clear_password+ is the correct user's password, otherwise false
206 207 def check_password?(clear_password)
207 208 if auth_source_id.present?
208 209 auth_source.authenticate(self.login, clear_password)
209 210 else
210 211 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
211 212 end
212 213 end
213 214
214 215 # Generates a random salt and computes hashed_password for +clear_password+
215 216 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
216 217 def salt_password(clear_password)
217 218 self.salt = User.generate_salt
218 219 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
219 220 end
220 221
221 222 # Does the backend storage allow this user to change their password?
222 223 def change_password_allowed?
223 224 return true if auth_source_id.blank?
224 225 return auth_source.allow_password_changes?
225 226 end
226 227
227 228 # Generate and set a random password. Useful for automated user creation
228 229 # Based on Token#generate_token_value
229 230 #
230 231 def random_password
231 232 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
232 233 password = ''
233 234 40.times { |i| password << chars[rand(chars.size-1)] }
234 235 self.password = password
235 236 self.password_confirmation = password
236 237 self
237 238 end
238 239
239 240 def pref
240 241 self.preference ||= UserPreference.new(:user => self)
241 242 end
242 243
243 244 def time_zone
244 245 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
245 246 end
246 247
247 248 def wants_comments_in_reverse_order?
248 249 self.pref[:comments_sorting] == 'desc'
249 250 end
250 251
251 252 # Return user's RSS key (a 40 chars long string), used to access feeds
252 253 def rss_key
253 254 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
254 255 token.value
255 256 end
256 257
257 258 # Return user's API key (a 40 chars long string), used to access the API
258 259 def api_key
259 260 token = self.api_token || self.create_api_token(:action => 'api')
260 261 token.value
261 262 end
262 263
263 264 # Return an array of project ids for which the user has explicitly turned mail notifications on
264 265 def notified_projects_ids
265 266 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
266 267 end
267 268
268 269 def notified_project_ids=(ids)
269 270 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
270 271 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
271 272 @notified_projects_ids = nil
272 273 notified_projects_ids
273 274 end
274 275
275 276 def valid_notification_options
276 277 self.class.valid_notification_options(self)
277 278 end
278 279
279 280 # Only users that belong to more than 1 project can select projects for which they are notified
280 281 def self.valid_notification_options(user=nil)
281 282 # Note that @user.membership.size would fail since AR ignores
282 283 # :include association option when doing a count
283 284 if user.nil? || user.memberships.length < 1
284 285 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
285 286 else
286 287 MAIL_NOTIFICATION_OPTIONS
287 288 end
288 289 end
289 290
290 291 # Find a user account by matching the exact login and then a case-insensitive
291 292 # version. Exact matches will be given priority.
292 293 def self.find_by_login(login)
293 294 # force string comparison to be case sensitive on MySQL
294 295 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
295 296
296 297 # First look for an exact match
297 298 user = first(:conditions => ["#{type_cast} login = ?", login])
298 299 # Fail over to case-insensitive if none was found
299 300 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
300 301 end
301 302
302 303 def self.find_by_rss_key(key)
303 304 token = Token.find_by_value(key)
304 305 token && token.user.active? ? token.user : nil
305 306 end
306 307
307 308 def self.find_by_api_key(key)
308 309 token = Token.find_by_action_and_value('api', key)
309 310 token && token.user.active? ? token.user : nil
310 311 end
311 312
312 313 # Makes find_by_mail case-insensitive
313 314 def self.find_by_mail(mail)
314 315 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
315 316 end
316 317
317 318 def to_s
318 319 name
319 320 end
320 321
321 322 # Returns the current day according to user's time zone
322 323 def today
323 324 if time_zone.nil?
324 325 Date.today
325 326 else
326 327 Time.now.in_time_zone(time_zone).to_date
327 328 end
328 329 end
329 330
330 331 def logged?
331 332 true
332 333 end
333 334
334 335 def anonymous?
335 336 !logged?
336 337 end
337 338
338 339 # Return user's roles for project
339 340 def roles_for_project(project)
340 341 roles = []
341 342 # No role on archived projects
342 343 return roles unless project && project.active?
343 344 if logged?
344 345 # Find project membership
345 346 membership = memberships.detect {|m| m.project_id == project.id}
346 347 if membership
347 348 roles = membership.roles
348 349 else
349 350 @role_non_member ||= Role.non_member
350 351 roles << @role_non_member
351 352 end
352 353 else
353 354 @role_anonymous ||= Role.anonymous
354 355 roles << @role_anonymous
355 356 end
356 357 roles
357 358 end
358 359
359 360 # Return true if the user is a member of project
360 361 def member_of?(project)
361 362 !roles_for_project(project).detect {|role| role.member?}.nil?
362 363 end
363 364
365 # Returns a hash of user's projects grouped by roles
366 def projects_by_role
367 return @projects_by_role if @projects_by_role
368
369 @projects_by_role = Hash.new {|h,k| h[k]=[]}
370 memberships.each do |membership|
371 membership.roles.each do |role|
372 @projects_by_role[role] << membership.project if membership.project
373 end
374 end
375 @projects_by_role.each do |role, projects|
376 projects.uniq!
377 end
378
379 @projects_by_role
380 end
381
364 382 # Return true if the user is allowed to do the specified action on a specific context
365 383 # Action can be:
366 384 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
367 385 # * a permission Symbol (eg. :edit_project)
368 386 # Context can be:
369 387 # * a project : returns true if user is allowed to do the specified action on this project
370 388 # * a group of projects : returns true if user is allowed on every project
371 389 # * nil with options[:global] set : check if user has at least one role allowed for this action,
372 390 # or falls back to Non Member / Anonymous permissions depending if the user is logged
373 391 def allowed_to?(action, context, options={})
374 392 if context && context.is_a?(Project)
375 393 # No action allowed on archived projects
376 394 return false unless context.active?
377 395 # No action allowed on disabled modules
378 396 return false unless context.allows_to?(action)
379 397 # Admin users are authorized for anything else
380 398 return true if admin?
381 399
382 400 roles = roles_for_project(context)
383 401 return false unless roles
384 402 roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)}
385 403
386 404 elsif context && context.is_a?(Array)
387 405 # Authorize if user is authorized on every element of the array
388 406 context.map do |project|
389 407 allowed_to?(action,project,options)
390 408 end.inject do |memo,allowed|
391 409 memo && allowed
392 410 end
393 411 elsif options[:global]
394 412 # Admin users are always authorized
395 413 return true if admin?
396 414
397 415 # authorize if user has at least one role that has this permission
398 416 roles = memberships.collect {|m| m.roles}.flatten.uniq
399 417 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
400 418 else
401 419 false
402 420 end
403 421 end
404 422
405 423 # Is the user allowed to do the specified action on any project?
406 424 # See allowed_to? for the actions and valid options.
407 425 def allowed_to_globally?(action, options)
408 426 allowed_to?(action, nil, options.reverse_merge(:global => true))
409 427 end
410 428
411 429 safe_attributes 'login',
412 430 'firstname',
413 431 'lastname',
414 432 'mail',
415 433 'mail_notification',
416 434 'language',
417 435 'custom_field_values',
418 436 'custom_fields',
419 437 'identity_url'
420 438
421 439 safe_attributes 'status',
422 440 'auth_source_id',
423 441 :if => lambda {|user, current_user| current_user.admin?}
424 442
425 443 safe_attributes 'group_ids',
426 444 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
427 445
428 446 # Utility method to help check if a user should be notified about an
429 447 # event.
430 448 #
431 449 # TODO: only supports Issue events currently
432 450 def notify_about?(object)
433 451 case mail_notification
434 452 when 'all'
435 453 true
436 454 when 'selected'
437 455 # user receives notifications for created/assigned issues on unselected projects
438 456 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
439 457 true
440 458 else
441 459 false
442 460 end
443 461 when 'none'
444 462 false
445 463 when 'only_my_events'
446 464 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
447 465 true
448 466 else
449 467 false
450 468 end
451 469 when 'only_assigned'
452 470 if object.is_a?(Issue) && object.assigned_to == self
453 471 true
454 472 else
455 473 false
456 474 end
457 475 when 'only_owner'
458 476 if object.is_a?(Issue) && object.author == self
459 477 true
460 478 else
461 479 false
462 480 end
463 481 else
464 482 false
465 483 end
466 484 end
467 485
468 486 def self.current=(user)
469 487 @current_user = user
470 488 end
471 489
472 490 def self.current
473 491 @current_user ||= User.anonymous
474 492 end
475 493
476 494 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
477 495 # one anonymous user per database.
478 496 def self.anonymous
479 497 anonymous_user = AnonymousUser.find(:first)
480 498 if anonymous_user.nil?
481 499 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
482 500 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
483 501 end
484 502 anonymous_user
485 503 end
486 504
487 505 # Salts all existing unsalted passwords
488 506 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
489 507 # This method is used in the SaltPasswords migration and is to be kept as is
490 508 def self.salt_unsalted_passwords!
491 509 transaction do
492 510 User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
493 511 next if user.hashed_password.blank?
494 512 salt = User.generate_salt
495 513 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
496 514 User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
497 515 end
498 516 end
499 517 end
500 518
501 519 protected
502 520
503 521 def validate
504 522 # Password length validation based on setting
505 523 if !password.nil? && password.size < Setting.password_min_length.to_i
506 524 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
507 525 end
508 526 end
509 527
510 528 private
511 529
512 530 # Removes references that are not handled by associations
513 531 # Things that are not deleted are reassociated with the anonymous user
514 532 def remove_references_before_destroy
515 533 return if self.id.nil?
516 534
517 535 substitute = User.anonymous
518 536 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
519 537 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
520 538 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
521 539 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
522 540 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
523 541 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
524 542 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
525 543 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
526 544 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
527 545 # Remove private queries and keep public ones
528 546 Query.delete_all ['user_id = ? AND is_public = ?', id, false]
529 547 Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
530 548 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
531 549 Token.delete_all ['user_id = ?', id]
532 550 Watcher.delete_all ['user_id = ?', id]
533 551 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
534 552 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
535 553 end
536 554
537 555 # Return password digest
538 556 def self.hash_password(clear_password)
539 557 Digest::SHA1.hexdigest(clear_password || "")
540 558 end
541 559
542 560 # Returns a 128bits random salt as a hex string (32 chars long)
543 561 def self.generate_salt
544 562 ActiveSupport::SecureRandom.hex(16)
545 563 end
546 564
547 565 end
548 566
549 567 class AnonymousUser < User
550 568
551 569 def validate_on_create
552 570 # There should be only one AnonymousUser in the database
553 571 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
554 572 end
555 573
556 574 def available_custom_fields
557 575 []
558 576 end
559 577
560 578 # Overrides a few properties
561 579 def logged?; false end
562 580 def admin; false end
563 581 def name(*args); I18n.t(:label_user_anonymous) end
564 582 def mail; nil end
565 583 def time_zone; nil end
566 584 def rss_key; nil end
567 585
568 586 # Anonymous user can not be destroyed
569 587 def destroy
570 588 false
571 589 end
572 590 end
@@ -1,798 +1,815
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 UserTest < ActiveSupport::TestCase
21 21 fixtures :users, :members, :projects, :roles, :member_roles, :auth_sources
22 22
23 23 def setup
24 24 @admin = User.find(1)
25 25 @jsmith = User.find(2)
26 26 @dlopper = User.find(3)
27 27 end
28 28
29 29 test 'object_daddy creation' do
30 30 User.generate_with_protected!(:firstname => 'Testing connection')
31 31 User.generate_with_protected!(:firstname => 'Testing connection')
32 32 assert_equal 2, User.count(:all, :conditions => {:firstname => 'Testing connection'})
33 33 end
34 34
35 35 def test_truth
36 36 assert_kind_of User, @jsmith
37 37 end
38 38
39 39 def test_mail_should_be_stripped
40 40 u = User.new
41 41 u.mail = " foo@bar.com "
42 42 assert_equal "foo@bar.com", u.mail
43 43 end
44 44
45 45 def test_create
46 46 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
47 47
48 48 user.login = "jsmith"
49 49 user.password, user.password_confirmation = "password", "password"
50 50 # login uniqueness
51 51 assert !user.save
52 52 assert_equal 1, user.errors.count
53 53
54 54 user.login = "newuser"
55 55 user.password, user.password_confirmation = "passwd", "password"
56 56 # password confirmation
57 57 assert !user.save
58 58 assert_equal 1, user.errors.count
59 59
60 60 user.password, user.password_confirmation = "password", "password"
61 61 assert user.save
62 62 end
63 63
64 64 context "User#before_create" do
65 65 should "set the mail_notification to the default Setting" do
66 66 @user1 = User.generate_with_protected!
67 67 assert_equal 'only_my_events', @user1.mail_notification
68 68
69 69 with_settings :default_notification_option => 'all' do
70 70 @user2 = User.generate_with_protected!
71 71 assert_equal 'all', @user2.mail_notification
72 72 end
73 73 end
74 74 end
75 75
76 76 context "User.login" do
77 77 should "be case-insensitive." do
78 78 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
79 79 u.login = 'newuser'
80 80 u.password, u.password_confirmation = "password", "password"
81 81 assert u.save
82 82
83 83 u = User.new(:firstname => "Similar", :lastname => "User", :mail => "similaruser@somenet.foo")
84 84 u.login = 'NewUser'
85 85 u.password, u.password_confirmation = "password", "password"
86 86 assert !u.save
87 87 assert_equal I18n.translate('activerecord.errors.messages.taken'), u.errors.on(:login)
88 88 end
89 89 end
90 90
91 91 def test_mail_uniqueness_should_not_be_case_sensitive
92 92 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
93 93 u.login = 'newuser1'
94 94 u.password, u.password_confirmation = "password", "password"
95 95 assert u.save
96 96
97 97 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
98 98 u.login = 'newuser2'
99 99 u.password, u.password_confirmation = "password", "password"
100 100 assert !u.save
101 101 assert_equal I18n.translate('activerecord.errors.messages.taken'), u.errors.on(:mail)
102 102 end
103 103
104 104 def test_update
105 105 assert_equal "admin", @admin.login
106 106 @admin.login = "john"
107 107 assert @admin.save, @admin.errors.full_messages.join("; ")
108 108 @admin.reload
109 109 assert_equal "john", @admin.login
110 110 end
111 111
112 112 def test_destroy_should_delete_members_and_roles
113 113 members = Member.find_all_by_user_id(2)
114 114 ms = members.size
115 115 rs = members.collect(&:roles).flatten.size
116 116
117 117 assert_difference 'Member.count', - ms do
118 118 assert_difference 'MemberRole.count', - rs do
119 119 User.find(2).destroy
120 120 end
121 121 end
122 122
123 123 assert_nil User.find_by_id(2)
124 124 assert Member.find_all_by_user_id(2).empty?
125 125 end
126 126
127 127 def test_destroy_should_update_attachments
128 128 attachment = Attachment.create!(:container => Project.find(1),
129 129 :file => uploaded_test_file("testfile.txt", "text/plain"),
130 130 :author_id => 2)
131 131
132 132 User.find(2).destroy
133 133 assert_nil User.find_by_id(2)
134 134 assert_equal User.anonymous, attachment.reload.author
135 135 end
136 136
137 137 def test_destroy_should_update_comments
138 138 comment = Comment.create!(
139 139 :commented => News.create!(:project_id => 1, :author_id => 1, :title => 'foo', :description => 'foo'),
140 140 :author => User.find(2),
141 141 :comments => 'foo'
142 142 )
143 143
144 144 User.find(2).destroy
145 145 assert_nil User.find_by_id(2)
146 146 assert_equal User.anonymous, comment.reload.author
147 147 end
148 148
149 149 def test_destroy_should_update_issues
150 150 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
151 151
152 152 User.find(2).destroy
153 153 assert_nil User.find_by_id(2)
154 154 assert_equal User.anonymous, issue.reload.author
155 155 end
156 156
157 157 def test_destroy_should_unassign_issues
158 158 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
159 159
160 160 User.find(2).destroy
161 161 assert_nil User.find_by_id(2)
162 162 assert_nil issue.reload.assigned_to
163 163 end
164 164
165 165 def test_destroy_should_update_journals
166 166 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
167 167 issue.init_journal(User.find(2), "update")
168 168 issue.save!
169 169
170 170 User.find(2).destroy
171 171 assert_nil User.find_by_id(2)
172 172 assert_equal User.anonymous, issue.journals.first.reload.user
173 173 end
174 174
175 175 def test_destroy_should_update_journal_details_old_value
176 176 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
177 177 issue.init_journal(User.find(1), "update")
178 178 issue.assigned_to_id = nil
179 179 assert_difference 'JournalDetail.count' do
180 180 issue.save!
181 181 end
182 182 journal_detail = JournalDetail.first(:order => 'id DESC')
183 183 assert_equal '2', journal_detail.old_value
184 184
185 185 User.find(2).destroy
186 186 assert_nil User.find_by_id(2)
187 187 assert_equal User.anonymous.id.to_s, journal_detail.reload.old_value
188 188 end
189 189
190 190 def test_destroy_should_update_journal_details_value
191 191 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
192 192 issue.init_journal(User.find(1), "update")
193 193 issue.assigned_to_id = 2
194 194 assert_difference 'JournalDetail.count' do
195 195 issue.save!
196 196 end
197 197 journal_detail = JournalDetail.first(:order => 'id DESC')
198 198 assert_equal '2', journal_detail.value
199 199
200 200 User.find(2).destroy
201 201 assert_nil User.find_by_id(2)
202 202 assert_equal User.anonymous.id.to_s, journal_detail.reload.value
203 203 end
204 204
205 205 def test_destroy_should_update_messages
206 206 board = Board.create!(:project_id => 1, :name => 'Board', :description => 'Board')
207 207 message = Message.create!(:board_id => board.id, :author_id => 2, :subject => 'foo', :content => 'foo')
208 208
209 209 User.find(2).destroy
210 210 assert_nil User.find_by_id(2)
211 211 assert_equal User.anonymous, message.reload.author
212 212 end
213 213
214 214 def test_destroy_should_update_news
215 215 news = News.create!(:project_id => 1, :author_id => 2, :title => 'foo', :description => 'foo')
216 216
217 217 User.find(2).destroy
218 218 assert_nil User.find_by_id(2)
219 219 assert_equal User.anonymous, news.reload.author
220 220 end
221 221
222 222 def test_destroy_should_delete_private_queries
223 223 query = Query.new(:name => 'foo', :is_public => false)
224 224 query.project_id = 1
225 225 query.user_id = 2
226 226 query.save!
227 227
228 228 User.find(2).destroy
229 229 assert_nil User.find_by_id(2)
230 230 assert_nil Query.find_by_id(query.id)
231 231 end
232 232
233 233 def test_destroy_should_update_public_queries
234 234 query = Query.new(:name => 'foo', :is_public => true)
235 235 query.project_id = 1
236 236 query.user_id = 2
237 237 query.save!
238 238
239 239 User.find(2).destroy
240 240 assert_nil User.find_by_id(2)
241 241 assert_equal User.anonymous, query.reload.user
242 242 end
243 243
244 244 def test_destroy_should_update_time_entries
245 245 entry = TimeEntry.new(:hours => '2', :spent_on => Date.today, :activity => TimeEntryActivity.create!(:name => 'foo'))
246 246 entry.project_id = 1
247 247 entry.user_id = 2
248 248 entry.save!
249 249
250 250 User.find(2).destroy
251 251 assert_nil User.find_by_id(2)
252 252 assert_equal User.anonymous, entry.reload.user
253 253 end
254 254
255 255 def test_destroy_should_delete_tokens
256 256 token = Token.create!(:user_id => 2, :value => 'foo')
257 257
258 258 User.find(2).destroy
259 259 assert_nil User.find_by_id(2)
260 260 assert_nil Token.find_by_id(token.id)
261 261 end
262 262
263 263 def test_destroy_should_delete_watchers
264 264 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
265 265 watcher = Watcher.create!(:user_id => 2, :watchable => issue)
266 266
267 267 User.find(2).destroy
268 268 assert_nil User.find_by_id(2)
269 269 assert_nil Watcher.find_by_id(watcher.id)
270 270 end
271 271
272 272 def test_destroy_should_update_wiki_contents
273 273 wiki_content = WikiContent.create!(
274 274 :text => 'foo',
275 275 :author_id => 2,
276 276 :page => WikiPage.create!(:title => 'Foo', :wiki => Wiki.create!(:project_id => 1, :start_page => 'Start'))
277 277 )
278 278 wiki_content.text = 'bar'
279 279 assert_difference 'WikiContent::Version.count' do
280 280 wiki_content.save!
281 281 end
282 282
283 283 User.find(2).destroy
284 284 assert_nil User.find_by_id(2)
285 285 assert_equal User.anonymous, wiki_content.reload.author
286 286 wiki_content.versions.each do |version|
287 287 assert_equal User.anonymous, version.reload.author
288 288 end
289 289 end
290 290
291 291 def test_destroy_should_nullify_issue_categories
292 292 category = IssueCategory.create!(:project_id => 1, :assigned_to_id => 2, :name => 'foo')
293 293
294 294 User.find(2).destroy
295 295 assert_nil User.find_by_id(2)
296 296 assert_nil category.reload.assigned_to_id
297 297 end
298 298
299 299 def test_destroy_should_nullify_changesets
300 300 changeset = Changeset.create!(
301 301 :repository => Repository::Subversion.create!(
302 302 :project_id => 1,
303 303 :url => 'file:///var/svn'
304 304 ),
305 305 :revision => '12',
306 306 :committed_on => Time.now,
307 307 :committer => 'jsmith'
308 308 )
309 309 assert_equal 2, changeset.user_id
310 310
311 311 User.find(2).destroy
312 312 assert_nil User.find_by_id(2)
313 313 assert_nil changeset.reload.user_id
314 314 end
315 315
316 316 def test_anonymous_user_should_not_be_destroyable
317 317 assert_no_difference 'User.count' do
318 318 assert_equal false, User.anonymous.destroy
319 319 end
320 320 end
321 321
322 322 def test_validate_login_presence
323 323 @admin.login = ""
324 324 assert !@admin.save
325 325 assert_equal 1, @admin.errors.count
326 326 end
327 327
328 328 def test_validate_mail_notification_inclusion
329 329 u = User.new
330 330 u.mail_notification = 'foo'
331 331 u.save
332 332 assert_not_nil u.errors.on(:mail_notification)
333 333 end
334 334
335 335 context "User#try_to_login" do
336 336 should "fall-back to case-insensitive if user login is not found as-typed." do
337 337 user = User.try_to_login("AdMin", "admin")
338 338 assert_kind_of User, user
339 339 assert_equal "admin", user.login
340 340 end
341 341
342 342 should "select the exact matching user first" do
343 343 case_sensitive_user = User.generate_with_protected!(:login => 'changed', :password => 'admin', :password_confirmation => 'admin')
344 344 # bypass validations to make it appear like existing data
345 345 case_sensitive_user.update_attribute(:login, 'ADMIN')
346 346
347 347 user = User.try_to_login("ADMIN", "admin")
348 348 assert_kind_of User, user
349 349 assert_equal "ADMIN", user.login
350 350
351 351 end
352 352 end
353 353
354 354 def test_password
355 355 user = User.try_to_login("admin", "admin")
356 356 assert_kind_of User, user
357 357 assert_equal "admin", user.login
358 358 user.password = "hello"
359 359 assert user.save
360 360
361 361 user = User.try_to_login("admin", "hello")
362 362 assert_kind_of User, user
363 363 assert_equal "admin", user.login
364 364 end
365 365
366 366 def test_name_format
367 367 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
368 368 Setting.user_format = :firstname_lastname
369 369 assert_equal 'John Smith', @jsmith.reload.name
370 370 Setting.user_format = :username
371 371 assert_equal 'jsmith', @jsmith.reload.name
372 372 end
373 373
374 374 def test_lock
375 375 user = User.try_to_login("jsmith", "jsmith")
376 376 assert_equal @jsmith, user
377 377
378 378 @jsmith.status = User::STATUS_LOCKED
379 379 assert @jsmith.save
380 380
381 381 user = User.try_to_login("jsmith", "jsmith")
382 382 assert_equal nil, user
383 383 end
384 384
385 385 context ".try_to_login" do
386 386 context "with good credentials" do
387 387 should "return the user" do
388 388 user = User.try_to_login("admin", "admin")
389 389 assert_kind_of User, user
390 390 assert_equal "admin", user.login
391 391 end
392 392 end
393 393
394 394 context "with wrong credentials" do
395 395 should "return nil" do
396 396 assert_nil User.try_to_login("admin", "foo")
397 397 end
398 398 end
399 399 end
400 400
401 401 if ldap_configured?
402 402 context "#try_to_login using LDAP" do
403 403 context "with failed connection to the LDAP server" do
404 404 should "return nil" do
405 405 @auth_source = AuthSourceLdap.find(1)
406 406 AuthSource.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError, 'Cannot connect')
407 407
408 408 assert_equal nil, User.try_to_login('edavis', 'wrong')
409 409 end
410 410 end
411 411
412 412 context "with an unsuccessful authentication" do
413 413 should "return nil" do
414 414 assert_equal nil, User.try_to_login('edavis', 'wrong')
415 415 end
416 416 end
417 417
418 418 context "on the fly registration" do
419 419 setup do
420 420 @auth_source = AuthSourceLdap.find(1)
421 421 end
422 422
423 423 context "with a successful authentication" do
424 424 should "create a new user account if it doesn't exist" do
425 425 assert_difference('User.count') do
426 426 user = User.try_to_login('edavis', '123456')
427 427 assert !user.admin?
428 428 end
429 429 end
430 430
431 431 should "retrieve existing user" do
432 432 user = User.try_to_login('edavis', '123456')
433 433 user.admin = true
434 434 user.save!
435 435
436 436 assert_no_difference('User.count') do
437 437 user = User.try_to_login('edavis', '123456')
438 438 assert user.admin?
439 439 end
440 440 end
441 441 end
442 442 end
443 443 end
444 444
445 445 else
446 446 puts "Skipping LDAP tests."
447 447 end
448 448
449 449 def test_create_anonymous
450 450 AnonymousUser.delete_all
451 451 anon = User.anonymous
452 452 assert !anon.new_record?
453 453 assert_kind_of AnonymousUser, anon
454 454 end
455 455
456 456 should_have_one :rss_token
457 457
458 458 def test_rss_key
459 459 assert_nil @jsmith.rss_token
460 460 key = @jsmith.rss_key
461 461 assert_equal 40, key.length
462 462
463 463 @jsmith.reload
464 464 assert_equal key, @jsmith.rss_key
465 465 end
466 466
467 467
468 468 should_have_one :api_token
469 469
470 470 context "User#api_key" do
471 471 should "generate a new one if the user doesn't have one" do
472 472 user = User.generate_with_protected!(:api_token => nil)
473 473 assert_nil user.api_token
474 474
475 475 key = user.api_key
476 476 assert_equal 40, key.length
477 477 user.reload
478 478 assert_equal key, user.api_key
479 479 end
480 480
481 481 should "return the existing api token value" do
482 482 user = User.generate_with_protected!
483 483 token = Token.generate!(:action => 'api')
484 484 user.api_token = token
485 485 assert user.save
486 486
487 487 assert_equal token.value, user.api_key
488 488 end
489 489 end
490 490
491 491 context "User#find_by_api_key" do
492 492 should "return nil if no matching key is found" do
493 493 assert_nil User.find_by_api_key('zzzzzzzzz')
494 494 end
495 495
496 496 should "return nil if the key is found for an inactive user" do
497 497 user = User.generate_with_protected!(:status => User::STATUS_LOCKED)
498 498 token = Token.generate!(:action => 'api')
499 499 user.api_token = token
500 500 user.save
501 501
502 502 assert_nil User.find_by_api_key(token.value)
503 503 end
504 504
505 505 should "return the user if the key is found for an active user" do
506 506 user = User.generate_with_protected!(:status => User::STATUS_ACTIVE)
507 507 token = Token.generate!(:action => 'api')
508 508 user.api_token = token
509 509 user.save
510 510
511 511 assert_equal user, User.find_by_api_key(token.value)
512 512 end
513 513 end
514 514
515 515 def test_roles_for_project
516 516 # user with a role
517 517 roles = @jsmith.roles_for_project(Project.find(1))
518 518 assert_kind_of Role, roles.first
519 519 assert_equal "Manager", roles.first.name
520 520
521 521 # user with no role
522 522 assert_nil @dlopper.roles_for_project(Project.find(2)).detect {|role| role.member?}
523 523 end
524 524
525 def test_projects_by_role_for_user_with_role
526 user = User.find(2)
527 assert_kind_of Hash, user.projects_by_role
528 assert_equal 2, user.projects_by_role.size
529 assert_equal [1,5], user.projects_by_role[Role.find(1)].collect(&:id).sort
530 assert_equal [2], user.projects_by_role[Role.find(2)].collect(&:id).sort
531 end
532
533 def test_projects_by_role_for_user_with_no_role
534 user = User.generate!
535 assert_equal({}, user.projects_by_role)
536 end
537
538 def test_projects_by_role_for_anonymous
539 assert_equal({}, User.anonymous.projects_by_role)
540 end
541
525 542 def test_valid_notification_options
526 543 # without memberships
527 544 assert_equal 5, User.find(7).valid_notification_options.size
528 545 # with memberships
529 546 assert_equal 6, User.find(2).valid_notification_options.size
530 547 end
531 548
532 549 def test_valid_notification_options_class_method
533 550 assert_equal 5, User.valid_notification_options.size
534 551 assert_equal 5, User.valid_notification_options(User.find(7)).size
535 552 assert_equal 6, User.valid_notification_options(User.find(2)).size
536 553 end
537 554
538 555 def test_mail_notification_all
539 556 @jsmith.mail_notification = 'all'
540 557 @jsmith.notified_project_ids = []
541 558 @jsmith.save
542 559 @jsmith.reload
543 560 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
544 561 end
545 562
546 563 def test_mail_notification_selected
547 564 @jsmith.mail_notification = 'selected'
548 565 @jsmith.notified_project_ids = [1]
549 566 @jsmith.save
550 567 @jsmith.reload
551 568 assert Project.find(1).recipients.include?(@jsmith.mail)
552 569 end
553 570
554 571 def test_mail_notification_only_my_events
555 572 @jsmith.mail_notification = 'only_my_events'
556 573 @jsmith.notified_project_ids = []
557 574 @jsmith.save
558 575 @jsmith.reload
559 576 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
560 577 end
561 578
562 579 def test_comments_sorting_preference
563 580 assert !@jsmith.wants_comments_in_reverse_order?
564 581 @jsmith.pref.comments_sorting = 'asc'
565 582 assert !@jsmith.wants_comments_in_reverse_order?
566 583 @jsmith.pref.comments_sorting = 'desc'
567 584 assert @jsmith.wants_comments_in_reverse_order?
568 585 end
569 586
570 587 def test_find_by_mail_should_be_case_insensitive
571 588 u = User.find_by_mail('JSmith@somenet.foo')
572 589 assert_not_nil u
573 590 assert_equal 'jsmith@somenet.foo', u.mail
574 591 end
575 592
576 593 def test_random_password
577 594 u = User.new
578 595 u.random_password
579 596 assert !u.password.blank?
580 597 assert !u.password_confirmation.blank?
581 598 end
582 599
583 600 context "#change_password_allowed?" do
584 601 should "be allowed if no auth source is set" do
585 602 user = User.generate_with_protected!
586 603 assert user.change_password_allowed?
587 604 end
588 605
589 606 should "delegate to the auth source" do
590 607 user = User.generate_with_protected!
591 608
592 609 allowed_auth_source = AuthSource.generate!
593 610 def allowed_auth_source.allow_password_changes?; true; end
594 611
595 612 denied_auth_source = AuthSource.generate!
596 613 def denied_auth_source.allow_password_changes?; false; end
597 614
598 615 assert user.change_password_allowed?
599 616
600 617 user.auth_source = allowed_auth_source
601 618 assert user.change_password_allowed?, "User not allowed to change password, though auth source does"
602 619
603 620 user.auth_source = denied_auth_source
604 621 assert !user.change_password_allowed?, "User allowed to change password, though auth source does not"
605 622 end
606 623
607 624 end
608 625
609 626 context "#allowed_to?" do
610 627 context "with a unique project" do
611 628 should "return false if project is archived" do
612 629 project = Project.find(1)
613 630 Project.any_instance.stubs(:status).returns(Project::STATUS_ARCHIVED)
614 631 assert ! @admin.allowed_to?(:view_issues, Project.find(1))
615 632 end
616 633
617 634 should "return false if related module is disabled" do
618 635 project = Project.find(1)
619 636 project.enabled_module_names = ["issue_tracking"]
620 637 assert @admin.allowed_to?(:add_issues, project)
621 638 assert ! @admin.allowed_to?(:view_wiki_pages, project)
622 639 end
623 640
624 641 should "authorize nearly everything for admin users" do
625 642 project = Project.find(1)
626 643 assert ! @admin.member_of?(project)
627 644 %w(edit_issues delete_issues manage_news manage_documents manage_wiki).each do |p|
628 645 assert @admin.allowed_to?(p.to_sym, project)
629 646 end
630 647 end
631 648
632 649 should "authorize normal users depending on their roles" do
633 650 project = Project.find(1)
634 651 assert @jsmith.allowed_to?(:delete_messages, project) #Manager
635 652 assert ! @dlopper.allowed_to?(:delete_messages, project) #Developper
636 653 end
637 654 end
638 655
639 656 context "with multiple projects" do
640 657 should "return false if array is empty" do
641 658 assert ! @admin.allowed_to?(:view_project, [])
642 659 end
643 660
644 661 should "return true only if user has permission on all these projects" do
645 662 assert @admin.allowed_to?(:view_project, Project.all)
646 663 assert ! @dlopper.allowed_to?(:view_project, Project.all) #cannot see Project(2)
647 664 assert @jsmith.allowed_to?(:edit_issues, @jsmith.projects) #Manager or Developer everywhere
648 665 assert ! @jsmith.allowed_to?(:delete_issue_watchers, @jsmith.projects) #Dev cannot delete_issue_watchers
649 666 end
650 667
651 668 should "behave correctly with arrays of 1 project" do
652 669 assert ! User.anonymous.allowed_to?(:delete_issues, [Project.first])
653 670 end
654 671 end
655 672
656 673 context "with options[:global]" do
657 674 should "authorize if user has at least one role that has this permission" do
658 675 @dlopper2 = User.find(5) #only Developper on a project, not Manager anywhere
659 676 @anonymous = User.find(6)
660 677 assert @jsmith.allowed_to?(:delete_issue_watchers, nil, :global => true)
661 678 assert ! @dlopper2.allowed_to?(:delete_issue_watchers, nil, :global => true)
662 679 assert @dlopper2.allowed_to?(:add_issues, nil, :global => true)
663 680 assert ! @anonymous.allowed_to?(:add_issues, nil, :global => true)
664 681 assert @anonymous.allowed_to?(:view_issues, nil, :global => true)
665 682 end
666 683 end
667 684 end
668 685
669 686 context "User#notify_about?" do
670 687 context "Issues" do
671 688 setup do
672 689 @project = Project.find(1)
673 690 @author = User.generate_with_protected!
674 691 @assignee = User.generate_with_protected!
675 692 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
676 693 end
677 694
678 695 should "be true for a user with :all" do
679 696 @author.update_attribute(:mail_notification, 'all')
680 697 assert @author.notify_about?(@issue)
681 698 end
682 699
683 700 should "be false for a user with :none" do
684 701 @author.update_attribute(:mail_notification, 'none')
685 702 assert ! @author.notify_about?(@issue)
686 703 end
687 704
688 705 should "be false for a user with :only_my_events and isn't an author, creator, or assignee" do
689 706 @user = User.generate_with_protected!(:mail_notification => 'only_my_events')
690 707 Member.create!(:user => @user, :project => @project, :role_ids => [1])
691 708 assert ! @user.notify_about?(@issue)
692 709 end
693 710
694 711 should "be true for a user with :only_my_events and is the author" do
695 712 @author.update_attribute(:mail_notification, 'only_my_events')
696 713 assert @author.notify_about?(@issue)
697 714 end
698 715
699 716 should "be true for a user with :only_my_events and is the assignee" do
700 717 @assignee.update_attribute(:mail_notification, 'only_my_events')
701 718 assert @assignee.notify_about?(@issue)
702 719 end
703 720
704 721 should "be true for a user with :only_assigned and is the assignee" do
705 722 @assignee.update_attribute(:mail_notification, 'only_assigned')
706 723 assert @assignee.notify_about?(@issue)
707 724 end
708 725
709 726 should "be false for a user with :only_assigned and is not the assignee" do
710 727 @author.update_attribute(:mail_notification, 'only_assigned')
711 728 assert ! @author.notify_about?(@issue)
712 729 end
713 730
714 731 should "be true for a user with :only_owner and is the author" do
715 732 @author.update_attribute(:mail_notification, 'only_owner')
716 733 assert @author.notify_about?(@issue)
717 734 end
718 735
719 736 should "be false for a user with :only_owner and is not the author" do
720 737 @assignee.update_attribute(:mail_notification, 'only_owner')
721 738 assert ! @assignee.notify_about?(@issue)
722 739 end
723 740
724 741 should "be true for a user with :selected and is the author" do
725 742 @author.update_attribute(:mail_notification, 'selected')
726 743 assert @author.notify_about?(@issue)
727 744 end
728 745
729 746 should "be true for a user with :selected and is the assignee" do
730 747 @assignee.update_attribute(:mail_notification, 'selected')
731 748 assert @assignee.notify_about?(@issue)
732 749 end
733 750
734 751 should "be false for a user with :selected and is not the author or assignee" do
735 752 @user = User.generate_with_protected!(:mail_notification => 'selected')
736 753 Member.create!(:user => @user, :project => @project, :role_ids => [1])
737 754 assert ! @user.notify_about?(@issue)
738 755 end
739 756 end
740 757
741 758 context "other events" do
742 759 should 'be added and tested'
743 760 end
744 761 end
745 762
746 763 def test_salt_unsalted_passwords
747 764 # Restore a user with an unsalted password
748 765 user = User.find(1)
749 766 user.salt = nil
750 767 user.hashed_password = User.hash_password("unsalted")
751 768 user.save!
752 769
753 770 User.salt_unsalted_passwords!
754 771
755 772 user.reload
756 773 # Salt added
757 774 assert !user.salt.blank?
758 775 # Password still valid
759 776 assert user.check_password?("unsalted")
760 777 assert_equal user, User.try_to_login(user.login, "unsalted")
761 778 end
762 779
763 780 if Object.const_defined?(:OpenID)
764 781
765 782 def test_setting_identity_url
766 783 normalized_open_id_url = 'http://example.com/'
767 784 u = User.new( :identity_url => 'http://example.com/' )
768 785 assert_equal normalized_open_id_url, u.identity_url
769 786 end
770 787
771 788 def test_setting_identity_url_without_trailing_slash
772 789 normalized_open_id_url = 'http://example.com/'
773 790 u = User.new( :identity_url => 'http://example.com' )
774 791 assert_equal normalized_open_id_url, u.identity_url
775 792 end
776 793
777 794 def test_setting_identity_url_without_protocol
778 795 normalized_open_id_url = 'http://example.com/'
779 796 u = User.new( :identity_url => 'example.com' )
780 797 assert_equal normalized_open_id_url, u.identity_url
781 798 end
782 799
783 800 def test_setting_blank_identity_url
784 801 u = User.new( :identity_url => 'example.com' )
785 802 u.identity_url = ''
786 803 assert u.identity_url.blank?
787 804 end
788 805
789 806 def test_setting_invalid_identity_url
790 807 u = User.new( :identity_url => 'this is not an openid url' )
791 808 assert u.identity_url.blank?
792 809 end
793 810
794 811 else
795 812 puts "Skipping openid tests."
796 813 end
797 814
798 815 end
General Comments 0
You need to be logged in to leave comments. Login now