annotate app/models/project.rb @ 8:0c83d98252d9 yuya

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