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