##// END OF EJS Templates
fix typo of Enumeration#overridding_change?...
Toshi MARUYAMA -
r12781:5e0d93b689b5
parent child
Show More
@@ -1,142 +1,142
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Enumeration < ActiveRecord::Base
18 class Enumeration < ActiveRecord::Base
19 include Redmine::SubclassFactory
19 include Redmine::SubclassFactory
20
20
21 default_scope :order => "#{Enumeration.table_name}.position ASC"
21 default_scope :order => "#{Enumeration.table_name}.position ASC"
22
22
23 belongs_to :project
23 belongs_to :project
24
24
25 acts_as_list :scope => 'type = \'#{type}\''
25 acts_as_list :scope => 'type = \'#{type}\''
26 acts_as_customizable
26 acts_as_customizable
27 acts_as_tree :order => "#{Enumeration.table_name}.position ASC"
27 acts_as_tree :order => "#{Enumeration.table_name}.position ASC"
28
28
29 before_destroy :check_integrity
29 before_destroy :check_integrity
30 before_save :check_default
30 before_save :check_default
31
31
32 attr_protected :type
32 attr_protected :type
33
33
34 validates_presence_of :name
34 validates_presence_of :name
35 validates_uniqueness_of :name, :scope => [:type, :project_id]
35 validates_uniqueness_of :name, :scope => [:type, :project_id]
36 validates_length_of :name, :maximum => 30
36 validates_length_of :name, :maximum => 30
37
37
38 scope :shared, lambda { where(:project_id => nil) }
38 scope :shared, lambda { where(:project_id => nil) }
39 scope :sorted, lambda { order("#{table_name}.position ASC") }
39 scope :sorted, lambda { order("#{table_name}.position ASC") }
40 scope :active, lambda { where(:active => true) }
40 scope :active, lambda { where(:active => true) }
41 scope :system, lambda { where(:project_id => nil) }
41 scope :system, lambda { where(:project_id => nil) }
42 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
42 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
43
43
44 def self.default
44 def self.default
45 # Creates a fake default scope so Enumeration.default will check
45 # Creates a fake default scope so Enumeration.default will check
46 # it's type. STI subclasses will automatically add their own
46 # it's type. STI subclasses will automatically add their own
47 # types to the finder.
47 # types to the finder.
48 if self.descends_from_active_record?
48 if self.descends_from_active_record?
49 where(:is_default => true, :type => 'Enumeration').first
49 where(:is_default => true, :type => 'Enumeration').first
50 else
50 else
51 # STI classes are
51 # STI classes are
52 where(:is_default => true).first
52 where(:is_default => true).first
53 end
53 end
54 end
54 end
55
55
56 # Overloaded on concrete classes
56 # Overloaded on concrete classes
57 def option_name
57 def option_name
58 nil
58 nil
59 end
59 end
60
60
61 def check_default
61 def check_default
62 if is_default? && is_default_changed?
62 if is_default? && is_default_changed?
63 Enumeration.where({:type => type}).update_all({:is_default => false})
63 Enumeration.where({:type => type}).update_all({:is_default => false})
64 end
64 end
65 end
65 end
66
66
67 # Overloaded on concrete classes
67 # Overloaded on concrete classes
68 def objects_count
68 def objects_count
69 0
69 0
70 end
70 end
71
71
72 def in_use?
72 def in_use?
73 self.objects_count != 0
73 self.objects_count != 0
74 end
74 end
75
75
76 # Is this enumeration overiding a system level enumeration?
76 # Is this enumeration overiding a system level enumeration?
77 def is_override?
77 def is_override?
78 !self.parent.nil?
78 !self.parent.nil?
79 end
79 end
80
80
81 alias :destroy_without_reassign :destroy
81 alias :destroy_without_reassign :destroy
82
82
83 # Destroy the enumeration
83 # Destroy the enumeration
84 # If a enumeration is specified, objects are reassigned
84 # If a enumeration is specified, objects are reassigned
85 def destroy(reassign_to = nil)
85 def destroy(reassign_to = nil)
86 if reassign_to && reassign_to.is_a?(Enumeration)
86 if reassign_to && reassign_to.is_a?(Enumeration)
87 self.transfer_relations(reassign_to)
87 self.transfer_relations(reassign_to)
88 end
88 end
89 destroy_without_reassign
89 destroy_without_reassign
90 end
90 end
91
91
92 def <=>(enumeration)
92 def <=>(enumeration)
93 position <=> enumeration.position
93 position <=> enumeration.position
94 end
94 end
95
95
96 def to_s; name end
96 def to_s; name end
97
97
98 # Returns the Subclasses of Enumeration. Each Subclass needs to be
98 # Returns the Subclasses of Enumeration. Each Subclass needs to be
99 # required in development mode.
99 # required in development mode.
100 #
100 #
101 # Note: subclasses is protected in ActiveRecord
101 # Note: subclasses is protected in ActiveRecord
102 def self.get_subclasses
102 def self.get_subclasses
103 subclasses
103 subclasses
104 end
104 end
105
105
106 # Does the +new+ Hash override the previous Enumeration?
106 # Does the +new+ Hash override the previous Enumeration?
107 def self.overridding_change?(new, previous)
107 def self.overriding_change?(new, previous)
108 if (same_active_state?(new['active'], previous.active)) && same_custom_values?(new,previous)
108 if (same_active_state?(new['active'], previous.active)) && same_custom_values?(new,previous)
109 return false
109 return false
110 else
110 else
111 return true
111 return true
112 end
112 end
113 end
113 end
114
114
115 # Does the +new+ Hash have the same custom values as the previous Enumeration?
115 # Does the +new+ Hash have the same custom values as the previous Enumeration?
116 def self.same_custom_values?(new, previous)
116 def self.same_custom_values?(new, previous)
117 previous.custom_field_values.each do |custom_value|
117 previous.custom_field_values.each do |custom_value|
118 if custom_value.value != new["custom_field_values"][custom_value.custom_field_id.to_s]
118 if custom_value.value != new["custom_field_values"][custom_value.custom_field_id.to_s]
119 return false
119 return false
120 end
120 end
121 end
121 end
122
122
123 return true
123 return true
124 end
124 end
125
125
126 # Are the new and previous fields equal?
126 # Are the new and previous fields equal?
127 def self.same_active_state?(new, previous)
127 def self.same_active_state?(new, previous)
128 new = (new == "1" ? true : false)
128 new = (new == "1" ? true : false)
129 return new == previous
129 return new == previous
130 end
130 end
131
131
132 private
132 private
133 def check_integrity
133 def check_integrity
134 raise "Can't delete enumeration" if self.in_use?
134 raise "Can't delete enumeration" if self.in_use?
135 end
135 end
136
136
137 end
137 end
138
138
139 # Force load the subclasses in development mode
139 # Force load the subclasses in development mode
140 require_dependency 'time_entry_activity'
140 require_dependency 'time_entry_activity'
141 require_dependency 'document_category'
141 require_dependency 'document_category'
142 require_dependency 'issue_priority'
142 require_dependency 'issue_priority'
@@ -1,1053 +1,1053
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 # Project statuses
21 # Project statuses
22 STATUS_ACTIVE = 1
22 STATUS_ACTIVE = 1
23 STATUS_CLOSED = 5
23 STATUS_CLOSED = 5
24 STATUS_ARCHIVED = 9
24 STATUS_ARCHIVED = 9
25
25
26 # Maximum length for project identifiers
26 # Maximum length for project identifiers
27 IDENTIFIER_MAX_LENGTH = 100
27 IDENTIFIER_MAX_LENGTH = 100
28
28
29 # Specific overidden Activities
29 # Specific overidden Activities
30 has_many :time_entry_activities
30 has_many :time_entry_activities
31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
32 has_many :memberships, :class_name => 'Member'
32 has_many :memberships, :class_name => 'Member'
33 has_many :member_principals, :class_name => 'Member',
33 has_many :member_principals, :class_name => 'Member',
34 :include => :principal,
34 :include => :principal,
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
36
36
37 has_many :enabled_modules, :dependent => :delete_all
37 has_many :enabled_modules, :dependent => :delete_all
38 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
38 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
39 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
39 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
40 has_many :issue_changes, :through => :issues, :source => :journals
40 has_many :issue_changes, :through => :issues, :source => :journals
41 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
41 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
42 has_many :time_entries, :dependent => :destroy
42 has_many :time_entries, :dependent => :destroy
43 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
43 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
44 has_many :documents, :dependent => :destroy
44 has_many :documents, :dependent => :destroy
45 has_many :news, :dependent => :destroy, :include => :author
45 has_many :news, :dependent => :destroy, :include => :author
46 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
46 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
47 has_many :boards, :dependent => :destroy, :order => "position ASC"
47 has_many :boards, :dependent => :destroy, :order => "position ASC"
48 has_one :repository, :conditions => ["is_default = ?", true]
48 has_one :repository, :conditions => ["is_default = ?", true]
49 has_many :repositories, :dependent => :destroy
49 has_many :repositories, :dependent => :destroy
50 has_many :changesets, :through => :repository
50 has_many :changesets, :through => :repository
51 has_one :wiki, :dependent => :destroy
51 has_one :wiki, :dependent => :destroy
52 # Custom field for the project issues
52 # Custom field for the project issues
53 has_and_belongs_to_many :issue_custom_fields,
53 has_and_belongs_to_many :issue_custom_fields,
54 :class_name => 'IssueCustomField',
54 :class_name => 'IssueCustomField',
55 :order => "#{CustomField.table_name}.position",
55 :order => "#{CustomField.table_name}.position",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 :association_foreign_key => 'custom_field_id'
57 :association_foreign_key => 'custom_field_id'
58
58
59 acts_as_nested_set :dependent => :destroy
59 acts_as_nested_set :dependent => :destroy
60 acts_as_attachable :view_permission => :view_files,
60 acts_as_attachable :view_permission => :view_files,
61 :delete_permission => :manage_files
61 :delete_permission => :manage_files
62
62
63 acts_as_customizable
63 acts_as_customizable
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 :author => nil
67 :author => nil
68
68
69 attr_protected :status
69 attr_protected :status
70
70
71 validates_presence_of :name, :identifier
71 validates_presence_of :name, :identifier
72 validates_uniqueness_of :identifier
72 validates_uniqueness_of :identifier
73 validates_associated :repository, :wiki
73 validates_associated :repository, :wiki
74 validates_length_of :name, :maximum => 255
74 validates_length_of :name, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 # donwcase letters, digits, dashes but not digits only
77 # donwcase letters, digits, dashes but not digits only
78 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
78 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
79 # reserved words
79 # reserved words
80 validates_exclusion_of :identifier, :in => %w( new )
80 validates_exclusion_of :identifier, :in => %w( new )
81
81
82 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
82 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
83 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
83 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
84 before_destroy :delete_all_members
84 before_destroy :delete_all_members
85
85
86 scope :has_module, lambda {|mod|
86 scope :has_module, lambda {|mod|
87 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
87 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
88 }
88 }
89 scope :active, lambda { where(:status => STATUS_ACTIVE) }
89 scope :active, lambda { where(:status => STATUS_ACTIVE) }
90 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
90 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
91 scope :all_public, lambda { where(:is_public => true) }
91 scope :all_public, lambda { where(:is_public => true) }
92 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
92 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
93 scope :allowed_to, lambda {|*args|
93 scope :allowed_to, lambda {|*args|
94 user = User.current
94 user = User.current
95 permission = nil
95 permission = nil
96 if args.first.is_a?(Symbol)
96 if args.first.is_a?(Symbol)
97 permission = args.shift
97 permission = args.shift
98 else
98 else
99 user = args.shift
99 user = args.shift
100 permission = args.shift
100 permission = args.shift
101 end
101 end
102 where(Project.allowed_to_condition(user, permission, *args))
102 where(Project.allowed_to_condition(user, permission, *args))
103 }
103 }
104 scope :like, lambda {|arg|
104 scope :like, lambda {|arg|
105 if arg.blank?
105 if arg.blank?
106 where(nil)
106 where(nil)
107 else
107 else
108 pattern = "%#{arg.to_s.strip.downcase}%"
108 pattern = "%#{arg.to_s.strip.downcase}%"
109 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
109 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
110 end
110 end
111 }
111 }
112
112
113 def initialize(attributes=nil, *args)
113 def initialize(attributes=nil, *args)
114 super
114 super
115
115
116 initialized = (attributes || {}).stringify_keys
116 initialized = (attributes || {}).stringify_keys
117 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
117 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
118 self.identifier = Project.next_identifier
118 self.identifier = Project.next_identifier
119 end
119 end
120 if !initialized.key?('is_public')
120 if !initialized.key?('is_public')
121 self.is_public = Setting.default_projects_public?
121 self.is_public = Setting.default_projects_public?
122 end
122 end
123 if !initialized.key?('enabled_module_names')
123 if !initialized.key?('enabled_module_names')
124 self.enabled_module_names = Setting.default_projects_modules
124 self.enabled_module_names = Setting.default_projects_modules
125 end
125 end
126 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
126 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
127 default = Setting.default_projects_tracker_ids
127 default = Setting.default_projects_tracker_ids
128 if default.is_a?(Array)
128 if default.is_a?(Array)
129 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.all
129 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.all
130 else
130 else
131 self.trackers = Tracker.sorted.all
131 self.trackers = Tracker.sorted.all
132 end
132 end
133 end
133 end
134 end
134 end
135
135
136 def identifier=(identifier)
136 def identifier=(identifier)
137 super unless identifier_frozen?
137 super unless identifier_frozen?
138 end
138 end
139
139
140 def identifier_frozen?
140 def identifier_frozen?
141 errors[:identifier].blank? && !(new_record? || identifier.blank?)
141 errors[:identifier].blank? && !(new_record? || identifier.blank?)
142 end
142 end
143
143
144 # returns latest created projects
144 # returns latest created projects
145 # non public projects will be returned only if user is a member of those
145 # non public projects will be returned only if user is a member of those
146 def self.latest(user=nil, count=5)
146 def self.latest(user=nil, count=5)
147 visible(user).limit(count).order("created_on DESC").all
147 visible(user).limit(count).order("created_on DESC").all
148 end
148 end
149
149
150 # Returns true if the project is visible to +user+ or to the current user.
150 # Returns true if the project is visible to +user+ or to the current user.
151 def visible?(user=User.current)
151 def visible?(user=User.current)
152 user.allowed_to?(:view_project, self)
152 user.allowed_to?(:view_project, self)
153 end
153 end
154
154
155 # Returns a SQL conditions string used to find all projects visible by the specified user.
155 # Returns a SQL conditions string used to find all projects visible by the specified user.
156 #
156 #
157 # Examples:
157 # Examples:
158 # Project.visible_condition(admin) => "projects.status = 1"
158 # Project.visible_condition(admin) => "projects.status = 1"
159 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
159 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
160 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
160 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
161 def self.visible_condition(user, options={})
161 def self.visible_condition(user, options={})
162 allowed_to_condition(user, :view_project, options)
162 allowed_to_condition(user, :view_project, options)
163 end
163 end
164
164
165 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
165 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
166 #
166 #
167 # Valid options:
167 # Valid options:
168 # * :project => limit the condition to project
168 # * :project => limit the condition to project
169 # * :with_subprojects => limit the condition to project and its subprojects
169 # * :with_subprojects => limit the condition to project and its subprojects
170 # * :member => limit the condition to the user projects
170 # * :member => limit the condition to the user projects
171 def self.allowed_to_condition(user, permission, options={})
171 def self.allowed_to_condition(user, permission, options={})
172 perm = Redmine::AccessControl.permission(permission)
172 perm = Redmine::AccessControl.permission(permission)
173 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
173 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
174 if perm && perm.project_module
174 if perm && perm.project_module
175 # If the permission belongs to a project module, make sure the module is enabled
175 # If the permission belongs to a project module, make sure the module is enabled
176 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
176 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
177 end
177 end
178 if options[:project]
178 if options[:project]
179 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
179 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
180 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
180 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
181 base_statement = "(#{project_statement}) AND (#{base_statement})"
181 base_statement = "(#{project_statement}) AND (#{base_statement})"
182 end
182 end
183
183
184 if user.admin?
184 if user.admin?
185 base_statement
185 base_statement
186 else
186 else
187 statement_by_role = {}
187 statement_by_role = {}
188 unless options[:member]
188 unless options[:member]
189 role = user.builtin_role
189 role = user.builtin_role
190 if role.allowed_to?(permission)
190 if role.allowed_to?(permission)
191 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
191 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
192 end
192 end
193 end
193 end
194 if user.logged?
194 if user.logged?
195 user.projects_by_role.each do |role, projects|
195 user.projects_by_role.each do |role, projects|
196 if role.allowed_to?(permission) && projects.any?
196 if role.allowed_to?(permission) && projects.any?
197 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
197 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
198 end
198 end
199 end
199 end
200 end
200 end
201 if statement_by_role.empty?
201 if statement_by_role.empty?
202 "1=0"
202 "1=0"
203 else
203 else
204 if block_given?
204 if block_given?
205 statement_by_role.each do |role, statement|
205 statement_by_role.each do |role, statement|
206 if s = yield(role, user)
206 if s = yield(role, user)
207 statement_by_role[role] = "(#{statement} AND (#{s}))"
207 statement_by_role[role] = "(#{statement} AND (#{s}))"
208 end
208 end
209 end
209 end
210 end
210 end
211 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
211 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
212 end
212 end
213 end
213 end
214 end
214 end
215
215
216 def principals
216 def principals
217 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
217 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
218 end
218 end
219
219
220 def users
220 def users
221 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
221 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
222 end
222 end
223
223
224 # Returns the Systemwide and project specific activities
224 # Returns the Systemwide and project specific activities
225 def activities(include_inactive=false)
225 def activities(include_inactive=false)
226 if include_inactive
226 if include_inactive
227 return all_activities
227 return all_activities
228 else
228 else
229 return active_activities
229 return active_activities
230 end
230 end
231 end
231 end
232
232
233 # Will create a new Project specific Activity or update an existing one
233 # Will create a new Project specific Activity or update an existing one
234 #
234 #
235 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
235 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
236 # does not successfully save.
236 # does not successfully save.
237 def update_or_create_time_entry_activity(id, activity_hash)
237 def update_or_create_time_entry_activity(id, activity_hash)
238 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
238 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
239 self.create_time_entry_activity_if_needed(activity_hash)
239 self.create_time_entry_activity_if_needed(activity_hash)
240 else
240 else
241 activity = project.time_entry_activities.find_by_id(id.to_i)
241 activity = project.time_entry_activities.find_by_id(id.to_i)
242 activity.update_attributes(activity_hash) if activity
242 activity.update_attributes(activity_hash) if activity
243 end
243 end
244 end
244 end
245
245
246 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
246 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
247 #
247 #
248 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
248 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
249 # does not successfully save.
249 # does not successfully save.
250 def create_time_entry_activity_if_needed(activity)
250 def create_time_entry_activity_if_needed(activity)
251 if activity['parent_id']
251 if activity['parent_id']
252 parent_activity = TimeEntryActivity.find(activity['parent_id'])
252 parent_activity = TimeEntryActivity.find(activity['parent_id'])
253 activity['name'] = parent_activity.name
253 activity['name'] = parent_activity.name
254 activity['position'] = parent_activity.position
254 activity['position'] = parent_activity.position
255 if Enumeration.overridding_change?(activity, parent_activity)
255 if Enumeration.overriding_change?(activity, parent_activity)
256 project_activity = self.time_entry_activities.create(activity)
256 project_activity = self.time_entry_activities.create(activity)
257 if project_activity.new_record?
257 if project_activity.new_record?
258 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
258 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
259 else
259 else
260 self.time_entries.
260 self.time_entries.
261 where(["activity_id = ?", parent_activity.id]).
261 where(["activity_id = ?", parent_activity.id]).
262 update_all("activity_id = #{project_activity.id}")
262 update_all("activity_id = #{project_activity.id}")
263 end
263 end
264 end
264 end
265 end
265 end
266 end
266 end
267
267
268 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
268 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
269 #
269 #
270 # Examples:
270 # Examples:
271 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
271 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
272 # project.project_condition(false) => "projects.id = 1"
272 # project.project_condition(false) => "projects.id = 1"
273 def project_condition(with_subprojects)
273 def project_condition(with_subprojects)
274 cond = "#{Project.table_name}.id = #{id}"
274 cond = "#{Project.table_name}.id = #{id}"
275 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
275 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
276 cond
276 cond
277 end
277 end
278
278
279 def self.find(*args)
279 def self.find(*args)
280 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
280 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
281 project = find_by_identifier(*args)
281 project = find_by_identifier(*args)
282 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
282 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
283 project
283 project
284 else
284 else
285 super
285 super
286 end
286 end
287 end
287 end
288
288
289 def self.find_by_param(*args)
289 def self.find_by_param(*args)
290 self.find(*args)
290 self.find(*args)
291 end
291 end
292
292
293 alias :base_reload :reload
293 alias :base_reload :reload
294 def reload(*args)
294 def reload(*args)
295 @principals = nil
295 @principals = nil
296 @users = nil
296 @users = nil
297 @shared_versions = nil
297 @shared_versions = nil
298 @rolled_up_versions = nil
298 @rolled_up_versions = nil
299 @rolled_up_trackers = nil
299 @rolled_up_trackers = nil
300 @all_issue_custom_fields = nil
300 @all_issue_custom_fields = nil
301 @all_time_entry_custom_fields = nil
301 @all_time_entry_custom_fields = nil
302 @to_param = nil
302 @to_param = nil
303 @allowed_parents = nil
303 @allowed_parents = nil
304 @allowed_permissions = nil
304 @allowed_permissions = nil
305 @actions_allowed = nil
305 @actions_allowed = nil
306 @start_date = nil
306 @start_date = nil
307 @due_date = nil
307 @due_date = nil
308 base_reload(*args)
308 base_reload(*args)
309 end
309 end
310
310
311 def to_param
311 def to_param
312 # id is used for projects with a numeric identifier (compatibility)
312 # id is used for projects with a numeric identifier (compatibility)
313 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
313 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
314 end
314 end
315
315
316 def active?
316 def active?
317 self.status == STATUS_ACTIVE
317 self.status == STATUS_ACTIVE
318 end
318 end
319
319
320 def archived?
320 def archived?
321 self.status == STATUS_ARCHIVED
321 self.status == STATUS_ARCHIVED
322 end
322 end
323
323
324 # Archives the project and its descendants
324 # Archives the project and its descendants
325 def archive
325 def archive
326 # Check that there is no issue of a non descendant project that is assigned
326 # Check that there is no issue of a non descendant project that is assigned
327 # to one of the project or descendant versions
327 # to one of the project or descendant versions
328 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
328 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
329 if v_ids.any? &&
329 if v_ids.any? &&
330 Issue.
330 Issue.
331 includes(:project).
331 includes(:project).
332 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
332 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
333 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
333 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
334 exists?
334 exists?
335 return false
335 return false
336 end
336 end
337 Project.transaction do
337 Project.transaction do
338 archive!
338 archive!
339 end
339 end
340 true
340 true
341 end
341 end
342
342
343 # Unarchives the project
343 # Unarchives the project
344 # All its ancestors must be active
344 # All its ancestors must be active
345 def unarchive
345 def unarchive
346 return false if ancestors.detect {|a| !a.active?}
346 return false if ancestors.detect {|a| !a.active?}
347 update_attribute :status, STATUS_ACTIVE
347 update_attribute :status, STATUS_ACTIVE
348 end
348 end
349
349
350 def close
350 def close
351 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
351 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
352 end
352 end
353
353
354 def reopen
354 def reopen
355 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
355 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
356 end
356 end
357
357
358 # Returns an array of projects the project can be moved to
358 # Returns an array of projects the project can be moved to
359 # by the current user
359 # by the current user
360 def allowed_parents
360 def allowed_parents
361 return @allowed_parents if @allowed_parents
361 return @allowed_parents if @allowed_parents
362 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
362 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
363 @allowed_parents = @allowed_parents - self_and_descendants
363 @allowed_parents = @allowed_parents - self_and_descendants
364 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
364 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
365 @allowed_parents << nil
365 @allowed_parents << nil
366 end
366 end
367 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
367 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
368 @allowed_parents << parent
368 @allowed_parents << parent
369 end
369 end
370 @allowed_parents
370 @allowed_parents
371 end
371 end
372
372
373 # Sets the parent of the project with authorization check
373 # Sets the parent of the project with authorization check
374 def set_allowed_parent!(p)
374 def set_allowed_parent!(p)
375 unless p.nil? || p.is_a?(Project)
375 unless p.nil? || p.is_a?(Project)
376 if p.to_s.blank?
376 if p.to_s.blank?
377 p = nil
377 p = nil
378 else
378 else
379 p = Project.find_by_id(p)
379 p = Project.find_by_id(p)
380 return false unless p
380 return false unless p
381 end
381 end
382 end
382 end
383 if p.nil?
383 if p.nil?
384 if !new_record? && allowed_parents.empty?
384 if !new_record? && allowed_parents.empty?
385 return false
385 return false
386 end
386 end
387 elsif !allowed_parents.include?(p)
387 elsif !allowed_parents.include?(p)
388 return false
388 return false
389 end
389 end
390 set_parent!(p)
390 set_parent!(p)
391 end
391 end
392
392
393 # Sets the parent of the project
393 # Sets the parent of the project
394 # Argument can be either a Project, a String, a Fixnum or nil
394 # Argument can be either a Project, a String, a Fixnum or nil
395 def set_parent!(p)
395 def set_parent!(p)
396 unless p.nil? || p.is_a?(Project)
396 unless p.nil? || p.is_a?(Project)
397 if p.to_s.blank?
397 if p.to_s.blank?
398 p = nil
398 p = nil
399 else
399 else
400 p = Project.find_by_id(p)
400 p = Project.find_by_id(p)
401 return false unless p
401 return false unless p
402 end
402 end
403 end
403 end
404 if p == parent && !p.nil?
404 if p == parent && !p.nil?
405 # Nothing to do
405 # Nothing to do
406 true
406 true
407 elsif p.nil? || (p.active? && move_possible?(p))
407 elsif p.nil? || (p.active? && move_possible?(p))
408 set_or_update_position_under(p)
408 set_or_update_position_under(p)
409 Issue.update_versions_from_hierarchy_change(self)
409 Issue.update_versions_from_hierarchy_change(self)
410 true
410 true
411 else
411 else
412 # Can not move to the given target
412 # Can not move to the given target
413 false
413 false
414 end
414 end
415 end
415 end
416
416
417 # Recalculates all lft and rgt values based on project names
417 # Recalculates all lft and rgt values based on project names
418 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
418 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
419 # Used in BuildProjectsTree migration
419 # Used in BuildProjectsTree migration
420 def self.rebuild_tree!
420 def self.rebuild_tree!
421 transaction do
421 transaction do
422 update_all "lft = NULL, rgt = NULL"
422 update_all "lft = NULL, rgt = NULL"
423 rebuild!(false)
423 rebuild!(false)
424 all.each { |p| p.set_or_update_position_under(p.parent) }
424 all.each { |p| p.set_or_update_position_under(p.parent) }
425 end
425 end
426 end
426 end
427
427
428 # Returns an array of the trackers used by the project and its active sub projects
428 # Returns an array of the trackers used by the project and its active sub projects
429 def rolled_up_trackers
429 def rolled_up_trackers
430 @rolled_up_trackers ||=
430 @rolled_up_trackers ||=
431 Tracker.
431 Tracker.
432 joins(:projects).
432 joins(:projects).
433 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
433 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
434 select("DISTINCT #{Tracker.table_name}.*").
434 select("DISTINCT #{Tracker.table_name}.*").
435 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
435 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
436 sorted.
436 sorted.
437 all
437 all
438 end
438 end
439
439
440 # Closes open and locked project versions that are completed
440 # Closes open and locked project versions that are completed
441 def close_completed_versions
441 def close_completed_versions
442 Version.transaction do
442 Version.transaction do
443 versions.where(:status => %w(open locked)).each do |version|
443 versions.where(:status => %w(open locked)).each do |version|
444 if version.completed?
444 if version.completed?
445 version.update_attribute(:status, 'closed')
445 version.update_attribute(:status, 'closed')
446 end
446 end
447 end
447 end
448 end
448 end
449 end
449 end
450
450
451 # Returns a scope of the Versions on subprojects
451 # Returns a scope of the Versions on subprojects
452 def rolled_up_versions
452 def rolled_up_versions
453 @rolled_up_versions ||=
453 @rolled_up_versions ||=
454 Version.
454 Version.
455 includes(:project).
455 includes(:project).
456 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
456 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
457 end
457 end
458
458
459 # Returns a scope of the Versions used by the project
459 # Returns a scope of the Versions used by the project
460 def shared_versions
460 def shared_versions
461 if new_record?
461 if new_record?
462 Version.
462 Version.
463 includes(:project).
463 includes(:project).
464 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
464 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
465 else
465 else
466 @shared_versions ||= begin
466 @shared_versions ||= begin
467 r = root? ? self : root
467 r = root? ? self : root
468 Version.
468 Version.
469 includes(:project).
469 includes(:project).
470 where("#{Project.table_name}.id = #{id}" +
470 where("#{Project.table_name}.id = #{id}" +
471 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
471 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
472 " #{Version.table_name}.sharing = 'system'" +
472 " #{Version.table_name}.sharing = 'system'" +
473 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
473 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
474 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
474 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
475 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
475 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
476 "))")
476 "))")
477 end
477 end
478 end
478 end
479 end
479 end
480
480
481 # Returns a hash of project users grouped by role
481 # Returns a hash of project users grouped by role
482 def users_by_role
482 def users_by_role
483 members.includes(:user, :roles).inject({}) do |h, m|
483 members.includes(:user, :roles).inject({}) do |h, m|
484 m.roles.each do |r|
484 m.roles.each do |r|
485 h[r] ||= []
485 h[r] ||= []
486 h[r] << m.user
486 h[r] << m.user
487 end
487 end
488 h
488 h
489 end
489 end
490 end
490 end
491
491
492 # Deletes all project's members
492 # Deletes all project's members
493 def delete_all_members
493 def delete_all_members
494 me, mr = Member.table_name, MemberRole.table_name
494 me, mr = Member.table_name, MemberRole.table_name
495 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
495 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
496 Member.delete_all(['project_id = ?', id])
496 Member.delete_all(['project_id = ?', id])
497 end
497 end
498
498
499 # Users/groups issues can be assigned to
499 # Users/groups issues can be assigned to
500 def assignable_users
500 def assignable_users
501 assignable = Setting.issue_group_assignment? ? member_principals : members
501 assignable = Setting.issue_group_assignment? ? member_principals : members
502 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
502 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
503 end
503 end
504
504
505 # Returns the mail adresses of users that should be always notified on project events
505 # Returns the mail adresses of users that should be always notified on project events
506 def recipients
506 def recipients
507 notified_users.collect {|user| user.mail}
507 notified_users.collect {|user| user.mail}
508 end
508 end
509
509
510 # Returns the users that should be notified on project events
510 # Returns the users that should be notified on project events
511 def notified_users
511 def notified_users
512 # TODO: User part should be extracted to User#notify_about?
512 # TODO: User part should be extracted to User#notify_about?
513 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
513 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
514 end
514 end
515
515
516 # Returns a scope of all custom fields enabled for project issues
516 # Returns a scope of all custom fields enabled for project issues
517 # (explictly associated custom fields and custom fields enabled for all projects)
517 # (explictly associated custom fields and custom fields enabled for all projects)
518 def all_issue_custom_fields
518 def all_issue_custom_fields
519 @all_issue_custom_fields ||= IssueCustomField.
519 @all_issue_custom_fields ||= IssueCustomField.
520 sorted.
520 sorted.
521 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
521 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
522 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
522 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
523 " WHERE cfp.project_id = ?)", true, id)
523 " WHERE cfp.project_id = ?)", true, id)
524 end
524 end
525
525
526 # Returns an array of all custom fields enabled for project time entries
526 # Returns an array of all custom fields enabled for project time entries
527 # (explictly associated custom fields and custom fields enabled for all projects)
527 # (explictly associated custom fields and custom fields enabled for all projects)
528 def all_time_entry_custom_fields
528 def all_time_entry_custom_fields
529 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
529 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
530 end
530 end
531
531
532 def project
532 def project
533 self
533 self
534 end
534 end
535
535
536 def <=>(project)
536 def <=>(project)
537 name.downcase <=> project.name.downcase
537 name.downcase <=> project.name.downcase
538 end
538 end
539
539
540 def to_s
540 def to_s
541 name
541 name
542 end
542 end
543
543
544 # Returns a short description of the projects (first lines)
544 # Returns a short description of the projects (first lines)
545 def short_description(length = 255)
545 def short_description(length = 255)
546 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
546 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
547 end
547 end
548
548
549 def css_classes
549 def css_classes
550 s = 'project'
550 s = 'project'
551 s << ' root' if root?
551 s << ' root' if root?
552 s << ' child' if child?
552 s << ' child' if child?
553 s << (leaf? ? ' leaf' : ' parent')
553 s << (leaf? ? ' leaf' : ' parent')
554 unless active?
554 unless active?
555 if archived?
555 if archived?
556 s << ' archived'
556 s << ' archived'
557 else
557 else
558 s << ' closed'
558 s << ' closed'
559 end
559 end
560 end
560 end
561 s
561 s
562 end
562 end
563
563
564 # The earliest start date of a project, based on it's issues and versions
564 # The earliest start date of a project, based on it's issues and versions
565 def start_date
565 def start_date
566 @start_date ||= [
566 @start_date ||= [
567 issues.minimum('start_date'),
567 issues.minimum('start_date'),
568 shared_versions.minimum('effective_date'),
568 shared_versions.minimum('effective_date'),
569 Issue.fixed_version(shared_versions).minimum('start_date')
569 Issue.fixed_version(shared_versions).minimum('start_date')
570 ].compact.min
570 ].compact.min
571 end
571 end
572
572
573 # The latest due date of an issue or version
573 # The latest due date of an issue or version
574 def due_date
574 def due_date
575 @due_date ||= [
575 @due_date ||= [
576 issues.maximum('due_date'),
576 issues.maximum('due_date'),
577 shared_versions.maximum('effective_date'),
577 shared_versions.maximum('effective_date'),
578 Issue.fixed_version(shared_versions).maximum('due_date')
578 Issue.fixed_version(shared_versions).maximum('due_date')
579 ].compact.max
579 ].compact.max
580 end
580 end
581
581
582 def overdue?
582 def overdue?
583 active? && !due_date.nil? && (due_date < Date.today)
583 active? && !due_date.nil? && (due_date < Date.today)
584 end
584 end
585
585
586 # Returns the percent completed for this project, based on the
586 # Returns the percent completed for this project, based on the
587 # progress on it's versions.
587 # progress on it's versions.
588 def completed_percent(options={:include_subprojects => false})
588 def completed_percent(options={:include_subprojects => false})
589 if options.delete(:include_subprojects)
589 if options.delete(:include_subprojects)
590 total = self_and_descendants.collect(&:completed_percent).sum
590 total = self_and_descendants.collect(&:completed_percent).sum
591
591
592 total / self_and_descendants.count
592 total / self_and_descendants.count
593 else
593 else
594 if versions.count > 0
594 if versions.count > 0
595 total = versions.collect(&:completed_percent).sum
595 total = versions.collect(&:completed_percent).sum
596
596
597 total / versions.count
597 total / versions.count
598 else
598 else
599 100
599 100
600 end
600 end
601 end
601 end
602 end
602 end
603
603
604 # Return true if this project allows to do the specified action.
604 # Return true if this project allows to do the specified action.
605 # action can be:
605 # action can be:
606 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
606 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
607 # * a permission Symbol (eg. :edit_project)
607 # * a permission Symbol (eg. :edit_project)
608 def allows_to?(action)
608 def allows_to?(action)
609 if archived?
609 if archived?
610 # No action allowed on archived projects
610 # No action allowed on archived projects
611 return false
611 return false
612 end
612 end
613 unless active? || Redmine::AccessControl.read_action?(action)
613 unless active? || Redmine::AccessControl.read_action?(action)
614 # No write action allowed on closed projects
614 # No write action allowed on closed projects
615 return false
615 return false
616 end
616 end
617 # No action allowed on disabled modules
617 # No action allowed on disabled modules
618 if action.is_a? Hash
618 if action.is_a? Hash
619 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
619 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
620 else
620 else
621 allowed_permissions.include? action
621 allowed_permissions.include? action
622 end
622 end
623 end
623 end
624
624
625 # Return the enabled module with the given name
625 # Return the enabled module with the given name
626 # or nil if the module is not enabled for the project
626 # or nil if the module is not enabled for the project
627 def enabled_module(name)
627 def enabled_module(name)
628 name = name.to_s
628 name = name.to_s
629 enabled_modules.detect {|m| m.name == name}
629 enabled_modules.detect {|m| m.name == name}
630 end
630 end
631
631
632 # Return true if the module with the given name is enabled
632 # Return true if the module with the given name is enabled
633 def module_enabled?(name)
633 def module_enabled?(name)
634 enabled_module(name).present?
634 enabled_module(name).present?
635 end
635 end
636
636
637 def enabled_module_names=(module_names)
637 def enabled_module_names=(module_names)
638 if module_names && module_names.is_a?(Array)
638 if module_names && module_names.is_a?(Array)
639 module_names = module_names.collect(&:to_s).reject(&:blank?)
639 module_names = module_names.collect(&:to_s).reject(&:blank?)
640 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
640 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
641 else
641 else
642 enabled_modules.clear
642 enabled_modules.clear
643 end
643 end
644 end
644 end
645
645
646 # Returns an array of the enabled modules names
646 # Returns an array of the enabled modules names
647 def enabled_module_names
647 def enabled_module_names
648 enabled_modules.collect(&:name)
648 enabled_modules.collect(&:name)
649 end
649 end
650
650
651 # Enable a specific module
651 # Enable a specific module
652 #
652 #
653 # Examples:
653 # Examples:
654 # project.enable_module!(:issue_tracking)
654 # project.enable_module!(:issue_tracking)
655 # project.enable_module!("issue_tracking")
655 # project.enable_module!("issue_tracking")
656 def enable_module!(name)
656 def enable_module!(name)
657 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
657 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
658 end
658 end
659
659
660 # Disable a module if it exists
660 # Disable a module if it exists
661 #
661 #
662 # Examples:
662 # Examples:
663 # project.disable_module!(:issue_tracking)
663 # project.disable_module!(:issue_tracking)
664 # project.disable_module!("issue_tracking")
664 # project.disable_module!("issue_tracking")
665 # project.disable_module!(project.enabled_modules.first)
665 # project.disable_module!(project.enabled_modules.first)
666 def disable_module!(target)
666 def disable_module!(target)
667 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
667 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
668 target.destroy unless target.blank?
668 target.destroy unless target.blank?
669 end
669 end
670
670
671 safe_attributes 'name',
671 safe_attributes 'name',
672 'description',
672 'description',
673 'homepage',
673 'homepage',
674 'is_public',
674 'is_public',
675 'identifier',
675 'identifier',
676 'custom_field_values',
676 'custom_field_values',
677 'custom_fields',
677 'custom_fields',
678 'tracker_ids',
678 'tracker_ids',
679 'issue_custom_field_ids'
679 'issue_custom_field_ids'
680
680
681 safe_attributes 'enabled_module_names',
681 safe_attributes 'enabled_module_names',
682 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
682 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
683
683
684 safe_attributes 'inherit_members',
684 safe_attributes 'inherit_members',
685 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
685 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
686
686
687 # Returns an array of projects that are in this project's hierarchy
687 # Returns an array of projects that are in this project's hierarchy
688 #
688 #
689 # Example: parents, children, siblings
689 # Example: parents, children, siblings
690 def hierarchy
690 def hierarchy
691 parents = project.self_and_ancestors || []
691 parents = project.self_and_ancestors || []
692 descendants = project.descendants || []
692 descendants = project.descendants || []
693 project_hierarchy = parents | descendants # Set union
693 project_hierarchy = parents | descendants # Set union
694 end
694 end
695
695
696 # Returns an auto-generated project identifier based on the last identifier used
696 # Returns an auto-generated project identifier based on the last identifier used
697 def self.next_identifier
697 def self.next_identifier
698 p = Project.order('id DESC').first
698 p = Project.order('id DESC').first
699 p.nil? ? nil : p.identifier.to_s.succ
699 p.nil? ? nil : p.identifier.to_s.succ
700 end
700 end
701
701
702 # Copies and saves the Project instance based on the +project+.
702 # Copies and saves the Project instance based on the +project+.
703 # Duplicates the source project's:
703 # Duplicates the source project's:
704 # * Wiki
704 # * Wiki
705 # * Versions
705 # * Versions
706 # * Categories
706 # * Categories
707 # * Issues
707 # * Issues
708 # * Members
708 # * Members
709 # * Queries
709 # * Queries
710 #
710 #
711 # Accepts an +options+ argument to specify what to copy
711 # Accepts an +options+ argument to specify what to copy
712 #
712 #
713 # Examples:
713 # Examples:
714 # project.copy(1) # => copies everything
714 # project.copy(1) # => copies everything
715 # project.copy(1, :only => 'members') # => copies members only
715 # project.copy(1, :only => 'members') # => copies members only
716 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
716 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
717 def copy(project, options={})
717 def copy(project, options={})
718 project = project.is_a?(Project) ? project : Project.find(project)
718 project = project.is_a?(Project) ? project : Project.find(project)
719
719
720 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
720 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
721 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
721 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
722
722
723 Project.transaction do
723 Project.transaction do
724 if save
724 if save
725 reload
725 reload
726 to_be_copied.each do |name|
726 to_be_copied.each do |name|
727 send "copy_#{name}", project
727 send "copy_#{name}", project
728 end
728 end
729 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
729 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
730 save
730 save
731 end
731 end
732 end
732 end
733 end
733 end
734
734
735 # Returns a new unsaved Project instance with attributes copied from +project+
735 # Returns a new unsaved Project instance with attributes copied from +project+
736 def self.copy_from(project)
736 def self.copy_from(project)
737 project = project.is_a?(Project) ? project : Project.find(project)
737 project = project.is_a?(Project) ? project : Project.find(project)
738 # clear unique attributes
738 # clear unique attributes
739 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
739 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
740 copy = Project.new(attributes)
740 copy = Project.new(attributes)
741 copy.enabled_modules = project.enabled_modules
741 copy.enabled_modules = project.enabled_modules
742 copy.trackers = project.trackers
742 copy.trackers = project.trackers
743 copy.custom_values = project.custom_values.collect {|v| v.clone}
743 copy.custom_values = project.custom_values.collect {|v| v.clone}
744 copy.issue_custom_fields = project.issue_custom_fields
744 copy.issue_custom_fields = project.issue_custom_fields
745 copy
745 copy
746 end
746 end
747
747
748 # Yields the given block for each project with its level in the tree
748 # Yields the given block for each project with its level in the tree
749 def self.project_tree(projects, &block)
749 def self.project_tree(projects, &block)
750 ancestors = []
750 ancestors = []
751 projects.sort_by(&:lft).each do |project|
751 projects.sort_by(&:lft).each do |project|
752 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
752 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
753 ancestors.pop
753 ancestors.pop
754 end
754 end
755 yield project, ancestors.size
755 yield project, ancestors.size
756 ancestors << project
756 ancestors << project
757 end
757 end
758 end
758 end
759
759
760 private
760 private
761
761
762 def after_parent_changed(parent_was)
762 def after_parent_changed(parent_was)
763 remove_inherited_member_roles
763 remove_inherited_member_roles
764 add_inherited_member_roles
764 add_inherited_member_roles
765 end
765 end
766
766
767 def update_inherited_members
767 def update_inherited_members
768 if parent
768 if parent
769 if inherit_members? && !inherit_members_was
769 if inherit_members? && !inherit_members_was
770 remove_inherited_member_roles
770 remove_inherited_member_roles
771 add_inherited_member_roles
771 add_inherited_member_roles
772 elsif !inherit_members? && inherit_members_was
772 elsif !inherit_members? && inherit_members_was
773 remove_inherited_member_roles
773 remove_inherited_member_roles
774 end
774 end
775 end
775 end
776 end
776 end
777
777
778 def remove_inherited_member_roles
778 def remove_inherited_member_roles
779 member_roles = memberships.map(&:member_roles).flatten
779 member_roles = memberships.map(&:member_roles).flatten
780 member_role_ids = member_roles.map(&:id)
780 member_role_ids = member_roles.map(&:id)
781 member_roles.each do |member_role|
781 member_roles.each do |member_role|
782 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
782 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
783 member_role.destroy
783 member_role.destroy
784 end
784 end
785 end
785 end
786 end
786 end
787
787
788 def add_inherited_member_roles
788 def add_inherited_member_roles
789 if inherit_members? && parent
789 if inherit_members? && parent
790 parent.memberships.each do |parent_member|
790 parent.memberships.each do |parent_member|
791 member = Member.find_or_new(self.id, parent_member.user_id)
791 member = Member.find_or_new(self.id, parent_member.user_id)
792 parent_member.member_roles.each do |parent_member_role|
792 parent_member.member_roles.each do |parent_member_role|
793 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
793 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
794 end
794 end
795 member.save!
795 member.save!
796 end
796 end
797 end
797 end
798 end
798 end
799
799
800 # Copies wiki from +project+
800 # Copies wiki from +project+
801 def copy_wiki(project)
801 def copy_wiki(project)
802 # Check that the source project has a wiki first
802 # Check that the source project has a wiki first
803 unless project.wiki.nil?
803 unless project.wiki.nil?
804 wiki = self.wiki || Wiki.new
804 wiki = self.wiki || Wiki.new
805 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
805 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
806 wiki_pages_map = {}
806 wiki_pages_map = {}
807 project.wiki.pages.each do |page|
807 project.wiki.pages.each do |page|
808 # Skip pages without content
808 # Skip pages without content
809 next if page.content.nil?
809 next if page.content.nil?
810 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
810 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
811 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
811 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
812 new_wiki_page.content = new_wiki_content
812 new_wiki_page.content = new_wiki_content
813 wiki.pages << new_wiki_page
813 wiki.pages << new_wiki_page
814 wiki_pages_map[page.id] = new_wiki_page
814 wiki_pages_map[page.id] = new_wiki_page
815 end
815 end
816
816
817 self.wiki = wiki
817 self.wiki = wiki
818 wiki.save
818 wiki.save
819 # Reproduce page hierarchy
819 # Reproduce page hierarchy
820 project.wiki.pages.each do |page|
820 project.wiki.pages.each do |page|
821 if page.parent_id && wiki_pages_map[page.id]
821 if page.parent_id && wiki_pages_map[page.id]
822 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
822 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
823 wiki_pages_map[page.id].save
823 wiki_pages_map[page.id].save
824 end
824 end
825 end
825 end
826 end
826 end
827 end
827 end
828
828
829 # Copies versions from +project+
829 # Copies versions from +project+
830 def copy_versions(project)
830 def copy_versions(project)
831 project.versions.each do |version|
831 project.versions.each do |version|
832 new_version = Version.new
832 new_version = Version.new
833 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
833 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
834 self.versions << new_version
834 self.versions << new_version
835 end
835 end
836 end
836 end
837
837
838 # Copies issue categories from +project+
838 # Copies issue categories from +project+
839 def copy_issue_categories(project)
839 def copy_issue_categories(project)
840 project.issue_categories.each do |issue_category|
840 project.issue_categories.each do |issue_category|
841 new_issue_category = IssueCategory.new
841 new_issue_category = IssueCategory.new
842 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
842 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
843 self.issue_categories << new_issue_category
843 self.issue_categories << new_issue_category
844 end
844 end
845 end
845 end
846
846
847 # Copies issues from +project+
847 # Copies issues from +project+
848 def copy_issues(project)
848 def copy_issues(project)
849 # Stores the source issue id as a key and the copied issues as the
849 # Stores the source issue id as a key and the copied issues as the
850 # value. Used to map the two togeather for issue relations.
850 # value. Used to map the two togeather for issue relations.
851 issues_map = {}
851 issues_map = {}
852
852
853 # Store status and reopen locked/closed versions
853 # Store status and reopen locked/closed versions
854 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
854 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
855 version_statuses.each do |version, status|
855 version_statuses.each do |version, status|
856 version.update_attribute :status, 'open'
856 version.update_attribute :status, 'open'
857 end
857 end
858
858
859 # Get issues sorted by root_id, lft so that parent issues
859 # Get issues sorted by root_id, lft so that parent issues
860 # get copied before their children
860 # get copied before their children
861 project.issues.reorder('root_id, lft').each do |issue|
861 project.issues.reorder('root_id, lft').each do |issue|
862 new_issue = Issue.new
862 new_issue = Issue.new
863 new_issue.copy_from(issue, :subtasks => false, :link => false)
863 new_issue.copy_from(issue, :subtasks => false, :link => false)
864 new_issue.project = self
864 new_issue.project = self
865 # Changing project resets the custom field values
865 # Changing project resets the custom field values
866 # TODO: handle this in Issue#project=
866 # TODO: handle this in Issue#project=
867 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
867 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
868 # Reassign fixed_versions by name, since names are unique per project
868 # Reassign fixed_versions by name, since names are unique per project
869 if issue.fixed_version && issue.fixed_version.project == project
869 if issue.fixed_version && issue.fixed_version.project == project
870 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
870 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
871 end
871 end
872 # Reassign the category by name, since names are unique per project
872 # Reassign the category by name, since names are unique per project
873 if issue.category
873 if issue.category
874 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
874 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
875 end
875 end
876 # Parent issue
876 # Parent issue
877 if issue.parent_id
877 if issue.parent_id
878 if copied_parent = issues_map[issue.parent_id]
878 if copied_parent = issues_map[issue.parent_id]
879 new_issue.parent_issue_id = copied_parent.id
879 new_issue.parent_issue_id = copied_parent.id
880 end
880 end
881 end
881 end
882
882
883 self.issues << new_issue
883 self.issues << new_issue
884 if new_issue.new_record?
884 if new_issue.new_record?
885 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
885 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
886 else
886 else
887 issues_map[issue.id] = new_issue unless new_issue.new_record?
887 issues_map[issue.id] = new_issue unless new_issue.new_record?
888 end
888 end
889 end
889 end
890
890
891 # Restore locked/closed version statuses
891 # Restore locked/closed version statuses
892 version_statuses.each do |version, status|
892 version_statuses.each do |version, status|
893 version.update_attribute :status, status
893 version.update_attribute :status, status
894 end
894 end
895
895
896 # Relations after in case issues related each other
896 # Relations after in case issues related each other
897 project.issues.each do |issue|
897 project.issues.each do |issue|
898 new_issue = issues_map[issue.id]
898 new_issue = issues_map[issue.id]
899 unless new_issue
899 unless new_issue
900 # Issue was not copied
900 # Issue was not copied
901 next
901 next
902 end
902 end
903
903
904 # Relations
904 # Relations
905 issue.relations_from.each do |source_relation|
905 issue.relations_from.each do |source_relation|
906 new_issue_relation = IssueRelation.new
906 new_issue_relation = IssueRelation.new
907 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
907 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
908 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
908 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
909 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
909 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
910 new_issue_relation.issue_to = source_relation.issue_to
910 new_issue_relation.issue_to = source_relation.issue_to
911 end
911 end
912 new_issue.relations_from << new_issue_relation
912 new_issue.relations_from << new_issue_relation
913 end
913 end
914
914
915 issue.relations_to.each do |source_relation|
915 issue.relations_to.each do |source_relation|
916 new_issue_relation = IssueRelation.new
916 new_issue_relation = IssueRelation.new
917 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
917 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
918 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
918 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
919 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
919 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
920 new_issue_relation.issue_from = source_relation.issue_from
920 new_issue_relation.issue_from = source_relation.issue_from
921 end
921 end
922 new_issue.relations_to << new_issue_relation
922 new_issue.relations_to << new_issue_relation
923 end
923 end
924 end
924 end
925 end
925 end
926
926
927 # Copies members from +project+
927 # Copies members from +project+
928 def copy_members(project)
928 def copy_members(project)
929 # Copy users first, then groups to handle members with inherited and given roles
929 # Copy users first, then groups to handle members with inherited and given roles
930 members_to_copy = []
930 members_to_copy = []
931 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
931 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
932 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
932 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
933
933
934 members_to_copy.each do |member|
934 members_to_copy.each do |member|
935 new_member = Member.new
935 new_member = Member.new
936 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
936 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
937 # only copy non inherited roles
937 # only copy non inherited roles
938 # inherited roles will be added when copying the group membership
938 # inherited roles will be added when copying the group membership
939 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
939 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
940 next if role_ids.empty?
940 next if role_ids.empty?
941 new_member.role_ids = role_ids
941 new_member.role_ids = role_ids
942 new_member.project = self
942 new_member.project = self
943 self.members << new_member
943 self.members << new_member
944 end
944 end
945 end
945 end
946
946
947 # Copies queries from +project+
947 # Copies queries from +project+
948 def copy_queries(project)
948 def copy_queries(project)
949 project.queries.each do |query|
949 project.queries.each do |query|
950 new_query = IssueQuery.new
950 new_query = IssueQuery.new
951 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
951 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
952 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
952 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
953 new_query.project = self
953 new_query.project = self
954 new_query.user_id = query.user_id
954 new_query.user_id = query.user_id
955 self.queries << new_query
955 self.queries << new_query
956 end
956 end
957 end
957 end
958
958
959 # Copies boards from +project+
959 # Copies boards from +project+
960 def copy_boards(project)
960 def copy_boards(project)
961 project.boards.each do |board|
961 project.boards.each do |board|
962 new_board = Board.new
962 new_board = Board.new
963 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
963 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
964 new_board.project = self
964 new_board.project = self
965 self.boards << new_board
965 self.boards << new_board
966 end
966 end
967 end
967 end
968
968
969 def allowed_permissions
969 def allowed_permissions
970 @allowed_permissions ||= begin
970 @allowed_permissions ||= begin
971 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
971 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
972 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
972 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
973 end
973 end
974 end
974 end
975
975
976 def allowed_actions
976 def allowed_actions
977 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
977 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
978 end
978 end
979
979
980 # Returns all the active Systemwide and project specific activities
980 # Returns all the active Systemwide and project specific activities
981 def active_activities
981 def active_activities
982 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
982 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
983
983
984 if overridden_activity_ids.empty?
984 if overridden_activity_ids.empty?
985 return TimeEntryActivity.shared.active
985 return TimeEntryActivity.shared.active
986 else
986 else
987 return system_activities_and_project_overrides
987 return system_activities_and_project_overrides
988 end
988 end
989 end
989 end
990
990
991 # Returns all the Systemwide and project specific activities
991 # Returns all the Systemwide and project specific activities
992 # (inactive and active)
992 # (inactive and active)
993 def all_activities
993 def all_activities
994 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
994 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
995
995
996 if overridden_activity_ids.empty?
996 if overridden_activity_ids.empty?
997 return TimeEntryActivity.shared
997 return TimeEntryActivity.shared
998 else
998 else
999 return system_activities_and_project_overrides(true)
999 return system_activities_and_project_overrides(true)
1000 end
1000 end
1001 end
1001 end
1002
1002
1003 # Returns the systemwide active activities merged with the project specific overrides
1003 # Returns the systemwide active activities merged with the project specific overrides
1004 def system_activities_and_project_overrides(include_inactive=false)
1004 def system_activities_and_project_overrides(include_inactive=false)
1005 t = TimeEntryActivity.table_name
1005 t = TimeEntryActivity.table_name
1006 scope = TimeEntryActivity.where(
1006 scope = TimeEntryActivity.where(
1007 "(#{t}.project_id IS NULL AND #{t}.id NOT IN (?)) OR (#{t}.project_id = ?)",
1007 "(#{t}.project_id IS NULL AND #{t}.id NOT IN (?)) OR (#{t}.project_id = ?)",
1008 time_entry_activities.map(&:parent_id), id
1008 time_entry_activities.map(&:parent_id), id
1009 )
1009 )
1010 unless include_inactive
1010 unless include_inactive
1011 scope = scope.active
1011 scope = scope.active
1012 end
1012 end
1013 scope
1013 scope
1014 end
1014 end
1015
1015
1016 # Archives subprojects recursively
1016 # Archives subprojects recursively
1017 def archive!
1017 def archive!
1018 children.each do |subproject|
1018 children.each do |subproject|
1019 subproject.send :archive!
1019 subproject.send :archive!
1020 end
1020 end
1021 update_attribute :status, STATUS_ARCHIVED
1021 update_attribute :status, STATUS_ARCHIVED
1022 end
1022 end
1023
1023
1024 def update_position_under_parent
1024 def update_position_under_parent
1025 set_or_update_position_under(parent)
1025 set_or_update_position_under(parent)
1026 end
1026 end
1027
1027
1028 public
1028 public
1029
1029
1030 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
1030 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
1031 def set_or_update_position_under(target_parent)
1031 def set_or_update_position_under(target_parent)
1032 parent_was = parent
1032 parent_was = parent
1033 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1033 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1034 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1034 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1035
1035
1036 if to_be_inserted_before
1036 if to_be_inserted_before
1037 move_to_left_of(to_be_inserted_before)
1037 move_to_left_of(to_be_inserted_before)
1038 elsif target_parent.nil?
1038 elsif target_parent.nil?
1039 if sibs.empty?
1039 if sibs.empty?
1040 # move_to_root adds the project in first (ie. left) position
1040 # move_to_root adds the project in first (ie. left) position
1041 move_to_root
1041 move_to_root
1042 else
1042 else
1043 move_to_right_of(sibs.last) unless self == sibs.last
1043 move_to_right_of(sibs.last) unless self == sibs.last
1044 end
1044 end
1045 else
1045 else
1046 # move_to_child_of adds the project in last (ie.right) position
1046 # move_to_child_of adds the project in last (ie.right) position
1047 move_to_child_of(target_parent)
1047 move_to_child_of(target_parent)
1048 end
1048 end
1049 if parent_was != target_parent
1049 if parent_was != target_parent
1050 after_parent_changed(parent_was)
1050 after_parent_changed(parent_was)
1051 end
1051 end
1052 end
1052 end
1053 end
1053 end
General Comments 0
You need to be logged in to leave comments. Login now