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