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