123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922 |
- # Redmine - project management software
- # Copyright (C) 2006-2011 Jean-Philippe Lang
- #
- # This program is free software; you can redistribute it and/or
- # modify it under the terms of the GNU General Public License
- # as published by the Free Software Foundation; either version 2
- # of the License, or (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program; if not, write to the Free Software
- # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
- class Project < ActiveRecord::Base
- include Redmine::SafeAttributes
-
- # Project statuses
- STATUS_ACTIVE = 1
- STATUS_ARCHIVED = 9
-
- # Maximum length for project identifiers
- IDENTIFIER_MAX_LENGTH = 100
-
- # Specific overidden Activities
- has_many :time_entry_activities
- has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
- has_many :memberships, :class_name => 'Member'
- has_many :member_principals, :class_name => 'Member',
- :include => :principal,
- :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
- has_many :users, :through => :members
- has_many :principals, :through => :member_principals, :source => :principal
-
- has_many :enabled_modules, :dependent => :delete_all
- has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
- has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
- has_many :issue_changes, :through => :issues, :source => :journals
- has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
- has_many :time_entries, :dependent => :delete_all
- has_many :queries, :dependent => :delete_all
- has_many :documents, :dependent => :destroy
- has_many :news, :dependent => :destroy, :include => :author
- has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
- has_many :boards, :dependent => :destroy, :order => "position ASC"
- has_one :repository, :conditions => ["is_default = ?", true]
- has_many :repositories, :dependent => :destroy
- has_many :changesets, :through => :repository
- has_one :wiki, :dependent => :destroy
- # Custom field for the project issues
- has_and_belongs_to_many :issue_custom_fields,
- :class_name => 'IssueCustomField',
- :order => "#{CustomField.table_name}.position",
- :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
- :association_foreign_key => 'custom_field_id'
-
- acts_as_nested_set :order => 'name', :dependent => :destroy
- acts_as_attachable :view_permission => :view_files,
- :delete_permission => :manage_files
-
- acts_as_customizable
- acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
- acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
- :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
- :author => nil
-
- attr_protected :status
-
- validates_presence_of :name, :identifier
- validates_uniqueness_of :identifier
- validates_associated :repository, :wiki
- validates_length_of :name, :maximum => 255
- validates_length_of :homepage, :maximum => 255
- validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
- # donwcase letters, digits, dashes but not digits only
- validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :if => Proc.new { |p| p.identifier_changed? }
- # reserved words
- validates_exclusion_of :identifier, :in => %w( new )
-
- before_destroy :delete_all_members
-
- 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] } }
- named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
- named_scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
- named_scope :all_public, { :conditions => { :is_public => true } }
- named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
- named_scope :allowed_to, lambda {|*args|
- user = User.current
- permission = nil
- if args.first.is_a?(Symbol)
- permission = args.shift
- else
- user = args.shift
- permission = args.shift
- end
- { :conditions => Project.allowed_to_condition(user, permission, *args) }
- }
- named_scope :like, lambda {|arg|
- if arg.blank?
- {}
- else
- pattern = "%#{arg.to_s.strip.downcase}%"
- {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
- end
- }
-
- def initialize(attributes=nil, *args)
- super
-
- initialized = (attributes || {}).stringify_keys
- if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
- self.identifier = Project.next_identifier
- end
- if !initialized.key?('is_public')
- self.is_public = Setting.default_projects_public?
- end
- if !initialized.key?('enabled_module_names')
- self.enabled_module_names = Setting.default_projects_modules
- end
- if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
- self.trackers = Tracker.all
- end
- end
-
- def identifier=(identifier)
- super unless identifier_frozen?
- end
-
- def identifier_frozen?
- errors[:identifier].nil? && !(new_record? || identifier.blank?)
- end
-
- # returns latest created projects
- # non public projects will be returned only if user is a member of those
- def self.latest(user=nil, count=5)
- visible(user).find(:all, :limit => count, :order => "created_on DESC")
- end
-
- # Returns true if the project is visible to +user+ or to the current user.
- def visible?(user=User.current)
- user.allowed_to?(:view_project, self)
- end
-
- # Returns a SQL conditions string used to find all projects visible by the specified user.
- #
- # Examples:
- # Project.visible_condition(admin) => "projects.status = 1"
- # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
- # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
- def self.visible_condition(user, options={})
- allowed_to_condition(user, :view_project, options)
- end
-
- # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
- #
- # Valid options:
- # * :project => limit the condition to project
- # * :with_subprojects => limit the condition to project and its subprojects
- # * :member => limit the condition to the user projects
- def self.allowed_to_condition(user, permission, options={})
- base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
- if perm = Redmine::AccessControl.permission(permission)
- unless perm.project_module.nil?
- # If the permission belongs to a project module, make sure the module is enabled
- base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
- end
- end
- if options[:project]
- project_statement = "#{Project.table_name}.id = #{options[:project].id}"
- project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
- base_statement = "(#{project_statement}) AND (#{base_statement})"
- end
-
- if user.admin?
- base_statement
- else
- statement_by_role = {}
- unless options[:member]
- role = user.logged? ? Role.non_member : Role.anonymous
- if role.allowed_to?(permission)
- statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
- end
- end
- if user.logged?
- user.projects_by_role.each do |role, projects|
- if role.allowed_to?(permission)
- statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
- end
- end
- end
- if statement_by_role.empty?
- "1=0"
- else
- if block_given?
- statement_by_role.each do |role, statement|
- if s = yield(role, user)
- statement_by_role[role] = "(#{statement} AND (#{s}))"
- end
- end
- end
- "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
- end
- end
- end
-
- # Returns the Systemwide and project specific activities
- def activities(include_inactive=false)
- if include_inactive
- return all_activities
- else
- return active_activities
- end
- end
-
- # Will create a new Project specific Activity or update an existing one
- #
- # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
- # does not successfully save.
- def update_or_create_time_entry_activity(id, activity_hash)
- if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
- self.create_time_entry_activity_if_needed(activity_hash)
- else
- activity = project.time_entry_activities.find_by_id(id.to_i)
- activity.update_attributes(activity_hash) if activity
- end
- end
-
- # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
- #
- # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
- # does not successfully save.
- def create_time_entry_activity_if_needed(activity)
- if activity['parent_id']
-
- parent_activity = TimeEntryActivity.find(activity['parent_id'])
- activity['name'] = parent_activity.name
- activity['position'] = parent_activity.position
-
- if Enumeration.overridding_change?(activity, parent_activity)
- project_activity = self.time_entry_activities.create(activity)
-
- if project_activity.new_record?
- raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
- else
- self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
- end
- end
- end
- end
-
- # Returns a :conditions SQL string that can be used to find the issues associated with this project.
- #
- # Examples:
- # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
- # project.project_condition(false) => "projects.id = 1"
- def project_condition(with_subprojects)
- cond = "#{Project.table_name}.id = #{id}"
- cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
- cond
- end
-
- def self.find(*args)
- if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
- project = find_by_identifier(*args)
- raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
- project
- else
- super
- end
- end
-
- def self.find_by_param(*args)
- self.find(*args)
- end
-
- def reload(*args)
- @shared_versions = nil
- @rolled_up_versions = nil
- @rolled_up_trackers = nil
- @all_issue_custom_fields = nil
- @all_time_entry_custom_fields = nil
- @to_param = nil
- @allowed_parents = nil
- @allowed_permissions = nil
- @actions_allowed = nil
- super
- end
-
- def to_param
- # id is used for projects with a numeric identifier (compatibility)
- @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
- end
-
- def active?
- self.status == STATUS_ACTIVE
- end
-
- def archived?
- self.status == STATUS_ARCHIVED
- end
-
- # Archives the project and its descendants
- def archive
- # Check that there is no issue of a non descendant project that is assigned
- # to one of the project or descendant versions
- v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
- if v_ids.any? && Issue.find(:first, :include => :project,
- :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
- " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
- return false
- end
- Project.transaction do
- archive!
- end
- true
- end
-
- # Unarchives the project
- # All its ancestors must be active
- def unarchive
- return false if ancestors.detect {|a| !a.active?}
- update_attribute :status, STATUS_ACTIVE
- end
-
- # Returns an array of projects the project can be moved to
- # by the current user
- def allowed_parents
- return @allowed_parents if @allowed_parents
- @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
- @allowed_parents = @allowed_parents - self_and_descendants
- if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
- @allowed_parents << nil
- end
- unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
- @allowed_parents << parent
- end
- @allowed_parents
- end
-
- # Sets the parent of the project with authorization check
- def set_allowed_parent!(p)
- unless p.nil? || p.is_a?(Project)
- if p.to_s.blank?
- p = nil
- else
- p = Project.find_by_id(p)
- return false unless p
- end
- end
- if p.nil?
- if !new_record? && allowed_parents.empty?
- return false
- end
- elsif !allowed_parents.include?(p)
- return false
- end
- set_parent!(p)
- end
-
- # Sets the parent of the project
- # Argument can be either a Project, a String, a Fixnum or nil
- def set_parent!(p)
- unless p.nil? || p.is_a?(Project)
- if p.to_s.blank?
- p = nil
- else
- p = Project.find_by_id(p)
- return false unless p
- end
- end
- if p == parent && !p.nil?
- # Nothing to do
- true
- elsif p.nil? || (p.active? && move_possible?(p))
- # Insert the project so that target's children or root projects stay alphabetically sorted
- sibs = (p.nil? ? self.class.roots : p.children)
- to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
- if to_be_inserted_before
- move_to_left_of(to_be_inserted_before)
- elsif p.nil?
- if sibs.empty?
- # move_to_root adds the project in first (ie. left) position
- move_to_root
- else
- move_to_right_of(sibs.last) unless self == sibs.last
- end
- else
- # move_to_child_of adds the project in last (ie.right) position
- move_to_child_of(p)
- end
- Issue.update_versions_from_hierarchy_change(self)
- true
- else
- # Can not move to the given target
- false
- end
- end
-
- # Returns an array of the trackers used by the project and its active sub projects
- def rolled_up_trackers
- @rolled_up_trackers ||=
- Tracker.find(:all, :joins => :projects,
- :select => "DISTINCT #{Tracker.table_name}.*",
- :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
- :order => "#{Tracker.table_name}.position")
- end
-
- # Closes open and locked project versions that are completed
- def close_completed_versions
- Version.transaction do
- versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
- if version.completed?
- version.update_attribute(:status, 'closed')
- end
- end
- end
- end
-
- # Returns a scope of the Versions on subprojects
- def rolled_up_versions
- @rolled_up_versions ||=
- Version.scoped(:include => :project,
- :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
- end
-
- # Returns a scope of the Versions used by the project
- def shared_versions
- if new_record?
- Version.scoped(:include => :project,
- :conditions => "#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND #{Version.table_name}.sharing = 'system'")
- else
- @shared_versions ||= begin
- r = root? ? self : root
- Version.scoped(:include => :project,
- :conditions => "#{Project.table_name}.id = #{id}" +
- " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
- " #{Version.table_name}.sharing = 'system'" +
- " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
- " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
- " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
- "))")
- end
- end
- end
-
- # Returns a hash of project users grouped by role
- def users_by_role
- members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
- m.roles.each do |r|
- h[r] ||= []
- h[r] << m.user
- end
- h
- end
- end
-
- # Deletes all project's members
- def delete_all_members
- me, mr = Member.table_name, MemberRole.table_name
- connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
- Member.delete_all(['project_id = ?', id])
- end
-
- # Users/groups issues can be assigned to
- def assignable_users
- assignable = Setting.issue_group_assignment? ? member_principals : members
- assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
- end
-
- # Returns the mail adresses of users that should be always notified on project events
- def recipients
- notified_users.collect {|user| user.mail}
- end
-
- # Returns the users that should be notified on project events
- def notified_users
- # TODO: User part should be extracted to User#notify_about?
- members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
- end
-
- # Returns an array of all custom fields enabled for project issues
- # (explictly associated custom fields and custom fields enabled for all projects)
- def all_issue_custom_fields
- @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
- end
-
- # Returns an array of all custom fields enabled for project time entries
- # (explictly associated custom fields and custom fields enabled for all projects)
- def all_time_entry_custom_fields
- @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
- end
-
- def project
- self
- end
-
- def <=>(project)
- name.downcase <=> project.name.downcase
- end
-
- def to_s
- name
- end
-
- # Returns a short description of the projects (first lines)
- def short_description(length = 255)
- description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
- end
-
- def css_classes
- s = 'project'
- s << ' root' if root?
- s << ' child' if child?
- s << (leaf? ? ' leaf' : ' parent')
- s
- end
-
- # The earliest start date of a project, based on it's issues and versions
- def start_date
- [
- issues.minimum('start_date'),
- shared_versions.collect(&:effective_date),
- shared_versions.collect(&:start_date)
- ].flatten.compact.min
- end
-
- # The latest due date of an issue or version
- def due_date
- [
- issues.maximum('due_date'),
- shared_versions.collect(&:effective_date),
- shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
- ].flatten.compact.max
- end
-
- def overdue?
- active? && !due_date.nil? && (due_date < Date.today)
- end
-
- # Returns the percent completed for this project, based on the
- # progress on it's versions.
- def completed_percent(options={:include_subprojects => false})
- if options.delete(:include_subprojects)
- total = self_and_descendants.collect(&:completed_percent).sum
-
- total / self_and_descendants.count
- else
- if versions.count > 0
- total = versions.collect(&:completed_pourcent).sum
-
- total / versions.count
- else
- 100
- end
- end
- end
-
- # Return true if this project is allowed to do the specified action.
- # action can be:
- # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
- # * a permission Symbol (eg. :edit_project)
- def allows_to?(action)
- if action.is_a? Hash
- allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
- else
- allowed_permissions.include? action
- end
- end
-
- def module_enabled?(module_name)
- module_name = module_name.to_s
- enabled_modules.detect {|m| m.name == module_name}
- end
-
- def enabled_module_names=(module_names)
- if module_names && module_names.is_a?(Array)
- module_names = module_names.collect(&:to_s).reject(&:blank?)
- self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
- else
- enabled_modules.clear
- end
- end
-
- # Returns an array of the enabled modules names
- def enabled_module_names
- enabled_modules.collect(&:name)
- end
-
- # Enable a specific module
- #
- # Examples:
- # project.enable_module!(:issue_tracking)
- # project.enable_module!("issue_tracking")
- def enable_module!(name)
- enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
- end
-
- # Disable a module if it exists
- #
- # Examples:
- # project.disable_module!(:issue_tracking)
- # project.disable_module!("issue_tracking")
- # project.disable_module!(project.enabled_modules.first)
- def disable_module!(target)
- target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
- target.destroy unless target.blank?
- end
-
- safe_attributes 'name',
- 'description',
- 'homepage',
- 'is_public',
- 'identifier',
- 'custom_field_values',
- 'custom_fields',
- 'tracker_ids',
- 'issue_custom_field_ids'
-
- safe_attributes 'enabled_module_names',
- :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
-
- # Returns an array of projects that are in this project's hierarchy
- #
- # Example: parents, children, siblings
- def hierarchy
- parents = project.self_and_ancestors || []
- descendants = project.descendants || []
- project_hierarchy = parents | descendants # Set union
- end
-
- # Returns an auto-generated project identifier based on the last identifier used
- def self.next_identifier
- p = Project.find(:first, :order => 'created_on DESC')
- p.nil? ? nil : p.identifier.to_s.succ
- end
-
- # Copies and saves the Project instance based on the +project+.
- # Duplicates the source project's:
- # * Wiki
- # * Versions
- # * Categories
- # * Issues
- # * Members
- # * Queries
- #
- # Accepts an +options+ argument to specify what to copy
- #
- # Examples:
- # project.copy(1) # => copies everything
- # project.copy(1, :only => 'members') # => copies members only
- # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
- def copy(project, options={})
- project = project.is_a?(Project) ? project : Project.find(project)
-
- to_be_copied = %w(wiki versions issue_categories issues members queries boards)
- to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
-
- Project.transaction do
- if save
- reload
- to_be_copied.each do |name|
- send "copy_#{name}", project
- end
- Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
- save
- end
- end
- end
-
-
- # Copies +project+ and returns the new instance. This will not save
- # the copy
- def self.copy_from(project)
- begin
- project = project.is_a?(Project) ? project : Project.find(project)
- if project
- # clear unique attributes
- attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
- copy = Project.new(attributes)
- copy.enabled_modules = project.enabled_modules
- copy.trackers = project.trackers
- copy.custom_values = project.custom_values.collect {|v| v.clone}
- copy.issue_custom_fields = project.issue_custom_fields
- return copy
- else
- return nil
- end
- rescue ActiveRecord::RecordNotFound
- return nil
- end
- end
-
- # Yields the given block for each project with its level in the tree
- def self.project_tree(projects, &block)
- ancestors = []
- projects.sort_by(&:lft).each do |project|
- while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
- ancestors.pop
- end
- yield project, ancestors.size
- ancestors << project
- end
- end
-
- private
-
- # Copies wiki from +project+
- def copy_wiki(project)
- # Check that the source project has a wiki first
- unless project.wiki.nil?
- self.wiki ||= Wiki.new
- wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
- wiki_pages_map = {}
- project.wiki.pages.each do |page|
- # Skip pages without content
- next if page.content.nil?
- new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
- new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
- new_wiki_page.content = new_wiki_content
- wiki.pages << new_wiki_page
- wiki_pages_map[page.id] = new_wiki_page
- end
- wiki.save
- # Reproduce page hierarchy
- project.wiki.pages.each do |page|
- if page.parent_id && wiki_pages_map[page.id]
- wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
- wiki_pages_map[page.id].save
- end
- end
- end
- end
-
- # Copies versions from +project+
- def copy_versions(project)
- project.versions.each do |version|
- new_version = Version.new
- new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
- self.versions << new_version
- end
- end
-
- # Copies issue categories from +project+
- def copy_issue_categories(project)
- project.issue_categories.each do |issue_category|
- new_issue_category = IssueCategory.new
- new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
- self.issue_categories << new_issue_category
- end
- end
-
- # Copies issues from +project+
- # Note: issues assigned to a closed version won't be copied due to validation rules
- def copy_issues(project)
- # Stores the source issue id as a key and the copied issues as the
- # value. Used to map the two togeather for issue relations.
- issues_map = {}
-
- # Get issues sorted by root_id, lft so that parent issues
- # get copied before their children
- project.issues.find(:all, :order => 'root_id, lft').each do |issue|
- new_issue = Issue.new
- new_issue.copy_from(issue)
- new_issue.project = self
- # Reassign fixed_versions by name, since names are unique per
- # project and the versions for self are not yet saved
- if issue.fixed_version
- new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
- end
- # Reassign the category by name, since names are unique per
- # project and the categories for self are not yet saved
- if issue.category
- new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
- end
- # Parent issue
- if issue.parent_id
- if copied_parent = issues_map[issue.parent_id]
- new_issue.parent_issue_id = copied_parent.id
- end
- end
-
- self.issues << new_issue
- if new_issue.new_record?
- logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
- else
- issues_map[issue.id] = new_issue unless new_issue.new_record?
- end
- end
-
- # Relations after in case issues related each other
- project.issues.each do |issue|
- new_issue = issues_map[issue.id]
- unless new_issue
- # Issue was not copied
- next
- end
-
- # Relations
- issue.relations_from.each do |source_relation|
- new_issue_relation = IssueRelation.new
- new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
- new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
- if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
- new_issue_relation.issue_to = source_relation.issue_to
- end
- new_issue.relations_from << new_issue_relation
- end
-
- issue.relations_to.each do |source_relation|
- new_issue_relation = IssueRelation.new
- new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
- new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
- if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
- new_issue_relation.issue_from = source_relation.issue_from
- end
- new_issue.relations_to << new_issue_relation
- end
- end
- end
-
- # Copies members from +project+
- def copy_members(project)
- # Copy users first, then groups to handle members with inherited and given roles
- members_to_copy = []
- members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
- members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
-
- members_to_copy.each do |member|
- new_member = Member.new
- new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
- # only copy non inherited roles
- # inherited roles will be added when copying the group membership
- role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
- next if role_ids.empty?
- new_member.role_ids = role_ids
- new_member.project = self
- self.members << new_member
- end
- end
-
- # Copies queries from +project+
- def copy_queries(project)
- project.queries.each do |query|
- new_query = ::Query.new
- new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
- new_query.sort_criteria = query.sort_criteria if query.sort_criteria
- new_query.project = self
- new_query.user_id = query.user_id
- self.queries << new_query
- end
- end
-
- # Copies boards from +project+
- def copy_boards(project)
- project.boards.each do |board|
- new_board = Board.new
- new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
- new_board.project = self
- self.boards << new_board
- end
- end
-
- def allowed_permissions
- @allowed_permissions ||= begin
- module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
- Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
- end
- end
-
- def allowed_actions
- @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
- end
-
- # Returns all the active Systemwide and project specific activities
- def active_activities
- overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
-
- if overridden_activity_ids.empty?
- return TimeEntryActivity.shared.active
- else
- return system_activities_and_project_overrides
- end
- end
-
- # Returns all the Systemwide and project specific activities
- # (inactive and active)
- def all_activities
- overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
-
- if overridden_activity_ids.empty?
- return TimeEntryActivity.shared
- else
- return system_activities_and_project_overrides(true)
- end
- end
-
- # Returns the systemwide active activities merged with the project specific overrides
- def system_activities_and_project_overrides(include_inactive=false)
- if include_inactive
- return TimeEntryActivity.shared.
- find(:all,
- :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
- self.time_entry_activities
- else
- return TimeEntryActivity.shared.active.
- find(:all,
- :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
- self.time_entry_activities.active
- end
- end
-
- # Archives subprojects recursively
- def archive!
- children.each do |subproject|
- subproject.send :archive!
- end
- update_attribute :status, STATUS_ARCHIVED
- end
- end
|