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