##// END OF EJS Templates
Fixed: journal details duplicated when an issue is saved twice (#3690)....
Jean-Philippe Lang -
r3385:02cc0efdd7b9
parent child
Show More
@@ -1,557 +1,577
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 belongs_to :project
19 belongs_to :project
20 belongs_to :tracker
20 belongs_to :tracker
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27
27
28 has_many :journals, :as => :journalized, :dependent => :destroy
28 has_many :journals, :as => :journalized, :dependent => :destroy
29 has_many :time_entries, :dependent => :delete_all
29 has_many :time_entries, :dependent => :delete_all
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
31
31
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
34
34
35 acts_as_attachable :after_remove => :attachment_removed
35 acts_as_attachable :after_remove => :attachment_removed
36 acts_as_customizable
36 acts_as_customizable
37 acts_as_watchable
37 acts_as_watchable
38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 :include => [:project, :journals],
39 :include => [:project, :journals],
40 # sort by id so that limited eager loading doesn't break with postgresql
40 # sort by id so that limited eager loading doesn't break with postgresql
41 :order_column => "#{table_name}.id"
41 :order_column => "#{table_name}.id"
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
44 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
44 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
45
45
46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
47 :author_key => :author_id
47 :author_key => :author_id
48
48
49 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
49 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
50
50
51 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
51 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
52 validates_length_of :subject, :maximum => 255
52 validates_length_of :subject, :maximum => 255
53 validates_inclusion_of :done_ratio, :in => 0..100
53 validates_inclusion_of :done_ratio, :in => 0..100
54 validates_numericality_of :estimated_hours, :allow_nil => true
54 validates_numericality_of :estimated_hours, :allow_nil => true
55
55
56 named_scope :visible, lambda {|*args| { :include => :project,
56 named_scope :visible, lambda {|*args| { :include => :project,
57 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
57 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
58
58
59 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
59 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
60
60
61 before_save :update_done_ratio_from_issue_status
61 before_create :default_assign
62 before_save :reschedule_following_issues, :close_duplicates, :update_done_ratio_from_issue_status
62 after_save :create_journal
63 after_save :create_journal
63
64
64 # Returns true if usr or current user is allowed to view the issue
65 # Returns true if usr or current user is allowed to view the issue
65 def visible?(usr=nil)
66 def visible?(usr=nil)
66 (usr || User.current).allowed_to?(:view_issues, self.project)
67 (usr || User.current).allowed_to?(:view_issues, self.project)
67 end
68 end
68
69
69 def after_initialize
70 def after_initialize
70 if new_record?
71 if new_record?
71 # set default values for new records only
72 # set default values for new records only
72 self.status ||= IssueStatus.default
73 self.status ||= IssueStatus.default
73 self.priority ||= IssuePriority.default
74 self.priority ||= IssuePriority.default
74 end
75 end
75 end
76 end
76
77
77 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
78 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
78 def available_custom_fields
79 def available_custom_fields
79 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
80 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
80 end
81 end
81
82
82 def copy_from(arg)
83 def copy_from(arg)
83 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
84 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
84 self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on")
85 self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on")
85 self.custom_values = issue.custom_values.collect {|v| v.clone}
86 self.custom_values = issue.custom_values.collect {|v| v.clone}
86 self.status = issue.status
87 self.status = issue.status
87 self
88 self
88 end
89 end
89
90
90 # Moves/copies an issue to a new project and tracker
91 # Moves/copies an issue to a new project and tracker
91 # Returns the moved/copied issue on success, false on failure
92 # Returns the moved/copied issue on success, false on failure
92 def move_to(new_project, new_tracker = nil, options = {})
93 def move_to(new_project, new_tracker = nil, options = {})
93 options ||= {}
94 options ||= {}
94 issue = options[:copy] ? self.clone : self
95 issue = options[:copy] ? self.clone : self
95 transaction do
96 transaction do
96 if new_project && issue.project_id != new_project.id
97 if new_project && issue.project_id != new_project.id
97 # delete issue relations
98 # delete issue relations
98 unless Setting.cross_project_issue_relations?
99 unless Setting.cross_project_issue_relations?
99 issue.relations_from.clear
100 issue.relations_from.clear
100 issue.relations_to.clear
101 issue.relations_to.clear
101 end
102 end
102 # issue is moved to another project
103 # issue is moved to another project
103 # reassign to the category with same name if any
104 # reassign to the category with same name if any
104 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
105 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
105 issue.category = new_category
106 issue.category = new_category
106 # Keep the fixed_version if it's still valid in the new_project
107 # Keep the fixed_version if it's still valid in the new_project
107 unless new_project.shared_versions.include?(issue.fixed_version)
108 unless new_project.shared_versions.include?(issue.fixed_version)
108 issue.fixed_version = nil
109 issue.fixed_version = nil
109 end
110 end
110 issue.project = new_project
111 issue.project = new_project
111 end
112 end
112 if new_tracker
113 if new_tracker
113 issue.tracker = new_tracker
114 issue.tracker = new_tracker
114 end
115 end
115 if options[:copy]
116 if options[:copy]
116 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
117 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
117 issue.status = if options[:attributes] && options[:attributes][:status_id]
118 issue.status = if options[:attributes] && options[:attributes][:status_id]
118 IssueStatus.find_by_id(options[:attributes][:status_id])
119 IssueStatus.find_by_id(options[:attributes][:status_id])
119 else
120 else
120 self.status
121 self.status
121 end
122 end
122 end
123 end
123 # Allow bulk setting of attributes on the issue
124 # Allow bulk setting of attributes on the issue
124 if options[:attributes]
125 if options[:attributes]
125 issue.attributes = options[:attributes]
126 issue.attributes = options[:attributes]
126 end
127 end
127 if issue.save
128 if issue.save
128 unless options[:copy]
129 unless options[:copy]
129 # Manually update project_id on related time entries
130 # Manually update project_id on related time entries
130 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
131 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
131 end
132 end
132 else
133 else
133 Issue.connection.rollback_db_transaction
134 Issue.connection.rollback_db_transaction
134 return false
135 return false
135 end
136 end
136 end
137 end
137 return issue
138 return issue
138 end
139 end
139
140
140 def priority_id=(pid)
141 def priority_id=(pid)
141 self.priority = nil
142 self.priority = nil
142 write_attribute(:priority_id, pid)
143 write_attribute(:priority_id, pid)
143 end
144 end
144
145
145 def tracker_id=(tid)
146 def tracker_id=(tid)
146 self.tracker = nil
147 self.tracker = nil
147 result = write_attribute(:tracker_id, tid)
148 result = write_attribute(:tracker_id, tid)
148 @custom_field_values = nil
149 @custom_field_values = nil
149 result
150 result
150 end
151 end
151
152
152 # Overrides attributes= so that tracker_id gets assigned first
153 # Overrides attributes= so that tracker_id gets assigned first
153 def attributes_with_tracker_first=(new_attributes, *args)
154 def attributes_with_tracker_first=(new_attributes, *args)
154 return if new_attributes.nil?
155 return if new_attributes.nil?
155 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
156 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
156 if new_tracker_id
157 if new_tracker_id
157 self.tracker_id = new_tracker_id
158 self.tracker_id = new_tracker_id
158 end
159 end
159 send :attributes_without_tracker_first=, new_attributes, *args
160 send :attributes_without_tracker_first=, new_attributes, *args
160 end
161 end
161 # Do not redefine alias chain on reload (see #4838)
162 # Do not redefine alias chain on reload (see #4838)
162 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
163 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
163
164
164 def estimated_hours=(h)
165 def estimated_hours=(h)
165 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
166 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
166 end
167 end
167
168
168 SAFE_ATTRIBUTES = %w(
169 SAFE_ATTRIBUTES = %w(
169 tracker_id
170 tracker_id
170 status_id
171 status_id
171 category_id
172 category_id
172 assigned_to_id
173 assigned_to_id
173 priority_id
174 priority_id
174 fixed_version_id
175 fixed_version_id
175 subject
176 subject
176 description
177 description
177 start_date
178 start_date
178 due_date
179 due_date
179 done_ratio
180 done_ratio
180 estimated_hours
181 estimated_hours
181 custom_field_values
182 custom_field_values
182 ) unless const_defined?(:SAFE_ATTRIBUTES)
183 ) unless const_defined?(:SAFE_ATTRIBUTES)
183
184
184 # Safely sets attributes
185 # Safely sets attributes
185 # Should be called from controllers instead of #attributes=
186 # Should be called from controllers instead of #attributes=
186 # attr_accessible is too rough because we still want things like
187 # attr_accessible is too rough because we still want things like
187 # Issue.new(:project => foo) to work
188 # Issue.new(:project => foo) to work
188 # TODO: move workflow/permission checks from controllers to here
189 # TODO: move workflow/permission checks from controllers to here
189 def safe_attributes=(attrs, user=User.current)
190 def safe_attributes=(attrs, user=User.current)
190 return if attrs.nil?
191 return if attrs.nil?
191 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
192 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
192 if attrs['status_id']
193 if attrs['status_id']
193 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
194 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
194 attrs.delete('status_id')
195 attrs.delete('status_id')
195 end
196 end
196 end
197 end
197 self.attributes = attrs
198 self.attributes = attrs
198 end
199 end
199
200
200 def done_ratio
201 def done_ratio
201 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
202 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
202 status.default_done_ratio
203 status.default_done_ratio
203 else
204 else
204 read_attribute(:done_ratio)
205 read_attribute(:done_ratio)
205 end
206 end
206 end
207 end
207
208
208 def self.use_status_for_done_ratio?
209 def self.use_status_for_done_ratio?
209 Setting.issue_done_ratio == 'issue_status'
210 Setting.issue_done_ratio == 'issue_status'
210 end
211 end
211
212
212 def self.use_field_for_done_ratio?
213 def self.use_field_for_done_ratio?
213 Setting.issue_done_ratio == 'issue_field'
214 Setting.issue_done_ratio == 'issue_field'
214 end
215 end
215
216
216 def validate
217 def validate
217 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
218 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
218 errors.add :due_date, :not_a_date
219 errors.add :due_date, :not_a_date
219 end
220 end
220
221
221 if self.due_date and self.start_date and self.due_date < self.start_date
222 if self.due_date and self.start_date and self.due_date < self.start_date
222 errors.add :due_date, :greater_than_start_date
223 errors.add :due_date, :greater_than_start_date
223 end
224 end
224
225
225 if start_date && soonest_start && start_date < soonest_start
226 if start_date && soonest_start && start_date < soonest_start
226 errors.add :start_date, :invalid
227 errors.add :start_date, :invalid
227 end
228 end
228
229
229 if fixed_version
230 if fixed_version
230 if !assignable_versions.include?(fixed_version)
231 if !assignable_versions.include?(fixed_version)
231 errors.add :fixed_version_id, :inclusion
232 errors.add :fixed_version_id, :inclusion
232 elsif reopened? && fixed_version.closed?
233 elsif reopened? && fixed_version.closed?
233 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
234 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
234 end
235 end
235 end
236 end
236
237
237 # Checks that the issue can not be added/moved to a disabled tracker
238 # Checks that the issue can not be added/moved to a disabled tracker
238 if project && (tracker_id_changed? || project_id_changed?)
239 if project && (tracker_id_changed? || project_id_changed?)
239 unless project.trackers.include?(tracker)
240 unless project.trackers.include?(tracker)
240 errors.add :tracker_id, :inclusion
241 errors.add :tracker_id, :inclusion
241 end
242 end
242 end
243 end
243 end
244 end
244
245
245 def before_create
246 # default assignment based on category
247 if assigned_to.nil? && category && category.assigned_to
248 self.assigned_to = category.assigned_to
249 end
250 end
251
252 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
246 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
253 # even if the user turns off the setting later
247 # even if the user turns off the setting later
254 def update_done_ratio_from_issue_status
248 def update_done_ratio_from_issue_status
255 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
249 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
256 self.done_ratio = status.default_done_ratio
250 self.done_ratio = status.default_done_ratio
257 end
251 end
258 end
252 end
259
253
260 def after_save
261 # Reload is needed in order to get the right status
262 reload
263
264 # Update start/due dates of following issues
265 relations_from.each(&:set_issue_to_dates)
266
267 # Close duplicates if the issue was closed
268 if @issue_before_change && !@issue_before_change.closed? && self.closed?
269 duplicates.each do |duplicate|
270 # Reload is need in case the duplicate was updated by a previous duplicate
271 duplicate.reload
272 # Don't re-close it if it's already closed
273 next if duplicate.closed?
274 # Same user and notes
275 duplicate.init_journal(@current_journal.user, @current_journal.notes)
276 duplicate.update_attribute :status, self.status
277 end
278 end
279 end
280
281 def init_journal(user, notes = "")
254 def init_journal(user, notes = "")
282 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
255 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
283 @issue_before_change = self.clone
256 @issue_before_change = self.clone
284 @issue_before_change.status = self.status
257 @issue_before_change.status = self.status
285 @custom_values_before_change = {}
258 @custom_values_before_change = {}
286 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
259 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
287 # Make sure updated_on is updated when adding a note.
260 # Make sure updated_on is updated when adding a note.
288 updated_on_will_change!
261 updated_on_will_change!
289 @current_journal
262 @current_journal
290 end
263 end
291
264
292 # Return true if the issue is closed, otherwise false
265 # Return true if the issue is closed, otherwise false
293 def closed?
266 def closed?
294 self.status.is_closed?
267 self.status.is_closed?
295 end
268 end
296
269
297 # Return true if the issue is being reopened
270 # Return true if the issue is being reopened
298 def reopened?
271 def reopened?
299 if !new_record? && status_id_changed?
272 if !new_record? && status_id_changed?
300 status_was = IssueStatus.find_by_id(status_id_was)
273 status_was = IssueStatus.find_by_id(status_id_was)
301 status_new = IssueStatus.find_by_id(status_id)
274 status_new = IssueStatus.find_by_id(status_id)
302 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
275 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
303 return true
276 return true
304 end
277 end
305 end
278 end
306 false
279 false
307 end
280 end
281
282 # Return true if the issue is being closed
283 def closing?
284 if !new_record? && status_id_changed?
285 status_was = IssueStatus.find_by_id(status_id_was)
286 status_new = IssueStatus.find_by_id(status_id)
287 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
288 return true
289 end
290 end
291 false
292 end
308
293
309 # Returns true if the issue is overdue
294 # Returns true if the issue is overdue
310 def overdue?
295 def overdue?
311 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
296 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
312 end
297 end
313
298
314 # Users the issue can be assigned to
299 # Users the issue can be assigned to
315 def assignable_users
300 def assignable_users
316 project.assignable_users
301 project.assignable_users
317 end
302 end
318
303
319 # Versions that the issue can be assigned to
304 # Versions that the issue can be assigned to
320 def assignable_versions
305 def assignable_versions
321 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
306 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
322 end
307 end
323
308
324 # Returns true if this issue is blocked by another issue that is still open
309 # Returns true if this issue is blocked by another issue that is still open
325 def blocked?
310 def blocked?
326 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
311 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
327 end
312 end
328
313
329 # Returns an array of status that user is able to apply
314 # Returns an array of status that user is able to apply
330 def new_statuses_allowed_to(user)
315 def new_statuses_allowed_to(user)
331 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
316 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
332 statuses << status unless statuses.empty?
317 statuses << status unless statuses.empty?
333 statuses = statuses.uniq.sort
318 statuses = statuses.uniq.sort
334 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
319 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
335 end
320 end
336
321
337 # Returns the mail adresses of users that should be notified
322 # Returns the mail adresses of users that should be notified
338 def recipients
323 def recipients
339 notified = project.notified_users
324 notified = project.notified_users
340 # Author and assignee are always notified unless they have been locked
325 # Author and assignee are always notified unless they have been locked
341 notified << author if author && author.active?
326 notified << author if author && author.active?
342 notified << assigned_to if assigned_to && assigned_to.active?
327 notified << assigned_to if assigned_to && assigned_to.active?
343 notified.uniq!
328 notified.uniq!
344 # Remove users that can not view the issue
329 # Remove users that can not view the issue
345 notified.reject! {|user| !visible?(user)}
330 notified.reject! {|user| !visible?(user)}
346 notified.collect(&:mail)
331 notified.collect(&:mail)
347 end
332 end
348
333
349 # Returns the total number of hours spent on this issue.
334 # Returns the total number of hours spent on this issue.
350 #
335 #
351 # Example:
336 # Example:
352 # spent_hours => 0
337 # spent_hours => 0
353 # spent_hours => 50
338 # spent_hours => 50
354 def spent_hours
339 def spent_hours
355 @spent_hours ||= time_entries.sum(:hours) || 0
340 @spent_hours ||= time_entries.sum(:hours) || 0
356 end
341 end
357
342
358 def relations
343 def relations
359 (relations_from + relations_to).sort
344 (relations_from + relations_to).sort
360 end
345 end
361
346
362 def all_dependent_issues
347 def all_dependent_issues
363 dependencies = []
348 dependencies = []
364 relations_from.each do |relation|
349 relations_from.each do |relation|
365 dependencies << relation.issue_to
350 dependencies << relation.issue_to
366 dependencies += relation.issue_to.all_dependent_issues
351 dependencies += relation.issue_to.all_dependent_issues
367 end
352 end
368 dependencies
353 dependencies
369 end
354 end
370
355
371 # Returns an array of issues that duplicate this one
356 # Returns an array of issues that duplicate this one
372 def duplicates
357 def duplicates
373 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
358 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
374 end
359 end
375
360
376 # Returns the due date or the target due date if any
361 # Returns the due date or the target due date if any
377 # Used on gantt chart
362 # Used on gantt chart
378 def due_before
363 def due_before
379 due_date || (fixed_version ? fixed_version.effective_date : nil)
364 due_date || (fixed_version ? fixed_version.effective_date : nil)
380 end
365 end
381
366
382 # Returns the time scheduled for this issue.
367 # Returns the time scheduled for this issue.
383 #
368 #
384 # Example:
369 # Example:
385 # Start Date: 2/26/09, End Date: 3/04/09
370 # Start Date: 2/26/09, End Date: 3/04/09
386 # duration => 6
371 # duration => 6
387 def duration
372 def duration
388 (start_date && due_date) ? due_date - start_date : 0
373 (start_date && due_date) ? due_date - start_date : 0
389 end
374 end
390
375
391 def soonest_start
376 def soonest_start
392 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
377 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
393 end
378 end
394
379
395 def to_s
380 def to_s
396 "#{tracker} ##{id}: #{subject}"
381 "#{tracker} ##{id}: #{subject}"
397 end
382 end
398
383
399 # Returns a string of css classes that apply to the issue
384 # Returns a string of css classes that apply to the issue
400 def css_classes
385 def css_classes
401 s = "issue status-#{status.position} priority-#{priority.position}"
386 s = "issue status-#{status.position} priority-#{priority.position}"
402 s << ' closed' if closed?
387 s << ' closed' if closed?
403 s << ' overdue' if overdue?
388 s << ' overdue' if overdue?
404 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
389 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
405 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
390 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
406 s
391 s
407 end
392 end
408
393
409 # Unassigns issues from +version+ if it's no longer shared with issue's project
394 # Unassigns issues from +version+ if it's no longer shared with issue's project
410 def self.update_versions_from_sharing_change(version)
395 def self.update_versions_from_sharing_change(version)
411 # Update issues assigned to the version
396 # Update issues assigned to the version
412 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
397 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
413 end
398 end
414
399
415 # Unassigns issues from versions that are no longer shared
400 # Unassigns issues from versions that are no longer shared
416 # after +project+ was moved
401 # after +project+ was moved
417 def self.update_versions_from_hierarchy_change(project)
402 def self.update_versions_from_hierarchy_change(project)
418 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
403 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
419 # Update issues of the moved projects and issues assigned to a version of a moved project
404 # Update issues of the moved projects and issues assigned to a version of a moved project
420 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
405 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
421 end
406 end
422
407
423 # Extracted from the ReportsController.
408 # Extracted from the ReportsController.
424 def self.by_tracker(project)
409 def self.by_tracker(project)
425 count_and_group_by(:project => project,
410 count_and_group_by(:project => project,
426 :field => 'tracker_id',
411 :field => 'tracker_id',
427 :joins => Tracker.table_name)
412 :joins => Tracker.table_name)
428 end
413 end
429
414
430 def self.by_version(project)
415 def self.by_version(project)
431 count_and_group_by(:project => project,
416 count_and_group_by(:project => project,
432 :field => 'fixed_version_id',
417 :field => 'fixed_version_id',
433 :joins => Version.table_name)
418 :joins => Version.table_name)
434 end
419 end
435
420
436 def self.by_priority(project)
421 def self.by_priority(project)
437 count_and_group_by(:project => project,
422 count_and_group_by(:project => project,
438 :field => 'priority_id',
423 :field => 'priority_id',
439 :joins => IssuePriority.table_name)
424 :joins => IssuePriority.table_name)
440 end
425 end
441
426
442 def self.by_category(project)
427 def self.by_category(project)
443 count_and_group_by(:project => project,
428 count_and_group_by(:project => project,
444 :field => 'category_id',
429 :field => 'category_id',
445 :joins => IssueCategory.table_name)
430 :joins => IssueCategory.table_name)
446 end
431 end
447
432
448 def self.by_assigned_to(project)
433 def self.by_assigned_to(project)
449 count_and_group_by(:project => project,
434 count_and_group_by(:project => project,
450 :field => 'assigned_to_id',
435 :field => 'assigned_to_id',
451 :joins => User.table_name)
436 :joins => User.table_name)
452 end
437 end
453
438
454 def self.by_author(project)
439 def self.by_author(project)
455 count_and_group_by(:project => project,
440 count_and_group_by(:project => project,
456 :field => 'author_id',
441 :field => 'author_id',
457 :joins => User.table_name)
442 :joins => User.table_name)
458 end
443 end
459
444
460 def self.by_subproject(project)
445 def self.by_subproject(project)
461 ActiveRecord::Base.connection.select_all("select s.id as status_id,
446 ActiveRecord::Base.connection.select_all("select s.id as status_id,
462 s.is_closed as closed,
447 s.is_closed as closed,
463 i.project_id as project_id,
448 i.project_id as project_id,
464 count(i.id) as total
449 count(i.id) as total
465 from
450 from
466 #{Issue.table_name} i, #{IssueStatus.table_name} s
451 #{Issue.table_name} i, #{IssueStatus.table_name} s
467 where
452 where
468 i.status_id=s.id
453 i.status_id=s.id
469 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
454 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
470 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
455 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
471 end
456 end
472 # End ReportsController extraction
457 # End ReportsController extraction
473
458
474 private
459 private
475
460
476 # Update issues so their versions are not pointing to a
461 # Update issues so their versions are not pointing to a
477 # fixed_version that is not shared with the issue's project
462 # fixed_version that is not shared with the issue's project
478 def self.update_versions(conditions=nil)
463 def self.update_versions(conditions=nil)
479 # Only need to update issues with a fixed_version from
464 # Only need to update issues with a fixed_version from
480 # a different project and that is not systemwide shared
465 # a different project and that is not systemwide shared
481 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
466 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
482 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
467 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
483 " AND #{Version.table_name}.sharing <> 'system'",
468 " AND #{Version.table_name}.sharing <> 'system'",
484 conditions),
469 conditions),
485 :include => [:project, :fixed_version]
470 :include => [:project, :fixed_version]
486 ).each do |issue|
471 ).each do |issue|
487 next if issue.project.nil? || issue.fixed_version.nil?
472 next if issue.project.nil? || issue.fixed_version.nil?
488 unless issue.project.shared_versions.include?(issue.fixed_version)
473 unless issue.project.shared_versions.include?(issue.fixed_version)
489 issue.init_journal(User.current)
474 issue.init_journal(User.current)
490 issue.fixed_version = nil
475 issue.fixed_version = nil
491 issue.save
476 issue.save
492 end
477 end
493 end
478 end
494 end
479 end
495
480
496 # Callback on attachment deletion
481 # Callback on attachment deletion
497 def attachment_removed(obj)
482 def attachment_removed(obj)
498 journal = init_journal(User.current)
483 journal = init_journal(User.current)
499 journal.details << JournalDetail.new(:property => 'attachment',
484 journal.details << JournalDetail.new(:property => 'attachment',
500 :prop_key => obj.id,
485 :prop_key => obj.id,
501 :old_value => obj.filename)
486 :old_value => obj.filename)
502 journal.save
487 journal.save
503 end
488 end
504
489
490 # Default assignment based on category
491 def default_assign
492 if assigned_to.nil? && category && category.assigned_to
493 self.assigned_to = category.assigned_to
494 end
495 end
496
497 # Updates start/due dates of following issues
498 def reschedule_following_issues
499 if start_date_changed? || due_date_changed?
500 relations_from.each do |relation|
501 relation.set_issue_to_dates
502 end
503 end
504 end
505
506 # Closes duplicates if the issue is being closed
507 def close_duplicates
508 if closing?
509 duplicates.each do |duplicate|
510 # Reload is need in case the duplicate was updated by a previous duplicate
511 duplicate.reload
512 # Don't re-close it if it's already closed
513 next if duplicate.closed?
514 # Same user and notes
515 if @current_journal
516 duplicate.init_journal(@current_journal.user, @current_journal.notes)
517 end
518 duplicate.update_attribute :status, self.status
519 end
520 end
521 end
522
505 # Saves the changes in a Journal
523 # Saves the changes in a Journal
506 # Called after_save
524 # Called after_save
507 def create_journal
525 def create_journal
508 if @current_journal
526 if @current_journal
509 # attributes changes
527 # attributes changes
510 (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
528 (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
511 @current_journal.details << JournalDetail.new(:property => 'attr',
529 @current_journal.details << JournalDetail.new(:property => 'attr',
512 :prop_key => c,
530 :prop_key => c,
513 :old_value => @issue_before_change.send(c),
531 :old_value => @issue_before_change.send(c),
514 :value => send(c)) unless send(c)==@issue_before_change.send(c)
532 :value => send(c)) unless send(c)==@issue_before_change.send(c)
515 }
533 }
516 # custom fields changes
534 # custom fields changes
517 custom_values.each {|c|
535 custom_values.each {|c|
518 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
536 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
519 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
537 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
520 @current_journal.details << JournalDetail.new(:property => 'cf',
538 @current_journal.details << JournalDetail.new(:property => 'cf',
521 :prop_key => c.custom_field_id,
539 :prop_key => c.custom_field_id,
522 :old_value => @custom_values_before_change[c.custom_field_id],
540 :old_value => @custom_values_before_change[c.custom_field_id],
523 :value => c.value)
541 :value => c.value)
524 }
542 }
525 @current_journal.save
543 @current_journal.save
544 # reset current journal
545 init_journal @current_journal.user, @current_journal.notes
526 end
546 end
527 end
547 end
528
548
529 # Query generator for selecting groups of issue counts for a project
549 # Query generator for selecting groups of issue counts for a project
530 # based on specific criteria
550 # based on specific criteria
531 #
551 #
532 # Options
552 # Options
533 # * project - Project to search in.
553 # * project - Project to search in.
534 # * field - String. Issue field to key off of in the grouping.
554 # * field - String. Issue field to key off of in the grouping.
535 # * joins - String. The table name to join against.
555 # * joins - String. The table name to join against.
536 def self.count_and_group_by(options)
556 def self.count_and_group_by(options)
537 project = options.delete(:project)
557 project = options.delete(:project)
538 select_field = options.delete(:field)
558 select_field = options.delete(:field)
539 joins = options.delete(:joins)
559 joins = options.delete(:joins)
540
560
541 where = "i.#{select_field}=j.id"
561 where = "i.#{select_field}=j.id"
542
562
543 ActiveRecord::Base.connection.select_all("select s.id as status_id,
563 ActiveRecord::Base.connection.select_all("select s.id as status_id,
544 s.is_closed as closed,
564 s.is_closed as closed,
545 j.id as #{select_field},
565 j.id as #{select_field},
546 count(i.id) as total
566 count(i.id) as total
547 from
567 from
548 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} as j
568 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} as j
549 where
569 where
550 i.status_id=s.id
570 i.status_id=s.id
551 and #{where}
571 and #{where}
552 and i.project_id=#{project.id}
572 and i.project_id=#{project.id}
553 group by s.id, s.is_closed, j.id")
573 group by s.id, s.is_closed, j.id")
554 end
574 end
555
575
556
576
557 end
577 end
@@ -1,634 +1,660
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class IssueTest < ActiveSupport::TestCase
20 class IssueTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :members, :member_roles, :roles,
21 fixtures :projects, :users, :members, :member_roles, :roles,
22 :trackers, :projects_trackers,
22 :trackers, :projects_trackers,
23 :versions,
23 :versions,
24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
25 :enumerations,
25 :enumerations,
26 :issues,
26 :issues,
27 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
27 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
28 :time_entries
28 :time_entries
29
29
30 def test_create
30 def test_create
31 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')
31 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 assert issue.save
32 assert issue.save
33 issue.reload
33 issue.reload
34 assert_equal 1.5, issue.estimated_hours
34 assert_equal 1.5, issue.estimated_hours
35 end
35 end
36
36
37 def test_create_minimal
37 def test_create_minimal
38 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
38 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
39 assert issue.save
39 assert issue.save
40 assert issue.description.nil?
40 assert issue.description.nil?
41 end
41 end
42
42
43 def test_create_with_required_custom_field
43 def test_create_with_required_custom_field
44 field = IssueCustomField.find_by_name('Database')
44 field = IssueCustomField.find_by_name('Database')
45 field.update_attribute(:is_required, true)
45 field.update_attribute(:is_required, true)
46
46
47 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')
47 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 assert issue.available_custom_fields.include?(field)
48 assert issue.available_custom_fields.include?(field)
49 # No value for the custom field
49 # No value for the custom field
50 assert !issue.save
50 assert !issue.save
51 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
51 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
52 # Blank value
52 # Blank value
53 issue.custom_field_values = { field.id => '' }
53 issue.custom_field_values = { field.id => '' }
54 assert !issue.save
54 assert !issue.save
55 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
55 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
56 # Invalid value
56 # Invalid value
57 issue.custom_field_values = { field.id => 'SQLServer' }
57 issue.custom_field_values = { field.id => 'SQLServer' }
58 assert !issue.save
58 assert !issue.save
59 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
59 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
60 # Valid value
60 # Valid value
61 issue.custom_field_values = { field.id => 'PostgreSQL' }
61 issue.custom_field_values = { field.id => 'PostgreSQL' }
62 assert issue.save
62 assert issue.save
63 issue.reload
63 issue.reload
64 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
64 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
65 end
65 end
66
66
67 def test_visible_scope_for_anonymous
67 def test_visible_scope_for_anonymous
68 # Anonymous user should see issues of public projects only
68 # Anonymous user should see issues of public projects only
69 issues = Issue.visible(User.anonymous).all
69 issues = Issue.visible(User.anonymous).all
70 assert issues.any?
70 assert issues.any?
71 assert_nil issues.detect {|issue| !issue.project.is_public?}
71 assert_nil issues.detect {|issue| !issue.project.is_public?}
72 # Anonymous user should not see issues without permission
72 # Anonymous user should not see issues without permission
73 Role.anonymous.remove_permission!(:view_issues)
73 Role.anonymous.remove_permission!(:view_issues)
74 issues = Issue.visible(User.anonymous).all
74 issues = Issue.visible(User.anonymous).all
75 assert issues.empty?
75 assert issues.empty?
76 end
76 end
77
77
78 def test_visible_scope_for_user
78 def test_visible_scope_for_user
79 user = User.find(9)
79 user = User.find(9)
80 assert user.projects.empty?
80 assert user.projects.empty?
81 # Non member user should see issues of public projects only
81 # Non member user should see issues of public projects only
82 issues = Issue.visible(user).all
82 issues = Issue.visible(user).all
83 assert issues.any?
83 assert issues.any?
84 assert_nil issues.detect {|issue| !issue.project.is_public?}
84 assert_nil issues.detect {|issue| !issue.project.is_public?}
85 # Non member user should not see issues without permission
85 # Non member user should not see issues without permission
86 Role.non_member.remove_permission!(:view_issues)
86 Role.non_member.remove_permission!(:view_issues)
87 user.reload
87 user.reload
88 issues = Issue.visible(user).all
88 issues = Issue.visible(user).all
89 assert issues.empty?
89 assert issues.empty?
90 # User should see issues of projects for which he has view_issues permissions only
90 # User should see issues of projects for which he has view_issues permissions only
91 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
91 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
92 user.reload
92 user.reload
93 issues = Issue.visible(user).all
93 issues = Issue.visible(user).all
94 assert issues.any?
94 assert issues.any?
95 assert_nil issues.detect {|issue| issue.project_id != 2}
95 assert_nil issues.detect {|issue| issue.project_id != 2}
96 end
96 end
97
97
98 def test_visible_scope_for_admin
98 def test_visible_scope_for_admin
99 user = User.find(1)
99 user = User.find(1)
100 user.members.each(&:destroy)
100 user.members.each(&:destroy)
101 assert user.projects.empty?
101 assert user.projects.empty?
102 issues = Issue.visible(user).all
102 issues = Issue.visible(user).all
103 assert issues.any?
103 assert issues.any?
104 # Admin should see issues on private projects that he does not belong to
104 # Admin should see issues on private projects that he does not belong to
105 assert issues.detect {|issue| !issue.project.is_public?}
105 assert issues.detect {|issue| !issue.project.is_public?}
106 end
106 end
107
107
108 def test_errors_full_messages_should_include_custom_fields_errors
108 def test_errors_full_messages_should_include_custom_fields_errors
109 field = IssueCustomField.find_by_name('Database')
109 field = IssueCustomField.find_by_name('Database')
110
110
111 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')
111 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 assert issue.available_custom_fields.include?(field)
112 assert issue.available_custom_fields.include?(field)
113 # Invalid value
113 # Invalid value
114 issue.custom_field_values = { field.id => 'SQLServer' }
114 issue.custom_field_values = { field.id => 'SQLServer' }
115
115
116 assert !issue.valid?
116 assert !issue.valid?
117 assert_equal 1, issue.errors.full_messages.size
117 assert_equal 1, issue.errors.full_messages.size
118 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
118 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
119 end
119 end
120
120
121 def test_update_issue_with_required_custom_field
121 def test_update_issue_with_required_custom_field
122 field = IssueCustomField.find_by_name('Database')
122 field = IssueCustomField.find_by_name('Database')
123 field.update_attribute(:is_required, true)
123 field.update_attribute(:is_required, true)
124
124
125 issue = Issue.find(1)
125 issue = Issue.find(1)
126 assert_nil issue.custom_value_for(field)
126 assert_nil issue.custom_value_for(field)
127 assert issue.available_custom_fields.include?(field)
127 assert issue.available_custom_fields.include?(field)
128 # No change to custom values, issue can be saved
128 # No change to custom values, issue can be saved
129 assert issue.save
129 assert issue.save
130 # Blank value
130 # Blank value
131 issue.custom_field_values = { field.id => '' }
131 issue.custom_field_values = { field.id => '' }
132 assert !issue.save
132 assert !issue.save
133 # Valid value
133 # Valid value
134 issue.custom_field_values = { field.id => 'PostgreSQL' }
134 issue.custom_field_values = { field.id => 'PostgreSQL' }
135 assert issue.save
135 assert issue.save
136 issue.reload
136 issue.reload
137 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
137 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
138 end
138 end
139
139
140 def test_should_not_update_attributes_if_custom_fields_validation_fails
140 def test_should_not_update_attributes_if_custom_fields_validation_fails
141 issue = Issue.find(1)
141 issue = Issue.find(1)
142 field = IssueCustomField.find_by_name('Database')
142 field = IssueCustomField.find_by_name('Database')
143 assert issue.available_custom_fields.include?(field)
143 assert issue.available_custom_fields.include?(field)
144
144
145 issue.custom_field_values = { field.id => 'Invalid' }
145 issue.custom_field_values = { field.id => 'Invalid' }
146 issue.subject = 'Should be not be saved'
146 issue.subject = 'Should be not be saved'
147 assert !issue.save
147 assert !issue.save
148
148
149 issue.reload
149 issue.reload
150 assert_equal "Can't print recipes", issue.subject
150 assert_equal "Can't print recipes", issue.subject
151 end
151 end
152
152
153 def test_should_not_recreate_custom_values_objects_on_update
153 def test_should_not_recreate_custom_values_objects_on_update
154 field = IssueCustomField.find_by_name('Database')
154 field = IssueCustomField.find_by_name('Database')
155
155
156 issue = Issue.find(1)
156 issue = Issue.find(1)
157 issue.custom_field_values = { field.id => 'PostgreSQL' }
157 issue.custom_field_values = { field.id => 'PostgreSQL' }
158 assert issue.save
158 assert issue.save
159 custom_value = issue.custom_value_for(field)
159 custom_value = issue.custom_value_for(field)
160 issue.reload
160 issue.reload
161 issue.custom_field_values = { field.id => 'MySQL' }
161 issue.custom_field_values = { field.id => 'MySQL' }
162 assert issue.save
162 assert issue.save
163 issue.reload
163 issue.reload
164 assert_equal custom_value.id, issue.custom_value_for(field).id
164 assert_equal custom_value.id, issue.custom_value_for(field).id
165 end
165 end
166
166
167 def test_assigning_tracker_id_should_reload_custom_fields_values
167 def test_assigning_tracker_id_should_reload_custom_fields_values
168 issue = Issue.new(:project => Project.find(1))
168 issue = Issue.new(:project => Project.find(1))
169 assert issue.custom_field_values.empty?
169 assert issue.custom_field_values.empty?
170 issue.tracker_id = 1
170 issue.tracker_id = 1
171 assert issue.custom_field_values.any?
171 assert issue.custom_field_values.any?
172 end
172 end
173
173
174 def test_assigning_attributes_should_assign_tracker_id_first
174 def test_assigning_attributes_should_assign_tracker_id_first
175 attributes = ActiveSupport::OrderedHash.new
175 attributes = ActiveSupport::OrderedHash.new
176 attributes['custom_field_values'] = { '1' => 'MySQL' }
176 attributes['custom_field_values'] = { '1' => 'MySQL' }
177 attributes['tracker_id'] = '1'
177 attributes['tracker_id'] = '1'
178 issue = Issue.new(:project => Project.find(1))
178 issue = Issue.new(:project => Project.find(1))
179 issue.attributes = attributes
179 issue.attributes = attributes
180 assert_not_nil issue.custom_value_for(1)
180 assert_not_nil issue.custom_value_for(1)
181 assert_equal 'MySQL', issue.custom_value_for(1).value
181 assert_equal 'MySQL', issue.custom_value_for(1).value
182 end
182 end
183
183
184 def test_should_update_issue_with_disabled_tracker
184 def test_should_update_issue_with_disabled_tracker
185 p = Project.find(1)
185 p = Project.find(1)
186 issue = Issue.find(1)
186 issue = Issue.find(1)
187
187
188 p.trackers.delete(issue.tracker)
188 p.trackers.delete(issue.tracker)
189 assert !p.trackers.include?(issue.tracker)
189 assert !p.trackers.include?(issue.tracker)
190
190
191 issue.reload
191 issue.reload
192 issue.subject = 'New subject'
192 issue.subject = 'New subject'
193 assert issue.save
193 assert issue.save
194 end
194 end
195
195
196 def test_should_not_set_a_disabled_tracker
196 def test_should_not_set_a_disabled_tracker
197 p = Project.find(1)
197 p = Project.find(1)
198 p.trackers.delete(Tracker.find(2))
198 p.trackers.delete(Tracker.find(2))
199
199
200 issue = Issue.find(1)
200 issue = Issue.find(1)
201 issue.tracker_id = 2
201 issue.tracker_id = 2
202 issue.subject = 'New subject'
202 issue.subject = 'New subject'
203 assert !issue.save
203 assert !issue.save
204 assert_not_nil issue.errors.on(:tracker_id)
204 assert_not_nil issue.errors.on(:tracker_id)
205 end
205 end
206
206
207 def test_category_based_assignment
207 def test_category_based_assignment
208 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)
208 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 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
209 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
210 end
210 end
211
211
212 def test_copy
212 def test_copy
213 issue = Issue.new.copy_from(1)
213 issue = Issue.new.copy_from(1)
214 assert issue.save
214 assert issue.save
215 issue.reload
215 issue.reload
216 orig = Issue.find(1)
216 orig = Issue.find(1)
217 assert_equal orig.subject, issue.subject
217 assert_equal orig.subject, issue.subject
218 assert_equal orig.tracker, issue.tracker
218 assert_equal orig.tracker, issue.tracker
219 assert_equal "125", issue.custom_value_for(2).value
219 assert_equal "125", issue.custom_value_for(2).value
220 end
220 end
221
221
222 def test_copy_should_copy_status
222 def test_copy_should_copy_status
223 orig = Issue.find(8)
223 orig = Issue.find(8)
224 assert orig.status != IssueStatus.default
224 assert orig.status != IssueStatus.default
225
225
226 issue = Issue.new.copy_from(orig)
226 issue = Issue.new.copy_from(orig)
227 assert issue.save
227 assert issue.save
228 issue.reload
228 issue.reload
229 assert_equal orig.status, issue.status
229 assert_equal orig.status, issue.status
230 end
230 end
231
231
232 def test_should_close_duplicates
232 def test_should_close_duplicates
233 # Create 3 issues
233 # Create 3 issues
234 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')
234 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 assert issue1.save
235 assert issue1.save
236 issue2 = issue1.clone
236 issue2 = issue1.clone
237 assert issue2.save
237 assert issue2.save
238 issue3 = issue1.clone
238 issue3 = issue1.clone
239 assert issue3.save
239 assert issue3.save
240
240
241 # 2 is a dupe of 1
241 # 2 is a dupe of 1
242 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
242 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
243 # And 3 is a dupe of 2
243 # And 3 is a dupe of 2
244 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
244 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
245 # And 3 is a dupe of 1 (circular duplicates)
245 # And 3 is a dupe of 1 (circular duplicates)
246 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
246 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
247
247
248 assert issue1.reload.duplicates.include?(issue2)
248 assert issue1.reload.duplicates.include?(issue2)
249
249
250 # Closing issue 1
250 # Closing issue 1
251 issue1.init_journal(User.find(:first), "Closing issue1")
251 issue1.init_journal(User.find(:first), "Closing issue1")
252 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
252 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
253 assert issue1.save
253 assert issue1.save
254 # 2 and 3 should be also closed
254 # 2 and 3 should be also closed
255 assert issue2.reload.closed?
255 assert issue2.reload.closed?
256 assert issue3.reload.closed?
256 assert issue3.reload.closed?
257 end
257 end
258
258
259 def test_should_not_close_duplicated_issue
259 def test_should_not_close_duplicated_issue
260 # Create 3 issues
260 # Create 3 issues
261 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')
261 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 assert issue1.save
262 assert issue1.save
263 issue2 = issue1.clone
263 issue2 = issue1.clone
264 assert issue2.save
264 assert issue2.save
265
265
266 # 2 is a dupe of 1
266 # 2 is a dupe of 1
267 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
267 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
268 # 2 is a dup of 1 but 1 is not a duplicate of 2
268 # 2 is a dup of 1 but 1 is not a duplicate of 2
269 assert !issue2.reload.duplicates.include?(issue1)
269 assert !issue2.reload.duplicates.include?(issue1)
270
270
271 # Closing issue 2
271 # Closing issue 2
272 issue2.init_journal(User.find(:first), "Closing issue2")
272 issue2.init_journal(User.find(:first), "Closing issue2")
273 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
273 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
274 assert issue2.save
274 assert issue2.save
275 # 1 should not be also closed
275 # 1 should not be also closed
276 assert !issue1.reload.closed?
276 assert !issue1.reload.closed?
277 end
277 end
278
278
279 def test_assignable_versions
279 def test_assignable_versions
280 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
280 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
281 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
281 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
282 end
282 end
283
283
284 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
284 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
285 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
285 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
286 assert !issue.save
286 assert !issue.save
287 assert_not_nil issue.errors.on(:fixed_version_id)
287 assert_not_nil issue.errors.on(:fixed_version_id)
288 end
288 end
289
289
290 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
290 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
291 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
291 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
292 assert !issue.save
292 assert !issue.save
293 assert_not_nil issue.errors.on(:fixed_version_id)
293 assert_not_nil issue.errors.on(:fixed_version_id)
294 end
294 end
295
295
296 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
296 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
297 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
297 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
298 assert issue.save
298 assert issue.save
299 end
299 end
300
300
301 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
301 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
302 issue = Issue.find(11)
302 issue = Issue.find(11)
303 assert_equal 'closed', issue.fixed_version.status
303 assert_equal 'closed', issue.fixed_version.status
304 issue.subject = 'Subject changed'
304 issue.subject = 'Subject changed'
305 assert issue.save
305 assert issue.save
306 end
306 end
307
307
308 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
308 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
309 issue = Issue.find(11)
309 issue = Issue.find(11)
310 issue.status_id = 1
310 issue.status_id = 1
311 assert !issue.save
311 assert !issue.save
312 assert_not_nil issue.errors.on_base
312 assert_not_nil issue.errors.on_base
313 end
313 end
314
314
315 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
315 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
316 issue = Issue.find(11)
316 issue = Issue.find(11)
317 issue.status_id = 1
317 issue.status_id = 1
318 issue.fixed_version_id = 3
318 issue.fixed_version_id = 3
319 assert issue.save
319 assert issue.save
320 end
320 end
321
321
322 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
322 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
323 issue = Issue.find(12)
323 issue = Issue.find(12)
324 assert_equal 'locked', issue.fixed_version.status
324 assert_equal 'locked', issue.fixed_version.status
325 issue.status_id = 1
325 issue.status_id = 1
326 assert issue.save
326 assert issue.save
327 end
327 end
328
328
329 def test_move_to_another_project_with_same_category
329 def test_move_to_another_project_with_same_category
330 issue = Issue.find(1)
330 issue = Issue.find(1)
331 assert issue.move_to(Project.find(2))
331 assert issue.move_to(Project.find(2))
332 issue.reload
332 issue.reload
333 assert_equal 2, issue.project_id
333 assert_equal 2, issue.project_id
334 # Category changes
334 # Category changes
335 assert_equal 4, issue.category_id
335 assert_equal 4, issue.category_id
336 # Make sure time entries were move to the target project
336 # Make sure time entries were move to the target project
337 assert_equal 2, issue.time_entries.first.project_id
337 assert_equal 2, issue.time_entries.first.project_id
338 end
338 end
339
339
340 def test_move_to_another_project_without_same_category
340 def test_move_to_another_project_without_same_category
341 issue = Issue.find(2)
341 issue = Issue.find(2)
342 assert issue.move_to(Project.find(2))
342 assert issue.move_to(Project.find(2))
343 issue.reload
343 issue.reload
344 assert_equal 2, issue.project_id
344 assert_equal 2, issue.project_id
345 # Category cleared
345 # Category cleared
346 assert_nil issue.category_id
346 assert_nil issue.category_id
347 end
347 end
348
348
349 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
349 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
350 issue = Issue.find(1)
350 issue = Issue.find(1)
351 issue.update_attribute(:fixed_version_id, 1)
351 issue.update_attribute(:fixed_version_id, 1)
352 assert issue.move_to(Project.find(2))
352 assert issue.move_to(Project.find(2))
353 issue.reload
353 issue.reload
354 assert_equal 2, issue.project_id
354 assert_equal 2, issue.project_id
355 # Cleared fixed_version
355 # Cleared fixed_version
356 assert_equal nil, issue.fixed_version
356 assert_equal nil, issue.fixed_version
357 end
357 end
358
358
359 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
359 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
360 issue = Issue.find(1)
360 issue = Issue.find(1)
361 issue.update_attribute(:fixed_version_id, 4)
361 issue.update_attribute(:fixed_version_id, 4)
362 assert issue.move_to(Project.find(5))
362 assert issue.move_to(Project.find(5))
363 issue.reload
363 issue.reload
364 assert_equal 5, issue.project_id
364 assert_equal 5, issue.project_id
365 # Keep fixed_version
365 # Keep fixed_version
366 assert_equal 4, issue.fixed_version_id
366 assert_equal 4, issue.fixed_version_id
367 end
367 end
368
368
369 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
369 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
370 issue = Issue.find(1)
370 issue = Issue.find(1)
371 issue.update_attribute(:fixed_version_id, 1)
371 issue.update_attribute(:fixed_version_id, 1)
372 assert issue.move_to(Project.find(5))
372 assert issue.move_to(Project.find(5))
373 issue.reload
373 issue.reload
374 assert_equal 5, issue.project_id
374 assert_equal 5, issue.project_id
375 # Cleared fixed_version
375 # Cleared fixed_version
376 assert_equal nil, issue.fixed_version
376 assert_equal nil, issue.fixed_version
377 end
377 end
378
378
379 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
379 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
380 issue = Issue.find(1)
380 issue = Issue.find(1)
381 issue.update_attribute(:fixed_version_id, 7)
381 issue.update_attribute(:fixed_version_id, 7)
382 assert issue.move_to(Project.find(2))
382 assert issue.move_to(Project.find(2))
383 issue.reload
383 issue.reload
384 assert_equal 2, issue.project_id
384 assert_equal 2, issue.project_id
385 # Keep fixed_version
385 # Keep fixed_version
386 assert_equal 7, issue.fixed_version_id
386 assert_equal 7, issue.fixed_version_id
387 end
387 end
388
388
389 def test_copy_to_the_same_project
389 def test_copy_to_the_same_project
390 issue = Issue.find(1)
390 issue = Issue.find(1)
391 copy = nil
391 copy = nil
392 assert_difference 'Issue.count' do
392 assert_difference 'Issue.count' do
393 copy = issue.move_to(issue.project, nil, :copy => true)
393 copy = issue.move_to(issue.project, nil, :copy => true)
394 end
394 end
395 assert_kind_of Issue, copy
395 assert_kind_of Issue, copy
396 assert_equal issue.project, copy.project
396 assert_equal issue.project, copy.project
397 assert_equal "125", copy.custom_value_for(2).value
397 assert_equal "125", copy.custom_value_for(2).value
398 end
398 end
399
399
400 def test_copy_to_another_project_and_tracker
400 def test_copy_to_another_project_and_tracker
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.find(3), Tracker.find(2), :copy => true)
404 copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
405 end
405 end
406 assert_kind_of Issue, copy
406 assert_kind_of Issue, copy
407 assert_equal Project.find(3), copy.project
407 assert_equal Project.find(3), copy.project
408 assert_equal Tracker.find(2), copy.tracker
408 assert_equal Tracker.find(2), copy.tracker
409 # Custom field #2 is not associated with target tracker
409 # Custom field #2 is not associated with target tracker
410 assert_nil copy.custom_value_for(2)
410 assert_nil copy.custom_value_for(2)
411 end
411 end
412
412
413 context "#move_to" do
413 context "#move_to" do
414 context "as a copy" do
414 context "as a copy" do
415 setup do
415 setup do
416 @issue = Issue.find(1)
416 @issue = Issue.find(1)
417 @copy = nil
417 @copy = nil
418 end
418 end
419
419
420 should "allow assigned_to changes" do
420 should "allow assigned_to changes" do
421 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
421 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
422 assert_equal 3, @copy.assigned_to_id
422 assert_equal 3, @copy.assigned_to_id
423 end
423 end
424
424
425 should "allow status changes" do
425 should "allow status changes" do
426 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
426 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
427 assert_equal 2, @copy.status_id
427 assert_equal 2, @copy.status_id
428 end
428 end
429
429
430 should "allow start date changes" do
430 should "allow start date changes" do
431 date = Date.today
431 date = Date.today
432 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
432 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
433 assert_equal date, @copy.start_date
433 assert_equal date, @copy.start_date
434 end
434 end
435
435
436 should "allow due date changes" do
436 should "allow due date changes" do
437 date = Date.today
437 date = Date.today
438 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
438 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
439
439
440 assert_equal date, @copy.due_date
440 assert_equal date, @copy.due_date
441 end
441 end
442 end
442 end
443 end
443 end
444
444
445 def test_recipients_should_not_include_users_that_cannot_view_the_issue
445 def test_recipients_should_not_include_users_that_cannot_view_the_issue
446 issue = Issue.find(12)
446 issue = Issue.find(12)
447 assert issue.recipients.include?(issue.author.mail)
447 assert issue.recipients.include?(issue.author.mail)
448 # move the issue to a private project
448 # move the issue to a private project
449 copy = issue.move_to(Project.find(5), Tracker.find(2), :copy => true)
449 copy = issue.move_to(Project.find(5), Tracker.find(2), :copy => true)
450 # author is not a member of project anymore
450 # author is not a member of project anymore
451 assert !copy.recipients.include?(copy.author.mail)
451 assert !copy.recipients.include?(copy.author.mail)
452 end
452 end
453
453
454 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
454 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
455 user = User.find(3)
455 user = User.find(3)
456 issue = Issue.find(9)
456 issue = Issue.find(9)
457 Watcher.create!(:user => user, :watchable => issue)
457 Watcher.create!(:user => user, :watchable => issue)
458 assert issue.watched_by?(user)
458 assert issue.watched_by?(user)
459 assert !issue.watcher_recipients.include?(user.mail)
459 assert !issue.watcher_recipients.include?(user.mail)
460 end
460 end
461
461
462 def test_issue_destroy
462 def test_issue_destroy
463 Issue.find(1).destroy
463 Issue.find(1).destroy
464 assert_nil Issue.find_by_id(1)
464 assert_nil Issue.find_by_id(1)
465 assert_nil TimeEntry.find_by_issue_id(1)
465 assert_nil TimeEntry.find_by_issue_id(1)
466 end
466 end
467
467
468 def test_blocked
468 def test_blocked
469 blocked_issue = Issue.find(9)
469 blocked_issue = Issue.find(9)
470 blocking_issue = Issue.find(10)
470 blocking_issue = Issue.find(10)
471
471
472 assert blocked_issue.blocked?
472 assert blocked_issue.blocked?
473 assert !blocking_issue.blocked?
473 assert !blocking_issue.blocked?
474 end
474 end
475
475
476 def test_blocked_issues_dont_allow_closed_statuses
476 def test_blocked_issues_dont_allow_closed_statuses
477 blocked_issue = Issue.find(9)
477 blocked_issue = Issue.find(9)
478
478
479 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
479 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
480 assert !allowed_statuses.empty?
480 assert !allowed_statuses.empty?
481 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
481 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
482 assert closed_statuses.empty?
482 assert closed_statuses.empty?
483 end
483 end
484
484
485 def test_unblocked_issues_allow_closed_statuses
485 def test_unblocked_issues_allow_closed_statuses
486 blocking_issue = Issue.find(10)
486 blocking_issue = Issue.find(10)
487
487
488 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
488 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
489 assert !allowed_statuses.empty?
489 assert !allowed_statuses.empty?
490 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
490 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
491 assert !closed_statuses.empty?
491 assert !closed_statuses.empty?
492 end
492 end
493
493
494 def test_overdue
494 def test_overdue
495 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
495 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
496 assert !Issue.new(:due_date => Date.today).overdue?
496 assert !Issue.new(:due_date => Date.today).overdue?
497 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
497 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
498 assert !Issue.new(:due_date => nil).overdue?
498 assert !Issue.new(:due_date => nil).overdue?
499 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
499 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
500 end
500 end
501
501
502 def test_assignable_users
502 def test_assignable_users
503 assert_kind_of User, Issue.find(1).assignable_users.first
503 assert_kind_of User, Issue.find(1).assignable_users.first
504 end
504 end
505
505
506 def test_create_should_send_email_notification
506 def test_create_should_send_email_notification
507 ActionMailer::Base.deliveries.clear
507 ActionMailer::Base.deliveries.clear
508 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')
508 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')
509
509
510 assert issue.save
510 assert issue.save
511 assert_equal 1, ActionMailer::Base.deliveries.size
511 assert_equal 1, ActionMailer::Base.deliveries.size
512 end
512 end
513
513
514 def test_stale_issue_should_not_send_email_notification
514 def test_stale_issue_should_not_send_email_notification
515 ActionMailer::Base.deliveries.clear
515 ActionMailer::Base.deliveries.clear
516 issue = Issue.find(1)
516 issue = Issue.find(1)
517 stale = Issue.find(1)
517 stale = Issue.find(1)
518
518
519 issue.init_journal(User.find(1))
519 issue.init_journal(User.find(1))
520 issue.subject = 'Subjet update'
520 issue.subject = 'Subjet update'
521 assert issue.save
521 assert issue.save
522 assert_equal 1, ActionMailer::Base.deliveries.size
522 assert_equal 1, ActionMailer::Base.deliveries.size
523 ActionMailer::Base.deliveries.clear
523 ActionMailer::Base.deliveries.clear
524
524
525 stale.init_journal(User.find(1))
525 stale.init_journal(User.find(1))
526 stale.subject = 'Another subjet update'
526 stale.subject = 'Another subjet update'
527 assert_raise ActiveRecord::StaleObjectError do
527 assert_raise ActiveRecord::StaleObjectError do
528 stale.save
528 stale.save
529 end
529 end
530 assert ActionMailer::Base.deliveries.empty?
530 assert ActionMailer::Base.deliveries.empty?
531 end
531 end
532
533 def test_saving_twice_should_not_duplicate_journal_details
534 i = Issue.find(:first)
535 i.init_journal(User.find(2), 'Some notes')
536 # 2 changes
537 i.subject = 'New subject'
538 i.done_ratio = i.done_ratio + 10
539 assert_difference 'Journal.count' do
540 assert_difference 'JournalDetail.count', 2 do
541 assert i.save
542 end
543 end
544 # 1 more change
545 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
546 assert_no_difference 'Journal.count' do
547 assert_difference 'JournalDetail.count', 1 do
548 i.save
549 end
550 end
551 # no more change
552 assert_no_difference 'Journal.count' do
553 assert_no_difference 'JournalDetail.count' do
554 i.save
555 end
556 end
557 end
532
558
533 context "#done_ratio" do
559 context "#done_ratio" do
534 setup do
560 setup do
535 @issue = Issue.find(1)
561 @issue = Issue.find(1)
536 @issue_status = IssueStatus.find(1)
562 @issue_status = IssueStatus.find(1)
537 @issue_status.update_attribute(:default_done_ratio, 50)
563 @issue_status.update_attribute(:default_done_ratio, 50)
538 end
564 end
539
565
540 context "with Setting.issue_done_ratio using the issue_field" do
566 context "with Setting.issue_done_ratio using the issue_field" do
541 setup do
567 setup do
542 Setting.issue_done_ratio = 'issue_field'
568 Setting.issue_done_ratio = 'issue_field'
543 end
569 end
544
570
545 should "read the issue's field" do
571 should "read the issue's field" do
546 assert_equal 0, @issue.done_ratio
572 assert_equal 0, @issue.done_ratio
547 end
573 end
548 end
574 end
549
575
550 context "with Setting.issue_done_ratio using the issue_status" do
576 context "with Setting.issue_done_ratio using the issue_status" do
551 setup do
577 setup do
552 Setting.issue_done_ratio = 'issue_status'
578 Setting.issue_done_ratio = 'issue_status'
553 end
579 end
554
580
555 should "read the Issue Status's default done ratio" do
581 should "read the Issue Status's default done ratio" do
556 assert_equal 50, @issue.done_ratio
582 assert_equal 50, @issue.done_ratio
557 end
583 end
558 end
584 end
559 end
585 end
560
586
561 context "#update_done_ratio_from_issue_status" do
587 context "#update_done_ratio_from_issue_status" do
562 setup do
588 setup do
563 @issue = Issue.find(1)
589 @issue = Issue.find(1)
564 @issue_status = IssueStatus.find(1)
590 @issue_status = IssueStatus.find(1)
565 @issue_status.update_attribute(:default_done_ratio, 50)
591 @issue_status.update_attribute(:default_done_ratio, 50)
566 end
592 end
567
593
568 context "with Setting.issue_done_ratio using the issue_field" do
594 context "with Setting.issue_done_ratio using the issue_field" do
569 setup do
595 setup do
570 Setting.issue_done_ratio = 'issue_field'
596 Setting.issue_done_ratio = 'issue_field'
571 end
597 end
572
598
573 should "not change the issue" do
599 should "not change the issue" do
574 @issue.update_done_ratio_from_issue_status
600 @issue.update_done_ratio_from_issue_status
575
601
576 assert_equal 0, @issue.done_ratio
602 assert_equal 0, @issue.done_ratio
577 end
603 end
578 end
604 end
579
605
580 context "with Setting.issue_done_ratio using the issue_status" do
606 context "with Setting.issue_done_ratio using the issue_status" do
581 setup do
607 setup do
582 Setting.issue_done_ratio = 'issue_status'
608 Setting.issue_done_ratio = 'issue_status'
583 end
609 end
584
610
585 should "not change the issue's done ratio" do
611 should "not change the issue's done ratio" do
586 @issue.update_done_ratio_from_issue_status
612 @issue.update_done_ratio_from_issue_status
587
613
588 assert_equal 50, @issue.done_ratio
614 assert_equal 50, @issue.done_ratio
589 end
615 end
590 end
616 end
591 end
617 end
592
618
593 test "#by_tracker" do
619 test "#by_tracker" do
594 groups = Issue.by_tracker(Project.find(1))
620 groups = Issue.by_tracker(Project.find(1))
595 assert_equal 3, groups.size
621 assert_equal 3, groups.size
596 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
622 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
597 end
623 end
598
624
599 test "#by_version" do
625 test "#by_version" do
600 groups = Issue.by_version(Project.find(1))
626 groups = Issue.by_version(Project.find(1))
601 assert_equal 3, groups.size
627 assert_equal 3, groups.size
602 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
628 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
603 end
629 end
604
630
605 test "#by_priority" do
631 test "#by_priority" do
606 groups = Issue.by_priority(Project.find(1))
632 groups = Issue.by_priority(Project.find(1))
607 assert_equal 4, groups.size
633 assert_equal 4, groups.size
608 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
634 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
609 end
635 end
610
636
611 test "#by_category" do
637 test "#by_category" do
612 groups = Issue.by_category(Project.find(1))
638 groups = Issue.by_category(Project.find(1))
613 assert_equal 2, groups.size
639 assert_equal 2, groups.size
614 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
640 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
615 end
641 end
616
642
617 test "#by_assigned_to" do
643 test "#by_assigned_to" do
618 groups = Issue.by_assigned_to(Project.find(1))
644 groups = Issue.by_assigned_to(Project.find(1))
619 assert_equal 2, groups.size
645 assert_equal 2, groups.size
620 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
646 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
621 end
647 end
622
648
623 test "#by_author" do
649 test "#by_author" do
624 groups = Issue.by_author(Project.find(1))
650 groups = Issue.by_author(Project.find(1))
625 assert_equal 4, groups.size
651 assert_equal 4, groups.size
626 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
652 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
627 end
653 end
628
654
629 test "#by_subproject" do
655 test "#by_subproject" do
630 groups = Issue.by_subproject(Project.find(1))
656 groups = Issue.by_subproject(Project.find(1))
631 assert_equal 2, groups.size
657 assert_equal 2, groups.size
632 assert_equal 5, groups.inject(0) {|sum, group| sum + group['total'].to_i}
658 assert_equal 5, groups.inject(0) {|sum, group| sum + group['total'].to_i}
633 end
659 end
634 end
660 end
General Comments 0
You need to be logged in to leave comments. Login now