##// END OF EJS Templates
Merged r11641 and r11642 from trunk (#8794)....
Jean-Philippe Lang -
r11426:5745a2a2e34e
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,1434 +1,1436
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 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 include Redmine::Utils::DateCalculation
21 21
22 22 belongs_to :project
23 23 belongs_to :tracker
24 24 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
25 25 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
26 26 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
27 27 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
28 28 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
29 29 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
30 30
31 31 has_many :journals, :as => :journalized, :dependent => :destroy
32 32 has_many :visible_journals,
33 33 :class_name => 'Journal',
34 34 :as => :journalized,
35 35 :conditions => Proc.new {
36 36 ["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false]
37 37 },
38 38 :readonly => true
39 39
40 40 has_many :time_entries, :dependent => :delete_all
41 41 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
42 42
43 43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
44 44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
45 45
46 46 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
47 47 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
48 48 acts_as_customizable
49 49 acts_as_watchable
50 50 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
51 51 :include => [:project, :visible_journals],
52 52 # sort by id so that limited eager loading doesn't break with postgresql
53 53 :order_column => "#{table_name}.id"
54 54 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
55 55 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
56 56 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
57 57
58 58 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
59 59 :author_key => :author_id
60 60
61 61 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
62 62
63 63 attr_reader :current_journal
64 64 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
65 65
66 66 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
67 67
68 68 validates_length_of :subject, :maximum => 255
69 69 validates_inclusion_of :done_ratio, :in => 0..100
70 70 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
71 71 validates :start_date, :date => true
72 72 validates :due_date, :date => true
73 73 validate :validate_issue, :validate_required_fields
74 74
75 75 scope :visible, lambda {|*args|
76 76 includes(:project).where(Issue.visible_condition(args.shift || User.current, *args))
77 77 }
78 78
79 79 scope :open, lambda {|*args|
80 80 is_closed = args.size > 0 ? !args.first : false
81 81 includes(:status).where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
82 82 }
83 83
84 84 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
85 85 scope :on_active_project, lambda {
86 86 includes(:status, :project, :tracker).where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
87 87 }
88 88 scope :fixed_version, lambda {|versions|
89 89 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
90 90 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
91 91 }
92 92
93 93 before_create :default_assign
94 94 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change, :update_closed_on
95 95 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
96 96 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
97 97 # Should be after_create but would be called before previous after_save callbacks
98 98 after_save :after_create_from_copy
99 99 after_destroy :update_parent_attributes
100 100
101 101 # Returns a SQL conditions string used to find all issues visible by the specified user
102 102 def self.visible_condition(user, options={})
103 103 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
104 104 if user.logged?
105 105 case role.issues_visibility
106 106 when 'all'
107 107 nil
108 108 when 'default'
109 109 user_ids = [user.id] + user.groups.map(&:id)
110 110 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
111 111 when 'own'
112 112 user_ids = [user.id] + user.groups.map(&:id)
113 113 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
114 114 else
115 115 '1=0'
116 116 end
117 117 else
118 118 "(#{table_name}.is_private = #{connection.quoted_false})"
119 119 end
120 120 end
121 121 end
122 122
123 123 # Returns true if usr or current user is allowed to view the issue
124 124 def visible?(usr=nil)
125 125 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
126 126 if user.logged?
127 127 case role.issues_visibility
128 128 when 'all'
129 129 true
130 130 when 'default'
131 131 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
132 132 when 'own'
133 133 self.author == user || user.is_or_belongs_to?(assigned_to)
134 134 else
135 135 false
136 136 end
137 137 else
138 138 !self.is_private?
139 139 end
140 140 end
141 141 end
142 142
143 143 # Returns true if user or current user is allowed to edit or add a note to the issue
144 144 def editable?(user=User.current)
145 145 user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
146 146 end
147 147
148 148 def initialize(attributes=nil, *args)
149 149 super
150 150 if new_record?
151 151 # set default values for new records only
152 152 self.status ||= IssueStatus.default
153 153 self.priority ||= IssuePriority.default
154 154 self.watcher_user_ids = []
155 155 end
156 156 end
157 157
158 158 def create_or_update
159 159 super
160 160 ensure
161 161 @status_was = nil
162 162 end
163 163 private :create_or_update
164 164
165 165 # AR#Persistence#destroy would raise and RecordNotFound exception
166 166 # if the issue was already deleted or updated (non matching lock_version).
167 167 # This is a problem when bulk deleting issues or deleting a project
168 168 # (because an issue may already be deleted if its parent was deleted
169 169 # first).
170 170 # The issue is reloaded by the nested_set before being deleted so
171 171 # the lock_version condition should not be an issue but we handle it.
172 172 def destroy
173 173 super
174 174 rescue ActiveRecord::RecordNotFound
175 175 # Stale or already deleted
176 176 begin
177 177 reload
178 178 rescue ActiveRecord::RecordNotFound
179 179 # The issue was actually already deleted
180 180 @destroyed = true
181 181 return freeze
182 182 end
183 183 # The issue was stale, retry to destroy
184 184 super
185 185 end
186 186
187 187 alias :base_reload :reload
188 188 def reload(*args)
189 189 @workflow_rule_by_attribute = nil
190 190 @assignable_versions = nil
191 191 @relations = nil
192 192 base_reload(*args)
193 193 end
194 194
195 195 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
196 196 def available_custom_fields
197 197 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
198 198 end
199 199
200 200 # Copies attributes from another issue, arg can be an id or an Issue
201 201 def copy_from(arg, options={})
202 202 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
203 203 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
204 204 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
205 205 self.status = issue.status
206 206 self.author = User.current
207 207 unless options[:attachments] == false
208 208 self.attachments = issue.attachments.map do |attachement|
209 209 attachement.copy(:container => self)
210 210 end
211 211 end
212 212 @copied_from = issue
213 213 @copy_options = options
214 214 self
215 215 end
216 216
217 217 # Returns an unsaved copy of the issue
218 218 def copy(attributes=nil, copy_options={})
219 219 copy = self.class.new.copy_from(self, copy_options)
220 220 copy.attributes = attributes if attributes
221 221 copy
222 222 end
223 223
224 224 # Returns true if the issue is a copy
225 225 def copy?
226 226 @copied_from.present?
227 227 end
228 228
229 229 # Moves/copies an issue to a new project and tracker
230 230 # Returns the moved/copied issue on success, false on failure
231 231 def move_to_project(new_project, new_tracker=nil, options={})
232 232 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
233 233
234 234 if options[:copy]
235 235 issue = self.copy
236 236 else
237 237 issue = self
238 238 end
239 239
240 240 issue.init_journal(User.current, options[:notes])
241 241
242 242 # Preserve previous behaviour
243 243 # #move_to_project doesn't change tracker automatically
244 244 issue.send :project=, new_project, true
245 245 if new_tracker
246 246 issue.tracker = new_tracker
247 247 end
248 248 # Allow bulk setting of attributes on the issue
249 249 if options[:attributes]
250 250 issue.attributes = options[:attributes]
251 251 end
252 252
253 253 issue.save ? issue : false
254 254 end
255 255
256 256 def status_id=(sid)
257 257 self.status = nil
258 258 result = write_attribute(:status_id, sid)
259 259 @workflow_rule_by_attribute = nil
260 260 result
261 261 end
262 262
263 263 def priority_id=(pid)
264 264 self.priority = nil
265 265 write_attribute(:priority_id, pid)
266 266 end
267 267
268 268 def category_id=(cid)
269 269 self.category = nil
270 270 write_attribute(:category_id, cid)
271 271 end
272 272
273 273 def fixed_version_id=(vid)
274 274 self.fixed_version = nil
275 275 write_attribute(:fixed_version_id, vid)
276 276 end
277 277
278 278 def tracker_id=(tid)
279 279 self.tracker = nil
280 280 result = write_attribute(:tracker_id, tid)
281 281 @custom_field_values = nil
282 282 @workflow_rule_by_attribute = nil
283 283 result
284 284 end
285 285
286 286 def project_id=(project_id)
287 287 if project_id.to_s != self.project_id.to_s
288 288 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
289 289 end
290 290 end
291 291
292 292 def project=(project, keep_tracker=false)
293 293 project_was = self.project
294 294 write_attribute(:project_id, project ? project.id : nil)
295 295 association_instance_set('project', project)
296 296 if project_was && project && project_was != project
297 297 @assignable_versions = nil
298 298
299 299 unless keep_tracker || project.trackers.include?(tracker)
300 300 self.tracker = project.trackers.first
301 301 end
302 302 # Reassign to the category with same name if any
303 303 if category
304 304 self.category = project.issue_categories.find_by_name(category.name)
305 305 end
306 306 # Keep the fixed_version if it's still valid in the new_project
307 307 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
308 308 self.fixed_version = nil
309 309 end
310 310 # Clear the parent task if it's no longer valid
311 311 unless valid_parent_project?
312 312 self.parent_issue_id = nil
313 313 end
314 314 @custom_field_values = nil
315 315 end
316 316 end
317 317
318 318 def description=(arg)
319 319 if arg.is_a?(String)
320 320 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
321 321 end
322 322 write_attribute(:description, arg)
323 323 end
324 324
325 325 # Overrides assign_attributes so that project and tracker get assigned first
326 326 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
327 327 return if new_attributes.nil?
328 328 attrs = new_attributes.dup
329 329 attrs.stringify_keys!
330 330
331 331 %w(project project_id tracker tracker_id).each do |attr|
332 332 if attrs.has_key?(attr)
333 333 send "#{attr}=", attrs.delete(attr)
334 334 end
335 335 end
336 336 send :assign_attributes_without_project_and_tracker_first, attrs, *args
337 337 end
338 338 # Do not redefine alias chain on reload (see #4838)
339 339 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
340 340
341 341 def estimated_hours=(h)
342 342 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
343 343 end
344 344
345 345 safe_attributes 'project_id',
346 346 :if => lambda {|issue, user|
347 347 if issue.new_record?
348 348 issue.copy?
349 349 elsif user.allowed_to?(:move_issues, issue.project)
350 350 projects = Issue.allowed_target_projects_on_move(user)
351 351 projects.include?(issue.project) && projects.size > 1
352 352 end
353 353 }
354 354
355 355 safe_attributes 'tracker_id',
356 356 'status_id',
357 357 'category_id',
358 358 'assigned_to_id',
359 359 'priority_id',
360 360 'fixed_version_id',
361 361 'subject',
362 362 'description',
363 363 'start_date',
364 364 'due_date',
365 365 'done_ratio',
366 366 'estimated_hours',
367 367 'custom_field_values',
368 368 'custom_fields',
369 369 'lock_version',
370 370 'notes',
371 371 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
372 372
373 373 safe_attributes 'status_id',
374 374 'assigned_to_id',
375 375 'fixed_version_id',
376 376 'done_ratio',
377 377 'lock_version',
378 378 'notes',
379 379 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
380 380
381 381 safe_attributes 'notes',
382 382 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
383 383
384 384 safe_attributes 'private_notes',
385 385 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
386 386
387 387 safe_attributes 'watcher_user_ids',
388 388 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
389 389
390 390 safe_attributes 'is_private',
391 391 :if => lambda {|issue, user|
392 392 user.allowed_to?(:set_issues_private, issue.project) ||
393 393 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
394 394 }
395 395
396 396 safe_attributes 'parent_issue_id',
397 397 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
398 398 user.allowed_to?(:manage_subtasks, issue.project)}
399 399
400 400 def safe_attribute_names(user=nil)
401 401 names = super
402 402 names -= disabled_core_fields
403 403 names -= read_only_attribute_names(user)
404 404 names
405 405 end
406 406
407 407 # Safely sets attributes
408 408 # Should be called from controllers instead of #attributes=
409 409 # attr_accessible is too rough because we still want things like
410 410 # Issue.new(:project => foo) to work
411 411 def safe_attributes=(attrs, user=User.current)
412 412 return unless attrs.is_a?(Hash)
413 413
414 414 attrs = attrs.dup
415 415
416 416 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
417 417 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
418 418 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
419 419 self.project_id = p
420 420 end
421 421 end
422 422
423 423 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
424 424 self.tracker_id = t
425 425 end
426 426
427 427 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
428 428 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
429 429 self.status_id = s
430 430 end
431 431 end
432 432
433 433 attrs = delete_unsafe_attributes(attrs, user)
434 434 return if attrs.empty?
435 435
436 436 unless leaf?
437 437 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
438 438 end
439 439
440 440 if attrs['parent_issue_id'].present?
441 441 s = attrs['parent_issue_id'].to_s
442 442 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
443 443 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
444 444 end
445 445 end
446 446
447 447 if attrs['custom_field_values'].present?
448 448 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
449 449 end
450 450
451 451 if attrs['custom_fields'].present?
452 452 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
453 453 end
454 454
455 455 # mass-assignment security bypass
456 456 assign_attributes attrs, :without_protection => true
457 457 end
458 458
459 459 def disabled_core_fields
460 460 tracker ? tracker.disabled_core_fields : []
461 461 end
462 462
463 463 # Returns the custom_field_values that can be edited by the given user
464 464 def editable_custom_field_values(user=nil)
465 465 custom_field_values.reject do |value|
466 466 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
467 467 end
468 468 end
469 469
470 470 # Returns the names of attributes that are read-only for user or the current user
471 471 # For users with multiple roles, the read-only fields are the intersection of
472 472 # read-only fields of each role
473 473 # The result is an array of strings where sustom fields are represented with their ids
474 474 #
475 475 # Examples:
476 476 # issue.read_only_attribute_names # => ['due_date', '2']
477 477 # issue.read_only_attribute_names(user) # => []
478 478 def read_only_attribute_names(user=nil)
479 479 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
480 480 end
481 481
482 482 # Returns the names of required attributes for user or the current user
483 483 # For users with multiple roles, the required fields are the intersection of
484 484 # required fields of each role
485 485 # The result is an array of strings where sustom fields are represented with their ids
486 486 #
487 487 # Examples:
488 488 # issue.required_attribute_names # => ['due_date', '2']
489 489 # issue.required_attribute_names(user) # => []
490 490 def required_attribute_names(user=nil)
491 491 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
492 492 end
493 493
494 494 # Returns true if the attribute is required for user
495 495 def required_attribute?(name, user=nil)
496 496 required_attribute_names(user).include?(name.to_s)
497 497 end
498 498
499 499 # Returns a hash of the workflow rule by attribute for the given user
500 500 #
501 501 # Examples:
502 502 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
503 503 def workflow_rule_by_attribute(user=nil)
504 504 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
505 505
506 506 user_real = user || User.current
507 507 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
508 508 return {} if roles.empty?
509 509
510 510 result = {}
511 511 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
512 512 if workflow_permissions.any?
513 513 workflow_rules = workflow_permissions.inject({}) do |h, wp|
514 514 h[wp.field_name] ||= []
515 515 h[wp.field_name] << wp.rule
516 516 h
517 517 end
518 518 workflow_rules.each do |attr, rules|
519 519 next if rules.size < roles.size
520 520 uniq_rules = rules.uniq
521 521 if uniq_rules.size == 1
522 522 result[attr] = uniq_rules.first
523 523 else
524 524 result[attr] = 'required'
525 525 end
526 526 end
527 527 end
528 528 @workflow_rule_by_attribute = result if user.nil?
529 529 result
530 530 end
531 531 private :workflow_rule_by_attribute
532 532
533 533 def done_ratio
534 534 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
535 535 status.default_done_ratio
536 536 else
537 537 read_attribute(:done_ratio)
538 538 end
539 539 end
540 540
541 541 def self.use_status_for_done_ratio?
542 542 Setting.issue_done_ratio == 'issue_status'
543 543 end
544 544
545 545 def self.use_field_for_done_ratio?
546 546 Setting.issue_done_ratio == 'issue_field'
547 547 end
548 548
549 549 def validate_issue
550 550 if due_date && start_date && due_date < start_date
551 551 errors.add :due_date, :greater_than_start_date
552 552 end
553 553
554 554 if start_date && soonest_start && start_date < soonest_start
555 555 errors.add :start_date, :invalid
556 556 end
557 557
558 558 if fixed_version
559 559 if !assignable_versions.include?(fixed_version)
560 560 errors.add :fixed_version_id, :inclusion
561 561 elsif reopened? && fixed_version.closed?
562 562 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
563 563 end
564 564 end
565 565
566 566 # Checks that the issue can not be added/moved to a disabled tracker
567 567 if project && (tracker_id_changed? || project_id_changed?)
568 568 unless project.trackers.include?(tracker)
569 569 errors.add :tracker_id, :inclusion
570 570 end
571 571 end
572 572
573 573 # Checks parent issue assignment
574 574 if @invalid_parent_issue_id.present?
575 575 errors.add :parent_issue_id, :invalid
576 576 elsif @parent_issue
577 577 if !valid_parent_project?(@parent_issue)
578 578 errors.add :parent_issue_id, :invalid
579 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
580 errors.add :parent_issue_id, :invalid
579 581 elsif !new_record?
580 582 # moving an existing issue
581 583 if @parent_issue.root_id != root_id
582 584 # we can always move to another tree
583 585 elsif move_possible?(@parent_issue)
584 586 # move accepted inside tree
585 587 else
586 588 errors.add :parent_issue_id, :invalid
587 589 end
588 590 end
589 591 end
590 592 end
591 593
592 594 # Validates the issue against additional workflow requirements
593 595 def validate_required_fields
594 596 user = new_record? ? author : current_journal.try(:user)
595 597
596 598 required_attribute_names(user).each do |attribute|
597 599 if attribute =~ /^\d+$/
598 600 attribute = attribute.to_i
599 601 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
600 602 if v && v.value.blank?
601 603 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
602 604 end
603 605 else
604 606 if respond_to?(attribute) && send(attribute).blank?
605 607 errors.add attribute, :blank
606 608 end
607 609 end
608 610 end
609 611 end
610 612
611 613 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
612 614 # even if the user turns off the setting later
613 615 def update_done_ratio_from_issue_status
614 616 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
615 617 self.done_ratio = status.default_done_ratio
616 618 end
617 619 end
618 620
619 621 def init_journal(user, notes = "")
620 622 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
621 623 if new_record?
622 624 @current_journal.notify = false
623 625 else
624 626 @attributes_before_change = attributes.dup
625 627 @custom_values_before_change = {}
626 628 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
627 629 end
628 630 @current_journal
629 631 end
630 632
631 633 # Returns the id of the last journal or nil
632 634 def last_journal_id
633 635 if new_record?
634 636 nil
635 637 else
636 638 journals.maximum(:id)
637 639 end
638 640 end
639 641
640 642 # Returns a scope for journals that have an id greater than journal_id
641 643 def journals_after(journal_id)
642 644 scope = journals.reorder("#{Journal.table_name}.id ASC")
643 645 if journal_id.present?
644 646 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
645 647 end
646 648 scope
647 649 end
648 650
649 651 # Returns the initial status of the issue
650 652 # Returns nil for a new issue
651 653 def status_was
652 654 if status_id_was && status_id_was.to_i > 0
653 655 @status_was ||= IssueStatus.find_by_id(status_id_was)
654 656 end
655 657 end
656 658
657 659 # Return true if the issue is closed, otherwise false
658 660 def closed?
659 661 self.status.is_closed?
660 662 end
661 663
662 664 # Return true if the issue is being reopened
663 665 def reopened?
664 666 if !new_record? && status_id_changed?
665 667 status_was = IssueStatus.find_by_id(status_id_was)
666 668 status_new = IssueStatus.find_by_id(status_id)
667 669 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
668 670 return true
669 671 end
670 672 end
671 673 false
672 674 end
673 675
674 676 # Return true if the issue is being closed
675 677 def closing?
676 678 if !new_record? && status_id_changed?
677 679 if status_was && status && !status_was.is_closed? && status.is_closed?
678 680 return true
679 681 end
680 682 end
681 683 false
682 684 end
683 685
684 686 # Returns true if the issue is overdue
685 687 def overdue?
686 688 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
687 689 end
688 690
689 691 # Is the amount of work done less than it should for the due date
690 692 def behind_schedule?
691 693 return false if start_date.nil? || due_date.nil?
692 694 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
693 695 return done_date <= Date.today
694 696 end
695 697
696 698 # Does this issue have children?
697 699 def children?
698 700 !leaf?
699 701 end
700 702
701 703 # Users the issue can be assigned to
702 704 def assignable_users
703 705 users = project.assignable_users
704 706 users << author if author
705 707 users << assigned_to if assigned_to
706 708 users.uniq.sort
707 709 end
708 710
709 711 # Versions that the issue can be assigned to
710 712 def assignable_versions
711 713 return @assignable_versions if @assignable_versions
712 714
713 715 versions = project.shared_versions.open.all
714 716 if fixed_version
715 717 if fixed_version_id_changed?
716 718 # nothing to do
717 719 elsif project_id_changed?
718 720 if project.shared_versions.include?(fixed_version)
719 721 versions << fixed_version
720 722 end
721 723 else
722 724 versions << fixed_version
723 725 end
724 726 end
725 727 @assignable_versions = versions.uniq.sort
726 728 end
727 729
728 730 # Returns true if this issue is blocked by another issue that is still open
729 731 def blocked?
730 732 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
731 733 end
732 734
733 735 # Returns an array of statuses that user is able to apply
734 736 def new_statuses_allowed_to(user=User.current, include_default=false)
735 737 if new_record? && @copied_from
736 738 [IssueStatus.default, @copied_from.status].compact.uniq.sort
737 739 else
738 740 initial_status = nil
739 741 if new_record?
740 742 initial_status = IssueStatus.default
741 743 elsif status_id_was
742 744 initial_status = IssueStatus.find_by_id(status_id_was)
743 745 end
744 746 initial_status ||= status
745 747
746 748 statuses = initial_status.find_new_statuses_allowed_to(
747 749 user.admin ? Role.all : user.roles_for_project(project),
748 750 tracker,
749 751 author == user,
750 752 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
751 753 )
752 754 statuses << initial_status unless statuses.empty?
753 755 statuses << IssueStatus.default if include_default
754 756 statuses = statuses.compact.uniq.sort
755 757 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
756 758 end
757 759 end
758 760
759 761 def assigned_to_was
760 762 if assigned_to_id_changed? && assigned_to_id_was.present?
761 763 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
762 764 end
763 765 end
764 766
765 767 # Returns the users that should be notified
766 768 def notified_users
767 769 notified = []
768 770 # Author and assignee are always notified unless they have been
769 771 # locked or don't want to be notified
770 772 notified << author if author
771 773 if assigned_to
772 774 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
773 775 end
774 776 if assigned_to_was
775 777 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
776 778 end
777 779 notified = notified.select {|u| u.active? && u.notify_about?(self)}
778 780
779 781 notified += project.notified_users
780 782 notified.uniq!
781 783 # Remove users that can not view the issue
782 784 notified.reject! {|user| !visible?(user)}
783 785 notified
784 786 end
785 787
786 788 # Returns the email addresses that should be notified
787 789 def recipients
788 790 notified_users.collect(&:mail)
789 791 end
790 792
791 793 # Returns the number of hours spent on this issue
792 794 def spent_hours
793 795 @spent_hours ||= time_entries.sum(:hours) || 0
794 796 end
795 797
796 798 # Returns the total number of hours spent on this issue and its descendants
797 799 #
798 800 # Example:
799 801 # spent_hours => 0.0
800 802 # spent_hours => 50.2
801 803 def total_spent_hours
802 804 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
803 805 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
804 806 end
805 807
806 808 def relations
807 809 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
808 810 end
809 811
810 812 # Preloads relations for a collection of issues
811 813 def self.load_relations(issues)
812 814 if issues.any?
813 815 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
814 816 issues.each do |issue|
815 817 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
816 818 end
817 819 end
818 820 end
819 821
820 822 # Preloads visible spent time for a collection of issues
821 823 def self.load_visible_spent_hours(issues, user=User.current)
822 824 if issues.any?
823 825 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
824 826 issues.each do |issue|
825 827 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
826 828 end
827 829 end
828 830 end
829 831
830 832 # Preloads visible relations for a collection of issues
831 833 def self.load_visible_relations(issues, user=User.current)
832 834 if issues.any?
833 835 issue_ids = issues.map(&:id)
834 836 # Relations with issue_from in given issues and visible issue_to
835 837 relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
836 838 # Relations with issue_to in given issues and visible issue_from
837 839 relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
838 840
839 841 issues.each do |issue|
840 842 relations =
841 843 relations_from.select {|relation| relation.issue_from_id == issue.id} +
842 844 relations_to.select {|relation| relation.issue_to_id == issue.id}
843 845
844 846 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
845 847 end
846 848 end
847 849 end
848 850
849 851 # Finds an issue relation given its id.
850 852 def find_relation(relation_id)
851 853 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
852 854 end
853 855
856 # Returns all the other issues that depend on the issue
854 857 def all_dependent_issues(except=[])
855 858 except << self
856 859 dependencies = []
857 relations_from.each do |relation|
858 if relation.issue_to && !except.include?(relation.issue_to)
859 dependencies << relation.issue_to
860 dependencies += relation.issue_to.all_dependent_issues(except)
861 end
862 end
863 dependencies
860 dependencies += relations_from.map(&:issue_to)
861 dependencies += children unless leaf?
862 dependencies << parent
863 dependencies.compact!
864 dependencies -= except
865 dependencies + dependencies.map {|issue| issue.all_dependent_issues(except)}.flatten
864 866 end
865 867
866 868 # Returns an array of issues that duplicate this one
867 869 def duplicates
868 870 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
869 871 end
870 872
871 873 # Returns the due date or the target due date if any
872 874 # Used on gantt chart
873 875 def due_before
874 876 due_date || (fixed_version ? fixed_version.effective_date : nil)
875 877 end
876 878
877 879 # Returns the time scheduled for this issue.
878 880 #
879 881 # Example:
880 882 # Start Date: 2/26/09, End Date: 3/04/09
881 883 # duration => 6
882 884 def duration
883 885 (start_date && due_date) ? due_date - start_date : 0
884 886 end
885 887
886 888 # Returns the duration in working days
887 889 def working_duration
888 890 (start_date && due_date) ? working_days(start_date, due_date) : 0
889 891 end
890 892
891 893 def soonest_start(reload=false)
892 894 @soonest_start = nil if reload
893 895 @soonest_start ||= (
894 896 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
895 897 [(@parent_issue || parent).try(:soonest_start)]
896 898 ).compact.max
897 899 end
898 900
899 901 # Sets start_date on the given date or the next working day
900 902 # and changes due_date to keep the same working duration.
901 903 def reschedule_on(date)
902 904 wd = working_duration
903 905 date = next_working_date(date)
904 906 self.start_date = date
905 907 self.due_date = add_working_days(date, wd)
906 908 end
907 909
908 910 # Reschedules the issue on the given date or the next working day and saves the record.
909 911 # If the issue is a parent task, this is done by rescheduling its subtasks.
910 912 def reschedule_on!(date)
911 913 return if date.nil?
912 914 if leaf?
913 915 if start_date.nil? || start_date != date
914 916 if start_date && start_date > date
915 917 # Issue can not be moved earlier than its soonest start date
916 918 date = [soonest_start(true), date].compact.max
917 919 end
918 920 reschedule_on(date)
919 921 begin
920 922 save
921 923 rescue ActiveRecord::StaleObjectError
922 924 reload
923 925 reschedule_on(date)
924 926 save
925 927 end
926 928 end
927 929 else
928 930 leaves.each do |leaf|
929 931 if leaf.start_date
930 932 # Only move subtask if it starts at the same date as the parent
931 933 # or if it starts before the given date
932 934 if start_date == leaf.start_date || date > leaf.start_date
933 935 leaf.reschedule_on!(date)
934 936 end
935 937 else
936 938 leaf.reschedule_on!(date)
937 939 end
938 940 end
939 941 end
940 942 end
941 943
942 944 def <=>(issue)
943 945 if issue.nil?
944 946 -1
945 947 elsif root_id != issue.root_id
946 948 (root_id || 0) <=> (issue.root_id || 0)
947 949 else
948 950 (lft || 0) <=> (issue.lft || 0)
949 951 end
950 952 end
951 953
952 954 def to_s
953 955 "#{tracker} ##{id}: #{subject}"
954 956 end
955 957
956 958 # Returns a string of css classes that apply to the issue
957 959 def css_classes
958 960 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
959 961 s << ' closed' if closed?
960 962 s << ' overdue' if overdue?
961 963 s << ' child' if child?
962 964 s << ' parent' unless leaf?
963 965 s << ' private' if is_private?
964 966 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
965 967 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
966 968 s
967 969 end
968 970
969 971 # Saves an issue and a time_entry from the parameters
970 972 def save_issue_with_child_records(params, existing_time_entry=nil)
971 973 Issue.transaction do
972 974 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
973 975 @time_entry = existing_time_entry || TimeEntry.new
974 976 @time_entry.project = project
975 977 @time_entry.issue = self
976 978 @time_entry.user = User.current
977 979 @time_entry.spent_on = User.current.today
978 980 @time_entry.attributes = params[:time_entry]
979 981 self.time_entries << @time_entry
980 982 end
981 983
982 984 # TODO: Rename hook
983 985 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
984 986 if save
985 987 # TODO: Rename hook
986 988 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
987 989 else
988 990 raise ActiveRecord::Rollback
989 991 end
990 992 end
991 993 end
992 994
993 995 # Unassigns issues from +version+ if it's no longer shared with issue's project
994 996 def self.update_versions_from_sharing_change(version)
995 997 # Update issues assigned to the version
996 998 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
997 999 end
998 1000
999 1001 # Unassigns issues from versions that are no longer shared
1000 1002 # after +project+ was moved
1001 1003 def self.update_versions_from_hierarchy_change(project)
1002 1004 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1003 1005 # Update issues of the moved projects and issues assigned to a version of a moved project
1004 1006 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
1005 1007 end
1006 1008
1007 1009 def parent_issue_id=(arg)
1008 1010 s = arg.to_s.strip.presence
1009 1011 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1010 1012 @parent_issue.id
1011 1013 else
1012 1014 @parent_issue = nil
1013 1015 @invalid_parent_issue_id = arg
1014 1016 end
1015 1017 end
1016 1018
1017 1019 def parent_issue_id
1018 1020 if @invalid_parent_issue_id
1019 1021 @invalid_parent_issue_id
1020 1022 elsif instance_variable_defined? :@parent_issue
1021 1023 @parent_issue.nil? ? nil : @parent_issue.id
1022 1024 else
1023 1025 parent_id
1024 1026 end
1025 1027 end
1026 1028
1027 1029 # Returns true if issue's project is a valid
1028 1030 # parent issue project
1029 1031 def valid_parent_project?(issue=parent)
1030 1032 return true if issue.nil? || issue.project_id == project_id
1031 1033
1032 1034 case Setting.cross_project_subtasks
1033 1035 when 'system'
1034 1036 true
1035 1037 when 'tree'
1036 1038 issue.project.root == project.root
1037 1039 when 'hierarchy'
1038 1040 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1039 1041 when 'descendants'
1040 1042 issue.project.is_or_is_ancestor_of?(project)
1041 1043 else
1042 1044 false
1043 1045 end
1044 1046 end
1045 1047
1046 1048 # Extracted from the ReportsController.
1047 1049 def self.by_tracker(project)
1048 1050 count_and_group_by(:project => project,
1049 1051 :field => 'tracker_id',
1050 1052 :joins => Tracker.table_name)
1051 1053 end
1052 1054
1053 1055 def self.by_version(project)
1054 1056 count_and_group_by(:project => project,
1055 1057 :field => 'fixed_version_id',
1056 1058 :joins => Version.table_name)
1057 1059 end
1058 1060
1059 1061 def self.by_priority(project)
1060 1062 count_and_group_by(:project => project,
1061 1063 :field => 'priority_id',
1062 1064 :joins => IssuePriority.table_name)
1063 1065 end
1064 1066
1065 1067 def self.by_category(project)
1066 1068 count_and_group_by(:project => project,
1067 1069 :field => 'category_id',
1068 1070 :joins => IssueCategory.table_name)
1069 1071 end
1070 1072
1071 1073 def self.by_assigned_to(project)
1072 1074 count_and_group_by(:project => project,
1073 1075 :field => 'assigned_to_id',
1074 1076 :joins => User.table_name)
1075 1077 end
1076 1078
1077 1079 def self.by_author(project)
1078 1080 count_and_group_by(:project => project,
1079 1081 :field => 'author_id',
1080 1082 :joins => User.table_name)
1081 1083 end
1082 1084
1083 1085 def self.by_subproject(project)
1084 1086 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1085 1087 s.is_closed as closed,
1086 1088 #{Issue.table_name}.project_id as project_id,
1087 1089 count(#{Issue.table_name}.id) as total
1088 1090 from
1089 1091 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
1090 1092 where
1091 1093 #{Issue.table_name}.status_id=s.id
1092 1094 and #{Issue.table_name}.project_id = #{Project.table_name}.id
1093 1095 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
1094 1096 and #{Issue.table_name}.project_id <> #{project.id}
1095 1097 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
1096 1098 end
1097 1099 # End ReportsController extraction
1098 1100
1099 1101 # Returns an array of projects that user can assign the issue to
1100 1102 def allowed_target_projects(user=User.current)
1101 1103 if new_record?
1102 1104 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
1103 1105 else
1104 1106 self.class.allowed_target_projects_on_move(user)
1105 1107 end
1106 1108 end
1107 1109
1108 1110 # Returns an array of projects that user can move issues to
1109 1111 def self.allowed_target_projects_on_move(user=User.current)
1110 1112 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
1111 1113 end
1112 1114
1113 1115 private
1114 1116
1115 1117 def after_project_change
1116 1118 # Update project_id on related time entries
1117 1119 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
1118 1120
1119 1121 # Delete issue relations
1120 1122 unless Setting.cross_project_issue_relations?
1121 1123 relations_from.clear
1122 1124 relations_to.clear
1123 1125 end
1124 1126
1125 1127 # Move subtasks that were in the same project
1126 1128 children.each do |child|
1127 1129 next unless child.project_id == project_id_was
1128 1130 # Change project and keep project
1129 1131 child.send :project=, project, true
1130 1132 unless child.save
1131 1133 raise ActiveRecord::Rollback
1132 1134 end
1133 1135 end
1134 1136 end
1135 1137
1136 1138 # Callback for after the creation of an issue by copy
1137 1139 # * adds a "copied to" relation with the copied issue
1138 1140 # * copies subtasks from the copied issue
1139 1141 def after_create_from_copy
1140 1142 return unless copy? && !@after_create_from_copy_handled
1141 1143
1142 1144 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1143 1145 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1144 1146 unless relation.save
1145 1147 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1146 1148 end
1147 1149 end
1148 1150
1149 1151 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1150 1152 copy_options = (@copy_options || {}).merge(:subtasks => false)
1151 1153 copied_issue_ids = {@copied_from.id => self.id}
1152 1154 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1153 1155 # Do not copy self when copying an issue as a descendant of the copied issue
1154 1156 next if child == self
1155 1157 # Do not copy subtasks of issues that were not copied
1156 1158 next unless copied_issue_ids[child.parent_id]
1157 1159 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1158 1160 unless child.visible?
1159 1161 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1160 1162 next
1161 1163 end
1162 1164 copy = Issue.new.copy_from(child, copy_options)
1163 1165 copy.author = author
1164 1166 copy.project = project
1165 1167 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1166 1168 unless copy.save
1167 1169 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1168 1170 next
1169 1171 end
1170 1172 copied_issue_ids[child.id] = copy.id
1171 1173 end
1172 1174 end
1173 1175 @after_create_from_copy_handled = true
1174 1176 end
1175 1177
1176 1178 def update_nested_set_attributes
1177 1179 if root_id.nil?
1178 1180 # issue was just created
1179 1181 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1180 1182 set_default_left_and_right
1181 1183 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
1182 1184 if @parent_issue
1183 1185 move_to_child_of(@parent_issue)
1184 1186 end
1185 1187 reload
1186 1188 elsif parent_issue_id != parent_id
1187 1189 former_parent_id = parent_id
1188 1190 # moving an existing issue
1189 1191 if @parent_issue && @parent_issue.root_id == root_id
1190 1192 # inside the same tree
1191 1193 move_to_child_of(@parent_issue)
1192 1194 else
1193 1195 # to another tree
1194 1196 unless root?
1195 1197 move_to_right_of(root)
1196 1198 reload
1197 1199 end
1198 1200 old_root_id = root_id
1199 1201 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1200 1202 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1201 1203 offset = target_maxright + 1 - lft
1202 1204 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
1203 1205 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1204 1206 self[left_column_name] = lft + offset
1205 1207 self[right_column_name] = rgt + offset
1206 1208 if @parent_issue
1207 1209 move_to_child_of(@parent_issue)
1208 1210 end
1209 1211 end
1210 1212 reload
1211 1213 # delete invalid relations of all descendants
1212 1214 self_and_descendants.each do |issue|
1213 1215 issue.relations.each do |relation|
1214 1216 relation.destroy unless relation.valid?
1215 1217 end
1216 1218 end
1217 1219 # update former parent
1218 1220 recalculate_attributes_for(former_parent_id) if former_parent_id
1219 1221 end
1220 1222 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1221 1223 end
1222 1224
1223 1225 def update_parent_attributes
1224 1226 recalculate_attributes_for(parent_id) if parent_id
1225 1227 end
1226 1228
1227 1229 def recalculate_attributes_for(issue_id)
1228 1230 if issue_id && p = Issue.find_by_id(issue_id)
1229 1231 # priority = highest priority of children
1230 1232 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1231 1233 p.priority = IssuePriority.find_by_position(priority_position)
1232 1234 end
1233 1235
1234 1236 # start/due dates = lowest/highest dates of children
1235 1237 p.start_date = p.children.minimum(:start_date)
1236 1238 p.due_date = p.children.maximum(:due_date)
1237 1239 if p.start_date && p.due_date && p.due_date < p.start_date
1238 1240 p.start_date, p.due_date = p.due_date, p.start_date
1239 1241 end
1240 1242
1241 1243 # done ratio = weighted average ratio of leaves
1242 1244 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1243 1245 leaves_count = p.leaves.count
1244 1246 if leaves_count > 0
1245 1247 average = p.leaves.average(:estimated_hours).to_f
1246 1248 if average == 0
1247 1249 average = 1
1248 1250 end
1249 1251 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
1250 1252 progress = done / (average * leaves_count)
1251 1253 p.done_ratio = progress.round
1252 1254 end
1253 1255 end
1254 1256
1255 1257 # estimate = sum of leaves estimates
1256 1258 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1257 1259 p.estimated_hours = nil if p.estimated_hours == 0.0
1258 1260
1259 1261 # ancestors will be recursively updated
1260 1262 p.save(:validate => false)
1261 1263 end
1262 1264 end
1263 1265
1264 1266 # Update issues so their versions are not pointing to a
1265 1267 # fixed_version that is not shared with the issue's project
1266 1268 def self.update_versions(conditions=nil)
1267 1269 # Only need to update issues with a fixed_version from
1268 1270 # a different project and that is not systemwide shared
1269 1271 Issue.scoped(:conditions => conditions).all(
1270 1272 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1271 1273 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1272 1274 " AND #{Version.table_name}.sharing <> 'system'",
1273 1275 :include => [:project, :fixed_version]
1274 1276 ).each do |issue|
1275 1277 next if issue.project.nil? || issue.fixed_version.nil?
1276 1278 unless issue.project.shared_versions.include?(issue.fixed_version)
1277 1279 issue.init_journal(User.current)
1278 1280 issue.fixed_version = nil
1279 1281 issue.save
1280 1282 end
1281 1283 end
1282 1284 end
1283 1285
1284 1286 # Callback on file attachment
1285 1287 def attachment_added(obj)
1286 1288 if @current_journal && !obj.new_record?
1287 1289 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1288 1290 end
1289 1291 end
1290 1292
1291 1293 # Callback on attachment deletion
1292 1294 def attachment_removed(obj)
1293 1295 if @current_journal && !obj.new_record?
1294 1296 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1295 1297 @current_journal.save
1296 1298 end
1297 1299 end
1298 1300
1299 1301 # Default assignment based on category
1300 1302 def default_assign
1301 1303 if assigned_to.nil? && category && category.assigned_to
1302 1304 self.assigned_to = category.assigned_to
1303 1305 end
1304 1306 end
1305 1307
1306 1308 # Updates start/due dates of following issues
1307 1309 def reschedule_following_issues
1308 1310 if start_date_changed? || due_date_changed?
1309 1311 relations_from.each do |relation|
1310 1312 relation.set_issue_to_dates
1311 1313 end
1312 1314 end
1313 1315 end
1314 1316
1315 1317 # Closes duplicates if the issue is being closed
1316 1318 def close_duplicates
1317 1319 if closing?
1318 1320 duplicates.each do |duplicate|
1319 1321 # Reload is need in case the duplicate was updated by a previous duplicate
1320 1322 duplicate.reload
1321 1323 # Don't re-close it if it's already closed
1322 1324 next if duplicate.closed?
1323 1325 # Same user and notes
1324 1326 if @current_journal
1325 1327 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1326 1328 end
1327 1329 duplicate.update_attribute :status, self.status
1328 1330 end
1329 1331 end
1330 1332 end
1331 1333
1332 1334 # Make sure updated_on is updated when adding a note and set updated_on now
1333 1335 # so we can set closed_on with the same value on closing
1334 1336 def force_updated_on_change
1335 1337 if @current_journal || changed?
1336 1338 self.updated_on = current_time_from_proper_timezone
1337 1339 if new_record?
1338 1340 self.created_on = updated_on
1339 1341 end
1340 1342 end
1341 1343 end
1342 1344
1343 1345 # Callback for setting closed_on when the issue is closed.
1344 1346 # The closed_on attribute stores the time of the last closing
1345 1347 # and is preserved when the issue is reopened.
1346 1348 def update_closed_on
1347 1349 if closing? || (new_record? && closed?)
1348 1350 self.closed_on = updated_on
1349 1351 end
1350 1352 end
1351 1353
1352 1354 # Saves the changes in a Journal
1353 1355 # Called after_save
1354 1356 def create_journal
1355 1357 if @current_journal
1356 1358 # attributes changes
1357 1359 if @attributes_before_change
1358 1360 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)).each {|c|
1359 1361 before = @attributes_before_change[c]
1360 1362 after = send(c)
1361 1363 next if before == after || (before.blank? && after.blank?)
1362 1364 @current_journal.details << JournalDetail.new(:property => 'attr',
1363 1365 :prop_key => c,
1364 1366 :old_value => before,
1365 1367 :value => after)
1366 1368 }
1367 1369 end
1368 1370 if @custom_values_before_change
1369 1371 # custom fields changes
1370 1372 custom_field_values.each {|c|
1371 1373 before = @custom_values_before_change[c.custom_field_id]
1372 1374 after = c.value
1373 1375 next if before == after || (before.blank? && after.blank?)
1374 1376
1375 1377 if before.is_a?(Array) || after.is_a?(Array)
1376 1378 before = [before] unless before.is_a?(Array)
1377 1379 after = [after] unless after.is_a?(Array)
1378 1380
1379 1381 # values removed
1380 1382 (before - after).reject(&:blank?).each do |value|
1381 1383 @current_journal.details << JournalDetail.new(:property => 'cf',
1382 1384 :prop_key => c.custom_field_id,
1383 1385 :old_value => value,
1384 1386 :value => nil)
1385 1387 end
1386 1388 # values added
1387 1389 (after - before).reject(&:blank?).each do |value|
1388 1390 @current_journal.details << JournalDetail.new(:property => 'cf',
1389 1391 :prop_key => c.custom_field_id,
1390 1392 :old_value => nil,
1391 1393 :value => value)
1392 1394 end
1393 1395 else
1394 1396 @current_journal.details << JournalDetail.new(:property => 'cf',
1395 1397 :prop_key => c.custom_field_id,
1396 1398 :old_value => before,
1397 1399 :value => after)
1398 1400 end
1399 1401 }
1400 1402 end
1401 1403 @current_journal.save
1402 1404 # reset current journal
1403 1405 init_journal @current_journal.user, @current_journal.notes
1404 1406 end
1405 1407 end
1406 1408
1407 1409 # Query generator for selecting groups of issue counts for a project
1408 1410 # based on specific criteria
1409 1411 #
1410 1412 # Options
1411 1413 # * project - Project to search in.
1412 1414 # * field - String. Issue field to key off of in the grouping.
1413 1415 # * joins - String. The table name to join against.
1414 1416 def self.count_and_group_by(options)
1415 1417 project = options.delete(:project)
1416 1418 select_field = options.delete(:field)
1417 1419 joins = options.delete(:joins)
1418 1420
1419 1421 where = "#{Issue.table_name}.#{select_field}=j.id"
1420 1422
1421 1423 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1422 1424 s.is_closed as closed,
1423 1425 j.id as #{select_field},
1424 1426 count(#{Issue.table_name}.id) as total
1425 1427 from
1426 1428 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1427 1429 where
1428 1430 #{Issue.table_name}.status_id=s.id
1429 1431 and #{where}
1430 1432 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1431 1433 and #{visible_condition(User.current, :project => project)}
1432 1434 group by s.id, s.is_closed, j.id")
1433 1435 end
1434 1436 end
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,374 +1,358
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 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 IssueNestedSetTest < ActiveSupport::TestCase
21 21 fixtures :projects, :users, :roles,
22 22 :trackers, :projects_trackers,
23 23 :issue_statuses, :issue_categories, :issue_relations,
24 24 :enumerations,
25 25 :issues
26 26
27 27 def test_create_root_issue
28 28 issue1 = Issue.generate!
29 29 issue2 = Issue.generate!
30 30 issue1.reload
31 31 issue2.reload
32 32
33 33 assert_equal [issue1.id, nil, 1, 2], [issue1.root_id, issue1.parent_id, issue1.lft, issue1.rgt]
34 34 assert_equal [issue2.id, nil, 1, 2], [issue2.root_id, issue2.parent_id, issue2.lft, issue2.rgt]
35 35 end
36 36
37 37 def test_create_child_issue
38 38 parent = Issue.generate!
39 39 child = Issue.generate!(:parent_issue_id => parent.id)
40 40 parent.reload
41 41 child.reload
42 42
43 43 assert_equal [parent.id, nil, 1, 4], [parent.root_id, parent.parent_id, parent.lft, parent.rgt]
44 44 assert_equal [parent.id, parent.id, 2, 3], [child.root_id, child.parent_id, child.lft, child.rgt]
45 45 end
46 46
47 47 def test_creating_a_child_in_a_subproject_should_validate
48 48 issue = Issue.generate!
49 49 child = Issue.new(:project_id => 3, :tracker_id => 2, :author_id => 1,
50 50 :subject => 'child', :parent_issue_id => issue.id)
51 51 assert_save child
52 52 assert_equal issue, child.reload.parent
53 53 end
54 54
55 55 def test_creating_a_child_in_an_invalid_project_should_not_validate
56 56 issue = Issue.generate!
57 57 child = Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
58 58 :subject => 'child', :parent_issue_id => issue.id)
59 59 assert !child.save
60 60 assert_not_nil child.errors[:parent_issue_id]
61 61 end
62 62
63 63 def test_move_a_root_to_child
64 64 parent1 = Issue.generate!
65 65 parent2 = Issue.generate!
66 66 child = Issue.generate!(:parent_issue_id => parent1.id)
67 67
68 68 parent2.parent_issue_id = parent1.id
69 69 parent2.save!
70 70 child.reload
71 71 parent1.reload
72 72 parent2.reload
73 73
74 74 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
75 75 assert_equal [parent1.id, 4, 5], [parent2.root_id, parent2.lft, parent2.rgt]
76 76 assert_equal [parent1.id, 2, 3], [child.root_id, child.lft, child.rgt]
77 77 end
78 78
79 79 def test_move_a_child_to_root
80 80 parent1 = Issue.generate!
81 81 parent2 = Issue.generate!
82 82 child = Issue.generate!(:parent_issue_id => parent1.id)
83 83
84 84 child.parent_issue_id = nil
85 85 child.save!
86 86 child.reload
87 87 parent1.reload
88 88 parent2.reload
89 89
90 90 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
91 91 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
92 92 assert_equal [child.id, 1, 2], [child.root_id, child.lft, child.rgt]
93 93 end
94 94
95 95 def test_move_a_child_to_another_issue
96 96 parent1 = Issue.generate!
97 97 parent2 = Issue.generate!
98 98 child = Issue.generate!(:parent_issue_id => parent1.id)
99 99
100 100 child.parent_issue_id = parent2.id
101 101 child.save!
102 102 child.reload
103 103 parent1.reload
104 104 parent2.reload
105 105
106 106 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
107 107 assert_equal [parent2.id, 1, 4], [parent2.root_id, parent2.lft, parent2.rgt]
108 108 assert_equal [parent2.id, 2, 3], [child.root_id, child.lft, child.rgt]
109 109 end
110 110
111 111 def test_move_a_child_with_descendants_to_another_issue
112 112 parent1 = Issue.generate!
113 113 parent2 = Issue.generate!
114 114 child = Issue.generate!(:parent_issue_id => parent1.id)
115 115 grandchild = Issue.generate!(:parent_issue_id => child.id)
116 116
117 117 parent1.reload
118 118 parent2.reload
119 119 child.reload
120 120 grandchild.reload
121 121
122 122 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
123 123 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
124 124 assert_equal [parent1.id, 2, 5], [child.root_id, child.lft, child.rgt]
125 125 assert_equal [parent1.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
126 126
127 127 child.reload.parent_issue_id = parent2.id
128 128 child.save!
129 129 child.reload
130 130 grandchild.reload
131 131 parent1.reload
132 132 parent2.reload
133 133
134 134 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
135 135 assert_equal [parent2.id, 1, 6], [parent2.root_id, parent2.lft, parent2.rgt]
136 136 assert_equal [parent2.id, 2, 5], [child.root_id, child.lft, child.rgt]
137 137 assert_equal [parent2.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
138 138 end
139 139
140 140 def test_move_a_child_with_descendants_to_another_project
141 141 parent1 = Issue.generate!
142 142 child = Issue.generate!(:parent_issue_id => parent1.id)
143 143 grandchild = Issue.generate!(:parent_issue_id => child.id)
144 144
145 145 child.reload
146 146 child.project = Project.find(2)
147 147 assert child.save
148 148 child.reload
149 149 grandchild.reload
150 150 parent1.reload
151 151
152 152 assert_equal [1, parent1.id, 1, 2], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
153 153 assert_equal [2, child.id, 1, 4], [child.project_id, child.root_id, child.lft, child.rgt]
154 154 assert_equal [2, child.id, 2, 3], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
155 155 end
156 156
157 157 def test_moving_an_issue_to_a_descendant_should_not_validate
158 158 parent1 = Issue.generate!
159 159 parent2 = Issue.generate!
160 160 child = Issue.generate!(:parent_issue_id => parent1.id)
161 161 grandchild = Issue.generate!(:parent_issue_id => child.id)
162 162
163 163 child.reload
164 164 child.parent_issue_id = grandchild.id
165 165 assert !child.save
166 166 assert_not_nil child.errors[:parent_issue_id]
167 167 end
168 168
169 def test_moving_an_issue_should_keep_valid_relations_only
170 issue1 = Issue.generate!
171 issue2 = Issue.generate!
172 issue3 = Issue.generate!(:parent_issue_id => issue2.id)
173 issue4 = Issue.generate!
174 r1 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
175 r2 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES)
176 r3 = IssueRelation.create!(:issue_from => issue2, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
177 issue2.reload
178 issue2.parent_issue_id = issue1.id
179 issue2.save!
180 assert !IssueRelation.exists?(r1.id)
181 assert !IssueRelation.exists?(r2.id)
182 assert IssueRelation.exists?(r3.id)
183 end
184
185 169 def test_destroy_should_destroy_children
186 170 issue1 = Issue.generate!
187 171 issue2 = Issue.generate!
188 172 issue3 = Issue.generate!(:parent_issue_id => issue2.id)
189 173 issue4 = Issue.generate!(:parent_issue_id => issue1.id)
190 174
191 175 issue3.init_journal(User.find(2))
192 176 issue3.subject = 'child with journal'
193 177 issue3.save!
194 178
195 179 assert_difference 'Issue.count', -2 do
196 180 assert_difference 'Journal.count', -1 do
197 181 assert_difference 'JournalDetail.count', -1 do
198 182 Issue.find(issue2.id).destroy
199 183 end
200 184 end
201 185 end
202 186
203 187 issue1.reload
204 188 issue4.reload
205 189 assert !Issue.exists?(issue2.id)
206 190 assert !Issue.exists?(issue3.id)
207 191 assert_equal [issue1.id, 1, 4], [issue1.root_id, issue1.lft, issue1.rgt]
208 192 assert_equal [issue1.id, 2, 3], [issue4.root_id, issue4.lft, issue4.rgt]
209 193 end
210 194
211 195 def test_destroy_child_should_update_parent
212 196 issue = Issue.generate!
213 197 child1 = Issue.generate!(:parent_issue_id => issue.id)
214 198 child2 = Issue.generate!(:parent_issue_id => issue.id)
215 199
216 200 issue.reload
217 201 assert_equal [issue.id, 1, 6], [issue.root_id, issue.lft, issue.rgt]
218 202
219 203 child2.reload.destroy
220 204
221 205 issue.reload
222 206 assert_equal [issue.id, 1, 4], [issue.root_id, issue.lft, issue.rgt]
223 207 end
224 208
225 209 def test_destroy_parent_issue_updated_during_children_destroy
226 210 parent = Issue.generate!
227 211 Issue.generate!(:start_date => Date.today, :parent_issue_id => parent.id)
228 212 Issue.generate!(:start_date => 2.days.from_now, :parent_issue_id => parent.id)
229 213
230 214 assert_difference 'Issue.count', -3 do
231 215 Issue.find(parent.id).destroy
232 216 end
233 217 end
234 218
235 219 def test_destroy_child_issue_with_children
236 220 root = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'root')
237 221 child = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => root.id)
238 222 leaf = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'leaf', :parent_issue_id => child.id)
239 223 leaf.init_journal(User.find(2))
240 224 leaf.subject = 'leaf with journal'
241 225 leaf.save!
242 226
243 227 assert_difference 'Issue.count', -2 do
244 228 assert_difference 'Journal.count', -1 do
245 229 assert_difference 'JournalDetail.count', -1 do
246 230 Issue.find(child.id).destroy
247 231 end
248 232 end
249 233 end
250 234
251 235 root = Issue.find(root.id)
252 236 assert root.leaf?, "Root issue is not a leaf (lft: #{root.lft}, rgt: #{root.rgt})"
253 237 end
254 238
255 239 def test_destroy_issue_with_grand_child
256 240 parent = Issue.generate!
257 241 issue = Issue.generate!(:parent_issue_id => parent.id)
258 242 child = Issue.generate!(:parent_issue_id => issue.id)
259 243 grandchild1 = Issue.generate!(:parent_issue_id => child.id)
260 244 grandchild2 = Issue.generate!(:parent_issue_id => child.id)
261 245
262 246 assert_difference 'Issue.count', -4 do
263 247 Issue.find(issue.id).destroy
264 248 parent.reload
265 249 assert_equal [1, 2], [parent.lft, parent.rgt]
266 250 end
267 251 end
268 252
269 253 def test_parent_priority_should_be_the_highest_child_priority
270 254 parent = Issue.generate!(:priority => IssuePriority.find_by_name('Normal'))
271 255 # Create children
272 256 child1 = Issue.generate!(:priority => IssuePriority.find_by_name('High'), :parent_issue_id => parent.id)
273 257 assert_equal 'High', parent.reload.priority.name
274 258 child2 = Issue.generate!(:priority => IssuePriority.find_by_name('Immediate'), :parent_issue_id => child1.id)
275 259 assert_equal 'Immediate', child1.reload.priority.name
276 260 assert_equal 'Immediate', parent.reload.priority.name
277 261 child3 = Issue.generate!(:priority => IssuePriority.find_by_name('Low'), :parent_issue_id => parent.id)
278 262 assert_equal 'Immediate', parent.reload.priority.name
279 263 # Destroy a child
280 264 child1.destroy
281 265 assert_equal 'Low', parent.reload.priority.name
282 266 # Update a child
283 267 child3.reload.priority = IssuePriority.find_by_name('Normal')
284 268 child3.save!
285 269 assert_equal 'Normal', parent.reload.priority.name
286 270 end
287 271
288 272 def test_parent_dates_should_be_lowest_start_and_highest_due_dates
289 273 parent = Issue.generate!
290 274 Issue.generate!(:start_date => '2010-01-25', :due_date => '2010-02-15', :parent_issue_id => parent.id)
291 275 Issue.generate!( :due_date => '2010-02-13', :parent_issue_id => parent.id)
292 276 Issue.generate!(:start_date => '2010-02-01', :due_date => '2010-02-22', :parent_issue_id => parent.id)
293 277 parent.reload
294 278 assert_equal Date.parse('2010-01-25'), parent.start_date
295 279 assert_equal Date.parse('2010-02-22'), parent.due_date
296 280 end
297 281
298 282 def test_parent_done_ratio_should_be_average_done_ratio_of_leaves
299 283 parent = Issue.generate!
300 284 Issue.generate!(:done_ratio => 20, :parent_issue_id => parent.id)
301 285 assert_equal 20, parent.reload.done_ratio
302 286 Issue.generate!(:done_ratio => 70, :parent_issue_id => parent.id)
303 287 assert_equal 45, parent.reload.done_ratio
304 288
305 289 child = Issue.generate!(:done_ratio => 0, :parent_issue_id => parent.id)
306 290 assert_equal 30, parent.reload.done_ratio
307 291
308 292 Issue.generate!(:done_ratio => 30, :parent_issue_id => child.id)
309 293 assert_equal 30, child.reload.done_ratio
310 294 assert_equal 40, parent.reload.done_ratio
311 295 end
312 296
313 297 def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any
314 298 parent = Issue.generate!
315 299 Issue.generate!(:estimated_hours => 10, :done_ratio => 20, :parent_issue_id => parent.id)
316 300 assert_equal 20, parent.reload.done_ratio
317 301 Issue.generate!(:estimated_hours => 20, :done_ratio => 50, :parent_issue_id => parent.id)
318 302 assert_equal (50 * 20 + 20 * 10) / 30, parent.reload.done_ratio
319 303 end
320 304
321 305 def test_parent_estimate_should_be_sum_of_leaves
322 306 parent = Issue.generate!
323 307 Issue.generate!(:estimated_hours => nil, :parent_issue_id => parent.id)
324 308 assert_equal nil, parent.reload.estimated_hours
325 309 Issue.generate!(:estimated_hours => 5, :parent_issue_id => parent.id)
326 310 assert_equal 5, parent.reload.estimated_hours
327 311 Issue.generate!(:estimated_hours => 7, :parent_issue_id => parent.id)
328 312 assert_equal 12, parent.reload.estimated_hours
329 313 end
330 314
331 315 def test_move_parent_updates_old_parent_attributes
332 316 first_parent = Issue.generate!
333 317 second_parent = Issue.generate!
334 318 child = Issue.generate!(:estimated_hours => 5, :parent_issue_id => first_parent.id)
335 319 assert_equal 5, first_parent.reload.estimated_hours
336 320 child.update_attributes(:estimated_hours => 7, :parent_issue_id => second_parent.id)
337 321 assert_equal 7, second_parent.reload.estimated_hours
338 322 assert_nil first_parent.reload.estimated_hours
339 323 end
340 324
341 325 def test_reschuling_a_parent_should_reschedule_subtasks
342 326 parent = Issue.generate!
343 327 c1 = Issue.generate!(:start_date => '2010-05-12', :due_date => '2010-05-18', :parent_issue_id => parent.id)
344 328 c2 = Issue.generate!(:start_date => '2010-06-03', :due_date => '2010-06-10', :parent_issue_id => parent.id)
345 329 parent.reload
346 330 parent.reschedule_on!(Date.parse('2010-06-02'))
347 331 c1.reload
348 332 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-08')], [c1.start_date, c1.due_date]
349 333 c2.reload
350 334 assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date] # no change
351 335 parent.reload
352 336 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date]
353 337 end
354 338
355 339 def test_project_copy_should_copy_issue_tree
356 340 p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2])
357 341 i1 = Issue.generate!(:project => p, :subject => 'i1')
358 342 i2 = Issue.generate!(:project => p, :subject => 'i2', :parent_issue_id => i1.id)
359 343 i3 = Issue.generate!(:project => p, :subject => 'i3', :parent_issue_id => i1.id)
360 344 i4 = Issue.generate!(:project => p, :subject => 'i4', :parent_issue_id => i2.id)
361 345 i5 = Issue.generate!(:project => p, :subject => 'i5')
362 346 c = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1, 2])
363 347 c.copy(p, :only => 'issues')
364 348 c.reload
365 349
366 350 assert_equal 5, c.issues.count
367 351 ic1, ic2, ic3, ic4, ic5 = c.issues.order('subject').all
368 352 assert ic1.root?
369 353 assert_equal ic1, ic2.parent
370 354 assert_equal ic1, ic3.parent
371 355 assert_equal ic2, ic4.parent
372 356 assert ic5.root?
373 357 end
374 358 end
@@ -1,135 +1,157
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 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 IssueRelationTest < ActiveSupport::TestCase
21 21 fixtures :projects,
22 22 :users,
23 23 :roles,
24 24 :members,
25 25 :member_roles,
26 26 :issues,
27 27 :issue_statuses,
28 28 :issue_relations,
29 29 :enabled_modules,
30 30 :enumerations,
31 31 :trackers
32 32
33 include Redmine::I18n
34
33 35 def test_create
34 36 from = Issue.find(1)
35 37 to = Issue.find(2)
36 38
37 39 relation = IssueRelation.new :issue_from => from, :issue_to => to,
38 40 :relation_type => IssueRelation::TYPE_PRECEDES
39 41 assert relation.save
40 42 relation.reload
41 43 assert_equal IssueRelation::TYPE_PRECEDES, relation.relation_type
42 44 assert_equal from, relation.issue_from
43 45 assert_equal to, relation.issue_to
44 46 end
45 47
46 48 def test_create_minimum
47 49 relation = IssueRelation.new :issue_from => Issue.find(1), :issue_to => Issue.find(2)
48 50 assert relation.save
49 51 assert_equal IssueRelation::TYPE_RELATES, relation.relation_type
50 52 end
51 53
52 54 def test_follows_relation_should_be_reversed
53 55 from = Issue.find(1)
54 56 to = Issue.find(2)
55 57
56 58 relation = IssueRelation.new :issue_from => from, :issue_to => to,
57 59 :relation_type => IssueRelation::TYPE_FOLLOWS
58 60 assert relation.save
59 61 relation.reload
60 62 assert_equal IssueRelation::TYPE_PRECEDES, relation.relation_type
61 63 assert_equal to, relation.issue_from
62 64 assert_equal from, relation.issue_to
63 65 end
64 66
65 67 def test_follows_relation_should_not_be_reversed_if_validation_fails
66 68 from = Issue.find(1)
67 69 to = Issue.find(2)
68 70
69 71 relation = IssueRelation.new :issue_from => from, :issue_to => to,
70 72 :relation_type => IssueRelation::TYPE_FOLLOWS,
71 73 :delay => 'xx'
72 74 assert !relation.save
73 75 assert_equal IssueRelation::TYPE_FOLLOWS, relation.relation_type
74 76 assert_equal from, relation.issue_from
75 77 assert_equal to, relation.issue_to
76 78 end
77 79
78 80 def test_relation_type_for
79 81 from = Issue.find(1)
80 82 to = Issue.find(2)
81 83
82 84 relation = IssueRelation.new :issue_from => from, :issue_to => to,
83 85 :relation_type => IssueRelation::TYPE_PRECEDES
84 86 assert_equal IssueRelation::TYPE_PRECEDES, relation.relation_type_for(from)
85 87 assert_equal IssueRelation::TYPE_FOLLOWS, relation.relation_type_for(to)
86 88 end
87 89
88 90 def test_set_issue_to_dates_without_issue_to
89 91 r = IssueRelation.new(:issue_from => Issue.new(:start_date => Date.today),
90 92 :relation_type => IssueRelation::TYPE_PRECEDES,
91 93 :delay => 1)
92 94 assert_nil r.set_issue_to_dates
93 95 end
94 96
95 97 def test_set_issue_to_dates_without_issues
96 98 r = IssueRelation.new(:relation_type => IssueRelation::TYPE_PRECEDES, :delay => 1)
97 99 assert_nil r.set_issue_to_dates
98 100 end
99 101
100 102 def test_validates_circular_dependency
101 103 IssueRelation.delete_all
102 104 assert IssueRelation.create!(
103 105 :issue_from => Issue.find(1), :issue_to => Issue.find(2),
104 106 :relation_type => IssueRelation::TYPE_PRECEDES
105 107 )
106 108 assert IssueRelation.create!(
107 109 :issue_from => Issue.find(2), :issue_to => Issue.find(3),
108 110 :relation_type => IssueRelation::TYPE_PRECEDES
109 111 )
110 112 r = IssueRelation.new(
111 113 :issue_from => Issue.find(3), :issue_to => Issue.find(1),
112 114 :relation_type => IssueRelation::TYPE_PRECEDES
113 115 )
114 116 assert !r.save
115 117 assert_not_nil r.errors[:base]
116 118 end
117 119
120 def test_validates_circular_dependency_of_subtask
121 set_language_if_valid 'en'
122 issue1 = Issue.generate!
123 issue2 = Issue.generate!
124 IssueRelation.create!(
125 :issue_from => issue1, :issue_to => issue2,
126 :relation_type => IssueRelation::TYPE_PRECEDES
127 )
128 child = Issue.generate!(:parent_issue_id => issue2.id)
129 issue1.reload
130 child.reload
131
132 r = IssueRelation.new(
133 :issue_from => child, :issue_to => issue1,
134 :relation_type => IssueRelation::TYPE_PRECEDES
135 )
136 assert !r.save
137 assert_include 'This relation would create a circular dependency', r.errors.full_messages
138 end
139
118 140 def test_validates_circular_dependency_on_reverse_relations
119 141 IssueRelation.delete_all
120 142 assert IssueRelation.create!(
121 143 :issue_from => Issue.find(1), :issue_to => Issue.find(3),
122 144 :relation_type => IssueRelation::TYPE_BLOCKS
123 145 )
124 146 assert IssueRelation.create!(
125 147 :issue_from => Issue.find(1), :issue_to => Issue.find(2),
126 148 :relation_type => IssueRelation::TYPE_BLOCKED
127 149 )
128 150 r = IssueRelation.new(
129 151 :issue_from => Issue.find(2), :issue_to => Issue.find(1),
130 152 :relation_type => IssueRelation::TYPE_BLOCKED
131 153 )
132 154 assert !r.save
133 155 assert_not_nil r.errors[:base]
134 156 end
135 157 end
@@ -1,2074 +1,2104
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 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 :groups_users,
23 23 :trackers, :projects_trackers,
24 24 :enabled_modules,
25 25 :versions,
26 26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
27 27 :enumerations,
28 28 :issues, :journals, :journal_details,
29 29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
30 30 :time_entries
31 31
32 32 include Redmine::I18n
33 33
34 34 def teardown
35 35 User.current = nil
36 36 end
37 37
38 38 def test_initialize
39 39 issue = Issue.new
40 40
41 41 assert_nil issue.project_id
42 42 assert_nil issue.tracker_id
43 43 assert_nil issue.author_id
44 44 assert_nil issue.assigned_to_id
45 45 assert_nil issue.category_id
46 46
47 47 assert_equal IssueStatus.default, issue.status
48 48 assert_equal IssuePriority.default, issue.priority
49 49 end
50 50
51 51 def test_create
52 52 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
53 53 :status_id => 1, :priority => IssuePriority.all.first,
54 54 :subject => 'test_create',
55 55 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
56 56 assert issue.save
57 57 issue.reload
58 58 assert_equal 1.5, issue.estimated_hours
59 59 end
60 60
61 61 def test_create_minimal
62 62 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
63 63 :status_id => 1, :priority => IssuePriority.all.first,
64 64 :subject => 'test_create')
65 65 assert issue.save
66 66 assert issue.description.nil?
67 67 assert_nil issue.estimated_hours
68 68 end
69 69
70 70 def test_start_date_format_should_be_validated
71 71 set_language_if_valid 'en'
72 72 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
73 73 issue = Issue.new(:start_date => invalid_date)
74 74 assert !issue.valid?
75 75 assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
76 76 end
77 77 end
78 78
79 79 def test_due_date_format_should_be_validated
80 80 set_language_if_valid 'en'
81 81 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
82 82 issue = Issue.new(:due_date => invalid_date)
83 83 assert !issue.valid?
84 84 assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
85 85 end
86 86 end
87 87
88 88 def test_due_date_lesser_than_start_date_should_not_validate
89 89 set_language_if_valid 'en'
90 90 issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02')
91 91 assert !issue.valid?
92 92 assert_include 'Due date must be greater than start date', issue.errors.full_messages
93 93 end
94 94
95 95 def test_estimated_hours_should_be_validated
96 96 set_language_if_valid 'en'
97 97 ['-2'].each do |invalid|
98 98 issue = Issue.new(:estimated_hours => invalid)
99 99 assert !issue.valid?
100 100 assert_include 'Estimated time is invalid', issue.errors.full_messages
101 101 end
102 102 end
103 103
104 104 def test_create_with_required_custom_field
105 105 set_language_if_valid 'en'
106 106 field = IssueCustomField.find_by_name('Database')
107 107 field.update_attribute(:is_required, true)
108 108
109 109 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
110 110 :status_id => 1, :subject => 'test_create',
111 111 :description => 'IssueTest#test_create_with_required_custom_field')
112 112 assert issue.available_custom_fields.include?(field)
113 113 # No value for the custom field
114 114 assert !issue.save
115 115 assert_equal ["Database can't be blank"], issue.errors.full_messages
116 116 # Blank value
117 117 issue.custom_field_values = { field.id => '' }
118 118 assert !issue.save
119 119 assert_equal ["Database can't be blank"], issue.errors.full_messages
120 120 # Invalid value
121 121 issue.custom_field_values = { field.id => 'SQLServer' }
122 122 assert !issue.save
123 123 assert_equal ["Database is not included in the list"], issue.errors.full_messages
124 124 # Valid value
125 125 issue.custom_field_values = { field.id => 'PostgreSQL' }
126 126 assert issue.save
127 127 issue.reload
128 128 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
129 129 end
130 130
131 131 def test_create_with_group_assignment
132 132 with_settings :issue_group_assignment => '1' do
133 133 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
134 134 :subject => 'Group assignment',
135 135 :assigned_to_id => 11).save
136 136 issue = Issue.first(:order => 'id DESC')
137 137 assert_kind_of Group, issue.assigned_to
138 138 assert_equal Group.find(11), issue.assigned_to
139 139 end
140 140 end
141 141
142 142 def test_create_with_parent_issue_id
143 143 issue = Issue.new(:project_id => 1, :tracker_id => 1,
144 144 :author_id => 1, :subject => 'Group assignment',
145 145 :parent_issue_id => 1)
146 146 assert_save issue
147 147 assert_equal 1, issue.parent_issue_id
148 148 assert_equal Issue.find(1), issue.parent
149 149 end
150 150
151 151 def test_create_with_sharp_parent_issue_id
152 152 issue = Issue.new(:project_id => 1, :tracker_id => 1,
153 153 :author_id => 1, :subject => 'Group assignment',
154 154 :parent_issue_id => "#1")
155 155 assert_save issue
156 156 assert_equal 1, issue.parent_issue_id
157 157 assert_equal Issue.find(1), issue.parent
158 158 end
159 159
160 160 def test_create_with_invalid_parent_issue_id
161 161 set_language_if_valid 'en'
162 162 issue = Issue.new(:project_id => 1, :tracker_id => 1,
163 163 :author_id => 1, :subject => 'Group assignment',
164 164 :parent_issue_id => '01ABC')
165 165 assert !issue.save
166 166 assert_equal '01ABC', issue.parent_issue_id
167 167 assert_include 'Parent task is invalid', issue.errors.full_messages
168 168 end
169 169
170 170 def test_create_with_invalid_sharp_parent_issue_id
171 171 set_language_if_valid 'en'
172 172 issue = Issue.new(:project_id => 1, :tracker_id => 1,
173 173 :author_id => 1, :subject => 'Group assignment',
174 174 :parent_issue_id => '#01ABC')
175 175 assert !issue.save
176 176 assert_equal '#01ABC', issue.parent_issue_id
177 177 assert_include 'Parent task is invalid', issue.errors.full_messages
178 178 end
179 179
180 180 def assert_visibility_match(user, issues)
181 181 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
182 182 end
183 183
184 184 def test_visible_scope_for_anonymous
185 185 # Anonymous user should see issues of public projects only
186 186 issues = Issue.visible(User.anonymous).all
187 187 assert issues.any?
188 188 assert_nil issues.detect {|issue| !issue.project.is_public?}
189 189 assert_nil issues.detect {|issue| issue.is_private?}
190 190 assert_visibility_match User.anonymous, issues
191 191 end
192 192
193 193 def test_visible_scope_for_anonymous_without_view_issues_permissions
194 194 # Anonymous user should not see issues without permission
195 195 Role.anonymous.remove_permission!(:view_issues)
196 196 issues = Issue.visible(User.anonymous).all
197 197 assert issues.empty?
198 198 assert_visibility_match User.anonymous, issues
199 199 end
200 200
201 201 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default
202 202 assert Role.anonymous.update_attribute(:issues_visibility, 'default')
203 203 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
204 204 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
205 205 assert !issue.visible?(User.anonymous)
206 206 end
207 207
208 208 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own
209 209 assert Role.anonymous.update_attribute(:issues_visibility, 'own')
210 210 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
211 211 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
212 212 assert !issue.visible?(User.anonymous)
213 213 end
214 214
215 215 def test_visible_scope_for_non_member
216 216 user = User.find(9)
217 217 assert user.projects.empty?
218 218 # Non member user should see issues of public projects only
219 219 issues = Issue.visible(user).all
220 220 assert issues.any?
221 221 assert_nil issues.detect {|issue| !issue.project.is_public?}
222 222 assert_nil issues.detect {|issue| issue.is_private?}
223 223 assert_visibility_match user, issues
224 224 end
225 225
226 226 def test_visible_scope_for_non_member_with_own_issues_visibility
227 227 Role.non_member.update_attribute :issues_visibility, 'own'
228 228 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
229 229 user = User.find(9)
230 230
231 231 issues = Issue.visible(user).all
232 232 assert issues.any?
233 233 assert_nil issues.detect {|issue| issue.author != user}
234 234 assert_visibility_match user, issues
235 235 end
236 236
237 237 def test_visible_scope_for_non_member_without_view_issues_permissions
238 238 # Non member user should not see issues without permission
239 239 Role.non_member.remove_permission!(:view_issues)
240 240 user = User.find(9)
241 241 assert user.projects.empty?
242 242 issues = Issue.visible(user).all
243 243 assert issues.empty?
244 244 assert_visibility_match user, issues
245 245 end
246 246
247 247 def test_visible_scope_for_member
248 248 user = User.find(9)
249 249 # User should see issues of projects for which he has view_issues permissions only
250 250 Role.non_member.remove_permission!(:view_issues)
251 251 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
252 252 issues = Issue.visible(user).all
253 253 assert issues.any?
254 254 assert_nil issues.detect {|issue| issue.project_id != 3}
255 255 assert_nil issues.detect {|issue| issue.is_private?}
256 256 assert_visibility_match user, issues
257 257 end
258 258
259 259 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
260 260 user = User.find(8)
261 261 assert user.groups.any?
262 262 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
263 263 Role.non_member.remove_permission!(:view_issues)
264 264
265 265 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
266 266 :status_id => 1, :priority => IssuePriority.all.first,
267 267 :subject => 'Assignment test',
268 268 :assigned_to => user.groups.first,
269 269 :is_private => true)
270 270
271 271 Role.find(2).update_attribute :issues_visibility, 'default'
272 272 issues = Issue.visible(User.find(8)).all
273 273 assert issues.any?
274 274 assert issues.include?(issue)
275 275
276 276 Role.find(2).update_attribute :issues_visibility, 'own'
277 277 issues = Issue.visible(User.find(8)).all
278 278 assert issues.any?
279 279 assert issues.include?(issue)
280 280 end
281 281
282 282 def test_visible_scope_for_admin
283 283 user = User.find(1)
284 284 user.members.each(&:destroy)
285 285 assert user.projects.empty?
286 286 issues = Issue.visible(user).all
287 287 assert issues.any?
288 288 # Admin should see issues on private projects that he does not belong to
289 289 assert issues.detect {|issue| !issue.project.is_public?}
290 290 # Admin should see private issues of other users
291 291 assert issues.detect {|issue| issue.is_private? && issue.author != user}
292 292 assert_visibility_match user, issues
293 293 end
294 294
295 295 def test_visible_scope_with_project
296 296 project = Project.find(1)
297 297 issues = Issue.visible(User.find(2), :project => project).all
298 298 projects = issues.collect(&:project).uniq
299 299 assert_equal 1, projects.size
300 300 assert_equal project, projects.first
301 301 end
302 302
303 303 def test_visible_scope_with_project_and_subprojects
304 304 project = Project.find(1)
305 305 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
306 306 projects = issues.collect(&:project).uniq
307 307 assert projects.size > 1
308 308 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
309 309 end
310 310
311 311 def test_visible_and_nested_set_scopes
312 312 assert_equal 0, Issue.find(1).descendants.visible.all.size
313 313 end
314 314
315 315 def test_open_scope
316 316 issues = Issue.open.all
317 317 assert_nil issues.detect(&:closed?)
318 318 end
319 319
320 320 def test_open_scope_with_arg
321 321 issues = Issue.open(false).all
322 322 assert_equal issues, issues.select(&:closed?)
323 323 end
324 324
325 325 def test_fixed_version_scope_with_a_version_should_return_its_fixed_issues
326 326 version = Version.find(2)
327 327 assert version.fixed_issues.any?
328 328 assert_equal version.fixed_issues.to_a.sort, Issue.fixed_version(version).to_a.sort
329 329 end
330 330
331 331 def test_fixed_version_scope_with_empty_array_should_return_no_result
332 332 assert_equal 0, Issue.fixed_version([]).count
333 333 end
334 334
335 335 def test_errors_full_messages_should_include_custom_fields_errors
336 336 field = IssueCustomField.find_by_name('Database')
337 337
338 338 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
339 339 :status_id => 1, :subject => 'test_create',
340 340 :description => 'IssueTest#test_create_with_required_custom_field')
341 341 assert issue.available_custom_fields.include?(field)
342 342 # Invalid value
343 343 issue.custom_field_values = { field.id => 'SQLServer' }
344 344
345 345 assert !issue.valid?
346 346 assert_equal 1, issue.errors.full_messages.size
347 347 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
348 348 issue.errors.full_messages.first
349 349 end
350 350
351 351 def test_update_issue_with_required_custom_field
352 352 field = IssueCustomField.find_by_name('Database')
353 353 field.update_attribute(:is_required, true)
354 354
355 355 issue = Issue.find(1)
356 356 assert_nil issue.custom_value_for(field)
357 357 assert issue.available_custom_fields.include?(field)
358 358 # No change to custom values, issue can be saved
359 359 assert issue.save
360 360 # Blank value
361 361 issue.custom_field_values = { field.id => '' }
362 362 assert !issue.save
363 363 # Valid value
364 364 issue.custom_field_values = { field.id => 'PostgreSQL' }
365 365 assert issue.save
366 366 issue.reload
367 367 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
368 368 end
369 369
370 370 def test_should_not_update_attributes_if_custom_fields_validation_fails
371 371 issue = Issue.find(1)
372 372 field = IssueCustomField.find_by_name('Database')
373 373 assert issue.available_custom_fields.include?(field)
374 374
375 375 issue.custom_field_values = { field.id => 'Invalid' }
376 376 issue.subject = 'Should be not be saved'
377 377 assert !issue.save
378 378
379 379 issue.reload
380 380 assert_equal "Can't print recipes", issue.subject
381 381 end
382 382
383 383 def test_should_not_recreate_custom_values_objects_on_update
384 384 field = IssueCustomField.find_by_name('Database')
385 385
386 386 issue = Issue.find(1)
387 387 issue.custom_field_values = { field.id => 'PostgreSQL' }
388 388 assert issue.save
389 389 custom_value = issue.custom_value_for(field)
390 390 issue.reload
391 391 issue.custom_field_values = { field.id => 'MySQL' }
392 392 assert issue.save
393 393 issue.reload
394 394 assert_equal custom_value.id, issue.custom_value_for(field).id
395 395 end
396 396
397 397 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
398 398 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1,
399 399 :status_id => 1, :subject => 'Test',
400 400 :custom_field_values => {'2' => 'Test'})
401 401 assert !Tracker.find(2).custom_field_ids.include?(2)
402 402
403 403 issue = Issue.find(issue.id)
404 404 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
405 405
406 406 issue = Issue.find(issue.id)
407 407 custom_value = issue.custom_value_for(2)
408 408 assert_not_nil custom_value
409 409 assert_equal 'Test', custom_value.value
410 410 end
411 411
412 412 def test_assigning_tracker_id_should_reload_custom_fields_values
413 413 issue = Issue.new(:project => Project.find(1))
414 414 assert issue.custom_field_values.empty?
415 415 issue.tracker_id = 1
416 416 assert issue.custom_field_values.any?
417 417 end
418 418
419 419 def test_assigning_attributes_should_assign_project_and_tracker_first
420 420 seq = sequence('seq')
421 421 issue = Issue.new
422 422 issue.expects(:project_id=).in_sequence(seq)
423 423 issue.expects(:tracker_id=).in_sequence(seq)
424 424 issue.expects(:subject=).in_sequence(seq)
425 425 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
426 426 end
427 427
428 428 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
429 429 attributes = ActiveSupport::OrderedHash.new
430 430 attributes['custom_field_values'] = { '1' => 'MySQL' }
431 431 attributes['tracker_id'] = '1'
432 432 issue = Issue.new(:project => Project.find(1))
433 433 issue.attributes = attributes
434 434 assert_equal 'MySQL', issue.custom_field_value(1)
435 435 end
436 436
437 437 def test_reload_should_reload_custom_field_values
438 438 issue = Issue.generate!
439 439 issue.custom_field_values = {'2' => 'Foo'}
440 440 issue.save!
441 441
442 442 issue = Issue.order('id desc').first
443 443 assert_equal 'Foo', issue.custom_field_value(2)
444 444
445 445 issue.custom_field_values = {'2' => 'Bar'}
446 446 assert_equal 'Bar', issue.custom_field_value(2)
447 447
448 448 issue.reload
449 449 assert_equal 'Foo', issue.custom_field_value(2)
450 450 end
451 451
452 452 def test_should_update_issue_with_disabled_tracker
453 453 p = Project.find(1)
454 454 issue = Issue.find(1)
455 455
456 456 p.trackers.delete(issue.tracker)
457 457 assert !p.trackers.include?(issue.tracker)
458 458
459 459 issue.reload
460 460 issue.subject = 'New subject'
461 461 assert issue.save
462 462 end
463 463
464 464 def test_should_not_set_a_disabled_tracker
465 465 p = Project.find(1)
466 466 p.trackers.delete(Tracker.find(2))
467 467
468 468 issue = Issue.find(1)
469 469 issue.tracker_id = 2
470 470 issue.subject = 'New subject'
471 471 assert !issue.save
472 472 assert_not_nil issue.errors[:tracker_id]
473 473 end
474 474
475 475 def test_category_based_assignment
476 476 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
477 477 :status_id => 1, :priority => IssuePriority.all.first,
478 478 :subject => 'Assignment test',
479 479 :description => 'Assignment test', :category_id => 1)
480 480 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
481 481 end
482 482
483 483 def test_new_statuses_allowed_to
484 484 WorkflowTransition.delete_all
485 485 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
486 486 :old_status_id => 1, :new_status_id => 2,
487 487 :author => false, :assignee => false)
488 488 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
489 489 :old_status_id => 1, :new_status_id => 3,
490 490 :author => true, :assignee => false)
491 491 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1,
492 492 :new_status_id => 4, :author => false,
493 493 :assignee => true)
494 494 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
495 495 :old_status_id => 1, :new_status_id => 5,
496 496 :author => true, :assignee => true)
497 497 status = IssueStatus.find(1)
498 498 role = Role.find(1)
499 499 tracker = Tracker.find(1)
500 500 user = User.find(2)
501 501
502 502 issue = Issue.generate!(:tracker => tracker, :status => status,
503 503 :project_id => 1, :author_id => 1)
504 504 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
505 505
506 506 issue = Issue.generate!(:tracker => tracker, :status => status,
507 507 :project_id => 1, :author => user)
508 508 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
509 509
510 510 issue = Issue.generate!(:tracker => tracker, :status => status,
511 511 :project_id => 1, :author_id => 1,
512 512 :assigned_to => user)
513 513 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
514 514
515 515 issue = Issue.generate!(:tracker => tracker, :status => status,
516 516 :project_id => 1, :author => user,
517 517 :assigned_to => user)
518 518 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
519 519 end
520 520
521 521 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
522 522 admin = User.find(1)
523 523 issue = Issue.find(1)
524 524 assert !admin.member_of?(issue.project)
525 525 expected_statuses = [issue.status] +
526 526 WorkflowTransition.find_all_by_old_status_id(
527 527 issue.status_id).map(&:new_status).uniq.sort
528 528 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
529 529 end
530 530
531 531 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
532 532 issue = Issue.find(1).copy
533 533 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
534 534
535 535 issue = Issue.find(2).copy
536 536 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
537 537 end
538 538
539 539 def test_safe_attributes_names_should_not_include_disabled_field
540 540 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
541 541
542 542 issue = Issue.new(:tracker => tracker)
543 543 assert_include 'tracker_id', issue.safe_attribute_names
544 544 assert_include 'status_id', issue.safe_attribute_names
545 545 assert_include 'subject', issue.safe_attribute_names
546 546 assert_include 'description', issue.safe_attribute_names
547 547 assert_include 'custom_field_values', issue.safe_attribute_names
548 548 assert_include 'custom_fields', issue.safe_attribute_names
549 549 assert_include 'lock_version', issue.safe_attribute_names
550 550
551 551 tracker.core_fields.each do |field|
552 552 assert_include field, issue.safe_attribute_names
553 553 end
554 554
555 555 tracker.disabled_core_fields.each do |field|
556 556 assert_not_include field, issue.safe_attribute_names
557 557 end
558 558 end
559 559
560 560 def test_safe_attributes_should_ignore_disabled_fields
561 561 tracker = Tracker.find(1)
562 562 tracker.core_fields = %w(assigned_to_id due_date)
563 563 tracker.save!
564 564
565 565 issue = Issue.new(:tracker => tracker)
566 566 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
567 567 assert_nil issue.start_date
568 568 assert_equal Date.parse('2012-07-14'), issue.due_date
569 569 end
570 570
571 571 def test_safe_attributes_should_accept_target_tracker_enabled_fields
572 572 source = Tracker.find(1)
573 573 source.core_fields = []
574 574 source.save!
575 575 target = Tracker.find(2)
576 576 target.core_fields = %w(assigned_to_id due_date)
577 577 target.save!
578 578
579 579 issue = Issue.new(:tracker => source)
580 580 issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'}
581 581 assert_equal target, issue.tracker
582 582 assert_equal Date.parse('2012-07-14'), issue.due_date
583 583 end
584 584
585 585 def test_safe_attributes_should_not_include_readonly_fields
586 586 WorkflowPermission.delete_all
587 587 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
588 588 :role_id => 1, :field_name => 'due_date',
589 589 :rule => 'readonly')
590 590 user = User.find(2)
591 591
592 592 issue = Issue.new(:project_id => 1, :tracker_id => 1)
593 593 assert_equal %w(due_date), issue.read_only_attribute_names(user)
594 594 assert_not_include 'due_date', issue.safe_attribute_names(user)
595 595
596 596 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
597 597 assert_equal Date.parse('2012-07-14'), issue.start_date
598 598 assert_nil issue.due_date
599 599 end
600 600
601 601 def test_safe_attributes_should_not_include_readonly_custom_fields
602 602 cf1 = IssueCustomField.create!(:name => 'Writable field',
603 603 :field_format => 'string',
604 604 :is_for_all => true, :tracker_ids => [1])
605 605 cf2 = IssueCustomField.create!(:name => 'Readonly field',
606 606 :field_format => 'string',
607 607 :is_for_all => true, :tracker_ids => [1])
608 608 WorkflowPermission.delete_all
609 609 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
610 610 :role_id => 1, :field_name => cf2.id.to_s,
611 611 :rule => 'readonly')
612 612 user = User.find(2)
613 613 issue = Issue.new(:project_id => 1, :tracker_id => 1)
614 614 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
615 615 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
616 616
617 617 issue.send :safe_attributes=, {'custom_field_values' => {
618 618 cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'
619 619 }}, user
620 620 assert_equal 'value1', issue.custom_field_value(cf1)
621 621 assert_nil issue.custom_field_value(cf2)
622 622
623 623 issue.send :safe_attributes=, {'custom_fields' => [
624 624 {'id' => cf1.id.to_s, 'value' => 'valuea'},
625 625 {'id' => cf2.id.to_s, 'value' => 'valueb'}
626 626 ]}, user
627 627 assert_equal 'valuea', issue.custom_field_value(cf1)
628 628 assert_nil issue.custom_field_value(cf2)
629 629 end
630 630
631 631 def test_editable_custom_field_values_should_return_non_readonly_custom_values
632 632 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string',
633 633 :is_for_all => true, :tracker_ids => [1, 2])
634 634 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string',
635 635 :is_for_all => true, :tracker_ids => [1, 2])
636 636 WorkflowPermission.delete_all
637 637 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1,
638 638 :field_name => cf2.id.to_s, :rule => 'readonly')
639 639 user = User.find(2)
640 640
641 641 issue = Issue.new(:project_id => 1, :tracker_id => 1)
642 642 values = issue.editable_custom_field_values(user)
643 643 assert values.detect {|value| value.custom_field == cf1}
644 644 assert_nil values.detect {|value| value.custom_field == cf2}
645 645
646 646 issue.tracker_id = 2
647 647 values = issue.editable_custom_field_values(user)
648 648 assert values.detect {|value| value.custom_field == cf1}
649 649 assert values.detect {|value| value.custom_field == cf2}
650 650 end
651 651
652 652 def test_safe_attributes_should_accept_target_tracker_writable_fields
653 653 WorkflowPermission.delete_all
654 654 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
655 655 :role_id => 1, :field_name => 'due_date',
656 656 :rule => 'readonly')
657 657 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
658 658 :role_id => 1, :field_name => 'start_date',
659 659 :rule => 'readonly')
660 660 user = User.find(2)
661 661
662 662 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
663 663
664 664 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
665 665 'due_date' => '2012-07-14'}, user
666 666 assert_equal Date.parse('2012-07-12'), issue.start_date
667 667 assert_nil issue.due_date
668 668
669 669 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
670 670 'due_date' => '2012-07-16',
671 671 'tracker_id' => 2}, user
672 672 assert_equal Date.parse('2012-07-12'), issue.start_date
673 673 assert_equal Date.parse('2012-07-16'), issue.due_date
674 674 end
675 675
676 676 def test_safe_attributes_should_accept_target_status_writable_fields
677 677 WorkflowPermission.delete_all
678 678 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
679 679 :role_id => 1, :field_name => 'due_date',
680 680 :rule => 'readonly')
681 681 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1,
682 682 :role_id => 1, :field_name => 'start_date',
683 683 :rule => 'readonly')
684 684 user = User.find(2)
685 685
686 686 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
687 687
688 688 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
689 689 'due_date' => '2012-07-14'},
690 690 user
691 691 assert_equal Date.parse('2012-07-12'), issue.start_date
692 692 assert_nil issue.due_date
693 693
694 694 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
695 695 'due_date' => '2012-07-16',
696 696 'status_id' => 2},
697 697 user
698 698 assert_equal Date.parse('2012-07-12'), issue.start_date
699 699 assert_equal Date.parse('2012-07-16'), issue.due_date
700 700 end
701 701
702 702 def test_required_attributes_should_be_validated
703 703 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string',
704 704 :is_for_all => true, :tracker_ids => [1, 2])
705 705
706 706 WorkflowPermission.delete_all
707 707 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
708 708 :role_id => 1, :field_name => 'due_date',
709 709 :rule => 'required')
710 710 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
711 711 :role_id => 1, :field_name => 'category_id',
712 712 :rule => 'required')
713 713 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
714 714 :role_id => 1, :field_name => cf.id.to_s,
715 715 :rule => 'required')
716 716
717 717 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
718 718 :role_id => 1, :field_name => 'start_date',
719 719 :rule => 'required')
720 720 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
721 721 :role_id => 1, :field_name => cf.id.to_s,
722 722 :rule => 'required')
723 723 user = User.find(2)
724 724
725 725 issue = Issue.new(:project_id => 1, :tracker_id => 1,
726 726 :status_id => 1, :subject => 'Required fields',
727 727 :author => user)
728 728 assert_equal [cf.id.to_s, "category_id", "due_date"],
729 729 issue.required_attribute_names(user).sort
730 730 assert !issue.save, "Issue was saved"
731 731 assert_equal ["Category can't be blank", "Due date can't be blank", "Foo can't be blank"],
732 732 issue.errors.full_messages.sort
733 733
734 734 issue.tracker_id = 2
735 735 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
736 736 assert !issue.save, "Issue was saved"
737 737 assert_equal ["Foo can't be blank", "Start date can't be blank"],
738 738 issue.errors.full_messages.sort
739 739
740 740 issue.start_date = Date.today
741 741 issue.custom_field_values = {cf.id.to_s => 'bar'}
742 742 assert issue.save
743 743 end
744 744
745 745 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
746 746 WorkflowPermission.delete_all
747 747 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
748 748 :role_id => 1, :field_name => 'due_date',
749 749 :rule => 'required')
750 750 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
751 751 :role_id => 1, :field_name => 'start_date',
752 752 :rule => 'required')
753 753 user = User.find(2)
754 754 member = Member.find(1)
755 755 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
756 756
757 757 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
758 758
759 759 member.role_ids = [1, 2]
760 760 member.save!
761 761 assert_equal [], issue.required_attribute_names(user.reload)
762 762
763 763 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
764 764 :role_id => 2, :field_name => 'due_date',
765 765 :rule => 'required')
766 766 assert_equal %w(due_date), issue.required_attribute_names(user)
767 767
768 768 member.role_ids = [1, 2, 3]
769 769 member.save!
770 770 assert_equal [], issue.required_attribute_names(user.reload)
771 771
772 772 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
773 773 :role_id => 2, :field_name => 'due_date',
774 774 :rule => 'readonly')
775 775 # required + readonly => required
776 776 assert_equal %w(due_date), issue.required_attribute_names(user)
777 777 end
778 778
779 779 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
780 780 WorkflowPermission.delete_all
781 781 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
782 782 :role_id => 1, :field_name => 'due_date',
783 783 :rule => 'readonly')
784 784 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
785 785 :role_id => 1, :field_name => 'start_date',
786 786 :rule => 'readonly')
787 787 user = User.find(2)
788 788 member = Member.find(1)
789 789 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
790 790
791 791 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
792 792
793 793 member.role_ids = [1, 2]
794 794 member.save!
795 795 assert_equal [], issue.read_only_attribute_names(user.reload)
796 796
797 797 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
798 798 :role_id => 2, :field_name => 'due_date',
799 799 :rule => 'readonly')
800 800 assert_equal %w(due_date), issue.read_only_attribute_names(user)
801 801 end
802 802
803 803 def test_copy
804 804 issue = Issue.new.copy_from(1)
805 805 assert issue.copy?
806 806 assert issue.save
807 807 issue.reload
808 808 orig = Issue.find(1)
809 809 assert_equal orig.subject, issue.subject
810 810 assert_equal orig.tracker, issue.tracker
811 811 assert_equal "125", issue.custom_value_for(2).value
812 812 end
813 813
814 814 def test_copy_should_copy_status
815 815 orig = Issue.find(8)
816 816 assert orig.status != IssueStatus.default
817 817
818 818 issue = Issue.new.copy_from(orig)
819 819 assert issue.save
820 820 issue.reload
821 821 assert_equal orig.status, issue.status
822 822 end
823 823
824 824 def test_copy_should_add_relation_with_copied_issue
825 825 copied = Issue.find(1)
826 826 issue = Issue.new.copy_from(copied)
827 827 assert issue.save
828 828 issue.reload
829 829
830 830 assert_equal 1, issue.relations.size
831 831 relation = issue.relations.first
832 832 assert_equal 'copied_to', relation.relation_type
833 833 assert_equal copied, relation.issue_from
834 834 assert_equal issue, relation.issue_to
835 835 end
836 836
837 837 def test_copy_should_copy_subtasks
838 838 issue = Issue.generate_with_descendants!
839 839
840 840 copy = issue.reload.copy
841 841 copy.author = User.find(7)
842 842 assert_difference 'Issue.count', 1+issue.descendants.count do
843 843 assert copy.save
844 844 end
845 845 copy.reload
846 846 assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort
847 847 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
848 848 assert_equal %w(Child11), child_copy.children.map(&:subject).sort
849 849 assert_equal copy.author, child_copy.author
850 850 end
851 851
852 852 def test_copy_as_a_child_of_copied_issue_should_not_copy_itself
853 853 parent = Issue.generate!
854 854 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
855 855 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
856 856
857 857 copy = parent.reload.copy
858 858 copy.parent_issue_id = parent.id
859 859 copy.author = User.find(7)
860 860 assert_difference 'Issue.count', 3 do
861 861 assert copy.save
862 862 end
863 863 parent.reload
864 864 copy.reload
865 865 assert_equal parent, copy.parent
866 866 assert_equal 3, parent.children.count
867 867 assert_equal 5, parent.descendants.count
868 868 assert_equal 2, copy.children.count
869 869 assert_equal 2, copy.descendants.count
870 870 end
871 871
872 872 def test_copy_as_a_descendant_of_copied_issue_should_not_copy_itself
873 873 parent = Issue.generate!
874 874 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
875 875 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
876 876
877 877 copy = parent.reload.copy
878 878 copy.parent_issue_id = child1.id
879 879 copy.author = User.find(7)
880 880 assert_difference 'Issue.count', 3 do
881 881 assert copy.save
882 882 end
883 883 parent.reload
884 884 child1.reload
885 885 copy.reload
886 886 assert_equal child1, copy.parent
887 887 assert_equal 2, parent.children.count
888 888 assert_equal 5, parent.descendants.count
889 889 assert_equal 1, child1.children.count
890 890 assert_equal 3, child1.descendants.count
891 891 assert_equal 2, copy.children.count
892 892 assert_equal 2, copy.descendants.count
893 893 end
894 894
895 895 def test_copy_should_copy_subtasks_to_target_project
896 896 issue = Issue.generate_with_descendants!
897 897
898 898 copy = issue.copy(:project_id => 3)
899 899 assert_difference 'Issue.count', 1+issue.descendants.count do
900 900 assert copy.save
901 901 end
902 902 assert_equal [3], copy.reload.descendants.map(&:project_id).uniq
903 903 end
904 904
905 905 def test_copy_should_not_copy_subtasks_twice_when_saving_twice
906 906 issue = Issue.generate_with_descendants!
907 907
908 908 copy = issue.reload.copy
909 909 assert_difference 'Issue.count', 1+issue.descendants.count do
910 910 assert copy.save
911 911 assert copy.save
912 912 end
913 913 end
914 914
915 915 def test_should_not_call_after_project_change_on_creation
916 916 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
917 917 :subject => 'Test', :author_id => 1)
918 918 issue.expects(:after_project_change).never
919 919 issue.save!
920 920 end
921 921
922 922 def test_should_not_call_after_project_change_on_update
923 923 issue = Issue.find(1)
924 924 issue.project = Project.find(1)
925 925 issue.subject = 'No project change'
926 926 issue.expects(:after_project_change).never
927 927 issue.save!
928 928 end
929 929
930 930 def test_should_call_after_project_change_on_project_change
931 931 issue = Issue.find(1)
932 932 issue.project = Project.find(2)
933 933 issue.expects(:after_project_change).once
934 934 issue.save!
935 935 end
936 936
937 937 def test_adding_journal_should_update_timestamp
938 938 issue = Issue.find(1)
939 939 updated_on_was = issue.updated_on
940 940
941 941 issue.init_journal(User.first, "Adding notes")
942 942 assert_difference 'Journal.count' do
943 943 assert issue.save
944 944 end
945 945 issue.reload
946 946
947 947 assert_not_equal updated_on_was, issue.updated_on
948 948 end
949 949
950 950 def test_should_close_duplicates
951 951 # Create 3 issues
952 952 issue1 = Issue.generate!
953 953 issue2 = Issue.generate!
954 954 issue3 = Issue.generate!
955 955
956 956 # 2 is a dupe of 1
957 957 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1,
958 958 :relation_type => IssueRelation::TYPE_DUPLICATES)
959 959 # And 3 is a dupe of 2
960 960 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
961 961 :relation_type => IssueRelation::TYPE_DUPLICATES)
962 962 # And 3 is a dupe of 1 (circular duplicates)
963 963 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1,
964 964 :relation_type => IssueRelation::TYPE_DUPLICATES)
965 965
966 966 assert issue1.reload.duplicates.include?(issue2)
967 967
968 968 # Closing issue 1
969 969 issue1.init_journal(User.first, "Closing issue1")
970 970 issue1.status = IssueStatus.where(:is_closed => true).first
971 971 assert issue1.save
972 972 # 2 and 3 should be also closed
973 973 assert issue2.reload.closed?
974 974 assert issue3.reload.closed?
975 975 end
976 976
977 977 def test_should_not_close_duplicated_issue
978 978 issue1 = Issue.generate!
979 979 issue2 = Issue.generate!
980 980
981 981 # 2 is a dupe of 1
982 982 IssueRelation.create(:issue_from => issue2, :issue_to => issue1,
983 983 :relation_type => IssueRelation::TYPE_DUPLICATES)
984 984 # 2 is a dup of 1 but 1 is not a duplicate of 2
985 985 assert !issue2.reload.duplicates.include?(issue1)
986 986
987 987 # Closing issue 2
988 988 issue2.init_journal(User.first, "Closing issue2")
989 989 issue2.status = IssueStatus.where(:is_closed => true).first
990 990 assert issue2.save
991 991 # 1 should not be also closed
992 992 assert !issue1.reload.closed?
993 993 end
994 994
995 995 def test_assignable_versions
996 996 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
997 997 :status_id => 1, :fixed_version_id => 1,
998 998 :subject => 'New issue')
999 999 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
1000 1000 end
1001 1001
1002 1002 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
1003 1003 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1004 1004 :status_id => 1, :fixed_version_id => 1,
1005 1005 :subject => 'New issue')
1006 1006 assert !issue.save
1007 1007 assert_not_nil issue.errors[:fixed_version_id]
1008 1008 end
1009 1009
1010 1010 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
1011 1011 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1012 1012 :status_id => 1, :fixed_version_id => 2,
1013 1013 :subject => 'New issue')
1014 1014 assert !issue.save
1015 1015 assert_not_nil issue.errors[:fixed_version_id]
1016 1016 end
1017 1017
1018 1018 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
1019 1019 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1020 1020 :status_id => 1, :fixed_version_id => 3,
1021 1021 :subject => 'New issue')
1022 1022 assert issue.save
1023 1023 end
1024 1024
1025 1025 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
1026 1026 issue = Issue.find(11)
1027 1027 assert_equal 'closed', issue.fixed_version.status
1028 1028 issue.subject = 'Subject changed'
1029 1029 assert issue.save
1030 1030 end
1031 1031
1032 1032 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
1033 1033 issue = Issue.find(11)
1034 1034 issue.status_id = 1
1035 1035 assert !issue.save
1036 1036 assert_not_nil issue.errors[:base]
1037 1037 end
1038 1038
1039 1039 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
1040 1040 issue = Issue.find(11)
1041 1041 issue.status_id = 1
1042 1042 issue.fixed_version_id = 3
1043 1043 assert issue.save
1044 1044 end
1045 1045
1046 1046 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
1047 1047 issue = Issue.find(12)
1048 1048 assert_equal 'locked', issue.fixed_version.status
1049 1049 issue.status_id = 1
1050 1050 assert issue.save
1051 1051 end
1052 1052
1053 1053 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
1054 1054 issue = Issue.find(2)
1055 1055 assert_equal 2, issue.fixed_version_id
1056 1056 issue.project_id = 3
1057 1057 assert_nil issue.fixed_version_id
1058 1058 issue.fixed_version_id = 2
1059 1059 assert !issue.save
1060 1060 assert_include 'Target version is not included in the list', issue.errors.full_messages
1061 1061 end
1062 1062
1063 1063 def test_should_keep_shared_version_when_changing_project
1064 1064 Version.find(2).update_attribute :sharing, 'tree'
1065 1065
1066 1066 issue = Issue.find(2)
1067 1067 assert_equal 2, issue.fixed_version_id
1068 1068 issue.project_id = 3
1069 1069 assert_equal 2, issue.fixed_version_id
1070 1070 assert issue.save
1071 1071 end
1072 1072
1073 1073 def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled
1074 1074 assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
1075 1075 end
1076 1076
1077 1077 def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled
1078 1078 Project.find(2).disable_module! :issue_tracking
1079 1079 assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
1080 1080 end
1081 1081
1082 1082 def test_move_to_another_project_with_same_category
1083 1083 issue = Issue.find(1)
1084 1084 issue.project = Project.find(2)
1085 1085 assert issue.save
1086 1086 issue.reload
1087 1087 assert_equal 2, issue.project_id
1088 1088 # Category changes
1089 1089 assert_equal 4, issue.category_id
1090 1090 # Make sure time entries were move to the target project
1091 1091 assert_equal 2, issue.time_entries.first.project_id
1092 1092 end
1093 1093
1094 1094 def test_move_to_another_project_without_same_category
1095 1095 issue = Issue.find(2)
1096 1096 issue.project = Project.find(2)
1097 1097 assert issue.save
1098 1098 issue.reload
1099 1099 assert_equal 2, issue.project_id
1100 1100 # Category cleared
1101 1101 assert_nil issue.category_id
1102 1102 end
1103 1103
1104 1104 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
1105 1105 issue = Issue.find(1)
1106 1106 issue.update_attribute(:fixed_version_id, 1)
1107 1107 issue.project = Project.find(2)
1108 1108 assert issue.save
1109 1109 issue.reload
1110 1110 assert_equal 2, issue.project_id
1111 1111 # Cleared fixed_version
1112 1112 assert_equal nil, issue.fixed_version
1113 1113 end
1114 1114
1115 1115 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
1116 1116 issue = Issue.find(1)
1117 1117 issue.update_attribute(:fixed_version_id, 4)
1118 1118 issue.project = Project.find(5)
1119 1119 assert issue.save
1120 1120 issue.reload
1121 1121 assert_equal 5, issue.project_id
1122 1122 # Keep fixed_version
1123 1123 assert_equal 4, issue.fixed_version_id
1124 1124 end
1125 1125
1126 1126 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
1127 1127 issue = Issue.find(1)
1128 1128 issue.update_attribute(:fixed_version_id, 1)
1129 1129 issue.project = Project.find(5)
1130 1130 assert issue.save
1131 1131 issue.reload
1132 1132 assert_equal 5, issue.project_id
1133 1133 # Cleared fixed_version
1134 1134 assert_equal nil, issue.fixed_version
1135 1135 end
1136 1136
1137 1137 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
1138 1138 issue = Issue.find(1)
1139 1139 issue.update_attribute(:fixed_version_id, 7)
1140 1140 issue.project = Project.find(2)
1141 1141 assert issue.save
1142 1142 issue.reload
1143 1143 assert_equal 2, issue.project_id
1144 1144 # Keep fixed_version
1145 1145 assert_equal 7, issue.fixed_version_id
1146 1146 end
1147 1147
1148 1148 def test_move_to_another_project_should_keep_parent_if_valid
1149 1149 issue = Issue.find(1)
1150 1150 issue.update_attribute(:parent_issue_id, 2)
1151 1151 issue.project = Project.find(3)
1152 1152 assert issue.save
1153 1153 issue.reload
1154 1154 assert_equal 2, issue.parent_id
1155 1155 end
1156 1156
1157 1157 def test_move_to_another_project_should_clear_parent_if_not_valid
1158 1158 issue = Issue.find(1)
1159 1159 issue.update_attribute(:parent_issue_id, 2)
1160 1160 issue.project = Project.find(2)
1161 1161 assert issue.save
1162 1162 issue.reload
1163 1163 assert_nil issue.parent_id
1164 1164 end
1165 1165
1166 1166 def test_move_to_another_project_with_disabled_tracker
1167 1167 issue = Issue.find(1)
1168 1168 target = Project.find(2)
1169 1169 target.tracker_ids = [3]
1170 1170 target.save
1171 1171 issue.project = target
1172 1172 assert issue.save
1173 1173 issue.reload
1174 1174 assert_equal 2, issue.project_id
1175 1175 assert_equal 3, issue.tracker_id
1176 1176 end
1177 1177
1178 1178 def test_copy_to_the_same_project
1179 1179 issue = Issue.find(1)
1180 1180 copy = issue.copy
1181 1181 assert_difference 'Issue.count' do
1182 1182 copy.save!
1183 1183 end
1184 1184 assert_kind_of Issue, copy
1185 1185 assert_equal issue.project, copy.project
1186 1186 assert_equal "125", copy.custom_value_for(2).value
1187 1187 end
1188 1188
1189 1189 def test_copy_to_another_project_and_tracker
1190 1190 issue = Issue.find(1)
1191 1191 copy = issue.copy(:project_id => 3, :tracker_id => 2)
1192 1192 assert_difference 'Issue.count' do
1193 1193 copy.save!
1194 1194 end
1195 1195 copy.reload
1196 1196 assert_kind_of Issue, copy
1197 1197 assert_equal Project.find(3), copy.project
1198 1198 assert_equal Tracker.find(2), copy.tracker
1199 1199 # Custom field #2 is not associated with target tracker
1200 1200 assert_nil copy.custom_value_for(2)
1201 1201 end
1202 1202
1203 1203 test "#copy should not create a journal" do
1204 1204 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1205 1205 copy.save!
1206 1206 assert_equal 0, copy.reload.journals.size
1207 1207 end
1208 1208
1209 1209 test "#copy should allow assigned_to changes" do
1210 1210 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1211 1211 assert_equal 3, copy.assigned_to_id
1212 1212 end
1213 1213
1214 1214 test "#copy should allow status changes" do
1215 1215 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
1216 1216 assert_equal 2, copy.status_id
1217 1217 end
1218 1218
1219 1219 test "#copy should allow start date changes" do
1220 1220 date = Date.today
1221 1221 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1222 1222 assert_equal date, copy.start_date
1223 1223 end
1224 1224
1225 1225 test "#copy should allow due date changes" do
1226 1226 date = Date.today
1227 1227 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :due_date => date)
1228 1228 assert_equal date, copy.due_date
1229 1229 end
1230 1230
1231 1231 test "#copy should set current user as author" do
1232 1232 User.current = User.find(9)
1233 1233 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2)
1234 1234 assert_equal User.current, copy.author
1235 1235 end
1236 1236
1237 1237 test "#copy should create a journal with notes" do
1238 1238 date = Date.today
1239 1239 notes = "Notes added when copying"
1240 1240 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1241 1241 copy.init_journal(User.current, notes)
1242 1242 copy.save!
1243 1243
1244 1244 assert_equal 1, copy.journals.size
1245 1245 journal = copy.journals.first
1246 1246 assert_equal 0, journal.details.size
1247 1247 assert_equal notes, journal.notes
1248 1248 end
1249 1249
1250 1250 def test_valid_parent_project
1251 1251 issue = Issue.find(1)
1252 1252 issue_in_same_project = Issue.find(2)
1253 1253 issue_in_child_project = Issue.find(5)
1254 1254 issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1)
1255 1255 issue_in_other_child_project = Issue.find(6)
1256 1256 issue_in_different_tree = Issue.find(4)
1257 1257
1258 1258 with_settings :cross_project_subtasks => '' do
1259 1259 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1260 1260 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1261 1261 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1262 1262 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1263 1263 end
1264 1264
1265 1265 with_settings :cross_project_subtasks => 'system' do
1266 1266 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1267 1267 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1268 1268 assert_equal true, issue.valid_parent_project?(issue_in_different_tree)
1269 1269 end
1270 1270
1271 1271 with_settings :cross_project_subtasks => 'tree' do
1272 1272 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1273 1273 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1274 1274 assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project)
1275 1275 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1276 1276
1277 1277 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project)
1278 1278 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1279 1279 end
1280 1280
1281 1281 with_settings :cross_project_subtasks => 'descendants' do
1282 1282 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1283 1283 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1284 1284 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1285 1285 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1286 1286
1287 1287 assert_equal true, issue_in_child_project.valid_parent_project?(issue)
1288 1288 assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1289 1289 end
1290 1290 end
1291 1291
1292 1292 def test_recipients_should_include_previous_assignee
1293 1293 user = User.find(3)
1294 1294 user.members.update_all ["mail_notification = ?", false]
1295 1295 user.update_attribute :mail_notification, 'only_assigned'
1296 1296
1297 1297 issue = Issue.find(2)
1298 1298 issue.assigned_to = nil
1299 1299 assert_include user.mail, issue.recipients
1300 1300 issue.save!
1301 1301 assert !issue.recipients.include?(user.mail)
1302 1302 end
1303 1303
1304 1304 def test_recipients_should_not_include_users_that_cannot_view_the_issue
1305 1305 issue = Issue.find(12)
1306 1306 assert issue.recipients.include?(issue.author.mail)
1307 1307 # copy the issue to a private project
1308 1308 copy = issue.copy(:project_id => 5, :tracker_id => 2)
1309 1309 # author is not a member of project anymore
1310 1310 assert !copy.recipients.include?(copy.author.mail)
1311 1311 end
1312 1312
1313 1313 def test_recipients_should_include_the_assigned_group_members
1314 1314 group_member = User.generate!
1315 1315 group = Group.generate!
1316 1316 group.users << group_member
1317 1317
1318 1318 issue = Issue.find(12)
1319 1319 issue.assigned_to = group
1320 1320 assert issue.recipients.include?(group_member.mail)
1321 1321 end
1322 1322
1323 1323 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
1324 1324 user = User.find(3)
1325 1325 issue = Issue.find(9)
1326 1326 Watcher.create!(:user => user, :watchable => issue)
1327 1327 assert issue.watched_by?(user)
1328 1328 assert !issue.watcher_recipients.include?(user.mail)
1329 1329 end
1330 1330
1331 1331 def test_issue_destroy
1332 1332 Issue.find(1).destroy
1333 1333 assert_nil Issue.find_by_id(1)
1334 1334 assert_nil TimeEntry.find_by_issue_id(1)
1335 1335 end
1336 1336
1337 1337 def test_destroying_a_deleted_issue_should_not_raise_an_error
1338 1338 issue = Issue.find(1)
1339 1339 Issue.find(1).destroy
1340 1340
1341 1341 assert_nothing_raised do
1342 1342 assert_no_difference 'Issue.count' do
1343 1343 issue.destroy
1344 1344 end
1345 1345 assert issue.destroyed?
1346 1346 end
1347 1347 end
1348 1348
1349 1349 def test_destroying_a_stale_issue_should_not_raise_an_error
1350 1350 issue = Issue.find(1)
1351 1351 Issue.find(1).update_attribute :subject, "Updated"
1352 1352
1353 1353 assert_nothing_raised do
1354 1354 assert_difference 'Issue.count', -1 do
1355 1355 issue.destroy
1356 1356 end
1357 1357 assert issue.destroyed?
1358 1358 end
1359 1359 end
1360 1360
1361 1361 def test_blocked
1362 1362 blocked_issue = Issue.find(9)
1363 1363 blocking_issue = Issue.find(10)
1364 1364
1365 1365 assert blocked_issue.blocked?
1366 1366 assert !blocking_issue.blocked?
1367 1367 end
1368 1368
1369 1369 def test_blocked_issues_dont_allow_closed_statuses
1370 1370 blocked_issue = Issue.find(9)
1371 1371
1372 1372 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1373 1373 assert !allowed_statuses.empty?
1374 1374 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1375 1375 assert closed_statuses.empty?
1376 1376 end
1377 1377
1378 1378 def test_unblocked_issues_allow_closed_statuses
1379 1379 blocking_issue = Issue.find(10)
1380 1380
1381 1381 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1382 1382 assert !allowed_statuses.empty?
1383 1383 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1384 1384 assert !closed_statuses.empty?
1385 1385 end
1386 1386
1387 1387 def test_reschedule_an_issue_without_dates
1388 1388 with_settings :non_working_week_days => [] do
1389 1389 issue = Issue.new(:start_date => nil, :due_date => nil)
1390 1390 issue.reschedule_on '2012-10-09'.to_date
1391 1391 assert_equal '2012-10-09'.to_date, issue.start_date
1392 1392 assert_equal '2012-10-09'.to_date, issue.due_date
1393 1393 end
1394 1394
1395 1395 with_settings :non_working_week_days => %w(6 7) do
1396 1396 issue = Issue.new(:start_date => nil, :due_date => nil)
1397 1397 issue.reschedule_on '2012-10-09'.to_date
1398 1398 assert_equal '2012-10-09'.to_date, issue.start_date
1399 1399 assert_equal '2012-10-09'.to_date, issue.due_date
1400 1400
1401 1401 issue = Issue.new(:start_date => nil, :due_date => nil)
1402 1402 issue.reschedule_on '2012-10-13'.to_date
1403 1403 assert_equal '2012-10-15'.to_date, issue.start_date
1404 1404 assert_equal '2012-10-15'.to_date, issue.due_date
1405 1405 end
1406 1406 end
1407 1407
1408 1408 def test_reschedule_an_issue_with_start_date
1409 1409 with_settings :non_working_week_days => [] do
1410 1410 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1411 1411 issue.reschedule_on '2012-10-13'.to_date
1412 1412 assert_equal '2012-10-13'.to_date, issue.start_date
1413 1413 assert_equal '2012-10-13'.to_date, issue.due_date
1414 1414 end
1415 1415
1416 1416 with_settings :non_working_week_days => %w(6 7) do
1417 1417 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1418 1418 issue.reschedule_on '2012-10-11'.to_date
1419 1419 assert_equal '2012-10-11'.to_date, issue.start_date
1420 1420 assert_equal '2012-10-11'.to_date, issue.due_date
1421 1421
1422 1422 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1423 1423 issue.reschedule_on '2012-10-13'.to_date
1424 1424 assert_equal '2012-10-15'.to_date, issue.start_date
1425 1425 assert_equal '2012-10-15'.to_date, issue.due_date
1426 1426 end
1427 1427 end
1428 1428
1429 1429 def test_reschedule_an_issue_with_start_and_due_dates
1430 1430 with_settings :non_working_week_days => [] do
1431 1431 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-15')
1432 1432 issue.reschedule_on '2012-10-13'.to_date
1433 1433 assert_equal '2012-10-13'.to_date, issue.start_date
1434 1434 assert_equal '2012-10-19'.to_date, issue.due_date
1435 1435 end
1436 1436
1437 1437 with_settings :non_working_week_days => %w(6 7) do
1438 1438 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') # 8 working days
1439 1439 issue.reschedule_on '2012-10-11'.to_date
1440 1440 assert_equal '2012-10-11'.to_date, issue.start_date
1441 1441 assert_equal '2012-10-23'.to_date, issue.due_date
1442 1442
1443 1443 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19')
1444 1444 issue.reschedule_on '2012-10-13'.to_date
1445 1445 assert_equal '2012-10-15'.to_date, issue.start_date
1446 1446 assert_equal '2012-10-25'.to_date, issue.due_date
1447 1447 end
1448 1448 end
1449 1449
1450 1450 def test_rescheduling_an_issue_to_a_later_due_date_should_reschedule_following_issue
1451 1451 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1452 1452 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1453 1453 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1454 1454 :relation_type => IssueRelation::TYPE_PRECEDES)
1455 1455 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1456 1456
1457 1457 issue1.due_date = '2012-10-23'
1458 1458 issue1.save!
1459 1459 issue2.reload
1460 1460 assert_equal Date.parse('2012-10-24'), issue2.start_date
1461 1461 assert_equal Date.parse('2012-10-26'), issue2.due_date
1462 1462 end
1463 1463
1464 1464 def test_rescheduling_an_issue_to_an_earlier_due_date_should_reschedule_following_issue
1465 1465 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1466 1466 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1467 1467 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1468 1468 :relation_type => IssueRelation::TYPE_PRECEDES)
1469 1469 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1470 1470
1471 1471 issue1.start_date = '2012-09-17'
1472 1472 issue1.due_date = '2012-09-18'
1473 1473 issue1.save!
1474 1474 issue2.reload
1475 1475 assert_equal Date.parse('2012-09-19'), issue2.start_date
1476 1476 assert_equal Date.parse('2012-09-21'), issue2.due_date
1477 1477 end
1478 1478
1479 1479 def test_rescheduling_reschedule_following_issue_earlier_should_consider_other_preceding_issues
1480 1480 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1481 1481 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1482 1482 issue3 = Issue.generate!(:start_date => '2012-10-01', :due_date => '2012-10-02')
1483 1483 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1484 1484 :relation_type => IssueRelation::TYPE_PRECEDES)
1485 1485 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1486 1486 :relation_type => IssueRelation::TYPE_PRECEDES)
1487 1487 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1488 1488
1489 1489 issue1.start_date = '2012-09-17'
1490 1490 issue1.due_date = '2012-09-18'
1491 1491 issue1.save!
1492 1492 issue2.reload
1493 1493 # Issue 2 must start after Issue 3
1494 1494 assert_equal Date.parse('2012-10-03'), issue2.start_date
1495 1495 assert_equal Date.parse('2012-10-05'), issue2.due_date
1496 1496 end
1497 1497
1498 1498 def test_rescheduling_a_stale_issue_should_not_raise_an_error
1499 1499 with_settings :non_working_week_days => [] do
1500 1500 stale = Issue.find(1)
1501 1501 issue = Issue.find(1)
1502 1502 issue.subject = "Updated"
1503 1503 issue.save!
1504 1504 date = 10.days.from_now.to_date
1505 1505 assert_nothing_raised do
1506 1506 stale.reschedule_on!(date)
1507 1507 end
1508 1508 assert_equal date, stale.reload.start_date
1509 1509 end
1510 1510 end
1511 1511
1512 1512 def test_child_issue_should_consider_parent_soonest_start_on_create
1513 1513 set_language_if_valid 'en'
1514 1514 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1515 1515 issue2 = Issue.generate!(:start_date => '2012-10-18', :due_date => '2012-10-20')
1516 1516 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1517 1517 :relation_type => IssueRelation::TYPE_PRECEDES)
1518 1518 issue1.reload
1519 1519 issue2.reload
1520 1520 assert_equal Date.parse('2012-10-18'), issue2.start_date
1521 1521
1522 1522 child = Issue.new(:parent_issue_id => issue2.id, :start_date => '2012-10-16',
1523 1523 :project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Child', :author_id => 1)
1524 1524 assert !child.valid?
1525 1525 assert_include 'Start date is invalid', child.errors.full_messages
1526 1526 assert_equal Date.parse('2012-10-18'), child.soonest_start
1527 1527 child.start_date = '2012-10-18'
1528 1528 assert child.save
1529 1529 end
1530 1530
1531 def test_setting_parent_to_a_dependent_issue_should_not_validate
1532 set_language_if_valid 'en'
1533 issue1 = Issue.generate!
1534 issue2 = Issue.generate!
1535 issue3 = Issue.generate!
1536 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
1537 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_PRECEDES)
1538 issue3.reload
1539 issue3.parent_issue_id = issue2.id
1540 assert !issue3.valid?
1541 assert_include 'Parent task is invalid', issue3.errors.full_messages
1542 end
1543
1544 def test_setting_parent_should_not_allow_circular_dependency
1545 set_language_if_valid 'en'
1546 issue1 = Issue.generate!
1547 issue2 = Issue.generate!
1548 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
1549 issue3 = Issue.generate!
1550 issue2.reload
1551 issue2.parent_issue_id = issue3.id
1552 issue2.save!
1553 issue4 = Issue.generate!
1554 IssueRelation.create!(:issue_from => issue3, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
1555 issue4.reload
1556 issue4.parent_issue_id = issue1.id
1557 assert !issue4.valid?
1558 assert_include 'Parent task is invalid', issue4.errors.full_messages
1559 end
1560
1531 1561 def test_overdue
1532 1562 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
1533 1563 assert !Issue.new(:due_date => Date.today).overdue?
1534 1564 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
1535 1565 assert !Issue.new(:due_date => nil).overdue?
1536 1566 assert !Issue.new(:due_date => 1.day.ago.to_date,
1537 1567 :status => IssueStatus.where(:is_closed => true).first
1538 1568 ).overdue?
1539 1569 end
1540 1570
1541 1571 test "#behind_schedule? should be false if the issue has no start_date" do
1542 1572 assert !Issue.new(:start_date => nil,
1543 1573 :due_date => 1.day.from_now.to_date,
1544 1574 :done_ratio => 0).behind_schedule?
1545 1575 end
1546 1576
1547 1577 test "#behind_schedule? should be false if the issue has no end_date" do
1548 1578 assert !Issue.new(:start_date => 1.day.from_now.to_date,
1549 1579 :due_date => nil,
1550 1580 :done_ratio => 0).behind_schedule?
1551 1581 end
1552 1582
1553 1583 test "#behind_schedule? should be false if the issue has more done than it's calendar time" do
1554 1584 assert !Issue.new(:start_date => 50.days.ago.to_date,
1555 1585 :due_date => 50.days.from_now.to_date,
1556 1586 :done_ratio => 90).behind_schedule?
1557 1587 end
1558 1588
1559 1589 test "#behind_schedule? should be true if the issue hasn't been started at all" do
1560 1590 assert Issue.new(:start_date => 1.day.ago.to_date,
1561 1591 :due_date => 1.day.from_now.to_date,
1562 1592 :done_ratio => 0).behind_schedule?
1563 1593 end
1564 1594
1565 1595 test "#behind_schedule? should be true if the issue has used more calendar time than it's done ratio" do
1566 1596 assert Issue.new(:start_date => 100.days.ago.to_date,
1567 1597 :due_date => Date.today,
1568 1598 :done_ratio => 90).behind_schedule?
1569 1599 end
1570 1600
1571 1601 test "#assignable_users should be Users" do
1572 1602 assert_kind_of User, Issue.find(1).assignable_users.first
1573 1603 end
1574 1604
1575 1605 test "#assignable_users should include the issue author" do
1576 1606 non_project_member = User.generate!
1577 1607 issue = Issue.generate!(:author => non_project_member)
1578 1608
1579 1609 assert issue.assignable_users.include?(non_project_member)
1580 1610 end
1581 1611
1582 1612 test "#assignable_users should include the current assignee" do
1583 1613 user = User.generate!
1584 1614 issue = Issue.generate!(:assigned_to => user)
1585 1615 user.lock!
1586 1616
1587 1617 assert Issue.find(issue.id).assignable_users.include?(user)
1588 1618 end
1589 1619
1590 1620 test "#assignable_users should not show the issue author twice" do
1591 1621 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
1592 1622 assert_equal 2, assignable_user_ids.length
1593 1623
1594 1624 assignable_user_ids.each do |user_id|
1595 1625 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length,
1596 1626 "User #{user_id} appears more or less than once"
1597 1627 end
1598 1628 end
1599 1629
1600 1630 test "#assignable_users with issue_group_assignment should include groups" do
1601 1631 issue = Issue.new(:project => Project.find(2))
1602 1632
1603 1633 with_settings :issue_group_assignment => '1' do
1604 1634 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1605 1635 assert issue.assignable_users.include?(Group.find(11))
1606 1636 end
1607 1637 end
1608 1638
1609 1639 test "#assignable_users without issue_group_assignment should not include groups" do
1610 1640 issue = Issue.new(:project => Project.find(2))
1611 1641
1612 1642 with_settings :issue_group_assignment => '0' do
1613 1643 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1614 1644 assert !issue.assignable_users.include?(Group.find(11))
1615 1645 end
1616 1646 end
1617 1647
1618 1648 def test_create_should_send_email_notification
1619 1649 ActionMailer::Base.deliveries.clear
1620 1650 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1621 1651 :author_id => 3, :status_id => 1,
1622 1652 :priority => IssuePriority.all.first,
1623 1653 :subject => 'test_create', :estimated_hours => '1:30')
1624 1654
1625 1655 assert issue.save
1626 1656 assert_equal 1, ActionMailer::Base.deliveries.size
1627 1657 end
1628 1658
1629 1659 def test_stale_issue_should_not_send_email_notification
1630 1660 ActionMailer::Base.deliveries.clear
1631 1661 issue = Issue.find(1)
1632 1662 stale = Issue.find(1)
1633 1663
1634 1664 issue.init_journal(User.find(1))
1635 1665 issue.subject = 'Subjet update'
1636 1666 assert issue.save
1637 1667 assert_equal 1, ActionMailer::Base.deliveries.size
1638 1668 ActionMailer::Base.deliveries.clear
1639 1669
1640 1670 stale.init_journal(User.find(1))
1641 1671 stale.subject = 'Another subjet update'
1642 1672 assert_raise ActiveRecord::StaleObjectError do
1643 1673 stale.save
1644 1674 end
1645 1675 assert ActionMailer::Base.deliveries.empty?
1646 1676 end
1647 1677
1648 1678 def test_journalized_description
1649 1679 IssueCustomField.delete_all
1650 1680
1651 1681 i = Issue.first
1652 1682 old_description = i.description
1653 1683 new_description = "This is the new description"
1654 1684
1655 1685 i.init_journal(User.find(2))
1656 1686 i.description = new_description
1657 1687 assert_difference 'Journal.count', 1 do
1658 1688 assert_difference 'JournalDetail.count', 1 do
1659 1689 i.save!
1660 1690 end
1661 1691 end
1662 1692
1663 1693 detail = JournalDetail.first(:order => 'id DESC')
1664 1694 assert_equal i, detail.journal.journalized
1665 1695 assert_equal 'attr', detail.property
1666 1696 assert_equal 'description', detail.prop_key
1667 1697 assert_equal old_description, detail.old_value
1668 1698 assert_equal new_description, detail.value
1669 1699 end
1670 1700
1671 1701 def test_blank_descriptions_should_not_be_journalized
1672 1702 IssueCustomField.delete_all
1673 1703 Issue.update_all("description = NULL", "id=1")
1674 1704
1675 1705 i = Issue.find(1)
1676 1706 i.init_journal(User.find(2))
1677 1707 i.subject = "blank description"
1678 1708 i.description = "\r\n"
1679 1709
1680 1710 assert_difference 'Journal.count', 1 do
1681 1711 assert_difference 'JournalDetail.count', 1 do
1682 1712 i.save!
1683 1713 end
1684 1714 end
1685 1715 end
1686 1716
1687 1717 def test_journalized_multi_custom_field
1688 1718 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list',
1689 1719 :is_filter => true, :is_for_all => true,
1690 1720 :tracker_ids => [1],
1691 1721 :possible_values => ['value1', 'value2', 'value3'],
1692 1722 :multiple => true)
1693 1723
1694 1724 issue = Issue.create!(:project_id => 1, :tracker_id => 1,
1695 1725 :subject => 'Test', :author_id => 1)
1696 1726
1697 1727 assert_difference 'Journal.count' do
1698 1728 assert_difference 'JournalDetail.count' do
1699 1729 issue.init_journal(User.first)
1700 1730 issue.custom_field_values = {field.id => ['value1']}
1701 1731 issue.save!
1702 1732 end
1703 1733 assert_difference 'JournalDetail.count' do
1704 1734 issue.init_journal(User.first)
1705 1735 issue.custom_field_values = {field.id => ['value1', 'value2']}
1706 1736 issue.save!
1707 1737 end
1708 1738 assert_difference 'JournalDetail.count', 2 do
1709 1739 issue.init_journal(User.first)
1710 1740 issue.custom_field_values = {field.id => ['value3', 'value2']}
1711 1741 issue.save!
1712 1742 end
1713 1743 assert_difference 'JournalDetail.count', 2 do
1714 1744 issue.init_journal(User.first)
1715 1745 issue.custom_field_values = {field.id => nil}
1716 1746 issue.save!
1717 1747 end
1718 1748 end
1719 1749 end
1720 1750
1721 1751 def test_description_eol_should_be_normalized
1722 1752 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
1723 1753 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
1724 1754 end
1725 1755
1726 1756 def test_saving_twice_should_not_duplicate_journal_details
1727 1757 i = Issue.first
1728 1758 i.init_journal(User.find(2), 'Some notes')
1729 1759 # initial changes
1730 1760 i.subject = 'New subject'
1731 1761 i.done_ratio = i.done_ratio + 10
1732 1762 assert_difference 'Journal.count' do
1733 1763 assert i.save
1734 1764 end
1735 1765 # 1 more change
1736 1766 i.priority = IssuePriority.where("id <> ?", i.priority_id).first
1737 1767 assert_no_difference 'Journal.count' do
1738 1768 assert_difference 'JournalDetail.count', 1 do
1739 1769 i.save
1740 1770 end
1741 1771 end
1742 1772 # no more change
1743 1773 assert_no_difference 'Journal.count' do
1744 1774 assert_no_difference 'JournalDetail.count' do
1745 1775 i.save
1746 1776 end
1747 1777 end
1748 1778 end
1749 1779
1750 1780 def test_all_dependent_issues
1751 1781 IssueRelation.delete_all
1752 1782 assert IssueRelation.create!(:issue_from => Issue.find(1),
1753 1783 :issue_to => Issue.find(2),
1754 1784 :relation_type => IssueRelation::TYPE_PRECEDES)
1755 1785 assert IssueRelation.create!(:issue_from => Issue.find(2),
1756 1786 :issue_to => Issue.find(3),
1757 1787 :relation_type => IssueRelation::TYPE_PRECEDES)
1758 1788 assert IssueRelation.create!(:issue_from => Issue.find(3),
1759 1789 :issue_to => Issue.find(8),
1760 1790 :relation_type => IssueRelation::TYPE_PRECEDES)
1761 1791
1762 1792 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1763 1793 end
1764 1794
1765 1795 def test_all_dependent_issues_with_persistent_circular_dependency
1766 1796 IssueRelation.delete_all
1767 1797 assert IssueRelation.create!(:issue_from => Issue.find(1),
1768 1798 :issue_to => Issue.find(2),
1769 1799 :relation_type => IssueRelation::TYPE_PRECEDES)
1770 1800 assert IssueRelation.create!(:issue_from => Issue.find(2),
1771 1801 :issue_to => Issue.find(3),
1772 1802 :relation_type => IssueRelation::TYPE_PRECEDES)
1773 1803
1774 1804 r = IssueRelation.create!(:issue_from => Issue.find(3),
1775 1805 :issue_to => Issue.find(7),
1776 1806 :relation_type => IssueRelation::TYPE_PRECEDES)
1777 1807 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1778 1808
1779 1809 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
1780 1810 end
1781 1811
1782 1812 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
1783 1813 IssueRelation.delete_all
1784 1814 assert IssueRelation.create!(:issue_from => Issue.find(1),
1785 1815 :issue_to => Issue.find(2),
1786 1816 :relation_type => IssueRelation::TYPE_RELATES)
1787 1817 assert IssueRelation.create!(:issue_from => Issue.find(2),
1788 1818 :issue_to => Issue.find(3),
1789 1819 :relation_type => IssueRelation::TYPE_RELATES)
1790 1820 assert IssueRelation.create!(:issue_from => Issue.find(3),
1791 1821 :issue_to => Issue.find(8),
1792 1822 :relation_type => IssueRelation::TYPE_RELATES)
1793 1823
1794 1824 r = IssueRelation.create!(:issue_from => Issue.find(8),
1795 1825 :issue_to => Issue.find(7),
1796 1826 :relation_type => IssueRelation::TYPE_RELATES)
1797 1827 IssueRelation.update_all("issue_to_id = 2", ["id = ?", r.id])
1798 1828
1799 1829 r = IssueRelation.create!(:issue_from => Issue.find(3),
1800 1830 :issue_to => Issue.find(7),
1801 1831 :relation_type => IssueRelation::TYPE_RELATES)
1802 1832 IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id])
1803 1833
1804 1834 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1805 1835 end
1806 1836
1807 1837 test "#done_ratio should use the issue_status according to Setting.issue_done_ratio" do
1808 1838 @issue = Issue.find(1)
1809 1839 @issue_status = IssueStatus.find(1)
1810 1840 @issue_status.update_attribute(:default_done_ratio, 50)
1811 1841 @issue2 = Issue.find(2)
1812 1842 @issue_status2 = IssueStatus.find(2)
1813 1843 @issue_status2.update_attribute(:default_done_ratio, 0)
1814 1844
1815 1845 with_settings :issue_done_ratio => 'issue_field' do
1816 1846 assert_equal 0, @issue.done_ratio
1817 1847 assert_equal 30, @issue2.done_ratio
1818 1848 end
1819 1849
1820 1850 with_settings :issue_done_ratio => 'issue_status' do
1821 1851 assert_equal 50, @issue.done_ratio
1822 1852 assert_equal 0, @issue2.done_ratio
1823 1853 end
1824 1854 end
1825 1855
1826 1856 test "#update_done_ratio_from_issue_status should update done_ratio according to Setting.issue_done_ratio" do
1827 1857 @issue = Issue.find(1)
1828 1858 @issue_status = IssueStatus.find(1)
1829 1859 @issue_status.update_attribute(:default_done_ratio, 50)
1830 1860 @issue2 = Issue.find(2)
1831 1861 @issue_status2 = IssueStatus.find(2)
1832 1862 @issue_status2.update_attribute(:default_done_ratio, 0)
1833 1863
1834 1864 with_settings :issue_done_ratio => 'issue_field' do
1835 1865 @issue.update_done_ratio_from_issue_status
1836 1866 @issue2.update_done_ratio_from_issue_status
1837 1867
1838 1868 assert_equal 0, @issue.read_attribute(:done_ratio)
1839 1869 assert_equal 30, @issue2.read_attribute(:done_ratio)
1840 1870 end
1841 1871
1842 1872 with_settings :issue_done_ratio => 'issue_status' do
1843 1873 @issue.update_done_ratio_from_issue_status
1844 1874 @issue2.update_done_ratio_from_issue_status
1845 1875
1846 1876 assert_equal 50, @issue.read_attribute(:done_ratio)
1847 1877 assert_equal 0, @issue2.read_attribute(:done_ratio)
1848 1878 end
1849 1879 end
1850 1880
1851 1881 test "#by_tracker" do
1852 1882 User.current = User.anonymous
1853 1883 groups = Issue.by_tracker(Project.find(1))
1854 1884 assert_equal 3, groups.size
1855 1885 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1856 1886 end
1857 1887
1858 1888 test "#by_version" do
1859 1889 User.current = User.anonymous
1860 1890 groups = Issue.by_version(Project.find(1))
1861 1891 assert_equal 3, groups.size
1862 1892 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1863 1893 end
1864 1894
1865 1895 test "#by_priority" do
1866 1896 User.current = User.anonymous
1867 1897 groups = Issue.by_priority(Project.find(1))
1868 1898 assert_equal 4, groups.size
1869 1899 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1870 1900 end
1871 1901
1872 1902 test "#by_category" do
1873 1903 User.current = User.anonymous
1874 1904 groups = Issue.by_category(Project.find(1))
1875 1905 assert_equal 2, groups.size
1876 1906 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1877 1907 end
1878 1908
1879 1909 test "#by_assigned_to" do
1880 1910 User.current = User.anonymous
1881 1911 groups = Issue.by_assigned_to(Project.find(1))
1882 1912 assert_equal 2, groups.size
1883 1913 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1884 1914 end
1885 1915
1886 1916 test "#by_author" do
1887 1917 User.current = User.anonymous
1888 1918 groups = Issue.by_author(Project.find(1))
1889 1919 assert_equal 4, groups.size
1890 1920 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1891 1921 end
1892 1922
1893 1923 test "#by_subproject" do
1894 1924 User.current = User.anonymous
1895 1925 groups = Issue.by_subproject(Project.find(1))
1896 1926 # Private descendant not visible
1897 1927 assert_equal 1, groups.size
1898 1928 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1899 1929 end
1900 1930
1901 1931 def test_recently_updated_scope
1902 1932 #should return the last updated issue
1903 1933 assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first
1904 1934 end
1905 1935
1906 1936 def test_on_active_projects_scope
1907 1937 assert Project.find(2).archive
1908 1938
1909 1939 before = Issue.on_active_project.length
1910 1940 # test inclusion to results
1911 1941 issue = Issue.generate!(:tracker => Project.find(2).trackers.first)
1912 1942 assert_equal before + 1, Issue.on_active_project.length
1913 1943
1914 1944 # Move to an archived project
1915 1945 issue.project = Project.find(2)
1916 1946 assert issue.save
1917 1947 assert_equal before, Issue.on_active_project.length
1918 1948 end
1919 1949
1920 1950 test "Issue#recipients should include project recipients" do
1921 1951 issue = Issue.generate!
1922 1952 assert issue.project.recipients.present?
1923 1953 issue.project.recipients.each do |project_recipient|
1924 1954 assert issue.recipients.include?(project_recipient)
1925 1955 end
1926 1956 end
1927 1957
1928 1958 test "Issue#recipients should include the author if the author is active" do
1929 1959 issue = Issue.generate!(:author => User.generate!)
1930 1960 assert issue.author, "No author set for Issue"
1931 1961 assert issue.recipients.include?(issue.author.mail)
1932 1962 end
1933 1963
1934 1964 test "Issue#recipients should include the assigned to user if the assigned to user is active" do
1935 1965 issue = Issue.generate!(:assigned_to => User.generate!)
1936 1966 assert issue.assigned_to, "No assigned_to set for Issue"
1937 1967 assert issue.recipients.include?(issue.assigned_to.mail)
1938 1968 end
1939 1969
1940 1970 test "Issue#recipients should not include users who opt out of all email" do
1941 1971 issue = Issue.generate!(:author => User.generate!)
1942 1972 issue.author.update_attribute(:mail_notification, :none)
1943 1973 assert !issue.recipients.include?(issue.author.mail)
1944 1974 end
1945 1975
1946 1976 test "Issue#recipients should not include the issue author if they are only notified of assigned issues" do
1947 1977 issue = Issue.generate!(:author => User.generate!)
1948 1978 issue.author.update_attribute(:mail_notification, :only_assigned)
1949 1979 assert !issue.recipients.include?(issue.author.mail)
1950 1980 end
1951 1981
1952 1982 test "Issue#recipients should not include the assigned user if they are only notified of owned issues" do
1953 1983 issue = Issue.generate!(:assigned_to => User.generate!)
1954 1984 issue.assigned_to.update_attribute(:mail_notification, :only_owner)
1955 1985 assert !issue.recipients.include?(issue.assigned_to.mail)
1956 1986 end
1957 1987
1958 1988 def test_last_journal_id_with_journals_should_return_the_journal_id
1959 1989 assert_equal 2, Issue.find(1).last_journal_id
1960 1990 end
1961 1991
1962 1992 def test_last_journal_id_without_journals_should_return_nil
1963 1993 assert_nil Issue.find(3).last_journal_id
1964 1994 end
1965 1995
1966 1996 def test_journals_after_should_return_journals_with_greater_id
1967 1997 assert_equal [Journal.find(2)], Issue.find(1).journals_after('1')
1968 1998 assert_equal [], Issue.find(1).journals_after('2')
1969 1999 end
1970 2000
1971 2001 def test_journals_after_with_blank_arg_should_return_all_journals
1972 2002 assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('')
1973 2003 end
1974 2004
1975 2005 def test_css_classes_should_include_tracker
1976 2006 issue = Issue.new(:tracker => Tracker.find(2))
1977 2007 classes = issue.css_classes.split(' ')
1978 2008 assert_include 'tracker-2', classes
1979 2009 end
1980 2010
1981 2011 def test_css_classes_should_include_priority
1982 2012 issue = Issue.new(:priority => IssuePriority.find(8))
1983 2013 classes = issue.css_classes.split(' ')
1984 2014 assert_include 'priority-8', classes
1985 2015 assert_include 'priority-highest', classes
1986 2016 end
1987 2017
1988 2018 def test_save_attachments_with_hash_should_save_attachments_in_keys_order
1989 2019 set_tmp_attachments_directory
1990 2020 issue = Issue.generate!
1991 2021 issue.save_attachments({
1992 2022 'p0' => {'file' => mock_file_with_options(:original_filename => 'upload')},
1993 2023 '3' => {'file' => mock_file_with_options(:original_filename => 'bar')},
1994 2024 '1' => {'file' => mock_file_with_options(:original_filename => 'foo')}
1995 2025 })
1996 2026 issue.attach_saved_attachments
1997 2027
1998 2028 assert_equal 3, issue.reload.attachments.count
1999 2029 assert_equal %w(upload foo bar), issue.attachments.map(&:filename)
2000 2030 end
2001 2031
2002 2032 def test_closed_on_should_be_nil_when_creating_an_open_issue
2003 2033 issue = Issue.generate!(:status_id => 1).reload
2004 2034 assert !issue.closed?
2005 2035 assert_nil issue.closed_on
2006 2036 end
2007 2037
2008 2038 def test_closed_on_should_be_set_when_creating_a_closed_issue
2009 2039 issue = Issue.generate!(:status_id => 5).reload
2010 2040 assert issue.closed?
2011 2041 assert_not_nil issue.closed_on
2012 2042 assert_equal issue.updated_on, issue.closed_on
2013 2043 assert_equal issue.created_on, issue.closed_on
2014 2044 end
2015 2045
2016 2046 def test_closed_on_should_be_nil_when_updating_an_open_issue
2017 2047 issue = Issue.find(1)
2018 2048 issue.subject = 'Not closed yet'
2019 2049 issue.save!
2020 2050 issue.reload
2021 2051 assert_nil issue.closed_on
2022 2052 end
2023 2053
2024 2054 def test_closed_on_should_be_set_when_closing_an_open_issue
2025 2055 issue = Issue.find(1)
2026 2056 issue.subject = 'Now closed'
2027 2057 issue.status_id = 5
2028 2058 issue.save!
2029 2059 issue.reload
2030 2060 assert_not_nil issue.closed_on
2031 2061 assert_equal issue.updated_on, issue.closed_on
2032 2062 end
2033 2063
2034 2064 def test_closed_on_should_not_be_updated_when_updating_a_closed_issue
2035 2065 issue = Issue.open(false).first
2036 2066 was_closed_on = issue.closed_on
2037 2067 assert_not_nil was_closed_on
2038 2068 issue.subject = 'Updating a closed issue'
2039 2069 issue.save!
2040 2070 issue.reload
2041 2071 assert_equal was_closed_on, issue.closed_on
2042 2072 end
2043 2073
2044 2074 def test_closed_on_should_be_preserved_when_reopening_a_closed_issue
2045 2075 issue = Issue.open(false).first
2046 2076 was_closed_on = issue.closed_on
2047 2077 assert_not_nil was_closed_on
2048 2078 issue.subject = 'Reopening a closed issue'
2049 2079 issue.status_id = 1
2050 2080 issue.save!
2051 2081 issue.reload
2052 2082 assert !issue.closed?
2053 2083 assert_equal was_closed_on, issue.closed_on
2054 2084 end
2055 2085
2056 2086 def test_status_was_should_return_nil_for_new_issue
2057 2087 issue = Issue.new
2058 2088 assert_nil issue.status_was
2059 2089 end
2060 2090
2061 2091 def test_status_was_should_return_status_before_change
2062 2092 issue = Issue.find(1)
2063 2093 issue.status = IssueStatus.find(2)
2064 2094 assert_equal IssueStatus.find(1), issue.status_was
2065 2095 end
2066 2096
2067 2097 def test_status_was_should_be_reset_on_save
2068 2098 issue = Issue.find(1)
2069 2099 issue.status = IssueStatus.find(2)
2070 2100 assert_equal IssueStatus.find(1), issue.status_was
2071 2101 assert issue.save!
2072 2102 assert_equal IssueStatus.find(2), issue.status_was
2073 2103 end
2074 2104 end
General Comments 0
You need to be logged in to leave comments. Login now