##// END OF EJS Templates
Fixes reverting an issue to a status with a done_ratio of 0%. #5170...
Eric Davis -
r4072:83e0be5d07de
parent child
Show More
@@ -1,863 +1,863
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 => "#{Issue.table_name}.updated_on DESC"
65 named_scope :recently_updated, :order => "#{Issue.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 named_scope :for_gantt, lambda {
69 named_scope :for_gantt, lambda {
70 {
70 {
71 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
71 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
72 :order => "#{Issue.table_name}.due_date ASC, #{Issue.table_name}.start_date ASC, #{Issue.table_name}.id ASC"
72 :order => "#{Issue.table_name}.due_date ASC, #{Issue.table_name}.start_date ASC, #{Issue.table_name}.id ASC"
73 }
73 }
74 }
74 }
75
75
76 named_scope :without_version, lambda {
76 named_scope :without_version, lambda {
77 {
77 {
78 :conditions => { :fixed_version_id => nil}
78 :conditions => { :fixed_version_id => nil}
79 }
79 }
80 }
80 }
81
81
82 named_scope :with_query, lambda {|query|
82 named_scope :with_query, lambda {|query|
83 {
83 {
84 :conditions => Query.merge_conditions(query.statement)
84 :conditions => Query.merge_conditions(query.statement)
85 }
85 }
86 }
86 }
87
87
88 before_create :default_assign
88 before_create :default_assign
89 before_save :reschedule_following_issues, :close_duplicates, :update_done_ratio_from_issue_status
89 before_save :reschedule_following_issues, :close_duplicates, :update_done_ratio_from_issue_status
90 after_save :update_nested_set_attributes, :update_parent_attributes, :create_journal
90 after_save :update_nested_set_attributes, :update_parent_attributes, :create_journal
91 after_destroy :destroy_children
91 after_destroy :destroy_children
92 after_destroy :update_parent_attributes
92 after_destroy :update_parent_attributes
93
93
94 # Returns true if usr or current user is allowed to view the issue
94 # Returns true if usr or current user is allowed to view the issue
95 def visible?(usr=nil)
95 def visible?(usr=nil)
96 (usr || User.current).allowed_to?(:view_issues, self.project)
96 (usr || User.current).allowed_to?(:view_issues, self.project)
97 end
97 end
98
98
99 def after_initialize
99 def after_initialize
100 if new_record?
100 if new_record?
101 # set default values for new records only
101 # set default values for new records only
102 self.status ||= IssueStatus.default
102 self.status ||= IssueStatus.default
103 self.priority ||= IssuePriority.default
103 self.priority ||= IssuePriority.default
104 end
104 end
105 end
105 end
106
106
107 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
107 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
108 def available_custom_fields
108 def available_custom_fields
109 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
109 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
110 end
110 end
111
111
112 def copy_from(arg)
112 def copy_from(arg)
113 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
113 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
114 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
114 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
115 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
115 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
116 self.status = issue.status
116 self.status = issue.status
117 self
117 self
118 end
118 end
119
119
120 # Moves/copies an issue to a new project and tracker
120 # Moves/copies an issue to a new project and tracker
121 # Returns the moved/copied issue on success, false on failure
121 # Returns the moved/copied issue on success, false on failure
122 def move_to_project(*args)
122 def move_to_project(*args)
123 ret = Issue.transaction do
123 ret = Issue.transaction do
124 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
124 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
125 end || false
125 end || false
126 end
126 end
127
127
128 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
128 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
129 options ||= {}
129 options ||= {}
130 issue = options[:copy] ? self.class.new.copy_from(self) : self
130 issue = options[:copy] ? self.class.new.copy_from(self) : self
131
131
132 if new_project && issue.project_id != new_project.id
132 if new_project && issue.project_id != new_project.id
133 # delete issue relations
133 # delete issue relations
134 unless Setting.cross_project_issue_relations?
134 unless Setting.cross_project_issue_relations?
135 issue.relations_from.clear
135 issue.relations_from.clear
136 issue.relations_to.clear
136 issue.relations_to.clear
137 end
137 end
138 # issue is moved to another project
138 # issue is moved to another project
139 # reassign to the category with same name if any
139 # reassign to the category with same name if any
140 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
140 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
141 issue.category = new_category
141 issue.category = new_category
142 # Keep the fixed_version if it's still valid in the new_project
142 # Keep the fixed_version if it's still valid in the new_project
143 unless new_project.shared_versions.include?(issue.fixed_version)
143 unless new_project.shared_versions.include?(issue.fixed_version)
144 issue.fixed_version = nil
144 issue.fixed_version = nil
145 end
145 end
146 issue.project = new_project
146 issue.project = new_project
147 if issue.parent && issue.parent.project_id != issue.project_id
147 if issue.parent && issue.parent.project_id != issue.project_id
148 issue.parent_issue_id = nil
148 issue.parent_issue_id = nil
149 end
149 end
150 end
150 end
151 if new_tracker
151 if new_tracker
152 issue.tracker = new_tracker
152 issue.tracker = new_tracker
153 issue.reset_custom_values!
153 issue.reset_custom_values!
154 end
154 end
155 if options[:copy]
155 if options[:copy]
156 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
156 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
157 issue.status = if options[:attributes] && options[:attributes][:status_id]
157 issue.status = if options[:attributes] && options[:attributes][:status_id]
158 IssueStatus.find_by_id(options[:attributes][:status_id])
158 IssueStatus.find_by_id(options[:attributes][:status_id])
159 else
159 else
160 self.status
160 self.status
161 end
161 end
162 end
162 end
163 # Allow bulk setting of attributes on the issue
163 # Allow bulk setting of attributes on the issue
164 if options[:attributes]
164 if options[:attributes]
165 issue.attributes = options[:attributes]
165 issue.attributes = options[:attributes]
166 end
166 end
167 if issue.save
167 if issue.save
168 unless options[:copy]
168 unless options[:copy]
169 # Manually update project_id on related time entries
169 # Manually update project_id on related time entries
170 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
170 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
171
171
172 issue.children.each do |child|
172 issue.children.each do |child|
173 unless child.move_to_project_without_transaction(new_project)
173 unless child.move_to_project_without_transaction(new_project)
174 # Move failed and transaction was rollback'd
174 # Move failed and transaction was rollback'd
175 return false
175 return false
176 end
176 end
177 end
177 end
178 end
178 end
179 else
179 else
180 return false
180 return false
181 end
181 end
182 issue
182 issue
183 end
183 end
184
184
185 def status_id=(sid)
185 def status_id=(sid)
186 self.status = nil
186 self.status = nil
187 write_attribute(:status_id, sid)
187 write_attribute(:status_id, sid)
188 end
188 end
189
189
190 def priority_id=(pid)
190 def priority_id=(pid)
191 self.priority = nil
191 self.priority = nil
192 write_attribute(:priority_id, pid)
192 write_attribute(:priority_id, pid)
193 end
193 end
194
194
195 def tracker_id=(tid)
195 def tracker_id=(tid)
196 self.tracker = nil
196 self.tracker = nil
197 result = write_attribute(:tracker_id, tid)
197 result = write_attribute(:tracker_id, tid)
198 @custom_field_values = nil
198 @custom_field_values = nil
199 result
199 result
200 end
200 end
201
201
202 # Overrides attributes= so that tracker_id gets assigned first
202 # Overrides attributes= so that tracker_id gets assigned first
203 def attributes_with_tracker_first=(new_attributes, *args)
203 def attributes_with_tracker_first=(new_attributes, *args)
204 return if new_attributes.nil?
204 return if new_attributes.nil?
205 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
205 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
206 if new_tracker_id
206 if new_tracker_id
207 self.tracker_id = new_tracker_id
207 self.tracker_id = new_tracker_id
208 end
208 end
209 send :attributes_without_tracker_first=, new_attributes, *args
209 send :attributes_without_tracker_first=, new_attributes, *args
210 end
210 end
211 # Do not redefine alias chain on reload (see #4838)
211 # Do not redefine alias chain on reload (see #4838)
212 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
212 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
213
213
214 def estimated_hours=(h)
214 def estimated_hours=(h)
215 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
215 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
216 end
216 end
217
217
218 SAFE_ATTRIBUTES = %w(
218 SAFE_ATTRIBUTES = %w(
219 tracker_id
219 tracker_id
220 status_id
220 status_id
221 parent_issue_id
221 parent_issue_id
222 category_id
222 category_id
223 assigned_to_id
223 assigned_to_id
224 priority_id
224 priority_id
225 fixed_version_id
225 fixed_version_id
226 subject
226 subject
227 description
227 description
228 start_date
228 start_date
229 due_date
229 due_date
230 done_ratio
230 done_ratio
231 estimated_hours
231 estimated_hours
232 custom_field_values
232 custom_field_values
233 lock_version
233 lock_version
234 ) unless const_defined?(:SAFE_ATTRIBUTES)
234 ) unless const_defined?(:SAFE_ATTRIBUTES)
235
235
236 # Safely sets attributes
236 # Safely sets attributes
237 # Should be called from controllers instead of #attributes=
237 # Should be called from controllers instead of #attributes=
238 # attr_accessible is too rough because we still want things like
238 # attr_accessible is too rough because we still want things like
239 # Issue.new(:project => foo) to work
239 # Issue.new(:project => foo) to work
240 # TODO: move workflow/permission checks from controllers to here
240 # TODO: move workflow/permission checks from controllers to here
241 def safe_attributes=(attrs, user=User.current)
241 def safe_attributes=(attrs, user=User.current)
242 return if attrs.nil?
242 return if attrs.nil?
243 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
243 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
244 if attrs['status_id']
244 if attrs['status_id']
245 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
245 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
246 attrs.delete('status_id')
246 attrs.delete('status_id')
247 end
247 end
248 end
248 end
249
249
250 unless leaf?
250 unless leaf?
251 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
251 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
252 end
252 end
253
253
254 if attrs.has_key?('parent_issue_id')
254 if attrs.has_key?('parent_issue_id')
255 if !user.allowed_to?(:manage_subtasks, project)
255 if !user.allowed_to?(:manage_subtasks, project)
256 attrs.delete('parent_issue_id')
256 attrs.delete('parent_issue_id')
257 elsif !attrs['parent_issue_id'].blank?
257 elsif !attrs['parent_issue_id'].blank?
258 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'])
258 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'])
259 end
259 end
260 end
260 end
261
261
262 self.attributes = attrs
262 self.attributes = attrs
263 end
263 end
264
264
265 def done_ratio
265 def done_ratio
266 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
266 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
267 status.default_done_ratio
267 status.default_done_ratio
268 else
268 else
269 read_attribute(:done_ratio)
269 read_attribute(:done_ratio)
270 end
270 end
271 end
271 end
272
272
273 def self.use_status_for_done_ratio?
273 def self.use_status_for_done_ratio?
274 Setting.issue_done_ratio == 'issue_status'
274 Setting.issue_done_ratio == 'issue_status'
275 end
275 end
276
276
277 def self.use_field_for_done_ratio?
277 def self.use_field_for_done_ratio?
278 Setting.issue_done_ratio == 'issue_field'
278 Setting.issue_done_ratio == 'issue_field'
279 end
279 end
280
280
281 def validate
281 def validate
282 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
282 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
283 errors.add :due_date, :not_a_date
283 errors.add :due_date, :not_a_date
284 end
284 end
285
285
286 if self.due_date and self.start_date and self.due_date < self.start_date
286 if self.due_date and self.start_date and self.due_date < self.start_date
287 errors.add :due_date, :greater_than_start_date
287 errors.add :due_date, :greater_than_start_date
288 end
288 end
289
289
290 if start_date && soonest_start && start_date < soonest_start
290 if start_date && soonest_start && start_date < soonest_start
291 errors.add :start_date, :invalid
291 errors.add :start_date, :invalid
292 end
292 end
293
293
294 if fixed_version
294 if fixed_version
295 if !assignable_versions.include?(fixed_version)
295 if !assignable_versions.include?(fixed_version)
296 errors.add :fixed_version_id, :inclusion
296 errors.add :fixed_version_id, :inclusion
297 elsif reopened? && fixed_version.closed?
297 elsif reopened? && fixed_version.closed?
298 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
298 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
299 end
299 end
300 end
300 end
301
301
302 # Checks that the issue can not be added/moved to a disabled tracker
302 # Checks that the issue can not be added/moved to a disabled tracker
303 if project && (tracker_id_changed? || project_id_changed?)
303 if project && (tracker_id_changed? || project_id_changed?)
304 unless project.trackers.include?(tracker)
304 unless project.trackers.include?(tracker)
305 errors.add :tracker_id, :inclusion
305 errors.add :tracker_id, :inclusion
306 end
306 end
307 end
307 end
308
308
309 # Checks parent issue assignment
309 # Checks parent issue assignment
310 if @parent_issue
310 if @parent_issue
311 if @parent_issue.project_id != project_id
311 if @parent_issue.project_id != project_id
312 errors.add :parent_issue_id, :not_same_project
312 errors.add :parent_issue_id, :not_same_project
313 elsif !new_record?
313 elsif !new_record?
314 # moving an existing issue
314 # moving an existing issue
315 if @parent_issue.root_id != root_id
315 if @parent_issue.root_id != root_id
316 # we can always move to another tree
316 # we can always move to another tree
317 elsif move_possible?(@parent_issue)
317 elsif move_possible?(@parent_issue)
318 # move accepted inside tree
318 # move accepted inside tree
319 else
319 else
320 errors.add :parent_issue_id, :not_a_valid_parent
320 errors.add :parent_issue_id, :not_a_valid_parent
321 end
321 end
322 end
322 end
323 end
323 end
324 end
324 end
325
325
326 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
326 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
327 # even if the user turns off the setting later
327 # even if the user turns off the setting later
328 def update_done_ratio_from_issue_status
328 def update_done_ratio_from_issue_status
329 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
329 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
330 self.done_ratio = status.default_done_ratio
330 self.done_ratio = status.default_done_ratio
331 end
331 end
332 end
332 end
333
333
334 def init_journal(user, notes = "")
334 def init_journal(user, notes = "")
335 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
335 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
336 @issue_before_change = self.clone
336 @issue_before_change = self.clone
337 @issue_before_change.status = self.status
337 @issue_before_change.status = self.status
338 @custom_values_before_change = {}
338 @custom_values_before_change = {}
339 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
339 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
340 # Make sure updated_on is updated when adding a note.
340 # Make sure updated_on is updated when adding a note.
341 updated_on_will_change!
341 updated_on_will_change!
342 @current_journal
342 @current_journal
343 end
343 end
344
344
345 # Return true if the issue is closed, otherwise false
345 # Return true if the issue is closed, otherwise false
346 def closed?
346 def closed?
347 self.status.is_closed?
347 self.status.is_closed?
348 end
348 end
349
349
350 # Return true if the issue is being reopened
350 # Return true if the issue is being reopened
351 def reopened?
351 def reopened?
352 if !new_record? && status_id_changed?
352 if !new_record? && status_id_changed?
353 status_was = IssueStatus.find_by_id(status_id_was)
353 status_was = IssueStatus.find_by_id(status_id_was)
354 status_new = IssueStatus.find_by_id(status_id)
354 status_new = IssueStatus.find_by_id(status_id)
355 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
355 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
356 return true
356 return true
357 end
357 end
358 end
358 end
359 false
359 false
360 end
360 end
361
361
362 # Return true if the issue is being closed
362 # Return true if the issue is being closed
363 def closing?
363 def closing?
364 if !new_record? && status_id_changed?
364 if !new_record? && status_id_changed?
365 status_was = IssueStatus.find_by_id(status_id_was)
365 status_was = IssueStatus.find_by_id(status_id_was)
366 status_new = IssueStatus.find_by_id(status_id)
366 status_new = IssueStatus.find_by_id(status_id)
367 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
367 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
368 return true
368 return true
369 end
369 end
370 end
370 end
371 false
371 false
372 end
372 end
373
373
374 # Returns true if the issue is overdue
374 # Returns true if the issue is overdue
375 def overdue?
375 def overdue?
376 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
376 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
377 end
377 end
378
378
379 # Is the amount of work done less than it should for the due date
379 # Is the amount of work done less than it should for the due date
380 def behind_schedule?
380 def behind_schedule?
381 return false if start_date.nil? || due_date.nil?
381 return false if start_date.nil? || due_date.nil?
382 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
382 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
383 return done_date <= Date.today
383 return done_date <= Date.today
384 end
384 end
385
385
386 # Does this issue have children?
386 # Does this issue have children?
387 def children?
387 def children?
388 !leaf?
388 !leaf?
389 end
389 end
390
390
391 # Users the issue can be assigned to
391 # Users the issue can be assigned to
392 def assignable_users
392 def assignable_users
393 project.assignable_users
393 project.assignable_users
394 end
394 end
395
395
396 # Versions that the issue can be assigned to
396 # Versions that the issue can be assigned to
397 def assignable_versions
397 def assignable_versions
398 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
398 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
399 end
399 end
400
400
401 # Returns true if this issue is blocked by another issue that is still open
401 # Returns true if this issue is blocked by another issue that is still open
402 def blocked?
402 def blocked?
403 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
403 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
404 end
404 end
405
405
406 # Returns an array of status that user is able to apply
406 # Returns an array of status that user is able to apply
407 def new_statuses_allowed_to(user, include_default=false)
407 def new_statuses_allowed_to(user, include_default=false)
408 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
408 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
409 statuses << status unless statuses.empty?
409 statuses << status unless statuses.empty?
410 statuses << IssueStatus.default if include_default
410 statuses << IssueStatus.default if include_default
411 statuses = statuses.uniq.sort
411 statuses = statuses.uniq.sort
412 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
412 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
413 end
413 end
414
414
415 # Returns the mail adresses of users that should be notified
415 # Returns the mail adresses of users that should be notified
416 def recipients
416 def recipients
417 notified = project.notified_users
417 notified = project.notified_users
418 # Author and assignee are always notified unless they have been locked
418 # Author and assignee are always notified unless they have been locked
419 notified << author if author && author.active?
419 notified << author if author && author.active?
420 notified << assigned_to if assigned_to && assigned_to.active?
420 notified << assigned_to if assigned_to && assigned_to.active?
421 notified.uniq!
421 notified.uniq!
422 # Remove users that can not view the issue
422 # Remove users that can not view the issue
423 notified.reject! {|user| !visible?(user)}
423 notified.reject! {|user| !visible?(user)}
424 notified.collect(&:mail)
424 notified.collect(&:mail)
425 end
425 end
426
426
427 # Returns the total number of hours spent on this issue and its descendants
427 # Returns the total number of hours spent on this issue and its descendants
428 #
428 #
429 # Example:
429 # Example:
430 # spent_hours => 0.0
430 # spent_hours => 0.0
431 # spent_hours => 50.2
431 # spent_hours => 50.2
432 def spent_hours
432 def spent_hours
433 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
433 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
434 end
434 end
435
435
436 def relations
436 def relations
437 (relations_from + relations_to).sort
437 (relations_from + relations_to).sort
438 end
438 end
439
439
440 def all_dependent_issues
440 def all_dependent_issues
441 dependencies = []
441 dependencies = []
442 relations_from.each do |relation|
442 relations_from.each do |relation|
443 dependencies << relation.issue_to
443 dependencies << relation.issue_to
444 dependencies += relation.issue_to.all_dependent_issues
444 dependencies += relation.issue_to.all_dependent_issues
445 end
445 end
446 dependencies
446 dependencies
447 end
447 end
448
448
449 # Returns an array of issues that duplicate this one
449 # Returns an array of issues that duplicate this one
450 def duplicates
450 def duplicates
451 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
451 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
452 end
452 end
453
453
454 # Returns the due date or the target due date if any
454 # Returns the due date or the target due date if any
455 # Used on gantt chart
455 # Used on gantt chart
456 def due_before
456 def due_before
457 due_date || (fixed_version ? fixed_version.effective_date : nil)
457 due_date || (fixed_version ? fixed_version.effective_date : nil)
458 end
458 end
459
459
460 # Returns the time scheduled for this issue.
460 # Returns the time scheduled for this issue.
461 #
461 #
462 # Example:
462 # Example:
463 # Start Date: 2/26/09, End Date: 3/04/09
463 # Start Date: 2/26/09, End Date: 3/04/09
464 # duration => 6
464 # duration => 6
465 def duration
465 def duration
466 (start_date && due_date) ? due_date - start_date : 0
466 (start_date && due_date) ? due_date - start_date : 0
467 end
467 end
468
468
469 def soonest_start
469 def soonest_start
470 @soonest_start ||= (
470 @soonest_start ||= (
471 relations_to.collect{|relation| relation.successor_soonest_start} +
471 relations_to.collect{|relation| relation.successor_soonest_start} +
472 ancestors.collect(&:soonest_start)
472 ancestors.collect(&:soonest_start)
473 ).compact.max
473 ).compact.max
474 end
474 end
475
475
476 def reschedule_after(date)
476 def reschedule_after(date)
477 return if date.nil?
477 return if date.nil?
478 if leaf?
478 if leaf?
479 if start_date.nil? || start_date < date
479 if start_date.nil? || start_date < date
480 self.start_date, self.due_date = date, date + duration
480 self.start_date, self.due_date = date, date + duration
481 save
481 save
482 end
482 end
483 else
483 else
484 leaves.each do |leaf|
484 leaves.each do |leaf|
485 leaf.reschedule_after(date)
485 leaf.reschedule_after(date)
486 end
486 end
487 end
487 end
488 end
488 end
489
489
490 def <=>(issue)
490 def <=>(issue)
491 if issue.nil?
491 if issue.nil?
492 -1
492 -1
493 elsif root_id != issue.root_id
493 elsif root_id != issue.root_id
494 (root_id || 0) <=> (issue.root_id || 0)
494 (root_id || 0) <=> (issue.root_id || 0)
495 else
495 else
496 (lft || 0) <=> (issue.lft || 0)
496 (lft || 0) <=> (issue.lft || 0)
497 end
497 end
498 end
498 end
499
499
500 def to_s
500 def to_s
501 "#{tracker} ##{id}: #{subject}"
501 "#{tracker} ##{id}: #{subject}"
502 end
502 end
503
503
504 # Returns a string of css classes that apply to the issue
504 # Returns a string of css classes that apply to the issue
505 def css_classes
505 def css_classes
506 s = "issue status-#{status.position} priority-#{priority.position}"
506 s = "issue status-#{status.position} priority-#{priority.position}"
507 s << ' closed' if closed?
507 s << ' closed' if closed?
508 s << ' overdue' if overdue?
508 s << ' overdue' if overdue?
509 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
509 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
510 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
510 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
511 s
511 s
512 end
512 end
513
513
514 # Saves an issue, time_entry, attachments, and a journal from the parameters
514 # Saves an issue, time_entry, attachments, and a journal from the parameters
515 # Returns false if save fails
515 # Returns false if save fails
516 def save_issue_with_child_records(params, existing_time_entry=nil)
516 def save_issue_with_child_records(params, existing_time_entry=nil)
517 Issue.transaction do
517 Issue.transaction do
518 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
518 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
519 @time_entry = existing_time_entry || TimeEntry.new
519 @time_entry = existing_time_entry || TimeEntry.new
520 @time_entry.project = project
520 @time_entry.project = project
521 @time_entry.issue = self
521 @time_entry.issue = self
522 @time_entry.user = User.current
522 @time_entry.user = User.current
523 @time_entry.spent_on = Date.today
523 @time_entry.spent_on = Date.today
524 @time_entry.attributes = params[:time_entry]
524 @time_entry.attributes = params[:time_entry]
525 self.time_entries << @time_entry
525 self.time_entries << @time_entry
526 end
526 end
527
527
528 if valid?
528 if valid?
529 attachments = Attachment.attach_files(self, params[:attachments])
529 attachments = Attachment.attach_files(self, params[:attachments])
530
530
531 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
531 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
532 # TODO: Rename hook
532 # TODO: Rename hook
533 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
533 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
534 begin
534 begin
535 if save
535 if save
536 # TODO: Rename hook
536 # TODO: Rename hook
537 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
537 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
538 else
538 else
539 raise ActiveRecord::Rollback
539 raise ActiveRecord::Rollback
540 end
540 end
541 rescue ActiveRecord::StaleObjectError
541 rescue ActiveRecord::StaleObjectError
542 attachments[:files].each(&:destroy)
542 attachments[:files].each(&:destroy)
543 errors.add_to_base l(:notice_locking_conflict)
543 errors.add_to_base l(:notice_locking_conflict)
544 raise ActiveRecord::Rollback
544 raise ActiveRecord::Rollback
545 end
545 end
546 end
546 end
547 end
547 end
548 end
548 end
549
549
550 # Unassigns issues from +version+ if it's no longer shared with issue's project
550 # Unassigns issues from +version+ if it's no longer shared with issue's project
551 def self.update_versions_from_sharing_change(version)
551 def self.update_versions_from_sharing_change(version)
552 # Update issues assigned to the version
552 # Update issues assigned to the version
553 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
553 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
554 end
554 end
555
555
556 # Unassigns issues from versions that are no longer shared
556 # Unassigns issues from versions that are no longer shared
557 # after +project+ was moved
557 # after +project+ was moved
558 def self.update_versions_from_hierarchy_change(project)
558 def self.update_versions_from_hierarchy_change(project)
559 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
559 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
560 # Update issues of the moved projects and issues assigned to a version of a moved project
560 # Update issues of the moved projects and issues assigned to a version of a moved project
561 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
561 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
562 end
562 end
563
563
564 def parent_issue_id=(arg)
564 def parent_issue_id=(arg)
565 parent_issue_id = arg.blank? ? nil : arg.to_i
565 parent_issue_id = arg.blank? ? nil : arg.to_i
566 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
566 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
567 @parent_issue.id
567 @parent_issue.id
568 else
568 else
569 @parent_issue = nil
569 @parent_issue = nil
570 nil
570 nil
571 end
571 end
572 end
572 end
573
573
574 def parent_issue_id
574 def parent_issue_id
575 if instance_variable_defined? :@parent_issue
575 if instance_variable_defined? :@parent_issue
576 @parent_issue.nil? ? nil : @parent_issue.id
576 @parent_issue.nil? ? nil : @parent_issue.id
577 else
577 else
578 parent_id
578 parent_id
579 end
579 end
580 end
580 end
581
581
582 # Extracted from the ReportsController.
582 # Extracted from the ReportsController.
583 def self.by_tracker(project)
583 def self.by_tracker(project)
584 count_and_group_by(:project => project,
584 count_and_group_by(:project => project,
585 :field => 'tracker_id',
585 :field => 'tracker_id',
586 :joins => Tracker.table_name)
586 :joins => Tracker.table_name)
587 end
587 end
588
588
589 def self.by_version(project)
589 def self.by_version(project)
590 count_and_group_by(:project => project,
590 count_and_group_by(:project => project,
591 :field => 'fixed_version_id',
591 :field => 'fixed_version_id',
592 :joins => Version.table_name)
592 :joins => Version.table_name)
593 end
593 end
594
594
595 def self.by_priority(project)
595 def self.by_priority(project)
596 count_and_group_by(:project => project,
596 count_and_group_by(:project => project,
597 :field => 'priority_id',
597 :field => 'priority_id',
598 :joins => IssuePriority.table_name)
598 :joins => IssuePriority.table_name)
599 end
599 end
600
600
601 def self.by_category(project)
601 def self.by_category(project)
602 count_and_group_by(:project => project,
602 count_and_group_by(:project => project,
603 :field => 'category_id',
603 :field => 'category_id',
604 :joins => IssueCategory.table_name)
604 :joins => IssueCategory.table_name)
605 end
605 end
606
606
607 def self.by_assigned_to(project)
607 def self.by_assigned_to(project)
608 count_and_group_by(:project => project,
608 count_and_group_by(:project => project,
609 :field => 'assigned_to_id',
609 :field => 'assigned_to_id',
610 :joins => User.table_name)
610 :joins => User.table_name)
611 end
611 end
612
612
613 def self.by_author(project)
613 def self.by_author(project)
614 count_and_group_by(:project => project,
614 count_and_group_by(:project => project,
615 :field => 'author_id',
615 :field => 'author_id',
616 :joins => User.table_name)
616 :joins => User.table_name)
617 end
617 end
618
618
619 def self.by_subproject(project)
619 def self.by_subproject(project)
620 ActiveRecord::Base.connection.select_all("select s.id as status_id,
620 ActiveRecord::Base.connection.select_all("select s.id as status_id,
621 s.is_closed as closed,
621 s.is_closed as closed,
622 i.project_id as project_id,
622 i.project_id as project_id,
623 count(i.id) as total
623 count(i.id) as total
624 from
624 from
625 #{Issue.table_name} i, #{IssueStatus.table_name} s
625 #{Issue.table_name} i, #{IssueStatus.table_name} s
626 where
626 where
627 i.status_id=s.id
627 i.status_id=s.id
628 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
628 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
629 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
629 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
630 end
630 end
631 # End ReportsController extraction
631 # End ReportsController extraction
632
632
633 # Returns an array of projects that current user can move issues to
633 # Returns an array of projects that current user can move issues to
634 def self.allowed_target_projects_on_move
634 def self.allowed_target_projects_on_move
635 projects = []
635 projects = []
636 if User.current.admin?
636 if User.current.admin?
637 # admin is allowed to move issues to any active (visible) project
637 # admin is allowed to move issues to any active (visible) project
638 projects = Project.visible.all
638 projects = Project.visible.all
639 elsif User.current.logged?
639 elsif User.current.logged?
640 if Role.non_member.allowed_to?(:move_issues)
640 if Role.non_member.allowed_to?(:move_issues)
641 projects = Project.visible.all
641 projects = Project.visible.all
642 else
642 else
643 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
643 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
644 end
644 end
645 end
645 end
646 projects
646 projects
647 end
647 end
648
648
649 private
649 private
650
650
651 def update_nested_set_attributes
651 def update_nested_set_attributes
652 if root_id.nil?
652 if root_id.nil?
653 # issue was just created
653 # issue was just created
654 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
654 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
655 set_default_left_and_right
655 set_default_left_and_right
656 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
656 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
657 if @parent_issue
657 if @parent_issue
658 move_to_child_of(@parent_issue)
658 move_to_child_of(@parent_issue)
659 end
659 end
660 reload
660 reload
661 elsif parent_issue_id != parent_id
661 elsif parent_issue_id != parent_id
662 former_parent_id = parent_id
662 former_parent_id = parent_id
663 # moving an existing issue
663 # moving an existing issue
664 if @parent_issue && @parent_issue.root_id == root_id
664 if @parent_issue && @parent_issue.root_id == root_id
665 # inside the same tree
665 # inside the same tree
666 move_to_child_of(@parent_issue)
666 move_to_child_of(@parent_issue)
667 else
667 else
668 # to another tree
668 # to another tree
669 unless root?
669 unless root?
670 move_to_right_of(root)
670 move_to_right_of(root)
671 reload
671 reload
672 end
672 end
673 old_root_id = root_id
673 old_root_id = root_id
674 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
674 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
675 target_maxright = nested_set_scope.maximum(right_column_name) || 0
675 target_maxright = nested_set_scope.maximum(right_column_name) || 0
676 offset = target_maxright + 1 - lft
676 offset = target_maxright + 1 - lft
677 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
677 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
678 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
678 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
679 self[left_column_name] = lft + offset
679 self[left_column_name] = lft + offset
680 self[right_column_name] = rgt + offset
680 self[right_column_name] = rgt + offset
681 if @parent_issue
681 if @parent_issue
682 move_to_child_of(@parent_issue)
682 move_to_child_of(@parent_issue)
683 end
683 end
684 end
684 end
685 reload
685 reload
686 # delete invalid relations of all descendants
686 # delete invalid relations of all descendants
687 self_and_descendants.each do |issue|
687 self_and_descendants.each do |issue|
688 issue.relations.each do |relation|
688 issue.relations.each do |relation|
689 relation.destroy unless relation.valid?
689 relation.destroy unless relation.valid?
690 end
690 end
691 end
691 end
692 # update former parent
692 # update former parent
693 recalculate_attributes_for(former_parent_id) if former_parent_id
693 recalculate_attributes_for(former_parent_id) if former_parent_id
694 end
694 end
695 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
695 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
696 end
696 end
697
697
698 def update_parent_attributes
698 def update_parent_attributes
699 recalculate_attributes_for(parent_id) if parent_id
699 recalculate_attributes_for(parent_id) if parent_id
700 end
700 end
701
701
702 def recalculate_attributes_for(issue_id)
702 def recalculate_attributes_for(issue_id)
703 if issue_id && p = Issue.find_by_id(issue_id)
703 if issue_id && p = Issue.find_by_id(issue_id)
704 # priority = highest priority of children
704 # priority = highest priority of children
705 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
705 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
706 p.priority = IssuePriority.find_by_position(priority_position)
706 p.priority = IssuePriority.find_by_position(priority_position)
707 end
707 end
708
708
709 # start/due dates = lowest/highest dates of children
709 # start/due dates = lowest/highest dates of children
710 p.start_date = p.children.minimum(:start_date)
710 p.start_date = p.children.minimum(:start_date)
711 p.due_date = p.children.maximum(:due_date)
711 p.due_date = p.children.maximum(:due_date)
712 if p.start_date && p.due_date && p.due_date < p.start_date
712 if p.start_date && p.due_date && p.due_date < p.start_date
713 p.start_date, p.due_date = p.due_date, p.start_date
713 p.start_date, p.due_date = p.due_date, p.start_date
714 end
714 end
715
715
716 # done ratio = weighted average ratio of leaves
716 # done ratio = weighted average ratio of leaves
717 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio?
717 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
718 leaves_count = p.leaves.count
718 leaves_count = p.leaves.count
719 if leaves_count > 0
719 if leaves_count > 0
720 average = p.leaves.average(:estimated_hours).to_f
720 average = p.leaves.average(:estimated_hours).to_f
721 if average == 0
721 if average == 0
722 average = 1
722 average = 1
723 end
723 end
724 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
724 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
725 progress = done / (average * leaves_count)
725 progress = done / (average * leaves_count)
726 p.done_ratio = progress.round
726 p.done_ratio = progress.round
727 end
727 end
728 end
728 end
729
729
730 # estimate = sum of leaves estimates
730 # estimate = sum of leaves estimates
731 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
731 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
732 p.estimated_hours = nil if p.estimated_hours == 0.0
732 p.estimated_hours = nil if p.estimated_hours == 0.0
733
733
734 # ancestors will be recursively updated
734 # ancestors will be recursively updated
735 p.save(false)
735 p.save(false)
736 end
736 end
737 end
737 end
738
738
739 def destroy_children
739 def destroy_children
740 unless leaf?
740 unless leaf?
741 children.each do |child|
741 children.each do |child|
742 child.destroy
742 child.destroy
743 end
743 end
744 end
744 end
745 end
745 end
746
746
747 # Update issues so their versions are not pointing to a
747 # Update issues so their versions are not pointing to a
748 # fixed_version that is not shared with the issue's project
748 # fixed_version that is not shared with the issue's project
749 def self.update_versions(conditions=nil)
749 def self.update_versions(conditions=nil)
750 # Only need to update issues with a fixed_version from
750 # Only need to update issues with a fixed_version from
751 # a different project and that is not systemwide shared
751 # a different project and that is not systemwide shared
752 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
752 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
753 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
753 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
754 " AND #{Version.table_name}.sharing <> 'system'",
754 " AND #{Version.table_name}.sharing <> 'system'",
755 conditions),
755 conditions),
756 :include => [:project, :fixed_version]
756 :include => [:project, :fixed_version]
757 ).each do |issue|
757 ).each do |issue|
758 next if issue.project.nil? || issue.fixed_version.nil?
758 next if issue.project.nil? || issue.fixed_version.nil?
759 unless issue.project.shared_versions.include?(issue.fixed_version)
759 unless issue.project.shared_versions.include?(issue.fixed_version)
760 issue.init_journal(User.current)
760 issue.init_journal(User.current)
761 issue.fixed_version = nil
761 issue.fixed_version = nil
762 issue.save
762 issue.save
763 end
763 end
764 end
764 end
765 end
765 end
766
766
767 # Callback on attachment deletion
767 # Callback on attachment deletion
768 def attachment_removed(obj)
768 def attachment_removed(obj)
769 journal = init_journal(User.current)
769 journal = init_journal(User.current)
770 journal.details << JournalDetail.new(:property => 'attachment',
770 journal.details << JournalDetail.new(:property => 'attachment',
771 :prop_key => obj.id,
771 :prop_key => obj.id,
772 :old_value => obj.filename)
772 :old_value => obj.filename)
773 journal.save
773 journal.save
774 end
774 end
775
775
776 # Default assignment based on category
776 # Default assignment based on category
777 def default_assign
777 def default_assign
778 if assigned_to.nil? && category && category.assigned_to
778 if assigned_to.nil? && category && category.assigned_to
779 self.assigned_to = category.assigned_to
779 self.assigned_to = category.assigned_to
780 end
780 end
781 end
781 end
782
782
783 # Updates start/due dates of following issues
783 # Updates start/due dates of following issues
784 def reschedule_following_issues
784 def reschedule_following_issues
785 if start_date_changed? || due_date_changed?
785 if start_date_changed? || due_date_changed?
786 relations_from.each do |relation|
786 relations_from.each do |relation|
787 relation.set_issue_to_dates
787 relation.set_issue_to_dates
788 end
788 end
789 end
789 end
790 end
790 end
791
791
792 # Closes duplicates if the issue is being closed
792 # Closes duplicates if the issue is being closed
793 def close_duplicates
793 def close_duplicates
794 if closing?
794 if closing?
795 duplicates.each do |duplicate|
795 duplicates.each do |duplicate|
796 # Reload is need in case the duplicate was updated by a previous duplicate
796 # Reload is need in case the duplicate was updated by a previous duplicate
797 duplicate.reload
797 duplicate.reload
798 # Don't re-close it if it's already closed
798 # Don't re-close it if it's already closed
799 next if duplicate.closed?
799 next if duplicate.closed?
800 # Same user and notes
800 # Same user and notes
801 if @current_journal
801 if @current_journal
802 duplicate.init_journal(@current_journal.user, @current_journal.notes)
802 duplicate.init_journal(@current_journal.user, @current_journal.notes)
803 end
803 end
804 duplicate.update_attribute :status, self.status
804 duplicate.update_attribute :status, self.status
805 end
805 end
806 end
806 end
807 end
807 end
808
808
809 # Saves the changes in a Journal
809 # Saves the changes in a Journal
810 # Called after_save
810 # Called after_save
811 def create_journal
811 def create_journal
812 if @current_journal
812 if @current_journal
813 # attributes changes
813 # attributes changes
814 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
814 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
815 @current_journal.details << JournalDetail.new(:property => 'attr',
815 @current_journal.details << JournalDetail.new(:property => 'attr',
816 :prop_key => c,
816 :prop_key => c,
817 :old_value => @issue_before_change.send(c),
817 :old_value => @issue_before_change.send(c),
818 :value => send(c)) unless send(c)==@issue_before_change.send(c)
818 :value => send(c)) unless send(c)==@issue_before_change.send(c)
819 }
819 }
820 # custom fields changes
820 # custom fields changes
821 custom_values.each {|c|
821 custom_values.each {|c|
822 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
822 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
823 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
823 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
824 @current_journal.details << JournalDetail.new(:property => 'cf',
824 @current_journal.details << JournalDetail.new(:property => 'cf',
825 :prop_key => c.custom_field_id,
825 :prop_key => c.custom_field_id,
826 :old_value => @custom_values_before_change[c.custom_field_id],
826 :old_value => @custom_values_before_change[c.custom_field_id],
827 :value => c.value)
827 :value => c.value)
828 }
828 }
829 @current_journal.save
829 @current_journal.save
830 # reset current journal
830 # reset current journal
831 init_journal @current_journal.user, @current_journal.notes
831 init_journal @current_journal.user, @current_journal.notes
832 end
832 end
833 end
833 end
834
834
835 # Query generator for selecting groups of issue counts for a project
835 # Query generator for selecting groups of issue counts for a project
836 # based on specific criteria
836 # based on specific criteria
837 #
837 #
838 # Options
838 # Options
839 # * project - Project to search in.
839 # * project - Project to search in.
840 # * field - String. Issue field to key off of in the grouping.
840 # * field - String. Issue field to key off of in the grouping.
841 # * joins - String. The table name to join against.
841 # * joins - String. The table name to join against.
842 def self.count_and_group_by(options)
842 def self.count_and_group_by(options)
843 project = options.delete(:project)
843 project = options.delete(:project)
844 select_field = options.delete(:field)
844 select_field = options.delete(:field)
845 joins = options.delete(:joins)
845 joins = options.delete(:joins)
846
846
847 where = "i.#{select_field}=j.id"
847 where = "i.#{select_field}=j.id"
848
848
849 ActiveRecord::Base.connection.select_all("select s.id as status_id,
849 ActiveRecord::Base.connection.select_all("select s.id as status_id,
850 s.is_closed as closed,
850 s.is_closed as closed,
851 j.id as #{select_field},
851 j.id as #{select_field},
852 count(i.id) as total
852 count(i.id) as total
853 from
853 from
854 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
854 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
855 where
855 where
856 i.status_id=s.id
856 i.status_id=s.id
857 and #{where}
857 and #{where}
858 and i.project_id=#{project.id}
858 and i.project_id=#{project.id}
859 group by s.id, s.is_closed, j.id")
859 group by s.id, s.is_closed, j.id")
860 end
860 end
861
861
862
862
863 end
863 end
@@ -1,245 +1,246
1 ---
1 ---
2 issues_001:
2 issues_001:
3 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
3 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
4 project_id: 1
4 project_id: 1
5 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
5 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
6 priority_id: 4
6 priority_id: 4
7 subject: Can't print recipes
7 subject: Can't print recipes
8 id: 1
8 id: 1
9 fixed_version_id:
9 fixed_version_id:
10 category_id: 1
10 category_id: 1
11 description: Unable to print recipes
11 description: Unable to print recipes
12 tracker_id: 1
12 tracker_id: 1
13 assigned_to_id:
13 assigned_to_id:
14 author_id: 2
14 author_id: 2
15 status_id: 1
15 status_id: 1
16 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
16 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
17 due_date: <%= 10.day.from_now.to_date.to_s(:db) %>
17 due_date: <%= 10.day.from_now.to_date.to_s(:db) %>
18 root_id: 1
18 root_id: 1
19 lft: 1
19 lft: 1
20 rgt: 2
20 rgt: 2
21 issues_002:
21 issues_002:
22 created_on: 2006-07-19 21:04:21 +02:00
22 created_on: 2006-07-19 21:04:21 +02:00
23 project_id: 1
23 project_id: 1
24 updated_on: 2006-07-19 21:09:50 +02:00
24 updated_on: 2006-07-19 21:09:50 +02:00
25 priority_id: 5
25 priority_id: 5
26 subject: Add ingredients categories
26 subject: Add ingredients categories
27 id: 2
27 id: 2
28 fixed_version_id: 2
28 fixed_version_id: 2
29 category_id:
29 category_id:
30 description: Ingredients of the recipe should be classified by categories
30 description: Ingredients of the recipe should be classified by categories
31 tracker_id: 2
31 tracker_id: 2
32 assigned_to_id: 3
32 assigned_to_id: 3
33 author_id: 2
33 author_id: 2
34 status_id: 2
34 status_id: 2
35 start_date: <%= 2.day.ago.to_date.to_s(:db) %>
35 start_date: <%= 2.day.ago.to_date.to_s(:db) %>
36 due_date:
36 due_date:
37 root_id: 2
37 root_id: 2
38 lft: 1
38 lft: 1
39 rgt: 2
39 rgt: 2
40 lock_version: 3
40 lock_version: 3
41 done_ratio: 30
41 issues_003:
42 issues_003:
42 created_on: 2006-07-19 21:07:27 +02:00
43 created_on: 2006-07-19 21:07:27 +02:00
43 project_id: 1
44 project_id: 1
44 updated_on: 2006-07-19 21:07:27 +02:00
45 updated_on: 2006-07-19 21:07:27 +02:00
45 priority_id: 4
46 priority_id: 4
46 subject: Error 281 when updating a recipe
47 subject: Error 281 when updating a recipe
47 id: 3
48 id: 3
48 fixed_version_id:
49 fixed_version_id:
49 category_id:
50 category_id:
50 description: Error 281 is encountered when saving a recipe
51 description: Error 281 is encountered when saving a recipe
51 tracker_id: 1
52 tracker_id: 1
52 assigned_to_id: 3
53 assigned_to_id: 3
53 author_id: 2
54 author_id: 2
54 status_id: 1
55 status_id: 1
55 start_date: <%= 1.day.from_now.to_date.to_s(:db) %>
56 start_date: <%= 1.day.from_now.to_date.to_s(:db) %>
56 due_date: <%= 40.day.ago.to_date.to_s(:db) %>
57 due_date: <%= 40.day.ago.to_date.to_s(:db) %>
57 root_id: 3
58 root_id: 3
58 lft: 1
59 lft: 1
59 rgt: 2
60 rgt: 2
60 issues_004:
61 issues_004:
61 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
62 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
62 project_id: 2
63 project_id: 2
63 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
64 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
64 priority_id: 4
65 priority_id: 4
65 subject: Issue on project 2
66 subject: Issue on project 2
66 id: 4
67 id: 4
67 fixed_version_id:
68 fixed_version_id:
68 category_id:
69 category_id:
69 description: Issue on project 2
70 description: Issue on project 2
70 tracker_id: 1
71 tracker_id: 1
71 assigned_to_id: 2
72 assigned_to_id: 2
72 author_id: 2
73 author_id: 2
73 status_id: 1
74 status_id: 1
74 root_id: 4
75 root_id: 4
75 lft: 1
76 lft: 1
76 rgt: 2
77 rgt: 2
77 issues_005:
78 issues_005:
78 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
79 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
79 project_id: 3
80 project_id: 3
80 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
81 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
81 priority_id: 4
82 priority_id: 4
82 subject: Subproject issue
83 subject: Subproject issue
83 id: 5
84 id: 5
84 fixed_version_id:
85 fixed_version_id:
85 category_id:
86 category_id:
86 description: This is an issue on a cookbook subproject
87 description: This is an issue on a cookbook subproject
87 tracker_id: 1
88 tracker_id: 1
88 assigned_to_id:
89 assigned_to_id:
89 author_id: 2
90 author_id: 2
90 status_id: 1
91 status_id: 1
91 root_id: 5
92 root_id: 5
92 lft: 1
93 lft: 1
93 rgt: 2
94 rgt: 2
94 issues_006:
95 issues_006:
95 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
96 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
96 project_id: 5
97 project_id: 5
97 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
98 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
98 priority_id: 4
99 priority_id: 4
99 subject: Issue of a private subproject
100 subject: Issue of a private subproject
100 id: 6
101 id: 6
101 fixed_version_id:
102 fixed_version_id:
102 category_id:
103 category_id:
103 description: This is an issue of a private subproject of cookbook
104 description: This is an issue of a private subproject of cookbook
104 tracker_id: 1
105 tracker_id: 1
105 assigned_to_id:
106 assigned_to_id:
106 author_id: 2
107 author_id: 2
107 status_id: 1
108 status_id: 1
108 start_date: <%= Date.today.to_s(:db) %>
109 start_date: <%= Date.today.to_s(:db) %>
109 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
110 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
110 root_id: 6
111 root_id: 6
111 lft: 1
112 lft: 1
112 rgt: 2
113 rgt: 2
113 issues_007:
114 issues_007:
114 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
115 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
115 project_id: 1
116 project_id: 1
116 updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
117 updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
117 priority_id: 5
118 priority_id: 5
118 subject: Issue due today
119 subject: Issue due today
119 id: 7
120 id: 7
120 fixed_version_id:
121 fixed_version_id:
121 category_id:
122 category_id:
122 description: This is an issue that is due today
123 description: This is an issue that is due today
123 tracker_id: 1
124 tracker_id: 1
124 assigned_to_id:
125 assigned_to_id:
125 author_id: 2
126 author_id: 2
126 status_id: 1
127 status_id: 1
127 start_date: <%= 10.days.ago.to_s(:db) %>
128 start_date: <%= 10.days.ago.to_s(:db) %>
128 due_date: <%= Date.today.to_s(:db) %>
129 due_date: <%= Date.today.to_s(:db) %>
129 lock_version: 0
130 lock_version: 0
130 root_id: 7
131 root_id: 7
131 lft: 1
132 lft: 1
132 rgt: 2
133 rgt: 2
133 issues_008:
134 issues_008:
134 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
135 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
135 project_id: 1
136 project_id: 1
136 updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
137 updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
137 priority_id: 5
138 priority_id: 5
138 subject: Closed issue
139 subject: Closed issue
139 id: 8
140 id: 8
140 fixed_version_id:
141 fixed_version_id:
141 category_id:
142 category_id:
142 description: This is a closed issue.
143 description: This is a closed issue.
143 tracker_id: 1
144 tracker_id: 1
144 assigned_to_id:
145 assigned_to_id:
145 author_id: 2
146 author_id: 2
146 status_id: 5
147 status_id: 5
147 start_date:
148 start_date:
148 due_date:
149 due_date:
149 lock_version: 0
150 lock_version: 0
150 root_id: 8
151 root_id: 8
151 lft: 1
152 lft: 1
152 rgt: 2
153 rgt: 2
153 issues_009:
154 issues_009:
154 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
155 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
155 project_id: 5
156 project_id: 5
156 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
157 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
157 priority_id: 5
158 priority_id: 5
158 subject: Blocked Issue
159 subject: Blocked Issue
159 id: 9
160 id: 9
160 fixed_version_id:
161 fixed_version_id:
161 category_id:
162 category_id:
162 description: This is an issue that is blocked by issue #10
163 description: This is an issue that is blocked by issue #10
163 tracker_id: 1
164 tracker_id: 1
164 assigned_to_id:
165 assigned_to_id:
165 author_id: 2
166 author_id: 2
166 status_id: 1
167 status_id: 1
167 start_date: <%= Date.today.to_s(:db) %>
168 start_date: <%= Date.today.to_s(:db) %>
168 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
169 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
169 root_id: 9
170 root_id: 9
170 lft: 1
171 lft: 1
171 rgt: 2
172 rgt: 2
172 issues_010:
173 issues_010:
173 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
174 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
174 project_id: 5
175 project_id: 5
175 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
176 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
176 priority_id: 5
177 priority_id: 5
177 subject: Issue Doing the Blocking
178 subject: Issue Doing the Blocking
178 id: 10
179 id: 10
179 fixed_version_id:
180 fixed_version_id:
180 category_id:
181 category_id:
181 description: This is an issue that blocks issue #9
182 description: This is an issue that blocks issue #9
182 tracker_id: 1
183 tracker_id: 1
183 assigned_to_id:
184 assigned_to_id:
184 author_id: 2
185 author_id: 2
185 status_id: 1
186 status_id: 1
186 start_date: <%= Date.today.to_s(:db) %>
187 start_date: <%= Date.today.to_s(:db) %>
187 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
188 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
188 root_id: 10
189 root_id: 10
189 lft: 1
190 lft: 1
190 rgt: 2
191 rgt: 2
191 issues_011:
192 issues_011:
192 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
193 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
193 project_id: 1
194 project_id: 1
194 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
195 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
195 priority_id: 5
196 priority_id: 5
196 subject: Closed issue on a closed version
197 subject: Closed issue on a closed version
197 id: 11
198 id: 11
198 fixed_version_id: 1
199 fixed_version_id: 1
199 category_id: 1
200 category_id: 1
200 description:
201 description:
201 tracker_id: 1
202 tracker_id: 1
202 assigned_to_id:
203 assigned_to_id:
203 author_id: 2
204 author_id: 2
204 status_id: 5
205 status_id: 5
205 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
206 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
206 due_date:
207 due_date:
207 root_id: 11
208 root_id: 11
208 lft: 1
209 lft: 1
209 rgt: 2
210 rgt: 2
210 issues_012:
211 issues_012:
211 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
212 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
212 project_id: 1
213 project_id: 1
213 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
214 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
214 priority_id: 5
215 priority_id: 5
215 subject: Closed issue on a locked version
216 subject: Closed issue on a locked version
216 id: 12
217 id: 12
217 fixed_version_id: 2
218 fixed_version_id: 2
218 category_id: 1
219 category_id: 1
219 description:
220 description:
220 tracker_id: 1
221 tracker_id: 1
221 assigned_to_id:
222 assigned_to_id:
222 author_id: 3
223 author_id: 3
223 status_id: 5
224 status_id: 5
224 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
225 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
225 due_date:
226 due_date:
226 root_id: 12
227 root_id: 12
227 lft: 1
228 lft: 1
228 rgt: 2
229 rgt: 2
229 issues_013:
230 issues_013:
230 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
231 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
231 project_id: 3
232 project_id: 3
232 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
233 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
233 priority_id: 4
234 priority_id: 4
234 subject: Subproject issue two
235 subject: Subproject issue two
235 id: 13
236 id: 13
236 fixed_version_id:
237 fixed_version_id:
237 category_id:
238 category_id:
238 description: This is a second issue on a cookbook subproject
239 description: This is a second issue on a cookbook subproject
239 tracker_id: 1
240 tracker_id: 1
240 assigned_to_id:
241 assigned_to_id:
241 author_id: 2
242 author_id: 2
242 status_id: 1
243 status_id: 1
243 root_id: 13
244 root_id: 13
244 lft: 1
245 lft: 1
245 rgt: 2
246 rgt: 2
@@ -1,729 +1,741
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 IssueTest < ActiveSupport::TestCase
20 class IssueTest < 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 :enabled_modules,
23 :enabled_modules,
24 :versions,
24 :versions,
25 :issue_statuses, :issue_categories, :issue_relations, :workflows,
25 :issue_statuses, :issue_categories, :issue_relations, :workflows,
26 :enumerations,
26 :enumerations,
27 :issues,
27 :issues,
28 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
28 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
29 :time_entries
29 :time_entries
30
30
31 def test_create
31 def test_create
32 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
32 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
33 assert issue.save
33 assert issue.save
34 issue.reload
34 issue.reload
35 assert_equal 1.5, issue.estimated_hours
35 assert_equal 1.5, issue.estimated_hours
36 end
36 end
37
37
38 def test_create_minimal
38 def test_create_minimal
39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
40 assert issue.save
40 assert issue.save
41 assert issue.description.nil?
41 assert issue.description.nil?
42 end
42 end
43
43
44 def test_create_with_required_custom_field
44 def test_create_with_required_custom_field
45 field = IssueCustomField.find_by_name('Database')
45 field = IssueCustomField.find_by_name('Database')
46 field.update_attribute(:is_required, true)
46 field.update_attribute(:is_required, true)
47
47
48 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
48 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
49 assert issue.available_custom_fields.include?(field)
49 assert issue.available_custom_fields.include?(field)
50 # No value for the custom field
50 # No value for the custom field
51 assert !issue.save
51 assert !issue.save
52 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
52 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
53 # Blank value
53 # Blank value
54 issue.custom_field_values = { field.id => '' }
54 issue.custom_field_values = { field.id => '' }
55 assert !issue.save
55 assert !issue.save
56 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
56 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
57 # Invalid value
57 # Invalid value
58 issue.custom_field_values = { field.id => 'SQLServer' }
58 issue.custom_field_values = { field.id => 'SQLServer' }
59 assert !issue.save
59 assert !issue.save
60 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
60 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
61 # Valid value
61 # Valid value
62 issue.custom_field_values = { field.id => 'PostgreSQL' }
62 issue.custom_field_values = { field.id => 'PostgreSQL' }
63 assert issue.save
63 assert issue.save
64 issue.reload
64 issue.reload
65 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
65 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
66 end
66 end
67
67
68 def test_visible_scope_for_anonymous
68 def test_visible_scope_for_anonymous
69 # Anonymous user should see issues of public projects only
69 # Anonymous user should see issues of public projects only
70 issues = Issue.visible(User.anonymous).all
70 issues = Issue.visible(User.anonymous).all
71 assert issues.any?
71 assert issues.any?
72 assert_nil issues.detect {|issue| !issue.project.is_public?}
72 assert_nil issues.detect {|issue| !issue.project.is_public?}
73 # Anonymous user should not see issues without permission
73 # Anonymous user should not see issues without permission
74 Role.anonymous.remove_permission!(:view_issues)
74 Role.anonymous.remove_permission!(:view_issues)
75 issues = Issue.visible(User.anonymous).all
75 issues = Issue.visible(User.anonymous).all
76 assert issues.empty?
76 assert issues.empty?
77 end
77 end
78
78
79 def test_visible_scope_for_user
79 def test_visible_scope_for_user
80 user = User.find(9)
80 user = User.find(9)
81 assert user.projects.empty?
81 assert user.projects.empty?
82 # Non member user should see issues of public projects only
82 # Non member user should see issues of public projects only
83 issues = Issue.visible(user).all
83 issues = Issue.visible(user).all
84 assert issues.any?
84 assert issues.any?
85 assert_nil issues.detect {|issue| !issue.project.is_public?}
85 assert_nil issues.detect {|issue| !issue.project.is_public?}
86 # Non member user should not see issues without permission
86 # Non member user should not see issues without permission
87 Role.non_member.remove_permission!(:view_issues)
87 Role.non_member.remove_permission!(:view_issues)
88 user.reload
88 user.reload
89 issues = Issue.visible(user).all
89 issues = Issue.visible(user).all
90 assert issues.empty?
90 assert issues.empty?
91 # User should see issues of projects for which he has view_issues permissions only
91 # User should see issues of projects for which he has view_issues permissions only
92 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
92 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
93 user.reload
93 user.reload
94 issues = Issue.visible(user).all
94 issues = Issue.visible(user).all
95 assert issues.any?
95 assert issues.any?
96 assert_nil issues.detect {|issue| issue.project_id != 2}
96 assert_nil issues.detect {|issue| issue.project_id != 2}
97 end
97 end
98
98
99 def test_visible_scope_for_admin
99 def test_visible_scope_for_admin
100 user = User.find(1)
100 user = User.find(1)
101 user.members.each(&:destroy)
101 user.members.each(&:destroy)
102 assert user.projects.empty?
102 assert user.projects.empty?
103 issues = Issue.visible(user).all
103 issues = Issue.visible(user).all
104 assert issues.any?
104 assert issues.any?
105 # Admin should see issues on private projects that he does not belong to
105 # Admin should see issues on private projects that he does not belong to
106 assert issues.detect {|issue| !issue.project.is_public?}
106 assert issues.detect {|issue| !issue.project.is_public?}
107 end
107 end
108
108
109 def test_errors_full_messages_should_include_custom_fields_errors
109 def test_errors_full_messages_should_include_custom_fields_errors
110 field = IssueCustomField.find_by_name('Database')
110 field = IssueCustomField.find_by_name('Database')
111
111
112 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
112 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
113 assert issue.available_custom_fields.include?(field)
113 assert issue.available_custom_fields.include?(field)
114 # Invalid value
114 # Invalid value
115 issue.custom_field_values = { field.id => 'SQLServer' }
115 issue.custom_field_values = { field.id => 'SQLServer' }
116
116
117 assert !issue.valid?
117 assert !issue.valid?
118 assert_equal 1, issue.errors.full_messages.size
118 assert_equal 1, issue.errors.full_messages.size
119 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
119 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
120 end
120 end
121
121
122 def test_update_issue_with_required_custom_field
122 def test_update_issue_with_required_custom_field
123 field = IssueCustomField.find_by_name('Database')
123 field = IssueCustomField.find_by_name('Database')
124 field.update_attribute(:is_required, true)
124 field.update_attribute(:is_required, true)
125
125
126 issue = Issue.find(1)
126 issue = Issue.find(1)
127 assert_nil issue.custom_value_for(field)
127 assert_nil issue.custom_value_for(field)
128 assert issue.available_custom_fields.include?(field)
128 assert issue.available_custom_fields.include?(field)
129 # No change to custom values, issue can be saved
129 # No change to custom values, issue can be saved
130 assert issue.save
130 assert issue.save
131 # Blank value
131 # Blank value
132 issue.custom_field_values = { field.id => '' }
132 issue.custom_field_values = { field.id => '' }
133 assert !issue.save
133 assert !issue.save
134 # Valid value
134 # Valid value
135 issue.custom_field_values = { field.id => 'PostgreSQL' }
135 issue.custom_field_values = { field.id => 'PostgreSQL' }
136 assert issue.save
136 assert issue.save
137 issue.reload
137 issue.reload
138 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
138 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
139 end
139 end
140
140
141 def test_should_not_update_attributes_if_custom_fields_validation_fails
141 def test_should_not_update_attributes_if_custom_fields_validation_fails
142 issue = Issue.find(1)
142 issue = Issue.find(1)
143 field = IssueCustomField.find_by_name('Database')
143 field = IssueCustomField.find_by_name('Database')
144 assert issue.available_custom_fields.include?(field)
144 assert issue.available_custom_fields.include?(field)
145
145
146 issue.custom_field_values = { field.id => 'Invalid' }
146 issue.custom_field_values = { field.id => 'Invalid' }
147 issue.subject = 'Should be not be saved'
147 issue.subject = 'Should be not be saved'
148 assert !issue.save
148 assert !issue.save
149
149
150 issue.reload
150 issue.reload
151 assert_equal "Can't print recipes", issue.subject
151 assert_equal "Can't print recipes", issue.subject
152 end
152 end
153
153
154 def test_should_not_recreate_custom_values_objects_on_update
154 def test_should_not_recreate_custom_values_objects_on_update
155 field = IssueCustomField.find_by_name('Database')
155 field = IssueCustomField.find_by_name('Database')
156
156
157 issue = Issue.find(1)
157 issue = Issue.find(1)
158 issue.custom_field_values = { field.id => 'PostgreSQL' }
158 issue.custom_field_values = { field.id => 'PostgreSQL' }
159 assert issue.save
159 assert issue.save
160 custom_value = issue.custom_value_for(field)
160 custom_value = issue.custom_value_for(field)
161 issue.reload
161 issue.reload
162 issue.custom_field_values = { field.id => 'MySQL' }
162 issue.custom_field_values = { field.id => 'MySQL' }
163 assert issue.save
163 assert issue.save
164 issue.reload
164 issue.reload
165 assert_equal custom_value.id, issue.custom_value_for(field).id
165 assert_equal custom_value.id, issue.custom_value_for(field).id
166 end
166 end
167
167
168 def test_assigning_tracker_id_should_reload_custom_fields_values
168 def test_assigning_tracker_id_should_reload_custom_fields_values
169 issue = Issue.new(:project => Project.find(1))
169 issue = Issue.new(:project => Project.find(1))
170 assert issue.custom_field_values.empty?
170 assert issue.custom_field_values.empty?
171 issue.tracker_id = 1
171 issue.tracker_id = 1
172 assert issue.custom_field_values.any?
172 assert issue.custom_field_values.any?
173 end
173 end
174
174
175 def test_assigning_attributes_should_assign_tracker_id_first
175 def test_assigning_attributes_should_assign_tracker_id_first
176 attributes = ActiveSupport::OrderedHash.new
176 attributes = ActiveSupport::OrderedHash.new
177 attributes['custom_field_values'] = { '1' => 'MySQL' }
177 attributes['custom_field_values'] = { '1' => 'MySQL' }
178 attributes['tracker_id'] = '1'
178 attributes['tracker_id'] = '1'
179 issue = Issue.new(:project => Project.find(1))
179 issue = Issue.new(:project => Project.find(1))
180 issue.attributes = attributes
180 issue.attributes = attributes
181 assert_not_nil issue.custom_value_for(1)
181 assert_not_nil issue.custom_value_for(1)
182 assert_equal 'MySQL', issue.custom_value_for(1).value
182 assert_equal 'MySQL', issue.custom_value_for(1).value
183 end
183 end
184
184
185 def test_should_update_issue_with_disabled_tracker
185 def test_should_update_issue_with_disabled_tracker
186 p = Project.find(1)
186 p = Project.find(1)
187 issue = Issue.find(1)
187 issue = Issue.find(1)
188
188
189 p.trackers.delete(issue.tracker)
189 p.trackers.delete(issue.tracker)
190 assert !p.trackers.include?(issue.tracker)
190 assert !p.trackers.include?(issue.tracker)
191
191
192 issue.reload
192 issue.reload
193 issue.subject = 'New subject'
193 issue.subject = 'New subject'
194 assert issue.save
194 assert issue.save
195 end
195 end
196
196
197 def test_should_not_set_a_disabled_tracker
197 def test_should_not_set_a_disabled_tracker
198 p = Project.find(1)
198 p = Project.find(1)
199 p.trackers.delete(Tracker.find(2))
199 p.trackers.delete(Tracker.find(2))
200
200
201 issue = Issue.find(1)
201 issue = Issue.find(1)
202 issue.tracker_id = 2
202 issue.tracker_id = 2
203 issue.subject = 'New subject'
203 issue.subject = 'New subject'
204 assert !issue.save
204 assert !issue.save
205 assert_not_nil issue.errors.on(:tracker_id)
205 assert_not_nil issue.errors.on(:tracker_id)
206 end
206 end
207
207
208 def test_category_based_assignment
208 def test_category_based_assignment
209 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
209 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
210 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
210 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
211 end
211 end
212
212
213 def test_copy
213 def test_copy
214 issue = Issue.new.copy_from(1)
214 issue = Issue.new.copy_from(1)
215 assert issue.save
215 assert issue.save
216 issue.reload
216 issue.reload
217 orig = Issue.find(1)
217 orig = Issue.find(1)
218 assert_equal orig.subject, issue.subject
218 assert_equal orig.subject, issue.subject
219 assert_equal orig.tracker, issue.tracker
219 assert_equal orig.tracker, issue.tracker
220 assert_equal "125", issue.custom_value_for(2).value
220 assert_equal "125", issue.custom_value_for(2).value
221 end
221 end
222
222
223 def test_copy_should_copy_status
223 def test_copy_should_copy_status
224 orig = Issue.find(8)
224 orig = Issue.find(8)
225 assert orig.status != IssueStatus.default
225 assert orig.status != IssueStatus.default
226
226
227 issue = Issue.new.copy_from(orig)
227 issue = Issue.new.copy_from(orig)
228 assert issue.save
228 assert issue.save
229 issue.reload
229 issue.reload
230 assert_equal orig.status, issue.status
230 assert_equal orig.status, issue.status
231 end
231 end
232
232
233 def test_should_close_duplicates
233 def test_should_close_duplicates
234 # Create 3 issues
234 # Create 3 issues
235 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
235 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
236 assert issue1.save
236 assert issue1.save
237 issue2 = issue1.clone
237 issue2 = issue1.clone
238 assert issue2.save
238 assert issue2.save
239 issue3 = issue1.clone
239 issue3 = issue1.clone
240 assert issue3.save
240 assert issue3.save
241
241
242 # 2 is a dupe of 1
242 # 2 is a dupe of 1
243 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
243 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
244 # And 3 is a dupe of 2
244 # And 3 is a dupe of 2
245 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
245 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
246 # And 3 is a dupe of 1 (circular duplicates)
246 # And 3 is a dupe of 1 (circular duplicates)
247 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
247 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
248
248
249 assert issue1.reload.duplicates.include?(issue2)
249 assert issue1.reload.duplicates.include?(issue2)
250
250
251 # Closing issue 1
251 # Closing issue 1
252 issue1.init_journal(User.find(:first), "Closing issue1")
252 issue1.init_journal(User.find(:first), "Closing issue1")
253 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
253 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
254 assert issue1.save
254 assert issue1.save
255 # 2 and 3 should be also closed
255 # 2 and 3 should be also closed
256 assert issue2.reload.closed?
256 assert issue2.reload.closed?
257 assert issue3.reload.closed?
257 assert issue3.reload.closed?
258 end
258 end
259
259
260 def test_should_not_close_duplicated_issue
260 def test_should_not_close_duplicated_issue
261 # Create 3 issues
261 # Create 3 issues
262 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
262 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
263 assert issue1.save
263 assert issue1.save
264 issue2 = issue1.clone
264 issue2 = issue1.clone
265 assert issue2.save
265 assert issue2.save
266
266
267 # 2 is a dupe of 1
267 # 2 is a dupe of 1
268 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
268 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
269 # 2 is a dup of 1 but 1 is not a duplicate of 2
269 # 2 is a dup of 1 but 1 is not a duplicate of 2
270 assert !issue2.reload.duplicates.include?(issue1)
270 assert !issue2.reload.duplicates.include?(issue1)
271
271
272 # Closing issue 2
272 # Closing issue 2
273 issue2.init_journal(User.find(:first), "Closing issue2")
273 issue2.init_journal(User.find(:first), "Closing issue2")
274 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
274 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
275 assert issue2.save
275 assert issue2.save
276 # 1 should not be also closed
276 # 1 should not be also closed
277 assert !issue1.reload.closed?
277 assert !issue1.reload.closed?
278 end
278 end
279
279
280 def test_assignable_versions
280 def test_assignable_versions
281 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
281 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
282 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
282 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
283 end
283 end
284
284
285 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
285 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
286 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
286 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
287 assert !issue.save
287 assert !issue.save
288 assert_not_nil issue.errors.on(:fixed_version_id)
288 assert_not_nil issue.errors.on(:fixed_version_id)
289 end
289 end
290
290
291 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
291 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
292 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
292 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
293 assert !issue.save
293 assert !issue.save
294 assert_not_nil issue.errors.on(:fixed_version_id)
294 assert_not_nil issue.errors.on(:fixed_version_id)
295 end
295 end
296
296
297 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
297 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
298 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
298 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
299 assert issue.save
299 assert issue.save
300 end
300 end
301
301
302 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
302 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
303 issue = Issue.find(11)
303 issue = Issue.find(11)
304 assert_equal 'closed', issue.fixed_version.status
304 assert_equal 'closed', issue.fixed_version.status
305 issue.subject = 'Subject changed'
305 issue.subject = 'Subject changed'
306 assert issue.save
306 assert issue.save
307 end
307 end
308
308
309 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
309 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
310 issue = Issue.find(11)
310 issue = Issue.find(11)
311 issue.status_id = 1
311 issue.status_id = 1
312 assert !issue.save
312 assert !issue.save
313 assert_not_nil issue.errors.on_base
313 assert_not_nil issue.errors.on_base
314 end
314 end
315
315
316 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
316 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
317 issue = Issue.find(11)
317 issue = Issue.find(11)
318 issue.status_id = 1
318 issue.status_id = 1
319 issue.fixed_version_id = 3
319 issue.fixed_version_id = 3
320 assert issue.save
320 assert issue.save
321 end
321 end
322
322
323 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
323 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
324 issue = Issue.find(12)
324 issue = Issue.find(12)
325 assert_equal 'locked', issue.fixed_version.status
325 assert_equal 'locked', issue.fixed_version.status
326 issue.status_id = 1
326 issue.status_id = 1
327 assert issue.save
327 assert issue.save
328 end
328 end
329
329
330 def test_move_to_another_project_with_same_category
330 def test_move_to_another_project_with_same_category
331 issue = Issue.find(1)
331 issue = Issue.find(1)
332 assert issue.move_to_project(Project.find(2))
332 assert issue.move_to_project(Project.find(2))
333 issue.reload
333 issue.reload
334 assert_equal 2, issue.project_id
334 assert_equal 2, issue.project_id
335 # Category changes
335 # Category changes
336 assert_equal 4, issue.category_id
336 assert_equal 4, issue.category_id
337 # Make sure time entries were move to the target project
337 # Make sure time entries were move to the target project
338 assert_equal 2, issue.time_entries.first.project_id
338 assert_equal 2, issue.time_entries.first.project_id
339 end
339 end
340
340
341 def test_move_to_another_project_without_same_category
341 def test_move_to_another_project_without_same_category
342 issue = Issue.find(2)
342 issue = Issue.find(2)
343 assert issue.move_to_project(Project.find(2))
343 assert issue.move_to_project(Project.find(2))
344 issue.reload
344 issue.reload
345 assert_equal 2, issue.project_id
345 assert_equal 2, issue.project_id
346 # Category cleared
346 # Category cleared
347 assert_nil issue.category_id
347 assert_nil issue.category_id
348 end
348 end
349
349
350 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
350 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
351 issue = Issue.find(1)
351 issue = Issue.find(1)
352 issue.update_attribute(:fixed_version_id, 1)
352 issue.update_attribute(:fixed_version_id, 1)
353 assert issue.move_to_project(Project.find(2))
353 assert issue.move_to_project(Project.find(2))
354 issue.reload
354 issue.reload
355 assert_equal 2, issue.project_id
355 assert_equal 2, issue.project_id
356 # Cleared fixed_version
356 # Cleared fixed_version
357 assert_equal nil, issue.fixed_version
357 assert_equal nil, issue.fixed_version
358 end
358 end
359
359
360 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
360 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
361 issue = Issue.find(1)
361 issue = Issue.find(1)
362 issue.update_attribute(:fixed_version_id, 4)
362 issue.update_attribute(:fixed_version_id, 4)
363 assert issue.move_to_project(Project.find(5))
363 assert issue.move_to_project(Project.find(5))
364 issue.reload
364 issue.reload
365 assert_equal 5, issue.project_id
365 assert_equal 5, issue.project_id
366 # Keep fixed_version
366 # Keep fixed_version
367 assert_equal 4, issue.fixed_version_id
367 assert_equal 4, issue.fixed_version_id
368 end
368 end
369
369
370 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
370 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
371 issue = Issue.find(1)
371 issue = Issue.find(1)
372 issue.update_attribute(:fixed_version_id, 1)
372 issue.update_attribute(:fixed_version_id, 1)
373 assert issue.move_to_project(Project.find(5))
373 assert issue.move_to_project(Project.find(5))
374 issue.reload
374 issue.reload
375 assert_equal 5, issue.project_id
375 assert_equal 5, issue.project_id
376 # Cleared fixed_version
376 # Cleared fixed_version
377 assert_equal nil, issue.fixed_version
377 assert_equal nil, issue.fixed_version
378 end
378 end
379
379
380 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
380 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
381 issue = Issue.find(1)
381 issue = Issue.find(1)
382 issue.update_attribute(:fixed_version_id, 7)
382 issue.update_attribute(:fixed_version_id, 7)
383 assert issue.move_to_project(Project.find(2))
383 assert issue.move_to_project(Project.find(2))
384 issue.reload
384 issue.reload
385 assert_equal 2, issue.project_id
385 assert_equal 2, issue.project_id
386 # Keep fixed_version
386 # Keep fixed_version
387 assert_equal 7, issue.fixed_version_id
387 assert_equal 7, issue.fixed_version_id
388 end
388 end
389
389
390 def test_move_to_another_project_with_disabled_tracker
390 def test_move_to_another_project_with_disabled_tracker
391 issue = Issue.find(1)
391 issue = Issue.find(1)
392 target = Project.find(2)
392 target = Project.find(2)
393 target.tracker_ids = [3]
393 target.tracker_ids = [3]
394 target.save
394 target.save
395 assert_equal false, issue.move_to_project(target)
395 assert_equal false, issue.move_to_project(target)
396 issue.reload
396 issue.reload
397 assert_equal 1, issue.project_id
397 assert_equal 1, issue.project_id
398 end
398 end
399
399
400 def test_copy_to_the_same_project
400 def test_copy_to_the_same_project
401 issue = Issue.find(1)
401 issue = Issue.find(1)
402 copy = nil
402 copy = nil
403 assert_difference 'Issue.count' do
403 assert_difference 'Issue.count' do
404 copy = issue.move_to_project(issue.project, nil, :copy => true)
404 copy = issue.move_to_project(issue.project, nil, :copy => true)
405 end
405 end
406 assert_kind_of Issue, copy
406 assert_kind_of Issue, copy
407 assert_equal issue.project, copy.project
407 assert_equal issue.project, copy.project
408 assert_equal "125", copy.custom_value_for(2).value
408 assert_equal "125", copy.custom_value_for(2).value
409 end
409 end
410
410
411 def test_copy_to_another_project_and_tracker
411 def test_copy_to_another_project_and_tracker
412 issue = Issue.find(1)
412 issue = Issue.find(1)
413 copy = nil
413 copy = nil
414 assert_difference 'Issue.count' do
414 assert_difference 'Issue.count' do
415 copy = issue.move_to_project(Project.find(3), Tracker.find(2), :copy => true)
415 copy = issue.move_to_project(Project.find(3), Tracker.find(2), :copy => true)
416 end
416 end
417 copy.reload
417 copy.reload
418 assert_kind_of Issue, copy
418 assert_kind_of Issue, copy
419 assert_equal Project.find(3), copy.project
419 assert_equal Project.find(3), copy.project
420 assert_equal Tracker.find(2), copy.tracker
420 assert_equal Tracker.find(2), copy.tracker
421 # Custom field #2 is not associated with target tracker
421 # Custom field #2 is not associated with target tracker
422 assert_nil copy.custom_value_for(2)
422 assert_nil copy.custom_value_for(2)
423 end
423 end
424
424
425 context "#move_to_project" do
425 context "#move_to_project" do
426 context "as a copy" do
426 context "as a copy" do
427 setup do
427 setup do
428 @issue = Issue.find(1)
428 @issue = Issue.find(1)
429 @copy = nil
429 @copy = nil
430 end
430 end
431
431
432 should "allow assigned_to changes" do
432 should "allow assigned_to changes" do
433 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
433 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
434 assert_equal 3, @copy.assigned_to_id
434 assert_equal 3, @copy.assigned_to_id
435 end
435 end
436
436
437 should "allow status changes" do
437 should "allow status changes" do
438 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
438 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
439 assert_equal 2, @copy.status_id
439 assert_equal 2, @copy.status_id
440 end
440 end
441
441
442 should "allow start date changes" do
442 should "allow start date changes" do
443 date = Date.today
443 date = Date.today
444 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
444 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
445 assert_equal date, @copy.start_date
445 assert_equal date, @copy.start_date
446 end
446 end
447
447
448 should "allow due date changes" do
448 should "allow due date changes" do
449 date = Date.today
449 date = Date.today
450 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
450 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
451
451
452 assert_equal date, @copy.due_date
452 assert_equal date, @copy.due_date
453 end
453 end
454 end
454 end
455 end
455 end
456
456
457 def test_recipients_should_not_include_users_that_cannot_view_the_issue
457 def test_recipients_should_not_include_users_that_cannot_view_the_issue
458 issue = Issue.find(12)
458 issue = Issue.find(12)
459 assert issue.recipients.include?(issue.author.mail)
459 assert issue.recipients.include?(issue.author.mail)
460 # move the issue to a private project
460 # move the issue to a private project
461 copy = issue.move_to_project(Project.find(5), Tracker.find(2), :copy => true)
461 copy = issue.move_to_project(Project.find(5), Tracker.find(2), :copy => true)
462 # author is not a member of project anymore
462 # author is not a member of project anymore
463 assert !copy.recipients.include?(copy.author.mail)
463 assert !copy.recipients.include?(copy.author.mail)
464 end
464 end
465
465
466 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
466 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
467 user = User.find(3)
467 user = User.find(3)
468 issue = Issue.find(9)
468 issue = Issue.find(9)
469 Watcher.create!(:user => user, :watchable => issue)
469 Watcher.create!(:user => user, :watchable => issue)
470 assert issue.watched_by?(user)
470 assert issue.watched_by?(user)
471 assert !issue.watcher_recipients.include?(user.mail)
471 assert !issue.watcher_recipients.include?(user.mail)
472 end
472 end
473
473
474 def test_issue_destroy
474 def test_issue_destroy
475 Issue.find(1).destroy
475 Issue.find(1).destroy
476 assert_nil Issue.find_by_id(1)
476 assert_nil Issue.find_by_id(1)
477 assert_nil TimeEntry.find_by_issue_id(1)
477 assert_nil TimeEntry.find_by_issue_id(1)
478 end
478 end
479
479
480 def test_blocked
480 def test_blocked
481 blocked_issue = Issue.find(9)
481 blocked_issue = Issue.find(9)
482 blocking_issue = Issue.find(10)
482 blocking_issue = Issue.find(10)
483
483
484 assert blocked_issue.blocked?
484 assert blocked_issue.blocked?
485 assert !blocking_issue.blocked?
485 assert !blocking_issue.blocked?
486 end
486 end
487
487
488 def test_blocked_issues_dont_allow_closed_statuses
488 def test_blocked_issues_dont_allow_closed_statuses
489 blocked_issue = Issue.find(9)
489 blocked_issue = Issue.find(9)
490
490
491 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
491 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
492 assert !allowed_statuses.empty?
492 assert !allowed_statuses.empty?
493 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
493 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
494 assert closed_statuses.empty?
494 assert closed_statuses.empty?
495 end
495 end
496
496
497 def test_unblocked_issues_allow_closed_statuses
497 def test_unblocked_issues_allow_closed_statuses
498 blocking_issue = Issue.find(10)
498 blocking_issue = Issue.find(10)
499
499
500 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
500 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
501 assert !allowed_statuses.empty?
501 assert !allowed_statuses.empty?
502 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
502 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
503 assert !closed_statuses.empty?
503 assert !closed_statuses.empty?
504 end
504 end
505
505
506 def test_overdue
506 def test_overdue
507 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
507 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
508 assert !Issue.new(:due_date => Date.today).overdue?
508 assert !Issue.new(:due_date => Date.today).overdue?
509 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
509 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
510 assert !Issue.new(:due_date => nil).overdue?
510 assert !Issue.new(:due_date => nil).overdue?
511 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
511 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
512 end
512 end
513
513
514 context "#behind_schedule?" do
514 context "#behind_schedule?" do
515 should "be false if the issue has no start_date" do
515 should "be false if the issue has no start_date" do
516 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
516 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
517 end
517 end
518
518
519 should "be false if the issue has no end_date" do
519 should "be false if the issue has no end_date" do
520 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
520 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
521 end
521 end
522
522
523 should "be false if the issue has more done than it's calendar time" do
523 should "be false if the issue has more done than it's calendar time" do
524 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
524 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
525 end
525 end
526
526
527 should "be true if the issue hasn't been started at all" do
527 should "be true if the issue hasn't been started at all" do
528 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
528 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
529 end
529 end
530
530
531 should "be true if the issue has used more calendar time than it's done ratio" do
531 should "be true if the issue has used more calendar time than it's done ratio" do
532 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
532 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
533 end
533 end
534 end
534 end
535
535
536 def test_assignable_users
536 def test_assignable_users
537 assert_kind_of User, Issue.find(1).assignable_users.first
537 assert_kind_of User, Issue.find(1).assignable_users.first
538 end
538 end
539
539
540 def test_create_should_send_email_notification
540 def test_create_should_send_email_notification
541 ActionMailer::Base.deliveries.clear
541 ActionMailer::Base.deliveries.clear
542 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30')
542 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30')
543
543
544 assert issue.save
544 assert issue.save
545 assert_equal 1, ActionMailer::Base.deliveries.size
545 assert_equal 1, ActionMailer::Base.deliveries.size
546 end
546 end
547
547
548 def test_stale_issue_should_not_send_email_notification
548 def test_stale_issue_should_not_send_email_notification
549 ActionMailer::Base.deliveries.clear
549 ActionMailer::Base.deliveries.clear
550 issue = Issue.find(1)
550 issue = Issue.find(1)
551 stale = Issue.find(1)
551 stale = Issue.find(1)
552
552
553 issue.init_journal(User.find(1))
553 issue.init_journal(User.find(1))
554 issue.subject = 'Subjet update'
554 issue.subject = 'Subjet update'
555 assert issue.save
555 assert issue.save
556 assert_equal 1, ActionMailer::Base.deliveries.size
556 assert_equal 1, ActionMailer::Base.deliveries.size
557 ActionMailer::Base.deliveries.clear
557 ActionMailer::Base.deliveries.clear
558
558
559 stale.init_journal(User.find(1))
559 stale.init_journal(User.find(1))
560 stale.subject = 'Another subjet update'
560 stale.subject = 'Another subjet update'
561 assert_raise ActiveRecord::StaleObjectError do
561 assert_raise ActiveRecord::StaleObjectError do
562 stale.save
562 stale.save
563 end
563 end
564 assert ActionMailer::Base.deliveries.empty?
564 assert ActionMailer::Base.deliveries.empty?
565 end
565 end
566
566
567 def test_saving_twice_should_not_duplicate_journal_details
567 def test_saving_twice_should_not_duplicate_journal_details
568 i = Issue.find(:first)
568 i = Issue.find(:first)
569 i.init_journal(User.find(2), 'Some notes')
569 i.init_journal(User.find(2), 'Some notes')
570 # initial changes
570 # initial changes
571 i.subject = 'New subject'
571 i.subject = 'New subject'
572 i.done_ratio = i.done_ratio + 10
572 i.done_ratio = i.done_ratio + 10
573 assert_difference 'Journal.count' do
573 assert_difference 'Journal.count' do
574 assert i.save
574 assert i.save
575 end
575 end
576 # 1 more change
576 # 1 more change
577 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
577 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
578 assert_no_difference 'Journal.count' do
578 assert_no_difference 'Journal.count' do
579 assert_difference 'JournalDetail.count', 1 do
579 assert_difference 'JournalDetail.count', 1 do
580 i.save
580 i.save
581 end
581 end
582 end
582 end
583 # no more change
583 # no more change
584 assert_no_difference 'Journal.count' do
584 assert_no_difference 'Journal.count' do
585 assert_no_difference 'JournalDetail.count' do
585 assert_no_difference 'JournalDetail.count' do
586 i.save
586 i.save
587 end
587 end
588 end
588 end
589 end
589 end
590
590
591 context "#done_ratio" do
591 context "#done_ratio" do
592 setup do
592 setup do
593 @issue = Issue.find(1)
593 @issue = Issue.find(1)
594 @issue_status = IssueStatus.find(1)
594 @issue_status = IssueStatus.find(1)
595 @issue_status.update_attribute(:default_done_ratio, 50)
595 @issue_status.update_attribute(:default_done_ratio, 50)
596 @issue2 = Issue.find(2)
597 @issue_status2 = IssueStatus.find(2)
598 @issue_status2.update_attribute(:default_done_ratio, 0)
596 end
599 end
597
600
598 context "with Setting.issue_done_ratio using the issue_field" do
601 context "with Setting.issue_done_ratio using the issue_field" do
599 setup do
602 setup do
600 Setting.issue_done_ratio = 'issue_field'
603 Setting.issue_done_ratio = 'issue_field'
601 end
604 end
602
605
603 should "read the issue's field" do
606 should "read the issue's field" do
604 assert_equal 0, @issue.done_ratio
607 assert_equal 0, @issue.done_ratio
608 assert_equal 30, @issue2.done_ratio
605 end
609 end
606 end
610 end
607
611
608 context "with Setting.issue_done_ratio using the issue_status" do
612 context "with Setting.issue_done_ratio using the issue_status" do
609 setup do
613 setup do
610 Setting.issue_done_ratio = 'issue_status'
614 Setting.issue_done_ratio = 'issue_status'
611 end
615 end
612
616
613 should "read the Issue Status's default done ratio" do
617 should "read the Issue Status's default done ratio" do
614 assert_equal 50, @issue.done_ratio
618 assert_equal 50, @issue.done_ratio
619 assert_equal 0, @issue2.done_ratio
615 end
620 end
616 end
621 end
617 end
622 end
618
623
619 context "#update_done_ratio_from_issue_status" do
624 context "#update_done_ratio_from_issue_status" do
620 setup do
625 setup do
621 @issue = Issue.find(1)
626 @issue = Issue.find(1)
622 @issue_status = IssueStatus.find(1)
627 @issue_status = IssueStatus.find(1)
623 @issue_status.update_attribute(:default_done_ratio, 50)
628 @issue_status.update_attribute(:default_done_ratio, 50)
629 @issue2 = Issue.find(2)
630 @issue_status2 = IssueStatus.find(2)
631 @issue_status2.update_attribute(:default_done_ratio, 0)
624 end
632 end
625
633
626 context "with Setting.issue_done_ratio using the issue_field" do
634 context "with Setting.issue_done_ratio using the issue_field" do
627 setup do
635 setup do
628 Setting.issue_done_ratio = 'issue_field'
636 Setting.issue_done_ratio = 'issue_field'
629 end
637 end
630
638
631 should "not change the issue" do
639 should "not change the issue" do
632 @issue.update_done_ratio_from_issue_status
640 @issue.update_done_ratio_from_issue_status
641 @issue2.update_done_ratio_from_issue_status
633
642
634 assert_equal 0, @issue.done_ratio
643 assert_equal 0, @issue.read_attribute(:done_ratio)
644 assert_equal 30, @issue2.read_attribute(:done_ratio)
635 end
645 end
636 end
646 end
637
647
638 context "with Setting.issue_done_ratio using the issue_status" do
648 context "with Setting.issue_done_ratio using the issue_status" do
639 setup do
649 setup do
640 Setting.issue_done_ratio = 'issue_status'
650 Setting.issue_done_ratio = 'issue_status'
641 end
651 end
642
652
643 should "not change the issue's done ratio" do
653 should "change the issue's done ratio" do
644 @issue.update_done_ratio_from_issue_status
654 @issue.update_done_ratio_from_issue_status
655 @issue2.update_done_ratio_from_issue_status
645
656
646 assert_equal 50, @issue.done_ratio
657 assert_equal 50, @issue.read_attribute(:done_ratio)
658 assert_equal 0, @issue2.read_attribute(:done_ratio)
647 end
659 end
648 end
660 end
649 end
661 end
650
662
651 test "#by_tracker" do
663 test "#by_tracker" do
652 groups = Issue.by_tracker(Project.find(1))
664 groups = Issue.by_tracker(Project.find(1))
653 assert_equal 3, groups.size
665 assert_equal 3, groups.size
654 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
666 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
655 end
667 end
656
668
657 test "#by_version" do
669 test "#by_version" do
658 groups = Issue.by_version(Project.find(1))
670 groups = Issue.by_version(Project.find(1))
659 assert_equal 3, groups.size
671 assert_equal 3, groups.size
660 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
672 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
661 end
673 end
662
674
663 test "#by_priority" do
675 test "#by_priority" do
664 groups = Issue.by_priority(Project.find(1))
676 groups = Issue.by_priority(Project.find(1))
665 assert_equal 4, groups.size
677 assert_equal 4, groups.size
666 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
678 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
667 end
679 end
668
680
669 test "#by_category" do
681 test "#by_category" do
670 groups = Issue.by_category(Project.find(1))
682 groups = Issue.by_category(Project.find(1))
671 assert_equal 2, groups.size
683 assert_equal 2, groups.size
672 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
684 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
673 end
685 end
674
686
675 test "#by_assigned_to" do
687 test "#by_assigned_to" do
676 groups = Issue.by_assigned_to(Project.find(1))
688 groups = Issue.by_assigned_to(Project.find(1))
677 assert_equal 2, groups.size
689 assert_equal 2, groups.size
678 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
690 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
679 end
691 end
680
692
681 test "#by_author" do
693 test "#by_author" do
682 groups = Issue.by_author(Project.find(1))
694 groups = Issue.by_author(Project.find(1))
683 assert_equal 4, groups.size
695 assert_equal 4, groups.size
684 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
696 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
685 end
697 end
686
698
687 test "#by_subproject" do
699 test "#by_subproject" do
688 groups = Issue.by_subproject(Project.find(1))
700 groups = Issue.by_subproject(Project.find(1))
689 assert_equal 2, groups.size
701 assert_equal 2, groups.size
690 assert_equal 5, groups.inject(0) {|sum, group| sum + group['total'].to_i}
702 assert_equal 5, groups.inject(0) {|sum, group| sum + group['total'].to_i}
691 end
703 end
692
704
693
705
694 context ".allowed_target_projects_on_move" do
706 context ".allowed_target_projects_on_move" do
695 should "return all active projects for admin users" do
707 should "return all active projects for admin users" do
696 User.current = User.find(1)
708 User.current = User.find(1)
697 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
709 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
698 end
710 end
699
711
700 should "return allowed projects for non admin users" do
712 should "return allowed projects for non admin users" do
701 User.current = User.find(2)
713 User.current = User.find(2)
702 Role.non_member.remove_permission! :move_issues
714 Role.non_member.remove_permission! :move_issues
703 assert_equal 3, Issue.allowed_target_projects_on_move.size
715 assert_equal 3, Issue.allowed_target_projects_on_move.size
704
716
705 Role.non_member.add_permission! :move_issues
717 Role.non_member.add_permission! :move_issues
706 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
718 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
707 end
719 end
708 end
720 end
709
721
710 def test_recently_updated_with_limit_scopes
722 def test_recently_updated_with_limit_scopes
711 #should return the last updated issue
723 #should return the last updated issue
712 assert_equal 1, Issue.recently_updated.with_limit(1).length
724 assert_equal 1, Issue.recently_updated.with_limit(1).length
713 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
725 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
714 end
726 end
715
727
716 def test_on_active_projects_scope
728 def test_on_active_projects_scope
717 assert Project.find(2).archive
729 assert Project.find(2).archive
718
730
719 before = Issue.on_active_project.length
731 before = Issue.on_active_project.length
720 # test inclusion to results
732 # test inclusion to results
721 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
733 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
722 assert_equal before + 1, Issue.on_active_project.length
734 assert_equal before + 1, Issue.on_active_project.length
723
735
724 # Move to an archived project
736 # Move to an archived project
725 issue.project = Project.find(2)
737 issue.project = Project.find(2)
726 assert issue.save
738 assert issue.save
727 assert_equal before, Issue.on_active_project.length
739 assert_equal before, Issue.on_active_project.length
728 end
740 end
729 end
741 end
General Comments 0
You need to be logged in to leave comments. Login now