##// END OF EJS Templates
back out r12680...
Toshi MARUYAMA -
r12406:89c43d24c82f
parent child
Show More
@@ -1,1043 +1,1043
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 :order_column => '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.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 end
424 end
425 end
425 end
426
426
427 # Returns an array of the trackers used by the project and its active sub projects
427 # Returns an array of the trackers used by the project and its active sub projects
428 def rolled_up_trackers
428 def rolled_up_trackers
429 @rolled_up_trackers ||=
429 @rolled_up_trackers ||=
430 Tracker.
430 Tracker.
431 joins(:projects).
431 joins(:projects).
432 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
432 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
433 select("DISTINCT #{Tracker.table_name}.*").
433 select("DISTINCT #{Tracker.table_name}.*").
434 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
434 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
435 sorted.
435 sorted.
436 all
436 all
437 end
437 end
438
438
439 # Closes open and locked project versions that are completed
439 # Closes open and locked project versions that are completed
440 def close_completed_versions
440 def close_completed_versions
441 Version.transaction do
441 Version.transaction do
442 versions.where(:status => %w(open locked)).all.each do |version|
442 versions.where(:status => %w(open locked)).all.each do |version|
443 if version.completed?
443 if version.completed?
444 version.update_attribute(:status, 'closed')
444 version.update_attribute(:status, 'closed')
445 end
445 end
446 end
446 end
447 end
447 end
448 end
448 end
449
449
450 # Returns a scope of the Versions on subprojects
450 # Returns a scope of the Versions on subprojects
451 def rolled_up_versions
451 def rolled_up_versions
452 @rolled_up_versions ||=
452 @rolled_up_versions ||=
453 Version.
453 Version.
454 includes(:project).
454 includes(:project).
455 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
455 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
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.
461 Version.
462 includes(:project).
462 includes(:project).
463 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
463 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
464 else
464 else
465 @shared_versions ||= begin
465 @shared_versions ||= begin
466 r = root? ? self : root
466 r = root? ? self : root
467 Version.
467 Version.
468 includes(:project).
468 includes(:project).
469 where("#{Project.table_name}.id = #{id}" +
469 where("#{Project.table_name}.id = #{id}" +
470 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
470 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
471 " #{Version.table_name}.sharing = 'system'" +
471 " #{Version.table_name}.sharing = 'system'" +
472 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
472 " 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 < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
473 " 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 = 'hierarchy')" +
474 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
475 "))")
475 "))")
476 end
476 end
477 end
477 end
478 end
478 end
479
479
480 # Returns a hash of project users grouped by role
480 # Returns a hash of project users grouped by role
481 def users_by_role
481 def users_by_role
482 members.includes(:user, :roles).all.inject({}) do |h, m|
482 members.includes(:user, :roles).all.inject({}) do |h, m|
483 m.roles.each do |r|
483 m.roles.each do |r|
484 h[r] ||= []
484 h[r] ||= []
485 h[r] << m.user
485 h[r] << m.user
486 end
486 end
487 h
487 h
488 end
488 end
489 end
489 end
490
490
491 # Deletes all project's members
491 # Deletes all project's members
492 def delete_all_members
492 def delete_all_members
493 me, mr = Member.table_name, MemberRole.table_name
493 me, mr = Member.table_name, MemberRole.table_name
494 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
494 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
495 Member.delete_all(['project_id = ?', id])
495 Member.delete_all(['project_id = ?', id])
496 end
496 end
497
497
498 # Users/groups issues can be assigned to
498 # Users/groups issues can be assigned to
499 def assignable_users
499 def assignable_users
500 assignable = Setting.issue_group_assignment? ? member_principals : members
500 assignable = Setting.issue_group_assignment? ? member_principals : members
501 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
501 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
502 end
502 end
503
503
504 # Returns the mail adresses of users that should be always notified on project events
504 # Returns the mail adresses of users that should be always notified on project events
505 def recipients
505 def recipients
506 notified_users.collect {|user| user.mail}
506 notified_users.collect {|user| user.mail}
507 end
507 end
508
508
509 # Returns the users that should be notified on project events
509 # Returns the users that should be notified on project events
510 def notified_users
510 def notified_users
511 # TODO: User part should be extracted to User#notify_about?
511 # TODO: User part should be extracted to User#notify_about?
512 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
512 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
513 end
513 end
514
514
515 # Returns a scope of all custom fields enabled for project issues
515 # Returns a scope of all custom fields enabled for project issues
516 # (explictly associated custom fields and custom fields enabled for all projects)
516 # (explictly associated custom fields and custom fields enabled for all projects)
517 def all_issue_custom_fields
517 def all_issue_custom_fields
518 @all_issue_custom_fields ||= IssueCustomField.
518 @all_issue_custom_fields ||= IssueCustomField.
519 sorted.
519 sorted.
520 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
520 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
521 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
521 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
522 " WHERE cfp.project_id = ?)", true, id)
522 " WHERE cfp.project_id = ?)", true, id)
523 end
523 end
524
524
525 # Returns an array of all custom fields enabled for project time entries
525 # Returns an array of all custom fields enabled for project time entries
526 # (explictly associated custom fields and custom fields enabled for all projects)
526 # (explictly associated custom fields and custom fields enabled for all projects)
527 def all_time_entry_custom_fields
527 def all_time_entry_custom_fields
528 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
528 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
529 end
529 end
530
530
531 def project
531 def project
532 self
532 self
533 end
533 end
534
534
535 def <=>(project)
535 def <=>(project)
536 name.downcase <=> project.name.downcase
536 name.downcase <=> project.name.downcase
537 end
537 end
538
538
539 def to_s
539 def to_s
540 name
540 name
541 end
541 end
542
542
543 # Returns a short description of the projects (first lines)
543 # Returns a short description of the projects (first lines)
544 def short_description(length = 255)
544 def short_description(length = 255)
545 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
545 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
546 end
546 end
547
547
548 def css_classes
548 def css_classes
549 s = 'project'
549 s = 'project'
550 s << ' root' if root?
550 s << ' root' if root?
551 s << ' child' if child?
551 s << ' child' if child?
552 s << (leaf? ? ' leaf' : ' parent')
552 s << (leaf? ? ' leaf' : ' parent')
553 unless active?
553 unless active?
554 if archived?
554 if archived?
555 s << ' archived'
555 s << ' archived'
556 else
556 else
557 s << ' closed'
557 s << ' closed'
558 end
558 end
559 end
559 end
560 s
560 s
561 end
561 end
562
562
563 # The earliest start date of a project, based on it's issues and versions
563 # The earliest start date of a project, based on it's issues and versions
564 def start_date
564 def start_date
565 @start_date ||= [
565 @start_date ||= [
566 issues.minimum('start_date'),
566 issues.minimum('start_date'),
567 shared_versions.minimum('effective_date'),
567 shared_versions.minimum('effective_date'),
568 Issue.fixed_version(shared_versions).minimum('start_date')
568 Issue.fixed_version(shared_versions).minimum('start_date')
569 ].compact.min
569 ].compact.min
570 end
570 end
571
571
572 # The latest due date of an issue or version
572 # The latest due date of an issue or version
573 def due_date
573 def due_date
574 @due_date ||= [
574 @due_date ||= [
575 issues.maximum('due_date'),
575 issues.maximum('due_date'),
576 shared_versions.maximum('effective_date'),
576 shared_versions.maximum('effective_date'),
577 Issue.fixed_version(shared_versions).maximum('due_date')
577 Issue.fixed_version(shared_versions).maximum('due_date')
578 ].compact.max
578 ].compact.max
579 end
579 end
580
580
581 def overdue?
581 def overdue?
582 active? && !due_date.nil? && (due_date < Date.today)
582 active? && !due_date.nil? && (due_date < Date.today)
583 end
583 end
584
584
585 # Returns the percent completed for this project, based on the
585 # Returns the percent completed for this project, based on the
586 # progress on it's versions.
586 # progress on it's versions.
587 def completed_percent(options={:include_subprojects => false})
587 def completed_percent(options={:include_subprojects => false})
588 if options.delete(:include_subprojects)
588 if options.delete(:include_subprojects)
589 total = self_and_descendants.collect(&:completed_percent).sum
589 total = self_and_descendants.collect(&:completed_percent).sum
590
590
591 total / self_and_descendants.count
591 total / self_and_descendants.count
592 else
592 else
593 if versions.count > 0
593 if versions.count > 0
594 total = versions.collect(&:completed_percent).sum
594 total = versions.collect(&:completed_percent).sum
595
595
596 total / versions.count
596 total / versions.count
597 else
597 else
598 100
598 100
599 end
599 end
600 end
600 end
601 end
601 end
602
602
603 # Return true if this project allows to do the specified action.
603 # Return true if this project allows to do the specified action.
604 # action can be:
604 # action can be:
605 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
605 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
606 # * a permission Symbol (eg. :edit_project)
606 # * a permission Symbol (eg. :edit_project)
607 def allows_to?(action)
607 def allows_to?(action)
608 if archived?
608 if archived?
609 # No action allowed on archived projects
609 # No action allowed on archived projects
610 return false
610 return false
611 end
611 end
612 unless active? || Redmine::AccessControl.read_action?(action)
612 unless active? || Redmine::AccessControl.read_action?(action)
613 # No write action allowed on closed projects
613 # No write action allowed on closed projects
614 return false
614 return false
615 end
615 end
616 # No action allowed on disabled modules
616 # No action allowed on disabled modules
617 if action.is_a? Hash
617 if action.is_a? Hash
618 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
618 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
619 else
619 else
620 allowed_permissions.include? action
620 allowed_permissions.include? action
621 end
621 end
622 end
622 end
623
623
624 def module_enabled?(module_name)
624 def module_enabled?(module_name)
625 module_name = module_name.to_s
625 module_name = module_name.to_s
626 enabled_modules.detect {|m| m.name == module_name}
626 enabled_modules.detect {|m| m.name == module_name}
627 end
627 end
628
628
629 def enabled_module_names=(module_names)
629 def enabled_module_names=(module_names)
630 if module_names && module_names.is_a?(Array)
630 if module_names && module_names.is_a?(Array)
631 module_names = module_names.collect(&:to_s).reject(&:blank?)
631 module_names = module_names.collect(&:to_s).reject(&:blank?)
632 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
632 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
633 else
633 else
634 enabled_modules.clear
634 enabled_modules.clear
635 end
635 end
636 end
636 end
637
637
638 # Returns an array of the enabled modules names
638 # Returns an array of the enabled modules names
639 def enabled_module_names
639 def enabled_module_names
640 enabled_modules.collect(&:name)
640 enabled_modules.collect(&:name)
641 end
641 end
642
642
643 # Enable a specific module
643 # Enable a specific module
644 #
644 #
645 # Examples:
645 # Examples:
646 # project.enable_module!(:issue_tracking)
646 # project.enable_module!(:issue_tracking)
647 # project.enable_module!("issue_tracking")
647 # project.enable_module!("issue_tracking")
648 def enable_module!(name)
648 def enable_module!(name)
649 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
649 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
650 end
650 end
651
651
652 # Disable a module if it exists
652 # Disable a module if it exists
653 #
653 #
654 # Examples:
654 # Examples:
655 # project.disable_module!(:issue_tracking)
655 # project.disable_module!(:issue_tracking)
656 # project.disable_module!("issue_tracking")
656 # project.disable_module!("issue_tracking")
657 # project.disable_module!(project.enabled_modules.first)
657 # project.disable_module!(project.enabled_modules.first)
658 def disable_module!(target)
658 def disable_module!(target)
659 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
659 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
660 target.destroy unless target.blank?
660 target.destroy unless target.blank?
661 end
661 end
662
662
663 safe_attributes 'name',
663 safe_attributes 'name',
664 'description',
664 'description',
665 'homepage',
665 'homepage',
666 'is_public',
666 'is_public',
667 'identifier',
667 'identifier',
668 'custom_field_values',
668 'custom_field_values',
669 'custom_fields',
669 'custom_fields',
670 'tracker_ids',
670 'tracker_ids',
671 'issue_custom_field_ids'
671 'issue_custom_field_ids'
672
672
673 safe_attributes 'enabled_module_names',
673 safe_attributes 'enabled_module_names',
674 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
674 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
675
675
676 safe_attributes 'inherit_members',
676 safe_attributes 'inherit_members',
677 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
677 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
678
678
679 # Returns an array of projects that are in this project's hierarchy
679 # Returns an array of projects that are in this project's hierarchy
680 #
680 #
681 # Example: parents, children, siblings
681 # Example: parents, children, siblings
682 def hierarchy
682 def hierarchy
683 parents = project.self_and_ancestors || []
683 parents = project.self_and_ancestors || []
684 descendants = project.descendants || []
684 descendants = project.descendants || []
685 project_hierarchy = parents | descendants # Set union
685 project_hierarchy = parents | descendants # Set union
686 end
686 end
687
687
688 # Returns an auto-generated project identifier based on the last identifier used
688 # Returns an auto-generated project identifier based on the last identifier used
689 def self.next_identifier
689 def self.next_identifier
690 p = Project.order('id DESC').first
690 p = Project.order('id DESC').first
691 p.nil? ? nil : p.identifier.to_s.succ
691 p.nil? ? nil : p.identifier.to_s.succ
692 end
692 end
693
693
694 # Copies and saves the Project instance based on the +project+.
694 # Copies and saves the Project instance based on the +project+.
695 # Duplicates the source project's:
695 # Duplicates the source project's:
696 # * Wiki
696 # * Wiki
697 # * Versions
697 # * Versions
698 # * Categories
698 # * Categories
699 # * Issues
699 # * Issues
700 # * Members
700 # * Members
701 # * Queries
701 # * Queries
702 #
702 #
703 # Accepts an +options+ argument to specify what to copy
703 # Accepts an +options+ argument to specify what to copy
704 #
704 #
705 # Examples:
705 # Examples:
706 # project.copy(1) # => copies everything
706 # project.copy(1) # => copies everything
707 # project.copy(1, :only => 'members') # => copies members only
707 # project.copy(1, :only => 'members') # => copies members only
708 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
708 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
709 def copy(project, options={})
709 def copy(project, options={})
710 project = project.is_a?(Project) ? project : Project.find(project)
710 project = project.is_a?(Project) ? project : Project.find(project)
711
711
712 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
712 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
713 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
713 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
714
714
715 Project.transaction do
715 Project.transaction do
716 if save
716 if save
717 reload
717 reload
718 to_be_copied.each do |name|
718 to_be_copied.each do |name|
719 send "copy_#{name}", project
719 send "copy_#{name}", project
720 end
720 end
721 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
721 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
722 save
722 save
723 end
723 end
724 end
724 end
725 end
725 end
726
726
727 # Returns a new unsaved Project instance with attributes copied from +project+
727 # Returns a new unsaved Project instance with attributes copied from +project+
728 def self.copy_from(project)
728 def self.copy_from(project)
729 project = project.is_a?(Project) ? project : Project.find(project)
729 project = project.is_a?(Project) ? project : Project.find(project)
730 # clear unique attributes
730 # clear unique attributes
731 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
731 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
732 copy = Project.new(attributes)
732 copy = Project.new(attributes)
733 copy.enabled_modules = project.enabled_modules
733 copy.enabled_modules = project.enabled_modules
734 copy.trackers = project.trackers
734 copy.trackers = project.trackers
735 copy.custom_values = project.custom_values.collect {|v| v.clone}
735 copy.custom_values = project.custom_values.collect {|v| v.clone}
736 copy.issue_custom_fields = project.issue_custom_fields
736 copy.issue_custom_fields = project.issue_custom_fields
737 copy
737 copy
738 end
738 end
739
739
740 # Yields the given block for each project with its level in the tree
740 # Yields the given block for each project with its level in the tree
741 def self.project_tree(projects, &block)
741 def self.project_tree(projects, &block)
742 ancestors = []
742 ancestors = []
743 projects.sort_by(&:lft).each do |project|
743 projects.sort_by(&:lft).each do |project|
744 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
744 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
745 ancestors.pop
745 ancestors.pop
746 end
746 end
747 yield project, ancestors.size
747 yield project, ancestors.size
748 ancestors << project
748 ancestors << project
749 end
749 end
750 end
750 end
751
751
752 private
752 private
753
753
754 def after_parent_changed(parent_was)
754 def after_parent_changed(parent_was)
755 remove_inherited_member_roles
755 remove_inherited_member_roles
756 add_inherited_member_roles
756 add_inherited_member_roles
757 end
757 end
758
758
759 def update_inherited_members
759 def update_inherited_members
760 if parent
760 if parent
761 if inherit_members? && !inherit_members_was
761 if inherit_members? && !inherit_members_was
762 remove_inherited_member_roles
762 remove_inherited_member_roles
763 add_inherited_member_roles
763 add_inherited_member_roles
764 elsif !inherit_members? && inherit_members_was
764 elsif !inherit_members? && inherit_members_was
765 remove_inherited_member_roles
765 remove_inherited_member_roles
766 end
766 end
767 end
767 end
768 end
768 end
769
769
770 def remove_inherited_member_roles
770 def remove_inherited_member_roles
771 member_roles = memberships.map(&:member_roles).flatten
771 member_roles = memberships.map(&:member_roles).flatten
772 member_role_ids = member_roles.map(&:id)
772 member_role_ids = member_roles.map(&:id)
773 member_roles.each do |member_role|
773 member_roles.each do |member_role|
774 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
774 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
775 member_role.destroy
775 member_role.destroy
776 end
776 end
777 end
777 end
778 end
778 end
779
779
780 def add_inherited_member_roles
780 def add_inherited_member_roles
781 if inherit_members? && parent
781 if inherit_members? && parent
782 parent.memberships.each do |parent_member|
782 parent.memberships.each do |parent_member|
783 member = Member.find_or_new(self.id, parent_member.user_id)
783 member = Member.find_or_new(self.id, parent_member.user_id)
784 parent_member.member_roles.each do |parent_member_role|
784 parent_member.member_roles.each do |parent_member_role|
785 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
785 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
786 end
786 end
787 member.save!
787 member.save!
788 end
788 end
789 end
789 end
790 end
790 end
791
791
792 # Copies wiki from +project+
792 # Copies wiki from +project+
793 def copy_wiki(project)
793 def copy_wiki(project)
794 # Check that the source project has a wiki first
794 # Check that the source project has a wiki first
795 unless project.wiki.nil?
795 unless project.wiki.nil?
796 wiki = self.wiki || Wiki.new
796 wiki = self.wiki || Wiki.new
797 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
797 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
798 wiki_pages_map = {}
798 wiki_pages_map = {}
799 project.wiki.pages.each do |page|
799 project.wiki.pages.each do |page|
800 # Skip pages without content
800 # Skip pages without content
801 next if page.content.nil?
801 next if page.content.nil?
802 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
802 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
803 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
803 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
804 new_wiki_page.content = new_wiki_content
804 new_wiki_page.content = new_wiki_content
805 wiki.pages << new_wiki_page
805 wiki.pages << new_wiki_page
806 wiki_pages_map[page.id] = new_wiki_page
806 wiki_pages_map[page.id] = new_wiki_page
807 end
807 end
808
808
809 self.wiki = wiki
809 self.wiki = wiki
810 wiki.save
810 wiki.save
811 # Reproduce page hierarchy
811 # Reproduce page hierarchy
812 project.wiki.pages.each do |page|
812 project.wiki.pages.each do |page|
813 if page.parent_id && wiki_pages_map[page.id]
813 if page.parent_id && wiki_pages_map[page.id]
814 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
814 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
815 wiki_pages_map[page.id].save
815 wiki_pages_map[page.id].save
816 end
816 end
817 end
817 end
818 end
818 end
819 end
819 end
820
820
821 # Copies versions from +project+
821 # Copies versions from +project+
822 def copy_versions(project)
822 def copy_versions(project)
823 project.versions.each do |version|
823 project.versions.each do |version|
824 new_version = Version.new
824 new_version = Version.new
825 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
825 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
826 self.versions << new_version
826 self.versions << new_version
827 end
827 end
828 end
828 end
829
829
830 # Copies issue categories from +project+
830 # Copies issue categories from +project+
831 def copy_issue_categories(project)
831 def copy_issue_categories(project)
832 project.issue_categories.each do |issue_category|
832 project.issue_categories.each do |issue_category|
833 new_issue_category = IssueCategory.new
833 new_issue_category = IssueCategory.new
834 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
834 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
835 self.issue_categories << new_issue_category
835 self.issue_categories << new_issue_category
836 end
836 end
837 end
837 end
838
838
839 # Copies issues from +project+
839 # Copies issues from +project+
840 def copy_issues(project)
840 def copy_issues(project)
841 # Stores the source issue id as a key and the copied issues as the
841 # Stores the source issue id as a key and the copied issues as the
842 # value. Used to map the two togeather for issue relations.
842 # value. Used to map the two togeather for issue relations.
843 issues_map = {}
843 issues_map = {}
844
844
845 # Store status and reopen locked/closed versions
845 # Store status and reopen locked/closed versions
846 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
846 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
847 version_statuses.each do |version, status|
847 version_statuses.each do |version, status|
848 version.update_attribute :status, 'open'
848 version.update_attribute :status, 'open'
849 end
849 end
850
850
851 # Get issues sorted by root_id, lft so that parent issues
851 # Get issues sorted by root_id, lft so that parent issues
852 # get copied before their children
852 # get copied before their children
853 project.issues.reorder('root_id, lft').each do |issue|
853 project.issues.reorder('root_id, lft').each do |issue|
854 new_issue = Issue.new
854 new_issue = Issue.new
855 new_issue.copy_from(issue, :subtasks => false, :link => false)
855 new_issue.copy_from(issue, :subtasks => false, :link => false)
856 new_issue.project = self
856 new_issue.project = self
857 # Changing project resets the custom field values
857 # Changing project resets the custom field values
858 # TODO: handle this in Issue#project=
858 # TODO: handle this in Issue#project=
859 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
859 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
860 # Reassign fixed_versions by name, since names are unique per project
860 # Reassign fixed_versions by name, since names are unique per project
861 if issue.fixed_version && issue.fixed_version.project == project
861 if issue.fixed_version && issue.fixed_version.project == project
862 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
862 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
863 end
863 end
864 # Reassign the category by name, since names are unique per project
864 # Reassign the category by name, since names are unique per project
865 if issue.category
865 if issue.category
866 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
866 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
867 end
867 end
868 # Parent issue
868 # Parent issue
869 if issue.parent_id
869 if issue.parent_id
870 if copied_parent = issues_map[issue.parent_id]
870 if copied_parent = issues_map[issue.parent_id]
871 new_issue.parent_issue_id = copied_parent.id
871 new_issue.parent_issue_id = copied_parent.id
872 end
872 end
873 end
873 end
874
874
875 self.issues << new_issue
875 self.issues << new_issue
876 if new_issue.new_record?
876 if new_issue.new_record?
877 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
877 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
878 else
878 else
879 issues_map[issue.id] = new_issue unless new_issue.new_record?
879 issues_map[issue.id] = new_issue unless new_issue.new_record?
880 end
880 end
881 end
881 end
882
882
883 # Restore locked/closed version statuses
883 # Restore locked/closed version statuses
884 version_statuses.each do |version, status|
884 version_statuses.each do |version, status|
885 version.update_attribute :status, status
885 version.update_attribute :status, status
886 end
886 end
887
887
888 # Relations after in case issues related each other
888 # Relations after in case issues related each other
889 project.issues.each do |issue|
889 project.issues.each do |issue|
890 new_issue = issues_map[issue.id]
890 new_issue = issues_map[issue.id]
891 unless new_issue
891 unless new_issue
892 # Issue was not copied
892 # Issue was not copied
893 next
893 next
894 end
894 end
895
895
896 # Relations
896 # Relations
897 issue.relations_from.each do |source_relation|
897 issue.relations_from.each do |source_relation|
898 new_issue_relation = IssueRelation.new
898 new_issue_relation = IssueRelation.new
899 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
899 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
900 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
900 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
901 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
901 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
902 new_issue_relation.issue_to = source_relation.issue_to
902 new_issue_relation.issue_to = source_relation.issue_to
903 end
903 end
904 new_issue.relations_from << new_issue_relation
904 new_issue.relations_from << new_issue_relation
905 end
905 end
906
906
907 issue.relations_to.each do |source_relation|
907 issue.relations_to.each do |source_relation|
908 new_issue_relation = IssueRelation.new
908 new_issue_relation = IssueRelation.new
909 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
909 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
910 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
910 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
911 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
911 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
912 new_issue_relation.issue_from = source_relation.issue_from
912 new_issue_relation.issue_from = source_relation.issue_from
913 end
913 end
914 new_issue.relations_to << new_issue_relation
914 new_issue.relations_to << new_issue_relation
915 end
915 end
916 end
916 end
917 end
917 end
918
918
919 # Copies members from +project+
919 # Copies members from +project+
920 def copy_members(project)
920 def copy_members(project)
921 # Copy users first, then groups to handle members with inherited and given roles
921 # Copy users first, then groups to handle members with inherited and given roles
922 members_to_copy = []
922 members_to_copy = []
923 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
923 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)}
924 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
925
925
926 members_to_copy.each do |member|
926 members_to_copy.each do |member|
927 new_member = Member.new
927 new_member = Member.new
928 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
928 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
929 # only copy non inherited roles
929 # only copy non inherited roles
930 # inherited roles will be added when copying the group membership
930 # inherited roles will be added when copying the group membership
931 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
931 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
932 next if role_ids.empty?
932 next if role_ids.empty?
933 new_member.role_ids = role_ids
933 new_member.role_ids = role_ids
934 new_member.project = self
934 new_member.project = self
935 self.members << new_member
935 self.members << new_member
936 end
936 end
937 end
937 end
938
938
939 # Copies queries from +project+
939 # Copies queries from +project+
940 def copy_queries(project)
940 def copy_queries(project)
941 project.queries.each do |query|
941 project.queries.each do |query|
942 new_query = IssueQuery.new
942 new_query = IssueQuery.new
943 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
943 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
944 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
944 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
945 new_query.project = self
945 new_query.project = self
946 new_query.user_id = query.user_id
946 new_query.user_id = query.user_id
947 self.queries << new_query
947 self.queries << new_query
948 end
948 end
949 end
949 end
950
950
951 # Copies boards from +project+
951 # Copies boards from +project+
952 def copy_boards(project)
952 def copy_boards(project)
953 project.boards.each do |board|
953 project.boards.each do |board|
954 new_board = Board.new
954 new_board = Board.new
955 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
955 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
956 new_board.project = self
956 new_board.project = self
957 self.boards << new_board
957 self.boards << new_board
958 end
958 end
959 end
959 end
960
960
961 def allowed_permissions
961 def allowed_permissions
962 @allowed_permissions ||= begin
962 @allowed_permissions ||= begin
963 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
963 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
964 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
964 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
965 end
965 end
966 end
966 end
967
967
968 def allowed_actions
968 def allowed_actions
969 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
969 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
970 end
970 end
971
971
972 # Returns all the active Systemwide and project specific activities
972 # Returns all the active Systemwide and project specific activities
973 def active_activities
973 def active_activities
974 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
974 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
975
975
976 if overridden_activity_ids.empty?
976 if overridden_activity_ids.empty?
977 return TimeEntryActivity.shared.active
977 return TimeEntryActivity.shared.active
978 else
978 else
979 return system_activities_and_project_overrides
979 return system_activities_and_project_overrides
980 end
980 end
981 end
981 end
982
982
983 # Returns all the Systemwide and project specific activities
983 # Returns all the Systemwide and project specific activities
984 # (inactive and active)
984 # (inactive and active)
985 def all_activities
985 def all_activities
986 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
986 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
987
987
988 if overridden_activity_ids.empty?
988 if overridden_activity_ids.empty?
989 return TimeEntryActivity.shared
989 return TimeEntryActivity.shared
990 else
990 else
991 return system_activities_and_project_overrides(true)
991 return system_activities_and_project_overrides(true)
992 end
992 end
993 end
993 end
994
994
995 # Returns the systemwide active activities merged with the project specific overrides
995 # Returns the systemwide active activities merged with the project specific overrides
996 def system_activities_and_project_overrides(include_inactive=false)
996 def system_activities_and_project_overrides(include_inactive=false)
997 if include_inactive
997 if include_inactive
998 return TimeEntryActivity.shared.
998 return TimeEntryActivity.shared.
999 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
999 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
1000 self.time_entry_activities
1000 self.time_entry_activities
1001 else
1001 else
1002 return TimeEntryActivity.shared.active.
1002 return TimeEntryActivity.shared.active.
1003 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
1003 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
1004 self.time_entry_activities.active
1004 self.time_entry_activities.active
1005 end
1005 end
1006 end
1006 end
1007
1007
1008 # Archives subprojects recursively
1008 # Archives subprojects recursively
1009 def archive!
1009 def archive!
1010 children.each do |subproject|
1010 children.each do |subproject|
1011 subproject.send :archive!
1011 subproject.send :archive!
1012 end
1012 end
1013 update_attribute :status, STATUS_ARCHIVED
1013 update_attribute :status, STATUS_ARCHIVED
1014 end
1014 end
1015
1015
1016 def update_position_under_parent
1016 def update_position_under_parent
1017 set_or_update_position_under(parent)
1017 set_or_update_position_under(parent)
1018 end
1018 end
1019
1019
1020 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
1020 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
1021 def set_or_update_position_under(target_parent)
1021 def set_or_update_position_under(target_parent)
1022 parent_was = parent
1022 parent_was = parent
1023 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1023 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1024 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1024 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1025
1025
1026 if to_be_inserted_before
1026 if to_be_inserted_before
1027 move_to_left_of(to_be_inserted_before)
1027 move_to_left_of(to_be_inserted_before)
1028 elsif target_parent.nil?
1028 elsif target_parent.nil?
1029 if sibs.empty?
1029 if sibs.empty?
1030 # move_to_root adds the project in first (ie. left) position
1030 # move_to_root adds the project in first (ie. left) position
1031 move_to_root
1031 move_to_root
1032 else
1032 else
1033 move_to_right_of(sibs.last) unless self == sibs.last
1033 move_to_right_of(sibs.last) unless self == sibs.last
1034 end
1034 end
1035 else
1035 else
1036 # move_to_child_of adds the project in last (ie.right) position
1036 # move_to_child_of adds the project in last (ie.right) position
1037 move_to_child_of(target_parent)
1037 move_to_child_of(target_parent)
1038 end
1038 end
1039 if parent_was != target_parent
1039 if parent_was != target_parent
1040 after_parent_changed(parent_was)
1040 after_parent_changed(parent_was)
1041 end
1041 end
1042 end
1042 end
1043 end
1043 end
@@ -1,747 +1,748
1 module CollectiveIdea #:nodoc:
1 module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
2 module Acts #:nodoc:
3 module NestedSet #:nodoc:
3 module NestedSet #:nodoc:
4
4
5 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
5 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
6 # an _ordered_ tree, with the added feature that you can select the children and all of their
6 # an _ordered_ tree, with the added feature that you can select the children and all of their
7 # descendants with a single query. The drawback is that insertion or move need some complex
7 # descendants with a single query. The drawback is that insertion or move need some complex
8 # sql queries. But everything is done here by this module!
8 # sql queries. But everything is done here by this module!
9 #
9 #
10 # Nested sets are appropriate each time you want either an orderd tree (menus,
10 # Nested sets are appropriate each time you want either an orderd tree (menus,
11 # commercial categories) or an efficient way of querying big trees (threaded posts).
11 # commercial categories) or an efficient way of querying big trees (threaded posts).
12 #
12 #
13 # == API
13 # == API
14 #
14 #
15 # Methods names are aligned with acts_as_tree as much as possible to make replacment from one
15 # Methods names are aligned with acts_as_tree as much as possible to make replacment from one
16 # by another easier.
16 # by another easier.
17 #
17 #
18 # item.children.create(:name => "child1")
18 # item.children.create(:name => "child1")
19 #
19 #
20
20
21 # Configuration options are:
21 # Configuration options are:
22 #
22 #
23 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
23 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
24 # * +:left_column+ - column name for left boundry data, default "lft"
24 # * +:left_column+ - column name for left boundry data, default "lft"
25 # * +:right_column+ - column name for right boundry data, default "rgt"
25 # * +:right_column+ - column name for right boundry data, default "rgt"
26 # * +:depth_column+ - column name for the depth data, default "depth"
26 # * +:depth_column+ - column name for the depth data, default "depth"
27 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
27 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
28 # (if it hasn't been already) and use that as the foreign key restriction. You
28 # (if it hasn't been already) and use that as the foreign key restriction. You
29 # can also pass an array to scope by multiple attributes.
29 # can also pass an array to scope by multiple attributes.
30 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
30 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
31 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
31 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
32 # child objects are destroyed alongside this object by calling their destroy
32 # child objects are destroyed alongside this object by calling their destroy
33 # method. If set to :delete_all (default), all the child objects are deleted
33 # method. If set to :delete_all (default), all the child objects are deleted
34 # without calling their destroy method.
34 # without calling their destroy method.
35 # * +:counter_cache+ adds a counter cache for the number of children.
35 # * +:counter_cache+ adds a counter cache for the number of children.
36 # defaults to false.
36 # defaults to false.
37 # Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
37 # Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
38 # * +:order_column+ on which column to do sorting, by default it is the left_column_name
38 # * +:order_column+ on which column to do sorting, by default it is the left_column_name
39 # Example: <tt>acts_as_nested_set :order_column => :position</tt>
39 # Example: <tt>acts_as_nested_set :order_column => :position</tt>
40 #
40 #
41 # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
41 # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
42 # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
42 # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
43 # to acts_as_nested_set models
43 # to acts_as_nested_set models
44 def acts_as_nested_set(options = {})
44 def acts_as_nested_set(options = {})
45 options = {
45 options = {
46 :parent_column => 'parent_id',
46 :parent_column => 'parent_id',
47 :left_column => 'lft',
47 :left_column => 'lft',
48 :right_column => 'rgt',
48 :right_column => 'rgt',
49 :depth_column => 'depth',
49 :depth_column => 'depth',
50 :dependent => :delete_all, # or :destroy
50 :dependent => :delete_all, # or :destroy
51 :polymorphic => false,
51 :polymorphic => false,
52 :counter_cache => false
52 :counter_cache => false
53 }.merge(options)
53 }.merge(options)
54
54
55 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
55 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
56 options[:scope] = "#{options[:scope]}_id".intern
56 options[:scope] = "#{options[:scope]}_id".intern
57 end
57 end
58
58
59 class_attribute :acts_as_nested_set_options
59 class_attribute :acts_as_nested_set_options
60 self.acts_as_nested_set_options = options
60 self.acts_as_nested_set_options = options
61
61
62 include CollectiveIdea::Acts::NestedSet::Model
62 include CollectiveIdea::Acts::NestedSet::Model
63 include Columns
63 include Columns
64 extend Columns
64 extend Columns
65
65
66 belongs_to :parent, :class_name => self.base_class.to_s,
66 belongs_to :parent, :class_name => self.base_class.to_s,
67 :foreign_key => parent_column_name,
67 :foreign_key => parent_column_name,
68 :counter_cache => options[:counter_cache],
68 :counter_cache => options[:counter_cache],
69 :inverse_of => (:children unless options[:polymorphic]),
69 :inverse_of => (:children unless options[:polymorphic]),
70 :polymorphic => options[:polymorphic]
70 :polymorphic => options[:polymorphic]
71
71
72 has_many_children_options = {
72 has_many_children_options = {
73 :class_name => self.base_class.to_s,
73 :class_name => self.base_class.to_s,
74 :foreign_key => parent_column_name,
74 :foreign_key => parent_column_name,
75 :order => order_column,
75 :order => order_column,
76 :inverse_of => (:parent unless options[:polymorphic]),
76 :inverse_of => (:parent unless options[:polymorphic]),
77 }
77 }
78
78
79 # Add callbacks, if they were supplied.. otherwise, we don't want them.
79 # Add callbacks, if they were supplied.. otherwise, we don't want them.
80 [:before_add, :after_add, :before_remove, :after_remove].each do |ar_callback|
80 [:before_add, :after_add, :before_remove, :after_remove].each do |ar_callback|
81 has_many_children_options.update(ar_callback => options[ar_callback]) if options[ar_callback]
81 has_many_children_options.update(ar_callback => options[ar_callback]) if options[ar_callback]
82 end
82 end
83
83
84 has_many :children, has_many_children_options
84 has_many :children, has_many_children_options
85
85
86 attr_accessor :skip_before_destroy
86 attr_accessor :skip_before_destroy
87
87
88 before_create :set_default_left_and_right
88 before_create :set_default_left_and_right
89 before_save :store_new_parent
89 before_save :store_new_parent
90 after_save :move_to_new_parent, :set_depth!
90 after_save :move_to_new_parent, :set_depth!
91 before_destroy :destroy_descendants
91 before_destroy :destroy_descendants
92
92
93 # no assignment to structure fields
93 # no assignment to structure fields
94 [left_column_name, right_column_name, depth_column_name].each do |column|
94 [left_column_name, right_column_name, depth_column_name].each do |column|
95 module_eval <<-"end_eval", __FILE__, __LINE__
95 module_eval <<-"end_eval", __FILE__, __LINE__
96 def #{column}=(x)
96 def #{column}=(x)
97 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
97 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
98 end
98 end
99 end_eval
99 end_eval
100 end
100 end
101
101
102 define_model_callbacks :move
102 define_model_callbacks :move
103 end
103 end
104
104
105 module Model
105 module Model
106 extend ActiveSupport::Concern
106 extend ActiveSupport::Concern
107
107
108 included do
108 included do
109 delegate :quoted_table_name, :to => self
109 delegate :quoted_table_name, :to => self
110 end
110 end
111
111
112 module ClassMethods
112 module ClassMethods
113 # Returns the first root
113 # Returns the first root
114 def root
114 def root
115 roots.first
115 roots.first
116 end
116 end
117
117
118 def roots
118 def roots
119 where(parent_column_name => nil).order(quoted_left_column_full_name)
119 where(parent_column_name => nil).order(quoted_left_column_full_name)
120 end
120 end
121
121
122 def leaves
122 def leaves
123 where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1").order(quoted_left_column_full_name)
123 where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1").order(quoted_left_column_full_name)
124 end
124 end
125
125
126 def valid?
126 def valid?
127 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
127 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
128 end
128 end
129
129
130 def left_and_rights_valid?
130 def left_and_rights_valid?
131 ## AS clause not supported in Oracle in FROM clause for aliasing table name
131 ## AS clause not supported in Oracle in FROM clause for aliasing table name
132 joins("LEFT OUTER JOIN #{quoted_table_name}" +
132 joins("LEFT OUTER JOIN #{quoted_table_name}" +
133 (connection.adapter_name.match(/Oracle/).nil? ? " AS " : " ") +
133 (connection.adapter_name.match(/Oracle/).nil? ? " AS " : " ") +
134 "parent ON " +
134 "parent ON " +
135 "#{quoted_parent_column_full_name} = parent.#{primary_key}").
135 "#{quoted_parent_column_full_name} = parent.#{primary_key}").
136 where(
136 where(
137 "#{quoted_left_column_full_name} IS NULL OR " +
137 "#{quoted_left_column_full_name} IS NULL OR " +
138 "#{quoted_right_column_full_name} IS NULL OR " +
138 "#{quoted_right_column_full_name} IS NULL OR " +
139 "#{quoted_left_column_full_name} >= " +
139 "#{quoted_left_column_full_name} >= " +
140 "#{quoted_right_column_full_name} OR " +
140 "#{quoted_right_column_full_name} OR " +
141 "(#{quoted_parent_column_full_name} IS NOT NULL AND " +
141 "(#{quoted_parent_column_full_name} IS NOT NULL AND " +
142 "(#{quoted_left_column_full_name} <= parent.#{quoted_left_column_name} OR " +
142 "(#{quoted_left_column_full_name} <= parent.#{quoted_left_column_name} OR " +
143 "#{quoted_right_column_full_name} >= parent.#{quoted_right_column_name}))"
143 "#{quoted_right_column_full_name} >= parent.#{quoted_right_column_name}))"
144 ).count == 0
144 ).count == 0
145 end
145 end
146
146
147 def no_duplicates_for_columns?
147 def no_duplicates_for_columns?
148 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
148 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
149 connection.quote_column_name(c)
149 connection.quote_column_name(c)
150 end.push(nil).join(", ")
150 end.push(nil).join(", ")
151 [quoted_left_column_full_name, quoted_right_column_full_name].all? do |column|
151 [quoted_left_column_full_name, quoted_right_column_full_name].all? do |column|
152 # No duplicates
152 # No duplicates
153 select("#{scope_string}#{column}, COUNT(#{column})").
153 select("#{scope_string}#{column}, COUNT(#{column})").
154 group("#{scope_string}#{column}").
154 group("#{scope_string}#{column}").
155 having("COUNT(#{column}) > 1").
155 having("COUNT(#{column}) > 1").
156 first.nil?
156 first.nil?
157 end
157 end
158 end
158 end
159
159
160 # Wrapper for each_root_valid? that can deal with scope.
160 # Wrapper for each_root_valid? that can deal with scope.
161 def all_roots_valid?
161 def all_roots_valid?
162 if acts_as_nested_set_options[:scope]
162 if acts_as_nested_set_options[:scope]
163 roots.group_by {|record| scope_column_names.collect {|col| record.send(col.to_sym) } }.all? do |scope, grouped_roots|
163 roots.group_by {|record| scope_column_names.collect {|col| record.send(col.to_sym) } }.all? do |scope, grouped_roots|
164 each_root_valid?(grouped_roots)
164 each_root_valid?(grouped_roots)
165 end
165 end
166 else
166 else
167 each_root_valid?(roots)
167 each_root_valid?(roots)
168 end
168 end
169 end
169 end
170
170
171 def each_root_valid?(roots_to_validate)
171 def each_root_valid?(roots_to_validate)
172 left = right = 0
172 left = right = 0
173 roots_to_validate.all? do |root|
173 roots_to_validate.all? do |root|
174 (root.left > left && root.right > right).tap do
174 (root.left > left && root.right > right).tap do
175 left = root.left
175 left = root.left
176 right = root.right
176 right = root.right
177 end
177 end
178 end
178 end
179 end
179 end
180
180
181 # Rebuilds the left & rights if unset or invalid.
181 # Rebuilds the left & rights if unset or invalid.
182 # Also very useful for converting from acts_as_tree.
182 # Also very useful for converting from acts_as_tree.
183 def rebuild!(validate_nodes = true)
183 def rebuild!(validate_nodes = true)
184 # Don't rebuild a valid tree.
184 # Don't rebuild a valid tree.
185 return true if valid?
185 return true if valid?
186
186
187 scope = lambda{|node|}
187 scope = lambda{|node|}
188 if acts_as_nested_set_options[:scope]
188 if acts_as_nested_set_options[:scope]
189 scope = lambda{|node|
189 scope = lambda{|node|
190 scope_column_names.inject(""){|str, column_name|
190 scope_column_names.inject(""){|str, column_name|
191 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
191 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
192 }
192 }
193 }
193 }
194 end
194 end
195 indices = {}
195 indices = {}
196
196
197 set_left_and_rights = lambda do |node|
197 set_left_and_rights = lambda do |node|
198 # set left
198 # set left
199 node[left_column_name] = indices[scope.call(node)] += 1
199 node[left_column_name] = indices[scope.call(node)] += 1
200 # find
200 # find
201 where(["#{quoted_parent_column_full_name} = ? #{scope.call(node)}", node]).order("#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, id").each{|n| set_left_and_rights.call(n) }
201 where(["#{quoted_parent_column_name} = ? #{scope.call(node)}", node]).
202 order(acts_as_nested_set_options[:order]).
203 each{|n| set_left_and_rights.call(n) }
202 # set right
204 # set right
203 node[right_column_name] = indices[scope.call(node)] += 1
205 node[right_column_name] = indices[scope.call(node)] += 1
204 node.save!(:validate => validate_nodes)
206 node.save!(:validate => validate_nodes)
205 end
207 end
206
208
207 # Find root node(s)
209 # Find root node(s)
208 root_nodes = where("#{quoted_parent_column_full_name} IS NULL").
210 root_nodes = where("#{quoted_parent_column_name} IS NULL").
209 order(acts_as_nested_set_options[:order_column]).
211 order(acts_as_nested_set_options[:order]).each do |root_node|
210 each do |root_node|
211 # setup index for this scope
212 # setup index for this scope
212 indices[scope.call(root_node)] ||= 0
213 indices[scope.call(root_node)] ||= 0
213 set_left_and_rights.call(root_node)
214 set_left_and_rights.call(root_node)
214 end
215 end
215 end
216 end
216
217
217 # Iterates over tree elements and determines the current level in the tree.
218 # Iterates over tree elements and determines the current level in the tree.
218 # Only accepts default ordering, odering by an other column than lft
219 # Only accepts default ordering, odering by an other column than lft
219 # does not work. This method is much more efficent than calling level
220 # does not work. This method is much more efficent than calling level
220 # because it doesn't require any additional database queries.
221 # because it doesn't require any additional database queries.
221 #
222 #
222 # Example:
223 # Example:
223 # Category.each_with_level(Category.root.self_and_descendants) do |o, level|
224 # Category.each_with_level(Category.root.self_and_descendants) do |o, level|
224 #
225 #
225 def each_with_level(objects)
226 def each_with_level(objects)
226 path = [nil]
227 path = [nil]
227 objects.each do |o|
228 objects.each do |o|
228 if o.parent_id != path.last
229 if o.parent_id != path.last
229 # we are on a new level, did we descend or ascend?
230 # we are on a new level, did we descend or ascend?
230 if path.include?(o.parent_id)
231 if path.include?(o.parent_id)
231 # remove wrong wrong tailing paths elements
232 # remove wrong wrong tailing paths elements
232 path.pop while path.last != o.parent_id
233 path.pop while path.last != o.parent_id
233 else
234 else
234 path << o.parent_id
235 path << o.parent_id
235 end
236 end
236 end
237 end
237 yield(o, path.length - 1)
238 yield(o, path.length - 1)
238 end
239 end
239 end
240 end
240
241
241 # Same as each_with_level - Accepts a string as a second argument to sort the list
242 # Same as each_with_level - Accepts a string as a second argument to sort the list
242 # Example:
243 # Example:
243 # Category.each_with_level(Category.root.self_and_descendants, :sort_by_this_column) do |o, level|
244 # Category.each_with_level(Category.root.self_and_descendants, :sort_by_this_column) do |o, level|
244 def sorted_each_with_level(objects, order)
245 def sorted_each_with_level(objects, order)
245 path = [nil]
246 path = [nil]
246 children = []
247 children = []
247 objects.each do |o|
248 objects.each do |o|
248 children << o if o.leaf?
249 children << o if o.leaf?
249 if o.parent_id != path.last
250 if o.parent_id != path.last
250 if !children.empty? && !o.leaf?
251 if !children.empty? && !o.leaf?
251 children.sort_by! &order
252 children.sort_by! &order
252 children.each { |c| yield(c, path.length-1) }
253 children.each { |c| yield(c, path.length-1) }
253 children = []
254 children = []
254 end
255 end
255 # we are on a new level, did we decent or ascent?
256 # we are on a new level, did we decent or ascent?
256 if path.include?(o.parent_id)
257 if path.include?(o.parent_id)
257 # remove wrong wrong tailing paths elements
258 # remove wrong wrong tailing paths elements
258 path.pop while path.last != o.parent_id
259 path.pop while path.last != o.parent_id
259 else
260 else
260 path << o.parent_id
261 path << o.parent_id
261 end
262 end
262 end
263 end
263 yield(o,path.length-1) if !o.leaf?
264 yield(o,path.length-1) if !o.leaf?
264 end
265 end
265 if !children.empty?
266 if !children.empty?
266 children.sort_by! &order
267 children.sort_by! &order
267 children.each { |c| yield(c, path.length-1) }
268 children.each { |c| yield(c, path.length-1) }
268 end
269 end
269 end
270 end
270
271
271 def associate_parents(objects)
272 def associate_parents(objects)
272 if objects.all?{|o| o.respond_to?(:association)}
273 if objects.all?{|o| o.respond_to?(:association)}
273 id_indexed = objects.index_by(&:id)
274 id_indexed = objects.index_by(&:id)
274 objects.each do |object|
275 objects.each do |object|
275 if !(association = object.association(:parent)).loaded? && (parent = id_indexed[object.parent_id])
276 if !(association = object.association(:parent)).loaded? && (parent = id_indexed[object.parent_id])
276 association.target = parent
277 association.target = parent
277 association.set_inverse_instance(parent)
278 association.set_inverse_instance(parent)
278 end
279 end
279 end
280 end
280 else
281 else
281 objects
282 objects
282 end
283 end
283 end
284 end
284 end
285 end
285
286
286 # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
287 # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
287 #
288 #
288 # category.self_and_descendants.count
289 # category.self_and_descendants.count
289 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
290 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
290 # Value of the parent column
291 # Value of the parent column
291 def parent_id
292 def parent_id
292 self[parent_column_name]
293 self[parent_column_name]
293 end
294 end
294
295
295 # Value of the left column
296 # Value of the left column
296 def left
297 def left
297 self[left_column_name]
298 self[left_column_name]
298 end
299 end
299
300
300 # Value of the right column
301 # Value of the right column
301 def right
302 def right
302 self[right_column_name]
303 self[right_column_name]
303 end
304 end
304
305
305 # Returns true if this is a root node.
306 # Returns true if this is a root node.
306 def root?
307 def root?
307 parent_id.nil?
308 parent_id.nil?
308 end
309 end
309
310
310 # Returns true if this is the end of a branch.
311 # Returns true if this is the end of a branch.
311 def leaf?
312 def leaf?
312 new_record? || (persisted? && right.to_i - left.to_i == 1)
313 new_record? || (persisted? && right.to_i - left.to_i == 1)
313 end
314 end
314
315
315 # Returns true is this is a child node
316 # Returns true is this is a child node
316 def child?
317 def child?
317 !root?
318 !root?
318 end
319 end
319
320
320 # Returns root
321 # Returns root
321 def root
322 def root
322 if persisted?
323 if persisted?
323 self_and_ancestors.where(parent_column_name => nil).first
324 self_and_ancestors.where(parent_column_name => nil).first
324 else
325 else
325 if parent_id && current_parent = nested_set_scope.find(parent_id)
326 if parent_id && current_parent = nested_set_scope.find(parent_id)
326 current_parent.root
327 current_parent.root
327 else
328 else
328 self
329 self
329 end
330 end
330 end
331 end
331 end
332 end
332
333
333 # Returns the array of all parents and self
334 # Returns the array of all parents and self
334 def self_and_ancestors
335 def self_and_ancestors
335 nested_set_scope.where([
336 nested_set_scope.where([
336 "#{quoted_left_column_full_name} <= ? AND #{quoted_right_column_full_name} >= ?", left, right
337 "#{quoted_left_column_full_name} <= ? AND #{quoted_right_column_full_name} >= ?", left, right
337 ])
338 ])
338 end
339 end
339
340
340 # Returns an array of all parents
341 # Returns an array of all parents
341 def ancestors
342 def ancestors
342 without_self self_and_ancestors
343 without_self self_and_ancestors
343 end
344 end
344
345
345 # Returns the array of all children of the parent, including self
346 # Returns the array of all children of the parent, including self
346 def self_and_siblings
347 def self_and_siblings
347 nested_set_scope.where(parent_column_name => parent_id)
348 nested_set_scope.where(parent_column_name => parent_id)
348 end
349 end
349
350
350 # Returns the array of all children of the parent, except self
351 # Returns the array of all children of the parent, except self
351 def siblings
352 def siblings
352 without_self self_and_siblings
353 without_self self_and_siblings
353 end
354 end
354
355
355 # Returns a set of all of its nested children which do not have children
356 # Returns a set of all of its nested children which do not have children
356 def leaves
357 def leaves
357 descendants.where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1")
358 descendants.where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1")
358 end
359 end
359
360
360 # Returns the level of this object in the tree
361 # Returns the level of this object in the tree
361 # root level is 0
362 # root level is 0
362 def level
363 def level
363 parent_id.nil? ? 0 : compute_level
364 parent_id.nil? ? 0 : compute_level
364 end
365 end
365
366
366 # Returns a set of itself and all of its nested children
367 # Returns a set of itself and all of its nested children
367 def self_and_descendants
368 def self_and_descendants
368 nested_set_scope.where([
369 nested_set_scope.where([
369 "#{quoted_left_column_full_name} >= ? AND #{quoted_left_column_full_name} < ?", left, right
370 "#{quoted_left_column_full_name} >= ? AND #{quoted_left_column_full_name} < ?", left, right
370 # using _left_ for both sides here lets us benefit from an index on that column if one exists
371 # using _left_ for both sides here lets us benefit from an index on that column if one exists
371 ])
372 ])
372 end
373 end
373
374
374 # Returns a set of all of its children and nested children
375 # Returns a set of all of its children and nested children
375 def descendants
376 def descendants
376 without_self self_and_descendants
377 without_self self_and_descendants
377 end
378 end
378
379
379 def is_descendant_of?(other)
380 def is_descendant_of?(other)
380 other.left < self.left && self.left < other.right && same_scope?(other)
381 other.left < self.left && self.left < other.right && same_scope?(other)
381 end
382 end
382
383
383 def is_or_is_descendant_of?(other)
384 def is_or_is_descendant_of?(other)
384 other.left <= self.left && self.left < other.right && same_scope?(other)
385 other.left <= self.left && self.left < other.right && same_scope?(other)
385 end
386 end
386
387
387 def is_ancestor_of?(other)
388 def is_ancestor_of?(other)
388 self.left < other.left && other.left < self.right && same_scope?(other)
389 self.left < other.left && other.left < self.right && same_scope?(other)
389 end
390 end
390
391
391 def is_or_is_ancestor_of?(other)
392 def is_or_is_ancestor_of?(other)
392 self.left <= other.left && other.left < self.right && same_scope?(other)
393 self.left <= other.left && other.left < self.right && same_scope?(other)
393 end
394 end
394
395
395 # Check if other model is in the same scope
396 # Check if other model is in the same scope
396 def same_scope?(other)
397 def same_scope?(other)
397 Array(acts_as_nested_set_options[:scope]).all? do |attr|
398 Array(acts_as_nested_set_options[:scope]).all? do |attr|
398 self.send(attr) == other.send(attr)
399 self.send(attr) == other.send(attr)
399 end
400 end
400 end
401 end
401
402
402 # Find the first sibling to the left
403 # Find the first sibling to the left
403 def left_sibling
404 def left_sibling
404 siblings.where(["#{quoted_left_column_full_name} < ?", left]).
405 siblings.where(["#{quoted_left_column_full_name} < ?", left]).
405 order("#{quoted_left_column_full_name} DESC").last
406 order("#{quoted_left_column_full_name} DESC").last
406 end
407 end
407
408
408 # Find the first sibling to the right
409 # Find the first sibling to the right
409 def right_sibling
410 def right_sibling
410 siblings.where(["#{quoted_left_column_full_name} > ?", left]).first
411 siblings.where(["#{quoted_left_column_full_name} > ?", left]).first
411 end
412 end
412
413
413 # Shorthand method for finding the left sibling and moving to the left of it.
414 # Shorthand method for finding the left sibling and moving to the left of it.
414 def move_left
415 def move_left
415 move_to_left_of left_sibling
416 move_to_left_of left_sibling
416 end
417 end
417
418
418 # Shorthand method for finding the right sibling and moving to the right of it.
419 # Shorthand method for finding the right sibling and moving to the right of it.
419 def move_right
420 def move_right
420 move_to_right_of right_sibling
421 move_to_right_of right_sibling
421 end
422 end
422
423
423 # Move the node to the left of another node (you can pass id only)
424 # Move the node to the left of another node (you can pass id only)
424 def move_to_left_of(node)
425 def move_to_left_of(node)
425 move_to node, :left
426 move_to node, :left
426 end
427 end
427
428
428 # Move the node to the left of another node (you can pass id only)
429 # Move the node to the left of another node (you can pass id only)
429 def move_to_right_of(node)
430 def move_to_right_of(node)
430 move_to node, :right
431 move_to node, :right
431 end
432 end
432
433
433 # Move the node to the child of another node (you can pass id only)
434 # Move the node to the child of another node (you can pass id only)
434 def move_to_child_of(node)
435 def move_to_child_of(node)
435 move_to node, :child
436 move_to node, :child
436 end
437 end
437
438
438 # Move the node to the child of another node with specify index (you can pass id only)
439 # Move the node to the child of another node with specify index (you can pass id only)
439 def move_to_child_with_index(node, index)
440 def move_to_child_with_index(node, index)
440 if node.children.empty?
441 if node.children.empty?
441 move_to_child_of(node)
442 move_to_child_of(node)
442 elsif node.children.count == index
443 elsif node.children.count == index
443 move_to_right_of(node.children.last)
444 move_to_right_of(node.children.last)
444 else
445 else
445 move_to_left_of(node.children[index])
446 move_to_left_of(node.children[index])
446 end
447 end
447 end
448 end
448
449
449 # Move the node to root nodes
450 # Move the node to root nodes
450 def move_to_root
451 def move_to_root
451 move_to nil, :root
452 move_to nil, :root
452 end
453 end
453
454
454 # Order children in a nested set by an attribute
455 # Order children in a nested set by an attribute
455 # Can order by any attribute class that uses the Comparable mixin, for example a string or integer
456 # Can order by any attribute class that uses the Comparable mixin, for example a string or integer
456 # Usage example when sorting categories alphabetically: @new_category.move_to_ordered_child_of(@root, "name")
457 # Usage example when sorting categories alphabetically: @new_category.move_to_ordered_child_of(@root, "name")
457 def move_to_ordered_child_of(parent, order_attribute, ascending = true)
458 def move_to_ordered_child_of(parent, order_attribute, ascending = true)
458 self.move_to_root and return unless parent
459 self.move_to_root and return unless parent
459 left = nil # This is needed, at least for the tests.
460 left = nil # This is needed, at least for the tests.
460 parent.children.each do |n| # Find the node immediately to the left of this node.
461 parent.children.each do |n| # Find the node immediately to the left of this node.
461 if ascending
462 if ascending
462 left = n if n.send(order_attribute) < self.send(order_attribute)
463 left = n if n.send(order_attribute) < self.send(order_attribute)
463 else
464 else
464 left = n if n.send(order_attribute) > self.send(order_attribute)
465 left = n if n.send(order_attribute) > self.send(order_attribute)
465 end
466 end
466 end
467 end
467 self.move_to_child_of(parent)
468 self.move_to_child_of(parent)
468 return unless parent.children.count > 1 # Only need to order if there are multiple children.
469 return unless parent.children.count > 1 # Only need to order if there are multiple children.
469 if left # Self has a left neighbor.
470 if left # Self has a left neighbor.
470 self.move_to_right_of(left)
471 self.move_to_right_of(left)
471 else # Self is the left most node.
472 else # Self is the left most node.
472 self.move_to_left_of(parent.children[0])
473 self.move_to_left_of(parent.children[0])
473 end
474 end
474 end
475 end
475
476
476 def move_possible?(target)
477 def move_possible?(target)
477 self != target && # Can't target self
478 self != target && # Can't target self
478 same_scope?(target) && # can't be in different scopes
479 same_scope?(target) && # can't be in different scopes
479 # !(left..right).include?(target.left..target.right) # this needs tested more
480 # !(left..right).include?(target.left..target.right) # this needs tested more
480 # detect impossible move
481 # detect impossible move
481 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
482 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
482 end
483 end
483
484
484 def to_text
485 def to_text
485 self_and_descendants.map do |node|
486 self_and_descendants.map do |node|
486 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
487 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
487 end.join("\n")
488 end.join("\n")
488 end
489 end
489
490
490 protected
491 protected
491 def compute_level
492 def compute_level
492 node, nesting = self, 0
493 node, nesting = self, 0
493 while (association = node.association(:parent)).loaded? && association.target
494 while (association = node.association(:parent)).loaded? && association.target
494 nesting += 1
495 nesting += 1
495 node = node.parent
496 node = node.parent
496 end if node.respond_to? :association
497 end if node.respond_to? :association
497 node == self ? ancestors.count : node.level + nesting
498 node == self ? ancestors.count : node.level + nesting
498 end
499 end
499
500
500 def without_self(scope)
501 def without_self(scope)
501 scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
502 scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
502 end
503 end
503
504
504 # All nested set queries should use this nested_set_scope, which performs finds on
505 # All nested set queries should use this nested_set_scope, which performs finds on
505 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
506 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
506 # declaration.
507 # declaration.
507 def nested_set_scope(options = {})
508 def nested_set_scope(options = {})
508 options = {:order => quoted_left_column_full_name}.merge(options)
509 options = {:order => quoted_left_column_full_name}.merge(options)
509 scopes = Array(acts_as_nested_set_options[:scope])
510 scopes = Array(acts_as_nested_set_options[:scope])
510 options[:conditions] = scopes.inject({}) do |conditions,attr|
511 options[:conditions] = scopes.inject({}) do |conditions,attr|
511 conditions.merge attr => self[attr]
512 conditions.merge attr => self[attr]
512 end unless scopes.empty?
513 end unless scopes.empty?
513 self.class.base_class.unscoped.scoped options
514 self.class.base_class.unscoped.scoped options
514 end
515 end
515
516
516 def store_new_parent
517 def store_new_parent
517 @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
518 @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
518 true # force callback to return true
519 true # force callback to return true
519 end
520 end
520
521
521 def move_to_new_parent
522 def move_to_new_parent
522 if @move_to_new_parent_id.nil?
523 if @move_to_new_parent_id.nil?
523 move_to_root
524 move_to_root
524 elsif @move_to_new_parent_id
525 elsif @move_to_new_parent_id
525 move_to_child_of(@move_to_new_parent_id)
526 move_to_child_of(@move_to_new_parent_id)
526 end
527 end
527 end
528 end
528
529
529 def set_depth!
530 def set_depth!
530 if nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
531 if nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
531 in_tenacious_transaction do
532 in_tenacious_transaction do
532 reload
533 reload
533
534
534 nested_set_scope.where(:id => id).update_all(["#{quoted_depth_column_name} = ?", level])
535 nested_set_scope.where(:id => id).update_all(["#{quoted_depth_column_name} = ?", level])
535 end
536 end
536 self[depth_column_name.to_sym] = self.level
537 self[depth_column_name.to_sym] = self.level
537 end
538 end
538 end
539 end
539
540
540 # on creation, set automatically lft and rgt to the end of the tree
541 # on creation, set automatically lft and rgt to the end of the tree
541 def set_default_left_and_right
542 def set_default_left_and_right
542 highest_right_row = nested_set_scope(:order => "#{quoted_right_column_full_name} desc").limit(1).lock(true).first
543 highest_right_row = nested_set_scope(:order => "#{quoted_right_column_full_name} desc").limit(1).lock(true).first
543 maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
544 maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
544 # adds the new node to the right of all existing nodes
545 # adds the new node to the right of all existing nodes
545 self[left_column_name] = maxright + 1
546 self[left_column_name] = maxright + 1
546 self[right_column_name] = maxright + 2
547 self[right_column_name] = maxright + 2
547 end
548 end
548
549
549 def in_tenacious_transaction(&block)
550 def in_tenacious_transaction(&block)
550 retry_count = 0
551 retry_count = 0
551 begin
552 begin
552 transaction(&block)
553 transaction(&block)
553 rescue ActiveRecord::StatementInvalid => error
554 rescue ActiveRecord::StatementInvalid => error
554 raise unless connection.open_transactions.zero?
555 raise unless connection.open_transactions.zero?
555 raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
556 raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
556 raise unless retry_count < 10
557 raise unless retry_count < 10
557 retry_count += 1
558 retry_count += 1
558 logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
559 logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
559 sleep(rand(retry_count)*0.1) # Aloha protocol
560 sleep(rand(retry_count)*0.1) # Aloha protocol
560 retry
561 retry
561 end
562 end
562 end
563 end
563
564
564 # Prunes a branch off of the tree, shifting all of the elements on the right
565 # Prunes a branch off of the tree, shifting all of the elements on the right
565 # back to the left so the counts still work.
566 # back to the left so the counts still work.
566 def destroy_descendants
567 def destroy_descendants
567 return if right.nil? || left.nil? || skip_before_destroy
568 return if right.nil? || left.nil? || skip_before_destroy
568
569
569 in_tenacious_transaction do
570 in_tenacious_transaction do
570 reload_nested_set
571 reload_nested_set
571 # select the rows in the model that extend past the deletion point and apply a lock
572 # select the rows in the model that extend past the deletion point and apply a lock
572 nested_set_scope.where(["#{quoted_left_column_full_name} >= ?", left]).
573 nested_set_scope.where(["#{quoted_left_column_full_name} >= ?", left]).
573 select(id).lock(true)
574 select(id).lock(true)
574
575
575 if acts_as_nested_set_options[:dependent] == :destroy
576 if acts_as_nested_set_options[:dependent] == :destroy
576 descendants.each do |model|
577 descendants.each do |model|
577 model.skip_before_destroy = true
578 model.skip_before_destroy = true
578 model.destroy
579 model.destroy
579 end
580 end
580 else
581 else
581 nested_set_scope.where(["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", left, right]).
582 nested_set_scope.where(["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", left, right]).
582 delete_all
583 delete_all
583 end
584 end
584
585
585 # update lefts and rights for remaining nodes
586 # update lefts and rights for remaining nodes
586 diff = right - left + 1
587 diff = right - left + 1
587 nested_set_scope.where(["#{quoted_left_column_full_name} > ?", right]).update_all(
588 nested_set_scope.where(["#{quoted_left_column_full_name} > ?", right]).update_all(
588 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff]
589 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff]
589 )
590 )
590
591
591 nested_set_scope.where(["#{quoted_right_column_full_name} > ?", right]).update_all(
592 nested_set_scope.where(["#{quoted_right_column_full_name} > ?", right]).update_all(
592 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff]
593 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff]
593 )
594 )
594
595
595 reload
596 reload
596 # Don't allow multiple calls to destroy to corrupt the set
597 # Don't allow multiple calls to destroy to corrupt the set
597 self.skip_before_destroy = true
598 self.skip_before_destroy = true
598 end
599 end
599 end
600 end
600
601
601 # reload left, right, and parent
602 # reload left, right, and parent
602 def reload_nested_set
603 def reload_nested_set
603 reload(
604 reload(
604 :select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}",
605 :select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}",
605 :lock => true
606 :lock => true
606 )
607 )
607 end
608 end
608
609
609 def move_to(target, position)
610 def move_to(target, position)
610 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
611 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
611 run_callbacks :move do
612 run_callbacks :move do
612 in_tenacious_transaction do
613 in_tenacious_transaction do
613 if target.is_a? self.class.base_class
614 if target.is_a? self.class.base_class
614 target.reload_nested_set
615 target.reload_nested_set
615 elsif position != :root
616 elsif position != :root
616 # load object if node is not an object
617 # load object if node is not an object
617 target = nested_set_scope.find(target)
618 target = nested_set_scope.find(target)
618 end
619 end
619 self.reload_nested_set
620 self.reload_nested_set
620
621
621 unless position == :root || move_possible?(target)
622 unless position == :root || move_possible?(target)
622 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
623 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
623 end
624 end
624
625
625 bound = case position
626 bound = case position
626 when :child; target[right_column_name]
627 when :child; target[right_column_name]
627 when :left; target[left_column_name]
628 when :left; target[left_column_name]
628 when :right; target[right_column_name] + 1
629 when :right; target[right_column_name] + 1
629 when :root; 1
630 when :root; 1
630 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
631 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
631 end
632 end
632
633
633 if bound > self[right_column_name]
634 if bound > self[right_column_name]
634 bound = bound - 1
635 bound = bound - 1
635 other_bound = self[right_column_name] + 1
636 other_bound = self[right_column_name] + 1
636 else
637 else
637 other_bound = self[left_column_name] - 1
638 other_bound = self[left_column_name] - 1
638 end
639 end
639
640
640 # there would be no change
641 # there would be no change
641 return if bound == self[right_column_name] || bound == self[left_column_name]
642 return if bound == self[right_column_name] || bound == self[left_column_name]
642
643
643 # we have defined the boundaries of two non-overlapping intervals,
644 # we have defined the boundaries of two non-overlapping intervals,
644 # so sorting puts both the intervals and their boundaries in order
645 # so sorting puts both the intervals and their boundaries in order
645 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
646 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
646
647
647 # select the rows in the model between a and d, and apply a lock
648 # select the rows in the model between a and d, and apply a lock
648 self.class.base_class.select('id').lock(true).where(
649 self.class.base_class.select('id').lock(true).where(
649 ["#{quoted_left_column_full_name} >= :a and #{quoted_right_column_full_name} <= :d", {:a => a, :d => d}]
650 ["#{quoted_left_column_full_name} >= :a and #{quoted_right_column_full_name} <= :d", {:a => a, :d => d}]
650 )
651 )
651
652
652 new_parent = case position
653 new_parent = case position
653 when :child; target.id
654 when :child; target.id
654 when :root; nil
655 when :root; nil
655 else target[parent_column_name]
656 else target[parent_column_name]
656 end
657 end
657
658
658 self.nested_set_scope.update_all([
659 self.nested_set_scope.update_all([
659 "#{quoted_left_column_name} = CASE " +
660 "#{quoted_left_column_name} = CASE " +
660 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
661 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
661 "THEN #{quoted_left_column_name} + :d - :b " +
662 "THEN #{quoted_left_column_name} + :d - :b " +
662 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
663 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
663 "THEN #{quoted_left_column_name} + :a - :c " +
664 "THEN #{quoted_left_column_name} + :a - :c " +
664 "ELSE #{quoted_left_column_name} END, " +
665 "ELSE #{quoted_left_column_name} END, " +
665 "#{quoted_right_column_name} = CASE " +
666 "#{quoted_right_column_name} = CASE " +
666 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
667 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
667 "THEN #{quoted_right_column_name} + :d - :b " +
668 "THEN #{quoted_right_column_name} + :d - :b " +
668 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
669 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
669 "THEN #{quoted_right_column_name} + :a - :c " +
670 "THEN #{quoted_right_column_name} + :a - :c " +
670 "ELSE #{quoted_right_column_name} END, " +
671 "ELSE #{quoted_right_column_name} END, " +
671 "#{quoted_parent_column_name} = CASE " +
672 "#{quoted_parent_column_name} = CASE " +
672 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
673 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
673 "ELSE #{quoted_parent_column_name} END",
674 "ELSE #{quoted_parent_column_name} END",
674 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
675 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
675 ])
676 ])
676 end
677 end
677 target.reload_nested_set if target
678 target.reload_nested_set if target
678 self.set_depth!
679 self.set_depth!
679 self.descendants.each(&:save)
680 self.descendants.each(&:save)
680 self.reload_nested_set
681 self.reload_nested_set
681 end
682 end
682 end
683 end
683
684
684 end
685 end
685
686
686 # Mixed into both classes and instances to provide easy access to the column names
687 # Mixed into both classes and instances to provide easy access to the column names
687 module Columns
688 module Columns
688 def left_column_name
689 def left_column_name
689 acts_as_nested_set_options[:left_column]
690 acts_as_nested_set_options[:left_column]
690 end
691 end
691
692
692 def right_column_name
693 def right_column_name
693 acts_as_nested_set_options[:right_column]
694 acts_as_nested_set_options[:right_column]
694 end
695 end
695
696
696 def depth_column_name
697 def depth_column_name
697 acts_as_nested_set_options[:depth_column]
698 acts_as_nested_set_options[:depth_column]
698 end
699 end
699
700
700 def parent_column_name
701 def parent_column_name
701 acts_as_nested_set_options[:parent_column]
702 acts_as_nested_set_options[:parent_column]
702 end
703 end
703
704
704 def order_column
705 def order_column
705 acts_as_nested_set_options[:order_column] || left_column_name
706 acts_as_nested_set_options[:order_column] || left_column_name
706 end
707 end
707
708
708 def scope_column_names
709 def scope_column_names
709 Array(acts_as_nested_set_options[:scope])
710 Array(acts_as_nested_set_options[:scope])
710 end
711 end
711
712
712 def quoted_left_column_name
713 def quoted_left_column_name
713 connection.quote_column_name(left_column_name)
714 connection.quote_column_name(left_column_name)
714 end
715 end
715
716
716 def quoted_right_column_name
717 def quoted_right_column_name
717 connection.quote_column_name(right_column_name)
718 connection.quote_column_name(right_column_name)
718 end
719 end
719
720
720 def quoted_depth_column_name
721 def quoted_depth_column_name
721 connection.quote_column_name(depth_column_name)
722 connection.quote_column_name(depth_column_name)
722 end
723 end
723
724
724 def quoted_parent_column_name
725 def quoted_parent_column_name
725 connection.quote_column_name(parent_column_name)
726 connection.quote_column_name(parent_column_name)
726 end
727 end
727
728
728 def quoted_scope_column_names
729 def quoted_scope_column_names
729 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
730 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
730 end
731 end
731
732
732 def quoted_left_column_full_name
733 def quoted_left_column_full_name
733 "#{quoted_table_name}.#{quoted_left_column_name}"
734 "#{quoted_table_name}.#{quoted_left_column_name}"
734 end
735 end
735
736
736 def quoted_right_column_full_name
737 def quoted_right_column_full_name
737 "#{quoted_table_name}.#{quoted_right_column_name}"
738 "#{quoted_table_name}.#{quoted_right_column_name}"
738 end
739 end
739
740
740 def quoted_parent_column_full_name
741 def quoted_parent_column_full_name
741 "#{quoted_table_name}.#{quoted_parent_column_name}"
742 "#{quoted_table_name}.#{quoted_parent_column_name}"
742 end
743 end
743 end
744 end
744
745
745 end
746 end
746 end
747 end
747 end
748 end
General Comments 0
You need to be logged in to leave comments. Login now