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