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