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