annotate app/models/.svn/text-base/project.rb.svn-base @ 45:65d9e2cabaa3 luisf

Added tipoftheday to the config/settings in order to correct previous issues. Tip of the day is now working correctly. Added the heading strings to the locales files.
author luisf
date Tue, 23 Nov 2010 11:50:01 +0000
parents 94944d00e43c
children af80e5618e9b
rev   line source
Chris@0 1 # redMine - project management software
Chris@0 2 # Copyright (C) 2006 Jean-Philippe Lang
Chris@0 3 #
Chris@0 4 # This program is free software; you can redistribute it and/or
Chris@0 5 # modify it under the terms of the GNU General Public License
Chris@0 6 # as published by the Free Software Foundation; either version 2
Chris@0 7 # of the License, or (at your option) any later version.
Chris@0 8 #
Chris@0 9 # This program is distributed in the hope that it will be useful,
Chris@0 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
Chris@0 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Chris@0 12 # GNU General Public License for more details.
Chris@0 13 #
Chris@0 14 # You should have received a copy of the GNU General Public License
Chris@0 15 # along with this program; if not, write to the Free Software
Chris@0 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Chris@0 17
Chris@0 18 class Project < ActiveRecord::Base
Chris@0 19 # Project statuses
Chris@0 20 STATUS_ACTIVE = 1
Chris@0 21 STATUS_ARCHIVED = 9
Chris@0 22
chris@37 23 # Maximum length for project identifiers
chris@37 24 IDENTIFIER_MAX_LENGTH = 100
chris@37 25
Chris@0 26 # Specific overidden Activities
Chris@0 27 has_many :time_entry_activities
Chris@0 28 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
Chris@0 29 has_many :memberships, :class_name => 'Member'
Chris@0 30 has_many :member_principals, :class_name => 'Member',
Chris@0 31 :include => :principal,
Chris@0 32 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
Chris@0 33 has_many :users, :through => :members
Chris@0 34 has_many :principals, :through => :member_principals, :source => :principal
Chris@0 35
Chris@0 36 has_many :enabled_modules, :dependent => :delete_all
Chris@0 37 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
Chris@0 38 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
Chris@0 39 has_many :issue_changes, :through => :issues, :source => :journals
Chris@0 40 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
Chris@0 41 has_many :time_entries, :dependent => :delete_all
Chris@0 42 has_many :queries, :dependent => :delete_all
Chris@0 43 has_many :documents, :dependent => :destroy
Chris@0 44 has_many :news, :dependent => :delete_all, :include => :author
Chris@0 45 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
Chris@0 46 has_many :boards, :dependent => :destroy, :order => "position ASC"
Chris@0 47 has_one :repository, :dependent => :destroy
Chris@0 48 has_many :changesets, :through => :repository
Chris@0 49 has_one :wiki, :dependent => :destroy
Chris@0 50 # Custom field for the project issues
Chris@0 51 has_and_belongs_to_many :issue_custom_fields,
Chris@0 52 :class_name => 'IssueCustomField',
Chris@0 53 :order => "#{CustomField.table_name}.position",
Chris@0 54 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
Chris@0 55 :association_foreign_key => 'custom_field_id'
Chris@0 56
Chris@0 57 acts_as_nested_set :order => 'name'
Chris@0 58 acts_as_attachable :view_permission => :view_files,
Chris@0 59 :delete_permission => :manage_files
Chris@0 60
Chris@0 61 acts_as_customizable
Chris@0 62 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
Chris@0 63 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
Chris@0 64 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
Chris@0 65 :author => nil
Chris@0 66
Chris@0 67 attr_protected :status, :enabled_module_names
Chris@0 68
Chris@0 69 validates_presence_of :name, :identifier
chris@37 70 validates_uniqueness_of :identifier
Chris@0 71 validates_associated :repository, :wiki
chris@37 72 validates_length_of :name, :maximum => 255
Chris@0 73 validates_length_of :homepage, :maximum => 255
chris@37 74 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
Chris@0 75 # donwcase letters, digits, dashes but not digits only
Chris@0 76 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
Chris@0 77 # reserved words
Chris@0 78 validates_exclusion_of :identifier, :in => %w( new )
Chris@0 79
Chris@0 80 before_destroy :delete_all_members, :destroy_children
Chris@0 81
Chris@0 82 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] } }
Chris@0 83 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
Chris@0 84 named_scope :all_public, { :conditions => { :is_public => true } }
Chris@0 85 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
Chris@0 86
Chris@0 87 def identifier=(identifier)
Chris@0 88 super unless identifier_frozen?
Chris@0 89 end
Chris@0 90
Chris@0 91 def identifier_frozen?
Chris@0 92 errors[:identifier].nil? && !(new_record? || identifier.blank?)
Chris@0 93 end
Chris@0 94
Chris@0 95 # returns latest created projects
Chris@0 96 # non public projects will be returned only if user is a member of those
Chris@0 97 def self.latest(user=nil, count=5)
Chris@0 98 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
Chris@0 99 end
Chris@0 100
Chris@0 101 # Returns a SQL :conditions string used to find all active projects for the specified user.
Chris@0 102 #
Chris@0 103 # Examples:
Chris@0 104 # Projects.visible_by(admin) => "projects.status = 1"
Chris@0 105 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
Chris@0 106 def self.visible_by(user=nil)
Chris@0 107 user ||= User.current
Chris@0 108 if user && user.admin?
Chris@0 109 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
Chris@0 110 elsif user && user.memberships.any?
Chris@0 111 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(',')}))"
Chris@0 112 else
Chris@0 113 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
Chris@0 114 end
Chris@0 115 end
Chris@0 116
Chris@0 117 def self.allowed_to_condition(user, permission, options={})
Chris@0 118 statements = []
Chris@0 119 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
Chris@0 120 if perm = Redmine::AccessControl.permission(permission)
Chris@0 121 unless perm.project_module.nil?
Chris@0 122 # If the permission belongs to a project module, make sure the module is enabled
Chris@0 123 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
Chris@0 124 end
Chris@0 125 end
Chris@0 126 if options[:project]
Chris@0 127 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
Chris@0 128 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
Chris@0 129 base_statement = "(#{project_statement}) AND (#{base_statement})"
Chris@0 130 end
Chris@0 131 if user.admin?
Chris@0 132 # no restriction
Chris@0 133 else
Chris@0 134 statements << "1=0"
Chris@0 135 if user.logged?
Chris@0 136 if Role.non_member.allowed_to?(permission) && !options[:member]
Chris@0 137 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
Chris@0 138 end
Chris@0 139 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
Chris@0 140 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
Chris@0 141 else
Chris@0 142 if Role.anonymous.allowed_to?(permission) && !options[:member]
Chris@0 143 # anonymous user allowed on public project
Chris@0 144 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
Chris@0 145 end
Chris@0 146 end
Chris@0 147 end
Chris@0 148 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
Chris@0 149 end
Chris@0 150
Chris@0 151 # Returns the Systemwide and project specific activities
Chris@0 152 def activities(include_inactive=false)
Chris@0 153 if include_inactive
Chris@0 154 return all_activities
Chris@0 155 else
Chris@0 156 return active_activities
Chris@0 157 end
Chris@0 158 end
Chris@0 159
Chris@0 160 # Will create a new Project specific Activity or update an existing one
Chris@0 161 #
Chris@0 162 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
Chris@0 163 # does not successfully save.
Chris@0 164 def update_or_create_time_entry_activity(id, activity_hash)
Chris@0 165 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
Chris@0 166 self.create_time_entry_activity_if_needed(activity_hash)
Chris@0 167 else
Chris@0 168 activity = project.time_entry_activities.find_by_id(id.to_i)
Chris@0 169 activity.update_attributes(activity_hash) if activity
Chris@0 170 end
Chris@0 171 end
Chris@0 172
Chris@0 173 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
Chris@0 174 #
Chris@0 175 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
Chris@0 176 # does not successfully save.
Chris@0 177 def create_time_entry_activity_if_needed(activity)
Chris@0 178 if activity['parent_id']
Chris@0 179
Chris@0 180 parent_activity = TimeEntryActivity.find(activity['parent_id'])
Chris@0 181 activity['name'] = parent_activity.name
Chris@0 182 activity['position'] = parent_activity.position
Chris@0 183
Chris@0 184 if Enumeration.overridding_change?(activity, parent_activity)
Chris@0 185 project_activity = self.time_entry_activities.create(activity)
Chris@0 186
Chris@0 187 if project_activity.new_record?
Chris@0 188 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
Chris@0 189 else
Chris@0 190 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
Chris@0 191 end
Chris@0 192 end
Chris@0 193 end
Chris@0 194 end
Chris@0 195
Chris@0 196 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
Chris@0 197 #
Chris@0 198 # Examples:
Chris@0 199 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
Chris@0 200 # project.project_condition(false) => "projects.id = 1"
Chris@0 201 def project_condition(with_subprojects)
Chris@0 202 cond = "#{Project.table_name}.id = #{id}"
Chris@0 203 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
Chris@0 204 cond
Chris@0 205 end
Chris@0 206
Chris@0 207 def self.find(*args)
Chris@0 208 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
Chris@0 209 project = find_by_identifier(*args)
Chris@0 210 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
Chris@0 211 project
Chris@0 212 else
Chris@0 213 super
Chris@0 214 end
Chris@0 215 end
Chris@0 216
Chris@0 217 def to_param
Chris@0 218 # id is used for projects with a numeric identifier (compatibility)
Chris@0 219 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
Chris@0 220 end
Chris@0 221
Chris@0 222 def active?
Chris@0 223 self.status == STATUS_ACTIVE
Chris@0 224 end
Chris@0 225
chris@37 226 def archived?
chris@37 227 self.status == STATUS_ARCHIVED
chris@37 228 end
chris@37 229
Chris@0 230 # Archives the project and its descendants
Chris@0 231 def archive
Chris@0 232 # Check that there is no issue of a non descendant project that is assigned
Chris@0 233 # to one of the project or descendant versions
Chris@0 234 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
Chris@0 235 if v_ids.any? && Issue.find(:first, :include => :project,
Chris@0 236 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
Chris@0 237 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
Chris@0 238 return false
Chris@0 239 end
Chris@0 240 Project.transaction do
Chris@0 241 archive!
Chris@0 242 end
Chris@0 243 true
Chris@0 244 end
Chris@0 245
Chris@0 246 # Unarchives the project
Chris@0 247 # All its ancestors must be active
Chris@0 248 def unarchive
Chris@0 249 return false if ancestors.detect {|a| !a.active?}
Chris@0 250 update_attribute :status, STATUS_ACTIVE
Chris@0 251 end
Chris@0 252
Chris@0 253 # Returns an array of projects the project can be moved to
Chris@0 254 # by the current user
Chris@0 255 def allowed_parents
Chris@0 256 return @allowed_parents if @allowed_parents
Chris@0 257 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
Chris@0 258 @allowed_parents = @allowed_parents - self_and_descendants
Chris@0 259 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
Chris@0 260 @allowed_parents << nil
Chris@0 261 end
Chris@0 262 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
Chris@0 263 @allowed_parents << parent
Chris@0 264 end
Chris@0 265 @allowed_parents
Chris@0 266 end
Chris@0 267
Chris@0 268 # Sets the parent of the project with authorization check
Chris@0 269 def set_allowed_parent!(p)
Chris@0 270 unless p.nil? || p.is_a?(Project)
Chris@0 271 if p.to_s.blank?
Chris@0 272 p = nil
Chris@0 273 else
Chris@0 274 p = Project.find_by_id(p)
Chris@0 275 return false unless p
Chris@0 276 end
Chris@0 277 end
Chris@0 278 if p.nil?
Chris@0 279 if !new_record? && allowed_parents.empty?
Chris@0 280 return false
Chris@0 281 end
Chris@0 282 elsif !allowed_parents.include?(p)
Chris@0 283 return false
Chris@0 284 end
Chris@0 285 set_parent!(p)
Chris@0 286 end
Chris@0 287
Chris@0 288 # Sets the parent of the project
Chris@0 289 # Argument can be either a Project, a String, a Fixnum or nil
Chris@0 290 def set_parent!(p)
Chris@0 291 unless p.nil? || p.is_a?(Project)
Chris@0 292 if p.to_s.blank?
Chris@0 293 p = nil
Chris@0 294 else
Chris@0 295 p = Project.find_by_id(p)
Chris@0 296 return false unless p
Chris@0 297 end
Chris@0 298 end
Chris@0 299 if p == parent && !p.nil?
Chris@0 300 # Nothing to do
Chris@0 301 true
Chris@0 302 elsif p.nil? || (p.active? && move_possible?(p))
Chris@0 303 # Insert the project so that target's children or root projects stay alphabetically sorted
Chris@0 304 sibs = (p.nil? ? self.class.roots : p.children)
Chris@0 305 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
Chris@0 306 if to_be_inserted_before
Chris@0 307 move_to_left_of(to_be_inserted_before)
Chris@0 308 elsif p.nil?
Chris@0 309 if sibs.empty?
Chris@0 310 # move_to_root adds the project in first (ie. left) position
Chris@0 311 move_to_root
Chris@0 312 else
Chris@0 313 move_to_right_of(sibs.last) unless self == sibs.last
Chris@0 314 end
Chris@0 315 else
Chris@0 316 # move_to_child_of adds the project in last (ie.right) position
Chris@0 317 move_to_child_of(p)
Chris@0 318 end
Chris@0 319 Issue.update_versions_from_hierarchy_change(self)
Chris@0 320 true
Chris@0 321 else
Chris@0 322 # Can not move to the given target
Chris@0 323 false
Chris@0 324 end
Chris@0 325 end
Chris@0 326
Chris@0 327 # Returns an array of the trackers used by the project and its active sub projects
Chris@0 328 def rolled_up_trackers
Chris@0 329 @rolled_up_trackers ||=
Chris@0 330 Tracker.find(:all, :include => :projects,
Chris@0 331 :select => "DISTINCT #{Tracker.table_name}.*",
Chris@0 332 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
Chris@0 333 :order => "#{Tracker.table_name}.position")
Chris@0 334 end
Chris@0 335
Chris@0 336 # Closes open and locked project versions that are completed
Chris@0 337 def close_completed_versions
Chris@0 338 Version.transaction do
Chris@0 339 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
Chris@0 340 if version.completed?
Chris@0 341 version.update_attribute(:status, 'closed')
Chris@0 342 end
Chris@0 343 end
Chris@0 344 end
Chris@0 345 end
Chris@0 346
Chris@0 347 # Returns a scope of the Versions on subprojects
Chris@0 348 def rolled_up_versions
Chris@0 349 @rolled_up_versions ||=
Chris@0 350 Version.scoped(:include => :project,
Chris@0 351 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
Chris@0 352 end
Chris@0 353
Chris@0 354 # Returns a scope of the Versions used by the project
Chris@0 355 def shared_versions
Chris@0 356 @shared_versions ||=
Chris@0 357 Version.scoped(:include => :project,
Chris@0 358 :conditions => "#{Project.table_name}.id = #{id}" +
Chris@0 359 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
Chris@0 360 " #{Version.table_name}.sharing = 'system'" +
Chris@0 361 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
Chris@0 362 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
Chris@0 363 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
Chris@0 364 "))")
Chris@0 365 end
Chris@0 366
Chris@0 367 # Returns a hash of project users grouped by role
Chris@0 368 def users_by_role
Chris@0 369 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
Chris@0 370 m.roles.each do |r|
Chris@0 371 h[r] ||= []
Chris@0 372 h[r] << m.user
Chris@0 373 end
Chris@0 374 h
Chris@0 375 end
Chris@0 376 end
Chris@0 377
Chris@0 378 # Deletes all project's members
Chris@0 379 def delete_all_members
Chris@0 380 me, mr = Member.table_name, MemberRole.table_name
Chris@0 381 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
Chris@0 382 Member.delete_all(['project_id = ?', id])
Chris@0 383 end
Chris@0 384
Chris@0 385 # Users issues can be assigned to
Chris@0 386 def assignable_users
Chris@0 387 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
Chris@0 388 end
Chris@0 389
Chris@0 390 # Returns the mail adresses of users that should be always notified on project events
Chris@0 391 def recipients
chris@37 392 notified_users.collect {|user| user.mail}
Chris@0 393 end
Chris@0 394
Chris@0 395 # Returns the users that should be notified on project events
Chris@0 396 def notified_users
chris@37 397 # TODO: User part should be extracted to User#notify_about?
chris@37 398 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
Chris@0 399 end
Chris@0 400
Chris@0 401 # Returns an array of all custom fields enabled for project issues
Chris@0 402 # (explictly associated custom fields and custom fields enabled for all projects)
Chris@0 403 def all_issue_custom_fields
Chris@0 404 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
Chris@0 405 end
Chris@0 406
Chris@0 407 def project
Chris@0 408 self
Chris@0 409 end
Chris@0 410
Chris@0 411 def <=>(project)
Chris@0 412 name.downcase <=> project.name.downcase
Chris@0 413 end
Chris@0 414
Chris@0 415 def to_s
Chris@0 416 name
Chris@0 417 end
Chris@0 418
Chris@0 419 # Returns a short description of the projects (first lines)
Chris@0 420 def short_description(length = 255)
Chris@0 421 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
Chris@0 422 end
chris@22 423
chris@22 424 def css_classes
chris@22 425 s = 'project'
chris@22 426 s << ' root' if root?
chris@22 427 s << ' child' if child?
chris@22 428 s << (leaf? ? ' leaf' : ' parent')
chris@22 429 s
chris@22 430 end
chris@22 431
chris@22 432 # The earliest start date of a project, based on it's issues and versions
chris@22 433 def start_date
chris@22 434 if module_enabled?(:issue_tracking)
chris@22 435 [
chris@22 436 issues.minimum('start_date'),
chris@22 437 shared_versions.collect(&:effective_date),
chris@22 438 shared_versions.collect {|v| v.fixed_issues.minimum('start_date')}
chris@22 439 ].flatten.compact.min
chris@22 440 end
chris@22 441 end
chris@22 442
chris@22 443 # The latest due date of an issue or version
chris@22 444 def due_date
chris@22 445 if module_enabled?(:issue_tracking)
chris@22 446 [
chris@22 447 issues.maximum('due_date'),
chris@22 448 shared_versions.collect(&:effective_date),
chris@22 449 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
chris@22 450 ].flatten.compact.max
chris@22 451 end
chris@22 452 end
chris@22 453
chris@22 454 def overdue?
chris@22 455 active? && !due_date.nil? && (due_date < Date.today)
chris@22 456 end
chris@22 457
chris@22 458 # Returns the percent completed for this project, based on the
chris@22 459 # progress on it's versions.
chris@22 460 def completed_percent(options={:include_subprojects => false})
chris@22 461 if options.delete(:include_subprojects)
chris@22 462 total = self_and_descendants.collect(&:completed_percent).sum
chris@22 463
chris@22 464 total / self_and_descendants.count
chris@22 465 else
chris@22 466 if versions.count > 0
chris@22 467 total = versions.collect(&:completed_pourcent).sum
chris@22 468
chris@22 469 total / versions.count
chris@22 470 else
chris@22 471 100
chris@22 472 end
chris@22 473 end
chris@22 474 end
Chris@0 475
Chris@0 476 # Return true if this project is allowed to do the specified action.
Chris@0 477 # action can be:
Chris@0 478 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
Chris@0 479 # * a permission Symbol (eg. :edit_project)
Chris@0 480 def allows_to?(action)
Chris@0 481 if action.is_a? Hash
Chris@0 482 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
Chris@0 483 else
Chris@0 484 allowed_permissions.include? action
Chris@0 485 end
Chris@0 486 end
Chris@0 487
Chris@0 488 def module_enabled?(module_name)
Chris@0 489 module_name = module_name.to_s
Chris@0 490 enabled_modules.detect {|m| m.name == module_name}
Chris@0 491 end
Chris@0 492
Chris@0 493 def enabled_module_names=(module_names)
Chris@0 494 if module_names && module_names.is_a?(Array)
Chris@0 495 module_names = module_names.collect(&:to_s)
Chris@0 496 # remove disabled modules
Chris@0 497 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
Chris@0 498 # add new modules
Chris@0 499 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
Chris@0 500 else
Chris@0 501 enabled_modules.clear
Chris@0 502 end
Chris@0 503 end
chris@22 504
chris@22 505 # Returns an array of projects that are in this project's hierarchy
chris@22 506 #
chris@22 507 # Example: parents, children, siblings
chris@22 508 def hierarchy
chris@22 509 parents = project.self_and_ancestors || []
chris@22 510 descendants = project.descendants || []
chris@22 511 project_hierarchy = parents | descendants # Set union
chris@22 512 end
Chris@0 513
Chris@0 514 # Returns an auto-generated project identifier based on the last identifier used
Chris@0 515 def self.next_identifier
Chris@0 516 p = Project.find(:first, :order => 'created_on DESC')
Chris@0 517 p.nil? ? nil : p.identifier.to_s.succ
Chris@0 518 end
Chris@0 519
Chris@0 520 # Copies and saves the Project instance based on the +project+.
Chris@0 521 # Duplicates the source project's:
Chris@0 522 # * Wiki
Chris@0 523 # * Versions
Chris@0 524 # * Categories
Chris@0 525 # * Issues
Chris@0 526 # * Members
Chris@0 527 # * Queries
Chris@0 528 #
Chris@0 529 # Accepts an +options+ argument to specify what to copy
Chris@0 530 #
Chris@0 531 # Examples:
Chris@0 532 # project.copy(1) # => copies everything
Chris@0 533 # project.copy(1, :only => 'members') # => copies members only
Chris@0 534 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
Chris@0 535 def copy(project, options={})
Chris@0 536 project = project.is_a?(Project) ? project : Project.find(project)
Chris@0 537
Chris@0 538 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
Chris@0 539 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
Chris@0 540
Chris@0 541 Project.transaction do
Chris@0 542 if save
Chris@0 543 reload
Chris@0 544 to_be_copied.each do |name|
Chris@0 545 send "copy_#{name}", project
Chris@0 546 end
Chris@0 547 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
Chris@0 548 save
Chris@0 549 end
Chris@0 550 end
Chris@0 551 end
Chris@0 552
Chris@0 553
Chris@0 554 # Copies +project+ and returns the new instance. This will not save
Chris@0 555 # the copy
Chris@0 556 def self.copy_from(project)
Chris@0 557 begin
Chris@0 558 project = project.is_a?(Project) ? project : Project.find(project)
Chris@0 559 if project
Chris@0 560 # clear unique attributes
Chris@0 561 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
Chris@0 562 copy = Project.new(attributes)
Chris@0 563 copy.enabled_modules = project.enabled_modules
Chris@0 564 copy.trackers = project.trackers
Chris@0 565 copy.custom_values = project.custom_values.collect {|v| v.clone}
Chris@0 566 copy.issue_custom_fields = project.issue_custom_fields
Chris@0 567 return copy
Chris@0 568 else
Chris@0 569 return nil
Chris@0 570 end
Chris@0 571 rescue ActiveRecord::RecordNotFound
Chris@0 572 return nil
Chris@0 573 end
Chris@0 574 end
chris@37 575
chris@37 576 # Yields the given block for each project with its level in the tree
chris@37 577 def self.project_tree(projects, &block)
chris@37 578 ancestors = []
chris@37 579 projects.sort_by(&:lft).each do |project|
chris@37 580 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
chris@37 581 ancestors.pop
chris@37 582 end
chris@37 583 yield project, ancestors.size
chris@37 584 ancestors << project
chris@37 585 end
chris@37 586 end
Chris@0 587
Chris@0 588 private
Chris@0 589
Chris@0 590 # Destroys children before destroying self
Chris@0 591 def destroy_children
Chris@0 592 children.each do |child|
Chris@0 593 child.destroy
Chris@0 594 end
Chris@0 595 end
Chris@0 596
Chris@0 597 # Copies wiki from +project+
Chris@0 598 def copy_wiki(project)
Chris@0 599 # Check that the source project has a wiki first
Chris@0 600 unless project.wiki.nil?
Chris@0 601 self.wiki ||= Wiki.new
Chris@0 602 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
Chris@0 603 wiki_pages_map = {}
Chris@0 604 project.wiki.pages.each do |page|
Chris@0 605 # Skip pages without content
Chris@0 606 next if page.content.nil?
Chris@0 607 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
Chris@0 608 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
Chris@0 609 new_wiki_page.content = new_wiki_content
Chris@0 610 wiki.pages << new_wiki_page
Chris@0 611 wiki_pages_map[page.id] = new_wiki_page
Chris@0 612 end
Chris@0 613 wiki.save
Chris@0 614 # Reproduce page hierarchy
Chris@0 615 project.wiki.pages.each do |page|
Chris@0 616 if page.parent_id && wiki_pages_map[page.id]
Chris@0 617 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
Chris@0 618 wiki_pages_map[page.id].save
Chris@0 619 end
Chris@0 620 end
Chris@0 621 end
Chris@0 622 end
Chris@0 623
Chris@0 624 # Copies versions from +project+
Chris@0 625 def copy_versions(project)
Chris@0 626 project.versions.each do |version|
Chris@0 627 new_version = Version.new
Chris@0 628 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
Chris@0 629 self.versions << new_version
Chris@0 630 end
Chris@0 631 end
Chris@0 632
Chris@0 633 # Copies issue categories from +project+
Chris@0 634 def copy_issue_categories(project)
Chris@0 635 project.issue_categories.each do |issue_category|
Chris@0 636 new_issue_category = IssueCategory.new
Chris@0 637 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
Chris@0 638 self.issue_categories << new_issue_category
Chris@0 639 end
Chris@0 640 end
Chris@0 641
Chris@0 642 # Copies issues from +project+
Chris@0 643 def copy_issues(project)
Chris@0 644 # Stores the source issue id as a key and the copied issues as the
Chris@0 645 # value. Used to map the two togeather for issue relations.
Chris@0 646 issues_map = {}
Chris@0 647
Chris@0 648 # Get issues sorted by root_id, lft so that parent issues
Chris@0 649 # get copied before their children
Chris@0 650 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
Chris@0 651 new_issue = Issue.new
Chris@0 652 new_issue.copy_from(issue)
Chris@0 653 new_issue.project = self
Chris@0 654 # Reassign fixed_versions by name, since names are unique per
Chris@0 655 # project and the versions for self are not yet saved
Chris@0 656 if issue.fixed_version
Chris@0 657 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
Chris@0 658 end
Chris@0 659 # Reassign the category by name, since names are unique per
Chris@0 660 # project and the categories for self are not yet saved
Chris@0 661 if issue.category
Chris@0 662 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
Chris@0 663 end
Chris@0 664 # Parent issue
Chris@0 665 if issue.parent_id
Chris@0 666 if copied_parent = issues_map[issue.parent_id]
Chris@0 667 new_issue.parent_issue_id = copied_parent.id
Chris@0 668 end
Chris@0 669 end
Chris@0 670
Chris@0 671 self.issues << new_issue
Chris@0 672 issues_map[issue.id] = new_issue
Chris@0 673 end
Chris@0 674
Chris@0 675 # Relations after in case issues related each other
Chris@0 676 project.issues.each do |issue|
Chris@0 677 new_issue = issues_map[issue.id]
Chris@0 678
Chris@0 679 # Relations
Chris@0 680 issue.relations_from.each do |source_relation|
Chris@0 681 new_issue_relation = IssueRelation.new
Chris@0 682 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
Chris@0 683 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
Chris@0 684 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
Chris@0 685 new_issue_relation.issue_to = source_relation.issue_to
Chris@0 686 end
Chris@0 687 new_issue.relations_from << new_issue_relation
Chris@0 688 end
Chris@0 689
Chris@0 690 issue.relations_to.each do |source_relation|
Chris@0 691 new_issue_relation = IssueRelation.new
Chris@0 692 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
Chris@0 693 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
Chris@0 694 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
Chris@0 695 new_issue_relation.issue_from = source_relation.issue_from
Chris@0 696 end
Chris@0 697 new_issue.relations_to << new_issue_relation
Chris@0 698 end
Chris@0 699 end
Chris@0 700 end
Chris@0 701
Chris@0 702 # Copies members from +project+
Chris@0 703 def copy_members(project)
Chris@0 704 project.memberships.each do |member|
Chris@0 705 new_member = Member.new
Chris@0 706 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
Chris@0 707 # only copy non inherited roles
Chris@0 708 # inherited roles will be added when copying the group membership
Chris@0 709 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
Chris@0 710 next if role_ids.empty?
Chris@0 711 new_member.role_ids = role_ids
Chris@0 712 new_member.project = self
Chris@0 713 self.members << new_member
Chris@0 714 end
Chris@0 715 end
Chris@0 716
Chris@0 717 # Copies queries from +project+
Chris@0 718 def copy_queries(project)
Chris@0 719 project.queries.each do |query|
Chris@0 720 new_query = Query.new
Chris@0 721 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
Chris@0 722 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
Chris@0 723 new_query.project = self
Chris@0 724 self.queries << new_query
Chris@0 725 end
Chris@0 726 end
Chris@0 727
Chris@0 728 # Copies boards from +project+
Chris@0 729 def copy_boards(project)
Chris@0 730 project.boards.each do |board|
Chris@0 731 new_board = Board.new
Chris@0 732 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
Chris@0 733 new_board.project = self
Chris@0 734 self.boards << new_board
Chris@0 735 end
Chris@0 736 end
Chris@0 737
Chris@0 738 def allowed_permissions
Chris@0 739 @allowed_permissions ||= begin
Chris@0 740 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
Chris@0 741 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
Chris@0 742 end
Chris@0 743 end
Chris@0 744
Chris@0 745 def allowed_actions
Chris@0 746 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
Chris@0 747 end
Chris@0 748
Chris@0 749 # Returns all the active Systemwide and project specific activities
Chris@0 750 def active_activities
Chris@0 751 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
Chris@0 752
Chris@0 753 if overridden_activity_ids.empty?
Chris@0 754 return TimeEntryActivity.shared.active
Chris@0 755 else
Chris@0 756 return system_activities_and_project_overrides
Chris@0 757 end
Chris@0 758 end
Chris@0 759
Chris@0 760 # Returns all the Systemwide and project specific activities
Chris@0 761 # (inactive and active)
Chris@0 762 def all_activities
Chris@0 763 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
Chris@0 764
Chris@0 765 if overridden_activity_ids.empty?
Chris@0 766 return TimeEntryActivity.shared
Chris@0 767 else
Chris@0 768 return system_activities_and_project_overrides(true)
Chris@0 769 end
Chris@0 770 end
Chris@0 771
Chris@0 772 # Returns the systemwide active activities merged with the project specific overrides
Chris@0 773 def system_activities_and_project_overrides(include_inactive=false)
Chris@0 774 if include_inactive
Chris@0 775 return TimeEntryActivity.shared.
Chris@0 776 find(:all,
Chris@0 777 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
Chris@0 778 self.time_entry_activities
Chris@0 779 else
Chris@0 780 return TimeEntryActivity.shared.active.
Chris@0 781 find(:all,
Chris@0 782 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
Chris@0 783 self.time_entry_activities.active
Chris@0 784 end
Chris@0 785 end
Chris@0 786
Chris@0 787 # Archives subprojects recursively
Chris@0 788 def archive!
Chris@0 789 children.each do |subproject|
Chris@0 790 subproject.send :archive!
Chris@0 791 end
Chris@0 792 update_attribute :status, STATUS_ARCHIVED
Chris@0 793 end
Chris@0 794 end