##// END OF EJS Templates
Recalculate inherited attributes on parents when a child is moved under a new parent. #5524...
Eric Davis -
r3707:c6201ae15b4f
parent child
Show More
@@ -1,826 +1,833
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 belongs_to :project
19 belongs_to :project
20 belongs_to :tracker
20 belongs_to :tracker
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27
27
28 has_many :journals, :as => :journalized, :dependent => :destroy
28 has_many :journals, :as => :journalized, :dependent => :destroy
29 has_many :time_entries, :dependent => :delete_all
29 has_many :time_entries, :dependent => :delete_all
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
31
31
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
34
34
35 acts_as_nested_set :scope => 'root_id'
35 acts_as_nested_set :scope => 'root_id'
36 acts_as_attachable :after_remove => :attachment_removed
36 acts_as_attachable :after_remove => :attachment_removed
37 acts_as_customizable
37 acts_as_customizable
38 acts_as_watchable
38 acts_as_watchable
39 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
40 :include => [:project, :journals],
40 :include => [:project, :journals],
41 # sort by id so that limited eager loading doesn't break with postgresql
41 # sort by id so that limited eager loading doesn't break with postgresql
42 :order_column => "#{table_name}.id"
42 :order_column => "#{table_name}.id"
43 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
43 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
44 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
44 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
45 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
45 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
46
46
47 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
47 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
48 :author_key => :author_id
48 :author_key => :author_id
49
49
50 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
50 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
51
51
52 attr_reader :current_journal
52 attr_reader :current_journal
53
53
54 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
54 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
55
55
56 validates_length_of :subject, :maximum => 255
56 validates_length_of :subject, :maximum => 255
57 validates_inclusion_of :done_ratio, :in => 0..100
57 validates_inclusion_of :done_ratio, :in => 0..100
58 validates_numericality_of :estimated_hours, :allow_nil => true
58 validates_numericality_of :estimated_hours, :allow_nil => true
59
59
60 named_scope :visible, lambda {|*args| { :include => :project,
60 named_scope :visible, lambda {|*args| { :include => :project,
61 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
61 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
62
62
63 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
63 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
64
64
65 named_scope :recently_updated, :order => "#{self.table_name}.updated_on DESC"
65 named_scope :recently_updated, :order => "#{self.table_name}.updated_on DESC"
66 named_scope :with_limit, lambda { |limit| { :limit => limit} }
66 named_scope :with_limit, lambda { |limit| { :limit => limit} }
67 named_scope :on_active_project, :include => [:status, :project, :tracker],
67 named_scope :on_active_project, :include => [:status, :project, :tracker],
68 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
68 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
69
69
70 before_create :default_assign
70 before_create :default_assign
71 before_save :reschedule_following_issues, :close_duplicates, :update_done_ratio_from_issue_status
71 before_save :reschedule_following_issues, :close_duplicates, :update_done_ratio_from_issue_status
72 after_save :update_nested_set_attributes, :update_parent_attributes, :create_journal
72 after_save :update_nested_set_attributes, :update_parent_attributes, :create_journal
73 after_destroy :destroy_children
73 after_destroy :destroy_children
74 after_destroy :update_parent_attributes
74 after_destroy :update_parent_attributes
75
75
76 # Returns true if usr or current user is allowed to view the issue
76 # Returns true if usr or current user is allowed to view the issue
77 def visible?(usr=nil)
77 def visible?(usr=nil)
78 (usr || User.current).allowed_to?(:view_issues, self.project)
78 (usr || User.current).allowed_to?(:view_issues, self.project)
79 end
79 end
80
80
81 def after_initialize
81 def after_initialize
82 if new_record?
82 if new_record?
83 # set default values for new records only
83 # set default values for new records only
84 self.status ||= IssueStatus.default
84 self.status ||= IssueStatus.default
85 self.priority ||= IssuePriority.default
85 self.priority ||= IssuePriority.default
86 end
86 end
87 end
87 end
88
88
89 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
89 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
90 def available_custom_fields
90 def available_custom_fields
91 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
91 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
92 end
92 end
93
93
94 def copy_from(arg)
94 def copy_from(arg)
95 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
95 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
96 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
96 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
97 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
97 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
98 self.status = issue.status
98 self.status = issue.status
99 self
99 self
100 end
100 end
101
101
102 # Moves/copies an issue to a new project and tracker
102 # Moves/copies an issue to a new project and tracker
103 # Returns the moved/copied issue on success, false on failure
103 # Returns the moved/copied issue on success, false on failure
104 def move_to_project(*args)
104 def move_to_project(*args)
105 ret = Issue.transaction do
105 ret = Issue.transaction do
106 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
106 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
107 end || false
107 end || false
108 end
108 end
109
109
110 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
110 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
111 options ||= {}
111 options ||= {}
112 issue = options[:copy] ? self.class.new.copy_from(self) : self
112 issue = options[:copy] ? self.class.new.copy_from(self) : self
113
113
114 if new_project && issue.project_id != new_project.id
114 if new_project && issue.project_id != new_project.id
115 # delete issue relations
115 # delete issue relations
116 unless Setting.cross_project_issue_relations?
116 unless Setting.cross_project_issue_relations?
117 issue.relations_from.clear
117 issue.relations_from.clear
118 issue.relations_to.clear
118 issue.relations_to.clear
119 end
119 end
120 # issue is moved to another project
120 # issue is moved to another project
121 # reassign to the category with same name if any
121 # reassign to the category with same name if any
122 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
122 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
123 issue.category = new_category
123 issue.category = new_category
124 # Keep the fixed_version if it's still valid in the new_project
124 # Keep the fixed_version if it's still valid in the new_project
125 unless new_project.shared_versions.include?(issue.fixed_version)
125 unless new_project.shared_versions.include?(issue.fixed_version)
126 issue.fixed_version = nil
126 issue.fixed_version = nil
127 end
127 end
128 issue.project = new_project
128 issue.project = new_project
129 if issue.parent && issue.parent.project_id != issue.project_id
129 if issue.parent && issue.parent.project_id != issue.project_id
130 issue.parent_issue_id = nil
130 issue.parent_issue_id = nil
131 end
131 end
132 end
132 end
133 if new_tracker
133 if new_tracker
134 issue.tracker = new_tracker
134 issue.tracker = new_tracker
135 issue.reset_custom_values!
135 issue.reset_custom_values!
136 end
136 end
137 if options[:copy]
137 if options[:copy]
138 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
138 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
139 issue.status = if options[:attributes] && options[:attributes][:status_id]
139 issue.status = if options[:attributes] && options[:attributes][:status_id]
140 IssueStatus.find_by_id(options[:attributes][:status_id])
140 IssueStatus.find_by_id(options[:attributes][:status_id])
141 else
141 else
142 self.status
142 self.status
143 end
143 end
144 end
144 end
145 # Allow bulk setting of attributes on the issue
145 # Allow bulk setting of attributes on the issue
146 if options[:attributes]
146 if options[:attributes]
147 issue.attributes = options[:attributes]
147 issue.attributes = options[:attributes]
148 end
148 end
149 if issue.save
149 if issue.save
150 unless options[:copy]
150 unless options[:copy]
151 # Manually update project_id on related time entries
151 # Manually update project_id on related time entries
152 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
152 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
153
153
154 issue.children.each do |child|
154 issue.children.each do |child|
155 unless child.move_to_project_without_transaction(new_project)
155 unless child.move_to_project_without_transaction(new_project)
156 # Move failed and transaction was rollback'd
156 # Move failed and transaction was rollback'd
157 return false
157 return false
158 end
158 end
159 end
159 end
160 end
160 end
161 else
161 else
162 return false
162 return false
163 end
163 end
164 issue
164 issue
165 end
165 end
166
166
167 def status_id=(sid)
167 def status_id=(sid)
168 self.status = nil
168 self.status = nil
169 write_attribute(:status_id, sid)
169 write_attribute(:status_id, sid)
170 end
170 end
171
171
172 def priority_id=(pid)
172 def priority_id=(pid)
173 self.priority = nil
173 self.priority = nil
174 write_attribute(:priority_id, pid)
174 write_attribute(:priority_id, pid)
175 end
175 end
176
176
177 def tracker_id=(tid)
177 def tracker_id=(tid)
178 self.tracker = nil
178 self.tracker = nil
179 result = write_attribute(:tracker_id, tid)
179 result = write_attribute(:tracker_id, tid)
180 @custom_field_values = nil
180 @custom_field_values = nil
181 result
181 result
182 end
182 end
183
183
184 # Overrides attributes= so that tracker_id gets assigned first
184 # Overrides attributes= so that tracker_id gets assigned first
185 def attributes_with_tracker_first=(new_attributes, *args)
185 def attributes_with_tracker_first=(new_attributes, *args)
186 return if new_attributes.nil?
186 return if new_attributes.nil?
187 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
187 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
188 if new_tracker_id
188 if new_tracker_id
189 self.tracker_id = new_tracker_id
189 self.tracker_id = new_tracker_id
190 end
190 end
191 send :attributes_without_tracker_first=, new_attributes, *args
191 send :attributes_without_tracker_first=, new_attributes, *args
192 end
192 end
193 # Do not redefine alias chain on reload (see #4838)
193 # Do not redefine alias chain on reload (see #4838)
194 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
194 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
195
195
196 def estimated_hours=(h)
196 def estimated_hours=(h)
197 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
197 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
198 end
198 end
199
199
200 SAFE_ATTRIBUTES = %w(
200 SAFE_ATTRIBUTES = %w(
201 tracker_id
201 tracker_id
202 status_id
202 status_id
203 parent_issue_id
203 parent_issue_id
204 category_id
204 category_id
205 assigned_to_id
205 assigned_to_id
206 priority_id
206 priority_id
207 fixed_version_id
207 fixed_version_id
208 subject
208 subject
209 description
209 description
210 start_date
210 start_date
211 due_date
211 due_date
212 done_ratio
212 done_ratio
213 estimated_hours
213 estimated_hours
214 custom_field_values
214 custom_field_values
215 lock_version
215 lock_version
216 ) unless const_defined?(:SAFE_ATTRIBUTES)
216 ) unless const_defined?(:SAFE_ATTRIBUTES)
217
217
218 # Safely sets attributes
218 # Safely sets attributes
219 # Should be called from controllers instead of #attributes=
219 # Should be called from controllers instead of #attributes=
220 # attr_accessible is too rough because we still want things like
220 # attr_accessible is too rough because we still want things like
221 # Issue.new(:project => foo) to work
221 # Issue.new(:project => foo) to work
222 # TODO: move workflow/permission checks from controllers to here
222 # TODO: move workflow/permission checks from controllers to here
223 def safe_attributes=(attrs, user=User.current)
223 def safe_attributes=(attrs, user=User.current)
224 return if attrs.nil?
224 return if attrs.nil?
225 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
225 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
226 if attrs['status_id']
226 if attrs['status_id']
227 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
227 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
228 attrs.delete('status_id')
228 attrs.delete('status_id')
229 end
229 end
230 end
230 end
231
231
232 unless leaf?
232 unless leaf?
233 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
233 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
234 end
234 end
235
235
236 if attrs.has_key?('parent_issue_id')
236 if attrs.has_key?('parent_issue_id')
237 if !user.allowed_to?(:manage_subtasks, project)
237 if !user.allowed_to?(:manage_subtasks, project)
238 attrs.delete('parent_issue_id')
238 attrs.delete('parent_issue_id')
239 elsif !attrs['parent_issue_id'].blank?
239 elsif !attrs['parent_issue_id'].blank?
240 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'])
240 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'])
241 end
241 end
242 end
242 end
243
243
244 self.attributes = attrs
244 self.attributes = attrs
245 end
245 end
246
246
247 def done_ratio
247 def done_ratio
248 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
248 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
249 status.default_done_ratio
249 status.default_done_ratio
250 else
250 else
251 read_attribute(:done_ratio)
251 read_attribute(:done_ratio)
252 end
252 end
253 end
253 end
254
254
255 def self.use_status_for_done_ratio?
255 def self.use_status_for_done_ratio?
256 Setting.issue_done_ratio == 'issue_status'
256 Setting.issue_done_ratio == 'issue_status'
257 end
257 end
258
258
259 def self.use_field_for_done_ratio?
259 def self.use_field_for_done_ratio?
260 Setting.issue_done_ratio == 'issue_field'
260 Setting.issue_done_ratio == 'issue_field'
261 end
261 end
262
262
263 def validate
263 def validate
264 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
264 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
265 errors.add :due_date, :not_a_date
265 errors.add :due_date, :not_a_date
266 end
266 end
267
267
268 if self.due_date and self.start_date and self.due_date < self.start_date
268 if self.due_date and self.start_date and self.due_date < self.start_date
269 errors.add :due_date, :greater_than_start_date
269 errors.add :due_date, :greater_than_start_date
270 end
270 end
271
271
272 if start_date && soonest_start && start_date < soonest_start
272 if start_date && soonest_start && start_date < soonest_start
273 errors.add :start_date, :invalid
273 errors.add :start_date, :invalid
274 end
274 end
275
275
276 if fixed_version
276 if fixed_version
277 if !assignable_versions.include?(fixed_version)
277 if !assignable_versions.include?(fixed_version)
278 errors.add :fixed_version_id, :inclusion
278 errors.add :fixed_version_id, :inclusion
279 elsif reopened? && fixed_version.closed?
279 elsif reopened? && fixed_version.closed?
280 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
280 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
281 end
281 end
282 end
282 end
283
283
284 # Checks that the issue can not be added/moved to a disabled tracker
284 # Checks that the issue can not be added/moved to a disabled tracker
285 if project && (tracker_id_changed? || project_id_changed?)
285 if project && (tracker_id_changed? || project_id_changed?)
286 unless project.trackers.include?(tracker)
286 unless project.trackers.include?(tracker)
287 errors.add :tracker_id, :inclusion
287 errors.add :tracker_id, :inclusion
288 end
288 end
289 end
289 end
290
290
291 # Checks parent issue assignment
291 # Checks parent issue assignment
292 if @parent_issue
292 if @parent_issue
293 if @parent_issue.project_id != project_id
293 if @parent_issue.project_id != project_id
294 errors.add :parent_issue_id, :not_same_project
294 errors.add :parent_issue_id, :not_same_project
295 elsif !new_record?
295 elsif !new_record?
296 # moving an existing issue
296 # moving an existing issue
297 if @parent_issue.root_id != root_id
297 if @parent_issue.root_id != root_id
298 # we can always move to another tree
298 # we can always move to another tree
299 elsif move_possible?(@parent_issue)
299 elsif move_possible?(@parent_issue)
300 # move accepted inside tree
300 # move accepted inside tree
301 else
301 else
302 errors.add :parent_issue_id, :not_a_valid_parent
302 errors.add :parent_issue_id, :not_a_valid_parent
303 end
303 end
304 end
304 end
305 end
305 end
306 end
306 end
307
307
308 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
308 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
309 # even if the user turns off the setting later
309 # even if the user turns off the setting later
310 def update_done_ratio_from_issue_status
310 def update_done_ratio_from_issue_status
311 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
311 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
312 self.done_ratio = status.default_done_ratio
312 self.done_ratio = status.default_done_ratio
313 end
313 end
314 end
314 end
315
315
316 def init_journal(user, notes = "")
316 def init_journal(user, notes = "")
317 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
317 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
318 @issue_before_change = self.clone
318 @issue_before_change = self.clone
319 @issue_before_change.status = self.status
319 @issue_before_change.status = self.status
320 @custom_values_before_change = {}
320 @custom_values_before_change = {}
321 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
321 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
322 # Make sure updated_on is updated when adding a note.
322 # Make sure updated_on is updated when adding a note.
323 updated_on_will_change!
323 updated_on_will_change!
324 @current_journal
324 @current_journal
325 end
325 end
326
326
327 # Return true if the issue is closed, otherwise false
327 # Return true if the issue is closed, otherwise false
328 def closed?
328 def closed?
329 self.status.is_closed?
329 self.status.is_closed?
330 end
330 end
331
331
332 # Return true if the issue is being reopened
332 # Return true if the issue is being reopened
333 def reopened?
333 def reopened?
334 if !new_record? && status_id_changed?
334 if !new_record? && status_id_changed?
335 status_was = IssueStatus.find_by_id(status_id_was)
335 status_was = IssueStatus.find_by_id(status_id_was)
336 status_new = IssueStatus.find_by_id(status_id)
336 status_new = IssueStatus.find_by_id(status_id)
337 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
337 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
338 return true
338 return true
339 end
339 end
340 end
340 end
341 false
341 false
342 end
342 end
343
343
344 # Return true if the issue is being closed
344 # Return true if the issue is being closed
345 def closing?
345 def closing?
346 if !new_record? && status_id_changed?
346 if !new_record? && status_id_changed?
347 status_was = IssueStatus.find_by_id(status_id_was)
347 status_was = IssueStatus.find_by_id(status_id_was)
348 status_new = IssueStatus.find_by_id(status_id)
348 status_new = IssueStatus.find_by_id(status_id)
349 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
349 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
350 return true
350 return true
351 end
351 end
352 end
352 end
353 false
353 false
354 end
354 end
355
355
356 # Returns true if the issue is overdue
356 # Returns true if the issue is overdue
357 def overdue?
357 def overdue?
358 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
358 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
359 end
359 end
360
360
361 # Users the issue can be assigned to
361 # Users the issue can be assigned to
362 def assignable_users
362 def assignable_users
363 project.assignable_users
363 project.assignable_users
364 end
364 end
365
365
366 # Versions that the issue can be assigned to
366 # Versions that the issue can be assigned to
367 def assignable_versions
367 def assignable_versions
368 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
368 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
369 end
369 end
370
370
371 # Returns true if this issue is blocked by another issue that is still open
371 # Returns true if this issue is blocked by another issue that is still open
372 def blocked?
372 def blocked?
373 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
373 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
374 end
374 end
375
375
376 # Returns an array of status that user is able to apply
376 # Returns an array of status that user is able to apply
377 def new_statuses_allowed_to(user, include_default=false)
377 def new_statuses_allowed_to(user, include_default=false)
378 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
378 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
379 statuses << status unless statuses.empty?
379 statuses << status unless statuses.empty?
380 statuses << IssueStatus.default if include_default
380 statuses << IssueStatus.default if include_default
381 statuses = statuses.uniq.sort
381 statuses = statuses.uniq.sort
382 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
382 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
383 end
383 end
384
384
385 # Returns the mail adresses of users that should be notified
385 # Returns the mail adresses of users that should be notified
386 def recipients
386 def recipients
387 notified = project.notified_users
387 notified = project.notified_users
388 # Author and assignee are always notified unless they have been locked
388 # Author and assignee are always notified unless they have been locked
389 notified << author if author && author.active?
389 notified << author if author && author.active?
390 notified << assigned_to if assigned_to && assigned_to.active?
390 notified << assigned_to if assigned_to && assigned_to.active?
391 notified.uniq!
391 notified.uniq!
392 # Remove users that can not view the issue
392 # Remove users that can not view the issue
393 notified.reject! {|user| !visible?(user)}
393 notified.reject! {|user| !visible?(user)}
394 notified.collect(&:mail)
394 notified.collect(&:mail)
395 end
395 end
396
396
397 # Returns the total number of hours spent on this issue and its descendants
397 # Returns the total number of hours spent on this issue and its descendants
398 #
398 #
399 # Example:
399 # Example:
400 # spent_hours => 0.0
400 # spent_hours => 0.0
401 # spent_hours => 50.2
401 # spent_hours => 50.2
402 def spent_hours
402 def spent_hours
403 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
403 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
404 end
404 end
405
405
406 def relations
406 def relations
407 (relations_from + relations_to).sort
407 (relations_from + relations_to).sort
408 end
408 end
409
409
410 def all_dependent_issues
410 def all_dependent_issues
411 dependencies = []
411 dependencies = []
412 relations_from.each do |relation|
412 relations_from.each do |relation|
413 dependencies << relation.issue_to
413 dependencies << relation.issue_to
414 dependencies += relation.issue_to.all_dependent_issues
414 dependencies += relation.issue_to.all_dependent_issues
415 end
415 end
416 dependencies
416 dependencies
417 end
417 end
418
418
419 # Returns an array of issues that duplicate this one
419 # Returns an array of issues that duplicate this one
420 def duplicates
420 def duplicates
421 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
421 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
422 end
422 end
423
423
424 # Returns the due date or the target due date if any
424 # Returns the due date or the target due date if any
425 # Used on gantt chart
425 # Used on gantt chart
426 def due_before
426 def due_before
427 due_date || (fixed_version ? fixed_version.effective_date : nil)
427 due_date || (fixed_version ? fixed_version.effective_date : nil)
428 end
428 end
429
429
430 # Returns the time scheduled for this issue.
430 # Returns the time scheduled for this issue.
431 #
431 #
432 # Example:
432 # Example:
433 # Start Date: 2/26/09, End Date: 3/04/09
433 # Start Date: 2/26/09, End Date: 3/04/09
434 # duration => 6
434 # duration => 6
435 def duration
435 def duration
436 (start_date && due_date) ? due_date - start_date : 0
436 (start_date && due_date) ? due_date - start_date : 0
437 end
437 end
438
438
439 def soonest_start
439 def soonest_start
440 @soonest_start ||= (
440 @soonest_start ||= (
441 relations_to.collect{|relation| relation.successor_soonest_start} +
441 relations_to.collect{|relation| relation.successor_soonest_start} +
442 ancestors.collect(&:soonest_start)
442 ancestors.collect(&:soonest_start)
443 ).compact.max
443 ).compact.max
444 end
444 end
445
445
446 def reschedule_after(date)
446 def reschedule_after(date)
447 return if date.nil?
447 return if date.nil?
448 if leaf?
448 if leaf?
449 if start_date.nil? || start_date < date
449 if start_date.nil? || start_date < date
450 self.start_date, self.due_date = date, date + duration
450 self.start_date, self.due_date = date, date + duration
451 save
451 save
452 end
452 end
453 else
453 else
454 leaves.each do |leaf|
454 leaves.each do |leaf|
455 leaf.reschedule_after(date)
455 leaf.reschedule_after(date)
456 end
456 end
457 end
457 end
458 end
458 end
459
459
460 def <=>(issue)
460 def <=>(issue)
461 if issue.nil?
461 if issue.nil?
462 -1
462 -1
463 elsif root_id != issue.root_id
463 elsif root_id != issue.root_id
464 (root_id || 0) <=> (issue.root_id || 0)
464 (root_id || 0) <=> (issue.root_id || 0)
465 else
465 else
466 (lft || 0) <=> (issue.lft || 0)
466 (lft || 0) <=> (issue.lft || 0)
467 end
467 end
468 end
468 end
469
469
470 def to_s
470 def to_s
471 "#{tracker} ##{id}: #{subject}"
471 "#{tracker} ##{id}: #{subject}"
472 end
472 end
473
473
474 # Returns a string of css classes that apply to the issue
474 # Returns a string of css classes that apply to the issue
475 def css_classes
475 def css_classes
476 s = "issue status-#{status.position} priority-#{priority.position}"
476 s = "issue status-#{status.position} priority-#{priority.position}"
477 s << ' closed' if closed?
477 s << ' closed' if closed?
478 s << ' overdue' if overdue?
478 s << ' overdue' if overdue?
479 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
479 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
480 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
480 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
481 s
481 s
482 end
482 end
483
483
484 # Saves an issue, time_entry, attachments, and a journal from the parameters
484 # Saves an issue, time_entry, attachments, and a journal from the parameters
485 # Returns false if save fails
485 # Returns false if save fails
486 def save_issue_with_child_records(params, existing_time_entry=nil)
486 def save_issue_with_child_records(params, existing_time_entry=nil)
487 Issue.transaction do
487 Issue.transaction do
488 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
488 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
489 @time_entry = existing_time_entry || TimeEntry.new
489 @time_entry = existing_time_entry || TimeEntry.new
490 @time_entry.project = project
490 @time_entry.project = project
491 @time_entry.issue = self
491 @time_entry.issue = self
492 @time_entry.user = User.current
492 @time_entry.user = User.current
493 @time_entry.spent_on = Date.today
493 @time_entry.spent_on = Date.today
494 @time_entry.attributes = params[:time_entry]
494 @time_entry.attributes = params[:time_entry]
495 self.time_entries << @time_entry
495 self.time_entries << @time_entry
496 end
496 end
497
497
498 if valid?
498 if valid?
499 attachments = Attachment.attach_files(self, params[:attachments])
499 attachments = Attachment.attach_files(self, params[:attachments])
500
500
501 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
501 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
502 # TODO: Rename hook
502 # TODO: Rename hook
503 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
503 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
504 begin
504 begin
505 if save
505 if save
506 # TODO: Rename hook
506 # TODO: Rename hook
507 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
507 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
508 else
508 else
509 raise ActiveRecord::Rollback
509 raise ActiveRecord::Rollback
510 end
510 end
511 rescue ActiveRecord::StaleObjectError
511 rescue ActiveRecord::StaleObjectError
512 attachments[:files].each(&:destroy)
512 attachments[:files].each(&:destroy)
513 errors.add_to_base l(:notice_locking_conflict)
513 errors.add_to_base l(:notice_locking_conflict)
514 raise ActiveRecord::Rollback
514 raise ActiveRecord::Rollback
515 end
515 end
516 end
516 end
517 end
517 end
518 end
518 end
519
519
520 # Unassigns issues from +version+ if it's no longer shared with issue's project
520 # Unassigns issues from +version+ if it's no longer shared with issue's project
521 def self.update_versions_from_sharing_change(version)
521 def self.update_versions_from_sharing_change(version)
522 # Update issues assigned to the version
522 # Update issues assigned to the version
523 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
523 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
524 end
524 end
525
525
526 # Unassigns issues from versions that are no longer shared
526 # Unassigns issues from versions that are no longer shared
527 # after +project+ was moved
527 # after +project+ was moved
528 def self.update_versions_from_hierarchy_change(project)
528 def self.update_versions_from_hierarchy_change(project)
529 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
529 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
530 # Update issues of the moved projects and issues assigned to a version of a moved project
530 # Update issues of the moved projects and issues assigned to a version of a moved project
531 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
531 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
532 end
532 end
533
533
534 def parent_issue_id=(arg)
534 def parent_issue_id=(arg)
535 parent_issue_id = arg.blank? ? nil : arg.to_i
535 parent_issue_id = arg.blank? ? nil : arg.to_i
536 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
536 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
537 @parent_issue.id
537 @parent_issue.id
538 else
538 else
539 @parent_issue = nil
539 @parent_issue = nil
540 nil
540 nil
541 end
541 end
542 end
542 end
543
543
544 def parent_issue_id
544 def parent_issue_id
545 if instance_variable_defined? :@parent_issue
545 if instance_variable_defined? :@parent_issue
546 @parent_issue.nil? ? nil : @parent_issue.id
546 @parent_issue.nil? ? nil : @parent_issue.id
547 else
547 else
548 parent_id
548 parent_id
549 end
549 end
550 end
550 end
551
551
552 # Extracted from the ReportsController.
552 # Extracted from the ReportsController.
553 def self.by_tracker(project)
553 def self.by_tracker(project)
554 count_and_group_by(:project => project,
554 count_and_group_by(:project => project,
555 :field => 'tracker_id',
555 :field => 'tracker_id',
556 :joins => Tracker.table_name)
556 :joins => Tracker.table_name)
557 end
557 end
558
558
559 def self.by_version(project)
559 def self.by_version(project)
560 count_and_group_by(:project => project,
560 count_and_group_by(:project => project,
561 :field => 'fixed_version_id',
561 :field => 'fixed_version_id',
562 :joins => Version.table_name)
562 :joins => Version.table_name)
563 end
563 end
564
564
565 def self.by_priority(project)
565 def self.by_priority(project)
566 count_and_group_by(:project => project,
566 count_and_group_by(:project => project,
567 :field => 'priority_id',
567 :field => 'priority_id',
568 :joins => IssuePriority.table_name)
568 :joins => IssuePriority.table_name)
569 end
569 end
570
570
571 def self.by_category(project)
571 def self.by_category(project)
572 count_and_group_by(:project => project,
572 count_and_group_by(:project => project,
573 :field => 'category_id',
573 :field => 'category_id',
574 :joins => IssueCategory.table_name)
574 :joins => IssueCategory.table_name)
575 end
575 end
576
576
577 def self.by_assigned_to(project)
577 def self.by_assigned_to(project)
578 count_and_group_by(:project => project,
578 count_and_group_by(:project => project,
579 :field => 'assigned_to_id',
579 :field => 'assigned_to_id',
580 :joins => User.table_name)
580 :joins => User.table_name)
581 end
581 end
582
582
583 def self.by_author(project)
583 def self.by_author(project)
584 count_and_group_by(:project => project,
584 count_and_group_by(:project => project,
585 :field => 'author_id',
585 :field => 'author_id',
586 :joins => User.table_name)
586 :joins => User.table_name)
587 end
587 end
588
588
589 def self.by_subproject(project)
589 def self.by_subproject(project)
590 ActiveRecord::Base.connection.select_all("select s.id as status_id,
590 ActiveRecord::Base.connection.select_all("select s.id as status_id,
591 s.is_closed as closed,
591 s.is_closed as closed,
592 i.project_id as project_id,
592 i.project_id as project_id,
593 count(i.id) as total
593 count(i.id) as total
594 from
594 from
595 #{Issue.table_name} i, #{IssueStatus.table_name} s
595 #{Issue.table_name} i, #{IssueStatus.table_name} s
596 where
596 where
597 i.status_id=s.id
597 i.status_id=s.id
598 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
598 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
599 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
599 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
600 end
600 end
601 # End ReportsController extraction
601 # End ReportsController extraction
602
602
603 # Returns an array of projects that current user can move issues to
603 # Returns an array of projects that current user can move issues to
604 def self.allowed_target_projects_on_move
604 def self.allowed_target_projects_on_move
605 projects = []
605 projects = []
606 if User.current.admin?
606 if User.current.admin?
607 # admin is allowed to move issues to any active (visible) project
607 # admin is allowed to move issues to any active (visible) project
608 projects = Project.visible.all
608 projects = Project.visible.all
609 elsif User.current.logged?
609 elsif User.current.logged?
610 if Role.non_member.allowed_to?(:move_issues)
610 if Role.non_member.allowed_to?(:move_issues)
611 projects = Project.visible.all
611 projects = Project.visible.all
612 else
612 else
613 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
613 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
614 end
614 end
615 end
615 end
616 projects
616 projects
617 end
617 end
618
618
619 private
619 private
620
620
621 def update_nested_set_attributes
621 def update_nested_set_attributes
622 if root_id.nil?
622 if root_id.nil?
623 # issue was just created
623 # issue was just created
624 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
624 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
625 set_default_left_and_right
625 set_default_left_and_right
626 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
626 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
627 if @parent_issue
627 if @parent_issue
628 move_to_child_of(@parent_issue)
628 move_to_child_of(@parent_issue)
629 end
629 end
630 reload
630 reload
631 elsif parent_issue_id != parent_id
631 elsif parent_issue_id != parent_id
632 former_parent_id = parent_id
632 # moving an existing issue
633 # moving an existing issue
633 if @parent_issue && @parent_issue.root_id == root_id
634 if @parent_issue && @parent_issue.root_id == root_id
634 # inside the same tree
635 # inside the same tree
635 move_to_child_of(@parent_issue)
636 move_to_child_of(@parent_issue)
636 else
637 else
637 # to another tree
638 # to another tree
638 unless root?
639 unless root?
639 move_to_right_of(root)
640 move_to_right_of(root)
640 reload
641 reload
641 end
642 end
642 old_root_id = root_id
643 old_root_id = root_id
643 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
644 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
644 target_maxright = nested_set_scope.maximum(right_column_name) || 0
645 target_maxright = nested_set_scope.maximum(right_column_name) || 0
645 offset = target_maxright + 1 - lft
646 offset = target_maxright + 1 - lft
646 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
647 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
647 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
648 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
648 self[left_column_name] = lft + offset
649 self[left_column_name] = lft + offset
649 self[right_column_name] = rgt + offset
650 self[right_column_name] = rgt + offset
650 if @parent_issue
651 if @parent_issue
651 move_to_child_of(@parent_issue)
652 move_to_child_of(@parent_issue)
652 end
653 end
653 end
654 end
654 reload
655 reload
655 # delete invalid relations of all descendants
656 # delete invalid relations of all descendants
656 self_and_descendants.each do |issue|
657 self_and_descendants.each do |issue|
657 issue.relations.each do |relation|
658 issue.relations.each do |relation|
658 relation.destroy unless relation.valid?
659 relation.destroy unless relation.valid?
659 end
660 end
660 end
661 end
662 # update former parent
663 recalculate_attributes_for(former_parent_id) if former_parent_id
661 end
664 end
662 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
665 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
663 end
666 end
664
667
665 def update_parent_attributes
668 def update_parent_attributes
666 if parent_id && p = Issue.find_by_id(parent_id)
669 recalculate_attributes_for(parent_id) if parent_id
670 end
671
672 def recalculate_attributes_for(issue_id)
673 if issue_id && p = Issue.find_by_id(issue_id)
667 # priority = highest priority of children
674 # priority = highest priority of children
668 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
675 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
669 p.priority = IssuePriority.find_by_position(priority_position)
676 p.priority = IssuePriority.find_by_position(priority_position)
670 end
677 end
671
678
672 # start/due dates = lowest/highest dates of children
679 # start/due dates = lowest/highest dates of children
673 p.start_date = p.children.minimum(:start_date)
680 p.start_date = p.children.minimum(:start_date)
674 p.due_date = p.children.maximum(:due_date)
681 p.due_date = p.children.maximum(:due_date)
675 if p.start_date && p.due_date && p.due_date < p.start_date
682 if p.start_date && p.due_date && p.due_date < p.start_date
676 p.start_date, p.due_date = p.due_date, p.start_date
683 p.start_date, p.due_date = p.due_date, p.start_date
677 end
684 end
678
685
679 # done ratio = weighted average ratio of leaves
686 # done ratio = weighted average ratio of leaves
680 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio?
687 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio?
681 leaves_count = p.leaves.count
688 leaves_count = p.leaves.count
682 if leaves_count > 0
689 if leaves_count > 0
683 average = p.leaves.average(:estimated_hours).to_f
690 average = p.leaves.average(:estimated_hours).to_f
684 if average == 0
691 if average == 0
685 average = 1
692 average = 1
686 end
693 end
687 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
694 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
688 progress = done / (average * leaves_count)
695 progress = done / (average * leaves_count)
689 p.done_ratio = progress.round
696 p.done_ratio = progress.round
690 end
697 end
691 end
698 end
692
699
693 # estimate = sum of leaves estimates
700 # estimate = sum of leaves estimates
694 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
701 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
695 p.estimated_hours = nil if p.estimated_hours == 0.0
702 p.estimated_hours = nil if p.estimated_hours == 0.0
696
703
697 # ancestors will be recursively updated
704 # ancestors will be recursively updated
698 p.save(false)
705 p.save(false)
699 end
706 end
700 end
707 end
701
708
702 def destroy_children
709 def destroy_children
703 unless leaf?
710 unless leaf?
704 children.each do |child|
711 children.each do |child|
705 child.destroy
712 child.destroy
706 end
713 end
707 end
714 end
708 end
715 end
709
716
710 # Update issues so their versions are not pointing to a
717 # Update issues so their versions are not pointing to a
711 # fixed_version that is not shared with the issue's project
718 # fixed_version that is not shared with the issue's project
712 def self.update_versions(conditions=nil)
719 def self.update_versions(conditions=nil)
713 # Only need to update issues with a fixed_version from
720 # Only need to update issues with a fixed_version from
714 # a different project and that is not systemwide shared
721 # a different project and that is not systemwide shared
715 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
722 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
716 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
723 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
717 " AND #{Version.table_name}.sharing <> 'system'",
724 " AND #{Version.table_name}.sharing <> 'system'",
718 conditions),
725 conditions),
719 :include => [:project, :fixed_version]
726 :include => [:project, :fixed_version]
720 ).each do |issue|
727 ).each do |issue|
721 next if issue.project.nil? || issue.fixed_version.nil?
728 next if issue.project.nil? || issue.fixed_version.nil?
722 unless issue.project.shared_versions.include?(issue.fixed_version)
729 unless issue.project.shared_versions.include?(issue.fixed_version)
723 issue.init_journal(User.current)
730 issue.init_journal(User.current)
724 issue.fixed_version = nil
731 issue.fixed_version = nil
725 issue.save
732 issue.save
726 end
733 end
727 end
734 end
728 end
735 end
729
736
730 # Callback on attachment deletion
737 # Callback on attachment deletion
731 def attachment_removed(obj)
738 def attachment_removed(obj)
732 journal = init_journal(User.current)
739 journal = init_journal(User.current)
733 journal.details << JournalDetail.new(:property => 'attachment',
740 journal.details << JournalDetail.new(:property => 'attachment',
734 :prop_key => obj.id,
741 :prop_key => obj.id,
735 :old_value => obj.filename)
742 :old_value => obj.filename)
736 journal.save
743 journal.save
737 end
744 end
738
745
739 # Default assignment based on category
746 # Default assignment based on category
740 def default_assign
747 def default_assign
741 if assigned_to.nil? && category && category.assigned_to
748 if assigned_to.nil? && category && category.assigned_to
742 self.assigned_to = category.assigned_to
749 self.assigned_to = category.assigned_to
743 end
750 end
744 end
751 end
745
752
746 # Updates start/due dates of following issues
753 # Updates start/due dates of following issues
747 def reschedule_following_issues
754 def reschedule_following_issues
748 if start_date_changed? || due_date_changed?
755 if start_date_changed? || due_date_changed?
749 relations_from.each do |relation|
756 relations_from.each do |relation|
750 relation.set_issue_to_dates
757 relation.set_issue_to_dates
751 end
758 end
752 end
759 end
753 end
760 end
754
761
755 # Closes duplicates if the issue is being closed
762 # Closes duplicates if the issue is being closed
756 def close_duplicates
763 def close_duplicates
757 if closing?
764 if closing?
758 duplicates.each do |duplicate|
765 duplicates.each do |duplicate|
759 # Reload is need in case the duplicate was updated by a previous duplicate
766 # Reload is need in case the duplicate was updated by a previous duplicate
760 duplicate.reload
767 duplicate.reload
761 # Don't re-close it if it's already closed
768 # Don't re-close it if it's already closed
762 next if duplicate.closed?
769 next if duplicate.closed?
763 # Same user and notes
770 # Same user and notes
764 if @current_journal
771 if @current_journal
765 duplicate.init_journal(@current_journal.user, @current_journal.notes)
772 duplicate.init_journal(@current_journal.user, @current_journal.notes)
766 end
773 end
767 duplicate.update_attribute :status, self.status
774 duplicate.update_attribute :status, self.status
768 end
775 end
769 end
776 end
770 end
777 end
771
778
772 # Saves the changes in a Journal
779 # Saves the changes in a Journal
773 # Called after_save
780 # Called after_save
774 def create_journal
781 def create_journal
775 if @current_journal
782 if @current_journal
776 # attributes changes
783 # attributes changes
777 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
784 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
778 @current_journal.details << JournalDetail.new(:property => 'attr',
785 @current_journal.details << JournalDetail.new(:property => 'attr',
779 :prop_key => c,
786 :prop_key => c,
780 :old_value => @issue_before_change.send(c),
787 :old_value => @issue_before_change.send(c),
781 :value => send(c)) unless send(c)==@issue_before_change.send(c)
788 :value => send(c)) unless send(c)==@issue_before_change.send(c)
782 }
789 }
783 # custom fields changes
790 # custom fields changes
784 custom_values.each {|c|
791 custom_values.each {|c|
785 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
792 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
786 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
793 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
787 @current_journal.details << JournalDetail.new(:property => 'cf',
794 @current_journal.details << JournalDetail.new(:property => 'cf',
788 :prop_key => c.custom_field_id,
795 :prop_key => c.custom_field_id,
789 :old_value => @custom_values_before_change[c.custom_field_id],
796 :old_value => @custom_values_before_change[c.custom_field_id],
790 :value => c.value)
797 :value => c.value)
791 }
798 }
792 @current_journal.save
799 @current_journal.save
793 # reset current journal
800 # reset current journal
794 init_journal @current_journal.user, @current_journal.notes
801 init_journal @current_journal.user, @current_journal.notes
795 end
802 end
796 end
803 end
797
804
798 # Query generator for selecting groups of issue counts for a project
805 # Query generator for selecting groups of issue counts for a project
799 # based on specific criteria
806 # based on specific criteria
800 #
807 #
801 # Options
808 # Options
802 # * project - Project to search in.
809 # * project - Project to search in.
803 # * field - String. Issue field to key off of in the grouping.
810 # * field - String. Issue field to key off of in the grouping.
804 # * joins - String. The table name to join against.
811 # * joins - String. The table name to join against.
805 def self.count_and_group_by(options)
812 def self.count_and_group_by(options)
806 project = options.delete(:project)
813 project = options.delete(:project)
807 select_field = options.delete(:field)
814 select_field = options.delete(:field)
808 joins = options.delete(:joins)
815 joins = options.delete(:joins)
809
816
810 where = "i.#{select_field}=j.id"
817 where = "i.#{select_field}=j.id"
811
818
812 ActiveRecord::Base.connection.select_all("select s.id as status_id,
819 ActiveRecord::Base.connection.select_all("select s.id as status_id,
813 s.is_closed as closed,
820 s.is_closed as closed,
814 j.id as #{select_field},
821 j.id as #{select_field},
815 count(i.id) as total
822 count(i.id) as total
816 from
823 from
817 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} as j
824 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} as j
818 where
825 where
819 i.status_id=s.id
826 i.status_id=s.id
820 and #{where}
827 and #{where}
821 and i.project_id=#{project.id}
828 and i.project_id=#{project.id}
822 group by s.id, s.is_closed, j.id")
829 group by s.id, s.is_closed, j.id")
823 end
830 end
824
831
825
832
826 end
833 end
@@ -1,315 +1,324
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class IssueNestedSetTest < ActiveSupport::TestCase
20 class IssueNestedSetTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :members, :member_roles, :roles,
21 fixtures :projects, :users, :members, :member_roles, :roles,
22 :trackers, :projects_trackers,
22 :trackers, :projects_trackers,
23 :versions,
23 :versions,
24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
25 :enumerations,
25 :enumerations,
26 :issues,
26 :issues,
27 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
27 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
28 :time_entries
28 :time_entries
29
29
30 self.use_transactional_fixtures = false
30 self.use_transactional_fixtures = false
31
31
32 def test_create_root_issue
32 def test_create_root_issue
33 issue1 = create_issue!
33 issue1 = create_issue!
34 issue2 = create_issue!
34 issue2 = create_issue!
35 issue1.reload
35 issue1.reload
36 issue2.reload
36 issue2.reload
37
37
38 assert_equal [issue1.id, nil, 1, 2], [issue1.root_id, issue1.parent_id, issue1.lft, issue1.rgt]
38 assert_equal [issue1.id, nil, 1, 2], [issue1.root_id, issue1.parent_id, issue1.lft, issue1.rgt]
39 assert_equal [issue2.id, nil, 1, 2], [issue2.root_id, issue2.parent_id, issue2.lft, issue2.rgt]
39 assert_equal [issue2.id, nil, 1, 2], [issue2.root_id, issue2.parent_id, issue2.lft, issue2.rgt]
40 end
40 end
41
41
42 def test_create_child_issue
42 def test_create_child_issue
43 parent = create_issue!
43 parent = create_issue!
44 child = create_issue!(:parent_issue_id => parent.id)
44 child = create_issue!(:parent_issue_id => parent.id)
45 parent.reload
45 parent.reload
46 child.reload
46 child.reload
47
47
48 assert_equal [parent.id, nil, 1, 4], [parent.root_id, parent.parent_id, parent.lft, parent.rgt]
48 assert_equal [parent.id, nil, 1, 4], [parent.root_id, parent.parent_id, parent.lft, parent.rgt]
49 assert_equal [parent.id, parent.id, 2, 3], [child.root_id, child.parent_id, child.lft, child.rgt]
49 assert_equal [parent.id, parent.id, 2, 3], [child.root_id, child.parent_id, child.lft, child.rgt]
50 end
50 end
51
51
52 def test_creating_a_child_in_different_project_should_not_validate
52 def test_creating_a_child_in_different_project_should_not_validate
53 issue = create_issue!
53 issue = create_issue!
54 child = Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1, :subject => 'child', :parent_issue_id => issue.id)
54 child = Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1, :subject => 'child', :parent_issue_id => issue.id)
55 assert !child.save
55 assert !child.save
56 assert_not_nil child.errors.on(:parent_issue_id)
56 assert_not_nil child.errors.on(:parent_issue_id)
57 end
57 end
58
58
59 def test_move_a_root_to_child
59 def test_move_a_root_to_child
60 parent1 = create_issue!
60 parent1 = create_issue!
61 parent2 = create_issue!
61 parent2 = create_issue!
62 child = create_issue!(:parent_issue_id => parent1.id)
62 child = create_issue!(:parent_issue_id => parent1.id)
63
63
64 parent2.parent_issue_id = parent1.id
64 parent2.parent_issue_id = parent1.id
65 parent2.save!
65 parent2.save!
66 child.reload
66 child.reload
67 parent1.reload
67 parent1.reload
68 parent2.reload
68 parent2.reload
69
69
70 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
70 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
71 assert_equal [parent1.id, 4, 5], [parent2.root_id, parent2.lft, parent2.rgt]
71 assert_equal [parent1.id, 4, 5], [parent2.root_id, parent2.lft, parent2.rgt]
72 assert_equal [parent1.id, 2, 3], [child.root_id, child.lft, child.rgt]
72 assert_equal [parent1.id, 2, 3], [child.root_id, child.lft, child.rgt]
73 end
73 end
74
74
75 def test_move_a_child_to_root
75 def test_move_a_child_to_root
76 parent1 = create_issue!
76 parent1 = create_issue!
77 parent2 = create_issue!
77 parent2 = create_issue!
78 child = create_issue!(:parent_issue_id => parent1.id)
78 child = create_issue!(:parent_issue_id => parent1.id)
79
79
80 child.parent_issue_id = nil
80 child.parent_issue_id = nil
81 child.save!
81 child.save!
82 child.reload
82 child.reload
83 parent1.reload
83 parent1.reload
84 parent2.reload
84 parent2.reload
85
85
86 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
86 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
87 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
87 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
88 assert_equal [child.id, 1, 2], [child.root_id, child.lft, child.rgt]
88 assert_equal [child.id, 1, 2], [child.root_id, child.lft, child.rgt]
89 end
89 end
90
90
91 def test_move_a_child_to_another_issue
91 def test_move_a_child_to_another_issue
92 parent1 = create_issue!
92 parent1 = create_issue!
93 parent2 = create_issue!
93 parent2 = create_issue!
94 child = create_issue!(:parent_issue_id => parent1.id)
94 child = create_issue!(:parent_issue_id => parent1.id)
95
95
96 child.parent_issue_id = parent2.id
96 child.parent_issue_id = parent2.id
97 child.save!
97 child.save!
98 child.reload
98 child.reload
99 parent1.reload
99 parent1.reload
100 parent2.reload
100 parent2.reload
101
101
102 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
102 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
103 assert_equal [parent2.id, 1, 4], [parent2.root_id, parent2.lft, parent2.rgt]
103 assert_equal [parent2.id, 1, 4], [parent2.root_id, parent2.lft, parent2.rgt]
104 assert_equal [parent2.id, 2, 3], [child.root_id, child.lft, child.rgt]
104 assert_equal [parent2.id, 2, 3], [child.root_id, child.lft, child.rgt]
105 end
105 end
106
106
107 def test_move_a_child_with_descendants_to_another_issue
107 def test_move_a_child_with_descendants_to_another_issue
108 parent1 = create_issue!
108 parent1 = create_issue!
109 parent2 = create_issue!
109 parent2 = create_issue!
110 child = create_issue!(:parent_issue_id => parent1.id)
110 child = create_issue!(:parent_issue_id => parent1.id)
111 grandchild = create_issue!(:parent_issue_id => child.id)
111 grandchild = create_issue!(:parent_issue_id => child.id)
112
112
113 parent1.reload
113 parent1.reload
114 parent2.reload
114 parent2.reload
115 child.reload
115 child.reload
116 grandchild.reload
116 grandchild.reload
117
117
118 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
118 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
119 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
119 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
120 assert_equal [parent1.id, 2, 5], [child.root_id, child.lft, child.rgt]
120 assert_equal [parent1.id, 2, 5], [child.root_id, child.lft, child.rgt]
121 assert_equal [parent1.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
121 assert_equal [parent1.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
122
122
123 child.reload.parent_issue_id = parent2.id
123 child.reload.parent_issue_id = parent2.id
124 child.save!
124 child.save!
125 child.reload
125 child.reload
126 grandchild.reload
126 grandchild.reload
127 parent1.reload
127 parent1.reload
128 parent2.reload
128 parent2.reload
129
129
130 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
130 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
131 assert_equal [parent2.id, 1, 6], [parent2.root_id, parent2.lft, parent2.rgt]
131 assert_equal [parent2.id, 1, 6], [parent2.root_id, parent2.lft, parent2.rgt]
132 assert_equal [parent2.id, 2, 5], [child.root_id, child.lft, child.rgt]
132 assert_equal [parent2.id, 2, 5], [child.root_id, child.lft, child.rgt]
133 assert_equal [parent2.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
133 assert_equal [parent2.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
134 end
134 end
135
135
136 def test_move_a_child_with_descendants_to_another_project
136 def test_move_a_child_with_descendants_to_another_project
137 parent1 = create_issue!
137 parent1 = create_issue!
138 child = create_issue!(:parent_issue_id => parent1.id)
138 child = create_issue!(:parent_issue_id => parent1.id)
139 grandchild = create_issue!(:parent_issue_id => child.id)
139 grandchild = create_issue!(:parent_issue_id => child.id)
140
140
141 assert child.reload.move_to_project(Project.find(2))
141 assert child.reload.move_to_project(Project.find(2))
142 child.reload
142 child.reload
143 grandchild.reload
143 grandchild.reload
144 parent1.reload
144 parent1.reload
145
145
146 assert_equal [1, parent1.id, 1, 2], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
146 assert_equal [1, parent1.id, 1, 2], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
147 assert_equal [2, child.id, 1, 4], [child.project_id, child.root_id, child.lft, child.rgt]
147 assert_equal [2, child.id, 1, 4], [child.project_id, child.root_id, child.lft, child.rgt]
148 assert_equal [2, child.id, 2, 3], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
148 assert_equal [2, child.id, 2, 3], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
149 end
149 end
150
150
151 def test_invalid_move_to_another_project
151 def test_invalid_move_to_another_project
152 parent1 = create_issue!
152 parent1 = create_issue!
153 child = create_issue!(:parent_issue_id => parent1.id)
153 child = create_issue!(:parent_issue_id => parent1.id)
154 grandchild = create_issue!(:parent_issue_id => child.id, :tracker_id => 2)
154 grandchild = create_issue!(:parent_issue_id => child.id, :tracker_id => 2)
155 Project.find(2).tracker_ids = [1]
155 Project.find(2).tracker_ids = [1]
156
156
157 parent1.reload
157 parent1.reload
158 assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
158 assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
159
159
160 # child can not be moved to Project 2 because its child is on a disabled tracker
160 # child can not be moved to Project 2 because its child is on a disabled tracker
161 assert_equal false, Issue.find(child.id).move_to_project(Project.find(2))
161 assert_equal false, Issue.find(child.id).move_to_project(Project.find(2))
162 child.reload
162 child.reload
163 grandchild.reload
163 grandchild.reload
164 parent1.reload
164 parent1.reload
165
165
166 # no change
166 # no change
167 assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
167 assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
168 assert_equal [1, parent1.id, 2, 5], [child.project_id, child.root_id, child.lft, child.rgt]
168 assert_equal [1, parent1.id, 2, 5], [child.project_id, child.root_id, child.lft, child.rgt]
169 assert_equal [1, parent1.id, 3, 4], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
169 assert_equal [1, parent1.id, 3, 4], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
170 end
170 end
171
171
172 def test_moving_an_issue_to_a_descendant_should_not_validate
172 def test_moving_an_issue_to_a_descendant_should_not_validate
173 parent1 = create_issue!
173 parent1 = create_issue!
174 parent2 = create_issue!
174 parent2 = create_issue!
175 child = create_issue!(:parent_issue_id => parent1.id)
175 child = create_issue!(:parent_issue_id => parent1.id)
176 grandchild = create_issue!(:parent_issue_id => child.id)
176 grandchild = create_issue!(:parent_issue_id => child.id)
177
177
178 child.reload
178 child.reload
179 child.parent_issue_id = grandchild.id
179 child.parent_issue_id = grandchild.id
180 assert !child.save
180 assert !child.save
181 assert_not_nil child.errors.on(:parent_issue_id)
181 assert_not_nil child.errors.on(:parent_issue_id)
182 end
182 end
183
183
184 def test_moving_an_issue_should_keep_valid_relations_only
184 def test_moving_an_issue_should_keep_valid_relations_only
185 issue1 = create_issue!
185 issue1 = create_issue!
186 issue2 = create_issue!
186 issue2 = create_issue!
187 issue3 = create_issue!(:parent_issue_id => issue2.id)
187 issue3 = create_issue!(:parent_issue_id => issue2.id)
188 issue4 = create_issue!
188 issue4 = create_issue!
189 r1 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
189 r1 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
190 r2 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES)
190 r2 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES)
191 r3 = IssueRelation.create!(:issue_from => issue2, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
191 r3 = IssueRelation.create!(:issue_from => issue2, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
192 issue2.reload
192 issue2.reload
193 issue2.parent_issue_id = issue1.id
193 issue2.parent_issue_id = issue1.id
194 issue2.save!
194 issue2.save!
195 assert !IssueRelation.exists?(r1.id)
195 assert !IssueRelation.exists?(r1.id)
196 assert !IssueRelation.exists?(r2.id)
196 assert !IssueRelation.exists?(r2.id)
197 assert IssueRelation.exists?(r3.id)
197 assert IssueRelation.exists?(r3.id)
198 end
198 end
199
199
200 def test_destroy_should_destroy_children
200 def test_destroy_should_destroy_children
201 issue1 = create_issue!
201 issue1 = create_issue!
202 issue2 = create_issue!
202 issue2 = create_issue!
203 issue3 = create_issue!(:parent_issue_id => issue2.id)
203 issue3 = create_issue!(:parent_issue_id => issue2.id)
204 issue4 = create_issue!(:parent_issue_id => issue1.id)
204 issue4 = create_issue!(:parent_issue_id => issue1.id)
205 issue2.reload.destroy
205 issue2.reload.destroy
206 issue1.reload
206 issue1.reload
207 issue4.reload
207 issue4.reload
208 assert !Issue.exists?(issue2.id)
208 assert !Issue.exists?(issue2.id)
209 assert !Issue.exists?(issue3.id)
209 assert !Issue.exists?(issue3.id)
210 assert_equal [issue1.id, 1, 4], [issue1.root_id, issue1.lft, issue1.rgt]
210 assert_equal [issue1.id, 1, 4], [issue1.root_id, issue1.lft, issue1.rgt]
211 assert_equal [issue1.id, 2, 3], [issue4.root_id, issue4.lft, issue4.rgt]
211 assert_equal [issue1.id, 2, 3], [issue4.root_id, issue4.lft, issue4.rgt]
212 end
212 end
213
213
214 def test_parent_priority_should_be_the_highest_child_priority
214 def test_parent_priority_should_be_the_highest_child_priority
215 parent = create_issue!(:priority => IssuePriority.find_by_name('Normal'))
215 parent = create_issue!(:priority => IssuePriority.find_by_name('Normal'))
216 # Create children
216 # Create children
217 child1 = create_issue!(:priority => IssuePriority.find_by_name('High'), :parent_issue_id => parent.id)
217 child1 = create_issue!(:priority => IssuePriority.find_by_name('High'), :parent_issue_id => parent.id)
218 assert_equal 'High', parent.reload.priority.name
218 assert_equal 'High', parent.reload.priority.name
219 child2 = create_issue!(:priority => IssuePriority.find_by_name('Immediate'), :parent_issue_id => child1.id)
219 child2 = create_issue!(:priority => IssuePriority.find_by_name('Immediate'), :parent_issue_id => child1.id)
220 assert_equal 'Immediate', child1.reload.priority.name
220 assert_equal 'Immediate', child1.reload.priority.name
221 assert_equal 'Immediate', parent.reload.priority.name
221 assert_equal 'Immediate', parent.reload.priority.name
222 child3 = create_issue!(:priority => IssuePriority.find_by_name('Low'), :parent_issue_id => parent.id)
222 child3 = create_issue!(:priority => IssuePriority.find_by_name('Low'), :parent_issue_id => parent.id)
223 assert_equal 'Immediate', parent.reload.priority.name
223 assert_equal 'Immediate', parent.reload.priority.name
224 # Destroy a child
224 # Destroy a child
225 child1.destroy
225 child1.destroy
226 assert_equal 'Low', parent.reload.priority.name
226 assert_equal 'Low', parent.reload.priority.name
227 # Update a child
227 # Update a child
228 child3.reload.priority = IssuePriority.find_by_name('Normal')
228 child3.reload.priority = IssuePriority.find_by_name('Normal')
229 child3.save!
229 child3.save!
230 assert_equal 'Normal', parent.reload.priority.name
230 assert_equal 'Normal', parent.reload.priority.name
231 end
231 end
232
232
233 def test_parent_dates_should_be_lowest_start_and_highest_due_dates
233 def test_parent_dates_should_be_lowest_start_and_highest_due_dates
234 parent = create_issue!
234 parent = create_issue!
235 create_issue!(:start_date => '2010-01-25', :due_date => '2010-02-15', :parent_issue_id => parent.id)
235 create_issue!(:start_date => '2010-01-25', :due_date => '2010-02-15', :parent_issue_id => parent.id)
236 create_issue!( :due_date => '2010-02-13', :parent_issue_id => parent.id)
236 create_issue!( :due_date => '2010-02-13', :parent_issue_id => parent.id)
237 create_issue!(:start_date => '2010-02-01', :due_date => '2010-02-22', :parent_issue_id => parent.id)
237 create_issue!(:start_date => '2010-02-01', :due_date => '2010-02-22', :parent_issue_id => parent.id)
238 parent.reload
238 parent.reload
239 assert_equal Date.parse('2010-01-25'), parent.start_date
239 assert_equal Date.parse('2010-01-25'), parent.start_date
240 assert_equal Date.parse('2010-02-22'), parent.due_date
240 assert_equal Date.parse('2010-02-22'), parent.due_date
241 end
241 end
242
242
243 def test_parent_done_ratio_should_be_average_done_ratio_of_leaves
243 def test_parent_done_ratio_should_be_average_done_ratio_of_leaves
244 parent = create_issue!
244 parent = create_issue!
245 create_issue!(:done_ratio => 20, :parent_issue_id => parent.id)
245 create_issue!(:done_ratio => 20, :parent_issue_id => parent.id)
246 assert_equal 20, parent.reload.done_ratio
246 assert_equal 20, parent.reload.done_ratio
247 create_issue!(:done_ratio => 70, :parent_issue_id => parent.id)
247 create_issue!(:done_ratio => 70, :parent_issue_id => parent.id)
248 assert_equal 45, parent.reload.done_ratio
248 assert_equal 45, parent.reload.done_ratio
249
249
250 child = create_issue!(:done_ratio => 0, :parent_issue_id => parent.id)
250 child = create_issue!(:done_ratio => 0, :parent_issue_id => parent.id)
251 assert_equal 30, parent.reload.done_ratio
251 assert_equal 30, parent.reload.done_ratio
252
252
253 create_issue!(:done_ratio => 30, :parent_issue_id => child.id)
253 create_issue!(:done_ratio => 30, :parent_issue_id => child.id)
254 assert_equal 30, child.reload.done_ratio
254 assert_equal 30, child.reload.done_ratio
255 assert_equal 40, parent.reload.done_ratio
255 assert_equal 40, parent.reload.done_ratio
256 end
256 end
257
257
258 def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any
258 def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any
259 parent = create_issue!
259 parent = create_issue!
260 create_issue!(:estimated_hours => 10, :done_ratio => 20, :parent_issue_id => parent.id)
260 create_issue!(:estimated_hours => 10, :done_ratio => 20, :parent_issue_id => parent.id)
261 assert_equal 20, parent.reload.done_ratio
261 assert_equal 20, parent.reload.done_ratio
262 create_issue!(:estimated_hours => 20, :done_ratio => 50, :parent_issue_id => parent.id)
262 create_issue!(:estimated_hours => 20, :done_ratio => 50, :parent_issue_id => parent.id)
263 assert_equal (50 * 20 + 20 * 10) / 30, parent.reload.done_ratio
263 assert_equal (50 * 20 + 20 * 10) / 30, parent.reload.done_ratio
264 end
264 end
265
265
266 def test_parent_estimate_should_be_sum_of_leaves
266 def test_parent_estimate_should_be_sum_of_leaves
267 parent = create_issue!
267 parent = create_issue!
268 create_issue!(:estimated_hours => nil, :parent_issue_id => parent.id)
268 create_issue!(:estimated_hours => nil, :parent_issue_id => parent.id)
269 assert_equal nil, parent.reload.estimated_hours
269 assert_equal nil, parent.reload.estimated_hours
270 create_issue!(:estimated_hours => 5, :parent_issue_id => parent.id)
270 create_issue!(:estimated_hours => 5, :parent_issue_id => parent.id)
271 assert_equal 5, parent.reload.estimated_hours
271 assert_equal 5, parent.reload.estimated_hours
272 create_issue!(:estimated_hours => 7, :parent_issue_id => parent.id)
272 create_issue!(:estimated_hours => 7, :parent_issue_id => parent.id)
273 assert_equal 12, parent.reload.estimated_hours
273 assert_equal 12, parent.reload.estimated_hours
274 end
274 end
275
275
276 def test_move_parent_updates_old_parent_attributes
277 first_parent = create_issue!
278 second_parent = create_issue!
279 child = create_issue!(:estimated_hours => 5, :parent_issue_id => first_parent.id)
280 assert_equal 5, first_parent.reload.estimated_hours
281 child.update_attributes(:estimated_hours => 7, :parent_issue_id => second_parent.id)
282 assert_equal 7, second_parent.reload.estimated_hours
283 assert_nil first_parent.reload.estimated_hours
284 end
276
285
277 def test_reschuling_a_parent_should_reschedule_subtasks
286 def test_reschuling_a_parent_should_reschedule_subtasks
278 parent = create_issue!
287 parent = create_issue!
279 c1 = create_issue!(:start_date => '2010-05-12', :due_date => '2010-05-18', :parent_issue_id => parent.id)
288 c1 = create_issue!(:start_date => '2010-05-12', :due_date => '2010-05-18', :parent_issue_id => parent.id)
280 c2 = create_issue!(:start_date => '2010-06-03', :due_date => '2010-06-10', :parent_issue_id => parent.id)
289 c2 = create_issue!(:start_date => '2010-06-03', :due_date => '2010-06-10', :parent_issue_id => parent.id)
281 parent.reload
290 parent.reload
282 parent.reschedule_after(Date.parse('2010-06-02'))
291 parent.reschedule_after(Date.parse('2010-06-02'))
283 c1.reload
292 c1.reload
284 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-08')], [c1.start_date, c1.due_date]
293 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-08')], [c1.start_date, c1.due_date]
285 c2.reload
294 c2.reload
286 assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date] # no change
295 assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date] # no change
287 parent.reload
296 parent.reload
288 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date]
297 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date]
289 end
298 end
290
299
291 def test_project_copy_should_copy_issue_tree
300 def test_project_copy_should_copy_issue_tree
292 p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2])
301 p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2])
293 i1 = create_issue!(:project_id => p.id, :subject => 'i1')
302 i1 = create_issue!(:project_id => p.id, :subject => 'i1')
294 i2 = create_issue!(:project_id => p.id, :subject => 'i2', :parent_issue_id => i1.id)
303 i2 = create_issue!(:project_id => p.id, :subject => 'i2', :parent_issue_id => i1.id)
295 i3 = create_issue!(:project_id => p.id, :subject => 'i3', :parent_issue_id => i1.id)
304 i3 = create_issue!(:project_id => p.id, :subject => 'i3', :parent_issue_id => i1.id)
296 i4 = create_issue!(:project_id => p.id, :subject => 'i4', :parent_issue_id => i2.id)
305 i4 = create_issue!(:project_id => p.id, :subject => 'i4', :parent_issue_id => i2.id)
297 i5 = create_issue!(:project_id => p.id, :subject => 'i5')
306 i5 = create_issue!(:project_id => p.id, :subject => 'i5')
298 c = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1, 2])
307 c = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1, 2])
299 c.copy(p, :only => 'issues')
308 c.copy(p, :only => 'issues')
300 c.reload
309 c.reload
301
310
302 assert_equal 5, c.issues.count
311 assert_equal 5, c.issues.count
303 ic1, ic2, ic3, ic4, ic5 = c.issues.find(:all, :order => 'subject')
312 ic1, ic2, ic3, ic4, ic5 = c.issues.find(:all, :order => 'subject')
304 assert ic1.root?
313 assert ic1.root?
305 assert_equal ic1, ic2.parent
314 assert_equal ic1, ic2.parent
306 assert_equal ic1, ic3.parent
315 assert_equal ic1, ic3.parent
307 assert_equal ic2, ic4.parent
316 assert_equal ic2, ic4.parent
308 assert ic5.root?
317 assert ic5.root?
309 end
318 end
310
319
311 # Helper that creates an issue with default attributes
320 # Helper that creates an issue with default attributes
312 def create_issue!(attributes={})
321 def create_issue!(attributes={})
313 Issue.create!({:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test'}.merge(attributes))
322 Issue.create!({:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test'}.merge(attributes))
314 end
323 end
315 end
324 end
General Comments 0
You need to be logged in to leave comments. Login now