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