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