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