# 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',
+ 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, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
has_many :changesets, :through => :repository
has_one :wiki, :dependent => :destroy
# Custom field for the project issues
- has_and_belongs_to_many :issue_custom_fields,
+ 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
:author => nil
attr_protected :status
-
+
validates_presence_of :name, :identifier
validates_uniqueness_of :identifier
validates_associated :repository, :wiki
named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
named_scope :all_public, { :conditions => { :is_public => true } }
named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
-
+
def initialize(attributes = nil)
super
-
+
initialized = (attributes || {}).stringify_keys
- if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
+ if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
self.identifier = Project.next_identifier
end
if !initialized.key?('is_public')
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
def visible?(user=User.current)
user.allowed_to?(:view_project, self)
end
-
+
def self.visible_by(user=nil)
ActiveSupport::Deprecation.warn "Project.visible_by is deprecated and will be removed in Redmine 1.3.0. Use Project.visible_condition instead."
visible_condition(user || User.current)
end
-
+
# Returns a SQL conditions string used to find all projects visible by the specified user.
#
# Examples:
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_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
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
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)
super
end
end
-
+
def to_param
# id is used for projects with a numeric identifier (compatibility)
@to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : 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
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
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)
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)
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 ||=
: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
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
@shared_versions ||= begin
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
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
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')
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?)
enabled_modules.clear
end
end
-
+
# Returns an array of the enabled modules names
def enabled_module_names
enabled_modules.collect(&:name)
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
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')
# 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
end
end
-
+
# Copies +project+ and returns the new instance. This will not save
# the copy
def self.copy_from(project)
def self.project_tree(projects, &block)
ancestors = []
projects.sort_by(&:lft).each do |project|
- while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
+ 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
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.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
# Issue was not copied
next
end
-
+
# Relations
issue.relations_from.each do |source_relation|
new_issue_relation = IssueRelation.new
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")
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")
self.boards << new_board
end
end
-
+
def allowed_permissions
@allowed_permissions ||= begin
module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
# 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
self.time_entry_activities.active
end
end
-
+
# Archives subprojects recursively
def archive!
children.each do |subproject|