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