##// END OF EJS Templates
Adds User#builtin_role....
Jean-Philippe Lang -
r11735:e978b3ace0e8
parent child
Show More
@@ -1,1044 +1,1044
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 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_CLOSED = 5
24 24 STATUS_ARCHIVED = 9
25 25
26 26 # Maximum length for project identifiers
27 27 IDENTIFIER_MAX_LENGTH = 100
28 28
29 29 # Specific overidden Activities
30 30 has_many :time_entry_activities
31 31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
32 32 has_many :memberships, :class_name => 'Member'
33 33 has_many :member_principals, :class_name => 'Member',
34 34 :include => :principal,
35 35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
36 36
37 37 has_many :enabled_modules, :dependent => :delete_all
38 38 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
39 39 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
40 40 has_many :issue_changes, :through => :issues, :source => :journals
41 41 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
42 42 has_many :time_entries, :dependent => :delete_all
43 43 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
44 44 has_many :documents, :dependent => :destroy
45 45 has_many :news, :dependent => :destroy, :include => :author
46 46 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
47 47 has_many :boards, :dependent => :destroy, :order => "position ASC"
48 48 has_one :repository, :conditions => ["is_default = ?", true]
49 49 has_many :repositories, :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', :dependent => :destroy
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 => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
79 79 # reserved words
80 80 validates_exclusion_of :identifier, :in => %w( new )
81 81
82 82 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
83 83 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
84 84 before_destroy :delete_all_members
85 85
86 86 scope :has_module, lambda {|mod|
87 87 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
88 88 }
89 89 scope :active, lambda { where(:status => STATUS_ACTIVE) }
90 90 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
91 91 scope :all_public, lambda { where(:is_public => true) }
92 92 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
93 93 scope :allowed_to, lambda {|*args|
94 94 user = User.current
95 95 permission = nil
96 96 if args.first.is_a?(Symbol)
97 97 permission = args.shift
98 98 else
99 99 user = args.shift
100 100 permission = args.shift
101 101 end
102 102 where(Project.allowed_to_condition(user, permission, *args))
103 103 }
104 104 scope :like, lambda {|arg|
105 105 if arg.blank?
106 106 where(nil)
107 107 else
108 108 pattern = "%#{arg.to_s.strip.downcase}%"
109 109 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
110 110 end
111 111 }
112 112
113 113 def initialize(attributes=nil, *args)
114 114 super
115 115
116 116 initialized = (attributes || {}).stringify_keys
117 117 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
118 118 self.identifier = Project.next_identifier
119 119 end
120 120 if !initialized.key?('is_public')
121 121 self.is_public = Setting.default_projects_public?
122 122 end
123 123 if !initialized.key?('enabled_module_names')
124 124 self.enabled_module_names = Setting.default_projects_modules
125 125 end
126 126 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
127 127 default = Setting.default_projects_tracker_ids
128 128 if default.is_a?(Array)
129 129 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.all
130 130 else
131 131 self.trackers = Tracker.sorted.all
132 132 end
133 133 end
134 134 end
135 135
136 136 def identifier=(identifier)
137 137 super unless identifier_frozen?
138 138 end
139 139
140 140 def identifier_frozen?
141 141 errors[:identifier].blank? && !(new_record? || identifier.blank?)
142 142 end
143 143
144 144 # returns latest created projects
145 145 # non public projects will be returned only if user is a member of those
146 146 def self.latest(user=nil, count=5)
147 147 visible(user).limit(count).order("created_on DESC").all
148 148 end
149 149
150 150 # Returns true if the project is visible to +user+ or to the current user.
151 151 def visible?(user=User.current)
152 152 user.allowed_to?(:view_project, self)
153 153 end
154 154
155 155 # Returns a SQL conditions string used to find all projects visible by the specified user.
156 156 #
157 157 # Examples:
158 158 # Project.visible_condition(admin) => "projects.status = 1"
159 159 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
160 160 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
161 161 def self.visible_condition(user, options={})
162 162 allowed_to_condition(user, :view_project, options)
163 163 end
164 164
165 165 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
166 166 #
167 167 # Valid options:
168 168 # * :project => limit the condition to project
169 169 # * :with_subprojects => limit the condition to project and its subprojects
170 170 # * :member => limit the condition to the user projects
171 171 def self.allowed_to_condition(user, permission, options={})
172 172 perm = Redmine::AccessControl.permission(permission)
173 173 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
174 174 if perm && perm.project_module
175 175 # If the permission belongs to a project module, make sure the module is enabled
176 176 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
177 177 end
178 178 if options[:project]
179 179 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
180 180 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
181 181 base_statement = "(#{project_statement}) AND (#{base_statement})"
182 182 end
183 183
184 184 if user.admin?
185 185 base_statement
186 186 else
187 187 statement_by_role = {}
188 188 unless options[:member]
189 role = user.logged? ? Role.non_member : Role.anonymous
189 role = user.builtin_role
190 190 if role.allowed_to?(permission)
191 191 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
192 192 end
193 193 end
194 194 if user.logged?
195 195 user.projects_by_role.each do |role, projects|
196 196 if role.allowed_to?(permission) && projects.any?
197 197 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
198 198 end
199 199 end
200 200 end
201 201 if statement_by_role.empty?
202 202 "1=0"
203 203 else
204 204 if block_given?
205 205 statement_by_role.each do |role, statement|
206 206 if s = yield(role, user)
207 207 statement_by_role[role] = "(#{statement} AND (#{s}))"
208 208 end
209 209 end
210 210 end
211 211 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
212 212 end
213 213 end
214 214 end
215 215
216 216 def principals
217 217 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
218 218 end
219 219
220 220 def users
221 221 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
222 222 end
223 223
224 224 # Returns the Systemwide and project specific activities
225 225 def activities(include_inactive=false)
226 226 if include_inactive
227 227 return all_activities
228 228 else
229 229 return active_activities
230 230 end
231 231 end
232 232
233 233 # Will create a new Project specific Activity or update an existing one
234 234 #
235 235 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
236 236 # does not successfully save.
237 237 def update_or_create_time_entry_activity(id, activity_hash)
238 238 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
239 239 self.create_time_entry_activity_if_needed(activity_hash)
240 240 else
241 241 activity = project.time_entry_activities.find_by_id(id.to_i)
242 242 activity.update_attributes(activity_hash) if activity
243 243 end
244 244 end
245 245
246 246 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
247 247 #
248 248 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
249 249 # does not successfully save.
250 250 def create_time_entry_activity_if_needed(activity)
251 251 if activity['parent_id']
252 252
253 253 parent_activity = TimeEntryActivity.find(activity['parent_id'])
254 254 activity['name'] = parent_activity.name
255 255 activity['position'] = parent_activity.position
256 256
257 257 if Enumeration.overridding_change?(activity, parent_activity)
258 258 project_activity = self.time_entry_activities.create(activity)
259 259
260 260 if project_activity.new_record?
261 261 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
262 262 else
263 263 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
264 264 end
265 265 end
266 266 end
267 267 end
268 268
269 269 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
270 270 #
271 271 # Examples:
272 272 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
273 273 # project.project_condition(false) => "projects.id = 1"
274 274 def project_condition(with_subprojects)
275 275 cond = "#{Project.table_name}.id = #{id}"
276 276 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
277 277 cond
278 278 end
279 279
280 280 def self.find(*args)
281 281 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
282 282 project = find_by_identifier(*args)
283 283 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
284 284 project
285 285 else
286 286 super
287 287 end
288 288 end
289 289
290 290 def self.find_by_param(*args)
291 291 self.find(*args)
292 292 end
293 293
294 294 alias :base_reload :reload
295 295 def reload(*args)
296 296 @principals = nil
297 297 @users = nil
298 298 @shared_versions = nil
299 299 @rolled_up_versions = nil
300 300 @rolled_up_trackers = nil
301 301 @all_issue_custom_fields = nil
302 302 @all_time_entry_custom_fields = nil
303 303 @to_param = nil
304 304 @allowed_parents = nil
305 305 @allowed_permissions = nil
306 306 @actions_allowed = nil
307 307 @start_date = nil
308 308 @due_date = nil
309 309 base_reload(*args)
310 310 end
311 311
312 312 def to_param
313 313 # id is used for projects with a numeric identifier (compatibility)
314 314 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
315 315 end
316 316
317 317 def active?
318 318 self.status == STATUS_ACTIVE
319 319 end
320 320
321 321 def archived?
322 322 self.status == STATUS_ARCHIVED
323 323 end
324 324
325 325 # Archives the project and its descendants
326 326 def archive
327 327 # Check that there is no issue of a non descendant project that is assigned
328 328 # to one of the project or descendant versions
329 329 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
330 330 if v_ids.any? &&
331 331 Issue.
332 332 includes(:project).
333 333 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
334 334 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
335 335 exists?
336 336 return false
337 337 end
338 338 Project.transaction do
339 339 archive!
340 340 end
341 341 true
342 342 end
343 343
344 344 # Unarchives the project
345 345 # All its ancestors must be active
346 346 def unarchive
347 347 return false if ancestors.detect {|a| !a.active?}
348 348 update_attribute :status, STATUS_ACTIVE
349 349 end
350 350
351 351 def close
352 352 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
353 353 end
354 354
355 355 def reopen
356 356 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
357 357 end
358 358
359 359 # Returns an array of projects the project can be moved to
360 360 # by the current user
361 361 def allowed_parents
362 362 return @allowed_parents if @allowed_parents
363 363 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
364 364 @allowed_parents = @allowed_parents - self_and_descendants
365 365 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
366 366 @allowed_parents << nil
367 367 end
368 368 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
369 369 @allowed_parents << parent
370 370 end
371 371 @allowed_parents
372 372 end
373 373
374 374 # Sets the parent of the project with authorization check
375 375 def set_allowed_parent!(p)
376 376 unless p.nil? || p.is_a?(Project)
377 377 if p.to_s.blank?
378 378 p = nil
379 379 else
380 380 p = Project.find_by_id(p)
381 381 return false unless p
382 382 end
383 383 end
384 384 if p.nil?
385 385 if !new_record? && allowed_parents.empty?
386 386 return false
387 387 end
388 388 elsif !allowed_parents.include?(p)
389 389 return false
390 390 end
391 391 set_parent!(p)
392 392 end
393 393
394 394 # Sets the parent of the project
395 395 # Argument can be either a Project, a String, a Fixnum or nil
396 396 def set_parent!(p)
397 397 unless p.nil? || p.is_a?(Project)
398 398 if p.to_s.blank?
399 399 p = nil
400 400 else
401 401 p = Project.find_by_id(p)
402 402 return false unless p
403 403 end
404 404 end
405 405 if p == parent && !p.nil?
406 406 # Nothing to do
407 407 true
408 408 elsif p.nil? || (p.active? && move_possible?(p))
409 409 set_or_update_position_under(p)
410 410 Issue.update_versions_from_hierarchy_change(self)
411 411 true
412 412 else
413 413 # Can not move to the given target
414 414 false
415 415 end
416 416 end
417 417
418 418 # Recalculates all lft and rgt values based on project names
419 419 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
420 420 # Used in BuildProjectsTree migration
421 421 def self.rebuild_tree!
422 422 transaction do
423 423 update_all "lft = NULL, rgt = NULL"
424 424 rebuild!(false)
425 425 end
426 426 end
427 427
428 428 # Returns an array of the trackers used by the project and its active sub projects
429 429 def rolled_up_trackers
430 430 @rolled_up_trackers ||=
431 431 Tracker.
432 432 joins(:projects).
433 433 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
434 434 select("DISTINCT #{Tracker.table_name}.*").
435 435 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
436 436 sorted.
437 437 all
438 438 end
439 439
440 440 # Closes open and locked project versions that are completed
441 441 def close_completed_versions
442 442 Version.transaction do
443 443 versions.where(:status => %w(open locked)).all.each do |version|
444 444 if version.completed?
445 445 version.update_attribute(:status, 'closed')
446 446 end
447 447 end
448 448 end
449 449 end
450 450
451 451 # Returns a scope of the Versions on subprojects
452 452 def rolled_up_versions
453 453 @rolled_up_versions ||=
454 454 Version.
455 455 includes(:project).
456 456 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
457 457 end
458 458
459 459 # Returns a scope of the Versions used by the project
460 460 def shared_versions
461 461 if new_record?
462 462 Version.
463 463 includes(:project).
464 464 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
465 465 else
466 466 @shared_versions ||= begin
467 467 r = root? ? self : root
468 468 Version.
469 469 includes(:project).
470 470 where("#{Project.table_name}.id = #{id}" +
471 471 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
472 472 " #{Version.table_name}.sharing = 'system'" +
473 473 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
474 474 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
475 475 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
476 476 "))")
477 477 end
478 478 end
479 479 end
480 480
481 481 # Returns a hash of project users grouped by role
482 482 def users_by_role
483 483 members.includes(:user, :roles).all.inject({}) do |h, m|
484 484 m.roles.each do |r|
485 485 h[r] ||= []
486 486 h[r] << m.user
487 487 end
488 488 h
489 489 end
490 490 end
491 491
492 492 # Deletes all project's members
493 493 def delete_all_members
494 494 me, mr = Member.table_name, MemberRole.table_name
495 495 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
496 496 Member.delete_all(['project_id = ?', id])
497 497 end
498 498
499 499 # Users/groups issues can be assigned to
500 500 def assignable_users
501 501 assignable = Setting.issue_group_assignment? ? member_principals : members
502 502 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
503 503 end
504 504
505 505 # Returns the mail adresses of users that should be always notified on project events
506 506 def recipients
507 507 notified_users.collect {|user| user.mail}
508 508 end
509 509
510 510 # Returns the users that should be notified on project events
511 511 def notified_users
512 512 # TODO: User part should be extracted to User#notify_about?
513 513 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
514 514 end
515 515
516 516 # Returns a scope of all custom fields enabled for project issues
517 517 # (explictly associated custom fields and custom fields enabled for all projects)
518 518 def all_issue_custom_fields
519 519 @all_issue_custom_fields ||= IssueCustomField.
520 520 sorted.
521 521 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
522 522 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
523 523 " WHERE cfp.project_id = ?)", true, id)
524 524 end
525 525
526 526 # Returns an array of all custom fields enabled for project time entries
527 527 # (explictly associated custom fields and custom fields enabled for all projects)
528 528 def all_time_entry_custom_fields
529 529 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
530 530 end
531 531
532 532 def project
533 533 self
534 534 end
535 535
536 536 def <=>(project)
537 537 name.downcase <=> project.name.downcase
538 538 end
539 539
540 540 def to_s
541 541 name
542 542 end
543 543
544 544 # Returns a short description of the projects (first lines)
545 545 def short_description(length = 255)
546 546 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
547 547 end
548 548
549 549 def css_classes
550 550 s = 'project'
551 551 s << ' root' if root?
552 552 s << ' child' if child?
553 553 s << (leaf? ? ' leaf' : ' parent')
554 554 unless active?
555 555 if archived?
556 556 s << ' archived'
557 557 else
558 558 s << ' closed'
559 559 end
560 560 end
561 561 s
562 562 end
563 563
564 564 # The earliest start date of a project, based on it's issues and versions
565 565 def start_date
566 566 @start_date ||= [
567 567 issues.minimum('start_date'),
568 568 shared_versions.minimum('effective_date'),
569 569 Issue.fixed_version(shared_versions).minimum('start_date')
570 570 ].compact.min
571 571 end
572 572
573 573 # The latest due date of an issue or version
574 574 def due_date
575 575 @due_date ||= [
576 576 issues.maximum('due_date'),
577 577 shared_versions.maximum('effective_date'),
578 578 Issue.fixed_version(shared_versions).maximum('due_date')
579 579 ].compact.max
580 580 end
581 581
582 582 def overdue?
583 583 active? && !due_date.nil? && (due_date < Date.today)
584 584 end
585 585
586 586 # Returns the percent completed for this project, based on the
587 587 # progress on it's versions.
588 588 def completed_percent(options={:include_subprojects => false})
589 589 if options.delete(:include_subprojects)
590 590 total = self_and_descendants.collect(&:completed_percent).sum
591 591
592 592 total / self_and_descendants.count
593 593 else
594 594 if versions.count > 0
595 595 total = versions.collect(&:completed_percent).sum
596 596
597 597 total / versions.count
598 598 else
599 599 100
600 600 end
601 601 end
602 602 end
603 603
604 604 # Return true if this project allows to do the specified action.
605 605 # action can be:
606 606 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
607 607 # * a permission Symbol (eg. :edit_project)
608 608 def allows_to?(action)
609 609 if archived?
610 610 # No action allowed on archived projects
611 611 return false
612 612 end
613 613 unless active? || Redmine::AccessControl.read_action?(action)
614 614 # No write action allowed on closed projects
615 615 return false
616 616 end
617 617 # No action allowed on disabled modules
618 618 if action.is_a? Hash
619 619 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
620 620 else
621 621 allowed_permissions.include? action
622 622 end
623 623 end
624 624
625 625 def module_enabled?(module_name)
626 626 module_name = module_name.to_s
627 627 enabled_modules.detect {|m| m.name == module_name}
628 628 end
629 629
630 630 def enabled_module_names=(module_names)
631 631 if module_names && module_names.is_a?(Array)
632 632 module_names = module_names.collect(&:to_s).reject(&:blank?)
633 633 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
634 634 else
635 635 enabled_modules.clear
636 636 end
637 637 end
638 638
639 639 # Returns an array of the enabled modules names
640 640 def enabled_module_names
641 641 enabled_modules.collect(&:name)
642 642 end
643 643
644 644 # Enable a specific module
645 645 #
646 646 # Examples:
647 647 # project.enable_module!(:issue_tracking)
648 648 # project.enable_module!("issue_tracking")
649 649 def enable_module!(name)
650 650 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
651 651 end
652 652
653 653 # Disable a module if it exists
654 654 #
655 655 # Examples:
656 656 # project.disable_module!(:issue_tracking)
657 657 # project.disable_module!("issue_tracking")
658 658 # project.disable_module!(project.enabled_modules.first)
659 659 def disable_module!(target)
660 660 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
661 661 target.destroy unless target.blank?
662 662 end
663 663
664 664 safe_attributes 'name',
665 665 'description',
666 666 'homepage',
667 667 'is_public',
668 668 'identifier',
669 669 'custom_field_values',
670 670 'custom_fields',
671 671 'tracker_ids',
672 672 'issue_custom_field_ids'
673 673
674 674 safe_attributes 'enabled_module_names',
675 675 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
676 676
677 677 safe_attributes 'inherit_members',
678 678 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
679 679
680 680 # Returns an array of projects that are in this project's hierarchy
681 681 #
682 682 # Example: parents, children, siblings
683 683 def hierarchy
684 684 parents = project.self_and_ancestors || []
685 685 descendants = project.descendants || []
686 686 project_hierarchy = parents | descendants # Set union
687 687 end
688 688
689 689 # Returns an auto-generated project identifier based on the last identifier used
690 690 def self.next_identifier
691 691 p = Project.order('id DESC').first
692 692 p.nil? ? nil : p.identifier.to_s.succ
693 693 end
694 694
695 695 # Copies and saves the Project instance based on the +project+.
696 696 # Duplicates the source project's:
697 697 # * Wiki
698 698 # * Versions
699 699 # * Categories
700 700 # * Issues
701 701 # * Members
702 702 # * Queries
703 703 #
704 704 # Accepts an +options+ argument to specify what to copy
705 705 #
706 706 # Examples:
707 707 # project.copy(1) # => copies everything
708 708 # project.copy(1, :only => 'members') # => copies members only
709 709 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
710 710 def copy(project, options={})
711 711 project = project.is_a?(Project) ? project : Project.find(project)
712 712
713 713 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
714 714 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
715 715
716 716 Project.transaction do
717 717 if save
718 718 reload
719 719 to_be_copied.each do |name|
720 720 send "copy_#{name}", project
721 721 end
722 722 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
723 723 save
724 724 end
725 725 end
726 726 end
727 727
728 728 # Returns a new unsaved Project instance with attributes copied from +project+
729 729 def self.copy_from(project)
730 730 project = project.is_a?(Project) ? project : Project.find(project)
731 731 # clear unique attributes
732 732 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
733 733 copy = Project.new(attributes)
734 734 copy.enabled_modules = project.enabled_modules
735 735 copy.trackers = project.trackers
736 736 copy.custom_values = project.custom_values.collect {|v| v.clone}
737 737 copy.issue_custom_fields = project.issue_custom_fields
738 738 copy
739 739 end
740 740
741 741 # Yields the given block for each project with its level in the tree
742 742 def self.project_tree(projects, &block)
743 743 ancestors = []
744 744 projects.sort_by(&:lft).each do |project|
745 745 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
746 746 ancestors.pop
747 747 end
748 748 yield project, ancestors.size
749 749 ancestors << project
750 750 end
751 751 end
752 752
753 753 private
754 754
755 755 def after_parent_changed(parent_was)
756 756 remove_inherited_member_roles
757 757 add_inherited_member_roles
758 758 end
759 759
760 760 def update_inherited_members
761 761 if parent
762 762 if inherit_members? && !inherit_members_was
763 763 remove_inherited_member_roles
764 764 add_inherited_member_roles
765 765 elsif !inherit_members? && inherit_members_was
766 766 remove_inherited_member_roles
767 767 end
768 768 end
769 769 end
770 770
771 771 def remove_inherited_member_roles
772 772 member_roles = memberships.map(&:member_roles).flatten
773 773 member_role_ids = member_roles.map(&:id)
774 774 member_roles.each do |member_role|
775 775 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
776 776 member_role.destroy
777 777 end
778 778 end
779 779 end
780 780
781 781 def add_inherited_member_roles
782 782 if inherit_members? && parent
783 783 parent.memberships.each do |parent_member|
784 784 member = Member.find_or_new(self.id, parent_member.user_id)
785 785 parent_member.member_roles.each do |parent_member_role|
786 786 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
787 787 end
788 788 member.save!
789 789 end
790 790 end
791 791 end
792 792
793 793 # Copies wiki from +project+
794 794 def copy_wiki(project)
795 795 # Check that the source project has a wiki first
796 796 unless project.wiki.nil?
797 797 wiki = self.wiki || Wiki.new
798 798 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
799 799 wiki_pages_map = {}
800 800 project.wiki.pages.each do |page|
801 801 # Skip pages without content
802 802 next if page.content.nil?
803 803 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
804 804 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
805 805 new_wiki_page.content = new_wiki_content
806 806 wiki.pages << new_wiki_page
807 807 wiki_pages_map[page.id] = new_wiki_page
808 808 end
809 809
810 810 self.wiki = wiki
811 811 wiki.save
812 812 # Reproduce page hierarchy
813 813 project.wiki.pages.each do |page|
814 814 if page.parent_id && wiki_pages_map[page.id]
815 815 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
816 816 wiki_pages_map[page.id].save
817 817 end
818 818 end
819 819 end
820 820 end
821 821
822 822 # Copies versions from +project+
823 823 def copy_versions(project)
824 824 project.versions.each do |version|
825 825 new_version = Version.new
826 826 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
827 827 self.versions << new_version
828 828 end
829 829 end
830 830
831 831 # Copies issue categories from +project+
832 832 def copy_issue_categories(project)
833 833 project.issue_categories.each do |issue_category|
834 834 new_issue_category = IssueCategory.new
835 835 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
836 836 self.issue_categories << new_issue_category
837 837 end
838 838 end
839 839
840 840 # Copies issues from +project+
841 841 def copy_issues(project)
842 842 # Stores the source issue id as a key and the copied issues as the
843 843 # value. Used to map the two togeather for issue relations.
844 844 issues_map = {}
845 845
846 846 # Store status and reopen locked/closed versions
847 847 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
848 848 version_statuses.each do |version, status|
849 849 version.update_attribute :status, 'open'
850 850 end
851 851
852 852 # Get issues sorted by root_id, lft so that parent issues
853 853 # get copied before their children
854 854 project.issues.reorder('root_id, lft').all.each do |issue|
855 855 new_issue = Issue.new
856 856 new_issue.copy_from(issue, :subtasks => false, :link => false)
857 857 new_issue.project = self
858 858 # Changing project resets the custom field values
859 859 # TODO: handle this in Issue#project=
860 860 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
861 861 # Reassign fixed_versions by name, since names are unique per project
862 862 if issue.fixed_version && issue.fixed_version.project == project
863 863 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
864 864 end
865 865 # Reassign the category by name, since names are unique per project
866 866 if issue.category
867 867 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
868 868 end
869 869 # Parent issue
870 870 if issue.parent_id
871 871 if copied_parent = issues_map[issue.parent_id]
872 872 new_issue.parent_issue_id = copied_parent.id
873 873 end
874 874 end
875 875
876 876 self.issues << new_issue
877 877 if new_issue.new_record?
878 878 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
879 879 else
880 880 issues_map[issue.id] = new_issue unless new_issue.new_record?
881 881 end
882 882 end
883 883
884 884 # Restore locked/closed version statuses
885 885 version_statuses.each do |version, status|
886 886 version.update_attribute :status, status
887 887 end
888 888
889 889 # Relations after in case issues related each other
890 890 project.issues.each do |issue|
891 891 new_issue = issues_map[issue.id]
892 892 unless new_issue
893 893 # Issue was not copied
894 894 next
895 895 end
896 896
897 897 # Relations
898 898 issue.relations_from.each do |source_relation|
899 899 new_issue_relation = IssueRelation.new
900 900 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
901 901 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
902 902 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
903 903 new_issue_relation.issue_to = source_relation.issue_to
904 904 end
905 905 new_issue.relations_from << new_issue_relation
906 906 end
907 907
908 908 issue.relations_to.each do |source_relation|
909 909 new_issue_relation = IssueRelation.new
910 910 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
911 911 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
912 912 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
913 913 new_issue_relation.issue_from = source_relation.issue_from
914 914 end
915 915 new_issue.relations_to << new_issue_relation
916 916 end
917 917 end
918 918 end
919 919
920 920 # Copies members from +project+
921 921 def copy_members(project)
922 922 # Copy users first, then groups to handle members with inherited and given roles
923 923 members_to_copy = []
924 924 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
925 925 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
926 926
927 927 members_to_copy.each do |member|
928 928 new_member = Member.new
929 929 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
930 930 # only copy non inherited roles
931 931 # inherited roles will be added when copying the group membership
932 932 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
933 933 next if role_ids.empty?
934 934 new_member.role_ids = role_ids
935 935 new_member.project = self
936 936 self.members << new_member
937 937 end
938 938 end
939 939
940 940 # Copies queries from +project+
941 941 def copy_queries(project)
942 942 project.queries.each do |query|
943 943 new_query = IssueQuery.new
944 944 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
945 945 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
946 946 new_query.project = self
947 947 new_query.user_id = query.user_id
948 948 self.queries << new_query
949 949 end
950 950 end
951 951
952 952 # Copies boards from +project+
953 953 def copy_boards(project)
954 954 project.boards.each do |board|
955 955 new_board = Board.new
956 956 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
957 957 new_board.project = self
958 958 self.boards << new_board
959 959 end
960 960 end
961 961
962 962 def allowed_permissions
963 963 @allowed_permissions ||= begin
964 964 module_names = enabled_modules.pluck(:name)
965 965 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
966 966 end
967 967 end
968 968
969 969 def allowed_actions
970 970 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
971 971 end
972 972
973 973 # Returns all the active Systemwide and project specific activities
974 974 def active_activities
975 975 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
976 976
977 977 if overridden_activity_ids.empty?
978 978 return TimeEntryActivity.shared.active
979 979 else
980 980 return system_activities_and_project_overrides
981 981 end
982 982 end
983 983
984 984 # Returns all the Systemwide and project specific activities
985 985 # (inactive and active)
986 986 def all_activities
987 987 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
988 988
989 989 if overridden_activity_ids.empty?
990 990 return TimeEntryActivity.shared
991 991 else
992 992 return system_activities_and_project_overrides(true)
993 993 end
994 994 end
995 995
996 996 # Returns the systemwide active activities merged with the project specific overrides
997 997 def system_activities_and_project_overrides(include_inactive=false)
998 998 if include_inactive
999 999 return TimeEntryActivity.shared.
1000 1000 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
1001 1001 self.time_entry_activities
1002 1002 else
1003 1003 return TimeEntryActivity.shared.active.
1004 1004 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
1005 1005 self.time_entry_activities.active
1006 1006 end
1007 1007 end
1008 1008
1009 1009 # Archives subprojects recursively
1010 1010 def archive!
1011 1011 children.each do |subproject|
1012 1012 subproject.send :archive!
1013 1013 end
1014 1014 update_attribute :status, STATUS_ARCHIVED
1015 1015 end
1016 1016
1017 1017 def update_position_under_parent
1018 1018 set_or_update_position_under(parent)
1019 1019 end
1020 1020
1021 1021 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
1022 1022 def set_or_update_position_under(target_parent)
1023 1023 parent_was = parent
1024 1024 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1025 1025 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1026 1026
1027 1027 if to_be_inserted_before
1028 1028 move_to_left_of(to_be_inserted_before)
1029 1029 elsif target_parent.nil?
1030 1030 if sibs.empty?
1031 1031 # move_to_root adds the project in first (ie. left) position
1032 1032 move_to_root
1033 1033 else
1034 1034 move_to_right_of(sibs.last) unless self == sibs.last
1035 1035 end
1036 1036 else
1037 1037 # move_to_child_of adds the project in last (ie.right) position
1038 1038 move_to_child_of(target_parent)
1039 1039 end
1040 1040 if parent_was != target_parent
1041 1041 after_parent_changed(parent_was)
1042 1042 end
1043 1043 end
1044 1044 end
@@ -1,727 +1,734
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 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 # Different ways of displaying/sorting users
24 24 USER_FORMATS = {
25 25 :firstname_lastname => {
26 26 :string => '#{firstname} #{lastname}',
27 27 :order => %w(firstname lastname id),
28 28 :setting_order => 1
29 29 },
30 30 :firstname_lastinitial => {
31 31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
32 32 :order => %w(firstname lastname id),
33 33 :setting_order => 2
34 34 },
35 35 :firstname => {
36 36 :string => '#{firstname}',
37 37 :order => %w(firstname id),
38 38 :setting_order => 3
39 39 },
40 40 :lastname_firstname => {
41 41 :string => '#{lastname} #{firstname}',
42 42 :order => %w(lastname firstname id),
43 43 :setting_order => 4
44 44 },
45 45 :lastname_coma_firstname => {
46 46 :string => '#{lastname}, #{firstname}',
47 47 :order => %w(lastname firstname id),
48 48 :setting_order => 5
49 49 },
50 50 :lastname => {
51 51 :string => '#{lastname}',
52 52 :order => %w(lastname id),
53 53 :setting_order => 6
54 54 },
55 55 :username => {
56 56 :string => '#{login}',
57 57 :order => %w(login id),
58 58 :setting_order => 7
59 59 },
60 60 }
61 61
62 62 MAIL_NOTIFICATION_OPTIONS = [
63 63 ['all', :label_user_mail_option_all],
64 64 ['selected', :label_user_mail_option_selected],
65 65 ['only_my_events', :label_user_mail_option_only_my_events],
66 66 ['only_assigned', :label_user_mail_option_only_assigned],
67 67 ['only_owner', :label_user_mail_option_only_owner],
68 68 ['none', :label_user_mail_option_none]
69 69 ]
70 70
71 71 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
72 72 :after_remove => Proc.new {|user, group| group.user_removed(user)}
73 73 has_many :changesets, :dependent => :nullify
74 74 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
75 75 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
76 76 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
77 77 belongs_to :auth_source
78 78
79 79 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
80 80 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
81 81
82 82 acts_as_customizable
83 83
84 84 attr_accessor :password, :password_confirmation, :generate_password
85 85 attr_accessor :last_before_login_on
86 86 # Prevents unauthorized assignments
87 87 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
88 88
89 89 LOGIN_LENGTH_LIMIT = 60
90 90 MAIL_LENGTH_LIMIT = 60
91 91
92 92 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
93 93 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
94 94 validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
95 95 # Login must contain letters, numbers, underscores only
96 96 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
97 97 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
98 98 validates_length_of :firstname, :lastname, :maximum => 30
99 99 validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
100 100 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
101 101 validates_confirmation_of :password, :allow_nil => true
102 102 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
103 103 validate :validate_password_length
104 104
105 105 before_create :set_mail_notification
106 106 before_save :generate_password_if_needed, :update_hashed_password
107 107 before_destroy :remove_references_before_destroy
108 108 after_save :update_notified_project_ids
109 109
110 110 scope :in_group, lambda {|group|
111 111 group_id = group.is_a?(Group) ? group.id : group.to_i
112 112 where("#{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)
113 113 }
114 114 scope :not_in_group, lambda {|group|
115 115 group_id = group.is_a?(Group) ? group.id : group.to_i
116 116 where("#{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)
117 117 }
118 118 scope :sorted, lambda { order(*User.fields_for_order_statement)}
119 119
120 120 def set_mail_notification
121 121 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
122 122 true
123 123 end
124 124
125 125 def update_hashed_password
126 126 # update hashed_password if password was set
127 127 if self.password && self.auth_source_id.blank?
128 128 salt_password(password)
129 129 end
130 130 end
131 131
132 132 alias :base_reload :reload
133 133 def reload(*args)
134 134 @name = nil
135 135 @projects_by_role = nil
136 136 @membership_by_project_id = nil
137 137 @notified_projects_ids = nil
138 138 @notified_projects_ids_changed = false
139 139 base_reload(*args)
140 140 end
141 141
142 142 def mail=(arg)
143 143 write_attribute(:mail, arg.to_s.strip)
144 144 end
145 145
146 146 def identity_url=(url)
147 147 if url.blank?
148 148 write_attribute(:identity_url, '')
149 149 else
150 150 begin
151 151 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
152 152 rescue OpenIdAuthentication::InvalidOpenId
153 153 # Invalid url, don't save
154 154 end
155 155 end
156 156 self.read_attribute(:identity_url)
157 157 end
158 158
159 159 # Returns the user that matches provided login and password, or nil
160 160 def self.try_to_login(login, password, active_only=true)
161 161 login = login.to_s
162 162 password = password.to_s
163 163
164 164 # Make sure no one can sign in with an empty login or password
165 165 return nil if login.empty? || password.empty?
166 166 user = find_by_login(login)
167 167 if user
168 168 # user is already in local database
169 169 return nil unless user.check_password?(password)
170 170 return nil if !user.active? && active_only
171 171 else
172 172 # user is not yet registered, try to authenticate with available sources
173 173 attrs = AuthSource.authenticate(login, password)
174 174 if attrs
175 175 user = new(attrs)
176 176 user.login = login
177 177 user.language = Setting.default_language
178 178 if user.save
179 179 user.reload
180 180 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
181 181 end
182 182 end
183 183 end
184 184 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
185 185 user
186 186 rescue => text
187 187 raise text
188 188 end
189 189
190 190 # Returns the user who matches the given autologin +key+ or nil
191 191 def self.try_to_autologin(key)
192 192 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
193 193 if user
194 194 user.update_column(:last_login_on, Time.now)
195 195 user
196 196 end
197 197 end
198 198
199 199 def self.name_formatter(formatter = nil)
200 200 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
201 201 end
202 202
203 203 # Returns an array of fields names than can be used to make an order statement for users
204 204 # according to how user names are displayed
205 205 # Examples:
206 206 #
207 207 # User.fields_for_order_statement => ['users.login', 'users.id']
208 208 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
209 209 def self.fields_for_order_statement(table=nil)
210 210 table ||= table_name
211 211 name_formatter[:order].map {|field| "#{table}.#{field}"}
212 212 end
213 213
214 214 # Return user's full name for display
215 215 def name(formatter = nil)
216 216 f = self.class.name_formatter(formatter)
217 217 if formatter
218 218 eval('"' + f[:string] + '"')
219 219 else
220 220 @name ||= eval('"' + f[:string] + '"')
221 221 end
222 222 end
223 223
224 224 def active?
225 225 self.status == STATUS_ACTIVE
226 226 end
227 227
228 228 def registered?
229 229 self.status == STATUS_REGISTERED
230 230 end
231 231
232 232 def locked?
233 233 self.status == STATUS_LOCKED
234 234 end
235 235
236 236 def activate
237 237 self.status = STATUS_ACTIVE
238 238 end
239 239
240 240 def register
241 241 self.status = STATUS_REGISTERED
242 242 end
243 243
244 244 def lock
245 245 self.status = STATUS_LOCKED
246 246 end
247 247
248 248 def activate!
249 249 update_attribute(:status, STATUS_ACTIVE)
250 250 end
251 251
252 252 def register!
253 253 update_attribute(:status, STATUS_REGISTERED)
254 254 end
255 255
256 256 def lock!
257 257 update_attribute(:status, STATUS_LOCKED)
258 258 end
259 259
260 260 # Returns true if +clear_password+ is the correct user's password, otherwise false
261 261 def check_password?(clear_password)
262 262 if auth_source_id.present?
263 263 auth_source.authenticate(self.login, clear_password)
264 264 else
265 265 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
266 266 end
267 267 end
268 268
269 269 # Generates a random salt and computes hashed_password for +clear_password+
270 270 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
271 271 def salt_password(clear_password)
272 272 self.salt = User.generate_salt
273 273 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
274 274 end
275 275
276 276 # Does the backend storage allow this user to change their password?
277 277 def change_password_allowed?
278 278 return true if auth_source.nil?
279 279 return auth_source.allow_password_changes?
280 280 end
281 281
282 282 def generate_password?
283 283 generate_password == '1' || generate_password == true
284 284 end
285 285
286 286 # Generate and set a random password on given length
287 287 def random_password(length=40)
288 288 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
289 289 chars -= %w(0 O 1 l)
290 290 password = ''
291 291 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
292 292 self.password = password
293 293 self.password_confirmation = password
294 294 self
295 295 end
296 296
297 297 def pref
298 298 self.preference ||= UserPreference.new(:user => self)
299 299 end
300 300
301 301 def time_zone
302 302 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
303 303 end
304 304
305 305 def wants_comments_in_reverse_order?
306 306 self.pref[:comments_sorting] == 'desc'
307 307 end
308 308
309 309 # Return user's RSS key (a 40 chars long string), used to access feeds
310 310 def rss_key
311 311 if rss_token.nil?
312 312 create_rss_token(:action => 'feeds')
313 313 end
314 314 rss_token.value
315 315 end
316 316
317 317 # Return user's API key (a 40 chars long string), used to access the API
318 318 def api_key
319 319 if api_token.nil?
320 320 create_api_token(:action => 'api')
321 321 end
322 322 api_token.value
323 323 end
324 324
325 325 # Return an array of project ids for which the user has explicitly turned mail notifications on
326 326 def notified_projects_ids
327 327 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
328 328 end
329 329
330 330 def notified_project_ids=(ids)
331 331 @notified_projects_ids_changed = true
332 332 @notified_projects_ids = ids
333 333 end
334 334
335 335 # Updates per project notifications (after_save callback)
336 336 def update_notified_project_ids
337 337 if @notified_projects_ids_changed
338 338 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
339 339 members.update_all(:mail_notification => false)
340 340 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
341 341 end
342 342 end
343 343 private :update_notified_project_ids
344 344
345 345 def valid_notification_options
346 346 self.class.valid_notification_options(self)
347 347 end
348 348
349 349 # Only users that belong to more than 1 project can select projects for which they are notified
350 350 def self.valid_notification_options(user=nil)
351 351 # Note that @user.membership.size would fail since AR ignores
352 352 # :include association option when doing a count
353 353 if user.nil? || user.memberships.length < 1
354 354 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
355 355 else
356 356 MAIL_NOTIFICATION_OPTIONS
357 357 end
358 358 end
359 359
360 360 # Find a user account by matching the exact login and then a case-insensitive
361 361 # version. Exact matches will be given priority.
362 362 def self.find_by_login(login)
363 363 if login.present?
364 364 login = login.to_s
365 365 # First look for an exact match
366 366 user = where(:login => login).all.detect {|u| u.login == login}
367 367 unless user
368 368 # Fail over to case-insensitive if none was found
369 369 user = where("LOWER(login) = ?", login.downcase).first
370 370 end
371 371 user
372 372 end
373 373 end
374 374
375 375 def self.find_by_rss_key(key)
376 376 Token.find_active_user('feeds', key)
377 377 end
378 378
379 379 def self.find_by_api_key(key)
380 380 Token.find_active_user('api', key)
381 381 end
382 382
383 383 # Makes find_by_mail case-insensitive
384 384 def self.find_by_mail(mail)
385 385 where("LOWER(mail) = ?", mail.to_s.downcase).first
386 386 end
387 387
388 388 # Returns true if the default admin account can no longer be used
389 389 def self.default_admin_account_changed?
390 390 !User.active.find_by_login("admin").try(:check_password?, "admin")
391 391 end
392 392
393 393 def to_s
394 394 name
395 395 end
396 396
397 397 CSS_CLASS_BY_STATUS = {
398 398 STATUS_ANONYMOUS => 'anon',
399 399 STATUS_ACTIVE => 'active',
400 400 STATUS_REGISTERED => 'registered',
401 401 STATUS_LOCKED => 'locked'
402 402 }
403 403
404 404 def css_classes
405 405 "user #{CSS_CLASS_BY_STATUS[status]}"
406 406 end
407 407
408 408 # Returns the current day according to user's time zone
409 409 def today
410 410 if time_zone.nil?
411 411 Date.today
412 412 else
413 413 Time.now.in_time_zone(time_zone).to_date
414 414 end
415 415 end
416 416
417 417 # Returns the day of +time+ according to user's time zone
418 418 def time_to_date(time)
419 419 if time_zone.nil?
420 420 time.to_date
421 421 else
422 422 time.in_time_zone(time_zone).to_date
423 423 end
424 424 end
425 425
426 426 def logged?
427 427 true
428 428 end
429 429
430 430 def anonymous?
431 431 !logged?
432 432 end
433 433
434 434 # Returns user's membership for the given project
435 435 # or nil if the user is not a member of project
436 436 def membership(project)
437 437 project_id = project.is_a?(Project) ? project.id : project
438 438
439 439 @membership_by_project_id ||= Hash.new {|h, project_id|
440 440 h[project_id] = memberships.where(:project_id => project_id).first
441 441 }
442 442 @membership_by_project_id[project_id]
443 443 end
444 444
445 # Returns the user's bult-in role
446 def builtin_role
447 if logged?
448 @role_non_member ||= Role.non_member
449 else
450 @role_anonymous ||= Role.anonymous
451 end
452 end
453
445 454 # Return user's roles for project
446 455 def roles_for_project(project)
447 456 roles = []
448 457 # No role on archived projects
449 458 return roles if project.nil? || project.archived?
450 459 if logged?
451 460 # Find project membership
452 461 membership = membership(project)
453 462 if membership
454 463 roles = membership.roles
455 464 else
456 @role_non_member ||= Role.non_member
457 roles << @role_non_member
465 roles << builtin_role
458 466 end
459 467 else
460 @role_anonymous ||= Role.anonymous
461 roles << @role_anonymous
468 roles << builtin_role
462 469 end
463 470 roles
464 471 end
465 472
466 473 # Return true if the user is a member of project
467 474 def member_of?(project)
468 475 projects.to_a.include?(project)
469 476 end
470 477
471 478 # Returns a hash of user's projects grouped by roles
472 479 def projects_by_role
473 480 return @projects_by_role if @projects_by_role
474 481
475 482 @projects_by_role = Hash.new([])
476 483 memberships.each do |membership|
477 484 if membership.project
478 485 membership.roles.each do |role|
479 486 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
480 487 @projects_by_role[role] << membership.project
481 488 end
482 489 end
483 490 end
484 491 @projects_by_role.each do |role, projects|
485 492 projects.uniq!
486 493 end
487 494
488 495 @projects_by_role
489 496 end
490 497
491 498 # Returns true if user is arg or belongs to arg
492 499 def is_or_belongs_to?(arg)
493 500 if arg.is_a?(User)
494 501 self == arg
495 502 elsif arg.is_a?(Group)
496 503 arg.users.include?(self)
497 504 else
498 505 false
499 506 end
500 507 end
501 508
502 509 # Return true if the user is allowed to do the specified action on a specific context
503 510 # Action can be:
504 511 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
505 512 # * a permission Symbol (eg. :edit_project)
506 513 # Context can be:
507 514 # * a project : returns true if user is allowed to do the specified action on this project
508 515 # * an array of projects : returns true if user is allowed on every project
509 516 # * nil with options[:global] set : check if user has at least one role allowed for this action,
510 517 # or falls back to Non Member / Anonymous permissions depending if the user is logged
511 518 def allowed_to?(action, context, options={}, &block)
512 519 if context && context.is_a?(Project)
513 520 return false unless context.allows_to?(action)
514 521 # Admin users are authorized for anything else
515 522 return true if admin?
516 523
517 524 roles = roles_for_project(context)
518 525 return false unless roles
519 526 roles.any? {|role|
520 527 (context.is_public? || role.member?) &&
521 528 role.allowed_to?(action) &&
522 529 (block_given? ? yield(role, self) : true)
523 530 }
524 531 elsif context && context.is_a?(Array)
525 532 if context.empty?
526 533 false
527 534 else
528 535 # Authorize if user is authorized on every element of the array
529 536 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
530 537 end
531 538 elsif options[:global]
532 539 # Admin users are always authorized
533 540 return true if admin?
534 541
535 542 # authorize if user has at least one role that has this permission
536 543 roles = memberships.collect {|m| m.roles}.flatten.uniq
537 544 roles << (self.logged? ? Role.non_member : Role.anonymous)
538 545 roles.any? {|role|
539 546 role.allowed_to?(action) &&
540 547 (block_given? ? yield(role, self) : true)
541 548 }
542 549 else
543 550 false
544 551 end
545 552 end
546 553
547 554 # Is the user allowed to do the specified action on any project?
548 555 # See allowed_to? for the actions and valid options.
549 556 def allowed_to_globally?(action, options, &block)
550 557 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
551 558 end
552 559
553 560 # Returns true if the user is allowed to delete his own account
554 561 def own_account_deletable?
555 562 Setting.unsubscribe? &&
556 563 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
557 564 end
558 565
559 566 safe_attributes 'login',
560 567 'firstname',
561 568 'lastname',
562 569 'mail',
563 570 'mail_notification',
564 571 'notified_project_ids',
565 572 'language',
566 573 'custom_field_values',
567 574 'custom_fields',
568 575 'identity_url'
569 576
570 577 safe_attributes 'status',
571 578 'auth_source_id',
572 579 'generate_password',
573 580 :if => lambda {|user, current_user| current_user.admin?}
574 581
575 582 safe_attributes 'group_ids',
576 583 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
577 584
578 585 # Utility method to help check if a user should be notified about an
579 586 # event.
580 587 #
581 588 # TODO: only supports Issue events currently
582 589 def notify_about?(object)
583 590 if mail_notification == 'all'
584 591 true
585 592 elsif mail_notification.blank? || mail_notification == 'none'
586 593 false
587 594 else
588 595 case object
589 596 when Issue
590 597 case mail_notification
591 598 when 'selected', 'only_my_events'
592 599 # user receives notifications for created/assigned issues on unselected projects
593 600 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
594 601 when 'only_assigned'
595 602 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
596 603 when 'only_owner'
597 604 object.author == self
598 605 end
599 606 when News
600 607 # always send to project members except when mail_notification is set to 'none'
601 608 true
602 609 end
603 610 end
604 611 end
605 612
606 613 def self.current=(user)
607 614 Thread.current[:current_user] = user
608 615 end
609 616
610 617 def self.current
611 618 Thread.current[:current_user] ||= User.anonymous
612 619 end
613 620
614 621 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
615 622 # one anonymous user per database.
616 623 def self.anonymous
617 624 anonymous_user = AnonymousUser.first
618 625 if anonymous_user.nil?
619 626 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
620 627 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
621 628 end
622 629 anonymous_user
623 630 end
624 631
625 632 # Salts all existing unsalted passwords
626 633 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
627 634 # This method is used in the SaltPasswords migration and is to be kept as is
628 635 def self.salt_unsalted_passwords!
629 636 transaction do
630 637 User.where("salt IS NULL OR salt = ''").find_each do |user|
631 638 next if user.hashed_password.blank?
632 639 salt = User.generate_salt
633 640 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
634 641 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
635 642 end
636 643 end
637 644 end
638 645
639 646 protected
640 647
641 648 def validate_password_length
642 649 return if password.blank? && generate_password?
643 650 # Password length validation based on setting
644 651 if !password.nil? && password.size < Setting.password_min_length.to_i
645 652 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
646 653 end
647 654 end
648 655
649 656 private
650 657
651 658 def generate_password_if_needed
652 659 if generate_password? && auth_source.nil?
653 660 length = [Setting.password_min_length.to_i + 2, 10].max
654 661 random_password(length)
655 662 end
656 663 end
657 664
658 665 # Removes references that are not handled by associations
659 666 # Things that are not deleted are reassociated with the anonymous user
660 667 def remove_references_before_destroy
661 668 return if self.id.nil?
662 669
663 670 substitute = User.anonymous
664 671 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
665 672 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
666 673 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
667 674 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
668 675 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
669 676 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
670 677 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
671 678 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
672 679 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
673 680 # Remove private queries and keep public ones
674 681 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
675 682 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
676 683 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
677 684 Token.delete_all ['user_id = ?', id]
678 685 Watcher.delete_all ['user_id = ?', id]
679 686 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
680 687 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
681 688 end
682 689
683 690 # Return password digest
684 691 def self.hash_password(clear_password)
685 692 Digest::SHA1.hexdigest(clear_password || "")
686 693 end
687 694
688 695 # Returns a 128bits random salt as a hex string (32 chars long)
689 696 def self.generate_salt
690 697 Redmine::Utils.random_hex(16)
691 698 end
692 699
693 700 end
694 701
695 702 class AnonymousUser < User
696 703 validate :validate_anonymous_uniqueness, :on => :create
697 704
698 705 def validate_anonymous_uniqueness
699 706 # There should be only one AnonymousUser in the database
700 707 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
701 708 end
702 709
703 710 def available_custom_fields
704 711 []
705 712 end
706 713
707 714 # Overrides a few properties
708 715 def logged?; false end
709 716 def admin; false end
710 717 def name(*args); I18n.t(:label_user_anonymous) end
711 718 def mail; nil end
712 719 def time_zone; nil end
713 720 def rss_key; nil end
714 721
715 722 def pref
716 723 UserPreference.new(:user => self)
717 724 end
718 725
719 726 def member_of?(project)
720 727 false
721 728 end
722 729
723 730 # Anonymous user can not be destroyed
724 731 def destroy
725 732 false
726 733 end
727 734 end
General Comments 0
You need to be logged in to leave comments. Login now