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